Skip to content

Commit 2af2f25

Browse files
committed
feat: allow deleting variables used in expressions
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.
1 parent 9e710f4 commit 2af2f25

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,12 @@ import {
66
css,
77
CssValueListArrowFocus,
88
CssValueListItem,
9+
Dialog,
10+
DialogActions,
11+
DialogClose,
12+
DialogContent,
13+
DialogDescription,
14+
DialogTitle,
915
DropdownMenu,
1016
DropdownMenuContent,
1117
DropdownMenuItem,
@@ -32,7 +38,6 @@ import {
3238
$resources,
3339
$variableValuesByInstanceSelector,
3440
} from "~/shared/nano-states";
35-
import { serverSyncStore } from "~/shared/sync";
3641
import {
3742
CollapsibleSectionRoot,
3843
useOpenState,
@@ -51,6 +56,8 @@ import {
5156
$selectedInstancePath,
5257
$selectedPage,
5358
} from "~/shared/awareness";
59+
import { updateWebstudioData } from "~/shared/instance-utils";
60+
import { deleteVariableMutable } from "~/shared/data-variables";
5461

5562
/**
5663
* find variables defined specifically on this selected instance
@@ -157,22 +164,6 @@ const $usedVariables = computed(
157164
}
158165
);
159166

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-
176167
const EmptyVariables = () => {
177168
return (
178169
<Flex direction="column" gap="2">
@@ -213,6 +204,7 @@ const VariablesItem = ({
213204
}) => {
214205
const [inspectDialogOpen, setInspectDialogOpen] = useState(false);
215206
const [isMenuOpen, setIsMenuOpen] = useState(false);
207+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
216208
return (
217209
<VariablePopoverTrigger key={variable.id} variable={variable}>
218210
<CssValueListItem
@@ -243,6 +235,7 @@ const VariablesItem = ({
243235
>
244236
{undefined}
245237
</ValuePreviewDialog>
238+
246239
<DropdownMenu modal onOpenChange={setIsMenuOpen}>
247240
<DropdownMenuTrigger asChild>
248241
{/* a11y is completely broken here
@@ -263,17 +256,52 @@ const VariablesItem = ({
263256
<DropdownMenuItem onSelect={() => setInspectDialogOpen(true)}>
264257
Inspect
265258
</DropdownMenuItem>
266-
{source === "local" && (
259+
{source === "local" && variable.type !== "parameter" && (
267260
<DropdownMenuItem
268-
// allow to delete only unused variables
269-
disabled={variable.type === "parameter" || usageCount > 0}
270-
onSelect={() => deleteVariable(variable.id)}
261+
onSelect={() => {
262+
if (usageCount > 0) {
263+
setIsDeleteDialogOpen(true);
264+
} else {
265+
updateWebstudioData((data) => {
266+
deleteVariableMutable(data, variable.id);
267+
});
268+
}
269+
}}
271270
>
272271
Delete {usageCount > 0 && `(${usageCount} bindings)`}
273272
</DropdownMenuItem>
274273
)}
275274
</DropdownMenuContent>
276275
</DropdownMenu>
276+
277+
<Dialog
278+
open={isDeleteDialogOpen}
279+
onOpenChange={setIsDeleteDialogOpen}
280+
>
281+
<DialogContent>
282+
<DialogTitle>Do you want to delete this variable?</DialogTitle>
283+
<DialogDescription
284+
className={css({
285+
paddingInline: theme.panel.paddingInline,
286+
}).toString()}
287+
>
288+
This variable is used in {usageCount}{" "}
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)