mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 18:41:14 +00:00
feat(editor): Show workflow breadcrumbs in canvas (#14710)
This commit is contained in:
committed by
GitHub
parent
be53453def
commit
46df8b47d6
@@ -32,7 +32,6 @@ import InsightsSummary from '@/features/insights/components/InsightsSummary.vue'
|
||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type {
|
||||
FolderListItem,
|
||||
FolderPathItem,
|
||||
IUser,
|
||||
UserAction,
|
||||
WorkflowListItem,
|
||||
@@ -136,6 +135,8 @@ const currentFolderId = ref<string | null>(null);
|
||||
|
||||
const showCardsBadge = ref(false);
|
||||
|
||||
const isNameEditEnabled = ref(false);
|
||||
|
||||
/**
|
||||
* Folder actions
|
||||
* These can appear on the list header, and then they are applied to current folder
|
||||
@@ -213,6 +214,12 @@ const currentFolder = computed(() => {
|
||||
return currentFolderId.value ? foldersStore.breadcrumbsCache[currentFolderId.value] : null;
|
||||
});
|
||||
|
||||
const currentFolderParent = computed(() => {
|
||||
return currentFolder.value?.parentFolder
|
||||
? foldersStore.breadcrumbsCache[currentFolder.value.parentFolder]
|
||||
: null;
|
||||
});
|
||||
|
||||
const isDragging = computed(() => {
|
||||
return foldersStore.draggedElement !== null;
|
||||
});
|
||||
@@ -504,6 +511,7 @@ const fetchWorkflows = async () => {
|
||||
.map((r) => ({ id: r.id, name: r.name, parentFolder: r.parentFolder?.id })),
|
||||
);
|
||||
|
||||
// This is for the case when user lands straight on a folder page
|
||||
const isCurrentFolderCached = foldersStore.breadcrumbsCache[parentFolder ?? ''] !== undefined;
|
||||
const needToFetchFolderPath = parentFolder && !isCurrentFolderCached && routeProjectId;
|
||||
|
||||
@@ -857,63 +865,6 @@ const moveResourceOnDrop = async (draggedResource: DragTarget, dropTarget: DropT
|
||||
|
||||
// Breadcrumbs methods
|
||||
|
||||
/**
|
||||
* Breadcrumbs: Calculate visible and hidden items for both main breadcrumbs and card breadcrumbs
|
||||
* We do this here and pass to each component to avoid recalculating in each card
|
||||
*/
|
||||
const visibleBreadcrumbsItems = computed<FolderPathItem[]>(() => {
|
||||
if (!currentFolder.value) return [];
|
||||
const items: FolderPathItem[] = [];
|
||||
const parent = foldersStore.getCachedFolder(currentFolder.value.parentFolder ?? '');
|
||||
if (parent) {
|
||||
items.push({
|
||||
id: parent.id,
|
||||
label: parent.name,
|
||||
href: `/projects/${route.params.projectId}/folders/${parent.id}/workflows`,
|
||||
parentFolder: parent.parentFolder,
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
id: currentFolder.value.id,
|
||||
label: currentFolder.value.name,
|
||||
parentFolder: parent?.parentFolder,
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
const hiddenBreadcrumbsItems = computed<FolderPathItem[]>(() => {
|
||||
const lastVisibleParent: FolderPathItem =
|
||||
visibleBreadcrumbsItems.value[visibleBreadcrumbsItems.value.length - 1];
|
||||
if (!lastVisibleParent) return [];
|
||||
const items: FolderPathItem[] = [];
|
||||
// Go through all the parent folders and add them to the hidden items
|
||||
let parentFolder = lastVisibleParent.parentFolder;
|
||||
while (parentFolder) {
|
||||
const parent = foldersStore.getCachedFolder(parentFolder);
|
||||
|
||||
if (!parent) break;
|
||||
items.unshift({
|
||||
id: parent.id,
|
||||
label: parent.name,
|
||||
href: `/projects/${route.params.projectId}/folders/${parent.id}/workflows`,
|
||||
parentFolder: parent.parentFolder,
|
||||
});
|
||||
parentFolder = parent.parentFolder;
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
/**
|
||||
* Main breadcrumbs items that show on top of the list
|
||||
* These show path to the current folder with up to 2 parents visible
|
||||
*/
|
||||
const mainBreadcrumbs = computed(() => {
|
||||
return {
|
||||
visibleItems: visibleBreadcrumbsItems.value,
|
||||
hiddenItems: hiddenBreadcrumbsItems.value,
|
||||
};
|
||||
});
|
||||
|
||||
const onBreadcrumbItemClick = (item: PathItem) => {
|
||||
if (item.href) {
|
||||
loading.value = true;
|
||||
@@ -958,8 +909,7 @@ const onBreadCrumbsAction = async (action: string) => {
|
||||
);
|
||||
break;
|
||||
case FOLDER_LIST_ITEM_ACTIONS.RENAME:
|
||||
if (!route.params.folderId) return;
|
||||
await renameFolder(route.params.folderId as string);
|
||||
onNameToggle();
|
||||
break;
|
||||
case FOLDER_LIST_ITEM_ACTIONS.MOVE:
|
||||
if (!currentFolder.value) return;
|
||||
@@ -1307,6 +1257,69 @@ const onCreateWorkflowClick = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onNameToggle = () => {
|
||||
isNameEditEnabled.value = !isNameEditEnabled.value;
|
||||
};
|
||||
|
||||
const onNameSubmit = async ({
|
||||
name,
|
||||
onSubmit,
|
||||
}: {
|
||||
name: string;
|
||||
onSubmit: (saved: boolean) => void;
|
||||
}) => {
|
||||
if (!currentFolder.value || !currentProject.value) return;
|
||||
|
||||
const newName = name.trim();
|
||||
if (!newName) {
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('renameAction.emptyName.title'),
|
||||
message: i18n.baseText('renameAction.emptyName.message'),
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
onSubmit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newName === currentFolder.value.name) {
|
||||
isNameEditEnabled.value = false;
|
||||
|
||||
onSubmit(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResult = folderHelpers.validateFolderName(newName);
|
||||
if (typeof validationResult === 'string') {
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('renameAction.invalidName.title'),
|
||||
message: validationResult,
|
||||
type: 'error',
|
||||
});
|
||||
onSubmit(false);
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
await foldersStore.renameFolder(currentProject.value?.id, currentFolder.value.id, newName);
|
||||
foldersStore.breadcrumbsCache[currentFolder.value.id].name = newName;
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('folders.rename.success.message', {
|
||||
interpolate: { folderName: newName },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
telemetry.track('User renamed folder', {
|
||||
folder_id: currentFolder.value.id,
|
||||
});
|
||||
isNameEditEnabled.value = false;
|
||||
onSubmit(true);
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('folders.rename.error.title'));
|
||||
onSubmit(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1417,14 +1430,31 @@ const onCreateWorkflowClick = () => {
|
||||
data-test-id="main-breadcrumbs"
|
||||
>
|
||||
<FolderBreadcrumbs
|
||||
:breadcrumbs="mainBreadcrumbs"
|
||||
:current-folder="currentFolderParent"
|
||||
:actions="mainBreadcrumbsActions"
|
||||
:hidden-items-trigger="isDragging ? 'hover' : 'click'"
|
||||
:current-folder-as-link="true"
|
||||
@item-selected="onBreadcrumbItemClick"
|
||||
@action="onBreadCrumbsAction"
|
||||
@item-drop="onBreadCrumbsItemDrop"
|
||||
@project-drop="moveFolderToProjectRoot"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<span :class="$style['path-separator']">/</span>
|
||||
<InlineTextEdit
|
||||
data-test-id="breadcrumbs-item-current"
|
||||
:model-value="currentFolder.name"
|
||||
:preview-value="currentFolder.name"
|
||||
:is-edit-enabled="isNameEditEnabled"
|
||||
:max-length="30"
|
||||
:disabled="readOnlyEnv || !hasPermissionToUpdateFolders"
|
||||
:class="{ [$style.name]: true, [$style['pointer-disabled']]: isDragging }"
|
||||
:placeholder="i18n.baseText('folders.rename.placeholder')"
|
||||
@toggle="onNameToggle"
|
||||
@submit="onNameSubmit"
|
||||
/>
|
||||
</template>
|
||||
</FolderBreadcrumbs>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{ item: data, index }">
|
||||
@@ -1716,6 +1746,21 @@ const onCreateWorkflowClick = () => {
|
||||
background-color: var(--color-callout-secondary-background);
|
||||
}
|
||||
}
|
||||
|
||||
.path-separator {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--color-foreground-base);
|
||||
margin: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.name {
|
||||
color: $custom-font-dark;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.pointer-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user