feat(editor): Implement delete datastore row UI (no-changelog) (#18729)

This commit is contained in:
Svetoslav Dekov
2025-08-25 14:59:16 +02:00
committed by GitHub
parent 2dc34b2f17
commit 67352ce2f8
10 changed files with 329 additions and 52 deletions

View File

@@ -0,0 +1,61 @@
import { createComponentRenderer } from '@/__tests__/render';
import SelectedItemsInfo from '@/components/common/SelectedItemsInfo.vue';
const renderComponent = createComponentRenderer(SelectedItemsInfo);
vi.mock('@n8n/i18n', async (importOriginal) => ({
...(await importOriginal()),
useI18n: () => ({
baseText: (key: string) => key,
}),
}));
describe('SelectedItemsInfo', () => {
it('should not render when selectedCount is 0', () => {
const { queryByTestId } = renderComponent({
props: {
selectedCount: 0,
},
});
expect(queryByTestId('selected-items-info')).not.toBeInTheDocument();
});
it('should render when selectedCount is greater than 0', () => {
const { getByTestId } = renderComponent({
props: {
selectedCount: 3,
},
});
expect(getByTestId('selected-items-info')).toBeInTheDocument();
expect(getByTestId('delete-selected-button')).toBeInTheDocument();
expect(getByTestId('clear-selection-button')).toBeInTheDocument();
});
it('should emit deleteSelected event when delete button is clicked', () => {
const { getByTestId, emitted } = renderComponent({
props: {
selectedCount: 1,
},
});
getByTestId('delete-selected-button').click();
expect(emitted().deleteSelected).toBeTruthy();
expect(emitted().deleteSelected).toHaveLength(1);
});
it('should emit clearSelection event when clear button is clicked', () => {
const { getByTestId, emitted } = renderComponent({
props: {
selectedCount: 5,
},
});
getByTestId('clear-selection-button').click();
expect(emitted().clearSelection).toBeTruthy();
expect(emitted().clearSelection).toHaveLength(1);
});
});

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import { useI18n } from '@n8n/i18n';
import { N8nButton } from '@n8n/design-system';
interface Props {
selectedCount: number;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{
deleteSelected: [];
clearSelection: [];
}>();
const i18n = useI18n();
const getSelectedText = () => {
return i18n.baseText('generic.list.selected', {
adjustToNumber: props.selectedCount,
interpolate: { count: `${props.selectedCount}` },
});
};
const getClearSelectionText = () => {
return i18n.baseText('generic.list.clearSelection');
};
const handleDeleteSelected = () => {
emit('deleteSelected');
};
const handleClearSelection = () => {
emit('clearSelection');
};
</script>
<template>
<div
v-if="selectedCount > 0"
:class="$style.selectionOptions"
:data-test-id="`selected-items-info`"
>
<span>
{{ getSelectedText() }}
</span>
<N8nButton
:label="i18n.baseText('generic.delete')"
type="tertiary"
data-test-id="delete-selected-button"
@click="handleDeleteSelected"
/>
<N8nButton
:label="getClearSelectionText()"
type="tertiary"
data-test-id="clear-selection-button"
@click="handleClearSelection"
/>
</div>
</template>
<style module lang="scss">
.selectionOptions {
display: flex;
align-items: center;
position: absolute;
padding: var(--spacing-2xs);
z-index: 2;
left: 50%;
transform: translateX(-50%);
bottom: var(--spacing-3xl);
background: var(--execution-selector-background);
border-radius: var(--border-radius-base);
color: var(--execution-selector-text);
font-size: var(--font-size-2xs);
button {
margin-left: var(--spacing-2xs);
}
}
</style>

View File

@@ -26,6 +26,23 @@ vi.mock('vue-router', () => ({
RouterLink: vi.fn(),
}));
vi.mock('@n8n/i18n', async (importOriginal) => ({
...(await importOriginal()),
useI18n: () => ({
displayTimer: (timer: number) => timer,
baseText: (key: string, options: { interpolate: { count: string } }) => {
if (key === 'generic.list.selected') {
return `${options.interpolate.count} executions selected`;
} else if (key === 'executionsList.retryOf') {
return 'Retry of';
} else if (key === 'executionsList.successRetry') {
return 'Success retry';
}
return key;
},
}),
}));
let settingsStore: MockedStore<typeof useSettingsStore>;
const generateUndefinedNullOrString = () => {
@@ -148,7 +165,7 @@ describe('GlobalExecutionsList', () => {
).toBe(10),
);
expect(getByTestId('select-all-executions-checkbox')).toBeInTheDocument();
expect(getByTestId('selected-executions-info').textContent).toContain(10);
expect(getByTestId('selected-items-info').textContent).toContain(10);
await userEvent.click(getByTestId('load-more-button'));
await rerender({
@@ -170,7 +187,7 @@ describe('GlobalExecutionsList', () => {
el.contains(el.querySelector(':checked')),
).length,
).toBe(20);
expect(getByTestId('selected-executions-info').textContent).toContain(20);
expect(getByTestId('selected-items-info').textContent).toContain(20);
await userEvent.click(getAllByTestId('select-execution-checkbox')[2]);
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
@@ -179,7 +196,7 @@ describe('GlobalExecutionsList', () => {
el.contains(el.querySelector(':checked')),
).length,
).toBe(19);
expect(getByTestId('selected-executions-info').textContent).toContain(19);
expect(getByTestId('selected-items-info').textContent).toContain(19);
expect(getByTestId('select-visible-executions-checkbox')).toBeInTheDocument();
expect(queryByTestId('select-all-executions-checkbox')).not.toBeInTheDocument();
},

View File

@@ -2,6 +2,7 @@
import ConcurrentExecutionsHeader from '@/components/executions/ConcurrentExecutionsHeader.vue';
import ExecutionsFilter from '@/components/executions/ExecutionsFilter.vue';
import GlobalExecutionsListItem from '@/components/executions/global/GlobalExecutionsListItem.vue';
import SelectedItemsInfo from '@/components/common/SelectedItemsInfo.vue';
import { useI18n } from '@n8n/i18n';
import { useMessage } from '@/composables/useMessage';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
@@ -455,32 +456,11 @@ const goToUpgrade = () => {
</N8nTableBase>
</div>
</div>
<div
v-if="selectedCount > 0"
:class="$style.selectionOptions"
data-test-id="selected-executions-info"
>
<span>
{{
i18n.baseText('executionsList.selected', {
adjustToNumber: selectedCount,
interpolate: { count: `${selectedCount}` },
})
}}
</span>
<N8nButton
:label="i18n.baseText('generic.delete')"
type="tertiary"
data-test-id="delete-selected-button"
@click="handleDeleteSelected"
/>
<N8nButton
:label="i18n.baseText('executionsList.clearSelection')"
type="tertiary"
data-test-id="clear-selection-button"
@click="handleClearSelection"
/>
</div>
<SelectedItemsInfo
:selected-count="selectedCount"
@delete-selected="handleDeleteSelected"
@clear-selection="handleClearSelection"
/>
</div>
</template>
@@ -507,25 +487,6 @@ const goToUpgrade = () => {
margin-bottom: var(--spacing-s);
}
.selectionOptions {
display: flex;
align-items: center;
position: absolute;
padding: var(--spacing-2xs);
z-index: 2;
left: 50%;
transform: translateX(-50%);
bottom: var(--spacing-3xl);
background: var(--execution-selector-background);
border-radius: var(--border-radius-base);
color: var(--execution-selector-text);
font-size: var(--font-size-2xs);
button {
margin-left: var(--spacing-2xs);
}
}
.execTable {
height: 100%;
flex: 0 1 auto;