Skip to content

Commit

Permalink
feat: 3601 - Follow/Notes updates to My Grants (#3633)
Browse files Browse the repository at this point in the history
* feat: 3601 - my grants with follow notes

* feat: update tab specific urls when followNotesEnabled

* feat: get followed by grants

* feat: add in followed by, fix sorting

* feat: get tests passing again

* feat: fix CSV export

* feat: clean up enhance grant data with feature flag use

* feat: consistent use of tables constant
  • Loading branch information
phm200 authored Nov 22, 2024
1 parent 98e3b1a commit fa89610
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/client/src/components/GrantsTable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('GrantsTable component', () => {
{
interested_agencies: [],
viewed_by_agencies: [],
followed_by_agencies: [],
},
],
'grants/activeFilters': () => [],
Expand Down
31 changes: 26 additions & 5 deletions packages/client/src/components/GrantsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { datadogRum } from '@datadog/browser-rum';
import { newTerminologyEnabled, newGrantsDetailPageEnabled } from '@/helpers/featureFlags';
import { newTerminologyEnabled, newGrantsDetailPageEnabled, followNotesEnabled } from '@/helpers/featureFlags';
import { titleize } from '@/helpers/form-helpers';
import { daysUntil } from '@/helpers/dates';
import { defaultCloseDateThresholds } from '@/helpers/constants';
Expand All @@ -175,6 +175,10 @@ export default {
type: String,
default: undefined,
},
showFollowedByAgency: {
type: String,
default: undefined,
},
showSearchControls: {
type: Boolean,
default: true,
Expand All @@ -197,8 +201,9 @@ export default {
key: 'viewed_by',
},
{
key: 'interested_agencies',
label: `Interested ${newTerminologyEnabled() ? 'Teams' : 'Agencies'}`,
key: `${followNotesEnabled() ? 'followed_by_agencies' : 'interested_agencies'}`,
// eslint-disable-next-line no-nested-ternary -- can clean up once we remove newTerminologyEnabled feature flag
label: `${followNotesEnabled() ? 'Followed by' : newTerminologyEnabled() ? 'Interested Teams' : 'Interested Agencies'}`,
},
{
// opportunity_status
Expand Down Expand Up @@ -286,8 +291,15 @@ export default {
interested_agencies: grant.interested_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
viewed_by: grant.viewed_by_agencies
.map((v) => v.agency_abbreviation)
viewed_by: followNotesEnabled()
? grant.viewed_by_agencies
.map((v) => v.agency_name)
.join(', ')
: grant.viewed_by_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
followed_by_agencies: grant.followed_by_agencies
.map((v) => v.agency_name)
.join(', '),
status: grant.opportunity_status,
award_ceiling: grant.award_ceiling,
Expand All @@ -311,6 +323,12 @@ export default {
newGrantsDetailPageEnabled() {
return newGrantsDetailPageEnabled();
},
followedByColumnTitle() {
if (followNotesEnabled()) {
return 'Followed by';
}
return `Interested ${newTerminologyEnabled() ? 'Teams' : 'Agencies'}`;
},
},
watch: {
selectedAgency() {
Expand Down Expand Up @@ -433,6 +451,7 @@ export default {
`${this.showResult ? 'Applied' : ''}`,
`${this.showRejected ? 'Not Applying' : ''}`,
`${this.showAssignedToAgency ? 'Assigned' : ''}`,
`${this.showFollowedByAgency ? 'Followed' : ''}`,
].filter((r) => r),
});
}
Expand All @@ -445,6 +464,7 @@ export default {
showResult: this.showResult,
showRejected: this.showRejected,
assignedToAgency: this.showAssignedToAgency,
followedByAgency: this.showFollowedByAgency,
});
// Clamp currentPage to valid range
const clampedPage = Math.max(Math.min(this.currentPage, this.lastPage), 1);
Expand Down Expand Up @@ -550,6 +570,7 @@ export default {
showResult: this.showResult,
showRejected: this.showRejected,
assignedToAgency: this.showAssignedToAgency,
followedByAgency: this.showFollowedByAgency,
});
},
},
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/router/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router';

import BaseLayout from '@/components/BaseLayout.vue';
import { shareTerminologyEnabled, newTerminologyEnabled, newGrantsDetailPageEnabled } from '@/helpers/featureFlags';
import {
shareTerminologyEnabled, newTerminologyEnabled, newGrantsDetailPageEnabled, followNotesEnabled,
} from '@/helpers/featureFlags';
import LoginView from '@/views/LoginView.vue';

import store from '@/store';

const myGrantsTabs = [
const myGrantsTabs = followNotesEnabled() ? [
'shared-with-my-team',
'followed-by-my-team',
] : [
shareTerminologyEnabled() ? 'shared-with-your-team' : 'assigned',
'interested',
'not-applying',
Expand Down
127 changes: 104 additions & 23 deletions packages/client/src/views/MyGrantsView.spec.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,121 @@
import MyGrantsView from '@/views/MyGrantsView.vue';

import {
describe, it, expect, vi,
describe, it, expect, vi, beforeEach,
} from 'vitest';
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import { shareTerminologyEnabled, followNotesEnabled } from '@/helpers/featureFlags';
import GrantsTable from '@/components/GrantsTable.vue';

vi.mock('bootstrap-vue', async () => ({
// SavedSearchPanel imports bootstrap-vue, which triggers an error in testing, so we'll mock it out
VBToggle: vi.fn(),
}));

describe('MyGrantsView', () => {
const store = createStore({
getters: {
'users/selectedAgencyId': () => '123',
},
});
const $route = {
params: {
tab: 'applied',
},
meta: {
tabNames: ['interested', 'applied'],
},
};

it('renders', () => {
const wrapper = shallowMount(MyGrantsView, {
global: {
plugins: [store],
mocks: {
$route,
vi.mock('@/helpers/featureFlags', async (importOriginal) => ({
...await importOriginal(),
shareTerminologyEnabled: vi.fn(),
followNotesEnabled: vi.fn(),
}));

describe('MyGrantsView.vue', () => {
describe('when follow notes flag is off', () => {
const store = createStore({
getters: {
'users/selectedAgencyId': () => '123',
},
});
const $route = {
params: {
tab: 'applied',
},
meta: {
tabNames: ['interested', 'applied'],
},
};

beforeEach(() => {
vi.mocked(shareTerminologyEnabled).mockReturnValue(true);
vi.mocked(followNotesEnabled).mockReturnValue(false);
});

it('renders', () => {
const wrapper = shallowMount(MyGrantsView, {
global: {
plugins: [store],
mocks: {
$route,
},
},
});
expect(wrapper.exists()).toBe(true);

// check tab titles
const html = wrapper.html();
expect(html).toContain('title="Shared With Your Team"');
expect(html).toContain('title="Interested"');
expect(html).toContain('title="Not Applying"');
expect(html).toContain('title="Applied"');
expect(html).not.toContain('title="Shared With My Team"');
expect(html).not.toContain('title="Followed by My Team"');

// check tab order and table search title props
const tabs = wrapper.findAllComponents(GrantsTable);
expect(tabs.length).toEqual(4);
expect(tabs[0].props().searchTitle).toEqual('Shared With Your Team');
expect(tabs[1].props().searchTitle).toEqual('Interested');
expect(tabs[2].props().searchTitle).toEqual('Not Applying');
expect(tabs[3].props().searchTitle).toEqual('Applied');
});
});

describe('when follow notes flag is on', () => {
const store = createStore({
getters: {
'users/selectedAgencyId': () => '123',
},
});
const $route = {
// @todo: adjust these for new tab names?
params: {
tab: 'applied',
},
meta: {
tabNames: ['interested', 'applied'],
},
};

beforeEach(() => {
vi.mocked(shareTerminologyEnabled).mockReturnValue(true);
vi.mocked(followNotesEnabled).mockReturnValue(true);
});

it('renders', () => {
const wrapper = shallowMount(MyGrantsView, {
global: {
plugins: [store],
mocks: {
$route,
},
},
});
expect(wrapper.exists()).toBe(true);

// check tab titles
const html = wrapper.html();
expect(html).toContain('title="Shared With My Team"');
expect(html).toContain('title="Followed by My Team"');
expect(html).not.toContain('title="Shared With Your Team"');
expect(html).not.toContain('title="Interested"');
expect(html).not.toContain('title="Not Applying"');
expect(html).not.toContain('title="Applied"');

// check tab order and table search title props
const tabs = wrapper.findAllComponents(GrantsTable);
expect(tabs.length).toEqual(2);
expect(tabs[0].props().searchTitle).toEqual('Shared With My Team');
expect(tabs[1].props().searchTitle).toEqual('Followed by My Team');
});
expect(wrapper.exists()).toBe(true);
});
});
42 changes: 36 additions & 6 deletions packages/client/src/views/MyGrantsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,61 @@
style="margin-top: 1.5rem"
lazy
>
<b-tab :title="shareTerminologyEnabled ? 'Shared With Your Team' : 'Assigned'">
<b-tab :title="sharedTabTitle">
<GrantsTable
:search-title="shareTerminologyEnabled ? 'Shared With Your Team' : 'Assigned'"
:search-title="sharedTabTitle"
:show-assigned-to-agency="selectedAgencyId"
:show-search-controls="false"
/>
</b-tab>
<b-tab title="Interested">
<b-tab
v-if="!followNotesEnabled"
title="Interested"
>
<GrantsTable
search-title="Interested"
show-interested
:show-search-controls="false"
/>
</b-tab>
<b-tab title="Not Applying">
<b-tab
v-if="!followNotesEnabled"
title="Not Applying"
>
<GrantsTable
search-title="Not Applying"
show-rejected
:show-search-controls="false"
/>
</b-tab>
<b-tab title="Applied">
<b-tab
v-if="!followNotesEnabled"
title="Applied"
>
<GrantsTable
search-title="Applied"
show-result
:show-search-controls="false"
/>
</b-tab>
<b-tab
v-if="followNotesEnabled"
title="Followed by My Team"
>
<GrantsTable
search-title="Followed by My Team"
:show-followed-by-agency="selectedAgencyId"
:show-search-controls="false"
/>
</b-tab>
</b-tabs>
</div>
</template>

<script>
import { mapGetters } from 'vuex';
import GrantsTable from '@/components/GrantsTable.vue';
import { shareTerminologyEnabled } from '@/helpers/featureFlags';
import { shareTerminologyEnabled, followNotesEnabled } from '@/helpers/featureFlags';
export default {
components: {
Expand All @@ -60,6 +79,17 @@ export default {
shareTerminologyEnabled() {
return shareTerminologyEnabled();
},
followNotesEnabled() {
return followNotesEnabled();
},
sharedTabTitle() {
if (followNotesEnabled()) {
return 'Shared With My Team';
} if (shareTerminologyEnabled()) {
return 'Shared With Your Team';
}
return 'Assigned';
},
},
created() {
this.$watch('$route.params.tab', (tabName) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ COOKIE_SECRET=itsasecretsecretsecret
WEBSITE_DOMAIN=http://localhost:8080
API_DOMAIN=http://localhost:8080

# feature flags
ENABLE_GRANTS_SCRAPER=true
SHARE_TERMINOLOGY_ENABLED=true
ENABLE_NEW_TEAM_TERMINOLOGY=true
ENABLE_GRANT_DIGEST_SCHEDULED_TASK=true
ENABLE_SAVED_SEARCH_GRANTS_DIGEST=true
ENABLE_FOLLOW_NOTES=true


GRANTS_SCRAPER_DATE_RANGE=7
GRANTS_SCRAPER_DELAY=1000
NODE_OPTIONS=--max_old_space_size=1024
Expand Down
7 changes: 6 additions & 1 deletion packages/server/__tests__/api/grants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,11 +561,16 @@ describe('`/api/grants` endpoint', () => {
expect(response.headers.get('Content-Type')).to.include('text/csv');
expect(response.headers.get('Content-Disposition')).to.include('attachment');

// eslint-disable-next-line no-nested-ternary -- will remove ternary when remove feature flag
const followedByColumnHeaderName = process.env.ENABLE_FOLLOW_NOTES === 'true'
? 'Followed By' : process.env.ENABLE_NEW_TEAM_TERMINOLOGY === 'true'
? 'Interested Teams' : 'Interested Agencies';

const expectedCsvHeaders = [
'Opportunity Number',
'Title',
'Viewed By',
process.env.ENABLE_NEW_TEAM_TERMINOLOGY === 'true' ? 'Interested Teams' : 'Interested Agencies',
followedByColumnHeaderName,
'Opportunity Status',
'Opportunity Category',
'Cost Sharing',
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/db/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ module.exports = {
keywords: 'keywords',
email_subscriptions: 'email_subscriptions',
grants_saved_searches: 'grants_saved_searches',
grant_followers: 'grant_followers',
},
};
Loading

0 comments on commit fa89610

Please sign in to comment.