mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
feat: allow drag and drop in pages (#4358)
Closes https://github.com/webstudio-is/webstudio/issues/403 Now pages and folders can be maintained with drag and drop. https://github.com/user-attachments/assets/108230ea-95fa-40ac-bbcb-5d0785c5554f
This commit is contained in:
1 parent
65a58de7c0
commit
57fc3b9e73
6 files changed
+304
-178
No files matched your search
@@ -20,7 +20,6 @@ import {
|
||||
ScrollArea,
|
||||
rawTheme,
|
||||
Flex,
|
||||
Select,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Text,
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
registerFolderChildMutable,
|
||||
deleteFolderWithChildrenMutable,
|
||||
deletePageMutable,
|
||||
filterSelfAndChildren,
|
||||
} from "./page-utils";
|
||||
import { Form } from "./form";
|
||||
import { updateWebstudioData } from "~/shared/instance-utils";
|
||||
@@ -118,7 +116,6 @@ const FormFields = ({
|
||||
autoSelect,
|
||||
errors,
|
||||
values,
|
||||
folderId,
|
||||
onChange,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
@@ -166,25 +163,6 @@ const FormFields = ({
|
||||
</InputErrorsTooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid gap={1}>
|
||||
<Label htmlFor={fieldIds.parentFolderId}>Parent Folder</Label>
|
||||
<Select
|
||||
tabIndex={1}
|
||||
options={filterSelfAndChildren(folderId, pages.folders)}
|
||||
getValue={(folder) => folder.id}
|
||||
getLabel={(folder) => folder.name}
|
||||
value={pages.folders.find(
|
||||
({ id }) => id === values.parentFolderId
|
||||
)}
|
||||
onChange={(folder) => {
|
||||
onChange({
|
||||
field: "parentFolderId",
|
||||
value: folder.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid gap={1}>
|
||||
<Flex align="center" css={{ gap: theme.spacing[3] }}>
|
||||
<Label htmlFor={fieldIds.slug}>Slug</Label>
|
||||
|
||||
@@ -823,26 +823,6 @@ const FormFields = ({
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{values.isHomePage === false && (
|
||||
<Grid gap={1}>
|
||||
<Label htmlFor={fieldIds.parentFolderId}>Parent Folder</Label>
|
||||
<Select
|
||||
options={pages.folders}
|
||||
getValue={(folder) => folder.id}
|
||||
getLabel={(folder) => folder.name}
|
||||
value={pages.folders.find(
|
||||
({ id }) => id === values.parentFolderId
|
||||
)}
|
||||
onChange={(folder) => {
|
||||
onChange({
|
||||
field: "parentFolderId",
|
||||
value: folder.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{values.isHomePage === false && (
|
||||
<PathField
|
||||
errors={errors.path}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
isSlugAvailable,
|
||||
registerFolderChildMutable,
|
||||
reparentOrphansMutable,
|
||||
filterSelfAndChildren,
|
||||
$pageRootScope,
|
||||
isPathAvailable,
|
||||
reparentPageOrFolderMutable,
|
||||
} from "./page-utils";
|
||||
import {
|
||||
$dataSourceVariables,
|
||||
@@ -284,6 +284,49 @@ describe("registerFolderChildMutable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reparent pages and folders", () => {
|
||||
test("move page up within single parent", () => {
|
||||
const { f, p, register, pages } = createPages();
|
||||
register([
|
||||
f("folder", [
|
||||
p("page1", "/page1"),
|
||||
p("page2", "/page2"),
|
||||
p("page3", "/page3"),
|
||||
]),
|
||||
]);
|
||||
reparentPageOrFolderMutable(pages.folders, "page3", "folder", 1);
|
||||
const folder = pages.folders.find((folder) => folder.id === "folder");
|
||||
expect(folder?.children).toEqual(["page1", "page3", "page2"]);
|
||||
});
|
||||
|
||||
test("move page down within single parent", () => {
|
||||
const { f, p, register, pages } = createPages();
|
||||
register([
|
||||
f("folder", [
|
||||
p("page1", "/page1"),
|
||||
p("page2", "/page2"),
|
||||
p("page3", "/page3"),
|
||||
]),
|
||||
]);
|
||||
reparentPageOrFolderMutable(pages.folders, "page1", "folder", 2);
|
||||
const folder = pages.folders.find((folder) => folder.id === "folder");
|
||||
expect(folder?.children).toEqual(["page2", "page1", "page3"]);
|
||||
});
|
||||
|
||||
test("move page into another folder", () => {
|
||||
const { f, p, register, pages } = createPages();
|
||||
register([
|
||||
f("folder1", [p("page1", "/page1"), p("page2", "/page2")]),
|
||||
f("folder2", [p("page3", "/page3")]),
|
||||
]);
|
||||
reparentPageOrFolderMutable(pages.folders, "page1", "folder2", 1);
|
||||
const folder1 = pages.folders.find((folder) => folder.id === "folder1");
|
||||
const folder2 = pages.folders.find((folder) => folder.id === "folder2");
|
||||
expect(folder1?.children).toEqual(["page2"]);
|
||||
expect(folder2?.children).toEqual(["page3", "page1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllChildrenAndSelf", () => {
|
||||
const folders: Array<Folder> = [
|
||||
{
|
||||
@@ -353,34 +396,6 @@ describe("deleteFolderWithChildrenMutable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterSelfAndChildren", () => {
|
||||
const folders = [
|
||||
{
|
||||
id: "1",
|
||||
name: "1",
|
||||
slug: "1",
|
||||
children: ["2"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "2",
|
||||
slug: "2",
|
||||
children: ["page1"],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "3",
|
||||
slug: "3",
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
test("filter self and child folders", () => {
|
||||
const result = filterSelfAndChildren("1", folders);
|
||||
expect(result).toEqual([folders[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
test("page root scope should rely on editing page", () => {
|
||||
const pages = createDefaultPages({
|
||||
rootInstanceId: "homeRootId",
|
||||
|
||||
@@ -160,6 +160,27 @@ export const registerFolderChildMutable = (
|
||||
parentFolder?.children.push(id);
|
||||
};
|
||||
|
||||
export const reparentPageOrFolderMutable = (
|
||||
folders: Folder[],
|
||||
pageOrFolderId: string,
|
||||
newFolderId: string,
|
||||
newPosition: number
|
||||
) => {
|
||||
const prevParent = findParentFolderByChildId(pageOrFolderId, folders);
|
||||
const nextParent = folders.find((folder) => folder.id === newFolderId);
|
||||
if (prevParent === undefined || nextParent === undefined) {
|
||||
return;
|
||||
}
|
||||
// if parent is the same, we need to adjust the position
|
||||
// to account for the removal of the instance.
|
||||
const prevPosition = prevParent.children.indexOf(pageOrFolderId);
|
||||
if (prevParent.id === nextParent.id && prevPosition < newPosition) {
|
||||
newPosition -= 1;
|
||||
}
|
||||
prevParent.children.splice(prevPosition, 1);
|
||||
nextParent.children.splice(newPosition, 0, pageOrFolderId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all child folder ids of the current folder including itself.
|
||||
*/
|
||||
@@ -223,19 +244,6 @@ export const deleteFolderWithChildrenMutable = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter out folders that are children of the current folder or the current folder itself.
|
||||
*/
|
||||
export const filterSelfAndChildren = (
|
||||
folderId: Folder["id"],
|
||||
folders: Array<Folder>
|
||||
) => {
|
||||
const folderIds = getAllChildrenAndSelf(folderId, folders, "folder");
|
||||
return folders.filter((folder) => {
|
||||
return folderIds.includes(folder.id) === false;
|
||||
});
|
||||
};
|
||||
|
||||
const $editingPage = computed(
|
||||
[$editingPageId, $pages],
|
||||
(editingPageId, pages) => {
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
TreeNodeLabel,
|
||||
PanelTitle,
|
||||
Separator,
|
||||
TreeSortableItem,
|
||||
type TreeDropTarget,
|
||||
toast,
|
||||
} from "@webstudio-is/design-system";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
@@ -26,7 +29,11 @@ import { ExtendedPanel } from "../../shared/extended-sidebar-panel";
|
||||
import { NewPageSettings, PageSettings } from "./page-settings";
|
||||
import { $editingPageId, $pages, $selectedPageId } from "~/shared/nano-states";
|
||||
import { switchPage } from "~/shared/pages";
|
||||
import { getAllChildrenAndSelf, reparentOrphansMutable } from "./page-utils";
|
||||
import {
|
||||
getAllChildrenAndSelf,
|
||||
reparentOrphansMutable,
|
||||
reparentPageOrFolderMutable,
|
||||
} from "./page-utils";
|
||||
import {
|
||||
FolderSettings,
|
||||
NewFolderSettings,
|
||||
@@ -34,9 +41,15 @@ import {
|
||||
} from "./folder-settings";
|
||||
import { serverSyncStore } from "~/shared/sync";
|
||||
import { useMount } from "~/shared/hook-utils/use-mount";
|
||||
import { ROOT_FOLDER_ID, type Folder, type Page } from "@webstudio-is/sdk";
|
||||
import {
|
||||
isRootFolder,
|
||||
ROOT_FOLDER_ID,
|
||||
type Folder,
|
||||
type Page,
|
||||
} from "@webstudio-is/sdk";
|
||||
import { atom, computed } from "nanostores";
|
||||
import { isPathnamePattern } from "~/builder/shared/url-pattern";
|
||||
import { updateWebstudioData } from "~/shared/instance-utils";
|
||||
|
||||
const ItemSuffix = ({
|
||||
isParentSelected,
|
||||
@@ -119,22 +132,67 @@ const $expandedItems = atom(new Set<string>());
|
||||
type PagesTreeItem =
|
||||
| {
|
||||
id: string;
|
||||
selector: string[];
|
||||
level: number;
|
||||
isExpanded?: boolean;
|
||||
type: "page";
|
||||
page: Page;
|
||||
isLastChild: boolean;
|
||||
dropTarget?: TreeDropTarget;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
selector: string[];
|
||||
level: number;
|
||||
isExpanded?: boolean;
|
||||
type: "folder";
|
||||
folder: Folder;
|
||||
isLastChild: boolean;
|
||||
dropTarget?: TreeDropTarget;
|
||||
};
|
||||
|
||||
type DropTarget = {
|
||||
parentId: string;
|
||||
beforeId?: string;
|
||||
afterId?: string;
|
||||
indexWithinChildren: number;
|
||||
};
|
||||
|
||||
const $dropTarget = atom<undefined | DropTarget>();
|
||||
|
||||
const getStoredDropTarget = (
|
||||
selector: string[],
|
||||
dropTarget: TreeDropTarget
|
||||
): undefined | DropTarget => {
|
||||
const parentId = selector.at(-dropTarget.parentLevel - 1);
|
||||
const beforeId =
|
||||
dropTarget.beforeLevel === undefined
|
||||
? undefined
|
||||
: selector.at(-dropTarget.beforeLevel - 1);
|
||||
const afterId =
|
||||
dropTarget.afterLevel === undefined
|
||||
? undefined
|
||||
: selector.at(-dropTarget.afterLevel - 1);
|
||||
const pages = $pages.get();
|
||||
const parentFolder = pages?.folders.find((item) => item.id === parentId);
|
||||
let indexWithinChildren = 0;
|
||||
if (parentFolder) {
|
||||
const beforeIndex = parentFolder.children.indexOf(beforeId ?? "");
|
||||
const afterIndex = parentFolder.children.indexOf(afterId ?? "");
|
||||
if (beforeIndex > -1) {
|
||||
indexWithinChildren = beforeIndex;
|
||||
} else if (afterIndex > -1) {
|
||||
indexWithinChildren = afterIndex + 1;
|
||||
}
|
||||
}
|
||||
if (parentId) {
|
||||
return { parentId, beforeId, afterId, indexWithinChildren };
|
||||
}
|
||||
};
|
||||
|
||||
const $flatPagesTree = computed(
|
||||
[$pages, $expandedItems],
|
||||
(pagesData, expandedItems) => {
|
||||
[$pages, $expandedItems, $dropTarget],
|
||||
(pagesData, expandedItems, dropTarget) => {
|
||||
const flatPagesTree: PagesTreeItem[] = [];
|
||||
if (pagesData === undefined) {
|
||||
return flatPagesTree;
|
||||
@@ -144,9 +202,24 @@ const $flatPagesTree = computed(
|
||||
);
|
||||
const pages = new Map(pagesData.pages.map((page) => [page.id, page]));
|
||||
pages.set(pagesData.homePage.id, pagesData.homePage);
|
||||
const traverse = (itemId: string, level = 0) => {
|
||||
const traverse = (selector: string[], level = 0, isLastChild = false) => {
|
||||
const [itemId] = selector;
|
||||
let treeItem: undefined | PagesTreeItem;
|
||||
let lastTreeItem: undefined | PagesTreeItem;
|
||||
const folder = folders.get(itemId);
|
||||
const page = pages.get(itemId);
|
||||
if (page) {
|
||||
treeItem = {
|
||||
id: itemId,
|
||||
selector,
|
||||
level,
|
||||
type: "page",
|
||||
page,
|
||||
isLastChild,
|
||||
};
|
||||
lastTreeItem = treeItem;
|
||||
flatPagesTree.push(treeItem);
|
||||
}
|
||||
if (folder) {
|
||||
let isExpanded: undefined | boolean;
|
||||
if (level > 0 && folder.children.length > 0) {
|
||||
@@ -154,41 +227,72 @@ const $flatPagesTree = computed(
|
||||
}
|
||||
// hide root folder
|
||||
if (itemId !== ROOT_FOLDER_ID) {
|
||||
flatPagesTree.push({
|
||||
treeItem = {
|
||||
id: itemId,
|
||||
selector,
|
||||
level,
|
||||
isExpanded,
|
||||
type: "folder",
|
||||
folder,
|
||||
});
|
||||
isLastChild,
|
||||
};
|
||||
lastTreeItem = treeItem;
|
||||
flatPagesTree.push(treeItem);
|
||||
}
|
||||
if (level === 0 || isExpanded) {
|
||||
for (const childId of folder.children) {
|
||||
traverse(childId, level + 1);
|
||||
for (let index = 0; index < folder.children.length; index += 1) {
|
||||
const childId = folder.children[index];
|
||||
const isLastChild = index === folder.children.length - 1;
|
||||
lastTreeItem = traverse(
|
||||
[childId, ...selector],
|
||||
level + 1,
|
||||
isLastChild
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (page) {
|
||||
flatPagesTree.push({ id: itemId, level, type: "page", page });
|
||||
|
||||
if (treeItem && dropTarget?.beforeId === itemId) {
|
||||
treeItem.dropTarget = {
|
||||
parentLevel: level - 1,
|
||||
beforeLevel: level,
|
||||
};
|
||||
}
|
||||
if (lastTreeItem && dropTarget?.afterId === itemId) {
|
||||
lastTreeItem.dropTarget = {
|
||||
parentLevel: level - 1,
|
||||
afterLevel: level,
|
||||
};
|
||||
}
|
||||
return lastTreeItem;
|
||||
};
|
||||
traverse(ROOT_FOLDER_ID);
|
||||
traverse([ROOT_FOLDER_ID]);
|
||||
return flatPagesTree;
|
||||
}
|
||||
);
|
||||
|
||||
const canDrop = (dropTarget: DropTarget, folders: Folder[]) => {
|
||||
// allow dropping only inside folders
|
||||
if (isFolder(dropTarget.parentId, folders) === false) {
|
||||
return false;
|
||||
}
|
||||
// forbid dropping in the beginning of root folder
|
||||
// which is always used by home page
|
||||
if (
|
||||
isRootFolder({ id: dropTarget.parentId }) &&
|
||||
dropTarget.indexWithinChildren === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const PagesTree = ({
|
||||
onClose,
|
||||
onCreateNewFolder,
|
||||
onCreateNewPage,
|
||||
onSelect,
|
||||
selectedPageId,
|
||||
onEdit,
|
||||
editingItemId,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onCreateNewFolder: () => void;
|
||||
onCreateNewPage: () => void;
|
||||
onSelect: (pageId: string) => void;
|
||||
selectedPageId: string;
|
||||
onEdit: (pageId: string | undefined) => void;
|
||||
@@ -196,6 +300,7 @@ const PagesTree = ({
|
||||
}) => {
|
||||
const pages = useStore($pages);
|
||||
const flatPagesTree = useStore($flatPagesTree);
|
||||
const dropTarget = useStore($dropTarget);
|
||||
useReparentOrphans();
|
||||
|
||||
if (pages === undefined) {
|
||||
@@ -203,65 +308,77 @@ const PagesTree = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelTitle
|
||||
suffix={
|
||||
<>
|
||||
<Tooltip content="New folder" side="bottom">
|
||||
<Button
|
||||
onClick={() => onCreateNewFolder()}
|
||||
aria-label="New folder"
|
||||
prefix={<NewFolderIcon />}
|
||||
color="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="New page" side="bottom">
|
||||
<Button
|
||||
onClick={() => onCreateNewPage()}
|
||||
aria-label="New page"
|
||||
prefix={<NewPageIcon />}
|
||||
color="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Close panel" side="bottom">
|
||||
<Button
|
||||
color="ghost"
|
||||
prefix={<CrossIcon />}
|
||||
aria-label="Close panel"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
Pages
|
||||
</PanelTitle>
|
||||
<Separator />
|
||||
|
||||
<Box css={{ overflowY: "auto", flexBasis: 0, flexGrow: 1 }}>
|
||||
<TreeRoot>
|
||||
{flatPagesTree.map((item, index) => {
|
||||
const handleExpand = (isExpanded: boolean, all: boolean) => {
|
||||
const expandedItems = new Set($expandedItems.get());
|
||||
const items = all
|
||||
? getAllChildrenAndSelf(item.id, pages.folders, "folder")
|
||||
: [item.id];
|
||||
for (const itemId of items) {
|
||||
if (isExpanded) {
|
||||
expandedItems.add(itemId);
|
||||
} else {
|
||||
expandedItems.delete(itemId);
|
||||
}
|
||||
<Box css={{ overflowY: "auto", flexBasis: 0, flexGrow: 1 }}>
|
||||
<TreeRoot>
|
||||
{flatPagesTree.map((item, index) => {
|
||||
const handleExpand = (isExpanded: boolean, all: boolean) => {
|
||||
const expandedItems = new Set($expandedItems.get());
|
||||
const items = all
|
||||
? getAllChildrenAndSelf(item.id, pages.folders, "folder")
|
||||
: [item.id];
|
||||
for (const itemId of items) {
|
||||
if (isExpanded) {
|
||||
expandedItems.add(itemId);
|
||||
} else {
|
||||
expandedItems.delete(itemId);
|
||||
}
|
||||
$expandedItems.set(expandedItems);
|
||||
};
|
||||
}
|
||||
$expandedItems.set(expandedItems);
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<TreeSortableItem
|
||||
key={item.id}
|
||||
level={item.level}
|
||||
isExpanded={item.isExpanded}
|
||||
isLastChild={item.isLastChild}
|
||||
data={item}
|
||||
canDrag={() => {
|
||||
// forbid dragging home page
|
||||
if (item.id === pages.homePage.id) {
|
||||
toast.error("Home page cannot be moved");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
onExpand={(isExpanded) => handleExpand(isExpanded, false)}
|
||||
dropTarget={item.dropTarget}
|
||||
onDropTargetChange={(dropTarget) => {
|
||||
if (dropTarget) {
|
||||
const storedDropTarget = getStoredDropTarget(
|
||||
item.selector,
|
||||
dropTarget
|
||||
);
|
||||
if (
|
||||
storedDropTarget &&
|
||||
canDrop(storedDropTarget, pages.folders)
|
||||
) {
|
||||
$dropTarget.set(storedDropTarget);
|
||||
}
|
||||
} else {
|
||||
$dropTarget.set(undefined);
|
||||
}
|
||||
}}
|
||||
onDrop={(item) => {
|
||||
if (dropTarget === undefined) {
|
||||
return;
|
||||
}
|
||||
updateWebstudioData((data) => {
|
||||
reparentPageOrFolderMutable(
|
||||
data.pages.folders,
|
||||
item.id,
|
||||
dropTarget.parentId,
|
||||
dropTarget.indexWithinChildren
|
||||
);
|
||||
});
|
||||
$dropTarget.set(undefined);
|
||||
}}
|
||||
>
|
||||
<TreeNode
|
||||
key={item.id}
|
||||
level={item.level}
|
||||
tabbable={index === 0}
|
||||
isSelected={item.id === selectedPageId}
|
||||
isHighlighted={dropTarget?.parentId === item.id}
|
||||
isExpanded={item.isExpanded}
|
||||
onExpand={handleExpand}
|
||||
buttonProps={{
|
||||
@@ -305,11 +422,11 @@ const PagesTree = ({
|
||||
</TreeNodeLabel>
|
||||
)}
|
||||
</TreeNode>
|
||||
);
|
||||
})}
|
||||
</TreeRoot>
|
||||
</Box>
|
||||
</>
|
||||
</TreeSortableItem>
|
||||
);
|
||||
})}
|
||||
</TreeRoot>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -401,28 +518,56 @@ export const PagesPanel = ({ onClose }: { onClose: () => void }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PagesTree
|
||||
onClose={onClose}
|
||||
onCreateNewFolder={() => {
|
||||
$editingPageId.set(
|
||||
editingItemId === newFolderId ? undefined : newFolderId
|
||||
);
|
||||
}}
|
||||
onCreateNewPage={() =>
|
||||
$editingPageId.set(
|
||||
editingItemId === newPageId ? undefined : newPageId
|
||||
)
|
||||
<PanelTitle
|
||||
suffix={
|
||||
<>
|
||||
<Tooltip content="New folder" side="bottom">
|
||||
<Button
|
||||
onClick={() => {
|
||||
$editingPageId.set(
|
||||
editingItemId === newFolderId ? undefined : newFolderId
|
||||
);
|
||||
}}
|
||||
aria-label="New folder"
|
||||
prefix={<NewFolderIcon />}
|
||||
color="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="New page" side="bottom">
|
||||
<Button
|
||||
onClick={() => {
|
||||
$editingPageId.set(
|
||||
editingItemId === newPageId ? undefined : newPageId
|
||||
);
|
||||
}}
|
||||
aria-label="New page"
|
||||
prefix={<NewPageIcon />}
|
||||
color="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Close panel" side="bottom">
|
||||
<Button
|
||||
color="ghost"
|
||||
prefix={<CrossIcon />}
|
||||
aria-label="Close panel"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
Pages
|
||||
</PanelTitle>
|
||||
<Separator />
|
||||
|
||||
<PagesTree
|
||||
selectedPageId={currentPageId}
|
||||
onSelect={(itemId) => {
|
||||
if (isFolder(itemId, pages.folders)) {
|
||||
return;
|
||||
}
|
||||
switchPage(itemId);
|
||||
onClose();
|
||||
}}
|
||||
selectedPageId={currentPageId}
|
||||
onEdit={$editingPageId.set}
|
||||
editingItemId={editingItemId}
|
||||
onEdit={$editingPageId.set}
|
||||
/>
|
||||
|
||||
<ExtendedPanel isOpen={editingItemId !== undefined}>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"css-to-ws": "NODE_OPTIONS=--conditions=webstudio css-to-ws",
|
||||
"build:webflow-presets": "pnpm css-to-ws ./app/shared/copy-paste/plugin-webflow/style-presets.css ./app/shared/copy-paste/plugin-webflow/__generated__/style-presets.ts",
|
||||
"start": "remix-serve build/server/index.js",
|
||||
"dev": "remix vite:dev",
|
||||
"dev:auth": "DEBUG=OAuth2Strategy,ws:* remix vite:dev",
|
||||
"dev": "pnpm --filter=@webstudio-is/http-client build && remix vite:dev",
|
||||
"dev:auth": "pnpm --filter=@webstudio-is/http-client build && DEBUG=OAuth2Strategy,ws:* remix vite:dev",
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user