mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
feat(core): Change workflow deletions to soft deletes (#14894)
Adds soft‑deletion support for workflows through a new boolean column `isArchived`. When a workflow is archived we now set `isArchived` flag to true and the workflows stays in the database and is omitted from the default workflow listing query. Archived workflows can be viewed in read-only mode, but they cannot be activated. Archived workflows are still available by ID and can be invoked as sub-executions, so existing Execute Workflow nodes continue to work. Execution engine doesn't care about isArchived flag. Users can restore workflows via Unarchive action at the UI.
This commit is contained in:
@@ -69,6 +69,7 @@ const props = defineProps<{
|
||||
scopes: IWorkflowDb['scopes'];
|
||||
active: IWorkflowDb['active'];
|
||||
currentFolder?: FolderShortInfo;
|
||||
isArchived: IWorkflowDb['isArchived'];
|
||||
}>();
|
||||
|
||||
const $style = useCssModule();
|
||||
@@ -144,14 +145,20 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
||||
label: locale.baseText('menuActions.download'),
|
||||
disabled: !onWorkflowPage.value,
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (!props.readOnly && !props.isArchived) {
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.RENAME,
|
||||
label: locale.baseText('generic.rename'),
|
||||
disabled: !onWorkflowPage.value || workflowPermissions.value.update !== true,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if ((workflowPermissions.value.delete && !props.readOnly) || isNewWorkflow.value) {
|
||||
if (
|
||||
(workflowPermissions.value.delete === true && !props.readOnly && !props.isArchived) ||
|
||||
isNewWorkflow.value
|
||||
) {
|
||||
actions.unshift({
|
||||
id: WORKFLOW_MENU_ACTIONS.DUPLICATE,
|
||||
label: locale.baseText('menuActions.duplicate'),
|
||||
@@ -190,14 +197,29 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
||||
disabled: !onWorkflowPage.value || isNewWorkflow.value,
|
||||
});
|
||||
|
||||
if ((workflowPermissions.value.delete && !props.readOnly) || isNewWorkflow.value) {
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.DELETE,
|
||||
label: locale.baseText('menuActions.delete'),
|
||||
disabled: !onWorkflowPage.value || isNewWorkflow.value,
|
||||
customClass: $style.deleteItem,
|
||||
divided: true,
|
||||
});
|
||||
if ((workflowPermissions.value.delete === true && !props.readOnly) || isNewWorkflow.value) {
|
||||
if (props.isArchived) {
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.UNARCHIVE,
|
||||
label: locale.baseText('menuActions.unarchive'),
|
||||
disabled: !onWorkflowPage.value || isNewWorkflow.value,
|
||||
});
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.DELETE,
|
||||
label: locale.baseText('menuActions.delete'),
|
||||
disabled: !onWorkflowPage.value || isNewWorkflow.value,
|
||||
customClass: $style.deleteItem,
|
||||
divided: true,
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
id: WORKFLOW_MENU_ACTIONS.ARCHIVE,
|
||||
label: locale.baseText('menuActions.archive'),
|
||||
disabled: !onWorkflowPage.value || isNewWorkflow.value,
|
||||
customClass: $style.deleteItem,
|
||||
divided: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
@@ -512,6 +534,56 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.ARCHIVE: {
|
||||
const archiveConfirmed = await message.confirm(
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowArchive.message', {
|
||||
interpolate: { workflowName: props.name },
|
||||
}),
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowArchive.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowArchive.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowArchive.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (archiveConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await workflowsStore.archiveWorkflow(props.id);
|
||||
} catch (error) {
|
||||
toast.showError(error, locale.baseText('generic.archiveWorkflowError'));
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.stateIsDirty = false;
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleArchive.title', {
|
||||
interpolate: { workflowName: props.name },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
await router.push({ name: VIEWS.WORKFLOWS });
|
||||
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.UNARCHIVE: {
|
||||
await workflowsStore.unarchiveWorkflow(props.id);
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleUnarchive.title', {
|
||||
interpolate: { workflowName: props.name },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.DELETE: {
|
||||
const deleteConfirmed = await message.confirm(
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||
@@ -543,7 +615,9 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||
// Reset tab title since workflow is deleted.
|
||||
documentTitle.reset();
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title', {
|
||||
interpolate: { workflowName: props.name },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
@@ -628,7 +702,9 @@ const onBreadcrumbsItemSelected = (item: PathItem) => {
|
||||
:preview-value="shortenedName"
|
||||
:is-edit-enabled="isNameEditEnabled"
|
||||
:max-length="MAX_WORKFLOW_NAME_LENGTH"
|
||||
:disabled="readOnly || (!isNewWorkflow && !workflowPermissions.update)"
|
||||
:disabled="
|
||||
readOnly || isArchived || (!isNewWorkflow && !workflowPermissions.update)
|
||||
"
|
||||
placeholder="Enter workflow name"
|
||||
class="name"
|
||||
@toggle="onNameToggle"
|
||||
@@ -641,42 +717,62 @@ const onBreadcrumbsItemSelected = (item: PathItem) => {
|
||||
</template>
|
||||
</BreakpointsObserver>
|
||||
|
||||
<span v-if="settingsStore.areTagsEnabled" class="tags" data-test-id="workflow-tags-container">
|
||||
<WorkflowTagsDropdown
|
||||
v-if="isTagsEditEnabled && !readOnly && (isNewWorkflow || workflowPermissions.update)"
|
||||
ref="dropdown"
|
||||
v-model="appliedTagIds"
|
||||
:event-bus="tagsEventBus"
|
||||
:placeholder="i18n.baseText('workflowDetails.chooseOrCreateATag')"
|
||||
class="tags-edit"
|
||||
data-test-id="workflow-tags-dropdown"
|
||||
@blur="onTagsBlur"
|
||||
@esc="onTagsEditEsc"
|
||||
/>
|
||||
<div
|
||||
v-else-if="
|
||||
(tags ?? []).length === 0 && !readOnly && (isNewWorkflow || workflowPermissions.update)
|
||||
"
|
||||
>
|
||||
<span class="add-tag clickable" data-test-id="new-tag-link" @click="onTagsEditEnable">
|
||||
+ {{ i18n.baseText('workflowDetails.addTag') }}
|
||||
</span>
|
||||
</div>
|
||||
<WorkflowTagsContainer
|
||||
v-else
|
||||
:key="id"
|
||||
:tag-ids="workflowTagIds"
|
||||
:clickable="true"
|
||||
:responsive="true"
|
||||
data-test-id="workflow-tags"
|
||||
@click="onTagsEditEnable"
|
||||
/>
|
||||
<span class="tags" data-test-id="workflow-tags-container">
|
||||
<template v-if="settingsStore.areTagsEnabled">
|
||||
<WorkflowTagsDropdown
|
||||
v-if="
|
||||
isTagsEditEnabled &&
|
||||
!(readOnly || isArchived) &&
|
||||
(isNewWorkflow || workflowPermissions.update)
|
||||
"
|
||||
ref="dropdown"
|
||||
v-model="appliedTagIds"
|
||||
:event-bus="tagsEventBus"
|
||||
:placeholder="i18n.baseText('workflowDetails.chooseOrCreateATag')"
|
||||
class="tags-edit"
|
||||
data-test-id="workflow-tags-dropdown"
|
||||
@blur="onTagsBlur"
|
||||
@esc="onTagsEditEsc"
|
||||
/>
|
||||
<div
|
||||
v-else-if="
|
||||
(tags ?? []).length === 0 &&
|
||||
!(readOnly || isArchived) &&
|
||||
(isNewWorkflow || workflowPermissions.update)
|
||||
"
|
||||
>
|
||||
<span class="add-tag clickable" data-test-id="new-tag-link" @click="onTagsEditEnable">
|
||||
+ {{ i18n.baseText('workflowDetails.addTag') }}
|
||||
</span>
|
||||
</div>
|
||||
<WorkflowTagsContainer
|
||||
v-else
|
||||
:key="id"
|
||||
:tag-ids="workflowTagIds"
|
||||
:clickable="true"
|
||||
:responsive="true"
|
||||
data-test-id="workflow-tags"
|
||||
@click="onTagsEditEnable"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span class="archived">
|
||||
<N8nBadge
|
||||
v-if="isArchived"
|
||||
class="ml-3xs"
|
||||
theme="tertiary"
|
||||
bold
|
||||
data-test-id="workflow-archived-tag"
|
||||
>
|
||||
{{ locale.baseText('workflows.item.archived') }}
|
||||
</N8nBadge>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="tags"></span>
|
||||
|
||||
<PushConnectionTracker class="actions">
|
||||
<span :class="`activator ${$style.group}`">
|
||||
<WorkflowActivator
|
||||
:is-archived="isArchived"
|
||||
:workflow-active="active"
|
||||
:workflow-id="id"
|
||||
:workflow-permissions="workflowPermissions"
|
||||
@@ -726,10 +822,13 @@ const onBreadcrumbsItemSelected = (item: PathItem) => {
|
||||
type="primary"
|
||||
:saved="!uiStore.stateIsDirty && !isNewWorkflow"
|
||||
:disabled="
|
||||
isWorkflowSaving || readOnly || (!isNewWorkflow && !workflowPermissions.update)
|
||||
isWorkflowSaving ||
|
||||
readOnly ||
|
||||
isArchived ||
|
||||
(!isNewWorkflow && !workflowPermissions.update)
|
||||
"
|
||||
:is-saving="isWorkflowSaving"
|
||||
:with-shortcut="!readOnly && workflowPermissions.update"
|
||||
:with-shortcut="!readOnly && !isArchived && workflowPermissions.update"
|
||||
:shortcut-tooltip="i18n.baseText('saveWorkflowButton.hint')"
|
||||
data-test-id="workflow-save-button"
|
||||
@click="onSaveButtonClick"
|
||||
@@ -814,6 +913,14 @@ $--header-spacing: 20px;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.archived {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
margin-right: $--header-spacing;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user