experimental: autocomplete path params (#3346)

Here added support for autocompleting inferred path params where their
values are not specified by user.

<img width="312" alt="Screenshot 2024-05-12 at 14 33 23"
src="https://github.com/webstudio-is/webstudio/assets/5635476/0576039d-4fab-4499-923b-61867c12e096">
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2024-05-13 03:08:23 +03:00
1 parent 12e00186ab
commit ea5b5c8a32
9 files changed
+214 -73

No files matched your search

@@ -983,9 +983,22 @@ test("page root scope should prefill default system variable value", () => {
expect($pageRootScope.get()).toEqual({
aliases: new Map([["$ws$dataSource$systemId", "system"]]),
scope: {
$ws$dataSource$systemId: { params: {}, search: {} },
$ws$dataSource$systemId: {
origin: "https://undefined.wstd.work",
params: {},
search: {},
},
},
variableValues: new Map([["systemId", { params: {}, search: {} }]]),
variableValues: new Map([
[
"systemId",
{
params: {},
search: {},
origin: "https://undefined.wstd.work",
},
],
]),
});
$dataSourceVariables.set(
@@ -994,10 +1007,21 @@ test("page root scope should prefill default system variable value", () => {
expect($pageRootScope.get()).toEqual({
aliases: new Map([["$ws$dataSource$systemId", "system"]]),
scope: {
$ws$dataSource$systemId: { params: { slug: "my-post" }, search: {} },
$ws$dataSource$systemId: {
params: { slug: "my-post" },
search: {},
origin: "https://undefined.wstd.work",
},
},
variableValues: new Map([
["systemId", { params: { slug: "my-post" }, search: {} }],
[
"systemId",
{
params: { slug: "my-post" },
search: {},
origin: "https://undefined.wstd.work",
},
],
]),
});
});
@@ -15,6 +15,7 @@ import {
decodeDataSourceVariable,
encodeDataSourceVariable,
transpileExpression,
type System,
} from "@webstudio-is/sdk";
import { removeByMutable } from "~/shared/array-utils";
import {
@@ -27,9 +28,12 @@ import {
$dataSourceVariables,
$dataSources,
$pages,
$publishedOrigin,
$resourceValues,
$selectedInstanceSelector,
$selectedPageId,
getPageDefaultSystem,
mergeSystem,
} from "~/shared/nano-states";
type TreePage = {
@@ -345,9 +349,22 @@ const $editingPage = computed(
}
);
const $editingPagePath = computed($editingPage, (page) => page?.path);
const $editingPageDefaultSystem = computed(
[$publishedOrigin, $editingPagePath],
(origin, path) => getPageDefaultSystem({ origin, path })
);
const $pageRootVariableValues = computed(
[$editingPage, $dataSources, $dataSourceVariables, $resourceValues],
(page, dataSources, dataSourceVariables, resourceValues) => {
[
$editingPage,
$dataSources,
$dataSourceVariables,
$resourceValues,
$editingPageDefaultSystem,
],
(page, dataSources, dataSourceVariables, resourceValues, defaultSystem) => {
const variableValues = new Map<string, unknown>();
if (page === undefined) {
return variableValues;
@@ -363,11 +380,11 @@ const $pageRootVariableValues = computed(
if (variable.type === "parameter") {
const value = dataSourceVariables.get(variable.id);
variableValues.set(variable.id, value);
if (variable.id === page.systemDataSourceId && value === undefined) {
variableValues.set(variable.id, {
params: {},
search: {},
});
if (variable.id === page.systemDataSourceId) {
variableValues.set(
variable.id,
mergeSystem(defaultSystem, value as undefined | System)
);
}
}
if (variable.type === "resource") {
@@ -64,7 +64,7 @@ export const formatValuePreview = (value: unknown) => {
return value.toString();
}
if (value === undefined) {
return "undefined";
return "";
}
if (value === null) {
return "null";
@@ -1,3 +1,4 @@
import { shallowEqual } from "shallow-equal";
import type { ExoticComponent } from "react";
import { atom } from "nanostores";
import {
@@ -12,10 +13,10 @@ import {
import type { Instance } from "@webstudio-is/sdk";
import { decodeDataSourceVariable } from "@webstudio-is/sdk";
import type { InstanceSelector } from "../tree-utils";
import { $dataSourceVariables, $dataSources, $props } from "./nano-states";
import { $dataSources, $props } from "./nano-states";
import { $instances, $selectedInstanceSelector } from "./instances";
import { $selectedPage } from "./pages";
import { shallowEqual } from "shallow-equal";
import { $dataSourceVariables } from "./variables";
const createHookContext = (): HookContext => {
const metas = $registeredComponentMetas.get();
@@ -4,4 +4,5 @@ export * from "./instances";
export * from "./props";
export * from "./canvas";
export * from "./pages";
export * from "./variables";
export * from "./components";
@@ -6,10 +6,8 @@ import type { AuthPermit } from "@webstudio-is/trpc-interface/index.server";
import type { ItemDropTarget, Placement } from "@webstudio-is/design-system";
import type {
Assets,
DataSource,
DataSources,
Instance,
Page,
Prop,
Props,
Resource,
@@ -18,7 +16,6 @@ import type {
StyleSource,
StyleSources,
StyleSourceSelections,
System,
} from "@webstudio-is/sdk";
import type { Style } from "@webstudio-is/css-engine";
import type { Project } from "@webstudio-is/project";
@@ -57,30 +54,8 @@ export const $rootInstance = computed(
);
export const $dataSources = atom<DataSources>(new Map());
export const $dataSourceVariables = atom<Map<DataSource["id"], unknown>>(
new Map()
);
export const updateSystem = (page: Page, update: Partial<System>) => {
const dataSourceVariables = new Map($dataSourceVariables.get());
const system = dataSourceVariables.get(page.systemDataSourceId) as
| undefined
| System;
const newSystem: System = {
search: {},
params: {},
// This value seems like doesn't matter
origin: "",
...system,
...update,
};
dataSourceVariables.set(page.systemDataSourceId, newSystem);
$dataSourceVariables.set(dataSourceVariables);
};
export const $resources = atom(new Map<Resource["id"], Resource>());
export const $resourceValues = atom(new Map<Resource["id"], unknown>());
export const $props = atom<Props>(new Map());
export const $propsIndex = computed($props, (props) => {
@@ -14,15 +14,9 @@ import {
computeExpression,
} from "./props";
import { $pages, $selectedPageId } from "./pages";
import {
$assets,
$dataSourceVariables,
$dataSources,
$props,
$resourceValues,
$resources,
} from "./nano-states";
import { $assets, $dataSources, $props, $resources } from "./nano-states";
import { $params } from "~/canvas/stores";
import { $dataSourceVariables, $resourceValues } from "./variables";
setEnv("*");
@@ -547,6 +541,53 @@ test("compute instance text content bound to expression", () => {
cleanStores($propValuesByInstanceSelector);
});
test("use default system values in props", () => {
$instances.set(
toMap([
{
id: "body",
type: "instance",
component: "Body",
children: [],
},
])
);
$dataSources.set(
toMap([
{
id: "systemId",
scopeInstanceId: "body",
name: "system",
type: "parameter",
},
])
);
$props.set(
toMap([
{
id: "1",
instanceId: "body",
name: "data-origin",
type: "expression",
value: `$ws$dataSource$systemId.origin`,
},
])
);
selectPageRoot("body");
expect($propValuesByInstanceSelector.get()).toEqual(
new Map([
[
JSON.stringify(["body"]),
new Map<string, unknown>([
["data-origin", "https://undefined.wstd.work"],
]),
],
])
);
cleanStores($propValuesByInstanceSelector);
});
test("compute variable values for root", () => {
$instances.set(
toMap([{ id: "body", type: "instance", component: "Body", children: [] }])
@@ -929,7 +970,14 @@ test("prefill default system variable value", () => {
[
JSON.stringify(["body"]),
new Map<string, unknown>([
["systemId", { params: { slug: "my-post" }, search: {} }],
[
"systemId",
{
params: { slug: "my-post" },
search: {},
origin: "https://undefined.wstd.work",
},
],
]),
],
])
+27 -25
View File
@@ -20,20 +20,18 @@ import {
} from "@webstudio-is/react-sdk";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import { $instances } from "./instances";
import {
$dataSourceVariables,
$dataSources,
$props,
$assets,
$resources,
$resourceValues,
$publishedOrigin,
} from "./nano-states";
import { $dataSources, $props, $assets, $resources } from "./nano-states";
import { $selectedPage, $pages } from "./pages";
import { groupBy } from "../array-utils";
import type { InstanceSelector } from "../tree-utils";
import { $params } from "~/canvas/stores";
import { restResourcesLoader } from "../router-utils";
import {
$dataSourceVariables,
$resourceValues,
$selectedPageDefaultSystem,
mergeSystem,
} from "./variables";
export const getIndexedInstanceId = (
instanceId: Instance["id"],
@@ -120,8 +118,14 @@ const getAction = (
// result of executing generated code
// includes variables, computed expressions and action callbacks
const $dataSourcesLogic = computed(
[$dataSources, $dataSourceVariables, $resourceValues],
(dataSources, dataSourceVariables, resourceValues) => {
[
$dataSources,
$dataSourceVariables,
$resourceValues,
$selectedPage,
$selectedPageDefaultSystem,
],
(dataSources, dataSourceVariables, resourceValues, page, defaultSystem) => {
const values = new Map<string, unknown>();
for (const [dataSourceId, dataSource] of dataSources) {
if (dataSource.type === "variable") {
@@ -131,7 +135,11 @@ const $dataSourcesLogic = computed(
);
}
if (dataSource.type === "parameter") {
values.set(dataSourceId, dataSourceVariables.get(dataSourceId));
let value = dataSourceVariables.get(dataSourceId);
if (dataSource.id === page?.systemDataSourceId) {
value = mergeSystem(defaultSystem, value as undefined | System);
}
values.set(dataSourceId, value);
}
if (dataSource.type === "resource") {
values.set(dataSourceId, resourceValues.get(dataSource.resourceId));
@@ -301,7 +309,7 @@ export const $variableValuesByInstanceSelector = computed(
$dataSources,
$dataSourceVariables,
$resourceValues,
$publishedOrigin,
$selectedPageDefaultSystem,
],
(
instances,
@@ -310,7 +318,7 @@ export const $variableValuesByInstanceSelector = computed(
dataSources,
dataSourceVariables,
resourceValues,
publishedOrigin
defaultSystem
) => {
const propsByInstanceId = groupBy(
props.values(),
@@ -361,17 +369,11 @@ export const $variableValuesByInstanceSelector = computed(
if (variable.type === "parameter") {
const value = dataSourceVariables.get(variable.id);
variableValues.set(variable.id, value);
if (
variable.id === page.systemDataSourceId &&
value === undefined
) {
const systemDefaultValue: System = {
params: {},
search: {},
origin: publishedOrigin,
};
variableValues.set(variable.id, systemDefaultValue);
if (variable.id === page.systemDataSourceId) {
variableValues.set(
variable.id,
mergeSystem(defaultSystem, value as undefined | System)
);
}
}
if (variable.type === "resource") {
@@ -0,0 +1,73 @@
import { atom, computed } from "nanostores";
import type { DataSource, Page, Resource, System } from "@webstudio-is/sdk";
import { tokenizePathnamePattern } from "~/builder/shared/url-pattern";
import { $publishedOrigin } from "./nano-states";
import { $selectedPage } from "./pages";
export const $dataSourceVariables = atom<Map<DataSource["id"], unknown>>(
new Map()
);
export const $resourceValues = atom(new Map<Resource["id"], unknown>());
export const getPageDefaultSystem = ({
origin,
path,
}: {
origin: string;
path?: string;
}) => {
const defaultSystem: System = {
params: {},
search: {},
origin,
};
if (path) {
const tokens = tokenizePathnamePattern(path);
for (const token of tokens) {
if (token.type === "param") {
defaultSystem.params[token.name] = undefined;
}
}
}
return defaultSystem;
};
const $selectedPagePath = computed($selectedPage, (page) => page?.path);
export const $selectedPageDefaultSystem = computed(
[$publishedOrigin, $selectedPagePath],
(origin, path) => getPageDefaultSystem({ origin, path })
);
export const mergeSystem = (left: System, right?: System): System => {
return {
...left,
...right,
params: {
...left.params,
...right?.params,
},
search: {
...left.search,
...right?.search,
},
};
};
export const updateSystem = (page: Page, update: Partial<System>) => {
const dataSourceVariables = new Map($dataSourceVariables.get());
const system = dataSourceVariables.get(page.systemDataSourceId) as
| undefined
| System;
const newSystem: System = {
search: {},
params: {},
origin: $publishedOrigin.get(),
...system,
...update,
};
dataSourceVariables.set(page.systemDataSourceId, newSystem);
$dataSourceVariables.set(dataSourceVariables);
};