mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Improve Data Table UX based on feedback (no-changelog) (#19312)
This commit is contained in:
committed by
GitHub
parent
52d44c26db
commit
6e6a8f8be8
@@ -495,14 +495,14 @@ function confirmFolderDelete(folderName: string) {
|
|||||||
function deleteFolderAndMoveContents(folderName: string, destinationName: string) {
|
function deleteFolderAndMoveContents(folderName: string, destinationName: string) {
|
||||||
cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
|
cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
|
||||||
getFolderDeleteModal().should('be.visible');
|
getFolderDeleteModal().should('be.visible');
|
||||||
getFolderDeleteModal().find('h1').first().contains(`Delete "${folderName}"`);
|
getFolderDeleteModal().find('h1').first().contains(`Delete '${folderName}'`);
|
||||||
getTransferContentRadioButton().should('be.visible').click();
|
getTransferContentRadioButton().should('be.visible').click();
|
||||||
getMoveToFolderDropdown().click();
|
getMoveToFolderDropdown().click();
|
||||||
getMoveToFolderInput().type(destinationName);
|
getMoveToFolderInput().type(destinationName);
|
||||||
getMoveToFolderOption(destinationName).click();
|
getMoveToFolderOption(destinationName).click();
|
||||||
getDeleteFolderModalConfirmButton().should('be.enabled').click();
|
getDeleteFolderModalConfirmButton().should('be.enabled').click();
|
||||||
cy.wait('@deleteFolder');
|
cy.wait('@deleteFolder');
|
||||||
successToast().should('contain.text', `Data transferred to "${destinationName}"`);
|
successToast().should('contain.text', `Data transferred to '${destinationName}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveFolder(folderName: string, destinationName: string) {
|
function moveFolder(folderName: string, destinationName: string) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
"generic.upgradeToEnterprise": "Upgrade to Enterprise",
|
"generic.upgradeToEnterprise": "Upgrade to Enterprise",
|
||||||
"generic.never": "Never",
|
"generic.never": "Never",
|
||||||
"generic.list.clearSelection": "Clear selection",
|
"generic.list.clearSelection": "Clear selection",
|
||||||
"generic.list.selected": "{count} row selected: | {count} rows selected:",
|
"generic.list.selected": "{count} row selected | {count} rows selected",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
@@ -971,17 +971,17 @@
|
|||||||
"folders.actions.moveToFolder": "Move",
|
"folders.actions.moveToFolder": "Move",
|
||||||
"folders.add": "Add folder",
|
"folders.add": "Add folder",
|
||||||
"folders.add.here.message": "Create a new folder here",
|
"folders.add.here.message": "Create a new folder here",
|
||||||
"folders.add.to.parent.message": "Create folder in \"{parent}\"",
|
"folders.add.to.parent.message": "Create folder in '{parent}'",
|
||||||
"folders.add.overview.community.message": "Folders available in your personal space",
|
"folders.add.overview.community.message": "Folders available in your personal space",
|
||||||
"folders.add.overview.withProjects.message": "Folders available in projects or your personal space",
|
"folders.add.overview.withProjects.message": "Folders available in projects or your personal space",
|
||||||
"folders.add.success.title": "Folder created",
|
"folders.add.success.title": "Folder created",
|
||||||
"folders.add.success.message": "Created new folder \"{folderName}\"<br><a href=\"{link}\">Open folder</a>",
|
"folders.add.success.message": "Created new folder '{folderName}'<br><a href=\"{link}\">Open folder</a>",
|
||||||
"folders.invalidName.empty.message": "Folder name cannot be empty",
|
"folders.invalidName.empty.message": "Folder name cannot be empty",
|
||||||
"folders.invalidName.tooLong.message": "Folder name cannot be longer than {maxLength} characters",
|
"folders.invalidName.tooLong.message": "Folder name cannot be longer than {maxLength} characters",
|
||||||
"folders.invalidName.invalidCharacters.message": "Folder name cannot contain the following characters: {illegalChars}",
|
"folders.invalidName.invalidCharacters.message": "Folder name cannot contain the following characters: {illegalChars}",
|
||||||
"folders.invalidName.starts.with.dot..message": "Folder name cannot start with a dot",
|
"folders.invalidName.starts.with.dot..message": "Folder name cannot start with a dot",
|
||||||
"folders.invalidName.only.dots.message": "Folder name cannot contain only dots",
|
"folders.invalidName.only.dots.message": "Folder name cannot contain only dots",
|
||||||
"folders.delete.confirm.title": "Delete \"{folderName}\"",
|
"folders.delete.confirm.title": "Delete '{folderName}'",
|
||||||
"folders.delete.typeToConfirm": "delete {folderName}",
|
"folders.delete.typeToConfirm": "delete {folderName}",
|
||||||
"folders.delete.confirm.message": "Are to sure you want to delete this folder?",
|
"folders.delete.confirm.message": "Are to sure you want to delete this folder?",
|
||||||
"folders.delete.success.message": "Folder deleted",
|
"folders.delete.success.message": "Folder deleted",
|
||||||
@@ -991,13 +991,13 @@
|
|||||||
"folder.and.workflow.separator": "and",
|
"folder.and.workflow.separator": "and",
|
||||||
"folders.delete.action": "Archive all workflows and delete subfolders",
|
"folders.delete.action": "Archive all workflows and delete subfolders",
|
||||||
"folders.delete.error.message": "Problem while deleting folder",
|
"folders.delete.error.message": "Problem while deleting folder",
|
||||||
"folders.delete.confirmation.message": "Type \"delete {folderName}\" to confirm",
|
"folders.delete.confirmation.message": "Type 'delete {folderName}' to confirm",
|
||||||
"folders.transfer.confirm.message": "Data transferred to \"{folderName}\"",
|
"folders.transfer.confirm.message": "Data transferred to '{folderName}'",
|
||||||
"folders.transfer.action": "Transfer workflows and subfolders to another folder inside \"{projectName}\"",
|
"folders.transfer.action": "Transfer workflows and subfolders to another folder inside '{projectName}'",
|
||||||
"folders.transfer.action.noProject": "Transfer workflows and subfolders to another folder",
|
"folders.transfer.action.noProject": "Transfer workflows and subfolders to another folder",
|
||||||
"folders.transfer.selectFolder": "Folder to transfer to",
|
"folders.transfer.selectFolder": "Folder to transfer to",
|
||||||
"folders.transfer.select.placeholder": "Select folder",
|
"folders.transfer.select.placeholder": "Select folder",
|
||||||
"folders.rename.message": "Rename \"{folderName}\"",
|
"folders.rename.message": "Rename '{folderName}'",
|
||||||
"folders.rename.error.title": "Problem renaming folder",
|
"folders.rename.error.title": "Problem renaming folder",
|
||||||
"folders.rename.success.message": "Folder renamed",
|
"folders.rename.success.message": "Folder renamed",
|
||||||
"folders.rename.placeholder": "Enter new folder name",
|
"folders.rename.placeholder": "Enter new folder name",
|
||||||
@@ -2844,8 +2844,7 @@
|
|||||||
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
|
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
|
||||||
"dataStore.dataStores": "Data tables",
|
"dataStore.dataStores": "Data tables",
|
||||||
"dataStore.empty.label": "You don't have any data tables yet",
|
"dataStore.empty.label": "You don't have any data tables yet",
|
||||||
"dataStore.empty.description": "Once you create data tables for your projects, they will appear here",
|
"dataStore.empty.description": "Use data tables to persist execution results, share data between workflows, and track metrics for evaluation.",
|
||||||
"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",
|
||||||
@@ -2856,13 +2855,12 @@
|
|||||||
"dataStore.search.placeholder": "Search",
|
"dataStore.search.placeholder": "Search",
|
||||||
"dataStore.error.fetching": "Error loading data tables",
|
"dataStore.error.fetching": "Error loading data tables",
|
||||||
"dataStore.add.title": "Create new Data table",
|
"dataStore.add.title": "Create new Data table",
|
||||||
"dataStore.add.description": "Set up a new data table to organize and manage your data.",
|
|
||||||
"dataStore.add.button.label": "Create Data table",
|
"dataStore.add.button.label": "Create Data table",
|
||||||
"dataStore.add.input.name.label": "Data table name",
|
"dataStore.add.input.name.label": "Data table name",
|
||||||
"dataStore.add.input.name.placeholder": "Enter data table name",
|
"dataStore.add.input.name.placeholder": "Enter data table name",
|
||||||
"dataStore.add.error": "Error creating data table",
|
"dataStore.add.error": "Error creating data table",
|
||||||
"dataStore.delete.confirm.title": "Delete Data table",
|
"dataStore.delete.confirm.title": "Delete Data table",
|
||||||
"dataStore.delete.confirm.message": "Are you sure you want to delete the data table \"{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 table",
|
"dataStore.delete.error": "Error deleting data table",
|
||||||
"dataStore.rename.error": "Error renaming data table",
|
"dataStore.rename.error": "Error renaming data table",
|
||||||
"dataStore.getDetails.error": "Error fetching data table details",
|
"dataStore.getDetails.error": "Error fetching data table details",
|
||||||
@@ -2875,10 +2873,12 @@
|
|||||||
"dataStore.addColumn.nameInput.placeholder": "Enter column name",
|
"dataStore.addColumn.nameInput.placeholder": "Enter column name",
|
||||||
"dataStore.addColumn.typeInput.label": "@:_reusableBaseText.type",
|
"dataStore.addColumn.typeInput.label": "@:_reusableBaseText.type",
|
||||||
"dataStore.addColumn.error": "Error adding column",
|
"dataStore.addColumn.error": "Error adding column",
|
||||||
|
"dataStore.addColumn.alreadyExistsError": "This column already exists",
|
||||||
|
"dataStore.addColumn.alreadyExistsDescription": "Column name already exisits, choose a different name",
|
||||||
"dataStore.moveColumn.error": "Error moving column",
|
"dataStore.moveColumn.error": "Error moving column",
|
||||||
"dataStore.deleteColumn.error": "Error deleting column",
|
"dataStore.deleteColumn.error": "Error deleting column",
|
||||||
"dataStore.deleteColumn.confirm.title": "Delete column",
|
"dataStore.deleteColumn.confirm.title": "Delete column",
|
||||||
"dataStore.deleteColumn.confirm.message": "Are you sure you want to delete the column \"{name}\"? This action cannot be undone.",
|
"dataStore.deleteColumn.confirm.message": "Are you sure you want to delete the column '{name}'? This action cannot be undone.",
|
||||||
"dataStore.addColumn.invalidName.error": "Invalid column name",
|
"dataStore.addColumn.invalidName.error": "Invalid column name",
|
||||||
"dataStore.addColumn.invalidName.description": "Column names must begin with a letter and can only include letters, numbers, or underscores",
|
"dataStore.addColumn.invalidName.description": "Column names must begin with a letter and can only include letters, numbers, or underscores",
|
||||||
"dataStore.fetchContent.error": "Error fetching data store content",
|
"dataStore.fetchContent.error": "Error fetching data store content",
|
||||||
@@ -2887,8 +2887,9 @@
|
|||||||
"dataStore.updateRow.error": "Error updating row",
|
"dataStore.updateRow.error": "Error updating row",
|
||||||
"dataStore.deleteRows.title": "Delete Rows",
|
"dataStore.deleteRows.title": "Delete Rows",
|
||||||
"dataStore.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
"dataStore.deleteRows.confirmation": "Are you sure you want to delete {count} row? | Are you sure you want to delete {count} rows?",
|
||||||
"dataStore.deleteRows.success": "Rows deleted successfully",
|
|
||||||
"dataStore.deleteRows.error": "Error deleting rows",
|
"dataStore.deleteRows.error": "Error deleting rows",
|
||||||
|
"dataStore.error.tableNotInitialized": "Table not initialized",
|
||||||
|
"dataStore.noRows": "No rows",
|
||||||
"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>",
|
||||||
|
|||||||
@@ -45,15 +45,17 @@ const handleClearSelection = () => {
|
|||||||
{{ getSelectedText() }}
|
{{ getSelectedText() }}
|
||||||
</span>
|
</span>
|
||||||
<N8nButton
|
<N8nButton
|
||||||
:label="i18n.baseText('generic.delete')"
|
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
data-test-id="delete-selected-button"
|
data-test-id="delete-selected-button"
|
||||||
|
:label="i18n.baseText('generic.delete')"
|
||||||
|
:class="$style.button"
|
||||||
@click="handleDeleteSelected"
|
@click="handleDeleteSelected"
|
||||||
/>
|
/>
|
||||||
<N8nButton
|
<N8nButton
|
||||||
:label="getClearSelectionText()"
|
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
data-test-id="clear-selection-button"
|
data-test-id="clear-selection-button"
|
||||||
|
:label="getClearSelectionText()"
|
||||||
|
:class="$style.button"
|
||||||
@click="handleClearSelection"
|
@click="handleClearSelection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,9 +75,11 @@ const handleClearSelection = () => {
|
|||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
color: var(--execution-selector-text);
|
color: var(--execution-selector-text);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
.button {
|
||||||
margin-left: var(--spacing-2xs);
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import type { DataStore, DataStoreColumnCreatePayload } from '@/features/dataStore/datastore.types';
|
import type {
|
||||||
|
AddColumnResponse,
|
||||||
|
DataStore,
|
||||||
|
DataStoreColumnCreatePayload,
|
||||||
|
} from '@/features/dataStore/datastore.types';
|
||||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
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';
|
||||||
@@ -82,9 +86,12 @@ const onToggleSave = (value: boolean) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
|
const onAddColumn = async (column: DataStoreColumnCreatePayload): Promise<AddColumnResponse> => {
|
||||||
if (!dataStoreTableRef.value) {
|
if (!dataStoreTableRef.value) {
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
errorMessage: i18n.baseText('dataStore.error.tableNotInitialized'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return await dataStoreTableRef.value.addColumn(column);
|
return await dataStoreTableRef.value.addColumn(column);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useInsightsStore } from '@/features/insights/insights.store';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { SortingAndPaginationUpdates } from '@/Interface';
|
import type { SortingAndPaginationUpdates } from '@/Interface';
|
||||||
import type { DataStoreResource } from '@/features/dataStore/types';
|
import type { DataStoreResource } from '@/features/dataStore/types';
|
||||||
@@ -61,23 +60,6 @@ const currentProject = computed(() => {
|
|||||||
return projectsStore.currentProject;
|
return projectsStore.currentProject;
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectName = computed(() => {
|
|
||||||
if (currentProject.value?.type === ProjectTypes.Personal) {
|
|
||||||
return i18n.baseText('projects.menu.personal');
|
|
||||||
}
|
|
||||||
return currentProject.value?.name ?? '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyCalloutDescription = computed(() => {
|
|
||||||
return projectPages.isOverviewSubPage ? i18n.baseText('dataStore.empty.description') : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyCalloutButtonText = computed(() => {
|
|
||||||
return i18n.baseText('dataStore.empty.button.label', {
|
|
||||||
interpolate: { projectName: projectName.value },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
@@ -160,8 +142,8 @@ watch(
|
|||||||
<n8n-action-box
|
<n8n-action-box
|
||||||
data-test-id="empty-shared-action-box"
|
data-test-id="empty-shared-action-box"
|
||||||
:heading="i18n.baseText('dataStore.empty.label')"
|
:heading="i18n.baseText('dataStore.empty.label')"
|
||||||
:description="emptyCalloutDescription"
|
:description="i18n.baseText('dataStore.empty.description')"
|
||||||
:button-text="emptyCalloutButtonText"
|
:button-text="i18n.baseText('dataStore.add.button.label')"
|
||||||
button-type="secondary"
|
button-type="secondary"
|
||||||
@click:button="onAddModalClick"
|
@click:button="onAddModalClick"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ const redirectToDataStores = () => {
|
|||||||
<template>
|
<template>
|
||||||
<Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataStores">
|
<Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataStores">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
|
<div :class="$style.header">
|
||||||
|
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div :class="$style.content">
|
<div :class="$style.content">
|
||||||
<p>{{ i18n.baseText('dataStore.add.description') }}</p>
|
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
:label="i18n.baseText('dataStore.add.input.name.label')"
|
:label="i18n.baseText('dataStore.add.input.name.label')"
|
||||||
:required="true"
|
:required="true"
|
||||||
@@ -95,7 +96,7 @@ const redirectToDataStores = () => {
|
|||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
:disabled="!dataStoreName"
|
:disabled="!dataStoreName"
|
||||||
:label="i18n.baseText('dataStore.add.button.label')"
|
:label="i18n.baseText('generic.create')"
|
||||||
data-test-id="confirm-add-data-store-button"
|
data-test-id="confirm-add-data-store-button"
|
||||||
@click="onSubmit"
|
@click="onSubmit"
|
||||||
/>
|
/>
|
||||||
@@ -111,10 +112,13 @@ const redirectToDataStores = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
|
.header {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|||||||
@@ -137,11 +137,12 @@ watch(
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.data-store-breadcrumbs {
|
.data-store-breadcrumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: end;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-store-actions {
|
.data-store-actions {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
top: var(--spacing-5xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const dataStoreRoute = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="$style['card-header']" @click.prevent>
|
<div :class="$style['card-header']">
|
||||||
<n8n-text tag="h2" bold data-test-id="data-store-card-name">
|
<n8n-text tag="h2" bold data-test-id="data-store-card-name">
|
||||||
{{ props.dataStore.name }}
|
{{ props.dataStore.name }}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AddColumnButton', () => {
|
describe('AddColumnButton', () => {
|
||||||
const addColumnHandler = vi.fn().mockResolvedValue(true);
|
const addColumnHandler = vi.fn().mockResolvedValue({ success: true });
|
||||||
const renderComponent = createComponentRenderer(AddColumnButton, {
|
const renderComponent = createComponentRenderer(AddColumnButton, {
|
||||||
props: {
|
props: {
|
||||||
params: {
|
params: {
|
||||||
@@ -228,7 +228,7 @@ describe('AddColumnButton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should close popover after successful submission', async () => {
|
it('should close popover after successful submission', async () => {
|
||||||
const { getByPlaceholderText, getByTestId, queryByText } = renderComponent();
|
const { getByPlaceholderText, getByTestId, queryByTestId } = renderComponent();
|
||||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||||
|
|
||||||
await fireEvent.click(addButton);
|
await fireEvent.click(addButton);
|
||||||
@@ -240,13 +240,16 @@ describe('AddColumnButton', () => {
|
|||||||
await fireEvent.click(submitButton);
|
await fireEvent.click(submitButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(queryByText('Column name')).not.toBeInTheDocument();
|
expect(queryByTestId('add-column-popover-content')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not close popover if submission fails', async () => {
|
it('should not close popover if submission fails', async () => {
|
||||||
const { getByPlaceholderText, getByTestId } = renderComponent();
|
const { getByPlaceholderText, getByTestId } = renderComponent();
|
||||||
addColumnHandler.mockResolvedValueOnce(false);
|
addColumnHandler.mockResolvedValueOnce({
|
||||||
|
success: false,
|
||||||
|
error: 'Column name already exists',
|
||||||
|
});
|
||||||
const addButton = getByTestId('data-store-add-column-trigger-button');
|
const addButton = getByTestId('data-store-add-column-trigger-button');
|
||||||
|
|
||||||
await fireEvent.click(addButton);
|
await fireEvent.click(addButton);
|
||||||
@@ -258,7 +261,7 @@ describe('AddColumnButton', () => {
|
|||||||
await fireEvent.click(submitButton);
|
await fireEvent.click(submitButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByTestId('data-store-add-column-submit-button')).toBeInTheDocument();
|
expect(getByTestId('add-column-popover-content')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
|
AddColumnResponse,
|
||||||
DataStoreColumnCreatePayload,
|
DataStoreColumnCreatePayload,
|
||||||
DataStoreColumnType,
|
DataStoreColumnType,
|
||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
@@ -10,10 +11,15 @@ import { COLUMN_NAME_REGEX, MAX_COLUMN_NAME_LENGTH } from '@/features/dataStore/
|
|||||||
import Tooltip from '@n8n/design-system/components/N8nTooltip/Tooltip.vue';
|
import Tooltip from '@n8n/design-system/components/N8nTooltip/Tooltip.vue';
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
|
||||||
|
type FormError = {
|
||||||
|
message?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
// the params key is needed so that we can pass this directly to ag-grid as column
|
// the params key is needed so that we can pass this directly to ag-grid as column
|
||||||
params: {
|
params: {
|
||||||
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<boolean>;
|
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||||
};
|
};
|
||||||
popoverId?: string;
|
popoverId?: string;
|
||||||
useTextTrigger?: boolean;
|
useTextTrigger?: boolean;
|
||||||
@@ -30,7 +36,7 @@ const columnType = ref<DataStoreColumnType>('string');
|
|||||||
|
|
||||||
const columnTypes: DataStoreColumnType[] = ['string', 'number', 'boolean', 'date'];
|
const columnTypes: DataStoreColumnType[] = ['string', 'number', 'boolean', 'date'];
|
||||||
|
|
||||||
const error = ref<string | null>(null);
|
const error = ref<FormError | null>(null);
|
||||||
|
|
||||||
// Handling popover state manually to prevent it closing when interacting with dropdown
|
// Handling popover state manually to prevent it closing when interacting with dropdown
|
||||||
const popoverOpen = ref(false);
|
const popoverOpen = ref(false);
|
||||||
@@ -43,11 +49,26 @@ const onAddButtonClicked = async () => {
|
|||||||
if (!columnName.value || !columnType.value || error.value) {
|
if (!columnName.value || !columnType.value || error.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const success = await props.params.onAddColumn({
|
const response = await props.params.onAddColumn({
|
||||||
name: columnName.value,
|
name: columnName.value,
|
||||||
type: columnType.value,
|
type: columnType.value,
|
||||||
});
|
});
|
||||||
if (!success) {
|
|
||||||
|
if (!response.success) {
|
||||||
|
let errorMessage = i18n.baseText('dataStore.addColumn.error');
|
||||||
|
let errorDescription = response.errorMessage;
|
||||||
|
// Provide custom error message for conflict (column already exists)
|
||||||
|
if (response.httpStatus === 409) {
|
||||||
|
errorMessage = i18n.baseText('dataStore.addColumn.alreadyExistsError', {
|
||||||
|
interpolate: { name: columnName.value },
|
||||||
|
});
|
||||||
|
errorDescription = i18n.baseText('dataStore.addColumn.alreadyExistsDescription');
|
||||||
|
}
|
||||||
|
error.value = {
|
||||||
|
message: errorMessage,
|
||||||
|
description: errorDescription,
|
||||||
|
};
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
columnName.value = '';
|
columnName.value = '';
|
||||||
@@ -74,7 +95,10 @@ const validateName = () => {
|
|||||||
error.value = null;
|
error.value = null;
|
||||||
}
|
}
|
||||||
if (columnName.value && !COLUMN_NAME_REGEX.test(columnName.value)) {
|
if (columnName.value && !COLUMN_NAME_REGEX.test(columnName.value)) {
|
||||||
error.value = i18n.baseText('dataStore.addColumn.invalidName.error');
|
error.value = {
|
||||||
|
message: i18n.baseText('dataStore.addColumn.invalidName.error'),
|
||||||
|
description: i18n.baseText('dataStore.addColumn.invalidName.description'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +131,10 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="add-ds-column-header-popover-content">
|
<div
|
||||||
|
class="add-ds-column-header-popover-content"
|
||||||
|
data-test-id="add-column-popover-content"
|
||||||
|
>
|
||||||
<div class="popover-body">
|
<div class="popover-body">
|
||||||
<N8nInputLabel
|
<N8nInputLabel
|
||||||
:label="i18n.baseText('dataStore.addColumn.nameInput.label')"
|
:label="i18n.baseText('dataStore.addColumn.nameInput.label')"
|
||||||
@@ -123,10 +150,14 @@ const onInput = debounce(validateName, { debounceTime: 100 });
|
|||||||
@input="onInput"
|
@input="onInput"
|
||||||
/>
|
/>
|
||||||
<div v-if="error" class="error-message">
|
<div v-if="error" class="error-message">
|
||||||
<n8n-text size="small" color="danger" tag="span">
|
<n8n-text v-if="error.message" size="small" color="danger" tag="span">
|
||||||
{{ error }}
|
{{ error.message }}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
<Tooltip :content="i18n.baseText('dataStore.addColumn.invalidName.description')">
|
<Tooltip
|
||||||
|
:content="error.description"
|
||||||
|
placement="top"
|
||||||
|
:disabled="!error.description"
|
||||||
|
>
|
||||||
<N8nIcon
|
<N8nIcon
|
||||||
icon="circle-help"
|
icon="circle-help"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useDataStorePagination } from '@/features/dataStore/composables/useData
|
|||||||
import { useDataStoreGridBase } from '@/features/dataStore/composables/useDataStoreGridBase';
|
import { useDataStoreGridBase } from '@/features/dataStore/composables/useDataStoreGridBase';
|
||||||
import { useDataStoreSelection } from '@/features/dataStore/composables/useDataStoreSelection';
|
import { useDataStoreSelection } from '@/features/dataStore/composables/useDataStoreSelection';
|
||||||
import { useDataStoreOperations } from '@/features/dataStore/composables/useDataStoreOperations';
|
import { useDataStoreOperations } from '@/features/dataStore/composables/useDataStoreOperations';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
// Register only the modules we actually use
|
// Register only the modules we actually use
|
||||||
ModuleRegistry.registerModules([
|
ModuleRegistry.registerModules([
|
||||||
@@ -66,6 +67,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const gridContainerRef = useTemplateRef<HTMLDivElement>('gridContainerRef');
|
const gridContainerRef = useTemplateRef<HTMLDivElement>('gridContainerRef');
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
const dataStoreGridBase = useDataStoreGridBase({
|
const dataStoreGridBase = useDataStoreGridBase({
|
||||||
gridContainerRef,
|
gridContainerRef,
|
||||||
onDeleteColumn: onDeleteColumnFunction,
|
onDeleteColumn: onDeleteColumnFunction,
|
||||||
@@ -75,7 +78,18 @@ const dataStoreGridBase = useDataStoreGridBase({
|
|||||||
const rowData = ref<DataStoreRow[]>([]);
|
const rowData = ref<DataStoreRow[]>([]);
|
||||||
const hasRecords = computed(() => rowData.value.length > 0);
|
const hasRecords = computed(() => rowData.value.length > 0);
|
||||||
|
|
||||||
const pagination = useDataStorePagination({ onChange: fetchDataStoreRowsFunction });
|
const {
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalItems,
|
||||||
|
pageSizeOptions,
|
||||||
|
ensureItemOnPage,
|
||||||
|
setTotalItems,
|
||||||
|
setCurrentPage,
|
||||||
|
setPageSize,
|
||||||
|
} = useDataStorePagination({
|
||||||
|
onChange: fetchDataStoreRowsFunction,
|
||||||
|
});
|
||||||
|
|
||||||
const selection = useDataStoreSelection({
|
const selection = useDataStoreSelection({
|
||||||
gridApi: dataStoreGridBase.gridApi,
|
gridApi: dataStoreGridBase.gridApi,
|
||||||
@@ -92,13 +106,13 @@ const dataStoreOperations = useDataStoreOperations({
|
|||||||
addGridColumn: dataStoreGridBase.addColumn,
|
addGridColumn: dataStoreGridBase.addColumn,
|
||||||
moveGridColumn: dataStoreGridBase.moveColumn,
|
moveGridColumn: dataStoreGridBase.moveColumn,
|
||||||
gridApi: dataStoreGridBase.gridApi,
|
gridApi: dataStoreGridBase.gridApi,
|
||||||
totalItems: pagination.totalItems,
|
totalItems,
|
||||||
setTotalItems: pagination.setTotalItems,
|
setTotalItems,
|
||||||
ensureItemOnPage: pagination.ensureItemOnPage,
|
ensureItemOnPage,
|
||||||
focusFirstEditableCell: dataStoreGridBase.focusFirstEditableCell,
|
focusFirstEditableCell: dataStoreGridBase.focusFirstEditableCell,
|
||||||
toggleSave: emit.bind(null, 'toggleSave'),
|
toggleSave: emit.bind(null, 'toggleSave'),
|
||||||
currentPage: pagination.currentPage,
|
currentPage,
|
||||||
pageSize: pagination.pageSize,
|
pageSize,
|
||||||
currentSortBy: dataStoreGridBase.currentSortBy,
|
currentSortBy: dataStoreGridBase.currentSortBy,
|
||||||
currentSortOrder: dataStoreGridBase.currentSortOrder,
|
currentSortOrder: dataStoreGridBase.currentSortOrder,
|
||||||
handleClearSelection: selection.handleClearSelection,
|
handleClearSelection: selection.handleClearSelection,
|
||||||
@@ -128,8 +142,10 @@ const initialize = async (params: GridReadyEvent) => {
|
|||||||
await dataStoreOperations.fetchDataStoreRows();
|
await dataStoreOperations.fetchDataStoreRows();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customNoRowsOverlay = `<div class="no-rows-overlay ag-overlay-no-rows-center" data-test-id="data-store-no-rows-overlay">${i18n.baseText('dataStore.noRows')}</div>`;
|
||||||
|
|
||||||
watch([dataStoreGridBase.currentSortBy, dataStoreGridBase.currentSortOrder], async () => {
|
watch([dataStoreGridBase.currentSortBy, dataStoreGridBase.currentSortOrder], async () => {
|
||||||
await pagination.setCurrentPage(1);
|
await setCurrentPage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -159,6 +175,7 @@ defineExpose({
|
|||||||
:stop-editing-when-cells-lose-focus="true"
|
:stop-editing-when-cells-lose-focus="true"
|
||||||
:undo-redo-cell-editing="true"
|
:undo-redo-cell-editing="true"
|
||||||
:suppress-multi-sort="true"
|
:suppress-multi-sort="true"
|
||||||
|
:overlay-no-rows-template="customNoRowsOverlay"
|
||||||
@grid-ready="initialize"
|
@grid-ready="initialize"
|
||||||
@cell-value-changed="dataStoreOperations.onCellValueChanged"
|
@cell-value-changed="dataStoreOperations.onCellValueChanged"
|
||||||
@column-moved="dataStoreOperations.onColumnMoved"
|
@column-moved="dataStoreOperations.onColumnMoved"
|
||||||
@@ -173,15 +190,15 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pagination.currentPage"
|
v-model:current-page="currentPage"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pageSize"
|
||||||
data-test-id="data-store-content-pagination"
|
data-test-id="data-store-content-pagination"
|
||||||
background
|
background
|
||||||
:total="pagination.totalItems"
|
:total="totalItems"
|
||||||
:page-sizes="pagination.pageSizeOptions"
|
:page-sizes="pageSizeOptions"
|
||||||
layout="total, prev, pager, next, sizes"
|
layout="total, prev, pager, next, sizes"
|
||||||
@update:current-page="pagination.setCurrentPage"
|
@update:current-page="setCurrentPage"
|
||||||
@size-change="pagination.setPageSize"
|
@size-change="setPageSize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectedItemsInfo
|
<SelectedItemsInfo
|
||||||
@@ -263,7 +280,11 @@ defineExpose({
|
|||||||
|
|
||||||
:global(.id-column) {
|
:global(.id-column) {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
justify-content: center;
|
}
|
||||||
|
|
||||||
|
:global(.system-column),
|
||||||
|
:global(.system-cell) {
|
||||||
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.ag-header-cell[col-id='id']) {
|
:global(.ag-header-cell[col-id='id']) {
|
||||||
@@ -273,6 +294,12 @@ defineExpose({
|
|||||||
:global(.add-row-cell) {
|
:global(.add-row-cell) {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
left: calc(var(--spacing-4xs) * -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.ag-header-cell[col-id='add-column']) {
|
:global(.ag-header-cell[col-id='add-column']) {
|
||||||
@@ -298,7 +325,7 @@ defineExpose({
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
padding-top: var(--spacing-xs);
|
padding-top: var(--spacing-2xs);
|
||||||
|
|
||||||
&:where(:focus-within, :active) {
|
&:where(:focus-within, :active) {
|
||||||
border: var(--grid-cell-editing-border);
|
border: var(--grid-cell-editing-border);
|
||||||
@@ -341,6 +368,10 @@ defineExpose({
|
|||||||
:global(.ag-row[row-id='__n8n_add_row__']) {
|
:global(.ag-row[row-id='__n8n_add_row__']) {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.ag-row-last) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ defineExpose({
|
|||||||
:editable="false"
|
:editable="false"
|
||||||
:teleported="false"
|
:teleported="false"
|
||||||
:placeholder="''"
|
:placeholder="''"
|
||||||
|
size="small"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@clear="onClear"
|
@clear="onClear"
|
||||||
@keydown="onKeydown"
|
@keydown="onKeydown"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
SortDirection,
|
SortDirection,
|
||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import type {
|
import type {
|
||||||
|
AddColumnResponse,
|
||||||
DataStoreColumn,
|
DataStoreColumn,
|
||||||
DataStoreColumnCreatePayload,
|
DataStoreColumnCreatePayload,
|
||||||
DataStoreRow,
|
DataStoreRow,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
createStringValueSetter,
|
createStringValueSetter,
|
||||||
stringCellEditorParams,
|
stringCellEditorParams,
|
||||||
dateValueFormatter,
|
dateValueFormatter,
|
||||||
|
numberValueFormatter,
|
||||||
} from '@/features/dataStore/utils/columnUtils';
|
} from '@/features/dataStore/utils/columnUtils';
|
||||||
|
|
||||||
export const useDataStoreGridBase = ({
|
export const useDataStoreGridBase = ({
|
||||||
@@ -48,7 +50,7 @@ export const useDataStoreGridBase = ({
|
|||||||
gridContainerRef: Ref<HTMLElement | null>;
|
gridContainerRef: Ref<HTMLElement | null>;
|
||||||
onDeleteColumn: (columnId: string) => void;
|
onDeleteColumn: (columnId: string) => void;
|
||||||
onAddRowClick: () => void;
|
onAddRowClick: () => void;
|
||||||
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<boolean>;
|
onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<AddColumnResponse>;
|
||||||
}) => {
|
}) => {
|
||||||
const gridApi = ref<GridApi | null>(null);
|
const gridApi = ref<GridApi | null>(null);
|
||||||
const colDefs = ref<ColDef[]>([]);
|
const colDefs = ref<ColDef[]>([]);
|
||||||
@@ -98,15 +100,21 @@ export const useDataStoreGridBase = ({
|
|||||||
const focusFirstEditableCell = (rowId: number) => {
|
const focusFirstEditableCell = (rowId: number) => {
|
||||||
const rowNode = initializedGridApi.value.getRowNode(String(rowId));
|
const rowNode = initializedGridApi.value.getRowNode(String(rowId));
|
||||||
if (rowNode?.rowIndex === null) return;
|
if (rowNode?.rowIndex === null) return;
|
||||||
|
const rowIndex = rowNode!.rowIndex;
|
||||||
|
|
||||||
const firstEditableCol = colDefs.value[1];
|
const firstEditableCol = colDefs.value[1];
|
||||||
if (!firstEditableCol?.colId) return;
|
if (!firstEditableCol?.colId) return;
|
||||||
|
const columnId = firstEditableCol.colId;
|
||||||
|
|
||||||
initializedGridApi.value.ensureIndexVisible(rowNode!.rowIndex);
|
requestAnimationFrame(() => {
|
||||||
initializedGridApi.value.setFocusedCell(rowNode!.rowIndex, firstEditableCol.colId);
|
initializedGridApi.value.ensureIndexVisible(rowIndex);
|
||||||
initializedGridApi.value.startEditingCell({
|
requestAnimationFrame(() => {
|
||||||
rowIndex: rowNode!.rowIndex,
|
initializedGridApi.value.setFocusedCell(rowIndex, columnId);
|
||||||
colKey: firstEditableCol.colId,
|
initializedGridApi.value.startEditingCell({
|
||||||
|
rowIndex,
|
||||||
|
colKey: columnId,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,6 +148,8 @@ export const useDataStoreGridBase = ({
|
|||||||
component: ElDatePickerCellEditor,
|
component: ElDatePickerCellEditor,
|
||||||
});
|
});
|
||||||
columnDef.valueFormatter = dateValueFormatter;
|
columnDef.valueFormatter = dateValueFormatter;
|
||||||
|
} else if (col.type === 'number') {
|
||||||
|
columnDef.valueFormatter = numberValueFormatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -171,6 +181,8 @@ export const useDataStoreGridBase = ({
|
|||||||
headerComponentParams: {
|
headerComponentParams: {
|
||||||
allowMenuActions: false,
|
allowMenuActions: false,
|
||||||
},
|
},
|
||||||
|
cellClass: (params) => (params.data?.id === ADD_ROW_ROW_ID ? 'add-row-cell' : 'system-cell'),
|
||||||
|
headerClass: 'system-column',
|
||||||
};
|
};
|
||||||
return [
|
return [
|
||||||
// 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
|
||||||
@@ -184,13 +196,14 @@ export const useDataStoreGridBase = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
editable: false,
|
editable: false,
|
||||||
sortable: false,
|
sortable: true,
|
||||||
suppressMovable: true,
|
suppressMovable: true,
|
||||||
headerComponent: null,
|
headerComponent: null,
|
||||||
lockPosition: true,
|
lockPosition: true,
|
||||||
minWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
minWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
||||||
maxWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
maxWidth: DATA_STORE_ID_COLUMN_WIDTH,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
|
headerClass: 'system-column',
|
||||||
cellClass: (params) =>
|
cellClass: (params) =>
|
||||||
params.data?.id === ADD_ROW_ROW_ID ? 'add-row-cell' : 'id-column',
|
params.data?.id === ADD_ROW_ROW_ID ? 'add-row-cell' : 'id-column',
|
||||||
cellRendererSelector: (params: ICellRendererParams) => {
|
cellRendererSelector: (params: ICellRendererParams) => {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('useDataStoreOperations', () => {
|
|||||||
});
|
});
|
||||||
const { onAddColumn } = useDataStoreOperations(params);
|
const { onAddColumn } = useDataStoreOperations(params);
|
||||||
const result = await onAddColumn({ name: 'test', type: 'string' });
|
const result = await onAddColumn({ name: 'test', type: 'string' });
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add column when column is added', async () => {
|
it('should add column when column is added', async () => {
|
||||||
@@ -78,7 +78,7 @@ describe('useDataStoreOperations', () => {
|
|||||||
const rowData = ref([{ id: 1 }]);
|
const rowData = ref([{ id: 1 }]);
|
||||||
const { onAddColumn } = useDataStoreOperations({ ...params, rowData });
|
const { onAddColumn } = useDataStoreOperations({ ...params, rowData });
|
||||||
const result = await onAddColumn({ name: returnedColumn.name, type: returnedColumn.type });
|
const result = await onAddColumn({ name: returnedColumn.name, type: returnedColumn.type });
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(params.setGridData).toHaveBeenCalledWith({ rowData: [{ id: 1, test: null }] });
|
expect(params.setGridData).toHaveBeenCalledWith({ rowData: [{ id: 1, test: null }] });
|
||||||
expect(params.addGridColumn).toHaveBeenCalledWith(returnedColumn);
|
expect(params.addGridColumn).toHaveBeenCalledWith(returnedColumn);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMessage } from '@/composables/useMessage';
|
|||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import type {
|
import type {
|
||||||
|
AddColumnResponse,
|
||||||
DataStoreColumn,
|
DataStoreColumn,
|
||||||
DataStoreColumnCreatePayload,
|
DataStoreColumnCreatePayload,
|
||||||
DataStoreRow,
|
DataStoreRow,
|
||||||
@@ -18,6 +19,7 @@ import type {
|
|||||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
import { MODAL_CONFIRM } from '@/constants';
|
import { MODAL_CONFIRM } from '@/constants';
|
||||||
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
||||||
|
import { useDataStoreTypes } from './useDataStoreTypes';
|
||||||
|
|
||||||
export type UseDataStoreOperationsParams = {
|
export type UseDataStoreOperationsParams = {
|
||||||
colDefs: Ref<ColDef[]>;
|
colDefs: Ref<ColDef[]>;
|
||||||
@@ -74,6 +76,7 @@ export const useDataStoreOperations = ({
|
|||||||
const dataStoreStore = useDataStoreStore();
|
const dataStoreStore = useDataStoreStore();
|
||||||
const contentLoading = ref(false);
|
const contentLoading = ref(false);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const dataStoreTypes = useDataStoreTypes();
|
||||||
|
|
||||||
async function onDeleteColumn(columnId: string) {
|
async function onDeleteColumn(columnId: string) {
|
||||||
const columnToDelete = colDefs.value.find((col) => col.colId === columnId);
|
const columnToDelete = colDefs.value.find((col) => col.colId === columnId);
|
||||||
@@ -117,7 +120,7 @@ export const useDataStoreOperations = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAddColumn(column: DataStoreColumnCreatePayload) {
|
async function onAddColumn(column: DataStoreColumnCreatePayload): Promise<AddColumnResponse> {
|
||||||
try {
|
try {
|
||||||
const newColumn = await dataStoreStore.addDataStoreColumn(dataStoreId, projectId, column);
|
const newColumn = await dataStoreStore.addDataStoreColumn(dataStoreId, projectId, column);
|
||||||
addGridColumn(newColumn);
|
addGridColumn(newColumn);
|
||||||
@@ -130,10 +133,14 @@ export const useDataStoreOperations = ({
|
|||||||
column_type: newColumn.type,
|
column_type: newColumn.type,
|
||||||
data_table_id: dataStoreId,
|
data_table_id: dataStoreId,
|
||||||
});
|
});
|
||||||
return true;
|
return { success: true, httpStatus: 200 };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
|
const addColumnError = dataStoreTypes.getAddColumnError(error);
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
httpStatus: addColumnError.httpStatus,
|
||||||
|
errorMessage: addColumnError.message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +277,6 @@ export const useDataStoreOperations = ({
|
|||||||
await dataStoreStore.deleteRows(dataStoreId, projectId, idsToDelete);
|
await dataStoreStore.deleteRows(dataStoreId, projectId, idsToDelete);
|
||||||
await fetchDataStoreRows();
|
await fetchDataStoreRows();
|
||||||
|
|
||||||
toast.showToast({
|
|
||||||
title: i18n.baseText('dataStore.deleteRows.success'),
|
|
||||||
message: '',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
telemetry.track('User deleted rows in data table', {
|
telemetry.track('User deleted rows in data table', {
|
||||||
data_table_id: dataStoreId,
|
data_table_id: dataStoreId,
|
||||||
deleted_row_count: idsToDelete.length,
|
deleted_row_count: idsToDelete.length,
|
||||||
@@ -287,17 +289,28 @@ export const useDataStoreOperations = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCellKeyDown = async (params: CellKeyDownEvent<DataStoreRow>) => {
|
const onCellKeyDown = async (params: CellKeyDownEvent<DataStoreRow>) => {
|
||||||
if (params.api.getEditingCells().length > 0) {
|
const event = params.event as KeyboardEvent;
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
|
const isSelectionColumn = params.column.getColId() === 'ag-Grid-SelectionColumn';
|
||||||
|
const isEditing =
|
||||||
|
params.api.getEditingCells().length > 0 ||
|
||||||
|
(target instanceof HTMLInputElement && !isSelectionColumn);
|
||||||
|
if (isEditing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = params.event as KeyboardEvent;
|
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await handleCopyFocusedCell(params);
|
await handleCopyFocusedCell(params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
handleClearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((event.key !== 'Delete' && event.key !== 'Backspace') || selectedRowIds.value.size === 0) {
|
if ((event.key !== 'Delete' && event.key !== 'Backspace') || selectedRowIds.value.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type {
|
|||||||
DataStoreValue,
|
DataStoreValue,
|
||||||
} from '@/features/dataStore/datastore.types';
|
} from '@/features/dataStore/datastore.types';
|
||||||
import { isAGGridCellType } from '@/features/dataStore/typeGuards';
|
import { isAGGridCellType } from '@/features/dataStore/typeGuards';
|
||||||
|
import { ResponseError } from '@n8n/rest-api-client';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
/* eslint-disable id-denylist */
|
/* eslint-disable id-denylist */
|
||||||
const COLUMN_TYPE_ICONS: Record<DataStoreColumnType, IconName> = {
|
const COLUMN_TYPE_ICONS: Record<DataStoreColumnType, IconName> = {
|
||||||
@@ -17,6 +19,7 @@ const COLUMN_TYPE_ICONS: Record<DataStoreColumnType, IconName> = {
|
|||||||
|
|
||||||
export const useDataStoreTypes = () => {
|
export const useDataStoreTypes = () => {
|
||||||
const getIconForType = (type: DataStoreColumnType) => COLUMN_TYPE_ICONS[type];
|
const getIconForType = (type: DataStoreColumnType) => COLUMN_TYPE_ICONS[type];
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a DataStoreColumnType to an AGGridCellType.
|
* Maps a DataStoreColumnType to an AGGridCellType.
|
||||||
@@ -57,10 +60,33 @@ export const useDataStoreTypes = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAddColumnError = (error: unknown): { httpStatus: number; message: string } => {
|
||||||
|
const DEFAULT_HTTP_STATUS = 500;
|
||||||
|
const DEFAULT_MESSAGE = i18n.baseText('generic.unknownError');
|
||||||
|
|
||||||
|
if (error instanceof ResponseError) {
|
||||||
|
return {
|
||||||
|
httpStatus: error.httpStatusCode ?? 500,
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
httpStatus: DEFAULT_HTTP_STATUS,
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
httpStatus: DEFAULT_HTTP_STATUS,
|
||||||
|
message: DEFAULT_MESSAGE,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getIconForType,
|
getIconForType,
|
||||||
mapToAGCellType,
|
mapToAGCellType,
|
||||||
mapToDataStoreColumnType,
|
mapToDataStoreColumnType,
|
||||||
getDefaultValueForType,
|
getDefaultValueForType,
|
||||||
|
getAddColumnError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export const DATA_STORE_STORE = 'dataStoreStore';
|
|||||||
|
|
||||||
export const DEFAULT_DATA_STORE_PAGE_SIZE = 10;
|
export const DEFAULT_DATA_STORE_PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const DATA_STORE_ID_COLUMN_WIDTH = 46;
|
export const DATA_STORE_ID_COLUMN_WIDTH = 60;
|
||||||
|
|
||||||
export const DATA_STORE_HEADER_HEIGHT = 36;
|
export const DATA_STORE_HEADER_HEIGHT = 36;
|
||||||
export const DATA_STORE_ROW_HEIGHT = 43;
|
export const DATA_STORE_ROW_HEIGHT = 33;
|
||||||
|
|
||||||
export const ADD_ROW_ROW_ID = '__n8n_add_row__';
|
export const ADD_ROW_ROW_ID = '__n8n_add_row__';
|
||||||
|
|
||||||
@@ -35,3 +35,7 @@ export const NULL_VALUE = 'Null';
|
|||||||
export const EMPTY_VALUE = 'Empty';
|
export const EMPTY_VALUE = 'Empty';
|
||||||
|
|
||||||
export const DATA_STORE_MODULE_NAME = 'data-table';
|
export const DATA_STORE_MODULE_NAME = 'data-table';
|
||||||
|
|
||||||
|
export const NUMBER_WITH_SPACES_REGEX = /\B(?=(\d{3})+(?!\d))/g;
|
||||||
|
export const NUMBER_THOUSAND_SEPARATOR = ' ';
|
||||||
|
export const NUMBER_DECIMAL_SEPARATOR = '.';
|
||||||
|
|||||||
@@ -27,3 +27,9 @@ export type DataStoreColumnCreatePayload = Pick<DataStoreColumn, 'name' | 'type'
|
|||||||
export type DataStoreValue = string | number | boolean | Date | null;
|
export type DataStoreValue = string | number | boolean | Date | null;
|
||||||
|
|
||||||
export type DataStoreRow = Record<string, DataStoreValue>;
|
export type DataStoreRow = Record<string, DataStoreValue>;
|
||||||
|
|
||||||
|
export type AddColumnResponse = {
|
||||||
|
success: boolean;
|
||||||
|
httpStatus?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import type {
|
|||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { DataStoreColumn, DataStoreRow } from '@/features/dataStore/datastore.types';
|
import type { DataStoreColumn, DataStoreRow } from '@/features/dataStore/datastore.types';
|
||||||
import { ADD_ROW_ROW_ID, EMPTY_VALUE, NULL_VALUE } from '@/features/dataStore/constants';
|
import {
|
||||||
|
ADD_ROW_ROW_ID,
|
||||||
|
EMPTY_VALUE,
|
||||||
|
NULL_VALUE,
|
||||||
|
NUMBER_DECIMAL_SEPARATOR,
|
||||||
|
NUMBER_THOUSAND_SEPARATOR,
|
||||||
|
NUMBER_WITH_SPACES_REGEX,
|
||||||
|
} from '@/features/dataStore/constants';
|
||||||
import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
|
import NullEmptyCellRenderer from '@/features/dataStore/components/dataGrid/NullEmptyCellRenderer.vue';
|
||||||
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
import { isDataStoreValue } from '@/features/dataStore/typeGuards';
|
||||||
|
|
||||||
@@ -93,3 +100,17 @@ export const dateValueFormatter = (
|
|||||||
if (value === null || value === undefined) return '';
|
if (value === null || value === undefined) return '';
|
||||||
return value.toISOString();
|
return value.toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const numberWithSpaces = (num: number) => {
|
||||||
|
const parts = num.toString().split('.');
|
||||||
|
parts[0] = parts[0].replace(NUMBER_WITH_SPACES_REGEX, NUMBER_THOUSAND_SEPARATOR);
|
||||||
|
return parts.join(NUMBER_DECIMAL_SEPARATOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const numberValueFormatter = (
|
||||||
|
params: ValueFormatterParams<DataStoreRow, number | null | undefined>,
|
||||||
|
): string => {
|
||||||
|
const value = params.value;
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return numberWithSpaces(value);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user