mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(editor): Implement row management for data stores (no-changelog) (#18483)
This commit is contained in:
committed by
GitHub
parent
82876b711a
commit
fd94d71b6d
@@ -80,6 +80,7 @@
|
|||||||
"generic.tryNow": "Try now",
|
"generic.tryNow": "Try now",
|
||||||
"generic.startNow": "Start now",
|
"generic.startNow": "Start now",
|
||||||
"generic.dismiss": "Dismiss",
|
"generic.dismiss": "Dismiss",
|
||||||
|
"generic.saving": "Saving",
|
||||||
"generic.unsavedWork.confirmMessage.headline": "Save changes before leaving?",
|
"generic.unsavedWork.confirmMessage.headline": "Save changes before leaving?",
|
||||||
"generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.",
|
"generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.",
|
||||||
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
|
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
|
||||||
@@ -2870,6 +2871,10 @@
|
|||||||
"dataStore.addColumn.error": "Error adding column",
|
"dataStore.addColumn.error": "Error adding column",
|
||||||
"dataStore.addColumn.invalidName.error": "Invalid column name",
|
"dataStore.addColumn.invalidName.error": "Invalid column name",
|
||||||
"dataStore.addColumn.invalidName.description": "Only alphanumeric characters and non-leading dashes are allowed for column names",
|
"dataStore.addColumn.invalidName.description": "Only alphanumeric characters and non-leading dashes are allowed for column names",
|
||||||
|
"dataStore.fetchContent.error": "Error fetching data store content",
|
||||||
|
"dataStore.addRow.label": "Add Row",
|
||||||
|
"dataStore.addRow.error": "Error adding row",
|
||||||
|
"dataStore.updateRow.error": "Error updating row",
|
||||||
"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>",
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
|||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { DATA_STORE_VIEW } from '@/features/dataStore/constants';
|
import { DATA_STORE_VIEW, MIN_LOADING_TIME } from '@/features/dataStore/constants';
|
||||||
import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBreadcrumbs.vue';
|
import DataStoreBreadcrumbs from '@/features/dataStore/components/DataStoreBreadcrumbs.vue';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import DataStoreTable from './components/dataGrid/DataStoreTable.vue';
|
import DataStoreTable from './components/dataGrid/DataStoreTable.vue';
|
||||||
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,7 +26,9 @@ const documentTitle = useDocumentTitle();
|
|||||||
const dataStoreStore = useDataStoreStore();
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
const dataStore = ref<DataStore | null>(null);
|
const dataStore = ref<DataStore | null>(null);
|
||||||
|
const { debounce } = useDebounce();
|
||||||
|
|
||||||
const showErrorAndGoBackToList = async (error: unknown) => {
|
const showErrorAndGoBackToList = async (error: unknown) => {
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
@@ -51,6 +54,30 @@ const initialize = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debounce creating new timer slightly if saving is initiated fast in succession
|
||||||
|
const debouncedSetSaving = debounce(
|
||||||
|
(value: boolean) => {
|
||||||
|
saving.value = value;
|
||||||
|
},
|
||||||
|
{ debounceTime: 50, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounce hiding the saving indicator so users can see saving state
|
||||||
|
const debouncedHideSaving = debounce(
|
||||||
|
() => {
|
||||||
|
saving.value = false;
|
||||||
|
},
|
||||||
|
{ debounceTime: MIN_LOADING_TIME, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggleSave = (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
debouncedSetSaving(true);
|
||||||
|
} else {
|
||||||
|
debouncedHideSaving();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||||
await initialize();
|
await initialize();
|
||||||
@@ -72,9 +99,13 @@ onMounted(async () => {
|
|||||||
<div v-else-if="dataStore">
|
<div v-else-if="dataStore">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<DataStoreBreadcrumbs :data-store="dataStore" />
|
<DataStoreBreadcrumbs :data-store="dataStore" />
|
||||||
|
<div v-if="saving" :class="$style.saving">
|
||||||
|
<n8n-spinner />
|
||||||
|
<n8n-text>{{ i18n.baseText('generic.saving') }}...</n8n-text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.content">
|
<div :class="$style.content">
|
||||||
<DataStoreTable :data-store="dataStore" />
|
<DataStoreTable :data-store="dataStore" @toggle-save="onToggleSave" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,8 +133,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saving {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3xs);
|
||||||
|
margin-top: var(--spacing-5xs);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ vi.mock('ag-grid-vue3', () => ({
|
|||||||
mounted(this: MockComponentInstance) {
|
mounted(this: MockComponentInstance) {
|
||||||
this.$emit('gridReady', {
|
this.$emit('gridReady', {
|
||||||
api: {
|
api: {
|
||||||
// Mock API methods
|
refreshHeader: vi.fn(),
|
||||||
|
applyTransaction: vi.fn(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -37,6 +38,12 @@ vi.mock('ag-grid-community', () => ({
|
|||||||
ColumnAutoSizeModule: {},
|
ColumnAutoSizeModule: {},
|
||||||
CheckboxEditorModule: {},
|
CheckboxEditorModule: {},
|
||||||
NumberEditorModule: {},
|
NumberEditorModule: {},
|
||||||
|
RowSelectionModule: {},
|
||||||
|
RenderApiModule: {},
|
||||||
|
DateEditorModule: {},
|
||||||
|
ClientSideRowModelApiModule: {},
|
||||||
|
ValidationModule: {},
|
||||||
|
UndoRedoEditModule: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the n8n theme
|
// Mock the n8n theme
|
||||||
|
|||||||
@@ -15,24 +15,43 @@ import {
|
|||||||
ColumnAutoSizeModule,
|
ColumnAutoSizeModule,
|
||||||
CheckboxEditorModule,
|
CheckboxEditorModule,
|
||||||
NumberEditorModule,
|
NumberEditorModule,
|
||||||
|
RowSelectionModule,
|
||||||
|
RenderApiModule,
|
||||||
|
DateEditorModule,
|
||||||
|
ClientSideRowModelApiModule,
|
||||||
|
ValidationModule,
|
||||||
|
UndoRedoEditModule,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import type {
|
||||||
|
GridApi,
|
||||||
|
GridReadyEvent,
|
||||||
|
ColDef,
|
||||||
|
RowSelectionOptions,
|
||||||
|
CellValueChangedEvent,
|
||||||
|
ValueGetterParams,
|
||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import type { GridApi, GridReadyEvent, ColDef } from 'ag-grid-community';
|
|
||||||
import { n8nTheme } from '@/features/dataStore/components/dataGrid/n8nTheme';
|
import { n8nTheme } from '@/features/dataStore/components/dataGrid/n8nTheme';
|
||||||
import AddColumnPopover from '@/features/dataStore/components/dataGrid/AddColumnPopover.vue';
|
import AddColumnPopover from '@/features/dataStore/components/dataGrid/AddColumnPopover.vue';
|
||||||
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 } from '@/features/dataStore/constants';
|
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';
|
||||||
|
|
||||||
// Register only the modules we actually use
|
// Register only the modules we actually use
|
||||||
ModuleRegistry.registerModules([
|
ModuleRegistry.registerModules([
|
||||||
|
ValidationModule, // This module allows us to see AG Grid errors in browser console
|
||||||
ClientSideRowModelModule,
|
ClientSideRowModelModule,
|
||||||
TextEditorModule,
|
TextEditorModule,
|
||||||
LargeTextEditorModule,
|
LargeTextEditorModule,
|
||||||
ColumnAutoSizeModule,
|
ColumnAutoSizeModule,
|
||||||
CheckboxEditorModule,
|
CheckboxEditorModule,
|
||||||
NumberEditorModule,
|
NumberEditorModule,
|
||||||
|
RowSelectionModule,
|
||||||
|
RenderApiModule,
|
||||||
|
DateEditorModule,
|
||||||
|
ClientSideRowModelApiModule,
|
||||||
|
UndoRedoEditModule,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -41,6 +60,10 @@ type Props = {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggleSave: [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const dataStoreTypes = useDataStoreTypes();
|
const dataStoreTypes = useDataStoreTypes();
|
||||||
@@ -51,6 +74,13 @@ const dataStoreStore = useDataStoreStore();
|
|||||||
const gridApi = ref<GridApi | null>(null);
|
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' = {
|
||||||
|
mode: 'singleRow',
|
||||||
|
enableClickSelection: true,
|
||||||
|
checkboxes: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentLoading = ref(false);
|
||||||
|
|
||||||
// Shared config for all columns
|
// Shared config for all columns
|
||||||
const defaultColumnDef = {
|
const defaultColumnDef = {
|
||||||
@@ -60,22 +90,26 @@ const defaultColumnDef = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
const pageSizeOptions = [10, 20, 50];
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const pageSizeOptions = ref([10, 20, 50]);
|
|
||||||
const totalItems = ref(0);
|
const totalItems = ref(0);
|
||||||
|
|
||||||
|
// Data store content
|
||||||
|
const rows = ref<DataStoreRow[]>([]);
|
||||||
|
|
||||||
const onGridReady = (params: GridReadyEvent) => {
|
const onGridReady = (params: GridReadyEvent) => {
|
||||||
gridApi.value = params.api;
|
gridApi.value = params.api;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCurrentPage = (page: number) => {
|
const setCurrentPage = async (page: number) => {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
|
await fetchDataStoreContent();
|
||||||
};
|
};
|
||||||
|
const setPageSize = async (size: number) => {
|
||||||
const setPageSize = (size: number) => {
|
|
||||||
pageSize.value = size;
|
pageSize.value = size;
|
||||||
currentPage.value = 1; // Reset to first page on page size change
|
currentPage.value = 1; // Reset to first page on page size change
|
||||||
|
await fetchDataStoreContent();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
|
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
|
||||||
@@ -101,15 +135,52 @@ const createColumnDef = (col: DataStoreColumn) => {
|
|||||||
headerName: col.name,
|
headerName: col.name,
|
||||||
editable: col.name !== DEFAULT_ID_COLUMN_NAME,
|
editable: col.name !== DEFAULT_ID_COLUMN_NAME,
|
||||||
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
|
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
|
||||||
|
valueGetter: (params: ValueGetterParams<DataStoreRow>) => {
|
||||||
|
// If the value is null, return null to show empty cell
|
||||||
|
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Parse dates
|
||||||
|
if (col.type === 'date') {
|
||||||
|
const value = params.data?.[col.name];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return new Date(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params.data?.[col.name];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// 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;
|
columnDef.cellEditorPopup = true;
|
||||||
}
|
}
|
||||||
|
// Setup date editor
|
||||||
|
if (col.type === 'date') {
|
||||||
|
columnDef.cellEditor = 'agDateCellEditor';
|
||||||
|
}
|
||||||
return columnDef;
|
return columnDef;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddRowClick = async () => {
|
||||||
|
try {
|
||||||
|
// Go to last page if we are not there already
|
||||||
|
if (currentPage.value * pageSize.value < totalItems.value) {
|
||||||
|
await setCurrentPage(Math.ceil(totalItems.value / pageSize.value));
|
||||||
|
}
|
||||||
|
const inserted = await dataStoreStore.insertEmptyRow(props.dataStore);
|
||||||
|
if (!inserted) {
|
||||||
|
throw new Error(i18n.baseText('generic.unknownError'));
|
||||||
|
}
|
||||||
|
emit('toggleSave', true);
|
||||||
|
await fetchDataStoreContent();
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('dataStore.addRow.error'));
|
||||||
|
} finally {
|
||||||
|
emit('toggleSave', false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initColumnDefinitions = () => {
|
const initColumnDefinitions = () => {
|
||||||
colDefs.value = [
|
colDefs.value = [
|
||||||
// Always add the ID column, it's not returned by the back-end but all data stores have it
|
// Always add the ID column, it's not returned by the back-end but all data stores have it
|
||||||
@@ -125,12 +196,54 @@ const initColumnDefinitions = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = () => {
|
const onCellValueChanged = async (params: CellValueChangedEvent) => {
|
||||||
initColumnDefinitions();
|
const { data, api } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
emit('toggleSave', true);
|
||||||
|
await dataStoreStore.upsertRow(props.dataStore.id, props.dataStore.projectId, data);
|
||||||
|
} catch (error) {
|
||||||
|
// Revert cell to original value if the update fails
|
||||||
|
api.undoCellEditing();
|
||||||
|
toast.showError(error, i18n.baseText('dataStore.updateRow.error'));
|
||||||
|
} finally {
|
||||||
|
emit('toggleSave', false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
const fetchDataStoreContent = async () => {
|
||||||
initialize();
|
try {
|
||||||
|
contentLoading.value = true;
|
||||||
|
const fetchedRows = await dataStoreStore.fetchDataStoreContent(
|
||||||
|
props.dataStore.id,
|
||||||
|
props.dataStore.projectId,
|
||||||
|
currentPage.value,
|
||||||
|
pageSize.value,
|
||||||
|
);
|
||||||
|
rows.value = fetchedRows.data;
|
||||||
|
totalItems.value = fetchedRows.count;
|
||||||
|
rowData.value = rows.value;
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: We currently don't create user tables until user columns or rows are added
|
||||||
|
// 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 {
|
||||||
|
contentLoading.value = false;
|
||||||
|
if (gridApi.value) {
|
||||||
|
gridApi.value.refreshHeader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
initColumnDefinitions();
|
||||||
|
await fetchDataStoreContent();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initialize();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -145,7 +258,14 @@ onMounted(() => {
|
|||||||
:dom-layout="'autoHeight'"
|
:dom-layout="'autoHeight'"
|
||||||
:animate-rows="false"
|
:animate-rows="false"
|
||||||
:theme="n8nTheme"
|
:theme="n8nTheme"
|
||||||
|
:loading="contentLoading"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:get-row-id="(params) => String(params.data.id)"
|
||||||
|
:single-click-edit="true"
|
||||||
|
:stop-editing-when-cells-lose-focus="true"
|
||||||
|
:undo-redo-cell-editing="true"
|
||||||
@grid-ready="onGridReady"
|
@grid-ready="onGridReady"
|
||||||
|
@cell-value-changed="onCellValueChanged"
|
||||||
/>
|
/>
|
||||||
<AddColumnPopover
|
<AddColumnPopover
|
||||||
:data-store="props.dataStore"
|
:data-store="props.dataStore"
|
||||||
@@ -154,20 +274,23 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
|
<n8n-tooltip :content="i18n.baseText('dataStore.addRow.label')">
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
|
data-test-id="data-store-add-row-button"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
class="mb-xl"
|
class="mb-xl"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
data-test-id="data-store-add-row-button"
|
@click="onAddRowClick"
|
||||||
/>
|
/>
|
||||||
|
</n8n-tooltip>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="currentPage"
|
v-model:current-page="currentPage"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
|
data-test-id="data-store-content-pagination"
|
||||||
background
|
background
|
||||||
:total="totalItems"
|
:total="totalItems"
|
||||||
:page-sizes="pageSizeOptions"
|
:page-sizes="pageSizeOptions"
|
||||||
layout="total, prev, pager, next, sizes"
|
layout="total, prev, pager, next, sizes"
|
||||||
data-test-id="data-store-content-pagination"
|
|
||||||
@update:current-page="setCurrentPage"
|
@update:current-page="setCurrentPage"
|
||||||
@size-change="setPageSize"
|
@size-change="setPageSize"
|
||||||
/>
|
/>
|
||||||
@@ -199,7 +322,6 @@ onMounted(() => {
|
|||||||
--ag-font-family: var(--font-family);
|
--ag-font-family: var(--font-family);
|
||||||
--ag-font-size: var(--font-size-xs);
|
--ag-font-size: var(--font-size-xs);
|
||||||
--ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px);
|
--ag-row-height: calc(var(--ag-grid-size) * 0.8 + 32px);
|
||||||
|
|
||||||
--ag-header-background-color: var(--color-background-base);
|
--ag-header-background-color: var(--color-background-base);
|
||||||
--ag-header-font-size: var(--font-size-xs);
|
--ag-header-font-size: var(--font-size-xs);
|
||||||
--ag-header-font-weight: var(--font-weight-bold);
|
--ag-header-font-weight: var(--font-weight-bold);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { IconName } from '@n8n/design-system/components/N8nIcon/icons';
|
import type { IconName } from '@n8n/design-system/components/N8nIcon/icons';
|
||||||
import type { AGGridCellType, DataStoreColumnType } from '@/features/dataStore/datastore.types';
|
import type {
|
||||||
|
AGGridCellType,
|
||||||
|
DataStoreColumnType,
|
||||||
|
DataStoreValue,
|
||||||
|
} from '@/features/dataStore/datastore.types';
|
||||||
|
|
||||||
/* eslint-disable id-denylist */
|
/* eslint-disable id-denylist */
|
||||||
const COLUMN_TYPE_ICONS: Record<DataStoreColumnType, IconName> = {
|
const COLUMN_TYPE_ICONS: Record<DataStoreColumnType, IconName> = {
|
||||||
@@ -27,8 +31,24 @@ export const useDataStoreTypes = () => {
|
|||||||
return colType;
|
return colType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultValueForType = (colType: DataStoreColumnType): DataStoreValue => {
|
||||||
|
switch (colType) {
|
||||||
|
case 'string':
|
||||||
|
return '';
|
||||||
|
case 'number':
|
||||||
|
return 0;
|
||||||
|
case 'boolean':
|
||||||
|
return false;
|
||||||
|
case 'date':
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getIconForType,
|
getIconForType,
|
||||||
mapToAGCellType,
|
mapToAGCellType,
|
||||||
|
getDefaultValueForType,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ export const DEFAULT_ID_COLUMN_NAME = 'id';
|
|||||||
export const MAX_COLUMN_NAME_LENGTH = 128;
|
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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
DataStoreColumnCreatePayload,
|
DataStoreColumnCreatePayload,
|
||||||
DataStore,
|
DataStore,
|
||||||
DataStoreColumn,
|
DataStoreColumn,
|
||||||
|
DataStoreRow,
|
||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
|
|
||||||
export const fetchDataStoresApi = async (
|
export const fetchDataStoresApi = async (
|
||||||
@@ -17,7 +18,7 @@ export const fetchDataStoresApi = async (
|
|||||||
filter?: {
|
filter?: {
|
||||||
id?: string | string[];
|
id?: string | string[];
|
||||||
name?: string | string[];
|
name?: string | string[];
|
||||||
projectId?: string | string[];
|
projectId: string | string[];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const apiEndpoint = projectId ? `/projects/${projectId}/data-stores` : '/data-stores-global';
|
const apiEndpoint = projectId ? `/projects/${projectId}/data-stores` : '/data-stores-global';
|
||||||
@@ -26,8 +27,8 @@ export const fetchDataStoresApi = async (
|
|||||||
'GET',
|
'GET',
|
||||||
apiEndpoint,
|
apiEndpoint,
|
||||||
{
|
{
|
||||||
...options,
|
options: options ?? undefined,
|
||||||
...(filter ?? {}),
|
filter: filter ?? undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -35,7 +36,7 @@ export const fetchDataStoresApi = async (
|
|||||||
export const createDataStoreApi = async (
|
export const createDataStoreApi = async (
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
name: string,
|
name: string,
|
||||||
projectId?: string,
|
projectId: string,
|
||||||
columns?: DataStoreColumnCreatePayload[],
|
columns?: DataStoreColumnCreatePayload[],
|
||||||
) => {
|
) => {
|
||||||
return await makeRestApiRequest<DataStore>(
|
return await makeRestApiRequest<DataStore>(
|
||||||
@@ -52,7 +53,7 @@ export const createDataStoreApi = async (
|
|||||||
export const deleteDataStoreApi = async (
|
export const deleteDataStoreApi = async (
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
dataStoreId: string,
|
dataStoreId: string,
|
||||||
projectId?: string,
|
projectId: string,
|
||||||
) => {
|
) => {
|
||||||
return await makeRestApiRequest<boolean>(
|
return await makeRestApiRequest<boolean>(
|
||||||
context,
|
context,
|
||||||
@@ -69,7 +70,7 @@ export const updateDataStoreApi = async (
|
|||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
dataStoreId: string,
|
dataStoreId: string,
|
||||||
name: string,
|
name: string,
|
||||||
projectId?: string,
|
projectId: string,
|
||||||
) => {
|
) => {
|
||||||
return await makeRestApiRequest<DataStore>(
|
return await makeRestApiRequest<DataStore>(
|
||||||
context,
|
context,
|
||||||
@@ -96,3 +97,54 @@ export const addDataStoreColumnApi = async (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataStoreRowsApi = async (
|
||||||
|
context: IRestApiContext,
|
||||||
|
dataStoreId: string,
|
||||||
|
projectId: string,
|
||||||
|
options?: {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return await makeRestApiRequest<{
|
||||||
|
count: number;
|
||||||
|
data: DataStoreRow[];
|
||||||
|
}>(context, 'GET', `/projects/${projectId}/data-stores/${dataStoreId}/rows`, {
|
||||||
|
...(options ?? {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertDataStoreRowApi = async (
|
||||||
|
context: IRestApiContext,
|
||||||
|
dataStoreId: string,
|
||||||
|
row: DataStoreRow,
|
||||||
|
projectId: string,
|
||||||
|
) => {
|
||||||
|
return await makeRestApiRequest<boolean>(
|
||||||
|
context,
|
||||||
|
'POST',
|
||||||
|
`/projects/${projectId}/data-stores/${dataStoreId}/insert`,
|
||||||
|
{
|
||||||
|
data: [row],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertDataStoreRowsApi = async (
|
||||||
|
context: IRestApiContext,
|
||||||
|
dataStoreId: string,
|
||||||
|
rows: DataStoreRow[],
|
||||||
|
projectId: string,
|
||||||
|
matchFields: string[] = ['id'],
|
||||||
|
) => {
|
||||||
|
return await makeRestApiRequest<boolean>(
|
||||||
|
context,
|
||||||
|
'POST',
|
||||||
|
`/projects/${projectId}/data-stores/${dataStoreId}/upsert`,
|
||||||
|
{
|
||||||
|
rows,
|
||||||
|
matchFields,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ import {
|
|||||||
deleteDataStoreApi,
|
deleteDataStoreApi,
|
||||||
updateDataStoreApi,
|
updateDataStoreApi,
|
||||||
addDataStoreColumnApi,
|
addDataStoreColumnApi,
|
||||||
|
getDataStoreRowsApi,
|
||||||
|
insertDataStoreRowApi,
|
||||||
|
upsertDataStoreRowsApi,
|
||||||
} from '@/features/dataStore/dataStore.api';
|
} from '@/features/dataStore/dataStore.api';
|
||||||
import type { DataStore, DataStoreColumnCreatePayload } from '@/features/dataStore/datastore.types';
|
import type {
|
||||||
|
DataStore,
|
||||||
|
DataStoreColumnCreatePayload,
|
||||||
|
DataStoreRow,
|
||||||
|
} 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);
|
||||||
|
|
||||||
@@ -28,7 +38,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
totalCount.value = response.count;
|
totalCount.value = response.count;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDataStore = async (name: string, projectId?: string) => {
|
const createDataStore = async (name: string, projectId: string) => {
|
||||||
const newStore = await createDataStoreApi(rootStore.restApiContext, name, projectId);
|
const newStore = await createDataStoreApi(rootStore.restApiContext, name, projectId);
|
||||||
if (!newStore.project && projectId) {
|
if (!newStore.project && projectId) {
|
||||||
const project = await projectStore.fetchProject(projectId);
|
const project = await projectStore.fetchProject(projectId);
|
||||||
@@ -41,7 +51,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
return newStore;
|
return newStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDataStore = async (datastoreId: string, projectId?: string) => {
|
const deleteDataStore = async (datastoreId: string, projectId: string) => {
|
||||||
const deleted = await deleteDataStoreApi(rootStore.restApiContext, datastoreId, projectId);
|
const deleted = await deleteDataStoreApi(rootStore.restApiContext, datastoreId, projectId);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
dataStores.value = dataStores.value.filter((store) => store.id !== datastoreId);
|
dataStores.value = dataStores.value.filter((store) => store.id !== datastoreId);
|
||||||
@@ -50,7 +60,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
return deleted;
|
return deleted;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDataStore = async (datastoreId: string, name: string, projectId?: string) => {
|
const updateDataStore = async (datastoreId: string, name: string, projectId: string) => {
|
||||||
const updated = await updateDataStoreApi(
|
const updated = await updateDataStoreApi(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
datastoreId,
|
datastoreId,
|
||||||
@@ -68,6 +78,7 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
|
|
||||||
const fetchDataStoreDetails = async (datastoreId: string, projectId: string) => {
|
const fetchDataStoreDetails = async (datastoreId: string, projectId: string) => {
|
||||||
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, undefined, {
|
const response = await fetchDataStoresApi(rootStore.restApiContext, projectId, undefined, {
|
||||||
|
projectId,
|
||||||
id: datastoreId,
|
id: datastoreId,
|
||||||
});
|
});
|
||||||
if (response.data.length > 0) {
|
if (response.data.length > 0) {
|
||||||
@@ -104,6 +115,36 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
return newColumn;
|
return newColumn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDataStoreContent = async (
|
||||||
|
datastoreId: string,
|
||||||
|
projectId: string,
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
) => {
|
||||||
|
return await getDataStoreRowsApi(rootStore.restApiContext, datastoreId, projectId, {
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertEmptyRow = async (dataStore: DataStore) => {
|
||||||
|
const emptyRow: DataStoreRow = {};
|
||||||
|
dataStore.columns.forEach((column) => {
|
||||||
|
// Set default values based on column type
|
||||||
|
emptyRow[column.name] = dataStoreTypes.getDefaultValueForType(column.type);
|
||||||
|
});
|
||||||
|
return await insertDataStoreRowApi(
|
||||||
|
rootStore.restApiContext,
|
||||||
|
dataStore.id,
|
||||||
|
emptyRow,
|
||||||
|
dataStore.projectId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertRow = async (dataStoreId: string, projectId: string, row: DataStoreRow) => {
|
||||||
|
return await upsertDataStoreRowsApi(rootStore.restApiContext, dataStoreId, [row], projectId);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataStores,
|
dataStores,
|
||||||
totalCount,
|
totalCount,
|
||||||
@@ -114,5 +155,8 @@ export const useDataStoreStore = defineStore(DATA_STORE_STORE, () => {
|
|||||||
fetchDataStoreDetails,
|
fetchDataStoreDetails,
|
||||||
fetchOrFindDataStore,
|
fetchOrFindDataStore,
|
||||||
addDataStoreColumn,
|
addDataStoreColumn,
|
||||||
|
fetchDataStoreContent,
|
||||||
|
insertEmptyRow,
|
||||||
|
upsertRow,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { DataStoreValue } from '@/features/dataStore/datastore.types';
|
||||||
|
|
||||||
|
export const isDataStoreValue = (value: unknown): value is DataStoreValue => {
|
||||||
|
return (
|
||||||
|
value === null ||
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
typeof value === 'boolean' ||
|
||||||
|
value instanceof Date
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user