diff --git a/README.md b/README.md index 1a14cc80..99e3dedc 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ This project is using [knex](https://knexjs.org/guide/migrations.html#migration- NOTE: Migrations should use snake_case. While database table and column names use snake_case, we are using Sequelize for our models so that we get camelCase to match the JS standard, in the JS section of the codebase. -1. To create a new migration from the template [sample-migration](./api/src/db/template/sample-migration.ts) do: +1. To create a new migration from the template [sample-migration](./api/src/db/templates/sample-migration.ts) do: ```bash dev migrate make create-users-table diff --git a/agents/README.md b/agents/README.md index 1128d745..f8fb55e5 100644 --- a/agents/README.md +++ b/agents/README.md @@ -182,7 +182,7 @@ ls -li .cursor/workflows/ If you need agent-specific versions, you can: -1. **Keep in `agents/[agent-name]/`** instead of hardlinking: +1. **Keep in a dedicated `agents/` subdirectory for that agent** instead of hardlinking: ```bash mkdir -p agents/claude/workflows cp agents/workflows/create-admin-ui.md agents/claude/workflows/ diff --git a/agents/templates/README.md b/agents/templates/README.md index a2b4b86d..0a742a7b 100644 --- a/agents/templates/README.md +++ b/agents/templates/README.md @@ -27,7 +27,7 @@ templates/ ## Usage Templates are referenced by workflows. See: -- [create-admin-ui](../workflows/create-admin-ui/) - Full CRUD admin interface +- [create-admin-ui](../workflows/create-admin-ui.md) - Full CRUD admin interface Each template file contains: 1. Location path for the new file diff --git a/agents/workflows/create-admin-ui.md b/agents/workflows/create-admin-ui.md index 8ba6c473..1a9b7eaa 100644 --- a/agents/workflows/create-admin-ui.md +++ b/agents/workflows/create-admin-ui.md @@ -37,11 +37,11 @@ Templates are located in `agents/templates/` for reuse across workflows. | Template | Location | Description | |----------|----------|-------------| -| [Model](../../templates/backend/model.md) | `api/src/models/` | Sequelize model with scopes | -| [Controller](../../templates/backend/controller.md) | `api/src/controllers/` | CRUD endpoints | -| [Policy](../../templates/backend/policy.md) | `api/src/policies/` | Authorization rules | -| [Services](../../templates/backend/services.md) | `api/src/services/{resources}/` | Create, Update, Destroy | -| [Serializers](../../templates/backend/serializers.md) | `api/src/serializers/{resources}/` | Index, Show, Reference | +| [Model](../templates/backend/model.md) | `api/src/models/` | Sequelize model with scopes | +| [Controller](../templates/backend/controller.md) | `api/src/controllers/` | CRUD endpoints | +| [Policy](../templates/backend/policy.md) | `api/src/policies/` | Authorization rules | +| [Services](../templates/backend/services.md) | `api/src/services/{resources}/` | Create, Update, Destroy | +| [Serializers](../templates/backend/serializers.md) | `api/src/serializers/{resources}/` | Index, Show, Reference | **Integration Points:** - `api/src/controllers/index.ts` - Export controller @@ -52,10 +52,10 @@ Templates are located in `agents/templates/` for reuse across workflows. | Template | Location | Description | |----------|----------|-------------| -| [API Client](../../templates/frontend/api-client.md) | `web/src/api/` | Type-safe HTTP client | -| [Composables](../../templates/frontend/composables.md) | `web/src/use/` | Reactive data fetching | -| [Components](../../templates/frontend/components.md) | `web/src/components/{resources}/` | DataTable, Forms, UniqueTextField | -| [Pages](../../templates/frontend/pages.md) | `web/src/pages/admin/{resources}/` | List, New, Edit pages | +| [API Client](../templates/frontend/api-client.md) | `web/src/api/` | Type-safe HTTP client | +| [Composables](../templates/frontend/composables.md) | `web/src/use/` | Reactive data fetching | +| [Components](../templates/frontend/components.md) | `web/src/components/{resources}/` | DataTable, Forms, UniqueTextField | +| [Pages](../templates/frontend/pages.md) | `web/src/pages/admin/{resources}/` | List, New, Edit pages | **Integration Points:** - `web/src/routes.ts` - Add routes diff --git a/api/src/serializers/information-sharing-agreements/generate-confidentiality-acknowledgement/create-serializer.ts b/api/src/serializers/information-sharing-agreements/generate-confidentiality-acknowledgement/create-serializer.ts index fe6b5752..022efad5 100644 --- a/api/src/serializers/information-sharing-agreements/generate-confidentiality-acknowledgement/create-serializer.ts +++ b/api/src/serializers/information-sharing-agreements/generate-confidentiality-acknowledgement/create-serializer.ts @@ -36,6 +36,7 @@ export type InformationSharingAgreementAsConfidentialityAcknowledgement = { "access_level.is_internal": boolean "access_level.is_protected_and_limited": boolean "access_level.is_confidential_and_restricted": boolean + access_level_department_restriction: string department_branch_unit_hierarchy: string has_additional_access_restrictions: boolean additional_access_restrictions: string @@ -128,6 +129,7 @@ export class CreateSerializer extends BaseSerializer = { + [InformationSharingAgreement.AccessLevels.INTERNAL]: ArchiveItem.Levels.LOW, + [InformationSharingAgreement.AccessLevels.PROTECTED_AND_LIMITED]: ArchiveItem.Levels.MEDIUM, + [InformationSharingAgreement.AccessLevels.CONFIDENTIAL_AND_RESTRICTED]: ArchiveItem.Levels.HIGH, } export type ArchiveItemFilesAttributes = { @@ -21,7 +23,6 @@ export type ArchiveItemFilesAttributes = { } export type ArchiveItemCreationAttributes = Partial> & { - archiveItemCategoriesAttributes?: ArchiveItemCategoriesAttributes[] archiveItemFilesAttributes?: ArchiveItemFilesAttributes[] } @@ -35,41 +36,36 @@ export class CreateService extends BaseService { } async perform(): Promise { - const { - title, - status, - isDecision, - confidentialityReceipt, - securityLevel, - archiveItemCategoriesAttributes, - archiveItemFilesAttributes, - ...optionalAttributes - } = this.attributes - - if (isNil(title) || isEmpty(title)) { - throw new Error("Title is required") - } + const { confidentialityReceipt, archiveItemFilesAttributes, ...optionalAttributes } = + this.attributes if (isNil(confidentialityReceipt) || confidentialityReceipt !== true) { throw new Error("Confidentiality receipt is required, and must be true") } - if (isNil(securityLevel)) { - throw new Error("Security level is required") + const { title, purpose, authorizedApplication, accessLevel, externalGroupContactId } = + this.informationSharingAgreement + + if (isNil(title) || isEmpty(title)) { + throw new Error("Title is required") } - // TODO: remove these from the model if they are unused by the UI. - const isDecisionOrFallback = isDecision ?? false - const statusOrFallback = status ?? ArchiveItem.Statuses.ACCEPTED + const accessLevelOrDefault = accessLevel ?? InformationSharingAgreement.AccessLevels.INTERNAL + const securityLevel = ACCESS_LEVEL_TO_SECURITY_LEVEL[accessLevelOrDefault] + + const yukonFirstNations = await this.resolveYukonFirstNations(externalGroupContactId) return db.transaction(async () => { const archiveItem = await ArchiveItem.create({ ...optionalAttributes, title, confidentialityReceipt, - isDecision: isDecisionOrFallback, - status: statusOrFallback, + isDecision: false, + status: ArchiveItem.Statuses.ACCEPTED, securityLevel, + sharingPurpose: authorizedApplication, + description: purpose, + yukonFirstNations, userId: this.currentUser.id, }) @@ -79,8 +75,6 @@ export class CreateService extends BaseService { await this.uploadFilesForArchiveItem(archiveItem.id, archiveItemFilesAttributes) } - await this.assignCategoriesToArchiveItem(archiveItem.id, archiveItemCategoriesAttributes) - return archiveItem.reload({ include: ["categories", "accessGrants", "user"], }) @@ -104,21 +98,29 @@ export class CreateService extends BaseService { }) } - private async assignCategoriesToArchiveItem( - archiveItemId: number, - archiveItemCategoriesAttributes: { categoryId: number }[] | undefined - ): Promise { - if (isNil(archiveItemCategoriesAttributes) || isEmpty(archiveItemCategoriesAttributes)) { - return + private async resolveYukonFirstNations( + externalGroupContactId: number | null + ): Promise { + if (isNil(externalGroupContactId)) return null + + const organization = await ExternalOrganization.findOne({ + include: [ + { + association: "users", + attributes: [], + where: { + id: externalGroupContactId, + }, + }, + ], + }) + + if (isNil(organization)) { + throw new Error("External group contact is missing its associated external organization") } - const archiveItemCategoriesAttributesWithArchiveItemId = archiveItemCategoriesAttributes.map( - ({ categoryId }) => ({ - archiveItemId, - categoryId, - }) - ) - await ArchiveItemCategory.bulkCreate(archiveItemCategoriesAttributesWithArchiveItemId) + const { name } = organization + return [name] } } diff --git a/bin/open-in-editor-bridge.rb b/bin/open-in-editor-bridge.rb old mode 100644 new mode 100755 index 503200bc..b9ad63e3 --- a/bin/open-in-editor-bridge.rb +++ b/bin/open-in-editor-bridge.rb @@ -81,7 +81,7 @@ def shutdown return unless pid - Process.kill("TERM", pid) + Process.kill("TERM", -pid) delete_pid_file puts "Stopped editor bridge." diff --git a/web/src/api/information-sharing-agreements/archive-items-api.ts b/web/src/api/information-sharing-agreements/archive-items-api.ts index dbeed678..7b4c248a 100644 --- a/web/src/api/information-sharing-agreements/archive-items-api.ts +++ b/web/src/api/information-sharing-agreements/archive-items-api.ts @@ -1,19 +1,8 @@ import http from "@/api/http-client" import { type ArchiveItemAsShow } from "@/api/archive-items-api" -import { SecurityLevel } from "@/api/archive-items-api" export type ArchiveItemCreationAttributes = { - title: string - description: string | null - sharingPurpose: string | null confidentialityReceipt: boolean - yukonFirstNations: string[] - securityLevel: SecurityLevel - tags: string[] -} & { - archiveItemCategoriesAttributes: { - categoryId: number - }[] } export const archiveItemsApi = { diff --git a/web/src/components/archive-items/ArchiveItemNewButton.vue b/web/src/components/archive-items/ArchiveItemNewButton.vue deleted file mode 100644 index 454faa78..00000000 --- a/web/src/components/archive-items/ArchiveItemNewButton.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantEditDataTableServer.vue b/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantEditDataTableServer.vue index bb03391e..9355d29b 100644 --- a/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantEditDataTableServer.vue +++ b/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantEditDataTableServer.vue @@ -142,7 +142,7 @@ const router = useRouter() function goToUserPage(userId: number) { // TODO: standardize this route to redirect to user read page (once a read page exists) return router.push({ - name: "users/UserEditPage", + name: "users/UserInternalEditPage", params: { userId, }, diff --git a/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantsDataIterator.vue b/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantsDataIterator.vue index b5dedf90..4e8c42f1 100644 --- a/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantsDataIterator.vue +++ b/web/src/components/information-sharing-agreement-access-grants/InformationSharingAgreementAccessGrantsDataIterator.vue @@ -87,7 +87,7 @@ const router = useRouter() function goToUserPage(userId: number) { // TODO: standardize this route to redirect to user read page return router.push({ - name: "users/UserEditPage", + name: "users/UserInternalEditPage", params: { userId, }, diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminCreateForm.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminCreateForm.vue deleted file mode 100644 index 27dcd7db..00000000 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminCreateForm.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminEditForm.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminEditForm.vue deleted file mode 100644 index bd2605db..00000000 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementAdminEditForm.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementBasicInformationCard.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementBasicInformationCard.vue index e0db27f9..b7c73ab9 100644 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementBasicInformationCard.vue +++ b/web/src/components/information-sharing-agreements/InformationSharingAgreementBasicInformationCard.vue @@ -1,8 +1,10 @@ @@ -82,10 +44,8 @@ import { isNil } from "lodash" import { formatDate } from "@/utils/formatters" -import informationSharingAgreementsApi from "@/api/information-sharing-agreements-api" import useVuetifySortByToSafeRouteQuery from "@/use/utils/use-vuetify-sort-by-to-safe-route-query" import useVuetifySortByToSequelizeSafeOrder from "@/use/utils/use-vuetify-sort-by-to-sequelize-safe-order" -import useSnack from "@/use/use-snack" import useInformationSharingAgreements, { type InformationSharingAgreementAsIndex, type InformationSharingAgreementWhereOptions, @@ -146,11 +106,6 @@ const headers = ref([ title: "Creator", key: "creatorId", }, - { - title: "Actions", - key: "actions", - sortable: false, - }, ]) const page = useRouteQuery(`page${props.routeQuerySuffix}`, "1", { transform: Number }) @@ -185,27 +140,6 @@ function goToInformationSharingAgreementPage(informationSharingAgreementId: numb }) } -const snack = useSnack() -const isDeleting = ref(false) - -async function confirmThenDelete(informationSharingAgreement: InformationSharingAgreementAsIndex) { - const { title } = informationSharingAgreement - - const result = confirm(`Are you sure you want to delete ${title}.`) - if (result === false) return - - isDeleting.value = true - try { - await informationSharingAgreementsApi.delete(informationSharingAgreement.id) - await refresh() - } catch (error) { - console.error(error) - snack.error(`Failed to delete information sharing agreement: ${error}`) - } finally { - isDeleting.value = false - } -} - defineExpose({ refresh, }) diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateDialog.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateDialog.vue new file mode 100644 index 00000000..14117f9b --- /dev/null +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateDialog.vue @@ -0,0 +1,147 @@ + + + diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue deleted file mode 100644 index 130cfd57..00000000 --- a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - diff --git a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue index d6e6e0e4..099bf350 100644 --- a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue @@ -3,6 +3,16 @@ v-bind="primaryButtonAttributes" :loading="isLoading" > + diff --git a/web/src/components/user-groups/UserGroupsAsUsersEditDataTableServer.vue b/web/src/components/user-groups/UserGroupsAsUsersEditDataTableServer.vue index 7256a7c8..000931b2 100644 --- a/web/src/components/user-groups/UserGroupsAsUsersEditDataTableServer.vue +++ b/web/src/components/user-groups/UserGroupsAsUsersEditDataTableServer.vue @@ -129,7 +129,7 @@ const router = useRouter() function goToUserPage(userId: number) { // TODO: standardize this route to redirect to user read page return router.push({ - name: "users/UserEditPage", + name: "users/UserInternalEditPage", params: { userId, }, diff --git a/web/src/components/users/UserChip.vue b/web/src/components/users/UserChip.vue index a0a268b7..1c3adfb1 100644 --- a/web/src/components/users/UserChip.vue +++ b/web/src/components/users/UserChip.vue @@ -160,7 +160,7 @@ const userProfileLink = computed(() => { if (isSystemAdmin.value) { return { - name: "users/UserEditPage", + name: "users/UserInternalEditPage", params: { userId: userId.value, }, diff --git a/web/src/components/users/UserExternalEditForm.vue b/web/src/components/users/UserExternalEditForm.vue new file mode 100644 index 00000000..ce14422c --- /dev/null +++ b/web/src/components/users/UserExternalEditForm.vue @@ -0,0 +1,255 @@ + + + diff --git a/web/src/components/users/UserEditForm.vue b/web/src/components/users/UserInternalEditForm.vue similarity index 100% rename from web/src/components/users/UserEditForm.vue rename to web/src/components/users/UserInternalEditForm.vue diff --git a/web/src/components/users/UsersEditDataTableServer.vue b/web/src/components/users/UsersEditDataTableServer.vue index a670651c..e97ace81 100644 --- a/web/src/components/users/UsersEditDataTableServer.vue +++ b/web/src/components/users/UsersEditDataTableServer.vue @@ -7,7 +7,7 @@ :items="users" :items-length="totalCount" :loading="isLoading" - @click:row="(_event: unknown, { item }: UserTableRow) => goToUserEditPage(item.id)" + @click:row="(_event: unknown, { item }: UserTableRow) => goToUserEditPage(item)" > diff --git a/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementNewPage.vue b/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementNewPage.vue deleted file mode 100644 index 5b4ec80f..00000000 --- a/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementNewPage.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementPage.vue b/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementPage.vue index bceb9bfd..47fcb6a5 100644 --- a/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementPage.vue +++ b/web/src/pages/administration/information-sharing-agreements/InformationSharingAgreementPage.vue @@ -1,9 +1,33 @@ diff --git a/web/src/pages/users/UserExternalEditPage.vue b/web/src/pages/users/UserExternalEditPage.vue new file mode 100644 index 00000000..c76bb9ab --- /dev/null +++ b/web/src/pages/users/UserExternalEditPage.vue @@ -0,0 +1,35 @@ + + + diff --git a/web/src/pages/users/UserEditPage.vue b/web/src/pages/users/UserInternalEditPage.vue similarity index 86% rename from web/src/pages/users/UserEditPage.vue rename to web/src/pages/users/UserInternalEditPage.vue index 88d920e6..54f04a9a 100644 --- a/web/src/pages/users/UserEditPage.vue +++ b/web/src/pages/users/UserInternalEditPage.vue @@ -1,7 +1,7 @@