Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { useId, useMemo } from "react";
import { useStore } from "@nanostores/react";
import { computed } from "nanostores";
import { TextArea } from "@webstudio-is/design-system";
import { Flex, rawTheme, Text, TextArea } from "@webstudio-is/design-system";
import type { Instance } from "@webstudio-is/sdk";
import { AlertIcon } from "@webstudio-is/icons";
import { $instances } from "~/shared/nano-states";
import { serverSyncStore } from "~/shared/sync";
import {
BindingControl,
BindingPopover,
} from "~/builder/shared/binding-popover";
import { updateWebstudioData } from "~/shared/instance-utils";
import {
type ControlProps,
useLocalValue,
VerticalLayout,
$selectedInstanceScope,
Label,
updateExpressionValue,
useBindingState,
humanizeAttribute,
} from "../shared";
import { FieldLabel } from "../property-label";

const useInstance = (instanceId: Instance["id"]) => {
const $store = useMemo(() => {
Expand All @@ -32,22 +32,20 @@ const updateChildren = (
type: "text" | "expression",
value: string
) => {
serverSyncStore.createTransaction([$instances], (instances) => {
const instance = instances.get(instanceId);
if (instance === undefined) {
return;
updateWebstudioData((data) => {
const instance = data.instances.get(instanceId);
if (instance) {
instance.children = [{ type, value }];
}
instance.children = [{ type, value }];
});
};

export const TextContent = ({
instanceId,
meta,
propName,
computedValue,
}: ControlProps<"textContent">) => {
const instance = useInstance(instanceId);
const hasChildren = (instance?.children.length ?? 0) > 0;
// text content control is rendered only when empty or single child are present
const child = instance?.children?.[0] ?? { type: "text", value: "" };
const localValue = useLocalValue(String(computedValue ?? ""), (value) => {
Expand All @@ -58,7 +56,6 @@ export const TextContent = ({
}
});
const id = useId();
const label = humanizeAttribute(meta.label || propName);

const { scope, aliases } = useStore($selectedInstanceScope);
let expression: undefined | string;
Expand All @@ -76,13 +73,37 @@ export const TextContent = ({
return (
<VerticalLayout
label={
<Label
htmlFor={id}
description={meta.description}
readOnly={overwritable === false}
<FieldLabel
description={
<>
Plain text content that can be bound to either a variable or a
resource value.
{overwritable === false && (
<Flex gap="1">
<AlertIcon
color={rawTheme.colors.backgroundAlertMain}
style={{ flexShrink: 0 }}
/>
<Text>
The value is controlled by an expression and cannot be
changed.
</Text>
</Flex>
)}
</>
}
resettable={hasChildren}
onReset={() => {
updateWebstudioData((data) => {
const instance = data.instances.get(instanceId);
if (instance) {
instance.children = [];
}
});
}}
>
{label}
</Label>
Text Content
</FieldLabel>
}
>
<BindingControl>
Expand All @@ -102,7 +123,7 @@ export const TextContent = ({
aliases={aliases}
validate={(value) => {
if (value !== undefined && typeof value !== "string") {
return `${label} expects a string value`;
return `Text Content expects a string value`;
}
}}
variant={variant}
Expand Down
29 changes: 16 additions & 13 deletions apps/builder/app/builder/features/settings-panel/property-label.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { micromark } from "micromark";
import { useMemo, useState } from "react";
import { useMemo, useState, type ReactNode } from "react";
import { computed } from "nanostores";
import { useStore } from "@nanostores/react";
import {
Expand Down Expand Up @@ -182,9 +182,9 @@ export const FieldLabel = ({
children,
}: {
/**
* Markdown text to show in tooltip
* Markdown text to show in tooltip or react element
*/
description?: string;
description?: string | ReactNode;
/**
* when true means field has value and colored true
*/
Expand All @@ -193,6 +193,18 @@ export const FieldLabel = ({
children: string;
}) => {
const [isOpen, setIsOpen] = useState(false);
if (typeof description === "string") {
description = (
<Text
css={{
"> *": { marginTop: 0 },
}}
dangerouslySetInnerHTML={{ __html: micromark(description) }}
></Text>
);
} else if (description) {
description = <Text>{description}</Text>;
}
return (
<Flex align="center" css={{ gap: theme.spacing[3] }}>
{/* prevent label growing */}
Expand Down Expand Up @@ -221,16 +233,7 @@ export const FieldLabel = ({
<Text variant="titles" css={{ textTransform: "none" }}>
{children}
</Text>
{description && (
<Text
css={{
"> *": {
marginTop: 0,
},
}}
dangerouslySetInnerHTML={{ __html: micromark(description) }}
></Text>
)}
{description}
{resettable && (
<Button
color="dark"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,9 @@ export const usePropsLogic = ({
systemProps.push({
propName: textContentAttribute,
meta: {
label: "Text Content",
required: false,
control: "textContent",
type: "string",
defaultValue: "",
},
});
}
Expand Down
16 changes: 16 additions & 0 deletions apps/builder/app/shared/content-model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,22 @@ describe("rich text tree", () => {
})
).toEqual(["divId", "bodyId"]);
});

test("does not treat image component as rich text", () => {
expect(
findClosestRichText({
...renderData(
<ws.element ws:tag="body" ws:id="bodyId">
<ws.element ws:tag="div" ws:id="divId">
<$.Image ws:id="imgId" />
</ws.element>
</ws.element>
),
metas: defaultMetas,
instanceSelector: ["imgId", "divId", "bodyId"],
})
).toEqual(undefined);
});
});

describe("closest container", () => {
Expand Down
7 changes: 6 additions & 1 deletion apps/builder/app/shared/content-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,15 @@ export const isRichTextTree = ({
if (instance === undefined) {
return false;
}
const tag = getTag({ instance, metas, props });
const elementContentModel = tag ? elementsByTag[tag] : undefined;
const componentContentModel = getComponentContentModel(
metas.get(instance.component)
);
const isRichText = componentContentModel.children.includes("rich-text");
const isRichText =
(elementContentModel === undefined ||
elementContentModel.children.length > 0) &&
componentContentModel.children.includes("rich-text");
// only empty instance with rich text content can be edited
if (instance.children.length === 0) {
return isRichText;
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/app/shared/error/error-message.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const pageStyle = css({
inset: 0,
background: theme.colors.brandBackgroundDashboard,
paddingTop: "10vh",
// prevent global root styles override error color
color: theme.colors.black,
});

export const ErrorMessage = ({
Expand Down