@webstudio-is/react-sdk 0.83.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/component-renderer.js +125 -0
- package/lib/cjs/components/component-meta.js +5 -0
- package/lib/cjs/context.js +2 -1
- package/lib/cjs/css/style-rules.js +1 -1
- package/lib/cjs/embed-template.js +101 -55
- package/lib/cjs/hook.js +34 -0
- package/lib/cjs/index.js +6 -0
- package/lib/cjs/instance-utils.js +65 -0
- package/lib/cjs/props.js +12 -2
- package/lib/cjs/tree/create-elements-tree.js +5 -4
- package/lib/cjs/tree/root.js +6 -2
- package/lib/cjs/tree/webstudio-component.js +2 -0
- package/lib/component-renderer.js +111 -0
- package/lib/components/component-meta.js +5 -0
- package/lib/context.js +2 -1
- package/lib/css/style-rules.js +1 -1
- package/lib/embed-template.js +101 -55
- package/lib/hook.js +14 -0
- package/lib/index.js +8 -1
- package/lib/instance-utils.js +45 -0
- package/lib/props.js +13 -3
- package/lib/tree/create-elements-tree.js +5 -4
- package/lib/tree/root.js +6 -2
- package/lib/tree/webstudio-component.js +2 -0
- package/lib/types/component-renderer.d.ts +8 -0
- package/lib/types/components/component-meta.d.ts +9 -6
- package/lib/types/context.d.ts +2 -0
- package/lib/types/css/css.d.ts +19 -19
- package/lib/types/css/global-rules.d.ts +19 -19
- package/lib/types/css/normalize.d.ts +47 -47
- package/lib/types/embed-template.d.ts +291 -181
- package/lib/types/hook.d.ts +31 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/instance-utils.d.ts +16 -0
- package/lib/types/instance-utils.test.d.ts +1 -0
- package/lib/types/props.d.ts +47 -46
- package/lib/types/tree/create-elements-tree.d.ts +5 -2
- package/lib/types/tree/root.d.ts +5 -2
- package/lib/types/tree/webstudio-component.d.ts +1 -0
- package/package.json +11 -11
- package/src/component-renderer.tsx +117 -0
- package/src/components/component-meta.ts +5 -0
- package/src/context.tsx +3 -0
- package/src/css/style-rules.ts +1 -1
- package/src/embed-template.test.ts +81 -70
- package/src/embed-template.ts +116 -56
- package/src/hook.ts +42 -0
- package/src/index.ts +4 -0
- package/src/instance-utils.test.ts +89 -0
- package/src/instance-utils.ts +65 -0
- package/src/props.ts +13 -1
- package/src/tree/create-elements-tree.tsx +8 -3
- package/src/tree/root.ts +8 -0
- package/src/tree/webstudio-component.tsx +1 -0
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@jest/globals";
|
|
2
|
-
import {
|
|
3
|
-
generateDataFromEmbedTemplate,
|
|
4
|
-
namespaceEmbedTemplateComponents,
|
|
5
|
-
} from "./embed-template";
|
|
2
|
+
import { generateDataFromEmbedTemplate, namespaceMeta } from "./embed-template";
|
|
6
3
|
import { showAttribute } from "./tree";
|
|
7
4
|
|
|
8
5
|
const expectString = expect.any(String);
|
|
@@ -223,15 +220,14 @@ test("generate data for embedding from props bound to data source variables", ()
|
|
|
223
220
|
{
|
|
224
221
|
type: "instance",
|
|
225
222
|
component: "Box1",
|
|
223
|
+
dataSources: {
|
|
224
|
+
showOtherBoxDataSource: { type: "variable", initialValue: false },
|
|
225
|
+
},
|
|
226
226
|
props: [
|
|
227
227
|
{
|
|
228
|
-
type: "
|
|
228
|
+
type: "dataSource",
|
|
229
229
|
name: "showOtherBox",
|
|
230
|
-
|
|
231
|
-
dataSourceRef: {
|
|
232
|
-
type: "variable",
|
|
233
|
-
name: "showOtherBoxDataSource",
|
|
234
|
-
},
|
|
230
|
+
dataSourceName: "showOtherBoxDataSource",
|
|
235
231
|
},
|
|
236
232
|
],
|
|
237
233
|
children: [],
|
|
@@ -241,13 +237,9 @@ test("generate data for embedding from props bound to data source variables", ()
|
|
|
241
237
|
component: "Box2",
|
|
242
238
|
props: [
|
|
243
239
|
{
|
|
244
|
-
type: "
|
|
240
|
+
type: "dataSource",
|
|
245
241
|
name: showAttribute,
|
|
246
|
-
|
|
247
|
-
dataSourceRef: {
|
|
248
|
-
type: "variable",
|
|
249
|
-
name: "showOtherBoxDataSource",
|
|
250
|
-
},
|
|
242
|
+
dataSourceName: "showOtherBoxDataSource",
|
|
251
243
|
},
|
|
252
244
|
],
|
|
253
245
|
children: [],
|
|
@@ -305,12 +297,18 @@ test("generate data for embedding from props bound to data source expressions",
|
|
|
305
297
|
{
|
|
306
298
|
type: "instance",
|
|
307
299
|
component: "Box1",
|
|
300
|
+
dataSources: {
|
|
301
|
+
boxState: { type: "variable", initialValue: "initial" },
|
|
302
|
+
boxStateSuccess: {
|
|
303
|
+
type: "expression",
|
|
304
|
+
code: `boxState === 'success'`,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
308
307
|
props: [
|
|
309
308
|
{
|
|
310
|
-
type: "
|
|
309
|
+
type: "dataSource",
|
|
311
310
|
name: "state",
|
|
312
|
-
|
|
313
|
-
dataSourceRef: { type: "variable", name: "boxState" },
|
|
311
|
+
dataSourceName: "boxState",
|
|
314
312
|
},
|
|
315
313
|
],
|
|
316
314
|
children: [],
|
|
@@ -320,14 +318,9 @@ test("generate data for embedding from props bound to data source expressions",
|
|
|
320
318
|
component: "Box2",
|
|
321
319
|
props: [
|
|
322
320
|
{
|
|
323
|
-
type: "
|
|
321
|
+
type: "dataSource",
|
|
324
322
|
name: showAttribute,
|
|
325
|
-
|
|
326
|
-
dataSourceRef: {
|
|
327
|
-
type: "expression",
|
|
328
|
-
name: "boxStateSuccess",
|
|
329
|
-
code: `boxState === 'success'`,
|
|
330
|
-
},
|
|
323
|
+
dataSourceName: "boxStateSuccess",
|
|
331
324
|
},
|
|
332
325
|
],
|
|
333
326
|
children: [],
|
|
@@ -392,12 +385,14 @@ test("generate data for embedding from action props", () => {
|
|
|
392
385
|
{
|
|
393
386
|
type: "instance",
|
|
394
387
|
component: "Box1",
|
|
388
|
+
dataSources: {
|
|
389
|
+
boxState: { type: "variable", initialValue: "initial" },
|
|
390
|
+
},
|
|
395
391
|
props: [
|
|
396
392
|
{
|
|
397
|
-
type: "
|
|
393
|
+
type: "dataSource",
|
|
398
394
|
name: "state",
|
|
399
|
-
|
|
400
|
-
dataSourceRef: { type: "variable", name: "boxState" },
|
|
395
|
+
dataSourceName: "boxState",
|
|
401
396
|
},
|
|
402
397
|
],
|
|
403
398
|
children: [
|
|
@@ -495,48 +490,64 @@ test("generate data for embedding from action props", () => {
|
|
|
495
490
|
|
|
496
491
|
test("add namespace to selected components in embed template", () => {
|
|
497
492
|
expect(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
493
|
+
namespaceMeta(
|
|
494
|
+
{
|
|
495
|
+
type: "container",
|
|
496
|
+
label: "",
|
|
497
|
+
icon: "",
|
|
498
|
+
requiredAncestors: ["Button", "Box"],
|
|
499
|
+
invalidAncestors: ["Tooltip"],
|
|
500
|
+
indexWithinAncestor: "Tooltip",
|
|
501
|
+
template: [
|
|
502
|
+
{
|
|
503
|
+
type: "instance",
|
|
504
|
+
component: "Tooltip",
|
|
505
|
+
children: [
|
|
506
|
+
{ type: "text", value: "Some text" },
|
|
507
|
+
{
|
|
508
|
+
type: "instance",
|
|
509
|
+
component: "Box",
|
|
510
|
+
children: [
|
|
511
|
+
{
|
|
512
|
+
type: "instance",
|
|
513
|
+
component: "Button",
|
|
514
|
+
children: [],
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
},
|
|
519
522
|
"my-namespace",
|
|
520
523
|
new Set(["Tooltip", "Button"])
|
|
521
524
|
)
|
|
522
|
-
).toEqual(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
525
|
+
).toEqual({
|
|
526
|
+
type: "container",
|
|
527
|
+
label: "",
|
|
528
|
+
icon: "",
|
|
529
|
+
requiredAncestors: ["my-namespace:Button", "Box"],
|
|
530
|
+
invalidAncestors: ["my-namespace:Tooltip"],
|
|
531
|
+
indexWithinAncestor: "my-namespace:Tooltip",
|
|
532
|
+
template: [
|
|
533
|
+
{
|
|
534
|
+
type: "instance",
|
|
535
|
+
component: "my-namespace:Tooltip",
|
|
536
|
+
children: [
|
|
537
|
+
{ type: "text", value: "Some text" },
|
|
538
|
+
{
|
|
539
|
+
type: "instance",
|
|
540
|
+
component: "Box",
|
|
541
|
+
children: [
|
|
542
|
+
{
|
|
543
|
+
type: "instance",
|
|
544
|
+
component: "my-namespace:Button",
|
|
545
|
+
children: [],
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
});
|
|
542
553
|
});
|
package/src/embed-template.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { StyleValue, type StyleProperty } from "@webstudio-is/css-data";
|
|
14
14
|
import type { Simplify } from "type-fest";
|
|
15
15
|
import { encodeDataSourceVariable, validateExpression } from "./expression";
|
|
16
|
+
import type { WsComponentMeta } from "./components/component-meta";
|
|
16
17
|
|
|
17
18
|
const EmbedTemplateText = z.object({
|
|
18
19
|
type: z.literal("text"),
|
|
@@ -21,43 +22,48 @@ const EmbedTemplateText = z.object({
|
|
|
21
22
|
|
|
22
23
|
type EmbedTemplateText = z.infer<typeof EmbedTemplateText>;
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const EmbedTemplateDataSource = z.union([
|
|
26
|
+
z.object({
|
|
27
|
+
type: z.literal("variable"),
|
|
28
|
+
initialValue: z.union([
|
|
29
|
+
z.string(),
|
|
30
|
+
z.number(),
|
|
31
|
+
z.boolean(),
|
|
32
|
+
z.array(z.string()),
|
|
33
|
+
]),
|
|
34
|
+
}),
|
|
31
35
|
z.object({
|
|
32
36
|
type: z.literal("expression"),
|
|
33
|
-
name: z.string(),
|
|
34
37
|
code: z.string(),
|
|
35
38
|
}),
|
|
36
39
|
]);
|
|
37
40
|
|
|
41
|
+
type EmbedTemplateDataSource = z.infer<typeof EmbedTemplateDataSource>;
|
|
42
|
+
|
|
38
43
|
const EmbedTemplateProp = z.union([
|
|
44
|
+
z.object({
|
|
45
|
+
type: z.literal("dataSource"),
|
|
46
|
+
name: z.string(),
|
|
47
|
+
dataSourceName: z.string(),
|
|
48
|
+
}),
|
|
39
49
|
z.object({
|
|
40
50
|
type: z.literal("number"),
|
|
41
51
|
name: z.string(),
|
|
42
|
-
dataSourceRef: z.optional(DataSourceRef),
|
|
43
52
|
value: z.number(),
|
|
44
53
|
}),
|
|
45
54
|
z.object({
|
|
46
55
|
type: z.literal("string"),
|
|
47
56
|
name: z.string(),
|
|
48
|
-
dataSourceRef: z.optional(DataSourceRef),
|
|
49
57
|
value: z.string(),
|
|
50
58
|
}),
|
|
51
59
|
z.object({
|
|
52
60
|
type: z.literal("boolean"),
|
|
53
61
|
name: z.string(),
|
|
54
|
-
dataSourceRef: z.optional(DataSourceRef),
|
|
55
62
|
value: z.boolean(),
|
|
56
63
|
}),
|
|
57
64
|
z.object({
|
|
58
65
|
type: z.literal("string[]"),
|
|
59
66
|
name: z.string(),
|
|
60
|
-
dataSourceRef: z.optional(DataSourceRef),
|
|
61
67
|
value: z.array(z.string()),
|
|
62
68
|
}),
|
|
63
69
|
z.object({
|
|
@@ -95,6 +101,7 @@ export type EmbedTemplateInstance = {
|
|
|
95
101
|
type: "instance";
|
|
96
102
|
component: string;
|
|
97
103
|
label?: string;
|
|
104
|
+
dataSources?: Record<string, EmbedTemplateDataSource>;
|
|
98
105
|
props?: EmbedTemplateProp[];
|
|
99
106
|
styles?: EmbedTemplateStyleDecl[];
|
|
100
107
|
children: Array<EmbedTemplateInstance | EmbedTemplateText>;
|
|
@@ -106,6 +113,7 @@ export const EmbedTemplateInstance: z.ZodType<EmbedTemplateInstance> = z.lazy(
|
|
|
106
113
|
type: z.literal("instance"),
|
|
107
114
|
component: z.string(),
|
|
108
115
|
label: z.optional(z.string()),
|
|
116
|
+
dataSources: z.optional(z.record(z.string(), EmbedTemplateDataSource)),
|
|
109
117
|
props: z.optional(z.array(EmbedTemplateProp)),
|
|
110
118
|
styles: z.optional(z.array(EmbedTemplateStyleDecl)),
|
|
111
119
|
children: WsEmbedTemplate,
|
|
@@ -118,6 +126,25 @@ export const WsEmbedTemplate = z.lazy(() =>
|
|
|
118
126
|
|
|
119
127
|
export type WsEmbedTemplate = z.infer<typeof WsEmbedTemplate>;
|
|
120
128
|
|
|
129
|
+
const getDataSourceValue = (
|
|
130
|
+
value: Extract<EmbedTemplateDataSource, { type: "variable" }>["initialValue"]
|
|
131
|
+
): Extract<DataSource, { type: "variable" }>["value"] => {
|
|
132
|
+
if (typeof value === "string") {
|
|
133
|
+
return { type: "string", value };
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "number") {
|
|
136
|
+
return { type: "number", value };
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === "boolean") {
|
|
139
|
+
return { type: "boolean", value };
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
return { type: "string[]", value };
|
|
143
|
+
}
|
|
144
|
+
value satisfies never;
|
|
145
|
+
throw Error("Impossible case");
|
|
146
|
+
};
|
|
147
|
+
|
|
121
148
|
const createInstancesFromTemplate = (
|
|
122
149
|
treeTemplate: WsEmbedTemplate,
|
|
123
150
|
instances: InstancesList,
|
|
@@ -133,6 +160,38 @@ const createInstancesFromTemplate = (
|
|
|
133
160
|
if (item.type === "instance") {
|
|
134
161
|
const instanceId = nanoid();
|
|
135
162
|
|
|
163
|
+
if (item.dataSources) {
|
|
164
|
+
for (const [name, dataSource] of Object.entries(item.dataSources)) {
|
|
165
|
+
if (dataSourceByRef.has(name)) {
|
|
166
|
+
throw Error(`${name} data source already defined`);
|
|
167
|
+
}
|
|
168
|
+
if (dataSource.type === "variable") {
|
|
169
|
+
dataSourceByRef.set(name, {
|
|
170
|
+
type: "variable",
|
|
171
|
+
id: nanoid(),
|
|
172
|
+
scopeInstanceId: instanceId,
|
|
173
|
+
name,
|
|
174
|
+
value: getDataSourceValue(dataSource.initialValue),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (dataSource.type === "expression") {
|
|
178
|
+
dataSourceByRef.set(name, {
|
|
179
|
+
type: "expression",
|
|
180
|
+
id: nanoid(),
|
|
181
|
+
scopeInstanceId: instanceId,
|
|
182
|
+
name,
|
|
183
|
+
// replace all references with variable names
|
|
184
|
+
code: validateExpression(dataSource.code, {
|
|
185
|
+
transformIdentifier: (ref) => {
|
|
186
|
+
const id = dataSourceByRef.get(ref)?.id ?? ref;
|
|
187
|
+
return encodeDataSourceVariable(id);
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
136
195
|
// populate props
|
|
137
196
|
if (item.props) {
|
|
138
197
|
for (const prop of item.props) {
|
|
@@ -166,51 +225,21 @@ const createInstancesFromTemplate = (
|
|
|
166
225
|
});
|
|
167
226
|
continue;
|
|
168
227
|
}
|
|
169
|
-
if (prop.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let dataSource = dataSourceByRef.get(prop.dataSourceRef.name);
|
|
174
|
-
if (dataSource === undefined) {
|
|
175
|
-
const id = nanoid();
|
|
176
|
-
const { name: propName, dataSourceRef, ...rest } = prop;
|
|
177
|
-
if (dataSourceRef.type === "variable") {
|
|
178
|
-
dataSource = {
|
|
179
|
-
type: "variable",
|
|
180
|
-
id,
|
|
181
|
-
// the first instance where data source is appeared in becomes its scope
|
|
182
|
-
scopeInstanceId: instanceId,
|
|
183
|
-
name: dataSourceRef.name,
|
|
184
|
-
value: rest,
|
|
185
|
-
};
|
|
186
|
-
dataSourceByRef.set(dataSourceRef.name, dataSource);
|
|
187
|
-
} else if (dataSourceRef.type === "expression") {
|
|
188
|
-
dataSource = {
|
|
189
|
-
type: "expression",
|
|
190
|
-
id,
|
|
191
|
-
scopeInstanceId: instanceId,
|
|
192
|
-
name: dataSourceRef.name,
|
|
193
|
-
// replace all references with variable names
|
|
194
|
-
code: validateExpression(dataSourceRef.code, {
|
|
195
|
-
transformIdentifier: (ref) => {
|
|
196
|
-
const id = dataSourceByRef.get(ref)?.id ?? ref;
|
|
197
|
-
return encodeDataSourceVariable(id);
|
|
198
|
-
},
|
|
199
|
-
}),
|
|
200
|
-
};
|
|
201
|
-
dataSourceByRef.set(dataSourceRef.name, dataSource);
|
|
202
|
-
} else {
|
|
203
|
-
dataSourceRef satisfies never;
|
|
204
|
-
continue;
|
|
228
|
+
if (prop.type === "dataSource") {
|
|
229
|
+
const dataSource = dataSourceByRef.get(prop.dataSourceName);
|
|
230
|
+
if (dataSource === undefined) {
|
|
231
|
+
throw Error(`${prop.dataSourceName} data source is not defined`);
|
|
205
232
|
}
|
|
233
|
+
props.push({
|
|
234
|
+
id: propId,
|
|
235
|
+
instanceId,
|
|
236
|
+
type: "dataSource",
|
|
237
|
+
name: prop.name,
|
|
238
|
+
value: dataSource.id,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
206
241
|
}
|
|
207
|
-
props.push({
|
|
208
|
-
id: propId,
|
|
209
|
-
instanceId,
|
|
210
|
-
type: "dataSource",
|
|
211
|
-
name: prop.name,
|
|
212
|
-
value: dataSource.id,
|
|
213
|
-
});
|
|
242
|
+
props.push({ id: propId, instanceId, ...prop });
|
|
214
243
|
}
|
|
215
244
|
}
|
|
216
245
|
|
|
@@ -309,7 +338,7 @@ export type EmbedTemplateData = ReturnType<
|
|
|
309
338
|
typeof generateDataFromEmbedTemplate
|
|
310
339
|
>;
|
|
311
340
|
|
|
312
|
-
|
|
341
|
+
const namespaceEmbedTemplateComponents = (
|
|
313
342
|
template: WsEmbedTemplate,
|
|
314
343
|
namespace: string,
|
|
315
344
|
components: Set<EmbedTemplateInstance["component"]>
|
|
@@ -334,3 +363,34 @@ export const namespaceEmbedTemplateComponents = (
|
|
|
334
363
|
throw Error("Impossible case");
|
|
335
364
|
});
|
|
336
365
|
};
|
|
366
|
+
|
|
367
|
+
export const namespaceMeta = (
|
|
368
|
+
meta: WsComponentMeta,
|
|
369
|
+
namespace: string,
|
|
370
|
+
components: Set<EmbedTemplateInstance["component"]>
|
|
371
|
+
) => {
|
|
372
|
+
const newMeta = { ...meta };
|
|
373
|
+
if (newMeta.requiredAncestors) {
|
|
374
|
+
newMeta.requiredAncestors = newMeta.requiredAncestors.map((component) =>
|
|
375
|
+
components.has(component) ? `${namespace}:${component}` : component
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (newMeta.invalidAncestors) {
|
|
379
|
+
newMeta.invalidAncestors = newMeta.invalidAncestors.map((component) =>
|
|
380
|
+
components.has(component) ? `${namespace}:${component}` : component
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (newMeta.indexWithinAncestor) {
|
|
384
|
+
newMeta.indexWithinAncestor = components.has(newMeta.indexWithinAncestor)
|
|
385
|
+
? `${namespace}:${newMeta.indexWithinAncestor}`
|
|
386
|
+
: newMeta.indexWithinAncestor;
|
|
387
|
+
}
|
|
388
|
+
if (newMeta.template) {
|
|
389
|
+
newMeta.template = namespaceEmbedTemplateComponents(
|
|
390
|
+
newMeta.template,
|
|
391
|
+
namespace,
|
|
392
|
+
components
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return newMeta;
|
|
396
|
+
};
|
package/src/hook.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Instance, Prop } from "@webstudio-is/project-build";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hooks are subscriptions to builder events
|
|
5
|
+
* with limited way to interact with it.
|
|
6
|
+
* Called independently from components.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type HookContext = {
|
|
10
|
+
setPropVariable: (
|
|
11
|
+
instanceId: Instance["id"],
|
|
12
|
+
propName: Prop["name"],
|
|
13
|
+
value: unknown
|
|
14
|
+
) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type InstanceSelection = Instance[];
|
|
18
|
+
|
|
19
|
+
type NavigatorEvent = {
|
|
20
|
+
instanceSelection: InstanceSelection;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type Hook = {
|
|
24
|
+
onNavigatorSelect?: (context: HookContext, event: NavigatorEvent) => void;
|
|
25
|
+
onNavigatorUnselect?: (context: HookContext, event: NavigatorEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getClosestInstance = (
|
|
29
|
+
instanceSelection: InstanceSelection,
|
|
30
|
+
currentInstance: Instance,
|
|
31
|
+
closestComponent: Instance["component"]
|
|
32
|
+
) => {
|
|
33
|
+
let matched = false;
|
|
34
|
+
for (const instance of instanceSelection) {
|
|
35
|
+
if (currentInstance === instance) {
|
|
36
|
+
matched = true;
|
|
37
|
+
}
|
|
38
|
+
if (matched && instance.component === closestComponent) {
|
|
39
|
+
return instance;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
usePropUrl,
|
|
19
19
|
usePropAsset,
|
|
20
20
|
getInstanceIdFromComponentProps,
|
|
21
|
+
getIndexWithinAncestorFromComponentProps,
|
|
21
22
|
} from "./props";
|
|
22
23
|
export { type Params, ReactSdkContext } from "./context";
|
|
23
24
|
export {
|
|
@@ -32,3 +33,6 @@ export {
|
|
|
32
33
|
decodeDataSourceVariable,
|
|
33
34
|
decodeVariablesMap,
|
|
34
35
|
} from "./expression";
|
|
36
|
+
export { renderComponentTemplate } from "./component-renderer";
|
|
37
|
+
export { getIndexesWithinAncestors } from "./instance-utils";
|
|
38
|
+
export * from "./hook";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { test, expect } from "@jest/globals";
|
|
2
|
+
import type { Instance, Instances } from "@webstudio-is/project-build";
|
|
3
|
+
import { getIndexesWithinAncestors } from "./instance-utils";
|
|
4
|
+
import type { WsComponentMeta } from ".";
|
|
5
|
+
|
|
6
|
+
const getIdValuePair = <T extends { id: string }>(item: T) =>
|
|
7
|
+
[item.id, item] as const;
|
|
8
|
+
|
|
9
|
+
const toMap = <T extends { id: string }>(list: T[]) =>
|
|
10
|
+
new Map(list.map(getIdValuePair));
|
|
11
|
+
|
|
12
|
+
const createInstance = (
|
|
13
|
+
id: Instance["id"],
|
|
14
|
+
component: string,
|
|
15
|
+
children: Instance["children"]
|
|
16
|
+
): Instance => {
|
|
17
|
+
return { type: "instance", id, component, children };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const createMeta = (meta?: Partial<WsComponentMeta>) => {
|
|
21
|
+
return { type: "container", label: "", icon: "", ...meta } as const;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test("get indexes within ancestors", () => {
|
|
25
|
+
// body0
|
|
26
|
+
// tabs1
|
|
27
|
+
// tabs1list
|
|
28
|
+
// tabs1box
|
|
29
|
+
// tabs1trigger1
|
|
30
|
+
// tabs1trigger2
|
|
31
|
+
// tabs1content1
|
|
32
|
+
// tabs2
|
|
33
|
+
// tabs2list
|
|
34
|
+
// tabs2trigger1
|
|
35
|
+
// tabs2content1
|
|
36
|
+
// tabs1content2
|
|
37
|
+
const instances: Instances = toMap([
|
|
38
|
+
createInstance("body0", "Body", [{ type: "id", value: "tabs1" }]),
|
|
39
|
+
// tabs1
|
|
40
|
+
createInstance("tabs1", "Tabs", [
|
|
41
|
+
{ type: "id", value: "tabs1list" },
|
|
42
|
+
{ type: "id", value: "tabs1content1" },
|
|
43
|
+
{ type: "id", value: "tabs1content2" },
|
|
44
|
+
]),
|
|
45
|
+
createInstance("tabs1list", "TabsList", [
|
|
46
|
+
{ type: "id", value: "tabs1box" },
|
|
47
|
+
]),
|
|
48
|
+
createInstance("tabs1box", "Box", [
|
|
49
|
+
{ type: "id", value: "tabs1trigger1" },
|
|
50
|
+
{ type: "id", value: "tabs1trigger2" },
|
|
51
|
+
]),
|
|
52
|
+
createInstance("tabs1trigger1", "TabsTrigger", []),
|
|
53
|
+
createInstance("tabs1trigger2", "TabsTrigger", []),
|
|
54
|
+
createInstance("tabs1content1", "TabsContent", [
|
|
55
|
+
{ type: "id", value: "tabs2" },
|
|
56
|
+
]),
|
|
57
|
+
createInstance("tabs1content2", "TabsContent", []),
|
|
58
|
+
// tabs2
|
|
59
|
+
createInstance("tabs2", "Tabs", [
|
|
60
|
+
{ type: "id", value: "tabs2list" },
|
|
61
|
+
{ type: "id", value: "tabs2content1" },
|
|
62
|
+
]),
|
|
63
|
+
createInstance("tabs2list", "TabsList", [
|
|
64
|
+
{ type: "id", value: "tabs2trigger1" },
|
|
65
|
+
]),
|
|
66
|
+
createInstance("tabs2trigger1", "TabsTrigger", []),
|
|
67
|
+
createInstance("tabs2content1", "TabsContent", []),
|
|
68
|
+
] satisfies Instance[]);
|
|
69
|
+
const metas = new Map<Instance["component"], WsComponentMeta>([
|
|
70
|
+
["Body", createMeta()],
|
|
71
|
+
["Box", createMeta()],
|
|
72
|
+
["Tabs", createMeta()],
|
|
73
|
+
["TabsList", createMeta({ indexWithinAncestor: "Tabs" })],
|
|
74
|
+
["TabsTrigger", createMeta({ indexWithinAncestor: "TabsList" })],
|
|
75
|
+
["TabsContent", createMeta({ indexWithinAncestor: "Tabs" })],
|
|
76
|
+
]);
|
|
77
|
+
expect(getIndexesWithinAncestors(metas, instances, ["body0"])).toEqual(
|
|
78
|
+
new Map([
|
|
79
|
+
["tabs1list", 0],
|
|
80
|
+
["tabs1trigger1", 0],
|
|
81
|
+
["tabs1trigger2", 1],
|
|
82
|
+
["tabs1content1", 0],
|
|
83
|
+
["tabs1content2", 1],
|
|
84
|
+
["tabs2list", 0],
|
|
85
|
+
["tabs2trigger1", 0],
|
|
86
|
+
["tabs2content1", 0],
|
|
87
|
+
])
|
|
88
|
+
);
|
|
89
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Instance, Instances } from "@webstudio-is/project-build";
|
|
2
|
+
import type { WsComponentMeta } from "./components/component-meta";
|
|
3
|
+
|
|
4
|
+
export type IndexesWithinAncestors = Map<Instance["id"], number>;
|
|
5
|
+
|
|
6
|
+
export const getIndexesWithinAncestors = (
|
|
7
|
+
metas: Map<Instance["component"], WsComponentMeta>,
|
|
8
|
+
instances: Instances,
|
|
9
|
+
rootIds: Instance["id"][]
|
|
10
|
+
) => {
|
|
11
|
+
const ancestors = new Set<Instance["component"]>();
|
|
12
|
+
for (const meta of metas.values()) {
|
|
13
|
+
if (meta.indexWithinAncestor !== undefined) {
|
|
14
|
+
ancestors.add(meta.indexWithinAncestor);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const indexes: IndexesWithinAncestors = new Map();
|
|
19
|
+
|
|
20
|
+
const traverseInstances = (
|
|
21
|
+
instances: Instances,
|
|
22
|
+
instanceId: Instance["id"],
|
|
23
|
+
latestIndexes = new Map<
|
|
24
|
+
Instance["component"],
|
|
25
|
+
Map<Instance["component"], number>
|
|
26
|
+
>()
|
|
27
|
+
) => {
|
|
28
|
+
const instance = instances.get(instanceId);
|
|
29
|
+
if (instance === undefined) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const meta = metas.get(instance.component);
|
|
33
|
+
if (meta === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ancestors.has(instance.component)) {
|
|
38
|
+
latestIndexes = new Map(latestIndexes);
|
|
39
|
+
latestIndexes.set(instance.component, new Map());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (meta.indexWithinAncestor !== undefined) {
|
|
43
|
+
const ancestorIndexes = latestIndexes.get(meta.indexWithinAncestor);
|
|
44
|
+
if (ancestorIndexes !== undefined) {
|
|
45
|
+
let index = ancestorIndexes.get(instance.component) ?? -1;
|
|
46
|
+
index += 1;
|
|
47
|
+
ancestorIndexes.set(instance.component, index);
|
|
48
|
+
indexes.set(instance.id, index);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const child of instance.children) {
|
|
53
|
+
if (child.type === "id") {
|
|
54
|
+
traverseInstances(instances, child.value, latestIndexes);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const latestIndexes = new Map();
|
|
60
|
+
for (const instanceId of rootIds) {
|
|
61
|
+
traverseInstances(instances, instanceId, latestIndexes);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return indexes;
|
|
65
|
+
};
|