Compare commits
6 Commits
e3b8cd9067
...
cbf1aba219
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf1aba219 | ||
|
|
d555e3b2d2 | ||
|
|
c0a1f4d194 | ||
|
|
0531e1943f | ||
|
|
671c041764 | ||
|
|
e51e28d411 |
@@ -4,6 +4,11 @@ var postgres = builder.AddPostgres("postgres").WithDataVolume("miccheck-pgdata")
|
||||
|
||||
var miccheckDb = postgres.AddDatabase("miccheck");
|
||||
|
||||
builder.AddProject<Projects.MicCheck_Api>("api").WithReference(miccheckDb).WaitFor(miccheckDb);
|
||||
var api = builder.AddProject<Projects.MicCheck_Api>("api").WithReference(miccheckDb).WaitFor(miccheckDb);
|
||||
|
||||
builder.AddViteApp("admin", "../admin", "serve")
|
||||
.WithReference(api)
|
||||
.WaitFor(api)
|
||||
.WithExternalHttpEndpoints();
|
||||
|
||||
builder.Build().Run();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\api\MicCheck.Api\MicCheck.Api.csproj" />
|
||||
<ProjectReference Include="..\admin\Admin.esproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,5 +4,13 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Dashboard": {
|
||||
"Frontend": {
|
||||
"AuthMode": "Unsecured"
|
||||
},
|
||||
"Otlp": {
|
||||
"AuthMode": "Unsecured"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,3 +81,7 @@ export async function regenerateInviteLink(organizationId: number): Promise<Invi
|
||||
export async function acceptInvite(token: string): Promise<void> {
|
||||
await apiClient.post(`/v1/organisations/invite/${token}/accept`);
|
||||
}
|
||||
|
||||
export async function setPrimaryOrganization(id: number): Promise<void> {
|
||||
await apiClient.put(`/v1/organisations/${id}/primary`);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</v-tabs>
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0" style="height: 480px; overflow-y: auto;">
|
||||
<v-card-text class="pa-0" style="height: 580px; overflow-y: auto;">
|
||||
<v-tabs-window v-model="activeTab">
|
||||
<!-- ── Value tab ─────────────────────────────────────────────── -->
|
||||
<v-tabs-window-item value="value" class="pa-6">
|
||||
@@ -319,6 +319,7 @@ interface Props {
|
||||
modelValue: boolean;
|
||||
feature: FeatureResponse | null;
|
||||
featureState: FeatureStateResponse | null;
|
||||
initialTab?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -472,7 +473,7 @@ watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
activeTab.value = 'value';
|
||||
activeTab.value = props.initialTab ?? 'value';
|
||||
valueErrorMessage.value = null;
|
||||
settingsErrorMessage.value = null;
|
||||
segmentsErrorMessage.value = null;
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<div data-testid="feature-value-editor">
|
||||
<!-- Type selector -->
|
||||
<v-btn-toggle
|
||||
v-model="detectedMode"
|
||||
density="compact"
|
||||
data-testid="value-mode-toggle"
|
||||
>
|
||||
<v-btn value="text" size="small">Text</v-btn>
|
||||
<v-btn value="json" size="small">JSON</v-btn>
|
||||
</v-btn-toggle>
|
||||
<div class="d-flex mb-3" data-testid="value-mode-toggle">
|
||||
<v-btn
|
||||
size="small"
|
||||
:variant="detectedMode === 'text' ? 'flat' : 'outlined'"
|
||||
color="primary"
|
||||
rounded="0"
|
||||
style="border-radius: 4px 0 0 4px"
|
||||
@click="detectedMode = 'text'"
|
||||
>Text</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
:variant="detectedMode === 'json' ? 'flat' : 'outlined'"
|
||||
color="primary"
|
||||
rounded="0"
|
||||
style="border-radius: 0 4px 4px 0; margin-left: -1px"
|
||||
@click="detectedMode = 'json'"
|
||||
>JSON</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- JSON mode -->
|
||||
<v-textarea
|
||||
@@ -83,11 +93,3 @@ const rules = {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Materio forces square icon-toggle sizing; override for text-label toggles */
|
||||
:deep(.v-btn-toggle .v-btn) {
|
||||
block-size: auto !important;
|
||||
inline-size: auto !important;
|
||||
padding-inline: 16px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<v-form ref="createFormRef" @submit.prevent="onCreateTag">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-text-field
|
||||
ref="tagLabelInputRef"
|
||||
v-model="newTagLabel"
|
||||
label="Tag name"
|
||||
variant="outlined"
|
||||
@@ -47,6 +48,7 @@
|
||||
style="width:36px;height:36px;border:none;padding:2px;cursor:pointer;border-radius:4px"
|
||||
title="Tag colour"
|
||||
data-testid="new-tag-color-input"
|
||||
@blur="focusTagLabel"
|
||||
/>
|
||||
<v-btn
|
||||
icon="ri-check-line"
|
||||
@@ -99,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { listTags, createTag, deleteTag } from '@/api/tags';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { TagResponse } from '@/types/api';
|
||||
@@ -114,6 +116,15 @@ const newTagLabel = ref('');
|
||||
const newTagColor = ref('#1565C0');
|
||||
const errorMessage = ref<string | null>(null);
|
||||
const createFormRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
|
||||
const tagLabelInputRef = ref<{ focus: () => void } | null>(null);
|
||||
|
||||
function focusTagLabel(): void {
|
||||
nextTick(() => tagLabelInputRef.value?.focus());
|
||||
}
|
||||
|
||||
watch(showCreateForm, (open) => {
|
||||
if (open) focusTagLabel();
|
||||
});
|
||||
|
||||
const rules = {
|
||||
required: (v: string) => !!v || 'Tag name is required',
|
||||
|
||||
@@ -86,14 +86,25 @@ async function fetchEnvironments(projectId: number): Promise<void> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
environments.value = await listEnvironments(projectId);
|
||||
if (environments.value.length > 0 && !contextStore.currentEnvironment) {
|
||||
contextStore.setEnvironment(environments.value[0]);
|
||||
}
|
||||
autoSelectEnvironment();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function autoSelectEnvironment(): void {
|
||||
if (contextStore.currentEnvironment) {
|
||||
const fresh = environments.value.find(e => e.id === contextStore.currentEnvironment!.id);
|
||||
if (fresh) {
|
||||
contextStore.refreshEnvironment(fresh);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (environments.value.length > 0) {
|
||||
contextStore.setEnvironment(environments.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function onEnvironmentSelected(env: EnvironmentResponse | null): void {
|
||||
if (env) contextStore.setEnvironment(env);
|
||||
}
|
||||
@@ -127,11 +138,12 @@ async function onCreateConfirm(): Promise<void> {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => contextStore.currentProject,
|
||||
(project) => {
|
||||
() => contextStore.currentProject?.id,
|
||||
(projectId, oldProjectId) => {
|
||||
if (projectId === oldProjectId) return;
|
||||
environments.value = [];
|
||||
contextStore.setEnvironment(null);
|
||||
if (project) fetchEnvironments(project.id);
|
||||
if (oldProjectId !== undefined) contextStore.setEnvironment(null);
|
||||
if (projectId) fetchEnvironments(projectId);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import { listOrganizations, createOrganization } from '@/api/organizations';
|
||||
import { listOrganizations, createOrganization, setPrimaryOrganization } from '@/api/organizations';
|
||||
import type { OrganizationResponse } from '@/types/api';
|
||||
|
||||
const contextStore = useContextStore();
|
||||
@@ -84,6 +84,7 @@ async function fetchOrganizations(): Promise<void> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
organizations.value = await listOrganizations();
|
||||
autoSelectOrganization();
|
||||
} catch {
|
||||
organizations.value = [];
|
||||
} finally {
|
||||
@@ -91,8 +92,35 @@ async function fetchOrganizations(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function onOrgSelected(org: OrganizationResponse | null): void {
|
||||
function autoSelectOrganization(): void {
|
||||
if (contextStore.currentOrganization) {
|
||||
// Refresh stored org with latest data from API (name may have changed)
|
||||
const fresh = organizations.value.find(o => o.id === contextStore.currentOrganization!.id);
|
||||
if (fresh) contextStore.refreshOrganization(fresh);
|
||||
return;
|
||||
}
|
||||
|
||||
const primary = organizations.value.find(o => o.isPrimary);
|
||||
if (primary) {
|
||||
contextStore.setOrganization(primary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (organizations.value.length === 1) {
|
||||
contextStore.setOrganization(organizations.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function onOrgSelected(org: OrganizationResponse | null): Promise<void> {
|
||||
contextStore.setOrganization(org);
|
||||
if (org) {
|
||||
try {
|
||||
await setPrimaryOrganization(org.id);
|
||||
organizations.value = organizations.value.map(o => ({ ...o, isPrimary: o.id === org.id }));
|
||||
} catch {
|
||||
// Non-critical — selection is already saved to localStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog(): void {
|
||||
|
||||
@@ -86,11 +86,27 @@ async function fetchProjects(organizationId: number): Promise<void> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
projects.value = await listProjects(organizationId);
|
||||
autoSelectProject();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function autoSelectProject(): void {
|
||||
if (contextStore.currentProject) {
|
||||
const fresh = projects.value.find(p => p.id === contextStore.currentProject!.id);
|
||||
if (fresh) {
|
||||
contextStore.refreshProject(fresh);
|
||||
return;
|
||||
}
|
||||
contextStore.setProject(null);
|
||||
return;
|
||||
}
|
||||
if (projects.value.length === 1) {
|
||||
contextStore.setProject(projects.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function onProjectSelected(project: ProjectResponse | null): void {
|
||||
contextStore.setProject(project);
|
||||
}
|
||||
@@ -124,12 +140,11 @@ async function onCreateConfirm(): Promise<void> {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => contextStore.currentOrganization,
|
||||
(org) => {
|
||||
() => contextStore.currentOrganization?.id,
|
||||
(orgId, oldOrgId) => {
|
||||
if (orgId === oldOrgId) return;
|
||||
projects.value = [];
|
||||
if (org) {
|
||||
fetchProjects(org.id);
|
||||
}
|
||||
if (orgId) fetchProjects(orgId);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
@@ -3,18 +3,26 @@
|
||||
<!-- AND / OR toggle -->
|
||||
<div class="d-flex align-center ga-2 mb-3">
|
||||
<span class="text-body-2 font-weight-medium text-medium-emphasis">Match</span>
|
||||
<v-btn-toggle
|
||||
:model-value="rule.type"
|
||||
density="compact"
|
||||
mandatory
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
data-testid="rule-type-toggle"
|
||||
@update:model-value="onTypeChange"
|
||||
>
|
||||
<v-btn value="All" size="small" data-testid="rule-type-and">ALL (AND)</v-btn>
|
||||
<v-btn value="Any" size="small" data-testid="rule-type-or">ANY (OR)</v-btn>
|
||||
</v-btn-toggle>
|
||||
<div class="d-flex" data-testid="rule-type-toggle">
|
||||
<v-btn
|
||||
size="small"
|
||||
:variant="rule.type === 'All' ? 'flat' : 'outlined'"
|
||||
color="primary"
|
||||
rounded="0"
|
||||
style="border-radius: 4px 0 0 4px"
|
||||
data-testid="rule-type-and"
|
||||
@click="onTypeChange('All')"
|
||||
>ALL (AND)</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
:variant="rule.type === 'Any' ? 'flat' : 'outlined'"
|
||||
color="primary"
|
||||
rounded="0"
|
||||
style="border-radius: 0 4px 4px 0; margin-left: -1px"
|
||||
data-testid="rule-type-or"
|
||||
@click="onTypeChange('Any')"
|
||||
>ANY (OR)</v-btn>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="removable"
|
||||
|
||||
@@ -46,6 +46,21 @@ export const useContextStore = defineStore('context', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshOrganization(org: OrganizationResponse): void {
|
||||
currentOrganization.value = org;
|
||||
localStorage.setItem(STORAGE_KEYS.organization, JSON.stringify(org));
|
||||
}
|
||||
|
||||
function refreshProject(project: ProjectResponse): void {
|
||||
currentProject.value = project;
|
||||
localStorage.setItem(STORAGE_KEYS.project, JSON.stringify(project));
|
||||
}
|
||||
|
||||
function refreshEnvironment(environment: EnvironmentResponse): void {
|
||||
currentEnvironment.value = environment;
|
||||
localStorage.setItem(STORAGE_KEYS.environment, JSON.stringify(environment));
|
||||
}
|
||||
|
||||
function loadFromStorage(): void {
|
||||
try {
|
||||
const orgRaw = localStorage.getItem(STORAGE_KEYS.organization);
|
||||
@@ -79,6 +94,9 @@ export const useContextStore = defineStore('context', () => {
|
||||
setOrganization,
|
||||
setProject,
|
||||
setEnvironment,
|
||||
refreshOrganization,
|
||||
refreshProject,
|
||||
refreshEnvironment,
|
||||
loadFromStorage,
|
||||
clearContext,
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface OrganizationResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface CreateOrganizationRequest {
|
||||
|
||||
@@ -53,7 +53,26 @@
|
||||
<!-- Name + description -->
|
||||
<template #item.name="{ item }: { item: FeatureResponse }">
|
||||
<div>
|
||||
<span class="text-body-2 font-weight-medium">{{ item.name }}</span>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<span class="text-subtitle-1 font-weight-bold">{{ item.name }}</span>
|
||||
<v-tooltip
|
||||
v-if="(featureSegmentsMap.get(item.id) ?? []).length > 0"
|
||||
text="Has segment overrides"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
v-bind="tooltipProps"
|
||||
icon="ri-donut-chart-fill"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="secondary"
|
||||
:data-testid="`segment-override-icon-${item.id}`"
|
||||
@click.stop="openDetailOnSegments(item)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<p v-if="item.description" class="text-caption text-medium-emphasis mb-0">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
@@ -158,6 +177,7 @@
|
||||
v-model="showDetail"
|
||||
:feature="selectedFeature"
|
||||
:feature-state="selectedFeatureState"
|
||||
:initial-tab="detailInitialTab"
|
||||
@updated="onFeatureUpdated"
|
||||
@deleted="onFeatureDeleted"
|
||||
@state-updated="onStateUpdated"
|
||||
@@ -236,6 +256,7 @@ function showError(message: string): void {
|
||||
const showDialog = ref(false);
|
||||
const showDetail = ref(false);
|
||||
const selectedFeature = ref<FeatureResponse | null>(null);
|
||||
const detailInitialTab = ref('value');
|
||||
|
||||
// Delete dialog state
|
||||
const showDeleteDialog = ref(false);
|
||||
@@ -333,6 +354,13 @@ function openCreateDialog(): void {
|
||||
}
|
||||
|
||||
function onRowClick(_event: Event, { item }: { item: FeatureResponse }): void {
|
||||
detailInitialTab.value = 'value';
|
||||
selectedFeature.value = item;
|
||||
showDetail.value = true;
|
||||
}
|
||||
|
||||
function openDetailOnSegments(item: FeatureResponse): void {
|
||||
detailInitialTab.value = 'segments';
|
||||
selectedFeature.value = item;
|
||||
showDetail.value = true;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
</v-alert>
|
||||
|
||||
<v-form ref="formRef" @submit.prevent="onSubmit" data-testid="register-form">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" class="pb-0">
|
||||
<v-row no-gutters class="mb-3">
|
||||
<v-col cols="12" sm="6" class="pr-sm-2">
|
||||
<v-text-field
|
||||
v-model="firstName"
|
||||
label="First name"
|
||||
@@ -30,10 +30,11 @@
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="[rules.required]"
|
||||
:hide-details="false"
|
||||
data-testid="first-name-input"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="pb-0">
|
||||
<v-col cols="12" sm="6" class="pl-sm-2">
|
||||
<v-text-field
|
||||
v-model="lastName"
|
||||
label="Last name"
|
||||
@@ -41,6 +42,7 @@
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="[rules.required]"
|
||||
:hide-details="false"
|
||||
data-testid="last-name-input"
|
||||
/>
|
||||
</v-col>
|
||||
@@ -53,8 +55,9 @@
|
||||
autocomplete="email"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
:rules="[rules.required, rules.email]"
|
||||
:hide-details="false"
|
||||
class="mb-3"
|
||||
data-testid="email-input"
|
||||
/>
|
||||
|
||||
@@ -64,8 +67,9 @@
|
||||
autocomplete="organization"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
:rules="[rules.required]"
|
||||
:hide-details="false"
|
||||
class="mb-3"
|
||||
data-testid="org-name-input"
|
||||
/>
|
||||
|
||||
@@ -76,10 +80,11 @@
|
||||
autocomplete="new-password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
:rules="[rules.required, rules.minLength]"
|
||||
:append-inner-icon="showPassword ? 'ri-eye-off-line' : 'ri-eye-line'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
:hide-details="false"
|
||||
class="mb-3"
|
||||
data-testid="password-input"
|
||||
/>
|
||||
|
||||
@@ -90,8 +95,9 @@
|
||||
autocomplete="new-password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-4"
|
||||
:rules="[rules.required, rules.passwordMatch]"
|
||||
:hide-details="false"
|
||||
class="mb-4"
|
||||
data-testid="confirm-password-input"
|
||||
/>
|
||||
|
||||
@@ -184,4 +190,8 @@ async function onSubmit(): Promise<void> {
|
||||
.auth-bg {
|
||||
background: rgb(var(--v-theme-background));
|
||||
}
|
||||
|
||||
:deep(.v-input__details) {
|
||||
min-height: 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,13 +56,6 @@ describe('EnvironmentTabBar', () => {
|
||||
expect(envApi.listEnvironments).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ThenNoProjectMessageIsShown', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-no-project-message"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenCreateEnvironmentButtonIsDisabled', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
@@ -84,7 +77,7 @@ describe('EnvironmentTabBar', () => {
|
||||
expect(envApi.listEnvironments).toHaveBeenCalledWith(mockProject.id);
|
||||
});
|
||||
|
||||
it('ThenEnvironmentChipsAreRendered', async () => {
|
||||
it('ThenEnvironmentSelectIsEnabled', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
@@ -93,8 +86,7 @@ describe('EnvironmentTabBar', () => {
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-chip-development"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="env-chip-production"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="env-select"]').attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ThenFirstEnvironmentIsAutoSelected', async () => {
|
||||
@@ -122,7 +114,7 @@ describe('EnvironmentTabBar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenEnvironmentChipIsClicked', () => {
|
||||
describe('WhenEnvironmentIsSelected', () => {
|
||||
it('ThenContextStoreIsUpdatedWithSelectedEnvironment', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
@@ -132,7 +124,7 @@ describe('EnvironmentTabBar', () => {
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.findComponent({ name: 'VChipGroup' }).vm.$emit('update:modelValue', 1);
|
||||
await wrapper.findComponent({ name: 'VSelect' }).vm.$emit('update:modelValue', mockEnvironments[1]);
|
||||
|
||||
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[1]);
|
||||
});
|
||||
@@ -158,16 +150,49 @@ describe('EnvironmentTabBar', () => {
|
||||
});
|
||||
|
||||
describe('WhenApiReturnsNoEnvironments', () => {
|
||||
it('ThenEmptyMessageIsDisplayed', async () => {
|
||||
it('ThenNoEnvironmentIsAutoSelected', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
const wrapper = mountComponent();
|
||||
mountComponent();
|
||||
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-empty-message"]').exists()).toBe(true);
|
||||
expect(contextStore.currentEnvironment).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenStoredEnvironmentExistsInContextStore', () => {
|
||||
it('ThenStoredEnvironmentIsRestoredAfterFetch', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnvironments[1]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[1]);
|
||||
});
|
||||
|
||||
it('ThenEnvironmentIsClearedWhenProjectChanges', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnvironments[0]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const newProject: ProjectResponse = { ...mockProject, id: 20, name: 'Mobile' };
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
|
||||
contextStore.setProject(newProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentEnvironment).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import * as orgsApi from '@/api/organizations';
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
const mockOrgs: OrganizationResponse[] = [
|
||||
{ id: 1, name: 'Acme Corp', createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 1, name: 'Acme Corp', createdAt: '2026-01-01T00:00:00Z', isPrimary: false },
|
||||
{ id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false },
|
||||
];
|
||||
|
||||
function mountComponent() {
|
||||
@@ -132,7 +132,7 @@ describe('OrgSelector', () => {
|
||||
});
|
||||
|
||||
describe('WhenCreateOrganizationIsConfirmed', () => {
|
||||
const newOrg: OrganizationResponse = { id: 3, name: 'New Org', createdAt: '2026-04-13T00:00:00Z' };
|
||||
const newOrg: OrganizationResponse = { id: 3, name: 'New Org', createdAt: '2026-04-13T00:00:00Z', isPrimary: false };
|
||||
|
||||
it('ThenCreateOrganizationApiIsCalled', async () => {
|
||||
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as projectsApi from '@/api/projects';
|
||||
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
const mockProjects: ProjectResponse[] = [
|
||||
{ id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 11, name: 'Mobile App', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' },
|
||||
@@ -104,6 +104,54 @@ describe('ProjectSelector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenStoredProjectExistsInContextStore', () => {
|
||||
it('ThenStoredProjectIsRestoredAfterFetch', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization(mockOrg);
|
||||
contextStore.setProject(mockProjects[1]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toEqual(mockProjects[1]);
|
||||
});
|
||||
|
||||
it('ThenProjectIsClearedWhenNotFoundInNewOrg', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization(mockOrg);
|
||||
contextStore.setProject(mockProjects[0]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
|
||||
contextStore.setOrganization(newOrg);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenSingleProjectIsAvailable', () => {
|
||||
it('ThenItIsAutoSelected', async () => {
|
||||
const singleProject = mockProjects[0];
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue([singleProject]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
mountComponent();
|
||||
|
||||
contextStore.setOrganization(mockOrg);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toEqual(singleProject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenOrganizationChanges', () => {
|
||||
it('ThenProjectsAreReloaded', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
@@ -114,7 +162,7 @@ describe('ProjectSelector', () => {
|
||||
contextStore.setOrganization(mockOrg);
|
||||
await flushPromises();
|
||||
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
contextStore.setOrganization(newOrg);
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('FeatureDetail', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment({ id: 100, name: 'Development', apiKey: 'env-dev', projectId: 10, createdAt: '' });
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('FeatureDialog', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('TagsChipInput', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('IdentityDetail', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('SegmentEditor', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { setActivePinia, createPinia } from 'pinia';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { OrganizationResponse, ProjectResponse, EnvironmentResponse } from '@/types/api';
|
||||
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
const mockProject: ProjectResponse = {
|
||||
id: 10,
|
||||
name: 'Website',
|
||||
@@ -119,6 +119,57 @@ describe('useContextStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenRefreshOrganizationIsCalled', () => {
|
||||
it('ThenOrgIsUpdatedWithoutClearingProjectOrEnvironment', () => {
|
||||
const store = useContextStore();
|
||||
store.setOrganization(mockOrg);
|
||||
store.setProject(mockProject);
|
||||
store.setEnvironment(mockEnv);
|
||||
|
||||
const updatedOrg = { ...mockOrg, name: 'Acme Updated' };
|
||||
store.refreshOrganization(updatedOrg);
|
||||
|
||||
expect(store.currentOrganization).toEqual(updatedOrg);
|
||||
expect(store.currentProject).toEqual(mockProject);
|
||||
expect(store.currentEnvironment).toEqual(mockEnv);
|
||||
});
|
||||
|
||||
it('ThenOrgIsPersistedToLocalStorage', () => {
|
||||
const store = useContextStore();
|
||||
const updatedOrg = { ...mockOrg, name: 'Acme Updated' };
|
||||
store.refreshOrganization(updatedOrg);
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem('mic_ctx_organization')!);
|
||||
expect(stored).toEqual(updatedOrg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenRefreshProjectIsCalled', () => {
|
||||
it('ThenProjectIsUpdatedWithoutClearingEnvironment', () => {
|
||||
const store = useContextStore();
|
||||
store.setProject(mockProject);
|
||||
store.setEnvironment(mockEnv);
|
||||
|
||||
const updatedProject = { ...mockProject, name: 'Website Updated' };
|
||||
store.refreshProject(updatedProject);
|
||||
|
||||
expect(store.currentProject).toEqual(updatedProject);
|
||||
expect(store.currentEnvironment).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenRefreshEnvironmentIsCalled', () => {
|
||||
it('ThenEnvironmentIsUpdatedAndPersisted', () => {
|
||||
const store = useContextStore();
|
||||
const updatedEnv = { ...mockEnv, name: 'Production Updated' };
|
||||
store.refreshEnvironment(updatedEnv);
|
||||
|
||||
expect(store.currentEnvironment).toEqual(updatedEnv);
|
||||
const stored = JSON.parse(localStorage.getItem('mic_ctx_environment')!);
|
||||
expect(stored).toEqual(updatedEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenClearContextIsCalled', () => {
|
||||
it('ThenAllContextIsRemovedFromStateAndStorage', () => {
|
||||
const store = useContextStore();
|
||||
|
||||
@@ -23,12 +23,12 @@ const mockLogs: AuditLogResponse[] = [
|
||||
{
|
||||
id: 1, resourceType: 'Feature', resourceId: '42', action: 'Created',
|
||||
changes: null, organizationId: 1, projectId: 10, environmentId: null,
|
||||
actorUserId: 5, createdAt: '2026-01-10T10:00:00Z',
|
||||
actorUserId: 5, actorUserName: 'Alice', createdAt: '2026-01-10T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 2, resourceType: 'FeatureState', resourceId: '99', action: 'Updated',
|
||||
changes: '{"enabled":true}', organizationId: 1, projectId: 10, environmentId: 100,
|
||||
actorUserId: 5, createdAt: '2026-01-11T11:00:00Z',
|
||||
actorUserId: 5, actorUserName: 'Alice', createdAt: '2026-01-11T11:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
mountView();
|
||||
@@ -76,7 +76,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -95,7 +95,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -120,7 +120,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -145,7 +145,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -170,7 +170,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -195,7 +195,7 @@ describe('AuditLogsView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('FeaturesView', () => {
|
||||
jest.mocked(featureStatesApi.listFeatureStates).mockResolvedValue(mockStates);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('FeaturesView', () => {
|
||||
jest.mocked(featureStatesApi.listFeatureStates).mockResolvedValue(mockStates);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -92,7 +92,7 @@ describe('FeaturesView', () => {
|
||||
jest.mocked(featureStatesApi.patchFeatureState).mockResolvedValue({ ...mockStates[0], enabled: true });
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('FeaturesView', () => {
|
||||
jest.mocked(featureStatesApi.listFeatureStates).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('IdentitiesView', () => {
|
||||
});
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('ProfileView', () => {
|
||||
it('ThenOrgNameIsDisplayedWhenSelected', () => {
|
||||
const { wrapper } = mountView();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme Corp', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme Corp', createdAt: '', isPrimary: false });
|
||||
|
||||
expect(wrapper.find('[data-testid="profile-org-name"]').exists()).toBe(false); // org name shown in v-if block
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
mountView();
|
||||
@@ -74,7 +74,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -88,7 +88,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -104,7 +104,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -123,7 +123,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -143,7 +143,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.deleteSegment).mockResolvedValue(undefined);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -172,7 +172,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.deleteSegment).mockResolvedValue(undefined);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -196,7 +196,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
@@ -214,7 +214,7 @@ describe('SegmentsView', () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([...mockSegments]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
|
||||
@@ -26,7 +26,7 @@ import * as environmentsApi from '@/api/environments';
|
||||
import * as apiKeysApi from '@/api/apiKeys';
|
||||
import * as webhooksApi from '@/api/webhooks';
|
||||
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
const mockProject: ProjectResponse = { id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' };
|
||||
const mockEnvs: EnvironmentResponse[] = [
|
||||
{ id: 100, name: 'Development', apiKey: 'env-dev', projectId: 10, createdAt: '2026-01-01T00:00:00Z' },
|
||||
|
||||
@@ -10,5 +10,6 @@ public class OrganizationUserConfiguration : IEntityTypeConfiguration<Organizati
|
||||
{
|
||||
builder.HasKey(ou => new { ou.OrganizationId, ou.UserId });
|
||||
builder.Property(ou => ou.Role).IsRequired();
|
||||
builder.Property(ou => ou.IsPrimary).IsRequired().HasDefaultValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
941
src/api/MicCheck.Api/Migrations/20260611164933_AddOrganizationUserIsPrimary.Designer.cs
generated
Normal file
941
src/api/MicCheck.Api/Migrations/20260611164933_AddOrganizationUserIsPrimary.Designer.cs
generated
Normal file
@@ -0,0 +1,941 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MicCheck.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MicCheck.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(MicCheckDbContext))]
|
||||
[Migration("20260611164933_AddOrganizationUserIsPrimary")]
|
||||
partial class AddOrganizationUserIsPrimary
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FeatureTags", b =>
|
||||
{
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TagsId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("FeatureId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("FeatureTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Audit.AuditLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int?>("ActorUserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Changes")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ResourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId", "CreatedAt");
|
||||
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Common.Security.ApiKeys.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Prefix")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Common.Security.Authorization.UserProjectPermission", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "ProjectId");
|
||||
|
||||
b.ToTable("UserProjectPermissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApiKey")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Environments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("DefaultEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("InitialValue")
|
||||
.HasMaxLength(20000)
|
||||
.HasColumnType("character varying(20000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Features");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureSegment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FeatureSegments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureState", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FeatureSegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("IdentityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(20000)
|
||||
.HasColumnType("character varying(20000)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId");
|
||||
|
||||
b.HasIndex("FeatureSegmentId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("IdentityId");
|
||||
|
||||
b.HasIndex("FeatureId", "EnvironmentId", "IdentityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.Identity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId", "Identifier")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Identities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.IdentityTrait", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("IdentityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("ValueType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IdentityId", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("IdentityTraits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.Organization", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("InviteToken")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InviteToken")
|
||||
.IsUnique()
|
||||
.HasFilter("\"InviteToken\" IS NOT NULL");
|
||||
|
||||
b.ToTable("Organizations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.OrganizationUser", b =>
|
||||
{
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("OrganizationId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("OrganizationUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("HideDisabledFlags")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Segments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentCondition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Operator")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Property")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("RuleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RuleId");
|
||||
|
||||
b.ToTable("SegmentConditions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("ParentRuleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRuleId");
|
||||
|
||||
b.HasIndex("SegmentId");
|
||||
|
||||
b.ToTable("SegmentRules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.RefreshToken", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsRevoked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsTwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastLoginAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Webhooks.Webhook", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int?>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Scope")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId");
|
||||
|
||||
b.ToTable("Webhooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Webhooks.WebhookDeliveryLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AttemptNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("AttemptedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeSpan>("Duration")
|
||||
.HasColumnType("interval");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("PayloadJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ResponseBody")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("ResponseStatusCode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("Success")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("WebhookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WebhookId");
|
||||
|
||||
b.ToTable("WebhookDeliveryLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FeatureTags", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Features.Feature", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("FeatureId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Common.Security.ApiKeys.ApiKey", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("ApiKeys")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Environments")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Features")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureState", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Environments.Environment", null)
|
||||
.WithMany("FeatureStates")
|
||||
.HasForeignKey("EnvironmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.Feature", null)
|
||||
.WithMany("FeatureStates")
|
||||
.HasForeignKey("FeatureId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.FeatureSegment", null)
|
||||
.WithOne("FeatureState")
|
||||
.HasForeignKey("MicCheck.Api.Features.FeatureState", "FeatureSegmentId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("MicCheck.Api.Identities.Identity", null)
|
||||
.WithMany("FeatureStateOverrides")
|
||||
.HasForeignKey("IdentityId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Tag", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.IdentityTrait", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Identities.Identity", null)
|
||||
.WithMany("Traits")
|
||||
.HasForeignKey("IdentityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.OrganizationUser", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Users.User", "User")
|
||||
.WithMany("Organizations")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Segments")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentCondition", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Segments.SegmentRule", null)
|
||||
.WithMany("Conditions")
|
||||
.HasForeignKey("RuleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Segments.SegmentRule", null)
|
||||
.WithMany("ChildRules")
|
||||
.HasForeignKey("ParentRuleId")
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
|
||||
b.HasOne("MicCheck.Api.Segments.Segment", null)
|
||||
.WithMany("Rules")
|
||||
.HasForeignKey("SegmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.Navigation("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.Navigation("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureSegment", b =>
|
||||
{
|
||||
b.Navigation("FeatureState");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.Identity", b =>
|
||||
{
|
||||
b.Navigation("FeatureStateOverrides");
|
||||
|
||||
b.Navigation("Traits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.Organization", b =>
|
||||
{
|
||||
b.Navigation("ApiKeys");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Environments");
|
||||
|
||||
b.Navigation("Features");
|
||||
|
||||
b.Navigation("Segments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.Navigation("Rules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.Navigation("ChildRules");
|
||||
|
||||
b.Navigation("Conditions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.User", b =>
|
||||
{
|
||||
b.Navigation("Organizations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MicCheck.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOrganizationUserIsPrimary : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsPrimary",
|
||||
table: "OrganizationUsers",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsPrimary",
|
||||
table: "OrganizationUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,12 @@ namespace MicCheck.Api.Organizations;
|
||||
public record OrganizationResponse(
|
||||
int Id,
|
||||
string Name,
|
||||
DateTimeOffset CreatedAt
|
||||
DateTimeOffset CreatedAt,
|
||||
bool IsPrimary
|
||||
)
|
||||
{
|
||||
public static OrganizationResponse From(Organization org) => new(
|
||||
org.Id, org.Name, org.CreatedAt);
|
||||
public static OrganizationResponse From(Organization org, bool isPrimary = false) => new(
|
||||
org.Id, org.Name, org.CreatedAt, isPrimary);
|
||||
}
|
||||
|
||||
public record OrganizationMemberResponse(
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using MicCheck.Api.Audit;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Users;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public class OrganizationService(MicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
public async Task<IReadOnlyList<Organization>> ListForUserAsync(int userId, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<(Organization Org, bool IsPrimary)>> ListForUserAsync(int userId, CancellationToken ct = default)
|
||||
{
|
||||
return await db.Organizations
|
||||
.Where(o => o.Members.Any(m => m.UserId == userId))
|
||||
.ToListAsync(ct);
|
||||
var rows = await (
|
||||
from org in db.Organizations
|
||||
join member in db.OrganizationUsers on org.Id equals member.OrganizationId
|
||||
where member.UserId == userId
|
||||
select new { org, member.IsPrimary }
|
||||
).ToListAsync(ct);
|
||||
|
||||
return rows.Select(r => (r.org, r.IsPrimary)).ToList();
|
||||
}
|
||||
|
||||
public async Task<Organization?> FindByIdAsync(int id, CancellationToken ct = default)
|
||||
@@ -21,6 +25,8 @@ public class OrganizationService(MicCheckDbContext db, AuditService auditService
|
||||
|
||||
public async Task<Organization> CreateAsync(string name, int creatorUserId, CancellationToken ct = default)
|
||||
{
|
||||
var hasExistingOrg = await db.OrganizationUsers.AnyAsync(ou => ou.UserId == creatorUserId, ct);
|
||||
|
||||
var org = new Organization
|
||||
{
|
||||
Name = name,
|
||||
@@ -29,7 +35,8 @@ public class OrganizationService(MicCheckDbContext db, AuditService auditService
|
||||
org.Members.Add(new OrganizationUser
|
||||
{
|
||||
UserId = creatorUserId,
|
||||
Role = OrganizationRole.Admin
|
||||
Role = OrganizationRole.Admin,
|
||||
IsPrimary = !hasExistingOrg
|
||||
});
|
||||
db.Organizations.Add(org);
|
||||
await db.SaveChangesAsync(ct);
|
||||
@@ -39,6 +46,20 @@ public class OrganizationService(MicCheckDbContext db, AuditService auditService
|
||||
return org;
|
||||
}
|
||||
|
||||
public async Task SetPrimaryAsync(int organizationId, int userId, CancellationToken ct = default)
|
||||
{
|
||||
var member = await db.OrganizationUsers
|
||||
.FirstOrDefaultAsync(ou => ou.OrganizationId == organizationId && ou.UserId == userId, ct)
|
||||
?? throw new KeyNotFoundException($"User is not a member of organization {organizationId}.");
|
||||
|
||||
await db.OrganizationUsers
|
||||
.Where(ou => ou.UserId == userId && ou.IsPrimary)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(ou => ou.IsPrimary, false), ct);
|
||||
|
||||
member.IsPrimary = true;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Organization> UpdateAsync(int id, string name, CancellationToken ct = default)
|
||||
{
|
||||
var org = await db.Organizations.FirstOrDefaultAsync(o => o.Id == id, ct)
|
||||
@@ -129,8 +150,7 @@ public class OrganizationService(MicCheckDbContext db, AuditService auditService
|
||||
public async Task<Organization?> FindByInviteTokenAsync(string token, CancellationToken ct = default) =>
|
||||
await db.Organizations.FirstOrDefaultAsync(o => o.InviteToken == token, ct);
|
||||
|
||||
public async Task<IReadOnlyList<InviteByEmailResult>> InviteUsersByEmailAsync(
|
||||
int organizationId, IReadOnlyList<InviteByEmailEntry> invites, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<InviteByEmailResult>> InviteUsersByEmailAsync(int organizationId, IReadOnlyList<InviteByEmailEntry> invites, CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<InviteByEmailResult>();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ public class OrganizationUser
|
||||
public int OrganizationId { get; init; }
|
||||
public int UserId { get; init; }
|
||||
public OrganizationRole Role { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
public User User { get; init; } = null!;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
if (userId is null) return Unauthorized();
|
||||
|
||||
var all = await organizationService.ListForUserAsync(userId.Value, ct);
|
||||
var paged = all.Skip((page - 1) * pageSize).Take(pageSize).Select(OrganizationResponse.From).ToList();
|
||||
var paged = all.Skip((page - 1) * pageSize).Take(pageSize)
|
||||
.Select(x => OrganizationResponse.From(x.Org, x.IsPrimary)).ToList();
|
||||
|
||||
return Ok(new PaginatedResponse<OrganizationResponse>(all.Count, null, null, paged));
|
||||
}
|
||||
@@ -57,6 +58,23 @@ public class OrganizationsController(OrganizationService organizationService, We
|
||||
return Ok(OrganizationResponse.From(updated));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/primary")]
|
||||
public async Task<IActionResult> SetPrimary(int id, CancellationToken ct)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId is null) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
await organizationService.SetPrimaryAsync(id, userId.Value, ct);
|
||||
return NoContent();
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -5,15 +5,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Segments;
|
||||
|
||||
public record SegmentConditionDefinition(
|
||||
string Property,
|
||||
SegmentConditionOperator Operator,
|
||||
string Value);
|
||||
public record SegmentConditionDefinition(string Property, SegmentConditionOperator Operator, string Value);
|
||||
|
||||
public record SegmentRuleDefinition(
|
||||
SegmentRuleType Type,
|
||||
IReadOnlyList<SegmentConditionDefinition> Conditions,
|
||||
IReadOnlyList<SegmentRuleDefinition>? ChildRules = null);
|
||||
public record SegmentRuleDefinition(SegmentRuleType Type, IReadOnlyList<SegmentConditionDefinition> Conditions, IReadOnlyList<SegmentRuleDefinition>? ChildRules = null);
|
||||
|
||||
public class SegmentService(MicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
@@ -37,11 +31,7 @@ public class SegmentService(MicCheckDbContext db, AuditService auditService)
|
||||
.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
}
|
||||
|
||||
public async Task<Segment> CreateAsync(
|
||||
int projectId,
|
||||
string name,
|
||||
IReadOnlyList<SegmentRuleDefinition> rules,
|
||||
CancellationToken ct = default)
|
||||
public async Task<Segment> CreateAsync(int projectId, string name, IReadOnlyList<SegmentRuleDefinition> rules, CancellationToken ct = default)
|
||||
{
|
||||
var count = await db.Segments.CountAsync(s => s.ProjectId == projectId, ct);
|
||||
if (count >= MaxSegmentsPerProject)
|
||||
@@ -75,11 +65,7 @@ public class SegmentService(MicCheckDbContext db, AuditService auditService)
|
||||
return segment;
|
||||
}
|
||||
|
||||
public async Task<Segment> UpdateAsync(
|
||||
int id,
|
||||
string name,
|
||||
IReadOnlyList<SegmentRuleDefinition> rules,
|
||||
CancellationToken ct = default)
|
||||
public async Task<Segment> UpdateAsync(int id, string name, IReadOnlyList<SegmentRuleDefinition> rules, CancellationToken ct = default)
|
||||
{
|
||||
var segment = await db.Segments
|
||||
.Include(s => s.Rules)
|
||||
|
||||
Reference in New Issue
Block a user