From 1e9c4339997bb3d1a7cdf44475dd6d916c6e58e8 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 9 Jan 2024 13:37:40 +0500 Subject: [PATCH 01/19] add show recent functionality to multiview, uniform multi history view This adds the option of showing 4 latest histories in Multiview, instead of always tracking pinned histories. Once the user decides to track pinned histories, we do so, until the user "resets" it to recent histories mode. Also organized the multiple histories in Multiview to show up evenly, with the `HistoryDetails` taking up a fixed space at the top. --- client/src/components/Common/TextSummary.vue | 28 +++- .../History/CurrentHistory/HistoryDetails.vue | 22 ++- .../History/CurrentHistory/HistoryPanel.vue | 5 + .../History/Layout/DetailsLayout.vue | 43 ++++- .../History/Modals/SelectorModal.vue | 29 +++- .../History/Multiple/MultipleView.test.js | 63 ++++--- .../History/Multiple/MultipleView.vue | 156 ++++++++++++------ .../History/Multiple/MultipleViewItem.vue | 7 +- .../History/Multiple/MultipleViewList.vue | 6 +- client/src/stores/activitySetup.ts | 11 ++ client/src/stores/historyStore.ts | 6 +- 11 files changed, 285 insertions(+), 91 deletions(-) diff --git a/client/src/components/Common/TextSummary.vue b/client/src/components/Common/TextSummary.vue index 9f0ba44c566d..2a44a54fff44 100644 --- a/client/src/components/Common/TextSummary.vue +++ b/client/src/components/Common/TextSummary.vue @@ -7,12 +7,21 @@ import { computed, ref } from "vue"; library.add(faChevronUp, faChevronDown); interface Props { + /** The maximum length of the unexpanded text / summary */ maxLength?: number; + /** The text to summarize */ description: string; + /** If `true`, doesn't let unexpanded text go beyond height of one line */ + oneLineSummary?: boolean; + /** If `true`, doesn't show expand/collapse buttons */ + noExpand?: boolean; + /** The component to use for the summary, default = `

` */ + component?: string; } const props = withDefaults(defineProps(), { maxLength: 150, + component: "p", }); const showDetails = ref(false); @@ -27,14 +36,29 @@ const text = computed(() => + + diff --git a/client/src/components/History/CurrentHistory/HistoryDetails.vue b/client/src/components/History/CurrentHistory/HistoryDetails.vue index 3151019aeb60..d125b74850d2 100644 --- a/client/src/components/History/CurrentHistory/HistoryDetails.vue +++ b/client/src/components/History/CurrentHistory/HistoryDetails.vue @@ -2,15 +2,19 @@ import type { HistorySummary } from "@/api"; import { useHistoryStore } from "@/stores/historyStore"; +import TextSummary from "@/components/Common/TextSummary.vue"; import DetailsLayout from "@/components/History/Layout/DetailsLayout.vue"; +import UtcDate from "@/components/UtcDate.vue"; interface Props { history: HistorySummary; writeable: boolean; + summarized: boolean; } const props = withDefaults(defineProps(), { writeable: true, + summarized: false, }); const historyStore = useHistoryStore(); @@ -27,10 +31,26 @@ function onSave(newDetails: HistorySummary) { :annotation="history.annotation || ''" :tags="history.tags" :writeable="writeable" + :summarized="summarized" + :update-time="history.update_time" @save="onSave"> + diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue index 32cf6c822d91..264a66b2c289 100644 --- a/client/src/components/History/CurrentHistory/HistoryPanel.vue +++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue @@ -66,6 +66,10 @@ const props = defineProps({ type: Boolean, default: false, }, + isMultiViewItem: { + type: Boolean, + default: false + }, }); const filterClass = HistoryFilters; @@ -397,6 +401,7 @@ onMounted(async () => { diff --git a/client/src/components/History/Layout/DetailsLayout.vue b/client/src/components/History/Layout/DetailsLayout.vue index 5b1e975d7c55..1f73da6e7c76 100644 --- a/client/src/components/History/Layout/DetailsLayout.vue +++ b/client/src/components/History/Layout/DetailsLayout.vue @@ -9,6 +9,7 @@ import { computed, ref } from "vue"; import { useUserStore } from "@/stores/userStore"; import l from "@/utils/localization"; +import TextSummary from "@/components/Common/TextSummary.vue"; import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue"; library.add(faPen, faSave, faUndo); @@ -19,6 +20,7 @@ interface Props { writeable?: boolean; annotation?: string; showAnnotation?: boolean; + summarized?: boolean; } const props = withDefaults(defineProps(), { @@ -27,6 +29,7 @@ const props = withDefaults(defineProps(), { writeable: true, annotation: undefined, showAnnotation: true, + summarized: false, }); const emit = defineEmits(["save"]); @@ -87,7 +90,10 @@ function selectText() { + + \ No newline at end of file diff --git a/client/src/components/History/Modals/SelectorModal.vue b/client/src/components/History/Modals/SelectorModal.vue index 00052ea4e50a..98311f6906dc 100644 --- a/client/src/components/History/Modals/SelectorModal.vue +++ b/client/src/components/History/Modals/SelectorModal.vue @@ -5,6 +5,7 @@ import { faArrowDown, faColumns, faSignInAlt } from "@fortawesome/free-solid-svg import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { useInfiniteScroll } from "@vueuse/core"; import { BBadge, BButton, BButtonGroup, BFormGroup, BListGroup, BListGroupItem, BModal } from "bootstrap-vue"; +import { orderBy } from "lodash"; import isEqual from "lodash.isequal"; import { storeToRefs } from "pinia"; import { computed, onMounted, onUnmounted, type PropType, type Ref, ref, watch } from "vue"; @@ -72,10 +73,9 @@ const modal: Ref = ref(null); const scrollableDiv: Ref = ref(null); const historyStore = useHistoryStore(); -const { currentHistoryId, totalHistoryCount } = storeToRefs(useHistoryStore()); +const { currentHistoryId, totalHistoryCount, pinnedHistories } = storeToRefs(useHistoryStore()); const { currentUser } = storeToRefs(useUserStore()); -const pinnedHistories: Ref<{ id: string }[]> = computed(() => historyStore.pinnedHistories); const hasNoResults = computed(() => filter.value && filtered.value.length == 0); const validFilter = computed(() => filter.value && filter.value.length > 2); const allLoaded = computed(() => totalHistoryCount.value <= filtered.value.length); @@ -153,6 +153,12 @@ const filtered: Ref = computed(() => { }); }); +/** if pinned histories and selected histories are equal */ +const pinnedSelectedEqual = computed(() => { + // uses `orderBy` to ensure same ids are found in both `{ id: string }[]` arrays + return isEqual(orderBy(pinnedHistories.value, ["id"], ["asc"]), orderBy(selectedHistories.value, ["id"], ["asc"])); +}); + function historyClicked(history: HistorySummary) { if (props.multiple) { const index = selectedHistories.value.findIndex((item) => item.id == history.id); @@ -249,10 +255,18 @@ async function loadMore(noScroll = false) { :active="selectedHistories.some((h) => h.id === history.id)" @click="() => historyClicked(history)">

- - {{ history.name }} - (Current) - +
+ + {{ history.name }} + (Current) + + + (currently pinned) + +
@@ -333,10 +347,11 @@ async function loadMore(noScroll = false) {
+ {{ selectedHistories.length }} histories selected Change Selected diff --git a/client/src/components/History/Multiple/MultipleView.test.js b/client/src/components/History/Multiple/MultipleView.test.js index 13fa2ff4948b..6175c7c69bd1 100644 --- a/client/src/components/History/Multiple/MultipleView.test.js +++ b/client/src/components/History/Multiple/MultipleView.test.js @@ -10,55 +10,76 @@ import { getLocalVue } from "tests/jest/helpers"; import MultipleView from "./MultipleView"; -const COUNT = 8; const USER_ID = "test-user-id"; -const CURRENT_HISTORY_ID = "test-history-id-0"; - -const pinia = createPinia(); +const FIRST_HISTORY_ID = "test-history-id-0"; const getFakeHistorySummaries = (num, selectedIndex) => { return Array.from({ length: num }, (_, index) => ({ - id: selectedIndex === index ? CURRENT_HISTORY_ID : `test-history-id-${index}`, + id: `test-history-id-${index}`, name: `History-${index}`, tags: [], update_time: new Date().toISOString(), })); }; const currentUser = { id: USER_ID }; -const UserHistoriesMock = MockUserHistories({ id: CURRENT_HISTORY_ID }, getFakeHistorySummaries(COUNT, 0), false); - -const localVue = getLocalVue(); describe("MultipleView", () => { - let wrapper; - let axiosMock; - - beforeEach(async () => { - axiosMock = new MockAdapter(axios); - wrapper = mount(MultipleView, { - pinia, + async function setUpWrapper(UserHistoriesMock, count, currentHistoryId) { + const axiosMock = new MockAdapter(axios); + axiosMock.onGet(`api/histories/${FIRST_HISTORY_ID}`).reply(200, {}); + const wrapper = mount(MultipleView, { + pinia: createPinia(), stubs: { UserHistories: UserHistoriesMock, HistoryPanel: true, + icon: { template: "
" }, }, - localVue, + localVue: getLocalVue(), }); const userStore = useUserStore(); userStore.currentUser = currentUser; const historyStore = useHistoryStore(); - historyStore.setHistories(getFakeHistorySummaries(COUNT, 0)); - historyStore.setCurrentHistoryId(CURRENT_HISTORY_ID); + historyStore.setHistories(getFakeHistorySummaries(count, 0)); + historyStore.setCurrentHistoryId(currentHistoryId); await flushPromises(); - }); - afterEach(() => { + return { wrapper, axiosMock }; + } + + it("more than 4 histories should not show the current history", async () => { + const count = 8; + const currentHistoryId = FIRST_HISTORY_ID; + + // Set up UserHistories and wrapper + const UserHistoriesMock = MockUserHistories({ id: currentHistoryId }, getFakeHistorySummaries(count, 0), false); + const { wrapper, axiosMock } = await setUpWrapper(UserHistoriesMock, count, currentHistoryId); + + // Test: current (first) history should not be shown because only 4 latest are shown by default + expect(wrapper.find("button[title='Current History']").exists()).toBeFalsy(); + + expect(wrapper.find("button[title='Switch to this history']").exists()).toBeTruthy(); + + expect(wrapper.find("div[title='Currently showing 4 most recently updated histories']").exists()).toBeTruthy(); + + expect(wrapper.find("[data-description='open select histories modal']").exists()).toBeTruthy(); + axiosMock.reset(); }); - it("should show the current history", async () => { + it("less than or equal to 4 histories should not show the current history", async () => { + const count = 3; + const currentHistoryId = FIRST_HISTORY_ID; + + // Set up UserHistories and wrapper + const UserHistoriesMock = MockUserHistories({ id: currentHistoryId }, getFakeHistorySummaries(count, 0), false); + const { wrapper, axiosMock } = await setUpWrapper(UserHistoriesMock, count, currentHistoryId); + + // Test: current (first) history should be shown because only 4 latest are shown by default, and count = 3 expect(wrapper.find("button[title='Current History']").exists()).toBeTruthy(); + + axiosMock.reset(); }); }); diff --git a/client/src/components/History/Multiple/MultipleView.vue b/client/src/components/History/Multiple/MultipleView.vue index 048efe383dc9..9b2c5c92208f 100644 --- a/client/src/components/History/Multiple/MultipleView.vue +++ b/client/src/components/History/Multiple/MultipleView.vue @@ -1,10 +1,11 @@ + + + + diff --git a/client/src/components/History/Modals/SelectorModal.vue b/client/src/components/History/Modals/SelectorModal.vue index 98311f6906dc..7c9f466755da 100644 --- a/client/src/components/History/Modals/SelectorModal.vue +++ b/client/src/components/History/Modals/SelectorModal.vue @@ -1,4 +1,8 @@