feat(editor): Data table UI redesign (no-changelog) (#18814)

Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
Svetoslav Dekov
2025-08-27 15:01:17 +03:00
committed by GitHub
parent ed090f0e66
commit f67b7f2ba2
19 changed files with 375 additions and 205 deletions

View File

@@ -199,6 +199,7 @@ defineExpose({ open, close });
} }
.icon { .icon {
display: flex;
text-align: center; text-align: center;
margin-right: var(--spacing-2xs); margin-right: var(--spacing-2xs);

View File

@@ -520,6 +520,9 @@
--color-menu-background: var(--p-gray-740); --color-menu-background: var(--p-gray-740);
--color-menu-hover-background: var(--p-gray-670); --color-menu-hover-background: var(--p-gray-670);
--color-menu-active-background: var(--p-gray-670); --color-menu-active-background: var(--p-gray-670);
/* Ag Grid */
--grid-row-selected-background: var(--p-color-secondary-720);
} }
body[data-theme='dark'] { body[data-theme='dark'] {

View File

@@ -686,6 +686,9 @@
// Params // Params
--color-icon-base: var(--color-text-light); --color-icon-base: var(--color-text-light);
--color-icon-hover: var(--p-color-primary-320); --color-icon-hover: var(--p-color-primary-320);
/* Ag Grid */
--grid-row-selected-background: var(--p-color-secondary-070);
} }
:root { :root {

View File

@@ -2834,7 +2834,7 @@
"contextual.users.settings.unavailable.button.cloud": "Upgrade now", "contextual.users.settings.unavailable.button.cloud": "Upgrade now",
"contextual.feature.unavailable.title": "Available on the Enterprise Plan", "contextual.feature.unavailable.title": "Available on the Enterprise Plan",
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan", "contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
"dataStore.dataStores": "Data Tables", "dataStore.dataStores": "Data tables",
"dataStore.empty.label": "You don't have any data tables yet", "dataStore.empty.label": "You don't have any data tables yet",
"dataStore.empty.description": "Once you create data tables for your projects, they will appear here", "dataStore.empty.description": "Once you create data tables for your projects, they will appear here",
"dataStore.empty.button.label": "Create Data Table in \"{projectName}\"", "dataStore.empty.button.label": "Create Data Table in \"{projectName}\"",
@@ -3004,8 +3004,10 @@
"settings.mfa.updateConfiguration": "MFA configuration updated", "settings.mfa.updateConfiguration": "MFA configuration updated",
"settings.mfa.invalidAuthenticatorCode": "Invalid authenticator code", "settings.mfa.invalidAuthenticatorCode": "Invalid authenticator code",
"projects.header.overview.subtitle": "All the workflows, credentials and executions you have access to", "projects.header.overview.subtitle": "All the workflows, credentials and executions you have access to",
"projects.header.overview.subtitleWithDataTables": "All the workflows, credentials and data tables you have access to",
"projects.header.shared.title": "Shared with you", "projects.header.shared.title": "Shared with you",
"projects.header.personal.subtitle": "Workflows and credentials owned by you", "projects.header.personal.subtitle": "Workflows and credentials owned by you",
"projects.header.personal.subtitleWithDataTables": "Workflows, credentials and data tables owned by you",
"projects.header.shared.subtitle": "Workflows and credentials other users have shared with you", "projects.header.shared.subtitle": "Workflows and credentials other users have shared with you",
"projects.header.create.workflow": "Create Workflow", "projects.header.create.workflow": "Create Workflow",
"projects.header.create.credential": "Create Credential", "projects.header.create.credential": "Create Credential",

View File

@@ -123,6 +123,7 @@ describe('ProjectHeader', () => {
}); });
it('Overview: should render the correct title and subtitle', async () => { it('Overview: should render the correct title and subtitle', async () => {
settingsStore.isDataStoreFeatureEnabled = false;
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(true); vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(true);
const { getByTestId, rerender } = renderComponent(); const { getByTestId, rerender } = renderComponent();
const overviewSubtitle = 'All the workflows, credentials and executions you have access to'; const overviewSubtitle = 'All the workflows, credentials and executions you have access to';
@@ -146,6 +147,7 @@ describe('ProjectHeader', () => {
}); });
it('Personal: should render the correct title and subtitle', async () => { it('Personal: should render the correct title and subtitle', async () => {
settingsStore.isDataStoreFeatureEnabled = false;
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(false); vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(false);
vi.spyOn(projectPages, 'isSharedSubPage', 'get').mockReturnValue(false); vi.spyOn(projectPages, 'isSharedSubPage', 'get').mockReturnValue(false);
const { getByTestId, rerender } = renderComponent(); const { getByTestId, rerender } = renderComponent();

View File

@@ -241,9 +241,17 @@ const sectionDescription = computed(() => {
if (projectPages.isSharedSubPage) { if (projectPages.isSharedSubPage) {
return i18n.baseText('projects.header.shared.subtitle'); return i18n.baseText('projects.header.shared.subtitle');
} else if (projectPages.isOverviewSubPage) { } else if (projectPages.isOverviewSubPage) {
return i18n.baseText('projects.header.overview.subtitle'); return i18n.baseText(
settingsStore.isDataStoreFeatureEnabled
? 'projects.header.overview.subtitleWithDataTables'
: 'projects.header.overview.subtitle',
);
} else if (isPersonalProject.value) { } else if (isPersonalProject.value) {
return i18n.baseText('projects.header.personal.subtitle'); return i18n.baseText(
settingsStore.isDataStoreFeatureEnabled
? 'projects.header.personal.subtitleWithDataTables'
: 'projects.header.personal.subtitle',
);
} }
return null; return null;

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import type { DataStore } from '@/features/dataStore/datastore.types'; import type { DataStore, DataStoreColumnCreatePayload } from '@/features/dataStore/datastore.types';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
@@ -10,6 +10,7 @@ import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBread
import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useDocumentTitle } from '@/composables/useDocumentTitle';
import DataStoreTable from './components/dataGrid/DataStoreTable.vue'; import DataStoreTable from './components/dataGrid/DataStoreTable.vue';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import AddColumnButton from './components/dataGrid/AddColumnButton.vue';
type Props = { type Props = {
id: string; id: string;
@@ -28,6 +29,8 @@ const dataStoreStore = useDataStoreStore();
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const dataStore = ref<DataStore | null>(null); const dataStore = ref<DataStore | null>(null);
const dataStoreTableRef = ref<InstanceType<typeof DataStoreTable>>();
const { debounce } = useDebounce(); const { debounce } = useDebounce();
const showErrorAndGoBackToList = async (error: unknown) => { const showErrorAndGoBackToList = async (error: unknown) => {
@@ -79,6 +82,10 @@ const onToggleSave = (value: boolean) => {
} }
}; };
const onAddColumn = (column: DataStoreColumnCreatePayload) => {
dataStoreTableRef.value?.addColumn(column);
};
onMounted(async () => { onMounted(async () => {
documentTitle.set(i18n.baseText('dataStore.dataStores')); documentTitle.set(i18n.baseText('dataStore.dataStores'));
await initialize(); await initialize();
@@ -104,9 +111,23 @@ onMounted(async () => {
<n8n-spinner /> <n8n-spinner />
<n8n-text>{{ i18n.baseText('generic.saving') }}...</n8n-text> <n8n-text>{{ i18n.baseText('generic.saving') }}...</n8n-text>
</div> </div>
<div :class="$style.actions">
<n8n-button @click="dataStoreTableRef?.addRow">{{
i18n.baseText('dataStore.addRow.label')
}}</n8n-button>
<AddColumnButton
:use-text-trigger="true"
:popover-id="'ds-details-add-column-popover'"
:params="{ onAddColumn }"
/>
</div>
</div> </div>
<div :class="$style.content"> <div :class="$style.content">
<DataStoreTable :data-store="dataStore" @toggle-save="onToggleSave" /> <DataStoreTable
ref="dataStoreTableRef"
:data-store="dataStore"
@toggle-save="onToggleSave"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -118,15 +139,11 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 100%; width: 100%;
max-width: var(--content-container-width);
box-sizing: border-box; box-sizing: border-box;
align-content: start; align-content: start;
padding: var(--spacing-l) var(--spacing-2xl) 0;
} }
.header-loading { .header-loading {
margin-bottom: var(--spacing-2xl);
div { div {
height: 2em; height: 2em;
} }
@@ -136,7 +153,11 @@ onMounted(async () => {
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl); }
.header,
.header-loading {
padding: var(--spacing-s);
} }
.saving { .saving {
@@ -145,4 +166,10 @@ onMounted(async () => {
gap: var(--spacing-3xs); gap: var(--spacing-3xs);
margin-top: var(--spacing-5xs); margin-top: var(--spacing-5xs);
} }
.actions {
display: flex;
gap: var(--spacing-3xs);
margin-left: auto;
}
</style> </style>

View File

@@ -97,6 +97,7 @@ watch(
<n8n-breadcrumbs <n8n-breadcrumbs
:items="breadcrumbs" :items="breadcrumbs"
:separator="BREADCRUMBS_SEPARATOR" :separator="BREADCRUMBS_SEPARATOR"
:highlight-last-item="false"
@item-selected="onItemClicked" @item-selected="onItemClicked"
> >
<template #prepend> <template #prepend>

View File

@@ -1,5 +1,5 @@
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import AddColumnPopover from '@/features/dataStore/components/dataGrid/AddColumnPopover.vue'; import AddColumnButton from '@/features/dataStore/components/dataGrid/AddColumnButton.vue';
import { fireEvent, waitFor } from '@testing-library/vue'; import { fireEvent, waitFor } from '@testing-library/vue';
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/constants'; import { MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/constants';
@@ -42,8 +42,15 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
}), }),
})); }));
describe('AddColumnPopover', () => { describe('AddColumnButton', () => {
const renderComponent = createComponentRenderer(AddColumnPopover); const addColumnHandler = vi.fn();
const renderComponent = createComponentRenderer(AddColumnButton, {
props: {
params: {
onAddColumn: addColumnHandler,
},
},
});
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()); setActivePinia(createPinia());
@@ -66,8 +73,8 @@ describe('AddColumnPopover', () => {
}); });
}); });
it('should emit addColumn event with correct payload', async () => { it('should call addColumn with correct payload', async () => {
const { getByTestId, getByPlaceholderText, emitted } = renderComponent(); const { getByTestId, getByPlaceholderText } = renderComponent();
const addButton = getByTestId('data-store-add-column-trigger-button'); const addButton = getByTestId('data-store-add-column-trigger-button');
await fireEvent.click(addButton); await fireEvent.click(addButton);
@@ -79,15 +86,10 @@ describe('AddColumnPopover', () => {
expect(submitButton).not.toBeDisabled(); expect(submitButton).not.toBeDisabled();
await fireEvent.click(submitButton); await fireEvent.click(submitButton);
expect(emitted().addColumn).toBeTruthy(); expect(addColumnHandler).toHaveBeenCalledWith({
expect(emitted().addColumn[0]).toEqual([
{
column: {
name: 'newColumn', name: 'newColumn',
type: 'string', type: 'string',
}, });
},
]);
}); });
it('should disable submit button when name is empty', async () => { it('should disable submit button when name is empty', async () => {
@@ -173,13 +175,13 @@ describe('AddColumnPopover', () => {
await fireEvent.click(addButton); await fireEvent.click(addButton);
const nameInput = getByPlaceholderText('Enter column name') as HTMLInputElement; const nameInput = getByPlaceholderText('Enter column name');
expect(nameInput.maxLength).toBe(MAX_COLUMN_NAME_LENGTH); expect(nameInput.getAttribute('maxlength')).toBe(MAX_COLUMN_NAME_LENGTH.toString());
}); });
it('should allow selecting different column types', async () => { it('should allow selecting different column types', async () => {
const { getByPlaceholderText, getByRole, getByText, getByTestId, emitted } = renderComponent(); const { getByPlaceholderText, getByRole, getByText, getByTestId } = renderComponent();
const addButton = getByTestId('data-store-add-column-trigger-button'); const addButton = getByTestId('data-store-add-column-trigger-button');
await fireEvent.click(addButton); await fireEvent.click(addButton);
@@ -198,15 +200,10 @@ describe('AddColumnPopover', () => {
const submitButton = getByTestId('data-store-add-column-submit-button'); const submitButton = getByTestId('data-store-add-column-submit-button');
await fireEvent.click(submitButton); await fireEvent.click(submitButton);
expect(emitted().addColumn).toBeTruthy(); expect(addColumnHandler).toHaveBeenCalledWith({
expect(emitted().addColumn[0]).toEqual([
{
column: {
name: 'numberColumn', name: 'numberColumn',
type: 'number', type: 'number',
}, });
},
]);
}); });
it('should reset form after successful submission', async () => { it('should reset form after successful submission', async () => {
@@ -215,7 +212,7 @@ describe('AddColumnPopover', () => {
await fireEvent.click(addButton); await fireEvent.click(addButton);
const nameInput = getByPlaceholderText('Enter column name') as HTMLInputElement; const nameInput = getByPlaceholderText('Enter column name');
await fireEvent.update(nameInput, 'testColumn'); await fireEvent.update(nameInput, 'testColumn');
const submitButton = getByTestId('data-store-add-column-submit-button'); const submitButton = getByTestId('data-store-add-column-submit-button');
@@ -225,8 +222,8 @@ describe('AddColumnPopover', () => {
await fireEvent.click(addButton); await fireEvent.click(addButton);
await waitFor(() => { await waitFor(() => {
const resetNameInput = getByPlaceholderText('Enter column name') as HTMLInputElement; const resetNameInput = getByPlaceholderText('Enter column name');
expect(resetNameInput.value).toBe(''); expect((resetNameInput as HTMLInputElement).value).toBe('');
}); });
}); });
@@ -248,7 +245,7 @@ describe('AddColumnPopover', () => {
}); });
it('should allow submission with Enter key', async () => { it('should allow submission with Enter key', async () => {
const { getByTestId, getByPlaceholderText, emitted } = renderComponent(); const { getByTestId, getByPlaceholderText } = renderComponent();
const addButton = getByTestId('data-store-add-column-trigger-button'); const addButton = getByTestId('data-store-add-column-trigger-button');
await fireEvent.click(addButton); await fireEvent.click(addButton);
@@ -257,15 +254,10 @@ describe('AddColumnPopover', () => {
await fireEvent.update(nameInput, 'enterColumn'); await fireEvent.update(nameInput, 'enterColumn');
await fireEvent.keyUp(nameInput, { key: 'Enter' }); await fireEvent.keyUp(nameInput, { key: 'Enter' });
expect(emitted().addColumn).toBeTruthy(); expect(addColumnHandler).toHaveBeenCalledWith({
expect(emitted().addColumn[0]).toEqual([
{
column: {
name: 'enterColumn', name: 'enterColumn',
type: 'string', type: 'string',
}, });
},
]);
}); });
it('should display all column type options', async () => { it('should display all column type options', async () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import type { import type {
DataStoreColumnCreatePayload, DataStoreColumnCreatePayload,
DataStoreColumnType, DataStoreColumnType,
@@ -10,12 +10,13 @@ import { COLUMN_NAME_REGEX, MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/
import Tooltip from '@n8n/design-system/components/N8nTooltip/Tooltip.vue'; import Tooltip from '@n8n/design-system/components/N8nTooltip/Tooltip.vue';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
const emit = defineEmits<{ const props = defineProps<{
addColumn: [ // the params key is needed so that we can pass this directly to ag-grid as column
value: { params: {
column: DataStoreColumnCreatePayload; onAddColumn: (column: DataStoreColumnCreatePayload) => void;
}, };
]; popoverId?: string;
useTextTrigger?: boolean;
}>(); }>();
const i18n = useI18n(); const i18n = useI18n();
@@ -35,16 +36,13 @@ const error = ref<string | null>(null);
const popoverOpen = ref(false); const popoverOpen = ref(false);
const isSelectOpen = ref(false); const isSelectOpen = ref(false);
const popoverId = computed(() => props.popoverId ?? 'add-column-popover');
const onAddButtonClicked = () => { const onAddButtonClicked = () => {
if (!columnName.value || !columnType.value) { if (!columnName.value || !columnType.value) {
return; return;
} }
emit('addColumn', { props.params.onAddColumn({ name: columnName.value, type: columnType.value });
column: {
name: columnName.value,
type: columnType.value,
},
});
columnName.value = ''; columnName.value = '';
columnType.value = 'string'; columnType.value = 'string';
popoverOpen.value = false; popoverOpen.value = false;
@@ -78,15 +76,21 @@ const onInput = debounce(validateName, { debounceTime: 100 });
<template> <template>
<N8nTooltip :disabled="popoverOpen" :content="i18n.baseText('dataStore.addColumn.label')"> <N8nTooltip :disabled="popoverOpen" :content="i18n.baseText('dataStore.addColumn.label')">
<div :class="$style.wrapper"> <div class="add-column-header-component-wrapper">
<N8nPopoverReka <N8nPopoverReka
id="add-column-popover" :id="popoverId"
:open="popoverOpen" :open="popoverOpen"
:popper-options="{ strategy: 'fixed' }" :popper-options="{ strategy: 'fixed' }"
:show-arrow="false" :show-arrow="false"
@update:open="handlePopoverOpenChange" @update:open="handlePopoverOpenChange"
> >
<template #trigger> <template #trigger>
<template v-if="props.useTextTrigger">
<N8nButton data-test-id="data-store-add-column-trigger-button" type="tertiary">
{{ i18n.baseText('dataStore.addColumn.label') }}
</N8nButton>
</template>
<template v-else>
<N8nIconButton <N8nIconButton
data-test-id="data-store-add-column-trigger-button" data-test-id="data-store-add-column-trigger-button"
text text
@@ -94,9 +98,10 @@ const onInput = debounce(validateName, { debounceTime: 100 });
type="tertiary" type="tertiary"
/> />
</template> </template>
</template>
<template #content> <template #content>
<div :class="$style['popover-content']"> <div class="add-ds-column-header-popover-content">
<div :class="$style['popover-body']"> <div class="popover-body">
<N8nInputLabel <N8nInputLabel
:label="i18n.baseText('dataStore.addColumn.nameInput.label')" :label="i18n.baseText('dataStore.addColumn.nameInput.label')"
:required="true" :required="true"
@@ -110,7 +115,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
@keyup.enter="onAddButtonClicked" @keyup.enter="onAddButtonClicked"
@input="onInput" @input="onInput"
/> />
<div v-if="error" :class="$style['error-message']"> <div v-if="error" class="error-message">
<n8n-text size="small" color="danger" tag="span"> <n8n-text size="small" color="danger" tag="span">
{{ error }} {{ error }}
</n8n-text> </n8n-text>
@@ -118,7 +123,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
<N8nIcon <N8nIcon
icon="circle-help" icon="circle-help"
size="small" size="small"
:class="$style['error-tooltip']" class="error-tooltip"
color="text-base" color="text-base"
data-test-id="add-column-error-help-icon" data-test-id="add-column-error-help-icon"
/> />
@@ -128,15 +133,15 @@ const onInput = debounce(validateName, { debounceTime: 100 });
<N8nInputLabel <N8nInputLabel
:label="i18n.baseText('dataStore.addColumn.typeInput.label')" :label="i18n.baseText('dataStore.addColumn.typeInput.label')"
:required="true" :required="true"
:class="$style['type-label']" class="type-label"
> >
<N8nSelect <N8nSelect
v-model="columnType" v-model="columnType"
append-to="#add-column-popover" :append-to="`#${popoverId}`"
@visible-change="isSelectOpen = $event" @visible-change="isSelectOpen = $event"
> >
<N8nOption v-for="type in columnTypes" :key="type" :value="type"> <N8nOption v-for="type in columnTypes" :key="type" :value="type">
<div :class="$style['option-content']"> <div class="add-column-option-content">
<N8nIcon :icon="getIconForType(type)" /> <N8nIcon :icon="getIconForType(type)" />
<N8nText>{{ type }}</N8nText> <N8nText>{{ type }}</N8nText>
</div> </div>
@@ -161,49 +166,38 @@ const onInput = debounce(validateName, { debounceTime: 100 });
</N8nTooltip> </N8nTooltip>
</template> </template>
<style module lang="scss"> <style lang="scss">
.wrapper { .add-ds-column-header-popover-content {
display: flex;
align-items: center;
background: var(--color-background-base);
padding: var(--spacing-2xs);
border: var(--border-base);
border-left: none;
height: 38px;
}
.popover-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 300px; width: 300px;
}
.popover-header { .popover-header {
padding: var(--spacing-2xs); padding: var(--spacing-2xs);
border-bottom: var(--border-base); border-bottom: var(--border-base);
} }
.popover-body { .popover-body {
padding: var(--spacing-xs); padding: var(--spacing-xs);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} }
.option-content { .error-message {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.error-message {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-4xs); gap: var(--spacing-4xs);
color: var(--color-text-danger); color: var(--color-text-danger);
} }
.error-tooltip { .error-tooltip {
cursor: pointer; cursor: pointer;
}
}
.add-column-option-content {
display: flex;
align-items: center;
gap: var(--spacing-xs);
} }
</style> </style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { N8nIconButton, N8nTooltip } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
const i18n = useI18n();
const props = defineProps<{
params: {
onClick: () => void;
};
}>();
</script>
<template>
<N8nTooltip :content="i18n.baseText('dataStore.addRow.label')">
<N8nIconButton text type="tertiary" icon="plus" @click="props.params.onClick" />
</N8nTooltip>
</template>

View File

@@ -1,6 +1,5 @@
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import DataStoreTable from '@/features/dataStore/components/dataGrid/DataStoreTable.vue'; import DataStoreTable from '@/features/dataStore/components/dataGrid/DataStoreTable.vue';
import { fireEvent, waitFor } from '@testing-library/vue';
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import type { DataStore } from '@/features/dataStore/datastore.types'; import type { DataStore } from '@/features/dataStore/datastore.types';
@@ -44,6 +43,7 @@ vi.mock('ag-grid-community', () => ({
ClientSideRowModelApiModule: {}, ClientSideRowModelApiModule: {},
ValidationModule: {}, ValidationModule: {},
UndoRedoEditModule: {}, UndoRedoEditModule: {},
CellStyleModule: {},
})); }));
// Mock the n8n theme // Mock the n8n theme
@@ -137,47 +137,16 @@ describe('DataStoreTable', () => {
}); });
describe('Component Initialization', () => { describe('Component Initialization', () => {
it('should render the component with AG Grid and AddColumnPopover', () => { it('should render the component with AG Grid', () => {
const { getByTestId } = renderComponent(); const { getByTestId } = renderComponent();
expect(getByTestId('ag-grid-vue')).toBeInTheDocument(); expect(getByTestId('ag-grid-vue')).toBeInTheDocument();
expect(getByTestId('add-column-popover')).toBeInTheDocument();
}); });
it('should render pagination controls', () => { it('should render pagination controls', () => {
const { getByTestId } = renderComponent(); const { getByTestId } = renderComponent();
expect(getByTestId('data-store-content-pagination')).toBeInTheDocument(); expect(getByTestId('data-store-content-pagination')).toBeInTheDocument();
}); });
it('should render add row button', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('data-store-add-row-button')).toBeInTheDocument();
});
});
describe('Add Column Functionality', () => {
it('should handle add column event from AddColumnPopover', async () => {
const { getByTestId } = renderComponent();
const addColumnPopover = getByTestId('add-column-popover');
const addButton = addColumnPopover.querySelector(
'[data-test-id="data-store-add-column-button"]',
);
expect(addButton).toBeInTheDocument();
await fireEvent.click(addButton!);
await waitFor(() => {
expect(dataStoreStore.addDataStoreColumn).toHaveBeenCalledWith(
mockDataStore.id,
mockDataStore.projectId,
{ name: 'newColumn', type: 'string' },
);
});
});
}); });
describe('Empty Data Store', () => { describe('Empty Data Store', () => {

View File

@@ -38,18 +38,28 @@ import {
ClientSideRowModelApiModule, ClientSideRowModelApiModule,
ValidationModule, ValidationModule,
UndoRedoEditModule, UndoRedoEditModule,
CellStyleModule,
} from 'ag-grid-community'; } from 'ag-grid-community';
import { n8nTheme } from '@/features/dataStore/components/dataGrid/n8nTheme'; import { n8nTheme } from '@/features/dataStore/components/dataGrid/n8nTheme';
import AddColumnPopover from '@/features/dataStore/components/dataGrid/AddColumnPopover.vue';
import SelectedItemsInfo from '@/components/common/SelectedItemsInfo.vue'; import SelectedItemsInfo from '@/components/common/SelectedItemsInfo.vue';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { DEFAULT_ID_COLUMN_NAME, EMPTY_VALUE, NULL_VALUE } from '@/features/dataStore/constants'; import {
DEFAULT_ID_COLUMN_NAME,
EMPTY_VALUE,
NULL_VALUE,
DATA_STORE_ID_COLUMN_WIDTH,
DATA_STORE_HEADER_HEIGHT,
DATA_STORE_ROW_HEIGHT,
ADD_ROW_ROW_ID,
} from '@/features/dataStore/constants';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { MODAL_CONFIRM } from '@/constants'; import { MODAL_CONFIRM } from '@/constants';
import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue'; import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes'; import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
import AddColumnButton from '@/features/dataStore/components/dataGrid/AddColumnButton.vue';
import AddRowButton from '@/features/dataStore/components/dataGrid/AddRowButton.vue';
import { isDataStoreValue } from '@/features/dataStore/typeGuards'; import { isDataStoreValue } from '@/features/dataStore/typeGuards';
import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue'; import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
import { onClickOutside } from '@vueuse/core'; import { onClickOutside } from '@vueuse/core';
@@ -68,6 +78,7 @@ ModuleRegistry.registerModules([
DateEditorModule, DateEditorModule,
ClientSideRowModelApiModule, ClientSideRowModelApiModule,
UndoRedoEditModule, UndoRedoEditModule,
CellStyleModule,
]); ]);
type Props = { type Props = {
@@ -83,7 +94,7 @@ const emit = defineEmits<{
const i18n = useI18n(); const i18n = useI18n();
const toast = useToast(); const toast = useToast();
const message = useMessage(); const message = useMessage();
const { getDefaultValueForType, mapToAGCellType } = useDataStoreTypes(); const { mapToAGCellType } = useDataStoreTypes();
const dataStoreStore = useDataStoreStore(); const dataStoreStore = useDataStoreStore();
@@ -94,7 +105,8 @@ const rowData = ref<DataStoreRow[]>([]);
const rowSelection: RowSelectionOptions | 'single' | 'multiple' = { const rowSelection: RowSelectionOptions | 'single' | 'multiple' = {
mode: 'multiRow', mode: 'multiRow',
enableClickSelection: false, enableClickSelection: false,
checkboxes: true, checkboxes: (params) => params.data?.id !== ADD_ROW_ROW_ID,
isRowSelectable: (params) => params.data?.id !== ADD_ROW_ROW_ID,
}; };
const contentLoading = ref(false); const contentLoading = ref(false);
@@ -106,13 +118,6 @@ const isTextEditorOpen = ref(false);
const gridContainer = useTemplateRef('gridContainer'); const gridContainer = useTemplateRef('gridContainer');
// Shared config for all columns
const defaultColumnDef: ColDef = {
flex: 1,
sortable: false,
filter: false,
};
// Pagination // Pagination
const pageSizeOptions = [10, 20, 50]; const pageSizeOptions = [10, 20, 50];
const currentPage = ref(1); const currentPage = ref(1);
@@ -132,7 +137,12 @@ const onGridReady = (params: GridReadyEvent) => {
const refreshGridData = () => { const refreshGridData = () => {
if (gridApi.value) { if (gridApi.value) {
gridApi.value.setGridOption('columnDefs', colDefs.value); gridApi.value.setGridOption('columnDefs', colDefs.value);
gridApi.value.setGridOption('rowData', rowData.value); gridApi.value.setGridOption('rowData', [
...rowData.value,
{
id: ADD_ROW_ROW_ID,
},
]);
} }
}; };
@@ -189,7 +199,7 @@ const onDeleteColumn = async (columnId: string) => {
} }
}; };
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => { const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
try { try {
const newColumn = await dataStoreStore.addDataStoreColumn( const newColumn = await dataStoreStore.addDataStoreColumn(
props.dataStore.id, props.dataStore.id,
@@ -199,9 +209,13 @@ const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload })
if (!newColumn) { if (!newColumn) {
throw new Error(i18n.baseText('generic.unknownError')); throw new Error(i18n.baseText('generic.unknownError'));
} }
colDefs.value = [...colDefs.value, createColumnDef(newColumn)]; colDefs.value = [
...colDefs.value.slice(0, -1),
createColumnDef(newColumn),
...colDefs.value.slice(-1),
];
rowData.value = rowData.value.map((row) => { rowData.value = rowData.value.map((row) => {
return { ...row, [newColumn.name]: getDefaultValueForType(newColumn.type) }; return { ...row, [newColumn.name]: null };
}); });
refreshGridData(); refreshGridData();
} catch (error) { } catch (error) {
@@ -214,13 +228,24 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
colId: col.id, colId: col.id,
field: col.name, field: col.name,
headerName: col.name, headerName: col.name,
editable: true, sortable: false,
flex: 1,
editable: (params) => params.data?.id !== ADD_ROW_ROW_ID,
resizable: true, resizable: true,
lockPinned: true, lockPinned: true,
headerComponent: ColumnHeader, headerComponent: ColumnHeader,
cellEditorPopup: false, cellEditorPopup: false,
headerComponentParams: { onDelete: onDeleteColumn }, headerComponentParams: { onDelete: onDeleteColumn },
cellDataType: mapToAGCellType(col.type), cellDataType: mapToAGCellType(col.type),
cellClass: (params) => {
if (params.data?.id === ADD_ROW_ROW_ID) {
return 'add-row-cell';
}
if (params.column.getUserProvidedColDef()?.cellDataType === 'boolean') {
return 'boolean-cell';
}
return '';
},
valueGetter: (params: ValueGetterParams<DataStoreRow>) => { valueGetter: (params: ValueGetterParams<DataStoreRow>) => {
// If the value is null, return null to show empty cell // If the value is null, return null to show empty cell
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) { if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
@@ -236,6 +261,9 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
return params.data?.[col.name]; return params.data?.[col.name];
}, },
cellRendererSelector: (params: ICellRendererParams) => { cellRendererSelector: (params: ICellRendererParams) => {
if (params.data?.id === ADD_ROW_ROW_ID || col.id === 'add-column') {
return {};
}
let rowValue = params.data?.[col.name]; let rowValue = params.data?.[col.name];
// When adding new column, rowValue is undefined (same below, in string cell editor) // When adding new column, rowValue is undefined (same below, in string cell editor)
if (rowValue === undefined) { if (rowValue === undefined) {
@@ -289,7 +317,7 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
// Setup date editor // Setup date editor
if (col.type === 'date') { if (col.type === 'date') {
columnDef.cellEditor = 'agDateCellEditor'; columnDef.cellEditor = 'agDateCellEditor';
columnDef.cellEditorPopup = true; columnDef.cellEditorPopup = false;
} }
return { return {
...columnDef, ...columnDef,
@@ -324,8 +352,8 @@ const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
const onAddRowClick = async () => { const onAddRowClick = async () => {
try { try {
// Go to last page if we are not there already // Go to last page if we are not there already
if (currentPage.value * pageSize.value < totalItems.value) { if (currentPage.value * pageSize.value < totalItems.value + 1) {
await setCurrentPage(Math.ceil(totalItems.value / pageSize.value)); await setCurrentPage(Math.ceil((totalItems.value + 1) / pageSize.value));
} }
contentLoading.value = true; contentLoading.value = true;
emit('toggleSave', true); emit('toggleSave', true);
@@ -335,7 +363,9 @@ const onAddRowClick = async () => {
props.dataStore.columns.forEach((col) => { props.dataStore.columns.forEach((col) => {
newRow[col.name] = null; newRow[col.name] = null;
}); });
rows.value.push(newRow); rowData.value.push(newRow);
totalItems.value += 1;
refreshGridData();
} catch (error) { } catch (error) {
toast.showError(error, i18n.baseText('dataStore.addRow.error')); toast.showError(error, i18n.baseText('dataStore.addRow.error'));
} finally { } finally {
@@ -360,10 +390,42 @@ const initColumnDefinitions = () => {
suppressMovable: true, suppressMovable: true,
headerComponent: null, headerComponent: null,
lockPosition: true, lockPosition: true,
minWidth: DATA_STORE_ID_COLUMN_WIDTH,
maxWidth: DATA_STORE_ID_COLUMN_WIDTH,
resizable: false,
cellClass: (params) => (params.data?.id === ADD_ROW_ROW_ID ? 'add-row-cell' : 'id-column'),
cellRendererSelector: (params: ICellRendererParams) => {
if (params.value === ADD_ROW_ROW_ID) {
return {
component: AddRowButton,
params: {
onClick: onAddRowClick,
},
};
}
return undefined;
},
}, },
), ),
// Append other columns // Append other columns
...orderBy(props.dataStore.columns, 'index').map((col) => createColumnDef(col)), ...orderBy(props.dataStore.columns, 'index').map((col) => createColumnDef(col)),
createColumnDef(
{
index: props.dataStore.columns.length + 1,
id: 'add-column',
name: 'Add Column',
type: 'string',
},
{
editable: false,
suppressMovable: true,
lockPinned: true,
lockPosition: 'right',
resizable: false,
headerComponent: AddColumnButton,
headerComponentParams: { onAddColumn },
},
),
]; ];
}; };
@@ -404,8 +466,11 @@ const onCellClicked = (params: CellClickedEvent<DataStoreRow>) => {
const clickedCellColumn = params.column.getColId(); const clickedCellColumn = params.column.getColId();
const clickedCellRow = params.rowIndex; const clickedCellRow = params.rowIndex;
// Skip if rowIndex is null if (
if (clickedCellRow === null) return; clickedCellRow === null ||
params.api.isEditing({ rowIndex: clickedCellRow, column: params.column, rowPinned: null })
)
return;
// Check if this is the same cell that was focused before this click // Check if this is the same cell that was focused before this click
const wasAlreadyFocused = const wasAlreadyFocused =
@@ -437,10 +502,9 @@ const fetchDataStoreContent = async () => {
currentPage.value, currentPage.value,
pageSize.value, pageSize.value,
); );
rows.value = fetchedRows.data; rowData.value = fetchedRows.data;
totalItems.value = fetchedRows.count; totalItems.value = fetchedRows.count;
rowData.value = rows.value; refreshGridData();
handleClearSelection(); handleClearSelection();
} catch (error) { } catch (error) {
toast.showError(error, i18n.baseText('dataStore.fetchContent.error')); toast.showError(error, i18n.baseText('dataStore.fetchContent.error'));
@@ -544,6 +608,11 @@ const handleClearSelection = () => {
gridApi.value.deselectAll(); gridApi.value.deselectAll();
} }
}; };
defineExpose({
addRow: onAddRowClick,
addColumn: onAddColumn,
});
</script> </script>
<template> <template>
@@ -551,12 +620,9 @@ const handleClearSelection = () => {
<div ref="gridContainer" :class="$style['grid-container']" data-test-id="data-store-grid"> <div ref="gridContainer" :class="$style['grid-container']" data-test-id="data-store-grid">
<AgGridVue <AgGridVue
style="width: 100%" style="width: 100%"
:row-data="rowData"
:column-defs="colDefs"
:default-col-def="defaultColumnDef"
:dom-layout="'autoHeight'" :dom-layout="'autoHeight'"
:row-height="36" :row-height="DATA_STORE_ROW_HEIGHT"
:header-height="36" :header-height="DATA_STORE_HEADER_HEIGHT"
:animate-rows="false" :animate-rows="false"
:theme="n8nTheme" :theme="n8nTheme"
:suppress-drag-leave-hides-columns="true" :suppress-drag-leave-hides-columns="true"
@@ -574,22 +640,8 @@ const handleClearSelection = () => {
@column-header-clicked="resetLastFocusedCell" @column-header-clicked="resetLastFocusedCell"
@selection-changed="onSelectionChanged" @selection-changed="onSelectionChanged"
/> />
<AddColumnPopover
:data-store="props.dataStore"
:class="$style['add-column-popover']"
@add-column="onAddColumn"
/>
</div> </div>
<div :class="$style.footer"> <div :class="$style.footer">
<n8n-tooltip :content="i18n.baseText('dataStore.addRow.label')">
<n8n-icon-button
data-test-id="data-store-add-row-button"
icon="plus"
class="mb-xl"
type="secondary"
@click="onAddRowClick"
/>
</n8n-tooltip>
<el-pagination <el-pagination
v-model:current-page="currentPage" v-model:current-page="currentPage"
v-model:page-size="pageSize" v-model:page-size="pageSize"
@@ -615,7 +667,6 @@ const handleClearSelection = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-m); gap: var(--spacing-m);
width: calc(100% - var(--spacing-m) * 2);
align-items: center; align-items: center;
} }
@@ -626,22 +677,35 @@ const handleClearSelection = () => {
// AG Grid style overrides // AG Grid style overrides
--ag-foreground-color: var(--color-text-base); --ag-foreground-color: var(--color-text-base);
--ag-accent-color: var(--color-primary); --ag-cell-text-color: var(--color-text-dark);
--ag-accent-color: var(--p-color-secondary-470);
--ag-row-hover-color: var(--color-background-light-base);
--ag-background-color: var(--color-background-xlight); --ag-background-color: var(--color-background-xlight);
--ag-border-color: var(--border-color-base); --ag-border-color: var(--border-color-base);
--ag-border-radius: var(--border-radius-base); --ag-border-radius: var(--border-radius-base);
--ag-wrapper-border-radius: 0; --ag-wrapper-border-radius: 0;
--ag-font-family: var(--font-family); --ag-font-family: var(--font-family);
--ag-font-size: var(--font-size-xs); --ag-font-size: var(--font-size-xs);
--ag-font-color: var(--color-text-base);
--ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px); --ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px);
--ag-header-background-color: var(--color-background-light-base); --ag-header-background-color: var(--color-background-light-base);
--ag-header-font-size: var(--font-size-xs); --ag-header-font-size: var(--font-size-xs);
--ag-header-font-weight: var(--font-weight-bold); --ag-header-font-weight: var(--font-weight-medium);
--ag-header-foreground-color: var(--color-text-dark); --ag-header-foreground-color: var(--color-text-dark);
--ag-cell-horizontal-padding: var(--spacing-2xs); --ag-cell-horizontal-padding: var(--spacing-2xs);
--ag-header-column-resize-handle-color: var(--color-foreground-base);
--ag-header-column-resize-handle-height: 100%;
--ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px); --ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px);
--ag-header-column-border-height: 100%;
--ag-range-selection-border-color: var(--p-color-secondary-470);
--ag-input-padding-start: var(--spacing-2xs);
--ag-input-background-color: var(--color-text-xlight);
--ag-focus-shadow: none;
--cell-editing-border: 2px solid var(--color-secondary);
:global(.ag-cell) {
display: flex;
align-items: center;
}
:global(.ag-header-cell-resize) { :global(.ag-header-cell-resize) {
width: var(--spacing-xs); width: var(--spacing-xs);
@@ -649,26 +713,101 @@ const handleClearSelection = () => {
right: -7px; right: -7px;
} }
// Don't show borders for the checkbox cells
:global(.ag-cell[col-id='ag-Grid-SelectionColumn']) { :global(.ag-cell[col-id='ag-Grid-SelectionColumn']) {
border: none; border: none;
padding-left: var(--spacing-l);
}
:global(.ag-header-cell[col-id='ag-Grid-SelectionColumn']) {
padding-left: var(--spacing-l);
&:after {
display: none;
}
} }
:global(.ag-cell[col-id='ag-Grid-SelectionColumn'].ag-cell-focus) { :global(.ag-cell[col-id='ag-Grid-SelectionColumn'].ag-cell-focus) {
outline: none; outline: none;
} }
}
.add-column-popover { :global(.ag-root-wrapper) {
display: flex; border-left: none;
position: absolute; }
right: -47px;
:global(.id-column) {
color: var(--color-text-light);
justify-content: center;
}
:global(.ag-header-cell[col-id='id']) {
text-align: center;
}
:global(.add-row-cell) {
border: none !important;
background-color: transparent !important;
}
:global(.ag-header-cell[col-id='add-column']) {
&:after {
display: none;
}
}
:global(.ag-cell-value[col-id='add-column']),
:global(.ag-cell-value[col-id='id']),
:global(.ag-cell[col-id='ag-Grid-SelectionColumn']) {
border: none;
background-color: transparent;
}
:global(.ag-cell-value[col-id='id']) {
border-right: 1px solid var(--ag-border-color);
}
:global(.ag-large-text-input) {
position: fixed;
padding: 0;
textarea {
padding-top: var(--spacing-xs);
&:where(:focus-within, :active) {
border: var(--cell-editing-border);
}
}
}
:global(.ag-center-cols-viewport) {
min-height: auto;
}
:global(.ag-checkbox-input-wrapper) {
&:where(:focus-within, :active) {
box-shadow: none;
}
}
:global(.ag-cell-inline-editing) {
box-shadow: none;
&:global(.boolean-cell) {
border: var(--cell-editing-border) !important;
&:global(.ag-cell-focus) {
background-color: var(--grid-cell-active-background);
}
}
}
:global(.ag-cell-focus) {
background-color: var(--grid-row-selected-background);
}
} }
.footer { .footer {
display: flex; display: flex;
width: 100%; width: 100%;
justify-content: space-between; justify-content: flex-end;
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
padding-right: var(--spacing-xl); padding-right: var(--spacing-xl);

View File

@@ -13,6 +13,6 @@ const props = defineProps<{
<style lang="scss"> <style lang="scss">
.n8n-empty-value { .n8n-empty-value {
font-style: italic; font-style: italic;
color: var(--color-text-lighter); color: var(--color-text-base);
} }
</style> </style>

View File

@@ -6,4 +6,8 @@ export const n8nTheme = themeQuartz.withPart(iconSetAlpine).withParams({
rowVerticalPaddingScale: 0.8, rowVerticalPaddingScale: 0.8,
sidePanelBorder: true, sidePanelBorder: true,
wrapperBorder: true, wrapperBorder: true,
headerColumnBorder: { color: 'var(--color-foreground-base)' },
headerColumnBorderHeight: '100%',
checkboxUncheckedBackgroundColor: 'var(--color-background-light-base)',
checkboxCheckedBackgroundColor: 'var(--color-primary)',
}); });

View File

@@ -6,6 +6,13 @@ export const DATA_STORE_STORE = 'dataStoreStore';
export const DEFAULT_DATA_STORE_PAGE_SIZE = 10; export const DEFAULT_DATA_STORE_PAGE_SIZE = 10;
export const DATA_STORE_ID_COLUMN_WIDTH = 46;
export const DATA_STORE_HEADER_HEIGHT = 36;
export const DATA_STORE_ROW_HEIGHT = 43;
export const ADD_ROW_ROW_ID = '__n8n_add_row__';
export const DATA_STORE_CARD_ACTIONS = { export const DATA_STORE_CARD_ACTIONS = {
RENAME: 'rename', RENAME: 'rename',
DELETE: 'delete', DELETE: 'delete',

View File

@@ -151,7 +151,7 @@ export const insertDataStoreRowApi = async (
row: DataStoreRow, row: DataStoreRow,
projectId: string, projectId: string,
) => { ) => {
return await makeRestApiRequest<number[]>( return await makeRestApiRequest<Array<{ id: number }>>(
context, context,
'POST', 'POST',
`/projects/${projectId}/data-stores/${dataStoreId}/insert`, `/projects/${projectId}/data-stores/${dataStoreId}/insert`,

View File

@@ -191,7 +191,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
emptyRow, emptyRow,
dataStore.projectId, dataStore.projectId,
); );
return inserted[0]; return inserted[0].id;
}; };
const updateRow = async ( const updateRow = async (

View File

@@ -141,7 +141,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const isFoldersFeatureEnabled = computed(() => folders.value.enabled); const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-store')); const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-table'));
const areTagsEnabled = computed(() => const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true, settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,