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

#2337: New feature flag for moving Grant Details from a modal to an independent page #2338

Merged
merged 8 commits into from
Dec 15, 2023
1 change: 1 addition & 0 deletions packages/client/public/deploy-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ window.APP_CONFIG.overrideFeatureFlag = (flagName, overrideValue) => {
window.APP_CONFIG.featureFlags = {
myProfileEnabled: true,
newTerminologyEnabled: true,
newGrantsDetailPageEnabled: false,
};
11 changes: 7 additions & 4 deletions packages/client/src/components/GrantsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,22 @@
<div class="my-1 rounded py-1 px-2 page-item">{{ totalRows }} total grant{{ totalRows == 1 ? '' : 's' }}</div>
</b-col>
</b-row>
<GrantDetails :selected-grant.sync="selectedGrant" />
<GrantDetailsLegacy v-if="!newGrantsDetailPageEnabled" :selected-grant.sync="selectedGrant" />
</section>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import { newTerminologyEnabled } from '@/helpers/featureFlags';
import { newTerminologyEnabled, newGrantsDetailPageEnabled } from '@/helpers/featureFlags';
import { titleize } from '../helpers/form-helpers';
import GrantDetails from './Modals/GrantDetails.vue';
import GrantDetailsLegacy from './Modals/GrantDetailsLegacy.vue';
import SearchPanel from './Modals/SearchPanel.vue';
import SavedSearchPanel from './Modals/SavedSearchPanel.vue';
import SearchFilter from './SearchFilter.vue';

export default {
components: {
GrantDetails, SearchPanel, SavedSearchPanel, SearchFilter,
GrantDetailsLegacy, SearchPanel, SavedSearchPanel, SearchFilter,
},
props: {
showInterested: Boolean,
Expand Down Expand Up @@ -208,6 +208,9 @@ export default {
searchFilters() {
return this.activeFilters;
},
newGrantsDetailPageEnabled() {
return newGrantsDetailPageEnabled();
},
},
watch: {
selectedAgency() {
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/helpers/featureFlags/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export function myProfileEnabled() {
export function newTerminologyEnabled() {
return getFeatureFlags().newTerminologyEnabled === true;
}

export function newGrantsDetailPageEnabled() {
return getFeatureFlags().newGrantsDetailPageEnabled === true;
}
11 changes: 7 additions & 4 deletions packages/client/src/views/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
</template>
</b-table>
</b-card>
<GrantDetails :selected-grant.sync="selectedGrant" />
<GrantDetailsLegacy v-if="!newGrantsDetailPageEnabled" :selected-grant.sync="selectedGrant" />
</section>
</template>

Expand Down Expand Up @@ -144,11 +144,11 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import resizableTableMixin from '@/mixin/resizableTable';
import GrantDetails from '@/components/Modals/GrantDetails.vue';
import { newTerminologyEnabled } from '@/helpers/featureFlags';
import GrantDetailsLegacy from '@/components/Modals/GrantDetailsLegacy.vue';
import { newTerminologyEnabled, newGrantsDetailPageEnabled } from '@/helpers/featureFlags';

export default {
components: { GrantDetails },
components: { GrantDetailsLegacy },
data() {
return {
dateColors: [],
Expand Down Expand Up @@ -366,6 +366,9 @@ export default {
newTerminologyEnabled() {
return newTerminologyEnabled();
},
newGrantsDetailPageEnabled() {
return newGrantsDetailPageEnabled();
},
},
watch: {
async selectedTeam() {
Expand Down
275 changes: 275 additions & 0 deletions packages/client/src/views/GrantDetails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<!-- eslint-disable max-len -->
<template>
<b-modal v-model="showDialog" ok-only :title="selectedGrant && selectedGrant.grant_number"
@hide="resetSelectedGrant" scrollable size="xl" ok-title="Close">
<div v-if="selectedGrant">
<b-row class="mb-3 d-flex align-items-baseline">
<b-col cols="8">
<h1 class="mb-0 h2">{{ selectedGrant.title }}</h1>
</b-col>
<b-col cols="4" class="text-right">
<b-button :href="`https://www.grants.gov/web/grants/view-opportunity.html?oppId=${selectedGrant.grant_id}`"
target="_blank" rel="noopener noreferrer" variant="primary">
<b-icon icon="box-arrow-up-right" aria-hidden="true" class="mr-2"></b-icon>View on Grants.gov
</b-button>
</b-col>
</b-row>
<p><span class="data-label">Valid from:</span> {{ new
Date(selectedGrant.open_date).toLocaleDateString('en-US', { timeZone: 'UTC' })
}}-{{ new
Date(selectedGrant.close_date).toLocaleDateString('en-US', { timeZone: 'UTC' })
}}</p>
<div v-for="field in dialogFields" :key="field">
<p><span class="data-label">{{ titleize(field) }}:</span> {{ selectedGrant[field] }}</p>
</div>
<p class="data-label">Description:</p>
<div style="max-height: 170px; overflow-y: scroll">
<div style="white-space: pre-line" v-html="selectedGrant.description"></div>
</div>
<br />
<b-row class="ml-2 mb-2 d-flex align-items-baseline">
<h2 class="h4">{{newTerminologyEnabled ? 'Team': 'Agency'}} Status</h2>
<b-col class="text-right">
<b-row v-if="!interested">
<b-col cols="9">
<b-form-select v-model="selectedInterestedCode">
<b-form-select-option-group label="Interested">
<b-form-select-option v-for="code in interestedCodes.interested" :key="code.id" :value="code.id">
{{ code.name }}</b-form-select-option>
</b-form-select-option-group>
<b-form-select-option-group label="Applied">
<b-form-select-option v-for="code in interestedCodes.result" :key="code.id" :value="code.id">
{{ code.name }}</b-form-select-option>
</b-form-select-option-group>
<b-form-select-option-group label="Not Applying">
<b-form-select-option v-for="code in interestedCodes.rejections" :key="code.id" :value="code.id">
{{ code.name }}</b-form-select-option>
</b-form-select-option-group>
</b-form-select>
</b-col>
<b-button variant="outline-primary" @click="markGrantAsInterested">Submit</b-button>
</b-row>
<b-row v-if="interested && interested.interested_status_code !== 'Rejection'&& shouldShowSpocButton">
<b-col>
<b-button variant="primary" @click="generateSpoc">Generate SPOC</b-button>
</b-col>
</b-row>
</b-col>
</b-row>
<br />
<b-table :items="selectedGrant.interested_agencies" :fields="interestedAgenciesFields">
<template #cell(actions)="row">
<b-row
v-if="(String(row.item.agency_id) === selectedAgencyId) || isAbleToUnmark(row.item.agency_id)">
<b-button variant="outline-danger" class="mr-1 border-0" size="sm" @click="unmarkGrantAsInterested(row)">
<b-icon icon="trash-fill" aria-hidden="true"></b-icon>
</b-button>
</b-row>
</template>
</b-table>
<br />
<b-row class="ml-2 mb-2 d-flex align-items-baseline">
<h2 class="h4">Assigned {{newTerminologyEnabled ? 'Teams': 'Agencies'}}</h2>
<v-select v-model="selectedAgencies" :options="agencies" :multiple="true" :close-on-select="false"
:clear-on-select="false" :placeholder="`Select ${newTerminologyEnabled ? 'teams': 'agencies'}`" label="name" track-by="id"
style="width: 300px; margin: 0 16px;" :show-labels="false"
>
</v-select>
<b-button variant="outline-primary" @click="assignAgenciesToGrant">Assign</b-button>
</b-row>
<b-table :items="assignedAgencies" :fields="assignedAgenciesFields">
<template #cell(actions)="row">
<b-button variant="outline-danger" class="mr-1 border-0" size="sm" @click="unassignAgenciesToGrant(row)">
<b-icon icon="trash-fill" aria-hidden="true"></b-icon>
</b-button>
</template>
</b-table>
</div>
</b-modal>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import { debounce } from 'lodash';
import { newTerminologyEnabled } from '@/helpers/featureFlags';
import { titleize } from '../helpers/form-helpers';

export default {
props: {
selectedGrant: Object,
},
data() {
return {
showDialog: false,
dialogFields: ['grant_id', 'agency_code', 'award_ceiling', 'cfda_list', 'opportunity_category', 'bill'],
orderBy: '',
interestedAgenciesFields: [
{
key: 'agency_name',
label: `${newTerminologyEnabled ? 'Team' : 'Agency'}`,
},
{
key: 'agency_abbreviation',
label: 'Abbreviation',
},
{
label: 'Name',
key: 'user_name',
},
{
label: 'Email',
key: 'user_email',
},
{
label: 'Interested Code',
key: 'interested_code_name',
},
{
key: 'actions',
label: 'Actions',
},
],
assignedAgenciesFields: [
{
key: 'name',
},
{
key: 'abbreviation',
label: 'Abbreviation',
},
{
key: 'created_at',
},
{
key: 'actions',
label: 'Actions',
},
],
assignedAgencies: [],
selectedAgencies: [],
selectedInterestedCode: null,
searchInput: null,
debouncedSearchInput: null,
};
},
mounted() {
},
computed: {
...mapGetters({
agency: 'users/agency',
selectedAgencyId: 'users/selectedAgencyId',
agencies: 'agencies/agencies',
currentTenant: 'users/currentTenant',
users: 'users/users',
interestedCodes: 'grants/interestedCodes',
loggedInUser: 'users/loggedInUser',
selectedAgency: 'users/selectedAgency',
}),
alreadyViewed() {
if (!this.selectedGrant) {
return false;
}
return this.selectedGrant.viewed_by_agencies.find(
(viewed) => viewed.agency_id.toString() === this.selectedAgencyId,
);
},
shouldShowSpocButton() {
return this.currentTenant.uses_spoc_process;
},
interested() {
if (!this.selectedGrant) {
return undefined;
}
return this.selectedGrant.interested_agencies.find(
(interested) => interested.agency_id.toString() === this.selectedAgencyId,
);
},
newTerminologyEnabled() {
return newTerminologyEnabled();
},
},
watch: {
async selectedGrant() {
this.showDialog = Boolean(this.selectedGrant);
if (this.selectedGrant) {
this.fetchAgencies();
if (!this.alreadyViewed) {
try {
await this.markGrantAsViewed();
} catch (e) {
console.log(e);
}
}
this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.selectedGrant.grant_id });
}
},
},
methods: {
...mapActions({
markGrantAsViewedAction: 'grants/markGrantAsViewed',
generateGrantForm: 'grants/generateGrantForm',
markGrantAsInterestedAction: 'grants/markGrantAsInterested',
unmarkGrantAsInterestedAction: 'grants/unmarkGrantAsInterested',
getInterestedAgencies: 'grants/getInterestedAgencies',
getGrantAssignedAgencies: 'grants/getGrantAssignedAgencies',
assignAgenciesToGrantAction: 'grants/assignAgenciesToGrant',
unassignAgenciesToGrantAction: 'grants/unassignAgenciesToGrant',
fetchUsers: 'users/fetchUsers',
fetchAgencies: 'agencies/fetchAgencies',
}),
titleize,
debounceSearchInput: debounce(function bounce(newVal) {
this.debouncedSearchInput = newVal;
}, 500),
async markGrantAsViewed() {
await this.markGrantAsViewedAction({ grantId: this.selectedGrant.grant_id, agencyId: this.selectedAgencyId });
},
async markGrantAsInterested() {
if (this.selectedInterestedCode !== null) {
await this.markGrantAsInterestedAction({
grantId: this.selectedGrant.grant_id,
agencyId: this.selectedAgencyId,
interestedCode: this.selectedInterestedCode,
});
}
},
async unmarkGrantAsInterested(row) {
await this.unmarkGrantAsInterestedAction({
grantId: this.selectedGrant.grant_id,
agencyIds: [row.item.agency_id],
interestedCode: this.selectedInterestedCode,
});
this.selectedGrant.interested_agencies = await this.getInterestedAgencies({ grantId: this.selectedGrant.grant_id });
},
async assignAgenciesToGrant() {
const agencyIds = this.selectedAgencies.map((agency) => agency.id);
await this.assignAgenciesToGrantAction({
grantId: this.selectedGrant.grant_id,
agencyIds,
});
this.selectedAgencies = [];
this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.selectedGrant.grant_id });
},
async unassignAgenciesToGrant(row) {
await this.unassignAgenciesToGrantAction({
grantId: this.selectedGrant.grant_id,
agencyIds: [row.item.id],
});
this.assignedAgencies = await this.getGrantAssignedAgencies({ grantId: this.selectedGrant.grant_id });
},
async generateSpoc() {
await this.generateGrantForm({
grantId: this.selectedGrant.grant_id,
});
},
isAbleToUnmark(agencyId) {
return this.agencies.some((agency) => agency.id === agencyId);
},
resetSelectedGrant() {
this.$emit('update:selectedGrant', null);
this.assignedAgencies = [];
this.selectedAgencies = [];
},
},
};
</script>
10 changes: 7 additions & 3 deletions packages/client/src/views/RecentActivity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
aria-controls="grants-table" />
<b-button class="ml-2" variant="outline-primary disabled">{{ grantsInterested.length }} of {{ totalRows }}</b-button>
</b-row>
<GrantDetails :selected-grant.sync="selectedGrant" />
<GrantDetailsLegacy v-if="!newGrantsDetailPageEnabled" :selected-grant.sync="selectedGrant" />
</section>
</template>
<style scoped>
Expand All @@ -71,11 +71,12 @@

<script>
import { mapActions, mapGetters } from 'vuex';
import { newGrantsDetailPageEnabled } from '@/helpers/featureFlags';
import resizableTableMixin from '@/mixin/resizableTable';
import GrantDetails from '@/components/Modals/GrantDetails.vue';
import GrantDetailsLegacy from '@/components/Modals/GrantDetailsLegacy.vue';

export default {
components: { GrantDetails },
components: { GrantDetailsLegacy },
data() {
return {
perPage: 10,
Expand Down Expand Up @@ -156,6 +157,9 @@ export default {
totalRows() {
return this.totalInterestedGrants;
},
newGrantsDetailPageEnabled() {
return newGrantsDetailPageEnabled();
},
},
watch: {
async selectedGrant() {
Expand Down
Loading
Loading