mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31: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:
@@ -72,13 +72,14 @@ const FILTERS_DEBOUNCE_TIME = 100;
|
||||
|
||||
interface Filters extends BaseFilters {
|
||||
status: string | boolean;
|
||||
showArchived: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const StatusFilter = {
|
||||
ALL: '',
|
||||
ACTIVE: 'active',
|
||||
DEACTIVATED: 'deactivated',
|
||||
ALL: '',
|
||||
};
|
||||
|
||||
/** Maps sort values from the ResourcesListLayout component to values expected by workflows endpoint */
|
||||
@@ -121,6 +122,7 @@ const filters = ref<Filters>({
|
||||
search: '',
|
||||
homeProject: '',
|
||||
status: StatusFilter.ALL,
|
||||
showArchived: false,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
@@ -281,13 +283,14 @@ const workflowListResources = computed<Resource[]>(() => {
|
||||
workflowCount: resource.workflowCount,
|
||||
subFolderCount: resource.subFolderCount,
|
||||
parentFolder: resource.parentFolder,
|
||||
} as FolderResource;
|
||||
} satisfies FolderResource;
|
||||
} else {
|
||||
return {
|
||||
resourceType: 'workflow',
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
active: resource.active ?? false,
|
||||
isArchived: resource.isArchived,
|
||||
updatedAt: resource.updatedAt.toString(),
|
||||
createdAt: resource.createdAt.toString(),
|
||||
homeProject: resource.homeProject,
|
||||
@@ -296,7 +299,7 @@ const workflowListResources = computed<Resource[]>(() => {
|
||||
readOnly: !getResourcePermissions(resource.scopes).workflow.update,
|
||||
tags: resource.tags,
|
||||
parentFolder: resource.parentFolder,
|
||||
} as WorkflowResource;
|
||||
} satisfies WorkflowResource;
|
||||
}
|
||||
});
|
||||
return resources;
|
||||
@@ -345,6 +348,7 @@ const hasFilters = computed(() => {
|
||||
return !!(
|
||||
filters.value.search ||
|
||||
filters.value.status !== StatusFilter.ALL ||
|
||||
filters.value.showArchived ||
|
||||
filters.value.tags.length
|
||||
);
|
||||
});
|
||||
@@ -375,6 +379,7 @@ watch(
|
||||
async (newVal) => {
|
||||
currentFolderId.value = newVal as string;
|
||||
filters.value.search = '';
|
||||
saveFiltersOnQueryString();
|
||||
await fetchWorkflows();
|
||||
},
|
||||
);
|
||||
@@ -384,7 +389,7 @@ sourceControlStore.$onAction(({ name, after }) => {
|
||||
after(async () => await initialize());
|
||||
});
|
||||
|
||||
const onWorkflowDeleted = async () => {
|
||||
const refreshWorkflows = async () => {
|
||||
await Promise.all([
|
||||
fetchWorkflows(),
|
||||
foldersStore.fetchTotalWorkflowsAndFoldersCount(route.params.projectId as string | undefined),
|
||||
@@ -483,11 +488,14 @@ const fetchWorkflows = async () => {
|
||||
const tags = filters.value.tags.length
|
||||
? filters.value.tags.map((tagId) => tagsStore.tagsById[tagId]?.name)
|
||||
: [];
|
||||
|
||||
const activeFilter =
|
||||
filters.value.status === StatusFilter.ALL
|
||||
? undefined
|
||||
: filters.value.status === StatusFilter.ACTIVE;
|
||||
|
||||
const archivedFilter = filters.value.showArchived ? undefined : false;
|
||||
|
||||
// Only fetch folders if showFolders is enabled and there are not tags or active filter applied
|
||||
const fetchFolders = showFolders.value && !tags.length && activeFilter === undefined;
|
||||
|
||||
@@ -500,6 +508,7 @@ const fetchWorkflows = async () => {
|
||||
{
|
||||
name: filters.value.search || undefined,
|
||||
active: activeFilter,
|
||||
isArchived: archivedFilter,
|
||||
tags: tags.length ? tags : undefined,
|
||||
parentFolderId:
|
||||
parentFolder ??
|
||||
@@ -609,6 +618,12 @@ const saveFiltersOnQueryString = () => {
|
||||
delete currentQuery.status;
|
||||
}
|
||||
|
||||
if (filters.value.showArchived) {
|
||||
currentQuery.showArchived = 'true';
|
||||
} else {
|
||||
delete currentQuery.showArchived;
|
||||
}
|
||||
|
||||
if (filters.value.tags.length) {
|
||||
currentQuery.tags = filters.value.tags.join(',');
|
||||
} else {
|
||||
@@ -628,7 +643,7 @@ const saveFiltersOnQueryString = () => {
|
||||
|
||||
const setFiltersFromQueryString = async () => {
|
||||
const newQuery: LocationQueryRaw = { ...route.query };
|
||||
const { tags, status, search, homeProject, sort } = route.query ?? {};
|
||||
const { tags, status, search, homeProject, sort, showArchived } = route.query ?? {};
|
||||
|
||||
// Helper to check if string value is not empty
|
||||
const isValidString = (value: unknown): value is string =>
|
||||
@@ -673,8 +688,7 @@ const setFiltersFromQueryString = async () => {
|
||||
}
|
||||
|
||||
// Handle status
|
||||
const validStatusValues = ['true', 'false'];
|
||||
if (isValidString(status) && validStatusValues.includes(status)) {
|
||||
if (isValidString(status)) {
|
||||
newQuery.status = status;
|
||||
filters.value.status = status === 'true' ? StatusFilter.ACTIVE : StatusFilter.DEACTIVATED;
|
||||
} else {
|
||||
@@ -690,6 +704,14 @@ const setFiltersFromQueryString = async () => {
|
||||
delete newQuery.sort;
|
||||
}
|
||||
|
||||
if (isValidString(showArchived)) {
|
||||
newQuery.showArchived = showArchived;
|
||||
filters.value.showArchived = showArchived === 'true';
|
||||
} else {
|
||||
delete newQuery.showArchived;
|
||||
filters.value.showArchived = false;
|
||||
}
|
||||
|
||||
void router.replace({ query: newQuery });
|
||||
};
|
||||
|
||||
@@ -1531,7 +1553,9 @@ const onNameSubmit = async ({
|
||||
:show-ownership-badge="showCardsBadge"
|
||||
data-target="workflow"
|
||||
@click:tag="onClickTag"
|
||||
@workflow:deleted="onWorkflowDeleted"
|
||||
@workflow:deleted="refreshWorkflows"
|
||||
@workflow:archived="refreshWorkflows"
|
||||
@workflow:unarchived="refreshWorkflows"
|
||||
@workflow:moved="fetchWorkflows"
|
||||
@workflow:duplicated="fetchWorkflows"
|
||||
@workflow:active-toggle="onWorkflowActiveToggle"
|
||||
@@ -1623,6 +1647,14 @@ const onNameSubmit = async ({
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
<div class="mb-s">
|
||||
<N8nCheckbox
|
||||
:label="i18n.baseText('workflows.filters.showArchived')"
|
||||
:model-value="filters.showArchived || false"
|
||||
data-test-id="show-archived-checkbox"
|
||||
@update:model-value="setKeyValue('showArchived', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #postamble>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user