mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
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.
This commit is contained in:
1 parent
698584c664
commit
dc0236fafe
2 files changed
+184
-58
No files matched your search
@@ -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 (
|
||||
<Text variant="mono" css={{ whiteSpace: "pre" }}>
|
||||
{JSON.stringify(system, null, 2)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Builder/Address Bar",
|
||||
component: AddressBarPopover,
|
||||
} satisfies Meta;
|
||||
|
||||
export const AddressBar: StoryFn = () => (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
height: theme.spacing[15],
|
||||
background: theme.colors.backgroundTopbar,
|
||||
color: theme.colors.foregroundContrastMain,
|
||||
}}
|
||||
>
|
||||
<AddressBarPopover />
|
||||
</Box>
|
||||
<SystemInspect />
|
||||
</>
|
||||
);
|
||||
@@ -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<string, string> = {};
|
||||
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<HTMLFormElement, unknown>((_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<string, string>();
|
||||
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 (
|
||||
<InputErrorsTooltip errors={Array.from(errors.values())}>
|
||||
<Flex gap={1} css={{ padding: theme.spacing[5] }}>
|
||||
<Flex align="center" gap={1} css={textVariants.mono}>
|
||||
{tokens.map((token, index) => {
|
||||
if (token.type === "fragment") {
|
||||
return token.value;
|
||||
}
|
||||
if (token.type === "param") {
|
||||
return (
|
||||
<InputField
|
||||
key={index}
|
||||
fieldSizing="content"
|
||||
css={{ minWidth: theme.spacing[15] }}
|
||||
color={errors.has(token.name) ? "error" : undefined}
|
||||
id={`${id}-${token.name}`}
|
||||
placeholder={token.name}
|
||||
value={pathParams?.[token.name] ?? ""}
|
||||
onChange={(event) =>
|
||||
updatePathParam(token.name, event.target.value)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
token satisfies never;
|
||||
})}
|
||||
</Flex>
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const path = $selectedPagePath.get();
|
||||
const tokens = tokenizePathnamePattern(path);
|
||||
// delete stale fields
|
||||
const newParams: Record<string, string> = {};
|
||||
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 */}
|
||||
<button style={{ display: "none" }}>submit</button>
|
||||
<InputErrorsTooltip errors={Array.from(errors.values())}>
|
||||
<Flex gap={1} css={{ padding: theme.spacing[5] }}>
|
||||
<Flex align="center" gap={1} css={textVariants.mono}>
|
||||
{tokens.map((token, index) => {
|
||||
if (token.type === "fragment") {
|
||||
return token.value;
|
||||
}
|
||||
if (token.type === "param") {
|
||||
return (
|
||||
<InputField
|
||||
key={index}
|
||||
name={token.name}
|
||||
fieldSizing="content"
|
||||
css={{ minWidth: theme.spacing[15] }}
|
||||
color={errors.has(token.name) ? "error" : undefined}
|
||||
placeholder={token.name}
|
||||
value={pathParams[token.name] ?? ""}
|
||||
onChange={(event) =>
|
||||
setPathParams((prevPathParams) => ({
|
||||
...prevPathParams,
|
||||
[token.name]: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
token satisfies never;
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Tooltip {...tooltipProps}>
|
||||
<IconButton {...buttonProps} disabled={errors.size > 0} />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</InputErrorsTooltip>
|
||||
<Tooltip {...tooltipProps}>
|
||||
<IconButton
|
||||
{...buttonProps}
|
||||
disabled={errors.size > 0}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</InputErrorsTooltip>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
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<HTMLFormElement>(null);
|
||||
|
||||
// show only copy button when path is static
|
||||
if (isPathnamePattern(path) === false) {
|
||||
@@ -196,7 +216,15 @@ export const AddressBarPopover = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(newIsOpen) => {
|
||||
setIsOpen(newIsOpen);
|
||||
if (newIsOpen === false) {
|
||||
formRef.current?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<ToolbarButton aria-label="Toggle dynamic page address" tabIndex={0}>
|
||||
<DynamicPageIcon />
|
||||
@@ -209,7 +237,7 @@ export const AddressBarPopover = () => {
|
||||
collisionPadding={4}
|
||||
align="start"
|
||||
>
|
||||
<AddressBar />
|
||||
<AddressBar ref={formRef} />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
|
||||
Reference in new issue
Block a user