diff --git a/packages/client/src/components/GrantNotes.spec.js b/packages/client/src/components/GrantNotes.spec.js
index 7218fe952..5673c8b9f 100644
--- a/packages/client/src/components/GrantNotes.spec.js
+++ b/packages/client/src/components/GrantNotes.spec.js
@@ -10,11 +10,11 @@ import { id } from '@/helpers/testHelpers';
const CURRENT_USER_ID = id();
const getMockNotes = (count, hasMoreCursor = null, userId = id()) => ({
- notes: Array.from(Array(count), () => ({
+ notes: Array.from(Array(count), (el, i) => ({
id: id(),
createdAt: new Date().toISOString(),
isRevised: true,
- text: 'Text',
+ text: `Text ${i}`,
grant: {},
user: {
id: userId,
@@ -43,11 +43,18 @@ const mockStore = {
'grants/getNotesForGrant': vi.fn(),
'grants/getNotesForCurrentUser': vi.fn(),
'grants/saveNoteForGrant': vi.fn(),
+ 'grants/deleteGrantNoteForUser': vi.fn(),
},
};
const store = createStore(mockStore);
+const stubTextArea = {
+ template: '',
+ props: { modelValue: String },
+ methods: { focus: vi.fn() },
+};
+
beforeEach(() => {
vitest.clearAllMocks();
});
@@ -138,6 +145,65 @@ describe('GrantNotes component', () => {
expect(userNoteCmp.exists()).equal(true);
});
+ it('Correctly toggles edit mode for note input', async () => {
+ const userNote = getMockUserNote(1);
+ const allNotes = getMockNotes(2);
+
+ mockStore.actions['grants/getNotesForCurrentUser'].mockResolvedValue(userNote);
+ mockStore.actions['grants/getNotesForGrant'].mockResolvedValue(allNotes);
+
+ const wrapper = mount(GrantNotes, {
+ global: {
+ plugins: [store],
+ stubs: {
+ 'b-form-textarea': stubTextArea,
+ },
+ },
+ });
+
+ await flushPromises();
+
+ const editBtn = wrapper.findComponent('[data-test-edit-note-btn]');
+ editBtn.trigger('click');
+
+ await flushPromises();
+
+ const form = wrapper.find('[data-test-edit-form]');
+ expect(form.exists()).toBe(true);
+ expect(wrapper.findComponent(stubTextArea).props('modelValue')).toBe(userNote.notes[0].text);
+ });
+
+ it('Correctly deletes a user note', async () => {
+ const userNote = getMockUserNote(1);
+ const allNotes = getMockNotes(2);
+
+ mockStore.actions['grants/getNotesForCurrentUser'].mockResolvedValue(userNote);
+ mockStore.actions['grants/getNotesForGrant'].mockResolvedValue(allNotes);
+ mockStore.actions['grants/deleteGrantNoteForUser'].mockResolvedValue();
+
+ const wrapper = mount(GrantNotes, {
+ global: {
+ plugins: [store],
+ stubs: {
+ 'b-form-textarea': stubTextArea,
+ },
+ },
+ });
+
+ await flushPromises();
+
+ mockStore.actions['grants/getNotesForCurrentUser'].mockResolvedValue(getMockUserNote(0));
+
+ const deleteBtn = wrapper.findComponent('[data-test-delete-note-btn]');
+
+ deleteBtn.trigger('click');
+
+ await flushPromises();
+
+ expect(mockStore.actions['grants/deleteGrantNoteForUser']).toHaveBeenCalled();
+ expect(wrapper.findComponent(stubTextArea).props('modelValue')).toBe('');
+ });
+
it('Correctly retrieves more notes using "Show More"', async () => {
const userNote = getMockUserNote(1);
diff --git a/packages/client/src/components/GrantNotes.vue b/packages/client/src/components/GrantNotes.vue
index da8aed610..e25b32fe4 100644
--- a/packages/client/src/components/GrantNotes.vue
+++ b/packages/client/src/components/GrantNotes.vue
@@ -3,6 +3,7 @@
@@ -65,17 +66,38 @@
>
{{ userNote.text }}
-
-
- EDIT
-
+
+
+
+ EDIT
+
+
+
+ Edit
+
+
+ Delete
+
+
@@ -137,7 +159,7 @@ export default {
otherNotes: [],
userNote: null,
noteText: '',
- submittingNote: false,
+ savingNote: false,
editingNote: false,
notesNextCursor: null,
};
@@ -157,7 +179,7 @@ export default {
return this.filteredNoteText.length === 0;
},
noteSendBtnDisabled() {
- return this.emptyNoteText || this.submittingNote;
+ return this.emptyNoteText || this.savingNote;
},
charCountClass() {
const errColor = this.filteredNoteText.length === 300 ? 'text-error' : '';
@@ -179,13 +201,13 @@ export default {
getNotesForGrant: 'grants/getNotesForGrant',
getNotesForCurrentUser: 'grants/getNotesForCurrentUser',
saveNoteForGrant: 'grants/saveNoteForGrant',
+ deleteGrantNoteForUser: 'grants/deleteGrantNoteForUser',
}),
formatter(value) {
return value.substring(0, 300);
},
async toggleEditNote() {
this.editingNote = true;
- this.noteText = this.userNote.text;
await nextTick();
this.$refs.noteTextarea.focus();
@@ -196,7 +218,7 @@ export default {
}
},
async submitNote() {
- this.submittingNote = true;
+ this.savingNote = true;
try {
await this.saveNoteForGrant({ grantId: this.currentGrant.grant_id, text: this.filteredNoteText });
@@ -206,13 +228,16 @@ export default {
// Error already logged
}
- this.submittingNote = false;
+ this.savingNote = false;
},
- async fetchUsersNote() {
- const result = await this.getNotesForCurrentUser({ grantId: this.currentGrant.grant_id });
-
+ setUserNote(result) {
this.userNote = result && result.notes.length ? result.notes[0] : null;
this.editingNote = !this.userNote;
+ this.noteText = this.userNote ? this.userNote.text : '';
+ },
+ async fetchUsersNote() {
+ const result = await this.getNotesForCurrentUser({ grantId: this.currentGrant.grant_id });
+ this.setUserNote(result);
},
async fetchNextNotes() {
const query = {
@@ -232,6 +257,17 @@ export default {
this.notesNextCursor = result.pagination.next;
}
},
+ async deleteUserNote() {
+ this.savingNote = true;
+ try {
+ await this.deleteGrantNoteForUser({ grantId: this.currentGrant.grant_id });
+ this.$emit('noteSaved');
+ this.setUserNote(null);
+ } catch (e) {
+ // Error already logged
+ }
+ this.savingNote = false;
+ },
},
};
@@ -266,14 +302,18 @@ textarea.note-textarea::placeholder {
}
.note-edit-container {
- padding: 1rem 1.25rem 0;
+ padding: 1rem 1.25rem 0.25rem;
}
.note-edit-btn {
- font-size: 0.875rem;
+ vertical-align: middle;
color: $raw-gray-500
}
+.note-edit-btn-text {
+ font-size: 0.75rem;
+}
+
.note-send-btn {
bottom: 1.5625rem;
right: 0;
diff --git a/packages/client/src/store/modules/grants.js b/packages/client/src/store/modules/grants.js
index 476364a1f..f9042424c 100644
--- a/packages/client/src/store/modules/grants.js
+++ b/packages/client/src/store/modules/grants.js
@@ -282,6 +282,17 @@ export default {
throw e;
}
},
+ async deleteGrantNoteForUser({ rootGetters, commit }, { grantId }) {
+ try {
+ const userId = rootGetters['users/loggedInUser']?.id;
+ await fetchApi.deleteRequest(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/notes/user/${userId}`);
+ } catch (e) {
+ const text = `Error deleting grant note for user: ${e.message}`;
+ commit('alerts/addAlert', { text, level: 'err' }, { root: true });
+ datadogRum.addError(e, { grantId, text });
+ throw e;
+ }
+ },
async followGrantForCurrentUser({ rootGetters, commit }, { grantId }) {
try {
await fetchApi.put(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`);
diff --git a/packages/server/__tests__/api/grants.test.js b/packages/server/__tests__/api/grants.test.js
index be2699a45..a4e199582 100644
--- a/packages/server/__tests__/api/grants.test.js
+++ b/packages/server/__tests__/api/grants.test.js
@@ -1005,6 +1005,34 @@ HHS-2021-IHS-TPI-0001,Community Health Aide Program: Tribal Planning &`;
});
});
});
+ context('DELETE /:grantId/notes/user/:userId', () => {
+ const GRANT_ID = '335255';
+
+ let staffGrantNote;
+
+ beforeEach(async () => {
+ [staffGrantNote] = await knex('grant_notes')
+ .insert({ grant_id: GRANT_ID, user_id: staffUser.id }, 'id');
+
+ await knex('grant_notes_revisions')
+ .insert({ grant_note_id: staffGrantNote.id, text: 'Test note 1.' });
+
+ await knex('grant_notes_revisions')
+ .insert({ grant_note_id: staffGrantNote.id, text: 'Test note 2.' });
+ });
+
+ it('Deletes all notes for user', async () => {
+ const revisions = await knex('grant_notes_revisions').where('grant_note_id', staffGrantNote.id);
+ expect(revisions).to.have.length(2);
+
+ const resp = await fetchApi(`/grants/${GRANT_ID}/notes/user/${staffUser.id}`, agencies.own, {
+ ...fetchOptions.staff,
+ method: 'delete',
+ });
+
+ expect(resp.statusText).to.equal('OK');
+ });
+ });
context('GET /:grantId/followers', () => {
const GRANT_ID = 335255;
diff --git a/packages/server/__tests__/lib/grants-collaboration.test.js b/packages/server/__tests__/lib/grants-collaboration.test.js
index 53435e81c..d9b24fed5 100644
--- a/packages/server/__tests__/lib/grants-collaboration.test.js
+++ b/packages/server/__tests__/lib/grants-collaboration.test.js
@@ -3,7 +3,9 @@ const chaiAsPromised = require('chai-as-promised');
const { DateTime } = require('luxon');
const knex = require('../../src/db/connection');
const fixtures = require('../db/seeds/fixtures');
-const { saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser } = require('../../src/lib/grantsCollaboration/notes');
+const {
+ saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser, deleteGrantNotesByUser,
+} = require('../../src/lib/grantsCollaboration/notes');
const {
followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
} = require('../../src/lib/grantsCollaboration/followers');
@@ -12,16 +14,42 @@ use(chaiAsPromised);
describe('Grants Collaboration', () => {
context('saveNoteRevision', () => {
+ const { adminUser } = fixtures.users;
+ const grant = fixtures.grants.earFellowship;
+
it('creates new note', async () => {
- const result = await saveNoteRevision(knex, fixtures.grants.earFellowship.grant_id, fixtures.roles.adminRole.id, 'This is a test revision');
+ const result = await saveNoteRevision(knex, grant.grant_id, adminUser.id, 'This is a test revision');
expect(result.id).to.be.ok;
+
+ const [grantNote] = await knex('grant_notes').where({
+ grant_id: grant.grant_id,
+ user_id: adminUser.id,
+ }).returning(['is_published']);
+
+ console.log(grantNote);
+ expect(grantNote.is_published).to.equal(true);
});
it('creates new note revision', async () => {
- const result1 = await saveNoteRevision(knex, fixtures.grants.earFellowship.grant_id, fixtures.roles.adminRole.id, 'This is a test revision');
- const result2 = await saveNoteRevision(knex, fixtures.grants.earFellowship.grant_id, fixtures.roles.adminRole.id, 'This is a test revision #2');
+ const result1 = await saveNoteRevision(knex, grant.grant_id, adminUser.id, 'This is a test revision');
+ const result2 = await saveNoteRevision(knex, grant.grant_id, adminUser.id, 'This is a test revision #2');
expect(result2.id).not.to.equal(result1.id);
});
+ it('Re-publishes old notes upon new revision', async () => {
+ const expectedNote = {
+ grant_id: grant.grant_id,
+ user_id: adminUser.id,
+ };
+
+ await saveNoteRevision(knex, grant.grant_id, adminUser.id, 'This is a test revision');
+
+ await knex('grant_notes').where(expectedNote).update({ is_published: false });
+
+ await saveNoteRevision(knex, grant.grant_id, adminUser.id, 'This is a test revision #2');
+
+ const [note] = await knex('grant_notes').select('is_published').where(expectedNote);
+ expect(note.is_published).to.equal(true);
+ });
});
context('getOrganizationNotesByUser', () => {
@@ -68,6 +96,18 @@ describe('Grants Collaboration', () => {
expect(results.notes).to.have.lengthOf(0);
});
+ it('ignores unpublished notes', async () => {
+ await knex('grant_notes').where({ grant_id: grant.grant_id }).update({ is_published: false });
+
+ const results = await getOrganizationNotesForGrantByUser(
+ knex,
+ tenant.id, // organization ID
+ staffUser.id, // user ID
+ grant.grant_id, // grant ID
+ );
+
+ expect(results.notes).to.have.length(0);
+ });
});
context('getOrganizationNotesForGrant', () => {
@@ -80,17 +120,23 @@ describe('Grants Collaboration', () => {
let adminLastRevision;
beforeEach(async () => {
- const [staffGrantNote] = await knex('grant_notes')
- .insert({ grant_id: grant.grant_id, user_id: staffUser.id }, 'id');
+ await knex.transaction(async (trx) => {
+ const [staffGrantNote] = await trx('grant_notes')
+ .insert({ grant_id: grant.grant_id, user_id: staffUser.id }, 'id');
- [staffLastRevision] = await knex('grant_notes_revisions')
- .insert({ grant_note_id: staffGrantNote.id, text: 'This is a staff note' }, 'id');
+ [staffLastRevision] = await trx('grant_notes_revisions')
+ .insert({ grant_note_id: staffGrantNote.id, text: 'This is a staff note' }, 'id');
+ });
- const [adminGrantNote] = await knex('grant_notes')
- .insert({ grant_id: grant.grant_id, user_id: adminUser.id }, 'id');
+ let adminGrantNote;
- await knex('grant_notes_revisions')
- .insert({ grant_note_id: adminGrantNote.id, text: 'This is a test revision' }, 'id');
+ await knex.transaction(async (trx) => {
+ [adminGrantNote] = await trx('grant_notes')
+ .insert({ grant_id: grant.grant_id, user_id: adminUser.id }, 'id');
+
+ await trx('grant_notes_revisions')
+ .insert({ grant_note_id: adminGrantNote.id, text: 'This is a test revision' }, 'id');
+ });
[adminLastRevision] = await knex('grant_notes_revisions')
.insert({ grant_note_id: adminGrantNote.id, text: 'This is a test revision #2' }, 'id');
@@ -136,6 +182,14 @@ describe('Grants Collaboration', () => {
expect(result.pagination.next).to.be.null;
});
+ it('ignores unpublished notes', async () => {
+ await knex('grant_notes').where({ grant_id: grant.grant_id }).update({ is_published: false });
+
+ const result = await getOrganizationNotesForGrant(knex, grant.grant_id, tenant.id);
+
+ expect(result.notes).to.have.length(0);
+ });
+
it('gets filtered notes for grant using cursor (pagination)', async () => {
const results = await getOrganizationNotesForGrant(
knex,
@@ -165,6 +219,35 @@ describe('Grants Collaboration', () => {
expect(results.pagination.next).equal(adminLastRevision.id);
});
});
+ context('deleteGrantNotesByUser', () => {
+ const { staffUser } = fixtures.users;
+ const grant = fixtures.grants.earFellowship;
+
+ let staffGrantNote;
+
+ beforeEach(async () => {
+ [staffGrantNote] = await knex('grant_notes')
+ .insert({ grant_id: grant.grant_id, user_id: staffUser.id }, 'id');
+
+ await knex('grant_notes_revisions')
+ .insert({ grant_note_id: staffGrantNote.id, text: 'This is a staff note' });
+
+ await knex('grant_notes_revisions')
+ .insert({ grant_note_id: staffGrantNote.id, text: 'This is a staff note revision' });
+ });
+
+ it('Deletes (marks unpublished) all notes for user', async () => {
+ const revisions = await knex('grant_notes_revisions').where('grant_note_id', staffGrantNote.id);
+ expect(revisions).to.have.length(2);
+
+ await deleteGrantNotesByUser(knex, grant.grant_id, staffUser.id);
+
+ const notesAfter = await knex('grant_notes')
+ .where('id', staffGrantNote.id)
+ .andWhere('is_published', true);
+ expect(notesAfter).to.have.length(0);
+ });
+ });
context('followGrant', () => {
it('follows a grant', async () => {
await followGrant(knex, fixtures.grants.earFellowship.grant_id, fixtures.users.adminUser.id);
diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js
index 57e1027c5..b49417a02 100644
--- a/packages/server/knexfile.js
+++ b/packages/server/knexfile.js
@@ -111,4 +111,9 @@ module.exports = {
tableName: 'migrations',
},
},
+ // Set trigger for any updated timestamps
+ onUpdateTrigger: (table) => `
+ CREATE TRIGGER ${table}_updated_at BEFORE UPDATE ON ${table}
+ FOR EACH ROW EXECUTE PROCEDURE before_update_set_updated_at();
+ `,
};
diff --git a/packages/server/migrations/20241004230649_add_grant_activity_to_email_subscriptions_notification_type.js b/packages/server/migrations/20241004230649_add_grant_activity_to_email_subscriptions_notification_type.js
index fd64ffee7..e1b453dfe 100644
--- a/packages/server/migrations/20241004230649_add_grant_activity_to_email_subscriptions_notification_type.js
+++ b/packages/server/migrations/20241004230649_add_grant_activity_to_email_subscriptions_notification_type.js
@@ -17,7 +17,8 @@ exports.up = function (knex) {
* @param { import("knex").Knex } knex
* @returns { Promise }
*/
-exports.down = function (knex) {
+exports.down = async function (knex) {
+ await knex('email_subscriptions').where({ notification_type: 'GRANT_ACTIVITY' }).del();
return knex.schema.raw(
`
-- Drop the existing stale constraint
diff --git a/packages/server/migrations/20241011165653_cascade-delete-grant-notes.js b/packages/server/migrations/20241011165653_cascade-delete-grant-notes.js
new file mode 100644
index 000000000..eaeead0ec
--- /dev/null
+++ b/packages/server/migrations/20241011165653_cascade-delete-grant-notes.js
@@ -0,0 +1,47 @@
+const { onUpdateTrigger } = require('../knexfile');
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+exports.up = async function (knex) {
+ const ON_UPDATE_TIMESTAMP_FUNCTION = `
+ CREATE OR REPLACE FUNCTION before_update_set_updated_at()
+ RETURNS trigger AS $$
+ BEGIN
+ IF NEW IS DISTINCT FROM OLD THEN
+ NEW.updated_at = now();
+ END IF;
+ RETURN NEW;
+ END;
+ $$ language 'plpgsql';
+ `;
+
+ await knex.schema.table('grant_notes_revisions', (table) => {
+ table.dropForeign('grant_note_id');
+ table.foreign('grant_note_id').references('grant_notes.id').onDelete('CASCADE');
+ });
+ await knex.raw(ON_UPDATE_TIMESTAMP_FUNCTION);
+ await knex.schema.table('grant_notes', (table) => {
+ table.boolean('is_published').notNullable().defaultTo(true);
+ table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
+ });
+ await knex.raw(onUpdateTrigger('grant_notes'));
+};
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+exports.down = async function (knex) {
+ await knex.schema.table('grant_notes_revisions', (table) => {
+ table.dropForeign('grant_note_id');
+ table.foreign('grant_note_id').references('grant_notes.id');
+ });
+ await knex.schema.table('grant_notes', (table) => {
+ table.dropColumn('is_published');
+ table.dropColumn('updated_at');
+ });
+ await knex.raw('DROP TRIGGER IF EXISTS grant_notes_updated_at ON grant_notes');
+ await knex.raw('DROP FUNCTION IF EXISTS before_update_set_updated_at');
+};
diff --git a/packages/server/src/lib/grantsCollaboration/index.js b/packages/server/src/lib/grantsCollaboration/index.js
index 21bd4cd09..3d4ed8583 100644
--- a/packages/server/src/lib/grantsCollaboration/index.js
+++ b/packages/server/src/lib/grantsCollaboration/index.js
@@ -1,8 +1,10 @@
-const { saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser } = require('./notes');
+const {
+ saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser, deleteGrantNotesByUser,
+} = require('./notes');
const {
followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
} = require('./followers');
module.exports = {
- saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
+ saveNoteRevision, getOrganizationNotesForGrant, getOrganizationNotesForGrantByUser, deleteGrantNotesByUser, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant,
};
diff --git a/packages/server/src/lib/grantsCollaboration/notes.js b/packages/server/src/lib/grantsCollaboration/notes.js
index ca8ce2044..f34ad453f 100644
--- a/packages/server/src/lib/grantsCollaboration/notes.js
+++ b/packages/server/src/lib/grantsCollaboration/notes.js
@@ -3,12 +3,18 @@ async function saveNoteRevision(knex, grantId, userId, text) {
const grantNotesRevisionId = await knex.transaction(async (trx) => {
const existingNote = await trx('grant_notes')
- .select('id')
+ .select(['id', 'is_published'])
.where({ grant_id: grantId, user_id: userId })
.first();
if (existingNote) {
grantNoteId = existingNote.id;
+
+ if (!existingNote.is_published) {
+ await trx('grant_notes')
+ .where({ id: grantNoteId })
+ .update({ is_published: true });
+ }
} else {
const [newNoteId] = await trx('grant_notes')
.insert({
@@ -20,10 +26,7 @@ async function saveNoteRevision(knex, grantId, userId, text) {
}
const [revisionId] = await trx('grant_notes_revisions')
- .insert({
- grant_note_id: grantNoteId,
- text,
- })
+ .insert({ grant_note_id: grantNoteId, text })
.returning('id');
return revisionId;
@@ -37,28 +40,22 @@ async function getCurrentNoteRevisions(
{ grantId, organizationId, userId } = {},
{ cursor, limit = 50 } = {},
) {
- const recentRevsQuery = knex
- .select('r.*')
- .from({ r: 'grant_notes_revisions' })
- .whereRaw('r.grant_note_id = grant_notes.id')
- .orderBy('r.created_at', 'desc')
- .limit(2);
-
const revQuery = knex
- .select(knex.raw(`recent_revs.*, count(recent_revs.*) OVER() > 1 as is_revised`))
- .fromRaw(`(${recentRevsQuery.toQuery()}) as recent_revs`)
- .orderBy('recent_revs.created_at', 'desc')
+ .select(['rev.id', 'rev.grant_note_id', 'rev.created_at', 'rev.text'])
+ .from({ rev: 'grant_notes_revisions' })
+ .whereRaw('rev.grant_note_id = grant_notes.id')
+ .orderBy('rev.created_at', 'desc')
.limit(1);
let query = knex('grant_notes')
.select([
'grant_notes.id',
'grant_notes.created_at',
+ 'grant_notes.updated_at as updated_at',
'grant_notes.grant_id',
'rev.id as latest_revision_id',
'rev.created_at as revised_at',
'rev.text',
- 'rev.is_revised as is_revised',
'users.id as user_id',
'users.name as user_name',
'users.email as user_email',
@@ -68,14 +65,15 @@ async function getCurrentNoteRevisions(
'agencies.id as team_id',
'agencies.name as team_name',
])
- .joinRaw(`LEFT JOIN LATERAL (${revQuery.toQuery()}) AS rev ON rev.grant_note_id = grant_notes.id`)
+ .joinRaw(`LEFT JOIN LATERAL (${revQuery.toString()}) AS rev ON rev.grant_note_id = grant_notes.id`)
.join('users', 'users.id', 'grant_notes.user_id')
.join('agencies', 'agencies.id', 'users.agency_id')
- .join('tenants', 'tenants.id', 'users.tenant_id');
+ .join('tenants', 'tenants.id', 'users.tenant_id')
+ .where('grant_notes.is_published', true);
// Conditionally applying filters based on grantID if it is null or undefined or not
if (grantId !== null && grantId !== undefined) {
- query = query.where('grant_notes.grant_id', grantId);
+ query = query.andWhere('grant_notes.grant_id', grantId);
}
// Conditionally applying filters based on organizationID if it is null or undefined or not
@@ -104,7 +102,7 @@ async function getCurrentNoteRevisions(
notes: notes.map((note) => ({
id: note.latest_revision_id,
createdAt: note.revised_at,
- isRevised: note.is_revised,
+ isRevised: note.revised_at > note.updated_at,
text: note.text,
grant: { id: note.grant_id },
user: {
@@ -147,9 +145,16 @@ async function getOrganizationNotesForGrant(
return getCurrentNoteRevisions(knex, { grantId, organizationId }, { cursor, limit });
}
+async function deleteGrantNotesByUser(knex, grantId, userId) {
+ await knex('grant_notes')
+ .where({ grant_id: grantId, user_id: userId })
+ .update({ is_published: false });
+}
+
module.exports = {
saveNoteRevision,
getCurrentNoteRevisions,
getOrganizationNotesForGrant,
getOrganizationNotesForGrantByUser,
+ deleteGrantNotesByUser,
};
diff --git a/packages/server/src/routes/grants.js b/packages/server/src/routes/grants.js
index 7a04765b5..d7577e00e 100755
--- a/packages/server/src/routes/grants.js
+++ b/packages/server/src/routes/grants.js
@@ -7,7 +7,7 @@ const { requireUser, isUserAuthorized } = require('../lib/access-helpers');
const knex = require('../db/connection');
const {
saveNoteRevision, followGrant, unfollowGrant, getFollowerForGrant, getFollowersForGrant, getOrganizationNotesForGrant,
- getOrganizationNotesForGrantByUser,
+ getOrganizationNotesForGrantByUser, deleteGrantNotesByUser,
} = require('../lib/grantsCollaboration');
const router = express.Router({ mergeParams: true });
@@ -46,38 +46,6 @@ router.get('/', requireUser, async (req, res) => {
res.json(grants);
});
-// getting notes for a specific user and grant
-router.get('/:grantId/notes/user/:userId', requireUser, async (req, res) => {
- const { grantId, userId } = req.params;
- const { tenant_id: organizationId } = req.session.user;
- const { cursor, limit } = req.query;
-
- // Converting limit to an integer
- const limitInt = limit ? parseInt(limit, 10) : undefined;
-
- // Validating the limit query parameter
- if (limit && (!Number.isInteger(limitInt) || limitInt < 1 || limitInt > 100)) {
- res.status(400).send('Invalid limit parameter');
- return;
- }
-
- try {
- // Fetching the notes using getOrganizationNotesForGrantByUser function
- const notes = await getOrganizationNotesForGrantByUser(
- knex,
- organizationId,
- userId,
- grantId,
- { cursor, limit: limitInt },
- );
-
- // sending the notes as JSON response
- res.json(notes);
- } catch (error) {
- res.status(500).json({ error: 'Failed to retrieve notes' });
- }
-});
-
function criteriaToFiltersObj(criteria, agencyId) {
const filters = criteria || {};
const postedWithinOptions = {
@@ -515,6 +483,34 @@ router.get('/:grantId/notes', requireUser, async (req, res) => {
res.json(rows);
});
+// getting notes for a specific user and grant
+router.get('/:grantId/notes/user/:userId', requireUser, async (req, res) => {
+ const { grantId, userId } = req.params;
+ const { tenant_id: organizationId } = req.session.user;
+ const { cursor, limit } = req.query;
+
+ // Converting limit to an integer
+ const limitInt = limit ? parseInt(limit, 10) : undefined;
+
+ // Validating the limit query parameter
+ if (limit && (!Number.isInteger(limitInt) || limitInt < 1 || limitInt > 100)) {
+ res.status(400).send('Invalid limit parameter');
+ return;
+ }
+
+ // Fetching the notes using getOrganizationNotesForGrantByUser function
+ const notes = await getOrganizationNotesForGrantByUser(
+ knex,
+ organizationId,
+ userId,
+ grantId,
+ { cursor, limit: limitInt },
+ );
+
+ // sending the notes as JSON response
+ res.json(notes);
+});
+
router.put('/:grantId/notes/revision', requireUser, async (req, res) => {
const { grantId } = req.params;
const { user } = req.session;
@@ -543,6 +539,14 @@ router.put('/:grantId/notes/revision', requireUser, async (req, res) => {
}
});
+router.delete('/:grantId/notes/user/:userId', requireUser, async (req, res) => {
+ const { user } = req.session;
+ const { grantId } = req.params;
+
+ await deleteGrantNotesByUser(knex, grantId, user.id);
+ res.json({});
+});
+
router.get('/:grantId/followers', requireUser, async (req, res) => {
const { grantId } = req.params;
const { user } = req.session;