Skip to content

Commit 53ace3b

Browse files
authored
feat: unset variables when create or rename data variables (#4808)
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" />
1 parent b72a74c commit 53ace3b

File tree

6 files changed

+649
-182
lines changed

6 files changed

+649
-182
lines changed

apps/builder/app/builder/features/settings-panel/resource-panel.tsx

+30-32
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
import { TrashIcon, InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
3737
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
3838
import { humanizeString } from "~/shared/string-utils";
39-
import { serverSyncStore } from "~/shared/sync";
4039
import {
4140
$dataSources,
4241
$resources,
@@ -55,10 +54,12 @@ import {
5554
} from "~/builder/shared/code-editor-base";
5655
import { parseCurl, type CurlRequest } from "./curl";
5756
import {
58-
$selectedInstance,
5957
$selectedInstanceKey,
58+
$selectedInstancePath,
6059
$selectedPage,
6160
} from "~/shared/awareness";
61+
import { updateWebstudioData } from "~/shared/instance-utils";
62+
import { restoreTreeVariablesMutable } from "~/shared/data-variables";
6263

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

583584
useImperativeHandle(ref, () => ({
584585
save: (formData) => {
585-
const instanceId = $selectedInstance.get()?.id;
586-
if (instanceId === undefined) {
586+
const instancePath = $selectedInstancePath.get();
587+
if (instancePath === undefined) {
587588
return;
588589
}
590+
const [{ instance }] = instancePath;
589591
const name = z.string().parse(formData.get("name"));
590592
const newResource: Resource = {
591593
id: resource?.id ?? nanoid(),
@@ -598,18 +600,16 @@ export const ResourceForm = forwardRef<
598600
const newVariable: DataSource = {
599601
id: variable?.id ?? nanoid(),
600602
// preserve existing instance scope when edit
601-
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
603+
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
602604
name,
603605
type: "resource",
604606
resourceId: newResource.id,
605607
};
606-
serverSyncStore.createTransaction(
607-
[$dataSources, $resources],
608-
(dataSources, resources) => {
609-
dataSources.set(newVariable.id, newVariable);
610-
resources.set(newResource.id, newResource);
611-
}
612-
);
608+
updateWebstudioData((data) => {
609+
data.dataSources.set(newVariable.id, newVariable);
610+
data.resources.set(newResource.id, newResource);
611+
restoreTreeVariablesMutable({ instancePath, ...data });
612+
});
613613
},
614614
}));
615615

@@ -715,10 +715,11 @@ export const SystemResourceForm = forwardRef<
715715

716716
useImperativeHandle(ref, () => ({
717717
save: (formData) => {
718-
const instanceId = $selectedInstance.get()?.id;
719-
if (instanceId === undefined) {
718+
const instancePath = $selectedInstancePath.get();
719+
if (instancePath === undefined) {
720720
return;
721721
}
722+
const [{ instance }] = instancePath;
722723
const name = z.string().parse(formData.get("name"));
723724
const newResource: Resource = {
724725
id: resource?.id ?? nanoid(),
@@ -731,18 +732,16 @@ export const SystemResourceForm = forwardRef<
731732
const newVariable: DataSource = {
732733
id: variable?.id ?? nanoid(),
733734
// preserve existing instance scope when edit
734-
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
735+
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
735736
name,
736737
type: "resource",
737738
resourceId: newResource.id,
738739
};
739-
serverSyncStore.createTransaction(
740-
[$dataSources, $resources],
741-
(dataSources, resources) => {
742-
dataSources.set(newVariable.id, newVariable);
743-
resources.set(newResource.id, newResource);
744-
}
745-
);
740+
updateWebstudioData((data) => {
741+
data.dataSources.set(newVariable.id, newVariable);
742+
data.resources.set(newResource.id, newResource);
743+
restoreTreeVariablesMutable({ instancePath, ...data });
744+
});
746745
},
747746
}));
748747

@@ -825,10 +824,11 @@ export const GraphqlResourceForm = forwardRef<
825824

826825
useImperativeHandle(ref, () => ({
827826
save: (formData) => {
828-
const instanceId = $selectedInstance.get()?.id;
829-
if (instanceId === undefined) {
827+
const instancePath = $selectedInstancePath.get();
828+
if (instancePath === undefined) {
830829
return;
831830
}
831+
const [{ instance }] = instancePath;
832832
const name = z.string().parse(formData.get("name"));
833833
const body = generateObjectExpression(
834834
new Map([
@@ -848,18 +848,16 @@ export const GraphqlResourceForm = forwardRef<
848848
const newVariable: DataSource = {
849849
id: variable?.id ?? nanoid(),
850850
// preserve existing instance scope when edit
851-
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
851+
scopeInstanceId: variable?.scopeInstanceId ?? instance.id,
852852
name,
853853
type: "resource",
854854
resourceId: newResource.id,
855855
};
856-
serverSyncStore.createTransaction(
857-
[$dataSources, $resources],
858-
(dataSources, resources) => {
859-
dataSources.set(newVariable.id, newVariable);
860-
resources.set(newResource.id, newResource);
861-
}
862-
);
856+
updateWebstudioData((data) => {
857+
data.dataSources.set(newVariable.id, newVariable);
858+
data.resources.set(newResource.id, newResource);
859+
restoreTreeVariablesMutable({ instancePath, ...data });
860+
});
863861
},
864862
}));
865863

apps/builder/app/builder/features/settings-panel/variable-popover.tsx

+74-29
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { CopyIcon, RefreshIcon, UpgradeIcon } from "@webstudio-is/icons";
1919
import {
2020
Box,
2121
Button,
22+
Combobox,
2223
DialogClose,
2324
DialogTitle,
2425
DialogTitleActions,
@@ -55,9 +56,10 @@ import {
5556
invalidateResource,
5657
getComputedResource,
5758
$userPlanFeatures,
59+
$instances,
60+
$props,
5861
} from "~/shared/nano-states";
59-
import { serverSyncStore } from "~/shared/sync";
60-
import { $selectedInstance } from "~/shared/awareness";
62+
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
6163
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
6264
import {
6365
EditorDialog,
@@ -70,6 +72,11 @@ import {
7072
SystemResourceForm,
7173
} from "./resource-panel";
7274
import { generateCurl } from "./curl";
75+
import { updateWebstudioData } from "~/shared/instance-utils";
76+
import {
77+
findUnsetVariableNames,
78+
restoreTreeVariablesMutable,
79+
} from "~/shared/data-variables";
7380

7481
const $variablesByName = computed(
7582
[$selectedInstance, $dataSources],
@@ -84,6 +91,22 @@ const $variablesByName = computed(
8491
}
8592
);
8693

94+
const $unsetVariableNames = computed(
95+
[$selectedInstancePath, $instances, $props, $dataSources, $resources],
96+
(instancePath, instances, props, dataSources, resources) => {
97+
if (instancePath === undefined) {
98+
return [];
99+
}
100+
return findUnsetVariableNames({
101+
instancePath,
102+
instances,
103+
props,
104+
dataSources,
105+
resources,
106+
});
107+
}
108+
);
109+
87110
const NameField = ({
88111
variableId,
89112
defaultValue,
@@ -95,6 +118,7 @@ const NameField = ({
95118
const [error, setError] = useState("");
96119
const nameId = useId();
97120
const variablesByName = useStore($variablesByName);
121+
const unsetVariableNames = useStore($unsetVariableNames);
98122
const validateName = useCallback(
99123
(value: string) => {
100124
if (
@@ -110,22 +134,39 @@ const NameField = ({
110134
},
111135
[variablesByName, variableId]
112136
);
137+
const [value, setValue] = useState(defaultValue);
113138
useEffect(() => {
114-
ref.current?.setCustomValidity(validateName(defaultValue));
115-
}, [defaultValue, validateName]);
139+
ref.current?.setCustomValidity(validateName(value));
140+
}, [value, validateName]);
116141
return (
117142
<Grid gap={1}>
118143
<Label htmlFor={nameId}>Name</Label>
119144
<InputErrorsTooltip errors={error ? [error] : undefined}>
120-
<InputField
145+
<Combobox<string>
121146
inputRef={ref}
122147
name="name"
123148
id={nameId}
124-
autoComplete="off"
125149
color={error ? "error" : undefined}
126-
defaultValue={defaultValue}
127-
onChange={(event) => {
128-
event.target.setCustomValidity(validateName(event.target.value));
150+
itemToString={(item) => item ?? ""}
151+
getDescription={() => (
152+
<>
153+
Enter a new variable or select
154+
<br />
155+
a variable that has been used
156+
<br />
157+
in expressions but not yet created
158+
</>
159+
)}
160+
getItems={() => unsetVariableNames}
161+
value={value}
162+
onItemSelect={(newValue) => {
163+
ref.current?.setCustomValidity(validateName(newValue));
164+
setValue(newValue);
165+
setError("");
166+
}}
167+
onChange={(newValue = "") => {
168+
ref.current?.setCustomValidity(validateName(newValue));
169+
setValue(newValue);
129170
setError("");
130171
}}
131172
onBlur={() => ref.current?.checkValidity()}
@@ -247,13 +288,18 @@ const ParameterForm = forwardRef<
247288
>(({ variable }, ref) => {
248289
useImperativeHandle(ref, () => ({
249290
save: (formData) => {
291+
const instancePath = $selectedInstancePath.get();
292+
if (instancePath === undefined) {
293+
return;
294+
}
250295
// only existing parameter variables can be renamed
251296
if (variable === undefined) {
252297
return;
253298
}
254299
const name = z.string().parse(formData.get("name"));
255-
serverSyncStore.createTransaction([$dataSources], (dataSources) => {
256-
dataSources.set(variable.id, { ...variable, name });
300+
updateWebstudioData((data) => {
301+
data.dataSources.set(variable.id, { ...variable, name });
302+
restoreTreeVariablesMutable({ instancePath, ...data });
257303
});
258304
},
259305
}));
@@ -272,30 +318,29 @@ const useValuePanelRef = ({
272318
}) => {
273319
useImperativeHandle(ref, () => ({
274320
save: (formData) => {
275-
const instanceId = $selectedInstance.get()?.id;
276-
if (instanceId === undefined) {
321+
const instancePath = $selectedInstancePath.get();
322+
if (instancePath === undefined) {
277323
return;
278324
}
325+
const [{ instance: selectedInstance }] = instancePath;
279326
const dataSourceId = variable?.id ?? nanoid();
280327
// preserve existing instance scope when edit
281-
const scopeInstanceId = variable?.scopeInstanceId ?? instanceId;
328+
const scopeInstanceId = variable?.scopeInstanceId ?? selectedInstance.id;
282329
const name = z.string().parse(formData.get("name"));
283-
serverSyncStore.createTransaction(
284-
[$dataSources, $resources],
285-
(dataSources, resources) => {
286-
// cleanup resource when value variable is set
287-
if (variable?.type === "resource") {
288-
resources.delete(variable.resourceId);
289-
}
290-
dataSources.set(dataSourceId, {
291-
id: dataSourceId,
292-
scopeInstanceId,
293-
name,
294-
type: "variable",
295-
value: variableValue,
296-
});
330+
updateWebstudioData((data) => {
331+
// cleanup resource when value variable is set
332+
if (variable?.type === "resource") {
333+
data.resources.delete(variable.resourceId);
297334
}
298-
);
335+
data.dataSources.set(dataSourceId, {
336+
id: dataSourceId,
337+
scopeInstanceId,
338+
name,
339+
type: "variable",
340+
value: variableValue,
341+
});
342+
restoreTreeVariablesMutable({ instancePath, ...data });
343+
});
299344
},
300345
}));
301346
};

0 commit comments

Comments
 (0)