mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #14816 from assuntad23/6493/feature/undelete-workflows
6493/feature/undelete workflows
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<StatelessTags :value="tags" @input="onInput" @tag-click="onTagClick" />
|
||||
<StatelessTags :value="tags" :disabled="disabled" @input="onInput" @tag-click="onTagClick" />
|
||||
</template>
|
||||
<script>
|
||||
import StatelessTags from "components/Tags/StatelessTags";
|
||||
@@ -14,6 +14,10 @@ export default {
|
||||
tags: {
|
||||
type: Array,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(tags) {
|
||||
|
||||
@@ -93,7 +93,6 @@ describe("WorkflowDropdown.vue", () => {
|
||||
|
||||
describe("workflow clicking workflow deletion", () => {
|
||||
let axiosMock;
|
||||
let confirmRequest;
|
||||
|
||||
async function mountAndDelete() {
|
||||
const workflow = {
|
||||
@@ -109,33 +108,30 @@ describe("WorkflowDropdown.vue", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
confirmRequest = true;
|
||||
global.confirm = jest.fn(() => confirmRequest);
|
||||
axiosMock.onDelete("/api/workflows/workflowid123").reply(202, "deleted...");
|
||||
axiosMock.onPost("/api/workflows/workflowid123/undelete").reply(204, "restored...");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it("should confirm with localized deletion message", async () => {
|
||||
await mountAndDelete();
|
||||
expect(global.confirm).toHaveBeenCalledWith(expect.toBeLocalized());
|
||||
});
|
||||
|
||||
it("should fire deletion API request upon confirmation", async () => {
|
||||
it("should fire deletion API request upon remove action", async () => {
|
||||
await mountAndDelete();
|
||||
const emitted = wrapper.emitted();
|
||||
expect(emitted["onRemove"][0][0]).toEqual("workflowid123");
|
||||
expect(emitted["onSuccess"][0][0]).toEqual("deleted...");
|
||||
});
|
||||
|
||||
it("should not fire deletion API request if not confirmed", async () => {
|
||||
confirmRequest = false;
|
||||
it("should restore previously deleted workflows", async () => {
|
||||
await mountAndDelete();
|
||||
const emitted = wrapper.emitted();
|
||||
expect(emitted["onRemove"]).toBeFalsy();
|
||||
expect(emitted["onSuccess"]).toBeFalsy();
|
||||
expect(emitted["onRemove"][0][0]).toEqual("workflowid123");
|
||||
expect(emitted["onSuccess"][0][0]).toEqual("deleted...");
|
||||
await wrapper.vm.onRestore();
|
||||
await flushPromises();
|
||||
expect(emitted["onRestore"][0][0]).toEqual("workflowid123");
|
||||
expect(emitted["onSuccess"][1][0]).toEqual("restored...");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,57 +23,61 @@
|
||||
<p v-if="workflow.description" class="workflow-dropdown-description">{{ workflow.description }}</p>
|
||||
<div class="dropdown-menu" aria-labelledby="workflow-dropdown">
|
||||
<a
|
||||
v-if="!readOnly"
|
||||
v-if="!readOnly && !isDeleted"
|
||||
class="dropdown-item"
|
||||
@keypress="$router.push(urlEdit)"
|
||||
@click.prevent="$router.push(urlEdit)">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Edit</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="onCopy">
|
||||
<a v-if="!isDeleted" class="dropdown-item" href="#" @click.prevent="onCopy">
|
||||
<span class="fa fa-copy fa-fw mr-1" />
|
||||
<span v-localize>Copy</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="!readOnly"
|
||||
v-if="!readOnly && !isDeleted"
|
||||
class="dropdown-item"
|
||||
@keypress="$router.push(urlInvocations)"
|
||||
@click.prevent="$router.push(urlInvocations)">
|
||||
<span class="fa fa-list fa-fw mr-1" />
|
||||
<span v-localize>Invocations</span>
|
||||
</a>
|
||||
<a class="dropdown-item" :href="urlDownload">
|
||||
<a v-if="!isDeleted" class="dropdown-item" :href="urlDownload">
|
||||
<span class="fa fa-download fa-fw mr-1" />
|
||||
<span v-localize>Download</span>
|
||||
</a>
|
||||
<a v-if="!readOnly" class="dropdown-item" href="#" @click.prevent="onRename">
|
||||
<a v-if="!readOnly && !isDeleted" class="dropdown-item" href="#" @click.prevent="onRename">
|
||||
<span class="fa fa-signature fa-fw mr-1" />
|
||||
<span v-localize>Rename</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="!readOnly"
|
||||
v-if="!readOnly && !isDeleted"
|
||||
class="dropdown-item"
|
||||
@keypress="$router.push(urlShare)"
|
||||
@click.prevent="$router.push(urlShare)">
|
||||
<span class="fa fa-share-alt fa-fw mr-1" />
|
||||
<span v-localize>Share</span>
|
||||
</a>
|
||||
<a v-if="!readOnly" class="dropdown-item" :href="urlExport">
|
||||
<a v-if="!readOnly && !isDeleted" class="dropdown-item" :href="urlExport">
|
||||
<span class="fa fa-file-export fa-fw mr-1" />
|
||||
<span v-localize>Export</span>
|
||||
</a>
|
||||
<a class="dropdown-item" :href="urlView">
|
||||
<a v-if="!isDeleted" class="dropdown-item" :href="urlView">
|
||||
<span class="fa fa-eye fa-fw mr-1" />
|
||||
<span v-localize>View</span>
|
||||
</a>
|
||||
<a v-if="sourceLabel" class="dropdown-item" :href="sourceUrl">
|
||||
<a v-if="sourceLabel && !isDeleted" class="dropdown-item" :href="sourceUrl">
|
||||
<span class="fa fa-globe fa-fw mr-1" />
|
||||
<span v-localize>{{ sourceLabel }}</span>
|
||||
</a>
|
||||
<a v-if="!readOnly" class="dropdown-item" href="#" @click.prevent="onDelete">
|
||||
<a v-if="!readOnly && !isDeleted" class="dropdown-item" href="#" @click.prevent="onDelete">
|
||||
<span class="fa fa-trash fa-fw mr-1" />
|
||||
<span v-localize>Delete</span>
|
||||
</a>
|
||||
<a v-if="isDeleted" class="dropdown-item" href="#" @click.prevent="onRestore">
|
||||
<span class="fa fa-trash fa-fw mr-1" />
|
||||
<span v-localize>Restore</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,6 +125,9 @@ export default {
|
||||
readOnly() {
|
||||
return !!this.workflow.shared;
|
||||
},
|
||||
isDeleted() {
|
||||
return this.workflow.deleted;
|
||||
},
|
||||
sourceUrl() {
|
||||
if (this.workflow.source_metadata?.url) {
|
||||
return this.workflow.source_metadata.url;
|
||||
@@ -175,19 +182,15 @@ export default {
|
||||
},
|
||||
onDelete: function () {
|
||||
const id = this.workflow.id;
|
||||
const name = this.workflow.name;
|
||||
const confirmationMessage = this.l(`Are you sure you want to delete workflow '${name}'?`);
|
||||
if (window.confirm(confirmationMessage)) {
|
||||
this.services
|
||||
.deleteWorkflow(id)
|
||||
.then((message) => {
|
||||
this.$emit("onRemove", id);
|
||||
this.$emit("onSuccess", message);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit("onError", error);
|
||||
});
|
||||
}
|
||||
this.services
|
||||
.deleteWorkflow(id)
|
||||
.then((message) => {
|
||||
this.$emit("onRemove", id);
|
||||
this.$emit("onSuccess", message);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit("onError", error);
|
||||
});
|
||||
},
|
||||
onRename: function () {
|
||||
const id = this.workflow.id;
|
||||
@@ -206,6 +209,18 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
onRestore: function () {
|
||||
const id = this.workflow.id;
|
||||
this.services
|
||||
.undeleteWorkflow(id)
|
||||
.then((message) => {
|
||||
this.$emit("onRestore", id);
|
||||
this.$emit("onSuccess", message);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit("onError", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -33,24 +33,37 @@
|
||||
@onRemove="onRemove"
|
||||
@onUpdate="onUpdate"
|
||||
@onSuccess="onSuccess"
|
||||
@onError="onError" />
|
||||
@onError="onError"
|
||||
@onRestore="onRestore" />
|
||||
</template>
|
||||
<template v-slot:cell(tags)="row">
|
||||
<Tags :index="row.index" :tags="row.item.tags" @input="onTags" @tag-click="onTagClick" />
|
||||
<Tags
|
||||
:index="row.index"
|
||||
:tags="row.item.tags"
|
||||
:disabled="row.item.deleted"
|
||||
@input="onTags"
|
||||
@tag-click="onTagClick" />
|
||||
</template>
|
||||
<template v-slot:cell(published)="row">
|
||||
<SharingIndicators :object="row.item" @filter="(filter) => appendFilter(filter)" />
|
||||
<SharingIndicators
|
||||
v-if="!row.item.deleted"
|
||||
:object="row.item"
|
||||
@filter="(filter) => appendFilter(filter)" />
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
<template v-slot:cell(show_in_tool_panel)="row">
|
||||
<WorkflowBookmark
|
||||
v-if="!row.item.deleted"
|
||||
:checked="row.item.show_in_tool_panel"
|
||||
@bookmark="(checked) => bookmarkWorkflow(row.item.id, checked)" />
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<UtcDate :date="data.value" mode="elapsed" />
|
||||
</template>
|
||||
<template v-slot:cell(execute)="row">
|
||||
<WorkflowRunButton :id="row.item.id" :root="root" />
|
||||
<WorkflowRunButton v-if="!row.item.deleted" :id="row.item.id" :root="root" />
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-pagination
|
||||
@@ -78,30 +91,49 @@ import WorkflowRunButton from "./WorkflowRunButton.vue";
|
||||
import SharingIndicators from "components/Indices/SharingIndicators";
|
||||
|
||||
const helpHtml = `<div>
|
||||
<p>This textbox box can be used to filter the workflows displayed.
|
||||
<p>This input can be used to filter the workflows displayed.</p>
|
||||
|
||||
<p>Text entered here will be searched against workflow names and tags. Additionally, advanced
|
||||
filtering tags can be used to refine the search more precisely. Tags are of the form
|
||||
<code><tag_name>:<tag_value></code> or <code><tag_name>:'<tag_value>'</code>.
|
||||
For instance to search just for RNAseq in the workflow name, <code>name:rnsseq</code> can be used.
|
||||
Notice by default the search is not case-sensitive.
|
||||
<p>
|
||||
Text entered here will be searched against workflow names and workflow
|
||||
tags. Additionally, advanced filtering tags can be used to refine the
|
||||
search more precisely. Filtering tags are of the form
|
||||
<code><tag_name>:<tag_value></code> or
|
||||
<code><tag_name>:'<tag_value>'</code>. For instance to
|
||||
search just for RNAseq in the workflow name,
|
||||
<code>name:rnsseq</code> can be used. Notice by default the search is
|
||||
not case-sensitive. If the quoted version of tag is used, the search is
|
||||
case sensitive and only full matches will be returned. So
|
||||
<code>name:'RNAseq'</code> would show only workflows named exactly
|
||||
<code>RNAseq</code>.
|
||||
</p>
|
||||
|
||||
If the quoted version of tag is used, the search is not case sensitive and only full matches will be
|
||||
returned. So <code>name:'RNAseq'</code> would show only workflows named exactly <code>RNAseq</code>.
|
||||
|
||||
<p>The available tags are:
|
||||
<dl>
|
||||
<dt><code>name</code></dt>
|
||||
<dd>This filters only against the workflow name.</dd>
|
||||
<dt><code>tag</code></dt>
|
||||
<dd>This filters only against the workflow tag. You may also just click on a tag in your list of workflows to filter on that tag using this directly.</dd>
|
||||
<dt><code>is:published</code></dt>
|
||||
<dd>This filters the workflows such that only published workflows are shown. You may also just click on the "published" icon of a workflow in your list to filter on this directly.</dd>
|
||||
<dt><code>is:shared</code></dt>
|
||||
<dd>This filters the workflows such that only workflows shared from another user directly with you are are shown. You may also just click on the "shared with me" icon of a workflow in your list to filter on this directly.</dd>
|
||||
</dl>
|
||||
</div>
|
||||
`;
|
||||
<p>The available filtering tags are:</p>
|
||||
<dl>
|
||||
<dt><code>name</code></dt>
|
||||
<dd>
|
||||
Shows workflows with given sequence of characters in their names.
|
||||
</dd>
|
||||
<dt><code>tag</code></dt>
|
||||
<dd>
|
||||
Shows workflows with the given workflow tag. You may also just click
|
||||
on a tag in your list of workflows to filter on that tag directly.
|
||||
</dd>
|
||||
<dt><code>is:published</code></dt>
|
||||
<dd>
|
||||
Shows published workflows. You may also just click on the
|
||||
"published" icon of a workflow in your list to filter on this
|
||||
directly.
|
||||
</dd>
|
||||
<dt><code>is:shared</code></dt>
|
||||
<dd>
|
||||
Shows workflows shared by another user directly with you. You may
|
||||
also just click on the "shared with me" icon of a workflow in your
|
||||
list to filter on this directly.
|
||||
</dd>
|
||||
<dt><code>is:deleted</code></dt>
|
||||
<dd>Shows deleted workflows.</dd>
|
||||
</dl>
|
||||
</div>`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -234,6 +266,9 @@ export default {
|
||||
onUpdate: function (id, data) {
|
||||
this.refresh();
|
||||
},
|
||||
onRestore: function (id) {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -37,6 +37,16 @@ export class Services {
|
||||
}
|
||||
}
|
||||
|
||||
async undeleteWorkflow(id) {
|
||||
const url = safePath(`/api/workflows/${id}/undelete`);
|
||||
try {
|
||||
const response = await axios.post(url);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
async updateWorkflow(id, data) {
|
||||
const url = safePath(`/api/workflows/${id}`);
|
||||
try {
|
||||
|
||||
@@ -166,7 +166,6 @@ class WorkflowsManager(sharable.SharableModelManager, deletable.DeletableManager
|
||||
query = query.options(latest_workflow_load)
|
||||
query = query.filter(or_(*filters))
|
||||
query = query.filter(model.StoredWorkflow.table.c.hidden == (true() if show_hidden else false()))
|
||||
query = query.filter(model.StoredWorkflow.table.c.deleted == (true() if show_deleted else false()))
|
||||
if payload.search:
|
||||
search_query = payload.search
|
||||
parsed_search = parse_filters_structured(search_query, INDEX_SEARCH_FILTERS)
|
||||
@@ -194,6 +193,9 @@ class WorkflowsManager(sharable.SharableModelManager, deletable.DeletableManager
|
||||
elif key == "is":
|
||||
if q == "published":
|
||||
query = query.filter(model.StoredWorkflow.published == true())
|
||||
elif q == "deleted":
|
||||
query = query.filter(model.StoredWorkflow.deleted == true())
|
||||
show_deleted = true
|
||||
elif q == "shared_with_me":
|
||||
if not show_shared:
|
||||
message = "Can only use tag is:shared_with_me if show_shared parameter also true."
|
||||
@@ -213,6 +215,7 @@ class WorkflowsManager(sharable.SharableModelManager, deletable.DeletableManager
|
||||
term,
|
||||
)
|
||||
)
|
||||
query = query.filter(model.StoredWorkflow.table.c.deleted == (true() if show_deleted else false()))
|
||||
if include_total_count:
|
||||
total_matches = query.count()
|
||||
else:
|
||||
|
||||
@@ -158,7 +158,6 @@ class TestWorkflowManagement(SeleniumTestCase, TestsGalaxyPagers, UsesWorkflowAs
|
||||
self.workflow_index_rename("fordelete")
|
||||
self._assert_showing_n_workflows(1)
|
||||
self.workflow_index_click_option("Delete")
|
||||
self.accept_alert()
|
||||
self._assert_showing_n_workflows(0)
|
||||
|
||||
self.workflow_index_open()
|
||||
|
||||
Reference in New Issue
Block a user