Merge pull request #14816 from assuntad23/6493/feature/undelete-workflows

6493/feature/undelete workflows
This commit is contained in:
Dannon
2022-10-25 18:04:38 -04:00
committed by GitHub
7 changed files with 127 additions and 65 deletions
+5 -1
View File
@@ -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>
+61 -26
View File
@@ -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>&#8212;</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>&#8212;</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>&#8212;</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>&lt;tag_name&gt;:&lt;tag_value&gt;</code> or <code>&lt;tag_name&gt;:'&lt;tag_value&gt;'</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>&lt;tag_name&gt;:&lt;tag_value&gt;</code> or
<code>&lt;tag_name&gt;:'&lt;tag_value&gt;'</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 {
+4 -1
View File
@@ -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()