mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Data store UI/UX improvements (no-changelog) (#18587)
Co-authored-by: Svetoslav Dekov <svetoslav.dekov@n8n.io>
This commit is contained in:
committed by
GitHub
parent
c8dc7d9ab6
commit
802157a329
@@ -2834,10 +2834,10 @@
|
|||||||
"contextual.users.settings.unavailable.button.cloud": "Upgrade now",
|
"contextual.users.settings.unavailable.button.cloud": "Upgrade now",
|
||||||
"contextual.feature.unavailable.title": "Available on the Enterprise Plan",
|
"contextual.feature.unavailable.title": "Available on the Enterprise Plan",
|
||||||
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
|
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
|
||||||
"dataStore.dataStores": "Data Stores",
|
"dataStore.dataStores": "Data Tables",
|
||||||
"dataStore.empty.label": "You don't have any data stores yet",
|
"dataStore.empty.label": "You don't have any data tables yet",
|
||||||
"dataStore.empty.description": "Once you create data stores for your projects, they will appear here",
|
"dataStore.empty.description": "Once you create data tables for your projects, they will appear here",
|
||||||
"dataStore.empty.button.label": "Create data store in \"{projectName}\"",
|
"dataStore.empty.button.label": "Create data table in \"{projectName}\"",
|
||||||
"dataStore.card.size": "{size}MB",
|
"dataStore.card.size": "{size}MB",
|
||||||
"dataStore.card.column.count": "{count} column | {count} columns",
|
"dataStore.card.column.count": "{count} column | {count} columns",
|
||||||
"dataStore.card.row.count": "{count} record | {count} records",
|
"dataStore.card.row.count": "{count} record | {count} records",
|
||||||
@@ -2846,21 +2846,21 @@
|
|||||||
"dataStore.sort.nameAsc": "Sort by name (A-Z)",
|
"dataStore.sort.nameAsc": "Sort by name (A-Z)",
|
||||||
"dataStore.sort.nameDesc": "Sort by name (Z-A)",
|
"dataStore.sort.nameDesc": "Sort by name (Z-A)",
|
||||||
"dataStore.search.placeholder": "Search",
|
"dataStore.search.placeholder": "Search",
|
||||||
"dataStore.error.fetching": "Error loading data stores",
|
"dataStore.error.fetching": "Error loading data tables",
|
||||||
"dataStore.add.title": "Create data store",
|
"dataStore.add.title": "Create data table",
|
||||||
"dataStore.add.description": "Set up a new data store to organize and manage your data.",
|
"dataStore.add.description": "Set up a new data table to organize and manage your data.",
|
||||||
"dataStore.add.button.label": "Create Data Store",
|
"dataStore.add.button.label": "Create data table",
|
||||||
"dataStore.add.input.name.label": "Data Store Name",
|
"dataStore.add.input.name.label": "Data Table Name",
|
||||||
"dataStore.add.input.name.placeholder": "Enter data store name",
|
"dataStore.add.input.name.placeholder": "Enter data table name",
|
||||||
"dataStore.add.error": "Error creating data store",
|
"dataStore.add.error": "Error creating data table",
|
||||||
"dataStore.delete.confirm.title": "Delete data store",
|
"dataStore.delete.confirm.title": "Delete data table",
|
||||||
"dataStore.delete.confirm.message": "Are you sure you want to delete the data store \"{name}\"? This action cannot be undone.",
|
"dataStore.delete.confirm.message": "Are you sure you want to delete the data table \"{name}\"? This action cannot be undone.",
|
||||||
"dataStore.delete.error": "Error deleting data store",
|
"dataStore.delete.error": "Error deleting data table",
|
||||||
"dataStore.rename.error": "Error renaming data store",
|
"dataStore.rename.error": "Error renaming data table",
|
||||||
"dataStore.getDetails.error": "Error fetching data store details",
|
"dataStore.getDetails.error": "Error fetching data table details",
|
||||||
"dataStore.notFound": "Data store not found",
|
"dataStore.notFound": "Data table not found",
|
||||||
"dataStore.noColumns.heading": "No columns yet",
|
"dataStore.noColumns.heading": "No columns yet",
|
||||||
"dataStore.noColumns.description": "Add columns to start storing data in this data store.",
|
"dataStore.noColumns.description": "Add columns to start storing data in this data table.",
|
||||||
"dataStore.noColumns.button.label": "Add first column",
|
"dataStore.noColumns.button.label": "Add first column",
|
||||||
"dataStore.addColumn.label": "Add Column",
|
"dataStore.addColumn.label": "Add Column",
|
||||||
"dataStore.addColumn.nameInput.label": "@:_reusableBaseText.name",
|
"dataStore.addColumn.nameInput.label": "@:_reusableBaseText.name",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const initialize = async () => {
|
|||||||
const response = await dataStoreStore.fetchOrFindDataStore(props.id, props.projectId);
|
const response = await dataStoreStore.fetchOrFindDataStore(props.id, props.projectId);
|
||||||
if (response) {
|
if (response) {
|
||||||
dataStore.value = response;
|
dataStore.value = response;
|
||||||
|
documentTitle.set(`${i18n.baseText('dataStore.dataStores')} > ${response.name}`);
|
||||||
} else {
|
} else {
|
||||||
await showErrorAndGoBackToList(new Error(i18n.baseText('dataStore.notFound')));
|
await showErrorAndGoBackToList(new Error(i18n.baseText('dataStore.notFound')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,24 +120,6 @@ const onProjectHeaderAction = (action: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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(() => {
|
onMounted(() => {
|
||||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||||
});
|
});
|
||||||
@@ -190,7 +172,6 @@ onMounted(() => {
|
|||||||
:data-store="data as DataStoreResource"
|
:data-store="data as DataStoreResource"
|
||||||
:show-ownership-badge="projectPages.isOverviewSubPage"
|
:show-ownership-badge="projectPages.isOverviewSubPage"
|
||||||
:read-only="readOnlyEnv"
|
:read-only="readOnlyEnv"
|
||||||
@rename="onCardRename"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ResourcesListLayout>
|
</ResourcesListLayout>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const renderComponent = createComponentRenderer(DataStoreActions, {
|
|||||||
props: {
|
props: {
|
||||||
dataStore: mockDataStore,
|
dataStore: mockDataStore,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
location: 'breadcrumbs',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -236,4 +237,50 @@ describe('DataStoreActions', () => {
|
|||||||
'Something went wrong while deleting the data store.',
|
'Something went wrong while deleting the data store.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('rename action visibility', () => {
|
||||||
|
it('should show rename action when location is breadcrumbs', async () => {
|
||||||
|
const { getByTestId, queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
dataStore: mockDataStore,
|
||||||
|
isReadOnly: false,
|
||||||
|
location: 'breadcrumbs',
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Check that rename action is present
|
||||||
|
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show rename action when location is card', async () => {
|
||||||
|
const { getByTestId, queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
dataStore: mockDataStore,
|
||||||
|
isReadOnly: false,
|
||||||
|
location: 'card',
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Check that rename action is NOT present
|
||||||
|
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.RENAME}`)).not.toBeInTheDocument();
|
||||||
|
// But delete action should still be present
|
||||||
|
expect(queryByTestId(`action-${DATA_STORE_CARD_ACTIONS.DELETE}`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useToast } from '@/composables/useToast';
|
|||||||
type Props = {
|
type Props = {
|
||||||
dataStore: DataStore;
|
dataStore: DataStore;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
|
location: 'card' | 'breadcrumbs';
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -34,18 +35,23 @@ const i18n = useI18n();
|
|||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const actions = computed<Array<UserAction<IUser>>>(() => [
|
const actions = computed<Array<UserAction<IUser>>>(() => {
|
||||||
{
|
const availableActions = [
|
||||||
label: i18n.baseText('generic.rename'),
|
{
|
||||||
value: DATA_STORE_CARD_ACTIONS.RENAME,
|
label: i18n.baseText('generic.delete'),
|
||||||
disabled: props.isReadOnly,
|
value: DATA_STORE_CARD_ACTIONS.DELETE,
|
||||||
},
|
disabled: props.isReadOnly,
|
||||||
{
|
},
|
||||||
label: i18n.baseText('generic.delete'),
|
];
|
||||||
value: DATA_STORE_CARD_ACTIONS.DELETE,
|
if (props.location === 'breadcrumbs') {
|
||||||
disabled: props.isReadOnly,
|
availableActions.unshift({
|
||||||
},
|
label: i18n.baseText('generic.rename'),
|
||||||
]);
|
value: DATA_STORE_CARD_ACTIONS.RENAME,
|
||||||
|
disabled: props.isReadOnly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return availableActions;
|
||||||
|
});
|
||||||
|
|
||||||
const onAction = async (action: string) => {
|
const onAction = async (action: string) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ describe('DataStoreBreadcrumbs', () => {
|
|||||||
const datastoresLink = getByText('Data Stores');
|
const datastoresLink = getByText('Data Stores');
|
||||||
await userEvent.click(datastoresLink);
|
await userEvent.click(datastoresLink);
|
||||||
|
|
||||||
expect(mockRouter.push).toHaveBeenCalledWith('/projects/project-1/datastores');
|
expect(mockRouter.push).toHaveBeenCalledWith('/projects/project-1/datatables');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render DataStoreActions component that can trigger navigation', () => {
|
it('should render DataStoreActions component that can trigger navigation', () => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const breadcrumbs = computed<PathItem[]>(() => {
|
|||||||
{
|
{
|
||||||
id: 'datastores',
|
id: 'datastores',
|
||||||
label: i18n.baseText('dataStore.dataStores'),
|
label: i18n.baseText('dataStore.dataStores'),
|
||||||
href: `/projects/${project.value.id}/datastores`,
|
href: `/projects/${project.value.id}/datatables`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -118,7 +118,12 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
</n8n-breadcrumbs>
|
</n8n-breadcrumbs>
|
||||||
<div :class="$style['data-store-actions']">
|
<div :class="$style['data-store-actions']">
|
||||||
<DataStoreActions :data-store="props.dataStore" @rename="onRename" @on-deleted="onDelete" />
|
<DataStoreActions
|
||||||
|
:data-store="props.dataStore"
|
||||||
|
location="breadcrumbs"
|
||||||
|
@rename="onRename"
|
||||||
|
@on-deleted="onDelete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { IUser } from '@n8n/rest-api-client/api/users';
|
|||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
const push = vi.fn();
|
const push = vi.fn();
|
||||||
const resolve = vi.fn().mockReturnValue({ href: '/projects/1/datastores/1' });
|
const resolve = vi.fn().mockReturnValue({ href: '/projects/1/datatables/1' });
|
||||||
return {
|
return {
|
||||||
useRouter: vi.fn().mockReturnValue({
|
useRouter: vi.fn().mockReturnValue({
|
||||||
push,
|
push,
|
||||||
@@ -55,7 +55,7 @@ const renderComponent = createComponentRenderer(DataStoreCard, {
|
|||||||
href() {
|
href() {
|
||||||
// Generate href from the route object
|
// Generate href from the route object
|
||||||
if (this.to && typeof this.to === 'object') {
|
if (this.to && typeof this.to === 'object') {
|
||||||
return `/projects/${this.to.params.projectId}/datastores/${this.to.params.id}`;
|
return `/projects/${this.to.params.projectId}/datatables/${this.to.params.id}`;
|
||||||
}
|
}
|
||||||
return '#';
|
return '#';
|
||||||
},
|
},
|
||||||
@@ -83,7 +83,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('datastore-name-input')).toHaveTextContent(DEFAULT_DATA_STORE.name);
|
expect(getByTestId('data-store-card-name')).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');
|
||||||
@@ -110,7 +110,7 @@ describe('DataStoreCard', () => {
|
|||||||
expect(link).toBeInTheDocument();
|
expect(link).toBeInTheDocument();
|
||||||
expect(link).toHaveAttribute(
|
expect(link).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
`/projects/${DEFAULT_DATA_STORE.projectId}/datastores/${DEFAULT_DATA_STORE.id}`,
|
`/projects/${DEFAULT_DATA_STORE.projectId}/datatables/${DEFAULT_DATA_STORE.id}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||||
import { DATA_STORE_DETAILS } from '@/features/dataStore/constants';
|
import { DATA_STORE_DETAILS } from '@/features/dataStore/constants';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { computed, useTemplateRef } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
import DataStoreActions from '@/features/dataStore/components/DataStoreActions.vue';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -19,16 +19,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
showOwnershipBadge: false,
|
showOwnershipBadge: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
rename: [
|
|
||||||
value: {
|
|
||||||
dataStore: DataStoreResource;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const renameInput = useTemplateRef<{ forceFocus?: () => void }>('renameInput');
|
|
||||||
|
|
||||||
const dataStoreRoute = computed(() => {
|
const dataStoreRoute = computed(() => {
|
||||||
return {
|
return {
|
||||||
name: DATA_STORE_DETAILS,
|
name: DATA_STORE_DETAILS,
|
||||||
@@ -38,24 +28,6 @@ const dataStoreRoute = computed(() => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const onRename = () => {
|
|
||||||
// Focus rename input if the action is rename
|
|
||||||
// We need this timeout to ensure action toggle is closed before focusing
|
|
||||||
if (renameInput.value && typeof renameInput.value.forceFocus === 'function') {
|
|
||||||
setTimeout(() => {
|
|
||||||
renameInput.value?.forceFocus?.();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNameSubmit = (name: string) => {
|
|
||||||
if (props.dataStore.name === name) return;
|
|
||||||
|
|
||||||
emit('rename', {
|
|
||||||
dataStore: { ...props.dataStore, name },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div data-test-id="data-store-card">
|
<div data-test-id="data-store-card">
|
||||||
@@ -72,17 +44,9 @@ const onNameSubmit = (name: string) => {
|
|||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="$style['card-header']" @click.prevent>
|
<div :class="$style['card-header']" @click.prevent>
|
||||||
<N8nInlineTextEdit
|
<n8n-text tag="h2" bold data-test-id="data-store-card-name">
|
||||||
ref="renameInput"
|
{{ props.dataStore.name }}
|
||||||
data-test-id="datastore-name-input"
|
</n8n-text>
|
||||||
: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>
|
||||||
@@ -139,7 +103,7 @@ const onNameSubmit = (name: string) => {
|
|||||||
<DataStoreActions
|
<DataStoreActions
|
||||||
:data-store="props.dataStore"
|
:data-store="props.dataStore"
|
||||||
:is-read-only="props.readOnly"
|
:is-read-only="props.readOnly"
|
||||||
@rename="onRename"
|
location="card"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -158,12 +122,6 @@ const onNameSubmit = (name: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const validateName = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInput = debounce(validateName, { debounceTime: 300 });
|
const onInput = debounce(validateName, { debounceTime: 100 });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ vi.mock('@/composables/useToast', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
useI18n: () => ({
|
||||||
|
baseText: (key: string) => {
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
'dataStore.addRow.label': 'Add Row',
|
||||||
|
'dataStore.addRow.disabled.tooltip': 'Add a column first',
|
||||||
|
};
|
||||||
|
return translations[key] || key;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/features/dataStore/composables/useDataStoreTypes', () => ({
|
vi.mock('@/features/dataStore/composables/useDataStoreTypes', () => ({
|
||||||
useDataStoreTypes: () => ({
|
useDataStoreTypes: () => ({
|
||||||
mapToAGCellType: (type: string) => {
|
mapToAGCellType: (type: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import type {
|
import type {
|
||||||
DataStore,
|
DataStore,
|
||||||
@@ -16,6 +16,13 @@ import type {
|
|||||||
ValueGetterParams,
|
ValueGetterParams,
|
||||||
RowSelectionOptions,
|
RowSelectionOptions,
|
||||||
CellValueChangedEvent,
|
CellValueChangedEvent,
|
||||||
|
GetRowIdParams,
|
||||||
|
ICellRendererParams,
|
||||||
|
CellEditRequestEvent,
|
||||||
|
CellClickedEvent,
|
||||||
|
ValueSetterParams,
|
||||||
|
CellEditingStartedEvent,
|
||||||
|
CellEditingStoppedEvent,
|
||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import {
|
import {
|
||||||
ModuleRegistry,
|
ModuleRegistry,
|
||||||
@@ -37,11 +44,14 @@ import AddColumnPopover from '@/features/dataStore/components/dataGrid/AddColumn
|
|||||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { DEFAULT_ID_COLUMN_NAME, EMPTY_VALUE, NULL_VALUE } from '@/features/dataStore/constants';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { MODAL_CONFIRM } from '@/constants';
|
import { MODAL_CONFIRM } from '@/constants';
|
||||||
import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
|
import ColumnHeader from '@/features/dataStore/components/dataGrid/ColumnHeader.vue';
|
||||||
import { DEFAULT_ID_COLUMN_NAME, NO_TABLE_YET_MESSAGE } from '@/features/dataStore/constants';
|
|
||||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
||||||
|
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
||||||
|
import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
|
||||||
|
import { onClickOutside } from '@vueuse/core';
|
||||||
|
|
||||||
// Register only the modules we actually use
|
// Register only the modules we actually use
|
||||||
ModuleRegistry.registerModules([
|
ModuleRegistry.registerModules([
|
||||||
@@ -81,15 +91,22 @@ const gridApi = ref<GridApi | null>(null);
|
|||||||
const colDefs = ref<ColDef[]>([]);
|
const colDefs = ref<ColDef[]>([]);
|
||||||
const rowData = ref<DataStoreRow[]>([]);
|
const rowData = ref<DataStoreRow[]>([]);
|
||||||
const rowSelection: RowSelectionOptions | 'single' | 'multiple' = {
|
const rowSelection: RowSelectionOptions | 'single' | 'multiple' = {
|
||||||
mode: 'singleRow',
|
mode: 'multiRow',
|
||||||
enableClickSelection: true,
|
enableClickSelection: false,
|
||||||
checkboxes: false,
|
checkboxes: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentLoading = ref(false);
|
const contentLoading = ref(false);
|
||||||
|
|
||||||
|
// Track the last focused cell so we can start editing when users click on it
|
||||||
|
// AG Grid doesn't provide cell blur event so we need to reset this manually
|
||||||
|
const lastFocusedCell = ref<{ rowIndex: number; colId: string } | null>(null);
|
||||||
|
const isTextEditorOpen = ref(false);
|
||||||
|
|
||||||
|
const gridContainer = useTemplateRef('gridContainer');
|
||||||
|
|
||||||
// Shared config for all columns
|
// Shared config for all columns
|
||||||
const defaultColumnDef = {
|
const defaultColumnDef: ColDef = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filter: false,
|
filter: false,
|
||||||
@@ -184,6 +201,7 @@ const onDeleteColumn = async (columnId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Split this up to create column def based on type
|
||||||
const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {}) => {
|
const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {}) => {
|
||||||
const columnDef: ColDef = {
|
const columnDef: ColDef = {
|
||||||
colId: col.id,
|
colId: col.id,
|
||||||
@@ -192,6 +210,7 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
|||||||
editable: true,
|
editable: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
headerComponent: ColumnHeader,
|
headerComponent: ColumnHeader,
|
||||||
|
cellEditorPopup: false,
|
||||||
headerComponentParams: { onDelete: onDeleteColumn },
|
headerComponentParams: { onDelete: onDeleteColumn },
|
||||||
...extraProps,
|
...extraProps,
|
||||||
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
|
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
|
||||||
@@ -209,15 +228,61 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
|||||||
}
|
}
|
||||||
return params.data?.[col.name];
|
return params.data?.[col.name];
|
||||||
},
|
},
|
||||||
|
cellRendererSelector: (params: ICellRendererParams) => {
|
||||||
|
let rowValue = params.data?.[col.name];
|
||||||
|
// When adding new column, rowValue is undefined (same below, in string cell editor)
|
||||||
|
if (rowValue === undefined) {
|
||||||
|
rowValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom renderer for null or empty values
|
||||||
|
if (rowValue === null) {
|
||||||
|
return { component: NullEmptyCellRenderer, params: { value: NULL_VALUE } };
|
||||||
|
}
|
||||||
|
if (rowValue === '') {
|
||||||
|
return { component: NullEmptyCellRenderer, params: { value: EMPTY_VALUE } };
|
||||||
|
}
|
||||||
|
// Fallback to default cell renderer
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// Enable large text editor for text columns
|
// Enable large text editor for text columns
|
||||||
if (col.type === 'string') {
|
if (col.type === 'string') {
|
||||||
columnDef.cellEditor = 'agLargeTextCellEditor';
|
columnDef.cellEditor = 'agLargeTextCellEditor';
|
||||||
columnDef.cellEditorPopup = true;
|
// Provide initial value for the editor, otherwise agLargeTextCellEditor breaks
|
||||||
|
columnDef.cellEditorParams = (params: CellEditRequestEvent<DataStoreRow>) => ({
|
||||||
|
value: params.value ?? '',
|
||||||
|
});
|
||||||
|
columnDef.valueSetter = (params: ValueSetterParams<DataStoreRow>) => {
|
||||||
|
let originalValue = params.data[col.name];
|
||||||
|
if (originalValue === undefined) {
|
||||||
|
originalValue = null;
|
||||||
|
}
|
||||||
|
let newValue = params.newValue;
|
||||||
|
|
||||||
|
if (!isDataStoreValue(newValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure not to trigger update if cell content is not set and value was null
|
||||||
|
if (originalValue === null && newValue === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When clearing editor content, set value to empty string
|
||||||
|
if (isTextEditorOpen.value && newValue === null) {
|
||||||
|
newValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise update the value
|
||||||
|
params.data[col.name] = newValue;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// Setup date editor
|
// Setup date editor
|
||||||
if (col.type === 'date') {
|
if (col.type === 'date') {
|
||||||
columnDef.cellEditor = 'agDateCellEditor';
|
columnDef.cellEditor = 'agDateCellEditor';
|
||||||
|
columnDef.cellEditorPopup = true;
|
||||||
}
|
}
|
||||||
return columnDef;
|
return columnDef;
|
||||||
};
|
};
|
||||||
@@ -252,16 +317,20 @@ const onAddRowClick = async () => {
|
|||||||
if (currentPage.value * pageSize.value < totalItems.value) {
|
if (currentPage.value * pageSize.value < totalItems.value) {
|
||||||
await setCurrentPage(Math.ceil(totalItems.value / pageSize.value));
|
await setCurrentPage(Math.ceil(totalItems.value / pageSize.value));
|
||||||
}
|
}
|
||||||
const inserted = await dataStoreStore.insertEmptyRow(props.dataStore);
|
contentLoading.value = true;
|
||||||
if (!inserted) {
|
|
||||||
throw new Error(i18n.baseText('generic.unknownError'));
|
|
||||||
}
|
|
||||||
emit('toggleSave', true);
|
emit('toggleSave', true);
|
||||||
await fetchDataStoreContent();
|
const newRowId = await dataStoreStore.insertEmptyRow(props.dataStore);
|
||||||
|
const newRow: DataStoreRow = { id: newRowId };
|
||||||
|
// Add nulls for the rest of the columns
|
||||||
|
props.dataStore.columns.forEach((col) => {
|
||||||
|
newRow[col.name] = null;
|
||||||
|
});
|
||||||
|
rows.value.push(newRow);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('dataStore.addRow.error'));
|
toast.showError(error, i18n.baseText('dataStore.addRow.error'));
|
||||||
} finally {
|
} finally {
|
||||||
emit('toggleSave', false);
|
emit('toggleSave', false);
|
||||||
|
contentLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,21 +357,67 @@ const initColumnDefinitions = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCellValueChanged = async (params: CellValueChangedEvent) => {
|
const onCellValueChanged = async (params: CellValueChangedEvent<DataStoreRow>) => {
|
||||||
const { data, api } = params;
|
const { data, api, oldValue, colDef } = params;
|
||||||
|
const value = params.data[colDef.field!];
|
||||||
|
|
||||||
|
if (value === undefined || value === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.id !== 'number') {
|
||||||
|
throw new Error('Expected row id to be a number');
|
||||||
|
}
|
||||||
|
const fieldName = String(colDef.field);
|
||||||
|
const id = data.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emit('toggleSave', true);
|
emit('toggleSave', true);
|
||||||
await dataStoreStore.upsertRow(props.dataStore.id, props.dataStore.projectId, data);
|
await dataStoreStore.updateRow(props.dataStore.id, props.dataStore.projectId, id, {
|
||||||
|
[fieldName]: value,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Revert cell to original value if the update fails
|
// Revert cell to original value if the update fails
|
||||||
api.undoCellEditing();
|
const validOldValue = isDataStoreValue(oldValue) ? oldValue : null;
|
||||||
|
const revertedData: DataStoreRow = { ...data, [fieldName]: validOldValue };
|
||||||
|
api.applyTransaction({
|
||||||
|
update: [revertedData],
|
||||||
|
});
|
||||||
toast.showError(error, i18n.baseText('dataStore.updateRow.error'));
|
toast.showError(error, i18n.baseText('dataStore.updateRow.error'));
|
||||||
} finally {
|
} finally {
|
||||||
emit('toggleSave', false);
|
emit('toggleSave', false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Start editing when users click on already focused cells
|
||||||
|
const onCellClicked = (params: CellClickedEvent<DataStoreRow>) => {
|
||||||
|
const clickedCellColumn = params.column.getColId();
|
||||||
|
const clickedCellRow = params.rowIndex;
|
||||||
|
|
||||||
|
// Skip if rowIndex is null
|
||||||
|
if (clickedCellRow === null) return;
|
||||||
|
|
||||||
|
// Check if this is the same cell that was focused before this click
|
||||||
|
const wasAlreadyFocused =
|
||||||
|
lastFocusedCell.value &&
|
||||||
|
lastFocusedCell.value.rowIndex === clickedCellRow &&
|
||||||
|
lastFocusedCell.value.colId === clickedCellColumn;
|
||||||
|
|
||||||
|
if (wasAlreadyFocused && params.column.getColDef()?.editable) {
|
||||||
|
// Cell was already selected, start editing
|
||||||
|
params.api.startEditingCell({
|
||||||
|
rowIndex: clickedCellRow,
|
||||||
|
colKey: clickedCellColumn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the last focused cell for next click
|
||||||
|
lastFocusedCell.value = {
|
||||||
|
rowIndex: clickedCellRow,
|
||||||
|
colId: clickedCellColumn,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const fetchDataStoreContent = async () => {
|
const fetchDataStoreContent = async () => {
|
||||||
try {
|
try {
|
||||||
contentLoading.value = true;
|
contentLoading.value = true;
|
||||||
@@ -316,11 +431,7 @@ const fetchDataStoreContent = async () => {
|
|||||||
totalItems.value = fetchedRows.count;
|
totalItems.value = fetchedRows.count;
|
||||||
rowData.value = rows.value;
|
rowData.value = rows.value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: We currently don't create user tables until user columns or rows are added
|
toast.showError(error, i18n.baseText('dataStore.fetchContent.error'));
|
||||||
// so we need to ignore NO_TABLE_YET_MESSAGE error here
|
|
||||||
if ('message' in error && !error.message.includes(NO_TABLE_YET_MESSAGE)) {
|
|
||||||
toast.showError(error, i18n.baseText('dataStore.fetchContent.error'));
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
contentLoading.value = false;
|
contentLoading.value = false;
|
||||||
if (gridApi.value) {
|
if (gridApi.value) {
|
||||||
@@ -329,6 +440,14 @@ const fetchDataStoreContent = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClickOutside(gridContainer, () => {
|
||||||
|
resetLastFocusedCell();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetLastFocusedCell = () => {
|
||||||
|
lastFocusedCell.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
initColumnDefinitions();
|
initColumnDefinitions();
|
||||||
await fetchDataStoreContent();
|
await fetchDataStoreContent();
|
||||||
@@ -337,11 +456,25 @@ const initialize = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initialize();
|
await initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onCellEditingStarted = (params: CellEditingStartedEvent<DataStoreRow>) => {
|
||||||
|
if (params.column.getColDef().cellDataType === 'text') {
|
||||||
|
isTextEditorOpen.value = true;
|
||||||
|
} else {
|
||||||
|
isTextEditorOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCellEditingStopped = (params: CellEditingStoppedEvent<DataStoreRow>) => {
|
||||||
|
if (params.column.getColDef().cellDataType === 'text') {
|
||||||
|
isTextEditorOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.wrapper">
|
<div :class="$style.wrapper">
|
||||||
<div :class="$style['grid-container']" data-test-id="data-store-grid">
|
<div ref="gridContainer" :class="$style['grid-container']" data-test-id="data-store-grid">
|
||||||
<AgGridVue
|
<AgGridVue
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:row-data="rowData"
|
:row-data="rowData"
|
||||||
@@ -355,13 +488,17 @@ onMounted(async () => {
|
|||||||
:suppress-drag-leave-hides-columns="true"
|
:suppress-drag-leave-hides-columns="true"
|
||||||
:loading="contentLoading"
|
:loading="contentLoading"
|
||||||
:row-selection="rowSelection"
|
:row-selection="rowSelection"
|
||||||
:get-row-id="(params) => String(params.data.id)"
|
:get-row-id="(params: GetRowIdParams) => String(params.data.id)"
|
||||||
:single-click-edit="true"
|
|
||||||
:stop-editing-when-cells-lose-focus="true"
|
:stop-editing-when-cells-lose-focus="true"
|
||||||
:undo-redo-cell-editing="true"
|
:undo-redo-cell-editing="true"
|
||||||
@grid-ready="onGridReady"
|
@grid-ready="onGridReady"
|
||||||
@cell-value-changed="onCellValueChanged"
|
@cell-value-changed="onCellValueChanged"
|
||||||
@column-moved="onColumnMoved"
|
@column-moved="onColumnMoved"
|
||||||
|
@cell-clicked="onCellClicked"
|
||||||
|
@cell-editing-started="onCellEditingStarted"
|
||||||
|
@cell-editing-stopped="onCellEditingStopped"
|
||||||
|
@column-header-clicked="resetLastFocusedCell"
|
||||||
|
@selection-changed="resetLastFocusedCell"
|
||||||
/>
|
/>
|
||||||
<AddColumnPopover
|
<AddColumnPopover
|
||||||
:data-store="props.dataStore"
|
:data-store="props.dataStore"
|
||||||
@@ -430,6 +567,15 @@ onMounted(async () => {
|
|||||||
:global(.ag-header-cell-resize) {
|
:global(.ag-header-cell-resize) {
|
||||||
width: var(--spacing-4xs);
|
width: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't show borders for the checkbox cells
|
||||||
|
:global(.ag-cell[col-id='ag-Grid-SelectionColumn']) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ag-cell[col-id='ag-Grid-SelectionColumn'].ag-cell-focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-column-popover {
|
.add-column-popover {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
params: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="n8n-empty-value">{{ props.params.value }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.n8n-empty-value {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,8 +20,9 @@ export const MAX_COLUMN_NAME_LENGTH = 128;
|
|||||||
|
|
||||||
export const COLUMN_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
|
export const COLUMN_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
|
||||||
|
|
||||||
export const NO_TABLE_YET_MESSAGE = 'SQLITE_ERROR: no such table:';
|
|
||||||
|
|
||||||
export const MIN_LOADING_TIME = 500; // ms
|
export const MIN_LOADING_TIME = 500; // ms
|
||||||
|
|
||||||
|
export const NULL_VALUE = 'Null';
|
||||||
|
export const EMPTY_VALUE = 'Empty';
|
||||||
|
|
||||||
export const DATA_STORE_MODULE_NAME = 'data-store';
|
export const DATA_STORE_MODULE_NAME = 'data-store';
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const insertDataStoreRowApi = async (
|
|||||||
row: DataStoreRow,
|
row: DataStoreRow,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
) => {
|
) => {
|
||||||
return await makeRestApiRequest<boolean>(
|
return await makeRestApiRequest<number[]>(
|
||||||
context,
|
context,
|
||||||
'POST',
|
'POST',
|
||||||
`/projects/${projectId}/data-stores/${dataStoreId}/insert`,
|
`/projects/${projectId}/data-stores/${dataStoreId}/insert`,
|
||||||
@@ -161,20 +161,20 @@ export const insertDataStoreRowApi = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDataStoreRowsApi = async (
|
export const updateDataStoreRowsApi = async (
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
dataStoreId: string,
|
dataStoreId: string,
|
||||||
rows: DataStoreRow[],
|
rowId: number,
|
||||||
|
rowData: DataStoreRow,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
matchFields: string[] = ['id'],
|
|
||||||
) => {
|
) => {
|
||||||
return await makeRestApiRequest<boolean>(
|
return await makeRestApiRequest<boolean>(
|
||||||
context,
|
context,
|
||||||
'POST',
|
'PATCH',
|
||||||
`/projects/${projectId}/data-stores/${dataStoreId}/upsert`,
|
`/projects/${projectId}/data-stores/${dataStoreId}/rows`,
|
||||||
{
|
{
|
||||||
rows,
|
filter: { id: rowId },
|
||||||
matchFields,
|
data: rowData,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
moveDataStoreColumnApi,
|
moveDataStoreColumnApi,
|
||||||
getDataStoreRowsApi,
|
getDataStoreRowsApi,
|
||||||
insertDataStoreRowApi,
|
insertDataStoreRowApi,
|
||||||
upsertDataStoreRowsApi,
|
updateDataStoreRowsApi,
|
||||||
} from '@/features/dataStore/dataStore.api';
|
} from '@/features/dataStore/dataStore.api';
|
||||||
import type {
|
import type {
|
||||||
DataStore,
|
DataStore,
|
||||||
@@ -20,14 +20,11 @@ import type {
|
|||||||
DataStoreRow,
|
DataStoreRow,
|
||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useDataStoreTypes } from '@/features/dataStore/composables/useDataStoreTypes';
|
|
||||||
|
|
||||||
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const projectStore = useProjectsStore();
|
const projectStore = useProjectsStore();
|
||||||
|
|
||||||
const dataStoreTypes = useDataStoreTypes();
|
|
||||||
|
|
||||||
const dataStores = ref<DataStore[]>([]);
|
const dataStores = ref<DataStore[]>([]);
|
||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
|
|
||||||
@@ -185,19 +182,30 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
const insertEmptyRow = async (dataStore: DataStore) => {
|
const insertEmptyRow = async (dataStore: DataStore) => {
|
||||||
const emptyRow: DataStoreRow = {};
|
const emptyRow: DataStoreRow = {};
|
||||||
dataStore.columns.forEach((column) => {
|
dataStore.columns.forEach((column) => {
|
||||||
// Set default values based on column type
|
emptyRow[column.name] = null;
|
||||||
emptyRow[column.name] = dataStoreTypes.getDefaultValueForType(column.type);
|
|
||||||
});
|
});
|
||||||
return await insertDataStoreRowApi(
|
const inserted = await insertDataStoreRowApi(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
dataStore.id,
|
dataStore.id,
|
||||||
emptyRow,
|
emptyRow,
|
||||||
dataStore.projectId,
|
dataStore.projectId,
|
||||||
);
|
);
|
||||||
|
return inserted[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsertRow = async (dataStoreId: string, projectId: string, row: DataStoreRow) => {
|
const updateRow = async (
|
||||||
return await upsertDataStoreRowsApi(rootStore.restApiContext, dataStoreId, [row], projectId);
|
dataStoreId: string,
|
||||||
|
projectId: string,
|
||||||
|
rowId: number,
|
||||||
|
rowData: DataStoreRow,
|
||||||
|
) => {
|
||||||
|
return await updateDataStoreRowsApi(
|
||||||
|
rootStore.restApiContext,
|
||||||
|
dataStoreId,
|
||||||
|
rowId,
|
||||||
|
rowData,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -214,6 +222,6 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
moveDataStoreColumn,
|
moveDataStoreColumn,
|
||||||
fetchDataStoreContent,
|
fetchDataStoreContent,
|
||||||
insertEmptyRow,
|
insertEmptyRow,
|
||||||
upsertRow,
|
updateRow,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
|||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
name: DATA_STORE_VIEW,
|
name: DATA_STORE_VIEW,
|
||||||
path: '/home/datastores',
|
path: '/home/datatables',
|
||||||
components: {
|
components: {
|
||||||
default: DataStoreView,
|
default: DataStoreView,
|
||||||
sidebar: MainSidebar,
|
sidebar: MainSidebar,
|
||||||
@@ -40,7 +40,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: PROJECT_DATA_STORES,
|
name: PROJECT_DATA_STORES,
|
||||||
path: 'datastores',
|
path: 'datatables',
|
||||||
props: true,
|
props: true,
|
||||||
components: {
|
components: {
|
||||||
default: DataStoreView,
|
default: DataStoreView,
|
||||||
@@ -53,7 +53,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: DATA_STORE_DETAILS,
|
name: DATA_STORE_DETAILS,
|
||||||
path: 'datastores/:id',
|
path: 'datatables/:id',
|
||||||
props: true,
|
props: true,
|
||||||
components: {
|
components: {
|
||||||
default: DataStoreDetailsView,
|
default: DataStoreDetailsView,
|
||||||
|
|||||||
Reference in New Issue
Block a user