diff --git a/app/src/controllers/document.ts b/app/src/controllers/document.ts index 2062bc8f..a53ed34a 100644 --- a/app/src/controllers/document.ts +++ b/app/src/controllers/document.ts @@ -20,7 +20,7 @@ const controller = { req.body.mimeType, req.body.length ); - res.status(200).json(response); + res.status(201).json(response); } catch (e: unknown) { next(e); } diff --git a/app/src/controllers/note.ts b/app/src/controllers/note.ts index 951619af..d58ec8ee 100644 --- a/app/src/controllers/note.ts +++ b/app/src/controllers/note.ts @@ -17,7 +17,7 @@ const controller = { ...body, createdBy: userId }); - res.status(200).json(response); + res.status(201).json(response); } catch (e: unknown) { next(e); } diff --git a/app/src/controllers/permit.ts b/app/src/controllers/permit.ts index 27e2eabb..5d5ad836 100644 --- a/app/src/controllers/permit.ts +++ b/app/src/controllers/permit.ts @@ -11,7 +11,7 @@ const controller = { try { const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL); const response = await permitService.createPermit({ ...(req.body as Permit), updatedBy: userId }); - res.status(200).json(response); + res.status(201).json(response); } catch (e: unknown) { next(e); } diff --git a/app/src/controllers/submission.ts b/app/src/controllers/submission.ts index 1eab45a9..397d63a8 100644 --- a/app/src/controllers/submission.ts +++ b/app/src/controllers/submission.ts @@ -9,28 +9,6 @@ import type { NextFunction, Request, Response } from '../interfaces/IExpress'; import type { ChefsFormConfig, ChefsFormConfigData, Submission, ChefsSubmissionExport, Permit } from '../types'; const controller = { - createEmptySubmission: async (req: Request, res: Response, next: NextFunction) => { - let testSubmissionId; - let submissionQuery; - - // Testing for activityId collisions, which are truncated UUIDs - // If a collision is detected, generate new UUID and test again - do { - testSubmissionId = uuidv4(); - submissionQuery = await submissionService.getSubmission(testSubmissionId.substring(0, 8).toUpperCase()); - } while (submissionQuery); - - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const submitter = (req.currentUser?.tokenPayload as any)?.idir_username; - const result = await submissionService.createEmptySubmission(testSubmissionId, submitter); - - res.status(201).json({ activityId: result.activity_id }); - } catch (e: unknown) { - next(e); - } - }, - checkAndStoreNewSubmissions: async () => { const cfg = config.get('server.chefs.forms') as ChefsFormConfig; @@ -175,6 +153,28 @@ const controller = { notStored.map((x) => x.permits?.map(async (y) => await permitService.createPermit(y))); }, + createEmptySubmission: async (req: Request, res: Response, next: NextFunction) => { + let testSubmissionId; + let submissionQuery; + + // Testing for activityId collisions, which are truncated UUIDs + // If a collision is detected, generate new UUID and test again + do { + testSubmissionId = uuidv4(); + submissionQuery = await submissionService.getSubmission(testSubmissionId.substring(0, 8).toUpperCase()); + } while (submissionQuery); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const submitter = (req.currentUser?.tokenPayload as any)?.idir_username; + const result = await submissionService.createEmptySubmission(testSubmissionId, submitter); + + res.status(201).json({ activityId: result.activity_id }); + } catch (e: unknown) { + next(e); + } + }, + getStatistics: async ( req: Request, res: Response, diff --git a/app/src/services/note.ts b/app/src/services/note.ts index 2e1b1427..ed62185c 100644 --- a/app/src/services/note.ts +++ b/app/src/services/note.ts @@ -24,6 +24,12 @@ const service = { return note.fromPrismaModel(response); }, + /** + * @function deleteNote + * Soft deletes a note by marking is as deleted + * @param {string} noteId ID of the note to delete + * @returns {Promise} The result of running the update operation + */ deleteNote: async (noteId: string) => { const result = await prisma.note.update({ where: { @@ -76,8 +82,15 @@ const service = { return response.map((x) => note.fromPrismaModel(x)); }, + /** + * @function updateNote + * Updates a note by marking the old note as deleted and creating a new one + * @param {Note} data New Note object + * @returns {Promise} The result of running the transaction + */ updateNote: async (data: Note) => { return await prisma.$transaction(async (trx) => { + // Mark old note as deleted await trx.note.update({ where: { note_id: data.noteId @@ -87,16 +100,15 @@ const service = { } }); - const newNote = { - ...data, - noteId: uuidv4() - }; - - const newCreatedNote = await trx.note.create({ - data: note.toPrismaModel(newNote) + // Create new note + const response = await trx.note.create({ + data: note.toPrismaModel({ + ...data, + noteId: uuidv4() + }) }); - return note.fromPrismaModel(newCreatedNote); + return note.fromPrismaModel(response); }); } }; diff --git a/app/tests/unit/controllers/document.spec.ts b/app/tests/unit/controllers/document.spec.ts index 81e8188b..4277aace 100644 --- a/app/tests/unit/controllers/document.spec.ts +++ b/app/tests/unit/controllers/document.spec.ts @@ -29,7 +29,7 @@ describe('createDocument', () => { // Mock service calls const createSpy = jest.spyOn(documentService, 'createDocument'); - it('should return 200 if all good', async () => { + it('should return 201 if all good', async () => { const req = { body: { documentId: 'abc123', activityId: '1', filename: 'testfile', mimeType: 'imgjpg', length: 1234567 }, currentUser: CURRENT_USER @@ -56,7 +56,7 @@ describe('createDocument', () => { req.body.mimeType, req.body.length ); - expect(res.status).toHaveBeenCalledWith(200); + expect(res.status).toHaveBeenCalledWith(201); expect(res.json).toHaveBeenCalledWith(created); }); diff --git a/app/tests/unit/controllers/note.spec.ts b/app/tests/unit/controllers/note.spec.ts index 415e0c25..d56876cd 100644 --- a/app/tests/unit/controllers/note.spec.ts +++ b/app/tests/unit/controllers/note.spec.ts @@ -34,7 +34,7 @@ describe('createNote', () => { const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); - it('should return 200 if all good', async () => { + it('should return 201 if all good', async () => { const req = { body: { noteId: '123-123', @@ -75,7 +75,7 @@ describe('createNote', () => { expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY, NIL); expect(createSpy).toHaveBeenCalledTimes(1); expect(createSpy).toHaveBeenCalledWith({ ...req.body, createdBy: USR_ID }); - expect(res.status).toHaveBeenCalledWith(200); + expect(res.status).toHaveBeenCalledWith(201); expect(res.json).toHaveBeenCalledWith(created); }); diff --git a/app/tests/unit/controllers/permit.spec.ts b/app/tests/unit/controllers/permit.spec.ts index 78f1733c..c1ed3e22 100644 --- a/app/tests/unit/controllers/permit.spec.ts +++ b/app/tests/unit/controllers/permit.spec.ts @@ -34,7 +34,7 @@ describe('createPermit', () => { const getCurrentIdentitySpy = jest.spyOn(utils, 'getCurrentIdentity'); const getCurrentUserIdSpy = jest.spyOn(userService, 'getCurrentUserId'); - it('should return 200 if all good', async () => { + it('should return 201 if all good', async () => { const now = new Date(); const req = { body: { @@ -81,7 +81,7 @@ describe('createPermit', () => { expect(getCurrentUserIdSpy).toHaveBeenCalledWith(USR_IDENTITY, NIL); expect(createSpy).toHaveBeenCalledTimes(1); expect(createSpy).toHaveBeenCalledWith({ ...req.body, updatedBy: USR_ID }); - expect(res.status).toHaveBeenCalledWith(200); + expect(res.status).toHaveBeenCalledWith(201); expect(res.json).toHaveBeenCalledWith(created); }); diff --git a/frontend/src/components/file/DocumentCard.vue b/frontend/src/components/file/DocumentCard.vue index fd2e6a64..7df2f754 100644 --- a/frontend/src/components/file/DocumentCard.vue +++ b/frontend/src/components/file/DocumentCard.vue @@ -4,6 +4,7 @@ import { ref } from 'vue'; import { Button, Card, useConfirm, useToast } from '@/lib/primevue'; import { documentService } from '@/services'; +import { useSubmissionStore } from '@/store'; import { FILE_CATEGORIES } from '@/utils/constants'; import { formatDateLong } from '@/utils/formatters'; import { getFileCategory } from '@/utils/utils'; @@ -22,8 +23,8 @@ import type { Document } from '@/types'; // Props type Props = { - document: Document; deleteButton?: boolean; + document: Document; selectable?: boolean; selected?: boolean; }; @@ -35,7 +36,10 @@ const props = withDefaults(defineProps(), { }); // Emits -const emit = defineEmits(['document:clicked', 'document:deleted']); +const emit = defineEmits(['document:clicked']); + +// Store +const submissionStore = useSubmissionStore(); // State const isSelected: Ref = ref(props.selected); @@ -44,18 +48,18 @@ const isSelected: Ref = ref(props.selected); const confirm = useConfirm(); const toast = useToast(); -const confirmDelete = (documentId: string, filename: string) => { - if (documentId) { +const confirmDelete = (document: Document) => { + if (document) { confirm.require({ - message: `Please confirm that you want to delete ${filename}.`, + message: `Please confirm that you want to delete ${document.filename}.`, header: 'Delete document?', acceptLabel: 'Confirm', rejectLabel: 'Cancel', accept: () => { documentService - .deleteDocument(documentId) + .deleteDocument(document.documentId) .then(() => { - emit('document:deleted', documentId); + submissionStore.removeDocument(document); toast.success('Document deleted'); }) .catch((e: any) => toast.error('Failed to deleted document', e.message)); @@ -133,7 +137,7 @@ function onClick() { aria-label="Delete object" @click=" (e) => { - confirmDelete(props.document.documentId, props.document.filename); + confirmDelete(props.document); e.stopPropagation(); } " diff --git a/frontend/src/components/file/FileUpload.vue b/frontend/src/components/file/FileUpload.vue index 82371df3..e7c0d078 100644 --- a/frontend/src/components/file/FileUpload.vue +++ b/frontend/src/components/file/FileUpload.vue @@ -4,11 +4,10 @@ import { ref } from 'vue'; import { FileUpload, useToast } from '@/lib/primevue'; import { documentService } from '@/services'; -import { useConfigStore } from '@/store'; +import { useConfigStore, useSubmissionStore } from '@/store'; import type { FileUploadUploaderEvent } from 'primevue/fileupload'; import type { Ref } from 'vue'; -import type { Document } from '@/types'; // Props type Props = { @@ -17,10 +16,9 @@ type Props = { const props = withDefaults(defineProps(), {}); -const lastUploadedDocument = defineModel('lastUploadedDocument'); - // Store const { getConfig } = storeToRefs(useConfigStore()); +const submissionStore = useSubmissionStore(); // State const fileInput: Ref = ref(null); @@ -28,24 +26,23 @@ const fileInput: Ref = ref(null); // Actions const toast = useToast(); -const onFileUploadDragAndDrop = (event: FileUploadUploaderEvent) => { - onUpload(Array.isArray(event.files) ? event.files[0] : event.files); -}; - const onFileUploadClick = () => { fileInput.value.click(); }; +const onFileUploadDragAndDrop = (event: FileUploadUploaderEvent) => { + onUpload(Array.isArray(event.files) ? event.files[0] : event.files); +}; + const onUpload = async (file: File) => { try { const response = (await documentService.createDocument(file, props.activityId, getConfig.value.coms.bucketId)) ?.data; if (response) { - lastUploadedDocument.value = response; + submissionStore.addDocument(response); + toast.success('Document uploaded'); } - - toast.success('Document uploaded'); } catch (e: any) { toast.error('Failed to upload document', e); } diff --git a/frontend/src/components/note/NoteCard.vue b/frontend/src/components/note/NoteCard.vue index f0466a0f..7bd583f6 100644 --- a/frontend/src/components/note/NoteCard.vue +++ b/frontend/src/components/note/NoteCard.vue @@ -1,9 +1,9 @@