chore: Reorganize project settings (#2865)

## Description

No functional changes, only namings and structure - each section,
separate module

## Steps for reproduction

1. open project settings
2. expect no changes

## 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
This commit is contained in:
Oleg Isonen authored and GitHub committed 2024-02-03 16:41:29 +01:00
1 parent 9cb07e6c73
commit 00fa1e97ff
10 files changed
+285 -270

No files matched your search

+1 -1
View File
@@ -48,7 +48,7 @@ import { useSyncPageUrl } from "~/shared/pages";
import { useMount } from "~/shared/hook-utils/use-mount";
import { subscribeCommands } from "~/builder/shared/commands";
import { AiCommandBar } from "./features/ai/ai-command-bar";
import { ProjectSettings } from "./features/seo/project-settings";
import { ProjectSettings } from "./features/project-settings";
import type { UserPlanFeatures } from "~/shared/db/user-plan-features.server";
import { $userPlanFeatures } from "./shared/nano-states";
import { useNavigatorLayout } from "./features/sidebar-left/navigator";
@@ -2,6 +2,7 @@ import { FloatingPanel } from "~/builder/shared/floating-panel";
import { ImageManager } from "~/builder/shared/image-manager";
import type { ReactElement } from "react";
// @todo should be moved to shared as its being reused in another feature
export const ImageControl = (props: {
onAssetIdChange: (assetId: string) => void;
children: ReactElement;
@@ -0,0 +1,2 @@
export * from "./project-settings";
export * from "./image-control";
@@ -0,0 +1,117 @@
import { useStore } from "@nanostores/react";
import {
Grid,
InputField,
Label,
theme,
Text,
TextArea,
Separator,
Button,
css,
} from "@webstudio-is/design-system";
import { ImageControl } from "./image-control";
import { $assets } from "~/shared/nano-states";
import env from "~/shared/env";
import { Image, createImageLoader } from "@webstudio-is/image";
import { useIds } from "~/shared/form-utils";
import type { ProjectMeta } from "@webstudio-is/sdk";
const imgStyle = css({
width: 72,
height: 72,
borderRadius: theme.borderRadius[4],
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.colors.borderMain,
});
export const MetaSection = (props: {
meta: ProjectMeta;
onMetaChange: (value: ProjectMeta) => void;
}) => {
const ids = useIds(["siteName", "favicon", "code"]);
const handleChange =
<Name extends keyof ProjectMeta>(name: Name) =>
(value: ProjectMeta[Name]) => {
props.onMetaChange({
...props.meta,
[name]: value,
});
};
const assets = useStore($assets);
const asset = assets.get(props.meta.faviconAssetId ?? "");
const favIconUrl = asset ? `${asset.name}` : undefined;
const imageLoader = createImageLoader({
imageBaseUrl: env.IMAGE_BASE_URL,
});
return (
<>
<Grid
gap={1}
css={{
mx: theme.spacing[5],
px: theme.spacing[5],
}}
>
<Label htmlFor={ids.siteName}>Project Name</Label>
<InputField
id={ids.siteName}
value={props.meta.siteName ?? ""}
onChange={(event) => handleChange("siteName")(event.target.value)}
placeholder="Current Project Name"
name="Name"
autoFocus
/>
</Grid>
<Separator />
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Favicon</Text>
<Grid flow="column" gap={3}>
<Image
width={72}
height={72}
className={imgStyle()}
src={favIconUrl}
loader={imageLoader}
/>
<Grid gap={2}>
<Text color="subtle">
Upload a 32 x 32 px image to display in browser tabs.
</Text>
<ImageControl onAssetIdChange={handleChange("faviconAssetId")}>
<Button id={ids.favicon} css={{ justifySelf: "start" }}>
Upload
</Button>
</ImageControl>
</Grid>
</Grid>
</Grid>
<Separator />
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Custom Code</Text>
<Text color="subtle">
Custom code and scripts will be added at the end of the &lt;head&gt;
tag to every page across the published project.
</Text>
<TextArea
id={ids.code}
rows={5}
autoGrow
maxRows={10}
value={props.meta.code ?? ""}
onChange={handleChange("code")}
/>
</Grid>
</>
);
};
@@ -0,0 +1,114 @@
import { useStore } from "@nanostores/react";
import {
Dialog,
DialogContent,
DialogTitle,
Grid,
theme,
Separator,
ScrollArea,
} from "@webstudio-is/design-system";
import { useEffect, useState } from "react";
import { $isProjectSettingsOpen } from "~/shared/nano-states/seo";
import { $pages } from "~/shared/nano-states";
import { serverSyncStore } from "~/shared/sync";
import { useEffectEvent } from "../ai/hooks/effect-event";
import type { Pages } from "@webstudio-is/sdk";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import { MetaSection } from "./meta-section";
import { PublishSection } from "./publish-section";
import { RedirectSection } from "./redirect-settings";
export type ProjectSettings = NonNullable<Pages["settings"]>;
const ProjectSettingsView = () => {
const [meta, setMeta] = useState(
$pages.get()?.meta ?? {
siteName: "",
faviconAssetId: "",
code: "",
}
);
const [settings, setSettings] = useState(
$pages.get()?.settings ?? {
atomicStyles: true,
}
);
const isOpen = useStore($isProjectSettingsOpen);
const handleSave = useEffectEvent(() => {
serverSyncStore.createTransaction([$pages], (pages) => {
if (pages === undefined) {
return;
}
pages.meta = meta;
pages.settings = settings;
});
});
return (
<Dialog
open={isOpen}
onOpenChange={(isOpen) => {
handleSave();
$isProjectSettingsOpen.set(isOpen);
}}
>
<DialogContent
onBlur={handleSave}
// Left Aside panels (e.g., Pages, Components) use zIndex: theme.zIndices[1].
// For a dialog to appear above these panels, both overlay and content should also have zIndex: theme.zIndices[1].
css={{
width: theme.spacing[34],
zIndex: theme.zIndices[1],
}}
overlayCss={{
zIndex: theme.zIndices[1],
}}
>
<ScrollArea>
<Grid gap={2} css={{ my: theme.spacing[5] }}>
<MetaSection meta={meta} onMetaChange={setMeta} />
<Separator />
<PublishSection
settings={settings}
onSettingsChange={setSettings}
/>
{isFeatureEnabled("redirects") ? (
<RedirectSection
settings={settings}
onSettingsChange={setSettings}
/>
) : null}
<div />
</Grid>
</ScrollArea>
{/* Title is at the end intentionally,
* to make the close button last in the tab order
*/}
<DialogTitle>Project Settings</DialogTitle>
</DialogContent>
</Dialog>
);
};
export const ProjectSettings = () => {
const [settingDialogLazyOpen, setSettingDialogLazyOpen] = useState(false);
useEffect(() => {
return $isProjectSettingsOpen.subscribe((isOpen) => {
if (isOpen) {
setSettingDialogLazyOpen(true);
}
});
}, []);
if (settingDialogLazyOpen === false) {
return null;
}
return <ProjectSettingsView />;
};
@@ -0,0 +1,46 @@
import {
Grid,
Label,
theme,
Text,
CheckboxAndLabel,
Checkbox,
} from "@webstudio-is/design-system";
import { useIds } from "~/shared/form-utils";
import type { ProjectSettings } from "./project-settings";
export const PublishSection = (props: {
settings: ProjectSettings;
onSettingsChange: (settings: ProjectSettings) => void;
}) => {
const ids = useIds(["atomicStyles"]);
const handleChange =
<Name extends keyof ProjectSettings>(name: Name) =>
(value: ProjectSettings[Name]) => {
props.onSettingsChange({
...props.settings,
[name]: value,
});
};
return (
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Publish Settings</Text>
<CheckboxAndLabel>
<Checkbox
checked={props.settings.atomicStyles ?? true}
id={ids.atomicStyles}
onCheckedChange={(checked) => {
if (typeof checked === "boolean") {
handleChange("atomicStyles")(checked);
}
}}
/>
<Label htmlFor={ids.atomicStyles}>
Generate atomic CSS when publishing
</Label>
</CheckboxAndLabel>
</Grid>
);
};
@@ -19,7 +19,7 @@ import { useStore } from "@nanostores/react";
import { getExistingRoutePaths } from "../sidebar-left/panels/pages/page-utils";
import { $pages } from "~/shared/nano-states";
export const ProjectRedirectionSettings = (props: {
export const RedirectSection = (props: {
settings: ProjectSettings;
onSettingsChange: (settings: ProjectSettings) => void;
}) => {
@@ -1,267 +0,0 @@
import { useStore } from "@nanostores/react";
import {
Dialog,
DialogContent,
DialogTitle,
Grid,
InputField,
Label,
theme,
Text,
TextArea,
Separator,
Button,
css,
CheckboxAndLabel,
Checkbox,
ScrollArea,
} from "@webstudio-is/design-system";
import { useEffect, useState } from "react";
import { $isProjectSettingsOpen } from "~/shared/nano-states/seo";
import { ImageControl } from "./image-control";
import { $assets, $pages } from "~/shared/nano-states";
import env from "~/shared/env";
import { Image, createImageLoader } from "@webstudio-is/image";
import { useIds } from "~/shared/form-utils";
import { serverSyncStore } from "~/shared/sync";
import { useEffectEvent } from "../ai/hooks/effect-event";
import type { Pages } from "@webstudio-is/sdk";
import { ProjectRedirectionSettings } from "./project-redirects";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
type ProjectMeta = NonNullable<Pages["meta"]>;
export type ProjectSettings = NonNullable<Pages["settings"]>;
const imgStyle = css({
width: 72,
height: 72,
borderRadius: theme.borderRadius[4],
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.colors.borderMain,
});
const ProjectSettingsContentMeta = (props: {
meta: ProjectMeta;
onMetaChange: (value: ProjectMeta) => void;
}) => {
const ids = useIds(["siteName", "favicon", "code"]);
const handleChange =
<T extends keyof ProjectMeta>(name: T) =>
(value: ProjectMeta[T]) => {
props.onMetaChange({
...props.meta,
[name]: value,
});
};
const assets = useStore($assets);
const asset = assets.get(props.meta.faviconAssetId ?? "");
const favIconUrl = asset ? `${asset.name}` : undefined;
const imageLoader = createImageLoader({
imageBaseUrl: env.IMAGE_BASE_URL,
});
return (
<>
<Grid
gap={1}
css={{
mx: theme.spacing[5],
px: theme.spacing[5],
}}
>
<Label htmlFor={ids.siteName}>Project Name</Label>
<InputField
id={ids.siteName}
value={props.meta.siteName ?? ""}
onChange={(event) => handleChange("siteName")(event.target.value)}
placeholder="Current Project Name"
name="Name"
autoFocus
/>
</Grid>
<Separator />
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Favicon</Text>
<Grid flow="column" gap={3}>
<Image
width={72}
height={72}
className={imgStyle()}
src={favIconUrl}
loader={imageLoader}
/>
<Grid gap={2}>
<Text color="subtle">
Upload a 32 x 32 px image to display in browser tabs.
</Text>
<ImageControl onAssetIdChange={handleChange("faviconAssetId")}>
<Button id={ids.favicon} css={{ justifySelf: "start" }}>
Upload
</Button>
</ImageControl>
</Grid>
</Grid>
</Grid>
<Separator />
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Custom Code</Text>
<Text color="subtle">
Custom code and scripts will be added at the end of the &lt;head&gt;
tag to every page across the published project.
</Text>
<TextArea
id={ids.code}
rows={5}
autoGrow
maxRows={10}
value={props.meta.code ?? ""}
onChange={handleChange("code")}
/>
</Grid>
</>
);
};
const ProjectAdvancedSettings = (props: {
settings: ProjectSettings;
onSettingsChange: (settings: ProjectSettings) => void;
}) => {
const ids = useIds(["atomicStyles"]);
const handleChange =
<T extends keyof ProjectSettings>(name: T) =>
(val: ProjectSettings[T]) => {
props.onSettingsChange({
...props.settings,
[name]: val,
});
};
return (
<>
<Separator />
<Grid gap={2} css={{ mx: theme.spacing[5], px: theme.spacing[5] }}>
<Text variant="titles">Publish Settings</Text>
<CheckboxAndLabel>
<Checkbox
checked={props.settings.atomicStyles ?? true}
id={ids.atomicStyles}
onCheckedChange={(checked) => {
if (typeof checked === "boolean") {
handleChange("atomicStyles")(checked);
}
}}
/>
<Label htmlFor={ids.atomicStyles}>
Generate atomic CSS when publishing
</Label>
</CheckboxAndLabel>
</Grid>
</>
);
};
const ProjectSettingsView = () => {
const [meta, setMeta] = useState(
$pages.get()?.meta ?? {
siteName: "",
faviconAssetId: "",
code: "",
}
);
const [settings, setSettings] = useState(
$pages.get()?.settings ?? {
atomicStyles: true,
}
);
const isOpen = useStore($isProjectSettingsOpen);
const handleSave = useEffectEvent(() => {
serverSyncStore.createTransaction([$pages], (pages) => {
if (pages === undefined) {
return;
}
pages.meta = meta;
pages.settings = settings;
});
});
return (
<Dialog
open={isOpen}
onOpenChange={(isOpen) => {
handleSave();
$isProjectSettingsOpen.set(isOpen);
}}
>
<DialogContent
onBlur={handleSave}
// Left Aside panels (e.g., Pages, Components) use zIndex: theme.zIndices[1].
// For a dialog to appear above these panels, both overlay and content should also have zIndex: theme.zIndices[1].
css={{
width: theme.spacing[34],
zIndex: theme.zIndices[1],
}}
overlayCss={{
zIndex: theme.zIndices[1],
}}
>
<ScrollArea>
<Grid
gap={2}
css={{
my: theme.spacing[5],
}}
>
<ProjectSettingsContentMeta meta={meta} onMetaChange={setMeta} />
<ProjectAdvancedSettings
settings={settings}
onSettingsChange={setSettings}
/>
{isFeatureEnabled("redirects") ? (
<ProjectRedirectionSettings
settings={settings}
onSettingsChange={setSettings}
/>
) : null}
<div />
</Grid>
</ScrollArea>
{/* Title is at the end intentionally,
* to make the close button last in the tab order
*/}
<DialogTitle>Project Settings</DialogTitle>
</DialogContent>
</Dialog>
);
};
export const ProjectSettings = () => {
const [settingDialogLazyOpen, setSettingDialogLazyOpen] = useState(false);
useEffect(() => {
return $isProjectSettingsOpen.subscribe((isOpen) => {
if (isOpen) {
setSettingDialogLazyOpen(true);
}
});
}, []);
if (settingDialogLazyOpen === false) {
return null;
}
return <ProjectSettingsView />;
};
@@ -72,9 +72,11 @@ import {
} from "~/builder/shared/binding-popover";
import { serverSyncStore } from "~/shared/sync";
import { SearchPreview } from "./search-preview";
import { ImageControl } from "~/builder/features/seo/image-control";
// @todo should be moved to shared because features should not depend on features
import { ImageControl } from "~/builder/features/project-settings";
import { ImageInfo } from "./image-info";
import { SocialPreview } from "./social-preview";
// @todo should be moved to shared because features should not depend on features
import { useEffectEvent } from "~/builder/features/ai/hooks/effect-event";
import { CustomMetadata } from "./custom-metadata";
import env from "~/shared/env";