feat(editor): Update data store front-end (no-changelog) (#18000)

This commit is contained in:
Milorad FIlipović
2025-08-05 22:47:57 +02:00
committed by GitHub
parent acfb79bd97
commit d6ee6067cf
10 changed files with 301 additions and 70 deletions

View File

@@ -105,6 +105,7 @@
"generic.rename": "Rename", "generic.rename": "Rename",
"generic.missing.permissions": "Missing permissions to perform this action", "generic.missing.permissions": "Missing permissions to perform this action",
"generic.shortcutHint": "Or press", "generic.shortcutHint": "Or press",
"generic.unknownError": "An unknown error occurred",
"generic.upgradeToEnterprise": "Upgrade to Enterprise", "generic.upgradeToEnterprise": "Upgrade to Enterprise",
"generic.never": "Never", "generic.never": "Never",
"about.aboutN8n": "About n8n", "about.aboutN8n": "About n8n",
@@ -2817,10 +2818,14 @@
"dataStore.error.fetching": "Error loading data stores", "dataStore.error.fetching": "Error loading data stores",
"dataStore.add.title": "Create data store", "dataStore.add.title": "Create data store",
"dataStore.add.description": "Set up a new data store to organize and manage your data.", "dataStore.add.description": "Set up a new data store to organize and manage your data.",
"dataStore.add.button.label": "Create data store", "dataStore.add.button.label": "Create Data Store",
"dataStore.add.input.name.label": "Data Store Name", "dataStore.add.input.name.label": "Data Store Name",
"dataStore.add.input.name.placeholder": "Enter data store name", "dataStore.add.input.name.placeholder": "Enter data store name",
"dataStore.add.error": "Error creating data store", "dataStore.add.error": "Error creating data store",
"dataStore.delete.confirm.title": "Delete data store",
"dataStore.delete.confirm.message": "Are you sure you want to delete the data store \"{name}\"? This action cannot be undone.",
"dataStore.delete.error": "Error deleting data store",
"dataStore.rename.error": "Error renaming data store",
"settings.ldap": "LDAP", "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.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>", "settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",

View File

@@ -21,6 +21,12 @@ 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';
export type CustomAction = {
id: string;
label: string;
disabled?: boolean;
};
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const i18n = useI18n(); const i18n = useI18n();
@@ -31,8 +37,17 @@ 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 => {
@@ -139,6 +154,17 @@ const menu = computed(() => {
!getResourcePermissions(homeProject.value?.scopes).folder.create, !getResourcePermissions(homeProject.value?.scopes).folder.create,
}); });
} }
// Append custom actions
if (props.customActions?.length) {
props.customActions.forEach((customAction) => {
items.push({
value: customAction.id,
label: customAction.label,
disabled: customAction.disabled ?? false,
});
});
}
return items; return items;
}); });
@@ -240,6 +266,13 @@ const onSelect = (action: string) => {
if (!homeProject.value) { if (!homeProject.value) {
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

@@ -100,9 +100,9 @@ const initialState = {
const TEST_DATA_STORE: DataStoreResource = { const TEST_DATA_STORE: DataStoreResource = {
id: '1', id: '1',
name: 'Test Data Store', name: 'Test Data Store',
size: 1024, sizeBytes: 1024,
recordCount: 100, recordCount: 100,
columnCount: 5, columns: [],
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
resourceType: 'datastore', resourceType: 'datastore',
@@ -123,7 +123,7 @@ describe('DataStoreView', () => {
// Mock dataStore store state // Mock dataStore store state
dataStoreStore.dataStores = [TEST_DATA_STORE]; dataStoreStore.dataStores = [TEST_DATA_STORE];
dataStoreStore.totalCount = 1; dataStoreStore.totalCount = 1;
dataStoreStore.loadDataStores = vi.fn().mockResolvedValue(undefined); dataStoreStore.fetchDataStores = vi.fn().mockResolvedValue(undefined);
projectsStore.getCurrentProjectId = vi.fn(() => 'test-project'); projectsStore.getCurrentProjectId = vi.fn(() => 'test-project');
sourceControlStore.isProjectShared = vi.fn(() => false); sourceControlStore.isProjectShared = vi.fn(() => false);
@@ -134,7 +134,7 @@ describe('DataStoreView', () => {
const { getByTestId } = renderComponent({ pinia }); const { getByTestId } = renderComponent({ pinia });
await waitAllPromises(); await waitAllPromises();
expect(dataStoreStore.loadDataStores).toHaveBeenCalledWith('test-project', 1, 25); expect(dataStoreStore.fetchDataStores).toHaveBeenCalledWith('test-project', 1, 25);
expect(getByTestId('resources-list-wrapper')).toBeInTheDocument(); expect(getByTestId('resources-list-wrapper')).toBeInTheDocument();
}); });
@@ -147,7 +147,7 @@ describe('DataStoreView', () => {
it('should handle initialization error', async () => { it('should handle initialization error', async () => {
const error = new Error('Store Error'); const error = new Error('Store Error');
dataStoreStore.loadDataStores = vi.fn().mockRejectedValue(error); dataStoreStore.fetchDataStores = vi.fn().mockRejectedValue(error);
renderComponent({ pinia }); renderComponent({ pinia });
await waitAllPromises(); await waitAllPromises();
@@ -201,7 +201,7 @@ describe('DataStoreView', () => {
await waitAllPromises(); await waitAllPromises();
// Clear the initial call // Clear the initial call
dataStoreStore.loadDataStores = vi.fn().mockClear(); dataStoreStore.fetchDataStores = vi.fn().mockClear();
mockDebounce.callDebounced.mockClear(); mockDebounce.callDebounced.mockClear();
// The component should be rendered and ready to handle pagination // The component should be rendered and ready to handle pagination
@@ -223,7 +223,7 @@ describe('DataStoreView', () => {
await waitAllPromises(); await waitAllPromises();
// Initial call should use default page size of 25 // Initial call should use default page size of 25
expect(dataStoreStore.loadDataStores).toHaveBeenCalledWith('test-project', 1, 25); expect(dataStoreStore.fetchDataStores).toHaveBeenCalledWith('test-project', 1, 25);
}); });
}); });
}); });

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import ProjectHeader from '@/components/Projects/ProjectHeader.vue'; import ProjectHeader, { type CustomAction } from '@/components/Projects/ProjectHeader.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.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';
@@ -24,6 +24,8 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useDataStoreStore } from '@/features/dataStore/dataStore.store'; import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
import { useMessage } from '@/composables/useMessage';
import { MODAL_CONFIRM } from '@/constants';
const i18n = useI18n(); const i18n = useI18n();
const route = useRoute(); const route = useRoute();
@@ -31,6 +33,7 @@ const projectPages = useProjectPages();
const { callDebounced } = useDebounce(); const { callDebounced } = useDebounce();
const documentTitle = useDocumentTitle(); const documentTitle = useDocumentTitle();
const toast = useToast(); const toast = useToast();
const message = useMessage();
const dataStoreStore = useDataStoreStore(); const dataStoreStore = useDataStoreStore();
const insightsStore = useInsightsStore(); const insightsStore = useInsightsStore();
@@ -42,6 +45,14 @@ 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 {
@@ -88,11 +99,6 @@ const cardActions = computed<Array<UserAction<IUser>>>(() => [
value: DATA_STORE_CARD_ACTIONS.DELETE, value: DATA_STORE_CARD_ACTIONS.DELETE,
disabled: readOnlyEnv.value, disabled: readOnlyEnv.value,
}, },
{
label: i18n.baseText('generic.clear'),
value: DATA_STORE_CARD_ACTIONS.CLEAR,
disabled: readOnlyEnv.value,
},
]); ]);
const initialize = async () => { const initialize = async () => {
@@ -101,7 +107,7 @@ const initialize = async () => {
? route.params.projectId[0] ? route.params.projectId[0]
: route.params.projectId; : route.params.projectId;
try { try {
await dataStoreStore.loadDataStores(projectId, currentPage.value, pageSize.value); await dataStoreStore.fetchDataStores(projectId, currentPage.value, pageSize.value);
} catch (error) { } catch (error) {
toast.showError(error, 'Error loading data stores'); toast.showError(error, 'Error loading data stores');
} finally { } finally {
@@ -125,6 +131,64 @@ const onAddModalClick = () => {
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY); useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY);
}; };
const onCardAction = async (payload: { action: string; dataStore: DataStoreResource }) => {
switch (payload.action) {
case DATA_STORE_CARD_ACTIONS.DELETE: {
const promptResponse = await message.confirm(
i18n.baseText('dataStore.delete.confirm.message', {
interpolate: { name: payload.dataStore.name },
}),
i18n.baseText('dataStore.delete.confirm.title'),
{
confirmButtonText: i18n.baseText('generic.delete'),
cancelButtonText: i18n.baseText('generic.cancel'),
},
);
if (promptResponse === MODAL_CONFIRM) {
try {
const deleted = await dataStoreStore.deleteDataStore(
payload.dataStore.id,
payload.dataStore.projectId,
);
if (!deleted) {
toast.showError(
new Error(i18n.baseText('generic.unknownError')),
i18n.baseText('dataStore.delete.error'),
);
}
} catch (error) {
toast.showError(error, i18n.baseText('dataStore.delete.error'));
}
}
break;
}
case DATA_STORE_CARD_ACTIONS.RENAME: {
try {
const updated = await dataStoreStore.updateDataStore(
payload.dataStore.id,
payload.dataStore.name,
payload.dataStore.projectId,
);
if (!updated) {
toast.showError(
new Error(i18n.baseText('generic.unknownError')),
i18n.baseText('dataStore.rename.error'),
);
}
} catch (error) {
toast.showError(error, i18n.baseText('dataStore.rename.error'));
}
break;
}
}
};
const onProjectHeaderAction = (action: string) => {
if (action === 'add-data-store') {
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY);
}
};
onMounted(() => { onMounted(() => {
documentTitle.set(i18n.baseText('dataStore.tab.label')); documentTitle.set(i18n.baseText('dataStore.tab.label'));
}); });
@@ -149,7 +213,10 @@ 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"
@@ -165,7 +232,7 @@ onMounted(() => {
:description="emptyCalloutDescription" :description="emptyCalloutDescription"
:button-text="emptyCalloutButtonText" :button-text="emptyCalloutButtonText"
button-type="secondary" button-type="secondary"
@click="onAddModalClick" @click:button="onAddModalClick"
/> />
</template> </template>
<template #item="{ item: data }"> <template #item="{ item: data }">
@@ -175,6 +242,7 @@ onMounted(() => {
:show-ownership-badge="projectPages.isOverviewSubPage" :show-ownership-badge="projectPages.isOverviewSubPage"
:actions="cardActions" :actions="cardActions"
:read-only="readOnlyEnv" :read-only="readOnlyEnv"
@action="onCardAction"
/> />
</template> </template>
</ResourcesListLayout> </ResourcesListLayout>

View File

@@ -31,7 +31,7 @@ onMounted(() => {
const onSubmit = async () => { const onSubmit = async () => {
try { try {
await dataStoreStore.createNewDataStore(dataStoreName.value, route.params.projectId as string); await dataStoreStore.createDataStore(dataStoreName.value, route.params.projectId as string);
} catch (error) { } catch (error) {
toast.showError(error, i18n.baseText('dataStore.add.error')); toast.showError(error, i18n.baseText('dataStore.add.error'));
} finally { } finally {

View File

@@ -26,9 +26,9 @@ vi.mock('vue-router', () => {
const DEFAULT_DATA_STORE: DataStoreResource = { const DEFAULT_DATA_STORE: DataStoreResource = {
id: '1', id: '1',
name: 'Test Data Store', name: 'Test Data Store',
size: 1024, sizeBytes: 1024,
recordCount: 100, recordCount: 100,
columnCount: 5, columns: [],
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
resourceType: 'datastore', resourceType: 'datastore',
@@ -72,7 +72,7 @@ describe('DataStoreCard', () => {
it('should render data store info correctly', () => { it('should render data store info correctly', () => {
const { getByTestId } = renderComponent(); const { getByTestId } = renderComponent();
expect(getByTestId('data-store-card-icon')).toBeInTheDocument(); expect(getByTestId('data-store-card-icon')).toBeInTheDocument();
expect(getByTestId('folder-card-name')).toHaveTextContent(DEFAULT_DATA_STORE.name); expect(getByTestId('datastore-name-input')).toHaveTextContent(DEFAULT_DATA_STORE.name);
expect(getByTestId('data-store-card-record-count')).toBeInTheDocument(); expect(getByTestId('data-store-card-record-count')).toBeInTheDocument();
expect(getByTestId('data-store-card-column-count')).toBeInTheDocument(); expect(getByTestId('data-store-card-column-count')).toBeInTheDocument();
expect(getByTestId('data-store-card-last-updated')).toHaveTextContent('Last updated'); expect(getByTestId('data-store-card-last-updated')).toHaveTextContent('Last updated');
@@ -99,12 +99,12 @@ describe('DataStoreCard', () => {
actions: [], actions: [],
}, },
}); });
expect(queryByTestId('folder-card-actions')).not.toBeInTheDocument(); expect(queryByTestId('data-store-card-actions')).not.toBeInTheDocument();
}); });
it('should render action dropdown if actions are provided', () => { it('should render action dropdown if actions are provided', () => {
const { getByTestId } = renderComponent(); const { getByTestId } = renderComponent();
expect(getByTestId('folder-card-actions')).toBeInTheDocument(); expect(getByTestId('data-store-card-actions')).toBeInTheDocument();
}); });
it('should render correct route to data store details', () => { it('should render correct route to data store details', () => {
@@ -113,12 +113,6 @@ describe('DataStoreCard', () => {
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
}); });
it('should display size information', () => {
const { getByTestId } = renderComponent();
const sizeElement = getByTestId('folder-card-folder-count');
expect(sizeElement).toBeInTheDocument();
});
it('should display record count information', () => { it('should display record count information', () => {
const { getByTestId } = renderComponent(); const { getByTestId } = renderComponent();
const recordCountElement = getByTestId('data-store-card-record-count'); const recordCountElement = getByTestId('data-store-card-record-count');

View File

@@ -3,7 +3,7 @@ import type { IUser, UserAction } from '@/Interface';
import type { DataStoreResource } from '@/features/dataStore/types'; import type { DataStoreResource } from '@/features/dataStore/types';
import { DATA_STORE_DETAILS } from '../constants'; import { DATA_STORE_DETAILS } from '../constants';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { computed } from 'vue'; import { computed, useTemplateRef } from 'vue';
type Props = { type Props = {
dataStore: DataStoreResource; dataStore: DataStoreResource;
@@ -20,6 +20,17 @@ const props = withDefaults(defineProps<Props>(), {
showOwnershipBadge: false, showOwnershipBadge: false,
}); });
const emit = defineEmits<{
action: [
value: {
dataStore: DataStoreResource;
action: string;
},
];
}>();
const renameInput = useTemplateRef('renameInput');
const dataStoreRoute = computed(() => { const dataStoreRoute = computed(() => {
return { return {
name: DATA_STORE_DETAILS, name: DATA_STORE_DETAILS,
@@ -29,6 +40,33 @@ const dataStoreRoute = computed(() => {
}, },
}; };
}); });
const onCardAction = (action: string) => {
// Focus rename input if the action is rename
// We need this timeout to ensure action toggle is closed before focusing
if (action === 'rename') {
if (renameInput.value?.forceFocus) {
setTimeout(() => {
renameInput.value?.forceFocus();
}, 100);
}
return;
}
// Otherwise, emit the action directly
emit('action', {
dataStore: props.dataStore,
action,
});
};
const onNameSubmit = (name: string) => {
if (props.dataStore.name === name) return;
emit('action', {
dataStore: { ...props.dataStore, name },
action: 'rename',
});
};
</script> </script>
<template> <template>
<div data-test-id="data-store-card"> <div data-test-id="data-store-card">
@@ -44,10 +82,18 @@ const dataStoreRoute = computed(() => {
/> />
</template> </template>
<template #header> <template #header>
<div :class="$style['card-header']"> <div :class="$style['card-header']" @click.prevent>
<N8nHeading tag="h2" bold size="small" data-test-id="folder-card-name"> <N8nInlineTextEdit
{{ props.dataStore.name }} ref="renameInput"
</N8nHeading> data-test-id="datastore-name-input"
:placeholder="i18n.baseText('dataStore.add.input.name.label')"
:class="$style['card-name']"
:model-value="props.dataStore.name"
:max-length="50"
:read-only="props.readOnly"
:disabled="props.readOnly"
@update:model-value="onNameSubmit"
/>
<N8nBadge v-if="props.readOnly" class="ml-3xs" theme="tertiary" bold> <N8nBadge v-if="props.readOnly" class="ml-3xs" theme="tertiary" bold>
{{ i18n.baseText('workflows.item.readonly') }} {{ i18n.baseText('workflows.item.readonly') }}
</N8nBadge> </N8nBadge>
@@ -55,14 +101,6 @@ const dataStoreRoute = computed(() => {
</template> </template>
<template #footer> <template #footer>
<div :class="$style['card-footer']"> <div :class="$style['card-footer']">
<N8nText
size="small"
color="text-light"
:class="[$style['info-cell'], $style['info-cell--size']]"
data-test-id="folder-card-folder-count"
>
{{ i18n.baseText('dataStore.card.size', { interpolate: { size: dataStore.size } }) }}
</N8nText>
<N8nText <N8nText
size="small" size="small"
color="text-light" color="text-light"
@@ -71,7 +109,7 @@ const dataStoreRoute = computed(() => {
> >
{{ {{
i18n.baseText('dataStore.card.row.count', { i18n.baseText('dataStore.card.row.count', {
interpolate: { count: props.dataStore.recordCount }, interpolate: { count: props.dataStore.recordCount ?? 0 },
}) })
}} }}
</N8nText> </N8nText>
@@ -83,7 +121,7 @@ const dataStoreRoute = computed(() => {
> >
{{ {{
i18n.baseText('dataStore.card.column.count', { i18n.baseText('dataStore.card.column.count', {
interpolate: { count: props.dataStore.columnCount }, interpolate: { count: props.dataStore.columns.length },
}) })
}} }}
</N8nText> </N8nText>
@@ -113,7 +151,8 @@ const dataStoreRoute = computed(() => {
v-if="props.actions.length" v-if="props.actions.length"
:actions="props.actions" :actions="props.actions"
theme="dark" theme="dark"
data-test-id="folder-card-actions" data-test-id="data-store-card-actions"
@action="onCardAction"
/> />
</div> </div>
</template> </template>
@@ -132,6 +171,12 @@ const dataStoreRoute = computed(() => {
} }
} }
.card-name {
color: $custom-font-dark;
font-size: var(--font-size-m);
margin-bottom: var(--spacing-5xs);
}
.card-icon { .card-icon {
flex-shrink: 0; flex-shrink: 0;
color: var(--color-text-base); color: var(--color-text-base);

View File

@@ -2,7 +2,12 @@ import { defineStore } from 'pinia';
import { DATA_STORE_STORE } from '@/features/dataStore/constants'; import { DATA_STORE_STORE } from '@/features/dataStore/constants';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { fetchDataStores, createDataStore } from '@/features/dataStore/datastore.api'; import {
fetchDataStoresApi,
createDataStoreApi,
deleteDataStoreApi,
updateDataStoreApi,
} from '@/features/dataStore/datastore.api';
import type { DataStoreEntity } from '@/features/dataStore/datastore.types'; import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => { export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
@@ -11,26 +16,55 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
const dataStores = ref<DataStoreEntity[]>([]); const dataStores = ref<DataStoreEntity[]>([]);
const totalCount = ref(0); const totalCount = ref(0);
const loadDataStores = async (projectId: string, page: number, pageSize: number) => { const fetchDataStores = async (projectId: string, page: number, pageSize: number) => {
const response = await fetchDataStores(rootStore.restApiContext, projectId, { const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, {
page, skip: (page - 1) * pageSize,
pageSize, take: pageSize,
}); });
console.log('Data stores fetched:', response);
dataStores.value = response.data; dataStores.value = response.data;
totalCount.value = response.count; totalCount.value = response.count;
}; };
const createNewDataStore = async (name: string, projectId?: string) => { const createDataStore = async (name: string, projectId?: string) => {
const newStore = await createDataStore(rootStore.restApiContext, name, projectId); const newStore = await createDataStoreApi(rootStore.restApiContext, name, projectId);
dataStores.value.push(newStore.data); dataStores.value.push(newStore);
totalCount.value += 1; totalCount.value += 1;
return newStore; 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 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;
};
return { return {
dataStores, dataStores,
totalCount, totalCount,
loadDataStores, fetchDataStores,
createNewDataStore, createDataStore,
deleteDataStore,
updateDataStore,
}; };
}); });

View File

@@ -1,29 +1,71 @@
import { getFullApiResponse } from '@n8n/rest-api-client'; import { makeRestApiRequest } from '@n8n/rest-api-client';
import type { IRestApiContext } from '@n8n/rest-api-client'; import type { IRestApiContext } from '@n8n/rest-api-client';
import { type DataStoreEntity } from '@/features/dataStore/datastore.types'; import { type DataStoreEntity } from '@/features/dataStore/datastore.types';
export const fetchDataStores = async ( export const fetchDataStoresApi = async (
context: IRestApiContext, context: IRestApiContext,
projectId?: string, projectId?: string,
options?: { options?: {
page?: number; skip?: number;
pageSize?: number; take?: number;
}, },
) => { ) => {
return await getFullApiResponse<DataStoreEntity[]>(context, 'GET', '/data-stores', { const apiEndpoint = projectId ? `/projects/${projectId}/data-stores` : '/data-stores-global';
projectId, return await makeRestApiRequest<{ count: number; data: DataStoreEntity[] }>(
options, context,
}); 'GET',
apiEndpoint,
{
...options,
},
);
}; };
export const createDataStore = async ( export const createDataStoreApi = async (
context: IRestApiContext, context: IRestApiContext,
name: string, name: string,
projectId?: string, projectId?: string,
) => { ) => {
return await getFullApiResponse<DataStoreEntity>(context, 'POST', '/data-stores', { return await makeRestApiRequest<DataStoreEntity>(
name, context,
projectId, 'POST',
}); `/projects/${projectId}/data-stores`,
{
name,
columns: [],
},
);
};
export const deleteDataStoreApi = async (
context: IRestApiContext,
dataStoreId: string,
projectId?: string,
) => {
return await makeRestApiRequest<boolean>(
context,
'DELETE',
`/projects/${projectId}/data-stores/${dataStoreId}`,
{
dataStoreId,
projectId,
},
);
};
export const updateDataStoreApi = async (
context: IRestApiContext,
dataStoreId: string,
name: string,
projectId?: string,
) => {
return await makeRestApiRequest<DataStoreEntity>(
context,
'PATCH',
`/projects/${projectId}/data-stores/${dataStoreId}`,
{
name,
},
);
}; };

View File

@@ -1,10 +1,20 @@
import type { ProjectSharingData } from 'n8n-workflow';
export type DataStoreEntity = { export type DataStoreEntity = {
id: string; id: string;
name: string; name: string;
size: number; sizeBytes: number;
recordCount: number; recordCount: number;
columnCount: number; columns: DataStoreColumnEntity[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
projectId?: string; projectId?: string;
project?: ProjectSharingData;
};
export type DataStoreColumnEntity = {
id: string;
name: string;
type: string;
index: number;
}; };