Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use native validation for parameter form #3349

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
144 changes: 88 additions & 56 deletions apps/builder/app/builder/features/settings-panel/variable-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
type ReactNode,
type Ref,
type RefObject,
type ForwardedRef,
forwardRef,
useId,
useState,
useImperativeHandle,
useRef,
createContext,
useContext,
useCallback,
} from "react";
import { mergeRefs } from "@react-aria/utils";
import { CopyIcon, RefreshIcon } from "@webstudio-is/icons";
Expand All @@ -22,6 +24,7 @@ import {
FloatingPanelPopoverContent,
FloatingPanelPopoverTitle,
FloatingPanelPopoverTrigger,
Grid,
InputErrorsTooltip,
InputField,
Label,
Expand Down Expand Up @@ -53,6 +56,9 @@ import {
type Field,
composeFields,
type ComposedFields,
useFormField,
Form,
checkCanRequestSubmit,
} from "~/shared/form-utils";
import { $userPlanFeatures } from "~/builder/shared/nano-states";
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
Expand All @@ -65,6 +71,56 @@ import {
import { ResourceForm, SystemResourceForm } from "./resource-panel";
import { generateCurl } from "./curl";

const NameField = ({ defaultValue }: { defaultValue: string }) => {
const nameId = useId();
const { ref, error, props } = useFormField({
defaultValue,
validate: useCallback(
(value: string) => (value.trim().length === 0 ? "Name is required" : ""),
[]
),
});
return (
<Grid gap={1}>
<Label htmlFor={nameId}>Name</Label>
<InputErrorsTooltip errors={error ? [error] : undefined}>
<InputField
inputRef={ref}
name="name"
id={nameId}
color={error ? "error" : undefined}
defaultValue={defaultValue}
{...props}
/>
</InputErrorsTooltip>
</Grid>
);
};

const ParameterForm = forwardRef<HTMLFormElement, { variable?: DataSource }>(
({ variable }, ref) => {
return (
<Form
ref={ref}
onSubmit={(event) => {
const formData = new FormData(event.currentTarget);
const name = String(formData.get("name"));
// only existing parameter variables can be renamed
if (variable === undefined) {
return;
}
serverSyncStore.createTransaction([$dataSources], (dataSources) => {
dataSources.set(variable.id, { ...variable, name });
});
}}
>
<NameField defaultValue={variable?.name ?? ""} />
</Form>
);
}
);
ParameterForm.displayName = "ParameterForm";

/**
* convert value expression to js value
* validating out accessing any identifier
Expand Down Expand Up @@ -115,28 +171,6 @@ type PanelApi = ComposedFields & {
save: () => void;
};

const ParameterForm = forwardRef<
undefined | PanelApi,
{ variable?: DataSource; nameField: Field<string> }
>(({ variable, nameField }, ref) => {
const form = composeFields(nameField);
useImperativeHandle(ref, () => ({
...form,
save: () => {
// only existing parameter variables can be renamed
if (variable === undefined) {
return;
}
const name = nameField.value;
serverSyncStore.createTransaction([$dataSources], (dataSources) => {
dataSources.set(variable.id, { ...variable, name });
});
},
}));
return <></>;
});
ParameterForm.displayName = "ParameterForm";

const useValuePanelRef = ({
ref,
variable,
Expand Down Expand Up @@ -356,9 +390,11 @@ JsonForm.displayName = "JsonForm";
const VariablePanel = forwardRef<
undefined | PanelApi,
{
formRef: ForwardedRef<HTMLFormElement>;
variable?: DataSource;
onSubmit: () => void;
}
>(({ variable }, ref) => {
>(({ formRef, variable, onSubmit }, ref) => {
const { allowDynamicData } = useStore($userPlanFeatures);
const resources = useStore($resources);

Expand Down Expand Up @@ -474,71 +510,66 @@ const VariablePanel = forwardRef<
);

if (variableType === "parameter") {
return (
<>
{nameFieldElement}
<ParameterForm ref={ref} variable={variable} nameField={nameField} />
</>
);
return <ParameterForm ref={formRef} variable={variable} />;
}
if (variableType === "string") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<StringForm ref={ref} variable={variable} nameField={nameField} />
</>
</Form>
);
}
if (variableType === "number") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<NumberForm ref={ref} variable={variable} nameField={nameField} />
</>
</Form>
);
}
if (variableType === "boolean") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<BooleanForm ref={ref} variable={variable} nameField={nameField} />
</>
</Form>
);
}
if (variableType === "json") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<JsonForm ref={ref} variable={variable} nameField={nameField} />
</>
</Form>
);
}

if (variableType === "resource") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<ResourceForm ref={ref} variable={variable} nameField={nameField} />
</>
</Form>
);
}

if (variableType === "system-resource") {
return (
<>
<Form onSubmit={onSubmit}>
{nameFieldElement}
{typeFieldElement}
<SystemResourceForm
ref={ref}
variable={variable}
nameField={nameField}
/>
</>
</Form>
);
}

Expand All @@ -563,8 +594,15 @@ export const VariablePopoverTrigger = forwardRef<
const bindingPopoverContainerRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<undefined | PanelApi>();
const resources = useStore($resources);
const form = useRef<HTMLFormElement>(null);

const saveAndClose = () => {
if (form.current) {
if (checkCanRequestSubmit(form.current) === false) {
return;
}
form.current.requestSubmit();
}
if (panelRef.current) {
if (panelRef.current.allErrorsVisible === false) {
panelRef.current.showAllErrors();
Expand Down Expand Up @@ -615,22 +653,16 @@ export const VariablePopoverTrigger = forwardRef<
pb: theme.spacing[9],
}}
>
<form
// exclude from the flow
style={{ display: "contents" }}
onSubmit={(event) => {
event.preventDefault();
saveAndClose();
}}
<BindingPopoverProvider
value={{ containerRef: bindingPopoverContainerRef }}
>
{/* submit is not triggered when press enter on input without submit button */}
<button style={{ display: "none" }}>submit</button>
<BindingPopoverProvider
value={{ containerRef: bindingPopoverContainerRef }}
>
<VariablePanel ref={panelRef} variable={variable} />
</BindingPopoverProvider>
</form>
<VariablePanel
ref={panelRef}
variable={variable}
formRef={form}
onSubmit={saveAndClose}
/>
</BindingPopoverProvider>
</Flex>
</ScrollArea>
{/* put after content to avoid auto focusing "Close" button */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
$selectedInstanceSelector,
$selectedPageId,
$instances,
$dataSources,
} from "~/shared/nano-states";
import { registerContainers } from "~/shared/sync";
import { createDefaultPages } from "@webstudio-is/project-build";
Expand All @@ -26,6 +27,19 @@ $selectedPageId.set("home");
$pages.set(
createDefaultPages({ rootInstanceId: "root", systemDataSourceId: "system" })
);
$dataSources.set(
new Map([
[
"systemId",
{
id: "systemId",
scopeInstanceId: "root",
name: "system",
type: "parameter",
},
],
])
);

export const VariablesSection: StoryObj = {
render: () => (
Expand Down
61 changes: 0 additions & 61 deletions apps/builder/app/shared/form-utils/form-utils.ts

This file was deleted.