Skip to content

feat(editor): Combine Move and Change owner modals #15756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f47eeaa
feat(editor): Change the move folder modal to open and focus automati…
Cadiac May 19, 2025
50563fc
feat(editor): Display path breadcrumbs on move folder modal
Cadiac May 20, 2025
9ecbaa0
feat(editor): Selecting the project to move resources to
Cadiac May 20, 2025
07c2a8a
feat(editor): wip on cross project transfers on the new modal
Cadiac May 20, 2025
8b53e20
feat(editor): Transferring workflows and projects with the single modal
Cadiac May 20, 2025
e581fa0
feat(editor): Make transferring workflows and folders to other users …
Cadiac May 22, 2025
ff0c6cc
feat(editor): Fix picking the project on non admin users with limited…
Cadiac May 22, 2025
25d1b90
wip
Cadiac May 23, 2025
9820ac7
feat(editor): Update dropdown comments and make it filter parent fold…
Cadiac May 26, 2025
9c61718
feat(editor): Ask to share credentials when transferring folders
Cadiac May 26, 2025
8e9b4bb
feat(editor): Disable moving of workflows on Shared with you page
Cadiac May 26, 2025
a8e2dfb
feat(editor): Make folder picker work if there are many folders
Cadiac May 26, 2025
3473338
Sort the folder results by path
Cadiac May 26, 2025
42df1dd
feat(editor): Shorten the folder dropdown paths and match design styles
Cadiac May 26, 2025
e0e9bda
feat(editor): Display project icon on ProjectSharing picker
Cadiac May 26, 2025
fa0b06b
feat(editor): Display a warning when not sharing credentials at transfer
Cadiac May 26, 2025
d9d4c69
test(editor): Make UI tests pass with the updated move modal
Cadiac May 26, 2025
c428181
test(editor): Add tests for MoveToFolderModal
Cadiac May 27, 2025
32f397e
feat(editor): Only show root folder if query matches to it
Cadiac May 27, 2025
58eff79
test(editor): Fix e2e tests with the new move folder modal
Cadiac May 27, 2025
ba282f1
feat(editor): Disable move option from workflows page as it doesn't w…
Cadiac May 27, 2025
b20a188
refactor(editor): Make MoveToFolderDropdown not use projectsStore and…
Cadiac May 27, 2025
24362cc
feat(editor): Make the DeleteFolderModal work with the new folder pic…
Cadiac May 27, 2025
890883b
refactor(editor): Remove unused code
Cadiac May 27, 2025
c5a2b7c
refactor(editor): Bit of cleanup
Cadiac May 27, 2025
2bc6cec
test(editor): Add tests for folder store
Cadiac May 27, 2025
06adddc
test(editor): Cover case of transferring to personal project on move …
Cadiac May 27, 2025
eb8ae8d
fix(editor): Show correct link on workflow transfer target
Cadiac May 28, 2025
e7d15f1
refactor(editor): Move shared project name truncate logic to utils
Cadiac May 28, 2025
f52b15c
fix(editor): Prevent extra space from end when only transferring fold…
Cadiac May 28, 2025
f43cd7e
Merge branch 'master' into ado-3161-fe-combine-change-owner-and-move-…
Cadiac May 28, 2025
fe31b8f
test(editor): Make e2e tests pass with move to folder modal changes
Cadiac May 28, 2025
aa5499c
fix(editor): don't accidentally apply the list item padding to all it…
Cadiac May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cypress/composables/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,12 @@ function deleteFolderAndMoveContents(folderName: string, destinationName: string
function moveFolder(folderName: string, destinationName: string) {
cy.intercept('PATCH', '/rest/projects/**').as('moveFolder');
getMoveFolderModal().should('be.visible');
getMoveFolderModal().find('h1').first().contains(`Move "${folderName}" to another folder`);
getMoveFolderModal().find('h1').first().contains(`Move folder ${folderName}`);

// The dropdown focuses after a small delay (once modal's slide in animation is done).
// On the component we listen for an event, but here the wait should be very predictable.
cy.wait(500);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a fixed wait time (500ms) can lead to flaky tests. Consider using Cypress's built-in retry and timeout mechanisms instead of arbitrary waits.

Suggested change
cy.wait(500);
getMoveFolderModal().find('input').should('be.visible').should('be.enabled');

Copy link
Contributor Author

@Cadiac Cadiac May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is tricky, and while I'm not a fan of the fixed timeout here it is way longer than the brief slide in animation that the modal does before opening the dropdown and focusing on it. If the idea is to test that the autofocus works some kind of wait has to happen here - the first couple of letteres typed were ignored before I added this.

Perhaps waiting for a focused field to appear could be the solution...

The reason the dropdown opens & focuses only once the modal stops is that otherwise the popper dropdown gets misplaced if opened mid animation.


// Try to find current folder in the dropdown
// This tests that auto-focus worked as expected
cy.focused().type(folderName, { delay: 50 });
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/49-folders.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ describe('Folders', () => {
goToPersonalProject();
createFolderFromProjectHeader('Test parent');
createFolderInsideFolder('Move me to root', 'Test parent');
moveFolderFromFolderCardActions('Move me to root', 'Personal');
moveFolderFromFolderCardActions('Move me to root', 'No folder (project root)');
// Parent folder should be empty
getFolderEmptyState().should('exist');
// Child folder should be in the root
Expand Down
9 changes: 6 additions & 3 deletions packages/frontend/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ export type BaseFolderItem = BaseResource & {
subFolderCount: number;
parentFolder?: ResourceParentFolder;
homeProject?: ProjectSharingData;
sharedWithProjects?: ProjectSharingData[];
tags?: ITag[];
};

Expand All @@ -387,12 +386,16 @@ export interface FolderListItem extends BaseFolderItem {
resource: 'folder';
}

export interface ChangeLocationSearchResult extends BaseFolderItem {
resource: 'folder' | 'project';
export interface ChangeLocationSearchResponseItem extends BaseFolderItem {
path: string[];
}

export type FolderPathItem = PathItem & { parentFolder?: string };

export interface ChangeLocationSearchResult extends ChangeLocationSearchResponseItem {
resource: 'folder' | 'project';
}

export type WorkflowListResource = WorkflowListItem | FolderListItem;

export type FolderCreateResponse = Omit<
Expand Down
20 changes: 20 additions & 0 deletions packages/frontend/editor-ui/src/api/workflows.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,23 @@ export async function moveWorkflowToProject(
): Promise<void> {
return await makeRestApiRequest(context, 'PUT', `/workflows/${id}/transfer`, body);
}

export async function moveFolderToProject(
context: IRestApiContext,
projectId: string,
folderId: string,
destinationProjectId: string,
destinationParentFolderId?: string,
shareCredentials?: string[],
): Promise<void> {
return await makeRestApiRequest(
context,
'PUT',
`/projects/${projectId}/folders/${folderId}/transfer`,
{
destinationProjectId,
destinationParentFolderId: destinationParentFolderId ?? '0',
shareCredentials,
},
);
}
25 changes: 22 additions & 3 deletions packages/frontend/editor-ui/src/api/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {
ChangeLocationSearchResult,
ChangeLocationSearchResponseItem,
FolderCreateResponse,
FolderTreeResponseItem,
IExecutionResponse,
IExecutionsCurrentSummaryExtended,
IRestApiContext,
IUsedCredential,
IWorkflowDb,
NewWorkflowResponse,
WorkflowListResource,
Expand Down Expand Up @@ -146,16 +147,34 @@ export async function getProjectFolders(
excludeFolderIdAndDescendants?: string;
name?: string;
},
): Promise<ChangeLocationSearchResult[]> {
const res = await getFullApiResponse<ChangeLocationSearchResult[]>(
select?: string[],
): Promise<{ data: ChangeLocationSearchResponseItem[]; count: number }> {
const res = await getFullApiResponse<ChangeLocationSearchResponseItem[]>(
context,
'GET',
`/projects/${projectId}/folders`,
{
...(filter ? { filter } : {}),
...(options ? options : {}),
...(select ? { select: JSON.stringify(select) } : {}),
},
);
return {
data: res.data,
count: res.count,
};
}

export async function getFolderUsedCredentials(
context: IRestApiContext,
projectId: string,
folderId: string,
): Promise<IUsedCredential[]> {
const res = await getFullApiResponse<IUsedCredential[]>(
context,
'GET',
`/projects/${projectId}/folders/${folderId}/credentials`,
);
return res.data;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useFoldersStore } from '@/stores/folders.store';
import { useRoute } from 'vue-router';
import { useProjectsStore } from '@/stores/projects.store';
import { ProjectTypes } from '@/types/projects.types';
import type { ChangeLocationSearchResult } from '@/Interface';

const props = defineProps<{
modalName: string;
Expand All @@ -32,7 +33,7 @@ const projectsStore = useProjectsStore();
const loading = ref(false);
const operation = ref('');
const deleteConfirmText = ref('');
const selectedFolder = ref<{ id: string; name: string; type: 'folder' | 'project' } | null>(null);
const selectedFolder = ref<ChangeLocationSearchResult | null>(null);

const folderToDelete = computed(() => {
if (!props.activeId) return null;
Expand Down Expand Up @@ -106,7 +107,7 @@ async function onSubmit() {
loading.value = true;

const newParentId =
selectedFolder.value?.type === 'project' ? '0' : (selectedFolder.value?.id ?? undefined);
selectedFolder.value?.resource === 'project' ? '0' : (selectedFolder.value?.id ?? undefined);

await foldersStore.deleteFolder(route.params.projectId as string, props.activeId, newParentId);

Expand Down Expand Up @@ -134,7 +135,7 @@ async function onSubmit() {
}
}

const onFolderSelected = (payload: { id: string; name: string; type: 'folder' | 'project' }) => {
const onFolderSelected = (payload: ChangeLocationSearchResult) => {
selectedFolder.value = payload;
};
</script>
Expand Down Expand Up @@ -180,8 +181,10 @@ const onFolderSelected = (payload: { id: string; name: string; type: 'folder' |
}}</n8n-text>
<MoveToFolderDropdown
v-if="projectsStore.currentProject"
:current-folder-id="props.activeId"
:selected-location="selectedFolder"
:selected-project-id="projectsStore.currentProject?.id"
:current-project-id="projectsStore.currentProject?.id"
:current-folder-id="props.activeId"
:parent-folder-id="folderToDelete?.parentFolder"
@location:selected="onFolderSelected"
/>
Expand Down
Loading