Skip to content

Commit

Permalink
add show recent functionality to multiview, uniform multi history view
Browse files Browse the repository at this point in the history
This adds the option of showing 4 latest histories in selector modal,
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.
  • Loading branch information
ahmedhamidawan committed Jan 9, 2024
1 parent 4e33216 commit b22e033
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 91 deletions.
34 changes: 32 additions & 2 deletions client/src/components/Common/TextSummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<p>` */
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,
Expand Down Expand Up @@ -47,8 +65,11 @@ const text = computed(() =>

<template>
<div>
{{ text }}
<span v-if="summary">
<component :is="props.component" v-if="props.oneLineSummary" class="one-line-summary">{{
props.description
}}</component>
<span v-else>{{ text }}</span>
<span v-if="!noExpand && summary">
<a
v-if="!propShowDetails"
class="text-summary-expand"
Expand All @@ -62,3 +83,12 @@ const text = computed(() =>
</span>
</div>
</template>

<style scoped>
.one-line-summary {
max-height: 2em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
23 changes: 22 additions & 1 deletion client/src/components/History/CurrentHistory/HistoryDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@
:annotation="history.annotation"
:tags="history.tags"
:writeable="writeable"
:summarized="summarized"
:update-time="history.update_time"
@save="onSave">
<template v-slot:name>
<!-- eslint-disable-next-line vuejs-accessibility/heading-has-content -->
<h3 v-short="history.name || 'History'" data-description="name display" class="my-2" />
<h3 v-if="!summarized" v-short="history.name || 'History'" data-description="name display" class="my-2" />
<TextSummary
v-else
:description="history.name"
data-description="name display"
class="my-2"
component="h3"
one-line-summary
no-expand />
</template>
<template v-if="summarized" v-slot:update-time>
<b-badge v-b-tooltip pill>
<span v-localize>last edited </span>
<UtcDate v-if="history.update_time" :date="history.update_time" mode="elapsed" />
</b-badge>
</template>
</DetailsLayout>
</template>
Expand All @@ -18,18 +34,23 @@ 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,
},
props: {
history: { type: Object, required: true },
writeable: { type: Boolean, default: true },
summarized: { type: Boolean, default: false },
},
methods: {
...mapActions(useHistoryStore, ["updateHistory"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@
:search-error="searchError"
:show-advanced.sync="showAdvanced" />
<section v-if="!showAdvanced">
<HistoryDetails :history="history" :writeable="writable" @update:history="updateHistory($event)" />
<HistoryDetails
:history="history"
:writeable="writable"
:summarized="isMultiViewItem"
@update:history="updateHistory($event)" />
<HistoryMessages :history="history" />
<HistoryCounter
:history="history"
Expand Down Expand Up @@ -199,6 +203,7 @@ export default {
writable: { type: Boolean, default: true },
showControls: { type: Boolean, default: true },
filterable: { type: Boolean, default: false },
isMultiViewItem: { type: Boolean, default: false },
},
setup() {
const { currentFilterText, currentHistoryId } = storeToRefs(useHistoryStore());
Expand Down
42 changes: 39 additions & 3 deletions client/src/components/History/Layout/DetailsLayout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<section class="m-3 details" data-description="edit details">
<section
class="details"
:class="summarized && !editing ? 'summarized-details' : 'm-3'"
data-description="edit details">
<b-button
:disabled="isAnonymous || !writeable"
class="edit-button ml-1 float-right"
Expand All @@ -15,8 +18,27 @@

<!-- display annotation, tags -->
<div v-if="!editing">
<div v-if="annotation" v-short="annotation" class="mt-2" data-description="annotation value" />
<StatelessTags v-if="tags" class="tags mt-2" :value="tags" :disabled="true" />
<div
v-if="annotation && !summarized"
v-short="annotation"
class="mt-2"
data-description="annotation value" />
<div v-else-if="summarized" style="min-height: 2rem">
<TextSummary
v-if="annotation"
:description="annotation"
data-description="annotation value"
one-line-summary
no-expand />
</div>
<StatelessTags
v-if="tags"
class="tags"
:class="!summarized && 'mt-2'"
:value="tags"
disabled
:max-visible-tags="summarized ? 1 : 5" />
<slot v-if="summarized" name="update-time" />
</div>

<!-- edit form, change title, annotation, or tags -->
Expand Down Expand Up @@ -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,
Expand All @@ -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 },
},
Expand Down Expand Up @@ -140,3 +165,14 @@ export default {
},
};
</script>

<style lang="scss" scoped>
.summarized-details {
min-height: 8.5em;
margin: 1rem 1rem 0 1rem;
.tags {
min-height: 2rem;
}
}
</style>
29 changes: 22 additions & 7 deletions client/src/components/History/Modals/SelectorModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -72,10 +73,9 @@ const modal: Ref<BModal | null> = ref(null);
const scrollableDiv: Ref<HTMLElement | null> = 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);
Expand Down Expand Up @@ -153,6 +153,12 @@ const filtered: Ref<HistorySummary[]> = 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);
Expand Down Expand Up @@ -249,10 +255,18 @@ async function loadMore(noScroll = false) {
:active="selectedHistories.some((h) => h.id === history.id)"
@click="() => historyClicked(history)">
<div class="d-flex justify-content-between align-items-center">
<Heading h3 inline bold size="text">
{{ history.name }}
<i v-if="history.id === currentHistoryId">(Current)</i>
</Heading>
<div>
<Heading h3 inline bold size="text">
{{ history.name }}
<i v-if="history.id === currentHistoryId">(Current)</i>
</Heading>
<i
v-if="props.multiple && pinnedHistories.some((h) => h.id === history.id)"
v-b-tooltip.noninteractive.hover
title="This history is currently pinned in the multi-history view">
(currently pinned)
</i>
</div>

<div class="d-flex align-items-center flex-gapx-1">
<BBadge v-b-tooltip pill :title="localize('Amount of items in history')">
Expand Down Expand Up @@ -333,10 +347,11 @@ async function loadMore(noScroll = false) {
<FontAwesomeIcon icon="fa-arrow-down" />
</BButton>
</div>
<i v-if="multiple">{{ selectedHistories.length }} histories selected</i>
<BButton
v-if="multiple"
v-localize
:disabled="selectedHistories.length === 0 || isEqual(selectedHistories, pinnedHistories)"
:disabled="pinnedSelectedEqual || showAdvanced"
variant="primary"
@click="selectHistories">
Change Selected
Expand Down
63 changes: 42 additions & 21 deletions client/src/components/History/Multiple/MultipleView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<div></div>" },
},
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();
});
});
Loading

0 comments on commit b22e033

Please sign in to comment.