From dc0236fafeeffb67388693cc27a26be0f20dddb3 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 17 May 2024 16:37:48 +0300 Subject: [PATCH] experimental: save address bar on enter or close (#3373) To keep in history only consistent paths and avoid overfetching resources I changed address bar saving logic. Before each type in address bar params triggered save. Now only "enter" while param is focused or address bar popover is closed triggers saving. Can be tested with new story. --- .../builder/features/address-bar.stories.tsx | 98 ++++++++++++ .../app/builder/features/address-bar.tsx | 144 +++++++++++------- 2 files changed, 184 insertions(+), 58 deletions(-) create mode 100644 apps/builder/app/builder/features/address-bar.stories.tsx 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" > - +