Skip to content

Commit 23034da

Browse files
authored
feat: add global system variable in builder (#4896)
Closes #4166 Now system is global variable and can be accessed in slots. Legacy one can be deleted <img width="264" alt="image" src="https://github.com/user-attachments/assets/1c1d60f5-6be2-4871-9970-3593e3e7c08a" />
1 parent 2127108 commit 23034da

18 files changed

+479
-414
lines changed

apps/builder/app/builder/features/pages/page-settings.tsx

+2-13
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ import {
7171
$assets,
7272
$instances,
7373
$pages,
74-
$dataSources,
7574
$publishedOrigin,
7675
$project,
7776
$userPlanFeatures,
@@ -1365,36 +1364,26 @@ const NewPageSettingsView = ({
13651364

13661365
const createPage = (pageId: Page["id"], values: Values) => {
13671366
serverSyncStore.createTransaction(
1368-
[$pages, $instances, $dataSources],
1369-
(pages, instances, dataSources) => {
1367+
[$pages, $instances],
1368+
(pages, instances) => {
13701369
if (pages === undefined) {
13711370
return;
13721371
}
13731372
const rootInstanceId = nanoid();
1374-
const systemDataSourceId = nanoid();
13751373
pages.pages.push({
13761374
id: pageId,
13771375
name: values.name,
13781376
path: values.path,
13791377
title: values.title,
13801378
rootInstanceId,
1381-
systemDataSourceId,
13821379
meta: {},
13831380
});
1384-
13851381
instances.set(rootInstanceId, {
13861382
type: "instance",
13871383
id: rootInstanceId,
13881384
component: "Body",
13891385
children: [],
13901386
});
1391-
dataSources.set(systemDataSourceId, {
1392-
id: systemDataSourceId,
1393-
scopeInstanceId: rootInstanceId,
1394-
name: "system",
1395-
type: "parameter",
1396-
});
1397-
13981387
registerFolderChildMutable(pages.folders, pageId, values.parentFolderId);
13991388
selectInstance(undefined);
14001389
}

apps/builder/app/builder/features/pages/page-utils.test.ts

+24-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type Folder,
77
ROOT_FOLDER_ID,
88
type Page,
9+
SYSTEM_VARIABLE_ID,
910
} from "@webstudio-is/sdk";
1011
import {
1112
cleanupChildRefsMutable,
@@ -31,10 +32,15 @@ import { updateCurrentSystem } from "~/shared/system";
3132
setEnv("*");
3233
registerContainers();
3334

35+
const initialSystem = {
36+
origin: "https://undefined.wstd.work",
37+
params: {},
38+
search: {},
39+
};
40+
3441
const createPages = () => {
3542
const data = createDefaultPages({
3643
rootInstanceId: "rootInstanceId",
37-
systemDataSourceId: "systemDataSourceId",
3844
homePageId: "homePageId",
3945
});
4046

@@ -65,7 +71,6 @@ const createPages = () => {
6571
name: id,
6672
path,
6773
rootInstanceId: "rootInstanceId",
68-
systemDataSourceId: "systemDataSourceId",
6974
title: `"${id}"`,
7075
};
7176
return page;
@@ -108,7 +113,6 @@ describe("reparentOrphansMutable", () => {
108113
name: "Page",
109114
path: "/page",
110115
rootInstanceId: "rootInstanceId",
111-
systemDataSourceId: "systemDataSourceId",
112116
title: `"Page"`,
113117
});
114118
pages.folders.push({
@@ -402,7 +406,6 @@ test("page root scope should rely on selected page", () => {
402406
const pages = createDefaultPages({
403407
rootInstanceId: "homeRootId",
404408
homePageId: "homePageId",
405-
systemDataSourceId: "system",
406409
});
407410
pages.pages.push({
408411
id: "pageId",
@@ -411,7 +414,6 @@ test("page root scope should rely on selected page", () => {
411414
path: "/",
412415
title: `"My Title"`,
413416
meta: {},
414-
systemDataSourceId: "system",
415417
});
416418
$pages.set(pages);
417419
$awareness.set({ pageId: "pageId" });
@@ -434,9 +436,18 @@ test("page root scope should rely on selected page", () => {
434436
])
435437
);
436438
expect($pageRootScope.get()).toEqual({
437-
aliases: new Map([["$ws$dataSource$2", "page variable"]]),
438-
scope: { $ws$dataSource$2: "" },
439-
variableValues: new Map([["2", ""]]),
439+
aliases: new Map([
440+
["$ws$system", "system"],
441+
["$ws$dataSource$2", "page variable"],
442+
]),
443+
scope: {
444+
$ws$system: initialSystem,
445+
$ws$dataSource$2: "",
446+
},
447+
variableValues: new Map<string, unknown>([
448+
[SYSTEM_VARIABLE_ID, initialSystem],
449+
["2", ""],
450+
]),
440451
});
441452
});
442453

@@ -445,7 +456,6 @@ test("page root scope should use variable and resource values", () => {
445456
createDefaultPages({
446457
rootInstanceId: "homeRootId",
447458
homePageId: "homePageId",
448-
systemDataSourceId: "system",
449459
})
450460
);
451461
$awareness.set({ pageId: "homePageId" });
@@ -473,21 +483,24 @@ test("page root scope should use variable and resource values", () => {
473483
$resourceValues.set(new Map([["resourceId", "resource variable value"]]));
474484
expect($pageRootScope.get()).toEqual({
475485
aliases: new Map([
486+
["$ws$system", "system"],
476487
["$ws$dataSource$valueVariableId", "value variable"],
477488
["$ws$dataSource$resourceVariableId", "resource variable"],
478489
]),
479490
scope: {
491+
$ws$system: initialSystem,
480492
$ws$dataSource$resourceVariableId: "resource variable value",
481493
$ws$dataSource$valueVariableId: "value variable value",
482494
},
483-
variableValues: new Map([
495+
variableValues: new Map<string, unknown>([
496+
[SYSTEM_VARIABLE_ID, initialSystem],
484497
["valueVariableId", "value variable value"],
485498
["resourceVariableId", "resource variable value"],
486499
]),
487500
});
488501
});
489502

490-
test("page root scope should prefill default system variable value", () => {
503+
test("page root scope should provide page system variable value", () => {
491504
$pages.set(
492505
createDefaultPages({
493506
rootInstanceId: "homeRootId",

apps/builder/app/builder/features/pages/page-utils.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
ROOT_FOLDER_ID,
1313
isRootFolder,
1414
ROOT_INSTANCE_ID,
15+
systemParameter,
16+
SYSTEM_VARIABLE_ID,
1517
} from "@webstudio-is/sdk";
1618
import { removeByMutable } from "~/shared/array-utils";
1719
import {
@@ -259,7 +261,10 @@ export const $pageRootScope = computed(
259261
getInstanceKey([page.rootInstanceId, ROOT_INSTANCE_ID])
260262
) ?? new Map<string, unknown>();
261263
for (const [dataSourceId, value] of values) {
262-
const dataSource = dataSources.get(dataSourceId);
264+
let dataSource = dataSources.get(dataSourceId);
265+
if (dataSourceId === SYSTEM_VARIABLE_ID) {
266+
dataSource = systemParameter;
267+
}
263268
if (dataSource === undefined) {
264269
continue;
265270
}

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

+7-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
generateObjectExpression,
1818
isLiteralExpression,
1919
parseObjectExpression,
20+
SYSTEM_VARIABLE_ID,
21+
systemParameter,
2022
} from "@webstudio-is/sdk";
2123
import { sitemapResourceUrl } from "@webstudio-is/sdk/runtime";
2224
import {
@@ -34,7 +36,6 @@ import {
3436
theme,
3537
} from "@webstudio-is/design-system";
3638
import { TrashIcon, InfoCircleIcon, PlusIcon } from "@webstudio-is/icons";
37-
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
3839
import { humanizeString } from "~/shared/string-utils";
3940
import {
4041
$dataSources,
@@ -375,7 +376,7 @@ const $hiddenDataSourceIds = computed(
375376
dataSourceIds.add(dataSource.id);
376377
}
377378
}
378-
if (page?.systemDataSourceId && isFeatureEnabled("filters")) {
379+
if (page?.systemDataSourceId) {
379380
dataSourceIds.delete(page.systemDataSourceId);
380381
}
381382
return dataSourceIds;
@@ -406,7 +407,10 @@ const $selectedInstanceScope = computed(
406407
if (hiddenDataSourceIds.has(dataSourceId)) {
407408
continue;
408409
}
409-
const dataSource = dataSources.get(dataSourceId);
410+
let dataSource = dataSources.get(dataSourceId);
411+
if (dataSourceId === SYSTEM_VARIABLE_ID) {
412+
dataSource = systemParameter;
413+
}
410414
if (dataSource === undefined) {
411415
continue;
412416
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import equal from "fast-deep-equal";
1414
import {
1515
decodeDataSourceVariable,
1616
encodeDataSourceVariable,
17+
SYSTEM_VARIABLE_ID,
18+
systemParameter,
1719
} from "@webstudio-is/sdk";
1820
import type { PropMeta, Prop, Asset } from "@webstudio-is/sdk";
1921
import { InfoCircleIcon, MinusIcon } from "@webstudio-is/icons";
@@ -328,7 +330,10 @@ export const $selectedInstanceScope = computed(
328330
const values = variableValuesByInstanceSelector.get(instanceKey);
329331
if (values) {
330332
for (const [dataSourceId, value] of values) {
331-
const dataSource = dataSources.get(dataSourceId);
333+
let dataSource = dataSources.get(dataSourceId);
334+
if (dataSourceId === SYSTEM_VARIABLE_ID) {
335+
dataSource = systemParameter;
336+
}
332337
if (dataSource === undefined) {
333338
continue;
334339
}

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
type DataSource,
4545
transpileExpression,
4646
lintExpression,
47+
SYSTEM_VARIABLE_ID,
4748
} from "@webstudio-is/sdk";
4849
import {
4950
ExpressionEditor,
@@ -735,6 +736,7 @@ export const VariablePopoverTrigger = ({
735736
const { allowDynamicData } = useStore($userPlanFeatures);
736737
const [isResource, setIsResource] = useState(variable?.type === "resource");
737738
const requiresUpgrade = allowDynamicData === false && isResource;
739+
const isSystemVariable = variable?.id === SYSTEM_VARIABLE_ID;
738740

739741
return (
740742
<FloatingPanel
@@ -856,7 +858,7 @@ export const VariablePopoverTrigger = ({
856858
}}
857859
onSubmit={(event) => {
858860
event.preventDefault();
859-
if (requiresUpgrade) {
861+
if (requiresUpgrade || isSystemVariable) {
860862
return;
861863
}
862864
const nameElement =
@@ -879,11 +881,17 @@ export const VariablePopoverTrigger = ({
879881
>
880882
{/* submit is not triggered when press enter on input without submit button */}
881883
<button hidden></button>
882-
<BindingPopoverProvider
883-
value={{ containerRef: bindingPopoverContainerRef }}
884+
<fieldset
885+
style={{ display: "contents" }}
886+
// forbid editing system variable
887+
disabled={isSystemVariable}
884888
>
885-
<VariablePanel ref={panelRef} variable={variable} />
886-
</BindingPopoverProvider>
889+
<BindingPopoverProvider
890+
value={{ containerRef: bindingPopoverContainerRef }}
891+
>
892+
<VariablePanel ref={panelRef} variable={variable} />
893+
</BindingPopoverProvider>
894+
</fieldset>
887895
</form>
888896
</Flex>
889897
</ScrollArea>

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

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { EllipsesIcon, PlusIcon } from "@webstudio-is/icons";
2828
import type { DataSource } from "@webstudio-is/sdk";
2929
import {
3030
decodeDataSourceVariable,
31+
findPageByIdOrPath,
3132
getExpressionIdentifiers,
3233
} from "@webstudio-is/sdk";
3334
import {
@@ -196,6 +197,7 @@ const VariablesItem = ({
196197
value: unknown;
197198
usageCount: number;
198199
}) => {
200+
const selectedPage = useStore($selectedPage);
199201
const [inspectDialogOpen, setInspectDialogOpen] = useState(false);
200202
const [isMenuOpen, setIsMenuOpen] = useState(false);
201203
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@@ -265,6 +267,23 @@ const VariablesItem = ({
265267
Delete {usageCount > 0 && `(${usageCount} bindings)`}
266268
</DropdownMenuItem>
267269
)}
270+
{source === "local" &&
271+
variable.id === selectedPage?.systemDataSourceId && (
272+
<DropdownMenuItem
273+
onSelect={() => {
274+
updateWebstudioData((data) => {
275+
const page = findPageByIdOrPath(
276+
selectedPage.id,
277+
data.pages
278+
);
279+
delete page?.systemDataSourceId;
280+
deleteVariableMutable(data, variable.id);
281+
});
282+
}}
283+
>
284+
Delete
285+
</DropdownMenuItem>
286+
)}
268287
</DropdownMenuContent>
269288
</DropdownMenu>
270289

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
Variable,
99
ws,
1010
} from "@webstudio-is/template";
11-
import { encodeDataVariableId, ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
11+
import {
12+
encodeDataVariableId,
13+
ROOT_INSTANCE_ID,
14+
SYSTEM_VARIABLE_ID,
15+
} from "@webstudio-is/sdk";
1216
import {
1317
computeExpression,
1418
decodeDataVariableName,
@@ -56,6 +60,7 @@ test("find available variables", () => {
5660
expect(
5761
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
5862
).toEqual([
63+
expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }),
5964
expect.objectContaining({ name: "bodyVariable" }),
6065
expect.objectContaining({ name: "boxVariable" }),
6166
]);
@@ -72,6 +77,7 @@ test("find masked variables", () => {
7277
expect(
7378
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
7479
).toEqual([
80+
expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }),
7581
expect.objectContaining({ scopeInstanceId: "boxId", name: "myVariable" }),
7682
]);
7783
});
@@ -90,6 +96,7 @@ test("find global variables", () => {
9096
expect(
9197
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
9298
).toEqual([
99+
expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }),
93100
expect.objectContaining({ name: "globalVariable" }),
94101
expect.objectContaining({ name: "boxVariable" }),
95102
]);
@@ -114,6 +121,7 @@ test("find global variables in slots", () => {
114121
expect(
115122
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
116123
).toEqual([
124+
expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }),
117125
expect.objectContaining({ name: "globalVariable" }),
118126
expect.objectContaining({ name: "boxVariable" }),
119127
]);

0 commit comments

Comments
 (0)