✨ Add tagging of workflows (#1647)
* clean up dropdown * clean up focusoncreate * ⚡ Ignore mistaken ID in POST /workflows * ⚡ Fix undefined tag ID in PATCH /workflows * ⚡ Shorten response for POST /tags * remove scss mixins * clean up imports * ⚡ Implement validation with class-validator * address ivan's comments * implement modals * Fix lint issues * fix disabling shortcuts * fix focus issues * fix focus issues * fix focus issues with modal * fix linting issues * use dispatch * use constants for modal keys * fix focus * fix lint issues * remove unused prop * add modal root * fix lint issues * remove unused methods * fix shortcut * remove max width * ⚡ Fix duplicate entry error for pg and MySQL * update rename messaging * update order of buttons * fix firefox overflow on windows * fix dropdown height * 🔨 refactor tag crud controllers * 🧹 remove unused imports * use variable for number of items * fix dropdown spacing * ⚡ Restore type to fix build * ⚡ Fix post-refactor PATCH /workflows/:id * ⚡ Fix PATCH /workflows/:id for zero tags * ⚡ Fix usage count becoming stringified * address max's comments * fix filter spacing * fix blur bug * address most of ivan's comments * address tags type concern * remove defaults * ⚡ return tag id as string * 🔨 add hooks to tag CUD operations * 🏎 simplify timestamp pruning * remove blur event * fix onblur bug * ⚡ Fix fs import to fix build * address max's comments * implement responsive tag container * fix lint issues * Set default dates in entities * 👕 Fix lint in migrations * update tag limits * address ivan's comments * remove rename, refactor header, implement new designs for save, remove responsive tag container * update styling * update styling * implement responsive tag container * implement header tags edit * implement header tags edit * fix lint issues * implement expandable input * minor fixes * minor fixes * use variable * rename save as * duplicate fixes * ⚡ Implement unique workflow names * ⚡ Create /workflows/new endpoint * minor edit fixes * lint fixes * style fixes * hook up saving name * hook up tags * clean up impl * fix dirty state bug * update limit * update notification messages * on click outside * fix minor bug with count * lint fixes * ⚡ Add query string params to /workflows/new * handle minor edge cases * handle minor edge cases * handle minor bugs; fix firefox dropdown issue * Fix min width * apply tags only after api success * remove count fix * 🚧 Adjust to new qs requirements * clean up workflow tags impl, fix tags delete bug * fix minor issue * fix minor spacing issue * disable wrap for ops * fix viewport root; save on click in dropdown * save button loading when saving name/tags * implement max width on tags container * implement cleaner create experience * disable edit while updating * codacy hex color * refactor tags container * fix clickability * fix workflow open and count * clean up structure * fix up lint issues * ⚡ Create migrations for unique workflow names * fix button size * increase workflow name limit for larger screen * tslint fixes * disable responsiveness for workflow modal * rename event * change min width for tags * clean up pr * ⚡ Adjust quotes in MySQL migration * ⚡ Adjust quotes in Postgres migration * address max's comments on styles * remove success toasts * add hover mode to name * minor fixes * refactor name preview * fix name input not to jiggle * finish up name input * Fix up add tags * clean up param * clean up scss * fix resizing name * fix resizing name * fix resize bug * clean up edit spacing * ignore on esc * fix input bug * focus input on clear * build * fix up add tags clickablity * remove scrollbars * move into folders * clean up multiple patch req * remove padding top from edit * update tags on enter * build * rollout blur on enter behavior * rollout esc behavior * fix tags bug when duplicating tags * move key to reload tags * update header spacing * build * update hex case * refactor workflow title * remove unusued prop * keep focus on error, fix bug on error * Fix bug with name / tags toggle on error * impl creating new workflow name * ⚡ Refactor endpoint per new guidelines * support naming endpoint * ⚡ Refactor to support numeric suffixes * 👕 Lint migrations for unique workflow names * ⚡ Add migrations set default dates to indexes * fix connection push bug * ⚡ Lowercase default workflow name * ⚡ Add prefixes to set default dates migration * ⚡ Fix indentation on default dates migrations * ⚡ Add temp ts-ignore for unrelated change * ⚡ Adjust default dates migration for MySQL Remove change to data column in credentials_entity, already covered by Omar's migration. Also, fix quotes from table prefix addition. * ⚡ Adjust quotes in dates migration for PG * fix safari color bug * fix count bug * fix scroll bugs in dropdown * expand filter size * apply box-sizing to main header * update workflow names in executions to be wrapped by quotes * fix bug where key is same in dropdown * fix firefox bug * move up push connection session * 🔨 Remove mistakenly added nullable property * 🔥 Remove unneeded index drop-create (PG) * 🔥 Remove unneeded table copying * ⚡ Merge dates migration with tags migration * 🔨 Refactor endpoint and make wf name env * dropdown colors in firefox * update colors to use variables * update thumb color * change error message * remove 100 char maximum * fix bug with saving tags dropdowns multiple times * update error message when no name * ⚡ Update name missing toast message * ⚡ Update workflow already exists message * disable saving for executions * fix bug causing modal to close * make tags in workflow open clickable * increase workflow limit to 3 * remove success notifications * update header spacing * escape tag names * update tag and table colors * remove tags from export * build * clean up push connection dependencies * address ben's comments * revert tags optional interface * address comments * update duplicate message * build * fix eol * add one more eol * ⚡ Update comment * add hover style for workflow open, fix up font weight Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
This commit is contained in:
101
packages/editor-ui/src/components/BreakpointsObserver.vue
Normal file
101
packages/editor-ui/src/components/BreakpointsObserver.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<span>
|
||||
<slot v-bind:bp="bp" v-bind:value="value" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
BREAKPOINT_SM,
|
||||
BREAKPOINT_MD,
|
||||
BREAKPOINT_LG,
|
||||
BREAKPOINT_XL,
|
||||
} from "@/constants";
|
||||
|
||||
/**
|
||||
* matching element.io https://element.eleme.io/#/en-US/component/layout#col-attributes
|
||||
* xs < 768
|
||||
* sm >= 768
|
||||
* md >= 992
|
||||
* lg >= 1200
|
||||
* xl >= 1920
|
||||
*/
|
||||
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { genericHelpers } from "@/components/mixins/genericHelpers";
|
||||
|
||||
export default mixins(genericHelpers).extend({
|
||||
name: "BreakpointsObserver",
|
||||
props: [
|
||||
"valueXS",
|
||||
"valueXL",
|
||||
"valueLG",
|
||||
"valueMD",
|
||||
"valueSM",
|
||||
"valueDefault",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
},
|
||||
methods: {
|
||||
onResize() {
|
||||
this.callDebounced("onResizeEnd", 50);
|
||||
},
|
||||
onResizeEnd() {
|
||||
this.$data.width = window.innerWidth;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
bp(): string {
|
||||
if (this.$data.width < BREAKPOINT_SM) {
|
||||
return "XS";
|
||||
}
|
||||
|
||||
if (this.$data.width >= BREAKPOINT_XL) {
|
||||
return "XL";
|
||||
}
|
||||
|
||||
if (this.$data.width >= BREAKPOINT_LG) {
|
||||
return "LG";
|
||||
}
|
||||
|
||||
if (this.$data.width >= BREAKPOINT_MD) {
|
||||
return "MD";
|
||||
}
|
||||
|
||||
return "SM";
|
||||
},
|
||||
value(): any | undefined { // tslint:disable-line:no-any
|
||||
if (this.$props.valueXS !== undefined && this.$data.width < BREAKPOINT_SM) {
|
||||
return this.$props.valueXS;
|
||||
}
|
||||
|
||||
if (this.$props.valueXL !== undefined && this.$data.width >= BREAKPOINT_XL) {
|
||||
return this.$props.valueXL;
|
||||
}
|
||||
|
||||
if (this.$props.valueLG !== undefined && this.$data.width >= BREAKPOINT_LG) {
|
||||
return this.$props.valueLG;
|
||||
}
|
||||
|
||||
if (this.$props.valueMD !== undefined && this.$data.width >= BREAKPOINT_MD) {
|
||||
return this.$props.valueMD;
|
||||
}
|
||||
|
||||
if (this.$props.valueSM !== undefined) {
|
||||
return this.$props.valueSM;
|
||||
}
|
||||
|
||||
return this.$props.valueDefault;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
125
packages/editor-ui/src/components/DuplicateWorkflowDialog.vue
Normal file
125
packages/editor-ui/src/components/DuplicateWorkflowDialog.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<Modal
|
||||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
@enter="save"
|
||||
size="sm"
|
||||
title="Duplicate Workflow"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<el-row>
|
||||
<el-input
|
||||
v-model="name"
|
||||
ref="nameInput"
|
||||
placeholder="Enter workflow name"
|
||||
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
|
||||
/>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<TagsDropdown
|
||||
:createEnabled="true"
|
||||
:currentTagIds="currentTagIds"
|
||||
:eventBus="dropdownBus"
|
||||
@blur="onTagsBlur"
|
||||
@esc="onTagsEsc"
|
||||
@update="onTagsUpdate"
|
||||
placeholder="Choose or create a tag"
|
||||
ref="dropdown"
|
||||
/>
|
||||
</el-row>
|
||||
</template>
|
||||
<template v-slot:footer="{ close }">
|
||||
<el-button size="small" @click="save" :loading="isSaving">Save</el-button>
|
||||
<el-button size="small" @click="close" :disabled="isSaving">Cancel</el-button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import mixins from "vue-typed-mixins";
|
||||
|
||||
import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants";
|
||||
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||
import { showMessage } from "@/components/mixins/showMessage";
|
||||
import TagsDropdown from "@/components/TagsDropdown.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
export default mixins(showMessage, workflowHelpers).extend({
|
||||
components: { TagsDropdown, Modal },
|
||||
name: "DuplicateWorkflow",
|
||||
props: ["dialogVisible", "modalName", "isActive"],
|
||||
data() {
|
||||
const currentTagIds = this.$store.getters[
|
||||
"workflowTags"
|
||||
] as string[];
|
||||
|
||||
return {
|
||||
name: '',
|
||||
currentTagIds,
|
||||
isSaving: false,
|
||||
modalBus: new Vue(),
|
||||
dropdownBus: new Vue(),
|
||||
MAX_WORKFLOW_NAME_LENGTH,
|
||||
prevTagIds: currentTagIds,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.$data.name = await this.$store.dispatch('workflows/getDuplicateCurrentWorkflowName');
|
||||
this.$nextTick(() => this.focusOnNameInput());
|
||||
},
|
||||
watch: {
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
this.focusOnSelect();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focusOnSelect() {
|
||||
this.dropdownBus.$emit('focus');
|
||||
},
|
||||
focusOnNameInput() {
|
||||
const input = this.$refs.nameInput as HTMLElement;
|
||||
if (input && input.focus) {
|
||||
input.focus();
|
||||
}
|
||||
},
|
||||
onTagsBlur() {
|
||||
this.prevTagIds = this.currentTagIds;
|
||||
},
|
||||
onTagsEsc() {
|
||||
// revert last changes
|
||||
this.currentTagIds = this.prevTagIds;
|
||||
},
|
||||
onTagsUpdate(tagIds: string[]) {
|
||||
this.currentTagIds = tagIds;
|
||||
},
|
||||
async save(): Promise<void> {
|
||||
const name = this.name.trim();
|
||||
if (!name) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name.`,
|
||||
type: "error",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$data.isSaving = true;
|
||||
|
||||
const saved = await this.saveAsNewWorkflow({name, tags: this.currentTagIds});
|
||||
|
||||
if (saved) {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
this.$data.isSaving = false;
|
||||
},
|
||||
closeDialog(): void {
|
||||
this.modalBus.$emit("close");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<!-- mock el-input element to apply styles -->
|
||||
<div :class="{'el-input': true, 'static-size': staticSize}" :data-value="hiddenValue">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ExpandableInputBase",
|
||||
props: ['value', 'placeholder', 'staticSize'],
|
||||
computed: {
|
||||
hiddenValue() {
|
||||
let value = (this.value as string).replace(/\s/g, '.'); // force input to expand on space chars
|
||||
if (!value) {
|
||||
// @ts-ignore
|
||||
value = this.$props.placeholder;
|
||||
}
|
||||
|
||||
return `${value}`; // adjust for padding
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$--horiz-padding: 15px;
|
||||
|
||||
*,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 1px solid transparent;
|
||||
padding: 0 $--horiz-padding - 2px; // -2px for borders
|
||||
}
|
||||
|
||||
div.el-input {
|
||||
display: inline-grid;
|
||||
font: inherit;
|
||||
padding: 10px 0;
|
||||
|
||||
&::after,
|
||||
input {
|
||||
grid-area: 1 / 2;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
|
||||
&::after {
|
||||
content: attr(data-value) ' ';
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 $--horiz-padding;
|
||||
}
|
||||
|
||||
&:not(.static-size)::after {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
input {
|
||||
border: $--custom-input-border-shadow
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<ExpandableInputBase :value="value" :placeholder="placeholder">
|
||||
<input
|
||||
class="el-input__inner"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
@input="onInput"
|
||||
@keydown.enter="onEnter"
|
||||
@keydown.esc="onEscape"
|
||||
ref="input"
|
||||
size="4"
|
||||
v-click-outside="onBlur"
|
||||
/>
|
||||
</ExpandableInputBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import ExpandableInputBase from "./ExpandableInputBase.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
components: { ExpandableInputBase },
|
||||
name: "ExpandableInputEdit",
|
||||
props: ['value', 'placeholder', 'maxlength', 'autofocus', 'eventBus'],
|
||||
mounted() {
|
||||
// autofocus on input element is not reliable
|
||||
if (this.$props.autofocus && this.$refs.input) {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
if (this.$props.eventBus) {
|
||||
this.$props.eventBus.$on('focus', () => {
|
||||
this.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
if (this.$refs.input) {
|
||||
(this.$refs.input as HTMLInputElement).focus();
|
||||
}
|
||||
},
|
||||
onInput() {
|
||||
this.$emit('input', (this.$refs.input as HTMLInputElement).value);
|
||||
},
|
||||
onEnter() {
|
||||
this.$emit('enter', (this.$refs.input as HTMLInputElement).value);
|
||||
},
|
||||
onBlur() {
|
||||
this.$emit('blur', (this.$refs.input as HTMLInputElement).value);
|
||||
},
|
||||
onEscape() {
|
||||
this.$emit('esc');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-input input.el-input__inner {
|
||||
border: 1px solid $--color-primary !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<ExpandableInputBase :value="value" :staticSize="true">
|
||||
<template>
|
||||
<input
|
||||
:class="{ 'el-input__inner': true, clickable: true }"
|
||||
:value="value"
|
||||
:disabled="true"
|
||||
size="4"
|
||||
/>
|
||||
</template>
|
||||
</ExpandableInputBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import ExpandableInputBase from "./ExpandableInputBase.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
components: { ExpandableInputBase },
|
||||
name: "ExpandableInputPreview",
|
||||
props: ["value"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
input,
|
||||
input:hover {
|
||||
background-color: unset;
|
||||
transition: unset;
|
||||
pointer-events: none; // fix firefox bug
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: $--custom-font-black;
|
||||
|
||||
// override safari colors
|
||||
-webkit-text-fill-color: $--custom-font-black;
|
||||
-webkit-opacity: 1;
|
||||
}
|
||||
</style>
|
||||
100
packages/editor-ui/src/components/InlineTextEdit.vue
Normal file
100
packages/editor-ui/src/components/InlineTextEdit.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<span @keydown.stop class="inline-edit" >
|
||||
<span v-if="isEditEnabled">
|
||||
<ExpandableInputEdit
|
||||
:placeholder="placeholder"
|
||||
:value="newValue"
|
||||
:maxlength="maxLength"
|
||||
:autofocus="true"
|
||||
:eventBus="inputBus"
|
||||
@input="onInput"
|
||||
@esc="onEscape"
|
||||
@blur="onBlur"
|
||||
@enter="submit"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span @click="onClick" class="preview" v-else>
|
||||
<ExpandableInputPreview
|
||||
:value="previewValue || value"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import ExpandableInputEdit from "@/components/ExpandableInput/ExpandableInputEdit.vue";
|
||||
import ExpandableInputPreview from "@/components/ExpandableInput/ExpandableInputPreview.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "InlineTextEdit",
|
||||
components: { ExpandableInputEdit, ExpandableInputPreview },
|
||||
props: ['isEditEnabled', 'value', 'placeholder', 'maxLength', 'previewValue'],
|
||||
data() {
|
||||
return {
|
||||
newValue: '',
|
||||
escPressed: false,
|
||||
disabled: false,
|
||||
inputBus: new Vue(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onInput(newValue: string) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newValue = newValue;
|
||||
},
|
||||
onClick() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$data.newValue = this.$props.value;
|
||||
this.$emit('toggle');
|
||||
},
|
||||
onBlur() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.$data.escPressed) {
|
||||
this.submit();
|
||||
}
|
||||
this.$data.escPressed = false;
|
||||
},
|
||||
submit() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onSubmit = (updated: boolean) => {
|
||||
this.$data.disabled = false;
|
||||
|
||||
if (!updated) {
|
||||
this.$data.inputBus.$emit('focus');
|
||||
}
|
||||
};
|
||||
|
||||
this.$data.disabled = true;
|
||||
this.$emit('submit', this.newValue, onSubmit);
|
||||
},
|
||||
onEscape() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$data.escPressed = true;
|
||||
this.$emit('toggle');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
30
packages/editor-ui/src/components/IntersectionObserved.vue
Normal file
30
packages/editor-ui/src/components/IntersectionObserved.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<span ref="observed">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import emitter from '@/components/mixins/emitter';
|
||||
|
||||
export default mixins(emitter).extend({
|
||||
name: 'IntersectionObserved',
|
||||
props: ['enabled'],
|
||||
mounted() {
|
||||
if (!this.$props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$dispatch('IntersectionObserver', 'observe', this.$refs.observed);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$props.enabled) {
|
||||
this.$dispatch('IntersectionObserver', 'unobserve', this.$refs.observed);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
56
packages/editor-ui/src/components/IntersectionObserver.vue
Normal file
56
packages/editor-ui/src/components/IntersectionObserver.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div ref="root">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'IntersectionObserver',
|
||||
props: ['threshold', 'enabled'],
|
||||
data() {
|
||||
return {
|
||||
observer: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
root: this.$refs.root as Element,
|
||||
rootMargin: '0px',
|
||||
threshold: this.$props.threshold,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(({target, isIntersecting}) => {
|
||||
this.$emit('observed', {
|
||||
el: target,
|
||||
isIntersecting,
|
||||
});
|
||||
});
|
||||
}, options);
|
||||
|
||||
this.$data.observer = observer;
|
||||
|
||||
this.$on('observe', (observed: Element) => {
|
||||
observer.observe(observed);
|
||||
});
|
||||
|
||||
this.$on('unobserve', (observed: Element) => {
|
||||
observer.unobserve(observed);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$props.enabled) {
|
||||
this.$data.observer.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,288 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="main-header">
|
||||
<input type="file" ref="importFile" style="display: none" v-on:change="handleFileImport()">
|
||||
|
||||
<div class="top-menu">
|
||||
<div class="center-item">
|
||||
<span v-if="isExecutionPage">
|
||||
Execution Id:
|
||||
<span v-if="isExecutionPage" class="execution-name">
|
||||
<strong>{{executionId}}</strong>
|
||||
<font-awesome-icon icon="check" class="execution-icon success" v-if="executionFinished" title="Execution was successful" />
|
||||
<font-awesome-icon icon="times" class="execution-icon error" v-else title="Execution did fail" />
|
||||
</span>
|
||||
of
|
||||
<span class="workflow-name clickable" title="Open Workflow">
|
||||
<span @click="openWorkflow(workflowExecution.workflowId)">"{{workflowName}}"</span>
|
||||
</span>
|
||||
workflow
|
||||
</span>
|
||||
<span index="workflow-name" class="current-workflow" v-if="!isReadOnly">
|
||||
<span v-if="currentWorkflow">Workflow: <span class="workflow-name">{{workflowName}}<span v-if="isDirty">*</span></span></span>
|
||||
<span v-else class="workflow-not-saved">Workflow was not saved!</span>
|
||||
</span>
|
||||
|
||||
<span class="saving-workflow" v-if="isWorkflowSaving">
|
||||
<font-awesome-icon icon="spinner" spin />
|
||||
Saving...
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="push-connection-lost" v-if="!isPushConnectionActive">
|
||||
<el-tooltip placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
Cannot connect to server.<br />
|
||||
It is either down or you have a connection issue. <br />
|
||||
It should reconnect automatically once the issue is resolved.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
Connection lost
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="workflow-active" v-else-if="!isReadOnly">
|
||||
Active:
|
||||
<workflow-activator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflow" :disabled="!currentWorkflow"/>
|
||||
</div>
|
||||
|
||||
<div class="read-only" v-if="isReadOnly">
|
||||
<el-tooltip placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
You're viewing the log of a previous execution. You cannot<br />
|
||||
make changes since this execution already occured. Make changes<br /> to this workflow by clicking on it`s name on the left.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
Read only
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import {
|
||||
IExecutionResponse,
|
||||
IExecutionsStopData,
|
||||
IWorkflowDataUpdate,
|
||||
} from '../Interface';
|
||||
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { pushConnection } from '@/components/mixins/pushConnection';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
pushConnection,
|
||||
restApi,
|
||||
showMessage,
|
||||
titleChange,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'MainHeader',
|
||||
components: {
|
||||
WorkflowActivator,
|
||||
},
|
||||
computed: {
|
||||
executionId (): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
executionFinished (): boolean {
|
||||
if (!this.isExecutionPage) {
|
||||
// We are not on an execution page so return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const fullExecution = this.$store.getters.getWorkflowExecution;
|
||||
|
||||
if (fullExecution === null) {
|
||||
// No execution loaded so return also false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fullExecution.finished === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isExecutionPage (): boolean {
|
||||
if (['ExecutionById'].includes(this.$route.name as string)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isPushConnectionActive (): boolean {
|
||||
return this.$store.getters.pushConnectionActive;
|
||||
},
|
||||
isWorkflowActive (): boolean {
|
||||
return this.$store.getters.isActive;
|
||||
},
|
||||
isWorkflowSaving (): boolean {
|
||||
return this.$store.getters.isActionActive('workflowSaving');
|
||||
},
|
||||
currentWorkflow (): string {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
workflowExecution (): IExecutionResponse | null {
|
||||
return this.$store.getters.getWorkflowExecution;
|
||||
},
|
||||
workflowName (): string {
|
||||
return this.$store.getters.workflowName;
|
||||
},
|
||||
workflowRunning (): boolean {
|
||||
return this.$store.getters.isActionActive('workflowRunning');
|
||||
},
|
||||
isDirty () : boolean {
|
||||
return this.$store.getters.getStateIsDirty;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async openWorkflow (workflowId: string) {
|
||||
this.$titleSet(this.workflowName, 'IDLE');
|
||||
// Change to other workflow
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowId },
|
||||
});
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
// Initialize the push connection
|
||||
this.pushConnect();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.pushDisconnect();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu--horizontal>.el-submenu .el-submenu__title,
|
||||
.el-menu-item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
}
|
||||
|
||||
.el-submenu .el-submenu__title,
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu.el-menu--horizontal {
|
||||
border: none !important;
|
||||
}
|
||||
.el-menu--popup-bottom-start {
|
||||
margin-top: 0px;
|
||||
border-top: 1px solid #464646;
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
height: 65px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
position: relative;
|
||||
font-size: 0.9em;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
|
||||
.center-item {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
line-height: 65px;
|
||||
|
||||
.saving-workflow {
|
||||
display: inline-block;
|
||||
margin-left: 2em;
|
||||
padding: 0 15px;
|
||||
color: $--color-primary;
|
||||
background-color: $--color-primary-light;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.read-only {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
line-height: 65px;
|
||||
margin-right: 5em;
|
||||
right: 0;
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.push-connection-lost {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
line-height: 65px;
|
||||
margin-right: 5em;
|
||||
right: 0;
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.workflow-active {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
line-height: 65px;
|
||||
margin-right: 5em;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.workflow-name {
|
||||
color: $--color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.current-execution,
|
||||
.current-workflow {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.execution-icon.error,
|
||||
.workflow-not-saved {
|
||||
color: #FF2244;
|
||||
}
|
||||
|
||||
.execution-icon.success {
|
||||
color: #22FF44;
|
||||
}
|
||||
|
||||
.menu-separator-bottom {
|
||||
border-bottom: 1px solid #707070;
|
||||
}
|
||||
|
||||
.menu-separator-top {
|
||||
border-top: 1px solid #707070;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<span class="title">
|
||||
Execution Id:
|
||||
<span>
|
||||
<strong>{{ executionId }}</strong
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="check"
|
||||
class="execution-icon success"
|
||||
v-if="executionFinished"
|
||||
title="Execution was successful"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="times"
|
||||
class="execution-icon error"
|
||||
v-else
|
||||
title="Execution failed"
|
||||
/>
|
||||
</span>
|
||||
of
|
||||
<span class="primary-color clickable" title="Open Workflow">
|
||||
<WorkflowNameShort :name="workflowName">
|
||||
<template v-slot="{ shortenedName }">
|
||||
<span @click="openWorkflow(workflowExecution.workflowId)">
|
||||
"{{ shortenedName }}"
|
||||
</span>
|
||||
</template>
|
||||
</WorkflowNameShort>
|
||||
</span>
|
||||
workflow
|
||||
</span>
|
||||
<ReadOnly class="read-only" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from "vue-typed-mixins";
|
||||
|
||||
import { IExecutionResponse } from "../../../Interface";
|
||||
|
||||
import { titleChange } from "@/components/mixins/titleChange";
|
||||
|
||||
import WorkflowNameShort from "@/components/WorkflowNameShort.vue";
|
||||
import ReadOnly from "@/components/MainHeader/ExecutionDetails/ReadOnly.vue";
|
||||
|
||||
export default mixins(titleChange).extend({
|
||||
name: "ExecutionDetails",
|
||||
components: {
|
||||
WorkflowNameShort,
|
||||
ReadOnly,
|
||||
},
|
||||
computed: {
|
||||
executionId(): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
executionFinished(): boolean {
|
||||
const fullExecution = this.$store.getters.getWorkflowExecution;
|
||||
|
||||
return !!fullExecution && fullExecution.finished;
|
||||
},
|
||||
workflowExecution(): IExecutionResponse | null {
|
||||
return this.$store.getters.getWorkflowExecution;
|
||||
},
|
||||
workflowName(): string {
|
||||
return this.$store.getters.workflowName;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async openWorkflow(workflowId: string) {
|
||||
this.$titleSet(this.workflowName, "IDLE");
|
||||
// Change to other workflow
|
||||
this.$router.push({
|
||||
name: "NodeViewExisting",
|
||||
params: { name: workflowId },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.execution-icon.success {
|
||||
color: $--custom-success-text-light;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.read-only {
|
||||
align-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-tooltip class="primary-color" placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
You're viewing the log of a previous execution. You cannot<br />
|
||||
make changes since this execution already occured. Make changes<br />
|
||||
to this workflow by clicking on its name on the left.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
Read only
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
94
packages/editor-ui/src/components/MainHeader/MainHeader.vue
Normal file
94
packages/editor-ui/src/components/MainHeader/MainHeader.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="{'main-header': true, expanded: !sidebarMenuCollapsed}">
|
||||
<div class="top-menu">
|
||||
<ExecutionDetails v-if="isExecutionPage" />
|
||||
<WorkflowDetails v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { pushConnection } from '@/components/mixins/pushConnection';
|
||||
|
||||
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
|
||||
import ExecutionDetails from '@/components/MainHeader/ExecutionDetails/ExecutionDetails.vue';
|
||||
|
||||
export default mixins(
|
||||
pushConnection,
|
||||
)
|
||||
.extend({
|
||||
name: 'MainHeader',
|
||||
components: {
|
||||
WorkflowDetails,
|
||||
ExecutionDetails,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('ui', [
|
||||
'sidebarMenuCollapsed',
|
||||
]),
|
||||
isExecutionPage (): boolean {
|
||||
return ['ExecutionById'].includes(this.$route.name as string);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
// Initialize the push connection
|
||||
this.pushConnect();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pushDisconnect();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu--horizontal>.el-submenu .el-submenu__title,
|
||||
.el-menu-item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
}
|
||||
|
||||
.el-submenu .el-submenu__title,
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu.el-menu--horizontal {
|
||||
border: none !important;
|
||||
}
|
||||
.el-menu--popup-bottom-start {
|
||||
margin-top: 0px;
|
||||
border-top: 1px solid #464646;
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
height: 65px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-left: $--sidebar-width;
|
||||
|
||||
&.expanded {
|
||||
padding-left: $--sidebar-expanded-width;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
height: $--header-height;
|
||||
font-weight: 400;
|
||||
padding: 0 20px;
|
||||
}
|
||||
</style>
|
||||
279
packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue
Normal file
279
packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="container" v-if="workflowName">
|
||||
<BreakpointsObserver :valueXS="15" :valueSM="25" :valueMD="50" class="name-container">
|
||||
<template v-slot="{ value }">
|
||||
<WorkflowNameShort
|
||||
:name="workflowName"
|
||||
:limit="value"
|
||||
:custom="true"
|
||||
>
|
||||
<template v-slot="{ shortenedName }">
|
||||
<InlineTextEdit
|
||||
:value="workflowName"
|
||||
:previewValue="shortenedName"
|
||||
:isEditEnabled="isNameEditEnabled"
|
||||
:maxLength="MAX_WORKFLOW_NAME_LENGTH"
|
||||
@toggle="onNameToggle"
|
||||
@submit="onNameSubmit"
|
||||
placeholder="Enter workflow name"
|
||||
class="name"
|
||||
/>
|
||||
</template>
|
||||
</WorkflowNameShort>
|
||||
</template>
|
||||
</BreakpointsObserver>
|
||||
|
||||
<div
|
||||
v-if="isTagsEditEnabled"
|
||||
class="tags">
|
||||
<TagsDropdown
|
||||
:createEnabled="true"
|
||||
:currentTagIds="appliedTagIds"
|
||||
:eventBus="tagsEditBus"
|
||||
@blur="onTagsBlur"
|
||||
@update="onTagsUpdate"
|
||||
@esc="onTagsEditEsc"
|
||||
placeholder="Choose or create a tag"
|
||||
ref="dropdown"
|
||||
class="tags-edit"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tags"
|
||||
v-else-if="currentWorkflowTagIds.length === 0"
|
||||
>
|
||||
<span
|
||||
class="add-tag clickable"
|
||||
@click="onTagsEditEnable"
|
||||
>
|
||||
+ Add tag
|
||||
</span>
|
||||
</div>
|
||||
<TagsContainer
|
||||
v-else
|
||||
:tagIds="currentWorkflowTagIds"
|
||||
:clickable="true"
|
||||
:responsive="true"
|
||||
:key="currentWorkflowId"
|
||||
@click="onTagsEditEnable"
|
||||
class="tags"
|
||||
/>
|
||||
|
||||
<PushConnectionTracker class="actions">
|
||||
<template>
|
||||
<span class="activator">
|
||||
<span>Active:</span>
|
||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" :disabled="!currentWorkflowId"/>
|
||||
</span>
|
||||
<SaveWorkflowButton />
|
||||
</template>
|
||||
</PushConnectionTracker>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants";
|
||||
|
||||
import WorkflowNameShort from "@/components/WorkflowNameShort.vue";
|
||||
import TagsContainer from "@/components/TagsContainer.vue";
|
||||
import PushConnectionTracker from "@/components/PushConnectionTracker.vue";
|
||||
import WorkflowActivator from "@/components/WorkflowActivator.vue";
|
||||
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||
import SaveWorkflowButton from "@/components/SaveWorkflowButton.vue";
|
||||
import TagsDropdown from "@/components/TagsDropdown.vue";
|
||||
import InlineTextEdit from "@/components/InlineTextEdit.vue";
|
||||
import BreakpointsObserver from "@/components/BreakpointsObserver.vue";
|
||||
|
||||
const hasChanged = (prev: string[], curr: string[]) => {
|
||||
if (prev.length !== curr.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const set = new Set(prev);
|
||||
return curr.reduce((accu, val) => accu || !set.has(val), false);
|
||||
};
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: "WorkflowDetails",
|
||||
components: {
|
||||
TagsContainer,
|
||||
PushConnectionTracker,
|
||||
WorkflowNameShort,
|
||||
WorkflowActivator,
|
||||
SaveWorkflowButton,
|
||||
TagsDropdown,
|
||||
InlineTextEdit,
|
||||
BreakpointsObserver,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isTagsEditEnabled: false,
|
||||
isNameEditEnabled: false,
|
||||
appliedTagIds: [],
|
||||
tagsEditBus: new Vue(),
|
||||
MAX_WORKFLOW_NAME_LENGTH,
|
||||
tagsSaving: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isWorkflowActive: "isActive",
|
||||
workflowName: "workflowName",
|
||||
isDirty: "getStateIsDirty",
|
||||
currentWorkflowTagIds: "workflowTags",
|
||||
}),
|
||||
isWorkflowSaving(): boolean {
|
||||
return this.$store.getters.isActionActive("workflowSaving");
|
||||
},
|
||||
currentWorkflowId() {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTagsEditEnable() {
|
||||
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
||||
this.$data.isTagsEditEnabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
// allow name update to occur before disabling name edit
|
||||
this.$data.isNameEditEnabled = false;
|
||||
this.$data.tagsEditBus.$emit('focus');
|
||||
}, 0);
|
||||
},
|
||||
async onTagsUpdate(tags: string[]) {
|
||||
this.$data.appliedTagIds = tags;
|
||||
},
|
||||
|
||||
async onTagsBlur() {
|
||||
const current = this.currentWorkflowTagIds;
|
||||
const tags = this.$data.appliedTagIds;
|
||||
if (!hasChanged(current, tags)) {
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if (this.$data.tagsSaving) {
|
||||
return;
|
||||
}
|
||||
this.$data.tagsSaving = true;
|
||||
|
||||
const saved = await this.saveCurrentWorkflow({ tags });
|
||||
this.$data.tagsSaving = false;
|
||||
if (saved) {
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
}
|
||||
},
|
||||
onTagsEditEsc() {
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
},
|
||||
onNameToggle() {
|
||||
this.$data.isNameEditEnabled = !this.$data.isNameEditEnabled;
|
||||
if (this.$data.isNameEditEnabled) {
|
||||
if (this.$data.isTagsEditEnabled) {
|
||||
// @ts-ignore
|
||||
this.onTagsBlur();
|
||||
}
|
||||
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
}
|
||||
},
|
||||
async onNameSubmit(name: string, cb: (saved: boolean) => void) {
|
||||
const newName = name.trim();
|
||||
if (!newName) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name, or press 'esc' to go back to the old one.`,
|
||||
type: "error",
|
||||
});
|
||||
|
||||
cb(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newName === this.workflowName) {
|
||||
this.$data.isNameEditEnabled = false;
|
||||
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = await this.saveCurrentWorkflow({ name });
|
||||
if (saved) {
|
||||
this.$data.isNameEditEnabled = false;
|
||||
}
|
||||
cb(saved);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentWorkflowId() {
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
this.$data.isNameEditEnabled = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$--text-line-height: 24px;
|
||||
$--header-spacing: 20px;
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name-container {
|
||||
margin-right: $--header-spacing;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: $--custom-font-dark;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.activator {
|
||||
color: $--custom-font-dark;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: $--text-line-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30px;
|
||||
|
||||
> span {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-tag {
|
||||
font-size: 12px;
|
||||
padding: 20px 0; // to be more clickable
|
||||
color: $--custom-font-very-light;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $--color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
flex: 1;
|
||||
padding-right: 20px;
|
||||
margin-right: $--header-spacing;
|
||||
}
|
||||
|
||||
.tags-edit {
|
||||
min-width: 100px;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,11 @@
|
||||
<executions-list :dialogVisible="executionsListDialogVisible" @closeDialog="closeExecutionsListOpenDialog"></executions-list>
|
||||
<credentials-list :dialogVisible="credentialOpenDialogVisible" @closeDialog="closeCredentialOpenDialog"></credentials-list>
|
||||
<credentials-edit :dialogVisible="credentialNewDialogVisible" @closeDialog="closeCredentialNewDialog"></credentials-edit>
|
||||
<workflow-open @openWorkflow="openWorkflow" :dialogVisible="workflowOpenDialogVisible" @closeDialog="closeWorkflowOpenDialog"></workflow-open>
|
||||
<workflow-settings :dialogVisible="workflowSettingsDialogVisible" @closeDialog="closeWorkflowSettingsDialog"></workflow-settings>
|
||||
<input type="file" ref="importFile" style="display: none" v-on:change="handleFileImport()">
|
||||
|
||||
<div class="side-menu-wrapper" :class="{expanded: !isCollapsed}">
|
||||
<div id="collapse-change-button" class="clickable" @click="isCollapsed=!isCollapsed">
|
||||
<div id="collapse-change-button" class="clickable" @click="toggleCollapse">
|
||||
<font-awesome-icon icon="angle-right" class="icon" />
|
||||
</div>
|
||||
<el-menu default-active="workflow" @select="handleSelect" :collapse="isCollapsed">
|
||||
@@ -41,22 +40,16 @@
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-save" :disabled="!currentWorkflow">
|
||||
<el-menu-item index="workflow-save">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="save"/>
|
||||
<span slot="title" class="item-title">Save</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-save-as">
|
||||
<el-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="copy"/>
|
||||
<span slot="title" class="item-title">Save As</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-rename" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="edit"/>
|
||||
<span slot="title" class="item-title">Rename</span>
|
||||
<span slot="title" class="item-title">Duplicate</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
@@ -143,7 +136,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||
|
||||
import {
|
||||
@@ -157,7 +149,6 @@ import About from '@/components/About.vue';
|
||||
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
||||
import CredentialsList from '@/components/CredentialsList.vue';
|
||||
import ExecutionsList from '@/components/ExecutionsList.vue';
|
||||
import WorkflowOpen from '@/components/WorkflowOpen.vue';
|
||||
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
@@ -170,6 +161,7 @@ import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
||||
|
||||
const helpMenuItems: IMenuItem[] = [
|
||||
@@ -220,7 +212,6 @@ export default mixins(
|
||||
CredentialsEdit,
|
||||
CredentialsList,
|
||||
ExecutionsList,
|
||||
WorkflowOpen,
|
||||
WorkflowSettings,
|
||||
MenuItemsIterator,
|
||||
},
|
||||
@@ -229,17 +220,18 @@ export default mixins(
|
||||
aboutDialogVisible: false,
|
||||
// @ts-ignore
|
||||
basePath: this.$store.getters.getBaseUrl,
|
||||
isCollapsed: true,
|
||||
credentialNewDialogVisible: false,
|
||||
credentialOpenDialogVisible: false,
|
||||
executionsListDialogVisible: false,
|
||||
stopExecutionInProgress: false,
|
||||
workflowOpenDialogVisible: false,
|
||||
workflowSettingsDialogVisible: false,
|
||||
helpMenuItems,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('ui', {
|
||||
isCollapsed: 'sidebarMenuCollapsed',
|
||||
}),
|
||||
exeuctionId (): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
@@ -294,6 +286,9 @@ export default mixins(
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse () {
|
||||
this.$store.commit('ui/toggleSidebarMenuCollapse');
|
||||
},
|
||||
clearExecutionData () {
|
||||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
this.updateNodesExecutionIssues();
|
||||
@@ -301,9 +296,6 @@ export default mixins(
|
||||
closeAboutDialog () {
|
||||
this.aboutDialogVisible = false;
|
||||
},
|
||||
closeWorkflowOpenDialog () {
|
||||
this.workflowOpenDialogVisible = false;
|
||||
},
|
||||
closeWorkflowSettingsDialog () {
|
||||
this.workflowSettingsDialogVisible = false;
|
||||
},
|
||||
@@ -316,6 +308,9 @@ export default mixins(
|
||||
closeCredentialNewDialog () {
|
||||
this.credentialNewDialogVisible = false;
|
||||
},
|
||||
openTagManager() {
|
||||
this.$store.dispatch('ui/openTagsManagerModal');
|
||||
},
|
||||
async stopExecution () {
|
||||
const executionId = this.$store.getters.activeExecutionId;
|
||||
if (executionId === null) {
|
||||
@@ -342,7 +337,7 @@ export default mixins(
|
||||
params: { name: workflowId },
|
||||
});
|
||||
|
||||
this.workflowOpenDialogVisible = false;
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
},
|
||||
async handleFileImport () {
|
||||
const reader = new FileReader();
|
||||
@@ -372,7 +367,7 @@ export default mixins(
|
||||
},
|
||||
async handleSelect (key: string, keyPath: string) {
|
||||
if (key === 'workflow-open') {
|
||||
this.workflowOpenDialogVisible = true;
|
||||
this.$store.dispatch('ui/openWorklfowOpenModal');
|
||||
} else if (key === 'workflow-import-file') {
|
||||
(this.$refs.importFile as HTMLInputElement).click();
|
||||
} else if (key === 'workflow-import-url') {
|
||||
@@ -386,49 +381,6 @@ export default mixins(
|
||||
|
||||
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
|
||||
} catch (e) {}
|
||||
} else if (key === 'workflow-rename') {
|
||||
const workflowName = await this.$prompt(
|
||||
'Enter new workflow name',
|
||||
'Rename',
|
||||
{
|
||||
inputValue: this.workflowName,
|
||||
confirmButtonText: 'Rename',
|
||||
cancelButtonText: 'Cancel',
|
||||
},
|
||||
)
|
||||
.then((data) => {
|
||||
// @ts-ignore
|
||||
return data.value;
|
||||
})
|
||||
.catch(() => {
|
||||
// User did cancel
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (workflowName === undefined || workflowName === this.workflowName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowId = this.$store.getters.workflowId;
|
||||
|
||||
const updateData = {
|
||||
name: workflowName,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.restApi().updateWorkflow(workflowId, updateData);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem renaming the workflow', 'There was a problem renaming the workflow:');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: false});
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow renamed',
|
||||
message: `The workflow got renamed to "${workflowName}"!`,
|
||||
type: 'success',
|
||||
});
|
||||
} else if (key === 'workflow-delete') {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the workflow "${this.workflowName}"?`, 'Delete Workflow?', 'warning', 'Yes, delete!');
|
||||
|
||||
@@ -454,7 +406,9 @@ export default mixins(
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
} else if (key === 'workflow-download') {
|
||||
const workflowData = await this.getWorkflowDataToSave();
|
||||
const blob = new Blob([JSON.stringify(workflowData, null, 2)], {
|
||||
|
||||
const {tags, ...data} = workflowData;
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
|
||||
@@ -465,8 +419,8 @@ export default mixins(
|
||||
saveAs(blob, workflowName + '.json');
|
||||
} else if (key === 'workflow-save') {
|
||||
this.saveCurrentWorkflow();
|
||||
} else if (key === 'workflow-save-as') {
|
||||
this.saveCurrentWorkflow(true);
|
||||
} else if (key === 'workflow-duplicate') {
|
||||
this.$store.dispatch('ui/openDuplicateModal');
|
||||
} else if (key === 'help-about') {
|
||||
this.aboutDialogVisible = true;
|
||||
} else if (key === 'workflow-settings') {
|
||||
@@ -508,11 +462,6 @@ export default mixins(
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.$root.$on('openWorkflowDialog', async () => {
|
||||
this.workflowOpenDialogVisible = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -568,7 +517,7 @@ export default mixins(
|
||||
|
||||
&.logo-item {
|
||||
background-color: $--color-primary !important;
|
||||
height: 65px;
|
||||
height: $--header-height;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
@@ -610,10 +559,10 @@ a.logo {
|
||||
|
||||
.side-menu-wrapper {
|
||||
height: 100%;
|
||||
width: 65px;
|
||||
width: $--sidebar-width;
|
||||
|
||||
&.expanded {
|
||||
width: 200px;
|
||||
width: $--sidebar-expanded-width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
115
packages/editor-ui/src/components/Modal.vue
Normal file
115
packages/editor-ui/src/components/Modal.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<el-dialog
|
||||
:visible="dialogVisible"
|
||||
:before-close="closeDialog"
|
||||
:title="title"
|
||||
:class="{ 'dialog-wrapper': true, [size]: true }"
|
||||
:width="width"
|
||||
append-to-body
|
||||
>
|
||||
<template v-slot:title>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<div class="modal-content" @keydown.stop @keydown.enter="handleEnter" @keydown.esc="closeDialog">
|
||||
<slot name="content"/>
|
||||
</div>
|
||||
<el-row class="modal-footer">
|
||||
<slot name="footer" :close="closeDialog" />
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
const sizeMap: {[size: string]: string} = {
|
||||
xl: '80%',
|
||||
m: '50%',
|
||||
default: '50%',
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modal",
|
||||
props: ['name', 'title', 'eventBus', 'size'],
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.onWindowKeydown);
|
||||
|
||||
if (this.$props.eventBus) {
|
||||
this.$props.eventBus.$on('close', () => {
|
||||
this.closeDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keydown', this.onWindowKeydown);
|
||||
},
|
||||
methods: {
|
||||
onWindowKeydown(event: KeyboardEvent) {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && event.keyCode === 13) {
|
||||
this.handleEnter();
|
||||
}
|
||||
},
|
||||
handleEnter() {
|
||||
if (this.isActive) {
|
||||
this.$emit('enter');
|
||||
}
|
||||
},
|
||||
closeDialog() {
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
width(): string {
|
||||
return this.$props.size ? sizeMap[this.$props.size] : sizeMap.default;
|
||||
},
|
||||
isActive(): boolean {
|
||||
return this.$store.getters['ui/isModalActive'](this.$props.name);
|
||||
},
|
||||
dialogVisible(): boolean {
|
||||
return this.$store.getters['ui/isModalOpen'](this.$props.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dialog-wrapper {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.xl > div, &.md > div {
|
||||
min-width: 620px;
|
||||
}
|
||||
|
||||
&.sm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content > .el-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-footer > .el-button {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
24
packages/editor-ui/src/components/ModalRoot.vue
Normal file
24
packages/editor-ui/src/components/ModalRoot.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isOpen(name)"
|
||||
>
|
||||
<slot :modalName="name" :active="isActive(name)"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ModalRoot",
|
||||
props: ["name"],
|
||||
methods: {
|
||||
isActive(name: string) {
|
||||
return this.$store.getters['ui/isModalActive'](name);
|
||||
},
|
||||
isOpen(name: string) {
|
||||
return this.$store.getters['ui/isModalOpen'](name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
53
packages/editor-ui/src/components/Modals.vue
Normal file
53
packages/editor-ui/src/components/Modals.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalRoot :name="DUPLICATE_MODAL_KEY">
|
||||
<template v-slot:default="{ modalName, active }">
|
||||
<DuplicateWorkflowDialog
|
||||
:isActive="active"
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="TAGS_MANAGER_MODAL_KEY">
|
||||
<template v-slot="{ modalName }">
|
||||
<TagsManager
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="WORKLOW_OPEN_MODAL_KEY">
|
||||
<template v-slot="{ modalName }">
|
||||
<WorkflowOpen
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
|
||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
||||
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
||||
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
||||
import ModalRoot from "./ModalRoot.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modals",
|
||||
components: {
|
||||
TagsManager,
|
||||
DuplicateWorkflowDialog,
|
||||
WorkflowOpen,
|
||||
ModalRoot,
|
||||
},
|
||||
data: () => ({
|
||||
DUPLICATE_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
WORKLOW_OPEN_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
29
packages/editor-ui/src/components/PushConnectionTracker.vue
Normal file
29
packages/editor-ui/src/components/PushConnectionTracker.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<span>
|
||||
<div class="push-connection-lost primary-color" v-if="!pushConnectionActive">
|
||||
<el-tooltip placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
Cannot connect to server.<br />
|
||||
It is either down or you have a connection issue. <br />
|
||||
It should reconnect automatically once the issue is resolved.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" /> Connection lost
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<slot v-else />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "PushConnectionTracker",
|
||||
computed: {
|
||||
...mapGetters(["pushConnectionActive"]),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -762,7 +762,7 @@ export default mixins(
|
||||
background: #fff;;
|
||||
}
|
||||
tr:nth-child(odd) {
|
||||
background: $--custom-table-background-alternative;
|
||||
background: $--custom-table-background-stripe-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
packages/editor-ui/src/components/SaveWorkflowButton.vue
Normal file
65
packages/editor-ui/src/components/SaveWorkflowButton.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<el-button :disabled="isWorkflowSaving" :class="{saved: isSaved}" size="small" @click="save">
|
||||
<font-awesome-icon v-if="isWorkflowSaving" icon="spinner" spin />
|
||||
<span v-else-if="isDirty || isNewWorkflow">
|
||||
Save
|
||||
</span>
|
||||
<span v-else>Saved</span>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: "SaveWorkflowButton",
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isDirty: "getStateIsDirty",
|
||||
}),
|
||||
isWorkflowSaving(): boolean {
|
||||
return this.$store.getters.isActionActive("workflowSaving");
|
||||
},
|
||||
isNewWorkflow(): boolean {
|
||||
return !this.$route.params.name;
|
||||
},
|
||||
isSaved(): boolean {
|
||||
return !this.isWorkflowSaving && !this.isDirty && !this.isNewWorkflow;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.saveCurrentWorkflow();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-button {
|
||||
width: 65px;
|
||||
|
||||
// override disabled colors
|
||||
color: white;
|
||||
background-color: $--color-primary;
|
||||
|
||||
&:hover:not(.saved) {
|
||||
color: white;
|
||||
background-color: $--color-primary;
|
||||
}
|
||||
|
||||
&.saved {
|
||||
color: $--custom-font-very-light;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
background-color: unset;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
packages/editor-ui/src/components/TagsContainer.vue
Normal file
151
packages/editor-ui/src/components/TagsContainer.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<IntersectionObserver :threshold="1.0" @observed="onObserved" class="tags-container" :enabled="responsive">
|
||||
<template>
|
||||
<span class="tags">
|
||||
<span
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
:class="{clickable: !tag.hidden}"
|
||||
@click="(e) => onClick(e, tag)"
|
||||
>
|
||||
<el-tag
|
||||
:title="tag.title"
|
||||
type="info"
|
||||
size="small"
|
||||
v-if="tag.isCount"
|
||||
class="count-container"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</el-tag>
|
||||
<IntersectionObserved
|
||||
:class="{hidden: tag.hidden}"
|
||||
:data-id="tag.id"
|
||||
:enabled="responsive"
|
||||
v-else
|
||||
>
|
||||
<el-tag
|
||||
:title="tag.name"
|
||||
type="info"
|
||||
size="small"
|
||||
:class="{hoverable}"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</el-tag>
|
||||
</IntersectionObserved>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</IntersectionObserver>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { ITag } from '@/Interface';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
import IntersectionObserved from './IntersectionObserved.vue';
|
||||
|
||||
// random upper limit if none is set to minimize performance impact of observers
|
||||
const DEFAULT_MAX_TAGS_LIMIT = 20;
|
||||
|
||||
interface TagEl extends ITag {
|
||||
hidden?: boolean;
|
||||
title?: string;
|
||||
isCount?: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: { IntersectionObserver, IntersectionObserved },
|
||||
name: 'TagsContainer',
|
||||
props: [
|
||||
"tagIds",
|
||||
"limit",
|
||||
"clickable",
|
||||
"responsive",
|
||||
"hoverable",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
visibility: {} as {[id: string]: boolean},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tags() {
|
||||
const tags = this.$props.tagIds.map((tagId: string) => this.$store.getters['tags/getTagById'](tagId))
|
||||
.filter(Boolean); // if tag has been deleted from store
|
||||
|
||||
const limit = this.$props.limit || DEFAULT_MAX_TAGS_LIMIT;
|
||||
|
||||
let toDisplay: TagEl[] = limit ? tags.slice(0, limit) : tags;
|
||||
toDisplay = toDisplay.map((tag: ITag) => ({...tag, hidden: this.$props.responsive && !this.$data.visibility[tag.id]}));
|
||||
|
||||
let visibleCount = toDisplay.length;
|
||||
if (this.$props.responsive) {
|
||||
visibleCount = Object.values(this.visibility).reduce((accu, val) => val ? accu + 1 : accu, 0);
|
||||
}
|
||||
|
||||
if (visibleCount < tags.length) {
|
||||
const hidden = tags.slice(visibleCount);
|
||||
const hiddenTitle = hidden.reduce((accu: string, tag: ITag) => {
|
||||
return accu ? `${accu}, ${tag.name}` : tag.name;
|
||||
}, '');
|
||||
|
||||
const countTag: TagEl = {
|
||||
id: 'count',
|
||||
name: `+${hidden.length}`,
|
||||
title: hiddenTitle,
|
||||
isCount: true,
|
||||
};
|
||||
toDisplay.splice(visibleCount, 0, countTag);
|
||||
}
|
||||
|
||||
return toDisplay;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onObserved({el, isIntersecting}: {el: HTMLElement, isIntersecting: boolean}) {
|
||||
if (el.dataset.id) {
|
||||
Vue.set(this.$data.visibility, el.dataset.id, isIntersecting);
|
||||
}
|
||||
},
|
||||
onClick(e: MouseEvent, tag: TagEl) {
|
||||
e.stopPropagation();
|
||||
|
||||
// if tag is hidden or not displayed
|
||||
if (!tag.hidden) {
|
||||
this.$emit('click', tag.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
padding-right: 4px; // why not margin? for space between tags to be clickable
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.el-tag.hoverable:hover {
|
||||
border-color: $--color-primary;
|
||||
}
|
||||
|
||||
.count-container {
|
||||
position: absolute;
|
||||
max-width: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
369
packages/editor-ui/src/components/TagsDropdown.vue
Normal file
369
packages/editor-ui/src/components/TagsDropdown.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div :class="{'tags-container': true, focused}" @keydown.stop v-click-outside="onBlur">
|
||||
<el-select
|
||||
:popperAppendToBody="false"
|
||||
:value="appliedTags"
|
||||
:loading="isLoading"
|
||||
:placeholder="placeholder"
|
||||
:filter-method="filterOptions"
|
||||
@change="onTagsUpdated"
|
||||
@visible-change="onVisibleChange"
|
||||
@remove-tag="onRemoveTag"
|
||||
filterable
|
||||
multiple
|
||||
ref="select"
|
||||
loading-text="..."
|
||||
popper-class="tags-dropdown"
|
||||
>
|
||||
<el-option
|
||||
v-if="options.length === 0 && filter && createEnabled"
|
||||
:key="CREATE_KEY"
|
||||
:value="CREATE_KEY"
|
||||
class="ops"
|
||||
ref="create"
|
||||
>
|
||||
<font-awesome-icon icon="plus-circle" />
|
||||
<span>Create tag "{{ filter }}"</span>
|
||||
</el-option>
|
||||
<el-option v-else-if="options.length === 0" value="message" disabled>
|
||||
<span v-if="createEnabled">Type to create a tag</span>
|
||||
<span v-else-if="allTags.length > 0">No matching tags exist</span>
|
||||
<span v-else>No tags exist</span>
|
||||
</el-option>
|
||||
|
||||
<!-- key is id+index for keyboard navigation to work well with filter -->
|
||||
<el-option
|
||||
v-for="(tag, i) in options"
|
||||
:value="tag.id"
|
||||
:key="tag.id + '_' + i"
|
||||
:label="tag.name"
|
||||
class="tag"
|
||||
ref="tag"
|
||||
/>
|
||||
|
||||
<el-option :key="MANAGE_KEY" :value="MANAGE_KEY" class="ops manage-tags">
|
||||
<font-awesome-icon icon="cog" />
|
||||
<span>Manage tags</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import { ITag } from "@/Interface";
|
||||
import { MAX_TAG_NAME_LENGTH } from "@/constants";
|
||||
|
||||
import { showMessage } from "@/components/mixins/showMessage";
|
||||
|
||||
const MANAGE_KEY = "__manage";
|
||||
const CREATE_KEY = "__create";
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: "TagsDropdown",
|
||||
props: ["placeholder", "currentTagIds", "createEnabled", "eventBus"],
|
||||
data() {
|
||||
return {
|
||||
filter: "",
|
||||
MANAGE_KEY,
|
||||
CREATE_KEY,
|
||||
focused: false,
|
||||
preventUpdate: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const select = this.$refs.select as (Vue | undefined);
|
||||
if (select) {
|
||||
const input = select.$refs.input as (Element | undefined);
|
||||
if (input) {
|
||||
input.setAttribute('maxlength', `${MAX_TAG_NAME_LENGTH}`);
|
||||
input.addEventListener('keydown', (e: Event) => {
|
||||
const keyboardEvent = e as KeyboardEvent;
|
||||
// events don't bubble outside of select, so need to hook onto input
|
||||
if (keyboardEvent.key === 'Escape') {
|
||||
this.$emit('esc');
|
||||
}
|
||||
else if (keyboardEvent.key === 'Enter' && this.filter.length === 0) {
|
||||
this.$data.preventUpdate = true;
|
||||
this.$emit('blur');
|
||||
|
||||
// @ts-ignore
|
||||
if (this.$refs.select && typeof this.$refs.select.blur === 'function') {
|
||||
// @ts-ignore
|
||||
this.$refs.select.blur();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$props.eventBus) {
|
||||
this.$props.eventBus.$on('focus', () => {
|
||||
this.focusOnInput();
|
||||
this.focusOnTopOption();
|
||||
});
|
||||
}
|
||||
|
||||
this.$store.dispatch("tags/fetchAll");
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("tags", ["allTags", "isLoading", "hasTags"]),
|
||||
options(): ITag[] {
|
||||
return this.allTags
|
||||
.filter((tag: ITag) =>
|
||||
tag && tag.name.toLowerCase().includes(this.$data.filter.toLowerCase()),
|
||||
);
|
||||
},
|
||||
appliedTags(): string[] {
|
||||
return this.$props.currentTagIds.filter((id: string) =>
|
||||
this.$store.getters['tags/getTagById'](id),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filterOptions(filter = "") {
|
||||
this.$data.filter = filter.trim();
|
||||
this.$nextTick(() => this.focusOnTopOption());
|
||||
},
|
||||
async onCreate() {
|
||||
const name = this.$data.filter;
|
||||
try {
|
||||
const newTag = await this.$store.dispatch("tags/create", name);
|
||||
this.$emit("update", [...this.$props.currentTagIds, newTag.id]);
|
||||
this.$nextTick(() => this.focusOnTag(newTag.id));
|
||||
|
||||
this.$data.filter = "";
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
"New tag was not created",
|
||||
`A problem occurred when trying to create the "${name}" tag`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTagsUpdated(selected: string[]) {
|
||||
const ops = selected.find(
|
||||
(value) => value === MANAGE_KEY || value === CREATE_KEY,
|
||||
);
|
||||
if (ops === MANAGE_KEY) {
|
||||
this.$data.filter = "";
|
||||
this.$store.dispatch("ui/openTagsManagerModal");
|
||||
} else if (ops === CREATE_KEY) {
|
||||
this.onCreate();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!this.$data.preventUpdate) {
|
||||
this.$emit("update", selected);
|
||||
}
|
||||
this.$data.preventUpdate = false;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
focusOnTopOption() {
|
||||
const tags = this.$refs.tag as Vue[] | undefined;
|
||||
const create = this.$refs.create as Vue | undefined;
|
||||
//@ts-ignore // focus on create option
|
||||
if (create && create.hoverItem) {
|
||||
// @ts-ignore
|
||||
create.hoverItem();
|
||||
}
|
||||
//@ts-ignore // focus on top option after filter
|
||||
else if (tags && tags[0] && tags[0].hoverItem) {
|
||||
// @ts-ignore
|
||||
tags[0].hoverItem();
|
||||
|
||||
// @ts-ignore
|
||||
if (tags[0] && tags[0].$el && tags[0].$el.scrollIntoView) {
|
||||
// @ts-ignore
|
||||
tags[0].$el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
},
|
||||
focusOnTag(tagId: string) {
|
||||
const tagOptions = (this.$refs.tag as Vue[]) || [];
|
||||
if (tagOptions && tagOptions.length) {
|
||||
const added = tagOptions.find((ref: any) => ref.value === tagId); // tslint:disable-line:no-any
|
||||
// @ts-ignore // focus on newly created item
|
||||
if (added && added.$el && added.$el.scrollIntoView && added.hoverItem) {
|
||||
// @ts-ignore
|
||||
added.hoverItem();
|
||||
added.$el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
},
|
||||
focusOnInput() {
|
||||
const select = this.$refs.select as Vue;
|
||||
const input = select && select.$refs.input as HTMLElement;
|
||||
if (input && input.focus) {
|
||||
input.focus();
|
||||
this.focused = true;
|
||||
}
|
||||
},
|
||||
onVisibleChange(visible: boolean) {
|
||||
if (!visible) {
|
||||
this.$data.filter = '';
|
||||
this.focused = false;
|
||||
}
|
||||
else {
|
||||
this.focused = true;
|
||||
}
|
||||
},
|
||||
onRemoveTag() {
|
||||
this.$nextTick(() => {
|
||||
this.focusOnInput();
|
||||
});
|
||||
},
|
||||
onBlur() {
|
||||
this.$emit('blur');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
allTags() {
|
||||
// keep applied tags in sync with store
|
||||
// for example in case tag is deleted from store
|
||||
if (this.currentTagIds.length !== this.appliedTags.length) {
|
||||
this.$emit("update", this.appliedTags);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$--max-input-height: 60px;
|
||||
$--border-radius: 20px;
|
||||
|
||||
.tags-container {
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $--border-radius;
|
||||
|
||||
&.focused {
|
||||
border: 1px solid $--color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .el-select {
|
||||
.el-select__tags {
|
||||
max-height: $--max-input-height;
|
||||
border-radius: $--border-radius;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
// firefox fix for scrollbars
|
||||
scrollbar-color: $--scrollbar-thumb-color transparent;
|
||||
}
|
||||
|
||||
.el-input.is-focus {
|
||||
border-radius: $--border-radius;
|
||||
}
|
||||
|
||||
input {
|
||||
max-height: $--max-input-height;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.tags-dropdown {
|
||||
$--item-font-size: 14px;
|
||||
$--item-line-height: 18px;
|
||||
$--item-vertical-padding: 10px;
|
||||
$--item-horizontal-padding: 20px;
|
||||
$--item-height: $--item-line-height + $--item-vertical-padding * 2;
|
||||
$--items-to-show: 7;
|
||||
$--item-padding: $--item-vertical-padding $--item-horizontal-padding;
|
||||
$--dropdown-height: $--item-height * $--items-to-show;
|
||||
$--dropdown-width: 224px;
|
||||
|
||||
min-width: $--dropdown-width !important;
|
||||
max-width: $--dropdown-width;
|
||||
|
||||
*,*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
position: relative;
|
||||
max-height: $--dropdown-height;
|
||||
|
||||
> div {
|
||||
overflow: auto;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
max-height: $--dropdown-height - $--item-height;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
min-height: $--item-height;
|
||||
width: $--dropdown-width;
|
||||
padding: $--item-padding;
|
||||
}
|
||||
|
||||
// override theme scrollbars in safari when overscrolling
|
||||
::-webkit-scrollbar-thumb {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
height: $--item-height;
|
||||
background-color: white;
|
||||
padding: $--item-padding;
|
||||
margin: 0;
|
||||
line-height: $--item-line-height;
|
||||
font-weight: 400;
|
||||
font-size: $--item-font-size;
|
||||
|
||||
&.is-disabled {
|
||||
color: $--custom-font-light;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: calc(100% - #{$--item-font-size});
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:after { // selected check
|
||||
font-size: $--item-font-size !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ops {
|
||||
color: $--color-primary;
|
||||
cursor: pointer;
|
||||
|
||||
:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tag {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.manage-tags {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
min-width: $--dropdown-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
packages/editor-ui/src/components/TagsManager/NoTagsView.vue
Normal file
61
packages/editor-ui/src/components/TagsManager/NoTagsView.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-col class="notags" :span="16">
|
||||
<div class="icon">🗄️</div>
|
||||
<div>
|
||||
<div class="headline">Ready to organize your workflows?</div>
|
||||
<div class="description">
|
||||
With workflow tags, you're free to create the perfect tagging system for
|
||||
your flows
|
||||
</div>
|
||||
</div>
|
||||
<el-button ref="create" @click="$emit('enableCreate')"> Create a tag </el-button>
|
||||
</el-col>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'NoTagsView',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$--footer-spacing: 45px;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: $--tags-manager-min-height - $--footer-spacing;
|
||||
margin-top: $--footer-spacing;
|
||||
}
|
||||
|
||||
.notags {
|
||||
word-break: normal;
|
||||
text-align: center;
|
||||
|
||||
> * {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 36px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: 17.6px;
|
||||
color: black;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
</style>
|
||||
190
packages/editor-ui/src/components/TagsManager/TagsManager.vue
Normal file
190
packages/editor-ui/src/components/TagsManager/TagsManager.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<Modal
|
||||
title="Manage tags"
|
||||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
@enter="onEnter"
|
||||
size="md"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<el-row>
|
||||
<TagsView
|
||||
v-if="hasTags || isCreating"
|
||||
:isLoading="isLoading"
|
||||
:tags="tags"
|
||||
|
||||
@create="onCreate"
|
||||
@update="onUpdate"
|
||||
@delete="onDelete"
|
||||
@disableCreate="onDisableCreate"
|
||||
/>
|
||||
<NoTagsView
|
||||
@enableCreate="onEnableCreate"
|
||||
v-else />
|
||||
</el-row>
|
||||
</template>
|
||||
<template v-slot:footer="{ close }">
|
||||
<el-button size="small" @click="close">Done</el-button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import { ITag } from "@/Interface";
|
||||
|
||||
import { showMessage } from "@/components/mixins/showMessage";
|
||||
import TagsView from "@/components/TagsManager/TagsView/TagsView.vue";
|
||||
import NoTagsView from "@/components/TagsManager/NoTagsView.vue";
|
||||
import Modal from "@/components/Modal.vue";
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: "TagsManager",
|
||||
created() {
|
||||
this.$store.dispatch("tags/fetchAll", {force: true, withUsageCount: true});
|
||||
},
|
||||
props: ['modalName'],
|
||||
data() {
|
||||
const tagIds = (this.$store.getters['tags/allTags'] as ITag[])
|
||||
.map((tag) => tag.id);
|
||||
|
||||
return {
|
||||
tagIds,
|
||||
isCreating: false,
|
||||
modalBus: new Vue(),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
TagsView,
|
||||
NoTagsView,
|
||||
Modal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("tags", ["isLoading"]),
|
||||
tags(): ITag[] {
|
||||
return this.$data.tagIds.map((tagId: string) => this.$store.getters['tags/getTagById'](tagId))
|
||||
.filter(Boolean); // if tag is deleted from store
|
||||
},
|
||||
hasTags(): boolean {
|
||||
return this.tags.length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEnableCreate() {
|
||||
this.$data.isCreating = true;
|
||||
},
|
||||
|
||||
onDisableCreate() {
|
||||
this.$data.isCreating = false;
|
||||
},
|
||||
|
||||
async onCreate(name: string, cb: (tag: ITag | null, error?: Error) => void) {
|
||||
try {
|
||||
if (!name) {
|
||||
throw new Error("Tag name cannot be empty");
|
||||
}
|
||||
|
||||
const newTag = await this.$store.dispatch("tags/create", name);
|
||||
this.$data.tagIds = [newTag.id].concat(this.$data.tagIds);
|
||||
cb(newTag);
|
||||
} catch (error) {
|
||||
const escapedName = escape(name);
|
||||
this.$showError(
|
||||
error,
|
||||
"New tag was not created",
|
||||
`A problem occurred when trying to create the "${escapedName}" tag`,
|
||||
);
|
||||
cb(null, error);
|
||||
}
|
||||
},
|
||||
|
||||
async onUpdate(id: string, name: string, cb: (tag: boolean, error?: Error) => void) {
|
||||
const tag = this.$store.getters['tags/getTagById'](id);
|
||||
const oldName = tag.name;
|
||||
|
||||
try {
|
||||
if (!name) {
|
||||
throw new Error("Tag name cannot be empty");
|
||||
}
|
||||
|
||||
if (name === oldName) {
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTag = await this.$store.dispatch("tags/rename", { id, name });
|
||||
cb(!!updatedTag);
|
||||
|
||||
const escapedName = escape(name);
|
||||
const escapedOldName = escape(oldName);
|
||||
|
||||
this.$showMessage({
|
||||
title: "Tag was updated",
|
||||
message: `The "${escapedOldName}" tag was successfully updated to "${escapedName}"`,
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
const escapedName = escape(oldName);
|
||||
this.$showError(
|
||||
error,
|
||||
"Tag was not updated",
|
||||
`A problem occurred when trying to update the "${escapedName}" tag`,
|
||||
);
|
||||
cb(false, error);
|
||||
}
|
||||
},
|
||||
|
||||
async onDelete(id: string, cb: (deleted: boolean, error?: Error) => void) {
|
||||
const tag = this.$store.getters['tags/getTagById'](id);
|
||||
const name = tag.name;
|
||||
|
||||
try {
|
||||
const deleted = await this.$store.dispatch("tags/delete", id);
|
||||
if (!deleted) {
|
||||
throw new Error('Could not delete tag');
|
||||
}
|
||||
|
||||
this.$data.tagIds = this.$data.tagIds.filter((tagId: string) => tagId !== id);
|
||||
|
||||
cb(deleted);
|
||||
|
||||
const escapedName = escape(name);
|
||||
this.$showMessage({
|
||||
title: "Tag was deleted",
|
||||
message: `The "${escapedName}" tag was successfully deleted from your tag collection`,
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
const escapedName = escape(name);
|
||||
this.$showError(
|
||||
error,
|
||||
"Tag was not deleted",
|
||||
`A problem occurred when trying to delete the "${escapedName}" tag`,
|
||||
);
|
||||
cb(false, error);
|
||||
}
|
||||
},
|
||||
|
||||
onEnter() {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
else if (!this.hasTags) {
|
||||
this.onEnableCreate();
|
||||
}
|
||||
else {
|
||||
this.modalBus.$emit('close');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-row {
|
||||
min-height: $--tags-manager-min-height;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<el-table
|
||||
stripe
|
||||
max-height="450"
|
||||
ref="table"
|
||||
empty-text="No matching tags exist"
|
||||
:data="rows"
|
||||
:span-method="getSpan"
|
||||
:row-class-name="getRowClasses"
|
||||
v-loading="isLoading"
|
||||
>
|
||||
<el-table-column label="Name">
|
||||
<template slot-scope="scope">
|
||||
<div class="name" :key="scope.row.id" @keydown.stop>
|
||||
<transition name="fade" mode="out-in">
|
||||
<el-input
|
||||
v-if="scope.row.create || scope.row.update"
|
||||
:value="newName"
|
||||
:maxlength="maxLength"
|
||||
@input="onNewNameChange"
|
||||
ref="nameInput"
|
||||
></el-input>
|
||||
<span v-else-if="scope.row.delete">
|
||||
<span>Are you sure you want to delete this tag?</span>
|
||||
<input ref="deleteHiddenInput" class="hidden" />
|
||||
</span>
|
||||
<span v-else :class="{ disabled: scope.row.disable }">
|
||||
{{ scope.row.tag.name }}
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Usage" width="150">
|
||||
<template slot-scope="scope">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="!scope.row.create && !scope.row.delete" :class="{ disabled: scope.row.disable }">
|
||||
{{ scope.row.usage }}
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column>
|
||||
<template slot-scope="scope">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="ops" v-if="scope.row.create">
|
||||
<el-button title="Cancel" @click.stop="cancel" size="small" plain :disabled="isSaving">Cancel</el-button>
|
||||
<el-button title="Create Tag" @click.stop="apply" size="small" :loading="isSaving">
|
||||
Create tag
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.update">
|
||||
<el-button title="Cancel" @click.stop="cancel" size="small" plain :disabled="isSaving">Cancel</el-button>
|
||||
<el-button title="Save Tag" @click.stop="apply" size="small" :loading="isSaving">Save changes</el-button>
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.delete">
|
||||
<el-button title="Cancel" @click.stop="cancel" size="small" plain :disabled="isSaving">Cancel</el-button>
|
||||
<el-button title="Delete Tag" @click.stop="apply" size="small" :loading="isSaving">Delete tag</el-button>
|
||||
</div>
|
||||
<div class="ops main" v-else-if="!scope.row.disable">
|
||||
<el-button title="Edit Tag" @click.stop="enableUpdate(scope.row)" icon="el-icon-edit" circle></el-button>
|
||||
<el-button title="Delete Tag" @click.stop="enableDelete(scope.row)" icon="el-icon-delete" circle></el-button>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { MAX_TAG_NAME_LENGTH } from "@/constants";
|
||||
import { ITagRow } from "@/Interface";
|
||||
import Vue from "vue";
|
||||
|
||||
const INPUT_TRANSITION_TIMEOUT = 350;
|
||||
const DELETE_TRANSITION_TIMEOUT = 100;
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TagsTable",
|
||||
props: ["rows", "isLoading", "newName", "isSaving"],
|
||||
data() {
|
||||
return {
|
||||
maxLength: MAX_TAG_NAME_LENGTH,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$props.rows.length === 1 && this.$props.rows[0].create) {
|
||||
this.focusOnInput();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRowClasses: ({ row }: { row: ITagRow }): string => {
|
||||
return row.disable ? "disabled" : "";
|
||||
},
|
||||
|
||||
getSpan({ row, columnIndex }: { row: ITagRow, columnIndex: number }): number | number[] {
|
||||
// expand text column with delete message
|
||||
if (columnIndex === 0 && row.tag && row.delete) {
|
||||
return [1, 2];
|
||||
}
|
||||
// hide usage column on delete
|
||||
if (columnIndex === 1 && row.tag && row.delete) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
return 1;
|
||||
},
|
||||
|
||||
enableUpdate(row: ITagRow): void {
|
||||
if (row.tag) {
|
||||
this.$emit('updateEnable', row.tag.id);
|
||||
this.$emit('newNameChange', row.tag.name);
|
||||
this.focusOnInput();
|
||||
}
|
||||
},
|
||||
|
||||
enableDelete(row: ITagRow): void {
|
||||
if (row.tag) {
|
||||
this.$emit('deleteEnable', row.tag.id);
|
||||
this.focusOnDelete();
|
||||
}
|
||||
},
|
||||
|
||||
cancel(): void {
|
||||
this.$emit('cancelOperation');
|
||||
},
|
||||
apply(): void {
|
||||
this.$emit('applyOperation');
|
||||
},
|
||||
|
||||
onNewNameChange(name: string): void {
|
||||
this.$emit('newNameChange', name);
|
||||
},
|
||||
|
||||
focusOnInput(): void {
|
||||
setTimeout(() => {
|
||||
const input = this.$refs.nameInput as any; // tslint:disable-line:no-any
|
||||
if (input && input.focus) {
|
||||
input.focus();
|
||||
}
|
||||
}, INPUT_TRANSITION_TIMEOUT);
|
||||
},
|
||||
|
||||
focusOnDelete(): void {
|
||||
setTimeout(() => {
|
||||
const input = this.$refs.deleteHiddenInput as any; // tslint:disable-line:no-any
|
||||
if (input && input.focus) {
|
||||
input.focus();
|
||||
}
|
||||
}, DELETE_TRANSITION_TIMEOUT);
|
||||
},
|
||||
|
||||
focusOnCreate(): void {
|
||||
((this.$refs.table as Vue).$refs.bodyWrapper as Element).scrollTop = 0;
|
||||
this.focusOnInput();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
rows(newValue: ITagRow[] | undefined) {
|
||||
if (newValue && newValue[0] && newValue[0].create) {
|
||||
this.focusOnCreate();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.name {
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
/deep/ input {
|
||||
border: 1px solid $--color-primary;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.ops {
|
||||
min-height: 45px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> .el-button {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ops.main > .el-button {
|
||||
display: none;
|
||||
float: right;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/deep/ tr.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/deep/ tr:hover .ops:not(.disabled) .el-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/deep/ .el-input.is-disabled > input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<el-row class="tags-header">
|
||||
<el-col :span="10">
|
||||
<el-input
|
||||
placeholder="Search tags"
|
||||
:value="search"
|
||||
@input="onSearchChange"
|
||||
:disabled="disabled"
|
||||
clearable
|
||||
:maxlength="maxLength"
|
||||
>
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-button @click="onAddNew" :disabled="disabled" plain>
|
||||
<font-awesome-icon icon="plus" />
|
||||
<div class="next-icon-text">Add new</div>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { MAX_TAG_NAME_LENGTH } from "@/constants";
|
||||
import Vue from "vue";
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
search: {
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
maxLength: MAX_TAG_NAME_LENGTH,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onAddNew() {
|
||||
this.$emit("createEnable");
|
||||
},
|
||||
onSearchChange(search: string) {
|
||||
this.$emit("searchChange", search);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div @keyup.enter="applyOperation" @keyup.esc="cancelOperation">
|
||||
<TagsTableHeader
|
||||
:search="search"
|
||||
:disabled="isHeaderDisabled()"
|
||||
@searchChange="onSearchChange"
|
||||
@createEnable="onCreateEnable"
|
||||
/>
|
||||
<TagsTable
|
||||
:rows="rows"
|
||||
:isLoading="isLoading"
|
||||
:isSaving="isSaving"
|
||||
|
||||
:newName="newName"
|
||||
@newNameChange="onNewNameChange"
|
||||
|
||||
@updateEnable="onUpdateEnable"
|
||||
@deleteEnable="onDeleteEnable"
|
||||
|
||||
@cancelOperation="cancelOperation"
|
||||
@applyOperation="applyOperation"
|
||||
|
||||
ref="tagsTable"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import { ITag, ITagRow } from "@/Interface";
|
||||
import TagsTableHeader from "@/components/TagsManager/TagsView/TagsTableHeader.vue";
|
||||
import TagsTable from "@/components/TagsManager/TagsView/TagsTable.vue";
|
||||
|
||||
const matches = (name: string, filter: string) => name.toLowerCase().trim().includes(filter.toLowerCase().trim());
|
||||
const getUsage = (count: number | undefined) => count && count > 0 ? `${count} workflow${count > 1 ? "s" : ""}` : 'Not being used';
|
||||
|
||||
export default Vue.extend({
|
||||
components: { TagsTableHeader, TagsTable },
|
||||
name: "TagsView",
|
||||
props: ["tags", "isLoading"],
|
||||
data() {
|
||||
return {
|
||||
createEnabled: false,
|
||||
deleteId: "",
|
||||
updateId: "",
|
||||
search: "",
|
||||
newName: "",
|
||||
stickyIds: new Set(),
|
||||
isSaving: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isCreateEnabled(): boolean {
|
||||
return (this.$props.tags || []).length === 0 || this.$data.createEnabled;
|
||||
},
|
||||
rows(): ITagRow[] {
|
||||
const disabled = this.isCreateEnabled || this.$data.updateId || this.$data.deleteId;
|
||||
const tagRows = (this.$props.tags || [])
|
||||
.filter((tag: ITag) => this.stickyIds.has(tag.id) || matches(tag.name, this.$data.search))
|
||||
.map((tag: ITag): ITagRow => ({
|
||||
tag,
|
||||
usage: getUsage(tag.usageCount),
|
||||
disable: disabled && tag.id !== this.deleteId && tag.id !== this.$data.updateId,
|
||||
update: disabled && tag.id === this.$data.updateId,
|
||||
delete: disabled && tag.id === this.$data.deleteId,
|
||||
}));
|
||||
|
||||
return this.isCreateEnabled
|
||||
? [{ create: true }, ...tagRows]
|
||||
: tagRows;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNewNameChange(name: string): void {
|
||||
this.newName = name;
|
||||
},
|
||||
onSearchChange(search: string): void {
|
||||
this.$data.stickyIds.clear();
|
||||
this.$data.search = search;
|
||||
},
|
||||
isHeaderDisabled(): boolean {
|
||||
return (
|
||||
this.$props.isLoading ||
|
||||
!!(this.isCreateEnabled || this.$data.updateId || this.$data.deleteId)
|
||||
);
|
||||
},
|
||||
|
||||
onUpdateEnable(updateId: string): void {
|
||||
this.updateId = updateId;
|
||||
},
|
||||
disableUpdate(): void {
|
||||
this.updateId = "";
|
||||
this.newName = "";
|
||||
},
|
||||
updateTag(): void {
|
||||
this.$data.isSaving = true;
|
||||
const name = this.newName.trim();
|
||||
const onUpdate = (updated: boolean) => {
|
||||
this.$data.isSaving = false;
|
||||
if (updated) {
|
||||
this.stickyIds.add(this.updateId);
|
||||
this.disableUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
this.$emit("update", this.updateId, name, onUpdate);
|
||||
},
|
||||
|
||||
onDeleteEnable(deleteId: string): void {
|
||||
this.deleteId = deleteId;
|
||||
},
|
||||
disableDelete(): void {
|
||||
this.deleteId = "";
|
||||
},
|
||||
deleteTag(): void {
|
||||
this.$data.isSaving = true;
|
||||
const onDelete = (deleted: boolean) => {
|
||||
if (deleted) {
|
||||
this.disableDelete();
|
||||
}
|
||||
this.$data.isSaving = false;
|
||||
};
|
||||
|
||||
this.$emit("delete", this.deleteId, onDelete);
|
||||
},
|
||||
|
||||
onCreateEnable(): void {
|
||||
this.$data.createEnabled = true;
|
||||
this.$data.newName = "";
|
||||
},
|
||||
disableCreate(): void {
|
||||
this.$data.createEnabled = false;
|
||||
this.$emit("disableCreate");
|
||||
},
|
||||
createTag(): void {
|
||||
this.$data.isSaving = true;
|
||||
const name = this.$data.newName.trim();
|
||||
const onCreate = (created: ITag | null, error?: Error) => {
|
||||
if (created) {
|
||||
this.stickyIds.add(created.id);
|
||||
this.disableCreate();
|
||||
}
|
||||
this.$data.isSaving = false;
|
||||
};
|
||||
|
||||
this.$emit("create", name, onCreate);
|
||||
},
|
||||
|
||||
applyOperation(): void {
|
||||
if (this.$data.isSaving) {
|
||||
return;
|
||||
}
|
||||
else if (this.isCreateEnabled) {
|
||||
this.createTag();
|
||||
}
|
||||
else if (this.$data.updateId) {
|
||||
this.updateTag();
|
||||
}
|
||||
else if (this.$data.deleteId) {
|
||||
this.deleteTag();
|
||||
}
|
||||
},
|
||||
cancelOperation(): void {
|
||||
if (this.$data.isSaving) {
|
||||
return;
|
||||
}
|
||||
else if (this.isCreateEnabled) {
|
||||
this.disableCreate();
|
||||
}
|
||||
else if (this.$data.updateId) {
|
||||
this.disableUpdate();
|
||||
}
|
||||
else if (this.$data.deleteId) {
|
||||
this.disableDelete();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
32
packages/editor-ui/src/components/WorkflowNameShort.vue
Normal file
32
packages/editor-ui/src/components/WorkflowNameShort.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<span :title="name">
|
||||
<slot :shortenedName="shortenedName"></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME_LIMIT = 25;
|
||||
const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4;
|
||||
|
||||
export default Vue.extend({
|
||||
name: "WorkflowNameShort",
|
||||
props: ["name", "limit"],
|
||||
computed: {
|
||||
shortenedName(): string {
|
||||
const name = this.$props.name;
|
||||
|
||||
const limit = this.$props.limit || DEFAULT_WORKFLOW_NAME_LIMIT;
|
||||
if (name.length <= limit) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const first = name.slice(0, limit - WORKFLOW_NAME_END_COUNT_TO_KEEP);
|
||||
const last = name.slice(name.length - WORKFLOW_NAME_END_COUNT_TO_KEEP, name.length);
|
||||
|
||||
return `${first}...${last}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,44 +1,68 @@
|
||||
<template>
|
||||
<span>
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" title="Open Workflow" :before-close="closeDialog" top="5vh">
|
||||
<Modal
|
||||
:name="modalName"
|
||||
size="xl"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div class="workflows-header">
|
||||
<div class="title">
|
||||
<h1>Open Workflow</h1>
|
||||
</div>
|
||||
<div class="tags-filter">
|
||||
<TagsDropdown
|
||||
placeholder="Filter by tags..."
|
||||
:currentTagIds="filterTagIds"
|
||||
:createEnabled="false"
|
||||
@update="updateTagsFilter"
|
||||
@esc="onTagsFilterEsc"
|
||||
@blur="onTagsFilterBlur"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-filter">
|
||||
<el-input placeholder="Search workflows..." ref="inputFieldFilter" v-model="filterText">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="text-very-light">
|
||||
Select a workflow to open:
|
||||
</div>
|
||||
|
||||
<div class="search-wrapper ignore-key-press">
|
||||
<el-input placeholder="Workflow filter..." ref="inputFieldFilter" v-model="filterText">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-table class="search-table" :data="filteredWorkflows" stripe @cell-click="openWorkflow" :default-sort = "{prop: 'updatedAt', order: 'descending'}" v-loading="isDataLoading">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" width="225" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" width="225" sortable></el-table-column>
|
||||
<el-table-column label="Active" width="90">
|
||||
<template slot-scope="scope">
|
||||
<workflow-activator :workflow-active="scope.row.active" :workflow-id="scope.row.id" @workflowActiveChanged="workflowActiveChanged" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</span>
|
||||
<template v-slot:content>
|
||||
<el-table class="search-table" :data="filteredWorkflows" stripe @cell-click="openWorkflow" :default-sort = "{prop: 'updatedAt', order: 'descending'}" v-loading="isDataLoading">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable>
|
||||
<template slot-scope="scope">
|
||||
<div :key="scope.row.id">
|
||||
<span class="name">{{scope.row.name}}</span>
|
||||
<TagsContainer class="hidden-sm-and-down" :tagIds="getIds(scope.row.tags)" :limit="3" @click="onTagClick" :hoverable="true"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column label="Active" width="75">
|
||||
<template slot-scope="scope">
|
||||
<workflow-activator :workflow-active="scope.row.active" :workflow-id="scope.row.id" @workflowActiveChanged="workflowActiveChanged" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import { ITag, IWorkflowShortResponse } from '@/Interface';
|
||||
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { IWorkflowShortResponse } from '@/Interface';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import TagsContainer from '@/components/TagsContainer.vue';
|
||||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
@@ -47,48 +71,63 @@ export default mixins(
|
||||
workflowHelpers,
|
||||
).extend({
|
||||
name: 'WorkflowOpen',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
],
|
||||
components: {
|
||||
WorkflowActivator,
|
||||
TagsContainer,
|
||||
TagsDropdown,
|
||||
Modal,
|
||||
},
|
||||
props: ['modalName'],
|
||||
data () {
|
||||
return {
|
||||
filterText: '',
|
||||
isDataLoading: false,
|
||||
workflows: [] as IWorkflowShortResponse[],
|
||||
filterTagIds: [] as string[],
|
||||
prevFilterTagIds: [] as string[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredWorkflows (): IWorkflowShortResponse[] {
|
||||
return this.workflows.filter((workflow: IWorkflowShortResponse) => {
|
||||
if (this.filterText === '' || workflow.name.toLowerCase().indexOf(this.filterText.toLowerCase()) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return this.workflows
|
||||
.filter((workflow: IWorkflowShortResponse) => {
|
||||
if (this.filterText && !workflow.name.toLowerCase().includes(this.filterText.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.filterTagIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!workflow.tags || workflow.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.filterTagIds.reduce((accu: boolean, id: string) => accu && !!workflow.tags.find(tag => tag.id === id), true);
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialogVisible (newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.filterText = '';
|
||||
this.openDialog();
|
||||
mounted() {
|
||||
this.filterText = '';
|
||||
this.filterTagIds = [];
|
||||
this.openDialog();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// Make sure that users can directly type in the filter
|
||||
(this.$refs.inputFieldFilter as HTMLInputElement).focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
Vue.nextTick(() => {
|
||||
// Make sure that users can directly type in the filter
|
||||
(this.$refs.inputFieldFilter as HTMLInputElement).focus();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
getIds(tags: ITag[] | undefined) {
|
||||
return (tags || []).map((tag) => tag.id);
|
||||
},
|
||||
updateTagsFilter(tags: string[]) {
|
||||
this.filterTagIds = tags;
|
||||
},
|
||||
onTagClick(tagId: string) {
|
||||
if (tagId !== 'count' && !this.filterTagIds.includes(tagId)) {
|
||||
this.filterTagIds.push(tagId);
|
||||
}
|
||||
},
|
||||
async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any
|
||||
if (column.label !== 'Active') {
|
||||
@@ -114,11 +153,19 @@ export default mixins(
|
||||
} else {
|
||||
// This is used to avoid duplicating the message
|
||||
this.$store.commit('setStateDirty', false);
|
||||
this.$emit('openWorkflow', data.id);
|
||||
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: data.id },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.$emit('openWorkflow', data.id);
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: data.id },
|
||||
});
|
||||
}
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
}
|
||||
},
|
||||
openDialog () {
|
||||
@@ -149,21 +196,45 @@ export default mixins(
|
||||
}
|
||||
}
|
||||
},
|
||||
onTagsFilterBlur() {
|
||||
this.prevFilterTagIds = this.filterTagIds;
|
||||
},
|
||||
onTagsFilterEsc() {
|
||||
// revert last applied tags
|
||||
this.filterTagIds = this.prevFilterTagIds;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.workflows-header {
|
||||
display: flex;
|
||||
|
||||
.search-wrapper {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
width: 200px;
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-filter {
|
||||
margin-left: 10px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.tags-filter {
|
||||
flex-grow: 1;
|
||||
max-width: 270px;
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-table {
|
||||
margin-top: 2em;
|
||||
.search-table .name {
|
||||
font-weight: 400;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
40
packages/editor-ui/src/components/mixins/emitter.ts
Normal file
40
packages/editor-ui/src/components/mixins/emitter.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
function broadcast(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
|
||||
// @ts-ignore
|
||||
(this as Vue).$children.forEach(child => {
|
||||
const name = child.$options.name;
|
||||
|
||||
if (name === componentName) {
|
||||
// @ts-ignore
|
||||
child.$emit.apply(child, [eventName].concat(params));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
broadcast.apply(child, [componentName, eventName].concat([params]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
methods: {
|
||||
$dispatch(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
|
||||
let parent = this.$parent || this.$root;
|
||||
let name = parent.$options.name;
|
||||
|
||||
while (parent && (!name || name !== componentName)) {
|
||||
parent = parent.$parent;
|
||||
|
||||
if (parent) {
|
||||
name = parent.$options.name;
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
// @ts-ignore
|
||||
parent.$emit.apply(parent, [eventName].concat(params));
|
||||
}
|
||||
},
|
||||
$broadcast(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
|
||||
broadcast.call(this, componentName, eventName, params);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IExternalHooks } from '@/Interface';
|
||||
import { IExternalHooks, IRootState } from '@/Interface';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
import { Store } from 'vuex';
|
||||
|
||||
export async function runExternalHook(
|
||||
eventName: string,
|
||||
store: Store<IDataObject>,
|
||||
store: Store<IRootState>,
|
||||
metadata?: IDataObject,
|
||||
) {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -2,6 +2,7 @@ import dateformat from 'dateformat';
|
||||
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { MessageType } from '@/Interface';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
@@ -9,6 +10,7 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||
data () {
|
||||
return {
|
||||
loadingService: null as any | null, // tslint:disable-line:no-any
|
||||
debouncedFunctions: [] as any[], // tslint:disable-line:no-any
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -73,6 +75,19 @@ export const genericHelpers = mixins(showMessage).extend({
|
||||
}
|
||||
},
|
||||
|
||||
async callDebounced (...inputParameters: any[]): Promise<void> { // tslint:disable-line:no-any
|
||||
const functionName = inputParameters.shift() as string;
|
||||
const debounceTime = inputParameters.shift() as number;
|
||||
|
||||
// @ts-ignore
|
||||
if (this.debouncedFunctions[functionName] === undefined) {
|
||||
// @ts-ignore
|
||||
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, { leading: true });
|
||||
}
|
||||
// @ts-ignore
|
||||
await this.debouncedFunctions[functionName].apply(this, inputParameters);
|
||||
},
|
||||
|
||||
async confirmMessage (message: string, headline: string, type = 'warning' as MessageType, confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
||||
try {
|
||||
await this.$confirm(message, headline, {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
@@ -20,6 +21,7 @@ export const pushConnection = mixins(
|
||||
nodeHelpers,
|
||||
showMessage,
|
||||
titleChange,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
data () {
|
||||
@@ -227,7 +229,7 @@ export const pushConnection = mixins(
|
||||
|
||||
runDataExecutedErrorMessage = errorMessage;
|
||||
|
||||
this.$titleSet(workflow.name, 'ERROR');
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
this.$showMessage({
|
||||
title: 'Problem executing workflow',
|
||||
message: errorMessage,
|
||||
@@ -235,7 +237,7 @@ export const pushConnection = mixins(
|
||||
});
|
||||
} else {
|
||||
// Workflow did execute without a problem
|
||||
this.$titleSet(workflow.name, 'IDLE');
|
||||
this.$titleSet(workflow.name as string, 'IDLE');
|
||||
this.$showMessage({
|
||||
title: 'Workflow got executed',
|
||||
message: 'Workflow did get executed successfully!',
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { makeRestApiRequest } from '@/api/helpers';
|
||||
|
||||
/**
|
||||
* Unflattens the Execution data.
|
||||
@@ -55,75 +56,13 @@ function unflattenExecutionData (fullExecutionData: IExecutionFlattedResponse):
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export class ResponseError extends Error {
|
||||
// The HTTP status code of response
|
||||
httpStatusCode?: number;
|
||||
|
||||
// The error code in the resonse
|
||||
errorCode?: number;
|
||||
|
||||
// The stack trace of the server
|
||||
serverStackTrace?: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of ResponseError.
|
||||
* @param {string} message The error message
|
||||
* @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
|
||||
* @param {number} [httpStatusCode] The HTTP status code the response should have
|
||||
* @param {string} [stack] The stack trace
|
||||
* @memberof ResponseError
|
||||
*/
|
||||
constructor (message: string, errorCode?: number, httpStatusCode?: number, stack?: string) {
|
||||
super(message);
|
||||
this.name = 'ResponseError';
|
||||
|
||||
if (errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
if (httpStatusCode) {
|
||||
this.httpStatusCode = httpStatusCode;
|
||||
}
|
||||
if (stack) {
|
||||
this.serverStackTrace = stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const restApi = Vue.extend({
|
||||
methods: {
|
||||
restApi (): IRestApi {
|
||||
const self = this;
|
||||
return {
|
||||
async makeRestApiRequest (method: Method, endpoint: string, data?: IDataObject): Promise<any> { // tslint:disable-line:no-any
|
||||
try {
|
||||
const options: AxiosRequestConfig = {
|
||||
method,
|
||||
url: endpoint,
|
||||
baseURL: self.$store.getters.getRestUrl,
|
||||
headers: {
|
||||
sessionid: self.$store.getters.sessionId,
|
||||
},
|
||||
};
|
||||
if (['PATCH', 'POST', 'PUT'].includes(method)) {
|
||||
options.data = data;
|
||||
} else {
|
||||
options.params = data;
|
||||
}
|
||||
|
||||
const response = await axios.request(options);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (error.message === 'Network Error') {
|
||||
throw new ResponseError('API-Server can not be reached. It is probably down.');
|
||||
}
|
||||
|
||||
const errorResponseData = error.response.data;
|
||||
if (errorResponseData !== undefined && errorResponseData.message !== undefined) {
|
||||
throw new ResponseError(errorResponseData.message, errorResponseData.code, error.response.status, errorResponseData.stack);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
return makeRestApiRequest(self.$store.getters.getRestApiContext, method, endpoint, data);
|
||||
},
|
||||
getActiveWorkflows: (): Promise<string[]> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/active`);
|
||||
@@ -179,7 +118,7 @@ export const restApi = Vue.extend({
|
||||
},
|
||||
|
||||
// Creates new credentials
|
||||
createNewWorkflow: (sendData: IWorkflowData): Promise<IWorkflowDb> => {
|
||||
createNewWorkflow: (sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> => {
|
||||
return self.restApi().makeRestApiRequest('POST', `/workflows`, sendData);
|
||||
},
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
IWorkflowDb,
|
||||
IWorkflowDataUpdate,
|
||||
XYPositon,
|
||||
ITag,
|
||||
} from '../../Interface';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
@@ -238,6 +239,7 @@ export const workflowHelpers = mixins(
|
||||
connections: workflowConnections,
|
||||
active: this.$store.getters.isActive,
|
||||
settings: this.$store.getters.workflowSettings,
|
||||
tags: this.$store.getters.workflowTags,
|
||||
};
|
||||
|
||||
const workflowId = this.$store.getters.workflowId;
|
||||
@@ -383,86 +385,43 @@ export const workflowHelpers = mixins(
|
||||
return returnData['__xxxxxxx__'];
|
||||
},
|
||||
|
||||
// Saves the currently loaded workflow to the database.
|
||||
async saveCurrentWorkflow (withNewName = false) {
|
||||
async saveCurrentWorkflow({name, tags}: {name?: string, tags?: string[]} = {}): Promise<boolean> {
|
||||
const currentWorkflow = this.$route.params.name;
|
||||
let workflowName: string | null | undefined = '';
|
||||
if (currentWorkflow === undefined || withNewName === true) {
|
||||
// Currently no workflow name is set to get it from user
|
||||
workflowName = await this.$prompt(
|
||||
'Enter workflow name',
|
||||
'Name',
|
||||
{
|
||||
confirmButtonText: 'Save',
|
||||
cancelButtonText: 'Cancel',
|
||||
},
|
||||
)
|
||||
.then((data) => {
|
||||
// @ts-ignore
|
||||
return data.value;
|
||||
})
|
||||
.catch(() => {
|
||||
// User did cancel
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (workflowName === undefined) {
|
||||
// User did cancel
|
||||
return;
|
||||
} else if (['', null].includes(workflowName)) {
|
||||
// User did not enter a name
|
||||
this.$showMessage({
|
||||
title: 'Name missing',
|
||||
message: `No name for the workflow got entered and could so not be saved!`,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!currentWorkflow) {
|
||||
return this.saveAsNewWorkflow({name, tags});
|
||||
}
|
||||
|
||||
// Workflow exists already so update it
|
||||
try {
|
||||
this.$store.commit('addActiveAction', 'workflowSaving');
|
||||
|
||||
let workflowData: IWorkflowData = await this.getWorkflowDataToSave();
|
||||
const workflowDataRequest: IWorkflowDataUpdate = await this.getWorkflowDataToSave();
|
||||
|
||||
if (currentWorkflow === undefined || withNewName === true) {
|
||||
// Workflow is new or is supposed to get saved under a new name
|
||||
// so create a new entry in database
|
||||
workflowData.name = workflowName!.trim() as string;
|
||||
|
||||
if (withNewName === true) {
|
||||
// If an existing workflow gets resaved with a new name
|
||||
// make sure that the new ones is not active
|
||||
workflowData.active = false;
|
||||
}
|
||||
|
||||
workflowData = await this.restApi().createNewWorkflow(workflowData);
|
||||
|
||||
this.$store.commit('setActive', workflowData.active || false);
|
||||
this.$store.commit('setWorkflowId', workflowData.id);
|
||||
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
|
||||
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
|
||||
this.$store.commit('setStateDirty', false);
|
||||
} else {
|
||||
// Workflow exists already so update it
|
||||
await this.restApi().updateWorkflow(currentWorkflow, workflowData);
|
||||
if (name) {
|
||||
workflowDataRequest.name = name.trim();
|
||||
}
|
||||
|
||||
if (this.$route.params.name !== workflowData.id) {
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
||||
});
|
||||
if (tags) {
|
||||
workflowDataRequest.tags = tags;
|
||||
}
|
||||
|
||||
const workflowData = await this.restApi().updateWorkflow(currentWorkflow, workflowDataRequest);
|
||||
|
||||
if (name) {
|
||||
this.$store.commit('setWorkflowName', {newName: workflowData.name});
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const createdTags = (workflowData.tags || []) as ITag[];
|
||||
const tagIds = createdTags.map((tag: ITag): string => tag.id);
|
||||
this.$store.commit('setWorkflowTagIds', tagIds);
|
||||
}
|
||||
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
this.$store.commit('setStateDirty', false);
|
||||
this.$showMessage({
|
||||
title: 'Workflow saved',
|
||||
message: `The workflow "${workflowData.name}" got saved!`,
|
||||
type: 'success',
|
||||
});
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
@@ -471,6 +430,58 @@ export const workflowHelpers = mixins(
|
||||
message: `There was a problem saving the workflow: "${e.message}"`,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveAsNewWorkflow ({name, tags}: {name?: string, tags?: string[]} = {}): Promise<boolean> {
|
||||
try {
|
||||
this.$store.commit('addActiveAction', 'workflowSaving');
|
||||
|
||||
const workflowDataRequest: IWorkflowDataUpdate = await this.getWorkflowDataToSave();
|
||||
// make sure that the new ones are not active
|
||||
workflowDataRequest.active = false;
|
||||
|
||||
if (name) {
|
||||
workflowDataRequest.name = name.trim();
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
workflowDataRequest.tags = tags;
|
||||
}
|
||||
const workflowData = await this.restApi().createNewWorkflow(workflowDataRequest);
|
||||
|
||||
this.$store.commit('setActive', workflowData.active || false);
|
||||
this.$store.commit('setWorkflowId', workflowData.id);
|
||||
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
|
||||
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
const createdTags = (workflowData.tags || []) as ITag[];
|
||||
const tagIds = createdTags.map((tag: ITag): string => tag.id);
|
||||
this.$store.commit('setWorkflowTagIds', tagIds);
|
||||
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
||||
});
|
||||
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
this.$store.commit('setStateDirty', false);
|
||||
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Problem saving workflow',
|
||||
message: `There was a problem saving the workflow: "${e.message}"`,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user