Merge pull request #18265 from ahmedhamidawan/multiview_top_bar

Move history multiview item actions to top
This commit is contained in:
David López
2024-07-26 15:33:52 +02:00
committed by GitHub
16 changed files with 384 additions and 162 deletions
@@ -1,4 +1,5 @@
import {
faBurn,
faExchangeAlt,
faEye,
faPlus,
@@ -10,7 +11,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { useEventBus } from "@vueuse/core";
import { deleteHistories, deleteHistory, historiesFetcher, undeleteHistories, undeleteHistory } from "@/api/histories";
import { historiesFetcher } from "@/api/histories";
import { updateTags } from "@/api/tags";
import { useHistoryStore } from "@/stores/historyStore";
import Filtering, { contains, equals, expandNameTag, toBool, type ValidFilter } from "@/utils/filtering";
@@ -70,7 +71,8 @@ const batch: BatchOperationArray = [
if (confirm(_l(`Are you sure that you want to delete the selected histories?`))) {
try {
const historyIds = data.map((x) => String(x.id));
await deleteHistories({ ids: historyIds });
const historyStore = useHistoryStore();
await historyStore.deleteHistories(historyIds);
return {
status: "success",
message: `Deleted ${data.length} histories.`,
@@ -92,7 +94,8 @@ const batch: BatchOperationArray = [
if (confirm(_l(`Are you sure that you want to restore the selected histories?`))) {
try {
const historyIds = data.map((x) => String(x.id));
await undeleteHistories({ ids: historyIds });
const historyStore = useHistoryStore();
await historyStore.restoreHistories(historyIds);
return {
status: "success",
message: `Restored ${data.length} histories.`,
@@ -108,13 +111,14 @@ const batch: BatchOperationArray = [
},
{
title: "Purge",
icon: faTrash,
icon: faBurn,
condition: (data: Array<HistoryEntry>) => !data.some((x) => x.purged),
handler: async (data: Array<HistoryEntry>) => {
if (confirm(_l(`Are you sure that you want to permanently delete the selected histories?`))) {
try {
const historyIds = data.map((x) => String(x.id));
await deleteHistories({ ids: historyIds, purge: true });
const historyStore = useHistoryStore();
await historyStore.deleteHistories(historyIds, true);
return {
status: "success",
message: `Purged ${data.length} histories.`,
@@ -185,7 +189,8 @@ const fields: FieldArray = [
handler: async (data: HistoryEntry) => {
if (confirm(_l("Are you sure that you want to delete this history?"))) {
try {
await deleteHistory({ history_id: String(data.id) });
const historyStore = useHistoryStore();
await historyStore.deleteHistory(String(data.id));
return {
status: "success",
message: `'${data.name}' has been deleted.`,
@@ -201,12 +206,13 @@ const fields: FieldArray = [
},
{
title: "Delete Permanently",
icon: faTrash,
icon: faBurn,
condition: (data: HistoryEntry) => !data.purged,
handler: async (data: HistoryEntry) => {
if (confirm(_l("Are you sure that you want to permanently delete this history?"))) {
try {
await deleteHistory({ history_id: String(data.id), purge: true });
const historyStore = useHistoryStore();
await historyStore.deleteHistory(String(data.id), true);
return {
status: "success",
message: `'${data.name}' has been permanently deleted.`,
@@ -226,7 +232,8 @@ const fields: FieldArray = [
condition: (data: HistoryEntry) => !!data.deleted && !data.purged,
handler: async (data: HistoryEntry) => {
try {
await undeleteHistory({ history_id: String(data.id) });
const historyStore = useHistoryStore();
await historyStore.restoreHistory(String(data.id));
return {
status: "success",
message: `'${data.name}' has been restored.`,
@@ -1,22 +1,22 @@
<script setup lang="ts">
import { BBadge } from "bootstrap-vue";
import type { HistorySummary } from "@/api";
import { useHistoryStore } from "@/stores/historyStore";
import type { DetailsLayoutSummarized } from "../Layout/types";
import HistoryIndicators from "../HistoryIndicators.vue";
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;
summarized?: DetailsLayoutSummarized;
}
const props = withDefaults(defineProps<Props>(), {
writeable: true,
summarized: false,
summarized: undefined,
});
const historyStore = useHistoryStore();
@@ -49,10 +49,7 @@ function onSave(newDetails: HistorySummary) {
no-expand />
</template>
<template v-if="summarized" v-slot:update-time>
<BBadge v-b-tooltip pill>
<span v-localize>last edited </span>
<UtcDate v-if="history.update_time" :date="history.update_time" mode="elapsed" />
</BBadge>
<HistoryIndicators :history="history" detailed-time />
</template>
</DetailsLayout>
</template>
@@ -32,7 +32,7 @@ const currentUserOwnsHistory = computed(() => {
<div v-if="hasMessages" class="mx-3 mt-2" data-description="history messages">
<BAlert v-if="history.purged" :show="history.purged" variant="warning">
<FontAwesomeIcon :icon="faBurn" fixed-width />
{{ localize("History has been purged") }}
{{ localize("History has been permanently deleted") }}
</BAlert>
<BAlert v-else-if="history.deleted" :show="history.deleted" variant="warning">
<FontAwesomeIcon :icon="faTrash" fixed-width />
@@ -3,6 +3,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArchive,
faBars,
faBurn,
faColumns,
faCopy,
faExchangeAlt,
@@ -44,6 +45,7 @@ import SelectorModal from "@/components/History/Modals/SelectorModal.vue";
library.add(
faArchive,
faBars,
faBurn,
faColumns,
faCopy,
faExchangeAlt,
@@ -62,16 +64,21 @@ library.add(
interface Props {
histories: HistorySummary[];
history: HistorySummary;
title?: string;
historiesLoading?: boolean;
minimal?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
title: "Histories",
historiesLoading: false,
minimal: false,
});
// modal refs
const showSwitchModal = ref(false);
const showDeleteModal = ref(false);
const showPrivacyModal = ref(false);
const showCopyModal = ref(false);
const purgeHistory = ref(false);
const userStore = useUserStore();
@@ -84,6 +91,10 @@ const canEditHistory = computed(() => {
return canMutateHistory(props.history);
});
const isDeletedNotPurged = computed(() => {
return props.history.deleted && !props.history.purged;
});
const historyState = computed(() => {
if (props.history.purged) {
return "purged";
@@ -115,11 +126,14 @@ function userTitle(title: string) {
<template>
<div>
<nav class="d-flex justify-content-between mx-3 my-2" aria-label="current history management">
<h2 class="m-1 h-sm">History</h2>
<nav
:class="{ 'd-flex justify-content-between mx-3 my-2': !props.minimal }"
aria-label="current history management">
<h2 v-if="!props.minimal" class="m-1 h-sm">History</h2>
<BButtonGroup>
<BButton
v-if="!props.minimal"
v-b-tooltip.top.hover.noninteractive
class="create-hist-btn"
data-description="create new history"
@@ -132,6 +146,7 @@ function userTitle(title: string) {
</BButton>
<BButton
v-if="!props.minimal"
v-b-tooltip.top.hover.noninteractive
data-description="switch to another history"
size="sm"
@@ -146,10 +161,11 @@ function userTitle(title: string) {
v-b-tooltip.top.hover.noninteractive
no-caret
size="sm"
variant="link"
:variant="props.minimal ? 'outline-info' : 'link'"
toggle-class="text-decoration-none"
menu-class="history-options-button-menu"
title="History options"
right
data-description="history options">
<template v-slot:button-content>
<FontAwesomeIcon fixed-width :icon="faBars" />
@@ -162,10 +178,12 @@ function userTitle(title: string) {
<span>Fetching histories from server</span>
</div>
<span v-else>You have {{ totalHistoryCount }} histories.</span>
<span v-else-if="!props.minimal">You have {{ totalHistoryCount }} histories.</span>
<span v-else>Manage History</span>
</BDropdownText>
<BDropdownItem
v-if="!props.minimal"
data-description="switch to multi history view"
:disabled="isAnonymous"
:title="userTitle('Open History Multiview')"
@@ -174,7 +192,7 @@ function userTitle(title: string) {
<span v-localize>Show Histories Side-by-Side</span>
</BDropdownItem>
<BDropdownDivider />
<BDropdownDivider v-if="!props.minimal" />
<BDropdownText v-if="!canEditHistory">
This history has been <span class="font-weight-bold">{{ historyState }}</span
@@ -195,19 +213,20 @@ function userTitle(title: string) {
<BDropdownDivider />
<BDropdownItem
v-b-modal:copy-current-history-modal
:disabled="isAnonymous"
:title="userTitle('Copy History to a New History')">
:title="userTitle('Copy History to a New History')"
@click="showCopyModal = !showCopyModal">
<FontAwesomeIcon fixed-width :icon="faCopy" class="mr-1" />
<span v-localize>Copy this History</span>
</BDropdownItem>
<BDropdownItem
v-b-modal:delete-history-modal
:disabled="!canEditHistory"
:title="localize('Permanently Delete History')">
<FontAwesomeIcon fixed-width :icon="faTrash" class="mr-1" />
<span v-localize>Delete this History</span>
:title="localize(isDeletedNotPurged ? 'Permanently Delete History' : 'Delete History')"
@click="showDeleteModal = !showDeleteModal">
<FontAwesomeIcon fixed-width :icon="isDeletedNotPurged ? faBurn : faTrash" class="mr-1" />
<span v-if="isDeletedNotPurged" v-localize>Permanently Delete History</span>
<span v-else v-localize>Delete this History</span>
</BDropdownItem>
<BDropdownItem
@@ -271,9 +290,9 @@ function userTitle(title: string) {
</BDropdownItem>
<BDropdownItem
v-b-modal:history-privacy-modal
:disabled="isAnonymous || !canEditHistory"
:title="userTitle('Make this History Private')">
:title="userTitle('Make this History Private')"
@click="showPrivacyModal = !showPrivacyModal">
<FontAwesomeIcon fixed-width :icon="faLock" class="mr-1" />
<span v-localize>Make Private</span>
</BDropdownItem>
@@ -282,6 +301,7 @@ function userTitle(title: string) {
</nav>
<SelectorModal
v-if="!props.minimal"
v-show="showSwitchModal"
id="selector-history-modal"
:histories="histories"
@@ -289,13 +309,19 @@ function userTitle(title: string) {
:show-modal.sync="showSwitchModal"
@selectHistory="historyStore.setCurrentHistory($event.id)" />
<CopyModal id="copy-current-history-modal" :history="history" />
<CopyModal :history="history" :show-modal.sync="showCopyModal" />
<BModal
id="history-privacy-modal"
v-model="showPrivacyModal"
title="Make History Private"
title-tag="h2"
@ok="historyStore.secureHistory(history)">
<h4>
History:
<b>
<i>{{ history.name }}</i>
</b>
</h4>
<p v-localize>
This will make all the data in this history private (excluding library datasets), and will set
permissions such that all new data is created as private. Any datasets within that are currently shared
@@ -304,15 +330,15 @@ function userTitle(title: string) {
</BModal>
<BModal
id="delete-history-modal"
title="Delete History?"
v-model="showDeleteModal"
:title="isDeletedNotPurged ? 'Permanently Delete History?' : 'Delete History?'"
title-tag="h2"
@ok="onDelete"
@show="purgeHistory = false">
@show="purgeHistory = isDeletedNotPurged">
<p v-localize>
Do you also want to permanently delete the history <i class="ml-1">{{ history.name }}</i>
</p>
<BFormCheckbox id="purge-history" v-model="purgeHistory">
<BFormCheckbox id="purge-history" v-model="purgeHistory" :disabled="isDeletedNotPurged">
<span v-localize>Yes, permanently delete this history.</span>
</BFormCheckbox>
</BModal>
@@ -12,6 +12,7 @@ library.add(faCheckSquare, faCompress);
interface Props {
history: HistorySummaryExtended;
hasMatches: boolean;
editable: boolean;
expandedCount: number;
showSelection: boolean;
isMultiViewItem: boolean;
@@ -32,7 +33,7 @@ function onUpdateOperationStatus(updateTime: number) {
<template>
<section>
<nav class="content-operations d-flex justify-content-between bg-secondary">
<nav v-if="editable" class="content-operations d-flex justify-content-between bg-secondary">
<BButtonGroup>
<BButton
title="Select Items"
@@ -66,6 +67,17 @@ function onUpdateOperationStatus(updateTime: number) {
:history="history"
@update:operation-running="onUpdateOperationStatus" />
</nav>
<nav v-else-if="isMultiViewItem" class="content-operations bg-secondary">
<BButton
title="Collapse Items"
class="rounded-0"
size="sm"
variant="link"
:disabled="!expandedCount"
@click="$emit('collapse-all')">
<FontAwesomeIcon :icon="faCompress" fixed-width />
</BButton>
</nav>
</section>
</template>
@@ -84,7 +84,7 @@ const contentItemRefs = computed(() => {
const currItemFocused = useActiveElement();
const lastItemId = ref<string | null>(null);
const { currentFilterText, currentHistoryId } = storeToRefs(useHistoryStore());
const { currentFilterText, currentHistoryId, pinnedHistories, storedHistories } = storeToRefs(useHistoryStore());
const { lastCheckedTime, totalMatchesCount, isWatching } = storeToRefs(useHistoryItemsStore());
const historyStore = useHistoryStore();
@@ -143,6 +143,47 @@ const formattedSearchError = computed(() => {
};
});
/** Returns a string that indicates whether all the pinned histories have annotations,
* tags, both, or none of them.
* This is used to format the `DetailsLayout` header uniformly for multi-view histories.
*/
const detailsSummarized = computed(() => {
if (!props.isMultiViewItem) {
return undefined;
}
if (pinnedHistories.value.length === 0) {
return "hidden";
}
let annotation = false;
let tags = false;
let loaded = true;
for (const h of pinnedHistories.value) {
const history = storedHistories.value[h.id];
if (!history) {
loaded = false;
break;
}
if (history.annotation) {
annotation = true;
}
if (history.tags.length > 0) {
tags = true;
}
}
if (!loaded || (annotation && tags)) {
return "both";
} else if (annotation) {
return "annotation";
} else if (tags) {
return "tags";
} else {
return "none";
}
});
const storeFilterText = computed(() => {
if (props.history.id !== currentHistoryId.value) {
return "";
@@ -523,10 +564,10 @@ function setItemDragstart(
<HistoryDetails
:history="history"
:writeable="canEditHistory"
:summarized="isMultiViewItem"
:summarized="detailsSummarized"
@update:history="historyStore.updateHistory($event)" />
<HistoryMessages :history="history" :current-user="currentUser" />
<HistoryMessages v-if="!isMultiViewItem" :history="history" :current-user="currentUser" />
<HistoryCounter
:history="history"
@@ -539,8 +580,8 @@ function setItemDragstart(
@reloadContents="reloadContents" />
<HistoryOperations
v-if="canEditHistory"
:history="history"
:editable="canEditHistory"
:is-multi-view-item="isMultiViewItem"
:show-selection="showSelection"
:expanded-count="expandedCount"
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faArchive, faBurn, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { type HistorySummary, userOwnsHistory } from "@/api";
import { useUserStore } from "@/stores/userStore";
import localize from "@/utils/localization";
import UtcDate from "../UtcDate.vue";
library.add(faArchive, faBurn, faTrash);
const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
const props = defineProps<{
history: HistorySummary;
includeCount?: boolean;
detailedTime?: boolean;
}>();
</script>
<template>
<div class="d-flex align-items-center flex-gapx-1">
<BBadge v-if="props.includeCount" pill :title="localize('Amount of items in history')">
{{ history.count }} {{ localize("items") }}
</BBadge>
<BBadge v-if="history.update_time" pill :title="localize('Last edited')">
<span v-if="props.detailedTime" v-localize>last edited </span>
<UtcDate :date="history.update_time" mode="elapsed" />
</BBadge>
<BBadge v-if="props.history.purged" pill class="alert-warning" title="Permanently deleted">
<FontAwesomeIcon :icon="faBurn" fixed-width />
</BBadge>
<BBadge v-else-if="history.deleted" pill class="alert-danger" title="Deleted">
<FontAwesomeIcon :icon="faTrash" fixed-width />
</BBadge>
<BBadge v-if="history.archived && userOwnsHistory(currentUser, props.history)" pill title="Archived">
<FontAwesomeIcon :icon="faArchive" fixed-width />
</BBadge>
</div>
</template>
@@ -19,10 +19,10 @@ import localize from "@/utils/localization";
import { HistoriesFilters } from "./HistoriesFilters";
import TextSummary from "../Common/TextSummary.vue";
import HistoryIndicators from "./HistoryIndicators.vue";
import Heading from "@/components/Common/Heading.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
import ScrollToTopButton from "@/components/ToolsList/ScrollToTopButton.vue";
import UtcDate from "@/components/UtcDate.vue";
type AdditionalOptions = "set-current" | "multi" | "center";
type PinnedHistory = { id: string };
@@ -277,18 +277,7 @@ async function loadMore(noScroll = false) {
</i>
</div>
<TextSummary v-else component="h4" :description="history.name" one-line-summary />
<div class="d-flex align-items-center flex-gapx-1">
<BBadge pill :title="localize('Amount of items in history')">
{{ history.count }} {{ localize("items") }}
</BBadge>
<BBadge
v-if="history.update_time"
v-b-tooltip.noninteractive.hover
pill
:title="localize('Last edited')">
<UtcDate :date="history.update_time" mode="elapsed" />
</BBadge>
</div>
<HistoryIndicators :history="history" include-count />
</div>
<p v-if="!isMultiviewPanel && history.annotation" class="my-1">{{ history.annotation }}</p>
@@ -185,7 +185,9 @@ describe("History center panel View", () => {
expect(storageDashboardButtonDisabled(wrapper)).toBeFalsy();
// instead we have an alert
expect(wrapper.find("[data-description='history messages']").text()).toBe("History has been purged");
expect(wrapper.find("[data-description='history messages']").text()).toBe(
"History has been permanently deleted"
);
});
it("should not display archived message and should be importable when user is not owner and history is archived", async () => {
+1 -2
View File
@@ -40,8 +40,7 @@ function onViewCollection(collection: CollectionEntry, currentOffset?: number) {
<HistoryNavigation
:history="currentHistory"
:histories="histories"
:histories-loading="historiesLoading"
title="Histories" />
:histories-loading="historiesLoading" />
</template>
</HistoryPanel>
@@ -9,6 +9,8 @@ import { computed, ref } from "vue";
import { useUserStore } from "@/stores/userStore";
import l from "@/utils/localization";
import type { DetailsLayoutSummarized } from "./types";
import TextSummary from "@/components/Common/TextSummary.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
@@ -20,7 +22,7 @@ interface Props {
writeable?: boolean;
annotation?: string;
showAnnotation?: boolean;
summarized?: boolean;
summarized?: DetailsLayoutSummarized;
}
const props = withDefaults(defineProps<Props>(), {
@@ -29,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
writeable: true,
annotation: undefined,
showAnnotation: true,
summarized: false,
summarized: undefined,
});
const emit = defineEmits(["save"]);
@@ -47,6 +49,20 @@ const localProps = ref<{ name: string; annotation: string | null; tags: string[]
tags: [],
});
const detailsClass = computed(() => {
const classes: Record<string, boolean> = {
details: true,
"summarized-details": props.summarized && !editing.value,
"m-3": !props.summarized || editing.value,
};
if (props.summarized) {
classes[props.summarized] = true;
}
return classes;
});
const editButtonTitle = computed(() => {
if (isAnonymous.value) {
return l("Log in to Rename History");
@@ -90,10 +106,7 @@ function selectText() {
</script>
<template>
<section
class="details"
:class="summarized && !editing ? 'summarized-details' : 'm-3'"
data-description="edit details">
<section :class="detailsClass" data-description="edit details">
<BButton
:disabled="isAnonymous || !writeable"
class="edit-button ml-1 float-right"
@@ -114,7 +127,9 @@ function selectText() {
v-short="annotation"
class="mt-2"
data-description="annotation value" />
<div v-else-if="summarized" style="min-height: 2rem">
<div
v-else-if="summarized"
:class="{ annotation: ['both', 'annotation'].includes(summarized), hidden: summarized === 'hidden' }">
<TextSummary
v-if="annotation"
:description="annotation"
@@ -124,8 +139,11 @@ function selectText() {
</div>
<StatelessTags
v-if="tags"
class="tags"
:class="!summarized && 'mt-2'"
:class="{
'mt-2': !summarized,
tags: ['both', 'tags'].includes(summarized),
hidden: summarized === 'hidden',
}"
:value="tags"
disabled
:max-visible-tags="summarized ? 1 : 5" />
@@ -184,12 +202,20 @@ function selectText() {
<style lang="scss" scoped>
.summarized-details {
min-height: 8.5em;
margin: 1rem 1rem 0 1rem;
margin-left: 0.5rem;
max-width: 15rem;
&.both {
min-height: 8.5em;
}
.tags {
min-height: 2rem;
}
.annotation {
min-height: 2rem;
}
.hidden {
display: none;
}
}
</style>
@@ -0,0 +1 @@
export type DetailsLayoutSummarized = "both" | "annotation" | "tags" | "none" | "hidden";
@@ -20,9 +20,16 @@ import localize from "@/utils/localization";
interface Props {
history: HistorySummary;
showModal?: boolean;
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
showModal: false,
});
const emit = defineEmits<{
(e: "update:show-modal", value: boolean): void;
}>();
const userStore = useUserStore();
const historyStore = useHistoryStore();
@@ -32,6 +39,7 @@ const { currentUser, isAnonymous } = storeToRefs(userStore);
const name = ref("");
const copyAll = ref(false);
const loading = ref(false);
const localShowModal = ref(props.showModal);
const title = computed(() => {
return `Copying History: ${props.history.name}`;
@@ -55,6 +63,18 @@ const formValid = computed(() => {
return newNameValid.value;
});
watch(
() => props.showModal,
(newVal) => {
localShowModal.value = newVal;
}
);
watch(
() => localShowModal.value,
(newVal) => {
emit("update:show-modal", newVal);
}
);
watch(
() => props.history,
(newHistory) => {
@@ -74,7 +94,7 @@ async function copy(close: () => void) {
</script>
<template>
<BModal v-bind="$attrs" :title="title" title-tag="h2" v-on="$listeners">
<BModal v-model="localShowModal" v-bind="$attrs" :title="title" title-tag="h2" v-on="$listeners">
<transition name="fade">
<BAlert v-localize :show="isAnonymous" variant="warning">
As an anonymous user, unless you log in or register, you will lose your current history after copying
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
@@ -6,10 +9,13 @@ import { computed, ref } from "vue";
import { useExtendedHistory } from "@/composables/detailedHistory";
import { useHistoryStore } from "@/stores/historyStore";
import HistoryNavigation from "../CurrentHistory/HistoryNavigation.vue";
import CollectionPanel from "@/components/History/CurrentCollection/CollectionPanel.vue";
import HistoryPanel from "@/components/History/CurrentHistory/HistoryPanel.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
library.add(faTimes);
interface Props {
source: {
id: string;
@@ -20,7 +26,7 @@ interface Props {
const props = defineProps<Props>();
const historyStore = useHistoryStore();
const { currentHistoryId, pinnedHistories } = storeToRefs(historyStore);
const { currentHistoryId, histories, historiesLoading, pinnedHistories } = storeToRefs(historyStore);
const { history } = useExtendedHistory(props.source.id);
@@ -42,7 +48,38 @@ function onViewCollection(collection: object) {
</div>
</div>
<div v-else id="list-item" class="d-flex flex-column align-items-center w-100">
<div v-else id="list-item" class="d-flex flex-column w-100">
<div class="d-flex justify-content-between align-items-center">
<div>
<BButton
size="sm"
class="my-1"
:disabled="sameToCurrent"
:variant="sameToCurrent ? 'disabled' : 'outline-info'"
:title="sameToCurrent ? 'Current History' : 'Switch to this history'"
@click="historyStore.setCurrentHistory(source.id)">
{{ sameToCurrent ? "Current History" : "Switch to" }}
</BButton>
<BButton
v-if="Object.keys(pinnedHistories).length > 0"
size="sm"
class="my-1"
variant="outline-danger"
title="Hide this history from the list"
@click="historyStore.unpinHistories([source.id])">
<FontAwesomeIcon :icon="faTimes" />
Hide
</BButton>
</div>
<HistoryNavigation
:history="history"
:histories="histories"
:histories-loading="historiesLoading"
minimal />
</div>
<hr class="w-100 m-1" />
<CollectionPanel
v-if="selectedCollections.length && selectedCollections[0]?.history_id === source.id"
:history="history"
@@ -57,28 +94,5 @@ function onViewCollection(collection: object) {
:show-controls="false"
is-multi-view-item
@view-collection="onViewCollection" />
<hr class="w-100 m-2" />
<div class="flex-row flex-grow-0">
<BButton
size="sm"
class="my-1"
:disabled="sameToCurrent"
:variant="sameToCurrent ? 'disabled' : 'outline-info'"
:title="sameToCurrent ? 'Current History' : 'Switch to this history'"
@click="historyStore.setCurrentHistory(source.id)">
{{ sameToCurrent ? "Current History" : "Switch to" }}
</BButton>
<BButton
v-if="Object.keys(pinnedHistories).length > 0"
size="sm"
class="my-1"
variant="outline-danger"
title="Hide this history from the list"
@click="historyStore.unpinHistories([source.id])">
Hide
</BButton>
</div>
</div>
</template>
+69 -12
View File
@@ -8,14 +8,19 @@ import {
type HistorySummary,
type HistorySummaryExtended,
} from "@/api";
import { historyFetcher } from "@/api/histories";
import {
deleteHistories as deleteHistoriesByIds,
deleteHistory as deleteHistoryById,
historyFetcher,
undeleteHistories,
undeleteHistory,
} from "@/api/histories";
import { archiveHistory, unarchiveHistory } from "@/api/histories.archived";
import { HistoryFilters } from "@/components/History/HistoryFilters";
import { useUserLocalStorage } from "@/composables/userLocalStorage";
import {
cloneHistory,
createAndSelectNewHistory,
deleteHistoryById,
getCurrentHistoryFromServer,
getHistoryByIdFromServer,
getHistoryCount,
@@ -190,17 +195,64 @@ export const useHistoryStore = defineStore("historyStore", () => {
return filteredHistoryIds[0];
}
async function deleteHistory(historyId: string, purge: boolean) {
const deletedHistory = (await deleteHistoryById(historyId, purge)) as HistorySummary;
const nextAvailableHistoryId = getNextAvailableHistoryId([deletedHistory.id]);
if (nextAvailableHistoryId) {
await setCurrentHistory(nextAvailableHistoryId);
} else {
await createNewHistory();
async function setNextAvailableHistoryId(excludedIds: string[]) {
if (currentHistoryId.value && excludedIds.includes(currentHistoryId.value)) {
const nextAvailableHistoryId = getNextAvailableHistoryId(excludedIds);
if (nextAvailableHistoryId) {
await setCurrentHistory(nextAvailableHistoryId);
} else {
await createNewHistory();
}
}
}
async function deleteHistory(historyId: string, purge = false) {
try {
const { data } = await deleteHistoryById({ history_id: historyId, purge });
const deletedHistory = data as AnyHistory;
await setNextAvailableHistoryId([deletedHistory.id]);
del(storedHistories.value, deletedHistory.id);
await handleTotalCountChange(1, true);
} catch (error) {
rethrowSimple(error);
}
}
async function deleteHistories(ids: string[], purge = false) {
try {
const { data } = await deleteHistoriesByIds({ ids, purge });
const deletedHistories = data as AnyHistory[];
const historyIds = deletedHistories.map((x) => String(x.id));
await setNextAvailableHistoryId(historyIds);
deletedHistories.forEach((history) => {
del(storedHistories.value, history.id);
});
await handleTotalCountChange(deletedHistories.length, true);
} catch (error) {
rethrowSimple(error);
}
}
async function restoreHistory(historyId: string) {
try {
const { data } = await undeleteHistory({ history_id: historyId });
const restoredHistory = data as AnyHistory;
await handleTotalCountChange(1);
setHistory(restoredHistory);
} catch (error) {
rethrowSimple(error);
}
}
async function restoreHistories(ids: string[]) {
try {
const { data } = await undeleteHistories({ ids });
const restoredHistories = data as AnyHistory[];
await handleTotalCountChange(restoredHistories.length);
setHistories(restoredHistories);
} catch (error) {
rethrowSimple(error);
}
del(storedHistories.value, deletedHistory.id);
unpinHistories([deletedHistory.id]);
await handleTotalCountChange(1, true);
}
async function loadCurrentHistory(since?: string): Promise<HistoryDevDetailed | undefined> {
@@ -334,6 +386,7 @@ export const useHistoryStore = defineStore("historyStore", () => {
currentHistoryId,
currentFilterText,
pinnedHistories,
storedHistories,
getHistoryById,
getHistoryNameById,
setCurrentHistory,
@@ -348,6 +401,10 @@ export const useHistoryStore = defineStore("historyStore", () => {
copyHistory,
createNewHistory,
deleteHistory,
deleteHistories,
restoreHistory,
restoreHistories,
handleTotalCountChange,
loadCurrentHistory,
loadHistories,
loadHistoryById,
@@ -1,14 +1,17 @@
import axios from "axios";
import { prependPath } from "utils/redirect";
import axios, { type AxiosResponse } from "axios";
import type { ApiResponse } from "openapi-typescript-fetch";
import type { AnyHistory } from "@/api";
import { prependPath } from "@/utils/redirect";
/**
* Generic json getter
* @param {*} response
* @return {Object} response.data or throws an error if response status is not 200
* @param response
* @return response.data or throws an error if response status is not 200
*/
function doResponse(response) {
function doResponse(response: AxiosResponse | ApiResponse) {
if (response.status !== 200) {
throw new Error(response);
throw new Error(response.statusText);
}
return response.data;
}
@@ -16,18 +19,15 @@ function doResponse(response) {
/**
* Some current endpoints don't accept JSON, so we need to
* do some massaging to send in old form post data.
* @param {Object} fields
*/
function formData(fields = {}) {
function formData(fields = {} as Record<string, any>) {
return Object.keys(fields).reduce((result, fieldName) => {
result.set(fieldName, fields[fieldName]);
return result;
}, new FormData());
}
/**
* Default history request parameters.
*/
/** Default history request parameters. */
const stdHistoryParams = {
view: "summary",
};
@@ -43,7 +43,7 @@ const extendedHistoryParams = {
/**
* Create a new history, select it as the current history, and return it if successful.
* @return {Object} the new history or throws an error if new history creation fails
* @return the new history or throws an error if new history creation fails
*/
export async function createAndSelectNewHistory() {
const url = "history/create_new_current";
@@ -57,11 +57,11 @@ export async function createAndSelectNewHistory() {
/**
* Generates copy of history on server.
* @param {Object} history Source history
* @param {String} name New history name
* @param {Boolean} copyAll Copy existing contents
* @param history Source history
* @param name New history name
* @param copyAll Copy existing contents
*/
export async function cloneHistory(history, name, copyAll) {
export async function cloneHistory(history: AnyHistory, name: string, copyAll: boolean) {
const url = "api/histories";
const payload = {
name,
@@ -73,23 +73,12 @@ export async function cloneHistory(history, name, copyAll) {
return doResponse(response);
}
/**
* Delete history on server and return the deleted history.
* @param {String} id Encoded history id
* @param {Boolean} [purge=false] Permanent delete
*/
export async function deleteHistoryById(id, purge = false) {
const url = `api/histories/${id}` + (purge ? "?purge=True" : "");
const response = await axios.delete(prependPath(url), { params: stdHistoryParams });
return doResponse(response);
}
/**
* Get current history from server and return it.
* @param {String} since timestamp to get histories since
* @return {Object} the current history
* @param since timestamp to get histories since
* @return the current history
*/
export async function getCurrentHistoryFromServer(since = undefined) {
export async function getCurrentHistoryFromServer(since: string | undefined = undefined) {
const url = "history/current_history_json";
const response = await axios.get(prependPath(url), { params: { since: since } });
return doResponse(response);
@@ -97,10 +86,10 @@ export async function getCurrentHistoryFromServer(since = undefined) {
/**
* Set current history on server and return it.
* @param {String} historyId Encoded history id
* @return {Object} the current history
* @param historyId Encoded history id
* @return the current history
*/
export async function setCurrentHistoryOnServer(historyId) {
export async function setCurrentHistoryOnServer(historyId: string) {
const url = "history/set_as_current";
const response = await axios.get(prependPath(url), { params: { id: historyId } });
return doResponse(response);
@@ -108,12 +97,12 @@ export async function setCurrentHistoryOnServer(historyId) {
/**
* Get list of histories from server and return them.
* @param {Number} offset to start from (default = 0)
* @param {Number | null} limit of histories to load (default = null; in which case no limit)
* @param {String} queryString to append to url in the form `q=filter&qv=val&q=...`
* @return {Promise.<Array>} list of histories
* @param offset to start from (default = 0)
* @param limit of histories to load (default = null; in which case no limit)
* @param queryString to append to url in the form `q=filter&qv=val&q=...`
* @return list of histories
*/
export async function getHistoryList(offset = 0, limit = null, queryString = "") {
export async function getHistoryList(offset = 0, limit: number | null = null, queryString = "") {
const params = `view=summary&order=update_time&offset=${offset}`;
let url = `api/histories?${params}`;
if (limit !== null) {
@@ -128,7 +117,7 @@ export async function getHistoryList(offset = 0, limit = null, queryString = "")
/**
* Get number of histories for current user from server and return them.
* @return {Promise.<Number>} number of histories
* @return number of histories
*/
export async function getHistoryCount() {
// This url is temp. for this PR, waiting on:
@@ -138,11 +127,8 @@ export async function getHistoryCount() {
return doResponse(response);
}
/**
* Load one history by id
* @param {String} id
*/
export async function getHistoryByIdFromServer(id) {
/** Load one history by id */
export async function getHistoryByIdFromServer(id: string) {
const path = `api/histories/${id}`;
const response = await axios.get(prependPath(path), { params: extendedHistoryParams });
return doResponse(response);
@@ -151,10 +137,10 @@ export async function getHistoryByIdFromServer(id) {
/**
* Set permissions to private for indicated history
* TODO: rewrite API endpoint for this
* @param {Object} history the history to secure
* @return {Object} the secured history
* @param history the history to secure
* @return the secured history
*/
export async function secureHistoryOnServer(history) {
export async function secureHistoryOnServer(history: AnyHistory) {
const { id } = history;
const url = "history/make_private";
const response = await axios.post(prependPath(url), formData({ history_id: id }));
@@ -166,11 +152,11 @@ export async function secureHistoryOnServer(history) {
/**
* Update specific fields in history
* @param {Object} historyId the history id to update
* @param {Object} payload fields to update
* @return {Object} the updated history
* @param historyId the history id to update
* @param payload fields to update
* @return the updated history
*/
export async function updateHistoryFields(historyId, payload) {
export async function updateHistoryFields(historyId: string, payload: Record<string, any>) {
const url = `api/histories/${historyId}`;
const response = await axios.put(prependPath(url), payload, { params: extendedHistoryParams });
return doResponse(response);