feat(editor): Address data tables UI feedback (no-changelog) (#18566)

Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
Svetoslav Dekov
2025-08-25 11:25:28 +02:00
committed by GitHub
parent 01ff2bacc5
commit fb97ec876c
10 changed files with 151 additions and 97 deletions

View File

@@ -2837,7 +2837,7 @@
"dataStore.dataStores": "Data Tables", "dataStore.dataStores": "Data Tables",
"dataStore.empty.label": "You don't have any data tables yet", "dataStore.empty.label": "You don't have any data tables yet",
"dataStore.empty.description": "Once you create data tables for your projects, they will appear here", "dataStore.empty.description": "Once you create data tables for your projects, they will appear here",
"dataStore.empty.button.label": "Create data table in \"{projectName}\"", "dataStore.empty.button.label": "Create Data Table in \"{projectName}\"",
"dataStore.card.size": "{size}MB", "dataStore.card.size": "{size}MB",
"dataStore.card.column.count": "{count} column | {count} columns", "dataStore.card.column.count": "{count} column | {count} columns",
"dataStore.card.row.count": "{count} record | {count} records", "dataStore.card.row.count": "{count} record | {count} records",
@@ -2847,9 +2847,9 @@
"dataStore.sort.nameDesc": "Sort by name (Z-A)", "dataStore.sort.nameDesc": "Sort by name (Z-A)",
"dataStore.search.placeholder": "Search", "dataStore.search.placeholder": "Search",
"dataStore.error.fetching": "Error loading data tables", "dataStore.error.fetching": "Error loading data tables",
"dataStore.add.title": "Create data table", "dataStore.add.title": "Create new data table",
"dataStore.add.description": "Set up a new data table to organize and manage your data.", "dataStore.add.description": "Set up a new data table to organize and manage your data.",
"dataStore.add.button.label": "Create data table", "dataStore.add.button.label": "Create Data Table",
"dataStore.add.input.name.label": "Data Table Name", "dataStore.add.input.name.label": "Data Table Name",
"dataStore.add.input.name.placeholder": "Enter data table name", "dataStore.add.input.name.placeholder": "Enter data table name",
"dataStore.add.error": "Error creating data table", "dataStore.add.error": "Error creating data table",

View File

@@ -44,10 +44,27 @@ const projectTabsSpy = vi.fn().mockReturnValue({
render: vi.fn(), render: vi.fn(),
}); });
const ProjectCreateResourceStub = {
props: {
actions: Array,
},
template: `
<div>
<div data-test-id="add-resource"><button role="button"></button></div>
<button data-test-id="add-resource-workflow" @click="$emit('action', 'workflow')">Workflow</button>
<button data-test-id="action-credential" @click="$emit('action', 'credential')">Credentials</button>
<div data-test-id="add-resource-actions" >
<button v-for="action in $props.actions" :key="action.value"></button>
</div>
</div>
`,
};
const renderComponent = createComponentRenderer(ProjectHeader, { const renderComponent = createComponentRenderer(ProjectHeader, {
global: { global: {
stubs: { stubs: {
ProjectTabs: projectTabsSpy, ProjectTabs: projectTabsSpy,
ProjectCreateResource: ProjectCreateResourceStub,
}, },
}, },
}); });
@@ -69,6 +86,7 @@ describe('ProjectHeader', () => {
projectsStore.teamProjectsLimit = -1; projectsStore.teamProjectsLimit = -1;
settingsStore.settings.folders = { enabled: false }; settingsStore.settings.folders = { enabled: false };
settingsStore.isDataStoreFeatureEnabled = true;
// Setup default moduleTabs structure // Setup default moduleTabs structure
uiStore.moduleTabs = { uiStore.moduleTabs = {
@@ -436,4 +454,21 @@ describe('ProjectHeader', () => {
); );
}); });
}); });
describe('ProjectCreateResource', () => {
it('should render menu items', () => {
const { getByTestId } = renderComponent();
const actionsContainer = getByTestId('add-resource-actions');
expect(actionsContainer).toBeInTheDocument();
expect(actionsContainer.children).toHaveLength(2);
});
it('should not render datastore menu item if data store feature is disabled', () => {
settingsStore.isDataStoreFeatureEnabled = false;
const { getByTestId } = renderComponent();
const actionsContainer = getByTestId('add-resource-actions');
expect(actionsContainer).toBeInTheDocument();
expect(actionsContainer.children).toHaveLength(1);
});
});
}); });

View File

@@ -20,12 +20,7 @@ import { type IconName } from '@n8n/design-system/components/N8nIcon/icons';
import type { IUser } from 'n8n-workflow'; import type { IUser } from 'n8n-workflow';
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types'; import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
export type CustomAction = {
id: string;
label: string;
disabled?: boolean;
};
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -37,17 +32,8 @@ const uiStore = useUIStore();
const projectPages = useProjectPages(); const projectPages = useProjectPages();
type Props = {
customActions?: CustomAction[];
};
const props = withDefaults(defineProps<Props>(), {
customActions: () => [],
});
const emit = defineEmits<{ const emit = defineEmits<{
createFolder: []; createFolder: [];
customActionSelected: [actionId: string, projectId: string];
}>(); }>();
const headerIcon = computed((): IconOrEmoji => { const headerIcon = computed((): IconOrEmoji => {
@@ -122,6 +108,7 @@ const ACTION_TYPES = {
WORKFLOW: 'workflow', WORKFLOW: 'workflow',
CREDENTIAL: 'credential', CREDENTIAL: 'credential',
FOLDER: 'folder', FOLDER: 'folder',
DATA_STORE: 'dataStore',
} as const; } as const;
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES]; type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
@@ -155,16 +142,17 @@ const menu = computed(() => {
}); });
} }
// Append custom actions if (settingsStore.isDataStoreFeatureEnabled) {
if (props.customActions?.length) { // TODO: this should probably be moved to the module descriptor as a setting
props.customActions.forEach((customAction) => { items.push({
items.push({ value: ACTION_TYPES.DATA_STORE,
value: customAction.id, label: i18n.baseText('dataStore.add.button.label'),
label: customAction.label, disabled:
disabled: customAction.disabled ?? false, sourceControlStore.preferences.branchReadOnly ||
}); !getResourcePermissions(homeProject.value?.scopes)?.dataStore?.create,
}); });
} }
return items; return items;
}); });
@@ -196,6 +184,12 @@ const actions: Record<ActionTypes, (projectId: string) => void> = {
[ACTION_TYPES.FOLDER]: () => { [ACTION_TYPES.FOLDER]: () => {
emit('createFolder'); emit('createFolder');
}, },
[ACTION_TYPES.DATA_STORE]: (projectId: string) => {
void router.push({
name: PROJECT_DATA_STORES,
params: { projectId, new: 'new' },
});
},
} as const; } as const;
const pageType = computed(() => { const pageType = computed(() => {
@@ -267,12 +261,6 @@ const onSelect = (action: string) => {
return; return;
} }
// Check if this is a custom action
if (!executableAction) {
emit('customActionSelected', action, homeProject.value.id);
return;
}
executableAction(homeProject.value.id); executableAction(homeProject.value.id);
}; };
</script> </script>

View File

@@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import ProjectHeader, { type CustomAction } from '@/components/Projects/ProjectHeader.vue'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue'; import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
import { useProjectPages } from '@/composables/useProjectPages'; import { useProjectPages } from '@/composables/useProjectPages';
import { useInsightsStore } from '@/features/insights/insights.store'; import { useInsightsStore } from '@/features/insights/insights.store';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import type { SortingAndPaginationUpdates } from '@/Interface'; import type { SortingAndPaginationUpdates } from '@/Interface';
@@ -17,6 +16,7 @@ import { useSourceControlStore } from '@/stores/sourceControl.store';
import { import {
ADD_DATA_STORE_MODAL_KEY, ADD_DATA_STORE_MODAL_KEY,
DEFAULT_DATA_STORE_PAGE_SIZE, DEFAULT_DATA_STORE_PAGE_SIZE,
PROJECT_DATA_STORES,
} from '@/features/dataStore/constants'; } from '@/features/dataStore/constants';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useDocumentTitle } from '@/composables/useDocumentTitle';
@@ -26,6 +26,7 @@ import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
const i18n = useI18n(); const i18n = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const projectPages = useProjectPages(); const projectPages = useProjectPages();
const { callDebounced } = useDebounce(); const { callDebounced } = useDebounce();
const documentTitle = useDocumentTitle(); const documentTitle = useDocumentTitle();
@@ -35,20 +36,13 @@ const dataStoreStore = useDataStoreStore();
const insightsStore = useInsightsStore(); const insightsStore = useInsightsStore();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();
const sourceControlStore = useSourceControlStore(); const sourceControlStore = useSourceControlStore();
const uiStore = useUIStore();
const loading = ref(true); const loading = ref(true);
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(DEFAULT_DATA_STORE_PAGE_SIZE); const pageSize = ref(DEFAULT_DATA_STORE_PAGE_SIZE);
const customProjectActions = computed<CustomAction[]>(() => [
{
id: 'add-data-store',
label: i18n.baseText('dataStore.add.button.label'),
disabled: loading.value || projectPages.isOverviewSubPage,
},
]);
const dataStoreResources = computed<DataStoreResource[]>(() => const dataStoreResources = computed<DataStoreResource[]>(() =>
dataStoreStore.dataStores.map((ds) => { dataStoreStore.dataStores.map((ds) => {
return { return {
@@ -58,6 +52,10 @@ const dataStoreResources = computed<DataStoreResource[]>(() =>
}), }),
); );
const projectId = computed(() => {
return Array.isArray(route.params.projectId) ? route.params.projectId[0] : route.params.projectId;
});
const totalCount = computed(() => dataStoreStore.totalCount); const totalCount = computed(() => dataStoreStore.totalCount);
const currentProject = computed(() => projectsStore.currentProject); const currentProject = computed(() => projectsStore.currentProject);
@@ -86,11 +84,8 @@ const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly
const initialize = async () => { const initialize = async () => {
loading.value = true; loading.value = true;
const projectId = Array.isArray(route.params.projectId)
? route.params.projectId[0]
: route.params.projectId;
try { try {
await dataStoreStore.fetchDataStores(projectId, currentPage.value, pageSize.value); await dataStoreStore.fetchDataStores(projectId.value, currentPage.value, pageSize.value);
} catch (error) { } catch (error) {
toast.showError(error, 'Error loading data stores'); toast.showError(error, 'Error loading data stores');
} finally { } finally {
@@ -111,18 +106,27 @@ const onPaginationUpdate = async (payload: SortingAndPaginationUpdates) => {
}; };
const onAddModalClick = () => { const onAddModalClick = () => {
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY); void router.push({
}; name: PROJECT_DATA_STORES,
params: { projectId: projectId.value, new: 'new' },
const onProjectHeaderAction = (action: string) => { });
if (action === 'add-data-store') {
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY);
}
}; };
onMounted(() => { onMounted(() => {
documentTitle.set(i18n.baseText('dataStore.dataStores')); documentTitle.set(i18n.baseText('dataStore.dataStores'));
}); });
watch(
() => route.params.new,
() => {
if (route.params.new === 'new') {
uiStore.openModal(ADD_DATA_STORE_MODAL_KEY);
} else {
uiStore.closeModal(ADD_DATA_STORE_MODAL_KEY);
}
},
{ immediate: true },
);
</script> </script>
<template> <template>
<ResourcesListLayout <ResourcesListLayout
@@ -144,10 +148,7 @@ onMounted(() => {
@update:pagination-and-sort="onPaginationUpdate" @update:pagination-and-sort="onPaginationUpdate"
> >
<template #header> <template #header>
<ProjectHeader <ProjectHeader>
:custom-actions="customProjectActions"
@custom-action-selected="onProjectHeaderAction"
>
<InsightsSummary <InsightsSummary
v-if="projectPages.isOverviewSubPage && insightsStore.isSummaryEnabled" v-if="projectPages.isOverviewSubPage && insightsStore.isSummaryEnabled"
:loading="insightsStore.weeklySummary.isLoading" :loading="insightsStore.weeklySummary.isLoading"

View File

@@ -4,7 +4,8 @@ import { onMounted, ref } from 'vue';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { DATA_STORE_DETAILS, PROJECT_DATA_STORES } from '@/features/dataStore/constants';
type Props = { type Props = {
modalName: string; modalName: string;
@@ -16,6 +17,7 @@ const dataStoreStore = useDataStoreStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const i18n = useI18n(); const i18n = useI18n();
const toast = useToast(); const toast = useToast();
@@ -31,18 +33,35 @@ onMounted(() => {
const onSubmit = async () => { const onSubmit = async () => {
try { try {
await dataStoreStore.createDataStore(dataStoreName.value, route.params.projectId as string); const newDataStore = await dataStoreStore.createDataStore(
} catch (error) { dataStoreName.value,
toast.showError(error, i18n.baseText('dataStore.add.error')); route.params.projectId as string,
} finally { );
void router.push({
name: DATA_STORE_DETAILS,
params: {
id: newDataStore.id,
},
});
dataStoreName.value = ''; dataStoreName.value = '';
uiStore.closeModal(props.modalName); uiStore.closeModal(props.modalName);
} catch (error) {
toast.showError(error, i18n.baseText('dataStore.add.error'));
} }
}; };
const onCancel = () => {
uiStore.closeModal(props.modalName);
redirectToDataStores();
};
const redirectToDataStores = () => {
void router.replace({ name: PROJECT_DATA_STORES });
};
</script> </script>
<template> <template>
<Modal :name="props.modalName" :center="true" width="540px"> <Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataStores">
<template #header> <template #header>
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2> <h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
</template> </template>
@@ -66,7 +85,7 @@ const onSubmit = async () => {
</n8n-input-label> </n8n-input-label>
</div> </div>
</template> </template>
<template #footer="{ close }"> <template #footer>
<div :class="$style.footer"> <div :class="$style.footer">
<n8n-button <n8n-button
:disabled="!dataStoreName" :disabled="!dataStoreName"
@@ -78,7 +97,7 @@ const onSubmit = async () => {
type="secondary" type="secondary"
:label="i18n.baseText('generic.cancel')" :label="i18n.baseText('generic.cancel')"
data-test-id="cancel-add-data-store-button" data-test-id="cancel-add-data-store-button"
@click="close" @click="onCancel"
/> />
</div> </div>
</template> </template>

View File

@@ -96,7 +96,7 @@ describe('DataStoreBreadcrumbs', () => {
}); });
expect(getByText('Data Stores')).toBeInTheDocument(); expect(getByText('Data Stores')).toBeInTheDocument();
const separators = getAllByText(''); const separators = getAllByText('/');
expect(separators.length).toBeGreaterThan(0); expect(separators.length).toBeGreaterThan(0);
}); });
@@ -224,7 +224,7 @@ describe('DataStoreBreadcrumbs', () => {
}), }),
}); });
const separators = getAllByText(''); const separators = getAllByText('/');
expect(separators.length).toBeGreaterThan(0); expect(separators.length).toBeGreaterThan(0);
}); });
}); });

View File

@@ -9,7 +9,7 @@ import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
const BREADCRUMBS_SEPARATOR = ''; const BREADCRUMBS_SEPARATOR = '/';
type Props = { type Props = {
dataStore: DataStore; dataStore: DataStore;

View File

@@ -82,7 +82,7 @@ const emit = defineEmits<{
const i18n = useI18n(); const i18n = useI18n();
const toast = useToast(); const toast = useToast();
const message = useMessage(); const message = useMessage();
const dataStoreTypes = useDataStoreTypes(); const { getDefaultValueForType, mapToAGCellType } = useDataStoreTypes();
const dataStoreStore = useDataStoreStore(); const dataStoreStore = useDataStoreStore();
@@ -142,22 +142,6 @@ const setPageSize = async (size: number) => {
await fetchDataStoreContent(); await fetchDataStoreContent();
}; };
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
try {
const newColumn = await dataStoreStore.addDataStoreColumn(
props.dataStore.id,
props.dataStore.projectId,
column,
);
if (!newColumn) {
throw new Error(i18n.baseText('generic.unknownError'));
}
colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
} catch (error) {
toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
}
};
const onDeleteColumn = async (columnId: string) => { const onDeleteColumn = async (columnId: string) => {
if (!gridApi.value) return; if (!gridApi.value) return;
@@ -183,7 +167,7 @@ const onDeleteColumn = async (columnId: string) => {
colDefs.value = colDefs.value.filter((def) => def.colId !== columnId); colDefs.value = colDefs.value.filter((def) => def.colId !== columnId);
const rowDataOldValue = [...rowData.value]; const rowDataOldValue = [...rowData.value];
rowData.value = rowData.value.map((row) => { rowData.value = rowData.value.map((row) => {
const { [columnToDelete.field ?? '']: _, ...rest } = row; const { [columnToDelete.field!]: _, ...rest } = row;
return rest; return rest;
}); });
refreshGridData(); refreshGridData();
@@ -201,7 +185,26 @@ const onDeleteColumn = async (columnId: string) => {
} }
}; };
// TODO: Split this up to create column def based on type const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
try {
const newColumn = await dataStoreStore.addDataStoreColumn(
props.dataStore.id,
props.dataStore.projectId,
column,
);
if (!newColumn) {
throw new Error(i18n.baseText('generic.unknownError'));
}
colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
rowData.value = rowData.value.map((row) => {
return { ...row, [newColumn.name]: getDefaultValueForType(newColumn.type) };
});
refreshGridData();
} catch (error) {
toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
}
};
const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {}) => { const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {}) => {
const columnDef: ColDef = { const columnDef: ColDef = {
colId: col.id, colId: col.id,
@@ -209,11 +212,11 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
headerName: col.name, headerName: col.name,
editable: true, editable: true,
resizable: true, resizable: true,
lockPinned: true,
headerComponent: ColumnHeader, headerComponent: ColumnHeader,
cellEditorPopup: false, cellEditorPopup: false,
headerComponentParams: { onDelete: onDeleteColumn }, headerComponentParams: { onDelete: onDeleteColumn },
...extraProps, cellDataType: mapToAGCellType(col.type),
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
valueGetter: (params: ValueGetterParams<DataStoreRow>) => { valueGetter: (params: ValueGetterParams<DataStoreRow>) => {
// If the value is null, return null to show empty cell // If the value is null, return null to show empty cell
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) { if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
@@ -284,7 +287,10 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
columnDef.cellEditor = 'agDateCellEditor'; columnDef.cellEditor = 'agDateCellEditor';
columnDef.cellEditorPopup = true; columnDef.cellEditorPopup = true;
} }
return columnDef; return {
...columnDef,
...extraProps,
};
}; };
const onColumnMoved = async (moveEvent: ColumnMovedEvent) => { const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
@@ -555,17 +561,19 @@ const onCellEditingStopped = (params: CellEditingStoppedEvent<DataStoreRow>) =>
--ag-font-family: var(--font-family); --ag-font-family: var(--font-family);
--ag-font-size: var(--font-size-xs); --ag-font-size: var(--font-size-xs);
--ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px); --ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px);
--ag-header-background-color: var(--color-background-base); --ag-header-background-color: var(--color-background-light-base);
--ag-header-font-size: var(--font-size-xs); --ag-header-font-size: var(--font-size-xs);
--ag-header-font-weight: var(--font-weight-bold); --ag-header-font-weight: var(--font-weight-bold);
--ag-header-foreground-color: var(--color-text-dark); --ag-header-foreground-color: var(--color-text-dark);
--ag-cell-horizontal-padding: var(--spacing-2xs); --ag-cell-horizontal-padding: var(--spacing-2xs);
--ag-header-column-resize-handle-color: var(--border-color-base); --ag-header-column-resize-handle-color: var(--color-foreground-base);
--ag-header-column-resize-handle-height: 100%; --ag-header-column-resize-handle-height: 100%;
--ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px); --ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px);
:global(.ag-header-cell-resize) { :global(.ag-header-cell-resize) {
width: var(--spacing-4xs); width: var(--spacing-xs);
// this is needed so that we compensate for the width
right: -7px;
} }
// Don't show borders for the checkbox cells // Don't show borders for the checkbox cells

View File

@@ -40,7 +40,7 @@ export const DataStoreModule: FrontendModuleDescription = {
}, },
{ {
name: PROJECT_DATA_STORES, name: PROJECT_DATA_STORES,
path: 'datatables', path: 'datatables/:new(new)?',
props: true, props: true,
components: { components: {
default: DataStoreView, default: DataStoreView,

View File

@@ -98,7 +98,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const activeModules = computed(() => settings.value.activeModules); const activeModules = computed(() => settings.value.activeModules);
const isModuleActive = (moduleName: string) => { const isModuleActive = (moduleName: string) => {
return activeModules.value.includes(moduleName); return activeModules.value?.includes(moduleName);
}; };
const partialExecutionVersion = computed<1 | 2>(() => { const partialExecutionVersion = computed<1 | 2>(() => {
@@ -144,6 +144,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const isFoldersFeatureEnabled = computed(() => folders.value.enabled); const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-store'));
const areTagsEnabled = computed(() => const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true, settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
); );
@@ -396,5 +398,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isMFAEnforced, isMFAEnforced,
activeModules, activeModules,
isModuleActive, isModuleActive,
isDataStoreFeatureEnabled,
}; };
}); });