mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
feat(editor): Add Delete permanently link to workflow archive toast (#29157)
This commit is contained in:
parent
048e01e060
commit
98ec56ad77
|
|
@ -1619,6 +1619,7 @@
|
|||
"mainSidebar.showMessage.handleSelect2.title": "Workflow created",
|
||||
"mainSidebar.showMessage.handleSelect3.title": "Workflow created",
|
||||
"mainSidebar.showMessage.handleArchive.title": "Workflow '{workflowName}' archived",
|
||||
"mainSidebar.showMessage.handleArchive.message": "Delete permanently",
|
||||
"mainSidebar.showMessage.handleUnarchive.title": "Workflow '{workflowName}' unarchived",
|
||||
"mainSidebar.showMessage.stopExecution.title": "Execution stopped",
|
||||
"mainSidebar.workflows": "Workflows",
|
||||
|
|
|
|||
|
|
@ -64,10 +64,12 @@ vi.mock('@/app/stores/pushConnection.store', () => ({
|
|||
vi.mock('@/app/composables/useToast', () => {
|
||||
const showError = vi.fn();
|
||||
const showMessage = vi.fn();
|
||||
const showToast = vi.fn();
|
||||
return {
|
||||
useToast: () => ({
|
||||
showError,
|
||||
showMessage,
|
||||
showToast,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
|
@ -505,7 +507,7 @@ describe('WorkflowDetails', () => {
|
|||
|
||||
expect(message.confirm).toHaveBeenCalledTimes(0);
|
||||
expect(toast.showError).toHaveBeenCalledTimes(0);
|
||||
expect(toast.showMessage).toHaveBeenCalledTimes(1);
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledWith(workflow.id, 'test-checksum');
|
||||
expect(router.push).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -599,7 +601,7 @@ describe('WorkflowDetails', () => {
|
|||
|
||||
expect(message.confirm).toHaveBeenCalledTimes(1);
|
||||
expect(toast.showError).toHaveBeenCalledTimes(0);
|
||||
expect(toast.showMessage).toHaveBeenCalledTimes(1);
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledWith(workflow.id, 'test-checksum');
|
||||
expect(router.push).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -609,6 +611,31 @@ describe('WorkflowDetails', () => {
|
|||
expect(workflowDocumentStoreRef.value?.active).toBe(false);
|
||||
});
|
||||
|
||||
it('should show a "Delete permanently" link in the archive toast that deletes the archived workflow', async () => {
|
||||
workflowDocumentStoreRef.value?.setScopes(['workflow:delete']);
|
||||
const { getByTestId } = renderComponent({
|
||||
props: { ...defaultProps, isArchived: false },
|
||||
});
|
||||
|
||||
workflowsStore.archiveWorkflow.mockResolvedValue(undefined);
|
||||
|
||||
await userEvent.click(getByTestId('workflow-menu'));
|
||||
await userEvent.click(getByTestId('workflow-menu-item-archive'));
|
||||
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
const toastConfig = vi.mocked(toast.showToast).mock.calls[0][0];
|
||||
expect(toastConfig.message).toContain('archive-toast-delete-permanently-link');
|
||||
expect(toastConfig.onClick).toBeDefined();
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
toastConfig.onClick?.({ target: anchor, preventDefault: vi.fn() } as unknown as MouseEvent);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(workflowsListStore.deleteWorkflow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(workflowsListStore.deleteWorkflow).toHaveBeenCalledWith(workflow.id);
|
||||
});
|
||||
|
||||
it("should call onWorkflowMenuSelect on 'Unarchive' option click", async () => {
|
||||
workflowDocumentStoreRef.value?.setScopes(['workflow:delete']);
|
||||
const { getByTestId } = renderComponent({
|
||||
|
|
|
|||
|
|
@ -257,10 +257,19 @@ async function handleArchiveWorkflow() {
|
|||
}
|
||||
|
||||
uiStore.markStateClean();
|
||||
toast.showMessage({
|
||||
const archivedWorkflowId = props.id;
|
||||
const archivedWorkflowName = props.name;
|
||||
toast.showToast({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleArchive.title', {
|
||||
interpolate: { workflowName: props.name },
|
||||
interpolate: { workflowName: archivedWorkflowName },
|
||||
}),
|
||||
message: `<a href="#" data-test-id="archive-toast-delete-permanently-link">${locale.baseText('mainSidebar.showMessage.handleArchive.message')}</a>`,
|
||||
onClick: (event) => {
|
||||
if (event?.target instanceof HTMLAnchorElement) {
|
||||
event.preventDefault();
|
||||
void deleteArchivedWorkflow(archivedWorkflowId, archivedWorkflowName);
|
||||
}
|
||||
},
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
|
@ -276,6 +285,42 @@ async function handleArchiveWorkflow() {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteArchivedWorkflow(id: IWorkflowDb['id'], name: IWorkflowDb['name']) {
|
||||
const deleteConfirmed = await message.confirm(
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||
interpolate: { workflowName: name },
|
||||
}),
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await workflowsListStore.deleteWorkflow(id);
|
||||
} catch (error) {
|
||||
toast.showError(error, locale.baseText('generic.deleteWorkflowError'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title', {
|
||||
interpolate: { workflowName: name },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUnarchiveWorkflow() {
|
||||
await workflowsStore.unarchiveWorkflow(props.id);
|
||||
toast.showMessage({
|
||||
|
|
|
|||
|
|
@ -38,10 +38,12 @@ vi.mock('vue-router', () => {
|
|||
vi.mock('@/app/composables/useToast', () => {
|
||||
const showError = vi.fn();
|
||||
const showMessage = vi.fn();
|
||||
const showToast = vi.fn();
|
||||
return {
|
||||
useToast: () => ({
|
||||
showError,
|
||||
showMessage,
|
||||
showToast,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
|
@ -370,7 +372,7 @@ describe('WorkflowCard', () => {
|
|||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledWith(data.id);
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
expect(toast.showMessage).toHaveBeenCalledTimes(1);
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
expect(emitted()['workflow:archived']).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -406,10 +408,61 @@ describe('WorkflowCard', () => {
|
|||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.archiveWorkflow).toHaveBeenCalledWith(data.id);
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
expect(toast.showMessage).toHaveBeenCalledTimes(1);
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
expect(emitted()['workflow:archived']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show a "Delete permanently" link in the archive toast that deletes the archived workflow', async () => {
|
||||
const data = createWorkflow({
|
||||
active: false,
|
||||
isArchived: false,
|
||||
scopes: ['workflow:delete'],
|
||||
});
|
||||
|
||||
const { getByTestId, emitted, rerender } = renderComponent({ props: { data } });
|
||||
await userEvent.click(getByTestId('workflow-card-actions'));
|
||||
await userEvent.click(getByTestId('action-archive'));
|
||||
|
||||
expect(toast.showToast).toHaveBeenCalledTimes(1);
|
||||
const toastConfig = vi.mocked(toast.showToast).mock.calls[0][0];
|
||||
expect(toastConfig.message).toContain('archive-toast-delete-permanently-link');
|
||||
expect(toastConfig.onClick).toBeDefined();
|
||||
|
||||
// Simulate v-for reuse: parent replaces props.data with a different workflow.
|
||||
// The toast onClick must still delete the originally archived workflow.
|
||||
await rerender({ data: createWorkflow({ id: 'different-id', name: 'Other Workflow' }) });
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
toastConfig.onClick?.({ target: anchor, preventDefault: vi.fn() } as unknown as MouseEvent);
|
||||
await waitFor(() => {
|
||||
expect(workflowsListStore.deleteWorkflow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(workflowsListStore.deleteWorkflow).toHaveBeenCalledWith(data.id);
|
||||
expect(emitted()['workflow:deleted']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not delete when "Delete permanently" confirmation is cancelled', async () => {
|
||||
const data = createWorkflow({
|
||||
active: false,
|
||||
isArchived: false,
|
||||
scopes: ['workflow:delete'],
|
||||
});
|
||||
|
||||
const { getByTestId } = renderComponent({ props: { data } });
|
||||
await userEvent.click(getByTestId('workflow-card-actions'));
|
||||
await userEvent.click(getByTestId('action-archive'));
|
||||
|
||||
const toastConfig = vi.mocked(toast.showToast).mock.calls[0][0];
|
||||
vi.mocked(message.confirm).mockResolvedValueOnce('cancel');
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
toastConfig.onClick?.({ target: anchor, preventDefault: vi.fn() } as unknown as MouseEvent);
|
||||
await waitFor(() => {
|
||||
expect(message.confirm).toHaveBeenCalled();
|
||||
});
|
||||
expect(workflowsListStore.deleteWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should have 'Unarchive' action on archived workflows", async () => {
|
||||
const data = createWorkflow({
|
||||
isArchived: true,
|
||||
|
|
|
|||
|
|
@ -454,9 +454,13 @@ async function toggleMCPAccess(enabled: boolean) {
|
|||
}
|
||||
|
||||
async function deleteWorkflow() {
|
||||
await deleteWorkflowById(props.data.id, props.data.name);
|
||||
}
|
||||
|
||||
async function deleteWorkflowById(id: WorkflowResource['id'], name: WorkflowResource['name']) {
|
||||
const deleteConfirmed = await message.confirm(
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||
interpolate: { workflowName: props.data.name },
|
||||
interpolate: { workflowName: name },
|
||||
}),
|
||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
{
|
||||
|
|
@ -475,7 +479,7 @@ async function deleteWorkflow() {
|
|||
}
|
||||
|
||||
try {
|
||||
await workflowsListStore.deleteWorkflow(props.data.id);
|
||||
await workflowsListStore.deleteWorkflow(id);
|
||||
} catch (error) {
|
||||
toast.showError(error, locale.baseText('generic.deleteWorkflowError'));
|
||||
return;
|
||||
|
|
@ -484,7 +488,7 @@ async function deleteWorkflow() {
|
|||
// Reset tab title since workflow is deleted.
|
||||
toast.showMessage({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleSelect1.title', {
|
||||
interpolate: { workflowName: props.data.name },
|
||||
interpolate: { workflowName: name },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
|
|
@ -514,17 +518,27 @@ async function archiveWorkflow() {
|
|||
}
|
||||
}
|
||||
|
||||
const archivedWorkflowId = props.data.id;
|
||||
const archivedWorkflowName = props.data.name;
|
||||
|
||||
try {
|
||||
await workflowsStore.archiveWorkflow(props.data.id);
|
||||
await workflowsStore.archiveWorkflow(archivedWorkflowId);
|
||||
} catch (error) {
|
||||
toast.showError(error, locale.baseText('generic.archiveWorkflowError'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.showMessage({
|
||||
toast.showToast({
|
||||
title: locale.baseText('mainSidebar.showMessage.handleArchive.title', {
|
||||
interpolate: { workflowName: props.data.name },
|
||||
interpolate: { workflowName: archivedWorkflowName },
|
||||
}),
|
||||
message: `<a href="#" data-test-id="archive-toast-delete-permanently-link">${locale.baseText('mainSidebar.showMessage.handleArchive.message')}</a>`,
|
||||
onClick: (event) => {
|
||||
if (event?.target instanceof HTMLAnchorElement) {
|
||||
event.preventDefault();
|
||||
void deleteWorkflowById(archivedWorkflowId, archivedWorkflowName);
|
||||
}
|
||||
},
|
||||
type: 'success',
|
||||
});
|
||||
emit('workflow:archived');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user