From 2cb9d9e29fc961a417d06c1449b79d4a0a66658e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 28 Feb 2025 15:50:50 +0100 Subject: [PATCH] feat(editor): Add functionality to create folders (#13473) --- .../components/N8nBreadcrumbs/Breadcrumbs.vue | 6 +- packages/frontend/editor-ui/src/Interface.ts | 16 + .../frontend/editor-ui/src/api/workflows.ts | 26 + .../components/DuplicateWorkflowDialog.vue | 3 + .../components/Folders/FolderBreadcrumbs.vue | 89 +++ .../src/components/Folders/FolderCard.test.ts | 12 +- .../src/components/Folders/FolderCard.vue | 86 +-- .../components/Projects/ProjectHeader.test.ts | 4 + .../src/components/Projects/ProjectHeader.vue | 43 +- .../editor-ui/src/components/WorkflowCard.vue | 69 ++- .../layouts/ResourcesListLayout.vue | 49 +- .../src/composables/useWorkflowHelpers.ts | 14 +- packages/frontend/editor-ui/src/constants.ts | 3 +- .../src/plugins/i18n/locales/en.json | 10 + .../editor-ui/src/plugins/icons/index.ts | 2 + .../editor-ui/src/stores/folders.store.ts | 102 +++ .../editor-ui/src/stores/workflows.store.ts | 8 +- .../frontend/editor-ui/src/views/NodeView.vue | 9 +- .../src/views/ProjectSettings.test.ts | 3 + .../editor-ui/src/views/WorkflowsView.test.ts | 146 +++-- .../editor-ui/src/views/WorkflowsView.vue | 580 ++++++++++++++---- 21 files changed, 961 insertions(+), 319 deletions(-) create mode 100644 packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue create mode 100644 packages/frontend/editor-ui/src/stores/folders.store.ts diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue index ff017bf2aa..a7ecd21628 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue @@ -340,12 +340,12 @@ const handleTooltipClose = () => { .item, .item * { - color: var(--color-text-light); + color: var(--color-text-base); font-size: var(--font-size-m); } .item a:hover * { - color: var(--color-text-base); + color: var(--color-text-dark); } .ellipsis { @@ -359,7 +359,7 @@ const handleTooltipClose = () => { .separator { font-size: var(--font-size-xl); - color: var(--prim-gray-670); + color: var(--color-foreground-base); } } diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index f57339b134..ceb7b1579e 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -58,6 +58,7 @@ import type { import type { BulkCommand, Undoable } from '@/models/history'; import type { ProjectSharingData } from '@/types/projects.types'; +import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue'; export * from '@n8n/design-system/types'; @@ -243,6 +244,7 @@ export interface IWorkflowDataUpdate { export interface IWorkflowDataCreate extends IWorkflowDataUpdate { projectId?: string; + parentFolderId?: string; } /** @@ -333,6 +335,7 @@ export type WorkflowListItem = Omit< export type FolderShortInfo = { id: string; name: string; + parentFolder?: string; }; export type BaseFolderItem = BaseResource & { @@ -349,8 +352,21 @@ export interface FolderListItem extends BaseFolderItem { resource: 'folder'; } +export type FolderPathItem = PathItem & { parentFolder?: string }; + export type WorkflowListResource = WorkflowListItem | FolderListItem; +export type FolderCreateResponse = Omit< + FolderListItem, + 'workflowCount' | 'tags' | 'sharedWithProjects' | 'homeProject' +>; + +export type FolderTreeResponseItem = { + id: string; + name: string; + children: FolderTreeResponseItem[]; +}; + // Identical to cli.Interfaces.ts export interface IWorkflowShortResponse { id: string; diff --git a/packages/frontend/editor-ui/src/api/workflows.ts b/packages/frontend/editor-ui/src/api/workflows.ts index 1302e1f1f5..7ac2e74d34 100644 --- a/packages/frontend/editor-ui/src/api/workflows.ts +++ b/packages/frontend/editor-ui/src/api/workflows.ts @@ -1,4 +1,6 @@ import type { + FolderCreateResponse, + FolderTreeResponseItem, IExecutionResponse, IExecutionsCurrentSummaryExtended, IRestApiContext, @@ -84,3 +86,27 @@ export async function getExecutionData(context: IRestApiContext, executionId: st `/executions/${executionId}`, ); } + +export async function createFolder( + context: IRestApiContext, + projectId: string, + name: string, + parentFolderId?: string, +): Promise { + return await makeRestApiRequest(context, 'POST', `/projects/${projectId}/folders`, { + name, + parentFolderId, + }); +} + +export async function getFolderPath( + context: IRestApiContext, + projectId: string, + folderId: string, +): Promise { + return await makeRestApiRequest( + context, + 'GET', + `/projects/${projectId}/folders/${folderId}/tree`, + ); +} diff --git a/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue index 724d46eb0e..0f624311f0 100644 --- a/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/frontend/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -72,6 +72,8 @@ const save = async (): Promise => { return; } + const parentFolderId = router.currentRoute.value.params.folderId as string | undefined; + const currentWorkflowId = props.data.id; isSaving.value = true; @@ -102,6 +104,7 @@ const save = async (): Promise => { resetWebhookUrls: true, openInNewWindow: true, resetNodeIds: true, + parentFolderId, }); if (saved) { diff --git a/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue b/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue new file mode 100644 index 0000000000..1c8e0fbe0a --- /dev/null +++ b/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue @@ -0,0 +1,89 @@ + + + + diff --git a/packages/frontend/editor-ui/src/components/Folders/FolderCard.test.ts b/packages/frontend/editor-ui/src/components/Folders/FolderCard.test.ts index b8aa155d2c..bf19b9a2fc 100644 --- a/packages/frontend/editor-ui/src/components/Folders/FolderCard.test.ts +++ b/packages/frontend/editor-ui/src/components/Folders/FolderCard.test.ts @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import FolderCard from './FolderCard.vue'; import { createPinia, setActivePinia } from 'pinia'; import type { FolderResource } from '../layouts/ResourcesListLayout.vue'; -import type { UserAction } from '@/Interface'; +import type { FolderPathItem, UserAction } from '@/Interface'; vi.mock('vue-router', () => { const push = vi.fn(); @@ -61,6 +61,11 @@ const PARENT_FOLDER: FolderResource = { }, } as const satisfies FolderResource; +const DEFAULT_BREADCRUMBS: { visibleItems: FolderPathItem[]; hiddenItems: FolderPathItem[] } = { + visibleItems: [{ id: '1', label: 'Parent 2' }], + hiddenItems: [{ id: '2', label: 'Parent 1', parentFolder: '1' }], +}; + const renderComponent = createComponentRenderer(FolderCard, { props: { data: DEFAULT_FOLDER, @@ -68,6 +73,7 @@ const renderComponent = createComponentRenderer(FolderCard, { { label: 'Open', value: 'open', disabled: false }, { label: 'Delete', value: 'delete', disabled: false }, ] as const satisfies UserAction[], + breadcrumbs: DEFAULT_BREADCRUMBS, }, global: { stubs: { @@ -145,6 +151,10 @@ describe('FolderCard', () => { }, parentFolder: PARENT_FOLDER, }, + breadcrumbs: { + visibleItems: [{ id: PARENT_FOLDER.id, label: PARENT_FOLDER.name, parentFolder: '1' }], + hiddenItems: [], + }, }, }); expect(getByTestId('folder-card-icon')).toBeInTheDocument(); diff --git a/packages/frontend/editor-ui/src/components/Folders/FolderCard.vue b/packages/frontend/editor-ui/src/components/Folders/FolderCard.vue index 02d8a443d3..16f3ed3e5c 100644 --- a/packages/frontend/editor-ui/src/components/Folders/FolderCard.vue +++ b/packages/frontend/editor-ui/src/components/Folders/FolderCard.vue @@ -7,11 +7,15 @@ import { useI18n } from '@/composables/useI18n'; import { useRoute, useRouter } from 'vue-router'; import { VIEWS } from '@/constants'; import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue'; -import type { UserAction } from '@/Interface'; +import type { FolderPathItem, UserAction } from '@/Interface'; type Props = { data: FolderResource; actions: UserAction[]; + breadcrumbs: { + visibleItems: FolderPathItem[]; + hiddenItems: FolderPathItem[]; + }; }; const props = withDefaults(defineProps(), { @@ -27,18 +31,6 @@ const emit = defineEmits<{ folderOpened: [{ folder: FolderResource }]; }>(); -const breadCrumbsItems = computed(() => { - if (props.data.parentFolder) { - return [ - { - id: props.data.parentFolder.id, - label: props.data.parentFolder.name, - }, - ]; - } - return []; -}); - const projectIcon = computed(() => { const defaultIcon: ProjectIcon = { type: 'icon', value: 'layer-group' }; if (props.data.homeProject?.type === ProjectTypes.Personal) { @@ -109,7 +101,7 @@ const onBreadcrumbsItemClick = async (item: PathItem) => { {{ data.workflowCount }} {{ i18n.baseText('generic.workflows') }} @@ -117,7 +109,7 @@ const onBreadcrumbsItemClick = async (item: PathItem) => { {{ i18n.baseText('workerList.item.lastUpdated') }} @@ -126,7 +118,7 @@ const onBreadcrumbsItemClick = async (item: PathItem) => { {{ i18n.baseText('workflows.item.created') }} @@ -136,26 +128,29 @@ const onBreadcrumbsItemClick = async (item: PathItem) => {