Skip to content

Commit

Permalink
Merge branch 'main' into ms-hide-forecasted-grants
Browse files Browse the repository at this point in the history
  • Loading branch information
lsr-explore authored Oct 18, 2024
2 parents a27d07b + 60042d3 commit 34fa36a
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 104 deletions.
70 changes: 68 additions & 2 deletions packages/client/src/components/GrantNotes.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: '<textarea />',
props: { modelValue: String },
methods: { focus: vi.fn() },
};

beforeEach(() => {
vitest.clearAllMocks();
});
Expand Down Expand Up @@ -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);

Expand Down
80 changes: 60 additions & 20 deletions packages/client/src/components/GrantNotes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<!-- Note Edit -->
<div
v-if="editingNote"
data-test-edit-form
class="d-flex note-edit-container"
>
<UserAvatar
Expand All @@ -19,7 +20,7 @@
rows="2"
max-rows="8"
:formatter="formatter"
:disabled="submittingNote"
:disabled="savingNote"
data-test-note-input
@keydown="handleKeyDown"
/>
Expand Down Expand Up @@ -65,17 +66,38 @@
>
{{ userNote.text }}
<template #actions>
<b-button
class="note-edit-btn p-0"
<b-dropdown
right
variant="link"
@click="toggleEditNote"
toggle-class="p-0"
no-caret
:disabled="savingNote"
>
<b-icon
icon="pencil-square"
class="mr-1"
/>
<span>EDIT</span>
</b-button>
<template #button-content>
<span class="note-edit-btn">
<b-icon
icon="pencil-square"
class="mr-1"
/>
<span class="note-edit-btn-text">EDIT</span>
</span>
</template>
<b-dropdown-item-button
title="Edit note"
data-test-edit-note-btn
@click="toggleEditNote"
>
Edit
</b-dropdown-item-button>
<b-dropdown-item-button
title="Delete note"
variant="danger"
data-test-delete-note-btn
@click="deleteUserNote"
>
Delete
</b-dropdown-item-button>
</b-dropdown>
</template>
</UserActivityItem>

Expand Down Expand Up @@ -137,7 +159,7 @@ export default {
otherNotes: [],
userNote: null,
noteText: '',
submittingNote: false,
savingNote: false,
editingNote: false,
notesNextCursor: null,
};
Expand All @@ -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' : '';
Expand All @@ -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();
Expand All @@ -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 });
Expand All @@ -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 = {
Expand All @@ -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;
},
},
};
</script>
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions packages/client/src/store/modules/grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
43 changes: 43 additions & 0 deletions packages/server/__tests__/api/grants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,12 @@ describe('`/api/grants` endpoint', () => {
method: 'put',
body: JSON.stringify({ interestedCode: 1 }),
});

expect(response.statusText).to.equal('OK');
// Fetch and check the interested agencies list
const interestedResponse = await fetchApi(`/grants/${interestEndpoint}`, agencies.own, fetchOptions.admin);
const interestedAgencies = await interestedResponse.json();
expect(interestedAgencies.some((agency) => agency.agency_id === agencies.own)).to.be.true;
});
it('records this user\'s own agency\'s subagencies\' interest in a grant', async () => {
const response = await fetchApi(`/grants/${interestEndpoint}/${agencies.ownSub}`, agencies.ownSub, {
Expand All @@ -500,6 +505,11 @@ describe('`/api/grants` endpoint', () => {
body: JSON.stringify({ interestedCode: 1 }),
});
expect(response.statusText).to.equal('OK');

// Fetch and check the interested agencies list
const interestedResponse = await fetchApi(`/grants/${interestEndpoint}`, agencies.ownSub, fetchOptions.admin);
const interestedAgencies = await interestedResponse.json();
expect(interestedAgencies.some((agency) => agency.agency_id === agencies.ownSub)).to.be.true;
});
it('forbids requests for any agency outside this user\'s hierarchy', async () => {
const response = await fetchApi(`/grants/${interestEndpoint}/${agencies.offLimits}`, agencies.offLimits, {
Expand All @@ -519,6 +529,11 @@ describe('`/api/grants` endpoint', () => {
body: JSON.stringify({ interestedCode: 1 }),
});
expect(response.statusText).to.equal('OK');

// Fetch and check the interested agencies list
const interestedResponse = await fetchApi(`/grants/${interestEndpoint}`, agencies.own, fetchOptions.staff);
const interestedAgencies = await interestedResponse.json();
expect(interestedAgencies.some((agency) => agency.agency_id === agencies.own)).to.be.true;
});
it('forbids requests for any agency except this user\'s own agency', async () => {
let response = await fetchApi(`/grants/${interestEndpoint}/${agencies.ownSub}`, agencies.ownSub, {
Expand Down Expand Up @@ -990,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;

Expand Down
Loading

0 comments on commit 34fa36a

Please sign in to comment.