From c0dbf7fa23dfc57f3c93f26f2077b84d2bb0f07c Mon Sep 17 00:00:00 2001 From: greg-adams Date: Wed, 4 Sep 2024 16:27:28 -0700 Subject: [PATCH 1/6] add grant activity card --- packages/client/public/deploy-config.js | 1 + packages/client/src/helpers/dates.js | 4 + .../client/src/helpers/featureFlags/index.js | 4 + packages/client/src/store/modules/grants.js | 35 ++++ packages/client/src/views/GrantActivity.vue | 162 ++++++++++++++++++ .../client/src/views/GrantDetailsView.vue | 141 ++------------- packages/client/src/views/ShareGrant.vue | 154 +++++++++++++++++ 7 files changed, 373 insertions(+), 128 deletions(-) create mode 100644 packages/client/src/views/GrantActivity.vue create mode 100644 packages/client/src/views/ShareGrant.vue diff --git a/packages/client/public/deploy-config.js b/packages/client/public/deploy-config.js index 83ff64e09..6b2099679 100644 --- a/packages/client/public/deploy-config.js +++ b/packages/client/public/deploy-config.js @@ -23,6 +23,7 @@ window.APP_CONFIG.featureFlags = { newTerminologyEnabled: true, newGrantsDetailPageEnabled: true, shareTerminologyEnabled: true, + followNotesEnabled: true, }; // Setting a GOOGLE_TAG_ID enables Google Analytics. diff --git a/packages/client/src/helpers/dates.js b/packages/client/src/helpers/dates.js index 0ed34d6f1..efa6f91de 100644 --- a/packages/client/src/helpers/dates.js +++ b/packages/client/src/helpers/dates.js @@ -8,3 +8,7 @@ export function daysUntil(dateString) { const daysDiff = DateTime.fromISO(dateString).endOf('day').diffNow('days').days; return daysDiff < 0 ? 0 : Math.floor(daysDiff); } + +export const formatDate = (dateString) => DateTime.fromISO(dateString).toLocaleString(DateTime.DATE_MED); + +export const formatDateTime = (dateString) => DateTime.fromISO(dateString).toLocaleString(DateTime.DATETIME_MED); diff --git a/packages/client/src/helpers/featureFlags/index.js b/packages/client/src/helpers/featureFlags/index.js index 62fd2e074..7fa1d4655 100644 --- a/packages/client/src/helpers/featureFlags/index.js +++ b/packages/client/src/helpers/featureFlags/index.js @@ -11,3 +11,7 @@ export function newGrantsDetailPageEnabled() { export function shareTerminologyEnabled() { return getFeatureFlags().shareTerminologyEnabled === true; } + +export function followNotesEnabled() { + return getFeatureFlags().followNotesEnabled === true; +} diff --git a/packages/client/src/store/modules/grants.js b/packages/client/src/store/modules/grants.js index 1577b3b3d..1ba293744 100644 --- a/packages/client/src/store/modules/grants.js +++ b/packages/client/src/store/modules/grants.js @@ -212,6 +212,41 @@ export default { fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/interested-codes`) .then((data) => commit('SET_INTERESTED_CODES', data)); }, + async getFollowerForGrant({ rootGetters }, { grantId }) { + try { + return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); + } catch (e) { + return null; + } + }, + async getFollowersForGrant({ rootGetters }, { grantId }) { + try { + return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/followers?limit=51`); + } catch (e) { + return null; + } + }, + async getNotesForGrant({ rootGetters }, { grantId }) { + try { + return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/notes?limit=51`); + } catch (e) { + return null; + } + }, + async followGrantForCurrentUser({ rootGetters }, { grantId }) { + try { + await fetchApi.put(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); + } catch (e) { + // fail silently + } + }, + async unfollowGrantForCurrentUser({ rootGetters }, { grantId }) { + try { + await fetchApi.deleteRequest(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); + } catch (e) { + // fail silently + } + }, async setEligibilityCodeEnabled({ rootGetters }, { code, enabled }) { await fetchApi.put(`/api/organizations/${rootGetters['users/selectedAgencyId']}/eligibility-codes/${code}/enable/${enabled}`); }, diff --git a/packages/client/src/views/GrantActivity.vue b/packages/client/src/views/GrantActivity.vue new file mode 100644 index 000000000..3efe9dc49 --- /dev/null +++ b/packages/client/src/views/GrantActivity.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/packages/client/src/views/GrantDetailsView.vue b/packages/client/src/views/GrantDetailsView.vue index 300a475be..77b4bad37 100644 --- a/packages/client/src/views/GrantDetailsView.vue +++ b/packages/client/src/views/GrantDetailsView.vue @@ -123,79 +123,11 @@
- - -
-

- {{ shareTerminologyEnabled ? 'Share Grant' : 'Assign Grant' }} -

- - - -
-
@@ -269,13 +201,15 @@ import { mapActions, mapGetters } from 'vuex'; import { datadogRum } from '@datadog/browser-rum'; import { debounce } from 'lodash'; -import { DateTime } from 'luxon'; -import { newTerminologyEnabled, shareTerminologyEnabled } from '@/helpers/featureFlags'; +import { newTerminologyEnabled, shareTerminologyEnabled, followNotesEnabled } from '@/helpers/featureFlags'; import { formatCurrency } from '@/helpers/currency'; +import { formatDate, formatDateTime } from '@/helpers/dates'; import { titleize } from '@/helpers/form-helpers'; import { gtagEvent } from '@/helpers/gtag'; import UserAvatar from '@/components/UserAvatar.vue'; import CopyButton from '@/components/CopyButton.vue'; +import ShareGrant from '@/views/ShareGrant.vue'; +import GrantActivity from '@/views/GrantActivity.vue'; const HEADER = '__HEADER__'; const FAR_FUTURE_CLOSE_DATE = '2100-01-01'; @@ -285,6 +219,8 @@ export default { components: { UserAvatar, CopyButton, + ShareGrant, + GrantActivity, }, beforeRouteEnter(to, from, next) { const isFirstPageLoad = from.name === null && from.path === '/'; @@ -298,24 +234,6 @@ export default { return { isFirstPageLoad: false, orderBy: '', - assignedAgenciesFields: [ - { - key: 'name', - }, - { - key: 'abbreviation', - label: 'Abbreviation', - }, - { - key: 'created_at', - }, - { - key: 'actions', - label: 'Actions', - }, - ], - assignedAgencies: [], - selectedAgencyToAssign: null, selectedInterestedCode: null, searchInput: null, debouncedSearchInput: null, @@ -418,11 +336,7 @@ export default { }, newTerminologyEnabled, shareTerminologyEnabled, - unassignedAgencies() { - return this.agencies.filter( - (agency) => !this.assignedAgencies.map((assigned) => assigned.id).includes(agency.id), - ); - }, + followNotesEnabled, statusSubmitButtonDisabled() { return this.selectedInterestedCode === null; }, @@ -438,7 +352,6 @@ export default { console.log(e); } } - this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.currentGrant.grant_id }); } }, }, @@ -463,14 +376,13 @@ export default { markGrantAsInterestedAction: 'grants/markGrantAsInterested', unmarkGrantAsInterestedAction: 'grants/unmarkGrantAsInterested', getInterestedAgencies: 'grants/getInterestedAgencies', - getGrantAssignedAgencies: 'grants/getGrantAssignedAgencies', - assignAgenciesToGrantAction: 'grants/assignAgenciesToGrant', - unassignAgenciesToGrantAction: 'grants/unassignAgenciesToGrant', fetchAgencies: 'agencies/fetchAgencies', fetchGrantDetails: 'grants/fetchGrantDetails', }), titleize, formatCurrency, + formatDate, + formatDateTime, debounceSearchInput: debounce(function bounce(newVal) { this.debouncedSearchInput = newVal; }, 500), @@ -511,27 +423,6 @@ export default { gtagEvent(eventName); datadogRum.addAction(eventName); }, - async assignAgencyToGrant(agency) { - await this.assignAgenciesToGrantAction({ - grantId: this.currentGrant.grant_id, - agencyIds: [agency.id], - }); - this.selectedAgencyToAssign = null; - this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.currentGrant.grant_id }); - const eventName = 'assign team to grant'; - gtagEvent(eventName); - datadogRum.addAction(eventName); - }, - async unassignAgencyToGrant(agency) { - await this.unassignAgenciesToGrantAction({ - grantId: this.currentGrant.grant_id, - agencyIds: [agency.id], - }); - this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.currentGrant.grant_id }); - const eventName = 'remove team assignment from grant'; - gtagEvent(eventName); - datadogRum.addAction(eventName); - }, isAbleToUnmark(agencyId) { return this.agencies.some((agency) => agency.id === agencyId); }, @@ -559,12 +450,6 @@ export default { // load in a new tab. If it opened in the same tab, it would open the new URL before the event is logged. gtagEvent('grants.gov btn clicked'); }, - formatDate(dateString) { - return DateTime.fromISO(dateString).toLocaleString(DateTime.DATE_MED); - }, - formatDateTime(dateString) { - return DateTime.fromISO(dateString).toLocaleString(DateTime.DATETIME_MED); - }, selectableOption(option) { return option.status_code !== HEADER; }, diff --git a/packages/client/src/views/ShareGrant.vue b/packages/client/src/views/ShareGrant.vue new file mode 100644 index 000000000..234bbcf21 --- /dev/null +++ b/packages/client/src/views/ShareGrant.vue @@ -0,0 +1,154 @@ + + + + + + From bdef14e5fe557120ecf400ce0b6172766ddbe9b8 Mon Sep 17 00:00:00 2001 From: greg-adams Date: Wed, 4 Sep 2024 16:27:43 -0700 Subject: [PATCH 2/6] add tests --- .../client/src/views/GrantActivity.spec.js | 108 ++++++++++++++++++ packages/client/src/views/ShareGrant.spec.js | 41 +++++++ 2 files changed, 149 insertions(+) create mode 100644 packages/client/src/views/GrantActivity.spec.js create mode 100644 packages/client/src/views/ShareGrant.spec.js diff --git a/packages/client/src/views/GrantActivity.spec.js b/packages/client/src/views/GrantActivity.spec.js new file mode 100644 index 000000000..cc2577038 --- /dev/null +++ b/packages/client/src/views/GrantActivity.spec.js @@ -0,0 +1,108 @@ +import { + describe, it, expect, vi, beforeEach, +} from 'vitest'; +import { shallowMount, flushPromises } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import GrantActivity from '@/views/GrantActivity.vue'; + +const mockFollowers = { + followers: Array.from(Array(20), (item, i) => ({ + id: i, + user: { + id: i + 100, + name: `Follower ${i}`, + }, + })), +}; + +describe('GrantActivity', () => { + const mockStore = { + getters: { + 'users/loggedInUser': () => null, + 'grants/currentGrant': () => ({ + grant_id: 55, + }), + }, + actions: { + 'grants/getFollowerForGrant': vi.fn(), + 'grants/getFollowersForGrant': vi.fn(), + 'grants/getNotesForGrant': vi.fn(), + 'grants/followGrantForCurrentUser': vi.fn(), + 'grants/unfollowGrantForCurrentUser': vi.fn(), + }, + }; + + const store = createStore(mockStore); + + beforeEach(() => { + vi.resetAllMocks(); + }); + + const buttonSelector = '[data-follow-btn]'; + const followSummarySelector = '[data-follow-summary]'; + + it('renders', () => { + const wrapper = shallowMount(GrantActivity, { + global: { + plugins: [store], + }, + }); + expect(wrapper.exists()).toBe(true); + }); + + it('Correctly displays followers summary', async () => { + mockStore.actions['grants/getFollowersForGrant'].mockResolvedValue(mockFollowers); + + const wrapper = shallowMount(GrantActivity, { + global: { + plugins: [store], + }, + }); + + await flushPromises(); + + const summary = wrapper.find(followSummarySelector); + + expect(summary.text()).equal('Followed by Follower 0 and 19 others'); + }); + + it('Correctly follows grants when not followed by user', async () => { + mockStore.actions['grants/getFollowerForGrant'].mockResolvedValue(null); + + const wrapper = shallowMount(GrantActivity, { + global: { + plugins: [store], + }, + }); + + await flushPromises(); + + const btn = wrapper.find(buttonSelector); + + expect(btn.text()).equal('Follow'); + + btn.trigger('click'); + expect(mockStore.actions['grants/followGrantForCurrentUser']).toHaveBeenCalled(); + }); + + it('Correctly unfollows grants when followed by user', async () => { + mockStore.actions['grants/getFollowerForGrant'].mockResolvedValue({ + id: 1, + }); + + const wrapper = shallowMount(GrantActivity, { + global: { + plugins: [store], + }, + }); + + await flushPromises(); + + const btn = wrapper.find(buttonSelector); + + expect(btn.text()).equal('Following'); + + btn.trigger('click'); + expect(mockStore.actions['grants/unfollowGrantForCurrentUser']).toHaveBeenCalled(); + }); +}); diff --git a/packages/client/src/views/ShareGrant.spec.js b/packages/client/src/views/ShareGrant.spec.js new file mode 100644 index 000000000..97ea04158 --- /dev/null +++ b/packages/client/src/views/ShareGrant.spec.js @@ -0,0 +1,41 @@ +import { + describe, it, expect, vi, beforeEach, +} from 'vitest'; +import { + shallowMount, +} from '@vue/test-utils'; +import { createStore } from 'vuex'; + +import ShareGrant from '@/views/ShareGrant.vue'; + +describe('ShareGrant', () => { + const mockStore = { + getters: { + 'agencies/agencies': () => [], + 'grants/currentGrant': () => ({ + grant_id: 55, + }), + }, + actions: { + 'grants/getGrantAssignedAgencies': vi.fn(), + 'grants/assignAgenciesToGrant': vi.fn(), + 'grants/unassignAgenciesToGrant': vi.fn(), + 'agencies/fetchAgencies': vi.fn(), + }, + }; + + const store = createStore(mockStore); + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = shallowMount(ShareGrant, { + global: { + plugins: [store], + }, + }); + expect(wrapper.exists()).toBe(true); + }); +}); From c78abd91af569aaaadb0f9696de61cd109da7f14 Mon Sep 17 00:00:00 2001 From: greg-adams Date: Thu, 5 Sep 2024 09:45:23 -0700 Subject: [PATCH 3/6] adjust styling --- packages/client/src/views/GrantActivity.vue | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/client/src/views/GrantActivity.vue b/packages/client/src/views/GrantActivity.vue index 3efe9dc49..c76478f42 100644 --- a/packages/client/src/views/GrantActivity.vue +++ b/packages/client/src/views/GrantActivity.vue @@ -4,12 +4,12 @@ footer-bg-variant="white" >
-
+
Stay up to date with this grant. - + + + {{ followBtnLabel }} @@ -159,4 +161,7 @@ export default { From 852a3c45a9ffd42cd47cae6eb78ad3a0f6f11893 Mon Sep 17 00:00:00 2001 From: greg-adams Date: Thu, 5 Sep 2024 17:34:37 -0700 Subject: [PATCH 4/6] resolve pr comments --- .../GrantActivity.spec.js | 2 +- .../{views => components}/GrantActivity.vue | 26 +++++++++---------- .../{views => components}/ShareGrant.spec.js | 2 +- .../src/{views => components}/ShareGrant.vue | 0 packages/client/src/helpers/fetchApi.js | 17 ++++++++++++ packages/client/src/store/modules/grants.js | 11 +++++--- .../client/src/views/GrantDetailsView.vue | 14 +++++++--- 7 files changed, 50 insertions(+), 22 deletions(-) rename packages/client/src/{views => components}/GrantActivity.spec.js (97%) rename packages/client/src/{views => components}/GrantActivity.vue (89%) rename packages/client/src/{views => components}/ShareGrant.spec.js (93%) rename packages/client/src/{views => components}/ShareGrant.vue (100%) diff --git a/packages/client/src/views/GrantActivity.spec.js b/packages/client/src/components/GrantActivity.spec.js similarity index 97% rename from packages/client/src/views/GrantActivity.spec.js rename to packages/client/src/components/GrantActivity.spec.js index cc2577038..0f5384048 100644 --- a/packages/client/src/views/GrantActivity.spec.js +++ b/packages/client/src/components/GrantActivity.spec.js @@ -3,7 +3,7 @@ import { } from 'vitest'; import { shallowMount, flushPromises } from '@vue/test-utils'; import { createStore } from 'vuex'; -import GrantActivity from '@/views/GrantActivity.vue'; +import GrantActivity from '@/components/GrantActivity.vue'; const mockFollowers = { followers: Array.from(Array(20), (item, i) => ({ diff --git a/packages/client/src/views/GrantActivity.vue b/packages/client/src/components/GrantActivity.vue similarity index 89% rename from packages/client/src/views/GrantActivity.vue rename to packages/client/src/components/GrantActivity.vue index c76478f42..25ca7c246 100644 --- a/packages/client/src/views/GrantActivity.vue +++ b/packages/client/src/components/GrantActivity.vue @@ -4,7 +4,7 @@ footer-bg-variant="white" > @@ -24,7 +24,7 @@ :variant="followBtnVariant" class="mb-4" data-follow-btn - :disabled="loadingFollowState" + :disabled="!followStateLoaded" @click="toggleFollowState" > @@ -64,7 +64,7 @@ export default { data() { return { userIsFollowing: false, - loadingFollowState: false, + followStateLoaded: false, followers: [], notes: [], }; @@ -81,7 +81,7 @@ export default { return this.userIsFollowing ? 'success' : 'primary'; }, followSummaryText() { - const userIsFollower = this.followers.some((follower) => follower.user.id === this.loggedInUser?.id); + const userIsFollower = this.userIsFollowing; const firstFollowerName = userIsFollower ? 'you' : this.followers[0].user.name; let otherFollowerText = ''; @@ -116,8 +116,8 @@ export default { }, async beforeMount() { this.fetchFollowState(); - this.fetchFollowersState(); - this.fetchNotes(); + this.fetchAllFollowers(); + this.fetchAllNotes(); }, methods: { ...mapActions({ @@ -130,31 +130,31 @@ export default { async fetchFollowState() { const result = await this.getFollowerForGrant({ grantId: this.currentGrant.grant_id }); this.userIsFollowing = Boolean(result); + this.followStateLoaded = true; }, - async fetchFollowersState() { - const result = await this.getFollowersForGrant({ grantId: this.currentGrant.grant_id }); + async fetchAllFollowers() { + const result = await this.getFollowersForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }); if (result) { this.followers = result.followers; } }, - async fetchNotes() { - const result = await this.getNotesForGrant({ grantId: this.currentGrant.grant_id }); + async fetchAllNotes() { + const result = await this.getNotesForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }); if (result) { this.notes = result.notes; } }, async toggleFollowState() { - this.loadingFollowState = true; + this.followStateLoaded = false; if (this.userIsFollowing) { await this.unfollowGrantForCurrentUser({ grantId: this.currentGrant.grant_id }); } else { await this.followGrantForCurrentUser({ grantId: this.currentGrant.grant_id }); } - this.fetchFollowersState(); + this.fetchAllFollowers(); await this.fetchFollowState(); - this.loadingFollowState = false; }, }, }; diff --git a/packages/client/src/views/ShareGrant.spec.js b/packages/client/src/components/ShareGrant.spec.js similarity index 93% rename from packages/client/src/views/ShareGrant.spec.js rename to packages/client/src/components/ShareGrant.spec.js index 97ea04158..905d30060 100644 --- a/packages/client/src/views/ShareGrant.spec.js +++ b/packages/client/src/components/ShareGrant.spec.js @@ -6,7 +6,7 @@ import { } from '@vue/test-utils'; import { createStore } from 'vuex'; -import ShareGrant from '@/views/ShareGrant.vue'; +import ShareGrant from '@/components/ShareGrant.vue'; describe('ShareGrant', () => { const mockStore = { diff --git a/packages/client/src/views/ShareGrant.vue b/packages/client/src/components/ShareGrant.vue similarity index 100% rename from packages/client/src/views/ShareGrant.vue rename to packages/client/src/components/ShareGrant.vue diff --git a/packages/client/src/helpers/fetchApi.js b/packages/client/src/helpers/fetchApi.js index f85b8798c..7af7f6ea1 100644 --- a/packages/client/src/helpers/fetchApi.js +++ b/packages/client/src/helpers/fetchApi.js @@ -11,6 +11,23 @@ function getDefaultHeaders() { return headers; } +/* +* Turns param object {limit: 1, paginateFrom: 2} into uri string '?limit=1&paginateFrom=2 +*/ +export const serializeQuery = (params = {}) => { + const pairs = []; + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + pairs.push( + `${encodeURIComponent(key)}=${encodeURIComponent(value)}`, + ); + } + }); + + return pairs.length ? `?${pairs.join('&')}` : ''; +}; + export function get(url) { const options = { credentials: 'include', diff --git a/packages/client/src/store/modules/grants.js b/packages/client/src/store/modules/grants.js index 1ba293744..89ebe1f91 100644 --- a/packages/client/src/store/modules/grants.js +++ b/packages/client/src/store/modules/grants.js @@ -1,5 +1,6 @@ import * as fetchApi from '@/helpers/fetchApi'; import { formatFilterDisplay } from '@/helpers/filters'; +import { serializeQuery } from '@/helpers/fetchApi'; const tableModes = { VIEW: 'view', @@ -219,16 +220,18 @@ export default { return null; } }, - async getFollowersForGrant({ rootGetters }, { grantId }) { + async getFollowersForGrant({ rootGetters }, { grantId, limit, paginateFrom }) { + const queryParams = serializeQuery({ limit, paginateFrom }); try { - return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/followers?limit=51`); + return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/followers${queryParams}`); } catch (e) { return null; } }, - async getNotesForGrant({ rootGetters }, { grantId }) { + async getNotesForGrant({ rootGetters }, { grantId, limit, paginateFrom }) { + const queryParams = serializeQuery({ limit, paginateFrom }); try { - return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/notes?limit=51`); + return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/notes${queryParams}`); } catch (e) { return null; } diff --git a/packages/client/src/views/GrantDetailsView.vue b/packages/client/src/views/GrantDetailsView.vue index 80679e5ae..9f7b2ee25 100644 --- a/packages/client/src/views/GrantDetailsView.vue +++ b/packages/client/src/views/GrantDetailsView.vue @@ -211,8 +211,8 @@ import { titleize } from '@/helpers/form-helpers'; import { gtagEvent } from '@/helpers/gtag'; import UserAvatar from '@/components/UserAvatar.vue'; import CopyButton from '@/components/CopyButton.vue'; -import ShareGrant from '@/views/ShareGrant.vue'; -import GrantActivity from '@/views/GrantActivity.vue'; +import ShareGrant from '@/components/ShareGrant.vue'; +import GrantActivity from '@/components/GrantActivity.vue'; const HEADER = '__HEADER__'; const FAR_FUTURE_CLOSE_DATE = '2100-01-01'; @@ -531,7 +531,15 @@ export default { } .grant-details-container .card { - border-radius: 8px !important; + border-radius: .5rem !important; +} +.grant-details-container .card .card-header { + border-top-left-radius: .5rem !important; + border-top-right-radius: .5rem !important; +} +.grant-details-container .card .card-footer { + border-bottom-left-radius: .5rem !important; + border-bottom-right-radius: .5rem !important; } .background { From aecdc3c44e25c33a5f186c17e181039e6479897e Mon Sep 17 00:00:00 2001 From: greg-adams Date: Fri, 6 Sep 2024 15:48:36 -0700 Subject: [PATCH 5/6] adjust loading flow --- .../client/src/components/GrantActivity.vue | 28 ++++++++++--------- packages/client/src/helpers/fetchApi.js | 16 ++++------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/client/src/components/GrantActivity.vue b/packages/client/src/components/GrantActivity.vue index 25ca7c246..23f2f998b 100644 --- a/packages/client/src/components/GrantActivity.vue +++ b/packages/client/src/components/GrantActivity.vue @@ -39,7 +39,8 @@
{{ followSummaryText }} 0; }, showNotesSummary() { @@ -116,7 +120,6 @@ export default { }, async beforeMount() { this.fetchFollowState(); - this.fetchAllFollowers(); this.fetchAllNotes(); }, methods: { @@ -128,16 +131,16 @@ export default { unfollowGrantForCurrentUser: 'grants/unfollowGrantForCurrentUser', }), async fetchFollowState() { - const result = await this.getFollowerForGrant({ grantId: this.currentGrant.grant_id }); - this.userIsFollowing = Boolean(result); - this.followStateLoaded = true; - }, - async fetchAllFollowers() { - const result = await this.getFollowersForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }); + const followCalls = [ + this.getFollowerForGrant({ grantId: this.currentGrant.grant_id }), + this.getFollowersForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }), + ]; - if (result) { - this.followers = result.followers; - } + const [userFollowsResult, followersResult] = await Promise.all(followCalls); + + this.userIsFollowing = Boolean(userFollowsResult); + this.followers = followersResult ? followersResult.followers : []; + this.followStateLoaded = true; }, async fetchAllNotes() { const result = await this.getNotesForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }); @@ -153,7 +156,6 @@ export default { } else { await this.followGrantForCurrentUser({ grantId: this.currentGrant.grant_id }); } - this.fetchAllFollowers(); await this.fetchFollowState(); }, }, diff --git a/packages/client/src/helpers/fetchApi.js b/packages/client/src/helpers/fetchApi.js index 7af7f6ea1..40f2b2298 100644 --- a/packages/client/src/helpers/fetchApi.js +++ b/packages/client/src/helpers/fetchApi.js @@ -1,4 +1,5 @@ import urlJoin from 'url-join'; +import _ from 'lodash'; export function apiURL(endpointPath) { const baseURL = (window.APP_CONFIG || {}).apiURLForGOST || '/'; @@ -13,19 +14,12 @@ function getDefaultHeaders() { /* * Turns param object {limit: 1, paginateFrom: 2} into uri string '?limit=1&paginateFrom=2 +* Removes emtpy values */ export const serializeQuery = (params = {}) => { - const pairs = []; - - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined) { - pairs.push( - `${encodeURIComponent(key)}=${encodeURIComponent(value)}`, - ); - } - }); - - return pairs.length ? `?${pairs.join('&')}` : ''; + const cleaned = _.omitBy(params, (val) => val === null || val === undefined); + const searchParams = new URLSearchParams(cleaned); + return searchParams.size ? `?${searchParams.toString()}` : ''; }; export function get(url) { From 2609f54fd0610ab4189dd30d99427cdf11222f5b Mon Sep 17 00:00:00 2001 From: greg-adams Date: Mon, 9 Sep 2024 15:05:56 -0700 Subject: [PATCH 6/6] error handling --- packages/client/src/components/BaseLayout.vue | 5 +-- packages/client/src/helpers/fetchApi.js | 35 ++++++++++++++++--- packages/client/src/store/modules/grants.js | 33 +++++++++++++---- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/client/src/components/BaseLayout.vue b/packages/client/src/components/BaseLayout.vue index 8a8170254..7d4945213 100755 --- a/packages/client/src/components/BaseLayout.vue +++ b/packages/client/src/components/BaseLayout.vue @@ -164,10 +164,7 @@
-
+
Promise.reject(new Error(text || r.statusText))); + .then((text) => { + const err = new Error(text || r.statusText); + err.response = r; + + return Promise.reject(err); + }); }); } @@ -51,7 +56,12 @@ export function deleteRequest(url, body) { } return r .text() - .then((text) => Promise.reject(new Error(text || r.statusText))); + .then((text) => { + const err = new Error(text || r.statusText); + err.response = r; + + return Promise.reject(err); + }); }); } @@ -69,7 +79,12 @@ export function post(url, body) { } return r .text() - .then((text) => Promise.reject(new Error(text || r.statusText))); + .then((text) => { + const err = new Error(text || r.statusText); + err.response = r; + + return Promise.reject(err); + }); }); } @@ -87,7 +102,12 @@ export function put(url, body) { } return r .text() - .then((text) => Promise.reject(new Error(text || r.statusText))); + .then((text) => { + const err = new Error(text || r.statusText); + err.response = r; + + return Promise.reject(err); + }); }); } @@ -105,6 +125,11 @@ export function patch(url, body) { } return r .text() - .then((text) => Promise.reject(new Error(text || r.statusText))); + .then((text) => { + const err = new Error(text || r.statusText); + err.response = r; + + return Promise.reject(err); + }); }); } diff --git a/packages/client/src/store/modules/grants.js b/packages/client/src/store/modules/grants.js index 89ebe1f91..b03153c06 100644 --- a/packages/client/src/store/modules/grants.js +++ b/packages/client/src/store/modules/grants.js @@ -1,3 +1,4 @@ +import { datadogRum } from '@datadog/browser-rum'; import * as fetchApi from '@/helpers/fetchApi'; import { formatFilterDisplay } from '@/helpers/filters'; import { serializeQuery } from '@/helpers/fetchApi'; @@ -213,41 +214,59 @@ export default { fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/interested-codes`) .then((data) => commit('SET_INTERESTED_CODES', data)); }, - async getFollowerForGrant({ rootGetters }, { grantId }) { + async getFollowerForGrant({ rootGetters, commit }, { grantId }) { try { return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); } catch (e) { + // 404 -> User not following + if (e.response.status === 404) { + return null; + } + + const text = `Error retrieving grant follower: + ${e.message}`; + commit('alerts/addAlert', { text, level: 'err' }, { root: true }); + datadogRum.addError(e, { grantId, text }); return null; } }, - async getFollowersForGrant({ rootGetters }, { grantId, limit, paginateFrom }) { + async getFollowersForGrant({ rootGetters, commit }, { grantId, limit, paginateFrom }) { const queryParams = serializeQuery({ limit, paginateFrom }); try { return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/followers${queryParams}`); } catch (e) { + const text = `Error retrieving grant followers: + ${e.message}`; + commit('alerts/addAlert', { text, level: 'err' }, { root: true }); + datadogRum.addError(e, { grantId, text }); return null; } }, - async getNotesForGrant({ rootGetters }, { grantId, limit, paginateFrom }) { + async getNotesForGrant({ rootGetters, commit }, { grantId, limit, paginateFrom }) { const queryParams = serializeQuery({ limit, paginateFrom }); try { return await fetchApi.get(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/notes${queryParams}`); } catch (e) { + const text = `Error retrieving grant notes: + ${e.message}`; + commit('alerts/addAlert', { text, level: 'err' }, { root: true }); + datadogRum.addError(e, { grantId, text }); return null; } }, - async followGrantForCurrentUser({ rootGetters }, { grantId }) { + async followGrantForCurrentUser({ rootGetters, commit }, { grantId }) { try { await fetchApi.put(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); } catch (e) { - // fail silently + const text = `Error following grant: + ${e.message}`; + commit('alerts/addAlert', { text, level: 'err' }, { root: true }); + datadogRum.addError(e, { grantId, text }); } }, - async unfollowGrantForCurrentUser({ rootGetters }, { grantId }) { + async unfollowGrantForCurrentUser({ rootGetters, commit }, { grantId }) { try { await fetchApi.deleteRequest(`/api/organizations/${rootGetters['users/selectedAgencyId']}/grants/${grantId}/follow`); } catch (e) { - // fail silently + const text = `Error unfollowing grant: + ${e.message}`; + commit('alerts/addAlert', { text, level: 'err' }, { root: true }); + datadogRum.addError(e, { grantId, text }); } }, async setEligibilityCodeEnabled({ rootGetters }, { code, enabled }) {