mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
feat: allow using pro features in builder for free users (#5039)
We decided to not block testing of pro features in builder and block only publishing projects with these features. <img width="562" alt="Screenshot 2025-03-24 at 13 38 21" src="https://github.com/user-attachments/assets/ba796d3f-9e7d-43ca-8939-01423b83c340" /> <img width="492" alt="image" src="https://github.com/user-attachments/assets/8d8ccaa1-1126-470a-bc2a-6e75940c7e6e" /> <img width="973" alt="Screenshot 2025-03-24 at 13 04 07" src="https://github.com/user-attachments/assets/21a8a40c-937a-4845-9481-302a8272d8db" /> <img width="573" alt="Screenshot 2025-03-24 at 12 56 07" src="https://github.com/user-attachments/assets/e22e57c4-e134-4835-8dd4-24ba54faa30d" />
This commit is contained in:
1 parent
5b3144d656
commit
ad114eb37f
6 files changed
+192
-247
No files matched your search
@@ -55,6 +55,7 @@ import {
|
||||
PanelTitle,
|
||||
TitleSuffixSpacer,
|
||||
FloatingPanelProvider,
|
||||
ProBadge,
|
||||
} from "@webstudio-is/design-system";
|
||||
import {
|
||||
ChevronsLeftIcon,
|
||||
@@ -86,11 +87,9 @@ import { ImageControl } from "~/builder/features/project-settings";
|
||||
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
|
||||
import {
|
||||
compilePathnamePattern,
|
||||
isPathnamePattern,
|
||||
tokenizePathnamePattern,
|
||||
validatePathnamePattern,
|
||||
} from "~/builder/shared/url-pattern";
|
||||
import type { UserPlanFeatures } from "~/shared/db/user-plan-features.server";
|
||||
import { useUnmount } from "~/shared/hook-utils/use-mount";
|
||||
import { selectInstance } from "~/shared/awareness";
|
||||
import { computeExpression } from "~/shared/data-variables";
|
||||
@@ -120,7 +119,7 @@ const fieldDefaultValues = {
|
||||
language: `""`,
|
||||
socialImageUrl: `""`,
|
||||
socialImageAssetId: "",
|
||||
status: `200`,
|
||||
status: undefined as string | undefined,
|
||||
redirect: `""`,
|
||||
documentType: "html" as (typeof documentTypes)[number],
|
||||
customMetas: [{ property: "", content: `""` }],
|
||||
@@ -201,8 +200,7 @@ const validateValues = (
|
||||
// undefined page id means new page
|
||||
pageId: undefined | Page["id"],
|
||||
values: Values,
|
||||
variableValues: Map<string, unknown>,
|
||||
userPlanFeatures: UserPlanFeatures
|
||||
variableValues: Map<string, unknown>
|
||||
): Errors => {
|
||||
// Null or undefined in the field value is only possible if it’s a dynamic (expression) value.
|
||||
// We do not validate dynamic values but instead provide a default value for validation.
|
||||
@@ -221,7 +219,7 @@ const validateValues = (
|
||||
),
|
||||
language: computeExpression(values.language, variableValues),
|
||||
socialImageUrl: computeExpression(values.socialImageUrl, variableValues),
|
||||
status: computeExpression(values.status, variableValues),
|
||||
status: computeExpression(values.status ?? `undefined`, variableValues),
|
||||
redirect: computeExpression(values.redirect, variableValues),
|
||||
customMetas: values.customMetas.map((item) => ({
|
||||
property: item.property,
|
||||
@@ -252,14 +250,6 @@ const validateValues = (
|
||||
errors.path = errors.path ?? [];
|
||||
errors.path.push(...messages);
|
||||
}
|
||||
|
||||
if (
|
||||
userPlanFeatures.allowDynamicData === false &&
|
||||
isPathnamePattern(values.path)
|
||||
) {
|
||||
errors.path = errors.path ?? [];
|
||||
errors.path.push("Dynamic path is supported only in Pro");
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
@@ -311,59 +301,47 @@ const PathField = ({
|
||||
const id = useId();
|
||||
return (
|
||||
<Grid gap={1}>
|
||||
{allowDynamicData ? (
|
||||
<Flex align="center" gap={1}>
|
||||
<Label htmlFor={id}>Dynamic Path</Label>
|
||||
<Tooltip
|
||||
content="The path can include dynamic parameters like :name, which could be made optional using :name?, or have a wildcard such as /* or /:name* to store whole remaining part at the end of the URL."
|
||||
variant="wrapped"
|
||||
>
|
||||
<InfoCircleIcon
|
||||
color={rawTheme.colors.foregroundSubtle}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex align="center" gap={1}>
|
||||
<Label htmlFor={id}>Path</Label>
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<Text>
|
||||
Path is a subset of the URL that looks like this:
|
||||
"/blog".
|
||||
</Text>
|
||||
{allowDynamicData === false && (
|
||||
<>
|
||||
<Text>
|
||||
To make the path dynamic and use it with CMS, you can use
|
||||
parameters and other features. CMS features are part of
|
||||
the Pro plan.
|
||||
</Text>
|
||||
<Link
|
||||
className={buttonStyle({ color: "gradient" })}
|
||||
css={{ marginTop: theme.spacing[5], width: "100%" }}
|
||||
color="contrast"
|
||||
underline="none"
|
||||
target="_blank"
|
||||
href="https://webstudio.is/pricing"
|
||||
>
|
||||
Upgrade
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
variant="wrapped"
|
||||
>
|
||||
<InfoCircleIcon
|
||||
color={rawTheme.colors.foregroundSubtle}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex align="center" gap={1}>
|
||||
<Label htmlFor={id}>Path</Label>
|
||||
{allowDynamicData === false && <ProBadge>PRO</ProBadge>}
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<Text>
|
||||
The path can include dynamic parameters like :name, which could
|
||||
be made optional using :name?, or have a wildcard such as /* or
|
||||
/:name* to store whole remaining part at the end of the URL.
|
||||
</Text>
|
||||
{allowDynamicData === false && (
|
||||
<>
|
||||
<br />
|
||||
<Text>
|
||||
To make the path dynamic and use it with CMS, you can use
|
||||
parameters and other features. CMS features are part of the
|
||||
Pro plan.
|
||||
</Text>
|
||||
<Link
|
||||
className={buttonStyle({ color: "gradient" })}
|
||||
css={{ marginTop: theme.spacing[5], width: "100%" }}
|
||||
color="contrast"
|
||||
underline="none"
|
||||
target="_blank"
|
||||
href="https://webstudio.is/pricing"
|
||||
>
|
||||
Upgrade
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
variant="wrapped"
|
||||
>
|
||||
<InfoCircleIcon
|
||||
color={rawTheme.colors.foregroundSubtle}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<InputErrorsTooltip errors={errors}>
|
||||
<InputField
|
||||
color={errors && "error"}
|
||||
@@ -379,12 +357,12 @@ const PathField = ({
|
||||
|
||||
const StatusField = ({
|
||||
errors,
|
||||
value,
|
||||
value = `undefined`,
|
||||
onChange,
|
||||
}: {
|
||||
errors?: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
value: undefined | string;
|
||||
onChange: (value: undefined | string) => void;
|
||||
}) => {
|
||||
const id = useId();
|
||||
const { allowDynamicData } = useStore($userPlanFeatures);
|
||||
@@ -393,6 +371,7 @@ const StatusField = ({
|
||||
<Grid gap={1}>
|
||||
<Flex align="center" gap={1}>
|
||||
<Label htmlFor={id}>Status Code </Label>
|
||||
{allowDynamicData === false && <ProBadge>PRO</ProBadge>}
|
||||
<Tooltip
|
||||
content={
|
||||
<Text>
|
||||
@@ -417,35 +396,35 @@ const StatusField = ({
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<BindingControl>
|
||||
{allowDynamicData && (
|
||||
<BindingPopover
|
||||
scope={scope}
|
||||
aliases={aliases}
|
||||
variant={isLiteralExpression(value) ? "default" : "bound"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onRemove={(evaluatedValue) =>
|
||||
onChange(JSON.stringify(evaluatedValue ?? ""))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<BindingPopover
|
||||
scope={scope}
|
||||
aliases={aliases}
|
||||
variant={isLiteralExpression(value) ? "default" : "bound"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onRemove={(evaluatedValue) =>
|
||||
onChange(JSON.stringify(evaluatedValue ?? ""))
|
||||
}
|
||||
/>
|
||||
<InputErrorsTooltip errors={errors}>
|
||||
<InputField
|
||||
inputMode="numeric"
|
||||
color={errors && "error"}
|
||||
id={id}
|
||||
placeholder="200"
|
||||
disabled={
|
||||
allowDynamicData === false || isLiteralExpression(value) === false
|
||||
}
|
||||
value={String(computeExpression(value, variableValues))}
|
||||
disabled={isLiteralExpression(value) === false}
|
||||
value={String(computeExpression(value, variableValues) ?? "")}
|
||||
onChange={(event) => {
|
||||
const number = Number(event.target.value);
|
||||
const status =
|
||||
Number.isNaN(number) || String(number) !== event.target.value
|
||||
? event.target.value
|
||||
: number;
|
||||
onChange(JSON.stringify(status));
|
||||
if (event.target.value === "") {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
const number = Number(event.target.value);
|
||||
const status =
|
||||
Number.isNaN(number) || String(number) !== event.target.value
|
||||
? event.target.value
|
||||
: number;
|
||||
onChange(JSON.stringify(status));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</InputErrorsTooltip>
|
||||
@@ -470,6 +449,7 @@ const RedirectField = ({
|
||||
<Grid gap={1}>
|
||||
<Flex align="center" gap={1}>
|
||||
<Label htmlFor={id}>Redirect </Label>
|
||||
{allowDynamicData === false && <ProBadge>PRO</ProBadge>}
|
||||
<Tooltip
|
||||
content="Redirect value can be a path or an expression that returns a path for dynamic response handling."
|
||||
variant="wrapped"
|
||||
@@ -482,26 +462,22 @@ const RedirectField = ({
|
||||
</Flex>
|
||||
|
||||
<BindingControl>
|
||||
{allowDynamicData && (
|
||||
<BindingPopover
|
||||
scope={scope}
|
||||
aliases={aliases}
|
||||
variant={isLiteralExpression(value) ? "default" : "bound"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onRemove={(evaluatedValue) =>
|
||||
onChange(JSON.stringify(evaluatedValue ?? ""))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<BindingPopover
|
||||
scope={scope}
|
||||
aliases={aliases}
|
||||
variant={isLiteralExpression(value) ? "default" : "bound"}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onRemove={(evaluatedValue) =>
|
||||
onChange(JSON.stringify(evaluatedValue ?? ""))
|
||||
}
|
||||
/>
|
||||
<InputErrorsTooltip errors={errors}>
|
||||
<InputField
|
||||
color={errors && "error"}
|
||||
id={id}
|
||||
placeholder="/another-path"
|
||||
disabled={
|
||||
allowDynamicData === false || isLiteralExpression(value) === false
|
||||
}
|
||||
disabled={isLiteralExpression(value) === false}
|
||||
value={String(computeExpression(value, variableValues))}
|
||||
onChange={(event) => onChange(JSON.stringify(event.target.value))}
|
||||
/>
|
||||
@@ -1224,15 +1200,8 @@ export const NewPageSettings = ({
|
||||
...fieldDefaultValues,
|
||||
path: nameToPath(pages, fieldDefaultValues.name),
|
||||
});
|
||||
const userPlanFeatures = useStore($userPlanFeatures);
|
||||
const { variableValues } = useStore($pageRootScope);
|
||||
const errors = validateValues(
|
||||
pages,
|
||||
undefined,
|
||||
values,
|
||||
variableValues,
|
||||
userPlanFeatures
|
||||
);
|
||||
const errors = validateValues(pages, undefined, values, variableValues);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (Object.keys(errors).length === 0) {
|
||||
@@ -1376,7 +1345,7 @@ const updatePage = (pageId: Page["id"], values: Partial<Values>) => {
|
||||
values.language.length > 0 ? values.language : undefined;
|
||||
}
|
||||
|
||||
if (values.status !== undefined) {
|
||||
if ("status" in values) {
|
||||
page.meta.status = values.status;
|
||||
}
|
||||
|
||||
@@ -1514,14 +1483,7 @@ export const PageSettings = ({
|
||||
};
|
||||
|
||||
const { variableValues } = useStore($pageRootScope);
|
||||
const userPlanFeatures = useStore($userPlanFeatures);
|
||||
const errors = validateValues(
|
||||
pages,
|
||||
pageId,
|
||||
values,
|
||||
variableValues,
|
||||
userPlanFeatures
|
||||
);
|
||||
const errors = validateValues(pages, pageId, values, variableValues);
|
||||
|
||||
const debouncedFn = useEffectEvent(() => {
|
||||
if (
|
||||
|
||||
@@ -61,6 +61,9 @@ const validateContactEmail = (
|
||||
}
|
||||
const emails = contactEmail.split(/\s*,\s*/);
|
||||
if (emails.length > maxContactEmails) {
|
||||
if (maxContactEmails === 0) {
|
||||
return `Upgrade to PRO to customize the contact email.`;
|
||||
}
|
||||
return `Only ${maxContactEmails} emails are allowed.`;
|
||||
}
|
||||
if (emails.every((email) => Email.safeParse(email).success) === false) {
|
||||
@@ -170,7 +173,6 @@ export const SectionGeneral = () => {
|
||||
id={contactEmailId}
|
||||
color={contactEmailError ? "error" : undefined}
|
||||
placeholder="john@company.com, jane@company.com"
|
||||
disabled={allowContactEmail === false}
|
||||
autoGrow={true}
|
||||
rows={1}
|
||||
value={meta.contactEmail ?? ""}
|
||||
|
||||
@@ -558,11 +558,10 @@ JsonForm.displayName = "JsonForm";
|
||||
|
||||
const VariablePanel = forwardRef<
|
||||
undefined | PanelApi,
|
||||
{
|
||||
variable?: DataSource;
|
||||
}
|
||||
{ variable?: DataSource }
|
||||
>(({ variable }, ref) => {
|
||||
const resources = useStore($resources);
|
||||
const { allowDynamicData } = useStore($userPlanFeatures);
|
||||
|
||||
const [variableType, setVariableType] = useState<VariableType>(() => {
|
||||
if (variable?.type === "resource") {
|
||||
@@ -588,60 +587,37 @@ const VariablePanel = forwardRef<
|
||||
return "string";
|
||||
});
|
||||
|
||||
let fields: ReactNode;
|
||||
if (variableType === "parameter") {
|
||||
return (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<ParameterForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
fields = <ParameterForm ref={ref} variable={variable} />;
|
||||
}
|
||||
if (variableType === "string") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<StringForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (variableType === "number") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<NumberForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (variableType === "boolean") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<BooleanForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (variableType === "json") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<JsonForm ref={ref} variable={variable} />
|
||||
</>
|
||||
@@ -649,47 +625,70 @@ const VariablePanel = forwardRef<
|
||||
}
|
||||
|
||||
if (variableType === "resource") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<ResourceForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (variableType === "graphql-resource") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<GraphqlResourceForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (variableType === "system-resource") {
|
||||
return (
|
||||
fields = (
|
||||
<>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
<TypeField value={variableType} onChange={setVariableType} />
|
||||
<SystemResourceForm ref={ref} variable={variable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
variableType satisfies never;
|
||||
const isResource =
|
||||
variableType === "resource" ||
|
||||
variableType === "graphql-resource" ||
|
||||
variableType === "system-resource";
|
||||
const requiresUpgrade = allowDynamicData === false && isResource;
|
||||
return (
|
||||
<>
|
||||
{requiresUpgrade && (
|
||||
<PanelBanner>
|
||||
<Text>Resource fetching is part of the CMS functionality.</Text>
|
||||
<Flex align="center" gap={1}>
|
||||
<UpgradeIcon />
|
||||
<Link
|
||||
color="inherit"
|
||||
target="_blank"
|
||||
href="https://webstudio.is/pricing"
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Link>
|
||||
</Flex>
|
||||
</PanelBanner>
|
||||
)}
|
||||
<Flex
|
||||
direction="column"
|
||||
css={{
|
||||
overflow: "hidden",
|
||||
gap: theme.spacing[7],
|
||||
p: theme.panel.padding,
|
||||
}}
|
||||
>
|
||||
<NameField
|
||||
variableId={variable?.id}
|
||||
defaultValue={variable?.name ?? ""}
|
||||
/>
|
||||
{fields}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
});
|
||||
VariablePanel.displayName = "VariablePanel";
|
||||
VariablePanel.displayName = "VariableForm";
|
||||
|
||||
const VariablePopoverContext = createContext<{
|
||||
containerRef?: RefObject<null | HTMLElement>;
|
||||
@@ -733,9 +732,6 @@ export const VariablePopoverTrigger = ({
|
||||
const panelRef = useRef<undefined | PanelApi>(undefined);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const resources = useStore($resources);
|
||||
const { allowDynamicData } = useStore($userPlanFeatures);
|
||||
const [isResource, setIsResource] = useState(variable?.type === "resource");
|
||||
const requiresUpgrade = allowDynamicData === false && isResource;
|
||||
const isSystemVariable = variable?.id === SYSTEM_VARIABLE_ID;
|
||||
|
||||
return (
|
||||
@@ -821,79 +817,48 @@ export const VariablePopoverTrigger = ({
|
||||
width: theme.spacing[30],
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
css={{
|
||||
overflow: "hidden",
|
||||
gap: theme.spacing[7],
|
||||
p: theme.panel.padding,
|
||||
<form
|
||||
ref={formRef}
|
||||
noValidate={true}
|
||||
// exclude from the flow
|
||||
style={{ display: "contents" }}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (isSystemVariable) {
|
||||
return;
|
||||
}
|
||||
const nameElement =
|
||||
event.currentTarget.elements.namedItem("name");
|
||||
// make sure only name is valid and allow to save everything else
|
||||
// to avoid loosing complex configuration when closed accidentally
|
||||
if (
|
||||
nameElement instanceof HTMLInputElement &&
|
||||
nameElement.checkValidity()
|
||||
) {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
panelRef.current?.save(formData);
|
||||
// close popover whenever new variable is created
|
||||
// to prevent creating duplicated variable
|
||||
if (variable === undefined) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{requiresUpgrade && (
|
||||
<PanelBanner>
|
||||
<Text>Resource fetching is part of the CMS functionality.</Text>
|
||||
<Flex align="center" gap={1}>
|
||||
<UpgradeIcon />
|
||||
<Link
|
||||
color="inherit"
|
||||
target="_blank"
|
||||
href="https://webstudio.is/pricing"
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Link>
|
||||
</Flex>
|
||||
</PanelBanner>
|
||||
)}
|
||||
<form
|
||||
ref={formRef}
|
||||
noValidate={true}
|
||||
// exclude from the flow
|
||||
{/* submit is not triggered when press enter on input without submit button */}
|
||||
<button hidden></button>
|
||||
<fieldset
|
||||
style={{ display: "contents" }}
|
||||
onChange={(event) => {
|
||||
const { name, value } = event.target as HTMLSelectElement;
|
||||
// When type is changing, we need to show the upgrade banner.
|
||||
if (name === "type") {
|
||||
setIsResource(value.includes("resource"));
|
||||
}
|
||||
}}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (requiresUpgrade || isSystemVariable) {
|
||||
return;
|
||||
}
|
||||
const nameElement =
|
||||
event.currentTarget.elements.namedItem("name");
|
||||
// make sure only name is valid and allow to save everything else
|
||||
// to avoid loosing complex configuration when closed accidentally
|
||||
if (
|
||||
nameElement instanceof HTMLInputElement &&
|
||||
nameElement.checkValidity()
|
||||
) {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
panelRef.current?.save(formData);
|
||||
// close popover whenever new variable is created
|
||||
// to prevent creating duplicated variable
|
||||
if (variable === undefined) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
// forbid editing system variable
|
||||
disabled={isSystemVariable}
|
||||
>
|
||||
{/* submit is not triggered when press enter on input without submit button */}
|
||||
<button hidden></button>
|
||||
<fieldset
|
||||
style={{ display: "contents" }}
|
||||
// forbid editing system variable
|
||||
disabled={isSystemVariable}
|
||||
<BindingPopoverProvider
|
||||
value={{ containerRef: bindingPopoverContainerRef }}
|
||||
>
|
||||
<BindingPopoverProvider
|
||||
value={{ containerRef: bindingPopoverContainerRef }}
|
||||
>
|
||||
<VariablePanel ref={panelRef} variable={variable} />
|
||||
</BindingPopoverProvider>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Flex>
|
||||
<VariablePanel ref={panelRef} variable={variable} />
|
||||
</BindingPopoverProvider>
|
||||
</fieldset>
|
||||
</form>
|
||||
</ScrollArea>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -235,6 +235,18 @@ const $usedProFeatures = computed(
|
||||
instanceSelector: [page.rootInstanceId],
|
||||
});
|
||||
}
|
||||
if (page.meta.status && page.meta.status !== `200`) {
|
||||
features.set("Page status code", {
|
||||
pageId: page.id,
|
||||
instanceSelector: [page.rootInstanceId],
|
||||
});
|
||||
}
|
||||
if (page.meta.redirect && page.meta.redirect !== `""`) {
|
||||
features.set("Redirect", {
|
||||
pageId: page.id,
|
||||
instanceSelector: [page.rootInstanceId],
|
||||
});
|
||||
}
|
||||
}
|
||||
// has resource variables
|
||||
for (const dataSource of dataSources.values()) {
|
||||
|
||||
@@ -229,6 +229,7 @@ test("check simple literals", () => {
|
||||
expect(isLiteralExpression(`true`)).toEqual(true);
|
||||
expect(isLiteralExpression(`[]`)).toEqual(true);
|
||||
expect(isLiteralExpression(`{}`)).toEqual(true);
|
||||
expect(isLiteralExpression(`undefined`)).toEqual(true);
|
||||
expect(isLiteralExpression(`"" + ""`)).toEqual(false);
|
||||
expect(isLiteralExpression(`{}.field`)).toEqual(false);
|
||||
expect(isLiteralExpression(`variable`)).toEqual(false);
|
||||
|
||||
@@ -138,6 +138,9 @@ export const lintExpression = ({
|
||||
};
|
||||
|
||||
const isLiteralNode = (node: Expression): boolean => {
|
||||
if (node.type === "Identifier" && node.name === "undefined") {
|
||||
return true;
|
||||
}
|
||||
if (node.type === "Literal") {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user