Skip to content

Commit

Permalink
Merge branch 'release_24.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
jdavcs committed May 30, 2024
2 parents 32868aa + 4dda05f commit bad3f8a
Show file tree
Hide file tree
Showing 40 changed files with 539 additions and 185 deletions.
113 changes: 113 additions & 0 deletions client/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
type AnonymousUser,
type AnyHistory,
type HistorySummary,
type HistorySummaryExtended,
isRegisteredUser,
type User,
userOwnsHistory,
} from ".";

const REGISTERED_USER_ID = "fake-user-id";
const ANOTHER_USER_ID = "another-fake-user-id";
const ANONYMOUS_USER_ID = null;

const REGISTERED_USER: User = {
id: REGISTERED_USER_ID,
email: "[email protected]",
tags_used: [],
isAnonymous: false,
total_disk_usage: 0,
};

const ANONYMOUS_USER: AnonymousUser = {
isAnonymous: true,
};

const SESSIONLESS_USER = null;

function createFakeHistory<T>(historyId: string = "fake-id", user_id?: string | null): T {
const history: AnyHistory = {
id: historyId,
name: "test",
model_class: "History",
deleted: false,
archived: false,
purged: false,
published: false,
annotation: null,
update_time: "2021-09-01T00:00:00.000Z",
tags: [],
url: `/history/${historyId}`,
contents_active: { active: 0, deleted: 0, hidden: 0 },
count: 0,
size: 0,
};
if (user_id !== undefined) {
(history as HistorySummaryExtended).user_id = user_id;
}
return history as T;
}

const HISTORY_OWNED_BY_REGISTERED_USER = createFakeHistory<HistorySummaryExtended>("1234", REGISTERED_USER_ID);
const HISTORY_OWNED_BY_ANOTHER_USER = createFakeHistory<HistorySummaryExtended>("5678", ANOTHER_USER_ID);
const HISTORY_OWNED_BY_ANONYMOUS_USER = createFakeHistory<HistorySummaryExtended>("1234", ANONYMOUS_USER_ID);
const HISTORY_SUMMARY_WITHOUT_USER_ID = createFakeHistory<HistorySummary>("1234");

describe("API Types Helpers", () => {
describe("isRegisteredUser", () => {
it("should return true for a registered user", () => {
expect(isRegisteredUser(REGISTERED_USER)).toBe(true);
});

it("should return false for an anonymous user", () => {
expect(isRegisteredUser(ANONYMOUS_USER)).toBe(false);
});

it("should return false for sessionless users", () => {
expect(isRegisteredUser(SESSIONLESS_USER)).toBe(false);
});
});

describe("isAnonymousUser", () => {
it("should return true for an anonymous user", () => {
expect(isRegisteredUser(ANONYMOUS_USER)).toBe(false);
});

it("should return false for a registered user", () => {
expect(isRegisteredUser(REGISTERED_USER)).toBe(true);
});

it("should return false for sessionless users", () => {
expect(isRegisteredUser(SESSIONLESS_USER)).toBe(false);
});
});

describe("userOwnsHistory", () => {
it("should return true for a registered user owning the history", () => {
expect(userOwnsHistory(REGISTERED_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(true);
});

it("should return false for a registered user not owning the history", () => {
expect(userOwnsHistory(REGISTERED_USER, HISTORY_OWNED_BY_ANOTHER_USER)).toBe(false);
});

it("should return true for a registered user owning a history without user_id", () => {
expect(userOwnsHistory(REGISTERED_USER, HISTORY_SUMMARY_WITHOUT_USER_ID)).toBe(true);
});

it("should return true for an anonymous user owning a history with null user_id", () => {
expect(userOwnsHistory(ANONYMOUS_USER, HISTORY_OWNED_BY_ANONYMOUS_USER)).toBe(true);
});

it("should return false for an anonymous user not owning a history", () => {
expect(userOwnsHistory(ANONYMOUS_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(false);
});

it("should return false for sessionless users", () => {
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(false);
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_SUMMARY_WITHOUT_USER_ID)).toBe(false);
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_OWNED_BY_ANONYMOUS_USER)).toBe(false);
});
});
});
28 changes: 23 additions & 5 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export interface HistoryContentsStats {
* Data returned by the API when requesting `?view=summary&keys=size,contents_active,user_id`.
*/
export interface HistorySummaryExtended extends HistorySummary, HistoryContentsStats {
user_id: string;
/** The ID of the user that owns the history. Null if the history is owned by an anonymous user. */
user_id: string | null;
}

type HistoryDetailedModel = components["schemas"]["HistoryDetailed"];
Expand Down Expand Up @@ -212,6 +213,7 @@ export function isHistorySummaryExtended(history: AnyHistory): history is Histor

type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];

/** Represents a registered user.**/
export interface User extends QuotaUsageResponse {
id: string;
email: string;
Expand All @@ -230,22 +232,38 @@ export interface AnonymousUser {

export type GenericUser = User | AnonymousUser;

export function isRegisteredUser(user: User | AnonymousUser | null): user is User {
return !user?.isAnonymous;
/** Represents any user, including anonymous users or session-less (null) users.**/
export type AnyUser = GenericUser | null;

export function isRegisteredUser(user: AnyUser): user is User {
return user !== null && !user?.isAnonymous;
}

export function isAnonymousUser(user: AnyUser): user is AnonymousUser {
return user !== null && user.isAnonymous;
}

export function userOwnsHistory(user: User | AnonymousUser | null, history: AnyHistory) {
export function userOwnsHistory(user: AnyUser, history: AnyHistory) {
return (
// Assuming histories without user_id are owned by the current user
(isRegisteredUser(user) && !hasOwner(history)) ||
(isRegisteredUser(user) && hasOwner(history) && user.id === history.user_id)
(isRegisteredUser(user) && hasOwner(history) && user.id === history.user_id) ||
(isAnonymousUser(user) && hasAnonymousOwner(history))
);
}

function hasOwner(history: AnyHistory): history is HistorySummaryExtended {
return "user_id" in history && history.user_id !== null;
}

function hasAnonymousOwner(history: AnyHistory): history is HistorySummaryExtended {
return "user_id" in history && history.user_id === null;
}

export function canMutateHistory(history: AnyHistory): boolean {
return !history.purged && !history.archived;
}

export type DatasetHash = components["schemas"]["DatasetHash"];

export type DatasetTransform = {
Expand Down
1 change: 0 additions & 1 deletion client/src/components/Grid/configs/histories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ const fields: FieldArray = [
{
title: "Switch",
icon: faExchangeAlt,
condition: (data: HistoryEntry) => !data.deleted,
handler: (data: HistoryEntry) => {
const historyStore = useHistoryStore();
historyStore.setCurrentHistory(String(data.id));
Expand Down
18 changes: 16 additions & 2 deletions client/src/components/History/Archiving/ArchivedHistoryCard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCopy, faEye, faUndo } from "@fortawesome/free-solid-svg-icons";
import { faCopy, faExchangeAlt, faEye, faUndo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge, BButton, BButtonGroup } from "bootstrap-vue";
import { computed } from "vue";
Expand All @@ -23,16 +23,21 @@ const canImportCopy = computed(() => props.history.export_record_data?.target_ur
const emit = defineEmits<{
(e: "onView", history: ArchivedHistorySummary): void;
(e: "onSwitch", history: ArchivedHistorySummary): void;
(e: "onRestore", history: ArchivedHistorySummary): void;
(e: "onImportCopy", history: ArchivedHistorySummary): void;
}>();
library.add(faUndo, faCopy, faEye);
library.add(faExchangeAlt, faUndo, faCopy, faEye);
function onViewHistoryInCenterPanel() {
emit("onView", props.history);
}
function onSetAsCurrentHistory() {
emit("onSwitch", props.history);
}
async function onRestoreHistory() {
emit("onRestore", props.history);
}
Expand Down Expand Up @@ -84,6 +89,15 @@ async function onImportCopy() {
<FontAwesomeIcon :icon="faEye" size="lg" />
View
</BButton>
<BButton
v-b-tooltip
:title="localize('Set as current history')"
variant="link"
class="p-0 px-1"
@click.stop="onSetAsCurrentHistory">
<FontAwesomeIcon :icon="faExchangeAlt" size="lg" />
Switch
</BButton>
<BButton
v-b-tooltip
:title="localize('Unarchive this history and move it back to your active histories')"
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/History/Archiving/HistoryArchive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function onViewHistoryInCenterPanel(history: ArchivedHistorySummary) {
router.push(`/histories/view?id=${history.id}`);
}
function onSetAsCurrentHistory(history: ArchivedHistorySummary) {
historyStore.setCurrentHistory(history.id);
}
async function onRestoreHistory(history: ArchivedHistorySummary) {
const confirmTitle = localize(`Unarchive '${history.name}'?`);
const confirmMessage =
Expand Down Expand Up @@ -145,6 +149,7 @@ async function onImportCopy(history: ArchivedHistorySummary) {
<ArchivedHistoryCard
:history="history"
@onView="onViewHistoryInCenterPanel"
@onSwitch="onSetAsCurrentHistory"
@onRestore="onRestoreHistory"
@onImportCopy="onImportCopy" />
</BListGroupItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { computed, ref, watch } from "vue";

import type { CollectionEntry, DCESummary, HistorySummary, SubCollection } from "@/api";
import { isCollectionElement, isHDCA } from "@/api";
import { canMutateHistory, isCollectionElement, isHDCA } from "@/api";
import ExpandedItems from "@/components/History/Content/ExpandedItems";
import { updateContentFields } from "@/components/History/model/queries";
import { useCollectionElementsStore } from "@/stores/collectionElementsStore";
Expand Down Expand Up @@ -66,6 +66,7 @@ const rootCollection = computed(() => {
}
});
const isRoot = computed(() => dsc.value == rootCollection.value);
const canEdit = computed(() => isRoot.value && canMutateHistory(props.history));

function updateDsc(collection: any, fields: Object | undefined) {
updateContentFields(collection, fields).then((response) => {
Expand Down Expand Up @@ -124,8 +125,8 @@ watch(
:history-name="history.name"
:selected-collections="selectedCollections"
v-on="$listeners" />
<CollectionDetails :dsc="dsc" :writeable="isRoot" @update:dsc="updateDsc(dsc, $event)" />
<CollectionOperations v-if="isRoot && showControls" :dsc="dsc" />
<CollectionDetails :dsc="dsc" :writeable="canEdit" @update:dsc="updateDsc(dsc, $event)" />
<CollectionOperations v-if="canEdit && showControls" :dsc="dsc" />
</section>
<section class="position-relative flex-grow-1 scroller">
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import prettyBytes from "pretty-bytes";
import { computed, onMounted, ref, toRef } from "vue";
import { useRouter } from "vue-router/composables";
import type { HistorySummaryExtended } from "@/api";
import { type HistorySummaryExtended, userOwnsHistory } from "@/api";
import { HistoryFilters } from "@/components/History/HistoryFilters.js";
import { useConfig } from "@/composables/config";
import { useHistoryContentStats } from "@/composables/historyContentStats";
Expand Down Expand Up @@ -137,7 +137,7 @@ onMounted(() => {
variant="link"
size="sm"
class="rounded-0 text-decoration-none history-storage-overview-button"
:disabled="!showControls"
:disabled="!userOwnsHistory(currentUser, props.history)"
data-description="storage dashboard button"
@click="onDashboard">
<FontAwesomeIcon :icon="faDatabase" />
Expand Down
30 changes: 25 additions & 5 deletions client/src/components/History/CurrentHistory/HistoryMessages.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
<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 { BAlert } from "bootstrap-vue";
import { computed, ref } from "vue";
import type { HistorySummary } from "@/api";
import { type GenericUser, type HistorySummary, userOwnsHistory } from "@/api";
import localize from "@/utils/localization";
library.add(faArchive, faBurn, faTrash);
interface Props {
history: HistorySummary;
currentUser: GenericUser | null;
}
const props = defineProps<Props>();
const userOverQuota = ref(false);
const hasMessages = computed(() => {
return userOverQuota.value || props.history.deleted;
return userOverQuota.value || props.history.deleted || props.history.archived;
});
const currentUserOwnsHistory = computed(() => {
return userOwnsHistory(props.currentUser, props.history);
});
</script>

<template>
<div v-if="hasMessages" class="mx-3 my-2">
<BAlert :show="history.deleted" variant="warning">
{{ localize("This history has been deleted") }}
<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") }}
</BAlert>
<BAlert v-else-if="history.deleted" :show="history.deleted" variant="warning">
<FontAwesomeIcon :icon="faTrash" fixed-width />
{{ localize("History has been deleted") }}
</BAlert>

<BAlert :show="history.archived && currentUserOwnsHistory" variant="warning">
<FontAwesomeIcon :icon="faArchive" fixed-width />
{{ localize("History has been archived") }}
</BAlert>

<BAlert :show="userOverQuota" variant="warning">
Expand Down
Loading

0 comments on commit bad3f8a

Please sign in to comment.