mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Address folders feature feedback (no-changelog) (#13859)
This commit is contained in:
committed by
GitHub
parent
b4672b8deb
commit
31493a0cac
@@ -13,15 +13,11 @@ import {
|
|||||||
getFolderCard,
|
getFolderCard,
|
||||||
getFolderCardActionItem,
|
getFolderCardActionItem,
|
||||||
getFolderCardActionToggle,
|
getFolderCardActionToggle,
|
||||||
getFolderCardBreadCrumbsEllipsis,
|
|
||||||
getFolderCardCurrentBreadcrumb,
|
|
||||||
getFolderCardHomeProjectBreadcrumb,
|
|
||||||
getFolderCards,
|
getFolderCards,
|
||||||
getHomeProjectBreadcrumb,
|
getHomeProjectBreadcrumb,
|
||||||
getListBreadcrumbs,
|
getListBreadcrumbs,
|
||||||
getMainBreadcrumbsEllipsis,
|
getMainBreadcrumbsEllipsis,
|
||||||
getMainBreadcrumbsEllipsisMenuItems,
|
getMainBreadcrumbsEllipsisMenuItems,
|
||||||
getOpenHiddenItemsTooltip,
|
|
||||||
getOverviewMenuItem,
|
getOverviewMenuItem,
|
||||||
getPersonalProjectMenuItem,
|
getPersonalProjectMenuItem,
|
||||||
getVisibleListBreadcrumbs,
|
getVisibleListBreadcrumbs,
|
||||||
@@ -62,7 +58,7 @@ describe('Folders', () => {
|
|||||||
createFolderFromListHeaderButton('My Folder 2');
|
createFolderFromListHeaderButton('My Folder 2');
|
||||||
getFolderCards().should('have.length.greaterThan', 0);
|
getFolderCards().should('have.length.greaterThan', 0);
|
||||||
// Clicking on the success toast should navigate to the folder
|
// Clicking on the success toast should navigate to the folder
|
||||||
successToast().find('a').contains('My Folder 2').click();
|
successToast().contains('My Folder 2').find('a').contains('Open folder').click();
|
||||||
getCurrentBreadcrumb().should('contain.text', 'My Folder 2');
|
getCurrentBreadcrumb().should('contain.text', 'My Folder 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,29 +109,21 @@ describe('Folders', () => {
|
|||||||
createFolderFromProjectHeader('Multi-level Test');
|
createFolderFromProjectHeader('Multi-level Test');
|
||||||
createFolderInsideFolder('Child Folder', 'Multi-level Test');
|
createFolderInsideFolder('Child Folder', 'Multi-level Test');
|
||||||
// One level deep:
|
// One level deep:
|
||||||
// - Both main breadcrumbs & card breadcrumbs should only show home project and current folder
|
// - Breadcrumbs should only show home project and current folder
|
||||||
getHomeProjectBreadcrumb().should('exist');
|
getHomeProjectBreadcrumb().should('exist');
|
||||||
getCurrentBreadcrumb().should('contain.text', 'Multi-level Test');
|
getCurrentBreadcrumb().should('contain.text', 'Multi-level Test');
|
||||||
getFolderCard('Child Folder').should('exist');
|
getFolderCard('Child Folder').should('exist');
|
||||||
getFolderCardHomeProjectBreadcrumb('Child Folder').should('exist');
|
|
||||||
getFolderCardCurrentBreadcrumb('Child Folder').should('contain.text', 'Multi-level Test');
|
|
||||||
// No hidden items at this level
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder').should('not.exist');
|
|
||||||
|
|
||||||
createFolderInsideFolder('Child Folder 2', 'Child Folder');
|
createFolderInsideFolder('Child Folder 2', 'Child Folder');
|
||||||
// Two levels deep:
|
// Two levels deep:
|
||||||
// - Main breadcrumbs should also show parent folder, without hidden ellipsis
|
// - Breadcrumbs should also show parent folder, without hidden ellipsis
|
||||||
// - Card breadcrumbs should show home project, parent folder, with hidden ellipsis
|
|
||||||
getHomeProjectBreadcrumb().should('exist');
|
getHomeProjectBreadcrumb().should('exist');
|
||||||
getCurrentBreadcrumb().should('contain.text', 'Child Folder');
|
getCurrentBreadcrumb().should('contain.text', 'Child Folder');
|
||||||
getVisibleListBreadcrumbs().should('have.length', 1);
|
getVisibleListBreadcrumbs().should('have.length', 1);
|
||||||
getMainBreadcrumbsEllipsis().should('not.exist');
|
getMainBreadcrumbsEllipsis().should('not.exist');
|
||||||
getFolderCardCurrentBreadcrumb('Child Folder 2').should('contain.text', 'Child Folder');
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder 2').should('exist');
|
|
||||||
|
|
||||||
// Three levels deep:
|
// Three levels deep:
|
||||||
// - Main breadcrumbs should show parents up to the grandparent folder, with one hidden element
|
// - Breadcrumbs should show parents up to the grandparent folder, with one hidden element
|
||||||
// - Card breadcrumbs should now show two hidden elements
|
|
||||||
createFolderInsideFolder('Child Folder 3', 'Child Folder 2');
|
createFolderInsideFolder('Child Folder 3', 'Child Folder 2');
|
||||||
getVisibleListBreadcrumbs().should('have.length', 1);
|
getVisibleListBreadcrumbs().should('have.length', 1);
|
||||||
getMainBreadcrumbsEllipsis().should('exist');
|
getMainBreadcrumbsEllipsis().should('exist');
|
||||||
@@ -143,12 +131,6 @@ describe('Folders', () => {
|
|||||||
getMainBreadcrumbsEllipsis().click();
|
getMainBreadcrumbsEllipsis().click();
|
||||||
getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Multi-level Test');
|
getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Multi-level Test');
|
||||||
getMainBreadcrumbsEllipsis().click();
|
getMainBreadcrumbsEllipsis().click();
|
||||||
// Card breadcrumbs should show two hidden elements
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder 3').should('exist');
|
|
||||||
// Clicking on the ellipsis should show hidden element in card breadcrumbs
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder 3').click();
|
|
||||||
getOpenHiddenItemsTooltip().should('be.visible');
|
|
||||||
getOpenHiddenItemsTooltip().should('contain.text', 'Multi-level Test / Child Folder');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure breadcrumbs and folder card show correct info when landing straight on a folder page
|
// Make sure breadcrumbs and folder card show correct info when landing straight on a folder page
|
||||||
@@ -171,13 +153,6 @@ describe('Folders', () => {
|
|||||||
getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Landing Test');
|
getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Landing Test');
|
||||||
// Should load child folder card
|
// Should load child folder card
|
||||||
getFolderCard('Child Folder 3').should('exist');
|
getFolderCard('Child Folder 3').should('exist');
|
||||||
// Card breadcrumbs should show home project and parent, with two hidden elements
|
|
||||||
getFolderCardHomeProjectBreadcrumb('Child Folder 3').should('exist');
|
|
||||||
getFolderCardCurrentBreadcrumb('Child Folder 3').should('contain.text', 'Child Folder 2');
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder 3').should('exist');
|
|
||||||
getFolderCardBreadCrumbsEllipsis('Child Folder 3').click();
|
|
||||||
getOpenHiddenItemsTooltip().should('be.visible');
|
|
||||||
getOpenHiddenItemsTooltip().should('contain.text', 'Landing Test / Child Folder');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show folders only in projects', () => {
|
it('should show folders only in projects', () => {
|
||||||
@@ -239,9 +214,7 @@ describe('Folders', () => {
|
|||||||
getPersonalProjectMenuItem().find('li').should('have.class', 'is-active');
|
getPersonalProjectMenuItem().find('li').should('have.class', 'is-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Once we have a backend endpoint that returns sub-folder count, enable this
|
it('should warn before deleting non-empty folder from card dropdown', () => {
|
||||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
|
||||||
it.skip('should warn before deleting non-empty folder from card dropdown', () => {
|
|
||||||
goToPersonalProject();
|
goToPersonalProject();
|
||||||
createFolderFromProjectHeader('I also have family');
|
createFolderFromProjectHeader('I also have family');
|
||||||
createFolderInsideFolder('Child 1', 'I also have family');
|
createFolderInsideFolder('Child 1', 'I also have family');
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class WorkflowsPage extends BasePage {
|
|||||||
workflowDeleteButton: () =>
|
workflowDeleteButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||||
workflowMoveButton: () =>
|
workflowMoveButton: () =>
|
||||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Move'),
|
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Change owner'),
|
||||||
workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'),
|
workflowFilterButton: () => cy.getByTestId('resources-list-filters-trigger').filter(':visible'),
|
||||||
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
workflowTagsDropdown: () => cy.getByTestId('tags-dropdown'),
|
||||||
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
workflowTagItem: (tag: string) => cy.getByTestId('tag').contains(tag),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface ActionToggleProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
loadingRowCount?: number;
|
loadingRowCount?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
popperClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({ name: 'N8nActionToggle' });
|
defineOptions({ name: 'N8nActionToggle' });
|
||||||
@@ -33,6 +34,7 @@ withDefaults(defineProps<ActionToggleProps>(), {
|
|||||||
loading: false,
|
loading: false,
|
||||||
loadingRowCount: 3,
|
loadingRowCount: 3,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
popperClass: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null);
|
const actionToggleRef = ref<InstanceType<typeof ElDropdown> | null>(null);
|
||||||
@@ -62,6 +64,7 @@ defineExpose({
|
|||||||
:placement="placement"
|
:placement="placement"
|
||||||
:size="size"
|
:size="size"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:popper-class="popperClass"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@command="onCommand"
|
@command="onCommand"
|
||||||
@visible-change="onVisibleChange"
|
@visible-change="onVisibleChange"
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ const handleTooltipClose = () => {
|
|||||||
:loading-row-count="loadingSkeletonRows"
|
:loading-row-count="loadingSkeletonRows"
|
||||||
:disabled="dropdownDisabled"
|
:disabled="dropdownDisabled"
|
||||||
:class="$style['action-toggle']"
|
:class="$style['action-toggle']"
|
||||||
|
:popper-class="$style['hidden-items-menu-popper']"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -199,6 +200,7 @@ const handleTooltipClose = () => {
|
|||||||
[$style.item]: true,
|
[$style.item]: true,
|
||||||
[$style.current]: props.highlightLastItem && index === items.length - 1,
|
[$style.current]: props.highlightLastItem && index === items.length - 1,
|
||||||
}"
|
}"
|
||||||
|
:title="item.label"
|
||||||
:data-test-id="
|
:data-test-id="
|
||||||
index === items.length - 1 ? 'breadcrumbs-item-current' : 'breadcrumbs-item'
|
index === items.length - 1 ? 'breadcrumbs-item-current' : 'breadcrumbs-item'
|
||||||
"
|
"
|
||||||
@@ -238,6 +240,13 @@ const handleTooltipClose = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item * {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.item.current span {
|
.item.current span {
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
}
|
}
|
||||||
@@ -272,6 +281,21 @@ const handleTooltipClose = () => {
|
|||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-items-menu-popper {
|
||||||
|
& > div ul {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
max-width: var(--spacing-5xl);
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-loading {
|
.tooltip-loading {
|
||||||
min-width: var(--spacing-3xl);
|
min-width: var(--spacing-3xl);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -319,6 +343,10 @@ const handleTooltipClose = () => {
|
|||||||
gap: var(--spacing-5xs);
|
gap: var(--spacing-5xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
max-width: var(--spacing-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
.item,
|
.item,
|
||||||
.item * {
|
.item * {
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
@@ -331,7 +359,7 @@ const handleTooltipClose = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-s);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +373,11 @@ const handleTooltipClose = () => {
|
|||||||
.item,
|
.item,
|
||||||
.item * {
|
.item * {
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
max-width: var(--spacing-5xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item a:hover * {
|
.item a:hover * {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ exports[`Breadcrumbs > does not highlight last item for "highlightLastItem = fal
|
|||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
<li class="item" title="Folder 1" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
<li class="item" title="Folder 2" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
<li class="item" title="Folder 3" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
<li class="item" title="Current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>"
|
</div>"
|
||||||
@@ -24,13 +24,13 @@ exports[`Breadcrumbs > renders custom separator correctly 1`] = `
|
|||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
<li class="item" title="Folder 1" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
||||||
<li class="separator">➮</li>
|
<li class="separator">➮</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
<li class="item" title="Folder 2" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
||||||
<li class="separator">➮</li>
|
<li class="separator">➮</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
<li class="item" title="Folder 3" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
||||||
<li class="separator">➮</li>
|
<li class="separator">➮</li>
|
||||||
<li class="item current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
<li class="item current" title="Current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>"
|
</div>"
|
||||||
@@ -42,13 +42,13 @@ exports[`Breadcrumbs > renders default version correctly 1`] = `
|
|||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
<li class="item" title="Folder 1" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
<li class="item" title="Folder 2" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
<li class="item" title="Folder 3" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
<li class="item current" title="Current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>"
|
</div>"
|
||||||
@@ -61,13 +61,13 @@ exports[`Breadcrumbs > renders slots correctly 1`] = `
|
|||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
<li class="item" title="Folder 1" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
<li class="item" title="Folder 2" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
<li class="item" title="Folder 3" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
<li class="item current" title="Current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</ul>
|
</ul>
|
||||||
<div>[POST] Custom content</div>
|
<div>[POST] Custom content</div>
|
||||||
@@ -80,13 +80,13 @@ exports[`Breadcrumbs > renders small version correctly 1`] = `
|
|||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
<li class="item" title="Folder 1" data-test-id="breadcrumbs-item"><a href="/folder1" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 1</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
<li class="item" title="Folder 2" data-test-id="breadcrumbs-item"><a href="/folder2" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 2</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
<li class="item" title="Folder 3" data-test-id="breadcrumbs-item"><a href="/folder3" target="_blank" class="n8n-link"><span class="text"><span class="n8n-text size-medium regular">Folder 3</span></span></a></li>
|
||||||
<li class="separator">/</li>
|
<li class="separator">/</li>
|
||||||
<li class="item current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
<li class="item current" title="Current" data-test-id="breadcrumbs-item-current"><span class="n8n-text size-medium regular">Current</span></li>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>"
|
</div>"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TextColor } from '@n8n/design-system/types/text';
|
import type { TextColor } from '@n8n/design-system/types/text';
|
||||||
|
|
||||||
const ICON_SIZE = ['xsmall', 'small', 'medium', 'large'] as const;
|
const ICON_SIZE = ['xsmall', 'small', 'medium', 'large', 'xlarge'] as const;
|
||||||
export type IconSize = (typeof ICON_SIZE)[number];
|
export type IconSize = (typeof ICON_SIZE)[number];
|
||||||
|
|
||||||
export type IconColor = TextColor;
|
export type IconColor = TextColor;
|
||||||
|
|||||||
@@ -80,28 +80,6 @@ const enabled = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const folderContentWarningMessage = computed(() => {
|
|
||||||
const folderCount = props.data.content.subFolderCount ?? 0;
|
|
||||||
const workflowCount = props.data.content.workflowCount ?? 0;
|
|
||||||
let folderText = '';
|
|
||||||
let workflowText = '';
|
|
||||||
if (folderCount > 0) {
|
|
||||||
folderText = i18n.baseText('folder.count', { interpolate: { count: folderCount } });
|
|
||||||
}
|
|
||||||
if (workflowCount > 0) {
|
|
||||||
workflowText = i18n.baseText('workflow.count', { interpolate: { count: workflowCount } });
|
|
||||||
}
|
|
||||||
if (folderCount > 0 && workflowCount > 0) {
|
|
||||||
folderText += ` ${i18n.baseText('folder.and.workflow.separator')} `;
|
|
||||||
}
|
|
||||||
return i18n.baseText('folder.delete.modal.confirmation', {
|
|
||||||
interpolate: {
|
|
||||||
folders: folderText,
|
|
||||||
workflows: workflowText,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!enabled.value) {
|
if (!enabled.value) {
|
||||||
return;
|
return;
|
||||||
@@ -161,7 +139,9 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.content">
|
<div v-else :class="$style.content">
|
||||||
<div>
|
<div>
|
||||||
<n8n-text color="text-base">{{ folderContentWarningMessage }}</n8n-text>
|
<n8n-text color="text-base">{{
|
||||||
|
i18n.baseText('folder.delete.modal.confirmation')
|
||||||
|
}}</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
<el-radio
|
<el-radio
|
||||||
v-model="operation"
|
v-model="operation"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const onAction = (action: string) => {
|
|||||||
<template v-if="currentProject" #prepend>
|
<template v-if="currentProject" #prepend>
|
||||||
<div :class="$style['home-project']" data-test-id="home-project">
|
<div :class="$style['home-project']" data-test-id="home-project">
|
||||||
<n8n-link :to="`/projects/${currentProject.id}`">
|
<n8n-link :to="`/projects/${currentProject.id}`">
|
||||||
<N8nText size="large" color="text-base">{{ projectName }}</N8nText>
|
<N8nText size="medium" color="text-base">{{ projectName }}</N8nText>
|
||||||
</n8n-link>
|
</n8n-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -44,25 +44,6 @@ const DEFAULT_FOLDER: FolderResource = {
|
|||||||
},
|
},
|
||||||
} as const satisfies FolderResource;
|
} as const satisfies FolderResource;
|
||||||
|
|
||||||
const PARENT_FOLDER: FolderResource = {
|
|
||||||
id: '2',
|
|
||||||
name: 'Folder 2',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
resourceType: 'folder',
|
|
||||||
readOnly: false,
|
|
||||||
workflowCount: 0,
|
|
||||||
subFolderCount: 0,
|
|
||||||
homeProject: {
|
|
||||||
id: '1',
|
|
||||||
name: 'Project 1',
|
|
||||||
icon: null,
|
|
||||||
type: 'team',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
} as const satisfies FolderResource;
|
|
||||||
|
|
||||||
const DEFAULT_BREADCRUMBS: { visibleItems: FolderPathItem[]; hiddenItems: FolderPathItem[] } = {
|
const DEFAULT_BREADCRUMBS: { visibleItems: FolderPathItem[]; hiddenItems: FolderPathItem[] } = {
|
||||||
visibleItems: [{ id: '1', label: 'Parent 2' }],
|
visibleItems: [{ id: '1', label: 'Parent 2' }],
|
||||||
hiddenItems: [{ id: '2', label: 'Parent 1', parentFolder: '1' }],
|
hiddenItems: [{ id: '2', label: 'Parent 1', parentFolder: '1' }],
|
||||||
@@ -122,67 +103,6 @@ describe('FolderCard', () => {
|
|||||||
expect(queryByTestId('folder-card-folder-count')).not.toBeInTheDocument();
|
expect(queryByTestId('folder-card-folder-count')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render breadcrumbs with personal folder', () => {
|
|
||||||
const { getByTestId } = renderComponent();
|
|
||||||
expect(getByTestId('folder-card-icon')).toBeInTheDocument();
|
|
||||||
expect(getByTestId('folder-card-breadcrumbs')).toHaveTextContent('Personal');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render breadcrumbs with team project', () => {
|
|
||||||
const { getByTestId } = renderComponent({
|
|
||||||
props: {
|
|
||||||
data: {
|
|
||||||
...DEFAULT_FOLDER,
|
|
||||||
homeProject: {
|
|
||||||
id: '1',
|
|
||||||
name: 'Project 1',
|
|
||||||
icon: null,
|
|
||||||
type: 'team',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(getByTestId('folder-card-icon')).toBeInTheDocument();
|
|
||||||
if (!DEFAULT_FOLDER.homeProject?.name) {
|
|
||||||
throw new Error('homeProject should be defined for this test');
|
|
||||||
}
|
|
||||||
expect(getByTestId('folder-card-breadcrumbs')).toHaveTextContent(
|
|
||||||
DEFAULT_FOLDER.homeProject.name,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render breadcrumbs with home project and parent folder', () => {
|
|
||||||
const { getByTestId } = renderComponent({
|
|
||||||
props: {
|
|
||||||
data: {
|
|
||||||
...DEFAULT_FOLDER,
|
|
||||||
homeProject: {
|
|
||||||
id: '1',
|
|
||||||
name: 'Project 1',
|
|
||||||
icon: null,
|
|
||||||
type: 'team',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
parentFolder: PARENT_FOLDER,
|
|
||||||
},
|
|
||||||
breadcrumbs: {
|
|
||||||
visibleItems: [{ id: PARENT_FOLDER.id, label: PARENT_FOLDER.name, parentFolder: '1' }],
|
|
||||||
hiddenItems: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(getByTestId('folder-card-icon')).toBeInTheDocument();
|
|
||||||
if (!DEFAULT_FOLDER.homeProject?.name) {
|
|
||||||
throw new Error('homeProject should be defined for this test');
|
|
||||||
}
|
|
||||||
expect(getByTestId('folder-card-breadcrumbs')).toHaveTextContent(
|
|
||||||
`${DEFAULT_FOLDER.homeProject.name}/.../${PARENT_FOLDER.name}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render action dropdown if no actions are provided', () => {
|
it('should not render action dropdown if no actions are provided', () => {
|
||||||
const { queryByTestId } = renderComponent({
|
const { queryByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -6,20 +6,17 @@ import { type ProjectIcon, ProjectTypes } from '@/types/projects.types';
|
|||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { VIEWS } from '@/constants';
|
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 = {
|
type Props = {
|
||||||
data: FolderResource;
|
data: FolderResource;
|
||||||
actions: UserAction[];
|
actions: UserAction[];
|
||||||
breadcrumbs: {
|
readOnly?: boolean;
|
||||||
visibleItems: FolderPathItem[];
|
|
||||||
hiddenItems: FolderPathItem[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
actions: () => [],
|
actions: () => [],
|
||||||
|
readOnly: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -71,12 +68,6 @@ const onAction = async (action: string) => {
|
|||||||
}
|
}
|
||||||
emit('action', { action, folderId: props.data.id });
|
emit('action', { action, folderId: props.data.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBreadcrumbsItemClick = async (item: PathItem) => {
|
|
||||||
if (item.href) {
|
|
||||||
await router.push(item.href);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -88,13 +79,18 @@ const onBreadcrumbsItemClick = async (item: PathItem) => {
|
|||||||
data-test-id="folder-card-icon"
|
data-test-id="folder-card-icon"
|
||||||
:class="$style['folder-icon']"
|
:class="$style['folder-icon']"
|
||||||
icon="folder"
|
icon="folder"
|
||||||
size="large"
|
size="xlarge"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n8n-heading tag="h2" bold size="small" data-test-id="folder-card-name">
|
<div :class="$style['card-header']">
|
||||||
{{ data.name }}
|
<n8n-heading tag="h2" bold size="small" data-test-id="folder-card-name">
|
||||||
</n8n-heading>
|
{{ data.name }}
|
||||||
|
</n8n-heading>
|
||||||
|
<N8nBadge v-if="readOnly" class="ml-3xs" theme="tertiary" bold>
|
||||||
|
{{ i18n.baseText('workflows.item.readonly') }}
|
||||||
|
</N8nBadge>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div :class="$style['card-footer']">
|
<div :class="$style['card-footer']">
|
||||||
@@ -140,28 +136,15 @@ const onBreadcrumbsItemClick = async (item: PathItem) => {
|
|||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<div :class="$style['card-actions']" @click.prevent>
|
<div :class="$style['card-actions']" @click.prevent>
|
||||||
<div :class="$style.breadcrumbs">
|
<div v-if="data.homeProject" :class="$style['project-pill']">
|
||||||
<n8n-breadcrumbs
|
<div :class="$style['home-project']" data-test-id="folder-card-home-project">
|
||||||
:items="breadcrumbs.visibleItems"
|
<n8n-link :to="`/projects/${data.homeProject.id}`">
|
||||||
:hidden-items="breadcrumbs.hiddenItems"
|
<ProjectIcon :icon="projectIcon" :border-less="true" size="mini" />
|
||||||
:path-truncated="breadcrumbs.visibleItems[0]?.parentFolder"
|
<n8n-text size="small" :compact="true" :bold="true" color="text-base">
|
||||||
:show-border="true"
|
{{ projectName }}
|
||||||
:highlight-last-item="false"
|
</n8n-text>
|
||||||
theme="small"
|
</n8n-link>
|
||||||
data-test-id="folder-card-breadcrumbs"
|
</div>
|
||||||
@item-selected="onBreadcrumbsItemClick"
|
|
||||||
>
|
|
||||||
<template v-if="data.homeProject" #prepend>
|
|
||||||
<div :class="$style['home-project']" data-test-id="folder-card-home-project">
|
|
||||||
<n8n-link :to="`/projects/${data.homeProject.id}`">
|
|
||||||
<ProjectIcon :icon="projectIcon" :border-less="true" size="mini" />
|
|
||||||
<n8n-text size="small" :compact="true" :bold="true" color="text-base">
|
|
||||||
{{ projectName }}
|
|
||||||
</n8n-text>
|
|
||||||
</n8n-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n8n-breadcrumbs>
|
|
||||||
</div>
|
</div>
|
||||||
<n8n-action-toggle
|
<n8n-action-toggle
|
||||||
v-if="actions.length"
|
v-if="actions.length"
|
||||||
@@ -186,17 +169,24 @@ const onBreadcrumbsItemClick = async (item: PathItem) => {
|
|||||||
box-shadow: 0 2px 8px rgba(#441c17, 0.1);
|
box-shadow: 0 2px 8px rgba(#441c17, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-icon {
|
.folder-icon {
|
||||||
width: var(--spacing-xl);
|
width: var(--spacing-xl);
|
||||||
height: var(--spacing-xl);
|
height: var(--spacing-xl);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: var(--color-background-dark);
|
color: var(--color-text-base);
|
||||||
color: var(--color-background-light-base);
|
|
||||||
border-radius: 50%;
|
|
||||||
align-content: center;
|
align-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: var(--spacing-xs);
|
||||||
|
margin-bottom: var(--spacing-5xs);
|
||||||
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -215,6 +205,14 @@ const onBreadcrumbsItemClick = async (item: PathItem) => {
|
|||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-4xs) var(--spacing-2xs);
|
||||||
|
border: var(--border-base);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
.home-project span {
|
.home-project span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const showSettings = computed(
|
|||||||
|
|
||||||
const homeProject = computed(() => projectsStore.currentProject ?? projectsStore.personalProject);
|
const homeProject = computed(() => projectsStore.currentProject ?? projectsStore.personalProject);
|
||||||
const isFoldersFeatureEnabled = computed(() => settingsStore.settings.folders.enabled);
|
const isFoldersFeatureEnabled = computed(() => settingsStore.settings.folders.enabled);
|
||||||
const isOverviewPage = computed(() => route.name === VIEWS.WORKFLOWS);
|
const isProjectPage = computed(() => route.name === VIEWS.PROJECTS_WORKFLOWS);
|
||||||
|
|
||||||
const ACTION_TYPES = {
|
const ACTION_TYPES = {
|
||||||
WORKFLOW: 'workflow',
|
WORKFLOW: 'workflow',
|
||||||
@@ -85,11 +85,13 @@ const menu = computed(() => {
|
|||||||
!getResourcePermissions(homeProject.value?.scopes).credential.create,
|
!getResourcePermissions(homeProject.value?.scopes).credential.create,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (isFoldersFeatureEnabled.value && !isOverviewPage.value) {
|
if (isFoldersFeatureEnabled.value && isProjectPage.value) {
|
||||||
items.push({
|
items.push({
|
||||||
value: ACTION_TYPES.FOLDER,
|
value: ACTION_TYPES.FOLDER,
|
||||||
label: i18n.baseText('projects.header.create.folder'),
|
label: i18n.baseText('projects.header.create.folder'),
|
||||||
disabled: false,
|
disabled:
|
||||||
|
sourceControlStore.preferences.branchReadOnly ||
|
||||||
|
!getResourcePermissions(homeProject.value?.scopes).folder.create,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe('WorkflowCard', () => {
|
|||||||
if (!actions) {
|
if (!actions) {
|
||||||
throw new Error('Actions menu not found');
|
throw new Error('Actions menu not found');
|
||||||
}
|
}
|
||||||
expect(actions).toHaveTextContent('Move');
|
expect(actions).toHaveTextContent('Change owner');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show Read only mode', async () => {
|
it('should show Read only mode', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { FolderPathItem, IUser } from '@/Interface';
|
|
||||||
import {
|
import {
|
||||||
DUPLICATE_MODAL_KEY,
|
DUPLICATE_MODAL_KEY,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
@@ -26,7 +25,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
|||||||
import { ResourceType } from '@/utils/projects.utils';
|
import { ResourceType } from '@/utils/projects.utils';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import type { WorkflowResource } from './layouts/ResourcesListLayout.vue';
|
import type { WorkflowResource } from './layouts/ResourcesListLayout.vue';
|
||||||
import { type ProjectIcon as CardProjectIcon, ProjectTypes } from '@/types/projects.types';
|
import type { IUser } from 'n8n-workflow';
|
||||||
|
|
||||||
const WORKFLOW_LIST_ITEM_ACTIONS = {
|
const WORKFLOW_LIST_ITEM_ACTIONS = {
|
||||||
OPEN: 'open',
|
OPEN: 'open',
|
||||||
@@ -39,10 +38,6 @@ const WORKFLOW_LIST_ITEM_ACTIONS = {
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
data: WorkflowResource;
|
data: WorkflowResource;
|
||||||
breadcrumbs: {
|
|
||||||
visibleItems: FolderPathItem[];
|
|
||||||
hiddenItems: FolderPathItem[];
|
|
||||||
};
|
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
workflowListEventBus?: EventBus;
|
workflowListEventBus?: EventBus;
|
||||||
}>(),
|
}>(),
|
||||||
@@ -64,7 +59,6 @@ const message = useMessage();
|
|||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
@@ -96,7 +90,7 @@ const actions = computed(() => {
|
|||||||
|
|
||||||
if (workflowPermissions.value.move && projectsStore.isTeamProjectFeatureEnabled) {
|
if (workflowPermissions.value.move && projectsStore.isTeamProjectFeatureEnabled) {
|
||||||
items.push({
|
items.push({
|
||||||
label: locale.baseText('workflows.item.move'),
|
label: locale.baseText('workflows.item.changeOwner'),
|
||||||
value: WORKFLOW_LIST_ITEM_ACTIONS.MOVE,
|
value: WORKFLOW_LIST_ITEM_ACTIONS.MOVE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,23 +113,6 @@ const formattedCreatedAtDate = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectIcon = computed<CardProjectIcon>(() => {
|
|
||||||
const defaultIcon: CardProjectIcon = { type: 'icon', value: 'layer-group' };
|
|
||||||
if (props.data.homeProject?.type === ProjectTypes.Personal) {
|
|
||||||
return { type: 'icon', value: 'user' };
|
|
||||||
} else if (props.data.homeProject?.type === ProjectTypes.Team) {
|
|
||||||
return props.data.homeProject.icon ?? defaultIcon;
|
|
||||||
}
|
|
||||||
return defaultIcon;
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectName = computed(() => {
|
|
||||||
if (props.data.homeProject?.type === ProjectTypes.Personal) {
|
|
||||||
return i18n.baseText('projects.menu.personal');
|
|
||||||
}
|
|
||||||
return props.data.homeProject?.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onClick(event?: KeyboardEvent | PointerEvent) {
|
async function onClick(event?: KeyboardEvent | PointerEvent) {
|
||||||
if (event?.ctrlKey || event?.metaKey) {
|
if (event?.ctrlKey || event?.metaKey) {
|
||||||
const route = router.resolve({
|
const route = router.resolve({
|
||||||
@@ -291,35 +268,12 @@ const emitWorkflowActiveToggle = (value: { id: string; active: boolean }) => {
|
|||||||
<template #append>
|
<template #append>
|
||||||
<div :class="$style.cardActions" @click.stop>
|
<div :class="$style.cardActions" @click.stop>
|
||||||
<ProjectCardBadge
|
<ProjectCardBadge
|
||||||
v-if="!data.parentFolder"
|
|
||||||
:class="$style.cardBadge"
|
:class="$style.cardBadge"
|
||||||
:resource="data"
|
:resource="data"
|
||||||
:resource-type="ResourceType.Workflow"
|
:resource-type="ResourceType.Workflow"
|
||||||
:resource-type-label="resourceTypeLabel"
|
:resource-type-label="resourceTypeLabel"
|
||||||
:personal-project="projectsStore.personalProject"
|
:personal-project="projectsStore.personalProject"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.breadcrumbs">
|
|
||||||
<n8n-breadcrumbs
|
|
||||||
:items="breadcrumbs.visibleItems"
|
|
||||||
:hidden-items="breadcrumbs.hiddenItems"
|
|
||||||
:path-truncated="breadcrumbs.visibleItems[0]?.parentFolder"
|
|
||||||
:show-border="true"
|
|
||||||
:highlight-last-item="false"
|
|
||||||
theme="small"
|
|
||||||
data-test-id="folder-card-breadcrumbs"
|
|
||||||
>
|
|
||||||
<template v-if="data.homeProject" #prepend>
|
|
||||||
<div :class="$style['home-project']">
|
|
||||||
<n8n-link :to="`/projects/${data.homeProject.id}`">
|
|
||||||
<ProjectIcon :icon="projectIcon" :border-less="true" size="mini" />
|
|
||||||
<n8n-text size="small" :compact="true" :bold="true" color="text-base">{{
|
|
||||||
projectName
|
|
||||||
}}</n8n-text>
|
|
||||||
</n8n-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n8n-breadcrumbs>
|
|
||||||
</div>
|
|
||||||
<WorkflowActivator
|
<WorkflowActivator
|
||||||
class="mr-s"
|
class="mr-s"
|
||||||
:workflow-active="data.active"
|
:workflow-active="data.active"
|
||||||
@@ -383,7 +337,7 @@ const emitWorkflowActiveToggle = (value: { id: string; active: boolean }) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-3xs);
|
gap: var(--spacing-3xs);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.breakpoint('sm-and-down') {
|
@include mixins.breakpoint('sm-and-down') {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const props = withDefaults(
|
|||||||
resourcesRefreshing?: boolean;
|
resourcesRefreshing?: boolean;
|
||||||
// Set to true if sorting and filtering is done outside of the component
|
// Set to true if sorting and filtering is done outside of the component
|
||||||
dontPerformSortingAndFiltering?: boolean;
|
dontPerformSortingAndFiltering?: boolean;
|
||||||
|
hasEmptyState?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
displayName: (resource: Resource) => resource.name || '',
|
displayName: (resource: Resource) => resource.name || '',
|
||||||
@@ -114,6 +115,7 @@ const props = withDefaults(
|
|||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
dontPerformSortingAndFiltering: false,
|
dontPerformSortingAndFiltering: false,
|
||||||
resourcesRefreshing: false,
|
resourcesRefreshing: false,
|
||||||
|
hasEmptyState: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ const filtersModel = computed({
|
|||||||
|
|
||||||
const showEmptyState = computed(() => {
|
const showEmptyState = computed(() => {
|
||||||
return (
|
return (
|
||||||
route.params.folderId === undefined &&
|
props.hasEmptyState &&
|
||||||
props.resources.length === 0 &&
|
props.resources.length === 0 &&
|
||||||
// Don't show empty state if resources are refreshing or if filters are being set
|
// Don't show empty state if resources are refreshing or if filters are being set
|
||||||
!hasFilters.value &&
|
!hasFilters.value &&
|
||||||
@@ -700,6 +702,15 @@ const loadPaginationFromQueryString = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
max-width: 196px;
|
||||||
|
justify-self: end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
@include mixins.breakpoint('sm-and-down') {
|
@include mixins.breakpoint('sm-and-down') {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function useToast() {
|
|||||||
function showToast(config: {
|
function showToast(config: {
|
||||||
title: string;
|
title: string;
|
||||||
message: NotificationOptions['message'];
|
message: NotificationOptions['message'];
|
||||||
onClick?: () => void;
|
onClick?: (event?: MouseEvent) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
customClass?: string;
|
customClass?: string;
|
||||||
|
|||||||
@@ -901,14 +901,13 @@
|
|||||||
"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.success.title": "Folder created",
|
"folders.add.success.title": "Folder created",
|
||||||
"folders.add.success.message": "<a href=\"{link}\">Open {name}</a> now",
|
"folders.add.success.message": "Created new folder \"{folderName}\"<br><a href=\"{link}\">Open folder</a>",
|
||||||
"folders.invalidName.message": "Please provide a valid folder name",
|
"folders.invalidName.message": "Please provide a valid folder name",
|
||||||
"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",
|
||||||
"folders.delete.confirmActionAfterDelete": "What should we do with the data in this folder?",
|
"folder.delete.modal.confirmation": "What should we do with the folders and workflows within this folder?",
|
||||||
"folder.delete.modal.confirmation": "What should we do with {folders} {workflows} in this folder?",
|
|
||||||
"folder.count": "the {count} folder | the {count} folders",
|
"folder.count": "the {count} folder | the {count} folders",
|
||||||
"workflow.count": "the {count} workflow | the {count} workflows",
|
"workflow.count": "the {count} workflow | the {count} workflows",
|
||||||
"folder.and.workflow.separator": "and",
|
"folder.and.workflow.separator": "and",
|
||||||
@@ -917,7 +916,7 @@
|
|||||||
"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",
|
"folders.transfer.action": "Transfer workflows and subfolders to another folder",
|
||||||
"folders.transfer.selectFolder": "Folder to 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",
|
||||||
@@ -2375,6 +2374,7 @@
|
|||||||
"workflows.item.duplicate": "Duplicate",
|
"workflows.item.duplicate": "Duplicate",
|
||||||
"workflows.item.delete": "Delete",
|
"workflows.item.delete": "Delete",
|
||||||
"workflows.item.move": "Move",
|
"workflows.item.move": "Move",
|
||||||
|
"workflows.item.changeOwner": "Change owner",
|
||||||
"workflows.item.updated": "Last updated",
|
"workflows.item.updated": "Last updated",
|
||||||
"workflows.item.created": "Created",
|
"workflows.item.created": "Created",
|
||||||
"workflows.item.readonly": "Read only",
|
"workflows.item.readonly": "Read only",
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ const currentFolderId = ref<string | null>(null);
|
|||||||
* or on each folder card, and then they are applied to the clicked folder
|
* or on each folder card, and then they are applied to the clicked folder
|
||||||
* 'onlyAvailableOn' is used to specify where the action should be available, if not specified it will be available on both
|
* 'onlyAvailableOn' is used to specify where the action should be available, if not specified it will be available on both
|
||||||
*/
|
*/
|
||||||
const folderActions = ref<Array<UserAction & { onlyAvailableOn?: 'mainBreadcrumbs' | 'card' }>>([
|
const folderActions = computed<
|
||||||
|
Array<UserAction & { onlyAvailableOn?: 'mainBreadcrumbs' | 'card' }>
|
||||||
|
>(() => [
|
||||||
{
|
{
|
||||||
label: i18n.baseText('generic.open'),
|
label: i18n.baseText('generic.open'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.OPEN,
|
value: FOLDER_LIST_ITEM_ACTIONS.OPEN,
|
||||||
@@ -135,39 +137,42 @@ const folderActions = ref<Array<UserAction & { onlyAvailableOn?: 'mainBreadcrumb
|
|||||||
{
|
{
|
||||||
label: i18n.baseText('folders.actions.create'),
|
label: i18n.baseText('folders.actions.create'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.CREATE,
|
value: FOLDER_LIST_ITEM_ACTIONS.CREATE,
|
||||||
disabled: false,
|
disabled: readOnlyEnv.value || !hasPermissionToCreateFolders.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.baseText('folders.actions.create.workflow'),
|
label: i18n.baseText('folders.actions.create.workflow'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.CREATE_WORKFLOW,
|
value: FOLDER_LIST_ITEM_ACTIONS.CREATE_WORKFLOW,
|
||||||
disabled: false,
|
disabled: readOnlyEnv.value || !hasPermissionToCreateWorkflows.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.baseText('generic.rename'),
|
label: i18n.baseText('generic.rename'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.RENAME,
|
value: FOLDER_LIST_ITEM_ACTIONS.RENAME,
|
||||||
disabled: false,
|
disabled: readOnlyEnv.value || !hasPermissionToUpdateFolders.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.baseText('folders.actions.moveToFolder'),
|
label: i18n.baseText('folders.actions.moveToFolder'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.MOVE,
|
value: FOLDER_LIST_ITEM_ACTIONS.MOVE,
|
||||||
disabled: true,
|
disabled: readOnlyEnv.value || !hasPermissionToUpdateFolders.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.baseText('generic.delete'),
|
label: i18n.baseText('generic.delete'),
|
||||||
value: FOLDER_LIST_ITEM_ACTIONS.DELETE,
|
value: FOLDER_LIST_ITEM_ACTIONS.DELETE,
|
||||||
disabled: false,
|
disabled: readOnlyEnv.value || !hasPermissionToDeleteFolders.value,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const folderCardActions = computed(() =>
|
const folderCardActions = computed(() =>
|
||||||
folderActions.value.filter(
|
folderActions.value.filter(
|
||||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'card',
|
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'card',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainBreadcrumbsActions = computed(() =>
|
const mainBreadcrumbsActions = computed(() =>
|
||||||
folderActions.value.filter(
|
folderActions.value.filter(
|
||||||
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'mainBreadcrumbs',
|
(action) => !action.onlyAvailableOn || action.onlyAvailableOn === 'mainBreadcrumbs',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
const foldersEnabled = computed(() => settingsStore.settings.folders.enabled);
|
const foldersEnabled = computed(() => settingsStore.settings.folders.enabled);
|
||||||
const isOverviewPage = computed(() => route.name === VIEWS.WORKFLOWS);
|
const isOverviewPage = computed(() => route.name === VIEWS.WORKFLOWS);
|
||||||
@@ -181,6 +186,26 @@ const currentFolder = computed(() => {
|
|||||||
return currentFolderId.value ? foldersStore.breadcrumbsCache[currentFolderId.value] : null;
|
return currentFolderId.value ? foldersStore.breadcrumbsCache[currentFolderId.value] : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasPermissionToCreateFolders = computed(() => {
|
||||||
|
if (!currentProject.value) return false;
|
||||||
|
return getResourcePermissions(currentProject.value.scopes).folder.create === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPermissionToUpdateFolders = computed(() => {
|
||||||
|
if (!currentProject.value) return false;
|
||||||
|
return getResourcePermissions(currentProject.value.scopes).folder.update === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPermissionToDeleteFolders = computed(() => {
|
||||||
|
if (!currentProject.value) return false;
|
||||||
|
return getResourcePermissions(currentProject.value.scopes).folder.delete === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPermissionToCreateWorkflows = computed(() => {
|
||||||
|
if (!currentProject.value) return false;
|
||||||
|
return getResourcePermissions(currentProject.value.scopes).workflow.create === true;
|
||||||
|
});
|
||||||
|
|
||||||
const currentProject = computed(() => projectsStore.currentProject);
|
const currentProject = computed(() => projectsStore.currentProject);
|
||||||
|
|
||||||
const projectName = computed(() => {
|
const projectName = computed(() => {
|
||||||
@@ -385,8 +410,8 @@ const fetchWorkflows = async () => {
|
|||||||
{
|
{
|
||||||
name: filters.value.search || undefined,
|
name: filters.value.search || undefined,
|
||||||
active: activeFilter,
|
active: activeFilter,
|
||||||
tags,
|
tags: tags.length ? tags : undefined,
|
||||||
parentFolderId: parentFolder ?? '0', // 0 is the root folder in the API
|
parentFolderId: parentFolder ?? (isOverviewPage.value ? undefined : '0'), // Sending 0 will only show one level of folders
|
||||||
},
|
},
|
||||||
fetchFolders,
|
fetchFolders,
|
||||||
);
|
);
|
||||||
@@ -440,14 +465,14 @@ const onSortUpdated = async (sort: string) => {
|
|||||||
const onFiltersUpdated = async () => {
|
const onFiltersUpdated = async () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
saveFiltersOnQueryString();
|
saveFiltersOnQueryString();
|
||||||
await fetchWorkflows();
|
await callDebounced(fetchWorkflows, { debounceTime: 100, trailing: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearchUpdated = async (search: string) => {
|
const onSearchUpdated = async (search: string) => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
saveFiltersOnQueryString();
|
saveFiltersOnQueryString();
|
||||||
if (search) {
|
if (search) {
|
||||||
await callDebounced(fetchWorkflows, { debounceTime: 500, trailing: true });
|
await callDebounced(fetchWorkflows, { debounceTime: 100, trailing: true });
|
||||||
} else {
|
} else {
|
||||||
// No need to debounce when clearing search
|
// No need to debounce when clearing search
|
||||||
await fetchWorkflows();
|
await fetchWorkflows();
|
||||||
@@ -456,12 +481,12 @@ const onSearchUpdated = async (search: string) => {
|
|||||||
|
|
||||||
const setCurrentPage = async (page: number) => {
|
const setCurrentPage = async (page: number) => {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
await fetchWorkflows();
|
await callDebounced(fetchWorkflows, { debounceTime: 100, trailing: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPageSize = async (size: number) => {
|
const setPageSize = async (size: number) => {
|
||||||
pageSize.value = size;
|
pageSize.value = size;
|
||||||
await fetchWorkflows();
|
await callDebounced(fetchWorkflows, { debounceTime: 100, trailing: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickTag = async (tagId: string) => {
|
const onClickTag = async (tagId: string) => {
|
||||||
@@ -735,25 +760,6 @@ const mainBreadcrumbs = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Card breadcrumbs items that show on workflow and folder cards
|
|
||||||
* These show path to the current folder with up to one parent visible
|
|
||||||
*/
|
|
||||||
const cardBreadcrumbs = computed(() => {
|
|
||||||
const visibleItems = visibleBreadcrumbsItems.value;
|
|
||||||
const hiddenItems = hiddenBreadcrumbsItems.value;
|
|
||||||
if (visibleItems.length > 1) {
|
|
||||||
return {
|
|
||||||
visibleItems: [visibleItems[visibleItems.length - 1]],
|
|
||||||
hiddenItems: [...hiddenItems, ...visibleItems.slice(0, visibleItems.length - 1)],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
visibleItems,
|
|
||||||
hiddenItems,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const onBreadcrumbItemClick = (item: PathItem) => {
|
const onBreadcrumbItemClick = (item: PathItem) => {
|
||||||
if (item.href) {
|
if (item.href) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -839,14 +845,18 @@ const onFolderCardAction = async (payload: { action: string; folderId: string })
|
|||||||
// Reusable action handlers
|
// Reusable action handlers
|
||||||
// Both action handlers ultimately call these methods once folder to apply action to is determined
|
// Both action handlers ultimately call these methods once folder to apply action to is determined
|
||||||
const createFolder = async (parent: { id: string; name: string; type: 'project' | 'folder' }) => {
|
const createFolder = async (parent: { id: string; name: string; type: 'project' | 'folder' }) => {
|
||||||
|
// Rules for folder name:
|
||||||
|
// - Invalid characters: \/:*?"<>|
|
||||||
|
// - Invalid name: empty or only dots
|
||||||
|
const validFolderNameRegex = /^(?!\.+$)(?!\s+$)[^\\/:*?"<>|]{1,100}$/;
|
||||||
|
|
||||||
const promptResponsePromise = message.prompt(
|
const promptResponsePromise = message.prompt(
|
||||||
i18n.baseText('folders.add.to.parent.message', { interpolate: { parent: parent.name } }),
|
i18n.baseText('folders.add.to.parent.message', { interpolate: { parent: parent.name } }),
|
||||||
{
|
{
|
||||||
confirmButtonText: i18n.baseText('generic.create'),
|
confirmButtonText: i18n.baseText('generic.create'),
|
||||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||||
inputErrorMessage: i18n.baseText('folders.invalidName.message'),
|
inputErrorMessage: i18n.baseText('folders.invalidName.message'),
|
||||||
inputValue: '',
|
inputPattern: validFolderNameRegex,
|
||||||
inputPattern: /^[a-zA-Z0-9-_ ]{1,100}$/,
|
|
||||||
customClass: 'add-folder-modal',
|
customClass: 'add-folder-modal',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -864,14 +874,20 @@ const createFolder = async (parent: { id: string; name: string; type: 'project'
|
|||||||
if (newFolder.parentFolder) {
|
if (newFolder.parentFolder) {
|
||||||
newFolderURL = `/projects/${route.params.projectId}/folders/${newFolder.id}/workflows`;
|
newFolderURL = `/projects/${route.params.projectId}/folders/${newFolder.id}/workflows`;
|
||||||
}
|
}
|
||||||
toast.showMessage({
|
toast.showToast({
|
||||||
title: i18n.baseText('folders.add.success.title'),
|
title: i18n.baseText('folders.add.success.title'),
|
||||||
message: i18n.baseText('folders.add.success.message', {
|
message: i18n.baseText('folders.add.success.message', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
link: newFolderURL,
|
link: newFolderURL,
|
||||||
name: newFolder.name,
|
folderName: newFolder.name,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
onClick: (event: MouseEvent | undefined) => {
|
||||||
|
if (event?.target instanceof HTMLAnchorElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
void router.push(newFolderURL);
|
||||||
|
}
|
||||||
|
},
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
// If we are on an empty list, just add the new folder to the list
|
// If we are on an empty list, just add the new folder to the list
|
||||||
@@ -978,6 +994,7 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
:custom-page-size="DEFAULT_WORKFLOW_PAGE_SIZE"
|
:custom-page-size="DEFAULT_WORKFLOW_PAGE_SIZE"
|
||||||
:total-items="workflowsStore.totalWorkflowCount"
|
:total-items="workflowsStore.totalWorkflowCount"
|
||||||
:dont-perform-sorting-and-filtering="true"
|
:dont-perform-sorting-and-filtering="true"
|
||||||
|
:has-empty-state="foldersStore.totalWorkflowCount === 0 && !currentFolderId"
|
||||||
@click:add="addWorkflow"
|
@click:add="addWorkflow"
|
||||||
@update:search="onSearchUpdated"
|
@update:search="onSearchUpdated"
|
||||||
@update:current-page="setCurrentPage"
|
@update:current-page="setCurrentPage"
|
||||||
@@ -989,7 +1006,7 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
<ProjectHeader @create-folder="createFolderInCurrent" />
|
<ProjectHeader @create-folder="createFolderInCurrent" />
|
||||||
</template>
|
</template>
|
||||||
<template v-if="showFolders" #add-button>
|
<template v-if="showFolders" #add-button>
|
||||||
<N8nTooltip placement="top">
|
<N8nTooltip placement="top" :disabled="readOnlyEnv || !hasPermissionToCreateFolders">
|
||||||
<template #content>
|
<template #content>
|
||||||
{{
|
{{
|
||||||
currentParentName
|
currentParentName
|
||||||
@@ -1005,6 +1022,7 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
type="tertiary"
|
type="tertiary"
|
||||||
data-test-id="add-folder-button"
|
data-test-id="add-folder-button"
|
||||||
:class="$style['add-folder-button']"
|
:class="$style['add-folder-button']"
|
||||||
|
:disabled="readOnlyEnv || !hasPermissionToCreateFolders"
|
||||||
@click="createFolderInCurrent"
|
@click="createFolderInCurrent"
|
||||||
/>
|
/>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
@@ -1060,7 +1078,7 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
v-if="(data as FolderResource | WorkflowResource).resourceType === 'folder'"
|
v-if="(data as FolderResource | WorkflowResource).resourceType === 'folder'"
|
||||||
:data="data as FolderResource"
|
:data="data as FolderResource"
|
||||||
:actions="folderCardActions"
|
:actions="folderCardActions"
|
||||||
:breadcrumbs="cardBreadcrumbs"
|
:read-only="readOnlyEnv || (!hasPermissionToDeleteFolders && !hasPermissionToCreateFolders)"
|
||||||
class="mb-2xs"
|
class="mb-2xs"
|
||||||
@action="onFolderCardAction"
|
@action="onFolderCardAction"
|
||||||
/>
|
/>
|
||||||
@@ -1069,7 +1087,6 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
data-test-id="resources-list-item"
|
data-test-id="resources-list-item"
|
||||||
class="mb-2xs"
|
class="mb-2xs"
|
||||||
:data="data as WorkflowResource"
|
:data="data as WorkflowResource"
|
||||||
:breadcrumbs="cardBreadcrumbs"
|
|
||||||
:workflow-list-event-bus="workflowListEventBus"
|
:workflow-list-event-bus="workflowListEventBus"
|
||||||
:read-only="readOnlyEnv"
|
:read-only="readOnlyEnv"
|
||||||
@click:tag="onClickTag"
|
@click:tag="onClickTag"
|
||||||
@@ -1219,6 +1236,7 @@ const deleteFolder = async (folderId: string, workflowCount: number, subFolderCo
|
|||||||
.breadcrumbs-container {
|
.breadcrumbs-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs-loading {
|
.breadcrumbs-loading {
|
||||||
|
|||||||
Reference in New Issue
Block a user