mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 19:11:13 +00:00
feat(editor): Address data tables UI feedback (no-changelog) (#18566)
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
@@ -2837,7 +2837,7 @@
|
|||||||
"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": "Once you create data tables for your projects, they will appear here",
|
||||||
"dataStore.empty.button.label": "Create data table 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",
|
||||||
@@ -2847,9 +2847,9 @@
|
|||||||
"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 tables",
|
"dataStore.error.fetching": "Error loading data tables",
|
||||||
"dataStore.add.title": "Create 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.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",
|
||||||
|
|||||||
@@ -44,10 +44,27 @@ const projectTabsSpy = vi.fn().mockReturnValue({
|
|||||||
render: vi.fn(),
|
render: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ProjectCreateResourceStub = {
|
||||||
|
props: {
|
||||||
|
actions: Array,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<div data-test-id="add-resource"><button role="button"></button></div>
|
||||||
|
<button data-test-id="add-resource-workflow" @click="$emit('action', 'workflow')">Workflow</button>
|
||||||
|
<button data-test-id="action-credential" @click="$emit('action', 'credential')">Credentials</button>
|
||||||
|
<div data-test-id="add-resource-actions" >
|
||||||
|
<button v-for="action in $props.actions" :key="action.value"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(ProjectHeader, {
|
const renderComponent = createComponentRenderer(ProjectHeader, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
ProjectTabs: projectTabsSpy,
|
ProjectTabs: projectTabsSpy,
|
||||||
|
ProjectCreateResource: ProjectCreateResourceStub,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -69,6 +86,7 @@ describe('ProjectHeader', () => {
|
|||||||
|
|
||||||
projectsStore.teamProjectsLimit = -1;
|
projectsStore.teamProjectsLimit = -1;
|
||||||
settingsStore.settings.folders = { enabled: false };
|
settingsStore.settings.folders = { enabled: false };
|
||||||
|
settingsStore.isDataStoreFeatureEnabled = true;
|
||||||
|
|
||||||
// Setup default moduleTabs structure
|
// Setup default moduleTabs structure
|
||||||
uiStore.moduleTabs = {
|
uiStore.moduleTabs = {
|
||||||
@@ -436,4 +454,21 @@ describe('ProjectHeader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ProjectCreateResource', () => {
|
||||||
|
it('should render menu items', () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
const actionsContainer = getByTestId('add-resource-actions');
|
||||||
|
expect(actionsContainer).toBeInTheDocument();
|
||||||
|
expect(actionsContainer.children).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render datastore menu item if data store feature is disabled', () => {
|
||||||
|
settingsStore.isDataStoreFeatureEnabled = false;
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
const actionsContainer = getByTestId('add-resource-actions');
|
||||||
|
expect(actionsContainer).toBeInTheDocument();
|
||||||
|
expect(actionsContainer.children).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,12 +20,7 @@ import { type IconName } from '@n8n/design-system/components/N8nIcon/icons';
|
|||||||
import type { IUser } from 'n8n-workflow';
|
import type { IUser } from 'n8n-workflow';
|
||||||
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
|
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||||
export type CustomAction = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -37,17 +32,8 @@ const uiStore = useUIStore();
|
|||||||
|
|
||||||
const projectPages = useProjectPages();
|
const projectPages = useProjectPages();
|
||||||
|
|
||||||
type Props = {
|
|
||||||
customActions?: CustomAction[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
customActions: () => [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
createFolder: [];
|
createFolder: [];
|
||||||
customActionSelected: [actionId: string, projectId: string];
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const headerIcon = computed((): IconOrEmoji => {
|
const headerIcon = computed((): IconOrEmoji => {
|
||||||
@@ -122,6 +108,7 @@ const ACTION_TYPES = {
|
|||||||
WORKFLOW: 'workflow',
|
WORKFLOW: 'workflow',
|
||||||
CREDENTIAL: 'credential',
|
CREDENTIAL: 'credential',
|
||||||
FOLDER: 'folder',
|
FOLDER: 'folder',
|
||||||
|
DATA_STORE: 'dataStore',
|
||||||
} as const;
|
} as const;
|
||||||
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
|
type ActionTypes = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES];
|
||||||
|
|
||||||
@@ -155,16 +142,17 @@ const menu = computed(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append custom actions
|
if (settingsStore.isDataStoreFeatureEnabled) {
|
||||||
if (props.customActions?.length) {
|
// TODO: this should probably be moved to the module descriptor as a setting
|
||||||
props.customActions.forEach((customAction) => {
|
|
||||||
items.push({
|
items.push({
|
||||||
value: customAction.id,
|
value: ACTION_TYPES.DATA_STORE,
|
||||||
label: customAction.label,
|
label: i18n.baseText('dataStore.add.button.label'),
|
||||||
disabled: customAction.disabled ?? false,
|
disabled:
|
||||||
});
|
sourceControlStore.preferences.branchReadOnly ||
|
||||||
|
!getResourcePermissions(homeProject.value?.scopes)?.dataStore?.create,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +184,12 @@ const actions: Record<ActionTypes, (projectId: string) => void> = {
|
|||||||
[ACTION_TYPES.FOLDER]: () => {
|
[ACTION_TYPES.FOLDER]: () => {
|
||||||
emit('createFolder');
|
emit('createFolder');
|
||||||
},
|
},
|
||||||
|
[ACTION_TYPES.DATA_STORE]: (projectId: string) => {
|
||||||
|
void router.push({
|
||||||
|
name: PROJECT_DATA_STORES,
|
||||||
|
params: { projectId, new: 'new' },
|
||||||
|
});
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const pageType = computed(() => {
|
const pageType = computed(() => {
|
||||||
@@ -267,12 +261,6 @@ const onSelect = (action: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a custom action
|
|
||||||
if (!executableAction) {
|
|
||||||
emit('customActionSelected', action, homeProject.value.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
executableAction(homeProject.value.id);
|
executableAction(homeProject.value.id);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProjectHeader, { type CustomAction } from '@/components/Projects/ProjectHeader.vue';
|
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||||
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
|
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
import { useProjectPages } from '@/composables/useProjectPages';
|
import { useProjectPages } from '@/composables/useProjectPages';
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
|
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
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';
|
||||||
@@ -17,6 +16,7 @@ import { useSourceControlStore } from '@/stores/sourceControl.store';
|
|||||||
import {
|
import {
|
||||||
ADD_DATA_STORE_MODAL_KEY,
|
ADD_DATA_STORE_MODAL_KEY,
|
||||||
DEFAULT_DATA_STORE_PAGE_SIZE,
|
DEFAULT_DATA_STORE_PAGE_SIZE,
|
||||||
|
PROJECT_DATA_STORES,
|
||||||
} from '@/features/dataStore/constants';
|
} from '@/features/dataStore/constants';
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
@@ -26,6 +26,7 @@ import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
|||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const projectPages = useProjectPages();
|
const projectPages = useProjectPages();
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
@@ -35,20 +36,13 @@ const dataStoreStore = useDataStoreStore();
|
|||||||
const insightsStore = useInsightsStore();
|
const insightsStore = useInsightsStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const sourceControlStore = useSourceControlStore();
|
const sourceControlStore = useSourceControlStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(DEFAULT_DATA_STORE_PAGE_SIZE);
|
const pageSize = ref(DEFAULT_DATA_STORE_PAGE_SIZE);
|
||||||
|
|
||||||
const customProjectActions = computed<CustomAction[]>(() => [
|
|
||||||
{
|
|
||||||
id: 'add-data-store',
|
|
||||||
label: i18n.baseText('dataStore.add.button.label'),
|
|
||||||
disabled: loading.value || projectPages.isOverviewSubPage,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const dataStoreResources = computed<DataStoreResource[]>(() =>
|
const dataStoreResources = computed<DataStoreResource[]>(() =>
|
||||||
dataStoreStore.dataStores.map((ds) => {
|
dataStoreStore.dataStores.map((ds) => {
|
||||||
return {
|
return {
|
||||||
@@ -58,6 +52,10 @@ const dataStoreResources = computed<DataStoreResource[]>(() =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const projectId = computed(() => {
|
||||||
|
return Array.isArray(route.params.projectId) ? route.params.projectId[0] : route.params.projectId;
|
||||||
|
});
|
||||||
|
|
||||||
const totalCount = computed(() => dataStoreStore.totalCount);
|
const totalCount = computed(() => dataStoreStore.totalCount);
|
||||||
|
|
||||||
const currentProject = computed(() => projectsStore.currentProject);
|
const currentProject = computed(() => projectsStore.currentProject);
|
||||||
@@ -86,11 +84,8 @@ const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly
|
|||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const projectId = Array.isArray(route.params.projectId)
|
|
||||||
? route.params.projectId[0]
|
|
||||||
: route.params.projectId;
|
|
||||||
try {
|
try {
|
||||||
await dataStoreStore.fetchDataStores(projectId, currentPage.value, pageSize.value);
|
await dataStoreStore.fetchDataStores(projectId.value, currentPage.value, pageSize.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, 'Error loading data stores');
|
toast.showError(error, 'Error loading data stores');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -111,18 +106,27 @@ const onPaginationUpdate = async (payload: SortingAndPaginationUpdates) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddModalClick = () => {
|
const onAddModalClick = () => {
|
||||||
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY);
|
void router.push({
|
||||||
};
|
name: PROJECT_DATA_STORES,
|
||||||
|
params: { projectId: projectId.value, new: 'new' },
|
||||||
const onProjectHeaderAction = (action: string) => {
|
});
|
||||||
if (action === 'add-data-store') {
|
|
||||||
useUIStore().openModal(ADD_DATA_STORE_MODAL_KEY);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
documentTitle.set(i18n.baseText('dataStore.dataStores'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.new,
|
||||||
|
() => {
|
||||||
|
if (route.params.new === 'new') {
|
||||||
|
uiStore.openModal(ADD_DATA_STORE_MODAL_KEY);
|
||||||
|
} else {
|
||||||
|
uiStore.closeModal(ADD_DATA_STORE_MODAL_KEY);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ResourcesListLayout
|
<ResourcesListLayout
|
||||||
@@ -144,10 +148,7 @@ onMounted(() => {
|
|||||||
@update:pagination-and-sort="onPaginationUpdate"
|
@update:pagination-and-sort="onPaginationUpdate"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader
|
<ProjectHeader>
|
||||||
:custom-actions="customProjectActions"
|
|
||||||
@custom-action-selected="onProjectHeaderAction"
|
|
||||||
>
|
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="projectPages.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
v-if="projectPages.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
||||||
:loading="insightsStore.weeklySummary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { DATA_STORE_DETAILS, PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modalName: string;
|
modalName: string;
|
||||||
@@ -16,6 +17,7 @@ const dataStoreStore = useDataStoreStore();
|
|||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -31,18 +33,35 @@ onMounted(() => {
|
|||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await dataStoreStore.createDataStore(dataStoreName.value, route.params.projectId as string);
|
const newDataStore = await dataStoreStore.createDataStore(
|
||||||
} catch (error) {
|
dataStoreName.value,
|
||||||
toast.showError(error, i18n.baseText('dataStore.add.error'));
|
route.params.projectId as string,
|
||||||
} finally {
|
);
|
||||||
|
void router.push({
|
||||||
|
name: DATA_STORE_DETAILS,
|
||||||
|
params: {
|
||||||
|
id: newDataStore.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
dataStoreName.value = '';
|
dataStoreName.value = '';
|
||||||
uiStore.closeModal(props.modalName);
|
uiStore.closeModal(props.modalName);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('dataStore.add.error'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
uiStore.closeModal(props.modalName);
|
||||||
|
redirectToDataStores();
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToDataStores = () => {
|
||||||
|
void router.replace({ name: PROJECT_DATA_STORES });
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :name="props.modalName" :center="true" width="540px">
|
<Modal :name="props.modalName" :center="true" width="540px" :before-close="redirectToDataStores">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
|
<h2>{{ i18n.baseText('dataStore.add.title') }}</h2>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,7 +85,7 @@ const onSubmit = async () => {
|
|||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer="{ close }">
|
<template #footer>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
:disabled="!dataStoreName"
|
:disabled="!dataStoreName"
|
||||||
@@ -78,7 +97,7 @@ const onSubmit = async () => {
|
|||||||
type="secondary"
|
type="secondary"
|
||||||
:label="i18n.baseText('generic.cancel')"
|
:label="i18n.baseText('generic.cancel')"
|
||||||
data-test-id="cancel-add-data-store-button"
|
data-test-id="cancel-add-data-store-button"
|
||||||
@click="close"
|
@click="onCancel"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe('DataStoreBreadcrumbs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(getByText('Data Stores')).toBeInTheDocument();
|
expect(getByText('Data Stores')).toBeInTheDocument();
|
||||||
const separators = getAllByText('›');
|
const separators = getAllByText('/');
|
||||||
expect(separators.length).toBeGreaterThan(0);
|
expect(separators.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ describe('DataStoreBreadcrumbs', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const separators = getAllByText('›');
|
const separators = getAllByText('/');
|
||||||
expect(separators.length).toBeGreaterThan(0);
|
expect(separators.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
|||||||
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
import { useDataStoreStore } from '@/features/dataStore/dataStore.store';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
const BREADCRUMBS_SEPARATOR = '›';
|
const BREADCRUMBS_SEPARATOR = '/';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dataStore: DataStore;
|
dataStore: DataStore;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const emit = defineEmits<{
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const dataStoreTypes = useDataStoreTypes();
|
const { getDefaultValueForType, mapToAGCellType } = useDataStoreTypes();
|
||||||
|
|
||||||
const dataStoreStore = useDataStoreStore();
|
const dataStoreStore = useDataStoreStore();
|
||||||
|
|
||||||
@@ -142,22 +142,6 @@ const setPageSize = async (size: number) => {
|
|||||||
await fetchDataStoreContent();
|
await fetchDataStoreContent();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
|
|
||||||
try {
|
|
||||||
const newColumn = await dataStoreStore.addDataStoreColumn(
|
|
||||||
props.dataStore.id,
|
|
||||||
props.dataStore.projectId,
|
|
||||||
column,
|
|
||||||
);
|
|
||||||
if (!newColumn) {
|
|
||||||
throw new Error(i18n.baseText('generic.unknownError'));
|
|
||||||
}
|
|
||||||
colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
|
|
||||||
} catch (error) {
|
|
||||||
toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteColumn = async (columnId: string) => {
|
const onDeleteColumn = async (columnId: string) => {
|
||||||
if (!gridApi.value) return;
|
if (!gridApi.value) return;
|
||||||
|
|
||||||
@@ -183,7 +167,7 @@ const onDeleteColumn = async (columnId: string) => {
|
|||||||
colDefs.value = colDefs.value.filter((def) => def.colId !== columnId);
|
colDefs.value = colDefs.value.filter((def) => def.colId !== columnId);
|
||||||
const rowDataOldValue = [...rowData.value];
|
const rowDataOldValue = [...rowData.value];
|
||||||
rowData.value = rowData.value.map((row) => {
|
rowData.value = rowData.value.map((row) => {
|
||||||
const { [columnToDelete.field ?? '']: _, ...rest } = row;
|
const { [columnToDelete.field!]: _, ...rest } = row;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
refreshGridData();
|
refreshGridData();
|
||||||
@@ -201,7 +185,26 @@ const onDeleteColumn = async (columnId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Split this up to create column def based on type
|
const onAddColumn = async ({ column }: { column: DataStoreColumnCreatePayload }) => {
|
||||||
|
try {
|
||||||
|
const newColumn = await dataStoreStore.addDataStoreColumn(
|
||||||
|
props.dataStore.id,
|
||||||
|
props.dataStore.projectId,
|
||||||
|
column,
|
||||||
|
);
|
||||||
|
if (!newColumn) {
|
||||||
|
throw new Error(i18n.baseText('generic.unknownError'));
|
||||||
|
}
|
||||||
|
colDefs.value = [...colDefs.value, createColumnDef(newColumn)];
|
||||||
|
rowData.value = rowData.value.map((row) => {
|
||||||
|
return { ...row, [newColumn.name]: getDefaultValueForType(newColumn.type) };
|
||||||
|
});
|
||||||
|
refreshGridData();
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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,
|
||||||
@@ -209,11 +212,11 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
|||||||
headerName: col.name,
|
headerName: col.name,
|
||||||
editable: true,
|
editable: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
lockPinned: true,
|
||||||
headerComponent: ColumnHeader,
|
headerComponent: ColumnHeader,
|
||||||
cellEditorPopup: false,
|
cellEditorPopup: false,
|
||||||
headerComponentParams: { onDelete: onDeleteColumn },
|
headerComponentParams: { onDelete: onDeleteColumn },
|
||||||
...extraProps,
|
cellDataType: mapToAGCellType(col.type),
|
||||||
cellDataType: dataStoreTypes.mapToAGCellType(col.type),
|
|
||||||
valueGetter: (params: ValueGetterParams<DataStoreRow>) => {
|
valueGetter: (params: ValueGetterParams<DataStoreRow>) => {
|
||||||
// If the value is null, return null to show empty cell
|
// If the value is null, return null to show empty cell
|
||||||
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
|
if (params.data?.[col.name] === null || params.data?.[col.name] === undefined) {
|
||||||
@@ -284,7 +287,10 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
|
|||||||
columnDef.cellEditor = 'agDateCellEditor';
|
columnDef.cellEditor = 'agDateCellEditor';
|
||||||
columnDef.cellEditorPopup = true;
|
columnDef.cellEditorPopup = true;
|
||||||
}
|
}
|
||||||
return columnDef;
|
return {
|
||||||
|
...columnDef,
|
||||||
|
...extraProps,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
|
const onColumnMoved = async (moveEvent: ColumnMovedEvent) => {
|
||||||
@@ -555,17 +561,19 @@ const onCellEditingStopped = (params: CellEditingStoppedEvent<DataStoreRow>) =>
|
|||||||
--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-light-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);
|
||||||
--ag-header-foreground-color: var(--color-text-dark);
|
--ag-header-foreground-color: var(--color-text-dark);
|
||||||
--ag-cell-horizontal-padding: var(--spacing-2xs);
|
--ag-cell-horizontal-padding: var(--spacing-2xs);
|
||||||
--ag-header-column-resize-handle-color: var(--border-color-base);
|
--ag-header-column-resize-handle-color: var(--color-foreground-base);
|
||||||
--ag-header-column-resize-handle-height: 100%;
|
--ag-header-column-resize-handle-height: 100%;
|
||||||
--ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px);
|
--ag-header-height: calc(var(--ag-grid-size) * 0.8 + 32px);
|
||||||
|
|
||||||
:global(.ag-header-cell-resize) {
|
:global(.ag-header-cell-resize) {
|
||||||
width: var(--spacing-4xs);
|
width: var(--spacing-xs);
|
||||||
|
// this is needed so that we compensate for the width
|
||||||
|
right: -7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show borders for the checkbox cells
|
// Don't show borders for the checkbox cells
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const DataStoreModule: FrontendModuleDescription = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: PROJECT_DATA_STORES,
|
name: PROJECT_DATA_STORES,
|
||||||
path: 'datatables',
|
path: 'datatables/:new(new)?',
|
||||||
props: true,
|
props: true,
|
||||||
components: {
|
components: {
|
||||||
default: DataStoreView,
|
default: DataStoreView,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
const activeModules = computed(() => settings.value.activeModules);
|
const activeModules = computed(() => settings.value.activeModules);
|
||||||
|
|
||||||
const isModuleActive = (moduleName: string) => {
|
const isModuleActive = (moduleName: string) => {
|
||||||
return activeModules.value.includes(moduleName);
|
return activeModules.value?.includes(moduleName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const partialExecutionVersion = computed<1 | 2>(() => {
|
const partialExecutionVersion = computed<1 | 2>(() => {
|
||||||
@@ -144,6 +144,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
|
|
||||||
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
|
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
|
||||||
|
|
||||||
|
const isDataStoreFeatureEnabled = computed(() => isModuleActive('data-store'));
|
||||||
|
|
||||||
const areTagsEnabled = computed(() =>
|
const areTagsEnabled = computed(() =>
|
||||||
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
||||||
);
|
);
|
||||||
@@ -396,5 +398,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
isMFAEnforced,
|
isMFAEnforced,
|
||||||
activeModules,
|
activeModules,
|
||||||
isModuleActive,
|
isModuleActive,
|
||||||
|
isDataStoreFeatureEnabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user