mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +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 {
|
.icon {
|
||||||
|
display: flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-right: var(--spacing-2xs);
|
margin-right: var(--spacing-2xs);
|
||||||
|
|
||||||
|
|||||||
@@ -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'] {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
@@ -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,22 +166,11 @@ 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);
|
||||||
@@ -190,12 +184,6 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
|||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -206,4 +194,10 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
|||||||
.error-tooltip {
|
.error-tooltip {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.add-column-option-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
</style>
|
</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 { 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', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.ag-root-wrapper) {
|
||||||
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-column-popover {
|
:global(.id-column) {
|
||||||
display: flex;
|
color: var(--color-text-light);
|
||||||
position: absolute;
|
justify-content: center;
|
||||||
right: -47px;
|
}
|
||||||
|
|
||||||
|
: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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user