Feat: add persisted folder rename
This commit is contained in:
@@ -57,6 +57,8 @@ type PersistedProjectFolderRecord = {
|
||||
type ProjectFoldersResponse = {
|
||||
data?: {
|
||||
folders?: PersistedProjectFolderRecord[];
|
||||
renamedFolder?: PersistedProjectFolderRecord;
|
||||
previousFolderId?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
@@ -67,6 +69,11 @@ type PendingProjectFolderDraft = {
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type PendingProjectFolderRename = {
|
||||
folderId: string;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type ProjectDragTarget = NavTreeDropTarget;
|
||||
|
||||
type ProjectDragState = {
|
||||
@@ -173,6 +180,11 @@ const ProjectFolderBranch = (props: {
|
||||
onPendingFolderNameChange: (value: string) => void;
|
||||
onSubmitPendingFolder: () => void;
|
||||
onCancelPendingFolder: () => void;
|
||||
pendingFolderRename: PendingProjectFolderRename | null;
|
||||
pendingFolderRenameName: string;
|
||||
onPendingFolderRenameChange: (value: string) => void;
|
||||
onSubmitPendingFolderRename: () => void;
|
||||
onCancelPendingFolderRename: () => void;
|
||||
dragState: ProjectDragState | null;
|
||||
isTreeClickSuppressed: () => boolean;
|
||||
}): JSX.Element => (
|
||||
@@ -197,51 +209,65 @@ const ProjectFolderBranch = (props: {
|
||||
|
||||
if (node.kind === "folder") {
|
||||
const isCollapsed = (): boolean => props.isFolderCollapsed(node.id);
|
||||
const isRenaming = (): boolean => props.pendingFolderRename?.folderId === node.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
classList={{
|
||||
[styles.treeItem]: true,
|
||||
[styles.treeItemFolder]: true,
|
||||
[styles.treeItemDragging]: isDraggedNode(),
|
||||
[styles.treeItemDropBefore]: dropIntent() === "before",
|
||||
[styles.treeItemDropAfter]: dropIntent() === "after",
|
||||
[styles.treeItemDropInside]: dropIntent() === "inside",
|
||||
}}
|
||||
style={{ "--tree-depth": String(props.depth) }}
|
||||
aria-expanded={!isCollapsed()}
|
||||
onClick={() => {
|
||||
if (props.dragState || props.isTreeClickSuppressed()) {
|
||||
return;
|
||||
}
|
||||
<Show
|
||||
when={isRenaming()}
|
||||
fallback={
|
||||
<button
|
||||
type="button"
|
||||
classList={{
|
||||
[styles.treeItem]: true,
|
||||
[styles.treeItemFolder]: true,
|
||||
[styles.treeItemDragging]: isDraggedNode(),
|
||||
[styles.treeItemDropBefore]: dropIntent() === "before",
|
||||
[styles.treeItemDropAfter]: dropIntent() === "after",
|
||||
[styles.treeItemDropInside]: dropIntent() === "inside",
|
||||
}}
|
||||
style={{ "--tree-depth": String(props.depth) }}
|
||||
aria-expanded={!isCollapsed()}
|
||||
onClick={() => {
|
||||
if (props.dragState || props.isTreeClickSuppressed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onToggleFolder(node.id);
|
||||
}}
|
||||
onContextMenu={(event): void => props.onOpenFolderMenu(event, node)}
|
||||
onPointerDown={(event): void => props.onNodePointerDown(event, node.id)}
|
||||
onPointerMove={(event): void =>
|
||||
props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
|
||||
}
|
||||
onPointerEnter={(event): void =>
|
||||
props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
|
||||
props.onToggleFolder(node.id);
|
||||
}}
|
||||
onContextMenu={(event): void => props.onOpenFolderMenu(event, node)}
|
||||
onPointerDown={(event): void => props.onNodePointerDown(event, node.id)}
|
||||
onPointerMove={(event): void =>
|
||||
props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
|
||||
}
|
||||
onPointerEnter={(event): void =>
|
||||
props.onNodePointerMove(event, props.parentId, indexAccessor(), node)
|
||||
}
|
||||
>
|
||||
<ChevronRight
|
||||
classList={{
|
||||
[styles.folderChevron]: true,
|
||||
[styles.folderChevronOpen]: !isCollapsed(),
|
||||
}}
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Folder class={styles.icon} size={18} strokeWidth={2} />
|
||||
<span class={styles.label}>{node.label}</span>
|
||||
<Show when={node.meta}>
|
||||
<span class={styles.itemMeta}>{node.meta}</span>
|
||||
</Show>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<ChevronRight
|
||||
classList={{
|
||||
[styles.folderChevron]: true,
|
||||
[styles.folderChevronOpen]: !isCollapsed(),
|
||||
}}
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
<ProjectFolderDraftRow
|
||||
depth={props.pendingFolderRename?.depth ?? props.depth}
|
||||
value={props.pendingFolderRenameName}
|
||||
onInput={props.onPendingFolderRenameChange}
|
||||
onSubmit={props.onSubmitPendingFolderRename}
|
||||
onCancel={props.onCancelPendingFolderRename}
|
||||
/>
|
||||
<Folder class={styles.icon} size={18} strokeWidth={2} />
|
||||
<span class={styles.label}>{node.label}</span>
|
||||
<Show when={node.meta}>
|
||||
<span class={styles.itemMeta}>{node.meta}</span>
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={!isCollapsed() && ((node.children?.length ?? 0) > 0 || props.pendingFolderDraft?.parentId === node.id)}>
|
||||
<ProjectFolderBranch
|
||||
@@ -261,6 +287,11 @@ const ProjectFolderBranch = (props: {
|
||||
onPendingFolderNameChange={props.onPendingFolderNameChange}
|
||||
onSubmitPendingFolder={props.onSubmitPendingFolder}
|
||||
onCancelPendingFolder={props.onCancelPendingFolder}
|
||||
pendingFolderRename={props.pendingFolderRename}
|
||||
pendingFolderRenameName={props.pendingFolderRenameName}
|
||||
onPendingFolderRenameChange={props.onPendingFolderRenameChange}
|
||||
onSubmitPendingFolderRename={props.onSubmitPendingFolderRename}
|
||||
onCancelPendingFolderRename={props.onCancelPendingFolderRename}
|
||||
dragState={props.dragState}
|
||||
isTreeClickSuppressed={props.isTreeClickSuppressed}
|
||||
/>
|
||||
@@ -331,6 +362,8 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
);
|
||||
const [pendingFolderDraft, setPendingFolderDraft] = createSignal<PendingProjectFolderDraft | null>(null);
|
||||
const [pendingFolderName, setPendingFolderName] = createSignal("");
|
||||
const [pendingFolderRename, setPendingFolderRename] = createSignal<PendingProjectFolderRename | null>(null);
|
||||
const [pendingFolderRenameName, setPendingFolderRenameName] = createSignal("");
|
||||
const [dragState, setDragState] = createSignal<ProjectDragState | null>(null);
|
||||
const [suppressNextTreeClick, setSuppressNextTreeClick] = createSignal(false);
|
||||
let rootRef: HTMLDivElement | undefined;
|
||||
@@ -382,6 +415,8 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
setCollapsedFolderIds([]);
|
||||
setPendingFolderDraft(null);
|
||||
setPendingFolderName("");
|
||||
setPendingFolderRename(null);
|
||||
setPendingFolderRenameName("");
|
||||
setDragState(null);
|
||||
};
|
||||
|
||||
@@ -607,10 +642,19 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
setCollapsedFolderIds((current) => current.filter((id) => id !== parentId));
|
||||
}
|
||||
|
||||
setPendingFolderRename(null);
|
||||
setPendingFolderRenameName("");
|
||||
setPendingFolderName("");
|
||||
setPendingFolderDraft({ parentId, depth });
|
||||
};
|
||||
|
||||
const beginFolderRename = (folderId: string, label: string, depth: number): void => {
|
||||
setPendingFolderDraft(null);
|
||||
setPendingFolderName("");
|
||||
setPendingFolderRename({ folderId, depth });
|
||||
setPendingFolderRenameName(label);
|
||||
};
|
||||
|
||||
const submitPendingFolder = async (): Promise<void> => {
|
||||
const name = pendingFolderName().trim();
|
||||
const draft = pendingFolderDraft();
|
||||
@@ -688,11 +732,71 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitPendingFolderRename = async (): Promise<void> => {
|
||||
const draft = pendingFolderRename();
|
||||
const name = pendingFolderRenameName().trim();
|
||||
const projectId = selectedProject().id;
|
||||
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
setPendingFolderRename(null);
|
||||
setPendingFolderRenameName("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUuidString(projectId)) {
|
||||
cancelPendingFolderRename();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${resolveAPIBase()}/projects/${projectId}/folders`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderId: draft.folderId,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
|
||||
const body = (await response.json()) as ProjectFoldersResponse;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(body.message || "Failed to rename project folder.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelPendingFolder = (): void => {
|
||||
setPendingFolderDraft(null);
|
||||
setPendingFolderName("");
|
||||
};
|
||||
|
||||
const cancelPendingFolderRename = (): void => {
|
||||
setPendingFolderRename(null);
|
||||
setPendingFolderRenameName("");
|
||||
};
|
||||
|
||||
const handleContextActionSelect = (action: { id: string; label: string }, target: ProjectMenuTarget): void => {
|
||||
switch (action.id) {
|
||||
case "new-folder":
|
||||
@@ -715,6 +819,11 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
void deletePersistedFolder(target.id);
|
||||
}
|
||||
return;
|
||||
case "rename-folder":
|
||||
if (target.kind === "folder") {
|
||||
beginFolderRename(target.id, target.label, findTreeNodeDepth(projectTreeNodes(), target.id, projectTreeAdapter) ?? 0);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -887,6 +996,11 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
||||
onPendingFolderNameChange={setPendingFolderName}
|
||||
onSubmitPendingFolder={submitPendingFolder}
|
||||
onCancelPendingFolder={cancelPendingFolder}
|
||||
pendingFolderRename={pendingFolderRename()}
|
||||
pendingFolderRenameName={pendingFolderRenameName()}
|
||||
onPendingFolderRenameChange={setPendingFolderRenameName}
|
||||
onSubmitPendingFolderRename={submitPendingFolderRename}
|
||||
onCancelPendingFolderRename={cancelPendingFolderRename}
|
||||
dragState={dragState()}
|
||||
isTreeClickSuppressed={suppressNextTreeClick}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user