mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
## Description A user wants to be able to test in preview mode using dev tools screen simulation. ## Steps for reproduction 1. resize the window to mobile size 2. see there is no. "too small alert" 3. see under 640px topbar elements got reduced 4. see that canvas is almost full width ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
249 lines
7.6 KiB
TypeScript
249 lines
7.6 KiB
TypeScript
import { shallowEqual } from "shallow-equal";
|
|
import type { ExoticComponent } from "react";
|
|
import { atom } from "nanostores";
|
|
import {
|
|
type AnyComponent,
|
|
type WsComponentMeta,
|
|
type WsComponentPropsMeta,
|
|
type Hook,
|
|
type HookContext,
|
|
namespaceMeta,
|
|
getIndexesWithinAncestors,
|
|
} from "@webstudio-is/react-sdk";
|
|
import type { Instance } from "@webstudio-is/sdk";
|
|
import { decodeDataSourceVariable } from "@webstudio-is/sdk";
|
|
import type { InstanceSelector } from "../tree-utils";
|
|
import { $dataSources, $memoryProps, $props } from "./misc";
|
|
import { $instances, $selectedInstanceSelector } from "./instances";
|
|
import { $selectedPage } from "./pages";
|
|
import { $dataSourceVariables } from "./variables";
|
|
import { nanoid } from "nanoid";
|
|
|
|
const createHookContext = (): HookContext => {
|
|
const metas = $registeredComponentMetas.get();
|
|
const instances = $instances.get();
|
|
const page = $selectedPage.get();
|
|
const indexesWithinAncestors = getIndexesWithinAncestors(
|
|
metas,
|
|
instances,
|
|
page ? [page.rootInstanceId] : []
|
|
);
|
|
|
|
return {
|
|
indexesWithinAncestors,
|
|
|
|
getPropValue: (instanceId, propName) => {
|
|
const props = $props.get();
|
|
for (const prop of props.values()) {
|
|
if (prop.instanceId === instanceId && prop.name === propName) {
|
|
if (
|
|
prop.type === "string" ||
|
|
prop.type === "number" ||
|
|
prop.type === "boolean" ||
|
|
prop.type === "string[]"
|
|
) {
|
|
return prop.value;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
setMemoryProp: (instanceSelector, propName, value) => {
|
|
if (instanceSelector.length === 0) {
|
|
console.error("instanceSelector is empty");
|
|
return;
|
|
}
|
|
|
|
const props = $memoryProps.get();
|
|
|
|
const newProps = props.get(JSON.stringify(instanceSelector)) ?? new Map();
|
|
|
|
const propBase = {
|
|
id: nanoid(),
|
|
instanceId: instanceSelector[0]!,
|
|
name: propName,
|
|
};
|
|
|
|
if (value !== undefined) {
|
|
switch (typeof value) {
|
|
case "string":
|
|
newProps.set(propName, {
|
|
...propBase,
|
|
type: "string",
|
|
value,
|
|
});
|
|
break;
|
|
case "number":
|
|
newProps.set(propName, {
|
|
...propBase,
|
|
type: "number",
|
|
value,
|
|
});
|
|
break;
|
|
case "boolean":
|
|
newProps.set(propName, {
|
|
...propBase,
|
|
type: "boolean",
|
|
value,
|
|
});
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported type ${typeof value}`);
|
|
}
|
|
} else {
|
|
newProps.delete(propName);
|
|
}
|
|
|
|
props.set(JSON.stringify(instanceSelector), newProps);
|
|
|
|
$memoryProps.set(new Map(props));
|
|
},
|
|
|
|
setPropVariable: (instanceId, propName, value) => {
|
|
const dataSourceVariables = new Map($dataSourceVariables.get());
|
|
const dataSources = new Map($dataSources.get());
|
|
const props = $props.get();
|
|
for (const prop of props.values()) {
|
|
if (
|
|
prop.instanceId === instanceId &&
|
|
prop.name === propName &&
|
|
prop.type === "expression"
|
|
) {
|
|
// extract id without parsing expression
|
|
const potentialVariableId = decodeDataSourceVariable(prop.value);
|
|
if (
|
|
potentialVariableId !== undefined &&
|
|
dataSources.has(potentialVariableId)
|
|
) {
|
|
const dataSourceId = potentialVariableId;
|
|
dataSourceVariables.set(dataSourceId, value);
|
|
}
|
|
}
|
|
}
|
|
$dataSourceVariables.set(dataSourceVariables);
|
|
},
|
|
};
|
|
};
|
|
|
|
// subscribe builder events and invoke all component hooks
|
|
export const subscribeComponentHooks = () => {
|
|
let lastSelectedInstanceSelector: undefined | InstanceSelector = undefined;
|
|
const unsubscribeSelectedInstanceSelector =
|
|
$selectedInstanceSelector.subscribe((instanceSelector) => {
|
|
// prevent executing hooks when selected instance is not changed
|
|
if (shallowEqual(lastSelectedInstanceSelector, instanceSelector)) {
|
|
return;
|
|
}
|
|
const hooks = $registeredComponentHooks.get();
|
|
const instances = $instances.get();
|
|
if (lastSelectedInstanceSelector) {
|
|
for (const hook of hooks) {
|
|
hook.onNavigatorUnselect?.(createHookContext(), {
|
|
instancePath: lastSelectedInstanceSelector.flatMap((id) => {
|
|
const instance = instances.get(id);
|
|
return instance ? [instance] : [];
|
|
}),
|
|
instanceSelector: lastSelectedInstanceSelector,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (instanceSelector) {
|
|
for (const hook of hooks) {
|
|
hook.onNavigatorSelect?.(createHookContext(), {
|
|
instancePath: instanceSelector.flatMap((id) => {
|
|
const instance = instances.get(id);
|
|
return instance ? [instance] : [];
|
|
}),
|
|
instanceSelector,
|
|
});
|
|
}
|
|
}
|
|
|
|
// store converts values to readonly
|
|
lastSelectedInstanceSelector = instanceSelector as InstanceSelector;
|
|
});
|
|
|
|
return () => {
|
|
unsubscribeSelectedInstanceSelector();
|
|
};
|
|
};
|
|
|
|
export const $registeredComponents = atom(new Map<string, AnyComponent>());
|
|
|
|
export const $registeredComponentHooks = atom<Hook[]>([]);
|
|
|
|
export const $registeredComponentMetas = atom(
|
|
new Map<string, WsComponentMeta>()
|
|
);
|
|
|
|
export const $registeredComponentPropsMetas = atom(
|
|
new Map<string, WsComponentPropsMeta>()
|
|
);
|
|
|
|
export const registerComponentLibrary = ({
|
|
namespace,
|
|
components,
|
|
metas,
|
|
propsMetas,
|
|
hooks,
|
|
}: {
|
|
namespace?: string;
|
|
// simplify adding component libraries
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
components: Record<Instance["component"], ExoticComponent<any>>;
|
|
metas: Record<Instance["component"], WsComponentMeta>;
|
|
propsMetas: Record<Instance["component"], WsComponentPropsMeta>;
|
|
hooks?: Hook[];
|
|
}) => {
|
|
const prefix = namespace === undefined ? "" : `${namespace}:`;
|
|
|
|
const prevComponents = $registeredComponents.get();
|
|
const nextComponents = new Map(prevComponents);
|
|
for (const [componentName, component] of Object.entries(components)) {
|
|
nextComponents.set(`${prefix}${componentName}`, component);
|
|
}
|
|
$registeredComponents.set(nextComponents);
|
|
|
|
const prevMetas = $registeredComponentMetas.get();
|
|
const nextMetas = new Map(prevMetas);
|
|
for (const [componentName, meta] of Object.entries(metas)) {
|
|
nextMetas.set(
|
|
`${prefix}${componentName}`,
|
|
namespace === undefined
|
|
? meta
|
|
: namespaceMeta(meta, namespace, new Set(Object.keys(metas)))
|
|
);
|
|
}
|
|
$registeredComponentMetas.set(nextMetas);
|
|
|
|
if (hooks) {
|
|
const prevHooks = $registeredComponentHooks.get();
|
|
const nextHooks = [...prevHooks, ...hooks];
|
|
$registeredComponentHooks.set(nextHooks);
|
|
}
|
|
|
|
const prevPropsMetas = $registeredComponentPropsMetas.get();
|
|
const nextPropsMetas = new Map(prevPropsMetas);
|
|
for (const [componentName, propsMeta] of Object.entries(propsMetas)) {
|
|
const { initialProps = [], props } = propsMeta;
|
|
const requiredProps: string[] = [];
|
|
for (const [name, value] of Object.entries(props)) {
|
|
if (value.required && initialProps.includes(name) === false) {
|
|
requiredProps.push(name);
|
|
}
|
|
}
|
|
nextPropsMetas.set(`${prefix}${componentName}`, {
|
|
// order of initialProps must be preserved
|
|
initialProps: [...initialProps, ...requiredProps],
|
|
props,
|
|
});
|
|
}
|
|
$registeredComponentPropsMetas.set(nextPropsMetas);
|
|
};
|
|
|
|
export const synchronizedComponentsMetaStores = [
|
|
["registeredComponentMetas", $registeredComponentMetas],
|
|
["registeredComponentPropsMetas", $registeredComponentPropsMetas],
|
|
] as const;
|