From febb02e2df28390bd53a04fb82cb604278f94fa3 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 16 Jan 2024 14:12:01 +0100 Subject: [PATCH] feat: Folders select in pages (#2763) Ref https://github.com/webstudio-is/webstudio/issues/404 ## Description - Folders select in pages - Path rendered in page contains all slugs from all parent folders ## Steps for reproduction 1. open page settings 2. select a folder 3. see page was moved to a folder 4. see url preview contains the right folder slugs ## Code Review - [ ] hi @TrySound , I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env.example` and the `builder/env-check.js` if mandatory --- .../panels/pages/folder-settings.tsx | 126 +++++++------- .../panels/pages/page-settings.tsx | 154 +++++++++++++----- .../panels/pages/page-utils.test.ts | 114 ++++++++++++- .../sidebar-left/panels/pages/page-utils.ts | 51 +++++- packages/sdk/src/schema/pages.ts | 6 +- 5 files changed, 332 insertions(+), 119 deletions(-) diff --git a/apps/builder/app/builder/features/sidebar-left/panels/pages/folder-settings.tsx b/apps/builder/app/builder/features/sidebar-left/panels/pages/folder-settings.tsx index bce997df94..7174f76efe 100644 --- a/apps/builder/app/builder/features/sidebar-left/panels/pages/folder-settings.tsx +++ b/apps/builder/app/builder/features/sidebar-left/panels/pages/folder-settings.tsx @@ -4,7 +4,7 @@ import { useStore } from "@nanostores/react"; import { useDebouncedCallback } from "use-debounce"; import { useUnmount } from "react-use"; import slugify from "slugify"; -import { Folder, FolderName, FolderSlug, Pages } from "@webstudio-is/sdk"; +import { Folder, Pages } from "@webstudio-is/sdk"; import { theme, Button, @@ -35,38 +35,36 @@ import { findParentFolderByChildId, cleanupChildRefsMutable, isSlugUsed, + registerFolderChildMutable, } from "./page-utils"; -import { createRootFolder } from "@webstudio-is/project-build"; +import { ROOT_FOLDER_ID, createRootFolder } from "@webstudio-is/project-build"; -const fieldDefaultValues = { - name: "Untitled", - slug: "untitled", - parentFolderId: createRootFolder().id, -}; +const Values = Folder.pick({ name: true, slug: true }).extend({ + parentFolderId: z.string(), +}); -const fieldNames = Object.keys( - fieldDefaultValues -) as (keyof typeof fieldDefaultValues)[]; +type Values = z.infer; -type FieldName = (typeof fieldNames)[number]; - -type Values = typeof fieldDefaultValues; +type FieldName = keyof Values; type Errors = { [fieldName in FieldName]?: string[]; }; -const FolderValues = z.object({ - name: FolderName, - slug: FolderSlug, -}); +const fieldDefaultValues = { + name: "Untitled", + slug: "untitled", + parentFolderId: createRootFolder().id, +} satisfies Values; + +const fieldNames = Object.keys(fieldDefaultValues) as Array; const validateValues = ( pages: undefined | Pages, values: Values, folderId?: Folder["id"] ): Errors => { - const parsedResult = FolderValues.safeParse(values); + const parsedResult = Values.safeParse(values); const errors: Errors = {}; if (parsedResult.success === false) { return parsedResult.error.formErrors.fieldErrors; @@ -96,7 +94,7 @@ const toFormValues = ( return { name: folder?.name ?? "", slug: folder?.slug ?? "", - parentFolderId: parentFolder?.id ?? "root", + parentFolderId: parentFolder?.id ?? ROOT_FOLDER_ID, }; }; @@ -163,28 +161,26 @@ const FormFields = ({ - - - folderId !== id + )} + getValue={(folder) => folder.id} + getLabel={(folder) => folder.name} + value={pages.folders.find( + ({ id }) => id === values.parentFolderId + )} + onChange={(folder) => { + onChange({ + field: "parentFolderId", + value: folder.id, + }); + }} + /> @@ -254,23 +250,7 @@ export const NewFolderSettings = ({ const handleSubmit = () => { if (Object.keys(errors).length === 0) { const folderId = nanoid(); - // @todo move to a function - serverSyncStore.createTransaction([$pages], (pages) => { - if (pages === undefined) { - return; - } - pages.folders.push({ - id: folderId, - name: values.name, - slug: values.slug, - children: [], - } satisfies Folder); - const parentFolder = pages.folders.find( - ({ id }) => id === values.parentFolderId - ); - parentFolder?.children.push(folderId); - }); - + createFolder(folderId, values); onSuccess(folderId); } }; @@ -358,6 +338,24 @@ const NewFolderSettingsView = ({ ); }; +const createFolder = (folderId: Folder["id"], values: Values) => { + serverSyncStore.createTransaction([$pages], (pages) => { + if (pages === undefined) { + return; + } + pages.folders.push({ + id: folderId, + name: values.name, + slug: values.slug, + children: [], + } satisfies Folder); + const parentFolder = pages.folders.find( + ({ id }) => id === values.parentFolderId + ); + parentFolder?.children.push(folderId); + }); +}; + const updateFolder = (folderId: Folder["id"], values: Partial) => { serverSyncStore.createTransaction([$pages], (pages) => { if (pages === undefined) { @@ -374,13 +372,11 @@ const updateFolder = (folderId: Folder["id"], values: Partial) => { folder.slug = values.slug; } if (values.parentFolderId !== undefined) { - const newParentFolder = pages.folders.find( - ({ id }) => id === values.parentFolderId + registerFolderChildMutable( + pages.folders, + folderId, + values.parentFolderId ); - if (newParentFolder) { - cleanupChildRefsMutable(folderId, pages); - newParentFolder?.children.push(folderId); - } } }); }; @@ -391,7 +387,7 @@ const deleteFolder = (folderId: Folder["id"]) => { if (pages === undefined) { return; } - cleanupChildRefsMutable(folderId, pages); + cleanupChildRefsMutable(folderId, pages.folders); removeByMutable(pages.folders, (folder) => folder.id === folderId); }); }; diff --git a/apps/builder/app/builder/features/sidebar-left/panels/pages/page-settings.tsx b/apps/builder/app/builder/features/sidebar-left/panels/pages/page-settings.tsx index d73a01693e..163024f6ca 100644 --- a/apps/builder/app/builder/features/sidebar-left/panels/pages/page-settings.tsx +++ b/apps/builder/app/builder/features/sidebar-left/panels/pages/page-settings.tsx @@ -13,8 +13,13 @@ import { PageTitle, PagePath, DataSource, + Folder, } from "@webstudio-is/sdk"; -import { findPageByIdOrPath } from "@webstudio-is/project-build"; +import { + ROOT_FOLDER_ID, + createRootFolder, + findPageByIdOrPath, +} from "@webstudio-is/project-build"; import { theme, Button, @@ -31,6 +36,7 @@ import { ScrollArea, rawTheme, Flex, + Select, } from "@webstudio-is/design-system"; import { ChevronDoubleLeftIcon, @@ -74,10 +80,16 @@ import { parsePathnamePattern, validatePathnamePattern, } from "./url-pattern"; -import { isRoot } from "./page-utils"; +import { + cleanupChildRefsMutable, + compileSlugPath, + findParentFolderByChildId, + registerFolderChildMutable, +} from "./page-utils"; const fieldDefaultValues = { name: "Untitled", + parentFolderId: createRootFolder().id, path: "/untitled", title: "Untitled", description: "", @@ -188,9 +200,15 @@ const validateValues = ( return errors; }; -const toFormPage = (page: Page, isHomePage: boolean): Values => { +const toFormValues = ( + page: Page, + pages: Pages, + isHomePage: boolean +): Values => { + const parentFolder = findParentFolderByChildId(page.id, pages.folders); return { name: page.name, + parentFolderId: parentFolder?.id ?? ROOT_FOLDER_ID, path: page.path, title: page.title, description: page.meta.description ?? fieldDefaultValues.description, @@ -281,8 +299,14 @@ const FormFields = ({ const fieldIds = useIds(fieldNames); const assets = useStore($assets); const pages = useStore($pages); + const dataSourceVariables = useStore($dataSourceVariables); + + if (pages === undefined) { + return; + } + const socialImageAsset = assets.get(values.socialImageAssetId); - const faviconAsset = assets.get(pages?.meta?.faviconAssetId ?? ""); + const faviconAsset = assets.get(pages.meta?.faviconAssetId ?? ""); const faviconUrl = faviconAsset?.type === "image" ? faviconAsset.name : ""; @@ -296,14 +320,15 @@ const FormFields = ({ const publishedUrl = new URL(`https://${domain}`); const pathParamNames = parsePathnamePattern(values.path); - const dataSourceVariables = useStore($dataSourceVariables); const params = pathVariableId !== undefined ? (dataSourceVariables.get(pathVariableId) as Record) : undefined; const compiledPath = compilePathnamePattern(values.path ?? "", params ?? {}); - const pageDomainAndPath = [publishedUrl.host, compiledPath] + const slug = compileSlugPath(pages.folders, values.parentFolderId); + + const pageDomainAndPath = [publishedUrl.host, slug, compiledPath] .filter(Boolean) .join("/") .replace(/\/+/g, "/"); @@ -378,6 +403,27 @@ const FormFields = ({ )} + {isFeatureEnabled("folders") && values.isHomePage === false && ( + + +