import { For, Show, createSignal, type JSX } from "solid-js";
import { Portal } from "solid-js/web";
import { ChevronRight, Plus, X } from "../../../lib/icons";
import { useAppShellData } from "../data/app-shell.context";
import { createLongPressGesture } from "../createLongPressGesture";
import {
createWorkspaceStaticTarget,
createWorkspaceSurfaceTarget,
createWorkspaceTreeTarget,
getWorkspaceItemTypeDefinition,
getWorkspaceNodeIcon,
workspaceStaticItems,
type SidebarItem,
type WorkspaceContextMenuAction,
type WorkspaceContextMenuTarget,
type WorkspaceItemTypeId,
type WorkspaceStaticItem,
type WorkspaceTreeNode,
} from "../data/shell.data";
import { useWorkspaceTreeData } from "../shared/useWorkspaceTreeData";
import { WorkspaceMobileActionSheet } from "../WorkspaceMobileActionSheet/WorkspaceMobileActionSheet";
import styles from "./MobileWorkspaceBrowser.module.scss";
type MobileWorkspaceBrowserProps = {
open: boolean;
onClose: VoidFunction;
};
type MobileWorkspaceDialogState =
| {
kind: "text";
title: string;
message: string;
confirmLabel: string;
initialValue: string;
onConfirm: (value: string) => void;
}
| {
kind: "confirm";
title: string;
message: string;
confirmLabel: string;
tone?: "danger";
onConfirm: () => void;
};
type MobileMoveTargetState = {
kind: "folder" | "item";
id: string;
label: string;
};
type MobileMoveDestination = {
id: string | null;
label: string;
depth: number;
meta?: string;
};
const collectMoveDestinations = (
nodes: readonly WorkspaceTreeNode[],
movingTarget: MobileMoveTargetState,
depth = 0,
ancestorBlocked = false,
): MobileMoveDestination[] => {
const destinations: MobileMoveDestination[] = [];
for (const node of nodes) {
if (node.kind !== "folder") {
continue;
}
const isBlockedFolder = movingTarget.kind === "folder" && node.id === movingTarget.id;
if (!ancestorBlocked && !isBlockedFolder) {
destinations.push({
id: node.id,
label: node.label,
depth,
meta: "Folder",
});
}
destinations.push(
...collectMoveDestinations(node.children ?? [], movingTarget, depth + 1, ancestorBlocked || isBlockedFolder),
);
}
return destinations;
};
const findTreeNodeById = (nodes: readonly WorkspaceTreeNode[], nodeId: string): WorkspaceTreeNode | null => {
for (const node of nodes) {
if (node.id === nodeId) {
return node;
}
if (node.kind !== "folder") {
continue;
}
const nestedMatch = findTreeNodeById(node.children ?? [], nodeId);
if (nestedMatch) {
return nestedMatch;
}
}
return null;
};
const isDangerDialogState = (state: MobileWorkspaceDialogState): boolean => state.kind === "confirm" && state.tone === "danger";
const TreeRow = (props: { node: WorkspaceTreeNode; depth?: number }): JSX.Element => {
const depth = props.depth ?? 0;
const Icon = getWorkspaceNodeIcon(props.node);
const hasChildren = (props.node.children?.length ?? 0) > 0;
return (
);
};
const StaticRow = (props: { item: SidebarItem }): JSX.Element => {
const Icon = props.item.icon;
return (
);
};
const WorkspaceStaticRow = (props: {
item: WorkspaceStaticItem;
onOpenActionSheet: (target: WorkspaceContextMenuTarget) => void;
}): JSX.Element => {
const target = createWorkspaceStaticTarget(props.item);
const longPress = createLongPressGesture({
onLongPress: () => {
props.onOpenActionSheet(target);
},
});
return (
{
event.preventDefault();
props.onOpenActionSheet(target);
}}
{...longPress}
>
);
};
const WorkspaceTreeBranch = (props: {
nodes: readonly WorkspaceTreeNode[];
depth?: number;
onOpenActionSheet: (target: WorkspaceContextMenuTarget) => void;
}): JSX.Element => {
const depth = props.depth ?? 0;
return (
{(node): JSX.Element => {
const target = createWorkspaceTreeTarget(node);
const longPress = createLongPressGesture({
onLongPress: () => {
props.onOpenActionSheet(target);
},
});
return (
{
event.preventDefault();
props.onOpenActionSheet(target);
}}
{...longPress}
>
);
}}
);
};
export const MobileWorkspaceBrowser = (props: MobileWorkspaceBrowserProps): JSX.Element => {
const appShellData = useAppShellData();
const [actionSheetTarget, setActionSheetTarget] = createSignal(null);
const [dialogState, setDialogState] = createSignal(null);
const [dialogValue, setDialogValue] = createSignal("");
const [moveTarget, setMoveTarget] = createSignal(null);
const { workspaceTreeNodes, createFolder, renameFolder, deleteFolder, moveFolder, createItem, deleteItem, moveItem } = useWorkspaceTreeData({
activeProjectId: () => appShellData.activeProject().id,
fallbackWorkspaceTree: () => appShellData.workspaceTree(),
});
const workspaceTarget = () => createWorkspaceSurfaceTarget(appShellData.activeProject());
const moveDestinations = () => {
const target = moveTarget();
if (!target) {
return [] as MobileMoveDestination[];
}
return [
{ id: null, label: "Items root", depth: 0, meta: "Root" },
...collectMoveDestinations(workspaceTreeNodes(), target),
];
};
const resolveCreateItemType = (actionId: string): WorkspaceItemTypeId | null => {
switch (actionId) {
case "create-doc":
return "core.doc";
case "create-board":
return "core.board.kanban";
case "create-list-board":
return "core.board.list";
default:
return null;
}
};
const createPersistedItem = (itemType: WorkspaceItemTypeId, parentId: string | null): void => {
const definition = getWorkspaceItemTypeDefinition(itemType);
void createItem(definition.defaultCreateLabel, itemType, parentId);
};
const openActionSheet = (target: WorkspaceContextMenuTarget): void => {
setActionSheetTarget(target);
};
const closeActionSheet = (): void => {
setActionSheetTarget(null);
};
const closeMoveSheet = (): void => {
setMoveTarget(null);
};
const openWorkspaceActionSheet = (): void => {
openActionSheet(workspaceTarget());
};
const closeDialog = (): void => {
setDialogState(null);
setDialogValue("");
};
const openMoveSheet = (target: MobileMoveTargetState): void => {
setMoveTarget(target);
};
const openTextDialog = (config: Omit, "kind">): void => {
setDialogValue(config.initialValue);
setDialogState({ kind: "text", ...config });
};
const openConfirmDialog = (config: Omit, "kind">): void => {
setDialogValue("");
setDialogState({ kind: "confirm", ...config });
};
const submitDialog = (): void => {
const state = dialogState();
if (!state) {
return;
}
if (state.kind === "text") {
const value = dialogValue().trim();
if (!value) {
return;
}
closeDialog();
state.onConfirm(value);
return;
}
closeDialog();
state.onConfirm();
};
const handleMoveDestinationSelect = (destinationId: string | null): void => {
const target = moveTarget();
if (!target) {
return;
}
const destinationNode = destinationId ? findTreeNodeById(workspaceTreeNodes(), destinationId) : null;
const targetIndex = destinationNode?.kind === "folder"
? destinationNode.children?.length ?? 0
: destinationId
? 0
: workspaceTreeNodes().length;
closeMoveSheet();
if (target.kind === "folder") {
void moveFolder(target.id, destinationId, targetIndex);
return;
}
void moveItem(target.id, destinationId, targetIndex);
};
const handleActionSelect = (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget): void => {
const createItemType = resolveCreateItemType(action.id);
if (createItemType) {
switch (target.kind) {
case "workspace":
case "home":
createPersistedItem(createItemType, null);
return;
case "folder":
createPersistedItem(createItemType, target.id);
return;
case "settings":
case "item":
return;
}
}
switch (action.id) {
case "new-folder": {
if (target.kind === "settings" || target.kind === "item") {
return;
}
openTextDialog({
title: "New folder",
message: target.kind === "folder" ? `Create a folder inside "${target.label}".` : "Create a folder at the root of Items.",
confirmLabel: "Create",
initialValue: "Untitled folder",
onConfirm: (name) => {
void createFolder(name, target.kind === "folder" ? target.id : null);
},
});
return;
}
case "rename-folder": {
if (target.kind !== "folder") {
return;
}
openTextDialog({
title: "Rename folder",
message: `Update the name for "${target.label}".`,
confirmLabel: "Save",
initialValue: target.label,
onConfirm: (name) => {
if (name === target.label) {
return;
}
void renameFolder(target.id, name);
},
});
return;
}
case "move-folder": {
if (target.kind !== "folder") {
return;
}
openMoveSheet({
kind: "folder",
id: target.id,
label: target.label,
});
return;
}
case "delete-folder": {
if (target.kind !== "folder") {
return;
}
openConfirmDialog({
title: "Delete folder?",
message: `"${target.label}" and everything inside it will be removed.`,
confirmLabel: "Delete",
tone: "danger",
onConfirm: () => {
void deleteFolder(target.id);
},
});
return;
}
case "move-doc":
case "move-board":
case "move-list-board": {
if (target.kind !== "item") {
return;
}
openMoveSheet({
kind: "item",
id: target.id,
label: target.label,
});
return;
}
case "delete-doc":
case "delete-board":
case "delete-list-board": {
if (target.kind !== "item") {
return;
}
openConfirmDialog({
title: "Delete item?",
message: `"${target.label}" will be removed from the project tree.`,
confirmLabel: "Delete",
tone: "danger",
onConfirm: () => {
void deleteItem(target.id);
},
});
return;
}
default:
return;
}
};
const workspaceLongPress = createLongPressGesture({
onLongPress: openWorkspaceActionSheet,
});
return (
Workspace
{(item): JSX.Element => }
{(state): JSX.Element => (
Destination
{(destination): JSX.Element => (
)}
)}
{(state): JSX.Element => (
)}
);
};