diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index bec3227f1a..d27f49d3d2 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -2837,7 +2837,7 @@
"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}\"",
+ "dataStore.empty.button.label": "Create Data Table in \"{projectName}\"",
"dataStore.card.size": "{size}MB",
"dataStore.card.column.count": "{count} column | {count} columns",
"dataStore.card.row.count": "{count} record | {count} records",
@@ -2847,9 +2847,9 @@
"dataStore.sort.nameDesc": "Sort by name (Z-A)",
"dataStore.search.placeholder": "Search",
"dataStore.error.fetching": "Error loading data tables",
- "dataStore.add.title": "Create data table",
+ "dataStore.add.title": "Create new data table",
"dataStore.add.description": "Set up a new data table to organize and manage your data.",
- "dataStore.add.button.label": "Create data table",
+ "dataStore.add.button.label": "Create Data Table",
"dataStore.add.input.name.label": "Data Table Name",
"dataStore.add.input.name.placeholder": "Enter data table name",
"dataStore.add.error": "Error creating data table",
diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.test.ts b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.test.ts
index 8955d8f1b0..ed64094b95 100644
--- a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.test.ts
+++ b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.test.ts
@@ -44,10 +44,27 @@ const projectTabsSpy = vi.fn().mockReturnValue({
render: vi.fn(),
});
+const ProjectCreateResourceStub = {
+ props: {
+ actions: Array,
+ },
+ template: `
+
+
+
Workflow
+
Credentials
+
+
+
+
+ `,
+};
+
const renderComponent = createComponentRenderer(ProjectHeader, {
global: {
stubs: {
ProjectTabs: projectTabsSpy,
+ ProjectCreateResource: ProjectCreateResourceStub,
},
},
});
@@ -69,6 +86,7 @@ describe('ProjectHeader', () => {
projectsStore.teamProjectsLimit = -1;
settingsStore.settings.folders = { enabled: false };
+ settingsStore.isDataStoreFeatureEnabled = true;
// Setup default moduleTabs structure
uiStore.moduleTabs = {
@@ -436,4 +454,21 @@ describe('ProjectHeader', () => {
);
});
});
+
+ describe('ProjectCreateResource', () => {
+ it('should render menu items', () => {
+ const { getByTestId } = renderComponent();
+ const actionsContainer = getByTestId('add-resource-actions');
+ expect(actionsContainer).toBeInTheDocument();
+ expect(actionsContainer.children).toHaveLength(2);
+ });
+
+ it('should not render datastore menu item if data store feature is disabled', () => {
+ settingsStore.isDataStoreFeatureEnabled = false;
+ const { getByTestId } = renderComponent();
+ const actionsContainer = getByTestId('add-resource-actions');
+ expect(actionsContainer).toBeInTheDocument();
+ expect(actionsContainer.children).toHaveLength(1);
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
index b3e518a131..d2c71a3fa8 100644
--- a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
+++ b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
@@ -20,12 +20,7 @@ import { type IconName } from '@n8n/design-system/components/N8nIcon/icons';
import type { IUser } from 'n8n-workflow';
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
import { useUIStore } from '@/stores/ui.store';
-
-export type CustomAction = {
- id: string;
- label: string;
- disabled?: boolean;
-};
+import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
const route = useRoute();
const router = useRouter();
@@ -37,17 +32,8 @@ const uiStore = useUIStore();
const projectPages = useProjectPages();
-type Props = {
- customActions?: CustomAction[];
-};
-
-const props = withDefaults(defineProps(), {
- customActions: () => [],
-});
-
const emit = defineEmits<{
createFolder: [];
- customActionSelected: [actionId: string, projectId: string];
}>();
const headerIcon = computed((): IconOrEmoji => {
@@ -122,6 +108,7 @@ const ACTION_TYPES = {
WORKFLOW: 'workflow',
CREDENTIAL: 'credential',
FOLDER: 'folder',
+ DATA_STORE: 'dataStore',
} as const;
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
@@ -155,16 +142,17 @@ const menu = computed(() => {
});
}
- // Append custom actions
- if (props.customActions?.length) {
- props.customActions.forEach((customAction) => {
- items.push({
- value: customAction.id,
- label: customAction.label,
- disabled: customAction.disabled ?? false,
- });
+ if (settingsStore.isDataStoreFeatureEnabled) {
+ // TODO: this should probably be moved to the module descriptor as a setting
+ items.push({
+ value: ACTION_TYPES.DATA_STORE,
+ label: i18n.baseText('dataStore.add.button.label'),
+ disabled:
+ sourceControlStore.preferences.branchReadOnly ||
+ !getResourcePermissions(homeProject.value?.scopes)?.dataStore?.create,
});
}
+
return items;
});
@@ -196,6 +184,12 @@ const actions: Record void> = {
[ACTION_TYPES.FOLDER]: () => {
emit('createFolder');
},
+ [ACTION_TYPES.DATA_STORE]: (projectId: string) => {
+ void router.push({
+ name: PROJECT_DATA_STORES,
+ params: { projectId, new: 'new' },
+ });
+ },
} as const;
const pageType = computed(() => {
@@ -267,12 +261,6 @@ const onSelect = (action: string) => {
return;
}
- // Check if this is a custom action
- if (!executableAction) {
- emit('customActionSelected', action, homeProject.value.id);
- return;
- }
-
executableAction(homeProject.value.id);
};
diff --git a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.vue b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.vue
index 6ddf34be85..6b07d518a6 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.vue
+++ b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.vue
@@ -1,13 +1,12 @@
{
@update:pagination-and-sort="onPaginationUpdate"
>
-
+
{
const onSubmit = async () => {
try {
- await dataStoreStore.createDataStore(dataStoreName.value, route.params.projectId as string);
- } catch (error) {
- toast.showError(error, i18n.baseText('dataStore.add.error'));
- } finally {
+ const newDataStore = await dataStoreStore.createDataStore(
+ dataStoreName.value,
+ route.params.projectId as string,
+ );
+ void router.push({
+ name: DATA_STORE_DETAILS,
+ params: {
+ id: newDataStore.id,
+ },
+ });
dataStoreName.value = '';
uiStore.closeModal(props.modalName);
+ } catch (error) {
+ toast.showError(error, i18n.baseText('dataStore.add.error'));
}
};
+
+const onCancel = () => {
+ uiStore.closeModal(props.modalName);
+ redirectToDataStores();
+};
+
+const redirectToDataStores = () => {
+ void router.replace({ name: PROJECT_DATA_STORES });
+};
-
+
{{ i18n.baseText('dataStore.add.title') }}
@@ -66,7 +85,7 @@ const onSubmit = async () => {
-
+
{
type="secondary"
:label="i18n.baseText('generic.cancel')"
data-test-id="cancel-add-data-store-button"
- @click="close"
+ @click="onCancel"
/>
diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.test.ts b/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.test.ts
index 01bdbf1042..7be7c5fabc 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.test.ts
+++ b/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.test.ts
@@ -96,7 +96,7 @@ describe('DataStoreBreadcrumbs', () => {
});
expect(getByText('Data Stores')).toBeInTheDocument();
- const separators = getAllByText('›');
+ const separators = getAllByText('/');
expect(separators.length).toBeGreaterThan(0);
});
@@ -224,7 +224,7 @@ describe('DataStoreBreadcrumbs', () => {
}),
});
- const separators = getAllByText('›');
+ const separators = getAllByText('/');
expect(separators.length).toBeGreaterThan(0);
});
});
diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.vue b/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.vue
index 5c665b7d02..4a9ef0ed06 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.vue
+++ b/packages/frontend/editor-ui/src/features/dataStore/components/DataStoreBreadcrumbs.vue
@@ -9,7 +9,7 @@ import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useToast } from '@/composables/useToast';
-const BREADCRUMBS_SEPARATOR = '›';
+const BREADCRUMBS_SEPARATOR = '/';
type Props = {
dataStore: DataStore;
diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue
index 5851752ed7..22607d0a07 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue
+++ b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue
@@ -82,7 +82,7 @@ const emit = defineEmits<{
const i18n = useI18n();
const toast = useToast();
const message = useMessage();
-const dataStoreTypes = useDataStoreTypes();
+const { getDefaultValueForType, mapToAGCellType } = useDataStoreTypes();
const dataStoreStore = useDataStoreStore();
@@ -142,22 +142,6 @@ const setPageSize = async (size: number) => {
await fetchDataStoreContent();
};
-const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
- try {
- const newColumn = await dataStoreStore.addDataStoreColumn(
- props.dataStore.id,
- props.dataStore.projectId,
- column,
- );
- if (!newColumn) {
- throw new Error(i18n.baseText('generic.unknownError'));
- }
- colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
- } catch (error) {
- toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
- }
-};
-
const onDeleteColumn = async (columnId: string) => {
if (!gridApi.value) return;
@@ -183,7 +167,7 @@ const onDeleteColumn = async (columnId: string) => {
colDefs.value = colDefs.value.filter((def) => def.colId !== columnId);
const rowDataOldValue = [...rowData.value];
rowData.value = rowData.value.map((row) => {
- const { [columnToDelete.field ?? '']: _, ...rest } = row;
+ const { [columnToDelete.field!]: _, ...rest } = row;
return rest;
});
refreshGridData();
@@ -201,7 +185,26 @@ const onDeleteColumn = async (columnId: string) => {
}
};
-// TODO: Split this up to create column def based on type
+const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
+ try {
+ const newColumn = await dataStoreStore.addDataStoreColumn(
+ props.dataStore.id,
+ props.dataStore.projectId,
+ column,
+ );
+ if (!newColumn) {
+ throw new Error(i18n.baseText('generic.unknownError'));
+ }
+ colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
+ rowData.value = rowData.value.map((row) => {
+ return { ...row, [newColumn.name]: getDefaultValueForType(newColumn.type) };
+ });
+ refreshGridData();
+ } catch (error) {
+ toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
+ }
+};
+
const createColumnDef = (col: DataStoreColumn, extraProps: Partial = {}) => {
const columnDef: ColDef = {
colId: col.id,
@@ -209,11 +212,11 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial = {})
headerName: col.name,
editable: true,
resizable: true,
+ lockPinned: true,
headerComponent: ColumnHeader,
cellEditorPopup: false,
headerComponentParams: { onDelete: onDeleteColumn },
- ...extraProps,
- cellDataType: dataStoreTypes.mapToAGCellType(col.type),
+ cellDataType: mapToAGCellType(col.type),
valueGetter: (params: ValueGetterParams) => {
// If the value is null, return null to show empty cell
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
@@ -284,7 +287,10 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial = {})
columnDef.cellEditor = 'agDateCellEditor';
columnDef.cellEditorPopup = true;
}
- return columnDef;
+ return {
+ ...columnDef,
+ ...extraProps,
+ };
};
const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
@@ -555,17 +561,19 @@ const onCellEditingStopped = (params: CellEditingStoppedEvent) =>
--ag-font-family: var(--font-family);
--ag-font-size: var(--font-size-xs);
--ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px);
- --ag-header-background-color: var(--color-background-base);
+ --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-foreground-color: var(--color-text-dark);
--ag-cell-horizontal-padding: var(--spacing-2xs);
- --ag-header-column-resize-handle-color: var(--border-color-base);
+ --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);
:global(.ag-header-cell-resize) {
- width: var(--spacing-4xs);
+ width: var(--spacing-xs);
+ // this is needed so that we compensate for the width
+ right: -7px;
}
// Don't show borders for the checkbox cells
diff --git a/packages/frontend/editor-ui/src/features/dataStore/module.descriptor.ts b/packages/frontend/editor-ui/src/features/dataStore/module.descriptor.ts
index 4f5fc6e391..fe3bb01a09 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/module.descriptor.ts
+++ b/packages/frontend/editor-ui/src/features/dataStore/module.descriptor.ts
@@ -40,7 +40,7 @@ export const DataStoreModule: FrontendModuleDescription = {
},
{
name: PROJECT_DATA_STORES,
- path: 'datatables',
+ path: 'datatables/:new(new)?',
props: true,
components: {
default: DataStoreView,
diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts
index c787da8de1..8ee24fdbce 100644
--- a/packages/frontend/editor-ui/src/stores/settings.store.ts
+++ b/packages/frontend/editor-ui/src/stores/settings.store.ts
@@ -98,7 +98,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const activeModules = computed(() => settings.value.activeModules);
const isModuleActive = (moduleName: string) => {
- return activeModules.value.includes(moduleName);
+ return activeModules.value?.includes(moduleName);
};
const partialExecutionVersion = computed<1 | 2>(() => {
@@ -144,6 +144,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
+ const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-store'));
+
const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
);
@@ -396,5 +398,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isMFAEnforced,
activeModules,
isModuleActive,
+ isDataStoreFeatureEnabled,
};
});