From a16c9c065a0fa0a78280b0cb8d0e615bedf1796f Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 9 Jan 2024 13:37:40 +0500 Subject: [PATCH] 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 | 34 +++- .../History/CurrentHistory/HistoryDetails.vue | 23 ++- .../History/CurrentHistory/HistoryPanel.vue | 7 +- .../History/Layout/DetailsLayout.vue | 42 ++++- .../History/Modals/SelectorModal.vue | 29 +++- .../History/Multiple/MultipleView.test.js | 63 ++++--- .../History/Multiple/MultipleView.vue | 156 ++++++++++++------ .../History/Multiple/MultipleViewItem.vue | 8 +- .../History/Multiple/MultipleViewList.vue | 6 +- client/src/stores/activitySetup.ts | 11 ++ client/src/stores/historyStore.ts | 6 +- 11 files changed, 294 insertions(+), 91 deletions(-) diff --git a/client/src/components/Common/TextSummary.vue b/client/src/components/Common/TextSummary.vue index 277a46e07007..de9263f3687e 100644 --- a/client/src/components/Common/TextSummary.vue +++ b/client/src/components/Common/TextSummary.vue @@ -5,14 +5,32 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { computed } from "vue"; const props = defineProps({ + /** The text to summarize */ description: { type: String, required: true, }, + /** If `true`, doesn't let unexpanded text go beyond height of one line */ + oneLineSummary: { + type: Boolean, + default: false, + }, + /** If `true`, doesn't show expand/collapse buttons */ + noExpand: { + type: Boolean, + default: false, + }, + /** The component to use for the summary, default = `

` */ + component: { + type: String, + default: "p", + }, + /** Used as the toggle for expanding summary */ showDetails: { type: Boolean, default: false, }, + /** The maximum length of the unexpanded text / summary */ maxLength: { type: Number, default: 150, @@ -47,8 +65,11 @@ const text = computed(() => + + diff --git a/client/src/components/History/CurrentHistory/HistoryDetails.vue b/client/src/components/History/CurrentHistory/HistoryDetails.vue index 075100cc7517..fc38695c49e8 100644 --- a/client/src/components/History/CurrentHistory/HistoryDetails.vue +++ b/client/src/components/History/CurrentHistory/HistoryDetails.vue @@ -4,10 +4,26 @@ :annotation="history.annotation" :tags="history.tags" :writeable="writeable" + :summarized="summarized" + :update-time="history.update_time" @save="onSave"> + @@ -18,11 +34,15 @@ import { mapActions } from "pinia"; import short from "@/components/plugins/short.js"; 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"; export default { components: { DetailsLayout, + TextSummary, + UtcDate, }, directives: { short, @@ -30,6 +50,7 @@ export default { props: { history: { type: Object, required: true }, writeable: { type: Boolean, default: true }, + summarized: { type: Boolean, default: false }, }, methods: { ...mapActions(useHistoryStore, ["updateHistory"]), diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue index e335cfd8c709..496465b0fee2 100644 --- a/client/src/components/History/CurrentHistory/HistoryPanel.vue +++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue @@ -38,7 +38,11 @@ :search-error="searchError" :show-advanced.sync="showAdvanced" />

- + -
+
-
- +
+
+ +
+ +
@@ -72,11 +94,13 @@ import { mapState } from "pinia"; import short from "@/components/plugins/short.js"; import { useUserStore } from "@/stores/userStore"; +import TextSummary from "@/components/Common/TextSummary.vue"; import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue"; export default { components: { StatelessTags, + TextSummary, }, directives: { short, @@ -85,6 +109,7 @@ export default { name: { type: String, default: null }, annotation: { type: String, default: null }, showAnnotation: { type: Boolean, default: true }, + summarized: { type: Boolean, default: false }, tags: { type: Array, default: null }, writeable: { type: Boolean, default: true }, }, @@ -140,3 +165,14 @@ export default { }, }; + + 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 @@