mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Update data store front-end (no-changelog) (#18000)
This commit is contained in:
committed by
GitHub
parent
acfb79bd97
commit
d6ee6067cf
@@ -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>",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user