Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visually indicate currently viewed/edited dataset #16859

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/components/History/Content/ContentItem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { mount } from "@vue/test-utils";
import { updateContentFields } from "components/History/model/queries";
import { PiniaVuePlugin } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import VueRouter from "vue-router";

import ContentItem from "./ContentItem";

jest.mock("components/History/model/queries");

const localVue = getLocalVue();
localVue.use(VueRouter);
localVue.use(PiniaVuePlugin);
const router = new VueRouter();

// mock queries
updateContentFields.mockImplementation(async () => {});
Expand Down Expand Up @@ -48,6 +51,7 @@ describe("ContentItem", () => {
},
},
pinia: createTestingPinia(),
router,
});
});

Expand Down
12 changes: 10 additions & 2 deletions client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div
:id="contentId"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls]"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls, isBeingUsed]"
:data-hid="id"
:data-state="state"
tabindex="0"
Expand Down Expand Up @@ -89,7 +89,7 @@
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
<b-collapse :visible="expandDataset">
<DatasetDetails
v-if="expandDataset"
v-if="expandDataset && item.id"
:id="item.id"
:writable="writable"
:show-highlight="(isHistoryItem && filterable) || addHighlightBtn"
Expand Down Expand Up @@ -211,6 +211,9 @@ export default {
visualize: `/visualizations?dataset_id=${id}`,
};
},
isBeingUsed() {
return Object.values(this.itemUrls).includes(this.$route.path) ? "being-used" : "";
},
},
methods: {
onKeyDown(event) {
Expand Down Expand Up @@ -289,5 +292,10 @@ export default {
&:deep(.btn:focus) {
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
}

&.being-used {
border-left: 0.25rem solid $brand-primary;
margin-left: 0rem !important;
}
}
</style>
169 changes: 84 additions & 85 deletions client/src/components/History/Content/ContentOptions.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,81 @@
<script setup lang="ts">
import { BDropdown } from "bootstrap-vue";
import { computed, type Ref, ref } from "vue";

import { prependPath } from "@/utils/redirect";

const props = defineProps({
writable: { type: Boolean, default: true },
isDataset: { type: Boolean, required: true },
isDeleted: { type: Boolean, default: false },
isHistoryItem: { type: Boolean, required: true },
isVisible: { type: Boolean, default: true },
state: { type: String, default: "" },
itemUrls: { type: Object, required: true },
keyboardSelectable: { type: Boolean, default: true },
});

const emit = defineEmits<{
(e: "display"): void;
(e: "showCollectionInfo"): void;
(e: "edit"): void;
(e: "delete", recursive?: boolean): void;
(e: "undelete"): void;
(e: "unhide"): void;
}>();

const deleteCollectionMenu: Ref<BDropdown | null> = ref(null);

const displayButtonTitle = computed(() => (displayDisabled.value ? "This dataset is not yet viewable." : "Display"));

const displayDisabled = computed(() => ["discarded", "new", "upload", "queued"].includes(props.state));

const editButtonTitle = computed(() => (editDisabled.value ? "This dataset is not yet editable." : "Edit attributes"));

const editDisabled = computed(() =>
["discarded", "new", "upload", "queued", "running", "waiting"].includes(props.state)
);

const displayUrl = computed(() => prependPath(props.itemUrls.display));

const editUrl = computed(() => prependPath(props.itemUrls.edit));

const isCollection = computed(() => !props.isDataset);

const canShowCollectionDetails = computed(() => props.itemUrls.showDetails);

const showCollectionDetailsUrl = computed(() => prependPath(props.itemUrls.showDetails));

const tabindex = computed(() => (props.keyboardSelectable ? "0" : "-1"));

function onDelete($event: MouseEvent) {
if (isCollection.value) {
deleteCollectionMenu.value?.show();
} else {
onDeleteItem();
}
}

function onDeleteItem() {
emit("delete");
}

function onDeleteItemRecursively() {
const recursive = true;
emit("delete", recursive);
}

function onDisplay($event: MouseEvent) {
// Wrap display handler to allow ctrl/meta click to open in new tab
// instead of triggering display.
if ($event.ctrlKey || $event.metaKey) {
window.open(displayUrl.value, "_blank");
} else {
emit("display");
}
}
</script>

<template>
<span class="align-self-start btn-group align-items-baseline">
<!-- Special case for collections -->
Expand All @@ -8,7 +86,7 @@
size="sm"
variant="link"
:href="showCollectionDetailsUrl"
@click.prevent.stop="$emit('showCollectionInfo')">
@click.prevent.stop="emit('showCollectionInfo')">
<icon icon="info-circle" />
</b-button>
<!-- Common for all content items -->
Expand All @@ -33,7 +111,7 @@
size="sm"
variant="link"
:href="editUrl"
@click.prevent.stop="$emit('edit')">
@click.prevent.stop="emit('edit')">
<icon icon="pen" />
</b-button>
<b-button
Expand All @@ -45,7 +123,7 @@
variant="link"
@click.stop="onDelete($event)">
<icon v-if="isDataset" icon="trash" />
<b-dropdown v-else ref="deleteCollectionMenu" size="sm" variant="link" no-caret toggle-class="p-0 m-0">
<BDropdown v-else ref="deleteCollectionMenu" size="sm" variant="link" no-caret toggle-class="p-0 m-0">
<template v-slot:button-content>
<icon icon="trash" />
</template>
Expand All @@ -57,7 +135,7 @@
<icon icon="copy" />
Collection and elements
</b-dropdown-item>
</b-dropdown>
</BDropdown>
</b-button>
<b-button
v-if="writable && isHistoryItem && isDeleted"
Expand All @@ -66,7 +144,7 @@
title="Undelete"
size="sm"
variant="link"
@click.stop="$emit('undelete')">
@click.stop="emit('undelete')">
<icon icon="trash-restore" />
</b-button>
<b-button
Expand All @@ -76,91 +154,12 @@
title="Unhide"
size="sm"
variant="link"
@click.stop="$emit('unhide')">
@click.stop="emit('unhide')">
<icon icon="eye-slash" />
</b-button>
</span>
</template>

<script>
import { prependPath } from "@/utils/redirect";

export default {
props: {
writable: { type: Boolean, default: true },
isDataset: { type: Boolean, required: true },
isDeleted: { type: Boolean, default: false },
isHistoryItem: { type: Boolean, required: true },
isVisible: { type: Boolean, default: true },
state: { type: String, default: "" },
itemUrls: { type: Object, required: true },
keyboardSelectable: { type: Boolean, default: true },
},
computed: {
displayButtonTitle() {
if (this.displayDisabled) {
return "This dataset is not yet viewable.";
}
return "Display";
},
displayDisabled() {
return ["discarded", "new", "upload", "queued"].includes(this.state);
},
editButtonTitle() {
if (this.editDisabled) {
return "This dataset is not yet editable.";
}
return "Edit attributes";
},
editDisabled() {
return ["discarded", "new", "upload", "queued", "running", "waiting"].includes(this.state);
},
displayUrl() {
return prependPath(this.itemUrls.display);
},
editUrl() {
return prependPath(this.itemUrls.edit);
},
isCollection() {
return !this.isDataset;
},
canShowCollectionDetails() {
return !!this.itemUrls.showDetails;
},
showCollectionDetailsUrl() {
return prependPath(this.itemUrls.showDetails);
},
tabindex() {
return this.keyboardSelectable ? "0" : "-1";
},
},
methods: {
onDisplay($event) {
// Wrap display handler to allow ctrl/meta click to open in new tab
// instead of triggering display.
if ($event.ctrlKey || $event.metaKey) {
window.open(this.displayUrl, "_blank");
} else {
this.$emit("display");
}
},
onDelete() {
if (this.isCollection) {
this.$refs.deleteCollectionMenu.show();
} else {
this.onDeleteItem();
}
},
onDeleteItem() {
this.$emit("delete");
},
onDeleteItemRecursively() {
const recursive = true;
this.$emit("delete", recursive);
},
},
};
</script>
<style lang="css">
.dropdown-menu .dropdown-item {
font-weight: normal;
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/History/Content/GenericElement.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import VueRouter from "vue-router";

import GenericElement from "./GenericElement";

jest.mock("components/History/model/queries");

const localVue = getLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();

describe("GenericElement", () => {
let wrapper;
Expand Down Expand Up @@ -54,6 +59,7 @@ describe("GenericElement", () => {
},
},
localVue,
router,
});
});

Expand Down
10 changes: 5 additions & 5 deletions client/src/components/History/HistoryView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async function createWrapper(localVue, currentUserId, history) {
localVue,
stubs: {
icon: { template: "<div></div>" },
ContentItem: true,
},
provide: {
store: {
Expand Down Expand Up @@ -119,14 +120,13 @@ describe("History center panel View", () => {
// parts of the layout that should be similar for all cases
expectCorrectLayout(wrapper);

// all history items, make sure all show up with hids and names
const historyItems = wrapper.findAll(".content-item");
// make sure all history items show up
const historyItems = wrapper.findAll("contentitem-stub");
expect(historyItems.length).toBe(10);
for (let i = 0; i < historyItems.length; i++) {
const hid = historyItems.length - i;
const itemHeader = historyItems.at(i).find("[data-description='content item header info']");
const headerText = `${hid}: Dataset ${hid}`;
expect(itemHeader.text()).toBe(headerText);
expect(historyItems.at(i).attributes("id")).toBe(`${hid}`);
expect(historyItems.at(i).attributes("name")).toBe(`Dataset ${hid}`);
}
});

Expand Down
14 changes: 11 additions & 3 deletions client/src/components/JobParameters/JobParameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe("JobParameters/JobParameters.vue", () => {
propsData,
stubs: {
DatasetProvider: DatasetProvider,
ContentItem: true,
},
pinia,
});
Expand All @@ -54,12 +55,18 @@ describe("JobParameters/JobParameters.vue", () => {
const checkTableParameter = (
element: Wrapper<any>,
expectedTitle: string,
expectedValue: string,
expectedValue: string | { hid: number; name: string },
link?: string
) => {
const tds = element.findAll("td");
expect(tds.at(0).text()).toBe(expectedTitle);
expect(tds.at(1).text()).toContain(expectedValue);
if (typeof expectedValue === "string") {
expect(tds.at(1).text()).toContain(expectedValue);
} else {
const contentItem = tds.at(1).find("contentitem-stub");
expect(contentItem.attributes("id")).toBe(`${expectedValue.hid}`);
expect(contentItem.attributes("name")).toBe(expectedValue.name);
}
if (link) {
const a_element = tds.at(1).find("a");
expect(a_element.attributes("href")).toBe(link);
Expand All @@ -74,7 +81,7 @@ describe("JobParameters/JobParameters.vue", () => {
expect(elements.length).toBe(3);

checkTableParameter(elements.at(0), "Add this value", "22", undefined);
checkTableParameter(elements.at(1), linkParam.text, `${raw.hid}: ${raw.name}`, undefined);
checkTableParameter(elements.at(1), linkParam.text, { hid: raw.hid, name: raw.name }, undefined);
checkTableParameter(elements.at(2), "Iterate?", "NO", undefined);
});

Expand All @@ -89,6 +96,7 @@ describe("JobParameters/JobParameters.vue", () => {
propsData,
stubs: {
DatasetProvider: DatasetProvider,
ContentItem: true,
},
pinia,
});
Expand Down
Loading