Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions apps/builder/app/builder/features/inspector/inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useRef } from "react";
import { computed } from "nanostores";
import { useStore } from "@nanostores/react";
import type { Instance } from "@webstudio-is/sdk";
import { rootComponent } from "@webstudio-is/sdk";
import {
theme,
PanelTabs,
Expand All @@ -20,7 +19,7 @@ import {
FloatingPanelProvider,
} from "@webstudio-is/design-system";
import { ModeMenu, StylePanel } from "~/builder/features/style-panel";
import { SettingsPanelContainer } from "~/builder/features/settings-panel";
import { SettingsPanel } from "~/builder/features/settings-panel";
import {
$registeredComponentMetas,
$dragAndDropState,
Expand Down Expand Up @@ -93,6 +92,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
type PanelName = "style" | "settings";

const availablePanels = new Set<PanelName>();
availablePanels.add("settings");
if (
// forbid styling body in xml document
documentType === "html" &&
Expand All @@ -102,11 +102,6 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
) {
availablePanels.add("style");
}
// @todo hide root component settings until
// global data sources are implemented
if (selectedInstance.component !== rootComponent) {
availablePanels.add("settings");
}

return (
<EnhancedTooltipProvider
Expand Down Expand Up @@ -197,7 +192,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
>
<InstanceInfo instance={selectedInstance} />
</Flex>
<SettingsPanelContainer
<SettingsPanel
// Re-render when instance changes
key={selectedInstance.id}
selectedInstance={selectedInstance}
Expand Down
3 changes: 2 additions & 1 deletion apps/builder/app/builder/features/pages/page-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
encodeDataSourceVariable,
ROOT_FOLDER_ID,
isRootFolder,
ROOT_INSTANCE_ID,
} from "@webstudio-is/sdk";
import { removeByMutable } from "~/shared/array-utils";
import {
Expand Down Expand Up @@ -255,7 +256,7 @@ export const $pageRootScope = computed(
}
const values =
variableValuesByInstanceSelector.get(
getInstanceKey([page.rootInstanceId])
getInstanceKey([page.rootInstanceId, ROOT_INSTANCE_ID])
) ?? new Map<string, unknown>();
for (const [dataSourceId, value] of values) {
const dataSource = dataSources.get(dataSourceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
import { parseCurl, type CurlRequest } from "./curl";
import {
$selectedInstance,
$selectedInstanceKey,
$selectedInstanceKeyWithRoot,
$selectedPage,
} from "~/shared/awareness";
import { updateWebstudioData } from "~/shared/instance-utils";
Expand Down Expand Up @@ -384,7 +384,7 @@ const $hiddenDataSourceIds = computed(

const $selectedInstanceScope = computed(
[
$selectedInstanceKey,
$selectedInstanceKeyWithRoot,
$variableValuesByInstanceSelector,
$dataSources,
$hiddenDataSourceIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useStore } from "@nanostores/react";
import cmsUpgradeBanner from "./cms-upgrade-banner.svg?url";
import { $isDesignMode, $userPlanFeatures } from "~/shared/nano-states";

export const SettingsPanelContainer = ({
export const SettingsPanel = ({
selectedInstance,
}: {
selectedInstance: Instance;
Expand Down
8 changes: 6 additions & 2 deletions apps/builder/app/builder/features/settings-panel/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from "~/shared/nano-states";
import type { BindingVariant } from "~/builder/shared/binding-popover";
import { humanizeString } from "~/shared/string-utils";
import { $selectedInstanceKey } from "~/shared/awareness";
import { $selectedInstanceKeyWithRoot } from "~/shared/awareness";

export type PropValue =
| { type: "number"; value: number }
Expand Down Expand Up @@ -314,7 +314,11 @@ export const Row = ({
);

export const $selectedInstanceScope = computed(
[$selectedInstanceKey, $variableValuesByInstanceSelector, $dataSources],
[
$selectedInstanceKeyWithRoot,
$variableValuesByInstanceSelector,
$dataSources,
],
(instanceKey, variableValuesByInstanceSelector, dataSources) => {
const scope: Record<string, unknown> = {};
const aliases = new Map<string, string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,45 +51,40 @@ import {
} from "./variable-popover";
import {
$selectedInstance,
$selectedInstanceKey,
$selectedInstancePath,
$selectedInstanceKeyWithRoot,
$selectedPage,
} from "~/shared/awareness";
import { updateWebstudioData } from "~/shared/instance-utils";
import { deleteVariableMutable } from "~/shared/data-variables";
import {
deleteVariableMutable,
findAvailableVariables,
} from "~/shared/data-variables";

/**
* find variables defined specifically on this selected instance
*/
const $availableVariables = computed(
[$selectedInstancePath, $dataSources],
(instancePath, dataSources) => {
if (instancePath === undefined) {
[$selectedInstance, $instances, $dataSources],
(selectedInstance, instances, dataSources) => {
if (selectedInstance === undefined) {
return [];
}
const [{ instanceSelector }] = instancePath;
const [selectedInstanceId] = instanceSelector;
const availableVariables = new Map<DataSource["name"], DataSource>();
// order from ancestor to descendant
// so descendants can override ancestor variables
for (const { instance } of instancePath.slice().reverse()) {
for (const dataSource of dataSources.values()) {
if (dataSource.scopeInstanceId === instance.id) {
availableVariables.set(dataSource.name, dataSource);
}
}
}
const availableVariables = findAvailableVariables({
startingInstanceId: selectedInstance.id,
instances,
dataSources,
});
// order local variables first
return Array.from(availableVariables.values()).sort((left, right) => {
const leftRank = left.scopeInstanceId === selectedInstanceId ? 0 : 1;
const rightRank = right.scopeInstanceId === selectedInstanceId ? 0 : 1;
const leftRank = left.scopeInstanceId === selectedInstance.id ? 0 : 1;
const rightRank = right.scopeInstanceId === selectedInstance.id ? 0 : 1;
return leftRank - rightRank;
});
}
);

const $instanceVariableValues = computed(
[$selectedInstanceKey, $variableValuesByInstanceSelector],
[$selectedInstanceKeyWithRoot, $variableValuesByInstanceSelector],
(instanceKey, variableValuesByInstanceSelector) =>
variableValuesByInstanceSelector.get(instanceKey ?? "") ??
new Map<string, unknown>()
Expand Down
13 changes: 13 additions & 0 deletions apps/builder/app/shared/awareness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ export const getInstanceKey = <
): (InstanceSelector extends undefined ? undefined : never) | string =>
JSON.stringify(instanceSelector);

export const $selectedInstanceKeyWithRoot = computed(
$awareness,
(awareness) => {
const instanceSelector = awareness?.instanceSelector;
if (instanceSelector) {
if (instanceSelector[0] === ROOT_INSTANCE_ID) {
return getInstanceKey(instanceSelector);
}
return getInstanceKey([...instanceSelector, ROOT_INSTANCE_ID]);
}
}
);

export const $selectedInstanceKey = computed($awareness, (awareness) =>
getInstanceKey(awareness?.instanceSelector)
);
Expand Down
78 changes: 77 additions & 1 deletion apps/builder/app/shared/data-variables.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
renderData,
ResourceValue,
Variable,
ws,
} from "@webstudio-is/template";
import { encodeDataVariableId } from "@webstudio-is/sdk";
import { encodeDataVariableId, ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
import {
computeExpression,
decodeDataVariableName,
Expand All @@ -17,6 +18,7 @@ import {
rebindTreeVariablesMutable,
unsetExpressionVariables,
deleteVariableMutable,
findAvailableVariables,
} from "./data-variables";

test("encode data variable name when necessary", () => {
Expand All @@ -43,6 +45,80 @@ test("dencode data variable name with dollar sign", () => {
expect(decodeDataVariableName("$my$Variable")).toEqual("$my$Variable");
});

test("find available variables", () => {
const bodyVariable = new Variable("bodyVariable", "");
const boxVariable = new Variable("boxVariable", "");
const data = renderData(
<$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}>
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
</$.Body>
);
expect(
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
).toEqual([
expect.objectContaining({ name: "bodyVariable" }),
expect.objectContaining({ name: "boxVariable" }),
]);
});

test("find masked variables", () => {
const bodyVariable = new Variable("myVariable", "");
const boxVariable = new Variable("myVariable", "");
const data = renderData(
<$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}>
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
</$.Body>
);
expect(
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
).toEqual([
expect.objectContaining({ scopeInstanceId: "boxId", name: "myVariable" }),
]);
});

test("find global variables", () => {
const globalVariable = new Variable("globalVariable", "");
const boxVariable = new Variable("boxVariable", "");
const data = renderData(
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
<$.Body ws:id="bodyId">
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
</$.Body>
</ws.root>
);
data.instances.delete(ROOT_INSTANCE_ID);
expect(
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
).toEqual([
expect.objectContaining({ name: "globalVariable" }),
expect.objectContaining({ name: "boxVariable" }),
]);
});

test("find global variables in slots", () => {
const globalVariable = new Variable("globalVariable", "");
const bodyVariable = new Variable("bodyVariable", "");
const boxVariable = new Variable("boxVariable", "");
const data = renderData(
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
<$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}>
<$.Slot ws:id="slotId">
<$.Fragment ws:id="fragmentId">
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
</$.Fragment>
</$.Slot>
</$.Body>
</ws.root>
);
data.instances.delete(ROOT_INSTANCE_ID);
expect(
findAvailableVariables({ ...data, startingInstanceId: "boxId" })
).toEqual([
expect.objectContaining({ name: "globalVariable" }),
expect.objectContaining({ name: "boxVariable" }),
]);
});

test("unset expression variables", () => {
expect(
unsetExpressionVariables({
Expand Down
27 changes: 27 additions & 0 deletions apps/builder/app/shared/data-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type Resource,
type Resources,
type WebstudioData,
ROOT_INSTANCE_ID,
decodeDataVariableId,
encodeDataVariableId,
findTreeInstanceIdsExcludingSlotDescendants,
Expand Down Expand Up @@ -209,6 +210,8 @@ const findMaskedVariablesByInstanceId = ({
instanceIdsPath.push(currentId);
currentId = parentInstanceById.get(currentId);
}
// allow accessing global variables everywhere
instanceIdsPath.push(ROOT_INSTANCE_ID);
const maskedVariables = new Map<DataSource["name"], DataSource["id"]>();
// start from the root to descendant
// so child variables override parent variables
Expand All @@ -222,6 +225,30 @@ const findMaskedVariablesByInstanceId = ({
return maskedVariables;
};

export const findAvailableVariables = ({
startingInstanceId,
instances,
dataSources,
}: {
startingInstanceId: Instance["id"];
instances: Instances;
dataSources: DataSources;
}) => {
const maskedVariables = findMaskedVariablesByInstanceId({
startingInstanceId,
instances,
dataSources,
});
const availableVariables: DataSource[] = [];
for (const dataSourceId of maskedVariables.values()) {
const dataSource = dataSources.get(dataSourceId);
if (dataSource) {
availableVariables.push(dataSource);
}
}
return availableVariables;
};

const traverseExpressions = ({
startingInstanceId,
instances,
Expand Down
Loading
Loading