From 313a39897db790347963e348b4e08c3c62ad5374 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 19 Feb 2026 15:25:27 -0700 Subject: [PATCH 01/56] :sparkles: Merge ISA draft actions into and extended actions menu. Why? We need more actions and varying action menus per-state. Base actions menu code taken from https://github.com/icefoganalytics/wrap/blob/3abe4c91eaf2206578446090dec42ba2dec91ecd/web/src/components/common/BaseActionsMenuBtnGroup.vue --- .../common/BaseActionsMenuBtnGroup.vue | 105 +++++++++++++++ ...InformationSharingAgreementActionsMenu.vue | 33 +++++ ...mationSharingAgreementDraftActionsMenu.vue | 124 ++++++++++++++++++ .../InformationSharingAgreementPage.vue | 73 +++-------- .../use/utils/use-authenticated-download.ts | 51 +++++++ ...color-classes-as-specific-color-classes.ts | 25 ++++ 6 files changed, 355 insertions(+), 56 deletions(-) create mode 100644 web/src/components/common/BaseActionsMenuBtnGroup.vue create mode 100644 web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue create mode 100644 web/src/components/information-sharing-agreements/draft/InformationSharingAgreementDraftActionsMenu.vue create mode 100644 web/src/use/utils/use-authenticated-download.ts create mode 100644 web/src/use/utils/use-vuetify-color-classes-as-specific-color-classes.ts diff --git a/web/src/components/common/BaseActionsMenuBtnGroup.vue b/web/src/components/common/BaseActionsMenuBtnGroup.vue new file mode 100644 index 00000000..65e9555f --- /dev/null +++ b/web/src/components/common/BaseActionsMenuBtnGroup.vue @@ -0,0 +1,105 @@ + + + + + + + diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue new file mode 100644 index 00000000..30a1a5c1 --- /dev/null +++ b/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/web/src/components/information-sharing-agreements/draft/InformationSharingAgreementDraftActionsMenu.vue b/web/src/components/information-sharing-agreements/draft/InformationSharingAgreementDraftActionsMenu.vue new file mode 100644 index 00000000..536b0a29 --- /dev/null +++ b/web/src/components/information-sharing-agreements/draft/InformationSharingAgreementDraftActionsMenu.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue b/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue index 216a1c4e..1dfe7426 100644 --- a/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue +++ b/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue @@ -52,34 +52,13 @@
+ + -
-
- - Edit Agreement - - - Back to List - -
+ + Back to List +
@@ -149,9 +110,9 @@ import useInformationSharingAgreement, { } from "@/use/use-information-sharing-agreement" import InformationSharingAgreementAccessCard from "@/components/information-sharing-agreements/InformationSharingAgreementAccessCard.vue" +import InformationSharingAgreementActionsMenu from "@/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue" import InformationSharingAgreementBasicInformationCard from "@/components/information-sharing-agreements/InformationSharingAgreementBasicInformationCard.vue" import InformationSharingAgreementConfidentialityCard from "@/components/information-sharing-agreements/InformationSharingAgreementConfidentialityCard.vue" -import InformationSharingAgreementDownloadDraftButton from "@/components/information-sharing-agreements/InformationSharingAgreementDownloadDraftButton.vue" import InformationSharingAgreementDownloadSignedAcknowledgementButton from "@/components/information-sharing-agreements/InformationSharingAgreementDownloadSignedAcknowledgementButton.vue" import InformationSharingAgreementDurationCard from "@/components/information-sharing-agreements/InformationSharingAgreementDurationCard.vue" import InformationSharingAgreementRevertToDraftDialog from "@/components/information-sharing-agreements/InformationSharingAgreementRevertToDraftDialog.vue" @@ -163,7 +124,7 @@ const props = defineProps<{ const informationSharingAgreementIdAsNumber = computed(() => parseInt(props.informationSharingAgreementId) ) -const { informationSharingAgreement, policy, refresh } = useInformationSharingAgreement( +const { informationSharingAgreement, refresh } = useInformationSharingAgreement( informationSharingAgreementIdAsNumber ) diff --git a/web/src/use/utils/use-authenticated-download.ts b/web/src/use/utils/use-authenticated-download.ts new file mode 100644 index 00000000..c68f57a2 --- /dev/null +++ b/web/src/use/utils/use-authenticated-download.ts @@ -0,0 +1,51 @@ +import { ref, nextTick, type Ref } from "vue" +import { useAuth0 } from "@auth0/auth0-vue" + +export function useAuthenticatedDownload(actionUrl: Ref) { + const { getAccessTokenSilently } = useAuth0() + + const isLoading = ref(false) + + async function submit() { + isLoading.value = true + try { + const accessToken = await getAccessTokenSilently() + + // Create form element + const form = document.createElement("form") + form.action = actionUrl.value + form.method = "post" + form.target = "_blank" + + // Create hidden input + const input = document.createElement("input") + input.type = "hidden" + input.name = "HOISTABLE_AUTHORIZATION_TOKEN" + input.value = accessToken + + // Append input to form and form to body + form.appendChild(input) + document.body.appendChild(form) + + // Submit form + form.submit() + + await nextTick() + + // Clean up + document.body.removeChild(form) + } catch (error) { + console.error(`Error fetching new access token: ${error}`, { error }) + throw error + } finally { + isLoading.value = false + } + } + + return { + submit, + isLoading, + } +} + +export default useAuthenticatedDownload diff --git a/web/src/use/utils/use-vuetify-color-classes-as-specific-color-classes.ts b/web/src/use/utils/use-vuetify-color-classes-as-specific-color-classes.ts new file mode 100644 index 00000000..152f2bc2 --- /dev/null +++ b/web/src/use/utils/use-vuetify-color-classes-as-specific-color-classes.ts @@ -0,0 +1,25 @@ +import { computed, Ref, toValue } from "vue" +import { isNil, isEmpty } from "lodash" + +/** + * Returns a computed ref that returns a specific color class based on the provided color classes. + * Given + * "red" could be used produce "text-red" or "bg-red" or "border-red" + */ +export function useVuetifyColorClassesAsSpecificColorClasses( + colorClasses: Ref, + prefix: string = "text" +) { + const textColorClass = computed(() => { + const colorClassesValues = toValue(colorClasses) + if (isNil(colorClassesValues)) return "" + if (isEmpty(colorClassesValues)) return "" + + const normalizedClass = colorClassesValues.split(" ").join("-") + return `${prefix}-${normalizedClass}` + }) + + return textColorClass +} + +export default useVuetifyColorClassesAsSpecificColorClasses From ffb7858ce5e8e5ca7e4e81e6859bf746da5d0df5 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 19 Feb 2026 15:53:33 -0700 Subject: [PATCH 02/56] :recycle: Move sign actions to an extended actions menu. --- ...InformationSharingAgreementActionsMenu.vue | 12 ++ ...ationSharingAgreementSignedActionsMenu.vue | 105 ++++++++++++++++++ .../InformationSharingAgreementPage.vue | 43 +------ 3 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue index 30a1a5c1..ab302118 100644 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/InformationSharingAgreementActionsMenu.vue @@ -3,6 +3,17 @@ v-if="informationSharingAgreementStatus === InformationSharingAgreementStatuses.DRAFT" :information-sharing-agreement-id="informationSharingAgreementId" /> + + + + + Signed Acknowledgement + + + + + Revert to Draft + + + + + + + + Create Knowledge Item + + + + + + + + + diff --git a/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue b/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue index 1dfe7426..7d9878ca 100644 --- a/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue +++ b/web/src/pages/information-sharing-agreements/InformationSharingAgreementPage.vue @@ -51,38 +51,11 @@ />
-
- - - -
+ Date: Thu, 19 Feb 2026 17:09:13 -0700 Subject: [PATCH 03/56] :construction: Add most of front-end ArchiveItem creation page from ISA. TODO: need to add dedicated back-end endpoint for creation ArchiveItem from ISA namespace. --- web/src/api/archive-items-api.ts | 2 +- ...nSharingAgreementArchiveItemCreateForm.vue | 226 ++++++++++++++++++ ...ationSharingAgreementSignedActionsMenu.vue | 13 +- ...tionSharingAgreementArchiveItemNewPage.vue | 32 +++ web/src/routes.ts | 7 + web/src/utils/validators/email.ts | 10 + web/src/utils/validators/index.ts | 1 + 7 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue create mode 100644 web/src/pages/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemNewPage.vue create mode 100644 web/src/utils/validators/email.ts diff --git a/web/src/api/archive-items-api.ts b/web/src/api/archive-items-api.ts index 234bfba3..84f4b4f1 100644 --- a/web/src/api/archive-items-api.ts +++ b/web/src/api/archive-items-api.ts @@ -95,7 +95,7 @@ export const archiveItemsApi = { const { data } = await http.get(`/api/archive-items/${archiveItemId}`) return data }, - async create(attributes: FormData | GenericFormData | ArchiveItemCreate): Promise<{ + async create(attributes: FormData | GenericFormData | Partial): Promise<{ archiveItem: ArchiveItemShowView }> { const { data } = await http.post("/api/archive-items", attributes, { diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue new file mode 100644 index 00000000..3c807034 --- /dev/null +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue @@ -0,0 +1,226 @@ + + + diff --git a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue index fc4ac389..34a610e8 100644 --- a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue @@ -50,7 +50,14 @@ - + Create Knowledge Item diff --git a/web/src/utils/blocked-to-true-confirm.ts b/web/src/utils/blocked-to-true-confirm.ts new file mode 100644 index 00000000..2c07b074 --- /dev/null +++ b/web/src/utils/blocked-to-true-confirm.ts @@ -0,0 +1,19 @@ +/** + * Simple wrapper around window.confirm that returns true if the + * confirm dialog is blocked. + */ +export function blockedToTrueConfirm(message: string) { + const startTime = Date.now() + const result = window.confirm(message) + const endTime = Date.now() + + // If the confirm returns faster than a human could possibly click + // (e.g., in 1 millisecond), assume it's been blocked + if (endTime - startTime < 1) { + return true + } + + return result +} + +export default blockedToTrueConfirm From feb22a01f3e35dffb187bedf9aa05e180e992280 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 12:58:30 -0700 Subject: [PATCH 31/56] :recycle: Standardize order of methods in ArchiveItemsController. --- .../controllers/archive-items-controller.ts | 83 +++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/api/src/controllers/archive-items-controller.ts b/api/src/controllers/archive-items-controller.ts index 8c1df74c..d4816be8 100644 --- a/api/src/controllers/archive-items-controller.ts +++ b/api/src/controllers/archive-items-controller.ts @@ -35,6 +35,44 @@ export class ArchiveItemsController extends BaseController { } } + async show() { + try { + const archiveItem = await this.loadArchiveItem() + if (isNil(archiveItem)) { + return this.response.status(404).json({ + message: "Archive item not found", + }) + } + + const policy = this.buildPolicy(archiveItem) + if (!policy.show()) { + return this.response.status(403).json({ + message: "You are not authorized to view this item", + }) + } + + // TODO: move to /services/archive-items/show-service.ts + await ArchiveItemAudit.create({ + archiveItemId: archiveItem.id, + action: "Viewed Metadata", + userId: this.currentUser.id, + description: `${this.currentUser.displayName} viewed metadata`, + }) + + const serializedArchiveItem = ShowSerializer.perform(archiveItem) + + return this.response.json({ + archiveItem: serializedArchiveItem, + policy, + }) + } catch (error) { + logger.error(`Error fetching item: ${error}`, { error }) + return this.response.status(400).json({ + message: `Error fetching item: ${error}`, + }) + } + } + async create() { try { const policy = this.buildPolicy() @@ -75,46 +113,8 @@ export class ArchiveItemsController extends BaseController { } } - async show() { - try { - const archiveItem = await this.loadArchiveItem() - if (isNil(archiveItem)) { - return this.response.status(404).json({ - message: "Archive item not found", - }) - } - - const policy = this.buildPolicy(archiveItem) - if (!policy.show()) { - return this.response.status(403).json({ - message: "You are not authorized to view this item", - }) - } - - // TODO: move to /services/archive-items/show-service.ts - await ArchiveItemAudit.create({ - archiveItemId: archiveItem.id, - action: "Viewed Metadata", - userId: this.currentUser.id, - description: `${this.currentUser.displayName} viewed metadata`, - }) - - const serializedArchiveItem = ShowSerializer.perform(archiveItem) - - return this.response.json({ - archiveItem: serializedArchiveItem, - policy, - }) - } catch (error) { - logger.error(`Error fetching item: ${error}`, { error }) - return this.response.status(400).json({ - message: `Error fetching item: ${error}`, - }) - } - } - - private async loadArchiveItem() { - const item = await ArchiveItem.findByPk(this.params.id, { + private loadArchiveItem() { + return ArchiveItem.findByPk(this.params.id, { include: [ "files", "user", @@ -128,9 +128,6 @@ export class ArchiveItemsController extends BaseController { }, ], }) - if (isNil(item)) return null - - return item } private buildPolicy(archiveItem: ArchiveItem = ArchiveItem.build()) { From ed9aa1df140ae2022a5d5632c4ec9463c3985a7d Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 13:12:28 -0700 Subject: [PATCH 32/56] :sparkles: Add archive item destroy service. Also add archive item file destroy service. --- .../controllers/archive-items-controller.ts | 33 +++++- .../archive-item-files/destroy-service.ts | 58 ++++++++++ api/src/services/archive-item-files/index.ts | 1 + .../services/archive-items/destroy-service.ts | 102 ++++++++++++++++++ api/src/services/archive-items/index.ts | 1 + 5 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 api/src/services/archive-item-files/destroy-service.ts create mode 100644 api/src/services/archive-items/destroy-service.ts diff --git a/api/src/controllers/archive-items-controller.ts b/api/src/controllers/archive-items-controller.ts index d4816be8..3e18b3c5 100644 --- a/api/src/controllers/archive-items-controller.ts +++ b/api/src/controllers/archive-items-controller.ts @@ -1,11 +1,12 @@ import { isNil } from "lodash" +import logger from "@/utils/logger" + import { ArchiveItem, ArchiveItemAudit } from "@/models" import { ArchiveItemsPolicy } from "@/policies" -import BaseController from "@/controllers/base-controller" -import { CreateService } from "@/services/archive-items" +import { CreateService, DestroyService } from "@/services/archive-items" import { IndexSerializer, ShowSerializer } from "@/serializers/archive-items" -import logger from "@/utils/logger" +import BaseController from "@/controllers/base-controller" export class ArchiveItemsController extends BaseController { async index() { @@ -113,6 +114,32 @@ export class ArchiveItemsController extends BaseController { } } + async destroy() { + try { + const archiveItem = await this.loadArchiveItem() + if (isNil(archiveItem)) { + return this.response.status(404).json({ + message: "Archive item not found", + }) + } + + const policy = this.buildPolicy(archiveItem) + if (!policy.destroy()) { + return this.response.status(403).json({ + message: "You are not authorized to delete this item", + }) + } + + await DestroyService.perform(archiveItem, this.currentUser) + return this.response.status(204).send() + } catch (error) { + logger.error(`Error deleting archive item: ${error}`, { error }) + return this.response.status(422).json({ + message: `Error deleting archive item: ${error}`, + }) + } + } + private loadArchiveItem() { return ArchiveItem.findByPk(this.params.id, { include: [ diff --git a/api/src/services/archive-item-files/destroy-service.ts b/api/src/services/archive-item-files/destroy-service.ts new file mode 100644 index 00000000..d22b6649 --- /dev/null +++ b/api/src/services/archive-item-files/destroy-service.ts @@ -0,0 +1,58 @@ +import { isNil } from "lodash" + +import logger from "@/utils/logger" + +import { BlobStorageIntegration } from "@/integrations" + +import { ArchiveItemFile, User } from "@/models" +import BaseService from "@/services/base-service" + +/** + * Destroy service for ArchiveItemFile records. + * + * NOTE: File cleanup operations (blob storage deletion) are non-reversible regardless + * of transaction effects. Once files are deleted from blob storage, they cannot be + * recovered even if the database transaction rolls back. + */ +export class DestroyService extends BaseService { + constructor( + private archiveItemFile: ArchiveItemFile, + private currentUser: User + ) { + super() + } + + async perform(): Promise { + const { originalKey, pdfKey } = this.archiveItemFile + + if (!isNil(originalKey)) { + await this.deleteOriginalFile(originalKey) + } + + if (!isNil(pdfKey)) { + await this.deletePdfFile(pdfKey) + } + + await this.archiveItemFile.destroy() + } + + private async deleteOriginalFile(originalKey: string): Promise { + try { + await BlobStorageIntegration.deleteFile(originalKey) + } catch (error) { + logger.warn(`Failed to delete original file from blob storage: ${originalKey}: ${error}`, { + error, + }) + } + } + + private async deletePdfFile(pdfKey: string): Promise { + try { + await BlobStorageIntegration.deleteFile(pdfKey) + } catch (error) { + logger.warn(`Failed to delete PDF file from blob storage: ${pdfKey}: ${error}`, { error }) + } + } +} + +export default DestroyService diff --git a/api/src/services/archive-item-files/index.ts b/api/src/services/archive-item-files/index.ts index e323ee50..93eb763b 100644 --- a/api/src/services/archive-item-files/index.ts +++ b/api/src/services/archive-item-files/index.ts @@ -1 +1,2 @@ export { CreateService } from "./create-service" +export { DestroyService } from "./destroy-service" diff --git a/api/src/services/archive-items/destroy-service.ts b/api/src/services/archive-items/destroy-service.ts new file mode 100644 index 00000000..0c70f559 --- /dev/null +++ b/api/src/services/archive-items/destroy-service.ts @@ -0,0 +1,102 @@ +import db, { + ArchiveItem, + ArchiveItemAudit, + ArchiveItemCategory, + ArchiveItemFile, + InformationSharingAgreementArchiveItem, + ArchiveItemInformationSharingAgreementAccessGrant, + User, +} from "@/models" +import BaseService from "@/services/base-service" +import { ArchiveItemFiles } from "@/services" + +export class DestroyService extends BaseService { + constructor( + private archiveItem: ArchiveItem, + private currentUser: User + ) { + super() + } + + async perform(): Promise { + const { id: archiveItemId, title } = this.archiveItem + const { displayName } = this.currentUser + + return db.transaction(async () => { + await this.removeChildEntities(archiveItemId) + + await this.archiveItem.destroy() + + await this.trackArchiveItemDestroyEvent(archiveItemId, title, displayName) + }) + } + + private async removeChildEntities(archiveItemId: number): Promise { + await this.removeAccessGrants(archiveItemId) + await this.removeInformationSharingAgreementLinks(archiveItemId) + await this.removeCategories(archiveItemId) + await this.removeFiles(archiveItemId) + await this.removeAuditTrail(archiveItemId) + } + + private async removeAccessGrants(archiveItemId: number): Promise { + await ArchiveItemInformationSharingAgreementAccessGrant.destroy({ + where: { + archiveItemId, + }, + }) + } + + private async removeInformationSharingAgreementLinks(archiveItemId: number): Promise { + await InformationSharingAgreementArchiveItem.destroy({ + where: { + archiveItemId, + }, + }) + } + + private async removeCategories(archiveItemId: number): Promise { + await ArchiveItemCategory.destroy({ + where: { + archiveItemId, + }, + }) + } + + private async removeFiles(archiveItemId: number): Promise { + await ArchiveItemFile.findEach( + { + where: { + archiveItemId, + }, + }, + async (archiveItemFile) => { + await ArchiveItemFiles.DestroyService.perform(archiveItemFile, this.currentUser) + } + ) + } + + // TODO: Consider if we want to keep the audit trail even after the item is deleted. + private async removeAuditTrail(archiveItemId: number): Promise { + await ArchiveItemAudit.destroy({ + where: { + archiveItemId, + }, + }) + } + + private async trackArchiveItemDestroyEvent( + archiveItemId: number, + title: string, + displayName: string + ): Promise { + await ArchiveItemAudit.create({ + archiveItemId, + action: "Deleted", + userId: this.currentUser.id, + description: `${displayName} deleted archive item "${title}"`, + }) + } +} + +export default DestroyService diff --git a/api/src/services/archive-items/index.ts b/api/src/services/archive-items/index.ts index e323ee50..93eb763b 100644 --- a/api/src/services/archive-items/index.ts +++ b/api/src/services/archive-items/index.ts @@ -1 +1,2 @@ export { CreateService } from "./create-service" +export { DestroyService } from "./destroy-service" From e3685969782a6222202dc113c7a47da86f061ab1 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 13:18:24 -0700 Subject: [PATCH 33/56] :ok_hand: Standardize archive item controller param name. --- api/src/controllers/archive-items-controller.ts | 2 +- api/src/router.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/archive-items-controller.ts b/api/src/controllers/archive-items-controller.ts index 3e18b3c5..5a311727 100644 --- a/api/src/controllers/archive-items-controller.ts +++ b/api/src/controllers/archive-items-controller.ts @@ -141,7 +141,7 @@ export class ArchiveItemsController extends BaseController { } private loadArchiveItem() { - return ArchiveItem.findByPk(this.params.id, { + return ArchiveItem.findByPk(this.params.archiveItemId, { include: [ "files", "user", diff --git a/api/src/router.ts b/api/src/router.ts index c45bdbe4..932a7e7f 100644 --- a/api/src/router.ts +++ b/api/src/router.ts @@ -118,7 +118,7 @@ router .get(ArchiveItemsController.index) .post(ArchiveItemsController.create) router - .route("/api/archive-items/:id") + .route("/api/archive-items/:archiveItemId") .get(ArchiveItemsController.show) .patch(ArchiveItemsController.update) .delete(ArchiveItemsController.destroy) From 60636517adc04b12aafc621522163feb59f10870 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 13:25:10 -0700 Subject: [PATCH 34/56] :bug: Remove attempt to delete items from database "view". You can't (and don't need to) delete entires from database "views". Instead delete the data that feeds them. --- api/src/services/archive-items/destroy-service.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api/src/services/archive-items/destroy-service.ts b/api/src/services/archive-items/destroy-service.ts index 0c70f559..876f340f 100644 --- a/api/src/services/archive-items/destroy-service.ts +++ b/api/src/services/archive-items/destroy-service.ts @@ -4,7 +4,6 @@ import db, { ArchiveItemCategory, ArchiveItemFile, InformationSharingAgreementArchiveItem, - ArchiveItemInformationSharingAgreementAccessGrant, User, } from "@/models" import BaseService from "@/services/base-service" @@ -32,21 +31,12 @@ export class DestroyService extends BaseService { } private async removeChildEntities(archiveItemId: number): Promise { - await this.removeAccessGrants(archiveItemId) await this.removeInformationSharingAgreementLinks(archiveItemId) await this.removeCategories(archiveItemId) await this.removeFiles(archiveItemId) await this.removeAuditTrail(archiveItemId) } - private async removeAccessGrants(archiveItemId: number): Promise { - await ArchiveItemInformationSharingAgreementAccessGrant.destroy({ - where: { - archiveItemId, - }, - }) - } - private async removeInformationSharingAgreementLinks(archiveItemId: number): Promise { await InformationSharingAgreementArchiveItem.destroy({ where: { From 88dc0d6395a0011ff70c79b770f95af485087484 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 13:52:09 -0700 Subject: [PATCH 35/56] :bug: Fix policy scope for information sharing agreement archive item. --- ...nformation-sharing-agreement-archive-item-policy.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/api/src/policies/information-sharing-agreement-archive-item-policy.ts b/api/src/policies/information-sharing-agreement-archive-item-policy.ts index a5a441a4..75ff3554 100644 --- a/api/src/policies/information-sharing-agreement-archive-item-policy.ts +++ b/api/src/policies/information-sharing-agreement-archive-item-policy.ts @@ -47,15 +47,7 @@ export class InformationSharingAgreementArchiveItemPolicy extends PolicyFactory( { association: "informationSharingAgreement", attributes: ["id"], - include: [ - { - association: "accessGrants", - attributes: [], - where: { - userId: user.id, - }, - }, - ], + ...InformationSharingAgreementPolicy.policyScope(user), required: true, }, ], From 895771a90fadb004d7826400a5eac642af38c135 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 14:17:42 -0700 Subject: [PATCH 36/56] :butterfly: Make archive item access grant view more comprehensive. Why? Easier to understand relationship. --- ...on-sharing-agreement-access-grants-view.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 api/src/db/migrations/20260227210312_update-archive-item-information-sharing-agreement-access-grants-view.ts diff --git a/api/src/db/migrations/20260227210312_update-archive-item-information-sharing-agreement-access-grants-view.ts b/api/src/db/migrations/20260227210312_update-archive-item-information-sharing-agreement-access-grants-view.ts new file mode 100644 index 00000000..942d2fa2 --- /dev/null +++ b/api/src/db/migrations/20260227210312_update-archive-item-information-sharing-agreement-access-grants-view.ts @@ -0,0 +1,35 @@ +import type { Knex } from "knex" + +export async function up(knex: Knex): Promise { + await knex.raw(/* sql */ ` + CREATE OR ALTER VIEW archive_item_information_sharing_agreement_access_grants AS + SELECT + information_sharing_agreement_archive_items.archive_item_id, + information_sharing_agreement_archive_items.information_sharing_agreement_id, + information_sharing_agreement_access_grants.id AS access_grant_id, + information_sharing_agreement_access_grants.group_id, + information_sharing_agreement_access_grants.user_id, + information_sharing_agreement_access_grants.access_level + FROM + information_sharing_agreement_archive_items + JOIN information_sharing_agreement_access_grants ON information_sharing_agreement_access_grants.information_sharing_agreement_id = information_sharing_agreement_archive_items.information_sharing_agreement_id + WHERE + information_sharing_agreement_archive_items.deleted_at IS NULL + AND information_sharing_agreement_access_grants.deleted_at IS NULL; + `) +} + +export async function down(knex: Knex): Promise { + await knex.raw(/* sql */ ` + CREATE OR ALTER VIEW archive_item_information_sharing_agreement_access_grants AS + SELECT + information_sharing_agreement_archive_items.archive_item_id, + information_sharing_agreement_access_grants.id AS information_sharing_agreement_access_grant_id + FROM + information_sharing_agreement_archive_items + JOIN information_sharing_agreement_access_grants ON information_sharing_agreement_access_grants.information_sharing_agreement_id = information_sharing_agreement_archive_items.information_sharing_agreement_id + WHERE + information_sharing_agreement_archive_items.deleted_at IS NULL + AND information_sharing_agreement_access_grants.deleted_at IS NULL; + `) +} From 0ec777f1d2dace3a6a5fdad2ac26cfdabff992b5 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 14:18:40 -0700 Subject: [PATCH 37/56] :sparkles: Add new fields to ArchiveItemInformationSharingAgreementAccessGrant model. --- ...nformation-sharing-agreement-access-grant.ts | 17 +++++++++++++++-- api/src/models/archive-item.ts | 2 +- ...nformation-sharing-agreement-access-grant.ts | 4 ++-- api/src/policies/archive-items-policy.ts | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/api/src/models/archive-item-information-sharing-agreement-access-grant.ts b/api/src/models/archive-item-information-sharing-agreement-access-grant.ts index 4c1a304b..7a47b1c2 100644 --- a/api/src/models/archive-item-information-sharing-agreement-access-grant.ts +++ b/api/src/models/archive-item-information-sharing-agreement-access-grant.ts @@ -2,9 +2,10 @@ import { DataTypes, InferAttributes, InferCreationAttributes } from "@sequelize/ import { Attribute, PrimaryKey, Table } from "@sequelize/core/decorators-legacy" import BaseModel from "@/models/base-model" +import { type InformationSharingAgreementAccessGrantAccessLevels } from "@/models/information-sharing-agreement-access-grant" // NOTE: table is actually a "view" added to make policy checks simpler -// See api/src/db/migrations/20250523215742_create-archive-item-information-sharing-agreement-access-grants-table.ts +// See api/src/db/migrations/20260227210312_update-archive-item-information-sharing-agreement-access-grants-view.ts @Table({ timestamps: false, paranoid: false, @@ -17,9 +18,21 @@ export class ArchiveItemInformationSharingAgreementAccessGrant extends BaseModel @PrimaryKey declare archiveItemId: number + @Attribute(DataTypes.INTEGER) + declare informationSharingAgreementId: number + @Attribute(DataTypes.INTEGER) @PrimaryKey - declare informationSharingAgreementAccessGrantId: number + declare accessGrantId: number + + @Attribute(DataTypes.INTEGER) + declare groupId: number + + @Attribute(DataTypes.INTEGER) + declare userId: number + + @Attribute(DataTypes.INTEGER) + declare accessLevel: InformationSharingAgreementAccessGrantAccessLevels } export default ArchiveItemInformationSharingAgreementAccessGrant diff --git a/api/src/models/archive-item.ts b/api/src/models/archive-item.ts index bf60d990..31c040b7 100644 --- a/api/src/models/archive-item.ts +++ b/api/src/models/archive-item.ts @@ -224,7 +224,7 @@ export class ArchiveItem extends BaseModel< @BelongsToMany(() => InformationSharingAgreementAccessGrant, { through: () => ArchiveItemInformationSharingAgreementAccessGrant, foreignKey: "archiveItemId", - otherKey: "informationSharingAgreementAccessGrantId", + otherKey: "accessGrantId", inverse: "archiveItems", throughAssociations: { fromSource: "archiveItemAccessGrants", diff --git a/api/src/models/information-sharing-agreement-access-grant.ts b/api/src/models/information-sharing-agreement-access-grant.ts index 7e120ed9..70deb97a 100644 --- a/api/src/models/information-sharing-agreement-access-grant.ts +++ b/api/src/models/information-sharing-agreement-access-grant.ts @@ -172,7 +172,7 @@ export class InformationSharingAgreementAccessGrant extends BaseModel< @HasMany(() => ArchiveItemInformationSharingAgreementAccessGrant, { foreignKey: { - name: "informationSharingAgreementAccessGrantId", + name: "accessGrantId", allowNull: false, }, inverse: "informationSharingAgreementAccessGrant", @@ -183,7 +183,7 @@ export class InformationSharingAgreementAccessGrant extends BaseModel< @BelongsToMany(() => InformationSharingAgreementAccessGrant, { through: () => InformationSharingAgreementAccessGrantSibling, - foreignKey: "informationSharingAgreementAccessGrantId", + foreignKey: "accessGrantId", otherKey: "informationSharingAgreementAccessGrantSiblingId", inverse: "siblingsOf", }) diff --git a/api/src/policies/archive-items-policy.ts b/api/src/policies/archive-items-policy.ts index 6e2236be..b9a76b54 100644 --- a/api/src/policies/archive-items-policy.ts +++ b/api/src/policies/archive-items-policy.ts @@ -66,7 +66,7 @@ export class ArchiveItemsPolicy extends PolicyFactory(ArchiveItem) { archive_item_information_sharing_agreement_access_grants.archive_item_id FROM archive_item_information_sharing_agreement_access_grants - INNER JOIN information_sharing_agreement_access_grants ON archive_item_information_sharing_agreement_access_grants.information_sharing_agreement_access_grant_id = information_sharing_agreement_access_grants.id + INNER JOIN information_sharing_agreement_access_grants ON archive_item_information_sharing_agreement_access_grants.access_grant_id = information_sharing_agreement_access_grants.id WHERE information_sharing_agreement_access_grants.user_id = :userId ) From 524ebca0841e1713ef060cb7ef83889e352d95f4 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 15:02:21 -0700 Subject: [PATCH 38/56] :bug: Fix ISA group cleanup. How? Instead of relying on complex cascading services, we now do everything directly. This reduces complexity, at the cost of future code maybe going out of sync between services. --- api/src/services/groups/destroy-service.ts | 34 +++++++-- .../destroy-groups-service.ts | 70 +++++++++++++++---- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/api/src/services/groups/destroy-service.ts b/api/src/services/groups/destroy-service.ts index 3b20b414..f132cf42 100644 --- a/api/src/services/groups/destroy-service.ts +++ b/api/src/services/groups/destroy-service.ts @@ -1,4 +1,6 @@ -import db, { Group, UserGroup, User } from "@/models" +import { Op } from "@sequelize/core" + +import db, { Group, UserGroup, User, InformationSharingAgreement } from "@/models" import BaseService from "@/services/base-service" import { UserGroups } from "@/services" @@ -11,17 +13,41 @@ export class DestroyService extends BaseService { } async perform() { + const { id: groupId } = this.group return db.transaction(async () => { - await this.cleanupUserGroups() + await this.assertNoDependentEntitiesExist(groupId) + await this.cleanupUserGroups(groupId) await this.group.destroy() }) } - private async cleanupUserGroups() { + private async assertNoDependentEntitiesExist(groupId: number) { + await this.assertNoDependentInformationSharingAgreementsExist(groupId) + } + + private async assertNoDependentInformationSharingAgreementsExist(groupId: number) { + const dependentInformationSharingAgreementsCount = await InformationSharingAgreement.count({ + where: { + [Op.or]: [ + { + externalGroupId: groupId, + }, + { + internalGroupId: groupId, + }, + ], + }, + }) + if (dependentInformationSharingAgreementsCount > 0) { + throw new Error("Groups with dependent information sharing agreements cannot be deleted") + } + } + + private async cleanupUserGroups(groupId: number) { await UserGroup.findEach( { where: { - groupId: this.group.id, + groupId, }, include: ["user", "group"], }, diff --git a/api/src/services/information-sharing-agreements/destroy-groups-service.ts b/api/src/services/information-sharing-agreements/destroy-groups-service.ts index a769d411..b1af815c 100644 --- a/api/src/services/information-sharing-agreements/destroy-groups-service.ts +++ b/api/src/services/information-sharing-agreements/destroy-groups-service.ts @@ -1,8 +1,13 @@ import { isNil } from "lodash" -import db, { Group, InformationSharingAgreement, User } from "@/models" +import db, { + Group, + InformationSharingAgreement, + InformationSharingAgreementAccessGrant, + UserGroup, + User, +} from "@/models" import BaseService from "@/services/base-service" -import { Groups } from "@/services" export class DestroyGroupsService extends BaseService { constructor( @@ -13,7 +18,12 @@ export class DestroyGroupsService extends BaseService { } async perform(): Promise { - const { externalGroupId, internalGroupId } = this.informationSharingAgreement + const { + id: informationSharingAgreementId, + externalGroupId, + internalGroupId, + } = this.informationSharingAgreement + if (isNil(externalGroupId)) { throw new Error("External group ID is required") } @@ -23,16 +33,52 @@ export class DestroyGroupsService extends BaseService { } return db.transaction(async () => { - await Group.findEach( - { - where: { - id: [externalGroupId, internalGroupId], - }, - }, - async (group) => { - await Groups.DestroyService.perform(group, this.currentUser) - } + await this.removeChildEntities( + informationSharingAgreementId, + externalGroupId, + internalGroupId ) + await this.informationSharingAgreement.update({ + externalGroupId: null, + internalGroupId: null, + }) + }) + } + + private async removeChildEntities( + informationSharingAgreementId: number, + externalGroupId: number, + internalGroupId: number + ): Promise { + await this.removeChildAccessGrants(informationSharingAgreementId) + await this.removeChildUserGroups(externalGroupId, internalGroupId) + await this.removeChildGroup(externalGroupId, internalGroupId) + } + + private async removeChildAccessGrants(informationSharingAgreementId: number): Promise { + await InformationSharingAgreementAccessGrant.destroy({ + where: { + informationSharingAgreementId, + }, + }) + } + + private async removeChildUserGroups( + externalGroupId: number, + internalGroupId: number + ): Promise { + await UserGroup.destroy({ + where: { + groupId: [externalGroupId, internalGroupId], + }, + }) + } + + private async removeChildGroup(externalGroupId: number, internalGroupId: number): Promise { + await Group.destroy({ + where: { + id: [externalGroupId, internalGroupId], + }, }) } } From 294bd63c929db3a21fcfb70398605fead9237556 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 15:15:26 -0700 Subject: [PATCH 39/56] :see_no_evil: Hide create archive item button if archive item already exists. --- ...ormationSharingAgreementSignedActionsMenu.vue | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue index 34a610e8..f28bdec9 100644 --- a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue @@ -49,8 +49,8 @@ /> - import { computed, toRefs } from "vue" -import { isNil } from "lodash" +import { isEmpty, isNil } from "lodash" import Api from "@/api" import useAuthenticatedDownload from "@/use/utils/use-authenticated-download" import useInformationSharingAgreement from "@/use/use-information-sharing-agreement" +import useInformationSharingAgreementArchiveItems from "@/use/use-information-sharing-agreement-archive-items" import BaseActionsMenuBtnGroup from "@/components/common/BaseActionsMenuBtnGroup.vue" import InformationSharingAgreementRevertToDraftDialog from "@/components/information-sharing-agreements/InformationSharingAgreementRevertToDraftDialog.vue" @@ -96,6 +97,17 @@ const emit = defineEmits<{ const { informationSharingAgreementId } = toRefs(props) const { isLoading, policy } = useInformationSharingAgreement(informationSharingAgreementId) +const informationSharingAgreementArchiveItemsQuery = computed(() => ({ + where: { + informationSharingAgreementId: props.informationSharingAgreementId, + }, + perPage: 1, +})) +const { informationSharingAgreementArchiveItems } = useInformationSharingAgreementArchiveItems( + informationSharingAgreementArchiveItemsQuery +) +const hasArchiveItem = computed(() => !isEmpty(informationSharingAgreementArchiveItems.value)) + const generateSignedAcknowledgementUrl = computed(() => Api.Downloads.InformationSharingAgreements.signedAcknowledgementApi.downloadPath( props.informationSharingAgreementId From 27403089f01da957e1f6ab0b842de4e1de06aaff Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 15:19:40 -0700 Subject: [PATCH 40/56] :cherry_blossom: Redirect to archive item page after creation. Why? Better experience. --- ...formationSharingAgreementArchiveItemCreateForm.vue | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue index 20f39381..8bd380e8 100644 --- a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue @@ -251,16 +251,17 @@ async function saveAndRedirect() { isLoading.value = true try { - await Api.InformationSharingAgreements.archiveItemsApi.create( + const { archiveItem } = await Api.InformationSharingAgreements.archiveItemsApi.create( props.informationSharingAgreementId, archiveItemAttributes.value, files.value ) - snack.success("Item created.") - - router.push({ - name: "archive-items/ArchiveItemListPage", + await router.push({ + name: "archive-items/ArchiveItemInformationSharingAgreementsPage", + params: { + archiveItemId: archiveItem.id, + }, }) } catch (error) { snack.error("Save failed!") From a4c4d1279e1888c56485115ad7cf0f165e482df3 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Fri, 27 Feb 2026 15:20:10 -0700 Subject: [PATCH 41/56] :cherry_blossom: Make category selection closable when more than one is selected. --- .../InformationSharingAgreementArchiveItemCreateForm.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue index 8bd380e8..39a852a5 100644 --- a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue @@ -102,6 +102,7 @@ hide-selected multiple chips + :closable-chips="categoryIds.length > 1" /> Date: Thu, 12 Mar 2026 08:24:06 -0700 Subject: [PATCH 42/56] :mute: Avoid logging pdf conversion jobs every few seconds. Instead log only when job is doing something. --- archiver/src/jobs/pdf-converter-job.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archiver/src/jobs/pdf-converter-job.ts b/archiver/src/jobs/pdf-converter-job.ts index c8a0c3b5..290d05a7 100644 --- a/archiver/src/jobs/pdf-converter-job.ts +++ b/archiver/src/jobs/pdf-converter-job.ts @@ -16,11 +16,11 @@ export class PDFConverterJob { constructor() {} async run(statDate: Date) { - logger.info("Running PDF Converter Job", statDate) const toConvert = await cache.getKeysByPattern(`CONVERT_`) const fileStore = new FileStorageService() for (const key of toConvert) { + logger.info(`Processing PDF conversion for key: ${key} on ${statDate}`) const data = await cache.getValue(key) if (isNil(data)) return From bfc39c1d8d6eb42cce7411f8e7269c12bcae5e33 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:06:25 -0700 Subject: [PATCH 43/56] :hammer: Pull in some better agent workflows from WRAP. --- AGENTS.md | 4 +- agents/README.md | 8 +- agents/workflows/README.md | 89 ++--- agents/workflows/code-review.md | 114 ++++++ agents/workflows/jira-issue-creation.md | 323 ---------------- agents/workflows/jira-issue-management.md | 403 ++++++++++++++++++++ agents/workflows/pull-request-management.md | 12 +- agents/workflows/testing-instructions.md | 226 +++++++++++ 8 files changed, 795 insertions(+), 384 deletions(-) create mode 100644 agents/workflows/code-review.md delete mode 100644 agents/workflows/jira-issue-creation.md create mode 100644 agents/workflows/jira-issue-management.md create mode 100644 agents/workflows/testing-instructions.md diff --git a/AGENTS.md b/AGENTS.md index f174a449..361dfa53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -461,7 +461,9 @@ Multi-step guides that orchestrate templates. Use for complete features. - `create-admin-ui.md` - Full CRUD admin interface (references templates) - `pull-request-management.md` - Creating and editing PRs -- `jira-issue-creation.md` - Creating Jira issues in TK project +- `jira-issue-management.md` - Creating, enhancing, and managing Jira issues +- `code-review.md` - Code review quality control +- `testing-instructions.md` - Comprehensive testing instructions for PRs #### Usage Examples diff --git a/agents/README.md b/agents/README.md index c7e7e493..1128d745 100644 --- a/agents/README.md +++ b/agents/README.md @@ -12,7 +12,9 @@ agents/ │ └── frontend/ (api-client, composables, components, pages, searchable-autocomplete) ├── workflows/ (multi-step task guides) │ ├── create-admin-ui.md -│ ├── jira-issue-creation.md +│ ├── jira-issue-management.md +│ ├── code-review.md +│ ├── testing-instructions.md │ └── pull-request-management.md ├── plans/ (implementation plans for complex refactoring work) │ ├── README.md (planning guidelines and structure) @@ -58,7 +60,9 @@ Reusable code patterns organized by layer: Multi-step task guides: - [create-admin-ui.md](workflows/create-admin-ui.md) - Full CRUD admin UI (references templates) -- [jira-issue-creation.md](workflows/jira-issue-creation.md) - Create Jira issues in TK project +- [jira-issue-management.md](workflows/jira-issue-management.md) - Create, enhance, and manage Jira issues +- [code-review.md](workflows/code-review.md) - Code review quality control +- [testing-instructions.md](workflows/testing-instructions.md) - Comprehensive testing instructions for PRs - [pull-request-management.md](workflows/pull-request-management.md) - Create and manage PRs ### Plans (`agents/plans/`) diff --git a/agents/workflows/README.md b/agents/workflows/README.md index b1683d83..f868fd5e 100644 --- a/agents/workflows/README.md +++ b/agents/workflows/README.md @@ -15,59 +15,22 @@ These workflows exist to ensure consistent, high-quality output from AI agents. ## Available Workflows -### [create-admin-ui.md](create-admin-ui.md) +| Workflow | Description | +|----------|-------------| +| [jira-issue-management.md](./jira-issue-management.md) | Create well-structured Jira issues following project patterns | +| [code-review.md](./code-review.md) | Code review quality control for TypeScript code | +| [pull-request-management.md](./pull-request-management.md) | Create and edit well-structured pull requests following project patterns | +| [testing-instructions.md](./testing-instructions.md) | Generate comprehensive testing instructions for pull requests | +| [create-admin-ui.md](./create-admin-ui.md) | Complete workflow for adding full CRUD admin UI for any model | -Complete workflow for adding full CRUD admin UI for any model. +### Complete PR Creation Sequence -**Uses templates from:** [`agents/templates/`](../templates/) +For a full PR workflow, follow these steps in order: -**Produces:** -- Backend: Model, Controller, Policy, Services, Serializers, Routes -- Frontend: API Client, Composables, Components, Pages - -**Key Features:** -- References reusable templates from `agents/templates/` -- Includes implementation order, checklists, common pitfalls -- Matches actual implementation from External Organizations - -**Reference:** External Organizations (`d4a9366`, `1f1dac8`) - ---- - -### [jira-issue-creation.md](jira-issue-creation.md) - -Complete workflow for creating well-structured Jira issues in the Traditional Knowledge (TK) project. - -**Includes:** -- Step-by-step guidance for bugs, stories, tasks, and epics -- Proper issue type selection and formatting -- Acceptance criteria and definition of done -- Priority and component assignment -- Quality checklists and best practices - -**Project URL:** https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 - ---- - -### [pull-request-management.md](pull-request-management.md) - -Complete workflow for creating and editing well-structured pull requests following Traditional Knowledge project patterns and conventions. - -**Includes:** -- PR title patterns (TICKET-ID, Fix:, Action Verb + Noun) -- Comprehensive PR body template with Context, Implementation, Screenshots, Testing Instructions -- Traditional Knowledge-specific testing commands and navigation patterns -- Quality checklist and common pitfalls -- Examples from actual Traditional Knowledge pull requests -- Complete guide for editing existing pull requests - -**Key Features:** -- Draft PR creation process -- Standardized testing instructions -- UI navigation patterns for Traditional Knowledge -- Code quality standards integration -- Common editing scenarios and workflows -- Step-by-step examples for updating PR content +1. **[jira-issue-management.md](./jira-issue-management.md)** — Create/update the Jira issue +2. **[code-review.md](./code-review.md)** — Review code quality before PR +3. **[pull-request-management.md](./pull-request-management.md)** — Create the draft PR +4. **[testing-instructions.md](./testing-instructions.md)** — Add comprehensive testing instructions --- @@ -81,20 +44,32 @@ Follow the workflow in agents/workflows/create-admin-ui.md to create admin UI for the KnowledgeCategory model. ``` -**Example - Specific Template:** +**Example - Create PR:** ``` -Follow the template in agents/templates/frontend/components.md -to create the KnowledgeCategoriesDataTable component. +Follow the workflow in agents/workflows/pull-request-management.md +to create a PR for my changes. +``` + +**Example - Testing Instructions:** +``` +Follow the workflow in agents/workflows/testing-instructions.md +to create testing instructions for this PR. ``` -**Example - Backend Only:** +**Example - Code Review:** ``` -Follow the backend templates in agents/templates/backend/ -to create the API for KnowledgeCategory. +Follow the workflow in agents/workflows/code-review.md +to review the code changes on this branch. +``` + +**Example - Specific Template:** +``` +Follow the template in agents/templates/frontend/components.md +to create the KnowledgeCategoriesDataTable component. ``` See parent [agents/README.md](../README.md) for setup instructions. --- -**Last Updated:** 2026-01-27 +**Last Updated:** 2026-03-12 diff --git a/agents/workflows/code-review.md b/agents/workflows/code-review.md new file mode 100644 index 00000000..30763c2a --- /dev/null +++ b/agents/workflows/code-review.md @@ -0,0 +1,114 @@ +--- +description: Code Review Quality Control Workflow for TypeScript code in the Traditional Knowledge project +--- + +# Code Review Quality Control Workflow + +Use this workflow to review TypeScript code for the Traditional Knowledge project. Apply every rule below as a hard gate — flag any violation as a blocking issue. For each issue found, quote the offending lines and state which rule they violate. + +**Complete workflow sequence:** This is step 2 of 4 in the complete PR creation process. Always use after jira-issue-management workflow and before pull-request-management workflow to ensure code quality standards are met. + +## Steps + +1. **Review TypeScript Strictness** + - No `any` — not in source, not in tests, not in casts + - No `!` (non-null assertion) — use `isNil` / optional chaining / explicit guard clauses instead + - No `@ts-ignore` or `@ts-expect-error` + - No relative imports — always `@/...` path aliases. Exception: barrel `index.ts` files may use relative imports for re-exports + +2. **Check Type Cast Placement** + - Type casts with `as` must happen on assignment or creation, never at the point of use + - **When a cast is needed at point of use, create a new type definition** that returns the correct type by design, eliminating the need for casts + - Prefer proper type definitions over runtime type checks; only use guard clauses when runtime validation is genuinely needed, not just to satisfy TypeScript + - A cast is only acceptable when the type system genuinely cannot infer a type that is known at runtime + +3. **Check Cyclomatic Complexity** + - Each function or method should have at most one level of conditional nesting in its main body + - Use guard clauses (early returns) to flatten logic + - Each guard clause must be followed by a blank line before the next statement + +4. **Verify Test Completeness** + - Every test must include explicit Arrange / Act / Assert comments and be fully self-contained + - No shared state from `beforeEach` unless it is truly invariant setup (e.g., DB reset) + - Service test assertions should target database state, not service return values, unless the return value is specifically what is under test + - Spy assertions — use `expect(spy).not.toHaveBeenCalled()` without arguments. Never use `not.toHaveBeenCalledWith(...)` + +5. **Ensure One Expect Per Test** + - Consolidate assertions into a single `expect` using `expect.objectContaining` + - Exceptions: tests that assert something did NOT happen (e.g., `not.toHaveBeenCalled()`, `not.toBe()`) may be standalone single-line expects + - When asserting "not any of N values", split into N separate tests rather than looping + +6. **Check Test Naming** + - Tests must follow `"when [condition], [expected behaviour]"` with full English words (no abbreviations) + - Numbered entities in test bodies: `user1`, `user2`, `workflowStep1`, `workflowStep2` — never `existingUser`, `newUser` + - Describe hierarchy must mirror the source file path + +7. **Validate Error Messages** + - `expect(bool).toBe(true)` is never acceptable — a failing test must tell you what went wrong without reading the source + - Split complex boolean assertions into individual named assertions + +8. **Check Architecture Consistency** + - When multiple classes serve similar purposes, ensure they have consistent APIs and clear differentiation in their type signatures + - **When extending functionality, create new types rather than modifying existing ones to avoid breaking changes** + - **Ensure similar classes follow the same patterns** + - Flag inconsistent naming or patterns between similar classes + +9. **Check for Over-Engineering** + - Flag additions that were not requested: + - New abstractions, helpers, or utilities for a one-time use + - Features, flags, or configurability that no current caller needs + - Comments explaining what the code does when the names already say it + - Error handling for states that cannot be reached + +10. **Check for Orphaned or Non-Sensical Code** + - Flag functions, methods, or code paths that serve no purpose + - Look for placeholder implementations that never get called + - Identify dead code paths or unreachable logic + - **Flag code with comments like "legacy path" or "no-op here" that should be removed** + - **When removing features, remove all related types, imports, and exports** - don't leave partially implemented type systems + - Verify that all exported functions are actually used somewhere in the codebase + +11. **Validate Import Organization** + - Imports must follow PEP 8-style grouping with a blank line between each group: + 1. Node.js built-ins (`path`, `fs`, etc.) + 2. External packages from `node_modules` + 3. Internal imports from `@/` + - Within each group, alphabetical ordering is required. One import statement per module + +12. **Check Naming Conventions** + - No abbreviations — `workflow` not `wf`, `migration` not `mig` + - Fully qualified names at public boundaries — when data crosses a boundary (API response, email template, event payload), prefix with the parent model name to disambiguate + - SQL — fully spell out table and column names; no abbreviated aliases + - Function names — describe both trigger and behavior + +13. **Verify Expanded Style** + - Avoid terse functional chains. Each logical step must be on its own line or extracted to a named variable + - Extract and rename before constructing objects — never inline a property rename + - No chained transformations — break chains at each step + - Named constants — hoist every magic number or string to a named `const` at the top of the function or file + +14. **Check Service Pattern** + - Services encapsulate business logic and are invoked exclusively via their static `perform()` method + - Never instantiate a service directly outside of its own `static perform()` implementation + - Red flags: `new SomeService(...)` at a call site, business logic in a controller that belongs in a service, a service calling a query directly instead of delegating to another service + +## Output Format + +For each issue: + +``` +[RULE N] +File: : +> +Fix: +``` + +After listing all issues, conclude with one of: +- **APPROVED** — no blocking issues found. +- **CHANGES REQUESTED** — N blocking issues listed above. + +## Related Workflows + +- [`./jira-issue-management.md`](./jira-issue-management.md) - Creating well-structured Jira issues +- [`./pull-request-management.md`](./pull-request-management.md) - Create and update pull requests +- [`./testing-instructions.md`](./testing-instructions.md) - Generate comprehensive testing instructions for pull requests diff --git a/agents/workflows/jira-issue-creation.md b/agents/workflows/jira-issue-creation.md deleted file mode 100644 index 134d08f7..00000000 --- a/agents/workflows/jira-issue-creation.md +++ /dev/null @@ -1,323 +0,0 @@ ---- -description: Workflow for creating well-structured Jira issues in the Traditional Knowledge (TK) project -auto_execution_mode: 1 ---- - -# Jira Issue Creation Workflow - -## Intent - -**WHY this workflow exists:** Creating effective Jira issues requires consistent structure, clear problem descriptions, and actionable requirements. Poorly written issues lead to confusion, scope creep, and implementation delays. - -**WHAT this workflow produces:** Well-structured Jira issues that include: -- Clear problem descriptions or feature requests -- Specific reproduction steps or requirements -- Proper issue type, priority, and component assignment -- Acceptance criteria and testing requirements - -**Decision Rules:** -- **Use Issue Types:** Always select appropriate issue type (Bug, Story, Task, Epic) -- **Stories:** Use user story format "As a [role] I can [action] so that [benefit]" -- **Bugs:** Include reproduction steps and expected behavior -- **Acceptance Criteria:** Always include clear, testable acceptance criteria -- **Components:** Assign to appropriate component (Backend, Frontend, API, etc.) - -## Jira Project Details - -- **Project URL:** https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 -- **Project Key:** TK -- **Board:** Traditional Knowledge Board - ---- - -## Step 1: Choose Issue Type - -| Issue Type | When to Use | Example | -|------------|-------------|---------| -| **Bug** | Defects, errors, broken functionality | Search not returning results | -| **Story** | User-facing features with business value | User can export knowledge entries | -| **Task** | Technical work without direct user value | Update database schema | -| **Epic** | Large features that span multiple stories | Implement cultural protocols system | - ---- - -## Step 2: Fill Out Issue Fields - -### Summary (Title) -- Use clear, concise titles -- For Stories: "As a [role] I can [action]" -- For Bugs: "Area: Brief description of problem" -- For Tasks: "Verb + Noun" format - -### Description Template - -**For Bugs:** -``` -h2. Problem Description -[Clear description of what's wrong] - -h2. Steps to Reproduce -# [Step 1] -# [Step 2] -# [Step 3] - -h2. Expected Behavior -[What should happen] - -h2. Actual Behavior -[What actually happens] - -h2. Environment -* Browser: [e.g., Chrome 120.0] -* OS: [e.g., macOS 14.0] -* User: [Test account if applicable] - -h2. Additional Information -[Error logs, screenshots, related issues] -``` - -**For Stories:** -``` -h2. User Story -As a [user role] I can [perform action] so that [benefit/value] - -h2. Business Value -[Why this matters to users/stakeholders] - -h2. Acceptance Criteria -# Given [context] when [action] then [outcome] -# Given [context] when [action] then [outcome] -# Given [context] when [action] then [outcome] - -h2. Technical Notes -[Implementation considerations, constraints] - -h2. Dependencies -[Prerequisites, blocking issues] - -h2. Definition of Done -- [ ] Code reviewed and approved -- [ ] Tests written and passing -- [ ] Documentation updated -- [ ] Deployed to staging -- [ ] User acceptance testing complete -``` - -**For Tasks:** -``` -h2. Objective -[What needs to be accomplished] - -h2. Requirements -# [Requirement 1] -# [Requirement 2] -# [Requirement 3] - -h2. Technical Approach -[How to implement] - -h2. Definition of Done -- [ ] Task completed -- [ ] Code reviewed -- [ ] Tests pass -``` - ---- - -## Step 3: Set Priority and Components - -### Priority Levels -- **Highest** - Production down, critical security issue -- **High** - Major feature blocked, significant impact -- **Medium** - Important but not blocking -- **Low** - Nice to have, can be deferred - -### Components -- **Backend** - API, database, server-side logic -- **Frontend** - UI components, user interface -- **API** - External integrations, data exchange -- **Infrastructure** - Deployment, CI/CD, monitoring -- **Documentation** - User guides, technical docs - ---- - -## Step 4: Add Labels and Links - -### Common Labels -- `cultural-protocols` - Issues related to Indigenous cultural protocols -- `indigenous-language` - Language support and translation -- `access-control` - Permissions and access management -- `data-migration` - Data import/export/migration tasks -- `performance` - Optimization and performance issues -- `security` - Security-related issues -- `ui/ux` - User interface and experience - -### Linking Issues -- **Blocks/Blocked By** - Dependencies between issues -- **Relates To** - Related but not dependent issues -- **Clones** - Duplicate issues in different contexts - ---- - -## Complete Examples - -### Bug Example - -**Issue Type:** Bug -**Priority:** High -**Component:** Frontend -**Labels:** `ui/ux`, `search` - -``` -h2. Problem Description -Users are unable to search for traditional knowledge entries using Indigenous language terms. The search returns no results even when entries exist. - -h2. Steps to Reproduce -# Navigate to Knowledge Base -# Enter Indigenous language search term -# Click search -# Observe empty results - -h2. Expected Behavior -Search should return knowledge entries matching the Indigenous language terms. - -h2. Actual Behavior -Search returns no results for Indigenous language terms. - -h2. Environment -* Browser: Chrome 120.0 -* OS: Windows 11 -* User: test.knowledge.keeper@yg.gov.yk.ca - -h2. Additional Information -Error in console: "Search index does not include language metadata" -``` - -### Story Example - -**Issue Type:** Story -**Priority:** Medium -**Component:** Frontend, Backend -**Labels:** `cultural-protocols`, `access-control` - -``` -h2. User Story -As a Knowledge Keeper I can set cultural protocols on my entries so that sensitive information is only accessible to authorized community members. - -h2. Business Value -Protects culturally sensitive information while enabling appropriate sharing within the community. Meets legal and ethical obligations for Indigenous knowledge protection. - -h2. Acceptance Criteria -# Given I am a Knowledge Keeper when I create a new entry then I can set cultural protocol restrictions -# Given I set cultural protocols when unauthorized users search then they cannot access restricted content -# Given I am an authorized community member when I search restricted content then I can view it with proper attribution -# Given cultural protocols are set when the entry is exported then protocols are preserved and respected - -h2. Technical Notes -- Need to implement role-based access control -- Cultural protocols must be inherited from community settings -- Audit log required for all access to restricted content - -h2. Dependencies -- TK-123: Implement community management system -- TK-124: Create role-based permission system - -h2. Definition of Done -- [ ] Code reviewed and approved -- [ ] Tests written and passing -- [ ] Documentation updated -- [ ] Deployed to staging -- [ ] User acceptance testing complete with Knowledge Keepers -``` - -### Epic Example - -**Issue Type:** Epic -**Priority:** High -**Component:** Multiple -**Labels:** `cultural-protocols`, `indigenous-language` - -``` -h2. Epic Summary -Implement comprehensive cultural protocols and Indigenous language support throughout the Traditional Knowledge system. - -h2. Business Goal -Ensure the system respects and protects Indigenous cultural protocols while supporting knowledge preservation and sharing in Indigenous languages. - -h2. Stories in this Epic -- TK-201: Cultural protocol management for knowledge entries -- TK-202: Indigenous language search capabilities -- TK-203: Community-based access control -- TK-204: Cultural protocol audit logging -- TK-205: Indigenous language UI localization - -h2. Definition of Done -- All stories completed and accepted -- End-to-end testing with Knowledge Keepers -- Documentation and training materials created -- Cultural approval received from Indigenous partners -``` - ---- - -## Step 5: Create the Issue - -### Option 1: Create via Jira Web UI -1. Go to https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 -2. Click "Create" in top navigation -3. Select Issue Type (Bug, Story, Task, Epic) -4. Fill in fields following templates above -5. Set Priority and Components -6. Add Labels -7. Click "Create" - -### Option 2: Create via Jira CLI (if configured) - -```bash -# Create a new story -jira issue create -p TK -t Story \ - --summary "As a Knowledge Keeper I can export entries to PDF" \ - --description "User Story: As a Knowledge Keeper I can export entries to PDF so that I can share knowledge with community members who need offline access." - -# Create a new bug -jira issue create -p TK -t Bug \ - --summary "Search: Indigenous language terms not returning results" \ - --description "Problem: Search functionality doesn't index Indigenous language metadata" -``` - ---- - -## Quality Checklist - -**Before Creating:** -- [ ] Issue type selected correctly -- [ ] Title is clear and follows conventions -- [ ] Description includes all required sections -- [ ] Acceptance criteria are testable (for stories) -- [ ] Reproduction steps are clear (for bugs) -- [ ] Priority set appropriately -- [ ] Components assigned -- [ ] Relevant labels added - -**After Creating:** -- [ ] Issue appears in correct sprint/backlog -- [ ] Links to related issues added -- [ ] Assignee notified (if assigned) -- [ ] Watchers added as needed - ---- - -## Best Practices - -1. **One Issue, One Problem** - Don't combine multiple unrelated issues -2. **Clear Acceptance Criteria** - Make them specific and measurable -3. **User-Centric Stories** - Focus on user value, not technical implementation -4. **Proper Context** - Include enough information for anyone to understand -5. **Link Dependencies** - Always link related or blocking issues -6. **Regular Updates** - Keep issue status and comments current - ---- - -**Workflow Version:** 1.0 -**Last Updated:** 2026-01-22 -**Project:** Traditional Knowledge (TK) - https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 diff --git a/agents/workflows/jira-issue-management.md b/agents/workflows/jira-issue-management.md new file mode 100644 index 00000000..8f17f214 --- /dev/null +++ b/agents/workflows/jira-issue-management.md @@ -0,0 +1,403 @@ +--- +description: Create, enhance, and manage well-structured Jira issues using project conventions +auto_execution_mode: 1 +--- + +# Jira Issue Management Workflow + +## Intent + +**WHY this workflow exists:** Creating and managing effective Jira issues requires consistent structure, clear problem descriptions, and actionable requirements. Poorly written issues lead to confusion, scope creep, and implementation delays, while proper enhancement patterns ensure issues remain valuable throughout their lifecycle. + +**WHAT this workflow produces:** Well-structured Jira issues that include: + +- Clear problem descriptions or feature requests +- Specific reproduction steps or requirements +- Proper issue labeling and assignment +- Screenshots/mockups when relevant +- Enhanced descriptions with visual evidence and external source links +- Proper dependency relationships between issues + +**Decision Rules:** + +- **Use Jira Web UI:** Create issues directly in TK project at https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 +- **Title Case Formatting:** Use title case for Jira ticket titles - capitalize major words, keep minor words (articles, prepositions, conjunctions) lowercase +- **Bug Reports:** Use "Bug" issue type for defects and problems +- **Feature Requests:** Use "User Story" issue type for new functionality (validate with getJiraProjectIssueTypesMetadata first) +- **Improvements/Refactoring:** Use "Task" issue type +- **Large Features:** Use "Epic" issue type with multiple child issues +- **Labels:** Always include appropriate labels for type, priority, and component +- **User Reports:** Integrate into main description when primary context; add as separate comment for supplemental information +- **Visual Evidence:** Embed screenshots directly in descriptions when available for immediate context +- **External Sources:** Create remote links with proper global IDs (MD5 hash pattern) for external references +- **Dependencies:** Use proper issue linking ("causes"/"is caused by") to establish technical relationships +- **MCP Tool Validation:** Always validate available options before using MCP tools (e.g., check issue types with getJiraProjectIssueTypesMetadata before creating) +- **Complete workflow sequence:** This is step 1 of 4 in the complete PR creation process. Always use before code-review, pull-request-management, and testing-instructions workflows to ensure proper issue structure and requirements gathering. + +## Reference Files + +- TK Jira Board: https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 +- Jira Issue Types: Bug, User Story, Task, Epic (validate with getJiraProjectIssueTypesMetadata) +- TK Project Labels and Components + +--- + +## Step 1: Choose Issue Type + +**For Bugs/Defects:** Use "Bug" issue type +**For New Features:** Use "User Story" issue type (validate with getJiraProjectIssueTypesMetadata first) +**For Improvements/Refactoring:** Use "Task" issue type +**For Large Features:** Use "Epic" issue type with multiple child issues + +--- + +## Step 2: Fill Out Issue Fields + +**Bug Issue Fields:** +- **Summary:** Clear, concise description of the problem +- **Description:** Detailed problem explanation with reproduction steps +- **Priority:** Set appropriate priority level +- **Labels:** Add relevant labels (bug, easy-win, etc.) +- **Components:** Assign to relevant component if applicable + +**Story/Task Issue Fields:** +- **Summary:** Clear description of feature or improvement +- **Description:** Problem context and solution requirements +- **Priority:** Set appropriate priority level +- **Labels:** Add relevant labels (enhancement, refactor, etc.) +- **Components:** Assign to relevant component if applicable + +--- + +## Step 3: Write Effective Context + +**Enhanced Description Patterns:** + +- **Visual Evidence Integration**: Embed screenshots directly in descriptions when available - they provide immediate context that text alone cannot convey +- **Precise Reproduction Steps**: Include specific trigger conditions rather than generic instructions +- **Impact Conciseness**: Use single, powerful statements that capture the essence of the business impact +- **External Source Linking**: Create remote links with proper global IDs (using MD5 hash pattern) for external references +- **Dependency Relationships**: Use proper issue linking ("causes"/"is caused by") to establish clear technical relationships between tickets +- **Description Integration**: Move user reports into the main description rather than separate comments when they're the primary context +- **Clean Structure**: Remove redundant comments when the information is properly integrated into the main description +- **Factual Accuracy**: Differentiate clearly between definite facts (user quotes, confirmed behaviors) and probable hypotheses (potential causes). Never guess or invent technical details +- **Actionable Investigation**: Provide specific file names, service names, and concrete technical steps rather than general possibilities +- **Focused Root Causes**: Prioritize the most probable causes (2-3 focused hypotheses) rather than exhaustive possibilities, but always label them as "possible" or "potential" +- **Implementation Hints**: Include specific technical solutions when known + +**For Bug Reports:** + +- Include exact error messages +- Provide specific URLs or page names +- Include browser console errors if applicable +- Mention recent changes that might be related +- Add screenshots directly in description when available + +**For Feature Requests:** + +- Use simple structure: Context → User Report → Proposed Solution(s) +- Keep descriptions concise and focused +- Use numbered options for different approaches +- Reference related tickets where applicable + +--- + +## Step 4: Add Labels and Assignment + +**TK Project Labels:** +- `bug` - Something isn't working +- `enhancement` - Adds or modifies features to improve functionality or user experience +- `refactor` - Improves code's internal structure without changing its behavior +- `documentation` - Improvements or additions to documentation +- `easy-win` - Quick fixes that provide significant user value +- `cultural-protocols` - Issues related to Indigenous cultural protocols +- `indigenous-language` - Language support and translation +- `access-control` - Permissions and access management +- `data-migration` - Data import/export/migration tasks + +**Priority Levels:** +- **Highest** - Critical issues blocking production +- **High** - Important issues affecting core functionality +- **Medium** - Standard priority for most issues +- **Low** - Minor issues or nice-to-have improvements +- **Lowest** - Cosmetic issues or very low impact + +**Assignment:** +- Assign to appropriate team member based on expertise +- If unsure, leave unassigned for team lead to assign + +--- + +## Complete Examples + +### Bug Report Example + +**Title:** `Fix: Knowledge Entry Search Not Returning Indigenous Language Results` + +**Labels:** `bug`, `indigenous-language` + +**Body:** + +``` +**Describe the bug** +Users are unable to search for traditional knowledge entries using Indigenous language terms. The search returns no results even when entries exist with matching terms. + +**To Reproduce** +Steps to reproduce the behavior: +1. Navigate to Knowledge Base +2. Enter an Indigenous language search term +3. Click search +4. Observe empty results + +**Expected behavior** +Search should return knowledge entries matching the Indigenous language terms. + +**Desktop (please complete your following information):** +- OS: Windows 11 +- Browser: Chrome 120.0.6099.129 +- Version: Latest + +**Additional context** +Error in console: "Search index does not include language metadata" +``` + +### Feature Request Example + +**Title:** `Enhancement: Add Cultural Protocol Management for Knowledge Entries` + +**Labels:** `enhancement`, `cultural-protocols` + +**Body:** + +``` +# Context + +Knowledge Keepers need the ability to set cultural protocols on entries so that sensitive information is only accessible to authorized community members. Currently there is no way to restrict access based on cultural sensitivity. + +## User Report + +> We need to be able to mark certain knowledge as restricted so only authorized community members can view it. + +## Proposed Solutions + +### Option 1 + +Add cultural protocol settings directly on each knowledge entry with role-based access control. + +### Option 2 + +Implement community-level protocol templates that can be applied to entries. + +### Option 3 + +Create a tiered access system with community approval workflows. +``` + +### Refactoring Example + +**Title:** `Refactor: Standardize Information Sharing Agreement Service Pattern` + +**Labels:** `refactor` + +**Body:** + +``` +# Context + +Current ISA services have inconsistent patterns between create, update, and sign flows, leading to potential data integrity issues and maintenance overhead. + +## User Report + +> We're seeing inconsistencies in how agreements are processed depending on the action taken. + +## Proposed Solutions + +### Option 1 + +Standardize all ISA services to use the BaseService pattern with consistent validation and group management. + +### Option 2 + +Keep current service patterns with manual oversight. +``` + +--- + +## Step 5: Create the Issue + +### Option 1: Create via Jira Web UI + +1. Go to https://yg-hpw.atlassian.net/jira/software/projects/TK/boards/27 +2. Click **Create** in the top navigation +3. Select appropriate issue type (Bug, Story, Task, Epic) +4. Fill in Summary and Description fields following the examples above +5. Set Priority and add relevant Labels +6. Assign to appropriate team member if known +7. Click **Create** + +### Option 2: Create via Jira REST API + +**For Bug Reports:** +```bash +curl -X POST "https://yg-hpw.atlassian.net/rest/api/3/issue" \ + -H "Authorization: Basic $(echo -n 'email:api_token' | base64)" \ + -H "Content-Type: application/json" \ + -d '{ + "fields": { + "project": { "key": "TK" }, + "issuetype": { "name": "Bug" }, + "summary": "Bug: [Brief Description]", + "description": { + "type": "doc", + "version": 1, + "content": [{ + "type": "paragraph", + "content": [{ + "type": "text", + "text": "**Describe the bug**\n[Clear description]\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. [Step 1]\n2. [Step 2]\n3. [Step 3]\n\n**Expected behavior**\n[What should happen]\n\n**Desktop (please complete your following information):**\n- OS: [e.g. macOS 14.0]\n- Browser: [e.g. Chrome 120.0.6099.129]\n- Version: Latest\n\n**Additional context**\n[Any relevant extra information]" + }] + }] + }, + "priority": { "name": "Medium" }, + "labels": ["bug", "easy-win"] + } + }' +``` + +**For Feature Requests:** +```bash +curl -X POST "https://yg-hpw.atlassian.net/rest/api/3/issue" \ + -H "Authorization: Basic $(echo -n 'email:api_token' | base64)" \ + -H "Content-Type: application/json" \ + -d '{ + "fields": { + "project": { "key": "TK" }, + "issuetype": { "name": "Story" }, + "summary": "Feature: [Brief Description]", + "description": { + "type": "doc", + "version": 1, + "content": [{ + "type": "paragraph", + "content": [{ + "type": "text", + "text": "Relates to:\n- [Related issues or documentation]\n\n# Context\n\n**Is your feature request related to a problem? Please describe.**\n[Problem description]\n\n**Describe the solution you would like**\n[Clear solution description]\n\n**Describe alternatives you have considered**\n[Alternative approaches]\n\n**Additional context**\n[Extra details, examples, or context]" + }] + }] + }, + "priority": { "name": "Medium" }, + "labels": ["enhancement"] + } + }' +``` + +--- + +## Enhancing Existing Issues + +### When to Enhance + +- Adding visual evidence after initial creation +- Integrating user reports from external sources +- Adding dependency relationships between issues +- Updating descriptions with investigation findings + +### Enhancement Patterns + +**Adding Remote Links:** +```bash +# Generate MD5 hash global ID +GLOBAL_ID=$(echo -n "https://external-system.com/conversation/unique-id" | md5sum | cut -c1-8) + +# Create remote link +curl -X POST "https://yg-hpw.atlassian.net/rest/api/3/issue/{issueKey}/remotelink" \ + -H "Content-Type: application/json" \ + -H "Authorization: Basic $(echo -n '${JIRA_EMAIL}:${JIRA_API_TOKEN}' | base64)" \ + -d "{ + \"globalId\": \"${GLOBAL_ID}\", + \"object\": { + \"url\": \"https://external-system.com/conversation/unique-id\", + \"title\": \"User Report - UserName \" + } + }" +``` + +**Issue Linking Patterns:** +- Use "causes"/"is caused by" for technical dependencies +- Use "relates to" for related but independent issues +- Link to parent/child issues for hierarchical relationships + +--- + +## Quality Checklist + +**For Bug Reports:** + +- [ ] Bug description is clear and concise +- [ ] Reproduction steps are numbered and specific +- [ ] Expected vs actual behavior is clearly stated +- [ ] Screenshots included when applicable +- [ ] Browser/OS version information provided +- [ ] Error messages included verbatim +- [ ] Recent changes mentioned if relevant + +**For Feature Requests:** + +- [ ] Uses simple structure: Context → User Report → Proposed Solutions +- [ ] User report included as direct quote +- [ ] Problem context is clearly explained +- [ ] Multiple solution options provided when applicable +- [ ] Related tickets referenced where applicable + +**General:** + +- [ ] Appropriate labels assigned (type, priority, component) +- [ ] Related issues or documentation linked +- [ ] Title is descriptive and follows project conventions +- [ ] Title uses proper title case formatting +- [ ] Issue is assigned to appropriate team member if applicable + +--- + +## TK-Specific Considerations + +### User Reports Integration + +For issues reported by users: + +1. **Primary Context**: Integrate user reports into main description when they're the primary context for the issue +2. **Supplemental Information**: Add as separate comment with web link when providing additional context +3. **Remote Links**: Create remote links with proper global IDs for external references +4. **Format**: Use clear "User Report" section with reporter's email and original description +5. **Visual Evidence**: Include screenshots directly in description when available + +### Common TK Context Patterns + +**Knowledge Entry Issues:** +- Include specific entry types and categories +- Reference cultural protocol levels +- Include language metadata when relevant + +**Information Sharing Agreement Issues:** +- Include agreement status and parties involved +- Reference group creation and access control +- Include signing workflow context + +**Access Control Issues:** +- Include user roles and permissions +- Reference group membership +- Include cultural protocol restrictions + +--- + +## Related Workflows + +- [`./code-review.md`](./code-review.md) - Code review quality control +- [`./pull-request-management.md`](./pull-request-management.md) - Create and update pull requests +- [`./testing-instructions.md`](./testing-instructions.md) - Generate comprehensive testing instructions + +--- + +**Last Updated:** 2026-03-12 + +_Update this workflow when you discover better patterns or Traditional Knowledge project conventions evolve._ diff --git a/agents/workflows/pull-request-management.md b/agents/workflows/pull-request-management.md index 8545627a..1205dd5d 100644 --- a/agents/workflows/pull-request-management.md +++ b/agents/workflows/pull-request-management.md @@ -21,6 +21,8 @@ auto_execution_mode: 1 - **Implementation section:** Focus on purpose and intent, not specific files. A reviewer can see file changes in the diff - the Implementation section explains the reasoning behind those changes. - **Screenshots:** Check the diff for `web/src/components/` or `web/src/pages/` changes. If present, write "TODO" and let user add screenshots. Only write "N/A - backend changes only" if there are truly no frontend changes. - **Draft mode:** Always create PRs as drafts first +- **QA Testing:** Write testing instructions for someone with zero project knowledge, focusing on user interactions rather than technical implementation. Follow the `testing-instructions` workflow for comprehensive guidance on creating detailed, accurate testing instructions with exact UI element names and proper test case structure. +- **Complete workflow sequence:** This is step 3 of 4 in the complete PR creation process. Always use after jira-issue-management and code-review workflows, then follow with testing-instructions workflow for comprehensive test coverage. This workflow covers the process of creating and editing well-structured pull requests that follow the established patterns in the Traditional Knowledge project. @@ -478,6 +480,14 @@ The current system only supports screen viewing and printing, making it difficul --- -**Last Updated:** 2026-01-23 +## Related Workflows + +- [`./jira-issue-management.md`](./jira-issue-management.md) - Creating well-structured Jira issues +- [`./testing-instructions.md`](./testing-instructions.md) - Generate comprehensive testing instructions +- [`./code-review.md`](./code-review.md) - Code review quality control + +--- + +**Last Updated:** 2026-03-12 *Update this workflow when you discover better patterns or Traditional Knowledge project conventions evolve.* diff --git a/agents/workflows/testing-instructions.md b/agents/workflows/testing-instructions.md new file mode 100644 index 00000000..9e238846 --- /dev/null +++ b/agents/workflows/testing-instructions.md @@ -0,0 +1,226 @@ +--- +description: Generate comprehensive testing instructions for pull requests +--- + +# Testing Instructions Workflow + +This workflow guides you through creating comprehensive, accurate testing instructions for a pull request. + +## Intent + +**WHY this workflow exists:** Pull requests need clear, actionable testing instructions that developers can follow to validate changes. Without proper testing instructions, PR validation becomes inconsistent and error-prone. + +**WHAT this workflow produces:** +- Comprehensive testing instructions with exact UI element names +- Sequential test cases covering happy paths, edge cases, and error conditions +- Proper formatting following Traditional Knowledge project standards +- Navigation paths and verification steps for each test scenario + +**Decision Rules:** +- **Always verify UI element names**: Never guess button names, tab names, or labels - search the Vue code +- **Use exact formatting**: Follow the established bold formatting and sequential numbering patterns +- **Cover all scenarios**: Include happy paths, edge cases, error conditions, and cleanup flows +- **Be specific**: Include exact navigation paths, field labels, and expected outcomes +- **Complete workflow sequence:** This is step 4 of 4 in the complete PR creation process. Always use after jira-issue-management, code-review, and pull-request-management workflows to provide comprehensive test coverage for the created PR. + +--- + +## Process + +### Step 1: Understand the PR changes + +Read the PR description to understand: +- What feature or bug is being addressed +- What specific functionality changed +- What edge cases need testing +- Any concerns/questions raised + +Identify the main test scenarios that need coverage. + +### Step 2: Find actual UI element names from Vue code + +**CRITICAL**: Always verify exact button names, tab names, and UI element labels from the actual Vue components. Never guess or assume names. + +#### Finding button text and labels: + +```bash +# Find button text in Vue components +grep -r "v-btn" web/src --include="*.vue" | grep -i "keyword" + +# Find specific button labels +grep -r ">Create" web/src --include="*.vue" +grep -r ">Add" web/src --include="*.vue" +grep -r ">Save" web/src --include="*.vue" + +# Find tab names +grep -r "v-tab" web/src --include="*.vue" + +# Find form field labels +grep -r "label=" web/src --include="*.vue" | grep -i "keyword" +``` + +#### Finding navigation structure: + +```bash +# Find router paths and page names +grep -r "router-link" web/src --include="*.vue" | grep -i "keyword" +grep -r "to=" web/src --include="*.vue" | grep -i "keyword" + +# Find menu items +grep -r "v-list-item" web/src --include="*.vue" +grep -r "v-navigation" web/src --include="*.vue" +``` + +#### Example searches for common UI patterns: + +```bash +# Agreement-related buttons +grep -r "New Agreement" web/src --include="*.vue" +grep -r "Create" web/src --include="*.vue" + +# Tab components +grep -r 'v-tab.*Details' web/src --include="*.vue" +grep -r 'v-tab.*History' web/src --include="*.vue" + +# Form buttons +grep -r "Submit" web/src --include="*.vue" +grep -r "Cancel" web/src --include="*.vue" +``` + +### Step 3: Verify navigation paths + +Check the actual navigation structure: + +```bash +# Find page components and their routes +cat web/src/router/index.ts + +# Find sidebar navigation items +find web/src/components -name "*Nav*" -o -name "*Sidebar*" -o -name "*Menu*" +``` + +Read relevant navigation components to understand: +- Exact menu item text +- Navigation hierarchy +- Page locations + +### Step 4: Structure test cases + +Break testing into logical test cases: +- **Test Case 1**: Main happy path scenario +- **Test Case 2**: Edge cases and variations +- **Test Case 3**: Error conditions or negative tests +- **Test Case 4**: Additional scenarios specific to the feature + +Each test case should: +- Have a clear descriptive heading +- Test one specific aspect of the functionality +- Include verification steps with expected outcomes + +### Step 5: Write testing instructions following the standard format + +Use this exact structure: + +```markdown +# Testing Instructions + +1. Run the test suite via `dev test`. +2. Boot the app via `dev up`. +3. Log in to the app at http://localhost:3000. + +## Test Case 1: [Descriptive scenario name] + +4. From the main app Dashboard, [first action] +5. Click **[Exact Button Name]** button +6. Fill in [form/dialog details]: + - **[Field Label]**: [Instructions or example value] + - **[Another Field]**: [Instructions] +7. Click **[Submit Button Name]** to submit +8. Verify [expected outcome] +9. Navigate to **[Page/Tab Name]** via [navigation path] +10. Verify [specific validation] + +## Test Case 2: [Another scenario] + +11. [Continue numbered sequence] +... +``` + +### Step 6: Follow formatting guidelines + +**Required formatting rules:** + +1. **Bold all UI elements**: `**Create Entry**`, `**Details** tab`, `**Add User** button` +2. **Use exact button text**: Search the code to find the actual button text, don't guess +3. **Sequential numbering**: Number steps continuously across all test cases (don't restart at 1) +4. **Navigation arrows**: Use `→` for navigation paths: `**Information Sharing Agreements** → **New Agreement** → **Details** tab` +5. **Specific locations**: "From the main app Dashboard", "in the right hand side panel", "via the left sidebar nav" +6. **Clear verification steps**: "Verify the table displays **Column Name** column", "Verify success message: 'exact text'" +7. **Test case headings**: Use `## Test Case N: Description` format +8. **Inline code for values**: Use backticks for URLs, exact error messages, field values + +### Step 7: Include all test scenarios + +Ensure coverage of: +- Main happy path functionality +- Edge cases (e.g., empty states, boundary conditions) +- Error conditions (e.g., validation errors, permission errors) +- Data persistence (verify data survives page refresh if relevant) +- Cleanup/deletion flows +- Any concerns mentioned in the PR description + +### Step 8: Review and validate + +Before finalizing: +- [ ] All UI element names match actual code +- [ ] Navigation paths are accurate +- [ ] All test cases have clear expected outcomes +- [ ] Sequential numbering is correct +- [ ] Bold formatting is applied to all UI elements +- [ ] Each test case tests a distinct scenario +- [ ] Edge cases from PR description are covered + +## Example Output + +```markdown +# Testing Instructions + +1. Run the test suite via `dev test`. +2. Boot the app via `dev up`. +3. Log in to the app at http://localhost:3000. + +## Test Case 1: User can create a new information sharing agreement + +4. Click **Information Sharing Agreements** in the left sidebar nav +5. Click **New Agreement** button +6. Fill in the agreement details: + - **Title**: Enter "Test Agreement" + - **Yukon First Nation or Transboundary Contact Name**: Select a contact +7. Click **Save** to submit +8. Verify the agreement appears in the list with status "Draft" +9. Click the agreement to open it +10. Verify all fields are populated correctly + +## Test Case 2: User can sign an agreement and groups are created + +11. From the agreement detail page, click the **Sign** button +12. Fill in the signing details +13. Click **Confirm** +14. Verify the agreement status changes to "Signed" +15. Navigate to the **Groups** section +16. Verify sharing and receiving groups were created automatically +``` + +## Related Workflows + +- [`./jira-issue-management.md`](./jira-issue-management.md) - Creating well-structured Jira issues +- [`./pull-request-management.md`](./pull-request-management.md) - Create and update pull requests +- [`./code-review.md`](./code-review.md) - Code review quality control + +## Tips + +- When in doubt about UI element names, always check the code first +- Pay attention to Vuetify component patterns (v-btn, v-tab, v-dialog, etc.) +- Check both the component files and any related store/service files +- Test instructions should be detailed enough that someone unfamiliar with the feature can follow them +- Include both success paths and failure paths when relevant From ce024a21b4a307551dc23c30cc713bd4e5bf21c5 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:30:02 -0700 Subject: [PATCH 44/56] :sparkles: Show View Knowledge Item link when knowledge item exists. Otherwise show Create Knowledge Item link. --- ...ationSharingAgreementSignedActionsMenu.vue | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue index f28bdec9..77c31bdd 100644 --- a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue @@ -50,7 +50,29 @@ + View Knowledge Item + + + + import { computed, toRefs } from "vue" -import { isEmpty, isNil } from "lodash" +import { isNil } from "lodash" import Api from "@/api" import useAuthenticatedDownload from "@/use/utils/use-authenticated-download" @@ -106,7 +128,7 @@ const informationSharingAgreementArchiveItemsQuery = computed(() => ({ const { informationSharingAgreementArchiveItems } = useInformationSharingAgreementArchiveItems( informationSharingAgreementArchiveItemsQuery ) -const hasArchiveItem = computed(() => !isEmpty(informationSharingAgreementArchiveItems.value)) +const archiveItemId = computed(() => informationSharingAgreementArchiveItems.value?.at(0)?.archiveItemId) const generateSignedAcknowledgementUrl = computed(() => Api.Downloads.InformationSharingAgreements.signedAcknowledgementApi.downloadPath( From fa45e897a2a045c7cd3a1beb7d883188f6e99834 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:33:55 -0700 Subject: [PATCH 45/56] :hammer: Simplify agent plans formula. --- agents/plans/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/agents/plans/README.md b/agents/plans/README.md index 568af6b2..935d35f2 100644 --- a/agents/plans/README.md +++ b/agents/plans/README.md @@ -4,9 +4,7 @@ This directory contains implementation planning documents for the Traditional Kn ## Available Plans -| Plan | Description | -|------|-------------| -| [Plan, Vue Component Expansion Panels Refactor, 2026-02-03](Plan, Vue Component Expansion Panels Refactor, 2026-02-03.md) | Refactor InformationSharingAgreementLayout to use named slots component instead of router-view | +See this directory for available plans. Plans are created as needed for complex implementation work and follow the naming convention: `Type, Title, Date.md`. ## Using Plans @@ -107,4 +105,4 @@ All plans follow this template: --- -**Last Updated:** 2026-02-03 +**Last Updated:** 2026-03-12 From 262491a85e158c9ae39b1b3519f86316feeffbf2 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:36:18 -0700 Subject: [PATCH 46/56] :pencil: Fix typo in UI message. --- .../InformationSharingAgreementArchiveItemCreateForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue index 39a852a5..01e43e96 100644 --- a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue @@ -117,7 +117,7 @@ Attachments -

Drag and drop files or click the box belox

+

Drag and drop files or click the box below

Date: Thu, 12 Mar 2026 08:38:04 -0700 Subject: [PATCH 47/56] :ok_hand: Add loading state to save button in archive item create form. --- .../InformationSharingAgreementArchiveItemCreateForm.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue index 01e43e96..130cfd57 100644 --- a/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue +++ b/web/src/components/information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemCreateForm.vue @@ -129,6 +129,7 @@ Save
From ae81d096b57f677f9ae0a3745b421ed2cc799442 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:40:47 -0700 Subject: [PATCH 48/56] :recycle: Update route link to point to non-admin page now that ISA are more public. --- ...chiveItemsAsInformationSharingAgreementsEditDataIterator.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/information-sharing-agreement-archive-items/InformationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.vue b/web/src/components/information-sharing-agreement-archive-items/InformationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.vue index bac450ab..291801b9 100644 --- a/web/src/components/information-sharing-agreement-archive-items/InformationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.vue +++ b/web/src/components/information-sharing-agreement-archive-items/InformationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.vue @@ -128,7 +128,7 @@ const router = useRouter() function goToInformationSharingAgreementPage(informationSharingAgreementId: number) { return router.push({ - name: "administration/information-sharing-agreements/InformationSharingAgreementPage", + name: "information-sharing-agreements/InformationSharingAgreementPage", params: { informationSharingAgreementId, }, From 28cb30f44136fcc36b92e99df1ee631934811deb Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 08:43:54 -0700 Subject: [PATCH 49/56] :sparkles: Only show ArchiveItem share button when no existing link to an Sharing Agreement. --- ...rchiveItemInformationSharingAgreementsPage.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/web/src/pages/archive-items/ArchiveItemInformationSharingAgreementsPage.vue b/web/src/pages/archive-items/ArchiveItemInformationSharingAgreementsPage.vue index b38f588b..83b6ab8d 100644 --- a/web/src/pages/archive-items/ArchiveItemInformationSharingAgreementsPage.vue +++ b/web/src/pages/archive-items/ArchiveItemInformationSharingAgreementsPage.vue @@ -2,7 +2,7 @@ @@ -54,9 +52,10 @@ const informationSharingAgreementArchiveItemsQuery = computed(() => ({ }, perPage: 1, })) -const { informationSharingAgreementArchiveItems } = useInformationSharingAgreementArchiveItems( - informationSharingAgreementArchiveItemsQuery -) +const { + informationSharingAgreementArchiveItems, + refresh: refreshInformationSharingAgreementArchiveItems, +} = useInformationSharingAgreementArchiveItems(informationSharingAgreementArchiveItemsQuery) const hasExistingLink = computed(() => !isEmpty(informationSharingAgreementArchiveItems.value)) const informationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator = @@ -66,8 +65,15 @@ const informationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditD > >("informationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator") -function refreshInformationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator() { - refreshArchiveItem() - informationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.value?.refresh() +async function refreshAll() { + await Promise.all([ + refreshArchiveItem(), + refreshInformationSharingAgreementArchiveItems(), + informationSharingAgreementArchiveItemsAsInformationSharingAgreementsEditDataIterator.value?.refresh(), + ]) +} + +async function refreshArchiveAndLinks() { + await Promise.all([refreshArchiveItem(), refreshInformationSharingAgreementArchiveItems()]) } From 9cc1c34a3f419de92d5924eef3f32b2d32055ef5 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 09:07:55 -0700 Subject: [PATCH 51/56] :bug: Ensure that notAssociatedWithArchiveItem filter excludes deleted items. --- api/src/models/information-sharing-agreement.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/models/information-sharing-agreement.ts b/api/src/models/information-sharing-agreement.ts index 9c8a7a98..10923512 100644 --- a/api/src/models/information-sharing-agreement.ts +++ b/api/src/models/information-sharing-agreement.ts @@ -342,6 +342,7 @@ export class InformationSharingAgreement extends BaseModel< information_sharing_agreement_archive_items WHERE archive_item_id = :archiveItemId + AND deleted_at IS NULL ) `, }, From 4a82bbad37ff6a7d0dc17a4c105d8e5e20baa4ed Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 09:24:55 -0700 Subject: [PATCH 52/56] :sparkles: Restrict archive item linking in UI to one to one. NOTE: this restriction is UI only. --- .../models/information-sharing-agreement.ts | 20 +++++++++++++++++++ .../api/information-sharing-agreements-api.ts | 1 + ...temToInformationSharingAgreementDialog.vue | 1 + 3 files changed, 22 insertions(+) diff --git a/api/src/models/information-sharing-agreement.ts b/api/src/models/information-sharing-agreement.ts index 10923512..0ed670a0 100644 --- a/api/src/models/information-sharing-agreement.ts +++ b/api/src/models/information-sharing-agreement.ts @@ -352,6 +352,26 @@ export class InformationSharingAgreement extends BaseModel< }, } }) + + this.addScope("notLinkedToAnyArchiveItem", () => { + return { + where: { + [Op.and]: sql` + NOT EXISTS ( + SELECT + 1 + FROM + information_sharing_agreement_archive_items + WHERE + information_sharing_agreement_archive_items.information_sharing_agreement_id = ${sql.attribute( + "id" + )} + AND information_sharing_agreement_archive_items.deleted_at IS NULL + ) + `, + }, + } + }) } } diff --git a/web/src/api/information-sharing-agreements-api.ts b/web/src/api/information-sharing-agreements-api.ts index 8e009d82..ca2e1a03 100644 --- a/web/src/api/information-sharing-agreements-api.ts +++ b/web/src/api/information-sharing-agreements-api.ts @@ -196,6 +196,7 @@ export type InformationSharingAgreementWhereOptions = WhereOptions< export type InformationSharingAgreementFiltersOptions = FiltersOptions<{ search: string | string[] notAssociatedWithArchiveItem: number + notLinkedToAnyArchiveItem: boolean }> export type InformationSharingAgreementQueryOptions = QueryOptions< diff --git a/web/src/components/information-sharing-agreement-archive-items/AddArchiveItemToInformationSharingAgreementDialog.vue b/web/src/components/information-sharing-agreement-archive-items/AddArchiveItemToInformationSharingAgreementDialog.vue index c7f51b18..264cbad8 100644 --- a/web/src/components/information-sharing-agreement-archive-items/AddArchiveItemToInformationSharingAgreementDialog.vue +++ b/web/src/components/information-sharing-agreement-archive-items/AddArchiveItemToInformationSharingAgreementDialog.vue @@ -95,6 +95,7 @@ const informationSharingAgreementArchiveItemAttributes = ref< const informationSharingAgreementFilters = computed(() => ({ notAssociatedWithArchiveItem: archiveItemId.value, + notLinkedToAnyArchiveItem: true, })) const form = ref | null>(null) From de9b6f5a5b5800ed55cacae159d689b6a6b971f1 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 09:45:14 -0700 Subject: [PATCH 53/56] :lock: Block edits to signed ISAs. --- .../signed-state-policy.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/api/src/policies/information-sharing-agreements/signed-state-policy.ts b/api/src/policies/information-sharing-agreements/signed-state-policy.ts index b770e22a..297bab3e 100644 --- a/api/src/policies/information-sharing-agreements/signed-state-policy.ts +++ b/api/src/policies/information-sharing-agreements/signed-state-policy.ts @@ -13,20 +13,10 @@ export class SignedStatePolicy extends GenericStatePolicy { } update(): boolean { - if (this.user.id === this.record.creatorId) return true - if (this.user.isSystemAdmin) return true - if (this.isAdminOfInternalGroup()) return true - if (this.isAdminOfExternalGroup()) return true - return false } destroy(): boolean { - if (this.user.id === this.record.creatorId) return true - if (this.user.isSystemAdmin) return true - if (this.isAdminOfInternalGroup()) return true - if (this.isAdminOfExternalGroup()) return true - return false } @@ -43,20 +33,6 @@ export class SignedStatePolicy extends GenericStatePolicy { return this.user.isMemberOfGroup(externalGroupId) } - - private isAdminOfInternalGroup(): boolean { - const { internalGroupId } = this.record - if (isNil(internalGroupId)) return false - - return this.user.isGroupAdminOf(internalGroupId) - } - - private isAdminOfExternalGroup(): boolean { - const { externalGroupId } = this.record - if (isNil(externalGroupId)) return false - - return this.user.isGroupAdminOf(externalGroupId) - } } export default SignedStatePolicy From fe6af74b7ca9f14c41ceed6416fbc41ea1012f63 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 10:02:26 -0700 Subject: [PATCH 54/56] :see_no_evil: Hide edit/delete buttons on ISA edit tables based on policy check. --- ...formation-sharing-agreements-controller.ts | 3 ++- api/src/policies/base-policy.ts | 4 +++- .../index-serializer.ts | 23 ++++++++++++++++++- .../api/information-sharing-agreements-api.ts | 2 ++ ...SharingAgreementSearchableAutocomplete.vue | 8 ++++++- ...ringAgreementsAdminEditDataTableServer.vue | 9 ++++++++ ...onSharingAgreementsEditDataTableServer.vue | 11 +++++++-- 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/information-sharing-agreements-controller.ts b/api/src/controllers/information-sharing-agreements-controller.ts index 08abf2b8..be16b8ae 100644 --- a/api/src/controllers/information-sharing-agreements-controller.ts +++ b/api/src/controllers/information-sharing-agreements-controller.ts @@ -27,7 +27,8 @@ export class InformationSharingAgreementsController extends BaseController + export const NO_RECORDS_SCOPE = Object.freeze({ where: literal("1 = 0") }) export const ALL_RECORDS_SCOPE = Object.freeze({}) @@ -79,7 +81,7 @@ export class BasePolicy { * * @returns a JSON representation of the policy */ - toJSON(): Record { + toJSON(): PolicyAsReference { return { show: this.show(), create: this.create(), diff --git a/api/src/serializers/information-sharing-agreements/index-serializer.ts b/api/src/serializers/information-sharing-agreements/index-serializer.ts index 9398bce8..f61dd982 100644 --- a/api/src/serializers/information-sharing-agreements/index-serializer.ts +++ b/api/src/serializers/information-sharing-agreements/index-serializer.ts @@ -2,8 +2,10 @@ import { pick } from "lodash" import { formatDate } from "@/utils/formatters" -import { InformationSharingAgreement } from "@/models" +import { type PolicyAsReference } from "@/policies/base-policy" +import { InformationSharingAgreement, type User } from "@/models" import BaseSerializer from "@/serializers/base-serializer" +import { InformationSharingAgreementPolicy } from "@/policies" export type InformationSharingAgreementAsIndex = Pick< InformationSharingAgreement, @@ -20,13 +22,24 @@ export type InformationSharingAgreementAsIndex = Pick< > & { startDate: string | null endDate: string | null +} & { + policy: PolicyAsReference } export class IndexSerializer extends BaseSerializer { + constructor( + protected record: InformationSharingAgreement, + protected currentUser: User + ) { + super(record) + } + perform(): InformationSharingAgreementAsIndex { const { startDate, endDate } = this.record const formattedStartDate = formatDate(startDate) const formattedEndDate = formatDate(endDate) + + const serializedPolicy = this.serializePolicy(this.record, this.currentUser) return { ...pick(this.record, [ "id", @@ -42,8 +55,16 @@ export class IndexSerializer extends BaseSerializer ]), startDate: formattedStartDate, endDate: formattedEndDate, + policy: serializedPolicy, } } + + private serializePolicy( + record: InformationSharingAgreement, + currentUser: User + ): PolicyAsReference { + return new InformationSharingAgreementPolicy(currentUser, record).toJSON() + } } export default IndexSerializer diff --git a/web/src/api/information-sharing-agreements-api.ts b/web/src/api/information-sharing-agreements-api.ts index ca2e1a03..611bdefd 100644 --- a/web/src/api/information-sharing-agreements-api.ts +++ b/web/src/api/information-sharing-agreements-api.ts @@ -94,6 +94,8 @@ export type InformationSharingAgreementAsIndex = Pick< > & { startDate: string | null endDate: string | null +} & { + policy: Policy } /** Keep in sync with api/src/serializers/information-sharing-agreements/show-serializer.ts */ diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementSearchableAutocomplete.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementSearchableAutocomplete.vue index f2b630cd..412a6d31 100644 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementSearchableAutocomplete.vue +++ b/web/src/components/information-sharing-agreements/InformationSharingAgreementSearchableAutocomplete.vue @@ -121,7 +121,13 @@ const allInformationSharingAgreements = computed
+ +
+ — +
diff --git a/web/src/components/information-sharing-agreements/InformationSharingAgreementsEditDataTableServer.vue b/web/src/components/information-sharing-agreements/InformationSharingAgreementsEditDataTableServer.vue index 0f16d287..3459f694 100644 --- a/web/src/components/information-sharing-agreements/InformationSharingAgreementsEditDataTableServer.vue +++ b/web/src/components/information-sharing-agreements/InformationSharingAgreementsEditDataTableServer.vue @@ -35,8 +35,8 @@ From 21ea1d7a195b6d67b6d4e9f9ad95ddb004b5b99f Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 11:38:13 -0700 Subject: [PATCH 55/56] :recycle: Rework sharing agreement signed actions menu. Make primary button dynamic based on whether knowledge item exists. Remove "edit" call-to-action now that you can no longer edit signed agreements. "revert to draft" is still present in the extended actions menu. --- ...ationSharingAgreementSignedActionsMenu.vue | 82 +++++++------------ 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue index 77c31bdd..cf974b38 100644 --- a/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue +++ b/web/src/components/information-sharing-agreements/signed/InformationSharingAgreementSignedActionsMenu.vue @@ -1,12 +1,6 @@ @@ -128,7 +77,32 @@ const informationSharingAgreementArchiveItemsQuery = computed(() => ({ const { informationSharingAgreementArchiveItems } = useInformationSharingAgreementArchiveItems( informationSharingAgreementArchiveItemsQuery ) -const archiveItemId = computed(() => informationSharingAgreementArchiveItems.value?.at(0)?.archiveItemId) +const archiveItemId = computed( + () => informationSharingAgreementArchiveItems.value?.at(0)?.archiveItemId +) +const primaryButtonAttributes = computed(() => { + if (!isNil(archiveItemId.value)) { + return { + primaryButtonText: "View Knowledge Item", + primaryButtonTo: { + name: "archive-items/ArchiveItemInformationSharingAgreementsPage", + params: { + archiveItemId: archiveItemId.value, + }, + }, + } + } else { + return { + primaryButtonText: "Create Knowledge Item", + primaryButtonTo: { + name: "information-sharing-agreements/archive-items/InformationSharingAgreementArchiveItemNewPage", + params: { + informationSharingAgreementId: props.informationSharingAgreementId, + }, + }, + } + } +}) const generateSignedAcknowledgementUrl = computed(() => Api.Downloads.InformationSharingAgreements.signedAcknowledgementApi.downloadPath( From e756feff6aaef037cacf5f0d5e3e4531e739606b Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Thu, 12 Mar 2026 11:40:04 -0700 Subject: [PATCH 56/56] :unlock: Permitting linking/unlinking of archive items to/from signed sharing agreements. --- ...n-sharing-agreement-archive-item-policy.ts | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/api/src/policies/information-sharing-agreement-archive-item-policy.ts b/api/src/policies/information-sharing-agreement-archive-item-policy.ts index 75ff3554..3b555cf7 100644 --- a/api/src/policies/information-sharing-agreement-archive-item-policy.ts +++ b/api/src/policies/information-sharing-agreement-archive-item-policy.ts @@ -1,8 +1,8 @@ import { type Attributes, type FindOptions } from "@sequelize/core" -import { isUndefined } from "lodash" +import { isNil, isUndefined } from "lodash" import { type Path } from "@/utils/deep-pick" -import { InformationSharingAgreementArchiveItem, User } from "@/models" +import { InformationSharingAgreementArchiveItem, User, type InformationSharingAgreement } from "@/models" import { PolicyFactory } from "@/policies/base-policy" import InformationSharingAgreementPolicy from "@/policies/information-sharing-agreement-policy" @@ -16,19 +16,28 @@ export class InformationSharingAgreementArchiveItemPolicy extends PolicyFactory( } create(): boolean { - if (this.informationSharingAgreementPolicy.update()) return true + if (this.user.id === this.informationSharingAgreement.creatorId) return true + if (this.user.isSystemAdmin) return true + if (this.isAdminOfInternalGroup()) return true + if (this.isAdminOfExternalGroup()) return true return false } update(): boolean { - if (this.informationSharingAgreementPolicy.update()) return true + if (this.user.id === this.informationSharingAgreement.creatorId) return true + if (this.user.isSystemAdmin) return true + if (this.isAdminOfInternalGroup()) return true + if (this.isAdminOfExternalGroup()) return true return false } destroy(): boolean { - if (this.informationSharingAgreementPolicy.update()) return true + if (this.user.id === this.informationSharingAgreement.creatorId) return true + if (this.user.isSystemAdmin) return true + if (this.isAdminOfInternalGroup()) return true + if (this.isAdminOfExternalGroup()) return true return false } @@ -62,6 +71,29 @@ export class InformationSharingAgreementArchiveItemPolicy extends PolicyFactory( return new InformationSharingAgreementPolicy(this.user, informationSharingAgreement) } + + private isAdminOfInternalGroup(): boolean { + const { internalGroupId } = this.informationSharingAgreement + if (isNil(internalGroupId)) return false + + return this.user.isGroupAdminOf(internalGroupId) + } + + private isAdminOfExternalGroup(): boolean { + const { externalGroupId } = this.informationSharingAgreement + if (isNil(externalGroupId)) return false + + return this.user.isGroupAdminOf(externalGroupId) + } + + private get informationSharingAgreement(): InformationSharingAgreement { + const { informationSharingAgreement } = this.record + if (isUndefined(informationSharingAgreement)) { + throw new Error("Expected information sharing agreement association to be pre-loaded") + } + + return informationSharingAgreement + } } export default InformationSharingAgreementArchiveItemPolicy