Skip to content

Commit

Permalink
feat: unset variables when create or rename data variables (#4808)
Browse files Browse the repository at this point in the history
Ref #4768

Now user can write expressions with not existing variables and create
these variables later. Builder will automatically traverse all
expressions and bind to newly created variable by name.

Additionally user will see suggestions with all unset variables in
subtree.

<img width="504" alt="Screenshot 2025-01-30 at 12 35 21"
src="https://github.com/user-attachments/assets/6d042f45-ca4a-49b9-9259-7be2a3c74bdd"
/>
  • Loading branch information
TrySound authored Jan 31, 2025
1 parent b72a74c commit 53ace3b
Show file tree
Hide file tree
Showing 6 changed files with 649 additions and 182 deletions.
62 changes: 30 additions & 32 deletions apps/builder/app/builder/features/settings-panel/resource-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
import { TrashIcon, InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import { humanizeString } from "~/shared/string-utils";
import { serverSyncStore } from "~/shared/sync";
import {
$dataSources,
$resources,
Expand All @@ -55,10 +54,12 @@ import {
} from "~/builder/shared/code-editor-base";
import { parseCurl, type CurlRequest } from "./curl";
import {
$selectedInstance,
$selectedInstanceKey,
$selectedInstancePath,
$selectedPage,
} from "~/shared/awareness";
import { updateWebstudioData } from "~/shared/instance-utils";
import { restoreTreeVariablesMutable } from "~/shared/data-variables";

const validateUrl = (value: string, scope: Record<string, unknown>) => {
const evaluatedValue = evaluateExpressionWithinScope(value, scope);
Expand Down Expand Up @@ -582,10 +583,11 @@ export const ResourceForm = forwardRef<

useImperativeHandle(ref, () => ({
save: (formData) => {
const instanceId = $selectedInstance.get()?.id;
if (instanceId === undefined) {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return;
}
const [{ instance }] = instancePath;
const name = z.string().parse(formData.get("name"));
const newResource: Resource = {
id: resource?.id ?? nanoid(),
Expand All @@ -598,18 +600,16 @@ export const ResourceForm = forwardRef<
const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
// preserve existing instance scope when edit
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
name,
type: "resource",
resourceId: newResource.id,
};
serverSyncStore.createTransaction(
[$dataSources, $resources],
(dataSources, resources) => {
dataSources.set(newVariable.id, newVariable);
resources.set(newResource.id, newResource);
}
);
updateWebstudioData((data) => {
data.dataSources.set(newVariable.id, newVariable);
data.resources.set(newResource.id, newResource);
restoreTreeVariablesMutable({ instancePath, ...data });
});
},
}));

Expand Down Expand Up @@ -715,10 +715,11 @@ export const SystemResourceForm = forwardRef<

useImperativeHandle(ref, () => ({
save: (formData) => {
const instanceId = $selectedInstance.get()?.id;
if (instanceId === undefined) {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return;
}
const [{ instance }] = instancePath;
const name = z.string().parse(formData.get("name"));
const newResource: Resource = {
id: resource?.id ?? nanoid(),
Expand All @@ -731,18 +732,16 @@ export const SystemResourceForm = forwardRef<
const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
// preserve existing instance scope when edit
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
name,
type: "resource",
resourceId: newResource.id,
};
serverSyncStore.createTransaction(
[$dataSources, $resources],
(dataSources, resources) => {
dataSources.set(newVariable.id, newVariable);
resources.set(newResource.id, newResource);
}
);
updateWebstudioData((data) => {
data.dataSources.set(newVariable.id, newVariable);
data.resources.set(newResource.id, newResource);
restoreTreeVariablesMutable({ instancePath, ...data });
});
},
}));

Expand Down Expand Up @@ -825,10 +824,11 @@ export const GraphqlResourceForm = forwardRef<

useImperativeHandle(ref, () => ({
save: (formData) => {
const instanceId = $selectedInstance.get()?.id;
if (instanceId === undefined) {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return;
}
const [{ instance }] = instancePath;
const name = z.string().parse(formData.get("name"));
const body = generateObjectExpression(
new Map([
Expand All @@ -848,18 +848,16 @@ export const GraphqlResourceForm = forwardRef<
const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
// preserve existing instance scope when edit
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
name,
type: "resource",
resourceId: newResource.id,
};
serverSyncStore.createTransaction(
[$dataSources, $resources],
(dataSources, resources) => {
dataSources.set(newVariable.id, newVariable);
resources.set(newResource.id, newResource);
}
);
updateWebstudioData((data) => {
data.dataSources.set(newVariable.id, newVariable);
data.resources.set(newResource.id, newResource);
restoreTreeVariablesMutable({ instancePath, ...data });
});
},
}));

Expand Down
103 changes: 74 additions & 29 deletions apps/builder/app/builder/features/settings-panel/variable-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CopyIcon, RefreshIcon, UpgradeIcon } from "@webstudio-is/icons";
import {
Box,
Button,
Combobox,
DialogClose,
DialogTitle,
DialogTitleActions,
Expand Down Expand Up @@ -55,9 +56,10 @@ import {
invalidateResource,
getComputedResource,
$userPlanFeatures,
$instances,
$props,
} from "~/shared/nano-states";
import { serverSyncStore } from "~/shared/sync";
import { $selectedInstance } from "~/shared/awareness";
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
import {
EditorDialog,
Expand All @@ -70,6 +72,11 @@ import {
SystemResourceForm,
} from "./resource-panel";
import { generateCurl } from "./curl";
import { updateWebstudioData } from "~/shared/instance-utils";
import {
findUnsetVariableNames,
restoreTreeVariablesMutable,
} from "~/shared/data-variables";

const $variablesByName = computed(
[$selectedInstance, $dataSources],
Expand All @@ -84,6 +91,22 @@ const $variablesByName = computed(
}
);

const $unsetVariableNames = computed(
[$selectedInstancePath, $instances, $props, $dataSources, $resources],
(instancePath, instances, props, dataSources, resources) => {
if (instancePath === undefined) {
return [];
}
return findUnsetVariableNames({
instancePath,
instances,
props,
dataSources,
resources,
});
}
);

const NameField = ({
variableId,
defaultValue,
Expand All @@ -95,6 +118,7 @@ const NameField = ({
const [error, setError] = useState("");
const nameId = useId();
const variablesByName = useStore($variablesByName);
const unsetVariableNames = useStore($unsetVariableNames);
const validateName = useCallback(
(value: string) => {
if (
Expand All @@ -110,22 +134,39 @@ const NameField = ({
},
[variablesByName, variableId]
);
const [value, setValue] = useState(defaultValue);
useEffect(() => {
ref.current?.setCustomValidity(validateName(defaultValue));
}, [defaultValue, validateName]);
ref.current?.setCustomValidity(validateName(value));
}, [value, validateName]);
return (
<Grid gap={1}>
<Label htmlFor={nameId}>Name</Label>
<InputErrorsTooltip errors={error ? [error] : undefined}>
<InputField
<Combobox<string>
inputRef={ref}
name="name"
id={nameId}
autoComplete="off"
color={error ? "error" : undefined}
defaultValue={defaultValue}
onChange={(event) => {
event.target.setCustomValidity(validateName(event.target.value));
itemToString={(item) => item ?? ""}
getDescription={() => (
<>
Enter a new variable or select
<br />
a variable that has been used
<br />
in expressions but not yet created
</>
)}
getItems={() => unsetVariableNames}
value={value}
onItemSelect={(newValue) => {
ref.current?.setCustomValidity(validateName(newValue));
setValue(newValue);
setError("");
}}
onChange={(newValue = "") => {
ref.current?.setCustomValidity(validateName(newValue));
setValue(newValue);
setError("");
}}
onBlur={() => ref.current?.checkValidity()}
Expand Down Expand Up @@ -247,13 +288,18 @@ const ParameterForm = forwardRef<
>(({ variable }, ref) => {
useImperativeHandle(ref, () => ({
save: (formData) => {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return;
}
// only existing parameter variables can be renamed
if (variable === undefined) {
return;
}
const name = z.string().parse(formData.get("name"));
serverSyncStore.createTransaction([$dataSources], (dataSources) => {
dataSources.set(variable.id, { ...variable, name });
updateWebstudioData((data) => {
data.dataSources.set(variable.id, { ...variable, name });
restoreTreeVariablesMutable({ instancePath, ...data });
});
},
}));
Expand All @@ -272,30 +318,29 @@ const useValuePanelRef = ({
}) => {
useImperativeHandle(ref, () => ({
save: (formData) => {
const instanceId = $selectedInstance.get()?.id;
if (instanceId === undefined) {
const instancePath = $selectedInstancePath.get();
if (instancePath === undefined) {
return;
}
const [{ instance: selectedInstance }] = instancePath;
const dataSourceId = variable?.id ?? nanoid();
// preserve existing instance scope when edit
const scopeInstanceId = variable?.scopeInstanceId ?? instanceId;
const scopeInstanceId = variable?.scopeInstanceId ?? selectedInstance.id;
const name = z.string().parse(formData.get("name"));
serverSyncStore.createTransaction(
[$dataSources, $resources],
(dataSources, resources) => {
// cleanup resource when value variable is set
if (variable?.type === "resource") {
resources.delete(variable.resourceId);
}
dataSources.set(dataSourceId, {
id: dataSourceId,
scopeInstanceId,
name,
type: "variable",
value: variableValue,
});
updateWebstudioData((data) => {
// cleanup resource when value variable is set
if (variable?.type === "resource") {
data.resources.delete(variable.resourceId);
}
);
data.dataSources.set(dataSourceId, {
id: dataSourceId,
scopeInstanceId,
name,
type: "variable",
value: variableValue,
});
restoreTreeVariablesMutable({ instancePath, ...data });
});
},
}));
};
Expand Down
Loading

0 comments on commit 53ace3b

Please sign in to comment.