mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
chore(editor): Rename data store to data table in FE files (no-changelog) (#20427)
This commit is contained in:
@@ -56,8 +56,8 @@ describe('DismissBannerRequestDto', () => {
|
||||
'TRIAL',
|
||||
'NON_PRODUCTION_LICENSE',
|
||||
'EMAIL_CONFIRMATION',
|
||||
'DATA_STORE_STORAGE_LIMIT_WARNING',
|
||||
'DATA_STORE_STORAGE_LIMIT_ERROR',
|
||||
'DATA_TABLE_STORAGE_LIMIT_WARNING',
|
||||
'DATA_TABLE_STORAGE_LIMIT_ERROR',
|
||||
];
|
||||
|
||||
expect(bannerNameSchema.options).toEqual(expectedBanners);
|
||||
|
||||
@@ -6,8 +6,8 @@ export const bannerNameSchema = z.enum([
|
||||
'TRIAL',
|
||||
'NON_PRODUCTION_LICENSE',
|
||||
'EMAIL_CONFIRMATION',
|
||||
'DATA_STORE_STORAGE_LIMIT_WARNING',
|
||||
'DATA_STORE_STORAGE_LIMIT_ERROR',
|
||||
'DATA_TABLE_STORAGE_LIMIT_WARNING',
|
||||
'DATA_TABLE_STORAGE_LIMIT_ERROR',
|
||||
]);
|
||||
|
||||
export type BannerName = z.infer<typeof bannerNameSchema>;
|
||||
|
||||
@@ -2925,69 +2925,63 @@
|
||||
"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.empty.label": "You don't have any data tables yet",
|
||||
"dataStore.empty.description": "Use data tables to persist execution results, share data between workflows, and track metrics for evaluation.",
|
||||
"dataStore.card.size": "{size}MB",
|
||||
"dataStore.card.column.count": "{count} column | {count} columns",
|
||||
"dataStore.card.row.count": "{count} record | {count} records",
|
||||
"dataStore.sort.lastUpdated": "Sort by last updated",
|
||||
"dataStore.sort.lastCreated": "Sort by last created",
|
||||
"dataStore.sort.nameAsc": "Sort by name (A-Z)",
|
||||
"dataStore.sort.nameDesc": "Sort by name (Z-A)",
|
||||
"dataStore.search.placeholder": "Search",
|
||||
"dataStore.error.fetching": "Error loading data tables",
|
||||
"dataStore.add.title": "Create new 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",
|
||||
"dataStore.delete.confirm.title": "Delete Data table",
|
||||
"dataStore.delete.confirm.message": "Are you sure you want to delete the data table '{name}'? This action cannot be undone.",
|
||||
"dataStore.delete.error": "Error deleting data table",
|
||||
"dataStore.rename.error": "Error renaming data table",
|
||||
"dataStore.getDetails.error": "Error fetching data table details",
|
||||
"dataStore.notFound": "Data table not found",
|
||||
"dataStore.noColumns.heading": "No columns yet",
|
||||
"dataStore.noColumns.description": "Add columns to start storing data in this data table.",
|
||||
"dataStore.noColumns.button.label": "Add first column",
|
||||
"dataStore.addColumn.label": "Add Column",
|
||||
"dataStore.addColumn.nameInput.label": "@:_reusableBaseText.name",
|
||||
"dataStore.addColumn.nameInput.placeholder": "Enter column name",
|
||||
"dataStore.addColumn.typeInput.label": "@:_reusableBaseText.type",
|
||||
"dataStore.addColumn.error": "Error adding column",
|
||||
"dataStore.addColumn.alreadyExistsError": "This column already exists",
|
||||
"dataStore.addColumn.alreadyExistsDescription": "Column name already exists, choose a different name",
|
||||
"dataStore.addColumn.systemColumnDescription": "This is a system column, choose a different name",
|
||||
"dataStore.addColumn.testingColumnDescription": "This column is used for testing, choose a different name",
|
||||
"dataStore.moveColumn.error": "Error moving column",
|
||||
"dataStore.deleteColumn.error": "Error deleting column",
|
||||
"dataStore.deleteColumn.confirm.title": "Delete column",
|
||||
"dataStore.deleteColumn.confirm.message": "Are you sure you want to delete the column '{name}'? This action cannot be undone.",
|
||||
"dataStore.addColumn.invalidName.error": "Invalid column name",
|
||||
"dataStore.addColumn.invalidName.description": "Column names must begin with a letter and can only include letters, numbers, or underscores",
|
||||
"dataStore.fetchContent.error": "Error fetching data store content",
|
||||
"dataStore.addRow.label": "Add Row",
|
||||
"dataStore.addRow.error": "Error adding row",
|
||||
"dataStore.updateRow.error": "Error updating row",
|
||||
"dataStore.deleteRows.title": "Delete Rows",
|
||||
"dataStore.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
||||
"dataStore.deleteRows.error": "Error deleting rows",
|
||||
"dataStore.banner.storageLimitWarning.message": "{usage} of Data tables storage used. Delete data to avoid errors",
|
||||
"dataStore.banner.storageLimitError.message": "{usage} of Data tables storage used, operations may fail. Delete data to avoid errors",
|
||||
"dataStore.error.tableNotInitialized": "Table not initialized",
|
||||
"dataStore.noRows": "No rows",
|
||||
"dataStore.filters.isNull": "Is null",
|
||||
"dataStore.filters.isNotNull": "Is not null",
|
||||
"dataStore.filters.isEmpty": "Is empty",
|
||||
"dataStore.filters.isNotEmpty": "Is not empty",
|
||||
"dataStore.filters.between": "Between",
|
||||
"dataStore.filters.true": "True",
|
||||
"dataStore.filters.false": "False",
|
||||
"dataStore.filters.greaterThan": "Greater than",
|
||||
"dataStore.filters.greaterThanOrEqual": "Greater than or equal",
|
||||
"dataStore.filters.lessThan": "Less than",
|
||||
"dataStore.filters.lessThanOrEqual": "Less than or equal",
|
||||
"dataTable.dataTables": "Data tables",
|
||||
"dataTable.empty.label": "You don't have any data tables yet",
|
||||
"dataTable.empty.description": "Use data tables to persist execution results, share data between workflows, and track metrics for evaluation.",
|
||||
"dataTable.card.size": "{size}MB",
|
||||
"dataTable.card.column.count": "{count} column | {count} columns",
|
||||
"dataTable.add.title": "Create new Data table",
|
||||
"dataTable.add.button.label": "Create Data table",
|
||||
"dataTable.add.input.name.label": "Data table name",
|
||||
"dataTable.add.input.name.placeholder": "Enter data table name",
|
||||
"dataTable.add.error": "Error creating data table",
|
||||
"dataTable.delete.confirm.title": "Delete Data table",
|
||||
"dataTable.delete.confirm.message": "Are you sure you want to delete the data table '{name}'? This action cannot be undone.",
|
||||
"dataTable.delete.error": "Error deleting data table",
|
||||
"dataTable.rename.error": "Error renaming data table",
|
||||
"dataTable.getDetails.error": "Error fetching data table details",
|
||||
"dataTable.notFound": "Data table not found",
|
||||
"dataTable.noColumns.heading": "No columns yet",
|
||||
"dataTable.noColumns.description": "Add columns to start storing data in this data table.",
|
||||
"dataTable.noColumns.button.label": "Add first column",
|
||||
"dataTable.addColumn.label": "Add Column",
|
||||
"dataTable.addColumn.nameInput.label": "@:_reusableBaseText.name",
|
||||
"dataTable.addColumn.nameInput.placeholder": "Enter column name",
|
||||
"dataTable.addColumn.typeInput.label": "@:_reusableBaseText.type",
|
||||
"dataTable.addColumn.error": "Error adding column",
|
||||
"dataTable.addColumn.alreadyExistsError": "This column already exists",
|
||||
"dataTable.addColumn.alreadyExistsDescription": "Column name already exisits, choose a different name",
|
||||
"dataTable.moveColumn.error": "Error moving column",
|
||||
"dataTable.deleteColumn.error": "Error deleting column",
|
||||
"dataTable.deleteColumn.confirm.title": "Delete column",
|
||||
"dataTable.deleteColumn.confirm.message": "Are you sure you want to delete the column '{name}'? This action cannot be undone.",
|
||||
"dataTable.addColumn.invalidName.error": "Invalid column name",
|
||||
"dataTable.addColumn.invalidName.description": "Column names must begin with a letter and can only include letters, numbers, or underscores",
|
||||
"dataTable.fetchContent.error": "Error fetching data table content",
|
||||
"dataTable.addRow.label": "Add Row",
|
||||
"dataTable.addRow.error": "Error adding row",
|
||||
"dataTable.updateRow.error": "Error updating row",
|
||||
"dataTable.deleteRows.title": "Delete Rows",
|
||||
"dataTable.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
||||
"dataTable.deleteRows.error": "Error deleting rows",
|
||||
"dataTable.banner.storageLimitWarning.message": "{usage} of Data tables storage used. Delete data to avoid errors",
|
||||
"dataTable.banner.storageLimitError.message": "{usage} of Data tables storage used, operations may fail. Delete data to avoid errors",
|
||||
"dataTable.error.tableNotInitialized": "Table not initialized",
|
||||
"dataTable.noRows": "No rows",
|
||||
"dataTable.filters.isNull": "Is null",
|
||||
"dataTable.filters.isNotNull": "Is not null",
|
||||
"dataTable.filters.isEmpty": "Is empty",
|
||||
"dataTable.filters.isNotEmpty": "Is not empty",
|
||||
"dataTable.filters.between": "Between",
|
||||
"dataTable.filters.true": "True",
|
||||
"dataTable.filters.false": "False",
|
||||
"dataTable.filters.greaterThan": "Greater than",
|
||||
"dataTable.filters.greaterThanOrEqual": "Greater than or equal",
|
||||
"dataTable.filters.lessThan": "Less than",
|
||||
"dataTable.filters.lessThanOrEqual": "Less than or equal",
|
||||
"dataTable.addColumn.alreadyExistsDescription": "Column name already exists, choose a different name",
|
||||
"dataTable.addColumn.systemColumnDescription": "This is a system column, choose a different name",
|
||||
"dataTable.addColumn.testingColumnDescription": "This column is used for testing, choose a different name",
|
||||
"settings.ldap": "LDAP",
|
||||
"settings.ldap.note": "LDAP allows users to authenticate with their centralized account. It's compatible with services that provide an LDAP interface like Active Directory, Okta and Jumpcloud.",
|
||||
"settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
|
||||
|
||||
@@ -35,7 +35,7 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||
import NodeStorageLimitCallout from '@/features/dataStore/components/NodeStorageLimitCallout.vue';
|
||||
import NodeStorageLimitCallout from '@/features/dataTable/components/NodeStorageLimitCallout.vue';
|
||||
import NodeTitle from '@/components/NodeTitle.vue';
|
||||
import { RenameNodeCommand } from '@/models/history';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
|
||||
@@ -197,7 +197,9 @@ describe('ProjectHeader', () => {
|
||||
|
||||
it('should render ProjectTabs Settings if project is team project and user has update scope', () => {
|
||||
route.params.projectId = '123';
|
||||
projectsStore.currentProject = createTestProject({ scopes: ['project:update'] });
|
||||
projectsStore.currentProject = createTestProject({
|
||||
scopes: ['project:update'],
|
||||
});
|
||||
renderComponent();
|
||||
|
||||
expect(projectTabsSpy).toHaveBeenCalledWith(
|
||||
@@ -210,7 +212,9 @@ describe('ProjectHeader', () => {
|
||||
|
||||
it('should render ProjectTabs without Settings if no project update permission', () => {
|
||||
route.params.projectId = '123';
|
||||
projectsStore.currentProject = createTestProject({ scopes: ['project:read'] });
|
||||
projectsStore.currentProject = createTestProject({
|
||||
scopes: ['project:read'],
|
||||
});
|
||||
renderComponent();
|
||||
|
||||
expect(projectTabsSpy).toHaveBeenCalledWith(
|
||||
@@ -281,7 +285,9 @@ describe('ProjectHeader', () => {
|
||||
});
|
||||
|
||||
it('should not render creation button in setting page', async () => {
|
||||
projectsStore.currentProject = createTestProject({ type: ProjectTypes.Personal });
|
||||
projectsStore.currentProject = createTestProject({
|
||||
type: ProjectTypes.Personal,
|
||||
});
|
||||
vi.spyOn(router, 'useRoute').mockReturnValueOnce({
|
||||
name: VIEWS.PROJECT_SETTINGS,
|
||||
} as RouteLocationNormalizedLoadedGeneric);
|
||||
@@ -467,7 +473,7 @@ describe('ProjectHeader', () => {
|
||||
expect(actionsContainer.children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not render datastore menu item if data store feature is disabled', () => {
|
||||
it('should not render dataTable menu item if data table feature is disabled', () => {
|
||||
settingsStore.isDataTableFeatureEnabled = false;
|
||||
const { getByTestId } = renderComponent();
|
||||
const actionsContainer = getByTestId('add-resource-actions');
|
||||
|
||||
@@ -19,7 +19,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';
|
||||
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||
import { PROJECT_DATA_TABLES } from '@/features/dataTable/constants';
|
||||
import ReadyToRunV2Button from '@/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue';
|
||||
|
||||
import { N8nButton, N8nHeading, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
@@ -113,7 +113,7 @@ const ACTION_TYPES = {
|
||||
WORKFLOW: 'workflow',
|
||||
CREDENTIAL: 'credential',
|
||||
FOLDER: 'folder',
|
||||
DATA_STORE: 'dataStore',
|
||||
DATA_TABLE: 'dataTable',
|
||||
} as const;
|
||||
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
|
||||
|
||||
@@ -150,8 +150,8 @@ const menu = computed(() => {
|
||||
if (settingsStore.isDataTableFeatureEnabled) {
|
||||
// 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'),
|
||||
value: ACTION_TYPES.DATA_TABLE,
|
||||
label: i18n.baseText('dataTable.add.button.label'),
|
||||
disabled:
|
||||
sourceControlStore.preferences.branchReadOnly ||
|
||||
!getResourcePermissions(homeProject.value?.scopes)?.dataStore?.create,
|
||||
@@ -224,9 +224,9 @@ const actions: Record<ActionTypes, (projectId: string) => void> = {
|
||||
[ACTION_TYPES.FOLDER]: () => {
|
||||
emit('createFolder');
|
||||
},
|
||||
[ACTION_TYPES.DATA_STORE]: (projectId: string) => {
|
||||
[ACTION_TYPES.DATA_TABLE]: (projectId: string) => {
|
||||
void router.push({
|
||||
name: PROJECT_DATA_STORES,
|
||||
name: PROJECT_DATA_TABLES,
|
||||
params: { projectId, new: 'new' },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@ import TrialOverBanner from '@/components/banners/TrialOverBanner.vue';
|
||||
import TrialBanner from '@/components/banners/TrialBanner.vue';
|
||||
import V1Banner from '@/components/banners/V1Banner.vue';
|
||||
import EmailConfirmationBanner from '@/components/banners/EmailConfirmationBanner.vue';
|
||||
import DataStoreStorageLimitWarningBanner from '@/components/banners/DataStoreStorageLimitWarningBanner.vue';
|
||||
import DataStoreStorageLimitErrorBanner from '@/components/banners/DataStoreStorageLimitErrorBanner.vue';
|
||||
import DataTableStorageLimitWarningBanner from '@/components/banners/DataTableStorageLimitWarningBanner.vue';
|
||||
import DataTableStorageLimitErrorBanner from '@/components/banners/DataTableStorageLimitErrorBanner.vue';
|
||||
import type { Component } from 'vue';
|
||||
import type { N8nBanners } from '@/Interface';
|
||||
|
||||
@@ -19,13 +19,13 @@ export const N8N_BANNERS: N8nBanners = {
|
||||
EMAIL_CONFIRMATION: { priority: 250, component: EmailConfirmationBanner as Component },
|
||||
TRIAL: { priority: 150, component: TrialBanner as Component },
|
||||
NON_PRODUCTION_LICENSE: { priority: 140, component: NonProductionLicenseBanner as Component },
|
||||
DATA_STORE_STORAGE_LIMIT_WARNING: {
|
||||
DATA_TABLE_STORAGE_LIMIT_WARNING: {
|
||||
priority: 300,
|
||||
component: DataStoreStorageLimitWarningBanner as Component,
|
||||
component: DataTableStorageLimitWarningBanner as Component,
|
||||
},
|
||||
DATA_STORE_STORAGE_LIMIT_ERROR: {
|
||||
DATA_TABLE_STORAGE_LIMIT_ERROR: {
|
||||
priority: 400,
|
||||
component: DataStoreStorageLimitErrorBanner as Component,
|
||||
component: DataTableStorageLimitErrorBanner as Component,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBanner name="DATA_STORE_STORAGE_LIMIT_ERROR" :dismissible="true" theme="danger">
|
||||
<BaseBanner name="DATA_TABLE_STORAGE_LIMIT_ERROR" :dismissible="true" theme="danger">
|
||||
<template #mainContent>
|
||||
<span>{{
|
||||
i18n.baseText('dataStore.banner.storageLimitError.message', {
|
||||
i18n.baseText('dataTable.banner.storageLimitError.message', {
|
||||
interpolate: {
|
||||
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||
usage: `${dataTableStore.dataTableSize} / ${dataTableStore.maxSizeMB}MB`,
|
||||
},
|
||||
})
|
||||
}}</span>
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBanner name="DATA_STORE_STORAGE_LIMIT_WARNING" :dismissible="true" theme="warning">
|
||||
<BaseBanner name="DATA_TABLE_STORAGE_LIMIT_WARNING" :dismissible="true" theme="warning">
|
||||
<template #mainContent>
|
||||
<span>{{
|
||||
i18n.baseText('dataStore.banner.storageLimitWarning.message', {
|
||||
i18n.baseText('dataTable.banner.storageLimitWarning.message', {
|
||||
interpolate: {
|
||||
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||
usage: `${dataTableStore.dataTableSize} / ${dataTableStore.maxSizeMB}MB`,
|
||||
},
|
||||
})
|
||||
}}</span>
|
||||
+12
-12
@@ -2,11 +2,11 @@ import { computed, ref, type Ref } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { DATA_STORE_DETAILS, PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||
import { DATA_TABLE_DETAILS, PROJECT_DATA_TABLES } from '@/features/dataTable/constants';
|
||||
import type { CommandGroup, CommandBarItem } from './types';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
import { N8nIcon } from '@n8n/design-system';
|
||||
|
||||
const ITEM_ID = {
|
||||
@@ -21,13 +21,13 @@ export function useDataTableNavigationCommands(options: {
|
||||
}): CommandGroup {
|
||||
const i18n = useI18n();
|
||||
const { lastQuery, activeNodeId, currentProjectName } = options;
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const dataTableResults = ref<DataStore[]>([]);
|
||||
const dataTableResults = ref<DataTable[]>([]);
|
||||
|
||||
const currentProjectId = computed(() => {
|
||||
return typeof route.params.projectId === 'string'
|
||||
@@ -39,7 +39,7 @@ export function useDataTableNavigationCommands(options: {
|
||||
return projectsStore.myProjects.find((p) => p.type === 'personal')?.id;
|
||||
});
|
||||
|
||||
function orderResultByCurrentProjectFirst<T extends DataStore>(results: T[]) {
|
||||
function orderResultByCurrentProjectFirst<T extends DataTable>(results: T[]) {
|
||||
return results.sort((a, b) => {
|
||||
if (a.project?.id === currentProjectId.value) return -1;
|
||||
if (b.project?.id === currentProjectId.value) return 1;
|
||||
@@ -56,14 +56,14 @@ export function useDataTableNavigationCommands(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
await dataStoreStore.fetchDataStores(
|
||||
await dataTableStore.fetchDataTables(
|
||||
currentProjectId.value,
|
||||
1,
|
||||
100, // TODO: pagination/lazy loading
|
||||
);
|
||||
|
||||
const trimmedLower = trimmed.toLowerCase();
|
||||
const filtered = dataStoreStore.dataStores.filter((dataTable) =>
|
||||
const filtered = dataTableStore.dataTables.filter((dataTable) =>
|
||||
dataTable.name.toLowerCase().includes(trimmedLower),
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ export function useDataTableNavigationCommands(options: {
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const getDataTableTitle = (dataTable: DataStore, includeOpenDataTablePrefix: boolean) => {
|
||||
const getDataTableTitle = (dataTable: DataTable, includeOpenDataTablePrefix: boolean) => {
|
||||
let prefix = '';
|
||||
if (dataTable.project && dataTable.project.type === 'personal') {
|
||||
prefix = includeOpenDataTablePrefix
|
||||
@@ -92,7 +92,7 @@ export function useDataTableNavigationCommands(options: {
|
||||
};
|
||||
|
||||
const createDataTableCommand = (
|
||||
dataTable: DataStore,
|
||||
dataTable: DataTable,
|
||||
includeOpenDataTablePrefix: boolean,
|
||||
): CommandBarItem => {
|
||||
return {
|
||||
@@ -101,7 +101,7 @@ export function useDataTableNavigationCommands(options: {
|
||||
section: i18n.baseText('commandBar.sections.dataTables'),
|
||||
handler: () => {
|
||||
const targetRoute = router.resolve({
|
||||
name: DATA_STORE_DETAILS,
|
||||
name: DATA_TABLE_DETAILS,
|
||||
params: {
|
||||
projectId: dataTable.projectId,
|
||||
id: dataTable.id,
|
||||
@@ -140,7 +140,7 @@ export function useDataTableNavigationCommands(options: {
|
||||
handler: () => {
|
||||
if (!currentProjectId.value) return;
|
||||
void router.push({
|
||||
name: PROJECT_DATA_STORES,
|
||||
name: PROJECT_DATA_TABLES,
|
||||
params: { projectId: currentProjectId.value, new: 'new' },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
} from '@/types';
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue';
|
||||
import type { ExpressionLocalResolveContext } from './types/expressions';
|
||||
import { DATA_STORE_MODULE_NAME } from './features/dataStore/constants';
|
||||
import { DATA_TABLE_MODULE_NAME } from './features/dataTable/constants';
|
||||
import type { TelemetryContext } from './types/telemetry';
|
||||
import type { IconName } from '@n8n/design-system/src/components/N8nIcon/icons';
|
||||
import type { WorkflowState } from './composables/useWorkflowState';
|
||||
@@ -228,8 +228,8 @@ export const SLACK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.slackTrigger';
|
||||
export const TELEGRAM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.telegramTrigger';
|
||||
export const FACEBOOK_LEAD_ADS_TRIGGER_NODE_TYPE = 'n8n-nodes-base.facebookLeadAdsTrigger';
|
||||
export const RESPOND_TO_WEBHOOK_NODE_TYPE = 'n8n-nodes-base.respondToWebhook';
|
||||
export const DATA_STORE_NODE_TYPE = 'n8n-nodes-base.dataTable';
|
||||
export const DATA_STORE_TOOL_NODE_TYPE = 'n8n-nodes-base.dataTableTool';
|
||||
export const DATA_TABLE_NODE_TYPE = 'n8n-nodes-base.dataTable';
|
||||
export const DATA_TABLE_TOOL_NODE_TYPE = 'n8n-nodes-base.dataTableTool';
|
||||
|
||||
export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base';
|
||||
export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
|
||||
@@ -237,7 +237,7 @@ export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
|
||||
// template categories
|
||||
export const TEMPLATE_CATEGORY_AI = 'categories/ai';
|
||||
|
||||
export const DATA_STORE_NODES = [DATA_STORE_NODE_TYPE, DATA_STORE_TOOL_NODE_TYPE];
|
||||
export const DATA_TABLE_NODES = [DATA_TABLE_NODE_TYPE, DATA_TABLE_TOOL_NODE_TYPE];
|
||||
|
||||
export const EXECUTABLE_TRIGGER_NODE_TYPES = [
|
||||
START_NODE_TYPE,
|
||||
@@ -260,7 +260,7 @@ export const NODES_USING_CODE_NODE_EDITOR = [
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
];
|
||||
export const MODULE_ENABLED_NODES = [
|
||||
...DATA_STORE_NODES.map((nodeType) => ({ nodeType, module: DATA_STORE_MODULE_NAME })),
|
||||
...DATA_TABLE_NODES.map((nodeType) => ({ nodeType, module: DATA_TABLE_MODULE_NAME })),
|
||||
];
|
||||
|
||||
export const NODE_POSITION_CONFLICT_ALLOWLIST = [STICKY_NODE_TYPE];
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { DATA_STORE_STORE } from '@/features/dataStore/constants';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import {
|
||||
fetchDataStoresApi,
|
||||
createDataStoreApi,
|
||||
deleteDataStoreApi,
|
||||
updateDataStoreApi,
|
||||
addDataStoreColumnApi,
|
||||
deleteDataStoreColumnApi,
|
||||
moveDataStoreColumnApi,
|
||||
getDataStoreRowsApi,
|
||||
insertDataStoreRowApi,
|
||||
updateDataStoreRowsApi,
|
||||
deleteDataStoreRowsApi,
|
||||
fetchDataStoreGlobalLimitInBytes,
|
||||
} from '@/features/dataStore/dataStore.api';
|
||||
import type {
|
||||
DataStore,
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStoreRow,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { reorderItem } from '@/features/dataStore/utils';
|
||||
import { type DataTableSizeStatus } from 'n8n-workflow';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
const rootStore = useRootStore();
|
||||
const projectStore = useProjectsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const dataStores = ref<DataStore[]>([]);
|
||||
const totalCount = ref(0);
|
||||
const dataStoreSize = ref(0);
|
||||
const dataStoreSizeLimitState = ref<DataTableSizeStatus>('ok');
|
||||
const dataStoreTableSizes = ref<Record<string, number>>({});
|
||||
|
||||
const formatSize = (sizeBytes: number) => {
|
||||
return Number((sizeBytes / 1024 / 1024).toFixed(2));
|
||||
};
|
||||
|
||||
const maxSizeMB = computed(() =>
|
||||
Math.floor(settingsStore.settings?.dataTables?.maxSize / 1024 / 1024),
|
||||
);
|
||||
|
||||
const dataStoreSizes = computed(() => {
|
||||
const formattedSizes: Record<string, number> = {};
|
||||
for (const [dataStoreId, sizeBytes] of Object.entries(dataStoreTableSizes.value)) {
|
||||
formattedSizes[dataStoreId] = formatSize(sizeBytes);
|
||||
}
|
||||
return formattedSizes;
|
||||
});
|
||||
|
||||
const fetchDataStores = async (projectId: string, page: number, pageSize: number) => {
|
||||
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
dataStores.value = response.data;
|
||||
totalCount.value = response.count;
|
||||
};
|
||||
|
||||
const createDataStore = async (name: string, projectId: string) => {
|
||||
const newStore = await createDataStoreApi(rootStore.restApiContext, name, projectId);
|
||||
if (!newStore.project && projectId) {
|
||||
const project = await projectStore.fetchProject(projectId);
|
||||
if (project) {
|
||||
newStore.project = project;
|
||||
}
|
||||
}
|
||||
dataStores.value.push(newStore);
|
||||
totalCount.value += 1;
|
||||
return newStore;
|
||||
};
|
||||
|
||||
const deleteDataStore = async (datastoreId: string, projectId: string) => {
|
||||
const deleted = await deleteDataStoreApi(rootStore.restApiContext, datastoreId, projectId);
|
||||
if (deleted) {
|
||||
dataStores.value = dataStores.value.filter((store) => store.id !== datastoreId);
|
||||
totalCount.value -= 1;
|
||||
}
|
||||
return deleted;
|
||||
};
|
||||
|
||||
const deleteDataStoreColumn = async (
|
||||
datastoreId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
) => {
|
||||
const deleted = await deleteDataStoreColumnApi(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
projectId,
|
||||
columnId,
|
||||
);
|
||||
if (deleted) {
|
||||
const index = dataStores.value.findIndex((store) => store.id === datastoreId);
|
||||
if (index !== -1) {
|
||||
dataStores.value[index].columns = dataStores.value[index].columns.filter(
|
||||
(col) => col.id !== columnId,
|
||||
);
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
};
|
||||
|
||||
const updateDataStore = async (datastoreId: string, name: string, projectId: string) => {
|
||||
const updated = await updateDataStoreApi(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
name,
|
||||
projectId,
|
||||
);
|
||||
if (updated) {
|
||||
const index = dataStores.value.findIndex((store) => store.id === datastoreId);
|
||||
if (index !== -1) {
|
||||
dataStores.value[index] = { ...dataStores.value[index], name };
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
const fetchDataStoreDetails = async (datastoreId: string, projectId: string) => {
|
||||
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, undefined, {
|
||||
projectId,
|
||||
id: datastoreId,
|
||||
});
|
||||
if (response.data.length > 0) {
|
||||
dataStores.value = response.data;
|
||||
return response.data[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fetchOrFindDataStore = async (datastoreId: string, projectId: string) => {
|
||||
const existingStore = dataStores.value.find((store) => store.id === datastoreId);
|
||||
if (existingStore) {
|
||||
return existingStore;
|
||||
}
|
||||
return await fetchDataStoreDetails(datastoreId, projectId);
|
||||
};
|
||||
|
||||
const addDataStoreColumn = async (
|
||||
datastoreId: string,
|
||||
projectId: string,
|
||||
column: DataStoreColumnCreatePayload,
|
||||
) => {
|
||||
const newColumn = await addDataStoreColumnApi(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
projectId,
|
||||
column,
|
||||
);
|
||||
if (newColumn) {
|
||||
const index = dataStores.value.findIndex((store) => store.id === datastoreId);
|
||||
if (index !== -1) {
|
||||
dataStores.value[index].columns.push(newColumn);
|
||||
}
|
||||
}
|
||||
return newColumn;
|
||||
};
|
||||
|
||||
const moveDataStoreColumn = async (
|
||||
datastoreId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
targetIndex: number,
|
||||
) => {
|
||||
const moved = await moveDataStoreColumnApi(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
projectId,
|
||||
columnId,
|
||||
targetIndex,
|
||||
);
|
||||
if (moved) {
|
||||
const dsIndex = dataStores.value.findIndex((store) => store.id === datastoreId);
|
||||
const fromIndex = dataStores.value[dsIndex].columns.findIndex((col) => col.id === columnId);
|
||||
dataStores.value[dsIndex].columns = reorderItem(
|
||||
dataStores.value[dsIndex].columns,
|
||||
fromIndex,
|
||||
targetIndex,
|
||||
);
|
||||
}
|
||||
return moved;
|
||||
};
|
||||
|
||||
const fetchDataStoreContent = async (
|
||||
datastoreId: string,
|
||||
projectId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
sortBy: string,
|
||||
filter?: string,
|
||||
) => {
|
||||
return await getDataStoreRowsApi(rootStore.restApiContext, datastoreId, projectId, {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
sortBy,
|
||||
filter,
|
||||
});
|
||||
};
|
||||
|
||||
const insertEmptyRow = async (dataStoreId: string, projectId: string) => {
|
||||
const inserted = await insertDataStoreRowApi(
|
||||
rootStore.restApiContext,
|
||||
dataStoreId,
|
||||
{},
|
||||
projectId,
|
||||
);
|
||||
return inserted[0];
|
||||
};
|
||||
|
||||
const updateRow = async (
|
||||
dataStoreId: string,
|
||||
projectId: string,
|
||||
rowId: number,
|
||||
rowData: DataStoreRow,
|
||||
) => {
|
||||
return await updateDataStoreRowsApi(
|
||||
rootStore.restApiContext,
|
||||
dataStoreId,
|
||||
rowId,
|
||||
rowData,
|
||||
projectId,
|
||||
);
|
||||
};
|
||||
|
||||
const deleteRows = async (dataStoreId: string, projectId: string, rowIds: number[]) => {
|
||||
return await deleteDataStoreRowsApi(rootStore.restApiContext, dataStoreId, rowIds, projectId);
|
||||
};
|
||||
|
||||
const fetchDataStoreSize = async () => {
|
||||
const result = await fetchDataStoreGlobalLimitInBytes(rootStore.restApiContext);
|
||||
dataStoreSize.value = formatSize(result.totalBytes);
|
||||
dataStoreSizeLimitState.value = result.quotaStatus;
|
||||
|
||||
const tableSizes: Record<string, number> = {};
|
||||
for (const [dataStoreId, info] of Object.entries(result.dataTables)) {
|
||||
tableSizes[dataStoreId] = info.sizeBytes;
|
||||
}
|
||||
dataStoreTableSizes.value = tableSizes;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
dataStores,
|
||||
totalCount,
|
||||
fetchDataStores,
|
||||
fetchDataStoreSize,
|
||||
dataStoreSize: computed(() => dataStoreSize.value),
|
||||
dataStoreSizeLimitState: computed(() => dataStoreSizeLimitState.value),
|
||||
dataStoreSizes,
|
||||
maxSizeMB,
|
||||
createDataStore,
|
||||
deleteDataStore,
|
||||
updateDataStore,
|
||||
fetchDataStoreDetails,
|
||||
fetchOrFindDataStore,
|
||||
addDataStoreColumn,
|
||||
deleteDataStoreColumn,
|
||||
moveDataStoreColumn,
|
||||
fetchDataStoreContent,
|
||||
insertEmptyRow,
|
||||
updateRow,
|
||||
deleteRows,
|
||||
};
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import type {
|
||||
AGGridCellType,
|
||||
DataStoreValue,
|
||||
DataStoreColumnType,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
import { AG_GRID_CELL_TYPES, DATA_STORE_COLUMN_TYPES } from '@/features/dataStore/datastore.types';
|
||||
|
||||
export const isDataStoreValue = (value: unknown): value is DataStoreValue => {
|
||||
return (
|
||||
value === null ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value instanceof Date
|
||||
);
|
||||
};
|
||||
|
||||
export const isAGGridCellType = (value: unknown): value is AGGridCellType => {
|
||||
return typeof value === 'string' && (AG_GRID_CELL_TYPES as readonly string[]).includes(value);
|
||||
};
|
||||
|
||||
export const isDataStoreColumnType = (type: unknown): type is DataStoreColumnType => {
|
||||
return typeof type === 'string' && (DATA_STORE_COLUMN_TYPES as readonly string[]).includes(type);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { BaseResource } from '@/Interface';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
|
||||
/**
|
||||
* Data Store resource type definition
|
||||
* This extends the ModuleResources interface to add DataStore as a resource type
|
||||
*/
|
||||
export type DataStoreResource = BaseResource &
|
||||
DataStore & {
|
||||
resourceType: 'datastore';
|
||||
};
|
||||
|
||||
// Extend the ModuleResources interface to include DataStore
|
||||
declare module '@/Interface' {
|
||||
interface ModuleResources {
|
||||
dataStore: DataStoreResource;
|
||||
}
|
||||
}
|
||||
|
||||
// Export to make this a module
|
||||
export {};
|
||||
+34
-34
@@ -1,10 +1,10 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DataStoreDetailsView from '@/features/dataStore/DataStoreDetailsView.vue';
|
||||
import DataTableDetailsView from '@/features/dataTable/DataTableDetailsView.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
|
||||
vi.mock('@/composables/useToast');
|
||||
@@ -17,9 +17,9 @@ vi.mock('@/composables/useDocumentTitle', () => ({
|
||||
vi.mock('@n8n/i18n', () => {
|
||||
const baseText = (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dataStore.getDetails.error': 'Error fetching data store details',
|
||||
'dataStore.notFound': 'Data store not found',
|
||||
'dataStore.dataStores': 'Data Stores',
|
||||
'dataTable.getDetails.error': 'Error fetching data table details',
|
||||
'dataTable.notFound': 'Data table not found',
|
||||
'dataTable.dataTables': 'Data Tables',
|
||||
};
|
||||
return translations[key] || key;
|
||||
};
|
||||
@@ -43,9 +43,9 @@ const mockToast = {
|
||||
showError: vi.fn(),
|
||||
};
|
||||
|
||||
const DEFAULT_DATA_STORE: DataStore = {
|
||||
const DEFAULT_DATA_TABLE: DataTable = {
|
||||
id: 'ds1',
|
||||
name: 'Test Data Store',
|
||||
name: 'Test Data Table',
|
||||
sizeBytes: 2048,
|
||||
columns: [
|
||||
{ id: '1', name: 'id', type: 'string', index: 0 },
|
||||
@@ -56,20 +56,20 @@ const DEFAULT_DATA_STORE: DataStore = {
|
||||
projectId: 'proj1',
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreDetailsView, {
|
||||
const renderComponent = createComponentRenderer(DataTableDetailsView, {
|
||||
props: {
|
||||
id: 'ds1',
|
||||
projectId: 'proj1',
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataStoreBreadcrumbs: true,
|
||||
DataStoreTable: true,
|
||||
DataTableBreadcrumbs: true,
|
||||
DataTableTable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreDetailsView', () => {
|
||||
describe('DataTableDetailsView', () => {
|
||||
beforeEach(() => {
|
||||
(useToast as ReturnType<typeof vi.fn>).mockReturnValue(mockToast);
|
||||
(useRouter as ReturnType<typeof vi.fn>).mockReturnValue(mockRouter);
|
||||
@@ -79,34 +79,34 @@ describe('DataStoreDetailsView', () => {
|
||||
describe('Loading states', () => {
|
||||
it('should show loading state initially', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockImplementation(
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockImplementation(
|
||||
async () => await new Promise(() => {}),
|
||||
);
|
||||
|
||||
const { getByTestId } = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('data-store-details-loading')).toBeInTheDocument();
|
||||
expect(getByTestId('data-table-details-loading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide loading state after successful data fetch', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockResolvedValue(DEFAULT_DATA_STORE);
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockResolvedValue(DEFAULT_DATA_TABLE);
|
||||
|
||||
const { queryByTestId } = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('data-store-details-loading')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('data-table-details-loading')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide loading state after error', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockRejectedValue(new Error('Failed'));
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockRejectedValue(new Error('Failed'));
|
||||
|
||||
const { queryByTestId } = renderComponent({ pinia });
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('DataStoreDetailsView', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('data-store-details-loading')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('data-table-details-loading')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -123,21 +123,21 @@ describe('DataStoreDetailsView', () => {
|
||||
describe('Data rendering', () => {
|
||||
it('should render breadcrumbs and table when data is loaded', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockResolvedValue(DEFAULT_DATA_STORE);
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockResolvedValue(DEFAULT_DATA_TABLE);
|
||||
|
||||
const { container } = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('data-store-breadcrumbs-stub')).toBeInTheDocument();
|
||||
expect(container.querySelector('data-store-table-stub')).toBeInTheDocument();
|
||||
expect(container.querySelector('data-table-breadcrumbs-stub')).toBeInTheDocument();
|
||||
expect(container.querySelector('data-table-table-stub')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render content when data store is null', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockResolvedValue(null);
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockResolvedValue(null);
|
||||
|
||||
const { container } = renderComponent({ pinia });
|
||||
|
||||
@@ -145,16 +145,16 @@ describe('DataStoreDetailsView', () => {
|
||||
expect(mockToast.showError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(container.querySelector('data-store-breadcrumbs-stub')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('data-store-table-stub')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('data-table-breadcrumbs-stub')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('data-table-table-stub')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should show error and redirect when data store not found', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockResolvedValue(null);
|
||||
const dataTableStore = useDataTableStore();
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockResolvedValue(null);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
@@ -166,16 +166,16 @@ describe('DataStoreDetailsView', () => {
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const error = new Error('API Error');
|
||||
vi.spyOn(dataStoreStore, 'fetchOrFindDataStore').mockRejectedValue(error);
|
||||
vi.spyOn(dataTableStore, 'fetchOrFindDataTable').mockRejectedValue(error);
|
||||
|
||||
renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(
|
||||
error,
|
||||
'Error fetching data store details',
|
||||
'Error fetching data table details',
|
||||
);
|
||||
expect(mockRouter.push).toHaveBeenCalled();
|
||||
});
|
||||
+32
-32
@@ -2,17 +2,17 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type {
|
||||
AddColumnResponse,
|
||||
DataStore,
|
||||
DataStoreColumnCreatePayload,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
DataTable,
|
||||
DataTableColumnCreatePayload,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { DATA_STORE_VIEW, MIN_LOADING_TIME } from '@/features/dataStore/constants';
|
||||
import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBreadcrumbs.vue';
|
||||
import { DATA_TABLE_VIEW, MIN_LOADING_TIME } from '@/features/dataTable/constants';
|
||||
import DataTableBreadcrumbs from '@/features/dataTable/components/DataTableBreadcrumbs.vue';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import DataStoreTable from './components/dataGrid/DataStoreTable.vue';
|
||||
import DataTableTable from './components/dataGrid/DataTableTable.vue';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import AddColumnButton from './components/dataGrid/AddColumnButton.vue';
|
||||
|
||||
@@ -29,32 +29,32 @@ const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const documentTitle = useDocumentTitle();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dataStore = ref<DataStore | null>(null);
|
||||
const dataStoreTableRef = ref<InstanceType<typeof DataStoreTable>>();
|
||||
const dataTable = ref<DataTable | null>(null);
|
||||
const dataTableTableRef = ref<InstanceType<typeof DataTableTable>>();
|
||||
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const showErrorAndGoBackToList = async (error: unknown) => {
|
||||
if (!(error instanceof Error)) {
|
||||
error = new Error(String(i18n.baseText('dataStore.getDetails.error')));
|
||||
error = new Error(String(i18n.baseText('dataTable.getDetails.error')));
|
||||
}
|
||||
toast.showError(error, i18n.baseText('dataStore.getDetails.error'));
|
||||
await router.push({ name: DATA_STORE_VIEW, params: { projectId: props.projectId } });
|
||||
toast.showError(error, i18n.baseText('dataTable.getDetails.error'));
|
||||
await router.push({ name: DATA_TABLE_VIEW, params: { projectId: props.projectId } });
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await dataStoreStore.fetchOrFindDataStore(props.id, props.projectId);
|
||||
const response = await dataTableStore.fetchOrFindDataTable(props.id, props.projectId);
|
||||
if (response) {
|
||||
dataStore.value = response;
|
||||
documentTitle.set(`${i18n.baseText('dataStore.dataStores')} > ${response.name}`);
|
||||
dataTable.value = response;
|
||||
documentTitle.set(`${i18n.baseText('dataTable.dataTables')} > ${response.name}`);
|
||||
} else {
|
||||
await showErrorAndGoBackToList(new Error(i18n.baseText('dataStore.notFound')));
|
||||
await showErrorAndGoBackToList(new Error(i18n.baseText('dataTable.notFound')));
|
||||
}
|
||||
} catch (error) {
|
||||
await showErrorAndGoBackToList(error);
|
||||
@@ -87,25 +87,25 @@ const onToggleSave = (value: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onAddColumn = async (column: DataStoreColumnCreatePayload): Promise<AddColumnResponse> => {
|
||||
if (!dataStoreTableRef.value) {
|
||||
const onAddColumn = async (column: DataTableColumnCreatePayload): Promise<AddColumnResponse> => {
|
||||
if (!dataTableTableRef.value) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: i18n.baseText('dataStore.error.tableNotInitialized'),
|
||||
errorMessage: i18n.baseText('dataTable.error.tableNotInitialized'),
|
||||
};
|
||||
}
|
||||
return await dataStoreTableRef.value.addColumn(column);
|
||||
return await dataTableTableRef.value.addColumn(column);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||
documentTitle.set(i18n.baseText('dataTable.dataTables'));
|
||||
await initialize();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style['data-store-details-view']">
|
||||
<div v-if="loading" data-test-id="data-store-details-loading">
|
||||
<div :class="$style['data-table-details-view']">
|
||||
<div v-if="loading" data-test-id="data-table-details-loading">
|
||||
<N8nLoading
|
||||
variant="h1"
|
||||
:loading="true"
|
||||
@@ -115,16 +115,16 @@ onMounted(async () => {
|
||||
/>
|
||||
<N8nLoading :loading="true" variant="h1" :rows="10" :shrink-last="false" />
|
||||
</div>
|
||||
<div v-else-if="dataStore">
|
||||
<div v-else-if="dataTable">
|
||||
<div :class="$style.header">
|
||||
<DataStoreBreadcrumbs :data-store="dataStore" />
|
||||
<DataTableBreadcrumbs :data-table="dataTable" />
|
||||
<div v-if="saving" :class="$style.saving">
|
||||
<N8nSpinner />
|
||||
<N8nText>{{ i18n.baseText('generic.saving') }}...</N8nText>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<N8nButton @click="dataStoreTableRef?.addRow">{{
|
||||
i18n.baseText('dataStore.addRow.label')
|
||||
<N8nButton @click="dataTableTableRef?.addRow">{{
|
||||
i18n.baseText('dataTable.addRow.label')
|
||||
}}</N8nButton>
|
||||
<AddColumnButton
|
||||
:use-text-trigger="true"
|
||||
@@ -134,9 +134,9 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<DataStoreTable
|
||||
ref="dataStoreTableRef"
|
||||
:data-store="dataStore"
|
||||
<DataTableTable
|
||||
ref="dataTableTableRef"
|
||||
:data-table="dataTable"
|
||||
@toggle-save="onToggleSave"
|
||||
/>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.data-store-details-view {
|
||||
.data-table-details-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
+36
-36
@@ -2,13 +2,13 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore, waitAllPromises } from '@/__tests__/utils';
|
||||
import { useProjectPages } from '@/composables/useProjectPages';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import DataTableView from '@/features/dataTable/DataTableView.vue';
|
||||
import { useSourceControlStore } from '@/features/sourceControl.ee/sourceControl.store';
|
||||
import DataStoreView from '@/features/dataStore/DataStoreView.vue';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import type { DataTableResource } from '@/features/dataTable/types';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import type { Mock } from 'vitest';
|
||||
import { type Project } from '@/types/projects.types';
|
||||
|
||||
@@ -26,11 +26,11 @@ vi.mock('@n8n/i18n', async (importOriginal) => {
|
||||
...actualObj,
|
||||
useI18n: vi.fn(() => ({
|
||||
baseText: vi.fn((key: string) => {
|
||||
if (key === 'dataStore.dataStores') return 'Data Stores';
|
||||
if (key === 'dataTable.dataTables') return 'Data Tables';
|
||||
if (key === 'projects.menu.personal') return 'Personal';
|
||||
if (key === 'dataStore.empty.label') return 'No data stores';
|
||||
if (key === 'dataStore.empty.description') return 'No data stores description';
|
||||
if (key === 'dataStore.empty.button.label') return 'Create data store';
|
||||
if (key === 'dataTable.empty.label') return 'No data tables';
|
||||
if (key === 'dataTable.empty.description') return 'No data tables description';
|
||||
if (key === 'dataTable.empty.button.label') return 'Create data table';
|
||||
if (key === 'generic.rename') return 'Rename';
|
||||
if (key === 'generic.delete') return 'Delete';
|
||||
if (key === 'generic.clear') return 'Clear';
|
||||
@@ -85,9 +85,9 @@ const router = createRouter({
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let projectsStore: ReturnType<typeof mockedStore<typeof useProjectsStore>>;
|
||||
let sourceControlStore: ReturnType<typeof mockedStore<typeof useSourceControlStore>>;
|
||||
let dataStoreStore: ReturnType<typeof mockedStore<typeof useDataStoreStore>>;
|
||||
let dataTableStore: ReturnType<typeof mockedStore<typeof useDataTableStore>>;
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreView, {
|
||||
const renderComponent = createComponentRenderer(DataTableView, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
},
|
||||
@@ -101,18 +101,18 @@ const initialState = {
|
||||
},
|
||||
};
|
||||
|
||||
const TEST_DATA_STORE: DataStoreResource = {
|
||||
const TEST_DATA_TABLE: DataTableResource = {
|
||||
id: '1',
|
||||
name: 'Test Data Store',
|
||||
name: 'Test Data Table',
|
||||
sizeBytes: 1024,
|
||||
columns: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
resourceType: 'datastore',
|
||||
resourceType: 'dataTable',
|
||||
projectId: '1',
|
||||
};
|
||||
|
||||
describe('DataStoreView', () => {
|
||||
describe('DataTableView', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
await router.push('/projects/test-project/data-tables');
|
||||
@@ -121,23 +121,23 @@ describe('DataStoreView', () => {
|
||||
pinia = createTestingPinia({ initialState });
|
||||
projectsStore = mockedStore(useProjectsStore);
|
||||
sourceControlStore = mockedStore(useSourceControlStore);
|
||||
dataStoreStore = mockedStore(useDataStoreStore);
|
||||
dataTableStore = mockedStore(useDataTableStore);
|
||||
|
||||
// Mock dataStore store state
|
||||
dataStoreStore.dataStores = [TEST_DATA_STORE];
|
||||
dataStoreStore.totalCount = 1;
|
||||
dataStoreStore.fetchDataStores = vi.fn().mockResolvedValue(undefined);
|
||||
// Mock dataTable store state
|
||||
dataTableStore.dataTables = [TEST_DATA_TABLE];
|
||||
dataTableStore.totalCount = 1;
|
||||
dataTableStore.fetchDataTables = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
projectsStore.currentProjectId = '';
|
||||
sourceControlStore.isProjectShared = vi.fn(() => false);
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize and load data stores from store', async () => {
|
||||
it('should initialize and load data tables from store', async () => {
|
||||
const { getByTestId } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
expect(dataStoreStore.fetchDataStores).toHaveBeenCalledWith('', 1, 25);
|
||||
expect(dataTableStore.fetchDataTables).toHaveBeenCalledWith('', 1, 25);
|
||||
expect(getByTestId('resources-list-wrapper')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -152,33 +152,33 @@ describe('DataStoreView', () => {
|
||||
|
||||
renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
expect(dataStoreStore.fetchDataStores).toHaveBeenCalledWith('test-project', 1, 25);
|
||||
expect(dataTableStore.fetchDataTables).toHaveBeenCalledWith('test-project', 1, 25);
|
||||
});
|
||||
it('should set document title on mount', async () => {
|
||||
renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
expect(mockDocumentTitle.set).toHaveBeenCalledWith('Data Stores');
|
||||
expect(mockDocumentTitle.set).toHaveBeenCalledWith('Data Tables');
|
||||
});
|
||||
|
||||
it('should handle initialization error', async () => {
|
||||
const error = new Error('Store Error');
|
||||
dataStoreStore.fetchDataStores = vi.fn().mockRejectedValue(error);
|
||||
dataTableStore.fetchDataTables = vi.fn().mockRejectedValue(error);
|
||||
|
||||
renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(error, 'Error loading data stores');
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(error, 'Error loading data tables');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
beforeEach(() => {
|
||||
dataStoreStore.dataStores = [];
|
||||
dataStoreStore.totalCount = 0;
|
||||
dataTableStore.dataTables = [];
|
||||
dataTableStore.totalCount = 0;
|
||||
});
|
||||
|
||||
it('should show empty state when no data stores exist', async () => {
|
||||
it('should show empty state when no data tables exist', async () => {
|
||||
const { getByTestId } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
@@ -200,12 +200,12 @@ describe('DataStoreView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('data store cards', () => {
|
||||
it('should render data store cards', async () => {
|
||||
describe('data table cards', () => {
|
||||
it('should render data table cards', async () => {
|
||||
const { container } = renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
// Check if DataStoreCard components are rendered
|
||||
// Check if DataTableCard components are rendered
|
||||
const cards = container.querySelectorAll('.mb-2xs');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -217,7 +217,7 @@ describe('DataStoreView', () => {
|
||||
await waitAllPromises();
|
||||
|
||||
// Clear the initial call
|
||||
dataStoreStore.fetchDataStores = vi.fn().mockClear();
|
||||
dataTableStore.fetchDataTables = vi.fn().mockClear();
|
||||
mockDebounce.callDebounced.mockClear();
|
||||
|
||||
// The component should be rendered and ready to handle pagination
|
||||
@@ -228,18 +228,18 @@ describe('DataStoreView', () => {
|
||||
});
|
||||
|
||||
it('should update page size on pagination change', async () => {
|
||||
dataStoreStore.dataStores = Array.from({ length: 20 }, (_, i) => ({
|
||||
...TEST_DATA_STORE,
|
||||
dataTableStore.dataTables = Array.from({ length: 20 }, (_, i) => ({
|
||||
...TEST_DATA_TABLE,
|
||||
id: `${i + 1}`,
|
||||
name: `Data Store ${i + 1}`,
|
||||
name: `Data Table ${i + 1}`,
|
||||
}));
|
||||
dataStoreStore.totalCount = 20;
|
||||
dataTableStore.totalCount = 20;
|
||||
|
||||
renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
// Initial call should use default page size of 25
|
||||
expect(dataStoreStore.fetchDataStores).toHaveBeenCalledWith('', 1, 25);
|
||||
expect(dataTableStore.fetchDataTables).toHaveBeenCalledWith('', 1, 25);
|
||||
});
|
||||
});
|
||||
});
|
||||
+26
-26
@@ -9,19 +9,19 @@ import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { SortingAndPaginationUpdates } from '@/Interface';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import DataStoreCard from '@/features/dataStore/components/DataStoreCard.vue';
|
||||
import type { DataTableResource } from '@/features/dataTable/types';
|
||||
import DataTableCard from '@/features/dataTable/components/DataTableCard.vue';
|
||||
import { useSourceControlStore } from '@/features/sourceControl.ee/sourceControl.store';
|
||||
import {
|
||||
ADD_DATA_STORE_MODAL_KEY,
|
||||
DEFAULT_DATA_STORE_PAGE_SIZE,
|
||||
PROJECT_DATA_STORES,
|
||||
} from '@/features/dataStore/constants';
|
||||
ADD_DATA_TABLE_MODAL_KEY,
|
||||
DEFAULT_DATA_TABLE_PAGE_SIZE,
|
||||
PROJECT_DATA_TABLES,
|
||||
} from '@/features/dataTable/constants';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
import { N8nActionBox } from '@n8n/design-system';
|
||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
||||
@@ -34,7 +34,7 @@ const { callDebounced } = useDebounce();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const toast = useToast();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const insightsStore = useInsightsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
@@ -43,18 +43,18 @@ const uiStore = useUIStore();
|
||||
const loading = ref(true);
|
||||
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(DEFAULT_DATA_STORE_PAGE_SIZE);
|
||||
const pageSize = ref(DEFAULT_DATA_TABLE_PAGE_SIZE);
|
||||
|
||||
const dataStoreResources = computed<DataStoreResource[]>(() =>
|
||||
dataStoreStore.dataStores.map((ds) => {
|
||||
const dataTableResources = computed<DataTableResource[]>(() =>
|
||||
dataTableStore.dataTables.map((ds) => {
|
||||
return {
|
||||
...ds,
|
||||
resourceType: 'datastore',
|
||||
resourceType: 'dataTable',
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const totalCount = computed(() => dataStoreStore.totalCount);
|
||||
const totalCount = computed(() => dataTableStore.totalCount);
|
||||
|
||||
const currentProject = computed(() => {
|
||||
if (projectPages.isOverviewSubPage) {
|
||||
@@ -69,9 +69,9 @@ const initialize = async () => {
|
||||
loading.value = true;
|
||||
const projectIdFilter = projectPages.isOverviewSubPage ? '' : projectsStore.currentProjectId;
|
||||
try {
|
||||
await dataStoreStore.fetchDataStores(projectIdFilter ?? '', currentPage.value, pageSize.value);
|
||||
await dataTableStore.fetchDataTables(projectIdFilter ?? '', currentPage.value, pageSize.value);
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Error loading data stores');
|
||||
toast.showError(error, 'Error loading data tables');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -91,22 +91,22 @@ const onPaginationUpdate = async (payload: SortingAndPaginationUpdates) => {
|
||||
|
||||
const onAddModalClick = () => {
|
||||
void router.push({
|
||||
name: PROJECT_DATA_STORES,
|
||||
name: PROJECT_DATA_TABLES,
|
||||
params: { projectId: currentProject.value?.id, new: 'new' },
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||
documentTitle.set(i18n.baseText('dataTable.dataTables'));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.params.new,
|
||||
() => {
|
||||
if (route.params.new === 'new') {
|
||||
uiStore.openModal(ADD_DATA_STORE_MODAL_KEY);
|
||||
uiStore.openModal(ADD_DATA_TABLE_MODAL_KEY);
|
||||
} else {
|
||||
uiStore.closeModal(ADD_DATA_STORE_MODAL_KEY);
|
||||
uiStore.closeModal(ADD_DATA_TABLE_MODAL_KEY);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -115,9 +115,9 @@ watch(
|
||||
<template>
|
||||
<ResourcesListLayout
|
||||
ref="layout"
|
||||
resource-key="dataStore"
|
||||
resource-key="dataTable"
|
||||
type="list-paginated"
|
||||
:resources="dataStoreResources"
|
||||
:resources="dataTableResources"
|
||||
:initialize="initialize"
|
||||
:type-props="{ itemSize: 80 }"
|
||||
:loading="loading"
|
||||
@@ -144,17 +144,17 @@ watch(
|
||||
<template #empty>
|
||||
<N8nActionBox
|
||||
data-test-id="empty-shared-action-box"
|
||||
:heading="i18n.baseText('dataStore.empty.label')"
|
||||
:description="i18n.baseText('dataStore.empty.description')"
|
||||
:button-text="i18n.baseText('dataStore.add.button.label')"
|
||||
:heading="i18n.baseText('dataTable.empty.label')"
|
||||
:description="i18n.baseText('dataTable.empty.description')"
|
||||
:button-text="i18n.baseText('dataTable.add.button.label')"
|
||||
button-type="secondary"
|
||||
@click:button="onAddModalClick"
|
||||
/>
|
||||
</template>
|
||||
<template #item="{ item: data }">
|
||||
<DataStoreCard
|
||||
<DataTableCard
|
||||
class="mb-2xs"
|
||||
:data-store="data as DataStoreResource"
|
||||
:data-table="data as DataTableResource"
|
||||
:show-ownership-badge="projectPages.isOverviewSubPage"
|
||||
:read-only="readOnlyEnv"
|
||||
/>
|
||||
+26
-26
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { DATA_STORE_DETAILS, PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||
import { DATA_TABLE_DETAILS, PROJECT_DATA_TABLES } from '@/features/dataTable/constants';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
import { N8nButton, N8nInput, N8nInputLabel } from '@n8n/design-system';
|
||||
@@ -16,7 +16,7 @@ type Props = {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const route = useRoute();
|
||||
@@ -25,7 +25,7 @@ const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const dataStoreName = ref('');
|
||||
const dataTableName = ref('');
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -37,58 +37,58 @@ onMounted(() => {
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const newDataStore = await dataStoreStore.createDataStore(
|
||||
dataStoreName.value,
|
||||
const newDataTable = await dataTableStore.createDataTable(
|
||||
dataTableName.value,
|
||||
route.params.projectId as string,
|
||||
);
|
||||
telemetry.track('User created data table', {
|
||||
data_table_id: newDataStore.id,
|
||||
data_table_project_id: newDataStore.project?.id,
|
||||
data_table_id: newDataTable.id,
|
||||
data_table_project_id: newDataTable.project?.id,
|
||||
});
|
||||
dataStoreName.value = '';
|
||||
dataTableName.value = '';
|
||||
uiStore.closeModal(props.modalName);
|
||||
void router.push({
|
||||
name: DATA_STORE_DETAILS,
|
||||
name: DATA_TABLE_DETAILS,
|
||||
params: {
|
||||
id: newDataStore.id,
|
||||
id: newDataTable.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.add.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.add.error'));
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
uiStore.closeModal(props.modalName);
|
||||
redirectToDataStores();
|
||||
redirectToDataTables();
|
||||
};
|
||||
|
||||
const redirectToDataStores = () => {
|
||||
void router.replace({ name: PROJECT_DATA_STORES });
|
||||
const redirectToDataTables = () => {
|
||||
void router.replace({ name: PROJECT_DATA_TABLES });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataStores">
|
||||
<Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataTables">
|
||||
<template #header>
|
||||
<div :class="$style.header">
|
||||
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
|
||||
<h2>{{ i18n.baseText('dataTable.add.title') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="$style.content">
|
||||
<N8nInputLabel
|
||||
:label="i18n.baseText('dataStore.add.input.name.label')"
|
||||
:label="i18n.baseText('dataTable.add.input.name.label')"
|
||||
:required="true"
|
||||
input-name="dataStoreName"
|
||||
input-name="dataTableName"
|
||||
>
|
||||
<N8nInput
|
||||
ref="inputRef"
|
||||
v-model="dataStoreName"
|
||||
v-model="dataTableName"
|
||||
type="text"
|
||||
:placeholder="i18n.baseText('dataStore.add.input.name.placeholder')"
|
||||
data-test-id="data-store-name-input"
|
||||
name="dataStoreName"
|
||||
:placeholder="i18n.baseText('dataTable.add.input.name.placeholder')"
|
||||
data-test-id="data-table-name-input"
|
||||
name="dataTableName"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
</N8nInputLabel>
|
||||
@@ -97,15 +97,15 @@ const redirectToDataStores = () => {
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<N8nButton
|
||||
:disabled="!dataStoreName"
|
||||
:disabled="!dataTableName"
|
||||
:label="i18n.baseText('generic.create')"
|
||||
data-test-id="confirm-add-data-store-button"
|
||||
data-test-id="confirm-add-data-table-button"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
:label="i18n.baseText('generic.cancel')"
|
||||
data-test-id="cancel-add-data-store-button"
|
||||
data-test-id="cancel-add-data-table-button"
|
||||
@click="onCancel"
|
||||
/>
|
||||
</div>
|
||||
+47
-47
@@ -2,10 +2,10 @@ import userEvent from '@testing-library/user-event';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
||||
import { DATA_STORE_CARD_ACTIONS } from '@/features/dataStore/constants';
|
||||
import DataTableActions from '@/features/dataTable/components/DataTableActions.vue';
|
||||
import { DATA_TABLE_CARD_ACTIONS } from '@/features/dataTable/constants';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
const mockMessage = {
|
||||
confirm: vi.fn(),
|
||||
@@ -15,7 +15,7 @@ const mockToast = {
|
||||
showError: vi.fn(),
|
||||
};
|
||||
|
||||
const mockDeleteDataStore = vi.fn();
|
||||
const mockDeleteDataTable = vi.fn();
|
||||
|
||||
vi.mock('@/composables/useMessage', () => ({
|
||||
useMessage: () => mockMessage,
|
||||
@@ -25,9 +25,9 @@ vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => mockToast,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/dataStore.store', () => ({
|
||||
useDataStoreStore: () => ({
|
||||
deleteDataStore: mockDeleteDataStore,
|
||||
vi.mock('@/features/dataTable/dataTable.store', () => ({
|
||||
useDataTableStore: () => ({
|
||||
deleteDataTable: mockDeleteDataTable,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -39,19 +39,19 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
if (key === 'generic.delete') return 'Delete';
|
||||
if (key === 'generic.cancel') return 'Cancel';
|
||||
if (key === 'generic.unknownError') return 'Something went wrong';
|
||||
if (key === 'dataStore.delete.confirm.message')
|
||||
if (key === 'dataTable.delete.confirm.message')
|
||||
return `Are you sure that you want to delete "${options?.interpolate?.name}"?`;
|
||||
if (key === 'dataStore.delete.confirm.title') return 'Delete Data Store';
|
||||
if (key === 'dataStore.delete.error')
|
||||
return 'Something went wrong while deleting the data store.';
|
||||
if (key === 'dataTable.delete.confirm.title') return 'Delete Data Table';
|
||||
if (key === 'dataTable.delete.error')
|
||||
return 'Something went wrong while deleting the data table.';
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDataStore: DataStore = {
|
||||
const mockDataTable: DataTable = {
|
||||
id: '1',
|
||||
name: 'Test DataStore',
|
||||
name: 'Test DataTable',
|
||||
sizeBytes: 1024,
|
||||
columns: [],
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
@@ -59,18 +59,18 @@ const mockDataStore: DataStore = {
|
||||
projectId: 'project-1',
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreActions, {
|
||||
const renderComponent = createComponentRenderer(DataTableActions, {
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
isReadOnly: false,
|
||||
location: 'breadcrumbs',
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreActions', () => {
|
||||
describe('DataTableActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDeleteDataStore.mockResolvedValue(true);
|
||||
mockDeleteDataTable.mockResolvedValue(true);
|
||||
mockMessage.confirm.mockResolvedValue(MODAL_CONFIRM);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('DataStoreActions', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const actionToggle = getByTestId('data-store-card-actions');
|
||||
const actionToggle = getByTestId('data-table-card-actions');
|
||||
expect(actionToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('DataStoreActions', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const actionToggle = getByTestId('data-store-card-actions');
|
||||
const actionToggle = getByTestId('data-table-card-actions');
|
||||
expect(actionToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -110,16 +110,16 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the rename action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.RENAME}`));
|
||||
|
||||
expect(emitted().rename).toBeTruthy();
|
||||
expect(emitted().rename[0]).toEqual([
|
||||
{
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
action: 'rename',
|
||||
},
|
||||
]);
|
||||
@@ -134,15 +134,15 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockMessage.confirm).toHaveBeenCalledWith(
|
||||
'Are you sure that you want to delete "Test DataStore"?',
|
||||
'Delete Data Store',
|
||||
'Are you sure that you want to delete "Test DataTable"?',
|
||||
'Delete Data Table',
|
||||
{
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonText: 'Cancel',
|
||||
@@ -159,13 +159,13 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockDeleteDataStore).toHaveBeenCalledWith('1', 'project-1');
|
||||
expect(mockDeleteDataTable).toHaveBeenCalledWith('1', 'project-1');
|
||||
expect(emitted().onDeleted).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -180,18 +180,18 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockDeleteDataStore).not.toHaveBeenCalled();
|
||||
expect(mockDeleteDataTable).not.toHaveBeenCalled();
|
||||
expect(emitted().onDeleted).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should show error when delete fails', async () => {
|
||||
mockDeleteDataStore.mockResolvedValue(false);
|
||||
mockDeleteDataTable.mockResolvedValue(false);
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
@@ -201,21 +201,21 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
'Something went wrong while deleting the data store.',
|
||||
'Something went wrong while deleting the data table.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error when delete throws exception', async () => {
|
||||
const deleteError = new Error('Delete failed');
|
||||
mockDeleteDataStore.mockRejectedValue(deleteError);
|
||||
mockDeleteDataTable.mockRejectedValue(deleteError);
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
@@ -225,15 +225,15 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
await userEvent.click(getByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(
|
||||
deleteError,
|
||||
'Something went wrong while deleting the data store.',
|
||||
'Something went wrong while deleting the data table.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('DataStoreActions', () => {
|
||||
it('should show rename action when location is breadcrumbs', async () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent({
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
isReadOnly: false,
|
||||
location: 'breadcrumbs',
|
||||
},
|
||||
@@ -252,17 +252,17 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Check that rename action is present
|
||||
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`)).toBeInTheDocument();
|
||||
expect(queryByTestId(`action-${DATA_TABLE_CARD_ACTIONS.RENAME}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show rename action when location is card', async () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent({
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
isReadOnly: false,
|
||||
location: 'card',
|
||||
},
|
||||
@@ -273,13 +273,13 @@ describe('DataStoreActions', () => {
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
await userEvent.click(getByTestId('data-table-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Check that rename action is NOT present
|
||||
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(`action-${DATA_TABLE_CARD_ACTIONS.RENAME}`)).not.toBeInTheDocument();
|
||||
// But delete action should still be present
|
||||
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`)).toBeInTheDocument();
|
||||
expect(queryByTestId(`action-${DATA_TABLE_CARD_ACTIONS.DELETE}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
+23
-23
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
import type { IUser, UserAction } from '@n8n/design-system';
|
||||
import { DATA_STORE_CARD_ACTIONS } from '@/features/dataStore/constants';
|
||||
import { DATA_TABLE_CARD_ACTIONS } from '@/features/dataTable/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
import { N8nActionToggle } from '@n8n/design-system';
|
||||
type Props = {
|
||||
dataStore: DataStore;
|
||||
dataTable: DataTable;
|
||||
isReadOnly?: boolean;
|
||||
location: 'card' | 'breadcrumbs';
|
||||
};
|
||||
@@ -24,14 +24,14 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<{
|
||||
rename: [
|
||||
value: {
|
||||
dataStore: DataStore;
|
||||
dataTable: DataTable;
|
||||
action: string;
|
||||
},
|
||||
];
|
||||
onDeleted: [];
|
||||
}>();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
const message = useMessage();
|
||||
@@ -42,14 +42,14 @@ const actions = computed<Array<UserAction<IUser>>>(() => {
|
||||
const availableActions = [
|
||||
{
|
||||
label: i18n.baseText('generic.delete'),
|
||||
value: DATA_STORE_CARD_ACTIONS.DELETE,
|
||||
value: DATA_TABLE_CARD_ACTIONS.DELETE,
|
||||
disabled: props.isReadOnly,
|
||||
},
|
||||
];
|
||||
if (props.location === 'breadcrumbs') {
|
||||
availableActions.unshift({
|
||||
label: i18n.baseText('generic.rename'),
|
||||
value: DATA_STORE_CARD_ACTIONS.RENAME,
|
||||
value: DATA_TABLE_CARD_ACTIONS.RENAME,
|
||||
disabled: props.isReadOnly,
|
||||
});
|
||||
}
|
||||
@@ -58,50 +58,50 @@ const actions = computed<Array<UserAction<IUser>>>(() => {
|
||||
|
||||
const onAction = async (action: string) => {
|
||||
switch (action) {
|
||||
case DATA_STORE_CARD_ACTIONS.RENAME: {
|
||||
case DATA_TABLE_CARD_ACTIONS.RENAME: {
|
||||
// This is handled outside of this component
|
||||
// where editable label component is used
|
||||
emit('rename', {
|
||||
dataStore: props.dataStore,
|
||||
dataTable: props.dataTable,
|
||||
action: 'rename',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case DATA_STORE_CARD_ACTIONS.DELETE: {
|
||||
case DATA_TABLE_CARD_ACTIONS.DELETE: {
|
||||
const promptResponse = await message.confirm(
|
||||
i18n.baseText('dataStore.delete.confirm.message', {
|
||||
interpolate: { name: props.dataStore.name },
|
||||
i18n.baseText('dataTable.delete.confirm.message', {
|
||||
interpolate: { name: props.dataTable.name },
|
||||
}),
|
||||
i18n.baseText('dataStore.delete.confirm.title'),
|
||||
i18n.baseText('dataTable.delete.confirm.title'),
|
||||
{
|
||||
confirmButtonText: i18n.baseText('generic.delete'),
|
||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||
},
|
||||
);
|
||||
if (promptResponse === MODAL_CONFIRM) {
|
||||
await deleteDataStore();
|
||||
await deleteDataTable();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDataStore = async () => {
|
||||
const deleteDataTable = async () => {
|
||||
try {
|
||||
const deleted = await dataStoreStore.deleteDataStore(
|
||||
props.dataStore.id,
|
||||
props.dataStore.projectId,
|
||||
const deleted = await dataTableStore.deleteDataTable(
|
||||
props.dataTable.id,
|
||||
props.dataTable.projectId,
|
||||
);
|
||||
if (!deleted) {
|
||||
throw new Error(i18n.baseText('generic.unknownError'));
|
||||
}
|
||||
emit('onDeleted');
|
||||
telemetry.track('User deleted data table', {
|
||||
data_table_id: props.dataStore.id,
|
||||
data_table_project_id: props.dataStore.projectId,
|
||||
data_table_id: props.dataTable.id,
|
||||
data_table_project_id: props.dataTable.projectId,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.delete.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.delete.error'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -109,7 +109,7 @@ const deleteDataStore = async () => {
|
||||
<N8nActionToggle
|
||||
:actions="actions"
|
||||
theme="dark"
|
||||
data-test-id="data-store-card-actions"
|
||||
data-test-id="data-table-card-actions"
|
||||
@action="onAction"
|
||||
/>
|
||||
</template>
|
||||
+37
-37
@@ -2,8 +2,8 @@ import userEvent from '@testing-library/user-event';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBreadcrumbs.vue';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import DataTableBreadcrumbs from '@/features/dataTable/components/DataTableBreadcrumbs.vue';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
@@ -13,7 +13,7 @@ const mockToast = {
|
||||
showError: vi.fn(),
|
||||
};
|
||||
|
||||
const mockUpdateDataStore = vi.fn();
|
||||
const mockUpdateDataTable = vi.fn();
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
@@ -27,9 +27,9 @@ vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => mockToast,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/dataStore.store', () => ({
|
||||
useDataStoreStore: () => ({
|
||||
updateDataStore: mockUpdateDataStore,
|
||||
vi.mock('@/features/dataTable/dataTable.store', () => ({
|
||||
useDataTableStore: () => ({
|
||||
updateDataTable: mockUpdateDataTable,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -38,9 +38,9 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
useI18n: () => ({
|
||||
baseText: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dataStore.dataStores': 'Data Stores',
|
||||
'dataStore.add.input.name.label': 'Data store name',
|
||||
'dataStore.rename.error': 'Something went wrong while renaming the data store.',
|
||||
'dataTable.dataTables': 'Data Tables',
|
||||
'dataTable.add.input.name.label': 'Data table name',
|
||||
'dataTable.rename.error': 'Something went wrong while renaming the data table.',
|
||||
'generic.unknownError': 'Something went wrong',
|
||||
};
|
||||
return translations[key] || key;
|
||||
@@ -48,9 +48,9 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDataStore: DataStore = {
|
||||
const mockDataTable: DataTable = {
|
||||
id: '1',
|
||||
name: 'Test DataStore',
|
||||
name: 'Test DataTable',
|
||||
sizeBytes: 1024,
|
||||
columns: [],
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
@@ -68,21 +68,21 @@ const mockDataStore: DataStore = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockDataStoreWithoutProject: DataStore = {
|
||||
...mockDataStore,
|
||||
const mockDataTableWithoutProject: DataTable = {
|
||||
...mockDataTable,
|
||||
project: undefined,
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreBreadcrumbs, {
|
||||
const renderComponent = createComponentRenderer(DataTableBreadcrumbs, {
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreBreadcrumbs', () => {
|
||||
describe('DataTableBreadcrumbs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUpdateDataStore.mockResolvedValue(true);
|
||||
mockUpdateDataTable.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('Breadcrumbs rendering', () => {
|
||||
@@ -94,7 +94,7 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByText('Data Stores')).toBeInTheDocument();
|
||||
expect(getByText('Data Tables')).toBeInTheDocument();
|
||||
const separators = getAllByText('/');
|
||||
expect(separators.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
it('should render breadcrumbs component when project is null', () => {
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
dataStore: mockDataStoreWithoutProject,
|
||||
dataTable: mockDataTableWithoutProject,
|
||||
},
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -111,11 +111,11 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
});
|
||||
|
||||
// Should still render the breadcrumbs container even without project
|
||||
const breadcrumbsContainer = container.querySelector('.data-store-breadcrumbs');
|
||||
const breadcrumbsContainer = container.querySelector('.data-table-breadcrumbs');
|
||||
expect(breadcrumbsContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render inline text edit for datastore name', () => {
|
||||
it('should render inline text edit for data table name', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -123,11 +123,11 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const nameInput = getByTestId('datastore-header-name-input');
|
||||
const nameInput = getByTestId('data-table-header-name-input');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render DataStoreActions component', () => {
|
||||
it('should render DataTableActions component', () => {
|
||||
const { container } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -135,13 +135,13 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-table-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate to datastores list when breadcrumb item is clicked', async () => {
|
||||
it('should navigate to data tables list when breadcrumb item is clicked', async () => {
|
||||
const { getByText } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -149,13 +149,13 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const datastoresLink = getByText('Data Stores');
|
||||
await userEvent.click(datastoresLink);
|
||||
const dataTablesLink = getByText('Data Tables');
|
||||
await userEvent.click(dataTablesLink);
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/projects/project-1/datatables');
|
||||
});
|
||||
|
||||
it('should render DataStoreActions component that can trigger navigation', () => {
|
||||
it('should render DataTableActions component that can trigger navigation', () => {
|
||||
const { container } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -163,14 +163,14 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify DataStoreActions component is rendered
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
// Verify DataTableActions component is rendered
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-table-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Name editing', () => {
|
||||
it('should show current datastore name', () => {
|
||||
it('should show current data table name', () => {
|
||||
const { getByDisplayValue } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -178,7 +178,7 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByDisplayValue('Test DataStore')).toBeInTheDocument();
|
||||
expect(getByDisplayValue('Test DataTable')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,19 +192,19 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
});
|
||||
|
||||
// Check main structure
|
||||
const breadcrumbsContainer = container.querySelector('.data-store-breadcrumbs');
|
||||
const breadcrumbsContainer = container.querySelector('.data-table-breadcrumbs');
|
||||
expect(breadcrumbsContainer).toBeInTheDocument();
|
||||
|
||||
// Check name input
|
||||
const nameInput = getByTestId('datastore-header-name-input');
|
||||
const nameInput = getByTestId('data-table-header-name-input');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
|
||||
// Check actions component
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-table-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct datastore name', () => {
|
||||
it('should display correct data table name', () => {
|
||||
const { getByDisplayValue } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
@@ -212,7 +212,7 @@ describe('DataStoreBreadcrumbs', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByDisplayValue('Test DataStore')).toBeInTheDocument();
|
||||
expect(getByDisplayValue('Test DataTable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show breadcrumbs separator', () => {
|
||||
+28
-28
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
||||
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import DataTableActions from '@/features/dataTable/components/DataTableActions.vue';
|
||||
import { PROJECT_DATA_TABLES } from '@/features/dataTable/constants';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { telemetry } from '@/plugins/telemetry';
|
||||
|
||||
@@ -15,23 +15,23 @@ import ProjectBreadcrumb from '@/components/Folders/ProjectBreadcrumb.vue';
|
||||
const BREADCRUMBS_SEPARATOR = '/';
|
||||
|
||||
type Props = {
|
||||
dataStore: DataStore;
|
||||
dataTable: DataTable;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const renameInput = useTemplateRef<{ forceFocus: () => void }>('renameInput');
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const editableName = ref(props.dataStore.name);
|
||||
const editableName = ref(props.dataTable.name);
|
||||
|
||||
const project = computed(() => {
|
||||
return props.dataStore.project ?? null;
|
||||
return props.dataTable.project ?? null;
|
||||
});
|
||||
|
||||
const breadcrumbs = computed<PathItem[]>(() => {
|
||||
@@ -40,8 +40,8 @@ const breadcrumbs = computed<PathItem[]>(() => {
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'datastores',
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
id: 'datatables',
|
||||
label: i18n.baseText('dataTable.dataTables'),
|
||||
href: `/projects/${project.value.id}/datatables`,
|
||||
},
|
||||
];
|
||||
@@ -55,8 +55,8 @@ const onItemClicked = async (item: PathItem) => {
|
||||
|
||||
const onDelete = async () => {
|
||||
await router.push({
|
||||
name: PROJECT_DATA_STORES,
|
||||
params: { projectId: props.dataStore.projectId },
|
||||
name: PROJECT_DATA_TABLES,
|
||||
params: { projectId: props.dataTable.projectId },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -71,28 +71,28 @@ const onRename = async () => {
|
||||
|
||||
const onNameSubmit = async (name: string) => {
|
||||
try {
|
||||
const updated = await dataStoreStore.updateDataStore(
|
||||
props.dataStore.id,
|
||||
const updated = await dataTableStore.updateDataTable(
|
||||
props.dataTable.id,
|
||||
name,
|
||||
props.dataStore.projectId,
|
||||
props.dataTable.projectId,
|
||||
);
|
||||
if (!updated) {
|
||||
throw new Error(i18n.baseText('generic.unknownError'));
|
||||
}
|
||||
editableName.value = name;
|
||||
telemetry.track('User renamed data table', {
|
||||
data_table_id: props.dataStore.id,
|
||||
data_table_project_id: props.dataStore.projectId,
|
||||
data_table_id: props.dataTable.id,
|
||||
data_table_project_id: props.dataTable.projectId,
|
||||
});
|
||||
} catch (error) {
|
||||
// Revert to original name if rename fails
|
||||
editableName.value = props.dataStore.name;
|
||||
toast.showError(error, i18n.baseText('dataStore.rename.error'));
|
||||
editableName.value = props.dataTable.name;
|
||||
toast.showError(error, i18n.baseText('dataTable.rename.error'));
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.dataStore.name,
|
||||
() => props.dataTable.name,
|
||||
(newName) => {
|
||||
editableName.value = newName;
|
||||
},
|
||||
@@ -100,7 +100,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style['data-store-breadcrumbs']">
|
||||
<div :class="$style['data-table-breadcrumbs']">
|
||||
<N8nBreadcrumbs
|
||||
:items="breadcrumbs"
|
||||
:separator="BREADCRUMBS_SEPARATOR"
|
||||
@@ -115,8 +115,8 @@ watch(
|
||||
<N8nInlineTextEdit
|
||||
ref="renameInput"
|
||||
v-model="editableName"
|
||||
data-test-id="datastore-header-name-input"
|
||||
:placeholder="i18n.baseText('dataStore.add.input.name.label')"
|
||||
data-test-id="data-table-header-name-input"
|
||||
:placeholder="i18n.baseText('dataTable.add.input.name.label')"
|
||||
:class="$style['breadcrumb-current']"
|
||||
:max-length="30"
|
||||
:read-only="false"
|
||||
@@ -125,9 +125,9 @@ watch(
|
||||
/>
|
||||
</template>
|
||||
</N8nBreadcrumbs>
|
||||
<div :class="$style['data-store-actions']">
|
||||
<DataStoreActions
|
||||
:data-store="props.dataStore"
|
||||
<div :class="$style['data-table-actions']">
|
||||
<DataTableActions
|
||||
:data-table="props.dataTable"
|
||||
location="breadcrumbs"
|
||||
@rename="onRename"
|
||||
@on-deleted="onDelete"
|
||||
@@ -137,12 +137,12 @@ watch(
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.data-store-breadcrumbs {
|
||||
.data-table-breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.data-store-actions {
|
||||
.data-table-actions {
|
||||
position: relative;
|
||||
top: var(--spacing-5xs);
|
||||
}
|
||||
+21
-21
@@ -1,7 +1,7 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DataStoreCard from '@/features/dataStore/components/DataStoreCard.vue';
|
||||
import DataTableCard from '@/features/dataTable/components/DataTableCard.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import type { DataTableResource } from '@/features/dataTable/types';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
@@ -22,27 +22,27 @@ vi.mock('vue-router', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const DEFAULT_DATA_STORE: DataStoreResource = {
|
||||
const DEFAULT_DATA_TABLE: DataTableResource = {
|
||||
id: '1',
|
||||
name: 'Test Data Store',
|
||||
name: 'Test Data Table',
|
||||
sizeBytes: 1024,
|
||||
columns: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
resourceType: 'datastore',
|
||||
resourceType: 'dataTable',
|
||||
projectId: '1',
|
||||
} as const satisfies DataStoreResource;
|
||||
} as const satisfies DataTableResource;
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreCard, {
|
||||
const renderComponent = createComponentRenderer(DataTableCard, {
|
||||
props: {
|
||||
dataStore: DEFAULT_DATA_STORE,
|
||||
dataTable: DEFAULT_DATA_TABLE,
|
||||
readOnly: false,
|
||||
showOwnershipBadge: false,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
N8nLink: {
|
||||
template: '<a :href="href" data-test-id="data-store-card-link"><slot /></a>',
|
||||
template: '<a :href="href" data-test-id="data-table-card-link"><slot /></a>',
|
||||
props: ['to'],
|
||||
computed: {
|
||||
href() {
|
||||
@@ -61,7 +61,7 @@ const renderComponent = createComponentRenderer(DataStoreCard, {
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreCard', () => {
|
||||
describe('DataTableCard', () => {
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -73,13 +73,13 @@ describe('DataStoreCard', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render data store info correctly', () => {
|
||||
it('should render data table info correctly', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('data-store-card-icon')).toBeInTheDocument();
|
||||
expect(getByTestId('data-store-card-name')).toHaveTextContent(DEFAULT_DATA_STORE.name);
|
||||
expect(getByTestId('data-store-card-column-count')).toBeInTheDocument();
|
||||
expect(getByTestId('data-store-card-last-updated')).toHaveTextContent('Last updated');
|
||||
expect(getByTestId('data-store-card-created')).toHaveTextContent('Created');
|
||||
expect(getByTestId('data-table-card-icon')).toBeInTheDocument();
|
||||
expect(getByTestId('data-table-card-name')).toHaveTextContent(DEFAULT_DATA_TABLE.name);
|
||||
expect(getByTestId('data-table-card-column-count')).toBeInTheDocument();
|
||||
expect(getByTestId('data-table-card-last-updated')).toHaveTextContent('Last updated');
|
||||
expect(getByTestId('data-table-card-created')).toHaveTextContent('Created');
|
||||
});
|
||||
|
||||
it('should not render readonly badge when not readonly', () => {
|
||||
@@ -96,20 +96,20 @@ describe('DataStoreCard', () => {
|
||||
expect(getByText('Read only')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct route to data store details', () => {
|
||||
it('should render correct route to data table details', () => {
|
||||
const wrapper = renderComponent();
|
||||
const link = wrapper.getByTestId('data-store-card-link');
|
||||
const link = wrapper.getByTestId('data-table-card-link');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
`/projects/${DEFAULT_DATA_STORE.projectId}/datatables/${DEFAULT_DATA_STORE.id}`,
|
||||
`/projects/${DEFAULT_DATA_TABLE.projectId}/datatables/${DEFAULT_DATA_TABLE.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should display column count information', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const columnCountElement = getByTestId('data-store-card-column-count');
|
||||
const columnCountElement = getByTestId('data-table-card-column-count');
|
||||
expect(columnCountElement).toBeInTheDocument();
|
||||
expect(columnCountElement).toHaveTextContent(`${DEFAULT_DATA_STORE.columns.length + 1}`);
|
||||
expect(columnCountElement).toHaveTextContent(`${DEFAULT_DATA_TABLE.columns.length + 1}`);
|
||||
});
|
||||
});
|
||||
+29
-29
@@ -1,21 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import { DATA_STORE_DETAILS } from '@/features/dataStore/constants';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
import { DATA_TABLE_DETAILS } from '@/features/dataTable/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import DataTableActions from '@/features/dataTable/components/DataTableActions.vue';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
|
||||
import { N8nBadge, N8nCard, N8nIcon, N8nLink, N8nText } from '@n8n/design-system';
|
||||
type Props = {
|
||||
dataStore: DataStoreResource;
|
||||
dataTable: DataTable;
|
||||
readOnly?: boolean;
|
||||
showOwnershipBadge?: boolean;
|
||||
};
|
||||
|
||||
const i18n = useI18n();
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
actions: () => [],
|
||||
@@ -23,28 +23,28 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showOwnershipBadge: false,
|
||||
});
|
||||
|
||||
const dataStoreRoute = computed(() => {
|
||||
const dataTableRoute = computed(() => {
|
||||
return {
|
||||
name: DATA_STORE_DETAILS,
|
||||
name: DATA_TABLE_DETAILS,
|
||||
params: {
|
||||
projectId: props.dataStore.projectId,
|
||||
id: props.dataStore.id,
|
||||
projectId: props.dataTable.projectId,
|
||||
id: props.dataTable.id,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getDataStoreSize = computed(() => {
|
||||
const size = dataStoreStore.dataStoreSizes[props.dataStore.id] ?? 0;
|
||||
const getDataTableSize = computed(() => {
|
||||
const size = dataTableStore.dataTableSizes[props.dataTable.id] ?? 0;
|
||||
return size;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div data-test-id="data-store-card">
|
||||
<N8nLink :to="dataStoreRoute" class="data-store-card" data-test-id="data-store-card-link">
|
||||
<div data-test-id="data-table-card">
|
||||
<N8nLink :to="dataTableRoute" class="data-table-card" data-test-id="data-table-card-link">
|
||||
<N8nCard :class="$style.card">
|
||||
<template #prepend>
|
||||
<N8nIcon
|
||||
data-test-id="data-store-card-icon"
|
||||
data-test-id="data-table-card-icon"
|
||||
:class="$style['card-icon']"
|
||||
icon="database"
|
||||
size="xlarge"
|
||||
@@ -53,8 +53,8 @@ const getDataStoreSize = computed(() => {
|
||||
</template>
|
||||
<template #header>
|
||||
<div :class="$style['card-header']">
|
||||
<N8nText tag="h2" bold data-test-id="data-store-card-name">
|
||||
{{ props.dataStore.name }}
|
||||
<N8nText tag="h2" bold data-test-id="data-table-card-name">
|
||||
{{ props.dataTable.name }}
|
||||
</N8nText>
|
||||
<N8nBadge v-if="props.readOnly" class="ml-3xs" theme="tertiary" bold>
|
||||
{{ i18n.baseText('workflows.item.readonly') }}
|
||||
@@ -67,11 +67,11 @@ const getDataStoreSize = computed(() => {
|
||||
size="small"
|
||||
color="text-light"
|
||||
:class="[$style['info-cell'], $style['info-cell--size']]"
|
||||
data-test-id="data-store-card-size"
|
||||
data-test-id="data-table-card-size"
|
||||
>
|
||||
{{
|
||||
i18n.baseText('dataStore.card.size', {
|
||||
interpolate: { size: getDataStoreSize },
|
||||
i18n.baseText('dataTable.card.size', {
|
||||
interpolate: { size: getDataTableSize },
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
@@ -79,11 +79,11 @@ const getDataStoreSize = computed(() => {
|
||||
size="small"
|
||||
color="text-light"
|
||||
:class="[$style['info-cell'], $style['info-cell--column-count']]"
|
||||
data-test-id="data-store-card-column-count"
|
||||
data-test-id="data-table-card-column-count"
|
||||
>
|
||||
{{
|
||||
i18n.baseText('dataStore.card.column.count', {
|
||||
interpolate: { count: props.dataStore.columns.length + 1 },
|
||||
i18n.baseText('dataTable.card.column.count', {
|
||||
interpolate: { count: props.dataTable.columns.length + 1 },
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
@@ -91,26 +91,26 @@ const getDataStoreSize = computed(() => {
|
||||
size="small"
|
||||
color="text-light"
|
||||
:class="[$style['info-cell'], $style['info-cell--updated']]"
|
||||
data-test-id="data-store-card-last-updated"
|
||||
data-test-id="data-table-card-last-updated"
|
||||
>
|
||||
{{ i18n.baseText('workerList.item.lastUpdated') }}
|
||||
<TimeAgo :date="String(props.dataStore.updatedAt)" />
|
||||
<TimeAgo :date="String(props.dataTable.updatedAt)" />
|
||||
</N8nText>
|
||||
<N8nText
|
||||
size="small"
|
||||
color="text-light"
|
||||
:class="[$style['info-cell'], $style['info-cell--created']]"
|
||||
data-test-id="data-store-card-created"
|
||||
data-test-id="data-table-card-created"
|
||||
>
|
||||
{{ i18n.baseText('workflows.item.created') }}
|
||||
<TimeAgo :date="String(props.dataStore.createdAt)" />
|
||||
<TimeAgo :date="String(props.dataTable.createdAt)" />
|
||||
</N8nText>
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<div :class="$style['card-actions']" @click.prevent>
|
||||
<DataStoreActions
|
||||
:data-store="props.dataStore"
|
||||
<DataTableActions
|
||||
:data-table="props.dataTable"
|
||||
:is-read-only="props.readOnly"
|
||||
location="card"
|
||||
/>
|
||||
+9
-9
@@ -2,20 +2,20 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { DATA_STORE_NODES } from '@/constants';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { DATA_TABLE_NODES } from '@/constants';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
import { N8nCallout } from '@n8n/design-system';
|
||||
const i18n = useI18n();
|
||||
const nvdStore = useNDVStore();
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
const calloutType = computed(() => {
|
||||
if (!DATA_STORE_NODES.includes(nvdStore.activeNode?.type ?? '')) {
|
||||
if (!DATA_TABLE_NODES.includes(nvdStore.activeNode?.type ?? '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sizeLimitState = dataStoreStore.dataStoreSizeLimitState;
|
||||
const sizeLimitState = dataTableStore.dataTableSizeLimitState;
|
||||
switch (sizeLimitState) {
|
||||
case 'error':
|
||||
return 'danger';
|
||||
@@ -30,18 +30,18 @@ const calloutType = computed(() => {
|
||||
<N8nCallout v-if="calloutType" :theme="calloutType" class="mt-xs">
|
||||
<span v-if="calloutType === 'danger'">
|
||||
{{
|
||||
i18n.baseText('dataStore.banner.storageLimitError.message', {
|
||||
i18n.baseText('dataTable.banner.storageLimitError.message', {
|
||||
interpolate: {
|
||||
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||
usage: `${dataTableStore.dataTableSize} / ${dataTableStore.maxSizeMB}MB`,
|
||||
},
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
i18n.baseText('dataStore.banner.storageLimitWarning.message', {
|
||||
i18n.baseText('dataTable.banner.storageLimitWarning.message', {
|
||||
interpolate: {
|
||||
usage: `${dataStoreStore.dataStoreSize} / ${dataStoreStore.maxSizeMB}MB`,
|
||||
usage: `${dataTableStore.dataTableSize} / ${dataTableStore.maxSizeMB}MB`,
|
||||
},
|
||||
})
|
||||
}}
|
||||
+34
-34
@@ -1,11 +1,11 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import AddColumnButton from '@/features/dataStore/components/dataGrid/AddColumnButton.vue';
|
||||
import AddColumnButton from '@/features/dataTable/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';
|
||||
import { MAX_COLUMN_NAME_LENGTH } from '@/features/dataTable/constants';
|
||||
|
||||
vi.mock('@/features/dataStore/composables/useDataStoreTypes', () => ({
|
||||
useDataStoreTypes: () => ({
|
||||
vi.mock('@/features/dataTable/composables/useDataTableTypes', () => ({
|
||||
useDataTableTypes: () => ({
|
||||
getIconForType: (type: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
string: 'abc',
|
||||
@@ -29,12 +29,12 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
useI18n: () => ({
|
||||
baseText: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dataStore.addColumn.label': 'Add column',
|
||||
'dataStore.addColumn.nameInput.label': 'Column name',
|
||||
'dataStore.addColumn.nameInput.placeholder': 'Enter column name',
|
||||
'dataStore.addColumn.typeInput.label': 'Column type',
|
||||
'dataStore.addColumn.invalidName.error': 'Invalid column name',
|
||||
'dataStore.addColumn.invalidName.description':
|
||||
'dataTable.addColumn.label': 'Add column',
|
||||
'dataTable.addColumn.nameInput.label': 'Column name',
|
||||
'dataTable.addColumn.nameInput.placeholder': 'Enter column name',
|
||||
'dataTable.addColumn.typeInput.label': 'Column type',
|
||||
'dataTable.addColumn.invalidName.error': 'Invalid column name',
|
||||
'dataTable.addColumn.invalidName.description':
|
||||
'Column names must start with a letter and contain only letters, numbers, and hyphens',
|
||||
};
|
||||
return translations[key] || key;
|
||||
@@ -58,12 +58,12 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should render the add column button', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('data-store-add-column-trigger-button')).toBeInTheDocument();
|
||||
expect(getByTestId('data-table-add-column-trigger-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should focus name input when popover opens', async () => {
|
||||
const { getByTestId, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -75,14 +75,14 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should call addColumn with correct payload', async () => {
|
||||
const { getByTestId, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
const nameInput = getByPlaceholderText('Enter column name');
|
||||
await fireEvent.update(nameInput, 'newColumn');
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
@@ -94,19 +94,19 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should disable submit button when name is empty', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for invalid column names', async () => {
|
||||
const { getByPlaceholderText, getByText, getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -118,14 +118,14 @@ describe('AddColumnButton', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Invalid column name')).toBeInTheDocument();
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow valid column names', async () => {
|
||||
const { getByTestId, getByPlaceholderText, queryByText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should clear error when correcting invalid name', async () => {
|
||||
const { getByTestId, getByPlaceholderText, getByText, queryByText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should respect max column name length', async () => {
|
||||
const { getByTestId, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -182,7 +182,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should allow selecting different column types', async () => {
|
||||
const { getByPlaceholderText, getByRole, getByText, getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('AddColumnButton', () => {
|
||||
const numberOption = getByText('number');
|
||||
await fireEvent.click(numberOption);
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
expect(addColumnHandler).toHaveBeenCalledWith({
|
||||
@@ -208,14 +208,14 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should reset form after successful submission', async () => {
|
||||
const { getByPlaceholderText, getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
const nameInput = getByPlaceholderText('Enter column name');
|
||||
await fireEvent.update(nameInput, 'testColumn');
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
// Click button again to open popover
|
||||
@@ -229,14 +229,14 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should close popover after successful submission', async () => {
|
||||
const { getByPlaceholderText, getByTestId, queryByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
const nameInput = getByPlaceholderText('Enter column name');
|
||||
await fireEvent.update(nameInput, 'testColumn');
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -250,14 +250,14 @@ describe('AddColumnButton', () => {
|
||||
success: false,
|
||||
error: 'Column name already exists',
|
||||
});
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
const nameInput = getByPlaceholderText('Enter column name');
|
||||
await fireEvent.update(nameInput, 'testColumn');
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -267,7 +267,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should allow submission with Enter key', async () => {
|
||||
const { getByTestId, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -283,7 +283,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should display all column type options', async () => {
|
||||
const { getByTestId, getByRole, getByText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should set value to "date" when selecting "datetime" option', async () => {
|
||||
const { getByTestId, getByRole, getByText, getByPlaceholderText } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
@@ -313,7 +313,7 @@ describe('AddColumnButton', () => {
|
||||
const dateOption = getByText('datetime');
|
||||
await fireEvent.click(dateOption);
|
||||
|
||||
const submitButton = getByTestId('data-store-add-column-submit-button');
|
||||
const submitButton = getByTestId('data-table-add-column-submit-button');
|
||||
await fireEvent.click(submitButton);
|
||||
|
||||
expect(addColumnHandler).toHaveBeenCalledWith({
|
||||
@@ -324,7 +324,7 @@ describe('AddColumnButton', () => {
|
||||
|
||||
it('should show tooltip with error description', async () => {
|
||||
const { getByPlaceholderText, getByText, getByTestId } = renderComponent();
|
||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||
const addButton = getByTestId('data-table-add-column-trigger-button');
|
||||
|
||||
await fireEvent.click(addButton);
|
||||
|
||||
+26
-26
@@ -2,13 +2,13 @@
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import type {
|
||||
AddColumnResponse,
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStoreColumnType,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
import { DATA_STORE_COLUMN_TYPES } from '@/features/dataStore/datastore.types';
|
||||
DataTableColumnCreatePayload,
|
||||
DataTableColumnType,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { DATA_TABLE_COLUMN_TYPES } from '@/features/dataTable/dataTable.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
||||
import { COLUMN_NAME_REGEX, MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/constants';
|
||||
import { useDataTableTypes } from '@/features/dataTable/composables/useDataTableTypes';
|
||||
import { COLUMN_NAME_REGEX, MAX_COLUMN_NAME_LENGTH } from '@/features/dataTable/constants';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
|
||||
import {
|
||||
@@ -31,22 +31,22 @@ type FormError = {
|
||||
const props = defineProps<{
|
||||
// the params key is needed so that we can pass this directly to ag-grid as column
|
||||
params: {
|
||||
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||
onAddColumn: (column: DataTableColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||
};
|
||||
popoverId?: string;
|
||||
useTextTrigger?: boolean;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const { getIconForType } = useDataStoreTypes();
|
||||
const { getIconForType } = useDataTableTypes();
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const nameInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const columnName = ref('');
|
||||
const columnType = ref<DataStoreColumnType>('string');
|
||||
const columnType = ref<DataTableColumnType>('string');
|
||||
|
||||
const columnTypes: DataStoreColumnType[] = [...DATA_STORE_COLUMN_TYPES];
|
||||
const columnTypes: DataTableColumnType[] = [...DATA_TABLE_COLUMN_TYPES];
|
||||
|
||||
const error = ref<FormError | null>(null);
|
||||
|
||||
@@ -76,18 +76,18 @@ const onAddButtonClicked = async () => {
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
let errorMessage = i18n.baseText('dataStore.addColumn.error');
|
||||
let errorMessage = i18n.baseText('dataTable.addColumn.error');
|
||||
let errorDescription = response.errorMessage;
|
||||
// Provide custom error message for conflict (column already exists)
|
||||
if (response.httpStatus === 409) {
|
||||
errorMessage = i18n.baseText('dataStore.addColumn.alreadyExistsError', {
|
||||
errorMessage = i18n.baseText('dataTable.addColumn.alreadyExistsError', {
|
||||
interpolate: { name: columnName.value },
|
||||
});
|
||||
errorDescription = response.errorMessage?.includes('system')
|
||||
? i18n.baseText('dataStore.addColumn.systemColumnDescription')
|
||||
? i18n.baseText('dataTable.addColumn.systemColumnDescription')
|
||||
: response.errorMessage?.includes('testing')
|
||||
? i18n.baseText('dataStore.addColumn.testingColumnDescription')
|
||||
: i18n.baseText('dataStore.addColumn.alreadyExistsDescription');
|
||||
? i18n.baseText('dataTable.addColumn.testingColumnDescription')
|
||||
: i18n.baseText('dataTable.addColumn.alreadyExistsDescription');
|
||||
}
|
||||
error.value = {
|
||||
message: errorMessage,
|
||||
@@ -121,8 +121,8 @@ const validateName = () => {
|
||||
}
|
||||
if (columnName.value && !COLUMN_NAME_REGEX.test(columnName.value)) {
|
||||
error.value = {
|
||||
message: i18n.baseText('dataStore.addColumn.invalidName.error'),
|
||||
description: i18n.baseText('dataStore.addColumn.invalidName.description'),
|
||||
message: i18n.baseText('dataTable.addColumn.invalidName.error'),
|
||||
description: i18n.baseText('dataTable.addColumn.invalidName.description'),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -131,7 +131,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nTooltip :disabled="popoverOpen" :content="i18n.baseText('dataStore.addColumn.label')">
|
||||
<N8nTooltip :disabled="popoverOpen" :content="i18n.baseText('dataTable.addColumn.label')">
|
||||
<div class="add-column-header-component-wrapper">
|
||||
<N8nPopoverReka
|
||||
:id="popoverId"
|
||||
@@ -142,13 +142,13 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
>
|
||||
<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 data-test-id="data-table-add-column-trigger-button" type="tertiary">
|
||||
{{ i18n.baseText('dataTable.addColumn.label') }}
|
||||
</N8nButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<N8nIconButton
|
||||
data-test-id="data-store-add-column-trigger-button"
|
||||
data-test-id="data-table-add-column-trigger-button"
|
||||
text
|
||||
icon="plus"
|
||||
type="tertiary"
|
||||
@@ -162,14 +162,14 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
>
|
||||
<div class="popover-body">
|
||||
<N8nInputLabel
|
||||
:label="i18n.baseText('dataStore.addColumn.nameInput.label')"
|
||||
:label="i18n.baseText('dataTable.addColumn.nameInput.label')"
|
||||
:required="true"
|
||||
:class="error ? '' : 'mb-s'"
|
||||
>
|
||||
<N8nInput
|
||||
ref="nameInputRef"
|
||||
v-model="columnName"
|
||||
:placeholder="i18n.baseText('dataStore.addColumn.nameInput.placeholder')"
|
||||
:placeholder="i18n.baseText('dataTable.addColumn.nameInput.placeholder')"
|
||||
:maxlength="MAX_COLUMN_NAME_LENGTH"
|
||||
@keyup.enter="onAddButtonClicked"
|
||||
@input="onInput"
|
||||
@@ -194,7 +194,7 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
</div>
|
||||
</N8nInputLabel>
|
||||
<N8nInputLabel
|
||||
:label="i18n.baseText('dataStore.addColumn.typeInput.label')"
|
||||
:label="i18n.baseText('dataTable.addColumn.typeInput.label')"
|
||||
:required="true"
|
||||
class="type-label"
|
||||
>
|
||||
@@ -217,14 +217,14 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
||||
</N8nSelect>
|
||||
</N8nInputLabel>
|
||||
<N8nButton
|
||||
data-test-id="data-store-add-column-submit-button"
|
||||
data-test-id="data-table-add-column-submit-button"
|
||||
type="primary"
|
||||
class="mt-m"
|
||||
size="large"
|
||||
:disabled="!columnName || !columnType || !!error"
|
||||
@click="onAddButtonClicked"
|
||||
>
|
||||
{{ i18n.baseText('dataStore.addColumn.label') }}
|
||||
{{ i18n.baseText('dataTable.addColumn.label') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
+1
-1
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nTooltip :content="i18n.baseText('dataStore.addRow.label')">
|
||||
<N8nTooltip :content="i18n.baseText('dataTable.addRow.label')">
|
||||
<N8nIconButton text type="tertiary" icon="plus" @click="props.params.onClick" />
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { vi } from 'vitest';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import ColumnHeader, {
|
||||
type HeaderParamsWithDelete,
|
||||
} from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
|
||||
} from '@/features/dataTable/components/dataGrid/ColumnHeader.vue';
|
||||
|
||||
// Mock N8nActionDropdown to make it easy to trigger item selection
|
||||
vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||
@@ -57,12 +57,12 @@ const renderComponent = createComponentRenderer(ColumnHeader, {
|
||||
describe('ColumnHeader', () => {
|
||||
it('renders the column display name', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('data-store-column-header-text')).toHaveTextContent('My Column');
|
||||
expect(getByTestId('data-table-column-header-text')).toHaveTextContent('My Column');
|
||||
});
|
||||
|
||||
it('shows actions dropdown only on hover', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const wrapper = getByTestId('data-store-column-header');
|
||||
const wrapper = getByTestId('data-table-column-header');
|
||||
expect(wrapper).not.toBeNull();
|
||||
|
||||
const deleteButton = getByTestId('action-delete');
|
||||
+15
-15
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { IHeaderParams, SortDirection } from 'ag-grid-community';
|
||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
||||
import { useDataTableTypes } from '@/features/dataTable/composables/useDataTableTypes';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { isAGGridCellType } from '@/features/dataStore/typeGuards';
|
||||
import { isAGGridCellType } from '@/features/dataTable/typeGuards';
|
||||
import { N8nActionDropdown, N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
|
||||
export type HeaderParamsWithDelete = IHeaderParams & {
|
||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
||||
params: HeaderParamsWithDelete;
|
||||
}>();
|
||||
|
||||
const { getIconForType, mapToDataStoreColumnType } = useDataStoreTypes();
|
||||
const { getIconForType, mapToDataTableColumnType } = useDataTableTypes();
|
||||
const i18n = useI18n();
|
||||
|
||||
const isHovered = ref(false);
|
||||
@@ -84,15 +84,15 @@ const typeIcon = computed(() => {
|
||||
if (!isAGGridCellType(cellDataType)) {
|
||||
return null;
|
||||
}
|
||||
return getIconForType(mapToDataStoreColumnType(cellDataType));
|
||||
return getIconForType(mapToDataTableColumnType(cellDataType));
|
||||
});
|
||||
|
||||
const columnActionItems = [
|
||||
{
|
||||
id: ItemAction.Delete,
|
||||
label: i18n.baseText('dataStore.deleteColumn.confirm.title'),
|
||||
label: i18n.baseText('dataTable.deleteColumn.confirm.title'),
|
||||
icon: 'trash-2',
|
||||
customClass: 'data-store-column-header-action-item',
|
||||
customClass: 'data-table-column-header-action-item',
|
||||
} as const,
|
||||
];
|
||||
|
||||
@@ -150,15 +150,15 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['ag-header-cell-label', 'data-store-column-header-wrapper', { sortable: isSortable }]"
|
||||
data-test-id="data-store-column-header"
|
||||
:class="['ag-header-cell-label', 'data-table-column-header-wrapper', { sortable: isSortable }]"
|
||||
data-test-id="data-table-column-header"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
@click="onHeaderClick"
|
||||
>
|
||||
<div class="data-store-column-header-icon-wrapper">
|
||||
<div class="data-table-column-header-icon-wrapper">
|
||||
<N8nIcon v-if="typeIcon" :icon="typeIcon" />
|
||||
<span class="ag-header-cell-text" data-test-id="data-store-column-header-text">{{
|
||||
<span class="ag-header-cell-text" data-test-id="data-table-column-header-text">{{
|
||||
props.params.displayName
|
||||
}}</span>
|
||||
|
||||
@@ -179,7 +179,7 @@ onUnmounted(() => {
|
||||
|
||||
<N8nActionDropdown
|
||||
v-show="isMenuButtonVisible"
|
||||
data-test-id="data-store-column-header-actions"
|
||||
data-test-id="data-table-column-header-actions"
|
||||
:items="columnActionItems"
|
||||
:placement="'bottom-start'"
|
||||
:activator-icon="'ellipsis'"
|
||||
@@ -192,7 +192,7 @@ onUnmounted(() => {
|
||||
|
||||
<style lang="scss">
|
||||
// TODO: neither scoped nor module works here. Is there a way to resolve this?
|
||||
.data-store-column-header-wrapper {
|
||||
.data-table-column-header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -204,12 +204,12 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.data-store-column-header-action-item {
|
||||
.data-table-column-header-action-item {
|
||||
justify-content: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.data-store-column-header-icon-wrapper {
|
||||
.data-table-column-header-icon-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -217,7 +217,7 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-store-column-header-icon-wrapper .n8n-icon {
|
||||
.data-table-column-header-icon-wrapper .n8n-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
+28
-28
@@ -1,8 +1,8 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DataStoreTable from '@/features/dataStore/components/dataGrid/DataStoreTable.vue';
|
||||
import DataTableTable from '@/features/dataTable/components/dataGrid/DataTableTable.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import type { DataStore } from '@/features/dataStore/datastore.types';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
// Mock ag-grid-vue3
|
||||
interface MockComponentInstance {
|
||||
@@ -55,17 +55,17 @@ vi.mock('ag-grid-community', () => ({
|
||||
}));
|
||||
|
||||
// Mock the n8n theme
|
||||
vi.mock('@/features/dataStore/components/dataGrid/n8nTheme', () => ({
|
||||
vi.mock('@/features/dataTable/components/dataGrid/n8nTheme', () => ({
|
||||
n8nTheme: 'n8n-theme',
|
||||
}));
|
||||
|
||||
// Mock AddColumnPopover
|
||||
vi.mock('@/features/dataStore/components/dataGrid/AddColumnPopover.vue', () => ({
|
||||
vi.mock('@/features/dataTable/components/dataGrid/AddColumnPopover.vue', () => ({
|
||||
default: {
|
||||
name: 'AddColumnPopover',
|
||||
template:
|
||||
'<div data-test-id="add-column-popover"><button data-test-id="data-store-add-column-button" @click="$emit(\'add-column\', { column: { name: \'newColumn\', type: \'string\' } })">Add Column</button></div>',
|
||||
props: ['dataStore'],
|
||||
'<div data-test-id="add-column-popover"><button data-test-id="data-table-add-column-button" @click="$emit(\'add-column\', { column: { name: \'newColumn\', type: \'string\' } })">Add Column</button></div>',
|
||||
props: ['dataTable'],
|
||||
emits: ['add-column'],
|
||||
},
|
||||
}));
|
||||
@@ -78,8 +78,8 @@ vi.mock('@/composables/useToast', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/composables/useDataStorePagination', () => ({
|
||||
useDataStorePagination: () => ({
|
||||
vi.mock('@/features/dataTable/composables/useDataTablePagination', () => ({
|
||||
useDataTablePagination: () => ({
|
||||
totalItems: 0,
|
||||
setTotalItems: vi.fn(),
|
||||
ensureItemOnPage: vi.fn(),
|
||||
@@ -93,16 +93,16 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
useI18n: () => ({
|
||||
baseText: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dataStore.addRow.label': 'Add Row',
|
||||
'dataStore.addRow.disabled.tooltip': 'Add a column first',
|
||||
'dataTable.addRow.label': 'Add Row',
|
||||
'dataTable.addRow.disabled.tooltip': 'Add a column first',
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/composables/useDataStoreTypes', () => ({
|
||||
useDataStoreTypes: () => ({
|
||||
vi.mock('@/features/dataTable/composables/useDataTableTypes', () => ({
|
||||
useDataTableTypes: () => ({
|
||||
mapToAGCellType: (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
string: 'text',
|
||||
@@ -115,9 +115,9 @@ vi.mock('@/features/dataStore/composables/useDataStoreTypes', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDataStore: DataStore = {
|
||||
id: 'test-datastore-1',
|
||||
name: 'Test DataStore',
|
||||
const mockDataTable: DataTable = {
|
||||
id: 'test-dataTable-1',
|
||||
name: 'Test DataTable',
|
||||
projectId: 'project-1',
|
||||
columns: [
|
||||
{ id: 'col1', name: 'firstName', type: 'string', index: 1 },
|
||||
@@ -129,19 +129,19 @@ const mockDataStore: DataStore = {
|
||||
sizeBytes: 0,
|
||||
};
|
||||
|
||||
describe('DataStoreTable', () => {
|
||||
const renderComponent = createComponentRenderer(DataStoreTable, {
|
||||
describe('DataTableTable', () => {
|
||||
const renderComponent = createComponentRenderer(DataTableTable, {
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
dataTable: mockDataTable,
|
||||
},
|
||||
});
|
||||
|
||||
let dataStoreStore: ReturnType<typeof useDataStoreStore>;
|
||||
let dataTableStore: ReturnType<typeof useDataTableStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
dataStoreStore = useDataStoreStore();
|
||||
dataStoreStore.addDataStoreColumn = vi.fn().mockResolvedValue({
|
||||
dataTableStore = useDataTableStore();
|
||||
dataTableStore.addDataTableColumn = vi.fn().mockResolvedValue({
|
||||
id: 'new-col',
|
||||
name: 'newColumn',
|
||||
type: 'string',
|
||||
@@ -162,20 +162,20 @@ describe('DataStoreTable', () => {
|
||||
|
||||
it('should render pagination controls', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('data-store-content-pagination')).toBeInTheDocument();
|
||||
expect(getByTestId('data-table-content-pagination')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty Data Store', () => {
|
||||
it('should show grid for empty data store', () => {
|
||||
const emptyDataStore: DataStore = {
|
||||
...mockDataStore,
|
||||
describe('Empty DataTable', () => {
|
||||
it('should show grid for empty data table', () => {
|
||||
const emptyDataTable: DataTable = {
|
||||
...mockDataTable,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
dataStore: emptyDataStore,
|
||||
dataTable: emptyDataTable,
|
||||
},
|
||||
});
|
||||
|
||||
+62
-62
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import type {
|
||||
DataStore,
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStoreRow,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
DataTable,
|
||||
DataTableColumnCreatePayload,
|
||||
DataTableRow,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { AgGridVue } from 'ag-grid-vue3';
|
||||
import type { GetRowIdParams, GridReadyEvent } from 'ag-grid-community';
|
||||
import {
|
||||
@@ -30,14 +30,14 @@ import {
|
||||
DateFilterModule,
|
||||
EventApiModule,
|
||||
} from 'ag-grid-community';
|
||||
import { n8nTheme } from '@/features/dataStore/components/dataGrid/n8nTheme';
|
||||
import { n8nTheme } from '@/features/dataTable/components/dataGrid/n8nTheme';
|
||||
import SelectedItemsInfo from '@/components/common/SelectedItemsInfo.vue';
|
||||
import { DATA_STORE_HEADER_HEIGHT, DATA_STORE_ROW_HEIGHT } from '@/features/dataStore/constants';
|
||||
import { useDataStorePagination } from '@/features/dataStore/composables/useDataStorePagination';
|
||||
import { useDataStoreGridBase } from '@/features/dataStore/composables/useDataStoreGridBase';
|
||||
import { useDataStoreSelection } from '@/features/dataStore/composables/useDataStoreSelection';
|
||||
import { useDataStoreOperations } from '@/features/dataStore/composables/useDataStoreOperations';
|
||||
import { useDataStoreColumnFilters } from '@/features/dataStore/composables/useDataStoreColumnFilters';
|
||||
import { DATA_TABLE_HEADER_HEIGHT, DATA_TABLE_ROW_HEIGHT } from '@/features/dataTable/constants';
|
||||
import { useDataTablePagination } from '@/features/dataTable/composables/useDataTablePagination';
|
||||
import { useDataTableGridBase } from '@/features/dataTable/composables/useDataTableGridBase';
|
||||
import { useDataTableSelection } from '@/features/dataTable/composables/useDataTableSelection';
|
||||
import { useDataTableOperations } from '@/features/dataTable/composables/useDataTableOperations';
|
||||
import { useDataTableColumnFilters } from '@/features/dataTable/composables/useDataTableColumnFilters';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
import { ElPagination } from 'element-plus';
|
||||
@@ -66,7 +66,7 @@ ModuleRegistry.registerModules([
|
||||
]);
|
||||
|
||||
type Props = {
|
||||
dataStore: DataStore;
|
||||
dataTable: DataTable;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -79,19 +79,19 @@ const gridContainerRef = useTemplateRef<HTMLDivElement>('gridContainerRef');
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const dataStoreGridBase = useDataStoreGridBase({
|
||||
const dataTableGridBase = useDataTableGridBase({
|
||||
gridContainerRef,
|
||||
onDeleteColumn: onDeleteColumnFunction,
|
||||
onAddRowClick: onAddRowClickFunction,
|
||||
onAddColumn: onAddColumnFunction,
|
||||
});
|
||||
const rowData = ref<DataStoreRow[]>([]);
|
||||
const rowData = ref<DataTableRow[]>([]);
|
||||
const hasRecords = computed(() => rowData.value.length > 0);
|
||||
|
||||
const { initializeFilters, onFilterChanged, currentFilterJSON } = useDataStoreColumnFilters({
|
||||
gridApi: dataStoreGridBase.gridApi,
|
||||
colDefs: dataStoreGridBase.colDefs,
|
||||
setGridData: dataStoreGridBase.setGridData,
|
||||
const { initializeFilters, onFilterChanged, currentFilterJSON } = useDataTableColumnFilters({
|
||||
gridApi: dataTableGridBase.gridApi,
|
||||
colDefs: dataTableGridBase.colDefs,
|
||||
setGridData: dataTableGridBase.setGridData,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -103,64 +103,64 @@ const {
|
||||
setTotalItems,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
} = useDataStorePagination({ onChange: fetchDataStoreRowsFunction });
|
||||
} = useDataTablePagination({ onChange: fetchDataTableRowsFunction });
|
||||
|
||||
const selection = useDataStoreSelection({
|
||||
gridApi: dataStoreGridBase.gridApi,
|
||||
const selection = useDataTableSelection({
|
||||
gridApi: dataTableGridBase.gridApi,
|
||||
});
|
||||
|
||||
const dataStoreOperations = useDataStoreOperations({
|
||||
colDefs: dataStoreGridBase.colDefs,
|
||||
const dataTableOperations = useDataTableOperations({
|
||||
colDefs: dataTableGridBase.colDefs,
|
||||
rowData,
|
||||
deleteGridColumn: dataStoreGridBase.deleteColumn,
|
||||
setGridData: dataStoreGridBase.setGridData,
|
||||
insertGridColumnAtIndex: dataStoreGridBase.insertColumnAtIndex,
|
||||
dataStoreId: props.dataStore.id,
|
||||
projectId: props.dataStore.projectId,
|
||||
addGridColumn: dataStoreGridBase.addColumn,
|
||||
moveGridColumn: dataStoreGridBase.moveColumn,
|
||||
gridApi: dataStoreGridBase.gridApi,
|
||||
deleteGridColumn: dataTableGridBase.deleteColumn,
|
||||
setGridData: dataTableGridBase.setGridData,
|
||||
insertGridColumnAtIndex: dataTableGridBase.insertColumnAtIndex,
|
||||
dataTableId: props.dataTable.id,
|
||||
projectId: props.dataTable.projectId,
|
||||
addGridColumn: dataTableGridBase.addColumn,
|
||||
moveGridColumn: dataTableGridBase.moveColumn,
|
||||
gridApi: dataTableGridBase.gridApi,
|
||||
totalItems,
|
||||
setTotalItems,
|
||||
ensureItemOnPage,
|
||||
focusFirstEditableCell: dataStoreGridBase.focusFirstEditableCell,
|
||||
focusFirstEditableCell: dataTableGridBase.focusFirstEditableCell,
|
||||
toggleSave: emit.bind(null, 'toggleSave'),
|
||||
currentPage,
|
||||
pageSize,
|
||||
currentSortBy: dataStoreGridBase.currentSortBy,
|
||||
currentSortOrder: dataStoreGridBase.currentSortOrder,
|
||||
currentSortBy: dataTableGridBase.currentSortBy,
|
||||
currentSortOrder: dataTableGridBase.currentSortOrder,
|
||||
handleClearSelection: selection.handleClearSelection,
|
||||
selectedRowIds: selection.selectedRowIds,
|
||||
handleCopyFocusedCell: dataStoreGridBase.handleCopyFocusedCell,
|
||||
handleCopyFocusedCell: dataTableGridBase.handleCopyFocusedCell,
|
||||
currentFilterJSON,
|
||||
});
|
||||
|
||||
async function onDeleteColumnFunction(columnId: string) {
|
||||
await dataStoreOperations.onDeleteColumn(columnId);
|
||||
await dataTableOperations.onDeleteColumn(columnId);
|
||||
}
|
||||
|
||||
async function onAddColumnFunction(column: DataStoreColumnCreatePayload) {
|
||||
return await dataStoreOperations.onAddColumn(column);
|
||||
async function onAddColumnFunction(column: DataTableColumnCreatePayload) {
|
||||
return await dataTableOperations.onAddColumn(column);
|
||||
}
|
||||
|
||||
async function onAddRowClickFunction() {
|
||||
await dataStoreOperations.onAddRowClick();
|
||||
await dataTableOperations.onAddRowClick();
|
||||
}
|
||||
|
||||
async function fetchDataStoreRowsFunction() {
|
||||
await dataStoreOperations.fetchDataStoreRows();
|
||||
async function fetchDataTableRowsFunction() {
|
||||
await dataTableOperations.fetchDataTableRows();
|
||||
}
|
||||
|
||||
const initialize = async (params: GridReadyEvent) => {
|
||||
dataStoreGridBase.onGridReady(params);
|
||||
dataStoreGridBase.loadColumns(props.dataStore.columns);
|
||||
await dataStoreOperations.fetchDataStoreRows();
|
||||
dataTableGridBase.onGridReady(params);
|
||||
dataTableGridBase.loadColumns(props.dataTable.columns);
|
||||
await dataTableOperations.fetchDataTableRows();
|
||||
initializeFilters();
|
||||
};
|
||||
|
||||
const customNoRowsOverlay = `<div class="no-rows-overlay ag-overlay-no-rows-center" data-test-id="data-store-no-rows-overlay">${i18n.baseText('dataStore.noRows')}</div>`;
|
||||
const customNoRowsOverlay = `<div class="no-rows-overlay ag-overlay-no-rows-center" data-test-id="data-table-no-rows-overlay">${i18n.baseText('dataTable.noRows')}</div>`;
|
||||
|
||||
watch([dataStoreGridBase.currentSortBy, dataStoreGridBase.currentSortOrder], async () => {
|
||||
watch([dataTableGridBase.currentSortBy, dataTableGridBase.currentSortOrder], async () => {
|
||||
await setCurrentPage(1);
|
||||
});
|
||||
|
||||
@@ -169,8 +169,8 @@ watch(currentFilterJSON, async () => {
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
addRow: dataStoreOperations.onAddRowClick,
|
||||
addColumn: dataStoreOperations.onAddColumn,
|
||||
addRow: dataTableOperations.onAddRowClick,
|
||||
addColumn: dataTableOperations.onAddColumn,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -179,17 +179,17 @@ defineExpose({
|
||||
<div
|
||||
ref="gridContainerRef"
|
||||
:class="[$style['grid-container'], { [$style['has-records']]: hasRecords }]"
|
||||
data-test-id="data-store-grid"
|
||||
data-test-id="data-table-grid"
|
||||
>
|
||||
<AgGridVue
|
||||
style="width: 100%"
|
||||
:dom-layout="'autoHeight'"
|
||||
:row-height="DATA_STORE_ROW_HEIGHT"
|
||||
:header-height="DATA_STORE_HEADER_HEIGHT"
|
||||
:row-height="DATA_TABLE_ROW_HEIGHT"
|
||||
:header-height="DATA_TABLE_HEADER_HEIGHT"
|
||||
:animate-rows="false"
|
||||
:theme="n8nTheme"
|
||||
:suppress-drag-leave-hides-columns="true"
|
||||
:loading="dataStoreOperations.contentLoading.value"
|
||||
:loading="dataTableOperations.contentLoading.value"
|
||||
:row-selection="selection.rowSelection"
|
||||
:get-row-id="(params: GetRowIdParams) => String(params.data.id)"
|
||||
:stop-editing-when-cells-lose-focus="true"
|
||||
@@ -197,22 +197,22 @@ defineExpose({
|
||||
:suppress-multi-sort="true"
|
||||
:overlay-no-rows-template="customNoRowsOverlay"
|
||||
@grid-ready="initialize"
|
||||
@cell-value-changed="dataStoreOperations.onCellValueChanged"
|
||||
@column-moved="dataStoreOperations.onColumnMoved"
|
||||
@cell-clicked="dataStoreGridBase.onCellClicked"
|
||||
@cell-editing-started="dataStoreGridBase.onCellEditingStarted"
|
||||
@cell-editing-stopped="dataStoreGridBase.onCellEditingStopped"
|
||||
@column-header-clicked="dataStoreGridBase.resetLastFocusedCell"
|
||||
@cell-value-changed="dataTableOperations.onCellValueChanged"
|
||||
@column-moved="dataTableOperations.onColumnMoved"
|
||||
@cell-clicked="dataTableGridBase.onCellClicked"
|
||||
@cell-editing-started="dataTableGridBase.onCellEditingStarted"
|
||||
@cell-editing-stopped="dataTableGridBase.onCellEditingStopped"
|
||||
@column-header-clicked="dataTableGridBase.resetLastFocusedCell"
|
||||
@selection-changed="selection.onSelectionChanged"
|
||||
@sort-changed="dataStoreGridBase.onSortChanged"
|
||||
@cell-key-down="dataStoreOperations.onCellKeyDown"
|
||||
@sort-changed="dataTableGridBase.onSortChanged"
|
||||
@cell-key-down="dataTableOperations.onCellKeyDown"
|
||||
@filter-changed="onFilterChanged"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<ElPagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
data-test-id="data-store-content-pagination"
|
||||
data-test-id="data-table-content-pagination"
|
||||
background
|
||||
:total="totalItems"
|
||||
:page-sizes="pageSizeOptions"
|
||||
@@ -224,7 +224,7 @@ defineExpose({
|
||||
</div>
|
||||
<SelectedItemsInfo
|
||||
:selected-count="selection.selectedCount.value"
|
||||
@delete-selected="dataStoreOperations.handleDeleteSelected"
|
||||
@delete-selected="dataTableOperations.handleDeleteSelected"
|
||||
@clear-selection="selection.handleClearSelection"
|
||||
/>
|
||||
</div>
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import type { ICellEditorParams } from 'ag-grid-community';
|
||||
import { useDatePickerCommon } from '@/features/dataStore/composables/useDatePickerCommon';
|
||||
import { useDatePickerCommon } from '@/features/dataTable/composables/useDatePickerCommon';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
const props = defineProps<{
|
||||
@@ -45,9 +45,9 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapperRef" class="datastore-datepicker-wrapper">
|
||||
<div ref="wrapperRef" class="data-table-datepicker-wrapper">
|
||||
<ElDatePicker
|
||||
id="datastore-datepicker"
|
||||
id="data-table-datepicker"
|
||||
ref="pickerRef"
|
||||
v-model="dateValueComputed"
|
||||
type="datetime"
|
||||
@@ -55,7 +55,7 @@ defineExpose({
|
||||
:clearable="true"
|
||||
:editable="true"
|
||||
:teleported="false"
|
||||
popper-class="ag-custom-component-popup datastore-datepicker-popper"
|
||||
popper-class="ag-custom-component-popup data-table-datepicker-popper"
|
||||
placeholder="YYYY-MM-DD (HH:mm:ss)"
|
||||
size="small"
|
||||
@change="onDateChange"
|
||||
@@ -66,7 +66,7 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.datastore-datepicker-wrapper {
|
||||
.data-table-datepicker-wrapper {
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
.el-input__prefix {
|
||||
@@ -91,7 +91,7 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.datastore-datepicker-popper {
|
||||
.data-table-datepicker-popper {
|
||||
// Hide the date input in the popper
|
||||
.el-date-picker__time-header .el-date-picker__editor-wrap:first-child {
|
||||
display: none;
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import type { IDateParams } from 'ag-grid-community';
|
||||
import { useDatePickerCommon } from '@/features/dataStore/composables/useDatePickerCommon';
|
||||
import { useDatePickerCommon } from '@/features/dataTable/composables/useDatePickerCommon';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
const props = defineProps<{
|
||||
@@ -45,7 +45,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapperRef" class="datastore-date-filter-wrapper">
|
||||
<div ref="wrapperRef" class="data-table-date-filter-wrapper">
|
||||
<ElDatePicker
|
||||
ref="pickerRef"
|
||||
v-model="dateValueComputed"
|
||||
@@ -54,7 +54,7 @@ defineExpose({
|
||||
:editable="true"
|
||||
:teleported="true"
|
||||
placement="bottom"
|
||||
popper-class="ag-custom-component-popup datastore-date-filter-popper"
|
||||
popper-class="ag-custom-component-popup data-table-date-filter-popper"
|
||||
placeholder="YYYY-MM-DD (HH:mm:ss)"
|
||||
size="small"
|
||||
@change="onDateChange"
|
||||
@@ -65,7 +65,7 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.datastore-date-filter-wrapper {
|
||||
.data-table-date-filter-wrapper {
|
||||
border: var(--ag-picker-button-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
@@ -91,7 +91,7 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.datastore-date-filter-popper {
|
||||
.data-table-date-filter-popper {
|
||||
.el-date-picker__time-header .el-date-picker__editor-wrap:first-child {
|
||||
display: none;
|
||||
}
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { useDataStoreColumnFilters } from '../useDataStoreColumnFilters';
|
||||
import { useDataTableColumnFilters } from '../useDataTableColumnFilters';
|
||||
import type { ColDef, GridApi } from 'ag-grid-community';
|
||||
|
||||
describe('useDataStoreColumnFilters', () => {
|
||||
describe('useDataTableColumnFilters', () => {
|
||||
let mockGridApi: GridApi;
|
||||
let mockSetGridData: ReturnType<typeof vi.fn>;
|
||||
let colDefs: Ref<ColDef[]>;
|
||||
@@ -26,7 +26,7 @@ describe('useDataStoreColumnFilters', () => {
|
||||
describe('initializeFilters', () => {
|
||||
it('should disable filters for special columns', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { initializeFilters } = useDataStoreColumnFilters({
|
||||
const { initializeFilters } = useDataTableColumnFilters({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData: mockSetGridData,
|
||||
@@ -52,7 +52,7 @@ describe('useDataStoreColumnFilters', () => {
|
||||
|
||||
it('should process text filters', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { onFilterChanged, currentFilterJSON } = useDataStoreColumnFilters({
|
||||
const { onFilterChanged, currentFilterJSON } = useDataTableColumnFilters({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData: mockSetGridData,
|
||||
@@ -84,7 +84,7 @@ describe('useDataStoreColumnFilters', () => {
|
||||
|
||||
it('should return undefined when no filters', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { onFilterChanged, currentFilterJSON } = useDataStoreColumnFilters({
|
||||
const { onFilterChanged, currentFilterJSON } = useDataTableColumnFilters({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData: mockSetGridData,
|
||||
@@ -101,7 +101,7 @@ describe('useDataStoreColumnFilters', () => {
|
||||
describe('hasActiveFilters', () => {
|
||||
it('should be false when no filters are active', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { hasActiveFilters } = useDataStoreColumnFilters({
|
||||
const { hasActiveFilters } = useDataTableColumnFilters({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData: mockSetGridData,
|
||||
@@ -112,7 +112,7 @@ describe('useDataStoreColumnFilters', () => {
|
||||
|
||||
it('should be true when filters are active', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { onFilterChanged, hasActiveFilters } = useDataStoreColumnFilters({
|
||||
const { onFilterChanged, hasActiveFilters } = useDataTableColumnFilters({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData: mockSetGridData,
|
||||
+9
-6
@@ -1,11 +1,11 @@
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type { ColDef, GridApi } from 'ag-grid-community';
|
||||
import type { DataStoreRow } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTableRow } from '@/features/dataTable/dataTable.types';
|
||||
import type {
|
||||
BackendFilter,
|
||||
BackendFilterRecord,
|
||||
FilterModel,
|
||||
} from '../types/dataStoreFilters.types';
|
||||
} from '../types/dataTableFilters.types';
|
||||
import { GRID_FILTER_CONFIG, isSpecialColumn } from '../utils/filterMappings';
|
||||
import {
|
||||
processTextFilter,
|
||||
@@ -13,17 +13,20 @@ import {
|
||||
processDateFilter,
|
||||
} from '../utils/filterProcessors';
|
||||
|
||||
export type UseDataStoreColumnFiltersParams = {
|
||||
export type UseDataTableColumnFiltersParams = {
|
||||
gridApi: Ref<GridApi>;
|
||||
colDefs: Ref<ColDef[]>;
|
||||
setGridData: (params: { rowData?: DataStoreRow[]; colDefs?: ColDef[] }) => void;
|
||||
setGridData: (params: {
|
||||
rowData?: DataTableRow[];
|
||||
colDefs?: ColDef[];
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const useDataStoreColumnFilters = ({
|
||||
export const useDataTableColumnFilters = ({
|
||||
gridApi,
|
||||
colDefs,
|
||||
setGridData,
|
||||
}: UseDataStoreColumnFiltersParams) => {
|
||||
}: UseDataTableColumnFiltersParams) => {
|
||||
const currentFilterJSON = ref<string | undefined>(undefined);
|
||||
|
||||
const initializeFilters = () => {
|
||||
+44
-37
@@ -13,24 +13,24 @@ import type {
|
||||
} from 'ag-grid-community';
|
||||
import type {
|
||||
AddColumnResponse,
|
||||
DataStoreColumn,
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStoreRow,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
DataTableColumn,
|
||||
DataTableColumnCreatePayload,
|
||||
DataTableRow,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import {
|
||||
ADD_ROW_ROW_ID,
|
||||
DATA_STORE_ID_COLUMN_WIDTH,
|
||||
DATA_TABLE_ID_COLUMN_WIDTH,
|
||||
DEFAULT_COLUMN_WIDTH,
|
||||
DEFAULT_ID_COLUMN_NAME,
|
||||
} from '@/features/dataStore/constants';
|
||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
||||
import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
|
||||
import ElDatePickerCellEditor from '@/features/dataStore/components/dataGrid/ElDatePickerCellEditor.vue';
|
||||
import ElDatePickerFilter from '@/features/dataStore/components/dataGrid/ElDatePickerFilter.vue';
|
||||
} from '@/features/dataTable/constants';
|
||||
import { useDataTableTypes } from '@/features/dataTable/composables/useDataTableTypes';
|
||||
import ColumnHeader from '@/features/dataTable/components/dataGrid/ColumnHeader.vue';
|
||||
import ElDatePickerCellEditor from '@/features/dataTable/components/dataGrid/ElDatePickerCellEditor.vue';
|
||||
import ElDatePickerFilter from '@/features/dataTable/components/dataGrid/ElDatePickerFilter.vue';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import AddColumnButton from '@/features/dataStore/components/dataGrid/AddColumnButton.vue';
|
||||
import AddRowButton from '@/features/dataStore/components/dataGrid/AddRowButton.vue';
|
||||
import { reorderItem } from '@/features/dataStore/utils';
|
||||
import AddColumnButton from '@/features/dataTable/components/dataGrid/AddColumnButton.vue';
|
||||
import AddRowButton from '@/features/dataTable/components/dataGrid/AddRowButton.vue';
|
||||
import { reorderItem } from '@/features/dataTable/utils';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import {
|
||||
@@ -45,10 +45,10 @@ import {
|
||||
getDateColumnFilterOptions,
|
||||
getNumberColumnFilterOptions,
|
||||
getBooleanColumnFilterOptions,
|
||||
} from '@/features/dataStore/utils/columnUtils';
|
||||
} from '@/features/dataTable/utils/columnUtils';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
export const useDataStoreGridBase = ({
|
||||
export const useDataTableGridBase = ({
|
||||
gridContainerRef,
|
||||
onDeleteColumn,
|
||||
onAddRowClick,
|
||||
@@ -57,12 +57,12 @@ export const useDataStoreGridBase = ({
|
||||
gridContainerRef: Ref<HTMLElement | null>;
|
||||
onDeleteColumn: (columnId: string) => void;
|
||||
onAddRowClick: () => void;
|
||||
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||
onAddColumn: (column: DataTableColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||
}) => {
|
||||
const gridApi = ref<GridApi | null>(null);
|
||||
const colDefs = ref<ColDef[]>([]);
|
||||
const isTextEditorOpen = ref(false);
|
||||
const { mapToAGCellType } = useDataStoreTypes();
|
||||
const { mapToAGCellType } = useDataTableTypes();
|
||||
const { copy: copyToClipboard } = useClipboard({ onPaste: onClipboardPaste });
|
||||
const i18n = useI18n();
|
||||
const currentSortBy = ref<string>(DEFAULT_ID_COLUMN_NAME);
|
||||
@@ -92,7 +92,7 @@ export const useDataStoreGridBase = ({
|
||||
rowData,
|
||||
}: {
|
||||
colDefs?: ColDef[];
|
||||
rowData?: DataStoreRow[];
|
||||
rowData?: DataTableRow[];
|
||||
}) => {
|
||||
if (colDefs) {
|
||||
initializedGridApi.value.setGridOption('columnDefs', colDefs);
|
||||
@@ -132,7 +132,7 @@ export const useDataStoreGridBase = ({
|
||||
});
|
||||
};
|
||||
|
||||
const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {}) => {
|
||||
const createColumnDef = (col: DataTableColumn, extraProps: Partial<ColDef> = {}) => {
|
||||
const columnDef: ColDef = {
|
||||
colId: col.id,
|
||||
field: col.name,
|
||||
@@ -142,7 +142,10 @@ export const useDataStoreGridBase = ({
|
||||
resizable: true,
|
||||
lockPinned: true,
|
||||
headerComponent: ColumnHeader,
|
||||
headerComponentParams: { onDelete: onDeleteColumn, allowMenuActions: true },
|
||||
headerComponentParams: {
|
||||
onDelete: onDeleteColumn,
|
||||
allowMenuActions: true,
|
||||
},
|
||||
cellEditorPopup: false,
|
||||
cellDataType: mapToAGCellType(col.type),
|
||||
cellClass: getCellClass,
|
||||
@@ -187,7 +190,7 @@ export const useDataStoreGridBase = ({
|
||||
};
|
||||
};
|
||||
|
||||
const onCellEditingStarted = (params: CellEditingStartedEvent<DataStoreRow>) => {
|
||||
const onCellEditingStarted = (params: CellEditingStartedEvent<DataTableRow>) => {
|
||||
if (params.column.getColDef().cellDataType === 'text') {
|
||||
isTextEditorOpen.value = true;
|
||||
} else {
|
||||
@@ -195,13 +198,13 @@ export const useDataStoreGridBase = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onCellEditingStopped = (params: CellEditingStoppedEvent<DataStoreRow>) => {
|
||||
const onCellEditingStopped = (params: CellEditingStoppedEvent<DataTableRow>) => {
|
||||
if (params.column.getColDef().cellDataType === 'text') {
|
||||
isTextEditorOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnDefinitions = (dataStoreColumns: DataStoreColumn[]) => {
|
||||
const getColumnDefinitions = (dataTableColumns: DataTableColumn[]) => {
|
||||
const systemDateColumnOptions: Partial<ColDef> = {
|
||||
editable: false,
|
||||
suppressMovable: true,
|
||||
@@ -215,8 +218,8 @@ export const useDataStoreGridBase = ({
|
||||
width: DEFAULT_COLUMN_WIDTH,
|
||||
};
|
||||
return [
|
||||
// Always add the ID column, it's not returned by the back-end but all data stores have it
|
||||
// We use it as a placeholder for new datastores
|
||||
// Always add the ID column, it's not returned by the back-end but all data tables have it
|
||||
// We use it as a placeholder for new data tables
|
||||
createColumnDef(
|
||||
{
|
||||
index: 0,
|
||||
@@ -230,8 +233,8 @@ export const useDataStoreGridBase = ({
|
||||
filter: false,
|
||||
suppressMovable: true,
|
||||
lockPosition: true,
|
||||
minWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
||||
maxWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
||||
minWidth: DATA_TABLE_ID_COLUMN_WIDTH,
|
||||
maxWidth: DATA_TABLE_ID_COLUMN_WIDTH,
|
||||
resizable: false,
|
||||
headerClass: 'system-column',
|
||||
headerComponentParams: {
|
||||
@@ -252,10 +255,10 @@ export const useDataStoreGridBase = ({
|
||||
},
|
||||
),
|
||||
// Append other columns
|
||||
...orderBy(dataStoreColumns, 'index').map((col) => createColumnDef(col)),
|
||||
...orderBy(dataTableColumns, 'index').map((col) => createColumnDef(col)),
|
||||
createColumnDef(
|
||||
{
|
||||
index: dataStoreColumns.length + 1,
|
||||
index: dataTableColumns.length + 1,
|
||||
id: 'createdAt',
|
||||
name: 'createdAt',
|
||||
type: 'date',
|
||||
@@ -264,7 +267,7 @@ export const useDataStoreGridBase = ({
|
||||
),
|
||||
createColumnDef(
|
||||
{
|
||||
index: dataStoreColumns.length + 2,
|
||||
index: dataTableColumns.length + 2,
|
||||
id: 'updatedAt',
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
@@ -273,7 +276,7 @@ export const useDataStoreGridBase = ({
|
||||
),
|
||||
createColumnDef(
|
||||
{
|
||||
index: dataStoreColumns.length + 3,
|
||||
index: dataTableColumns.length + 3,
|
||||
id: 'add-column',
|
||||
name: 'Add Column',
|
||||
type: 'string',
|
||||
@@ -292,8 +295,8 @@ export const useDataStoreGridBase = ({
|
||||
];
|
||||
};
|
||||
|
||||
const loadColumns = (dataStoreColumns: DataStoreColumn[]) => {
|
||||
colDefs.value = getColumnDefinitions(dataStoreColumns);
|
||||
const loadColumns = (dataTableColumns: DataTableColumn[]) => {
|
||||
colDefs.value = getColumnDefinitions(dataTableColumns);
|
||||
setGridData({ colDefs: colDefs.value });
|
||||
};
|
||||
|
||||
@@ -307,7 +310,7 @@ export const useDataStoreGridBase = ({
|
||||
setGridData({ colDefs: colDefs.value });
|
||||
};
|
||||
|
||||
const addColumn = (column: DataStoreColumn) => {
|
||||
const addColumn = (column: DataTableColumn) => {
|
||||
colDefs.value = [
|
||||
...colDefs.value.slice(0, -1),
|
||||
createColumnDef(column),
|
||||
@@ -329,7 +332,7 @@ export const useDataStoreGridBase = ({
|
||||
colDefs.value = [colDefs.value[0], ...reorderedMiddle, colDefs.value[colDefs.value.length - 1]];
|
||||
};
|
||||
|
||||
const handleCopyFocusedCell = async (params: CellKeyDownEvent<DataStoreRow>) => {
|
||||
const handleCopyFocusedCell = async (params: CellKeyDownEvent<DataTableRow>) => {
|
||||
const focused = params.api.getFocusedCell();
|
||||
if (!focused) {
|
||||
return;
|
||||
@@ -370,13 +373,17 @@ export const useDataStoreGridBase = ({
|
||||
}
|
||||
}
|
||||
|
||||
const onCellClicked = (params: CellClickedEvent<DataStoreRow>) => {
|
||||
const onCellClicked = (params: CellClickedEvent<DataTableRow>) => {
|
||||
const clickedCellColumn = params.column.getColId();
|
||||
const clickedCellRow = params.rowIndex;
|
||||
|
||||
if (
|
||||
clickedCellRow === null ||
|
||||
params.api.isEditing({ rowIndex: clickedCellRow, column: params.column, rowPinned: null })
|
||||
params.api.isEditing({
|
||||
rowIndex: clickedCellRow,
|
||||
column: params.column,
|
||||
rowPinned: null,
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
+24
-24
@@ -1,32 +1,32 @@
|
||||
import {
|
||||
useDataStoreOperations,
|
||||
type UseDataStoreOperationsParams,
|
||||
} from '@/features/dataStore/composables/useDataStoreOperations';
|
||||
useDataTableOperations,
|
||||
type UseDataTableOperationsParams,
|
||||
} from '@/features/dataTable/composables/useDataTableOperations';
|
||||
import { ref } from 'vue';
|
||||
import type { GridApi } from 'ag-grid-community';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
vi.mock('@/features/dataStore/dataStore.store', () => ({
|
||||
useDataStoreStore: vi.fn(() => ({})),
|
||||
vi.mock('@/features/dataTable/dataTable.store', () => ({
|
||||
useDataTableStore: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
describe('useDataStoreOperations', () => {
|
||||
let params: UseDataStoreOperationsParams;
|
||||
let dataStoreStore: ReturnType<typeof useDataStoreStore>;
|
||||
describe('useDataTableOperations', () => {
|
||||
let params: UseDataTableOperationsParams;
|
||||
let dataTableStore: ReturnType<typeof useDataTableStore>;
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
dataStoreStore = {
|
||||
addDataStoreColumn: vi.fn(),
|
||||
deleteDataStoreColumn: vi.fn(),
|
||||
moveDataStoreColumn: vi.fn(),
|
||||
dataTableStore = {
|
||||
addDataTableColumn: vi.fn(),
|
||||
deleteDataTableColumn: vi.fn(),
|
||||
moveDataTableColumn: vi.fn(),
|
||||
deleteRows: vi.fn(),
|
||||
insertEmptyRow: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDataStoreStore>;
|
||||
} as unknown as ReturnType<typeof useDataTableStore>;
|
||||
|
||||
vi.mocked(useDataStoreStore).mockReturnValue(dataStoreStore);
|
||||
vi.mocked(useDataTableStore).mockReturnValue(dataTableStore);
|
||||
|
||||
params = {
|
||||
colDefs: ref([]),
|
||||
@@ -36,7 +36,7 @@ describe('useDataStoreOperations', () => {
|
||||
setGridData: vi.fn(),
|
||||
insertGridColumnAtIndex: vi.fn(),
|
||||
moveGridColumn: vi.fn(),
|
||||
dataStoreId: 'test',
|
||||
dataTableId: 'test',
|
||||
projectId: 'test',
|
||||
gridApi: ref(null as unknown as GridApi),
|
||||
totalItems: ref(0),
|
||||
@@ -60,23 +60,23 @@ describe('useDataStoreOperations', () => {
|
||||
|
||||
describe('onAddColumn', () => {
|
||||
it('should raise error when column is not added', async () => {
|
||||
vi.mocked(useDataStoreStore).mockReturnValue({
|
||||
...dataStoreStore,
|
||||
addDataStoreColumn: vi.fn().mockRejectedValue(new Error('test')),
|
||||
vi.mocked(useDataTableStore).mockReturnValue({
|
||||
...dataTableStore,
|
||||
addDataTableColumn: vi.fn().mockRejectedValue(new Error('test')),
|
||||
});
|
||||
const { onAddColumn } = useDataStoreOperations(params);
|
||||
const { onAddColumn } = useDataTableOperations(params);
|
||||
const result = await onAddColumn({ name: 'test', type: 'string' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should add column when column is added', async () => {
|
||||
const returnedColumn = { name: 'test', type: 'string' } as const;
|
||||
vi.mocked(useDataStoreStore).mockReturnValue({
|
||||
...dataStoreStore,
|
||||
addDataStoreColumn: vi.fn().mockResolvedValue(returnedColumn),
|
||||
vi.mocked(useDataTableStore).mockReturnValue({
|
||||
...dataTableStore,
|
||||
addDataTableColumn: vi.fn().mockResolvedValue(returnedColumn),
|
||||
});
|
||||
const rowData = ref([{ id: 1 }]);
|
||||
const { onAddColumn } = useDataStoreOperations({ ...params, rowData });
|
||||
const { onAddColumn } = useDataTableOperations({ ...params, rowData });
|
||||
const result = await onAddColumn({ name: returnedColumn.name, type: returnedColumn.type });
|
||||
expect(result.success).toBe(true);
|
||||
expect(params.setGridData).toHaveBeenCalledWith({ rowData: [{ id: 1, test: null }] });
|
||||
+60
-54
@@ -3,10 +3,10 @@ import { useToast } from '@/composables/useToast';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import type {
|
||||
AddColumnResponse,
|
||||
DataStoreColumn,
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStoreRow,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
DataTableColumn,
|
||||
DataTableColumnCreatePayload,
|
||||
DataTableRow,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type {
|
||||
@@ -16,21 +16,24 @@ import type {
|
||||
ColumnMovedEvent,
|
||||
GridApi,
|
||||
} from 'ag-grid-community';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { isDataStoreValue, isAGGridCellType } from '@/features/dataStore/typeGuards';
|
||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
||||
import { areValuesEqual } from '@/features/dataStore/utils/typeUtils';
|
||||
import { isDataTableValue, isAGGridCellType } from '@/features/dataTable/typeGuards';
|
||||
import { useDataTableTypes } from '@/features/dataTable/composables/useDataTableTypes';
|
||||
import { areValuesEqual } from '@/features/dataTable/utils/typeUtils';
|
||||
|
||||
export type UseDataStoreOperationsParams = {
|
||||
export type UseDataTableOperationsParams = {
|
||||
colDefs: Ref<ColDef[]>;
|
||||
rowData: Ref<DataStoreRow[]>;
|
||||
rowData: Ref<DataTableRow[]>;
|
||||
deleteGridColumn: (columnId: string) => void;
|
||||
addGridColumn: (column: DataStoreColumn) => void;
|
||||
setGridData: (params: { rowData?: DataStoreRow[]; colDefs?: ColDef[] }) => void;
|
||||
addGridColumn: (column: DataTableColumn) => void;
|
||||
setGridData: (params: {
|
||||
rowData?: DataTableRow[];
|
||||
colDefs?: ColDef[];
|
||||
}) => void;
|
||||
insertGridColumnAtIndex: (column: ColDef, index: number) => void;
|
||||
moveGridColumn: (oldIndex: number, newIndex: number) => void;
|
||||
dataStoreId: string;
|
||||
dataTableId: string;
|
||||
projectId: string;
|
||||
gridApi: Ref<GridApi>;
|
||||
totalItems: Ref<number>;
|
||||
@@ -45,10 +48,10 @@ export type UseDataStoreOperationsParams = {
|
||||
currentFilterJSON?: Ref<string | undefined>;
|
||||
handleClearSelection: () => void;
|
||||
selectedRowIds: Ref<Set<number>>;
|
||||
handleCopyFocusedCell: (params: CellKeyDownEvent<DataStoreRow>) => Promise<void>;
|
||||
handleCopyFocusedCell: (params: CellKeyDownEvent<DataTableRow>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useDataStoreOperations = ({
|
||||
export const useDataTableOperations = ({
|
||||
colDefs,
|
||||
rowData,
|
||||
deleteGridColumn,
|
||||
@@ -56,7 +59,7 @@ export const useDataStoreOperations = ({
|
||||
setGridData,
|
||||
insertGridColumnAtIndex,
|
||||
moveGridColumn,
|
||||
dataStoreId,
|
||||
dataTableId,
|
||||
projectId,
|
||||
gridApi,
|
||||
totalItems,
|
||||
@@ -72,24 +75,24 @@ export const useDataStoreOperations = ({
|
||||
handleClearSelection,
|
||||
selectedRowIds,
|
||||
handleCopyFocusedCell,
|
||||
}: UseDataStoreOperationsParams) => {
|
||||
}: UseDataTableOperationsParams) => {
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
const contentLoading = ref(false);
|
||||
const telemetry = useTelemetry();
|
||||
const dataStoreTypes = useDataStoreTypes();
|
||||
const dataTableTypes = useDataTableTypes();
|
||||
|
||||
async function onDeleteColumn(columnId: string) {
|
||||
const columnToDelete = colDefs.value.find((col) => col.colId === columnId);
|
||||
if (!columnToDelete) return;
|
||||
|
||||
const promptResponse = await message.confirm(
|
||||
i18n.baseText('dataStore.deleteColumn.confirm.message', {
|
||||
i18n.baseText('dataTable.deleteColumn.confirm.message', {
|
||||
interpolate: { name: columnToDelete.headerName ?? '' },
|
||||
}),
|
||||
i18n.baseText('dataStore.deleteColumn.confirm.title'),
|
||||
i18n.baseText('dataTable.deleteColumn.confirm.title'),
|
||||
{
|
||||
confirmButtonText: i18n.baseText('generic.delete'),
|
||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||
@@ -109,23 +112,23 @@ export const useDataStoreOperations = ({
|
||||
});
|
||||
setGridData({ rowData: rowData.value });
|
||||
try {
|
||||
await dataStoreStore.deleteDataStoreColumn(dataStoreId, projectId, columnId);
|
||||
await dataTableStore.deleteDataTableColumn(dataTableId, projectId, columnId);
|
||||
telemetry.track('User deleted data table column', {
|
||||
column_id: columnId,
|
||||
column_type: columnToDelete.cellDataType,
|
||||
data_table_id: dataStoreId,
|
||||
data_table_id: dataTableId,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.deleteColumn.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.deleteColumn.error'));
|
||||
insertGridColumnAtIndex(columnToDelete, columnToDeleteIndex);
|
||||
rowData.value = rowDataOldValue;
|
||||
setGridData({ rowData: rowData.value });
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddColumn(column: DataStoreColumnCreatePayload): Promise<AddColumnResponse> {
|
||||
async function onAddColumn(column: DataTableColumnCreatePayload): Promise<AddColumnResponse> {
|
||||
try {
|
||||
const newColumn = await dataStoreStore.addDataStoreColumn(dataStoreId, projectId, column);
|
||||
const newColumn = await dataTableStore.addDataTableColumn(dataTableId, projectId, column);
|
||||
addGridColumn(newColumn);
|
||||
rowData.value = rowData.value.map((row) => {
|
||||
return { ...row, [newColumn.name]: null };
|
||||
@@ -134,11 +137,11 @@ export const useDataStoreOperations = ({
|
||||
telemetry.track('User added data table column', {
|
||||
column_id: newColumn.id,
|
||||
column_type: newColumn.type,
|
||||
data_table_id: dataStoreId,
|
||||
data_table_id: dataTableId,
|
||||
});
|
||||
return { success: true, httpStatus: 200 };
|
||||
} catch (error) {
|
||||
const addColumnError = dataStoreTypes.getAddColumnError(error);
|
||||
const addColumnError = dataTableTypes.getAddColumnError(error);
|
||||
return {
|
||||
success: false,
|
||||
httpStatus: addColumnError.httpStatus,
|
||||
@@ -160,15 +163,15 @@ export const useDataStoreOperations = ({
|
||||
const oldIndex = colDefs.value.findIndex((col) => col.colId === moveEvent.column!.getColId());
|
||||
const newIndex = moveEvent.toIndex - 2; // selection and id columns are included here
|
||||
try {
|
||||
await dataStoreStore.moveDataStoreColumn(
|
||||
dataStoreId,
|
||||
await dataTableStore.moveDataTableColumn(
|
||||
dataTableId,
|
||||
projectId,
|
||||
moveEvent.column.getColId(),
|
||||
newIndex,
|
||||
);
|
||||
moveGridColumn(oldIndex, newIndex);
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.moveColumn.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.moveColumn.error'));
|
||||
gridApi.value.moveColumnByIndex(moveEvent.toIndex, oldIndex + 1);
|
||||
}
|
||||
};
|
||||
@@ -179,24 +182,24 @@ export const useDataStoreOperations = ({
|
||||
|
||||
contentLoading.value = true;
|
||||
toggleSave(true);
|
||||
const insertedRow = await dataStoreStore.insertEmptyRow(dataStoreId, projectId);
|
||||
const newRow: DataStoreRow = insertedRow;
|
||||
const insertedRow = await dataTableStore.insertEmptyRow(dataTableId, projectId);
|
||||
const newRow: DataTableRow = insertedRow;
|
||||
rowData.value.push(newRow);
|
||||
setTotalItems(totalItems.value + 1);
|
||||
setGridData({ rowData: rowData.value });
|
||||
focusFirstEditableCell(newRow.id as number);
|
||||
telemetry.track('User added row to data table', {
|
||||
data_table_id: dataStoreId,
|
||||
data_table_id: dataTableId,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.addRow.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.addRow.error'));
|
||||
} finally {
|
||||
toggleSave(false);
|
||||
contentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onCellValueChanged = async (params: CellValueChangedEvent<DataStoreRow>) => {
|
||||
const onCellValueChanged = async (params: CellValueChangedEvent<DataTableRow>) => {
|
||||
const { data, api, oldValue, colDef } = params;
|
||||
const fieldName = String(colDef.field ?? '');
|
||||
if (!fieldName) return;
|
||||
@@ -204,7 +207,7 @@ export const useDataStoreOperations = ({
|
||||
const value = params.data[fieldName];
|
||||
|
||||
const cellType = isAGGridCellType(colDef.cellDataType)
|
||||
? dataStoreTypes.mapToDataStoreColumnType(colDef.cellDataType)
|
||||
? dataTableTypes.mapToDataTableColumnType(colDef.cellDataType)
|
||||
: undefined;
|
||||
|
||||
if (value === undefined || areValuesEqual(oldValue, value, cellType)) {
|
||||
@@ -218,33 +221,36 @@ export const useDataStoreOperations = ({
|
||||
|
||||
try {
|
||||
toggleSave(true);
|
||||
await dataStoreStore.updateRow(dataStoreId, projectId, id, {
|
||||
await dataTableStore.updateRow(dataTableId, projectId, id, {
|
||||
[fieldName]: value,
|
||||
});
|
||||
telemetry.track('User edited data table content', {
|
||||
data_table_id: dataStoreId,
|
||||
data_table_id: dataTableId,
|
||||
column_id: colDef.colId,
|
||||
column_type: colDef.cellDataType,
|
||||
});
|
||||
} catch (error) {
|
||||
// Revert cell to original value if the update fails
|
||||
const validOldValue = isDataStoreValue(oldValue) ? oldValue : null;
|
||||
const revertedData: DataStoreRow = { ...data, [fieldName]: validOldValue };
|
||||
const validOldValue = isDataTableValue(oldValue) ? oldValue : null;
|
||||
const revertedData: DataTableRow = {
|
||||
...data,
|
||||
[fieldName]: validOldValue,
|
||||
};
|
||||
api.applyTransaction({
|
||||
update: [revertedData],
|
||||
});
|
||||
toast.showError(error, i18n.baseText('dataStore.updateRow.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.updateRow.error'));
|
||||
} finally {
|
||||
toggleSave(false);
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchDataStoreRows() {
|
||||
async function fetchDataTableRows() {
|
||||
try {
|
||||
contentLoading.value = true;
|
||||
|
||||
const fetchedRows = await dataStoreStore.fetchDataStoreContent(
|
||||
dataStoreId,
|
||||
const fetchedRows = await dataTableStore.fetchDataTableContent(
|
||||
dataTableId,
|
||||
projectId,
|
||||
currentPage.value,
|
||||
pageSize.value,
|
||||
@@ -256,7 +262,7 @@ export const useDataStoreOperations = ({
|
||||
setGridData({ rowData: rowData.value });
|
||||
handleClearSelection();
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.fetchContent.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.fetchContent.error'));
|
||||
} finally {
|
||||
contentLoading.value = false;
|
||||
}
|
||||
@@ -266,11 +272,11 @@ export const useDataStoreOperations = ({
|
||||
if (selectedRowIds.value.size === 0) return;
|
||||
|
||||
const confirmResponse = await message.confirm(
|
||||
i18n.baseText('dataStore.deleteRows.confirmation', {
|
||||
i18n.baseText('dataTable.deleteRows.confirmation', {
|
||||
adjustToNumber: selectedRowIds.value.size,
|
||||
interpolate: { count: selectedRowIds.value.size },
|
||||
}),
|
||||
i18n.baseText('dataStore.deleteRows.title'),
|
||||
i18n.baseText('dataTable.deleteRows.title'),
|
||||
{
|
||||
confirmButtonText: i18n.baseText('generic.delete'),
|
||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||
@@ -284,21 +290,21 @@ export const useDataStoreOperations = ({
|
||||
try {
|
||||
toggleSave(true);
|
||||
const idsToDelete = Array.from(selectedRowIds.value);
|
||||
await dataStoreStore.deleteRows(dataStoreId, projectId, idsToDelete);
|
||||
await fetchDataStoreRows();
|
||||
await dataTableStore.deleteRows(dataTableId, projectId, idsToDelete);
|
||||
await fetchDataTableRows();
|
||||
|
||||
telemetry.track('User deleted rows in data table', {
|
||||
data_table_id: dataStoreId,
|
||||
data_table_id: dataTableId,
|
||||
deleted_row_count: idsToDelete.length,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.deleteRows.error'));
|
||||
toast.showError(error, i18n.baseText('dataTable.deleteRows.error'));
|
||||
} finally {
|
||||
toggleSave(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onCellKeyDown = async (params: CellKeyDownEvent<DataStoreRow>) => {
|
||||
const onCellKeyDown = async (params: CellKeyDownEvent<DataTableRow>) => {
|
||||
const event = params.event as KeyboardEvent;
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
@@ -335,7 +341,7 @@ export const useDataStoreOperations = ({
|
||||
onAddRowClick,
|
||||
contentLoading,
|
||||
onCellValueChanged,
|
||||
fetchDataStoreRows,
|
||||
fetchDataTableRows,
|
||||
handleDeleteSelected,
|
||||
onCellKeyDown,
|
||||
};
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export type PageSize = 10 | 20 | 50;
|
||||
export type UseDataStorePaginationOptions = {
|
||||
export type UseDataTablePaginationOptions = {
|
||||
initialPage?: number;
|
||||
initialPageSize?: PageSize;
|
||||
pageSizeOptions?: PageSize[];
|
||||
onChange?: (page: number, pageSize: number) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useDataStorePagination = (options: UseDataStorePaginationOptions = {}) => {
|
||||
export const useDataTablePagination = (options: UseDataTablePaginationOptions = {}) => {
|
||||
const currentPage = ref<number>(options.initialPage ?? 1);
|
||||
const pageSize = ref<PageSize>(options.initialPageSize ?? 20);
|
||||
const totalItems = ref<number>(0);
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import type { GridApi, IRowNode } from 'ag-grid-community';
|
||||
import { useDataStoreSelection } from './useDataStoreSelection';
|
||||
import { useDataTableSelection } from './useDataTableSelection';
|
||||
|
||||
const createMockGridApi = () =>
|
||||
({
|
||||
@@ -8,7 +8,7 @@ const createMockGridApi = () =>
|
||||
deselectAll: vi.fn(),
|
||||
}) as unknown as GridApi;
|
||||
|
||||
describe('useDataStoreSelection', () => {
|
||||
describe('useDataTableSelection', () => {
|
||||
let mockGridApi: GridApi;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -22,7 +22,7 @@ describe('useDataStoreSelection', () => {
|
||||
describe('onSelectionChanged', () => {
|
||||
it('should update selectedRowIds with numeric IDs from selected nodes', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { selectedRowIds, onSelectionChanged } = useDataStoreSelection({ gridApi });
|
||||
const { selectedRowIds, onSelectionChanged } = useDataTableSelection({ gridApi });
|
||||
|
||||
const mockSelectedNodes = [{ data: { id: 1 } }, { data: { id: 2 } }, { data: { id: 3 } }];
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('useDataStoreSelection', () => {
|
||||
|
||||
it('should filter out non-numeric IDs', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { selectedRowIds, onSelectionChanged } = useDataStoreSelection({ gridApi });
|
||||
const { selectedRowIds, onSelectionChanged } = useDataTableSelection({ gridApi });
|
||||
|
||||
const mockSelectedNodes = [
|
||||
{ data: { id: 1 } },
|
||||
@@ -54,7 +54,7 @@ describe('useDataStoreSelection', () => {
|
||||
|
||||
it('should update selectedCount reactively', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { selectedCount, onSelectionChanged } = useDataStoreSelection({ gridApi });
|
||||
const { selectedCount, onSelectionChanged } = useDataTableSelection({ gridApi });
|
||||
|
||||
const mockSelectedNodes = [{ data: { id: 1 } }, { data: { id: 2 } }, { data: { id: 3 } }];
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('useDataStoreSelection', () => {
|
||||
describe('handleClearSelection', () => {
|
||||
it('should clear selectedRowIds and call deselectAll on grid', () => {
|
||||
const gridApi = ref(mockGridApi);
|
||||
const { selectedRowIds, handleClearSelection, onSelectionChanged } = useDataStoreSelection({
|
||||
const { selectedRowIds, handleClearSelection, onSelectionChanged } = useDataTableSelection({
|
||||
gridApi,
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { computed, ref, type Ref } from 'vue';
|
||||
import type { GridApi, RowSelectionOptions } from 'ag-grid-community';
|
||||
import { ADD_ROW_ROW_ID } from '@/features/dataStore/constants';
|
||||
import { ADD_ROW_ROW_ID } from '@/features/dataTable/constants';
|
||||
|
||||
export const useDataStoreSelection = ({
|
||||
export const useDataTableSelection = ({
|
||||
gridApi,
|
||||
}: {
|
||||
gridApi: Ref<GridApi>;
|
||||
+13
-13
@@ -1,42 +1,42 @@
|
||||
import type {
|
||||
AGGridCellType,
|
||||
DataStoreColumnType,
|
||||
DataStoreValue,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
import { isAGGridCellType } from '@/features/dataStore/typeGuards';
|
||||
DataTableColumnType,
|
||||
DataTableValue,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { isAGGridCellType } from '@/features/dataTable/typeGuards';
|
||||
import { ResponseError } from '@n8n/rest-api-client';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { DATA_TYPE_ICON_MAP } from '@/constants';
|
||||
|
||||
export const useDataStoreTypes = () => {
|
||||
const getIconForType = (type: DataStoreColumnType) => DATA_TYPE_ICON_MAP[type];
|
||||
export const useDataTableTypes = () => {
|
||||
const getIconForType = (type: DataTableColumnType) => DATA_TYPE_ICON_MAP[type];
|
||||
const i18n = useI18n();
|
||||
|
||||
/**
|
||||
* Maps a DataStoreColumnType to an AGGridCellType.
|
||||
* Maps a DataTableColumnType to an AGGridCellType.
|
||||
* For now the only mismatch is our 'string' type,
|
||||
* which needs to be mapped manually.
|
||||
* @param colType The DataStoreColumnType to map.
|
||||
* @param colType The DataTableColumnType to map.
|
||||
* @returns The corresponding AGGridCellType.
|
||||
*/
|
||||
const mapToAGCellType = (colType: DataStoreColumnType): AGGridCellType => {
|
||||
const mapToAGCellType = (colType: DataTableColumnType): AGGridCellType => {
|
||||
if (colType === 'string') {
|
||||
return 'text';
|
||||
}
|
||||
return colType;
|
||||
};
|
||||
|
||||
const mapToDataStoreColumnType = (colType: AGGridCellType): DataStoreColumnType => {
|
||||
const mapToDataTableColumnType = (colType: AGGridCellType): DataTableColumnType => {
|
||||
if (!isAGGridCellType(colType)) {
|
||||
return 'string';
|
||||
}
|
||||
if (colType === 'text') {
|
||||
return 'string';
|
||||
}
|
||||
return colType as DataStoreColumnType;
|
||||
return colType as DataTableColumnType;
|
||||
};
|
||||
|
||||
const getDefaultValueForType = (colType: DataStoreColumnType): DataStoreValue => {
|
||||
const getDefaultValueForType = (colType: DataTableColumnType): DataTableValue => {
|
||||
switch (colType) {
|
||||
case 'string':
|
||||
return '';
|
||||
@@ -76,7 +76,7 @@ export const useDataStoreTypes = () => {
|
||||
return {
|
||||
getIconForType,
|
||||
mapToAGCellType,
|
||||
mapToDataStoreColumnType,
|
||||
mapToDataTableColumnType,
|
||||
getDefaultValueForType,
|
||||
getAddColumnError,
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ref, nextTick, useTemplateRef } from 'vue';
|
||||
import { parseLooseDateInput } from '@/features/dataStore/utils/typeUtils';
|
||||
import { parseLooseDateInput } from '@/features/dataTable/utils/typeUtils';
|
||||
import { isFocusableEl } from '@/utils/typesUtils';
|
||||
|
||||
export interface DatePickerCallbacks {
|
||||
+11
-11
@@ -1,29 +1,29 @@
|
||||
import { DATA_TABLE_COLUMN_REGEX } from '@n8n/api-types';
|
||||
|
||||
// Route and view identifiers
|
||||
export const DATA_STORE_VIEW = 'data-stores';
|
||||
export const PROJECT_DATA_STORES = 'project-data-stores';
|
||||
export const DATA_STORE_DETAILS = 'data-store-details';
|
||||
export const DATA_STORE_STORE = 'dataStoreStore';
|
||||
export const DATA_TABLE_VIEW = 'data-tables';
|
||||
export const PROJECT_DATA_TABLES = 'project-data-tables';
|
||||
export const DATA_TABLE_DETAILS = 'data-table-details';
|
||||
export const DATA_TABLE_STORE = 'dataTableStore';
|
||||
|
||||
export const DEFAULT_DATA_STORE_PAGE_SIZE = 10;
|
||||
export const DEFAULT_DATA_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
export const DATA_STORE_ID_COLUMN_WIDTH = 60;
|
||||
export const DATA_TABLE_ID_COLUMN_WIDTH = 60;
|
||||
|
||||
export const DEFAULT_COLUMN_WIDTH = 250;
|
||||
|
||||
export const DATA_STORE_HEADER_HEIGHT = 36;
|
||||
export const DATA_STORE_ROW_HEIGHT = 33;
|
||||
export const DATA_TABLE_HEADER_HEIGHT = 36;
|
||||
export const DATA_TABLE_ROW_HEIGHT = 33;
|
||||
|
||||
export const ADD_ROW_ROW_ID = '__n8n_add_row__';
|
||||
|
||||
export const DATA_STORE_CARD_ACTIONS = {
|
||||
export const DATA_TABLE_CARD_ACTIONS = {
|
||||
RENAME: 'rename',
|
||||
DELETE: 'delete',
|
||||
CLEAR: 'clear',
|
||||
};
|
||||
|
||||
export const ADD_DATA_STORE_MODAL_KEY = 'addDataStoreModal';
|
||||
export const ADD_DATA_TABLE_MODAL_KEY = 'addDataTableModal';
|
||||
|
||||
export const DEFAULT_ID_COLUMN_NAME = 'id';
|
||||
|
||||
@@ -36,7 +36,7 @@ export const MIN_LOADING_TIME = 500; // ms
|
||||
export const NULL_VALUE = 'Null';
|
||||
export const EMPTY_VALUE = 'Empty';
|
||||
|
||||
export const DATA_STORE_MODULE_NAME = 'data-table';
|
||||
export const DATA_TABLE_MODULE_NAME = 'data-table';
|
||||
|
||||
export const NUMBER_WITH_SPACES_REGEX = /\B(?=(\d{3})+(?!\d))/g;
|
||||
export const NUMBER_THOUSAND_SEPARATOR = ' ';
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { deleteDataStoreRowsApi } from '@/features/dataStore/dataStore.api';
|
||||
import { deleteDataTableRowsApi } from '@/features/dataTable/dataTable.api';
|
||||
import { makeRestApiRequest } from '@n8n/rest-api-client';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
@@ -6,22 +6,22 @@ vi.mock('@n8n/rest-api-client', () => ({
|
||||
makeRestApiRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('dataStore.api', () => {
|
||||
describe('dataTable.api', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('deleteDataStoreRowsApi', () => {
|
||||
describe('deleteDataTableRowsApi', () => {
|
||||
it('should make DELETE request with correct parameters', async () => {
|
||||
const dataStoreId = 'test-datastore-id';
|
||||
const dataTableId = 'test-dataTable-id';
|
||||
const projectId = 'test-project-id';
|
||||
const rowIds = [1, 2, 3];
|
||||
|
||||
vi.mocked(makeRestApiRequest).mockResolvedValue(true);
|
||||
|
||||
const result = await deleteDataStoreRowsApi(
|
||||
const result = await deleteDataTableRowsApi(
|
||||
{ baseUrl: '/rest', pushRef: 'test-push-ref' },
|
||||
dataStoreId,
|
||||
dataTableId,
|
||||
rowIds,
|
||||
projectId,
|
||||
);
|
||||
@@ -29,7 +29,7 @@ describe('dataStore.api', () => {
|
||||
expect(makeRestApiRequest).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'DELETE',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/rows`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/rows`,
|
||||
{
|
||||
filter: {
|
||||
type: 'or',
|
||||
+51
-47
@@ -2,14 +2,14 @@ import { makeRestApiRequest } from '@n8n/rest-api-client';
|
||||
import type { IRestApiContext } from '@n8n/rest-api-client';
|
||||
|
||||
import type {
|
||||
DataStoreColumnCreatePayload,
|
||||
DataStore,
|
||||
DataStoreColumn,
|
||||
DataStoreRow,
|
||||
} from '@/features/dataStore/datastore.types';
|
||||
DataTableColumnCreatePayload,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DataTableRow,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import type { DataTablesSizeResult } from 'n8n-workflow';
|
||||
|
||||
export const fetchDataStoresApi = async (
|
||||
export const fetchDataTablesApi = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
options?: {
|
||||
@@ -23,7 +23,7 @@ export const fetchDataStoresApi = async (
|
||||
},
|
||||
) => {
|
||||
const apiEndpoint = projectId ? `/projects/${projectId}/data-tables` : '/data-tables-global';
|
||||
return await makeRestApiRequest<{ count: number; data: DataStore[] }>(
|
||||
return await makeRestApiRequest<{ count: number; data: DataTable[] }>(
|
||||
context,
|
||||
'GET',
|
||||
apiEndpoint,
|
||||
@@ -34,13 +34,13 @@ export const fetchDataStoresApi = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const createDataStoreApi = async (
|
||||
export const createDataTableApi = async (
|
||||
context: IRestApiContext,
|
||||
name: string,
|
||||
projectId: string,
|
||||
columns?: DataStoreColumnCreatePayload[],
|
||||
columns?: DataTableColumnCreatePayload[],
|
||||
) => {
|
||||
return await makeRestApiRequest<DataStore>(
|
||||
return await makeRestApiRequest<DataTable>(
|
||||
context,
|
||||
'POST',
|
||||
`/projects/${projectId}/data-tables`,
|
||||
@@ -51,70 +51,70 @@ export const createDataStoreApi = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDataStoreApi = async (
|
||||
export const deleteDataTableApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<boolean>(
|
||||
context,
|
||||
'DELETE',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}`,
|
||||
{
|
||||
dataStoreId,
|
||||
dataTableId,
|
||||
projectId,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const updateDataStoreApi = async (
|
||||
export const updateDataTableApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
name: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<DataStore>(
|
||||
return await makeRestApiRequest<DataTable>(
|
||||
context,
|
||||
'PATCH',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}`,
|
||||
{
|
||||
name,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const addDataStoreColumnApi = async (
|
||||
export const addDataTableColumnApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
column: DataStoreColumnCreatePayload,
|
||||
column: DataTableColumnCreatePayload,
|
||||
) => {
|
||||
return await makeRestApiRequest<DataStoreColumn>(
|
||||
return await makeRestApiRequest<DataTableColumn>(
|
||||
context,
|
||||
'POST',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/columns`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/columns`,
|
||||
{
|
||||
...column,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDataStoreColumnApi = async (
|
||||
export const deleteDataTableColumnApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<boolean>(
|
||||
context,
|
||||
'DELETE',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/columns/${columnId}`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/columns/${columnId}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const moveDataStoreColumnApi = async (
|
||||
export const moveDataTableColumnApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
targetIndex: number,
|
||||
@@ -122,16 +122,16 @@ export const moveDataStoreColumnApi = async (
|
||||
return await makeRestApiRequest<boolean>(
|
||||
context,
|
||||
'PATCH',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/columns/${columnId}/move`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/columns/${columnId}/move`,
|
||||
{
|
||||
targetIndex,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getDataStoreRowsApi = async (
|
||||
export const getDataTableRowsApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
options?: {
|
||||
skip?: number;
|
||||
@@ -142,22 +142,22 @@ export const getDataStoreRowsApi = async (
|
||||
) => {
|
||||
return await makeRestApiRequest<{
|
||||
count: number;
|
||||
data: DataStoreRow[];
|
||||
}>(context, 'GET', `/projects/${projectId}/data-tables/${dataStoreId}/rows`, {
|
||||
data: DataTableRow[];
|
||||
}>(context, 'GET', `/projects/${projectId}/data-tables/${dataTableId}/rows`, {
|
||||
...(options ?? {}),
|
||||
});
|
||||
};
|
||||
|
||||
export const insertDataStoreRowApi = async (
|
||||
export const insertDataTableRowApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
row: DataStoreRow,
|
||||
dataTableId: string,
|
||||
row: DataTableRow,
|
||||
projectId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<DataStoreRow[]>(
|
||||
return await makeRestApiRequest<DataTableRow[]>(
|
||||
context,
|
||||
'POST',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/insert`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/insert`,
|
||||
{
|
||||
returnType: 'all',
|
||||
data: [row],
|
||||
@@ -165,17 +165,17 @@ export const insertDataStoreRowApi = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const updateDataStoreRowsApi = async (
|
||||
export const updateDataTableRowsApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
rowId: number,
|
||||
rowData: DataStoreRow,
|
||||
rowData: DataTableRow,
|
||||
projectId: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<boolean>(
|
||||
context,
|
||||
'PATCH',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/rows`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/rows`,
|
||||
{
|
||||
filter: {
|
||||
type: 'and',
|
||||
@@ -186,17 +186,21 @@ export const updateDataStoreRowsApi = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDataStoreRowsApi = async (
|
||||
export const deleteDataTableRowsApi = async (
|
||||
context: IRestApiContext,
|
||||
dataStoreId: string,
|
||||
dataTableId: string,
|
||||
rowIds: number[],
|
||||
projectId: string,
|
||||
) => {
|
||||
const filters = rowIds.map((id) => ({ columnName: 'id', condition: 'eq', value: id }));
|
||||
const filters = rowIds.map((id) => ({
|
||||
columnName: 'id',
|
||||
condition: 'eq',
|
||||
value: id,
|
||||
}));
|
||||
return await makeRestApiRequest<boolean>(
|
||||
context,
|
||||
'DELETE',
|
||||
`/projects/${projectId}/data-tables/${dataStoreId}/rows`,
|
||||
`/projects/${projectId}/data-tables/${dataTableId}/rows`,
|
||||
{
|
||||
filter: {
|
||||
type: 'or',
|
||||
@@ -206,7 +210,7 @@ export const deleteDataStoreRowsApi = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchDataStoreGlobalLimitInBytes = async (context: IRestApiContext) => {
|
||||
export const fetchDataTableGlobalLimitInBytes = async (context: IRestApiContext) => {
|
||||
return await makeRestApiRequest<DataTablesSizeResult>(
|
||||
context,
|
||||
'GET',
|
||||
+29
-29
@@ -1,17 +1,17 @@
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import * as dataStoreApi from '@/features/dataStore/dataStore.api';
|
||||
import * as dataTableApi from '@/features/dataTable/dataTable.api';
|
||||
|
||||
describe('dataStore.store', () => {
|
||||
let dataStoreStore: ReturnType<typeof useDataStoreStore>;
|
||||
describe('dataTable.store', () => {
|
||||
let dataTableStore: ReturnType<typeof useDataTableStore>;
|
||||
let rootStore: ReturnType<typeof useRootStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
rootStore = useRootStore();
|
||||
dataStoreStore = useDataStoreStore();
|
||||
dataTableStore = useDataTableStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -19,14 +19,14 @@ describe('dataStore.store', () => {
|
||||
});
|
||||
|
||||
it('can move a column', async () => {
|
||||
const datastoreId = faker.string.alphanumeric(10);
|
||||
const dataTableId = faker.string.alphanumeric(10);
|
||||
const columnId = 'phone';
|
||||
const targetIndex = 3;
|
||||
const projectId = 'p1';
|
||||
dataStoreStore.$patch({
|
||||
dataStores: [
|
||||
dataTableStore.$patch({
|
||||
dataTables: [
|
||||
{
|
||||
id: datastoreId,
|
||||
id: dataTableId,
|
||||
name: 'Test',
|
||||
sizeBytes: 0,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
@@ -43,65 +43,65 @@ describe('dataStore.store', () => {
|
||||
],
|
||||
totalCount: 1,
|
||||
});
|
||||
vi.spyOn(dataStoreApi, 'moveDataStoreColumnApi').mockResolvedValue(true);
|
||||
vi.spyOn(dataTableApi, 'moveDataTableColumnApi').mockResolvedValue(true);
|
||||
|
||||
const moved = await dataStoreStore.moveDataStoreColumn(
|
||||
datastoreId,
|
||||
const moved = await dataTableStore.moveDataTableColumn(
|
||||
dataTableId,
|
||||
projectId,
|
||||
columnId,
|
||||
targetIndex,
|
||||
);
|
||||
|
||||
expect(moved).toBe(true);
|
||||
expect(dataStoreApi.moveDataStoreColumnApi).toHaveBeenCalledWith(
|
||||
expect(dataTableApi.moveDataTableColumnApi).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
dataTableId,
|
||||
projectId,
|
||||
columnId,
|
||||
targetIndex,
|
||||
);
|
||||
expect(dataStoreStore.dataStores[0].columns.find((c) => c.id === columnId)?.index).toBe(
|
||||
expect(dataTableStore.dataTables[0].columns.find((c) => c.id === columnId)?.index).toBe(
|
||||
targetIndex,
|
||||
);
|
||||
});
|
||||
|
||||
it('can delete a column', async () => {
|
||||
const datastoreId = faker.string.alphanumeric(10);
|
||||
const dataTableId = faker.string.alphanumeric(10);
|
||||
const columnId = 'phone';
|
||||
const projectId = 'p1';
|
||||
dataStoreStore.$patch({
|
||||
dataStores: [
|
||||
{ id: datastoreId, columns: [{ id: columnId, index: 0, name: 'phone', type: 'string' }] },
|
||||
dataTableStore.$patch({
|
||||
dataTables: [
|
||||
{ id: dataTableId, columns: [{ id: columnId, index: 0, name: 'phone', type: 'string' }] },
|
||||
],
|
||||
totalCount: 1,
|
||||
});
|
||||
vi.spyOn(dataStoreApi, 'deleteDataStoreColumnApi').mockResolvedValue(true);
|
||||
vi.spyOn(dataTableApi, 'deleteDataTableColumnApi').mockResolvedValue(true);
|
||||
|
||||
const deleted = await dataStoreStore.deleteDataStoreColumn(datastoreId, projectId, columnId);
|
||||
const deleted = await dataTableStore.deleteDataTableColumn(dataTableId, projectId, columnId);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(dataStoreApi.deleteDataStoreColumnApi).toHaveBeenCalledWith(
|
||||
expect(dataTableApi.deleteDataTableColumnApi).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
dataTableId,
|
||||
projectId,
|
||||
columnId,
|
||||
);
|
||||
expect(dataStoreStore.dataStores[0].columns.find((c) => c.id === columnId)).toBeUndefined();
|
||||
expect(dataTableStore.dataTables[0].columns.find((c) => c.id === columnId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can delete rows', async () => {
|
||||
const datastoreId = faker.string.alphanumeric(10);
|
||||
const dataTableId = faker.string.alphanumeric(10);
|
||||
const projectId = 'p1';
|
||||
const rowIds = [1, 2, 3];
|
||||
|
||||
vi.spyOn(dataStoreApi, 'deleteDataStoreRowsApi').mockResolvedValue(true);
|
||||
vi.spyOn(dataTableApi, 'deleteDataTableRowsApi').mockResolvedValue(true);
|
||||
|
||||
const result = await dataStoreStore.deleteRows(datastoreId, projectId, rowIds);
|
||||
const result = await dataTableStore.deleteRows(dataTableId, projectId, rowIds);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(dataStoreApi.deleteDataStoreRowsApi).toHaveBeenCalledWith(
|
||||
expect(dataTableApi.deleteDataTableRowsApi).toHaveBeenCalledWith(
|
||||
rootStore.restApiContext,
|
||||
datastoreId,
|
||||
dataTableId,
|
||||
rowIds,
|
||||
projectId,
|
||||
);
|
||||
@@ -0,0 +1,271 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { DATA_TABLE_STORE } from '@/features/dataTable/constants';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import {
|
||||
fetchDataTablesApi,
|
||||
createDataTableApi,
|
||||
deleteDataTableApi,
|
||||
updateDataTableApi,
|
||||
addDataTableColumnApi,
|
||||
deleteDataTableColumnApi,
|
||||
moveDataTableColumnApi,
|
||||
getDataTableRowsApi,
|
||||
insertDataTableRowApi,
|
||||
updateDataTableRowsApi,
|
||||
deleteDataTableRowsApi,
|
||||
fetchDataTableGlobalLimitInBytes,
|
||||
} from '@/features/dataTable/dataTable.api';
|
||||
import type {
|
||||
DataTable,
|
||||
DataTableColumnCreatePayload,
|
||||
DataTableRow,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { reorderItem } from '@/features/dataTable/utils';
|
||||
import { type DataTableSizeStatus } from 'n8n-workflow';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
export const useDataTableStore = defineStore(DATA_TABLE_STORE, () => {
|
||||
const rootStore = useRootStore();
|
||||
const projectStore = useProjectsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const dataTables = ref<DataTable[]>([]);
|
||||
const totalCount = ref(0);
|
||||
const dataTableSize = ref(0);
|
||||
const dataTableSizeLimitState = ref<DataTableSizeStatus>('ok');
|
||||
const dataTableTableSizes = ref<Record<string, number>>({});
|
||||
|
||||
const formatSize = (sizeBytes: number) => {
|
||||
return Number((sizeBytes / 1024 / 1024).toFixed(2));
|
||||
};
|
||||
|
||||
const maxSizeMB = computed(() =>
|
||||
Math.floor(settingsStore.settings?.dataTables?.maxSize / 1024 / 1024),
|
||||
);
|
||||
|
||||
const dataTableSizes = computed(() => {
|
||||
const formattedSizes: Record<string, number> = {};
|
||||
for (const [dataTableId, sizeBytes] of Object.entries(dataTableTableSizes.value)) {
|
||||
formattedSizes[dataTableId] = formatSize(sizeBytes);
|
||||
}
|
||||
return formattedSizes;
|
||||
});
|
||||
|
||||
const fetchDataTables = async (projectId: string, page: number, pageSize: number) => {
|
||||
const response = await fetchDataTablesApi(rootStore.restApiContext, projectId, {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
dataTables.value = response.data;
|
||||
totalCount.value = response.count;
|
||||
};
|
||||
|
||||
const createDataTable = async (name: string, projectId: string) => {
|
||||
const newTable = await createDataTableApi(rootStore.restApiContext, name, projectId);
|
||||
if (!newTable.project && projectId) {
|
||||
const project = await projectStore.fetchProject(projectId);
|
||||
if (project) {
|
||||
newTable.project = project;
|
||||
}
|
||||
}
|
||||
dataTables.value.push(newTable);
|
||||
totalCount.value += 1;
|
||||
return newTable;
|
||||
};
|
||||
|
||||
const deleteDataTable = async (dataTableId: string, projectId: string) => {
|
||||
const deleted = await deleteDataTableApi(rootStore.restApiContext, dataTableId, projectId);
|
||||
if (deleted) {
|
||||
dataTables.value = dataTables.value.filter((dataTable) => dataTable.id !== dataTableId);
|
||||
totalCount.value -= 1;
|
||||
}
|
||||
return deleted;
|
||||
};
|
||||
|
||||
const deleteDataTableColumn = async (
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
) => {
|
||||
const deleted = await deleteDataTableColumnApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
projectId,
|
||||
columnId,
|
||||
);
|
||||
if (deleted) {
|
||||
const index = dataTables.value.findIndex((dataTable) => dataTable.id === dataTableId);
|
||||
if (index !== -1) {
|
||||
dataTables.value[index].columns = dataTables.value[index].columns.filter(
|
||||
(column) => column.id !== columnId,
|
||||
);
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
};
|
||||
|
||||
const updateDataTable = async (dataTableId: string, name: string, projectId: string) => {
|
||||
const updated = await updateDataTableApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
name,
|
||||
projectId,
|
||||
);
|
||||
if (updated) {
|
||||
const index = dataTables.value.findIndex((table) => table.id === dataTableId);
|
||||
if (index !== -1) {
|
||||
dataTables.value[index] = { ...dataTables.value[index], name };
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
const fetchDataTableDetails = async (dataTableId: string, projectId: string) => {
|
||||
const response = await fetchDataTablesApi(rootStore.restApiContext, projectId, undefined, {
|
||||
projectId,
|
||||
id: dataTableId,
|
||||
});
|
||||
if (response.data.length > 0) {
|
||||
dataTables.value = response.data;
|
||||
return response.data[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fetchOrFindDataTable = async (dataTableId: string, projectId: string) => {
|
||||
const existingTable = dataTables.value.find((table) => table.id === dataTableId);
|
||||
if (existingTable) {
|
||||
return existingTable;
|
||||
}
|
||||
return await fetchDataTableDetails(dataTableId, projectId);
|
||||
};
|
||||
|
||||
const addDataTableColumn = async (
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
column: DataTableColumnCreatePayload,
|
||||
) => {
|
||||
const newColumn = await addDataTableColumnApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
projectId,
|
||||
column,
|
||||
);
|
||||
if (newColumn) {
|
||||
const index = dataTables.value.findIndex((table) => table.id === dataTableId);
|
||||
if (index !== -1) {
|
||||
dataTables.value[index].columns.push(newColumn);
|
||||
}
|
||||
}
|
||||
return newColumn;
|
||||
};
|
||||
|
||||
const moveDataTableColumn = async (
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
columnId: string,
|
||||
targetIndex: number,
|
||||
) => {
|
||||
const moved = await moveDataTableColumnApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
projectId,
|
||||
columnId,
|
||||
targetIndex,
|
||||
);
|
||||
if (moved) {
|
||||
const dsIndex = dataTables.value.findIndex((table) => table.id === dataTableId);
|
||||
const fromIndex = dataTables.value[dsIndex].columns.findIndex((col) => col.id === columnId);
|
||||
dataTables.value[dsIndex].columns = reorderItem(
|
||||
dataTables.value[dsIndex].columns,
|
||||
fromIndex,
|
||||
targetIndex,
|
||||
);
|
||||
}
|
||||
return moved;
|
||||
};
|
||||
|
||||
const fetchDataTableContent = async (
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
sortBy: string,
|
||||
filter?: string,
|
||||
) => {
|
||||
return await getDataTableRowsApi(rootStore.restApiContext, dataTableId, projectId, {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
sortBy,
|
||||
filter,
|
||||
});
|
||||
};
|
||||
|
||||
const insertEmptyRow = async (dataTableId: string, projectId: string) => {
|
||||
const inserted = await insertDataTableRowApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
{},
|
||||
projectId,
|
||||
);
|
||||
return inserted[0];
|
||||
};
|
||||
|
||||
const updateRow = async (
|
||||
dataTableId: string,
|
||||
projectId: string,
|
||||
rowId: number,
|
||||
rowData: DataTableRow,
|
||||
) => {
|
||||
return await updateDataTableRowsApi(
|
||||
rootStore.restApiContext,
|
||||
dataTableId,
|
||||
rowId,
|
||||
rowData,
|
||||
projectId,
|
||||
);
|
||||
};
|
||||
|
||||
const deleteRows = async (dataTableId: string, projectId: string, rowIds: number[]) => {
|
||||
return await deleteDataTableRowsApi(rootStore.restApiContext, dataTableId, rowIds, projectId);
|
||||
};
|
||||
|
||||
const fetchDataTableSize = async () => {
|
||||
const result = await fetchDataTableGlobalLimitInBytes(rootStore.restApiContext);
|
||||
dataTableSize.value = formatSize(result.totalBytes);
|
||||
dataTableSizeLimitState.value = result.quotaStatus;
|
||||
|
||||
const tableSizes: Record<string, number> = {};
|
||||
for (const [dataTableId, info] of Object.entries(result.dataTables)) {
|
||||
tableSizes[dataTableId] = info.sizeBytes;
|
||||
}
|
||||
dataTableTableSizes.value = tableSizes;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
dataTables,
|
||||
totalCount,
|
||||
fetchDataTables,
|
||||
fetchDataTableSize,
|
||||
dataTableSize: computed(() => dataTableSize.value),
|
||||
dataTableSizeLimitState: computed(() => dataTableSizeLimitState.value),
|
||||
dataTableSizes,
|
||||
maxSizeMB,
|
||||
createDataTable,
|
||||
deleteDataTable,
|
||||
updateDataTable,
|
||||
fetchDataTableDetails,
|
||||
fetchOrFindDataTable,
|
||||
addDataTableColumn,
|
||||
deleteDataTableColumn,
|
||||
moveDataTableColumn,
|
||||
fetchDataTableContent,
|
||||
insertEmptyRow,
|
||||
updateRow,
|
||||
deleteRows,
|
||||
};
|
||||
});
|
||||
+9
-9
@@ -1,10 +1,10 @@
|
||||
import type { Project } from '@/types/projects.types';
|
||||
|
||||
export type DataStore = {
|
||||
export type DataTable = {
|
||||
id: string;
|
||||
name: string;
|
||||
sizeBytes: number;
|
||||
columns: DataStoreColumn[];
|
||||
columns: DataTableColumn[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projectId: string;
|
||||
@@ -12,8 +12,8 @@ export type DataStore = {
|
||||
};
|
||||
|
||||
// Single sources of truth for supported types
|
||||
export const DATA_STORE_COLUMN_TYPES = ['string', 'number', 'boolean', 'date'] as const;
|
||||
export type DataStoreColumnType = (typeof DATA_STORE_COLUMN_TYPES)[number];
|
||||
export const DATA_TABLE_COLUMN_TYPES = ['string', 'number', 'boolean', 'date'] as const;
|
||||
export type DataTableColumnType = (typeof DATA_TABLE_COLUMN_TYPES)[number];
|
||||
|
||||
export const AG_GRID_CELL_TYPES = [
|
||||
'text',
|
||||
@@ -25,18 +25,18 @@ export const AG_GRID_CELL_TYPES = [
|
||||
] as const;
|
||||
export type AGGridCellType = (typeof AG_GRID_CELL_TYPES)[number];
|
||||
|
||||
export type DataStoreColumn = {
|
||||
export type DataTableColumn = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: DataStoreColumnType;
|
||||
type: DataTableColumnType;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type DataStoreColumnCreatePayload = Pick<DataStoreColumn, 'name' | 'type'>;
|
||||
export type DataTableColumnCreatePayload = Pick<DataTableColumn, 'name' | 'type'>;
|
||||
|
||||
export type DataStoreValue = string | number | boolean | Date | null;
|
||||
export type DataTableValue = string | number | boolean | Date | null;
|
||||
|
||||
export type DataStoreRow = Record<string, DataStoreValue>;
|
||||
export type DataTableRow = Record<string, DataTableValue>;
|
||||
|
||||
export type AddColumnResponse = {
|
||||
success: boolean;
|
||||
+26
-26
@@ -1,37 +1,37 @@
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { type FrontendModuleDescription } from '@/moduleInitializer/module.types';
|
||||
import {
|
||||
ADD_DATA_STORE_MODAL_KEY,
|
||||
DATA_STORE_DETAILS,
|
||||
DATA_STORE_VIEW,
|
||||
PROJECT_DATA_STORES,
|
||||
} from '@/features/dataStore/constants';
|
||||
ADD_DATA_TABLE_MODAL_KEY,
|
||||
DATA_TABLE_DETAILS,
|
||||
DATA_TABLE_VIEW,
|
||||
PROJECT_DATA_TABLES,
|
||||
} from '@/features/dataTable/constants';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const MainSidebar = async () => await import('@/components/MainSidebar.vue');
|
||||
const DataStoreView = async () => await import('@/features/dataStore/DataStoreView.vue');
|
||||
const DataStoreDetailsView = async () =>
|
||||
await import('@/features/dataStore/DataStoreDetailsView.vue');
|
||||
const DataTableView = async () => await import('@/features/dataTable/DataTableView.vue');
|
||||
const DataTableDetailsView = async () =>
|
||||
await import('@/features/dataTable/DataTableDetailsView.vue');
|
||||
|
||||
export const DataTableModule: FrontendModuleDescription = {
|
||||
id: 'data-table',
|
||||
name: 'Data Store',
|
||||
description: 'Manage and store data efficiently with the Data Store module.',
|
||||
name: 'Data Table',
|
||||
description: 'Manage and store data efficiently with the Data Table module.',
|
||||
icon: 'database',
|
||||
modals: [
|
||||
{
|
||||
key: ADD_DATA_STORE_MODAL_KEY,
|
||||
component: async () => await import('./components/AddDataStoreModal.vue'),
|
||||
key: ADD_DATA_TABLE_MODAL_KEY,
|
||||
component: async () => await import('./components/AddDataTableModal.vue'),
|
||||
initialState: { open: false },
|
||||
},
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
name: DATA_STORE_VIEW,
|
||||
name: DATA_TABLE_VIEW,
|
||||
path: '/home/datatables',
|
||||
components: {
|
||||
default: DataStoreView,
|
||||
default: DataTableView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
@@ -39,11 +39,11 @@ export const DataTableModule: FrontendModuleDescription = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PROJECT_DATA_STORES,
|
||||
name: PROJECT_DATA_TABLES,
|
||||
path: 'datatables/:new(new)?',
|
||||
props: true,
|
||||
components: {
|
||||
default: DataStoreView,
|
||||
default: DataTableView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
@@ -52,11 +52,11 @@ export const DataTableModule: FrontendModuleDescription = {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: DATA_STORE_DETAILS,
|
||||
name: DATA_TABLE_DETAILS,
|
||||
path: 'datatables/:id',
|
||||
props: true,
|
||||
components: {
|
||||
default: DataStoreDetailsView,
|
||||
default: DataTableDetailsView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
@@ -68,21 +68,21 @@ export const DataTableModule: FrontendModuleDescription = {
|
||||
projectTabs: {
|
||||
overview: [
|
||||
{
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
value: DATA_STORE_VIEW,
|
||||
label: i18n.baseText('dataTable.dataTables'),
|
||||
value: DATA_TABLE_VIEW,
|
||||
tag: i18n.baseText('generic.betaProper'),
|
||||
to: {
|
||||
name: DATA_STORE_VIEW,
|
||||
name: DATA_TABLE_VIEW,
|
||||
},
|
||||
},
|
||||
],
|
||||
project: [
|
||||
{
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
value: PROJECT_DATA_STORES,
|
||||
label: i18n.baseText('dataTable.dataTables'),
|
||||
value: PROJECT_DATA_TABLES,
|
||||
tag: i18n.baseText('generic.betaProper'),
|
||||
dynamicRoute: {
|
||||
name: PROJECT_DATA_STORES,
|
||||
name: PROJECT_DATA_TABLES,
|
||||
includeProjectId: true,
|
||||
},
|
||||
},
|
||||
@@ -90,8 +90,8 @@ export const DataTableModule: FrontendModuleDescription = {
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
key: 'dataStore',
|
||||
displayName: 'Data Store',
|
||||
key: 'dataTable',
|
||||
displayName: 'Data Table',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type {
|
||||
AGGridCellType,
|
||||
DataTableValue,
|
||||
DataTableColumnType,
|
||||
} from '@/features/dataTable/dataTable.types';
|
||||
import { AG_GRID_CELL_TYPES, DATA_TABLE_COLUMN_TYPES } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
export const isDataTableValue = (value: unknown): value is DataTableValue => {
|
||||
return (
|
||||
value === null ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value instanceof Date
|
||||
);
|
||||
};
|
||||
|
||||
export const isAGGridCellType = (value: unknown): value is AGGridCellType => {
|
||||
return typeof value === 'string' && (AG_GRID_CELL_TYPES as readonly string[]).includes(value);
|
||||
};
|
||||
|
||||
export const isDataTableColumnType = (type: unknown): type is DataTableColumnType => {
|
||||
return typeof type === 'string' && (DATA_TABLE_COLUMN_TYPES as readonly string[]).includes(type);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { BaseResource } from '@/Interface';
|
||||
import type { DataTable } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
/**
|
||||
* Data Table resource type definition
|
||||
* This extends the ModuleResources interface to add DataTable as a resource type
|
||||
*/
|
||||
export type DataTableResource = BaseResource &
|
||||
DataTable & {
|
||||
resourceType: 'dataTable';
|
||||
};
|
||||
|
||||
// Extend the ModuleResources interface to include DataTable
|
||||
declare module '@/Interface' {
|
||||
interface ModuleResources {
|
||||
dataTable: DataTableResource;
|
||||
}
|
||||
}
|
||||
|
||||
// Export to make this a module
|
||||
export {};
|
||||
+34
-28
@@ -9,7 +9,7 @@ import type {
|
||||
import type { Ref } from 'vue';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { I18nClass } from '@n8n/i18n';
|
||||
import type { DataStoreColumn, DataStoreRow } from '@/features/dataStore/datastore.types';
|
||||
import type { DataTableColumn, DataTableRow } from '@/features/dataTable/dataTable.types';
|
||||
import {
|
||||
ADD_ROW_ROW_ID,
|
||||
EMPTY_VALUE,
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
NUMBER_DECIMAL_SEPARATOR,
|
||||
NUMBER_THOUSAND_SEPARATOR,
|
||||
NUMBER_WITH_SPACES_REGEX,
|
||||
} from '@/features/dataStore/constants';
|
||||
import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
|
||||
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
||||
} from '@/features/dataTable/constants';
|
||||
import NullEmptyCellRenderer from '@/features/dataTable/components/dataGrid/NullEmptyCellRenderer.vue';
|
||||
import { isDataTableValue } from '@/features/dataTable/typeGuards';
|
||||
|
||||
export const getCellClass = (params: CellClassParams): string => {
|
||||
if (params.data?.id === ADD_ROW_ROW_ID) {
|
||||
@@ -32,7 +32,7 @@ export const getCellClass = (params: CellClassParams): string => {
|
||||
};
|
||||
|
||||
export const createValueGetter =
|
||||
(col: DataStoreColumn) => (params: ValueGetterParams<DataStoreRow>) => {
|
||||
(col: DataTableColumn) => (params: ValueGetterParams<DataTableRow>) => {
|
||||
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,33 +46,39 @@ export const createValueGetter =
|
||||
};
|
||||
|
||||
export const createCellRendererSelector =
|
||||
(col: DataStoreColumn) => (params: ICellRendererParams) => {
|
||||
(col: DataTableColumn) => (params: ICellRendererParams) => {
|
||||
if (params.data?.id === ADD_ROW_ROW_ID || col.id === 'add-column') {
|
||||
return {};
|
||||
}
|
||||
let rowValue = (params.data as DataStoreRow | undefined)?.[col.name];
|
||||
let rowValue = (params.data as DataTableRow | undefined)?.[col.name];
|
||||
if (rowValue === undefined) {
|
||||
rowValue = null;
|
||||
}
|
||||
if (rowValue === null) {
|
||||
return { component: NullEmptyCellRenderer, params: { value: NULL_VALUE } };
|
||||
return {
|
||||
component: NullEmptyCellRenderer,
|
||||
params: { value: NULL_VALUE },
|
||||
};
|
||||
}
|
||||
if (rowValue === '') {
|
||||
return { component: NullEmptyCellRenderer, params: { value: EMPTY_VALUE } };
|
||||
return {
|
||||
component: NullEmptyCellRenderer,
|
||||
params: { value: EMPTY_VALUE },
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const createStringValueSetter =
|
||||
(col: DataStoreColumn, isTextEditorOpen: Ref<boolean>) =>
|
||||
(params: ValueSetterParams<DataStoreRow>) => {
|
||||
(col: DataTableColumn, isTextEditorOpen: Ref<boolean>) =>
|
||||
(params: ValueSetterParams<DataTableRow>) => {
|
||||
let originalValue = params.data[col.name];
|
||||
if (originalValue === undefined) {
|
||||
originalValue = null;
|
||||
}
|
||||
let newValue = params.newValue as unknown as DataStoreRow[keyof DataStoreRow];
|
||||
let newValue = params.newValue as unknown as DataTableRow[keyof DataTableRow];
|
||||
|
||||
if (!isDataStoreValue(newValue)) {
|
||||
if (!isDataTableValue(newValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,14 +95,14 @@ export const createStringValueSetter =
|
||||
};
|
||||
|
||||
export const stringCellEditorParams = (
|
||||
params: CellEditRequestEvent<DataStoreRow>,
|
||||
params: CellEditRequestEvent<DataTableRow>,
|
||||
): { value: string; maxLength: number } => ({
|
||||
value: (params.value as string | null | undefined) ?? '',
|
||||
maxLength: 999999999,
|
||||
});
|
||||
|
||||
export const dateValueFormatter = (
|
||||
params: ValueFormatterParams<DataStoreRow, Date | null | undefined>,
|
||||
params: ValueFormatterParams<DataTableRow, Date | null | undefined>,
|
||||
): string => {
|
||||
const value = params.value;
|
||||
if (value === null || value === undefined) return '';
|
||||
@@ -111,7 +117,7 @@ const numberWithSpaces = (num: number) => {
|
||||
};
|
||||
|
||||
export const numberValueFormatter = (
|
||||
params: ValueFormatterParams<DataStoreRow, number | null | undefined>,
|
||||
params: ValueFormatterParams<DataTableRow, number | null | undefined>,
|
||||
): string => {
|
||||
const value = params.value;
|
||||
if (value === null || value === undefined) return '';
|
||||
@@ -120,70 +126,70 @@ export const numberValueFormatter = (
|
||||
|
||||
const createNullFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'null',
|
||||
displayName: i18n.baseText('dataStore.filters.isNull'),
|
||||
displayName: i18n.baseText('dataTable.filters.isNull'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 0,
|
||||
});
|
||||
|
||||
const createNotNullFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'notNull',
|
||||
displayName: i18n.baseText('dataStore.filters.isNotNull'),
|
||||
displayName: i18n.baseText('dataTable.filters.isNotNull'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 0,
|
||||
});
|
||||
|
||||
const createIsEmptyFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'isEmpty',
|
||||
displayName: i18n.baseText('dataStore.filters.isEmpty'),
|
||||
displayName: i18n.baseText('dataTable.filters.isEmpty'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 0,
|
||||
});
|
||||
|
||||
const createIsNotEmptyFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'notEmpty',
|
||||
displayName: i18n.baseText('dataStore.filters.isNotEmpty'),
|
||||
displayName: i18n.baseText('dataTable.filters.isNotEmpty'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 0,
|
||||
});
|
||||
|
||||
const createBetweenFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'between',
|
||||
displayName: i18n.baseText('dataStore.filters.between'),
|
||||
displayName: i18n.baseText('dataTable.filters.between'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 2,
|
||||
});
|
||||
|
||||
const createGreaterThanFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'greaterThan',
|
||||
displayName: i18n.baseText('dataStore.filters.greaterThan'),
|
||||
displayName: i18n.baseText('dataTable.filters.greaterThan'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 1,
|
||||
});
|
||||
|
||||
const createGreaterThanOrEqualFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'greaterThanOrEqual',
|
||||
displayName: i18n.baseText('dataStore.filters.greaterThanOrEqual'),
|
||||
displayName: i18n.baseText('dataTable.filters.greaterThanOrEqual'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 1,
|
||||
});
|
||||
|
||||
const createLessThanFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'lessThan',
|
||||
displayName: i18n.baseText('dataStore.filters.lessThan'),
|
||||
displayName: i18n.baseText('dataTable.filters.lessThan'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 1,
|
||||
});
|
||||
|
||||
const createLessThanOrEqualFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'lessThanOrEqual',
|
||||
displayName: i18n.baseText('dataStore.filters.lessThanOrEqual'),
|
||||
displayName: i18n.baseText('dataTable.filters.lessThanOrEqual'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 1,
|
||||
});
|
||||
|
||||
const createInRangeFilterOption = (i18n: I18nClass) => ({
|
||||
displayKey: 'inRange',
|
||||
displayName: i18n.baseText('dataStore.filters.between'),
|
||||
displayName: i18n.baseText('dataTable.filters.between'),
|
||||
predicate: () => true,
|
||||
numberOfInputs: 2,
|
||||
});
|
||||
@@ -226,13 +232,13 @@ export const getBooleanColumnFilterOptions = (i18n: I18nClass) => [
|
||||
'empty',
|
||||
{
|
||||
displayKey: 'true',
|
||||
displayName: i18n.baseText('dataStore.filters.true'),
|
||||
displayName: i18n.baseText('dataTable.filters.true'),
|
||||
numberOfInputs: 0,
|
||||
predicate: () => true,
|
||||
},
|
||||
{
|
||||
displayKey: 'false',
|
||||
displayName: i18n.baseText('dataStore.filters.false'),
|
||||
displayName: i18n.baseText('dataTable.filters.false'),
|
||||
numberOfInputs: 0,
|
||||
predicate: () => true,
|
||||
},
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { BackendFilterCondition, FilterOperation } from '../types/dataStoreFilters.types';
|
||||
import type { BackendFilterCondition, FilterOperation } from '../types/dataTableFilters.types';
|
||||
|
||||
export const SPECIAL_COLUMNS = ['add-column', 'ag-Grid-SelectionColumn'] as const;
|
||||
export const isSpecialColumn = (value: unknown): value is (typeof SPECIAL_COLUMNS)[number] =>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { processTextFilter, processNumberFilter, processDateFilter } from './filterProcessors';
|
||||
import type { FilterModel, FilterOperation } from '../types/dataStoreFilters.types';
|
||||
import type { FilterModel, FilterOperation } from '../types/dataTableFilters.types';
|
||||
|
||||
vi.mock('./filterMappings', () => ({
|
||||
mapTextTypeToBackend: vi.fn((type: string) => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { BackendFilterRecord, FilterModel } from '../types/dataStoreFilters.types';
|
||||
import type { BackendFilterRecord, FilterModel } from '../types/dataTableFilters.types';
|
||||
import { mapTextTypeToBackend, mapNumberDateTypeToBackend } from './filterMappings';
|
||||
|
||||
export function processTextFilter(
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { parseLooseDateInput, areValuesEqual } from '@/features/dataStore/utils/typeUtils';
|
||||
import { parseLooseDateInput, areValuesEqual } from '@/features/dataTable/utils/typeUtils';
|
||||
|
||||
describe('Loose Date Parsing', () => {
|
||||
it('parses full loose datetime with single-digit parts', () => {
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { LOOSE_DATE_REGEX } from '@/features/dataStore/constants';
|
||||
import type { DataStoreColumnType } from '@/features/dataStore/datastore.types';
|
||||
import { LOOSE_DATE_REGEX } from '@/features/dataTable/constants';
|
||||
import type { DataTableColumnType } from '@/features/dataTable/dataTable.types';
|
||||
|
||||
/**
|
||||
* Parses a loose date string into a Date object.
|
||||
@@ -43,13 +43,13 @@ export const parseLooseDateInput = (text: string): Date | null => {
|
||||
* Check if two column values are equal, with special handling for date types.
|
||||
* @param oldValue unknown
|
||||
* @param newValue unknown
|
||||
* @param type DataStoreColumnType | undefined
|
||||
* @param type DataTableColumnType | undefined
|
||||
* @returns boolean
|
||||
*/
|
||||
export const areValuesEqual = (
|
||||
oldValue: unknown,
|
||||
newValue: unknown,
|
||||
type: DataStoreColumnType | undefined,
|
||||
type: DataTableColumnType | undefined,
|
||||
) => {
|
||||
if (type && type === 'date') {
|
||||
if (oldValue instanceof Date && newValue instanceof Date) {
|
||||
@@ -28,7 +28,7 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { h } from 'vue';
|
||||
import { useRolesStore } from './stores/roles.store';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useDataTableStore } from '@/features/dataTable/dataTable.store';
|
||||
|
||||
export const state = {
|
||||
initialized: false,
|
||||
@@ -136,7 +136,7 @@ export async function initializeAuthenticatedFeatures(
|
||||
const insightsStore = useInsightsStore();
|
||||
const uiStore = useUIStore();
|
||||
const versionsStore = useVersionsStore();
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const dataTableStore = useDataTableStore();
|
||||
|
||||
if (sourceControlStore.isEnterpriseSourceControlEnabled) {
|
||||
try {
|
||||
@@ -176,13 +176,13 @@ export async function initializeAuthenticatedFeatures(
|
||||
}
|
||||
|
||||
if (settingsStore.isDataTableFeatureEnabled) {
|
||||
void dataStoreStore
|
||||
.fetchDataStoreSize()
|
||||
void dataTableStore
|
||||
.fetchDataTableSize()
|
||||
.then(({ quotaStatus }) => {
|
||||
if (quotaStatus === 'error') {
|
||||
uiStore.pushBannerToStack('DATA_STORE_STORAGE_LIMIT_ERROR');
|
||||
uiStore.pushBannerToStack('DATA_TABLE_STORAGE_LIMIT_ERROR');
|
||||
} else if (quotaStatus === 'warn') {
|
||||
uiStore.pushBannerToStack('DATA_STORE_STORAGE_LIMIT_WARNING');
|
||||
uiStore.pushBannerToStack('DATA_TABLE_STORAGE_LIMIT_WARNING');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Router } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { DataTableModule } from '@/features/dataStore/module.descriptor';
|
||||
import { DataTableModule } from '@/features/dataTable/module.descriptor';
|
||||
import { registerResource } from '@/moduleInitializer/resourceRegistry';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import * as dataStoreApi from '@/features/dataStore/dataStore.api';
|
||||
import * as dataTableApi from '@/features/dataTable/dataTable.api';
|
||||
import * as projectsApi from '@/api/projects.api';
|
||||
import * as workflowsApi from '@/api/workflows';
|
||||
import * as workflowsEEApi from '@/api/workflows.ee';
|
||||
@@ -255,7 +255,7 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||
const [credentials, workflows, dataTables] = await Promise.all([
|
||||
credentialsApi.getAllCredentials(rootStore.restApiContext, { projectId }),
|
||||
workflowsApi.getWorkflows(rootStore.restApiContext, { projectId }),
|
||||
dataStoreApi.fetchDataStoresApi(rootStore.restApiContext, projectId),
|
||||
dataTableApi.fetchDataTablesApi(rootStore.restApiContext, projectId),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -38,6 +38,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
|
||||
securityAudit: {},
|
||||
folder: {},
|
||||
insights: {},
|
||||
// TODO
|
||||
dataStore: {},
|
||||
execution: {},
|
||||
workflowTags: {},
|
||||
|
||||
Reference in New Issue
Block a user