Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 3601 - Follow/Notes updates to My Grants #3633

Merged
merged 11 commits into from
Nov 22, 2024
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: [],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component is very lightly tested and I'd like to return to add more test coverage here, but that can be done as a follow up PR; especially as we are likely removing some code soon for feature flag removal and therefore can reduce the test surface to cover

},
],
'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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding in the other feature flags I've seen used throughout the code for reference in the example env

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
Loading