Skip to content

Commit

Permalink
feat: GEO-861 admin announcements - search results are selectable and…
Browse files Browse the repository at this point in the history
… clickable (#633)
  • Loading branch information
banders authored Jul 31, 2024
1 parent a2b6dd4 commit d2c29ba
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 16 deletions.
7 changes: 6 additions & 1 deletion admin-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,16 @@ button:disabled.v-btn {
}
.v-btn.btn-link {
text-decoration: underline;
color: $link-color;
background-color: transparent !important;
border: none;
box-shadow: none;
font-weight: normal !important;
padding: 0px;
letter-spacing: 1px;
}
.v-btn.btn-link:hover > .v-btn__overlay {
opacity: 0 !important;
}
.v-alert .v-icon {
Expand Down
126 changes: 125 additions & 1 deletion admin-frontend/src/components/AnnouncementsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@
"
@update:options="updateSearch"
>
<template v-slot:header.selection="{ column }">
<v-checkbox
class="checkbox-no-details"
v-model="isSelectedAnnouncementsHeaderChecked"
@click="
toggleSelectAllAnnouncements(!isSelectedAnnouncementsHeaderChecked)
"
></v-checkbox>
</template>
<template v-slot:item.selection="{ item }">
<v-checkbox
class="checkbox-no-details"
v-model="selectedAnnouncements[item.announcement_id]"
>
</v-checkbox>
</template>
<template v-slot:item.title="{ item }">
<v-btn
variant="text"
class="btn-link"
color="link"
@click="showAnnouncement(item)"
>
{{ item.title }}
</v-btn>
</template>
<template v-slot:item.published_on="{ item }">
{{ formatDate(item.published_on) }}
</template>
Expand Down Expand Up @@ -82,8 +108,44 @@
</v-menu>
</v-btn>
</template>
<template v-slot:footer.prepend="">
<v-row class="d-flex justify-start">
<v-col>
<v-btn
class="btn-secondary"
:disabled="!selectedAnnouncementIds.length"
prepend-icon="mdi-delete"
>Delete</v-btn
>
</v-col>
</v-row>
</template>
</v-data-table-server>
</div>
<!-- dialogs -->
<v-dialog
v-model="isAnnouncementDialogVisible"
:close-on-content-click="true"
max-width="390"
>
<v-card>
<v-card-title>
{{ announcementInDialog?.title }}
</v-card-title>
<v-card-text>
{{ announcementInDialog?.description }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn class="btn-secondary" @click="showAnnouncement(undefined)">
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
Expand All @@ -94,7 +156,7 @@ export default {
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { ref, watch, computed } from 'vue';
import AnnouncementSearchFilters from './announcements/AnnouncementSearchFilters.vue';
import AnnouncementStatusChip from './announcements/AnnouncementStatusChip.vue';
import { useAnnouncementSearchStore } from '../store/modules/announcementSearchStore';
Expand All @@ -104,6 +166,21 @@ import { AnnouncementKeys } from '../types/announcements';
const announcementSearchStore = useAnnouncementSearchStore();
const { searchResults, isSearching, hasSearched, totalNum, pageSize } =
storeToRefs(announcementSearchStore);
const announcementInDialog = ref<any>(undefined);
const isAnnouncementDialogVisible = ref<boolean>(false);
const isSelectedAnnouncementsHeaderChecked = ref<boolean>(false);
const selectedAnnouncements = ref<object>({});
const selectedAnnouncementIds = computed(() =>
Object.entries(selectedAnnouncements.value)
.filter(([_, value]) => value)
.map(([key, _]) => key),
);
watch(searchResults, () => {
// Don't allow an announcement to be selected if it isn't in the current page
// of search results
clearSelectionOfNonSearchResults();
});
const itemsPerPageOptions = ref([
{ value: 10, title: '10' },
Expand All @@ -117,6 +194,12 @@ const itemsPerPageOptions = ref([
]);
const headers = ref<any>([
{
title: '',
key: 'selection',
align: 'center',
sortable: false,
},
{
title: 'Title',
align: 'start',
Expand Down Expand Up @@ -149,6 +232,44 @@ const headers = ref<any>([
},
]);
function selectAnnouncement(announcement, select: boolean = true) {
if (!announcement) {
return;
}
if (select) {
selectedAnnouncements.value[announcement.announcement_id] = true;
} else {
delete selectedAnnouncements.value[announcement.announcement_id];
}
}
function toggleSelectAllAnnouncements(select: boolean) {
clearSelectionOfNonSearchResults();
searchResults.value?.forEach((announcement) => {
selectAnnouncement(announcement, select);
});
}
/*
Removes from 'selectedAnnouncements' any announcements that are not also
in the current 'searchResults'.
*/
function clearSelectionOfNonSearchResults() {
Object.keys(selectedAnnouncements.value).forEach((announcementId) => {
const isInSearchResults = searchResults.value?.filter(
(announcement) => announcement.announcement_id == announcementId,
).length;
if (!isInSearchResults) {
delete selectedAnnouncements.value[announcementId];
}
});
}
function showAnnouncement(announcement) {
announcementInDialog.value = announcement;
isAnnouncementDialogVisible.value = announcement != undefined;
}
async function updateSearch(options) {
await announcementSearchStore.updateSearch(options);
}
Expand All @@ -170,4 +291,7 @@ async function addAnnouncement() {
.btn-actions {
opacity: 1 !important;
}
.checkbox-no-details > .v-input__details {
display: none;
}
</style>
115 changes: 112 additions & 3 deletions admin-frontend/src/components/__tests__/AnnouncementsPage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { faker } from '@faker-js/faker';
import { createTestingPinia } from '@pinia/testing';
import { flushPromises, mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
Expand Down Expand Up @@ -92,15 +93,123 @@ describe('AnnouncementsPage', () => {
}
});

// tests of UI state

it('has search results', async () => {
expect(wrapper.findAll('.search-results').length).toBe(1);
announcementSearchStore.searchResults = mockAnnouncements;
await nextTick();
expect(wrapper.html()).toContain(`Displaying ${mockAnnouncements.length}`);
});

it('has search filters', async () => {
//await nextTick();
//expect(wrapper.findComponent(AnnouncementsSearchFilters)).toBe(true);
// tests of computed properties

describe('selectedAnnouncementIds', () => {
describe('when two announcements are selected', () => {
it('property value is a list with the IDs of both selected announcements', async () => {
mockAnnouncements.forEach((announcement) => {
wrapper.vm.selectAnnouncement(announcement);
});
const result = wrapper.vm.selectedAnnouncementIds;
expect(result).toStrictEqual(
mockAnnouncements.map((a) => a.announcement_id),
);
});
});
});

// tests of functions

describe('showAnnouncement', () => {
describe('value is an announcement', () => {
it('shows the dialog', async () => {
const mockAnnouncement = {};
wrapper.vm.showAnnouncement(mockAnnouncement);
expect(wrapper.vm.isAnnouncementDialogVisible).toBeTruthy();
});
});
describe('value is undefined', () => {
it('hides the dialog', async () => {
const mockAnnouncement = undefined;
wrapper.vm.showAnnouncement(mockAnnouncement);
expect(wrapper.vm.isAnnouncementDialogVisible).toBeFalsy();
});
});
});

describe('selectAnnouncement', () => {
describe('select = true', () => {
it('adds the announcement ID as a key in the selectedAnnouncements object', async () => {
const mockAnnouncement = {
announcement_id: faker.string.uuid(),
};
wrapper.vm.selectedAnnouncements = {};
wrapper.vm.selectAnnouncement(mockAnnouncement, true);
expect(
wrapper.vm.selectedAnnouncements[mockAnnouncement.announcement_id],
).toBeTruthy();
});
});
describe('select = false', () => {
it('adds the announcement ID as a key in the selectedAnnouncements object', async () => {
const mockAnnouncement = {
announcement_id: faker.string.uuid(),
};
wrapper.vm.selectedAnnouncements = {};
wrapper.vm.selectAnnouncement(mockAnnouncement, false);
expect(
wrapper.vm.selectedAnnouncements.hasOwnProperty(
mockAnnouncement.announcement_id,
),
).toBeFalsy();
});
});

describe('toggleSelectAllAnnouncements', () => {
describe('select = true', () => {
it('selects all announcements', () => {
wrapper.vm.selectedAnnouncements = {};
wrapper.vm.searchResults = mockAnnouncements;
wrapper.vm.toggleSelectAllAnnouncements(true);
expect(wrapper.vm.selectedAnnouncements).toStrictEqual(
Object.fromEntries(
mockAnnouncements.map((a) => [a.announcement_id, true]),
),
);
});
});
describe('select = false', () => {
it('deselects all announcements', () => {
wrapper.vm.selectedAnnouncements = { mock_annoncement_id: true };
wrapper.vm.searchResults = mockAnnouncements;
wrapper.vm.toggleSelectAllAnnouncements(false);
expect(wrapper.vm.selectedAnnouncements).toStrictEqual({});
});
});
});
});

describe('clearSelectionOfNonSearchResults', () => {
describe("where there is one selected announcement and it isn't in the search results", () => {
it('deselects all announcements', () => {
wrapper.vm.selectedAnnouncements = { mock_annoncement_id: true };
wrapper.vm.searchResults = mockAnnouncements;
wrapper.vm.clearSelectionOfNonSearchResults();
expect(wrapper.vm.selectedAnnouncements).toStrictEqual({});
});
});
describe('where there is one selected announcement and it is in the search results', () => {
it('no change to the selected announcement', () => {
const initialSelection = Object.fromEntries([
[mockAnnouncements[0].announcement_id, true],
]);
wrapper.vm.searchResults = mockAnnouncements;
wrapper.vm.selectedAnnouncements = initialSelection;
wrapper.vm.clearSelectionOfNonSearchResults();
expect(wrapper.vm.selectedAnnouncements).toStrictEqual(
initialSelection,
);
});
});
});
});
22 changes: 11 additions & 11 deletions backend/db/sample-data/generate_fake_announcements.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ begin

insert into pay_transparency.announcement (title, description, published_on, expires_on, status, created_by, updated_by)
values
('announcement 1', '', now(), now() + interval '12 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 2', '', now() - interval '1 week', now() + interval '12 week', 'DRAFT', adminUserId, adminUserId),
('announcement 3', '', now() - interval '2 week', now() + interval '10 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 4', '', now() - interval '6 week', now() + interval '11 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 5', '', now() - interval '7 week', now() + interval '14 week', 'DRAFT', adminUserId, adminUserId),
('announcement 6', '', now(), now() + interval '12 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 7', '', now() - interval '1 week', now() + interval '12 week', 'DRAFT', adminUserId, adminUserId),
('announcement 8', '', now() - interval '2 week', now() + interval '11 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 9', '', now() - interval '3 week', now() + interval '5 week', 'DRAFT', adminUserId, adminUserId),
('announcement 10', '', now() - interval '2 week', now() + interval '8 week', 'PUBLISHED', adminUserId, adminUserId),
('announcement 11', '', now() - interval '7 week', now() + interval '13 week', 'DRAFT', adminUserId, adminUserId);
('Fugiat credo', 'Aeternus conculco nihil desparatus varietas curatio soleo sublime valetudo. Ulciscor comitatus tego atque deputo soluta suadeo concido compello. Et anser bos acsi alias.', now(), now() + interval '12 week', 'PUBLISHED', adminUserId, adminUserId),
('Totidem ambitus', 'Depraedor adfectus vita ventito stabilis. Vorax numquam sustineo cetera brevis laborum vos ex testimonium. Stultus valeo peccatus comes abscido vilis crepusculum viscus volo.', now() - interval '1 week', now() + interval '12 week', 'DRAFT', adminUserId, adminUserId),
('Delibero voluptate', 'Voveo cras tamdiu tenuis dolorem. Commodi deprecator tricesimus crux solium benigne soleo crur. Temeritas tamdiu defetiscor.', now() - interval '2 week', now() + interval '10 week', 'PUBLISHED', adminUserId, adminUserId),
('Vox aranea', 'Cavus utique vestrum asporto. Depono tamdiu truculenter cicuta tibi. Tubineus conatus thema ubi.', now() - interval '6 week', now() + interval '11 week', 'PUBLISHED', adminUserId, adminUserId),
('Creator solum', 'Adversus quos adeptio. Eius succurro alienus verecundia qui. Spargo basium trans corroboro ustulo.', now() - interval '7 week', now() + interval '14 week', 'DRAFT', adminUserId, adminUserId),
('Contego aperio', 'Ducimus cras provident tracto cohors vulgivagus. Cuppedia utor turba architecto capitulus ad aperio cur admitto. Tyrannus voluptates demo arguo attero.', now(), now() + interval '12 week', 'PUBLISHED', adminUserId, adminUserId),
('Eum benigne', 'Consectetur adstringo calculus talis ventito cibo supplanto vomer aegrus. Arbor eos aestivus ater tibi verecundia trans casso aegre. Thesis tribuo bene aggero strenuus ciminatio ver quibusdam virgo.', now() - interval '1 week', now() + interval '12 week', 'DRAFT', adminUserId, adminUserId),
('Constans validus', 'Cohaero velut temporibus antepono. Vergo pecus tondeo vergo argentum cribro distinctio torrens. Ullus temporibus facilis magnam bellicus vinum pax nobis.', now() - interval '2 week', now() + interval '11 week', 'PUBLISHED', adminUserId, adminUserId),
('Numquam tenus', 'Bonus combibo crebro blanditiis acer abbas acidus. Subnecto charisma viduo sulum expedita una excepturi recusandae causa pecto. Arceo voco cursus similique tempus claudeo sono cultellus abundans decerno.', now() - interval '3 week', now() + interval '5 week', 'DRAFT', adminUserId, adminUserId),
('Appono itaque', 'Antiquus acervus at vinum spectaculum adulescens adstringo villa. Tergiversatio assumenda defendo conventus usque calculus aeternus abutor cernuus totus. Arcus titulus stabilis perspiciatis.', now() - interval '2 week', now() + interval '8 week', 'PUBLISHED', adminUserId, adminUserId),
('Alarte Ascendare', 'Adficio sum perspiciatis umerus cetera cribro absum. Cubitum curiositas utrimque numquam aggredior talis aedificium sortitus aperio. Cetera administratio corpus suscipit patrocinor color suppono.', now() - interval '7 week', now() + interval '13 week', 'DRAFT', adminUserId, adminUserId);

end

Expand Down

0 comments on commit d2c29ba

Please sign in to comment.