Skip to content

Commit

Permalink
feat: Add builderMode, add mode "edit" (#4376)
Browse files Browse the repository at this point in the history
## Description
Part 1 Of #3994 

This PR introduces a feature flag, `contentEditableMode`, that hides the
`EditorButton` in the top bar. This button is now intended for **testing
purposes only** and will **never be used in production**.

<img width="154" alt="image"
src="https://github.com/user-attachments/assets/f4a37334-71b9-43d8-b4a8-afd518fc6833">

## Most Notable Things In The Content Edit Mode

1. **Notion-like Editing**: Elements are editable with a single click.
2. **Restricted Element Editing**: Binded elements, such as expressions,
are non-editable.
3. **Selective Property Editing**: Only properties for `img` and `link`
elements are editable.

Additional changes:
- **Copy/Paste and Drag/Drop**: Disabled to maintain a controlled
testing environment.

Refer to the issue `Part 1` for further details on upcoming changes and
planned enhancements.


## Steps for reproduction

Play play 

## Code Review

- [ ] hi @kof, 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` file
  • Loading branch information
istarkov authored Nov 14, 2024
1 parent f75a837 commit 36ba02d
Show file tree
Hide file tree
Showing 30 changed files with 663 additions and 198 deletions.
41 changes: 30 additions & 11 deletions apps/builder/app/builder/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
$publisherHost,
$imageLoader,
$textEditingInstanceSelector,
$isDesignMode,
$isContentMode,
} from "~/shared/nano-states";
import { $settings, type Settings } from "./shared/client-settings";
import { builderUrl, getCanvasUrl } from "~/shared/router-utils";
Expand Down Expand Up @@ -57,8 +59,11 @@ import { updateWebstudioData } from "~/shared/instance-utils";
import { migrateWebstudioDataMutable } from "~/shared/webstudio-data-migrator";
import { Loading, LoadingBackground } from "./shared/loading";
import { mergeRefs } from "@react-aria/utils";
import { initCopyPaste } from "~/shared/copy-paste";
import { CommandPanel } from "./features/command-panel";
import {
initCopyPaste,
initCopyPasteForContentEditMode,
} from "~/shared/copy-paste/init-copy-paste";

registerContainers();

Expand Down Expand Up @@ -281,6 +286,9 @@ export const Builder = ({
});
const isCloneDialogOpen = useStore($isCloneDialogOpen);
const isPreviewMode = useStore($isPreviewMode);
const isDesignMode = useStore($isDesignMode);
const isContentMode = useStore($isContentMode);

const { onRef: onRefReadCanvas, onTransitionEnd } = useReadCanvasRect();

useSetWindowTitle();
Expand All @@ -295,23 +303,33 @@ export const Builder = ({

useEffect(() => {
const abortController = new AbortController();
// We need to initialize this in both canvas and builder,
// because the events will fire in either one, depending on where the focus is
// @todo we need to forward the events from canvas to builder and avoid importing this
// in both places
initCopyPaste(abortController);

if (isDesignMode) {
// We need to initialize this in both canvas and builder,
// because the events will fire in either one, depending on where the focus is
// @todo we need to forward the events from canvas to builder and avoid importing this
// in both places
initCopyPaste(abortController);
}

if (isContentMode) {
initCopyPasteForContentEditMode(abortController);
}

return () => {
abortController.abort();
};
}, [isContentMode, isDesignMode]);

useEffect(() => {
const unsubscribe = $loadingState.subscribe((loadingState) => {
setLoadingState(loadingState);
// We need to stop updating it once it's ready in case in the future it changes again.
if (loadingState.state === "ready") {
unsubscribe();
}
});
return () => {
unsubscribe();
abortController.abort();
};
return unsubscribe;
}, []);

const canvasUrl = getCanvasUrl();
Expand Down Expand Up @@ -399,7 +417,8 @@ export const Builder = ({
/>
)}
</Workspace>
<AiCommandBar isPreviewMode={isPreviewMode} />

{isDesignMode && <AiCommandBar />}
</Main>
<SidePanel gridArea="sidebar">
<SidebarLeft publish={publish} />
Expand Down
6 changes: 1 addition & 5 deletions apps/builder/app/builder/features/ai/ai-command-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const initialPrompts = [
"Create a testimonials section on 2 rows. The first row has a heading and subheading, the second row has 3 testimonial cards with an image, headline, description and link.",
];

export const AiCommandBar = ({ isPreviewMode }: { isPreviewMode: boolean }) => {
export const AiCommandBar = () => {
const [value, setValue] = useState("");
const [prompts, setPrompts] = useState<string[]>(initialPrompts);
const isMenuOpen = getSetting("isAiMenuOpen");
Expand Down Expand Up @@ -167,10 +167,6 @@ export const AiCommandBar = ({ isPreviewMode }: { isPreviewMode: boolean }) => {
},
});

if (isPreviewMode) {
return;
}

const handleAiRequest = async (prompt: string) => {
if (abortController.current) {
if (abortController.current.signal.aborted === false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
$styles,
$selectedBreakpointId,
$selectedBreakpoint,
$isContentMode,
} from "~/shared/nano-states";
import {
$breakpointsMenuView,
Expand All @@ -47,6 +48,7 @@ export const BreakpointsPopover = () => {
const breakpoints = useStore($breakpoints);
const selectedBreakpoint = useStore($selectedBreakpoint);
const scale = useStore($scale);
const isContentMode = useStore($isContentMode);

if (selectedBreakpoint === undefined) {
return null;
Expand Down Expand Up @@ -183,18 +185,27 @@ export const BreakpointsPopover = () => {
padding: theme.spacing[5],
}}
>
<Button
color="neutral"
css={{ flexGrow: 1 }}
onClick={(event) => {
event.preventDefault();
$breakpointsMenuView.set(
view === "initial" ? "editor" : "initial"
);
}}
<Tooltip
content={
isContentMode
? "Editing is not allowed in content mode"
: undefined
}
>
{view === "editor" ? "Done" : "Edit breakpoints"}
</Button>
<Button
color="neutral"
css={{ flexGrow: 1 }}
disabled={isContentMode}
onClick={(event) => {
event.preventDefault();
$breakpointsMenuView.set(
view === "initial" ? "editor" : "initial"
);
}}
>
{view === "editor" ? "Done" : "Edit breakpoints"}
</Button>
</Tooltip>
</Flex>
</>
)}
Expand Down
4 changes: 3 additions & 1 deletion apps/builder/app/builder/features/inspector/inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { FloatingPanelProvider } from "~/builder/shared/floating-panel";
import {
$registeredComponentMetas,
$dragAndDropState,
$isDesignMode,
} from "~/shared/nano-states";
import { NavigatorTree } from "~/builder/features/navigator";
import type { Settings } from "~/builder/shared/client-settings";
Expand Down Expand Up @@ -78,6 +79,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
const metas = useStore($registeredComponentMetas);
const selectedPage = useStore($selectedPage);
const activeInspectorPanel = useStore($activeInspectorPanel);
const isDesignMode = useStore($isDesignMode);

if (navigatorLayout === "docked" && isDragging) {
return <NavigatorTree />;
Expand All @@ -100,7 +102,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
type PanelName = "style" | "settings";

const availablePanels = new Set<PanelName>();
if (documentType === "html" && (meta?.stylable ?? true)) {
if (documentType === "html" && (meta?.stylable ?? true) && isDesignMode) {
availablePanels.add("style");
}
// @todo hide root component settings until
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
$editingItemSelector,
$hoveredInstanceSelector,
$instances,
$isContentMode,
$props,
$propsIndex,
$propValuesByInstanceSelector,
Expand Down Expand Up @@ -404,6 +405,10 @@ const getBuilderDropTarget = (
};

const canDrag = (instance: Instance) => {
if ($isContentMode.get()) {
return false;
}

const meta = $registeredComponentMetas.get().get(instance.component);
if (meta === undefined) {
return true;
Expand Down
5 changes: 4 additions & 1 deletion apps/builder/app/builder/features/navigator/navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import {
import { CrossIcon } from "@webstudio-is/icons";
import { CssPreview } from "./css-preview";
import { NavigatorTree } from "./navigator-tree";
import { $isDesignMode } from "~/shared/nano-states";
import { useStore } from "@nanostores/react";

export const NavigatorPanel = ({ onClose }: { onClose: () => void }) => {
const isDesignMode = useStore($isDesignMode);
return (
<>
<PanelTitle
Expand All @@ -30,7 +33,7 @@ export const NavigatorPanel = ({ onClose }: { onClose: () => void }) => {
<Flex grow direction="column" justify="end">
<NavigatorTree />
<Separator />
<CssPreview />
{isDesignMode && <CssPreview />}
</Flex>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion apps/builder/app/builder/features/pages/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from "@webstudio-is/icons";
import { ExtendedPanel } from "../../shared/extended-sidebar-panel";
import { NewPageSettings, PageSettings } from "./page-settings";
import { $editingPageId, $pages } from "~/shared/nano-states";
import { $editingPageId, $isContentMode, $pages } from "~/shared/nano-states";
import {
getAllChildrenAndSelf,
reparentOrphansMutable,
Expand Down Expand Up @@ -334,6 +334,10 @@ const PagesTree = ({
isLastChild={item.isLastChild}
data={item}
canDrag={() => {
if ($isContentMode.get()) {
return false;
}

// forbid dragging home page
if (item.id === pages.homePage.id) {
toast.error("Home page cannot be moved");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const TextControl = ({
onBlur={localValue.save}
onSubmit={localValue.save}
/>

<BindingPopover
scope={scope}
aliases={aliases}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Separator,
Flex,
Box,
Grid,
} from "@webstudio-is/design-system";
import {
descendantComponent,
Expand All @@ -18,11 +19,12 @@ import {
$propValuesByInstanceSelector,
$propsIndex,
$props,
$isDesignMode,
$isContentMode,
} from "~/shared/nano-states";
import { CollapsibleSectionWithAddButton } from "~/builder/shared/collapsible-section";
import { renderControl } from "../controls/combined";
import { usePropsLogic, type PropAndMeta } from "./use-props-logic";
import { Row } from "../shared";
import { serverSyncStore } from "~/shared/sync";
import { $selectedInstanceKey } from "~/shared/awareness";

Expand Down Expand Up @@ -155,41 +157,57 @@ type PropsSectionProps = {
// A UI componet with minimum logic that can be demoed in Storybook etc.
export const PropsSection = (props: PropsSectionProps) => {
const { propsLogic: logic } = props;

const [addingProp, setAddingProp] = useState(false);
const isDesignMode = useStore($isDesignMode);
const isContentMode = useStore($isContentMode);

const hasItems =
logic.addedProps.length > 0 || addingProp || logic.initialProps.length > 0;

const showPropertiesSection =
isDesignMode || (isContentMode && logic.initialProps.length > 0);

return (
<>
<Row css={{ py: theme.panel.paddingBlock }}>
{logic.systemProps.map((item) => renderProperty(props, item))}
</Row>
<Grid
css={{
paddingBottom: theme.panel.paddingBlock,
}}
>
{logic.systemProps.map((item) => (
<Box
key={item.propName}
css={{ paddingInline: theme.panel.paddingInline }}
>
{renderProperty(props, item)}
</Box>
))}
</Grid>

<Separator />

<CollapsibleSectionWithAddButton
label="Properties & Attributes"
onAdd={() => setAddingProp(true)}
hasItems={hasItems}
>
<Flex gap="1" direction="column">
{addingProp && (
<AddPropertyOrAttribute
availableProps={logic.availableProps}
onPropSelected={(propName) => {
setAddingProp(false);
logic.handleAdd(propName);
}}
/>
)}
{logic.addedProps.map((item) =>
renderProperty(props, item, { deletable: true })
)}
{logic.initialProps.map((item) => renderProperty(props, item))}
</Flex>
</CollapsibleSectionWithAddButton>
{showPropertiesSection && (
<CollapsibleSectionWithAddButton
label="Properties & Attributes"
onAdd={isDesignMode ? () => setAddingProp(true) : undefined}
hasItems={hasItems}
>
<Flex gap="1" direction="column">
{addingProp && (
<AddPropertyOrAttribute
availableProps={logic.availableProps}
onPropSelected={(propName) => {
setAddingProp(false);
logic.handleAdd(propName);
}}
/>
)}
{logic.addedProps.map((item) =>
renderProperty(props, item, { deletable: true })
)}
{logic.initialProps.map((item) => renderProperty(props, item))}
</Flex>
</CollapsibleSectionWithAddButton>
)}
</>
);
};
Expand Down
Loading

0 comments on commit 36ba02d

Please sign in to comment.