mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 02:04:04 +08:00
Merge pull request #13477 from guerler/new_history_data_plug
Enable infinite scrolling / pagination for history and collection items
This commit is contained in:
@@ -92,6 +92,7 @@
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-rx": "^6.2.0",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-virtual-scroll-list": "^2.3.3",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vueisotope": "^3.1.2",
|
||||
"vuex": "^3.6.2",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<h6 v-if="collectionType" class="description py-1">
|
||||
<span>a {{ collectionLabel | localize }}</span>
|
||||
<span v-if="elementCount == 1">with 1 item</span>
|
||||
<span v-else-if="elementCount > 1">with {{ elementCount }} items</span>
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
labels: {
|
||||
list: "list",
|
||||
paired: "dataset pair",
|
||||
"list:paired": "list of pairs",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
collectionLabel() {
|
||||
return this.labels[this.collectionType] || "nested list";
|
||||
},
|
||||
collectionType() {
|
||||
return this.item.collection_type;
|
||||
},
|
||||
elementCount() {
|
||||
return this.item.element_count;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div
|
||||
:id="contentId"
|
||||
:class="['content-item m-1 p-0 rounded', clsStatus]"
|
||||
draggable
|
||||
:data-hid="id"
|
||||
:data-state="state"
|
||||
@dragstart="onDragStart">
|
||||
<div class="p-1 cursor-pointer" @click.stop="onClick">
|
||||
<div class="clearfix overflow-hidden">
|
||||
<div class="btn-group float-right">
|
||||
<b-button
|
||||
v-if="isDataset"
|
||||
:disabled="displayDisabled"
|
||||
:title="displayButtonTitle"
|
||||
class="px-1"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onDisplay">
|
||||
<icon icon="eye" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="isHistoryItem"
|
||||
:disabled="editDisabled"
|
||||
class="px-1"
|
||||
title="Edit attributes"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onEdit">
|
||||
<icon icon="pen" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="isHistoryItem && !item.deleted"
|
||||
class="px-1"
|
||||
title="Delete"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:disabled="item.purged"
|
||||
@click.stop="$emit('delete', item)">
|
||||
<icon icon="trash" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="isHistoryItem && item.deleted"
|
||||
class="px-1"
|
||||
title="Undelete"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:disabled="item.purged"
|
||||
@click.stop="$emit('undelete', item)">
|
||||
<icon icon="trash-restore" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="isHistoryItem && !item.visible"
|
||||
class="px-1"
|
||||
title="Unhide"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="$emit('unhide', item)">
|
||||
<icon icon="unlock" />
|
||||
</b-button>
|
||||
</div>
|
||||
<h5 class="float-left p-1 w-75 font-weight-bold">
|
||||
<div v-if="selectable" class="selector float-left mr-2">
|
||||
<span
|
||||
v-if="selected"
|
||||
class="fa fa-lg fa-check-square-o"
|
||||
@click.stop="$emit('update:selected', false)" />
|
||||
<span v-else class="fa fa-lg fa-square-o" @click.stop="$emit('update:selected', true)" />
|
||||
</div>
|
||||
<icon v-if="stateIcon" :icon="stateIcon" />
|
||||
<span class="id hid">{{ id }}</span>
|
||||
<span>:</span>
|
||||
<span class="content-title name">{{ name }}</span>
|
||||
<CollectionDescription v-if="!isDataset" :item="item" />
|
||||
<div v-if="!expandDataset && item.tags && item.tags.length > 0" class="nametags">
|
||||
<Nametag v-for="tag in item.tags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
|
||||
<DatasetDetails v-if="expandDataset" @edit="onEdit" :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { backboneRoute, useGalaxy, iframeRedirect } from "components/plugins/legacyNavigation";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import CollectionDescription from "./Collection/CollectionDescription";
|
||||
import DatasetDetails from "./Dataset/DatasetDetails";
|
||||
import CONTENTSTATE from "./contentState";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CollectionDescription,
|
||||
DatasetDetails,
|
||||
Nametag,
|
||||
},
|
||||
props: {
|
||||
expandDataset: { type: Boolean, required: true },
|
||||
item: { type: Object, required: true },
|
||||
id: { type: Number, required: true },
|
||||
isDataset: { type: Boolean, default: true },
|
||||
isHistoryItem: { type: Boolean, default: true },
|
||||
name: { type: String, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
selectable: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
contentId() {
|
||||
return `dataset-${this.item.id}`;
|
||||
},
|
||||
state() {
|
||||
if (this.item.job_state_summary) {
|
||||
for (const key of ["error", "failed", "paused", "upload", "running"]) {
|
||||
if (this.item.job_state_summary[key] > 0) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return "ok";
|
||||
} else {
|
||||
return this.item.state;
|
||||
}
|
||||
},
|
||||
clsStatus() {
|
||||
const status = CONTENTSTATE[this.state] && CONTENTSTATE[this.state].status;
|
||||
if (this.selected) {
|
||||
return "alert-info";
|
||||
} else if (!status) {
|
||||
return `alert-success`;
|
||||
} else {
|
||||
return `alert-${status}`;
|
||||
}
|
||||
},
|
||||
displayButtonTitle() {
|
||||
if (this.item.purged) {
|
||||
return "Cannot display datasets removed from disk.";
|
||||
}
|
||||
if (this.displayDisabled) {
|
||||
return "This dataset is not yet viewable.";
|
||||
}
|
||||
return "Display";
|
||||
},
|
||||
displayDisabled() {
|
||||
return this.item.purged || ["discarded", "new", "upload"].includes(this.state);
|
||||
},
|
||||
editButtonTitle() {
|
||||
if (this.item.purged) {
|
||||
return "Cannot edit attributes of datasets removed from disk.";
|
||||
}
|
||||
if (this.editDisabled) {
|
||||
return "This dataset is not yet editable.";
|
||||
}
|
||||
return "Edit Attributes";
|
||||
},
|
||||
editDisabled() {
|
||||
return (
|
||||
this.item.purged || ["discarded", "new", "upload", "queued", "running", "waiting"].includes(this.state)
|
||||
);
|
||||
},
|
||||
stateIcon() {
|
||||
return CONTENTSTATE[this.state] && CONTENTSTATE[this.state].icon;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDragStart(evt) {
|
||||
evt.dataTransfer.dropEffect = "move";
|
||||
evt.dataTransfer.effectAllowed = "move";
|
||||
evt.dataTransfer.setData("text", JSON.stringify([this.item]));
|
||||
},
|
||||
onDisplay() {
|
||||
const id = this.item.id;
|
||||
useGalaxy((Galaxy) => {
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
Galaxy.frame.addDataset(id);
|
||||
} else {
|
||||
iframeRedirect(`datasets/${id}/display/?preview=True`);
|
||||
}
|
||||
});
|
||||
},
|
||||
onEdit() {
|
||||
if (this.item.collection_type) {
|
||||
backboneRoute(`collection/edit/${this.item.id}`);
|
||||
} else {
|
||||
backboneRoute("datasets/edit", { dataset_id: this.item.id });
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
if (this.isDataset) {
|
||||
this.$emit("update:expand-dataset", !this.expandDataset);
|
||||
} else {
|
||||
this.$emit("view-collection", this.item);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.content-item:hover {
|
||||
filter: brightness(105%);
|
||||
}
|
||||
.content-item {
|
||||
.name {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="dataset-actions mb-1">
|
||||
<div class="clearfix">
|
||||
<div class="btn-group float-left">
|
||||
<b-button v-if="showError" class="px-1" title="Error" size="sm" variant="link" @click.stop="onError">
|
||||
<span class="fa fa-bug" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showDownloads"
|
||||
class="download-btn px-1"
|
||||
title="Download"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onDownload">
|
||||
<span class="fa fa-save" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showDownloads"
|
||||
class="px-1"
|
||||
title="Copy Link"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onCopyLink">
|
||||
<span class="fa fa-link" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showInfo"
|
||||
class="params-btn px-1"
|
||||
title="Dataset Details"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onInfo">
|
||||
<span class="fa fa-info-circle" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showRerun"
|
||||
class="px-1"
|
||||
title="Run Job Again"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onRerun">
|
||||
<span class="fa fa-redo" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showVisualizations"
|
||||
class="px-1"
|
||||
title="Visualize"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="onVisualize">
|
||||
<span class="fa fa-bar-chart-o" />
|
||||
</b-button>
|
||||
<b-button v-if="showRerun" class="px-1" title="Help" size="sm" variant="link" @click.stop="onRerun">
|
||||
<span class="fa fa-question" />
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="btn-group float-right">
|
||||
<b-button
|
||||
class="px-1"
|
||||
title="Edit Tags"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:pressed="showTags"
|
||||
@click="onToggleTags">
|
||||
<span class="fa fa-tags" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showTags" class="mb-2">
|
||||
<StatelessTags class="tags" v-model="tags" @input="onTags" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import { prependPath } from "utils/redirect";
|
||||
import { copy as sendToClipboard } from "utils/clipboard";
|
||||
import { absPath } from "utils/redirect";
|
||||
import { updateContentFields } from "components/History/model/queries";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
StatelessTags,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTags: false,
|
||||
tags: this.item.tags,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
downloadUrl() {
|
||||
return prependPath(`datasets/${this.item.id}/display?to_ext=${this.item.extension}`);
|
||||
},
|
||||
showDownloads() {
|
||||
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
|
||||
},
|
||||
showError() {
|
||||
return this.item.state == "error";
|
||||
},
|
||||
showInfo() {
|
||||
return this.item.state != "noPermission";
|
||||
},
|
||||
showRerun() {
|
||||
return (
|
||||
this.item.rerunnable &&
|
||||
this.item.creating_job &&
|
||||
this.item.state != "upload" &&
|
||||
this.item.state != "noPermission"
|
||||
);
|
||||
},
|
||||
showVisualizations() {
|
||||
// TODO: Check hasViz, if visualizations are activated in the config
|
||||
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCopyLink() {
|
||||
const msg = this.localize("Link is copied to your clipboard");
|
||||
sendToClipboard(absPath(this.downloadUrl), msg);
|
||||
},
|
||||
onDownload() {
|
||||
window.location.href = this.downloadUrl;
|
||||
},
|
||||
onError() {
|
||||
this.backboneRoute("datasets/error", { dataset_id: this.item.id });
|
||||
},
|
||||
onInfo() {
|
||||
this.backboneRoute(`datasets/${this.item.id}/details`);
|
||||
},
|
||||
onRerun() {
|
||||
this.backboneRoute(`root?job_id=${this.item.creating_job}`);
|
||||
},
|
||||
onTags() {
|
||||
updateContentFields(this.item, { tags: this.tags });
|
||||
},
|
||||
onToggleTags() {
|
||||
this.showTags = !this.showTags;
|
||||
},
|
||||
onVisualize() {
|
||||
const path = `visualizations?dataset_id=${this.item.id}`;
|
||||
const redirectParams = {
|
||||
path: path,
|
||||
title: "Dataset details",
|
||||
tryIframe: false,
|
||||
};
|
||||
if (!this.iframeAdd(redirectParams)) {
|
||||
this.backboneRoute(path);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<UrlDataProvider :url="dataUrl" auto-refresh v-slot="{ loading, result }">
|
||||
<div v-if="!loading" class="dataset">
|
||||
<div class="p-2 details">
|
||||
<div class="summary">
|
||||
<div v-if="result.misc_blurb" class="blurb">
|
||||
<span class="value">{{ result.misc_blurb }}</span>
|
||||
</div>
|
||||
<span v-if="result.file_ext" class="datatype">
|
||||
<label class="prompt" v-localize>format</label>
|
||||
<span class="value">{{ result.file_ext }}</span>
|
||||
</span>
|
||||
<span v-if="result.genome_build" class="dbkey">
|
||||
<label class="prompt" v-localize>database</label>
|
||||
<b-link class="value" @click.stop="$emit('edit', dataset)" data-label="Database/Build">{{
|
||||
result.genome_build
|
||||
}}</b-link>
|
||||
</span>
|
||||
<div v-if="result.misc_info" class="info">
|
||||
<span class="value">{{ result.misc_info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DatasetActions :item="result" />
|
||||
<pre v-if="result.peek" class="dataset-peek p-1" v-html="result.peek" />
|
||||
</div>
|
||||
</div>
|
||||
</UrlDataProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import DatasetActions from "./DatasetActions";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetActions,
|
||||
UrlDataProvider,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
return this.item.object || this.item;
|
||||
},
|
||||
dataUrl() {
|
||||
const datasetId = this.dataset.id;
|
||||
return `api/datasets/${datasetId}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<ContentItem
|
||||
:item="item.object"
|
||||
:id="item.element_index"
|
||||
:name="item.element_identifier"
|
||||
:is-dataset="item.element_type == 'hda'"
|
||||
:expand-dataset="expandDataset"
|
||||
:is-history-item="false"
|
||||
@update:expand-dataset="expandDataset = $event"
|
||||
@view-collection="viewCollection = !viewCollection" />
|
||||
<GenericItem v-if="viewCollection" :itemId="item.object.id" :itemSrc="item.object.history_content_type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentItem from "./ContentItem";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentItem,
|
||||
GenericItem: () => import("components/History/Content/GenericItem"),
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewCollection: false,
|
||||
expandDataset: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<component :is="providerComponent" :id="itemId" auto-refresh v-slot="{ result: item, loading }">
|
||||
<loading-span v-if="loading" message="Loading dataset" />
|
||||
<div v-else>
|
||||
<ContentItem
|
||||
:item="item"
|
||||
:id="item.hid"
|
||||
:name="item.name"
|
||||
:expand-dataset="expandDataset"
|
||||
:is-dataset="item.history_content_type == 'dataset'"
|
||||
@update:expand-dataset="expandDataset = $event"
|
||||
@view-collection="viewCollection = !viewCollection"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@unhide="onUnhide" />
|
||||
<div v-if="viewCollection">
|
||||
<div v-for="(collectionItem, collectionIndex) in item.elements" :key="collectionIndex">
|
||||
<GenericElement :item="collectionItem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { DatasetProvider, DatasetCollectionProvider } from "components/providers";
|
||||
import { deleteContent, updateContentFields } from "components/History/model/queries";
|
||||
import ContentItem from "./ContentItem";
|
||||
import GenericElement from "./GenericElement";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentItem,
|
||||
GenericElement,
|
||||
DatasetProvider,
|
||||
DatasetCollectionProvider,
|
||||
LoadingSpan,
|
||||
},
|
||||
props: {
|
||||
itemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemSrc: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewCollection: false,
|
||||
expandDataset: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
providerComponent() {
|
||||
return this.itemSrc == "hda" ? "DatasetProvider" : "DatasetCollectionProvider";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDelete(item) {
|
||||
deleteContent(item);
|
||||
},
|
||||
onUndelete(item) {
|
||||
updateContentFields(item, { deleted: false });
|
||||
},
|
||||
onHide(item) {
|
||||
updateContentFields(item, { visible: false });
|
||||
},
|
||||
onUnhide(item) {
|
||||
updateContentFields(item, { visible: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+2
-2
@@ -28,10 +28,10 @@ export default {
|
||||
const key = this.getItemKey(item);
|
||||
return this.items.has(key);
|
||||
},
|
||||
setSelected(item, bSelected) {
|
||||
setSelected(item, selected) {
|
||||
const key = this.getItemKey(item);
|
||||
const newSelected = new Map(this.items);
|
||||
bSelected ? newSelected.set(key, item) : newSelected.delete(key);
|
||||
selected ? newSelected.set(key, item) : newSelected.delete(key);
|
||||
this.items = newSelected;
|
||||
},
|
||||
selectItems(items = []) {
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Client representation of state and state messages. See: https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/__init__.py#L3292
|
||||
for a list of available states.
|
||||
*/
|
||||
export default {
|
||||
/** deleted while uploading */
|
||||
discarded: {
|
||||
status: "danger",
|
||||
text: "The job creating this dataset was cancelled before completion.",
|
||||
icon: "exclamation-triangle",
|
||||
},
|
||||
/** has no data */
|
||||
empty: {
|
||||
status: "success",
|
||||
text: "No data",
|
||||
},
|
||||
/** the tool producing this dataset failed */
|
||||
error: {
|
||||
status: "danger",
|
||||
text: "An error occurred with this dataset",
|
||||
icon: "exclamation-triangle",
|
||||
},
|
||||
/** metadata discovery/setting failed or errored (but otherwise ok) */
|
||||
failed_metadata: {
|
||||
status: "danger",
|
||||
},
|
||||
/** was created without a tool */
|
||||
new: {
|
||||
status: "warning",
|
||||
text: "This is a new dataset and not all of its data are available yet.",
|
||||
icon: "clock",
|
||||
},
|
||||
/** has successfully completed running */
|
||||
ok: {
|
||||
status: "success",
|
||||
},
|
||||
/** the job that will produce the dataset paused */
|
||||
paused: {
|
||||
status: "info",
|
||||
text: "This job is paused. Use the 'Resume Paused Jobs' in the history menu to resume.",
|
||||
icon: "pause",
|
||||
},
|
||||
/** the job that will produce the dataset queued in the runner */
|
||||
queued: {
|
||||
status: "warning",
|
||||
text: "This job is waiting to run.",
|
||||
icon: "clock",
|
||||
},
|
||||
/** the job that will produce the dataset is running */
|
||||
running: {
|
||||
status: "warning",
|
||||
text: "This job is currently running.",
|
||||
icon: "clock",
|
||||
},
|
||||
/** metadata for the dataset is being discovered/set */
|
||||
setting_metadata: {
|
||||
status: "warning",
|
||||
text: "Metadata is being auto-detected.",
|
||||
icon: "clock",
|
||||
},
|
||||
/** is uploading and not ready */
|
||||
upload: {
|
||||
status: "warning",
|
||||
text: "This dataset is currently uploading.",
|
||||
icon: "clock",
|
||||
},
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import ContentItem from "./ContentItem";
|
||||
|
||||
export default {
|
||||
mixins: [ContentItem],
|
||||
|
||||
computed: {
|
||||
contentItemComponent() {
|
||||
const { element_type } = this.item;
|
||||
switch (element_type) {
|
||||
case "hda":
|
||||
return "Subdataset";
|
||||
case "hdca":
|
||||
return "Subcollection";
|
||||
default:
|
||||
return "Placeholder";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* This is a single item in the list. The main job of the contentItem is to
|
||||
* pick a specific selector for the :is attribute of the generic component
|
||||
* placeholder. This should be a dataset, collection, or other component
|
||||
* depending on the specific list implementation.
|
||||
*
|
||||
* The mixin also pulls out "selected", and "expanded" properties which are
|
||||
* derived from the current component state rather than the data itself.
|
||||
*/
|
||||
|
||||
import Placeholder from "./Placeholder";
|
||||
import Dataset from "./Dataset";
|
||||
import DatasetCollection from "./DatasetCollection";
|
||||
import Subdataset from "./Subdataset";
|
||||
import Subcollection from "./Subcollection";
|
||||
|
||||
export default {
|
||||
template: `
|
||||
<component :is="contentItemComponent"
|
||||
:data-ci-type="contentItemComponent"
|
||||
class="content-item"
|
||||
:class="{ loading }"
|
||||
:tabindex="index"
|
||||
:writable="writable"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:row-key="rowKey"
|
||||
:writable="writable"
|
||||
@mouseover.native.stop="setFocus(index)"
|
||||
@keydown.native.arrow-up.self.stop="setFocus(index - 1)"
|
||||
@keydown.native.arrow-down.self.stop="setFocus(index + 1)"
|
||||
/>
|
||||
`,
|
||||
|
||||
components: {
|
||||
Placeholder,
|
||||
Dataset,
|
||||
DatasetCollection,
|
||||
Subdataset,
|
||||
Subcollection,
|
||||
},
|
||||
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
index: { type: Number, required: false, default: null },
|
||||
rowKey: { type: [Number, String], required: false, default: "" },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
suppressFocus: true,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
setFocus(index) {
|
||||
if (this.suppressFocus) {
|
||||
return;
|
||||
}
|
||||
const ul = this.$el.closest(".scroller");
|
||||
const el = ul.querySelector(`[tabindex="${index}"]`);
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
loading() {
|
||||
return !this.item;
|
||||
},
|
||||
contentItemComponent() {
|
||||
// override me
|
||||
return "Placeholder";
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.$on("bv::dropdown::show", () => (this.suppressFocus = true));
|
||||
this.$root.$on("bv::dropdown::hide", () => (this.suppressFocus = false));
|
||||
},
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<DatasetUI
|
||||
v-if="dataset"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:dataset="dataset"
|
||||
@update:dataset="onUpdate"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@unhide="onUnhide"
|
||||
@copy-link="onCopyLink" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatasetUI from "./DatasetUI";
|
||||
import { Dataset } from "../../model";
|
||||
import { deleteContent, updateContentFields } from "../../model/queries";
|
||||
import { copy as sendToClipboard } from "utils/clipboard";
|
||||
import { absPath } from "utils/redirect";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
return new Dataset(this.item);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDelete(opts = {}) {
|
||||
await deleteContent(this.item, opts);
|
||||
},
|
||||
async onUnhide() {
|
||||
await this.onUpdate({ visible: true });
|
||||
},
|
||||
async onUndelete() {
|
||||
await this.onUpdate({ deleted: false });
|
||||
},
|
||||
async onUpdate(changes) {
|
||||
await updateContentFields(this.item, changes);
|
||||
},
|
||||
onCopyLink() {
|
||||
const relPath = this.dataset.getUrl("download");
|
||||
const msg = this.localize("Link is copied to your clipboard");
|
||||
sendToClipboard(absPath(relPath), msg);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,294 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
v-if="notIn(STATES.NOT_VIEWABLE, STATES.DISCARDED)"
|
||||
icon="eye"
|
||||
:title="displayButtonTitle"
|
||||
:disabled="dataset.purged || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="viewData"
|
||||
variant="link"
|
||||
class="px-1 display-btn" />
|
||||
<IconButton
|
||||
v-if="writable && notIn(STATES.DISCARDED)"
|
||||
icon="pen"
|
||||
:title="editButtonTitle"
|
||||
:disabled="dataset.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="$emit('edit')"
|
||||
variant="link"
|
||||
class="px-1 edit-btn" />
|
||||
<IconButton
|
||||
v-if="writable && dataset.accessible"
|
||||
:icon="dataset.deleted ? 'trash-restore' : 'trash'"
|
||||
:title="deleteButtonTitle"
|
||||
:disabled="dataset.purged"
|
||||
v-b-modal="bsId('delete-modal')"
|
||||
variant="link"
|
||||
class="px-1 delete-btn"
|
||||
@click.stop />
|
||||
|
||||
<b-dropdown
|
||||
right
|
||||
size="sm"
|
||||
variant="link"
|
||||
:text="'Dataset Operations' | l"
|
||||
toggle-class="p-1 pl-2"
|
||||
class="flex-grow-0 dataset-operations-dropdown"
|
||||
v-if="expanded"
|
||||
boundary="window">
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="ellipsis-v" variant="link" />
|
||||
<span class="sr-only">Dataset Operations</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="isIn(STATES.ERROR)"
|
||||
key="show-error"
|
||||
title="Error"
|
||||
@click.stop="
|
||||
backboneRoute('datasets/error', {
|
||||
dataset_id: dataset.id,
|
||||
})
|
||||
">
|
||||
<Icon icon="exclamation-triangle" class="mr-1" />
|
||||
<span v-localize>Show Error</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="showDownloads && dataset.getUrl('download')"
|
||||
title="Copy Link"
|
||||
@click.stop="$emit('copy-link')">
|
||||
<Icon icon="link" class="mr-1" />
|
||||
<span v-localize>Copy Link</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-group v-if="showDownloads && dataset.hasMetaData">
|
||||
<b-dropdown-item
|
||||
v-for="mf in dataset.meta_files"
|
||||
:key="'download-' + mf.download_url"
|
||||
:title="'Download ' + mf.file_type"
|
||||
:href="prependPath(dataset.getUrl('meta_download') + mf.file_type)"
|
||||
target="_blank"
|
||||
download>
|
||||
<Icon icon="download" class="mr-1" />
|
||||
<span v-localize>{{ "Download " + mf.file_type }}</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown-group>
|
||||
|
||||
<b-dropdown-item
|
||||
v-else-if="showDownloads"
|
||||
title="Download"
|
||||
:href="prependPath(dataset.getUrl('download'))"
|
||||
target="_blank"
|
||||
class="download-btn"
|
||||
download>
|
||||
<Icon icon="download" class="mr-1" />
|
||||
<span v-localize>Download</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="dataset.rerunnable && dataset.creating_job && notIn(STATES.UPLOAD, STATES.NOT_VIEWABLE)"
|
||||
title="Run job again"
|
||||
:href="prependPath(dataset.getUrl('rerun'))"
|
||||
@click.stop.prevent="
|
||||
backboneRoute('/', {
|
||||
job_id: dataset.creating_job,
|
||||
})
|
||||
">
|
||||
<Icon icon="play" class="mr-1" />
|
||||
<span v-localize>Run job again</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="showViz && hasViz && isIn(STATES.OK, STATES.FAILED_METADATA)"
|
||||
title="Visualize Data"
|
||||
@click.stop.prevent="visualize">
|
||||
<Icon icon="chart-area" class="mr-1" />
|
||||
<span v-localize>Visualize Data</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="currentUser && currentUser.id && dataset.creating_job"
|
||||
title="Tool Help"
|
||||
@click.stop="showToolHelp(dataset.creating_job)">
|
||||
<Icon icon="question" class="mr-1" />
|
||||
<span v-localize>Tool Help</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="notIn(STATES.NOT_VIEWABLE)"
|
||||
key="dataset-details"
|
||||
title="View Dataset Details"
|
||||
class="params-btn"
|
||||
@click.stop.prevent="showDetails">
|
||||
<Icon icon="info-circle" class="mr-1" />
|
||||
<span v-localize>View Dataset Details</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-button-group>
|
||||
|
||||
<!-- modals -->
|
||||
<b-modal :id="bsId('delete-modal')" title="Delete Dataset?" title-tag="h2" @ok="onDeleteClick">
|
||||
<p v-localize>Really delete this dataset?</p>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { Dataset } from "../../model";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
inject: ["STATES"],
|
||||
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
dataset: { type: Dataset, required: true },
|
||||
expanded: { type: Boolean, required: true },
|
||||
writable: { type: Boolean, default: true },
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
toolHelp: null,
|
||||
testVal: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters("user", ["currentUser"]),
|
||||
...mapGetters("config", ["config"]),
|
||||
|
||||
showViz() {
|
||||
return this.config.visualizations_visible;
|
||||
},
|
||||
|
||||
displayButtonTitle() {
|
||||
if (this.dataset.purged) {
|
||||
return "Cannot display datasets removed from disk";
|
||||
}
|
||||
if (this.dataset.state == this.STATES.UPLOAD) {
|
||||
return "This dataset must finish uploading before it can be viewed";
|
||||
}
|
||||
if (this.dataset.state == this.STATES.NEW) {
|
||||
return "This dataset is not yet viewable";
|
||||
}
|
||||
return "View data";
|
||||
},
|
||||
|
||||
editButtonTitle() {
|
||||
if (this.dataset.deleted) {
|
||||
return "Undelete dataset to edit attributes";
|
||||
}
|
||||
if (this.dataset.purged) {
|
||||
return "Cannot edit attributes of datasets removed from disk";
|
||||
}
|
||||
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
|
||||
if (unreadyStates.has(this.dataset.state)) {
|
||||
return "This dataset is not yet editable";
|
||||
}
|
||||
return "Edit attributes";
|
||||
},
|
||||
|
||||
deleteButtonTitle() {
|
||||
return this.dataset.purged
|
||||
? "Dataset has been permanently deleted"
|
||||
: this.dataset.deleted
|
||||
? "Undelete"
|
||||
: "Delete";
|
||||
},
|
||||
|
||||
showDownloads() {
|
||||
if (this.dataset.purged) {
|
||||
return false;
|
||||
}
|
||||
if (!this.dataset.hasData) {
|
||||
return false;
|
||||
}
|
||||
const okStates = new Set([this.STATES.OK, this.STATES.FAILED_METADATA, this.STATES.ERROR]);
|
||||
return okStates.has(this.dataset.state);
|
||||
},
|
||||
|
||||
hasViz() {
|
||||
const viz_count = this.dataset.viz_count || 0;
|
||||
return viz_count && this.dataset.hasData && !this.dataset.deleted;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
notIn(...states) {
|
||||
const badStates = new Set(states);
|
||||
return !badStates.has(this.dataset.state);
|
||||
},
|
||||
|
||||
isIn(...states) {
|
||||
const goodStates = new Set(states);
|
||||
return goodStates.has(this.dataset.state);
|
||||
},
|
||||
|
||||
viewData() {
|
||||
const id = this.dataset.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this.useGalaxy((Galaxy) => {
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
Galaxy.frame.addDataset(id);
|
||||
} else {
|
||||
const path = this.dataset.getUrl("display");
|
||||
if (path) {
|
||||
this.iframeRedirect(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// wierd iframe navigation
|
||||
visualize() {
|
||||
const showDetailsUrl = `/datasets/${this.dataset.id}/details`;
|
||||
const redirectParams = {
|
||||
path: showDetailsUrl,
|
||||
title: "Dataset details",
|
||||
tryIframe: false,
|
||||
};
|
||||
if (!this.iframeAdd(redirectParams)) {
|
||||
this.backboneRoute("visualizations", {
|
||||
dataset_id: this.dataset.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showDetails() {
|
||||
const showDetailsUrl = `/datasets/${this.dataset.id}/details`;
|
||||
const redirectParams = {
|
||||
path: showDetailsUrl,
|
||||
title: "Dataset details",
|
||||
tryIframe: false,
|
||||
};
|
||||
if (!this.iframeAdd(redirectParams)) {
|
||||
this.backboneRoute(showDetailsUrl);
|
||||
}
|
||||
},
|
||||
|
||||
showToolHelp(job_id) {
|
||||
this.eventHub.$emit("toggleToolHelp", job_id);
|
||||
},
|
||||
|
||||
onDeleteClick() {
|
||||
const eventName = this.dataset.deleted ? "undelete" : "delete";
|
||||
this.$emit(eventName);
|
||||
},
|
||||
|
||||
// bootstrap demands ids instead of simply using a ref so I guess we're doing this
|
||||
bsId(suffix) {
|
||||
return `dataset-${this.dataset.id}-${suffix}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,250 +0,0 @@
|
||||
<!--
|
||||
The guts and markup of the dataset item separated from the
|
||||
data assignment so we can re-use it on the collection listing
|
||||
|
||||
DO NOT put data retrieval into this component, access the dataset
|
||||
either through the props, and make updates through the events -->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dataset history-content"
|
||||
:id="typeId"
|
||||
:class="{ expanded, collapsed, selected }"
|
||||
:data-hid="dataset.hid"
|
||||
:data-state="dataset.state"
|
||||
@keydown.arrow-left.self.stop="$emit('update:expanded', false)"
|
||||
@keydown.arrow-right.self.stop="$emit('update:expanded', true)"
|
||||
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)">
|
||||
<!-- name, state buttons, menus -->
|
||||
<nav class="content-top-menu p-1 d-flex cursor-pointer" @click.stop="$emit('update:expanded', !expanded)">
|
||||
<div class="d-flex flex-grow-1 overflow-hidden">
|
||||
<div class="pl-1" v-if="showSelectionBox">
|
||||
<b-check class="selector" :checked="selected" @change="$emit('update:selected', $event)"></b-check>
|
||||
</div>
|
||||
|
||||
<StatusIcon
|
||||
v-if="!collapsed"
|
||||
class="status-icon px-1"
|
||||
:state="dataset.state"
|
||||
@click.stop="onStatusClick" />
|
||||
|
||||
<IconButton
|
||||
v-if="!collapsed && !dataset.visible"
|
||||
class="px-1"
|
||||
state="hidden"
|
||||
title="Unhide"
|
||||
icon="eye-slash"
|
||||
@click.stop="$emit('unhide')" />
|
||||
|
||||
<IconButton
|
||||
v-if="!collapsed && dataset.isDeleted && !dataset.purged"
|
||||
class="px-1"
|
||||
state="deleted"
|
||||
title="Undelete"
|
||||
icon="trash-restore"
|
||||
@click.stop="$emit('undelete')" />
|
||||
|
||||
<div class="content-title title p-1 overflow-hidden">
|
||||
<h5 class="text-truncate" v-if="collapsed">
|
||||
<span class="hid" data-description="dataset hid">{{ dataset.hid }}</span>
|
||||
<span class="name" data-description="dataset name">{{ dataset.title }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<slot name="menu">
|
||||
<DatasetMenu
|
||||
:dataset="dataset"
|
||||
:expanded="expanded"
|
||||
:writable="writable"
|
||||
@edit="edit"
|
||||
v-on="$listeners" />
|
||||
</slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="collapsed && dataset.nameTags.length" class="nametags px-2 pb-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
|
||||
<div v-if="expanded" class="p-3 details">
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<h4 data-description="dataset name">{{ name || "(Dataset Name)" }}</h4>
|
||||
|
||||
<template v-if="!editing">
|
||||
<p v-if="annotation">{{ annotation }}</p>
|
||||
<div v-if="dataset.nameTags.length" class="nametags mt-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="my-3">
|
||||
<b-textarea
|
||||
class="mb-3"
|
||||
v-model="name"
|
||||
placeholder="Dataset Name"
|
||||
trim
|
||||
max-rows="4"></b-textarea>
|
||||
|
||||
<b-textarea
|
||||
class="mb-3"
|
||||
v-model="annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"></b-textarea>
|
||||
|
||||
<StatelessTags class="mt-2" v-model="tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="Dataset"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert" />
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<DatasetSummary :dataset="dataset" class="summary" @edit="edit" />
|
||||
|
||||
<div class="display-applications" v-if="dataset.displayLinks.length">
|
||||
<div class="display-application" v-for="app in dataset.displayLinks" :key="app.label">
|
||||
<span class="display-application-location">
|
||||
{{ app.label }}
|
||||
</span>
|
||||
<span class="display-application-links">
|
||||
<a v-for="l in app.links" :key="l.href" :href="l.href" :target="l.target">
|
||||
{{ l.text }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre v-if="dataset.peek" class="dataset-peek p-1" v-html="dataset.peek"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dataset, STATES } from "../../model";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import StatusIcon from "../../StatusIcon";
|
||||
import DatasetMenu from "./DatasetMenu";
|
||||
import DatasetSummary from "./Summary";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import IconButton from "components/IconButton";
|
||||
import EditorMenu from "../../EditorMenu";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
StatusIcon,
|
||||
DatasetMenu,
|
||||
DatasetSummary,
|
||||
Nametag,
|
||||
IconButton,
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
},
|
||||
props: {
|
||||
dataset: { type: Dataset, required: true },
|
||||
expanded: { type: Boolean, required: true },
|
||||
selected: { type: Boolean, required: false, default: false },
|
||||
showSelection: { type: Boolean, required: false, default: false },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
selectable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
tempContent: this.dataset.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
collapsed() {
|
||||
return !this.expanded;
|
||||
},
|
||||
typeId() {
|
||||
return this.dataset.type_id;
|
||||
},
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
valid() {
|
||||
return true;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempContent.equals(this.dataset);
|
||||
},
|
||||
name: {
|
||||
get() {
|
||||
return this.tempContent?.name || "";
|
||||
},
|
||||
set(newVal) {
|
||||
this.patchTempContent({ name: newVal });
|
||||
},
|
||||
},
|
||||
annotation: {
|
||||
get() {
|
||||
return this.tempContent?.annotation || "";
|
||||
},
|
||||
set(newVal) {
|
||||
this.patchTempContent({ annotation: newVal });
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempContent?.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
if (Array.isArray(newTags)) {
|
||||
this.patchTempContent({ tags: newTags });
|
||||
}
|
||||
},
|
||||
},
|
||||
showSelectionBox() {
|
||||
return this.selectable && this.showSelection;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onStatusClick(evt) {
|
||||
switch (this.dataset.state) {
|
||||
case STATES.ERROR:
|
||||
this.backboneRoute("datasets/error", { dataset_id: this.dataset.id });
|
||||
break;
|
||||
default:
|
||||
console.log("unhandled status icon click", this.dataset.state);
|
||||
}
|
||||
},
|
||||
patchTempContent(props = {}) {
|
||||
this.tempContent = this.tempContent.patch(props);
|
||||
},
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:dataset", this.tempContent);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.tempContent = this.dataset.clone();
|
||||
},
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
edit() {
|
||||
this.backboneRoute("datasets/edit", { dataset_id: this.dataset.id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>The job creating this dataset was cancelled before completion.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div v-if="props.dataset">
|
||||
<span v-localize>No data</span>: <i>{{ props.dataset.misc_blurb }}</i>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template functional>
|
||||
<div v-if="props.dataset">
|
||||
<div v-if="!props.dataset.purged && props.dataset.misc_info">
|
||||
<span>{{ props.dataset.misc_info }}</span>
|
||||
</div>
|
||||
<span class="help-text" v-localize>An error occurred with this dataset</span>
|
||||
<div v-if="props.dataset.misc_info" class="job-error-text">
|
||||
<span>{{ props.dataset.misc_info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>This is a new dataset and not all of its data are available yet.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>You do not have permission to view this dataset.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template functional>
|
||||
<div v-if="props.dataset">
|
||||
<div v-if="props.dataset.misc_blurb" class="blurb">
|
||||
<span class="value">{{ props.dataset.misc_blurb }}</span>
|
||||
</div>
|
||||
<div v-if="props.dataset.file_ext" class="datatype">
|
||||
<label class="prompt" v-localize>format</label>
|
||||
<span class="value">{{ props.dataset.file_ext }}</span>
|
||||
</div>
|
||||
<div v-if="props.dataset.genome_build" class="dbkey">
|
||||
<label class="prompt" v-localize>database</label>
|
||||
<b-link class="value" @click.stop="listeners['edit']()">{{ props.dataset.genome_build }}</b-link>
|
||||
</div>
|
||||
<div v-if="props.dataset.misc_info" class="info">
|
||||
<span class="value">{{ props.dataset.misc_info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template functional>
|
||||
<div v-if="dataset">
|
||||
<p v-localize>This job is paused. Use the "Resume Paused Jobs" in the history menu to resume.</p>
|
||||
<div v-if="dataset.misc_info">
|
||||
<span>{{ props.dataset.misc_info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>This job is waiting to run.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>This job is currently running.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>Metadata is being auto-detected.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template functional>
|
||||
<div v-if="props.dataset">Error: unknown dataset state "{{ props.dataset.state }}"</div>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template functional>
|
||||
<div>
|
||||
<p v-localize>This dataset is currently uploading.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<component :is="summaryComponent" :dataset="dataset" @edit="edit" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { capitalize, camelize } from "underscore.string";
|
||||
|
||||
import Discarded from "./Discarded";
|
||||
import Empty from "./Empty";
|
||||
import Error from "./Error";
|
||||
import New from "./New";
|
||||
import NotViewable from "./NotViewable";
|
||||
import Ok from "./Ok";
|
||||
import Paused from "./Paused";
|
||||
import Queued from "./Queued";
|
||||
import Running from "./Running";
|
||||
import SettingMetadata from "./SettingMetadata";
|
||||
import Upload from "./Upload";
|
||||
|
||||
export default {
|
||||
inject: ["STATES"],
|
||||
components: {
|
||||
Discarded,
|
||||
Empty,
|
||||
Error,
|
||||
New,
|
||||
NotViewable,
|
||||
Ok,
|
||||
Paused,
|
||||
Queued,
|
||||
Running,
|
||||
SettingMetadata,
|
||||
Upload,
|
||||
},
|
||||
props: {
|
||||
dataset: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
summaryComponent() {
|
||||
let state = this.dataset.state;
|
||||
if (state == this.STATES.FAILED_METADATA) {
|
||||
state = this.STATES.OK;
|
||||
}
|
||||
return capitalize(camelize(state));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
edit() {
|
||||
this.$emit("edit");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
import Dataset from "./Dataset";
|
||||
|
||||
export { default as DatasetUI } from "./DatasetUI";
|
||||
export { default as DatasetMenu } from "./DatasetMenu";
|
||||
export default Dataset;
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<DscUI
|
||||
v-if="dsc"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
class="dataset-collection history-content"
|
||||
:dsc="dsc"
|
||||
@update:dsc="onUpdate"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@unhide="onUnhide" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DscUI from "./DscUI";
|
||||
import { DatasetCollection } from "../../model";
|
||||
import { deleteDatasetCollection, updateContentFields } from "../../model/queries";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DscUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
dsc() {
|
||||
return new DatasetCollection(this.item);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDelete(flags = {}) {
|
||||
const { recursive = false, purge = false } = flags;
|
||||
const collection = this.item;
|
||||
await deleteDatasetCollection(collection, recursive, purge);
|
||||
},
|
||||
async onUnhide() {
|
||||
await this.onUpdate({ visible: true });
|
||||
},
|
||||
async onUndelete() {
|
||||
await this.onUpdate({ deleted: false });
|
||||
},
|
||||
async onUpdate(changes) {
|
||||
await updateContentFields(this.item, changes);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="description">
|
||||
{{ dsc.collectionType | localize }} of {{ dsc.totalElements }}
|
||||
<b v-if="isHomogeneous">{{ homogeneousDatatype }} </b>datasets
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../../model/DatasetCollection";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
},
|
||||
computed: {
|
||||
/** @return {Boolean} */
|
||||
isHomogeneous() {
|
||||
return this.dsc.isHomogeneous;
|
||||
},
|
||||
/** @return {String} */
|
||||
homogeneousDatatype() {
|
||||
return this.dsc.homogeneousDatatype;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div class="collection-menu">
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
icon="info-circle"
|
||||
v-if="hasJob"
|
||||
key="dataset-details"
|
||||
title="View Dataset Collection Details"
|
||||
@click.stop.prevent="backboneRoute(collectionDetailsPath)"
|
||||
variant="link"
|
||||
class="px-1">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="notIn(STATES.DISCARDED)"
|
||||
icon="pen"
|
||||
:title="editButtonTitle"
|
||||
:disabled="dsc.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="backboneRoute('collection/edit/' + dsc.hdca_id)"
|
||||
variant="link"
|
||||
class="px-1 dsc-edit-view" />
|
||||
<b-dropdown v-if="notIn(STATES.DISCARDED)" no-caret right variant="link" size="sm" boundary="window">
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="trash" variant="link" />
|
||||
<span class="sr-only">Delete Collection</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete')">
|
||||
<span v-localize>Delete Collection Only</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete', { recursive: true })">
|
||||
<span v-localize>Delete Contained Datasets</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete', { recursive: true, purge: true })">
|
||||
<span v-localize>Purge Contained Datasets</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../../model";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
deleteSelected: null,
|
||||
deleteCollectionModalShow: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
mixins: [legacyNavigationMixin],
|
||||
inject: ["STATES"],
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
},
|
||||
computed: {
|
||||
editButtonTitle() {
|
||||
if (this.dsc.deleted) {
|
||||
return "Collection must not be deleted to edit";
|
||||
}
|
||||
if (this.dsc.purged) {
|
||||
return "Cannot edit attributes of collections removed from disk";
|
||||
}
|
||||
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
|
||||
if (unreadyStates.has(this.dsc.state)) {
|
||||
return "This collection is not yet editable";
|
||||
}
|
||||
return "Edit attributes";
|
||||
},
|
||||
deleteCollectionButtonTitle() {
|
||||
return "Delete Collection";
|
||||
},
|
||||
hasJob() {
|
||||
return this.dsc?.job_source_type == "Job";
|
||||
},
|
||||
collectionDetailsPath() {
|
||||
return `jobs/${this.dsc.job_source_id}/view`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
runDelete: function (kind) {
|
||||
if (kind) {
|
||||
this.$emit("delete", kind);
|
||||
} else {
|
||||
this.$emit("delete");
|
||||
}
|
||||
},
|
||||
notIn(...states) {
|
||||
const badStates = new Set(states);
|
||||
return !badStates.has(this.dsc.state);
|
||||
},
|
||||
isIn(...states) {
|
||||
const goodStates = new Set(states);
|
||||
return goodStates.has(this.dsc.state);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,115 +0,0 @@
|
||||
<!-- UI only, updates/changes to data should be handled outside
|
||||
this component via events emissions -->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dataset dataset-collection collapsed"
|
||||
:class="{ selected }"
|
||||
:id="typedId"
|
||||
:data-hid="dsc.hid"
|
||||
:data-state="dsc.state"
|
||||
@keydown.arrow-right.self.stop="$emit('viewCollection')"
|
||||
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)">
|
||||
<nav class="content-top-menu p-1 d-flex cursor-pointer">
|
||||
<div class="d-flex flex-grow-1 overflow-hidden">
|
||||
<div class="pl-1" v-if="showSelection">
|
||||
<b-check
|
||||
class="selector"
|
||||
:checked="selected"
|
||||
@click.stop
|
||||
@change="$emit('update:selected', $event)" />
|
||||
</div>
|
||||
|
||||
<StatusIcon
|
||||
v-if="dsc.state != 'ok'"
|
||||
class="status-icon px-1"
|
||||
:state="dsc.state"
|
||||
@click.stop="onStatusClick" />
|
||||
|
||||
<IconButton
|
||||
v-if="!dsc.visible"
|
||||
class="px-1"
|
||||
state="hidden"
|
||||
title="Unhide"
|
||||
icon="eye-slash"
|
||||
@click.stop="$emit('unhide')" />
|
||||
|
||||
<IconButton
|
||||
v-if="dsc.deleted"
|
||||
class="px-1"
|
||||
state="deleted"
|
||||
title="Undelete"
|
||||
icon="trash-restore"
|
||||
@click.stop="$emit('undelete')" />
|
||||
|
||||
<div class="content-title title flex-grow-1 overflow-hidden" @click.stop="$emit('viewCollection')">
|
||||
<h5 class="text-truncate">
|
||||
<span class="hid">{{ dsc.hid }}</span>
|
||||
<span class="name">{{ dsc.name }}</span>
|
||||
</h5>
|
||||
<DscDescription :dsc="dsc" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" v-if="writable">
|
||||
<slot name="menu">
|
||||
<DscMenu v-if="!dsc.deleted" v-on="$listeners" :dsc="dsc" />
|
||||
</slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="collapsed && dsc.nameTags.length" class="nametags px-2 pb-2">
|
||||
<Nametag v-for="tag in dsc.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
|
||||
<JobStateProgress class="m-2" v-if="dsc.jobSummary" :summary="dsc.jobSummary" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../../model/DatasetCollection";
|
||||
import StatusIcon from "../../StatusIcon";
|
||||
import JobStateProgress from "./JobStateProgress";
|
||||
import DscDescription from "./DscDescription";
|
||||
import DscMenu from "./DscMenu";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIcon,
|
||||
JobStateProgress,
|
||||
DscMenu,
|
||||
Nametag,
|
||||
IconButton,
|
||||
DscDescription,
|
||||
},
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
selected: { type: Boolean, required: false, default: false },
|
||||
showSelection: { type: Boolean, required: false, default: false },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
methods: {
|
||||
onStatusClick() {
|
||||
console.log("onStatusClick", ...arguments);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
typedId() {
|
||||
return this.dsc.type_id;
|
||||
},
|
||||
collapsed() {
|
||||
return !this.expanded;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
header,
|
||||
header * {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<!-- Job state progress bar for a collection. There's another similar component
|
||||
at components/JobStates/CollectionJobStates but it relies on the backbone data
|
||||
model, so probably has to go eventually.-->
|
||||
|
||||
<template>
|
||||
<b-progress v-if="maxJobs && runningJobs" :max="maxJobs" height="1em">
|
||||
<b-progress-bar v-if="errorJobs" :value="errorJobs" variant="danger"></b-progress-bar>
|
||||
<b-progress-bar v-if="okJobs" :value="okJobs" variant="success"></b-progress-bar>
|
||||
<b-progress-bar v-if="runningJobs" :value="runningJobs" variant="info" animated></b-progress-bar>
|
||||
<b-progress-bar v-if="waitingJobs" :value="waitingJobs" variant="dark" animated></b-progress-bar>
|
||||
</b-progress>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { JobStateSummary } from "../../model";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
summary: { type: JobStateSummary, required: true },
|
||||
},
|
||||
computed: {
|
||||
hasRunningJobs() {
|
||||
return this.summary && this.summary.running > 0;
|
||||
},
|
||||
maxJobs() {
|
||||
return this.summary.all_jobs;
|
||||
},
|
||||
okJobs() {
|
||||
return this.summary.ok;
|
||||
},
|
||||
runningJobs() {
|
||||
return this.summary.running;
|
||||
},
|
||||
errorJobs() {
|
||||
const { failed = 0, error = 0 } = this.summary;
|
||||
return failed + error;
|
||||
},
|
||||
waitingJobs() {
|
||||
const { waiting = 0, queued = 0 } = this.summary;
|
||||
return waiting + queued;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
import DatasetCollection from "./DatasetCollection";
|
||||
|
||||
export { default as DscUI } from "./DscUI";
|
||||
export { default as DscMenu } from "./DscMenu";
|
||||
export { default as JobStateProgress } from "./JobStateProgress";
|
||||
export default DatasetCollection;
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="contentItem in collectionContents" :index="1" :key="contentItem.id" style="margin-left: 2em">
|
||||
<DatasetProvider
|
||||
v-if="contentItem.element_type === 'hda'"
|
||||
:id="contentItem.object.id"
|
||||
v-slot="{ result: item, loading }">
|
||||
<div>
|
||||
<loading-span v-if="loading" message="Loading datasets" />
|
||||
<DatasetUIWrapper v-else :item="item" :element_identifier="contentItem.element_identifier">
|
||||
</DatasetUIWrapper>
|
||||
</div>
|
||||
</DatasetProvider>
|
||||
<DatasetCollectionUIWrapper
|
||||
v-if="contentItem.element_type === 'dataset_collection'"
|
||||
:item="contentItem"
|
||||
:element_count="collectionContents.length + 1">
|
||||
</DatasetCollectionUIWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { DatasetProvider } from "components/providers";
|
||||
import DatasetUIWrapper from "./DatasetUIWrapper";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetProvider,
|
||||
DatasetCollectionUIWrapper: () => import("./DatasetCollectionUIWrapper"),
|
||||
DatasetUIWrapper,
|
||||
LoadingSpan,
|
||||
},
|
||||
props: {
|
||||
collectionContents: { type: Array, required: true },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import DatasetCollectionUIWrapper from "./DatasetCollectionUIWrapper";
|
||||
import DscUI from "components/History/ContentItem/DatasetCollection/DscUI";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import datasetCollectionRaw from "components/providers/test/json/DatasetCollection.json";
|
||||
import datasetCollectionContent from "components/providers/test/json/DatasetCollection.nested.json";
|
||||
|
||||
describe("DatasetUIWrapper.vue with Dataset", () => {
|
||||
let wrapper;
|
||||
let propsData;
|
||||
let onHide;
|
||||
let onUnhide;
|
||||
let onDelete;
|
||||
let onUndelete;
|
||||
|
||||
beforeEach(async () => {
|
||||
propsData = {
|
||||
item: datasetCollectionRaw,
|
||||
};
|
||||
onHide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onHide").mockImplementation();
|
||||
onUnhide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUnhide").mockImplementation();
|
||||
onDelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onDelete").mockImplementation();
|
||||
onUndelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUndelete").mockImplementation();
|
||||
wrapper = shallowMount(DatasetCollectionUIWrapper, {
|
||||
propsData,
|
||||
});
|
||||
});
|
||||
|
||||
it("loads DscUI component", async () => {
|
||||
expect(wrapper.findComponent(DscUI).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("reacts to all emittted events", async () => {
|
||||
expect(onHide).not.toHaveBeenCalled();
|
||||
expect(onUnhide).not.toHaveBeenCalled();
|
||||
expect(onDelete).not.toHaveBeenCalled();
|
||||
expect(onUndelete).not.toHaveBeenCalled();
|
||||
wrapper.findComponent(DscUI).vm.$emit("hide");
|
||||
expect(onHide).toHaveBeenCalled();
|
||||
wrapper.findComponent(DscUI).vm.$emit("unhide");
|
||||
expect(onUnhide).toHaveBeenCalled();
|
||||
wrapper.findComponent(DscUI).vm.$emit("delete");
|
||||
expect(onDelete).toHaveBeenCalled();
|
||||
wrapper.findComponent(DscUI).vm.$emit("undelete");
|
||||
expect(onUndelete).toHaveBeenCalled();
|
||||
});
|
||||
it("creates DatasetCollection", async () => {
|
||||
const datasetCollection = wrapper.vm.datasetCollection;
|
||||
expect(datasetCollection.collectionType).toBe("list of pairs");
|
||||
});
|
||||
it("manages collection expansion", async () => {
|
||||
expect(wrapper.vm.expand).toBeFalsy();
|
||||
wrapper.findComponent(DscUI).vm.$emit("viewCollection", wrapper.vm.datasetCollection);
|
||||
expect(wrapper.vm.expand).toBeTruthy();
|
||||
});
|
||||
it("build dsc from collection content", async () => {
|
||||
await wrapper.setProps({ item: datasetCollectionContent });
|
||||
expect(wrapper.vm.datasetCollection.name).toBe(datasetCollectionContent.element_identifier);
|
||||
});
|
||||
});
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<DscUI
|
||||
v-if="datasetCollection"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:dsc="datasetCollection"
|
||||
:show-tags="true"
|
||||
@viewCollection="toggleExpand"
|
||||
@hide="onHide(datasetCollection)"
|
||||
@unhide="onUnhide(datasetCollection)"
|
||||
@delete="onDelete(datasetCollection, $event)"
|
||||
@undelete="onUndelete(datasetCollection)" />
|
||||
<DatasetCollectionContentProvider v-if="expand" :id="datasetCollection.contents_url" v-slot="{ item, loading }">
|
||||
<b-spinner v-if="loading">Loading Dataset Collection...</b-spinner>
|
||||
<DatasetCollectionContents v-else :collection-contents="item" />
|
||||
</DatasetCollectionContentProvider>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { deleteDatasetCollection, updateContentFields } from "../../model/queries";
|
||||
import { DatasetCollection } from "../../model/DatasetCollection";
|
||||
import DscUI from "components/History/ContentItem/DatasetCollection/DscUI";
|
||||
import { DatasetCollectionContentProvider } from "components/providers";
|
||||
import DatasetCollectionContents from "./DatasetCollectionContents";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetCollectionContentProvider,
|
||||
DatasetCollectionContents,
|
||||
DscUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
element_count: { type: Number, required: false },
|
||||
},
|
||||
data() {
|
||||
return { expand: false };
|
||||
},
|
||||
computed: {
|
||||
datasetCollection() {
|
||||
var rawObject = this.item;
|
||||
if (rawObject.model_class === "DatasetCollectionElement") {
|
||||
rawObject = this.item.object;
|
||||
rawObject.name = this.item.element_identifier;
|
||||
rawObject.contents_url = this.item.contents_url || rawObject.contents_url;
|
||||
rawObject.element_count = this.element_count;
|
||||
rawObject.populated_state = "ok";
|
||||
}
|
||||
return new DatasetCollection(rawObject);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleExpand() {
|
||||
this.expand = !this.expand;
|
||||
},
|
||||
async onDelete(collection, flags = {}) {
|
||||
const { recursive = false, purge = false } = flags;
|
||||
await deleteDatasetCollection(collection, recursive, purge);
|
||||
},
|
||||
|
||||
async onUndelete(collection) {
|
||||
await updateContentFields(collection, { deleted: false });
|
||||
},
|
||||
async onHide(collection) {
|
||||
await updateContentFields(collection, { visible: false });
|
||||
},
|
||||
async onUnhide(collection) {
|
||||
await updateContentFields(collection, { visible: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,51 +0,0 @@
|
||||
import DatasetUIWrapper from "./DatasetUIWrapper";
|
||||
import DatasetUI from "components/History/ContentItem/Dataset/DatasetUI";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import raw from "components/providers/test/json/Dataset.json";
|
||||
|
||||
describe("DatasetUIWrapper.vue with Dataset", () => {
|
||||
let wrapper;
|
||||
let propsData;
|
||||
let onDelete;
|
||||
let onUnhide;
|
||||
let onUndelete;
|
||||
let updateDataset;
|
||||
|
||||
beforeEach(async () => {
|
||||
propsData = {
|
||||
item: raw,
|
||||
};
|
||||
onDelete = jest.spyOn(DatasetUIWrapper.methods, "onDelete").mockImplementation();
|
||||
onUnhide = jest.spyOn(DatasetUIWrapper.methods, "onUnhide").mockImplementation();
|
||||
onUndelete = jest.spyOn(DatasetUIWrapper.methods, "onUndelete").mockImplementation();
|
||||
updateDataset = jest.spyOn(DatasetUIWrapper.methods, "onUpdate").mockImplementation();
|
||||
wrapper = shallowMount(DatasetUIWrapper, {
|
||||
propsData,
|
||||
});
|
||||
});
|
||||
|
||||
it("loads DatasetUI component", async () => {
|
||||
expect(wrapper.findComponent(DatasetUI).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("reacts to all emittted events", async () => {
|
||||
expect(onDelete).not.toHaveBeenCalled();
|
||||
expect(onUnhide).not.toHaveBeenCalled();
|
||||
expect(onUndelete).not.toHaveBeenCalled();
|
||||
expect(updateDataset).not.toHaveBeenCalled();
|
||||
wrapper.findComponent(DatasetUI).vm.$emit("delete");
|
||||
expect(onDelete).toHaveBeenCalled();
|
||||
wrapper.findComponent(DatasetUI).vm.$emit("undelete");
|
||||
expect(onUndelete).toHaveBeenCalled();
|
||||
wrapper.findComponent(DatasetUI).vm.$emit("unhide");
|
||||
expect(onUnhide).toHaveBeenCalled();
|
||||
wrapper.findComponent(DatasetUI).vm.$emit("update");
|
||||
expect(updateDataset).toHaveBeenCalled();
|
||||
});
|
||||
it("handles element identifier", async () => {
|
||||
expect(wrapper.vm.dataset.canEditName).toBeTruthy();
|
||||
await wrapper.setProps({ element_identifier: "a cool identifier" });
|
||||
expect(wrapper.vm.dataset.title).toBe("a cool identifier");
|
||||
expect(wrapper.vm.dataset.canEditName).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
<!-- controls display of DisplayUI and delegates modifications -->
|
||||
<template>
|
||||
<DatasetUI
|
||||
v-if="dataset"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@update:expanded="expand"
|
||||
:expanded="expanded"
|
||||
:dataset="dataset"
|
||||
:show-tags="true"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@unhide="onUnhide"
|
||||
@update="onUpdate">
|
||||
</DatasetUI>
|
||||
</template>
|
||||
<script>
|
||||
import DatasetUI from "components/History/ContentItem/Dataset/DatasetUI";
|
||||
import { Dataset } from "../../model";
|
||||
import { deleteContent, updateContentFields } from "../../model/queries";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
element_identifier: { type: String, required: false, default: "" },
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
const element = { ...this.item, element_identifier: this.element_identifier };
|
||||
return new Dataset(element);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
expand(event) {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
async onDelete(opts = {}) {
|
||||
await deleteContent(this.dataset, opts);
|
||||
},
|
||||
async onUnhide() {
|
||||
await this.onUpdate({ visible: true });
|
||||
},
|
||||
async onUndelete() {
|
||||
await this.onUpdate({ deleted: false });
|
||||
},
|
||||
async onUpdate(changes = {}) {
|
||||
await updateContentFields(this.dataset, changes);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<component :is="providerComponent" :id="data_item.id" auto-refresh v-slot="{ result: item, loading }">
|
||||
<div>
|
||||
<loading-span v-if="loading" message="Loading dataset" />
|
||||
<component :is="renderComponent" v-if="item" :item="item"> </component>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetProvider, DatasetCollectionProvider } from "components/providers";
|
||||
import DatasetCollectionUIWrapper from "./DatasetCollectionUIWrapper";
|
||||
import DatasetUIWrapper from "./DatasetUIWrapper";
|
||||
import { STATES } from "components/History/model";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetProvider,
|
||||
DatasetCollectionProvider,
|
||||
DatasetUIWrapper,
|
||||
DatasetCollectionUIWrapper,
|
||||
LoadingSpan,
|
||||
},
|
||||
props: {
|
||||
data_item: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
renderComponent() {
|
||||
return { hda: "DatasetUIWrapper", hdca: "DatasetCollectionUIWrapper" }[this.data_item.src];
|
||||
},
|
||||
providerComponent() {
|
||||
return { hda: "DatasetProvider", hdca: "DatasetCollectionProvider" }[this.data_item.src];
|
||||
},
|
||||
},
|
||||
provide: {
|
||||
STATES,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
import ContentItem from "./ContentItem";
|
||||
|
||||
export default {
|
||||
mixins: [ContentItem],
|
||||
|
||||
computed: {
|
||||
contentItemComponent() {
|
||||
if (this.item.id === undefined) {
|
||||
return "Placeholder";
|
||||
}
|
||||
const { history_content_type } = this.item;
|
||||
switch (history_content_type) {
|
||||
case "dataset":
|
||||
return "Dataset";
|
||||
case "dataset_collection":
|
||||
return "DatasetCollection";
|
||||
default:
|
||||
return "Placeholder";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="collapsed" data-state="waiting">
|
||||
<nav class="content-top-menu d-flex align-items-center justify-content-between p-1">
|
||||
<h5 class="flex-grow-1 overflow-hidden mr-auto text-nowrap text-truncate">
|
||||
<span class="name">Placholder: {{ item.hid }}</span>
|
||||
</h5>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,29 +0,0 @@
|
||||
<!-- a separate collection content item for collections
|
||||
inside of other collections -->
|
||||
|
||||
<template>
|
||||
<DscUI v-if="dsc" v-bind="$attrs" v-on="$listeners" class="dataset-collection history-content" :dsc="dsc" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../model/DatasetCollection";
|
||||
import { STATES } from "../model";
|
||||
import DscUI from "./DatasetCollection/DscUI";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DscUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
dsc() {
|
||||
// TODO: I'm sure this is not the right way to set the state, but the collection api
|
||||
// doesn't return any kind of state value at because the api is inconsistent
|
||||
// /api/dataset_collections/d071e794759ab192/contents/e144222fed44799a
|
||||
return new DatasetCollection({ ...this.item, populated_state: STATES.OK });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,61 +0,0 @@
|
||||
<!-- a read-only dataset inside a collection. The collection query does not return enough
|
||||
information to properly render the dataset, so what we do is look it up on expand, checking the
|
||||
cache first to see if we already have the data -->
|
||||
|
||||
<template>
|
||||
<DatasetUI
|
||||
v-if="dataset"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:dataset="dataset"
|
||||
:writable="false"
|
||||
:selectable="false"
|
||||
@update:expanded="onExpand" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dataset } from "../model";
|
||||
import { getContentDetails } from "../model/queries";
|
||||
import DatasetUI from "./Dataset/DatasetUI";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
datasetObject: this.extractDataset(this.item),
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
return new Dataset({ ...this.datasetObject });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
extractDataset() {
|
||||
return {
|
||||
id: this.item.object.id,
|
||||
element_identifier: this.item.element_identifier,
|
||||
state: this.item.object.state,
|
||||
history_id: this.item.object.history_id,
|
||||
history_content_type: "dataset",
|
||||
visible: true,
|
||||
};
|
||||
},
|
||||
onExpand(val) {
|
||||
if (val) {
|
||||
this.loadDetails();
|
||||
}
|
||||
this.$emit("update:expand", val);
|
||||
},
|
||||
async loadDetails() {
|
||||
this.datasetObject = await getContentDetails(this.datasetObject);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Scrolling components for history contents and collection contents.
|
||||
*/
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
export { default as ContentItem } from "./ContentItem";
|
||||
export { default as CollectionContentItem } from "./CollectionContentItem";
|
||||
export { default as HistoryContentItem } from "./HistoryContentItem";
|
||||
@@ -1,32 +0,0 @@
|
||||
@import "theme/blue.scss";
|
||||
@import "scss/mixins.scss";
|
||||
|
||||
.content-item {
|
||||
outline: none;
|
||||
border-bottom: 2px solid white;
|
||||
&:focus-within {
|
||||
box-shadow: inset 0 0 1em 0.25em rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// Nav across the top of the content item
|
||||
.content-item .content-top-menu {
|
||||
min-height: 2em;
|
||||
|
||||
.content-title {
|
||||
.hid {
|
||||
color: adjust-color($text-color, $alpha: -0.6);
|
||||
font-size: 80%;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
min-width: 0.25rem;
|
||||
&:not(:empty)::after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<Details
|
||||
:name="name"
|
||||
:tags="dsc.tags"
|
||||
:writeable="writeable"
|
||||
:show-annotation="false"
|
||||
@save="$emit('update:dsc', $event)">
|
||||
<template v-slot:name>
|
||||
<h3 data-description="collection name display">{{ name || "Collection" }}</h3>
|
||||
<CollectionDescription :item="dsc" />
|
||||
</template>
|
||||
</Details>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Details from "components/History/Layout/Details";
|
||||
import CollectionDescription from "components/History/Content/Collection/CollectionDescription";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CollectionDescription,
|
||||
Details,
|
||||
},
|
||||
props: {
|
||||
dsc: { type: Object, required: true },
|
||||
writeable: { type: Boolean, required: true },
|
||||
},
|
||||
computed: {
|
||||
name() {
|
||||
return this.dsc.name || this.dsc.element_identifier;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button size="sm" variant="link" @click="close">
|
||||
<icon icon="angle-double-left" class="mr-1" data-description="back to history" /><span>History</span>
|
||||
</b-button>
|
||||
<b-button size="sm" variant="link" v-if="previousName" @click="back">
|
||||
<span class="fa fa-angle-left mr-1" /><span>{{ previousName }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
selectedCollections: { type: Array, required: true, validate: (val) => val.length > 0 },
|
||||
},
|
||||
computed: {
|
||||
previousName() {
|
||||
const length = this.selectedCollections.length;
|
||||
if (length > 1) {
|
||||
const last = this.selectedCollections[length - 2];
|
||||
return last.name || last.element_identifier;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
const newList = this.selectedCollections.slice(0, -1);
|
||||
this.$emit("update:selected-collections", newList);
|
||||
},
|
||||
close() {
|
||||
this.$emit("update:selected-collections", []);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,32 +1,25 @@
|
||||
<template>
|
||||
<section>
|
||||
<nav class="d-flex justify-content-end">
|
||||
<nav class="content-operations d-flex justify-content-between bg-secondary">
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
v-if="isRoot"
|
||||
class="mb-2 mx-3"
|
||||
icon="download"
|
||||
title="Download Collection"
|
||||
:href="downloadCollectionUrl"
|
||||
download />
|
||||
<b-button title="Download Collection" size="sm" variant="link" download :href="downloadUrl">
|
||||
<Icon class="mr-1" icon="download" />
|
||||
<span>Download</span>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</nav>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
components: { IconButton },
|
||||
props: {
|
||||
isRoot: { type: Boolean, required: true },
|
||||
collection: { type: Object, required: true },
|
||||
dsc: { type: Object, required: true },
|
||||
},
|
||||
computed: {
|
||||
/** @return {String} */
|
||||
downloadCollectionUrl() {
|
||||
return `${this.collection.url}/download`;
|
||||
downloadUrl() {
|
||||
return `${this.dsc.url}/download`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- When a dataset collection is being viewed, this panel shows the contents of that collection -->
|
||||
|
||||
<template>
|
||||
<UrlDataProvider v-if="dsc" :key="dsc.id" :url="url" auto-refresh v-slot="{ result: payload }">
|
||||
<ExpandedItems :scope-key="dsc.id" :get-item-key="(item) => item.id" v-slot="{ isExpanded, setExpanded }">
|
||||
<Layout class="dataset-collection-panel">
|
||||
<template v-slot:navigation>
|
||||
<CollectionNavigation
|
||||
:history="history"
|
||||
:selected-collections="selectedCollections"
|
||||
v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<template v-slot:listcontrols>
|
||||
<CollectionOperations v-if="isRoot" :dsc="dsc" />
|
||||
</template>
|
||||
|
||||
<template v-slot:details>
|
||||
<CollectionDetails :dsc="dsc" :writeable="isRoot" @update:dsc="updateDsc(dsc, $event)" />
|
||||
</template>
|
||||
|
||||
<template v-slot:listing>
|
||||
<Listing item-key="element_index" :payload="payload" :limit="limit" @scroll="onScroll">
|
||||
<template v-slot:history-item="{ item }">
|
||||
<ContentItem
|
||||
:item="item.object"
|
||||
:id="item.element_index"
|
||||
:name="item.element_identifier"
|
||||
:is-dataset="item.element_type == 'hda'"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-history-item="false"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@view-collection="$emit('view-collection', item)" />
|
||||
</template>
|
||||
</Listing>
|
||||
</template>
|
||||
</Layout>
|
||||
</ExpandedItems>
|
||||
</UrlDataProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import { History } from "components/History/model";
|
||||
import { updateContentFields } from "components/History/model/queries";
|
||||
import ContentItem from "components/History/Content/ContentItem";
|
||||
import ExpandedItems from "components/History/Content/ExpandedItems";
|
||||
import Listing from "components/History/Layout/Listing";
|
||||
import Layout from "components/History/Layout/Layout";
|
||||
import CollectionNavigation from "./CollectionNavigation";
|
||||
import CollectionOperations from "./CollectionOperations";
|
||||
import CollectionDetails from "./CollectionDetails";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CollectionDetails,
|
||||
CollectionNavigation,
|
||||
CollectionOperations,
|
||||
ContentItem,
|
||||
ExpandedItems,
|
||||
Layout,
|
||||
Listing,
|
||||
UrlDataProvider,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
selectedCollections: { type: Array, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dsc() {
|
||||
const arr = this.selectedCollections;
|
||||
return arr[arr.length - 1];
|
||||
},
|
||||
isRoot() {
|
||||
return this.dsc == this.rootCollection;
|
||||
},
|
||||
rootCollection() {
|
||||
return this.selectedCollections[0];
|
||||
},
|
||||
url() {
|
||||
// either the observered source is a collection element or the initial history item i.e. root collection
|
||||
const source = this.dsc.object || this.rootCollection;
|
||||
const contentsUrl = source.contents_url.substring(1);
|
||||
return `${contentsUrl}?offset=${this.offset}&limit=${this.limit}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateDsc(collection, fields) {
|
||||
updateContentFields(collection, fields).then((response) => {
|
||||
Object.keys(response).forEach((key) => {
|
||||
collection[key] = response[key];
|
||||
});
|
||||
});
|
||||
},
|
||||
onScroll(offset) {
|
||||
this.offset = offset;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<!-- Edits a collection, if that collection is editable. -->
|
||||
|
||||
<template>
|
||||
<section class="history-details d-flex" data-description="edit details">
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="history-title" data-description="collection name display">
|
||||
{{ dscName || "(Collection Name)" }}
|
||||
</h3>
|
||||
<p class="mt-1">
|
||||
<DscDescription :dsc="dsc" />
|
||||
</p>
|
||||
|
||||
<div v-if="isEditing" class="mt-3" @keydown.esc="revertAndCancel" data-description="edit form">
|
||||
<b-textarea
|
||||
v-model="dscName"
|
||||
placeholder="Collection Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="name input"></b-textarea>
|
||||
<StatelessTags v-model="tags" class="mt-3 tags" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="dsc.tags && dsc.tags.length">
|
||||
<Nametag v-for="tag in dsc.tags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="Collection"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../model";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import EditorMenu from "../EditorMenu";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import DscDescription from "components/History/ContentItem/DatasetCollection/DscDescription";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nametag,
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
DscDescription,
|
||||
},
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
writable: { type: Boolean, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
tempContent: this.dsc.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dscId() {
|
||||
return this.dsc.id;
|
||||
},
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
valid() {
|
||||
return this.tempContent.name.length > 0;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempContent.equals(this.dsc);
|
||||
},
|
||||
dscName: {
|
||||
get() {
|
||||
return this.tempContent.name;
|
||||
},
|
||||
set(newName) {
|
||||
this.patchTempDsc({ name: newName });
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempContent?.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
if (Array.isArray(newTags)) {
|
||||
this.patchTempDsc({ tags: newTags });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dscId(newId, oldId) {
|
||||
if (newId != oldId) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.tempContent = this.dsc.clone();
|
||||
},
|
||||
patchTempDsc(props = {}) {
|
||||
this.tempContent = this.tempContent.patch(props);
|
||||
},
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:dsc", this.tempContent);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
revertAndCancel() {
|
||||
this.revert();
|
||||
this.editing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,126 +0,0 @@
|
||||
<!-- When a dataset collection is being viewed, this panel shows the contents of that collection -->
|
||||
|
||||
<template>
|
||||
<UrlDataProvider v-if="dsc" :url="getUrl(selectedCollection)" v-slot="{ result: payload }">
|
||||
<ExpandedItems
|
||||
:scope-key="selectedCollection.id"
|
||||
:get-item-key="(item) => item.element_index"
|
||||
v-slot="{ isExpanded, setExpanded }">
|
||||
<Layout class="dataset-collection-panel">
|
||||
<template v-slot:globalnav>
|
||||
<TopNav :history="history" :selected-collections="selectedCollections" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<template v-slot:localnav>
|
||||
<!-- Empty -->
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<template v-slot:listcontrols>
|
||||
<CollectionOperations :collection="selectedCollection" :is-root="isRoot" />
|
||||
</template>
|
||||
|
||||
<template v-slot:details>
|
||||
<Details :dsc="dsc" :writable="writable" @update:dsc="updateDsc(dsc, $event)" />
|
||||
</template>
|
||||
|
||||
<template v-slot:listing>
|
||||
<HistoryListing
|
||||
item-key="element_index"
|
||||
:page-size="pageSize"
|
||||
:payload="payload"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:history-item="{ item, index }">
|
||||
<CollectionContentItem
|
||||
:item="item"
|
||||
:index="index"
|
||||
:expanded="isExpanded(item)"
|
||||
:writable="false"
|
||||
:selectable="false"
|
||||
@update:expanded="setExpanded(item, $event)"
|
||||
@viewCollection="$emit('viewCollection', item)" />
|
||||
</template>
|
||||
</HistoryListing>
|
||||
</template>
|
||||
</Layout>
|
||||
</ExpandedItems>
|
||||
</UrlDataProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../model";
|
||||
import { History } from "../model";
|
||||
import { updateContentFields } from "../model/queries";
|
||||
import ExpandedItems from "../ExpandedItems";
|
||||
import Layout from "../Layout";
|
||||
import TopNav from "./TopNav";
|
||||
import CollectionOperations from "./CollectionOperations";
|
||||
import Details from "./Details";
|
||||
import { CollectionContentItem } from "../ContentItem";
|
||||
import HistoryListing from "components/History/HistoryListing";
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UrlDataProvider,
|
||||
Layout,
|
||||
TopNav,
|
||||
Details,
|
||||
CollectionContentItem,
|
||||
ExpandedItems,
|
||||
CollectionOperations,
|
||||
HistoryListing,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
selectedCollections: { type: Array, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageSize: 50,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.dsc = new DatasetCollection(this.selectedCollection);
|
||||
},
|
||||
computed: {
|
||||
selectedCollection() {
|
||||
const arr = this.selectedCollections;
|
||||
const selected = arr[arr.length - 1];
|
||||
return selected;
|
||||
},
|
||||
isRoot() {
|
||||
return this.selectedCollection == this.rootCollection;
|
||||
},
|
||||
writable() {
|
||||
return this.isRoot;
|
||||
},
|
||||
rootCollection() {
|
||||
return this.selectedCollections[0];
|
||||
},
|
||||
downloadCollectionUrl() {
|
||||
let url = "";
|
||||
if (this.rootCollection) {
|
||||
url = `${this.rootCollection.url}/download`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getUrl(dsc) {
|
||||
return dsc.contents_url.substring(1);
|
||||
},
|
||||
// change the data of the root collection, anything past the root
|
||||
// collection is part of the dataset collection, which i believe is supposed to
|
||||
// be immutable, so only edit name, tags, blah of top-level selected collection,
|
||||
async updateDsc(collection, fields) {
|
||||
if (this.writable) {
|
||||
await updateContentFields(collection, fields);
|
||||
}
|
||||
},
|
||||
onScroll(newHid) {
|
||||
this.maxHid = newHid;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<b-dropdown
|
||||
v-if="breadCrumbOptions.length"
|
||||
size="sm"
|
||||
text="Return to..."
|
||||
boundary="viewport"
|
||||
data-description="collection breadcrumbs menu"
|
||||
no-caret>
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="arrow-up" class="mr-1" />
|
||||
<b class="text-nowrap" v-localize>Return to...</b>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item @click="close" data-description="back to history">
|
||||
<span>History: {{ history.name }}</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-for="option in breadCrumbOptions" :key="option.key" @click="reselect(option.value)">
|
||||
<span>{{ option.text }}</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
class="back"
|
||||
size="sm"
|
||||
title="`Return to: ${history.name}`"
|
||||
@click="close"
|
||||
data-description="back to history">
|
||||
<Icon icon="arrow-up" class="mr-1" />
|
||||
<b class="text-nowrap">Return to: {{ history.name }}</b>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "../model";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
selectedCollections: { type: Array, required: true, validate: (val) => val.length > 0 },
|
||||
},
|
||||
computed: {
|
||||
// List for the dropdown. Should be the history and every parent
|
||||
// collection excepting the current one.
|
||||
breadCrumbOptions() {
|
||||
const parents = this.selectedCollections.slice(0, -1);
|
||||
const options = parents.map((bc, i) => {
|
||||
return {
|
||||
key: bc.type_id,
|
||||
value: parents.slice(0, i + 1),
|
||||
text: bc.name,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reselect(newList) {
|
||||
this.$emit("update:selectedCollections", newList);
|
||||
},
|
||||
back() {
|
||||
const newList = this.selectedCollections.slice(0, -1);
|
||||
this.reselect(newList);
|
||||
},
|
||||
close() {
|
||||
this.reselect([]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Details
|
||||
:name="history.name"
|
||||
:annotation="history.annotation"
|
||||
:tags="history.tags"
|
||||
:writeable="writeable"
|
||||
@save="$emit('update:currentHistory', $event)">
|
||||
<template v-slot:name>
|
||||
<h3 data-description="history name display">{{ history.name || "History" }}</h3>
|
||||
<h5 class="history-size">
|
||||
<span v-if="history.size">{{ history.size | niceFileSize }}</span>
|
||||
<span v-else v-localize>(empty)</span>
|
||||
</h5>
|
||||
</template>
|
||||
</Details>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { History } from "components/History/model";
|
||||
import Details from "components/History/Layout/Details";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Details,
|
||||
},
|
||||
filters: {
|
||||
niceFileSize(rawSize = 0) {
|
||||
return prettyBytes(rawSize);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
writeable: { type: Boolean, default: true },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+2
-2
@@ -106,8 +106,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
import CopyHistoryModal from "./CopyModal";
|
||||
import { History } from "components/History/model";
|
||||
import CopyHistoryModal from "components/History/Modals/CopyModal";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
|
||||
export default {
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
import { History } from "components/History/model";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
+3
-3
@@ -61,10 +61,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
import { History } from "components/History/model";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import { switchToLegacyHistoryPanel } from "./adapters/betaToggle";
|
||||
import HistorySelectorModal from "./HistorySelectorModal";
|
||||
import { switchToLegacyHistoryPanel } from "components/History/adapters/betaToggle";
|
||||
import HistorySelectorModal from "components/History/Modals/HistorySelectorModal";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
+65
-81
@@ -1,117 +1,103 @@
|
||||
<template>
|
||||
<section>
|
||||
<nav class="content-operations-menu d-flex justify-content-between bg-secondary">
|
||||
<nav class="content-operations d-flex justify-content-between bg-secondary">
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
<b-button
|
||||
title="Select Items"
|
||||
class="show-history-content-selectors-btn"
|
||||
icon="check-square"
|
||||
class="show-history-content-selectors-btn rounded-0"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:disabled="!hasMatches"
|
||||
:pressed="showSelection"
|
||||
@click="toggleSelection" />
|
||||
<IconButton
|
||||
title="Filter Items"
|
||||
icon="filter"
|
||||
@click="toggleSelection">
|
||||
<Icon icon="check-square" />
|
||||
</b-button>
|
||||
<b-button
|
||||
title="Search Items"
|
||||
class="rounded-0"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:pressed="showFilter"
|
||||
@click="toggleFilter"
|
||||
data-description="content filter toggle" />
|
||||
<IconButton
|
||||
data-description="content filter toggle">
|
||||
<Icon icon="search" />
|
||||
</b-button>
|
||||
<b-button
|
||||
title="Collapse Items"
|
||||
icon="compress"
|
||||
class="rounded-0"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:disabled="!expandedCount"
|
||||
@click="$emit('collapse-all')" />
|
||||
@click="$emit('collapse-all')">
|
||||
<Icon icon="compress" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
|
||||
<b-button-group>
|
||||
<b-dropdown
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="Selection"
|
||||
:disabled="!hasSelection"
|
||||
data-description="selected content menu">
|
||||
<b-dropdown-text id="history-op-selected-content">
|
||||
<span v-localize v-if="hasSelection">With {{ numSelected }} selected items...</span>
|
||||
<span v-localize v-else>You don't have any selected content.</span>
|
||||
size="sm"
|
||||
variant="link"
|
||||
toggle-class="text-decoration-none"
|
||||
data-description="selected content menu"
|
||||
:disabled="!hasSelection">
|
||||
<b-dropdown-text>
|
||||
<span v-localize>With {{ numSelected }} selected...</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:hide-selected-content>
|
||||
<span v-localize>Hide</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:show-selected-content>
|
||||
<b-dropdown-item v-if="params.showHidden" v-b-modal:show-selected-content>
|
||||
<span v-localize>Unhide</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:delete-selected-content>
|
||||
<span v-localize>Delete</span>
|
||||
<b-dropdown-item v-else v-b-modal:hide-selected-content>
|
||||
<span v-localize>Hide</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:restore-selected-content>
|
||||
<b-dropdown-item v-if="params.showDeleted" v-b-modal:restore-selected-content>
|
||||
<span v-localize>Undelete</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
v-b-modal:purge-selected-content
|
||||
:disabled="!hasSelection">
|
||||
<span v-localize>Permanently Delete</span>
|
||||
<b-dropdown-item v-else v-b-modal:delete-selected-content>
|
||||
<span v-localize>Delete</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
@click="buildDatasetList"
|
||||
data-description="build list">
|
||||
<b-dropdown-item v-if="!params.showDeleted" v-b-modal:purge-selected-content>
|
||||
<span v-localize>Delete (permanently)</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="showBuildOptions" @click="buildDatasetList" data-description="build list">
|
||||
<span v-localize>Build Dataset List</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
@click="buildDatasetPair"
|
||||
data-description="build pair">
|
||||
<b-dropdown-item v-if="showBuildOptions" @click="buildDatasetPair" data-description="build pair">
|
||||
<span v-localize>Build Dataset Pair</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
v-if="showBuildOptions"
|
||||
@click="buildListOfPairs"
|
||||
data-description="build list of pairs">
|
||||
<span v-localize>Build List of Dataset Pairs</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
v-if="showBuildOptions"
|
||||
@click="buildCollectionFromRules"
|
||||
data-description="build collection from rules">
|
||||
<span v-localize>Build Collection from Rules</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-dropdown
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="History"
|
||||
:disabled="!hasMatches"
|
||||
data-description="history action menu">
|
||||
size="sm"
|
||||
variant="link"
|
||||
class="rounded-0"
|
||||
toggle-class="text-decoration-none"
|
||||
data-description="history action menu"
|
||||
:disabled="!hasMatches">
|
||||
<b-dropdown-text id="history-op-all-content">
|
||||
<span v-localize>With entire history...</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-all-content"
|
||||
@click="iframeRedirect('/dataset/copy_datasets')"
|
||||
data-description="copy datasets">
|
||||
<b-dropdown-item @click="iframeRedirect('/dataset/copy_datasets')" data-description="copy datasets">
|
||||
<span v-localize>Copy Datasets</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:show-all-hidden-content aria-describedby="history-op-all-content">
|
||||
<b-dropdown-item v-b-modal:show-all-hidden-content>
|
||||
<span v-localize>Unhide All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:delete-all-hidden-content aria-describedby="history-op-all-content">
|
||||
<b-dropdown-item v-b-modal:delete-all-hidden-content>
|
||||
<span v-localize>Delete All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:purge-all-deleted-content aria-describedby="history-op-all-content">
|
||||
<b-dropdown-item v-b-modal:purge-all-deleted-content>
|
||||
<span v-localize>Purge All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
@@ -119,7 +105,7 @@
|
||||
</nav>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<content-filters v-if="showFilter" class="content-filters p-2" :params.sync="localParams" />
|
||||
<history-filters v-if="showFilter" class="content-operations-filters p-2" :params.sync="localParams" />
|
||||
</transition>
|
||||
|
||||
<b-modal id="hide-selected-content" title="Hide Selected Content?" title-tag="h2" @ok="hideSelected">
|
||||
@@ -157,7 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model/History";
|
||||
import { History } from "components/History/model/History";
|
||||
import {
|
||||
hideSelectedContent,
|
||||
unhideSelectedContent,
|
||||
@@ -167,18 +153,16 @@ import {
|
||||
unhideAllHiddenContent,
|
||||
deleteAllHiddenContent,
|
||||
purgeAllDeletedContent,
|
||||
} from "./model";
|
||||
import { createDatasetCollection } from "./model/queries";
|
||||
} from "components/History/model";
|
||||
import { createDatasetCollection } from "components/History/model/queries";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import { buildCollectionModal } from "./adapters/buildCollectionModal";
|
||||
import ContentFilters from "./ContentFilters";
|
||||
import IconButton from "components/IconButton";
|
||||
import { buildCollectionModal } from "components/History/adapters/buildCollectionModal";
|
||||
import HistoryFilters from "./HistoryFilters";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
ContentFilters,
|
||||
IconButton,
|
||||
HistoryFilters,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
@@ -194,7 +178,10 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasContentFilters() {
|
||||
showBuildOptions() {
|
||||
return !this.params.showHidden && !this.params.showDeleted;
|
||||
},
|
||||
hasHistoryFilters() {
|
||||
const { showHidden, showDeleted, filterText } = this.params;
|
||||
return showHidden || showDeleted || filterText;
|
||||
},
|
||||
@@ -212,9 +199,6 @@ export default {
|
||||
hasSelection() {
|
||||
return this.numSelected > 0;
|
||||
},
|
||||
countHidden() {
|
||||
return this.history.contents_active.hidden;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleSelection() {
|
||||
@@ -302,7 +286,7 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
// remove borders around buttons in menu
|
||||
.content-operations-menu .btn-group .btn {
|
||||
.content-operations .btn-group .btn {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
+59
-44
@@ -17,11 +17,8 @@
|
||||
resetSelection,
|
||||
}">
|
||||
<Layout>
|
||||
<template v-slot:globalnav>
|
||||
<slot name="globalnav" :history="history" />
|
||||
</template>
|
||||
|
||||
<template v-slot:localnav>
|
||||
<template v-slot:navigation>
|
||||
<slot name="navigation" :history="history" />
|
||||
<HistoryMenu :history="history" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
@@ -34,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:listcontrols>
|
||||
<ContentOperations
|
||||
<HistoryOperations
|
||||
:history="history"
|
||||
:params.sync="params"
|
||||
:content-selection="selectedItems"
|
||||
@@ -44,7 +41,7 @@
|
||||
@update:content-selection="selectItems"
|
||||
@update:show-selection="setShowSelection"
|
||||
@reset-selection="resetSelection"
|
||||
@hide-selection="onHiddenItems"
|
||||
@hide-selection="onHideSelection"
|
||||
@select-all="selectItems(payload)"
|
||||
@collapse-all="collapseAll" />
|
||||
</template>
|
||||
@@ -54,24 +51,31 @@
|
||||
<b-alert v-else-if="loading" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading History" />
|
||||
</b-alert>
|
||||
<HistoryListing
|
||||
<Listing
|
||||
v-else
|
||||
reversed
|
||||
:query-key="queryKey"
|
||||
:page-size="pageSize"
|
||||
:limit="limit"
|
||||
:payload="payload"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:history-item="{ item }">
|
||||
<HistoryContentItem
|
||||
v-if="!hiddenItems[item.hid]"
|
||||
<ContentItem
|
||||
v-if="!hideSelection[item.hid]"
|
||||
:item="item"
|
||||
:expanded="isExpanded(item)"
|
||||
:id="item.hid"
|
||||
:name="item.name"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="item.history_content_type == 'dataset'"
|
||||
:selected="isSelected(item)"
|
||||
:show-selection="showSelection"
|
||||
@update:expanded="setExpanded(item, $event)"
|
||||
:selectable="showSelection"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@viewCollection="$emit('viewCollection', item)" />
|
||||
@view-collection="$emit('view-collection', item)"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@unhide="onUnhide" />
|
||||
</template>
|
||||
</HistoryListing>
|
||||
</Listing>
|
||||
</template>
|
||||
|
||||
<template v-slot:modals>
|
||||
@@ -84,34 +88,34 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
import { SearchParams } from "./model/SearchParams";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import ExpandedItems from "./ExpandedItems";
|
||||
import SelectedItems from "./SelectedItems";
|
||||
import Layout from "./Layout";
|
||||
import HistoryMessages from "./HistoryMessages";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ContentItem from "components/History/Content/ContentItem";
|
||||
import { History } from "components/History/model";
|
||||
import { deleteContent, updateContentFields } from "components/History/model/queries";
|
||||
import ExpandedItems from "components/History/Content/ExpandedItems";
|
||||
import SelectedItems from "components/History/Content/SelectedItems";
|
||||
import Layout from "components/History/Layout/Layout";
|
||||
import Listing from "components/History/Layout/Listing";
|
||||
import ToolHelpModal from "components/History/Modals/ToolHelpModal";
|
||||
import HistoryOperations from "./HistoryOperations";
|
||||
import HistoryDetails from "./HistoryDetails";
|
||||
import HistoryEmpty from "./HistoryEmpty";
|
||||
import ContentOperations from "./ContentOperations";
|
||||
import ToolHelpModal from "./ToolHelpModal";
|
||||
import HistoryMenu from "./HistoryMenu";
|
||||
import HistoryListing from "./HistoryListing";
|
||||
import { HistoryContentItem } from "./ContentItem";
|
||||
import HistoryMessages from "./HistoryMessages";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
UrlDataProvider,
|
||||
Layout,
|
||||
HistoryContentItem,
|
||||
Listing,
|
||||
ContentItem,
|
||||
HistoryMessages,
|
||||
HistoryDetails,
|
||||
HistoryEmpty,
|
||||
HistoryMenu,
|
||||
HistoryListing,
|
||||
ContentOperations,
|
||||
HistoryOperations,
|
||||
ToolHelpModal,
|
||||
ExpandedItems,
|
||||
SelectedItems,
|
||||
@@ -121,24 +125,21 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hiddenItems: {},
|
||||
maxHid: this.history.hid_counter,
|
||||
maxNew: 10,
|
||||
pageSize: 50,
|
||||
hideSelection: {},
|
||||
offset: 0,
|
||||
limit: 500,
|
||||
params: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
queryKey() {
|
||||
this.hiddenItems = {};
|
||||
this.maxHid = this.history.hid_counter;
|
||||
this.hideSelection = {};
|
||||
this.offset = 0;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dataUrl() {
|
||||
return `api/histories/${this.historyId}/contents/before/${this.maxHid + this.maxNew}/${
|
||||
this.pageSize
|
||||
}?view=detailed&${this.queryString}`;
|
||||
return `api/histories/${this.historyId}/contents?v=dev&order=hid&offset=${this.offset}&limit=${this.limit}&${this.queryString}`;
|
||||
},
|
||||
historyId() {
|
||||
return this.history.id;
|
||||
@@ -147,21 +148,35 @@ export default {
|
||||
return `${this.history.id}&${this.queryString}`;
|
||||
},
|
||||
queryString() {
|
||||
return new SearchParams(this.params).historyContentQueryString;
|
||||
const deleted = this.params.showDeleted ? "True" : "False";
|
||||
const visible = this.params.showHidden ? "False" : "True";
|
||||
return `q=deleted&q=visible&qv=${deleted}&qv=${visible}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasMatches(payload) {
|
||||
return !!payload && payload.length > 0;
|
||||
},
|
||||
onScroll(newHid) {
|
||||
this.maxHid = newHid;
|
||||
onScroll(offset) {
|
||||
this.offset = offset;
|
||||
},
|
||||
onHiddenItems(selectedItems) {
|
||||
onHideSelection(selectedItems) {
|
||||
selectedItems.forEach((item) => {
|
||||
this.hiddenItems[item.hid] = true;
|
||||
this.hideSelection[item.hid] = true;
|
||||
});
|
||||
},
|
||||
onDelete(item) {
|
||||
this.hideSelection[item.hid] = true;
|
||||
deleteContent(item);
|
||||
},
|
||||
onUndelete(item) {
|
||||
this.hideSelection[item.hid] = true;
|
||||
updateContentFields(item, { deleted: false });
|
||||
},
|
||||
onUnhide(item) {
|
||||
this.hideSelection[item.hid] = true;
|
||||
updateContentFields(item, { visible: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<CurrentUser class="d-flex flex-column" v-slot="{ user }">
|
||||
<UserHistories v-if="user" :user="user" v-slot="{ currentHistory, histories, handlers }">
|
||||
<HistoryPanel v-if="currentHistory" :history="currentHistory" v-on="handlers" id="current-history-panel">
|
||||
<template v-slot:globalnav>
|
||||
<HistoriesMenu
|
||||
v-on="handlers"
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
title="Histories" />
|
||||
</template>
|
||||
</HistoryPanel>
|
||||
|
||||
<div v-else class="flex-grow-1 loadingBackground h-100">
|
||||
<span class="sr-only" v-localize>Loading History...</span>
|
||||
</div>
|
||||
</UserHistories>
|
||||
</CurrentUser>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
import UserHistories from "components/providers/UserHistories";
|
||||
import HistoryPanel from "./HistoryPanel";
|
||||
import HistoriesMenu from "./HistoriesMenu";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CurrentUser,
|
||||
UserHistories,
|
||||
HistoryPanel,
|
||||
HistoriesMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<b-modal ref="deleteModal" title="Delete History" v-model="showModal" title-tag="h2">
|
||||
<p>Delete?</p>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
value: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
computed: {
|
||||
showModal: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$emit("input", newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<nav class="edit-controls">
|
||||
<IconButton
|
||||
:title="label('Edit') | l"
|
||||
class="edit-button mb-1"
|
||||
:pressed="editing"
|
||||
icon="pen"
|
||||
@click="$emit('update:editing', !editing)"
|
||||
data-description="editor toggle" />
|
||||
|
||||
<template v-if="editing">
|
||||
<!-- save -->
|
||||
<IconButton
|
||||
:title="label('Save') | l"
|
||||
class="save-button mb-1"
|
||||
:class="{ disabled: !valid }"
|
||||
icon="save"
|
||||
@click="$emit('save')"
|
||||
data-description="editor save button" />
|
||||
|
||||
<!-- revert -->
|
||||
<IconButton
|
||||
:title="label('Revert') | l"
|
||||
class="revert-changes-button mb-1"
|
||||
:class="{ disabled: !dirty }"
|
||||
icon="undo"
|
||||
@click="$emit('revert')"
|
||||
data-description="editor revert button" />
|
||||
|
||||
<!-- form status -->
|
||||
<IconButton
|
||||
:title="statusLabel | l"
|
||||
class="status-button mb-1"
|
||||
:class="{ valid: valid, disabled: !valid }"
|
||||
:icon="valid ? 'check' : 'exclamation-triangle'"
|
||||
variant="link"
|
||||
data-description="editor status" />
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
modelName: { type: String, required: true },
|
||||
editing: { type: Boolean, required: true },
|
||||
dirty: { type: Boolean, required: false, default: false },
|
||||
valid: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
computed: {
|
||||
statusLabel() {
|
||||
const status = this.valid ? "Valid" : "Invalid";
|
||||
return `${status} ${this.modelName}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
label(verb) {
|
||||
return `${verb} ${this.modelName}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.edit-controls .disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<!--
|
||||
History object editor. Change name, edit tags, description.
|
||||
Emit new History model object when done via .sync syntax
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="history-details d-flex" data-description="edit details">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<h3 data-description="history name display">{{ historyName || "(History Name)" }}</h3>
|
||||
<h5 class="history-size">
|
||||
<span v-if="history.size">{{ history.size | niceFileSize }}</span>
|
||||
<span v-else v-localize>(empty)</span>
|
||||
</h5>
|
||||
|
||||
<!-- display title, annotation, tags -->
|
||||
<div v-if="!editing">
|
||||
<p data-description="history annotation" v-if="annotation" class="mt-1">{{ annotation }}</p>
|
||||
<StatelessTags class="mt-2 tags" :value="tags" :disabled="true" />
|
||||
</div>
|
||||
|
||||
<!-- edit form, change title, annotation, or tags -->
|
||||
<div v-else class="mt-3" @keydown.esc="revertAndCancel" data-description="edit form">
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="historyName"
|
||||
placeholder="History Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="name input"></b-textarea>
|
||||
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="annotation input"></b-textarea>
|
||||
|
||||
<StatelessTags class="mt-3 tags" v-model="tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="History"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { History } from "./model";
|
||||
import EditorMenu from "./EditorMenu";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
},
|
||||
filters: {
|
||||
niceFileSize(rawSize = 0) {
|
||||
return prettyBytes(rawSize);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
tempHistory: this.history.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
historyId() {
|
||||
return this.history.id;
|
||||
},
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
valid() {
|
||||
return this.tempHistory.name.length > 0;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempHistory.equals(this.history);
|
||||
},
|
||||
annotation: {
|
||||
get() {
|
||||
return this.tempHistory.annotation;
|
||||
},
|
||||
set(annotation) {
|
||||
this.patchTempHistory({ annotation });
|
||||
},
|
||||
},
|
||||
historyName: {
|
||||
get() {
|
||||
return this.tempHistory.name;
|
||||
},
|
||||
set(name) {
|
||||
this.patchTempHistory({ name });
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempHistory.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
this.patchTempHistory({ tags: newTags });
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
historyId(newId, oldId) {
|
||||
if (newId != oldId) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.tempHistory = this.history.clone();
|
||||
},
|
||||
patchTempHistory(props = {}) {
|
||||
this.tempHistory = this.tempHistory.patch(props);
|
||||
},
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:history", this.tempHistory);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
revertAndCancel() {
|
||||
this.revert();
|
||||
this.editing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div class="history-listing">
|
||||
<div @scroll="onScrollThrottle" class="listing">
|
||||
<div v-for="(item, index) in getItems" :key="index">
|
||||
<slot name="history-item" :item="item" :index="index" />
|
||||
</div>
|
||||
<div v-if="loading" class="m-2">
|
||||
<LoadingSpan message="Please wait" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { reverse, throttle } from "lodash";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
},
|
||||
props: {
|
||||
payload: { required: true },
|
||||
itemKey: { type: String, default: "hid" },
|
||||
queryKey: { type: String, default: null },
|
||||
pageSize: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
throttlePeriod: 100,
|
||||
queryCurrent: this.queryKey,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.updateItems();
|
||||
this.onScrollThrottle = throttle((event) => {
|
||||
this.onScroll(event);
|
||||
}, this.throttlePeriod);
|
||||
},
|
||||
watch: {
|
||||
payload() {
|
||||
this.updateItems();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return !!this.payload && this.payload.length == this.pageSize;
|
||||
},
|
||||
getItems() {
|
||||
return reverse(this.items.filter((n) => n));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateItems() {
|
||||
if (this.queryKey != this.queryCurrent) {
|
||||
this.queryCurrent = this.queryKey;
|
||||
this.items = [];
|
||||
} else if (this.payload) {
|
||||
for (const item of this.payload) {
|
||||
const itemIndex = item[this.itemKey];
|
||||
this.items[itemIndex] = item;
|
||||
}
|
||||
this.items = this.items.slice();
|
||||
}
|
||||
},
|
||||
onScroll(event) {
|
||||
const itemCount = this.items.length;
|
||||
if (itemCount > 0) {
|
||||
let topIndex = 0;
|
||||
for (const index in event.target.childNodes) {
|
||||
const child = event.target.childNodes[index];
|
||||
if (child.offsetTop > event.target.scrollTop) {
|
||||
topIndex = index - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
topIndex = Math.min(Math.max(topIndex, 0), itemCount - 1);
|
||||
const topItem = this.getItems[topIndex];
|
||||
if (topItem) {
|
||||
this.$emit("scroll", topItem[this.itemKey]);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "scss/mixins.scss";
|
||||
.history-listing {
|
||||
.listing {
|
||||
@include absfill();
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!-- A single history panel is either showing the history or one of the
|
||||
selected datset collections -->
|
||||
|
||||
<template>
|
||||
<HistoryComponent
|
||||
v-if="history && !breadcrumbs.length"
|
||||
:history="history"
|
||||
v-on="$listeners"
|
||||
@viewCollection="onViewCollection">
|
||||
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
|
||||
<slot :name="name" v-bind="slotData" :history="history" />
|
||||
</template>
|
||||
</HistoryComponent>
|
||||
<CurrentCollection
|
||||
v-else-if="breadcrumbs.length"
|
||||
:history="history"
|
||||
:selected-collections.sync="breadcrumbs"
|
||||
@viewCollection="onViewCollection" />
|
||||
<div v-else>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HistoryComponent from "./History";
|
||||
import CurrentCollection from "./CurrentCollection/Panel";
|
||||
import { History, STATES } from "./model";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HistoryComponent,
|
||||
CurrentCollection,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// list of collections we have drilled down into
|
||||
breadcrumbs: [],
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
STATES,
|
||||
},
|
||||
methods: {
|
||||
onViewCollection(collection) {
|
||||
this.breadcrumbs = [...this.breadcrumbs, collection];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<Tags :tags="history.tags" :store-key="storeKey" :tag-service="historyTagService" :disabled="disabled" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// TODO: Re-do tags as a provider instead of dependency injected service
|
||||
|
||||
import { History } from "./model";
|
||||
import { updateHistoryFields } from "./model/queries";
|
||||
import { Tags } from "components/Tags";
|
||||
import { TagService } from "components/Tags/tagService";
|
||||
import { createTag } from "components/Tags/model";
|
||||
|
||||
class HistoryTagService extends TagService {
|
||||
constructor(history) {
|
||||
super({
|
||||
id: history.id,
|
||||
itemClass: "History",
|
||||
context: "history-panel",
|
||||
});
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
async save(rawTag) {
|
||||
const tag = createTag(rawTag);
|
||||
const tagSet = new Set(this.history.tags);
|
||||
tagSet.add(tag.text);
|
||||
await this.saveHistoryTags(tagSet);
|
||||
return tag;
|
||||
}
|
||||
|
||||
async delete(rawTag) {
|
||||
const tag = createTag(rawTag);
|
||||
const tagSet = new Set(this.history.tags);
|
||||
tagSet.delete(tag.text);
|
||||
await this.saveHistoryTags(tagSet);
|
||||
return tag;
|
||||
}
|
||||
|
||||
async saveHistoryTags(rawTags) {
|
||||
// console.log("saveHistoryTags", rawTags);
|
||||
const tags = Array.from(rawTags);
|
||||
return await updateHistoryFields(this.history.id, { tags });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { Tags },
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
computed: {
|
||||
storeKey() {
|
||||
return `History-${this.history.id}`;
|
||||
},
|
||||
historyTagService() {
|
||||
return new HistoryTagService(this.history);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<CurrentUser class="d-flex flex-column" v-slot="{ user }">
|
||||
<UserHistories v-if="user" :user="user" v-slot="{ currentHistory, histories, handlers }">
|
||||
<div v-if="currentHistory" id="current-history-panel" class="history-index">
|
||||
<CurrentHistory
|
||||
v-if="!breadcrumbs.length"
|
||||
:history="currentHistory"
|
||||
v-on="handlers"
|
||||
@view-collection="onViewCollection">
|
||||
<template v-slot:navigation>
|
||||
<HistoryNavigation
|
||||
v-on="handlers"
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
title="Histories" />
|
||||
</template>
|
||||
</CurrentHistory>
|
||||
<CurrentCollection
|
||||
v-else-if="breadcrumbs.length"
|
||||
:history="currentHistory"
|
||||
:selected-collections.sync="breadcrumbs"
|
||||
@view-collection="onViewCollection" />
|
||||
<div v-else>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex-grow-1 loadingBackground h-100">
|
||||
<span class="sr-only" v-localize>Loading History...</span>
|
||||
</div>
|
||||
</UserHistories>
|
||||
</CurrentUser>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
import UserHistories from "components/providers/UserHistories";
|
||||
import HistoryNavigation from "./CurrentHistory/HistoryNavigation";
|
||||
import CurrentHistory from "./CurrentHistory/HistoryPanel";
|
||||
import CurrentCollection from "./CurrentCollection/CollectionPanel";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CurrentUser,
|
||||
CurrentHistory,
|
||||
CurrentCollection,
|
||||
UserHistories,
|
||||
HistoryNavigation,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// list of collections we have drilled down into
|
||||
breadcrumbs: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onViewCollection(collection) {
|
||||
this.breadcrumbs = [...this.breadcrumbs, collection];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<!-- Layout to house all the extraneous structural markup elements
|
||||
and non-semantic utility classes that are necessary when you use an
|
||||
amateur-hour baby-tool like Bootstrap instead of taking the afternoon
|
||||
to learn how CSS is supposed to work. -->
|
||||
|
||||
<template>
|
||||
<section class="history beta d-flex flex-column">
|
||||
<header>
|
||||
<nav class="d-flex justify-content-between bg-secondary">
|
||||
<slot name="globalnav">Global Navigation Default</slot>
|
||||
<slot name="localnav">Local Navigation Default</slot>
|
||||
</nav>
|
||||
|
||||
<!-- details about the thing you're looking at -->
|
||||
<div class="px-3 py-4 details">
|
||||
<slot name="details"></slot>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- warnings, messages, dirty limericks -->
|
||||
<section v-if="$slots.messages">
|
||||
<slot name="messages"></slot>
|
||||
</section>
|
||||
|
||||
<!-- controls atop the list -->
|
||||
<section v-if="$slots.listcontrols" class="list-controls">
|
||||
<slot name="listcontrols"></slot>
|
||||
</section>
|
||||
|
||||
<!-- scrolling list -->
|
||||
<section class="position-relative flex-grow-1 scroller">
|
||||
<slot name="listing"></slot>
|
||||
</section>
|
||||
|
||||
<!-- no dom layout, holds modal markup -->
|
||||
<section v-if="$slots.modals">
|
||||
<slot name="modals"></slot>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.history.beta {
|
||||
/* TODO: css reset? */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.list-controls:empty,
|
||||
.messages:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<section data-description="edit details">
|
||||
<b-button
|
||||
v-if="writeable"
|
||||
class="edit-button ml-1 float-right"
|
||||
data-description="editor toggle"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:title="'Edit' | l"
|
||||
:pressed="editing"
|
||||
@click="onToggle">
|
||||
<Icon icon="pen" />
|
||||
</b-button>
|
||||
<slot name="name" />
|
||||
|
||||
<!-- display annotation, tags -->
|
||||
<div v-if="!editing">
|
||||
<div v-if="annotation" class="mt-2" data-description="annotation value">{{ annotation }}</div>
|
||||
<StatelessTags v-if="tags" class="tags mt-3" :value="tags" :disabled="true" />
|
||||
</div>
|
||||
|
||||
<!-- edit form, change title, annotation, or tags -->
|
||||
<div v-else class="mt-3" data-description="edit form">
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="localProps.name"
|
||||
placeholder="Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="name input"
|
||||
@keyup.esc="onToggle" />
|
||||
<b-textarea
|
||||
v-if="showAnnotation"
|
||||
class="mb-2"
|
||||
v-model="localProps.annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="annotation input"
|
||||
@keyup.esc="onToggle" />
|
||||
<StatelessTags v-if="localProps.tags" class="mb-3 tags" v-model="localProps.tags" />
|
||||
</div>
|
||||
<nav class="edit-controls">
|
||||
<template v-if="editing">
|
||||
<b-button
|
||||
class="save-button mb-1"
|
||||
data-description="editor save button"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:disabled="!localProps.name"
|
||||
@click="onSave">
|
||||
<Icon icon="save" />
|
||||
<span v-localize>Save</span>
|
||||
</b-button>
|
||||
<b-button
|
||||
class="cancel-button mb-1"
|
||||
data-description="editor cancel button"
|
||||
size="sm"
|
||||
icon="undo"
|
||||
@click="onToggle">
|
||||
<Icon icon="undo" />
|
||||
<span v-localize>Cancel</span>
|
||||
</b-button>
|
||||
</template>
|
||||
</nav>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { StatelessTags } from "components/Tags";
|
||||
export default {
|
||||
components: {
|
||||
StatelessTags,
|
||||
},
|
||||
props: {
|
||||
name: { type: String, default: null },
|
||||
annotation: { type: String, default: null },
|
||||
showAnnotation: { type: Boolean, default: true },
|
||||
tags: { type: Array, default: null },
|
||||
writeable: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
localProps: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
name() {
|
||||
this.localProps.name = this.name;
|
||||
},
|
||||
annotation() {
|
||||
this.localProps.annotation = this.annotation;
|
||||
},
|
||||
tags() {
|
||||
this.localProps.tags = this.tags;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.onReset();
|
||||
},
|
||||
methods: {
|
||||
onReset() {
|
||||
this.localProps = {
|
||||
name: this.name,
|
||||
annotation: this.annotation,
|
||||
tags: this.tags,
|
||||
};
|
||||
},
|
||||
onSave() {
|
||||
this.editing = false;
|
||||
this.$emit("save", Object.assign({}, this.localProps));
|
||||
},
|
||||
onToggle() {
|
||||
this.editing = !this.editing;
|
||||
this.onReset();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<section class="history-layout d-flex flex-column">
|
||||
<header>
|
||||
<!-- navigation bar at the top-->
|
||||
<nav class="d-flex justify-content-between bg-secondary">
|
||||
<slot name="navigation" />
|
||||
</nav>
|
||||
|
||||
<!-- details about the thing you're looking at -->
|
||||
<div class="px-3 py-4 details">
|
||||
<slot name="details" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- warnings, messages -->
|
||||
<section v-if="$slots.messages">
|
||||
<slot name="messages" />
|
||||
</section>
|
||||
|
||||
<!-- controls above list -->
|
||||
<section v-if="$slots.listcontrols" class="list-controls">
|
||||
<slot name="listcontrols" />
|
||||
</section>
|
||||
|
||||
<!-- scrolling list -->
|
||||
<section class="position-relative flex-grow-1 scroller">
|
||||
<slot name="listing" />
|
||||
</section>
|
||||
|
||||
<!-- no dom layout, holds modal markup -->
|
||||
<section v-if="$slots.modals">
|
||||
<slot name="modals" />
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.history-layout {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
.list-controls:empty,
|
||||
.messages:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="history-listing">
|
||||
<virtual-list
|
||||
ref="listing"
|
||||
class="listing"
|
||||
:data-key="itemKey"
|
||||
:data-sources="getItems"
|
||||
:data-component="{}"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:item="{ item }">
|
||||
<slot name="history-item" :item="item" />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<LoadingSpan v-if="loading" class="m-2" message="Loading" />
|
||||
</template>
|
||||
</virtual-list>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import VirtualList from "vue-virtual-scroll-list";
|
||||
import { reverse, throttle } from "lodash";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
VirtualList,
|
||||
},
|
||||
props: {
|
||||
itemKey: { type: String, default: "hid" },
|
||||
limit: { type: Number, required: true },
|
||||
payload: { type: Array, default: null },
|
||||
queryKey: { type: String, default: null },
|
||||
reversed: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
throttlePeriod: 20,
|
||||
deltaMax: 20,
|
||||
queryCurrent: this.queryKey,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.updateItems();
|
||||
this.onScrollThrottle = throttle((event) => {
|
||||
this.onScroll(event);
|
||||
}, this.throttlePeriod);
|
||||
},
|
||||
watch: {
|
||||
payload() {
|
||||
this.updateItems();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return !!this.payload && this.payload.length == this.limit;
|
||||
},
|
||||
getItems() {
|
||||
const filtered = this.items.filter((n) => n);
|
||||
return this.reversed ? reverse(filtered) : filtered;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateItems() {
|
||||
/* This function merges the existing data with new incoming data. */
|
||||
if (this.queryKey != this.queryCurrent) {
|
||||
this.queryCurrent = this.queryKey;
|
||||
this.items.splice(0);
|
||||
} else if (this.payload) {
|
||||
for (const item of this.payload) {
|
||||
const itemIndex = item[this.itemKey];
|
||||
if (this.items[itemIndex]) {
|
||||
const localItem = this.items[itemIndex];
|
||||
Object.keys(localItem).forEach((key) => {
|
||||
localItem[key] = item[key];
|
||||
});
|
||||
} else {
|
||||
Vue.set(this.items, itemIndex, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onScrollHandler(event) {
|
||||
/* CURRENTLY UNUSED
|
||||
// this avoids diagonal scrolling, we either scroll left/right or top/down
|
||||
// both events are throttled and the default handler has been prevented.
|
||||
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
||||
// handle vertical scrolling with virtual scroller
|
||||
const listing = this.$refs.listing;
|
||||
const deltaMax = this.deltaMax;
|
||||
const deltaY = Math.max(Math.min(event.deltaY, deltaMax), -deltaMax);
|
||||
this.offset = Math.max(0, listing.getOffset() + deltaY);
|
||||
this.$refs.listing.scrollToOffset(this.offset);
|
||||
} else {
|
||||
// dispatch horizontal scrolling as regular event
|
||||
var wheelEvent = new WheelEvent("wheel", {
|
||||
deltaX: event.deltaX,
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
});
|
||||
event.target.dispatchEvent(wheelEvent);
|
||||
}
|
||||
*/
|
||||
},
|
||||
onScroll() {
|
||||
const rangeStart = this.$refs.listing.range.start;
|
||||
this.$emit("scroll", rangeStart);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "scss/mixins.scss";
|
||||
.history-listing {
|
||||
.listing {
|
||||
@include absfill();
|
||||
scroll-behavior: smooth;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model/History";
|
||||
import { History } from "components/History/model/History";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loadToolFromJob } from "./model/queries";
|
||||
import { loadToolFromJob } from "components/History/model/queries";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -3,67 +3,55 @@ This is not intended to be a complete listing, but a general idea of how the com
|
||||
to interact with each other.
|
||||
|
||||
```html static
|
||||
<CurrentHistoryPanel>
|
||||
<HistoryPanel :history="history">
|
||||
|
||||
<Index :history="history">
|
||||
|
||||
<!-- if main history selected -->
|
||||
<CurrentHistory :history="history">
|
||||
|
||||
<!-- for the right-hand side history we show some
|
||||
optional nav elements, can be ommitted for histories
|
||||
shown in multi-history view -->
|
||||
optional nav elements -->
|
||||
<slot:nav>
|
||||
<HistorySelector />
|
||||
<HistoryMenu />
|
||||
</slot:nav>
|
||||
|
||||
<!-- Data providers do the heavy-lifting of mixing params, history, and
|
||||
scroll position to deliver the content for the scroller -->
|
||||
<UrlDataProvider>
|
||||
<HistoryNavigation />
|
||||
<HistoryDetails />
|
||||
<HistoryMessages />
|
||||
<HistoryOperations />
|
||||
|
||||
<!-- if main history selected -->
|
||||
<History :history="history">
|
||||
<!-- Uses a virtual scroller plugin to render all ContentItems and throttles the scrolling pace by limiting
|
||||
the frequency and magnitude of offset changes. -->
|
||||
<Listing>
|
||||
|
||||
<!-- Data providers do the heavy-lifting of mixing params, history, and
|
||||
scroll position to deliver the content for the scroller -->
|
||||
<!-- The ContentItem renders a row in the list, showing the title, some attributes,
|
||||
and basic operation buttons such as display and edit for either a dataset or a collection.
|
||||
This component needs to be very efficient since it is re-rendered by the virtual scroller plugin. -->
|
||||
(<ContentItem />)
|
||||
|
||||
<UrlDataProvider>
|
||||
<HistoryDetails />
|
||||
<HistoryMessages />
|
||||
<ContentOperations />
|
||||
<HistoryListing>
|
||||
<!-- Is shown for datasets upon expansion, can be inefficent in comparison to the ContentItem component. -->
|
||||
<DatasetDetails />
|
||||
</Listing>
|
||||
</UrlDataProvider>
|
||||
</CurrentHistory>
|
||||
|
||||
<!-- HistoryContentItem is a dynamic component that becomes
|
||||
either Dataset or DatasetCollection depending
|
||||
on the props passed to it -->
|
||||
<!-- When a collection is selected for viewing, send in a
|
||||
breadcrumbs list of collections the user has selected -->
|
||||
<CurrentCollection :selected-collections="breadcrumbs">
|
||||
<UrlDataProvider>
|
||||
<CollectionNavigation />
|
||||
<CollectionDetails />
|
||||
<CollectionOperations />
|
||||
|
||||
(<HistoryContentItem />)
|
||||
<Dataset />
|
||||
<!-- or -->
|
||||
<DatasetCollection />
|
||||
|
||||
</HistoryListing>
|
||||
</UrlDataProvider>
|
||||
|
||||
</History>
|
||||
|
||||
|
||||
<!-- When a collection is selected for viewing, send in a
|
||||
breadcrumbs list of collections the user has selected -->
|
||||
|
||||
<CurrentCollection :selected-collections="breadcrumbs">
|
||||
<UrlDataProvider>
|
||||
<CollectionNav />
|
||||
<Details />
|
||||
<HistoryListing>
|
||||
|
||||
<!-- Subdataset and Subcollection are similar to the Dataset
|
||||
and DatasetCollection ContentItem components, but mostly
|
||||
read-only since they are part of the collection-->
|
||||
|
||||
(<CollectionContentItem />)
|
||||
<Subdataset />
|
||||
<!-- or -->
|
||||
<Subcollection />
|
||||
|
||||
</HistoryListing>
|
||||
</UrlDataProvider>
|
||||
</CurrentCollection>
|
||||
|
||||
</HistoryPanel>
|
||||
</CurrentHistoryPanel>
|
||||
<!-- As above, the same virtual scroller and ContentItem component is being used to render the elements. -->
|
||||
<Listing>
|
||||
(<ContentItem />)
|
||||
<DatasetDetails />
|
||||
</Listing>
|
||||
</UrlDataProvider>
|
||||
</CurrentCollection>
|
||||
</Index>
|
||||
```
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<IconButton
|
||||
v-if="stateIcon"
|
||||
v-b-tooltip
|
||||
:title="theTitle"
|
||||
:icon="stateIcon"
|
||||
:variant="variant"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import capitalize from "underscore.string/capitalize";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
inject: ["STATES"],
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
state: { type: String, required: true },
|
||||
variant: { type: String, required: false, default: "link" },
|
||||
},
|
||||
computed: {
|
||||
stateIcon() {
|
||||
if (this.inState(this.STATES.QUEUED, this.STATES.NEW, this.STATES.RUNNING)) {
|
||||
return "clock";
|
||||
}
|
||||
if (this.inState(this.STATES.PAUSED)) {
|
||||
return "pause";
|
||||
}
|
||||
if (this.inState(this.STATES.ERROR)) {
|
||||
return "exclamation-triangle";
|
||||
}
|
||||
if (this.inState(this.STATES.MIXED)) {
|
||||
return "exclamation-circle";
|
||||
}
|
||||
if (this.inState(this.STATES.OK)) {
|
||||
return "check";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
theTitle() {
|
||||
return this.title || capitalize(this.state);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
inState(...states) {
|
||||
return states.includes(this.state);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
import CurrentHistoryPanel from "../CurrentHistoryPanel";
|
||||
// import { genericProxy } from "utils/proxy";
|
||||
import HistoryIndex from "components/History/Index";
|
||||
|
||||
// mvc nonsense
|
||||
import $ from "jquery";
|
||||
@@ -88,7 +87,7 @@ export const HistoryPanelProxy = Backbone.View.extend({
|
||||
$("#right").addClass("beta").prepend(this.$el);
|
||||
|
||||
const container = this.$el[0];
|
||||
const mountHistory = mountVueComponent(CurrentHistoryPanel);
|
||||
const mountHistory = mountVueComponent(HistoryIndex);
|
||||
mountHistory({}, container);
|
||||
|
||||
return this;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Right-hand panel
|
||||
export { default as HistoryPanel } from "./HistoryPanel";
|
||||
export { default as CurrentHistoryPanel } from "./CurrentHistoryPanel";
|
||||
|
||||
// Adapter: backbone view that mounts HistoryPanel
|
||||
export { default as HistoryPanelProxy } from "./adapters/HistoryPanelProxy";
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<b-button
|
||||
class="galaxy-icon-button"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:title="title"
|
||||
:size="size"
|
||||
:pressed="pressed"
|
||||
:disabled="disabled"
|
||||
:variant="localVariant">
|
||||
<Icon :icon="icon" />
|
||||
<span class="sr-only">{{ title | l }}</span>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: { type: [String, Array, Object], required: true },
|
||||
title: { type: String, required: true },
|
||||
size: { type: String, required: false, default: "sm" },
|
||||
pressed: { type: Boolean, required: false, default: false },
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
// overrides variant set by pressed/disabled
|
||||
variant: { type: String, required: false, default: null },
|
||||
},
|
||||
computed: {
|
||||
localVariant() {
|
||||
if (this.variant) {
|
||||
// override with prop
|
||||
return this.variant;
|
||||
}
|
||||
if (this.disabled) {
|
||||
return "secondary";
|
||||
}
|
||||
return this.pressed ? "info" : "secondary";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// removes irritating bootstrap outline
|
||||
.galaxy-icon-button.btn {
|
||||
box-shadow: none !important;
|
||||
&.disabled > svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,11 @@
|
||||
{{ value[0].label || name }}
|
||||
</td>
|
||||
<td>
|
||||
<generic-history-content v-for="(item, index) in value" :key="index" :data_item="item.value" />
|
||||
<generic-history-item
|
||||
v-for="(item, index) in value"
|
||||
:key="index"
|
||||
:item-id="item.value.id"
|
||||
:item-src="item.value.src" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -23,11 +27,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericHistoryContent from "components/History/ContentItem/GenericContentItem/GenericHistoryContent";
|
||||
import GenericHistoryItem from "components/History/Content/GenericItem";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GenericHistoryContent,
|
||||
GenericHistoryItem,
|
||||
},
|
||||
props: {
|
||||
jobOutputs: Object,
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("JobMetrics/JobMetrics.vue", () => {
|
||||
expect(elements.length).toBe(3);
|
||||
|
||||
checkTableParameter(elements.at(0), "Add this value", "22");
|
||||
checkTableParameter(elements.at(1), linkParam.text, `${raw.hid} ${raw.name}`);
|
||||
checkTableParameter(elements.at(1), linkParam.text, `${raw.hid} : ${raw.name}`);
|
||||
checkTableParameter(elements.at(2), "Iterate?", "NO");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(elVal, pvIndex) in parameter_value" :key="pvIndex">
|
||||
<generic-history-content v-if="['hda', 'hdca'].includes(elVal.src)" :data_item="elVal" />
|
||||
<generic-history-item
|
||||
v-if="['hda', 'hdca'].includes(elVal.src)"
|
||||
:item-id="elVal.id"
|
||||
:item-src="elVal.src" />
|
||||
<span v-else> {{ elVal.hid }}: {{ elVal.name }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import GenericHistoryContent from "components/History/ContentItem/GenericContentItem/GenericHistoryContent";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
import GenericHistoryItem from "components/History/Content/GenericItem";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GenericHistoryContent,
|
||||
GenericHistoryItem,
|
||||
},
|
||||
props: {
|
||||
parameter_value: {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:key="input.id"
|
||||
:data-label="dataInputStepLabel(key, input)">
|
||||
<b>{{ dataInputStepLabel(key, input) }}</b>
|
||||
<generic-history-content :data_item="input" />
|
||||
<generic-history-item :item-id="input.id" :item-src="input.src" />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
<summary><b>Outputs</b></summary>
|
||||
<div v-for="(output, key) in invocation.outputs" :key="output.id">
|
||||
<b>{{ key }}:</b>
|
||||
<generic-history-content :data_item="output" />
|
||||
<generic-history-item :item-id="output.id" :item-src="output.src" />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<summary><b>Output Collections</b></summary>
|
||||
<div v-for="(output, key) in invocation.output_collections" :key="output.id">
|
||||
<b>{{ key }}:</b>
|
||||
<generic-history-content :data_item="output" />
|
||||
<generic-history-item :item-id="output.id" :item-src="output.src" />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -51,8 +51,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ParameterStep from "./ParameterStep.vue";
|
||||
import GenericHistoryContent from "components/History/ContentItem/GenericContentItem/GenericHistoryContent";
|
||||
import ParameterStep from "./ParameterStep";
|
||||
import GenericHistoryItem from "components/History/Content/GenericItem";
|
||||
import WorkflowInvocationStep from "./WorkflowInvocationStep";
|
||||
|
||||
import { mapGetters } from "vuex";
|
||||
@@ -60,7 +60,7 @@ import { mapCacheActions } from "vuex-cache";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GenericHistoryContent,
|
||||
GenericHistoryItem,
|
||||
WorkflowInvocationStep,
|
||||
ParameterStep,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<summary><b>Output Datasets</b></summary>
|
||||
<div v-for="(value, name) in stepDetails.outputs" :key="value.id">
|
||||
<b>{{ name }}</b>
|
||||
<generic-history-content :data_item="value" />
|
||||
<generic-history-item :item-id="value.id" :item-src="value.src" />
|
||||
</div>
|
||||
</details>
|
||||
<details
|
||||
@@ -31,7 +31,7 @@
|
||||
<summary><b>Output Dataset Collections</b></summary>
|
||||
<div v-for="(value, name) in stepDetails.output_collections" :key="value.id">
|
||||
<b>{{ name }}</b>
|
||||
<generic-history-content :data_item="value" />
|
||||
<generic-history-item :item-id="value.id" :item-src="value.src" />
|
||||
</div>
|
||||
</details>
|
||||
<div class="portlet-body" style="width: 100%; overflow-x: auto">
|
||||
@@ -41,16 +41,20 @@
|
||||
</details>
|
||||
<parameter-step
|
||||
v-else-if="workflowStepType == 'parameter_input'"
|
||||
:parameters="[
|
||||
invocation.input_step_parameters[stepDetails.workflow_step_label],
|
||||
]"></parameter-step>
|
||||
<generic-history-content
|
||||
v-else-if="isDataStep && invocation.inputs[workflowStep.id]"
|
||||
:data_item="invocation.inputs[workflowStep.id]" />
|
||||
:parameters="[invocation.input_step_parameters[stepDetails.workflow_step_label]]" />
|
||||
<generic-history-item
|
||||
v-else-if="
|
||||
isDataStep &&
|
||||
invocation &&
|
||||
invocation.inputs &&
|
||||
invocation.inputs[workflowStep.id]
|
||||
"
|
||||
:item-id="invocation.inputs[workflowStep.id].id"
|
||||
:item-src="invocation.inputs[workflowStep.id].src" />
|
||||
<div v-else-if="workflowStepType == 'subworkflow'">
|
||||
<div v-if="!stepDetails.subworkflow_invocation_id">
|
||||
Workflow invocation for this step is not yet scheduled.
|
||||
<p></p>
|
||||
<br />
|
||||
This step consumes outputs from these steps:
|
||||
<ul v-if="workflowStep">
|
||||
<li
|
||||
@@ -84,9 +88,9 @@ import { mapCacheActions } from "vuex-cache";
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import JobStep from "./JobStep";
|
||||
import ParameterStep from "./ParameterStep";
|
||||
import GenericHistoryContent from "components/History/ContentItem/GenericContentItem/GenericHistoryContent";
|
||||
import GenericHistoryItem from "components/History/Content/GenericItem";
|
||||
import { InvocationStepProvider } from "components/providers";
|
||||
import LoadingSpan from "../LoadingSpan";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -94,7 +98,7 @@ export default {
|
||||
JobStep,
|
||||
ParameterStep,
|
||||
InvocationStepProvider,
|
||||
GenericHistoryContent,
|
||||
GenericHistoryItem,
|
||||
WorkflowInvocationState: () => import("components/WorkflowInvocationState/WorkflowInvocationState"),
|
||||
},
|
||||
props: {
|
||||
|
||||
@@ -7,67 +7,71 @@ import { getGalaxyInstance } from "app";
|
||||
import { redirectToUrl, prependPath } from "utils/redirect";
|
||||
import _l from "utils/localization";
|
||||
|
||||
// wrapper for backbone router
|
||||
export function backboneRoute(path, ...args) {
|
||||
try {
|
||||
getGalaxyInstance().router.push(path, ...args);
|
||||
} catch (err) {
|
||||
console.warn("Failed galaxy route change", err, ...arguments);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper for scratchbook/multiview feature
|
||||
export function iframeAdd({ path, title = "Galaxy", tryIframe = true }) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
Galaxy.frame.add({
|
||||
url: prependPath(path),
|
||||
title: _l(title),
|
||||
});
|
||||
return true;
|
||||
} else if (tryIframe) {
|
||||
return iframeRedirect(path);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// straight ifrme redirect
|
||||
export function iframeRedirect(path, target = "galaxy_main") {
|
||||
try {
|
||||
const targetFrame = window.frames[target];
|
||||
if (!targetFrame) {
|
||||
throw new Error(`Requested frame ${target} doesn't exist`);
|
||||
}
|
||||
targetFrame.location = prependPath(path);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Failed iframe redirect", err, ...arguments);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// straight window.location
|
||||
export function redirect(path) {
|
||||
console.log("legacyNavigation: go", path);
|
||||
redirectToUrl(prependPath(path));
|
||||
}
|
||||
|
||||
// arbitrary Galaxy wrapper
|
||||
export function useGalaxy(fn) {
|
||||
return fn(getGalaxyInstance());
|
||||
}
|
||||
|
||||
// wrapper for navigation to be used as mixin
|
||||
export const legacyNavigationMixin = {
|
||||
methods: {
|
||||
// straight window.location
|
||||
redirect(path) {
|
||||
console.log("legacyNavigation: go", path);
|
||||
redirectToUrl(prependPath(path));
|
||||
},
|
||||
|
||||
// weird wrapper for Galaxy's wierd frame navigation
|
||||
// checks for existing galaxy frame reference, uses that or
|
||||
// loads a brand new document in the iframe
|
||||
iframeAdd({ path, title = "Galaxy", tryIframe = true }) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
Galaxy.frame.add({
|
||||
url: prependPath(path),
|
||||
title: _l(title),
|
||||
});
|
||||
return true;
|
||||
} else if (tryIframe) {
|
||||
return this.iframeRedirect(path);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// straight ifrme redirect
|
||||
iframeRedirect(path, target = "galaxy_main") {
|
||||
try {
|
||||
const targetFrame = window.frames[target];
|
||||
if (!targetFrame) {
|
||||
throw new Error(`Requested frame ${target} doesn't exist`);
|
||||
}
|
||||
targetFrame.location = prependPath(path);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Failed iframe redirect", err, ...arguments);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// galaxy router, wrapper for backbone router
|
||||
backboneRoute(path, ...args) {
|
||||
try {
|
||||
getGalaxyInstance().router.push(path, ...args);
|
||||
} catch (err) {
|
||||
console.warn("Failed galaxy route change", err, ...arguments);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// arbitrary Galaxy wrapper
|
||||
useGalaxy(fn) {
|
||||
return fn(getGalaxyInstance());
|
||||
},
|
||||
|
||||
// prepend server prefix from configs to a url
|
||||
redirect,
|
||||
iframeAdd,
|
||||
iframeRedirect,
|
||||
backboneRoute,
|
||||
useGalaxy,
|
||||
prependPath,
|
||||
},
|
||||
};
|
||||
|
||||
// wrapper for navigation to be used as plugin
|
||||
export const legacyNavigationPlugin = {
|
||||
install(Vue) {
|
||||
Vue.mixin(legacyNavigationMixin);
|
||||
|
||||
@@ -5,8 +5,9 @@ import { rethrowSimple } from "utils/simple-error";
|
||||
|
||||
async function urlData({ url }) {
|
||||
try {
|
||||
const { data } = await axios.get(`${getAppRoot()}${url}`);
|
||||
console.debug("Requesting data from: ", url);
|
||||
const { data } = await axios.get(`${getAppRoot()}${url}`);
|
||||
console.debug("Received response");
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
|
||||
@@ -5,7 +5,7 @@ import MvcHistoryPanel from "entry/panels/history-panel";
|
||||
import Page from "layout/page";
|
||||
|
||||
// Vue adapter emulates current features of backbone history panel
|
||||
import { HistoryPanelProxy } from "components/History";
|
||||
import { HistoryPanelProxy } from "components/History/adapters/HistoryPanelProxy";
|
||||
import { isBetaHistoryOpen } from "components/History/adapters/betaToggle";
|
||||
|
||||
addInitialization((Galaxy, { options = {} }) => {
|
||||
|
||||
@@ -72,7 +72,7 @@ const HistoryPanel = Backbone.View.extend({
|
||||
|
||||
this.model = new Backbone.Model({
|
||||
// define components
|
||||
cls: "history-right-panel history-details",
|
||||
cls: "history-right-panel details",
|
||||
title: _l("History"),
|
||||
buttons: panelHeaderButtons,
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ var AnnotationEditor = Backbone.View.extend(baseMVC.LoggableMixin)
|
||||
_l("Annotation"),
|
||||
"</label>",
|
||||
// set up initial tags by adding as CSV to input vals (necc. to init select2)
|
||||
'<div class="annotation" data-description="history annotation">',
|
||||
'<div class="annotation" data-description="annotation value">',
|
||||
_.escape(annotation),
|
||||
"</div>",
|
||||
].join("");
|
||||
|
||||
@@ -521,14 +521,14 @@ var HistoryView = _super.extend(
|
||||
HistoryView.prototype.templates = (() => {
|
||||
var mainTemplate = () =>
|
||||
`<div>
|
||||
<div class="controls history-details"></div>
|
||||
<div class="controls details"></div>
|
||||
<ul class="list-items"></ul>
|
||||
<div class="empty-message alert alert-info"></div>',
|
||||
</div>`;
|
||||
|
||||
var controlsTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="controls history-details">',
|
||||
'<div class="controls details">',
|
||||
'<div class="title">',
|
||||
'<div class="name history-title"><%- history.name %></div>',
|
||||
"</div>",
|
||||
|
||||
@@ -9915,6 +9915,11 @@ vue-template-es2015-compiler@^1.5.3, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-virtual-scroll-list@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.3.tgz#290f0d04be814b93585f2ef232097b671141e5b1"
|
||||
integrity sha512-heuwlZ+lEdcVSp66CpVXnyNdsiHl/XU1cvqJb0yUHGlLv6DGK8OX16FH8I3w/wKNXtHSPF9itdBBGJFrXkyHeg==
|
||||
|
||||
vue@^2.6.10, vue@^2.6.14:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
|
||||
@@ -134,7 +134,7 @@ def retry_during_transitions(
|
||||
return _retry
|
||||
|
||||
|
||||
def edit_details(f, scope=".beta.history"):
|
||||
def edit_details(f, scope=".history-index"):
|
||||
"""Open the editor, run the edits, hit the save button"""
|
||||
|
||||
@wraps(f)
|
||||
@@ -1342,7 +1342,7 @@ class NavigatesGalaxy(HasDriver):
|
||||
return not self.components.history_panel.beta.is_absent
|
||||
|
||||
# avoids problematic ID and classes on markup
|
||||
def beta_history_element(self, attribute_value, attribute_name="data-description", scope=".beta.history"):
|
||||
def beta_history_element(self, attribute_value, attribute_name="data-description", scope=".history-index"):
|
||||
return self.components._.by_attribute(name=attribute_name, value=attribute_value, scope=scope)
|
||||
|
||||
# join list of attrs into css attribute selectors and append to base beta_item selector
|
||||
@@ -1852,7 +1852,7 @@ class NavigatesGalaxy(HasDriver):
|
||||
def snapshot(self, description):
|
||||
"""Test case subclass overrides this to provide detailed logging."""
|
||||
|
||||
def open_history_editor(self, scope=".beta.history"):
|
||||
def open_history_editor(self, scope=".history-index"):
|
||||
if self.is_beta_history():
|
||||
panel = self.components.history_panel.editor.selector(scope=scope)
|
||||
toggle = panel.toggle
|
||||
@@ -1860,7 +1860,7 @@ class NavigatesGalaxy(HasDriver):
|
||||
editor = panel.form
|
||||
editor.wait_for_present()
|
||||
|
||||
def close_history_editor(self, scope=".beta.history"):
|
||||
def close_history_editor(self, scope=".history-index"):
|
||||
if self.is_beta_history():
|
||||
toggle = self.components.history_panel.edit_toggle
|
||||
toggle.wait_for_and_click()
|
||||
|
||||
@@ -154,7 +154,7 @@ history_panel:
|
||||
content_item:
|
||||
selectors:
|
||||
# specific rows selectable with attributes via suffix
|
||||
_: '.beta.history .content-item${suffix}'
|
||||
_: '.history-index .content-item${suffix}'
|
||||
title: '${_} .content-title'
|
||||
details: '${_} .details'
|
||||
summary: '${_} .summary'
|
||||
@@ -177,7 +177,7 @@ history_panel:
|
||||
info_button: '${_} .params-btn'
|
||||
nametags: '${_} .nametags .badge-tags'
|
||||
|
||||
dataset_operations_dropdown: '${_} .dataset-operations-dropdown'
|
||||
dataset_operations_dropdown: '${_} .dataset-actions'
|
||||
|
||||
# re-usable history editor, scoped for use in different layout scenarios (multi, etc.)
|
||||
editor:
|
||||
@@ -193,10 +193,11 @@ history_panel:
|
||||
# beta history tags
|
||||
tag_editor:
|
||||
selectors:
|
||||
_: '${scope} .history-details .tags'
|
||||
_: '${scope} .details .tags'
|
||||
toggle: '${_} .toggle-link'
|
||||
display: '${_} .tag-area .tag-name'
|
||||
input: '${_} input'
|
||||
tag_area: '${_} .tag-area'
|
||||
tag_close_btn: '${_} .ti-icon-close'
|
||||
|
||||
multi_operations:
|
||||
@@ -226,7 +227,7 @@ history_panel:
|
||||
list_items_beta: '.dataset-collection-panel .listing .content-item'
|
||||
|
||||
selectors:
|
||||
beta: '.beta.history'
|
||||
beta: '.history-index'
|
||||
_: '#current-history-panel'
|
||||
search: '#current-history-panel input.search-query'
|
||||
refresh_button: '.history-refresh-button'
|
||||
@@ -240,25 +241,25 @@ history_panel:
|
||||
empty_message: '.empty-message'
|
||||
size: '.history-size'
|
||||
tag_icon: '.actions .history-tag-btn'
|
||||
tag_area: '.history-details .tags-display'
|
||||
tag_area_input: '.history-details .tags-display .tags-input input'
|
||||
tag_area: '.details .tags-display'
|
||||
tag_area_input: '.details .tags-display .tags-input input'
|
||||
tag_close_btn: '.tags-display .ti-icon-close'
|
||||
tags: 'li.ti-tag.ti-valid .tag-name'
|
||||
|
||||
annotation_icon: '.actions .history-annotate-btn'
|
||||
annotation_area: '.history-details .history-annotation'
|
||||
annotation_area: '.details .history-annotation'
|
||||
annotation_editable_text:
|
||||
selector: 'history annotation'
|
||||
selector: 'annotation value'
|
||||
type: data-description
|
||||
annotation_edit:
|
||||
selector: 'annotation input'
|
||||
type: data-description
|
||||
|
||||
annotation_done: '.history-details .history-annotation .annotation button'
|
||||
annotation_done: '.details .history-annotation .annotation button'
|
||||
|
||||
options_button: '#history-options-button'
|
||||
options_button_icon: '.history-details .menu-expand-button'
|
||||
options_button_icon_beta: '.history-details .menu-expand-button'
|
||||
options_button_icon: '.details .menu-expand-button'
|
||||
options_button_icon_beta: '.details .menu-expand-button'
|
||||
options_button_copy_datasets_beta: '.copy-datasets-menu-item'
|
||||
options_menu: '#history-options-button-menu'
|
||||
options_menu_item:
|
||||
@@ -631,7 +632,7 @@ invocations:
|
||||
step_output_collection_toggle: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-output-collection-details .name'
|
||||
step_output_collection_element_identifier:
|
||||
type: xpath
|
||||
selector: '//span[@class="name"][text()="${element_identifier}"]'
|
||||
selector: '//span[@class="content-title name"][text()="${element_identifier}"]'
|
||||
step_output_collection_element_datatype: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-output-collection-details .datatype .value'
|
||||
step_job_details: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-job-details'
|
||||
step_job_table: '.workflow-invocation-state-component .invocation-steps-details [data-step="${order_index}"] .invocation-step-job-details table'
|
||||
|
||||
@@ -20,7 +20,7 @@ class HistoryPanelTestCase(SeleniumTestCase):
|
||||
name_element = self.history_panel_name_element()
|
||||
if self.is_beta_history():
|
||||
# look for the editor icon
|
||||
editor = self.components.history_panel.editor.selector(scope=".beta.history")
|
||||
editor = self.components.history_panel.editor.selector(scope=".history-index")
|
||||
toggle = editor.toggle
|
||||
toggle.wait_for_visible()
|
||||
else:
|
||||
@@ -60,7 +60,7 @@ class HistoryPanelTestCase(SeleniumTestCase):
|
||||
@edit_details
|
||||
def test_history_tags_and_annotations_buttons(self):
|
||||
if self.is_beta_history():
|
||||
history_editor = self.components.history_panel.editor.selector(scope=".beta.history")
|
||||
history_editor = self.components.history_panel.editor.selector(scope=".history-index")
|
||||
history_editor.annotation_input.wait_for_clickable()
|
||||
history_editor.tags_input.wait_for_clickable()
|
||||
else:
|
||||
@@ -158,7 +158,7 @@ class HistoryPanelTestCase(SeleniumTestCase):
|
||||
# looks like this is intended to check if the tag editor is open
|
||||
def assert_no_tags():
|
||||
if self.is_beta_history():
|
||||
tags_component = self.components.history_panel.tag_editor.selector(scope=".beta.history")
|
||||
tags_component = self.components.history_panel.tag_editor.selector(scope=".history-index")
|
||||
tags_component.display.assert_absent_or_hidden()
|
||||
else:
|
||||
self.components.history_panel.tag_area.assert_absent_or_hidden()
|
||||
@@ -172,18 +172,21 @@ class HistoryPanelTestCase(SeleniumTestCase):
|
||||
|
||||
# add more tags to non-empty tags area
|
||||
tags += add_tags(tags_size)
|
||||
self.sleep_for(self.wait_types.UX_RENDER)
|
||||
if self.is_beta_history():
|
||||
tags.sort()
|
||||
assert_current_tags(tags)
|
||||
|
||||
# delete all tags
|
||||
expected_tags_len = len(tags)
|
||||
self.clear_tags(expected_tags_len)
|
||||
|
||||
self.sleep_for(self.wait_types.UX_RENDER)
|
||||
assert_no_tags()
|
||||
|
||||
# after about 5 tags, a toggle link shows up and you have to click it to see the full list
|
||||
def open_tags(self):
|
||||
tags_component = self.components.history_panel.tag_editor.selector(scope=".beta.history")
|
||||
if not tags_component.toggle.is_absent:
|
||||
tags_component = self.components.history_panel.tag_editor.selector(scope=".history-index")
|
||||
if tags_component.tag_area.is_absent:
|
||||
tags_component.toggle.wait_for_and_click()
|
||||
tags_component.display.wait_for_visible()
|
||||
return tags_component.display
|
||||
@@ -192,7 +195,7 @@ class HistoryPanelTestCase(SeleniumTestCase):
|
||||
def clear_tags(self, expected_tags_size):
|
||||
if self.is_beta_history():
|
||||
self.open_tags()
|
||||
tags = self.components.history_panel.tag_editor.selector(scope=".beta.history")
|
||||
tags = self.components.history_panel.tag_editor.selector(scope=".history-index")
|
||||
close_tag_buttons = tags.tag_close_btn.all()
|
||||
else:
|
||||
close_tag_buttons = self.components.history_panel.tag_close_btn.all()
|
||||
|
||||
@@ -25,6 +25,8 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
|
||||
input_collection, failed_collection = self._generate_partially_failed_collection_with_input()
|
||||
input_hid = input_collection["hid"]
|
||||
failed_hid = failed_collection["hid"]
|
||||
self.home()
|
||||
|
||||
ok_inputs = {
|
||||
"input1": {"batch": True, "values": [{"src": "hdca", "id": input_collection["id"]}]},
|
||||
"sleep_time": 0,
|
||||
@@ -36,8 +38,6 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
|
||||
)
|
||||
ok_hid = ok_response["implicit_collections"][0]["hid"]
|
||||
|
||||
self.home()
|
||||
|
||||
self.history_panel_wait_for_hid_state(input_hid, "ok")
|
||||
self.history_panel_wait_for_hid_state(failed_hid, "error")
|
||||
self.history_panel_wait_for_hid_state(ok_hid, "ok")
|
||||
@@ -170,6 +170,7 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
|
||||
if self.is_beta_history():
|
||||
self._rename_collection(new_name)
|
||||
self.screenshot("history_panel_collection_view_rename_beta")
|
||||
self.sleep_for(WAIT_TYPES.UX_RENDER)
|
||||
title_element = self.beta_history_element("collection name display").wait_for_present()
|
||||
else:
|
||||
title_element = collection_view.title.wait_for_visible()
|
||||
@@ -215,16 +216,16 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
|
||||
input_collection = self._populated_paired_and_wait_for_it()
|
||||
collection_hid = input_collection["hid"]
|
||||
collection_view = self._click_and_wait_for_collection_view(collection_hid)
|
||||
|
||||
self.sleep_for(WAIT_TYPES.UX_TRANSITION)
|
||||
if self.is_beta_history():
|
||||
self.sleep_for(WAIT_TYPES.HISTORY_POLL)
|
||||
dataset_elements = collection_view.list_items_beta.all()
|
||||
else:
|
||||
dataset_elements = collection_view.list_items.all()
|
||||
|
||||
assert len(dataset_elements) == 2, dataset_elements
|
||||
titles = [de.find_element_by_css_selector(".title .name").text for de in dataset_elements]
|
||||
titles.sort()
|
||||
selector = ".title .name"
|
||||
if self.is_beta_history():
|
||||
selector = ".content-title"
|
||||
titles = [de.find_element_by_css_selector(selector).text for de in dataset_elements]
|
||||
assert titles == ["forward", "reverse"]
|
||||
self.screenshot("history_panel_collection_view_paired")
|
||||
|
||||
@@ -246,7 +247,10 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
|
||||
else:
|
||||
dataset_elements = collection_view.list_items.all()
|
||||
assert len(dataset_elements) == 4, dataset_elements
|
||||
title_elements = [de.find_element_by_css_selector(".title .name").text for de in dataset_elements]
|
||||
selector = ".title .name"
|
||||
if self.is_beta_history():
|
||||
selector = ".content-title"
|
||||
title_elements = [de.find_element_by_css_selector(selector).text for de in dataset_elements]
|
||||
assert title_elements == ["data1", "data2", "data3", "data4"]
|
||||
|
||||
check_four_datasets_shown()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user