mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Data table UI redesign (no-changelog) (#18814)
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
@@ -199,6 +199,7 @@ defineExpose({ open, close });
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
margin-right: var(--spacing-2xs);
|
||||
|
||||
|
||||
@@ -520,6 +520,9 @@
|
||||
--color-menu-background: var(--p-gray-740);
|
||||
--color-menu-hover-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'] {
|
||||
|
||||
@@ -686,6 +686,9 @@
|
||||
// Params
|
||||
--color-icon-base: var(--color-text-light);
|
||||
--color-icon-hover: var(--p-color-primary-320);
|
||||
|
||||
/* Ag Grid */
|
||||
--grid-row-selected-background: var(--p-color-secondary-070);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -2834,7 +2834,7 @@
|
||||
"contextual.users.settings.unavailable.button.cloud": "Upgrade now",
|
||||
"contextual.feature.unavailable.title": "Available on the Enterprise 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.description": "Once you create data tables for your projects, they will appear here",
|
||||
"dataStore.empty.button.label": "Create Data Table in \"{projectName}\"",
|
||||
@@ -3004,8 +3004,10 @@
|
||||
"settings.mfa.updateConfiguration": "MFA configuration updated",
|
||||
"settings.mfa.invalidAuthenticatorCode": "Invalid authenticator code",
|
||||
"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.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.create.workflow": "Create Workflow",
|
||||
"projects.header.create.credential": "Create Credential",
|
||||
|
||||
@@ -123,6 +123,7 @@ describe('ProjectHeader', () => {
|
||||
});
|
||||
|
||||
it('Overview: should render the correct title and subtitle', async () => {
|
||||
settingsStore.isDataStoreFeatureEnabled = false;
|
||||
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(true);
|
||||
const { getByTestId, rerender } = renderComponent();
|
||||
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 () => {
|
||||
settingsStore.isDataStoreFeatureEnabled = false;
|
||||
vi.spyOn(projectPages, 'isOverviewSubPage', 'get').mockReturnValue(false);
|
||||
vi.spyOn(projectPages, 'isSharedSubPage', 'get').mockReturnValue(false);
|
||||
const { getByTestId, rerender } = renderComponent();
|
||||
|
||||
@@ -241,9 +241,17 @@ const sectionDescription = computed(() => {
|
||||
if (projectPages.isSharedSubPage) {
|
||||
return i18n.baseText('projects.header.shared.subtitle');
|
||||
} 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) {
|
||||
return i18n.baseText('projects.header.personal.subtitle');
|
||||
return i18n.baseText(
|
||||
settingsStore.isDataStoreFeatureEnabled
|
||||
? 'projects.header.personal.subtitleWithDataTables'
|
||||
: 'projects.header.personal.subtitle',
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
@@ -10,6 +10,7 @@ import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBread
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import DataStoreTable from './components/dataGrid/DataStoreTable.vue';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import AddColumnButton from './components/dataGrid/AddColumnButton.vue';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
@@ -28,6 +29,8 @@ const dataStoreStore = useDataStoreStore();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dataStore = ref<DataStore | null>(null);
|
||||
const dataStoreTableRef = ref<InstanceType<typeof DataStoreTable>>();
|
||||
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const showErrorAndGoBackToList = async (error: unknown) => {
|
||||
@@ -79,6 +82,10 @@ const onToggleSave = (value: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onAddColumn = (column: DataStoreColumnCreatePayload) => {
|
||||
dataStoreTableRef.value?.addColumn(column);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||
await initialize();
|
||||
@@ -104,9 +111,23 @@ onMounted(async () => {
|
||||
<n8n-spinner />
|
||||
<n8n-text>{{ i18n.baseText('generic.saving') }}...</n8n-text>
|
||||
</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 :class="$style.content">
|
||||
<DataStoreTable :data-store="dataStore" @toggle-save="onToggleSave" />
|
||||
<DataStoreTable
|
||||
ref="dataStoreTableRef"
|
||||
:data-store="dataStore"
|
||||
@toggle-save="onToggleSave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,15 +139,11 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
box-sizing: border-box;
|
||||
align-content: start;
|
||||
padding: var(--spacing-l) var(--spacing-2xl) 0;
|
||||
}
|
||||
|
||||
.header-loading {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
div {
|
||||
height: 2em;
|
||||
}
|
||||
@@ -136,7 +153,11 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.header,
|
||||
.header-loading {
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
||||
.saving {
|
||||
@@ -145,4 +166,10 @@ onMounted(async () => {
|
||||
gap: var(--spacing-3xs);
|
||||
margin-top: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3xs);
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -97,6 +97,7 @@ watch(
|
||||
<n8n-breadcrumbs
|
||||
:items="breadcrumbs"
|
||||
:separator="BREADCRUMBS_SEPARATOR"
|
||||
:highlight-last-item="false"
|
||||
@item-selected="onItemClicked"
|
||||
>
|
||||
<template #prepend>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { createPinia, setActivePinia } from 'pinia';
|
||||
import { MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/constants';
|
||||
@@ -42,8 +42,15 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AddColumnPopover', () => {
|
||||
const renderComponent = createComponentRenderer(AddColumnPopover);
|
||||
describe('AddColumnButton', () => {
|
||||
const addColumnHandler = vi.fn();
|
||||
const renderComponent = createComponentRenderer(AddColumnButton, {
|
||||
props: {
|
||||
params: {
|
||||
onAddColumn: addColumnHandler,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
@@ -66,8 +73,8 @@ describe('AddColumnPopover', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit addColumn event with correct payload', async () => {
|
||||
const { getByTestId, getByPlaceholderText, emitted } = renderComponent();
|
||||
it('should call addColumn with correct payload', async () => {
|
||||
const { getByTestId, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
@@ -79,15 +86,10 @@ describe('AddColumnPopover', () => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
expect(emitted().addColumn).toBeTruthy();
|
||||
expect(emitted().addColumn[0]).toEqual([
|
||||
{
|
||||
column: {
|
||||
name: 'newColumn',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(addColumnHandler).toHaveBeenCalledWith({
|
||||
name: 'newColumn',
|
||||
type: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable submit button when name is empty', async () => {
|
||||
@@ -173,13 +175,13 @@ describe('AddColumnPopover', () => {
|
||||
|
||||
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 () => {
|
||||
const { getByPlaceholderText, getByRole, getByText, getByTestId, emitted } = renderComponent();
|
||||
const { getByPlaceholderText, getByRole, getByText, getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
@@ -198,15 +200,10 @@ describe('AddColumnPopover', () => {
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
expect(emitted().addColumn).toBeTruthy();
|
||||
expect(emitted().addColumn[0]).toEqual([
|
||||
{
|
||||
column: {
|
||||
name: 'numberColumn',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(addColumnHandler).toHaveBeenCalledWith({
|
||||
name: 'numberColumn',
|
||||
type: 'number',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset form after successful submission', async () => {
|
||||
@@ -215,7 +212,7 @@ describe('AddColumnPopover', () => {
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
const nameInput = getByPlaceholderText('Enter column name') as HTMLInputElement;
|
||||
const nameInput = getByPlaceholderText('Enter column name');
|
||||
await fireEvent.update(nameInput, 'testColumn');
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
@@ -225,8 +222,8 @@ describe('AddColumnPopover', () => {
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const resetNameInput = getByPlaceholderText('Enter column name') as HTMLInputElement;
|
||||
expect(resetNameInput.value).toBe('');
|
||||
const resetNameInput = getByPlaceholderText('Enter column name');
|
||||
expect((resetNameInput as HTMLInputElement).value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,7 +245,7 @@ describe('AddColumnPopover', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
@@ -257,15 +254,10 @@ describe('AddColumnPopover', () => {
|
||||
await fireEvent.update(nameInput, 'enterColumn');
|
||||
await fireEvent.keyUp(nameInput, { key: 'Enter' });
|
||||
|
||||
expect(emitted().addColumn).toBeTruthy();
|
||||
expect(emitted().addColumn[0]).toEqual([
|
||||
{
|
||||
column: {
|
||||
name: 'enterColumn',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(addColumnHandler).toHaveBeenCalledWith({
|
||||
name: 'enterColumn',
|
||||
type: 'string',
|
||||
});
|
||||
});
|
||||
|
||||
it('should display all column type options', async () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import type {
|
||||
DataStoreColumnCreatePayload,
|
||||
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 { useDebounce } from '@/composables/useDebounce';
|
||||
|
||||
const emit = defineEmits<{
|
||||
addColumn: [
|
||||
value: {
|
||||
column: DataStoreColumnCreatePayload;
|
||||
},
|
||||
];
|
||||
const props = defineProps<{
|
||||
// the params key is needed so that we can pass this directly to ag-grid as column
|
||||
params: {
|
||||
onAddColumn: (column: DataStoreColumnCreatePayload) => void;
|
||||
};
|
||||
popoverId?: string;
|
||||
useTextTrigger?: boolean;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
@@ -35,16 +36,13 @@ const error = ref<string | null>(null);
|
||||
const popoverOpen = ref(false);
|
||||
const isSelectOpen = ref(false);
|
||||
|
||||
const popoverId = computed(() => props.popoverId ?? 'add-column-popover');
|
||||
|
||||
const onAddButtonClicked = () => {
|
||||
if (!columnName.value || !columnType.value) {
|
||||
return;
|
||||
}
|
||||
emit('addColumn', {
|
||||
column: {
|
||||
name: columnName.value,
|
||||
type: columnType.value,
|
||||
},
|
||||
});
|
||||
props.params.onAddColumn({ name: columnName.value, type: columnType.value });
|
||||
columnName.value = '';
|
||||
columnType.value = 'string';
|
||||
popoverOpen.value = false;
|
||||
@@ -78,25 +76,32 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
|
||||
<template>
|
||||
<N8nTooltip :disabled="popoverOpen" :content="i18n.baseText('dataStore.addColumn.label')">
|
||||
<div :class="$style.wrapper">
|
||||
<div class="add-column-header-component-wrapper">
|
||||
<N8nPopoverReka
|
||||
id="add-column-popover"
|
||||
:id="popoverId"
|
||||
:open="popoverOpen"
|
||||
:popper-options="{ strategy: 'fixed' }"
|
||||
:show-arrow="false"
|
||||
@update:open="handlePopoverOpenChange"
|
||||
>
|
||||
<template #trigger>
|
||||
<N8nIconButton
|
||||
data-test-id="data-store-add-column-trigger-button"
|
||||
text
|
||||
icon="plus"
|
||||
type="tertiary"
|
||||
/>
|
||||
<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
|
||||
data-test-id="data-store-add-column-trigger-button"
|
||||
text
|
||||
icon="plus"
|
||||
type="tertiary"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="$style['popover-content']">
|
||||
<div :class="$style['popover-body']">
|
||||
<div class="add-ds-column-header-popover-content">
|
||||
<div class="popover-body">
|
||||
<N8nInputLabel
|
||||
:label="i18n.baseText('dataStore.addColumn.nameInput.label')"
|
||||
:required="true"
|
||||
@@ -110,7 +115,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
@keyup.enter="onAddButtonClicked"
|
||||
@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">
|
||||
{{ error }}
|
||||
</n8n-text>
|
||||
@@ -118,7 +123,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
<N8nIcon
|
||||
icon="circle-help"
|
||||
size="small"
|
||||
:class="$style['error-tooltip']"
|
||||
class="error-tooltip"
|
||||
color="text-base"
|
||||
data-test-id="add-column-error-help-icon"
|
||||
/>
|
||||
@@ -128,15 +133,15 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
<N8nInputLabel
|
||||
:label="i18n.baseText('dataStore.addColumn.typeInput.label')"
|
||||
:required="true"
|
||||
:class="$style['type-label']"
|
||||
class="type-label"
|
||||
>
|
||||
<N8nSelect
|
||||
v-model="columnType"
|
||||
append-to="#add-column-popover"
|
||||
:append-to="`#${popoverId}`"
|
||||
@visible-change="isSelectOpen = $event"
|
||||
>
|
||||
<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)" />
|
||||
<N8nText>{{ type }}</N8nText>
|
||||
</div>
|
||||
@@ -161,49 +166,38 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.wrapper {
|
||||
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 {
|
||||
<style lang="scss">
|
||||
.add-ds-column-header-popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
padding: var(--spacing-2xs);
|
||||
border-bottom: var(--border-base);
|
||||
}
|
||||
.popover-header {
|
||||
padding: var(--spacing-2xs);
|
||||
border-bottom: var(--border-base);
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
padding: var(--spacing-xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.popover-body {
|
||||
padding: var(--spacing-xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.option-content {
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4xs);
|
||||
color: var(--color-text-danger);
|
||||
}
|
||||
|
||||
.error-tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.add-column-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4xs);
|
||||
color: var(--color-text-danger);
|
||||
}
|
||||
|
||||
.error-tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DataStoreTable from '@/features/dataStore/components/dataGrid/DataStoreTable.vue';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
@@ -44,6 +43,7 @@ vi.mock('ag-grid-community', () => ({
|
||||
ClientSideRowModelApiModule: {},
|
||||
ValidationModule: {},
|
||||
UndoRedoEditModule: {},
|
||||
CellStyleModule: {},
|
||||
}));
|
||||
|
||||
// Mock the n8n theme
|
||||
@@ -137,47 +137,16 @@ describe('DataStoreTable', () => {
|
||||
});
|
||||
|
||||
describe('Component Initialization', () => {
|
||||
it('should render the component with AG Grid and AddColumnPopover', () => {
|
||||
it('should render the component with AG Grid', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('ag-grid-vue')).toBeInTheDocument();
|
||||
expect(getByTestId('add-column-popover')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pagination controls', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -38,18 +38,28 @@ import {
|
||||
ClientSideRowModelApiModule,
|
||||
ValidationModule,
|
||||
UndoRedoEditModule,
|
||||
CellStyleModule,
|
||||
} from 'ag-grid-community';
|
||||
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 { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
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 { MODAL_CONFIRM } from '@/constants';
|
||||
import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
|
||||
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 NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
@@ -68,6 +78,7 @@ ModuleRegistry.registerModules([
|
||||
DateEditorModule,
|
||||
ClientSideRowModelApiModule,
|
||||
UndoRedoEditModule,
|
||||
CellStyleModule,
|
||||
]);
|
||||
|
||||
type Props = {
|
||||
@@ -83,7 +94,7 @@ const emit = defineEmits<{
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const { getDefaultValueForType, mapToAGCellType } = useDataStoreTypes();
|
||||
const { mapToAGCellType } = useDataStoreTypes();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
|
||||
@@ -94,7 +105,8 @@ const rowData = ref<DataStoreRow[]>([]);
|
||||
const rowSelection: RowSelectionOptions | 'single' | 'multiple' = {
|
||||
mode: 'multiRow',
|
||||
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);
|
||||
@@ -106,13 +118,6 @@ const isTextEditorOpen = ref(false);
|
||||
|
||||
const gridContainer = useTemplateRef('gridContainer');
|
||||
|
||||
// Shared config for all columns
|
||||
const defaultColumnDef: ColDef = {
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
filter: false,
|
||||
};
|
||||
|
||||
// Pagination
|
||||
const pageSizeOptions = [10, 20, 50];
|
||||
const currentPage = ref(1);
|
||||
@@ -132,7 +137,12 @@ const onGridReady = (params: GridReadyEvent) => {
|
||||
const refreshGridData = () => {
|
||||
if (gridApi.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 {
|
||||
const newColumn = await dataStoreStore.addDataStoreColumn(
|
||||
props.dataStore.id,
|
||||
@@ -199,9 +209,13 @@ const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload })
|
||||
if (!newColumn) {
|
||||
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) => {
|
||||
return { ...row, [newColumn.name]: getDefaultValueForType(newColumn.type) };
|
||||
return { ...row, [newColumn.name]: null };
|
||||
});
|
||||
refreshGridData();
|
||||
} catch (error) {
|
||||
@@ -214,13 +228,24 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
||||
colId: col.id,
|
||||
field: col.name,
|
||||
headerName: col.name,
|
||||
editable: true,
|
||||
sortable: false,
|
||||
flex: 1,
|
||||
editable: (params) => params.data?.id !== ADD_ROW_ROW_ID,
|
||||
resizable: true,
|
||||
lockPinned: true,
|
||||
headerComponent: ColumnHeader,
|
||||
cellEditorPopup: false,
|
||||
headerComponentParams: { onDelete: onDeleteColumn },
|
||||
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>) => {
|
||||
// If the value is null, return null to show empty cell
|
||||
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];
|
||||
},
|
||||
cellRendererSelector: (params: ICellRendererParams) => {
|
||||
if (params.data?.id === ADD_ROW_ROW_ID || col.id === 'add-column') {
|
||||
return {};
|
||||
}
|
||||
let rowValue = params.data?.[col.name];
|
||||
// When adding new column, rowValue is undefined (same below, in string cell editor)
|
||||
if (rowValue === undefined) {
|
||||
@@ -289,7 +317,7 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
||||
// Setup date editor
|
||||
if (col.type === 'date') {
|
||||
columnDef.cellEditor = 'agDateCellEditor';
|
||||
columnDef.cellEditorPopup = true;
|
||||
columnDef.cellEditorPopup = false;
|
||||
}
|
||||
return {
|
||||
...columnDef,
|
||||
@@ -324,8 +352,8 @@ const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
|
||||
const onAddRowClick = async () => {
|
||||
try {
|
||||
// Go to last page if we are not there already
|
||||
if (currentPage.value * pageSize.value < totalItems.value) {
|
||||
await setCurrentPage(Math.ceil(totalItems.value / pageSize.value));
|
||||
if (currentPage.value * pageSize.value < totalItems.value + 1) {
|
||||
await setCurrentPage(Math.ceil((totalItems.value + 1) / pageSize.value));
|
||||
}
|
||||
contentLoading.value = true;
|
||||
emit('toggleSave', true);
|
||||
@@ -335,7 +363,9 @@ const onAddRowClick = async () => {
|
||||
props.dataStore.columns.forEach((col) => {
|
||||
newRow[col.name] = null;
|
||||
});
|
||||
rows.value.push(newRow);
|
||||
rowData.value.push(newRow);
|
||||
totalItems.value += 1;
|
||||
refreshGridData();
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.addRow.error'));
|
||||
} finally {
|
||||
@@ -360,10 +390,42 @@ const initColumnDefinitions = () => {
|
||||
suppressMovable: true,
|
||||
headerComponent: null,
|
||||
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
|
||||
...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 clickedCellRow = params.rowIndex;
|
||||
|
||||
// Skip if rowIndex is null
|
||||
if (clickedCellRow === null) return;
|
||||
if (
|
||||
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
|
||||
const wasAlreadyFocused =
|
||||
@@ -437,10 +502,9 @@ const fetchDataStoreContent = async () => {
|
||||
currentPage.value,
|
||||
pageSize.value,
|
||||
);
|
||||
rows.value = fetchedRows.data;
|
||||
rowData.value = fetchedRows.data;
|
||||
totalItems.value = fetchedRows.count;
|
||||
rowData.value = rows.value;
|
||||
|
||||
refreshGridData();
|
||||
handleClearSelection();
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.fetchContent.error'));
|
||||
@@ -544,6 +608,11 @@ const handleClearSelection = () => {
|
||||
gridApi.value.deselectAll();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addRow: onAddRowClick,
|
||||
addColumn: onAddColumn,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -551,12 +620,9 @@ const handleClearSelection = () => {
|
||||
<div ref="gridContainer" :class="$style['grid-container']" data-test-id="data-store-grid">
|
||||
<AgGridVue
|
||||
style="width: 100%"
|
||||
:row-data="rowData"
|
||||
:column-defs="colDefs"
|
||||
:default-col-def="defaultColumnDef"
|
||||
:dom-layout="'autoHeight'"
|
||||
:row-height="36"
|
||||
:header-height="36"
|
||||
:row-height="DATA_STORE_ROW_HEIGHT"
|
||||
:header-height="DATA_STORE_HEADER_HEIGHT"
|
||||
:animate-rows="false"
|
||||
:theme="n8nTheme"
|
||||
:suppress-drag-leave-hides-columns="true"
|
||||
@@ -574,22 +640,8 @@ const handleClearSelection = () => {
|
||||
@column-header-clicked="resetLastFocusedCell"
|
||||
@selection-changed="onSelectionChanged"
|
||||
/>
|
||||
<AddColumnPopover
|
||||
:data-store="props.dataStore"
|
||||
:class="$style['add-column-popover']"
|
||||
@add-column="onAddColumn"
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
@@ -615,7 +667,6 @@ const handleClearSelection = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-m);
|
||||
width: calc(100% - var(--spacing-m) * 2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -626,22 +677,35 @@ const handleClearSelection = () => {
|
||||
|
||||
// AG Grid style overrides
|
||||
--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-border-color: var(--border-color-base);
|
||||
--ag-border-radius: var(--border-radius-base);
|
||||
--ag-wrapper-border-radius: 0;
|
||||
--ag-font-family: var(--font-family);
|
||||
--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-header-background-color: var(--color-background-light-base);
|
||||
--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-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-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) {
|
||||
width: var(--spacing-xs);
|
||||
@@ -649,26 +713,101 @@ const handleClearSelection = () => {
|
||||
right: -7px;
|
||||
}
|
||||
|
||||
// Don't show borders for the checkbox cells
|
||||
:global(.ag-cell[col-id='ag-Grid-SelectionColumn']) {
|
||||
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) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add-column-popover {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: -47px;
|
||||
:global(.ag-root-wrapper) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
: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 {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: var(--spacing-l);
|
||||
padding-right: var(--spacing-xl);
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@ const props = defineProps<{
|
||||
<style lang="scss">
|
||||
.n8n-empty-value {
|
||||
font-style: italic;
|
||||
color: var(--color-text-lighter);
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,4 +6,8 @@ export const n8nTheme = themeQuartz.withPart(iconSetAlpine).withParams({
|
||||
rowVerticalPaddingScale: 0.8,
|
||||
sidePanelBorder: true,
|
||||
wrapperBorder: true,
|
||||
headerColumnBorder: { color: 'var(--color-foreground-base)' },
|
||||
headerColumnBorderHeight: '100%',
|
||||
checkboxUncheckedBackgroundColor: 'var(--color-background-light-base)',
|
||||
checkboxCheckedBackgroundColor: 'var(--color-primary)',
|
||||
});
|
||||
|
||||
@@ -6,6 +6,13 @@ export const DATA_STORE_STORE = 'dataStoreStore';
|
||||
|
||||
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 = {
|
||||
RENAME: 'rename',
|
||||
DELETE: 'delete',
|
||||
|
||||
@@ -151,7 +151,7 @@ export const insertDataStoreRowApi = async (
|
||||
row: DataStoreRow,
|
||||
projectId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<number[]>(
|
||||
return await makeRestApiRequest<Array<{ id: number }>>(
|
||||
context,
|
||||
'POST',
|
||||
`/projects/${projectId}/data-stores/${dataStoreId}/insert`,
|
||||
|
||||
@@ -191,7 +191,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
emptyRow,
|
||||
dataStore.projectId,
|
||||
);
|
||||
return inserted[0];
|
||||
return inserted[0].id;
|
||||
};
|
||||
|
||||
const updateRow = async (
|
||||
|
||||
@@ -141,7 +141,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
|
||||
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
|
||||
|
||||
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-store'));
|
||||
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-table'));
|
||||
|
||||
const areTagsEnabled = computed(() =>
|
||||
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
||||
|
||||
Reference in New Issue
Block a user