diff --git a/apps/builder/app/builder/features/address-bar.stories.tsx b/apps/builder/app/builder/features/address-bar.stories.tsx new file mode 100644 index 0000000000..a023c6a452 --- /dev/null +++ b/apps/builder/app/builder/features/address-bar.stories.tsx @@ -0,0 +1,98 @@ +import { computed } from "nanostores"; +import type { Meta, StoryFn } from "@storybook/react"; +import { Box, Text, theme } from "@webstudio-is/design-system"; +import { AddressBarPopover } from "./address-bar"; +import { + $dataSourceVariables, + $dataSources, + $pages, + $selectedPage, + $selectedPageId, +} from "~/shared/nano-states"; +import { useStore } from "@nanostores/react"; + +$dataSources.set( + new Map([ + [ + "systemId", + { + id: "systemId", + scopeInstanceId: "rootInstanceId", + name: "system", + type: "parameter", + }, + ], + ]) +); + +$pages.set({ + folders: [ + { + id: "rootId", + name: "", + slug: "", + children: ["homeId", "dynamicId"], + }, + ], + homePage: { + id: "homeId", + path: "", + name: "", + title: "", + meta: {}, + rootInstanceId: "", + systemDataSourceId: "", + }, + pages: [ + { + id: "dynamicId", + path: "/blog/:date/post/:slug", + name: "", + title: "", + meta: {}, + rootInstanceId: "rootInstanceId", + systemDataSourceId: "systemId", + }, + ], +}); + +$selectedPageId.set("dynamicId"); + +const $selectedPageSystem = computed( + [$selectedPage, $dataSourceVariables], + (selectedPage, dataSourceVariables) => { + if (selectedPage === undefined) { + return {}; + } + return dataSourceVariables.get(selectedPage.systemDataSourceId); + } +); + +const SystemInspect = () => { + const system = useStore($selectedPageSystem); + return ( + + {JSON.stringify(system, null, 2)} + + ); +}; + +export default { + title: "Builder/Address Bar", + component: AddressBarPopover, +} satisfies Meta; + +export const AddressBar: StoryFn = () => ( + <> + + + + + +); diff --git a/apps/builder/app/builder/features/address-bar.tsx b/apps/builder/app/builder/features/address-bar.tsx index ab38b85bed..dbf40ea4bb 100644 --- a/apps/builder/app/builder/features/address-bar.tsx +++ b/apps/builder/app/builder/features/address-bar.tsx @@ -1,6 +1,12 @@ import { computed } from "nanostores"; import { useStore } from "@nanostores/react"; -import { useEffect, useId, useState, type ComponentProps } from "react"; +import { + forwardRef, + useEffect, + useRef, + useState, + type ComponentProps, +} from "react"; import { Flex, InputField, @@ -61,24 +67,6 @@ const $selectedPagePathParams = computed( } ); -const updatePathParam = (name: string, value: string) => { - const pathParams = $selectedPagePathParams.get(); - const path = $selectedPagePath.get(); - const tokens = tokenizePathnamePattern(path); - // delete stale fields - const newParams: Record = {}; - for (const token of tokens) { - if (token.type === "param") { - newParams[token.name] = pathParams?.[token.name] ?? ""; - } - } - newParams[name] = value; - const page = $selectedPage.get(); - if (page) { - updateSystem(page, { params: newParams }); - } -}; - const useCopyUrl = (pageUrl: string) => { const [copyState, setCopyState] = useState<"copy" | "copied">("copy"); // reset copied state after 2 seconds @@ -113,13 +101,14 @@ const useCopyUrl = (pageUrl: string) => { }; }; -const AddressBar = () => { - const id = useId(); +const AddressBar = forwardRef((_props, ref) => { const publishedOrigin = useStore($publishedOrigin); const path = useStore($selectedPagePath); - const pathParams = useStore($selectedPagePathParams); + const [pathParams, setPathParams] = useState( + () => $selectedPagePathParams.get() ?? {} + ); const tokens = tokenizePathnamePattern(path); - const compiledPath = compilePathnamePattern(tokens, pathParams ?? {}); + const compiledPath = compilePathnamePattern(tokens, pathParams); const { tooltipProps, buttonProps } = useCopyUrl( `${publishedOrigin}${compiledPath}` ); @@ -127,7 +116,7 @@ const AddressBar = () => { const errors = new Map(); for (const token of tokens) { if (token.type === "param") { - const value = (pathParams?.[token.name] ?? "").trim(); + const value = (pathParams[token.name] ?? "").trim(); if (value === "" && token.optional === false) { errors.set(token.name, `"${token.name}" is required`); } @@ -141,46 +130,77 @@ const AddressBar = () => { } return ( - - - - {tokens.map((token, index) => { - if (token.type === "fragment") { - return token.value; - } - if (token.type === "param") { - return ( - - updatePathParam(token.name, event.target.value) - } - /> - ); - } - token satisfies never; - })} - +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const path = $selectedPagePath.get(); + const tokens = tokenizePathnamePattern(path); + // delete stale fields + const newParams: Record = {}; + for (const token of tokens) { + if (token.type === "param") { + newParams[token.name] = String(formData.get(token.name) ?? ""); + } + } + const page = $selectedPage.get(); + if (page) { + updateSystem(page, { params: newParams }); + } + }} + > + {/* submit is not triggered when press enter on input without submit button */} + + + + + {tokens.map((token, index) => { + if (token.type === "fragment") { + return token.value; + } + if (token.type === "param") { + return ( + + setPathParams((prevPathParams) => ({ + ...prevPathParams, + [token.name]: event.target.value, + })) + } + /> + ); + } + token satisfies never; + })} + - - 0} /> - - - + + 0} + type="button" + /> + + + + ); -}; +}); export const AddressBarPopover = () => { const [isOpen, setIsOpen] = useState(false); const path = useStore($selectedPagePath); const publishedOrigin = useStore($publishedOrigin); const { tooltipProps, buttonProps } = useCopyUrl(`${publishedOrigin}${path}`); + const formRef = useRef(null); // show only copy button when path is static if (isPathnamePattern(path) === false) { @@ -196,7 +216,15 @@ export const AddressBarPopover = () => { } return ( - + { + setIsOpen(newIsOpen); + if (newIsOpen === false) { + formRef.current?.requestSubmit(); + } + }} + > @@ -209,7 +237,7 @@ export const AddressBarPopover = () => { collisionPadding={4} align="start" > - +