Skip to content

Commit ae864ef

Browse files
authored
feat: allow deleting variables used in expressions (#4860)
Ref #4768 Before we disabled deleting variables which are used in expressions. Now we will ask confirmation and unset this variable. This way user will not be locked if variable is lost and we will show diagnostics (in the future) where all unset variables are. <img width="908" alt="Screenshot 2025-02-12 at 15 38 14" src="https://github.com/user-attachments/assets/ea9eea58-c276-4251-8b95-1214e9380621" />
1 parent ba3c964 commit ae864ef

File tree

3 files changed

+234
-143
lines changed

3 files changed

+234
-143
lines changed

apps/builder/app/builder/features/settings-panel/variables-section.tsx

+49-21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import {
66
css,
77
CssValueListArrowFocus,
88
CssValueListItem,
9+
Dialog,
10+
DialogActions,
11+
DialogContent,
12+
DialogDescription,
13+
DialogTitle,
914
DropdownMenu,
1015
DropdownMenuContent,
1116
DropdownMenuItem,
@@ -32,7 +37,6 @@ import {
3237
$resources,
3338
$variableValuesByInstanceSelector,
3439
} from "~/shared/nano-states";
35-
import { serverSyncStore } from "~/shared/sync";
3640
import {
3741
CollapsibleSectionRoot,
3842
useOpenState,
@@ -51,6 +55,8 @@ import {
5155
$selectedInstancePath,
5256
$selectedPage,
5357
} from "~/shared/awareness";
58+
import { updateWebstudioData } from "~/shared/instance-utils";
59+
import { deleteVariableMutable } from "~/shared/data-variables";
5460

5561
/**
5662
* find variables defined specifically on this selected instance
@@ -157,22 +163,6 @@ const $usedVariables = computed(
157163
}
158164
);
159165

160-
const deleteVariable = (variableId: DataSource["id"]) => {
161-
serverSyncStore.createTransaction(
162-
[$dataSources, $resources],
163-
(dataSources, resources) => {
164-
const dataSource = dataSources.get(variableId);
165-
if (dataSource === undefined) {
166-
return;
167-
}
168-
dataSources.delete(variableId);
169-
if (dataSource.type === "resource") {
170-
resources.delete(dataSource.resourceId);
171-
}
172-
}
173-
);
174-
};
175-
176166
const EmptyVariables = () => {
177167
return (
178168
<Flex direction="column" gap="2">
@@ -213,6 +203,7 @@ const VariablesItem = ({
213203
}) => {
214204
const [inspectDialogOpen, setInspectDialogOpen] = useState(false);
215205
const [isMenuOpen, setIsMenuOpen] = useState(false);
206+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
216207
return (
217208
<VariablePopoverTrigger key={variable.id} variable={variable}>
218209
<CssValueListItem
@@ -243,6 +234,7 @@ const VariablesItem = ({
243234
>
244235
{undefined}
245236
</ValuePreviewDialog>
237+
246238
<DropdownMenu modal onOpenChange={setIsMenuOpen}>
247239
<DropdownMenuTrigger asChild>
248240
{/* a11y is completely broken here
@@ -263,17 +255,53 @@ const VariablesItem = ({
263255
<DropdownMenuItem onSelect={() => setInspectDialogOpen(true)}>
264256
Inspect
265257
</DropdownMenuItem>
266-
{source === "local" && (
258+
{source === "local" && variable.type !== "parameter" && (
267259
<DropdownMenuItem
268-
// allow to delete only unused variables
269-
disabled={variable.type === "parameter" || usageCount > 0}
270-
onSelect={() => deleteVariable(variable.id)}
260+
onSelect={() => {
261+
if (usageCount > 0) {
262+
setIsDeleteDialogOpen(true);
263+
} else {
264+
updateWebstudioData((data) => {
265+
deleteVariableMutable(data, variable.id);
266+
});
267+
}
268+
}}
271269
>
272270
Delete {usageCount > 0 && `(${usageCount} bindings)`}
273271
</DropdownMenuItem>
274272
)}
275273
</DropdownMenuContent>
276274
</DropdownMenu>
275+
276+
<Dialog
277+
open={isDeleteDialogOpen}
278+
onOpenChange={setIsDeleteDialogOpen}
279+
>
280+
<DialogContent>
281+
<DialogTitle>Delete Variable?</DialogTitle>
282+
<DialogDescription
283+
className={css({
284+
paddingInline: theme.panel.paddingInline,
285+
textWrap: "nowrap",
286+
}).toString()}
287+
>
288+
Variable "{variable.name}" is used in {usageCount}&nbsp;
289+
{usageCount === 1 ? "expression" : "expressions"}.
290+
</DialogDescription>
291+
<DialogActions>
292+
<Button
293+
color="destructive"
294+
onClick={() => {
295+
updateWebstudioData((data) => {
296+
deleteVariableMutable(data, variable.id);
297+
});
298+
}}
299+
>
300+
Delete
301+
</Button>
302+
</DialogActions>
303+
</DialogContent>
304+
</Dialog>
277305
</>
278306
}
279307
/>

apps/builder/app/shared/data-variables.test.tsx

+75
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
restoreExpressionVariables,
1717
rebindTreeVariablesMutable,
1818
unsetExpressionVariables,
19+
deleteVariableMutable,
1920
} from "./data-variables";
2021
import { getInstancePath } from "./awareness";
2122

@@ -390,3 +391,77 @@ test("rebind tree variables in resources", () => {
390391
}),
391392
]);
392393
});
394+
395+
test("delete variable and unset it in expressions", () => {
396+
const bodyVariable = new Variable("bodyVariable", "one value of body");
397+
const data = renderData(
398+
<$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}>
399+
<$.Box
400+
ws:id="boxId"
401+
data-action={new ActionValue([], expression`${bodyVariable}`)}
402+
>
403+
{expression`${bodyVariable}`}
404+
</$.Box>
405+
</$.Body>
406+
);
407+
expect(Array.from(data.dataSources.values())).toEqual([
408+
expect.objectContaining({ scopeInstanceId: "bodyId" }),
409+
]);
410+
const [bodyVariableId] = data.dataSources.keys();
411+
deleteVariableMutable(data, bodyVariableId);
412+
expect(Array.from(data.props.values())).toEqual([
413+
expect.objectContaining({ name: "data-body-vars", value: "bodyVariable" }),
414+
expect.objectContaining({
415+
name: "data-action",
416+
value: [{ type: "execute", args: [], code: "bodyVariable" }],
417+
}),
418+
]);
419+
expect(data.instances.get("boxId")?.children).toEqual([
420+
{ type: "expression", value: "bodyVariable" },
421+
]);
422+
});
423+
424+
test("delete variable and unset it in resources", () => {
425+
const bodyVariable = new Variable("bodyVariable", "one value of body");
426+
const resourceVariable = new ResourceValue("resourceVariable", {
427+
url: expression`${bodyVariable}`,
428+
method: "post",
429+
headers: [{ name: "auth", value: expression`${bodyVariable}` }],
430+
body: expression`${bodyVariable}`,
431+
});
432+
const resourceProp = new ResourceValue("resourceProp", {
433+
url: expression`${bodyVariable}`,
434+
method: "post",
435+
headers: [{ name: "auth", value: expression`${bodyVariable}` }],
436+
body: expression`${bodyVariable}`,
437+
});
438+
const data = renderData(
439+
<$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}>
440+
<$.Box
441+
ws:id="boxId"
442+
data-box-vars={expression`${resourceVariable}`}
443+
data-resource={resourceProp}
444+
></$.Box>
445+
</$.Body>
446+
);
447+
expect(Array.from(data.dataSources.values())).toEqual([
448+
expect.objectContaining({ scopeInstanceId: "bodyId" }),
449+
expect.objectContaining({ scopeInstanceId: "boxId" }),
450+
]);
451+
const [bodyVariableId] = data.dataSources.keys();
452+
deleteVariableMutable(data, bodyVariableId);
453+
expect(Array.from(data.resources.values())).toEqual([
454+
expect.objectContaining({
455+
url: "bodyVariable",
456+
method: "post",
457+
headers: [{ name: "auth", value: "bodyVariable" }],
458+
body: "bodyVariable",
459+
}),
460+
expect.objectContaining({
461+
url: "bodyVariable",
462+
method: "post",
463+
headers: [{ name: "auth", value: "bodyVariable" }],
464+
body: "bodyVariable",
465+
}),
466+
]);
467+
});

0 commit comments

Comments
 (0)