@_linked/react 0.0.1
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/.context/jest-repro-bundler.config.js +20 -0
- package/.context/jest-repro.config.js +20 -0
- package/.context/notes.md +0 -0
- package/.context/todos.md +0 -0
- package/.context/tsconfig-repro-bundler.json +14 -0
- package/.context/tsconfig-repro-no-paths.json +12 -0
- package/.context/tsconfig-repro-node-modules-paths.json +16 -0
- package/.context/tsconfig-repro-node16.json +14 -0
- package/AGENTS.md +59 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/docs/001-react-extraction.md +361 -0
- package/jest.config.js +20 -0
- package/lib/cjs/index.d.ts +4 -0
- package/lib/cjs/index.js +21 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/package.d.ts +10 -0
- package/lib/cjs/package.js +33 -0
- package/lib/cjs/package.js.map +1 -0
- package/lib/cjs/package.json +3 -0
- package/lib/cjs/utils/Hooks.d.ts +5 -0
- package/lib/cjs/utils/Hooks.js +54 -0
- package/lib/cjs/utils/Hooks.js.map +1 -0
- package/lib/cjs/utils/LinkedComponent.d.ts +52 -0
- package/lib/cjs/utils/LinkedComponent.js +322 -0
- package/lib/cjs/utils/LinkedComponent.js.map +1 -0
- package/lib/cjs/utils/LinkedComponentClass.d.ts +11 -0
- package/lib/cjs/utils/LinkedComponentClass.js +34 -0
- package/lib/cjs/utils/LinkedComponentClass.js.map +1 -0
- package/lib/esm/index.d.ts +4 -0
- package/lib/esm/index.js +5 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/package.d.ts +10 -0
- package/lib/esm/package.js +22 -0
- package/lib/esm/package.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/lib/esm/utils/Hooks.d.ts +5 -0
- package/lib/esm/utils/Hooks.js +50 -0
- package/lib/esm/utils/Hooks.js.map +1 -0
- package/lib/esm/utils/LinkedComponent.d.ts +52 -0
- package/lib/esm/utils/LinkedComponent.js +284 -0
- package/lib/esm/utils/LinkedComponent.js.map +1 -0
- package/lib/esm/utils/LinkedComponentClass.d.ts +11 -0
- package/lib/esm/utils/LinkedComponentClass.js +27 -0
- package/lib/esm/utils/LinkedComponentClass.js.map +1 -0
- package/package.json +57 -0
- package/scripts/dual-package.js +25 -0
- package/src/index.ts +4 -0
- package/src/package.ts +62 -0
- package/src/tests/react-component-behavior.test.tsx +578 -0
- package/src/tests/react-component-integration.test.tsx +378 -0
- package/src/utils/Hooks.ts +56 -0
- package/src/utils/LinkedComponent.ts +545 -0
- package/src/utils/LinkedComponentClass.tsx +37 -0
- package/tsconfig-cjs.json +8 -0
- package/tsconfig-esm.json +8 -0
- package/tsconfig-test.json +15 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GetCustomObjectKeys,
|
|
3
|
+
GetQueryResponseType,
|
|
4
|
+
GetQueryShapeType,
|
|
5
|
+
QResult,
|
|
6
|
+
QueryController,
|
|
7
|
+
QueryControllerProps,
|
|
8
|
+
QueryResponseToResultType,
|
|
9
|
+
QueryWrapperObject,
|
|
10
|
+
SelectQueryFactory,
|
|
11
|
+
ToQueryResultSet,
|
|
12
|
+
} from '@_linked/core/queries/SelectQuery';
|
|
13
|
+
import {Shape} from '@_linked/core/shapes/Shape';
|
|
14
|
+
|
|
15
|
+
import React, {createElement, useCallback, useEffect, useState} from 'react';
|
|
16
|
+
import {LinkedStorage} from '@_linked/core/utils/LinkedStorage';
|
|
17
|
+
import {DEFAULT_LIMIT} from '@_linked/core/utils/Package';
|
|
18
|
+
import {ShapeSet} from '@_linked/core/collections/ShapeSet';
|
|
19
|
+
import {isNodeReferenceValue, NodeReferenceValue} from '@_linked/core/utils/NodeReference';
|
|
20
|
+
import {getShapeClass, hasSuperClass} from '@_linked/core/utils/ShapeClass';
|
|
21
|
+
|
|
22
|
+
// Kept for parity with legacy source shape processing.
|
|
23
|
+
type ProcessDataResultType<ShapeType extends Shape> = [
|
|
24
|
+
typeof Shape,
|
|
25
|
+
SelectQueryFactory<ShapeType>,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export type Component<P = any, ShapeType extends Shape = Shape> =
|
|
29
|
+
| ClassComponent<P, ShapeType>
|
|
30
|
+
| LinkedComponent<P, ShapeType>
|
|
31
|
+
| LinkedSetComponent<P, ShapeType>;
|
|
32
|
+
|
|
33
|
+
export interface ClassComponent<P, ShapeType extends Shape = Shape>
|
|
34
|
+
extends React.ComponentClass<P & LinkedComponentProps<ShapeType>> {
|
|
35
|
+
props: P & LinkedComponentProps<ShapeType>;
|
|
36
|
+
shape?: typeof Shape;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LinkedComponent<
|
|
40
|
+
P,
|
|
41
|
+
ShapeType extends Shape = Shape,
|
|
42
|
+
ResultType = any,
|
|
43
|
+
> extends React.FC<
|
|
44
|
+
P & LinkedComponentInputProps<ShapeType> & React.ComponentPropsWithRef<any>
|
|
45
|
+
> {
|
|
46
|
+
original?: LinkableComponent<P, ShapeType>;
|
|
47
|
+
query: SelectQueryFactory<any>;
|
|
48
|
+
shape?: typeof Shape;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LinkedSetComponent<
|
|
52
|
+
P,
|
|
53
|
+
ShapeType extends Shape = Shape,
|
|
54
|
+
Res = any,
|
|
55
|
+
> extends React.FC<
|
|
56
|
+
P &
|
|
57
|
+
LinkedSetComponentInputProps<ShapeType> &
|
|
58
|
+
React.ComponentPropsWithRef<any>
|
|
59
|
+
> {
|
|
60
|
+
original?: LinkableSetComponent<P, ShapeType>;
|
|
61
|
+
query: SelectQueryFactory<any> | QueryWrapperObject<ShapeType>;
|
|
62
|
+
shape?: typeof Shape;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type LinkableComponent<P, ShapeType extends Shape = Shape> = React.FC<
|
|
66
|
+
P & LinkedComponentProps<ShapeType>
|
|
67
|
+
>;
|
|
68
|
+
export type LinkableSetComponent<
|
|
69
|
+
P,
|
|
70
|
+
ShapeType extends Shape = Shape,
|
|
71
|
+
DataResultType = any,
|
|
72
|
+
> = React.FC<LinkedSetComponentProps<ShapeType, DataResultType> & P>;
|
|
73
|
+
|
|
74
|
+
export interface LinkedSetComponentProps<
|
|
75
|
+
ShapeType extends Shape,
|
|
76
|
+
DataResultType = any,
|
|
77
|
+
> extends LinkedComponentBaseProps<DataResultType>,
|
|
78
|
+
QueryControllerProps {
|
|
79
|
+
sources: ShapeSet<ShapeType>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface LinkedComponentProps<ShapeType extends Shape>
|
|
83
|
+
extends LinkedComponentBaseProps {
|
|
84
|
+
source: ShapeType;
|
|
85
|
+
_refresh: (updatedProps?: any) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface LinkedComponentBaseProps<DataResultType = any>
|
|
89
|
+
extends React.PropsWithChildren {
|
|
90
|
+
linkedData?: DataResultType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface LinkedSetComponentInputProps<ShapeType extends Shape = Shape>
|
|
94
|
+
extends LinkedComponentInputBaseProps {
|
|
95
|
+
of?: ShapeSet<ShapeType> | QResult<ShapeType>[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LinkedComponentInputProps<ShapeType extends Shape = Shape>
|
|
99
|
+
extends LinkedComponentInputBaseProps {
|
|
100
|
+
of: NodeReferenceValue | ShapeType | QResult<ShapeType>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface LinkedComponentInputBaseProps extends React.PropsWithChildren {
|
|
104
|
+
className?: string | string[];
|
|
105
|
+
style?: React.CSSProperties;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type LinkedSetComponentFactoryFn = <
|
|
109
|
+
QueryType extends
|
|
110
|
+
| SelectQueryFactory<any>
|
|
111
|
+
| {[key: string]: SelectQueryFactory<any>} = null,
|
|
112
|
+
CustomProps = {},
|
|
113
|
+
ShapeType extends Shape = GetQueryShapeType<QueryType>,
|
|
114
|
+
Res = ToQueryResultSet<QueryType>,
|
|
115
|
+
>(
|
|
116
|
+
requiredData: QueryType,
|
|
117
|
+
functionalComponent: LinkableSetComponent<
|
|
118
|
+
CustomProps & GetCustomObjectKeys<QueryType> & QueryControllerProps,
|
|
119
|
+
ShapeType,
|
|
120
|
+
Res
|
|
121
|
+
>,
|
|
122
|
+
) => LinkedSetComponent<CustomProps, ShapeType, Res>;
|
|
123
|
+
|
|
124
|
+
export type LinkedComponentFactoryFn = <
|
|
125
|
+
QueryType extends SelectQueryFactory<any> = null,
|
|
126
|
+
CustomProps = {},
|
|
127
|
+
ShapeType extends Shape = GetQueryShapeType<QueryType>,
|
|
128
|
+
Response = GetQueryResponseType<QueryType>,
|
|
129
|
+
ResultType = QueryResponseToResultType<Response, ShapeType>,
|
|
130
|
+
>(
|
|
131
|
+
query: QueryType,
|
|
132
|
+
functionalComponent: LinkableComponent<CustomProps & ResultType, ShapeType>,
|
|
133
|
+
) => LinkedComponent<CustomProps, ShapeType, ResultType>;
|
|
134
|
+
|
|
135
|
+
export function createLinkedComponentFn(
|
|
136
|
+
registerPackageExport,
|
|
137
|
+
registerComponent,
|
|
138
|
+
) {
|
|
139
|
+
return function linkedComponent<
|
|
140
|
+
QueryType extends SelectQueryFactory<any> = null,
|
|
141
|
+
CustomProps = {},
|
|
142
|
+
ShapeType extends Shape = GetQueryShapeType<QueryType>,
|
|
143
|
+
Res = GetQueryResponseType<QueryType>,
|
|
144
|
+
>(
|
|
145
|
+
query: QueryType,
|
|
146
|
+
functionalComponent: LinkableComponent<
|
|
147
|
+
CustomProps &
|
|
148
|
+
QueryResponseToResultType<Res, ShapeType>,
|
|
149
|
+
ShapeType
|
|
150
|
+
>,
|
|
151
|
+
): LinkedComponent<CustomProps, ShapeType, Res> {
|
|
152
|
+
let [shapeClass, actualQuery] = processQuery<ShapeType>(query);
|
|
153
|
+
|
|
154
|
+
let _wrappedComponent: LinkedComponent<CustomProps, ShapeType> =
|
|
155
|
+
React.forwardRef<any, CustomProps & LinkedComponentInputProps<ShapeType>>(
|
|
156
|
+
(props, ref) => {
|
|
157
|
+
let [queryResult, setQueryResult] = useState<any>(undefined);
|
|
158
|
+
let [loadingData, setLoadingData] = useState<string>();
|
|
159
|
+
|
|
160
|
+
let linkedProps: any = getLinkedComponentProps<
|
|
161
|
+
ShapeType,
|
|
162
|
+
CustomProps
|
|
163
|
+
>(props as any, shapeClass);
|
|
164
|
+
if (ref) {
|
|
165
|
+
linkedProps.ref = ref;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const loadData = () => {
|
|
169
|
+
const sourceId = linkedProps.source?.id;
|
|
170
|
+
if (!loadingData || loadingData !== sourceId) {
|
|
171
|
+
let requestQuery = (
|
|
172
|
+
actualQuery as SelectQueryFactory<any>
|
|
173
|
+
).clone();
|
|
174
|
+
if (linkedProps.source) {
|
|
175
|
+
requestQuery.setSubject(linkedProps.source);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setLoadingData(sourceId || requestQuery.subject?.id);
|
|
179
|
+
const parser =
|
|
180
|
+
(shapeClass as typeof Shape).queryParser || Shape.queryParser;
|
|
181
|
+
if (!parser) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`No query parser configured for ${shapeClass?.name || 'shape'}.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
parser.selectQuery(requestQuery).then((result) => {
|
|
187
|
+
setQueryResult(result);
|
|
188
|
+
setLoadingData(null);
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
console.warn(
|
|
192
|
+
`Already loading data for source ${loadingData}, ignoring request`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let sourceIsValidQResult = isValidQResult(props.of, query);
|
|
198
|
+
|
|
199
|
+
if (queryResult || sourceIsValidQResult) {
|
|
200
|
+
linkedProps = Object.assign(linkedProps, queryResult || props.of);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
linkedProps._refresh = useCallback(
|
|
204
|
+
(updatedProps) => {
|
|
205
|
+
if (updatedProps) {
|
|
206
|
+
if (queryResult) {
|
|
207
|
+
setQueryResult({...queryResult, ...updatedProps});
|
|
208
|
+
} else if (sourceIsValidQResult) {
|
|
209
|
+
setQueryResult({...props.of, ...updatedProps});
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
loadData();
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
[queryResult, props.of],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (!linkedProps.source && !actualQuery.subject) {
|
|
219
|
+
console.warn(
|
|
220
|
+
'This component requires a source to be provided (use the property "of"): ' +
|
|
221
|
+
functionalComponent.name,
|
|
222
|
+
);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let usingStorage = LinkedStorage.isInitialised();
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (queryResult) {
|
|
230
|
+
setQueryResult(undefined);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (usingStorage && !sourceIsValidQResult) {
|
|
234
|
+
loadData();
|
|
235
|
+
}
|
|
236
|
+
}, [linkedProps.source?.id]);
|
|
237
|
+
|
|
238
|
+
let dataIsLoaded =
|
|
239
|
+
queryResult || !usingStorage || sourceIsValidQResult;
|
|
240
|
+
|
|
241
|
+
// Keep legacy client-side guard to avoid hydration drift.
|
|
242
|
+
if (dataIsLoaded && typeof window !== 'undefined') {
|
|
243
|
+
return React.createElement(functionalComponent, linkedProps);
|
|
244
|
+
} else {
|
|
245
|
+
return createLoadingSpinner();
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
) as any;
|
|
249
|
+
|
|
250
|
+
_wrappedComponent.original = functionalComponent;
|
|
251
|
+
_wrappedComponent.query = query;
|
|
252
|
+
_wrappedComponent.shape = shapeClass;
|
|
253
|
+
if (functionalComponent.name) {
|
|
254
|
+
Object.defineProperty(_wrappedComponent, 'name', {
|
|
255
|
+
value: functionalComponent.name,
|
|
256
|
+
});
|
|
257
|
+
registerPackageExport(_wrappedComponent);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
registerComponent(_wrappedComponent, shapeClass);
|
|
261
|
+
|
|
262
|
+
return _wrappedComponent;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function createLinkedSetComponentFn(
|
|
267
|
+
registerPackageExport,
|
|
268
|
+
registerComponent,
|
|
269
|
+
) {
|
|
270
|
+
return function linkedSetComponent<
|
|
271
|
+
QueryType extends
|
|
272
|
+
| SelectQueryFactory<any>
|
|
273
|
+
| {[key: string]: SelectQueryFactory<any>} = null,
|
|
274
|
+
CustomProps = {},
|
|
275
|
+
ShapeType extends Shape = GetQueryShapeType<QueryType>,
|
|
276
|
+
Res = ToQueryResultSet<QueryType>,
|
|
277
|
+
>(
|
|
278
|
+
query: QueryType,
|
|
279
|
+
functionalComponent: LinkableSetComponent<
|
|
280
|
+
CustomProps & GetCustomObjectKeys<QueryType> & QueryControllerProps,
|
|
281
|
+
ShapeType
|
|
282
|
+
>,
|
|
283
|
+
): LinkedSetComponent<CustomProps, ShapeType, Res> {
|
|
284
|
+
let [shapeClass, actualQuery] = processQuery<ShapeType>(query as any, true);
|
|
285
|
+
|
|
286
|
+
let usingStorage = LinkedStorage.isInitialised();
|
|
287
|
+
|
|
288
|
+
let _wrappedComponent: LinkedSetComponent<CustomProps, ShapeType, Res> =
|
|
289
|
+
React.forwardRef<
|
|
290
|
+
any,
|
|
291
|
+
CustomProps & LinkedSetComponentInputProps<ShapeType>
|
|
292
|
+
>((props, ref) => {
|
|
293
|
+
let [queryResult, setQueryResult] = useState<any>(undefined);
|
|
294
|
+
|
|
295
|
+
let linkedProps = getLinkedSetComponentProps<
|
|
296
|
+
ShapeType,
|
|
297
|
+
any
|
|
298
|
+
>(props, shapeClass, functionalComponent);
|
|
299
|
+
|
|
300
|
+
let defaultLimit = actualQuery.getLimit() || DEFAULT_LIMIT;
|
|
301
|
+
let [limit, setLimit] = useState<number>(defaultLimit);
|
|
302
|
+
let [offset, setOffset] = useState<number>(0);
|
|
303
|
+
|
|
304
|
+
if (ref) {
|
|
305
|
+
(linkedProps as any).ref = ref;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let sourceIsValidQResult =
|
|
309
|
+
Array.isArray(props.of) &&
|
|
310
|
+
props.of.length > 0 &&
|
|
311
|
+
typeof (props.of[0] as QResult<any>)?.id === 'string' &&
|
|
312
|
+
actualQuery.isValidSetResult(props.of as QResult<any>[]);
|
|
313
|
+
|
|
314
|
+
if (queryResult || sourceIsValidQResult) {
|
|
315
|
+
let dataResult;
|
|
316
|
+
if (queryResult) {
|
|
317
|
+
dataResult = queryResult;
|
|
318
|
+
} else {
|
|
319
|
+
if (limit) {
|
|
320
|
+
dataResult = (props.of as Array<QResult<any>>).slice(
|
|
321
|
+
offset || 0,
|
|
322
|
+
offset + limit,
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
dataResult = props.of;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (query instanceof SelectQueryFactory) {
|
|
329
|
+
linkedProps = Object.assign(linkedProps, {
|
|
330
|
+
linkedData: dataResult,
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
let key = Object.keys(query)[0];
|
|
334
|
+
linkedProps[key] = dataResult;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (limit) {
|
|
339
|
+
linkedProps.query = {
|
|
340
|
+
nextPage: () => {
|
|
341
|
+
setOffset(offset + limit);
|
|
342
|
+
},
|
|
343
|
+
previousPage: () => {
|
|
344
|
+
setOffset(Math.max(0, offset - limit));
|
|
345
|
+
},
|
|
346
|
+
setLimit: (newLimit: number) => {
|
|
347
|
+
setLimit(newLimit);
|
|
348
|
+
},
|
|
349
|
+
setPage: (page: number) => {
|
|
350
|
+
setOffset(page * limit);
|
|
351
|
+
},
|
|
352
|
+
} as QueryController;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
if (usingStorage && !sourceIsValidQResult) {
|
|
357
|
+
let requestQuery = (actualQuery as SelectQueryFactory<any>).clone();
|
|
358
|
+
requestQuery.setSubject(linkedProps.sources);
|
|
359
|
+
|
|
360
|
+
if (limit) {
|
|
361
|
+
requestQuery.setLimit(limit);
|
|
362
|
+
}
|
|
363
|
+
if (offset) {
|
|
364
|
+
requestQuery.setOffset(offset);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const parser =
|
|
368
|
+
(shapeClass as typeof Shape).queryParser || Shape.queryParser;
|
|
369
|
+
if (!parser) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`No query parser configured for ${shapeClass?.name || 'shape'}.`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
parser.selectQuery(requestQuery).then((result) => {
|
|
375
|
+
setQueryResult(result);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}, [props.of, limit, offset]);
|
|
379
|
+
|
|
380
|
+
let dataIsLoaded = queryResult || !usingStorage || sourceIsValidQResult;
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
typeof queryResult === 'undefined' &&
|
|
384
|
+
usingStorage &&
|
|
385
|
+
!sourceIsValidQResult
|
|
386
|
+
) {
|
|
387
|
+
dataIsLoaded = false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (dataIsLoaded) {
|
|
391
|
+
return React.createElement(functionalComponent, linkedProps);
|
|
392
|
+
} else {
|
|
393
|
+
return createLoadingSpinner();
|
|
394
|
+
}
|
|
395
|
+
}) as any;
|
|
396
|
+
|
|
397
|
+
_wrappedComponent.original = functionalComponent;
|
|
398
|
+
_wrappedComponent.query = query;
|
|
399
|
+
|
|
400
|
+
_wrappedComponent.shape = shapeClass;
|
|
401
|
+
if (functionalComponent.name) {
|
|
402
|
+
Object.defineProperty(_wrappedComponent, 'name', {
|
|
403
|
+
value: functionalComponent.name,
|
|
404
|
+
});
|
|
405
|
+
registerPackageExport(_wrappedComponent);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
registerComponent(_wrappedComponent, shapeClass);
|
|
409
|
+
|
|
410
|
+
return _wrappedComponent;
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getLinkedComponentProps<ShapeType extends Shape, P>(
|
|
415
|
+
props: LinkedComponentInputProps<ShapeType> & P,
|
|
416
|
+
shapeClass,
|
|
417
|
+
): Omit<LinkedComponentProps<ShapeType>, '_refresh'> & P {
|
|
418
|
+
let newProps = {
|
|
419
|
+
...props,
|
|
420
|
+
source: getSourceFromInputProps(props, shapeClass),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
if (newProps.of) {
|
|
424
|
+
for (let key of Object.getOwnPropertyNames(newProps.of)) {
|
|
425
|
+
if (key !== 'shape' && key !== 'id') {
|
|
426
|
+
newProps[key] = (newProps.of as any)[key];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
delete (newProps as any).of;
|
|
432
|
+
return newProps;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function processQuery<ShapeType extends Shape>(
|
|
436
|
+
requiredData: SelectQueryFactory<ShapeType> | QueryWrapperObject<ShapeType>,
|
|
437
|
+
setComponent: boolean = false,
|
|
438
|
+
): ProcessDataResultType<ShapeType> {
|
|
439
|
+
let shapeClass: typeof Shape;
|
|
440
|
+
let query: SelectQueryFactory<ShapeType>;
|
|
441
|
+
|
|
442
|
+
if (requiredData instanceof SelectQueryFactory) {
|
|
443
|
+
query = requiredData;
|
|
444
|
+
shapeClass = requiredData.shape as any;
|
|
445
|
+
} else if (typeof requiredData === 'object' && setComponent) {
|
|
446
|
+
if (Object.keys(requiredData).length > 1) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
'Only one key is allowed to map a query to a property for linkedSetComponents',
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
for (let key in requiredData) {
|
|
452
|
+
if (requiredData[key] instanceof SelectQueryFactory) {
|
|
453
|
+
shapeClass = requiredData[key].shape as any;
|
|
454
|
+
query = requiredData[key];
|
|
455
|
+
} else {
|
|
456
|
+
throw new Error(
|
|
457
|
+
'Unknown value type for query object. Keep to this format: {propName: Shape.query(s => ...)}',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
throw new Error(
|
|
463
|
+
'Unknown data query type. Expected a LinkedQuery (from Shape.query()) or an object with 1 key whose value is a LinkedQuery',
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return [shapeClass, query];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function getLinkedSetComponentProps<ShapeType extends Shape, P>(
|
|
470
|
+
props: LinkedSetComponentInputProps<ShapeType>,
|
|
471
|
+
shapeClass,
|
|
472
|
+
functionalComponent,
|
|
473
|
+
): LinkedSetComponentProps<ShapeType> & P {
|
|
474
|
+
if (
|
|
475
|
+
props.of &&
|
|
476
|
+
!(props.of instanceof ShapeSet) &&
|
|
477
|
+
!Array.isArray(props.of)
|
|
478
|
+
) {
|
|
479
|
+
throw Error(
|
|
480
|
+
"Invalid argument 'of' provided to " +
|
|
481
|
+
functionalComponent.name.replace('_implementation', '') +
|
|
482
|
+
' component: ' +
|
|
483
|
+
props.of +
|
|
484
|
+
'. Make sure to provide a ShapeSet, an array of QResults, or no argument at all to load all instances.',
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let sources: ShapeSet<ShapeType>;
|
|
489
|
+
if (props.of instanceof ShapeSet) {
|
|
490
|
+
sources = props.of;
|
|
491
|
+
} else if (Array.isArray(props.of)) {
|
|
492
|
+
sources = new ShapeSet(
|
|
493
|
+
props.of.map((item) => {
|
|
494
|
+
return getSourceFromInputProps({of: item}, shapeClass);
|
|
495
|
+
}),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const newProps = {
|
|
500
|
+
...props,
|
|
501
|
+
sources,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
delete (newProps as any).of;
|
|
505
|
+
return newProps as LinkedSetComponentProps<ShapeType> & P;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function getSourceFromInputProps(props, shapeClass) {
|
|
509
|
+
const input = props?.of;
|
|
510
|
+
|
|
511
|
+
if (input instanceof Shape) {
|
|
512
|
+
if (
|
|
513
|
+
input.nodeShape !== shapeClass.shape &&
|
|
514
|
+
!hasSuperClass(getShapeClass(input.nodeShape.id), shapeClass)
|
|
515
|
+
) {
|
|
516
|
+
return new shapeClass(input.id);
|
|
517
|
+
}
|
|
518
|
+
return input;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (isNodeReferenceValue(input)) {
|
|
522
|
+
return new shapeClass(input);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// If nothing is provided, keep undefined; callers handle required source checks.
|
|
526
|
+
return input;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isValidQResult(of, query) {
|
|
530
|
+
return (
|
|
531
|
+
typeof (of as QResult<any>)?.id === 'string' &&
|
|
532
|
+
query.isValidResult(of as QResult<any>)
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function createLoadingSpinner() {
|
|
537
|
+
return React.createElement(
|
|
538
|
+
'div',
|
|
539
|
+
{
|
|
540
|
+
className: 'ld-loader',
|
|
541
|
+
'aria-label': 'Loading',
|
|
542
|
+
role: 'status',
|
|
543
|
+
},
|
|
544
|
+
);
|
|
545
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Shape} from '@_linked/core/shapes/Shape';
|
|
3
|
+
import {LinkedComponentProps} from './LinkedComponent.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Class component base for linked components.
|
|
7
|
+
*/
|
|
8
|
+
export class LinkedComponentClass<
|
|
9
|
+
ShapeClass extends Shape,
|
|
10
|
+
P = {},
|
|
11
|
+
S = any,
|
|
12
|
+
> extends React.Component<P & LinkedComponentProps<ShapeClass>, S> {
|
|
13
|
+
private _shape: ShapeClass;
|
|
14
|
+
|
|
15
|
+
get sourceShape(): ShapeClass {
|
|
16
|
+
if (typeof this._shape === 'undefined') {
|
|
17
|
+
if (!this.props.source) {
|
|
18
|
+
this._shape = null;
|
|
19
|
+
} else {
|
|
20
|
+
let shapeClass = this.constructor['shape'];
|
|
21
|
+
if (!shapeClass) {
|
|
22
|
+
throw new Error(`${this.constructor.name} is not linked to a shape`);
|
|
23
|
+
}
|
|
24
|
+
this._shape = new shapeClass(this.props.source) as ShapeClass;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return this._shape;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
componentDidUpdate(
|
|
31
|
+
prevProps: Readonly<P & LinkedComponentProps<ShapeClass>>,
|
|
32
|
+
) {
|
|
33
|
+
if (prevProps.source !== this.props.source) {
|
|
34
|
+
this._shape = undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@_linked/react": ["./src/index"],
|
|
8
|
+
"@_linked/react/*": ["./src/*"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"./src/**/*.ts",
|
|
13
|
+
"./src/**/*.tsx"
|
|
14
|
+
]
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"sourceMap": true,
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"experimentalDecorators": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"downlevelIteration": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"pretty": true,
|
|
12
|
+
"target": "es6",
|
|
13
|
+
"jsx": "react",
|
|
14
|
+
"moduleResolution": "node",
|
|
15
|
+
"types": ["node", "jest"],
|
|
16
|
+
"baseUrl": "./",
|
|
17
|
+
"paths": {
|
|
18
|
+
"@_linked/core": ["../core/src/index"],
|
|
19
|
+
"@_linked/core/*": ["../core/src/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": [
|
|
23
|
+
"./src/**/*.ts",
|
|
24
|
+
"./src/**/*.tsx"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"./src/tests/**"
|
|
28
|
+
]
|
|
29
|
+
}
|