Refactor: add stable folder layout foundation
This commit is contained in:
@@ -36,6 +36,7 @@ type ProjectSelectorProps = {
|
||||
type ProjectFolderNode = {
|
||||
kind: "folder";
|
||||
id: string;
|
||||
path: string;
|
||||
label: string;
|
||||
meta?: string;
|
||||
children: ProjectTreeNode[];
|
||||
@@ -50,6 +51,7 @@ type ProjectTreeNode = ProjectFolderNode | ProjectLeafNode;
|
||||
|
||||
type PersistedProjectFolderRecord = {
|
||||
id: string;
|
||||
path: string;
|
||||
label: string;
|
||||
children: PersistedProjectFolderRecord[];
|
||||
};
|
||||
@@ -60,6 +62,7 @@ type ProjectFoldersResponse = {
|
||||
renamedFolder?: PersistedProjectFolderRecord;
|
||||
movedFolder?: PersistedProjectFolderRecord;
|
||||
previousFolderId?: string;
|
||||
previousFolderPath?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
@@ -91,6 +94,7 @@ const buildPersistedFolderNodes = (folders: readonly PersistedProjectFolderRecor
|
||||
folders.map((folder) => ({
|
||||
kind: "folder",
|
||||
id: folder.id,
|
||||
path: folder.path,
|
||||
label: folder.label,
|
||||
children: buildPersistedFolderNodes(folder.children ?? []),
|
||||
}));
|
||||
@@ -578,18 +582,39 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
|
||||
const currentNodes = projectTreeNodes();
|
||||
const draggedLocation = findTreeNodeLocation(currentNodes, nextDragState.draggedNodeId, projectTreeAdapter);
|
||||
const persistedParentId = nextDragState.dropTarget.parentId;
|
||||
const canPersistMove = isUuidString(selectedProject().id);
|
||||
const persistedParentLocation = persistedParentId
|
||||
? findTreeNodeLocation(currentNodes, persistedParentId, projectTreeAdapter)
|
||||
const draggedFolderPath = draggedLocation?.node.kind === "folder" ? draggedLocation.node.path : null;
|
||||
const previewNodes = moveTreeNode(currentNodes, nextDragState.draggedNodeId, nextDragState.dropTarget as ProjectDragTarget, projectTreeAdapter);
|
||||
const previewLocation = findTreeNodeLocation(previewNodes, nextDragState.draggedNodeId, projectTreeAdapter);
|
||||
const persistedParentLocation = previewLocation?.parentId
|
||||
? findTreeNodeLocation(previewNodes, previewLocation.parentId, projectTreeAdapter)
|
||||
: null;
|
||||
const persistedParentFolderPath =
|
||||
persistedParentLocation?.node.kind === "folder" ? persistedParentLocation.node.path : null;
|
||||
const previewSiblings = previewLocation?.parentId
|
||||
? persistedParentLocation?.node.kind === "folder"
|
||||
? persistedParentLocation.node.children
|
||||
: []
|
||||
: previewNodes;
|
||||
const targetIndex = previewLocation
|
||||
? previewSiblings
|
||||
.slice(0, previewLocation.index)
|
||||
.filter((node) => node.kind === "folder").length
|
||||
: 0;
|
||||
|
||||
if (
|
||||
canPersistMove &&
|
||||
draggedLocation?.node.kind === "folder" &&
|
||||
(persistedParentId === null || persistedParentLocation?.node.kind === "folder")
|
||||
draggedFolderPath &&
|
||||
(!previewLocation?.parentId || persistedParentLocation?.node.kind === "folder")
|
||||
) {
|
||||
void movePersistedFolder(draggedLocation.node.id, persistedParentId);
|
||||
void movePersistedFolder(
|
||||
draggedFolderPath,
|
||||
persistedParentFolderPath,
|
||||
draggedLocation.node.id,
|
||||
persistedParentLocation?.node.kind === "folder" ? persistedParentLocation.node.id : null,
|
||||
targetIndex,
|
||||
);
|
||||
} else {
|
||||
setProjectTreeNodes((current) =>
|
||||
moveTreeNode(current, nextDragState.draggedNodeId, nextDragState.dropTarget as ProjectDragTarget, projectTreeAdapter),
|
||||
@@ -674,6 +699,11 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
setPendingFolderRenameName(label);
|
||||
};
|
||||
|
||||
const resolveFolderPath = (folderId: string): string | null => {
|
||||
const location = findTreeNodeLocation(projectTreeNodes(), folderId, projectTreeAdapter);
|
||||
return location && location.node.kind === "folder" ? location.node.path : null;
|
||||
};
|
||||
|
||||
const submitPendingFolder = async (): Promise<void> => {
|
||||
const name = pendingFolderName().trim();
|
||||
const draft = pendingFolderDraft();
|
||||
@@ -694,6 +724,12 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentFolderPath = draft.parentId ? resolveFolderPath(draft.parentId) : null;
|
||||
if (draft.parentId && !parentFolderPath) {
|
||||
cancelPendingFolder();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
|
||||
method: "POST",
|
||||
@@ -703,7 +739,7 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
parentFolderId: draft.parentId,
|
||||
parentFolderId: parentFolderPath,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -727,9 +763,14 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
|
||||
const folderPath = resolveFolderPath(folderId);
|
||||
if (!folderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${resolveAPIBase()}/projects/${projectId}/folders?folderId=${encodeURIComponent(folderId)}`,
|
||||
`${resolveAPIBase()}/projects/${projectId}/folders?folderId=${encodeURIComponent(folderPath)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -751,9 +792,15 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const movePersistedFolder = async (folderId: string, parentFolderId: string | null): Promise<void> => {
|
||||
const movePersistedFolder = async (
|
||||
folderPath: string,
|
||||
parentFolderPath: string | null,
|
||||
folderNodeId: string,
|
||||
parentNodeId: string | null,
|
||||
targetIndex: number,
|
||||
): Promise<void> => {
|
||||
const projectId = selectedProject().id;
|
||||
if (!folderId || !isUuidString(projectId)) {
|
||||
if (!folderPath || !folderNodeId || !isUuidString(projectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -765,8 +812,11 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderId,
|
||||
parentFolderId,
|
||||
folderId: folderPath,
|
||||
folderNodeId,
|
||||
parentFolderId: parentFolderPath,
|
||||
parentNodeId,
|
||||
targetIndex,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -777,14 +827,6 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
}
|
||||
|
||||
setPersistedFolders(readPersistedFolders(body));
|
||||
|
||||
const previousFolderId = body.data?.previousFolderId;
|
||||
const movedFolderId = body.data?.movedFolder?.id;
|
||||
if (previousFolderId && movedFolderId && previousFolderId !== movedFolderId) {
|
||||
setCollapsedFolderIds((current) =>
|
||||
current.map((id) => (id === previousFolderId ? movedFolderId : id)),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -810,6 +852,12 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
|
||||
const folderPath = resolveFolderPath(draft.folderId);
|
||||
if (!folderPath) {
|
||||
cancelPendingFolderRename();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
|
||||
method: "PATCH",
|
||||
@@ -818,7 +866,7 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderId: draft.folderId,
|
||||
folderId: folderPath,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
@@ -832,14 +880,6 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
setPersistedFolders(readPersistedFolders(body));
|
||||
setPendingFolderRename(null);
|
||||
setPendingFolderRenameName("");
|
||||
|
||||
const previousFolderId = body.data?.previousFolderId;
|
||||
const renamedFolderId = body.data?.renamedFolder?.id;
|
||||
if (previousFolderId && renamedFolderId && previousFolderId !== renamedFolderId) {
|
||||
setCollapsedFolderIds((current) =>
|
||||
current.map((id) => (id === previousFolderId ? renamedFolderId : id)),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user