mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Add initial data-store details page (no-changelog) (#18211)
This commit is contained in:
committed by
GitHub
parent
14787fd5a4
commit
ecc4f41a11
@@ -2829,7 +2829,7 @@
|
||||
"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.tab.label": "Data Store",
|
||||
"dataStore.dataStores": "Data Stores",
|
||||
"dataStore.empty.label": "You don't have any data stores yet",
|
||||
"dataStore.empty.description": "Once you create data stores for your projects, they will appear here",
|
||||
"dataStore.empty.button.label": "Create data store in \"{projectName}\"",
|
||||
@@ -2852,6 +2852,11 @@
|
||||
"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",
|
||||
"dataStore.getDetails.error": "Error fetching data store details",
|
||||
"dataStore.notFound": "Data store not found",
|
||||
"dataStore.noColumns.heading": "No columns yet",
|
||||
"dataStore.noColumns.description": "Add columns to start storing data in this data store.",
|
||||
"dataStore.noColumns.button.label": "Add first column",
|
||||
"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>",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { DATA_STORE_VIEW } from '@/features/dataStore/constants';
|
||||
import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBreadcrumbs.vue';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const documentTitle = useDocumentTitle();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataStore = ref<DataStoreEntity | null>(null);
|
||||
|
||||
const showErrorAndGoBackToList = async (error: unknown) => {
|
||||
if (!(error instanceof Error)) {
|
||||
error = new Error(String(i18n.baseText('dataStore.getDetails.error')));
|
||||
}
|
||||
toast.showError(error, i18n.baseText('dataStore.getDetails.error'));
|
||||
await router.push({ name: DATA_STORE_VIEW, params: { projectId: props.projectId } });
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await dataStoreStore.fetchOrFindDataStore(props.id, props.projectId);
|
||||
if (response) {
|
||||
dataStore.value = response;
|
||||
} else {
|
||||
await showErrorAndGoBackToList(new Error(i18n.baseText('dataStore.notFound')));
|
||||
}
|
||||
} catch (error) {
|
||||
await showErrorAndGoBackToList(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onAddColumnClick = () => {
|
||||
toast.showMessage({
|
||||
type: 'warning',
|
||||
message: 'Coming soon',
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||
await initialize();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style['data-store-details-view']">
|
||||
<div v-if="loading" class="loading">
|
||||
<n8n-loading
|
||||
variant="h1"
|
||||
:loading="true"
|
||||
:rows="1"
|
||||
:shrink-last="false"
|
||||
:class="$style['header-loading']"
|
||||
/>
|
||||
<n8n-loading :loading="true" variant="h1" :rows="10" :shrink-last="false" />
|
||||
</div>
|
||||
<div v-else-if="dataStore">
|
||||
<div :class="$style.header">
|
||||
<DataStoreBreadcrumbs :data-store="dataStore" />
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<n8n-action-box
|
||||
v-if="dataStore.columns.length === 0"
|
||||
data-test-id="empty-shared-action-box"
|
||||
:heading="i18n.baseText('dataStore.noColumns.heading')"
|
||||
:description="i18n.baseText('dataStore.noColumns.description')"
|
||||
:button-text="i18n.baseText('dataStore.noColumns.button.label')"
|
||||
button-type="secondary"
|
||||
@click:button="onAddColumnClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.data-store-details-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
box-sizing: border-box;
|
||||
align-content: start;
|
||||
padding: var(--spacing-l) var(--spacing-2xl) 0;
|
||||
}
|
||||
|
||||
.header-loading {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
div {
|
||||
height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ vi.mock('@n8n/i18n', async (importOriginal) => {
|
||||
...actualObj,
|
||||
useI18n: vi.fn(() => ({
|
||||
baseText: vi.fn((key: string) => {
|
||||
if (key === 'dataStore.tab.label') return 'Data Store';
|
||||
if (key === 'dataStore.dataStores') return 'Data Stores';
|
||||
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';
|
||||
@@ -142,7 +142,7 @@ describe('DataStoreView', () => {
|
||||
renderComponent({ pinia });
|
||||
await waitAllPromises();
|
||||
|
||||
expect(mockDocumentTitle.set).toHaveBeenCalledWith('Data Store');
|
||||
expect(mockDocumentTitle.set).toHaveBeenCalledWith('Data Stores');
|
||||
});
|
||||
|
||||
it('should handle initialization error', async () => {
|
||||
|
||||
@@ -10,14 +10,12 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { SortingAndPaginationUpdates, UserAction } from '@/Interface';
|
||||
import type { IUser } from '@n8n/rest-api-client/api/users';
|
||||
import type { SortingAndPaginationUpdates } from '@/Interface';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import DataStoreCard from '@/features/dataStore/components/DataStoreCard.vue';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import {
|
||||
ADD_DATA_STORE_MODAL_KEY,
|
||||
DATA_STORE_CARD_ACTIONS,
|
||||
DEFAULT_DATA_STORE_PAGE_SIZE,
|
||||
} from '@/features/dataStore/constants';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
@@ -25,8 +23,6 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
@@ -34,7 +30,6 @@ const projectPages = useProjectPages();
|
||||
const { callDebounced } = useDebounce();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
const insightsStore = useInsightsStore();
|
||||
@@ -89,19 +84,6 @@ const emptyCalloutButtonText = computed(() => {
|
||||
|
||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
|
||||
const cardActions = computed<Array<UserAction<IUser>>>(() => [
|
||||
{
|
||||
label: i18n.baseText('generic.rename'),
|
||||
value: DATA_STORE_CARD_ACTIONS.RENAME,
|
||||
disabled: readOnlyEnv.value,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('generic.delete'),
|
||||
value: DATA_STORE_CARD_ACTIONS.DELETE,
|
||||
disabled: readOnlyEnv.value,
|
||||
},
|
||||
]);
|
||||
|
||||
const initialize = async () => {
|
||||
loading.value = true;
|
||||
const projectId = Array.isArray(route.params.projectId)
|
||||
@@ -132,66 +114,32 @@ const onAddModalClick = () => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const onCardRename = async (payload: { dataStore: DataStoreResource }) => {
|
||||
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'));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
documentTitle.set(i18n.baseText('dataStore.tab.label'));
|
||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
@@ -241,9 +189,8 @@ onMounted(() => {
|
||||
class="mb-2xs"
|
||||
:data-store="data as DataStoreResource"
|
||||
:show-ownership-badge="projectPages.isOverviewSubPage"
|
||||
:actions="cardActions"
|
||||
:read-only="readOnlyEnv"
|
||||
@action="onCardAction"
|
||||
@rename="onCardRename"
|
||||
/>
|
||||
</template>
|
||||
</ResourcesListLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useDataStoreStore } from '../dataStore.store';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
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 { MODAL_CONFIRM } from '@/constants';
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
|
||||
const mockMessage = {
|
||||
confirm: vi.fn(),
|
||||
};
|
||||
|
||||
const mockToast = {
|
||||
showError: vi.fn(),
|
||||
};
|
||||
|
||||
const mockDeleteDataStore = vi.fn();
|
||||
|
||||
vi.mock('@/composables/useMessage', () => ({
|
||||
useMessage: () => mockMessage,
|
||||
}));
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => mockToast,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/dataStore.store', () => ({
|
||||
useDataStoreStore: () => ({
|
||||
deleteDataStore: mockDeleteDataStore,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: (key: string, options?: { interpolate?: { name?: string } }) => {
|
||||
if (key === 'generic.rename') return 'Rename';
|
||||
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')
|
||||
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.';
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDataStore: DataStoreEntity = {
|
||||
id: '1',
|
||||
name: 'Test DataStore',
|
||||
sizeBytes: 1024,
|
||||
recordCount: 100,
|
||||
columns: [],
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z',
|
||||
projectId: 'project-1',
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreActions, {
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
isReadOnly: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDeleteDataStore.mockResolvedValue(true);
|
||||
mockMessage.confirm.mockResolvedValue(MODAL_CONFIRM);
|
||||
});
|
||||
|
||||
it('should render N8nActionToggle with correct props', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const actionToggle = getByTestId('data-store-card-actions');
|
||||
expect(actionToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render actions when read-only', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
isReadOnly: true,
|
||||
},
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const actionToggle = getByTestId('data-store-card-actions');
|
||||
expect(actionToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should emit rename event when rename action is triggered', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the rename action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`));
|
||||
|
||||
expect(emitted().rename).toBeTruthy();
|
||||
expect(emitted().rename[0]).toEqual([
|
||||
{
|
||||
dataStore: mockDataStore,
|
||||
action: 'rename',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should show confirmation dialog when delete action is triggered', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockMessage.confirm).toHaveBeenCalledWith(
|
||||
'Are you sure that you want to delete "Test DataStore"?',
|
||||
'Delete Data Store',
|
||||
{
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonText: 'Cancel',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should call delete when confirmed and emit onDeleted', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockDeleteDataStore).toHaveBeenCalledWith('1', 'project-1');
|
||||
expect(emitted().onDeleted).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not delete when confirmation is cancelled', async () => {
|
||||
mockMessage.confirm.mockResolvedValue('cancel');
|
||||
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockDeleteDataStore).not.toHaveBeenCalled();
|
||||
expect(emitted().onDeleted).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should show error when delete fails', async () => {
|
||||
mockDeleteDataStore.mockResolvedValue(false);
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
'Something went wrong while deleting the data store.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error when delete throws exception', async () => {
|
||||
const deleteError = new Error('Delete failed');
|
||||
mockDeleteDataStore.mockRejectedValue(deleteError);
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Click on the action toggle to open dropdown
|
||||
await userEvent.click(getByTestId('data-store-card-actions'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
// Click on the delete action
|
||||
await userEvent.click(getByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`));
|
||||
|
||||
expect(mockToast.showError).toHaveBeenCalledWith(
|
||||
deleteError,
|
||||
'Something went wrong while deleting the data store.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
import type { IUser, UserAction } from '@n8n/design-system';
|
||||
import { DATA_STORE_CARD_ACTIONS } from '@/features/dataStore/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 { useToast } from '@/composables/useToast';
|
||||
|
||||
type Props = {
|
||||
dataStore: DataStoreEntity;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isReadOnly: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
rename: [
|
||||
value: {
|
||||
dataStore: DataStoreEntity;
|
||||
action: string;
|
||||
},
|
||||
];
|
||||
onDeleted: [];
|
||||
}>();
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
const message = useMessage();
|
||||
const toast = useToast();
|
||||
|
||||
const actions = computed<Array<UserAction<IUser>>>(() => [
|
||||
{
|
||||
label: i18n.baseText('generic.rename'),
|
||||
value: DATA_STORE_CARD_ACTIONS.RENAME,
|
||||
disabled: props.isReadOnly,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('generic.delete'),
|
||||
value: DATA_STORE_CARD_ACTIONS.DELETE,
|
||||
disabled: props.isReadOnly,
|
||||
},
|
||||
]);
|
||||
|
||||
const onAction = async (action: string) => {
|
||||
switch (action) {
|
||||
case DATA_STORE_CARD_ACTIONS.RENAME: {
|
||||
// This is handled outside of this component
|
||||
// where editable label component is used
|
||||
emit('rename', {
|
||||
dataStore: props.dataStore,
|
||||
action: 'rename',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case DATA_STORE_CARD_ACTIONS.DELETE: {
|
||||
const promptResponse = await message.confirm(
|
||||
i18n.baseText('dataStore.delete.confirm.message', {
|
||||
interpolate: { name: props.dataStore.name },
|
||||
}),
|
||||
i18n.baseText('dataStore.delete.confirm.title'),
|
||||
{
|
||||
confirmButtonText: i18n.baseText('generic.delete'),
|
||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||
},
|
||||
);
|
||||
if (promptResponse === MODAL_CONFIRM) {
|
||||
await deleteDataStore();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDataStore = async () => {
|
||||
try {
|
||||
const deleted = await dataStoreStore.deleteDataStore(
|
||||
props.dataStore.id,
|
||||
props.dataStore.projectId,
|
||||
);
|
||||
if (!deleted) {
|
||||
throw new Error(i18n.baseText('generic.unknownError'));
|
||||
}
|
||||
emit('onDeleted');
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('dataStore.delete.error'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<N8nActionToggle
|
||||
:actions="actions"
|
||||
theme="dark"
|
||||
data-test-id="data-store-card-actions"
|
||||
@action="onAction"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,231 @@
|
||||
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 { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
};
|
||||
|
||||
const mockToast = {
|
||||
showError: vi.fn(),
|
||||
};
|
||||
|
||||
const mockUpdateDataStore = vi.fn();
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...(actual as object),
|
||||
useRouter: () => mockRouter,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => mockToast,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/dataStore/dataStore.store', () => ({
|
||||
useDataStoreStore: () => ({
|
||||
updateDataStore: mockUpdateDataStore,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await 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.',
|
||||
'generic.unknownError': 'Something went wrong',
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDataStore: DataStoreEntity = {
|
||||
id: '1',
|
||||
name: 'Test DataStore',
|
||||
sizeBytes: 1024,
|
||||
recordCount: 100,
|
||||
columns: [],
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z',
|
||||
projectId: 'project-1',
|
||||
project: {
|
||||
id: 'project-1',
|
||||
name: 'Test Project',
|
||||
type: 'personal',
|
||||
icon: { type: 'icon', value: 'projects' },
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z',
|
||||
relations: [],
|
||||
scopes: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockDataStoreWithoutProject: DataStoreEntity = {
|
||||
...mockDataStore,
|
||||
project: undefined,
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(DataStoreBreadcrumbs, {
|
||||
props: {
|
||||
dataStore: mockDataStore,
|
||||
},
|
||||
});
|
||||
|
||||
describe('DataStoreBreadcrumbs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUpdateDataStore.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('Breadcrumbs rendering', () => {
|
||||
it('should render breadcrumbs with project data', () => {
|
||||
const { getByText, getAllByText } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByText('Data Stores')).toBeInTheDocument();
|
||||
const separators = getAllByText('›');
|
||||
expect(separators.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render breadcrumbs component when project is null', () => {
|
||||
const { container } = renderComponent({
|
||||
props: {
|
||||
dataStore: mockDataStoreWithoutProject,
|
||||
},
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Should still render the breadcrumbs container even without project
|
||||
const breadcrumbsContainer = container.querySelector('.data-store-breadcrumbs');
|
||||
expect(breadcrumbsContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render inline text edit for datastore name', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const nameInput = getByTestId('datastore-header-name-input');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render DataStoreActions component', () => {
|
||||
const { container } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate to datastores list when breadcrumb item is clicked', async () => {
|
||||
const { getByText } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const datastoresLink = getByText('Data Stores');
|
||||
await userEvent.click(datastoresLink);
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/projects/project-1/datastores');
|
||||
});
|
||||
|
||||
it('should render DataStoreActions component that can trigger navigation', () => {
|
||||
const { container } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Verify DataStoreActions component is rendered
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Name editing', () => {
|
||||
it('should show current datastore name', () => {
|
||||
const { getByDisplayValue } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByDisplayValue('Test DataStore')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component integration', () => {
|
||||
it('should render component structure correctly', () => {
|
||||
const { container, getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check main structure
|
||||
const breadcrumbsContainer = container.querySelector('.data-store-breadcrumbs');
|
||||
expect(breadcrumbsContainer).toBeInTheDocument();
|
||||
|
||||
// Check name input
|
||||
const nameInput = getByTestId('datastore-header-name-input');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
|
||||
// Check actions component
|
||||
const actionsComponent = container.querySelector('[data-test-id="data-store-card-actions"]');
|
||||
expect(actionsComponent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct datastore name', () => {
|
||||
const { getByDisplayValue } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(getByDisplayValue('Test DataStore')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show breadcrumbs separator', () => {
|
||||
const { getAllByText } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {},
|
||||
stubActions: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const separators = getAllByText('›');
|
||||
expect(separators.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.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 { DATA_STORE_VIEW } from '@/features/dataStore/constants';
|
||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
const BREADCRUMBS_SEPARATOR = '›';
|
||||
|
||||
type Props = {
|
||||
dataStore: DataStoreEntity;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const renameInput = useTemplateRef('renameInput');
|
||||
|
||||
const dataStoreStore = useDataStoreStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const editableName = ref(props.dataStore.name);
|
||||
|
||||
const project = computed(() => {
|
||||
return props.dataStore.project ?? null;
|
||||
});
|
||||
|
||||
const breadcrumbs = computed<PathItem[]>(() => {
|
||||
if (!project.value) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'datastores',
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
href: `/projects/${project.value.id}/datastores`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const onItemClicked = async (item: PathItem) => {
|
||||
if (item.href) {
|
||||
await router.push(item.href);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await router.push({ name: DATA_STORE_VIEW, params: { projectId: props.dataStore.projectId } });
|
||||
};
|
||||
|
||||
const onRename = async () => {
|
||||
// Focus rename input if the action is rename
|
||||
// We need this timeout to ensure action toggle is closed before focusing
|
||||
await nextTick();
|
||||
if (renameInput.value?.forceFocus) {
|
||||
renameInput.value.forceFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const onNameSubmit = async (name: string) => {
|
||||
try {
|
||||
const updated = await dataStoreStore.updateDataStore(
|
||||
props.dataStore.id,
|
||||
name,
|
||||
props.dataStore.projectId,
|
||||
);
|
||||
if (!updated) {
|
||||
throw new Error(i18n.baseText('generic.unknownError'));
|
||||
}
|
||||
editableName.value = name;
|
||||
} catch (error) {
|
||||
// Revert to original name if rename fails
|
||||
editableName.value = props.dataStore.name;
|
||||
toast.showError(error, i18n.baseText('dataStore.rename.error'));
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.dataStore.name,
|
||||
(newName) => {
|
||||
editableName.value = newName;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style['data-store-breadcrumbs']">
|
||||
<n8n-breadcrumbs
|
||||
:items="breadcrumbs"
|
||||
:separator="BREADCRUMBS_SEPARATOR"
|
||||
@item-selected="onItemClicked"
|
||||
>
|
||||
<template #prepend>
|
||||
<ProjectBreadcrumb v-if="project" :current-project="project" />
|
||||
</template>
|
||||
<template #append>
|
||||
<span :class="$style.separator">{{ BREADCRUMBS_SEPARATOR }}</span>
|
||||
<N8nInlineTextEdit
|
||||
ref="renameInput"
|
||||
v-model="editableName"
|
||||
data-test-id="datastore-header-name-input"
|
||||
:placeholder="i18n.baseText('dataStore.add.input.name.label')"
|
||||
:class="$style['breadcrumb-current']"
|
||||
:max-length="30"
|
||||
:read-only="false"
|
||||
:disabled="false"
|
||||
@update:model-value="onNameSubmit"
|
||||
/>
|
||||
</template>
|
||||
</n8n-breadcrumbs>
|
||||
<div :class="$style['data-store-actions']">
|
||||
<DataStoreActions :data-store="props.dataStore" @rename="onRename" @on-deleted="onDelete" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.data-store-breadcrumbs {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.data-store-actions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.separator {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--color-foreground-base);
|
||||
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: $custom-font-dark;
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DataStoreCard from './DataStoreCard.vue';
|
||||
import DataStoreCard from '@/features/dataStore/components/DataStoreCard.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import type { UserAction } from '@/Interface';
|
||||
@@ -94,20 +94,6 @@ describe('DataStoreCard', () => {
|
||||
expect(getByText('Read only')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render action dropdown if no actions are provided', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
props: {
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
expect(queryByTestId('data-store-card-actions')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render action dropdown if actions are provided', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('data-store-card-actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct route to data store details', () => {
|
||||
const wrapper = renderComponent();
|
||||
const link = wrapper.getByTestId('data-store-card-link');
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { IUser } from '@n8n/rest-api-client/api/users';
|
||||
import type { UserAction } from '@/Interface';
|
||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||
import { DATA_STORE_DETAILS } from '../constants';
|
||||
import { DATA_STORE_DETAILS } from '@/features/dataStore/constants';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
||||
|
||||
type Props = {
|
||||
dataStore: DataStoreResource;
|
||||
actions?: Array<UserAction<IUser>>;
|
||||
readOnly?: boolean;
|
||||
showOwnershipBadge?: boolean;
|
||||
};
|
||||
@@ -22,10 +20,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [
|
||||
rename: [
|
||||
value: {
|
||||
dataStore: DataStoreResource;
|
||||
action: string;
|
||||
},
|
||||
];
|
||||
}>();
|
||||
@@ -42,30 +39,21 @@ const dataStoreRoute = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const onCardAction = (action: string) => {
|
||||
const onRename = () => {
|
||||
// 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;
|
||||
if (renameInput.value && typeof renameInput.value.forceFocus === 'function') {
|
||||
setTimeout(() => {
|
||||
renameInput.value?.forceFocus?.();
|
||||
}, 100);
|
||||
}
|
||||
// Otherwise, emit the action directly
|
||||
emit('action', {
|
||||
dataStore: props.dataStore,
|
||||
action,
|
||||
});
|
||||
};
|
||||
|
||||
const onNameSubmit = (name: string) => {
|
||||
if (props.dataStore.name === name) return;
|
||||
|
||||
emit('action', {
|
||||
emit('rename', {
|
||||
dataStore: { ...props.dataStore, name },
|
||||
action: 'rename',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -148,12 +136,10 @@ const onNameSubmit = (name: string) => {
|
||||
</template>
|
||||
<template #append>
|
||||
<div :class="$style['card-actions']" @click.prevent>
|
||||
<N8nActionToggle
|
||||
v-if="props.actions.length"
|
||||
:actions="props.actions"
|
||||
theme="dark"
|
||||
data-test-id="data-store-card-actions"
|
||||
@action="onCardAction"
|
||||
<DataStoreActions
|
||||
:data-store="props.dataStore"
|
||||
:is-read-only="props.readOnly"
|
||||
@rename="onRename"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
updateDataStoreApi,
|
||||
} from '@/features/dataStore/datastore.api';
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
|
||||
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
const rootStore = useRootStore();
|
||||
const projectStore = useProjectsStore();
|
||||
|
||||
const dataStores = ref<DataStoreEntity[]>([]);
|
||||
const totalCount = ref(0);
|
||||
@@ -21,14 +23,18 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
console.log('Data stores fetched:', response);
|
||||
|
||||
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;
|
||||
@@ -59,6 +65,24 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
return updated;
|
||||
};
|
||||
|
||||
const fetchDataStoreDetails = async (datastoreId: string, projectId: string) => {
|
||||
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, undefined, {
|
||||
id: datastoreId,
|
||||
});
|
||||
if (response.data.length > 0) {
|
||||
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);
|
||||
};
|
||||
|
||||
return {
|
||||
dataStores,
|
||||
totalCount,
|
||||
@@ -66,5 +90,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||
createDataStore,
|
||||
deleteDataStore,
|
||||
updateDataStore,
|
||||
fetchDataStoreDetails,
|
||||
fetchOrFindDataStore,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,6 +10,11 @@ export const fetchDataStoresApi = async (
|
||||
skip?: number;
|
||||
take?: number;
|
||||
},
|
||||
filter?: {
|
||||
id?: string | string[];
|
||||
name?: string | string[];
|
||||
projectId?: string | string[];
|
||||
},
|
||||
) => {
|
||||
const apiEndpoint = projectId ? `/projects/${projectId}/data-stores` : '/data-stores-global';
|
||||
return await makeRestApiRequest<{ count: number; data: DataStoreEntity[] }>(
|
||||
@@ -18,6 +23,7 @@ export const fetchDataStoresApi = async (
|
||||
apiEndpoint,
|
||||
{
|
||||
...options,
|
||||
...(filter ?? {}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProjectSharingData } from 'n8n-workflow';
|
||||
import type { Project } from '@/types/projects.types';
|
||||
|
||||
export type DataStoreEntity = {
|
||||
id: string;
|
||||
@@ -9,7 +9,7 @@ export type DataStoreEntity = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projectId?: string;
|
||||
project?: ProjectSharingData;
|
||||
project?: Project;
|
||||
};
|
||||
|
||||
export type DataStoreColumnEntity = {
|
||||
|
||||
@@ -11,6 +11,8 @@ 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');
|
||||
|
||||
export const DataStoreModule: FrontendModuleDescription = {
|
||||
id: 'data-store',
|
||||
@@ -54,7 +56,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
||||
path: 'datastores/:id',
|
||||
props: true,
|
||||
components: {
|
||||
default: DataStoreView,
|
||||
default: DataStoreDetailsView,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
meta: {
|
||||
@@ -66,7 +68,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
||||
projectTabs: {
|
||||
overview: [
|
||||
{
|
||||
label: i18n.baseText('dataStore.tab.label'),
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
value: DATA_STORE_VIEW,
|
||||
to: {
|
||||
name: DATA_STORE_VIEW,
|
||||
@@ -75,7 +77,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
||||
],
|
||||
project: [
|
||||
{
|
||||
label: i18n.baseText('dataStore.tab.label'),
|
||||
label: i18n.baseText('dataStore.dataStores'),
|
||||
value: PROJECT_DATA_STORES,
|
||||
dynamicRoute: {
|
||||
name: PROJECT_DATA_STORES,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BaseResource } from '@/Interface';
|
||||
import type { DataStoreEntity } from './datastore.types';
|
||||
import type { DataStoreEntity } from '@/features/dataStore/datastore.types';
|
||||
|
||||
/**
|
||||
* Data Store resource type definition
|
||||
|
||||
Reference in New Issue
Block a user