Skip to content

Commit bb9524e

Browse files
committedJan 30, 2025·
feat: unset variables when create or rename data variables
Ref #4768 Now user can write expressions with not existing variables and create these variables later. They will automatically traverse all expressions and bind to newly created variable by name. Additionally user will see suggestions with all unset variables in subtree.
1 parent a0189c1 commit bb9524e

File tree

6 files changed

+646
-173
lines changed

6 files changed

+646
-173
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

+75-22
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 (
@@ -113,10 +137,13 @@ const NameField = ({
113137
useEffect(() => {
114138
ref.current?.setCustomValidity(validateName(defaultValue));
115139
}, [defaultValue, validateName]);
140+
const [value, setValue] = useState(defaultValue);
116141
return (
117142
<Grid gap={1}>
118143
<Label htmlFor={nameId}>Name</Label>
119144
<InputErrorsTooltip errors={error ? [error] : undefined}>
145+
{/* @todo autocomplete unset variables */}
146+
{/*
120147
<InputField
121148
inputRef={ref}
122149
name="name"
@@ -131,6 +158,28 @@ const NameField = ({
131158
onBlur={() => ref.current?.checkValidity()}
132159
onInvalid={(event) => setError(event.currentTarget.validationMessage)}
133160
/>
161+
*/}
162+
<Combobox<string>
163+
inputRef={ref}
164+
name="name"
165+
id={nameId}
166+
color={error ? "error" : undefined}
167+
itemToString={(item) => item ?? ""}
168+
getItems={() => unsetVariableNames}
169+
value={value}
170+
onItemSelect={(newValue) => {
171+
ref.current?.setCustomValidity(validateName(newValue));
172+
setValue(newValue);
173+
setError("");
174+
}}
175+
onChange={(newValue = "") => {
176+
ref.current?.setCustomValidity(validateName(newValue));
177+
setValue(newValue);
178+
setError("");
179+
}}
180+
onBlur={() => ref.current?.checkValidity()}
181+
onInvalid={(event) => setError(event.currentTarget.validationMessage)}
182+
/>
134183
</InputErrorsTooltip>
135184
</Grid>
136185
);
@@ -247,13 +296,18 @@ const ParameterForm = forwardRef<
247296
>(({ variable }, ref) => {
248297
useImperativeHandle(ref, () => ({
249298
save: (formData) => {
299+
const instancePath = $selectedInstancePath.get();
300+
if (instancePath === undefined) {
301+
return;
302+
}
250303
// only existing parameter variables can be renamed
251304
if (variable === undefined) {
252305
return;
253306
}
254307
const name = z.string().parse(formData.get("name"));
255-
serverSyncStore.createTransaction([$dataSources], (dataSources) => {
256-
dataSources.set(variable.id, { ...variable, name });
308+
updateWebstudioData((data) => {
309+
data.dataSources.set(variable.id, { ...variable, name });
310+
restoreTreeVariablesMutable({ instancePath, ...data });
257311
});
258312
},
259313
}));
@@ -272,30 +326,29 @@ const useValuePanelRef = ({
272326
}) => {
273327
useImperativeHandle(ref, () => ({
274328
save: (formData) => {
275-
const instanceId = $selectedInstance.get()?.id;
276-
if (instanceId === undefined) {
329+
const instancePath = $selectedInstancePath.get();
330+
if (instancePath === undefined) {
277331
return;
278332
}
333+
const [{ instance: selectedInstance }] = instancePath;
279334
const dataSourceId = variable?.id ?? nanoid();
280335
// preserve existing instance scope when edit
281-
const scopeInstanceId = variable?.scopeInstanceId ?? instanceId;
336+
const scopeInstanceId = variable?.scopeInstanceId ?? selectedInstance.id;
282337
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-
});
338+
updateWebstudioData((data) => {
339+
// cleanup resource when value variable is set
340+
if (variable?.type === "resource") {
341+
data.resources.delete(variable.resourceId);
297342
}
298-
);
343+
data.dataSources.set(dataSourceId, {
344+
id: dataSourceId,
345+
scopeInstanceId,
346+
name,
347+
type: "variable",
348+
value: variableValue,
349+
});
350+
restoreTreeVariablesMutable({ instancePath, ...data });
351+
});
299352
},
300353
}));
301354
};

0 commit comments

Comments
 (0)