@pihanga2/core 0.2.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.
Files changed (76) hide show
  1. package/dist/card.d.ts +22 -0
  2. package/dist/card.d.ts.map +1 -0
  3. package/dist/card.js +493 -0
  4. package/dist/card.js.map +1 -0
  5. package/dist/index.d.ts +50 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +88 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/logger.d.ts +5 -0
  10. package/dist/logger.d.ts.map +1 -0
  11. package/dist/logger.js +12 -0
  12. package/dist/logger.js.map +1 -0
  13. package/dist/reducer.d.ts +5 -0
  14. package/dist/reducer.d.ts.map +1 -0
  15. package/dist/reducer.js +77 -0
  16. package/dist/reducer.js.map +1 -0
  17. package/dist/redux.d.ts +31 -0
  18. package/dist/redux.d.ts.map +1 -0
  19. package/dist/redux.js +50 -0
  20. package/dist/redux.js.map +1 -0
  21. package/dist/rest/delete.d.ts +4 -0
  22. package/dist/rest/delete.d.ts.map +1 -0
  23. package/dist/rest/delete.js +23 -0
  24. package/dist/rest/delete.js.map +1 -0
  25. package/dist/rest/enums.d.ts +6 -0
  26. package/dist/rest/enums.d.ts.map +1 -0
  27. package/dist/rest/enums.js +9 -0
  28. package/dist/rest/enums.js.map +1 -0
  29. package/dist/rest/get.d.ts +10 -0
  30. package/dist/rest/get.d.ts.map +1 -0
  31. package/dist/rest/get.js +31 -0
  32. package/dist/rest/get.js.map +1 -0
  33. package/dist/rest/index.d.ts +6 -0
  34. package/dist/rest/index.d.ts.map +1 -0
  35. package/dist/rest/index.js +6 -0
  36. package/dist/rest/index.js.map +1 -0
  37. package/dist/rest/postPutPatch.d.ts +6 -0
  38. package/dist/rest/postPutPatch.d.ts.map +1 -0
  39. package/dist/rest/postPutPatch.js +72 -0
  40. package/dist/rest/postPutPatch.js.map +1 -0
  41. package/dist/rest/types.d.ts +93 -0
  42. package/dist/rest/types.d.ts.map +1 -0
  43. package/dist/rest/types.js +38 -0
  44. package/dist/rest/types.js.map +1 -0
  45. package/dist/rest/utils.d.ts +8 -0
  46. package/dist/rest/utils.d.ts.map +1 -0
  47. package/dist/rest/utils.js +212 -0
  48. package/dist/rest/utils.js.map +1 -0
  49. package/dist/router.d.ts +25 -0
  50. package/dist/router.d.ts.map +1 -0
  51. package/dist/router.js +136 -0
  52. package/dist/router.js.map +1 -0
  53. package/dist/store.d.ts +1 -0
  54. package/dist/store.d.ts.map +1 -0
  55. package/dist/store.js +18 -0
  56. package/dist/store.js.map +1 -0
  57. package/dist/types.d.ts +94 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +2 -0
  60. package/dist/types.js.map +1 -0
  61. package/package.json +71 -0
  62. package/src/card.tsx +628 -0
  63. package/src/index.tsx +193 -0
  64. package/src/logger.ts +13 -0
  65. package/src/reducer.ts +127 -0
  66. package/src/redux.ts +64 -0
  67. package/src/rest/delete.ts +41 -0
  68. package/src/rest/enums.ts +8 -0
  69. package/src/rest/get.ts +50 -0
  70. package/src/rest/index.ts +7 -0
  71. package/src/rest/postPutPatch.ts +118 -0
  72. package/src/rest/types.ts +135 -0
  73. package/src/rest/utils.ts +265 -0
  74. package/src/router.ts +171 -0
  75. package/src/store.ts +19 -0
  76. package/src/types.ts +146 -0
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@pihanga2/core",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "max.ott@data61.csiro.au",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "main": "./dist/index.js",
13
+ "keywords": [
14
+ "pihanga",
15
+ "module",
16
+ "library"
17
+ ],
18
+ "scripts": {
19
+ "docs": "yarn typedoc",
20
+ "build": "tsc --build && typedoc",
21
+ "build:watch": "tsc --build --watch && typedoc",
22
+ "prettier": "prettier --write .",
23
+ "test:coverage": "vitest run --coverage",
24
+ "test:run": "vitest run",
25
+ "test:watch": "vitest"
26
+ },
27
+ "dependencies": {
28
+ "history": "^5.3.0",
29
+ "deep-equal": "^2.2.3"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.2.0",
33
+ "react-dom": "^18.2.0"
34
+ },
35
+ "devDependencies": {
36
+ "@microsoft/tsdoc": "^0.14.1",
37
+ "@reduxjs/toolkit": "^1.8.1",
38
+ "@rollup/plugin-typescript": "^11.1.5",
39
+ "@testing-library/dom": "^9.2.0",
40
+ "@testing-library/jest-dom": "^5.11.4",
41
+ "@testing-library/react": "^14.0.0",
42
+ "@testing-library/user-event": "^14.2.5",
43
+ "@types/luxon": "^2.3.2",
44
+ "@types/node": "^17.0.35",
45
+ "@types/react": "^18.0.15",
46
+ "@types/react-dom": "^18.0.6",
47
+ "@types/stacktrace-js": "^2.0.3",
48
+ "@types/testing-library__jest-dom": "^5.14.5",
49
+ "@types/deep-equal": "^1.0.4",
50
+ "@vitest/coverage-v8": "^1.0.1",
51
+ "c8": "^7.11.3",
52
+ "eslint": "^8.0.0",
53
+ "eslint-config-react-app": "^7.0.1",
54
+ "eslint-plugin-prettier": "^4.2.1",
55
+ "jsdom": "^21.1.0",
56
+ "prettier": "^2.7.1",
57
+ "prettier-config-nick": "^1.0.2",
58
+ "react-redux": "^8.0.1",
59
+ "rollup-plugin-typescript-paths": "^1.4.0",
60
+ "rollup-plugin-visualizer": "^5.10.0",
61
+ "stacktrace-js": "^2.0.2",
62
+ "tslib": "^2.6.2",
63
+ "tslog": "^4.9.2",
64
+ "typedoc": "^0.25.4",
65
+ "typescript": "^5.0.2",
66
+ "vite": "^5.0.4",
67
+ "vite-plugin-dts": "^3.6.4",
68
+ "vite-plugin-progress": "^0.0.7",
69
+ "vitest": "^0.34.6"
70
+ }
71
+ }
package/src/card.tsx ADDED
@@ -0,0 +1,628 @@
1
+ import React from 'react'
2
+ import { useDispatch, useSelector } from "react-redux"
3
+ import equal from "deep-equal"
4
+
5
+ import { getLogger } from "./logger"
6
+ import {
7
+ CSSModuleClasses,
8
+ CardAction,
9
+ CardProp,
10
+ DispatchF,
11
+ MetaCardMapperF,
12
+ PiCardDef,
13
+ PiCardRef,
14
+ PiMapProps,
15
+ PiRegisterComponent,
16
+ PiRegisterMetaCard,
17
+ PiRegisterReducerF,
18
+ ReduxState,
19
+ RegisterCardF,
20
+ StateMapper,
21
+ StateMapperContext,
22
+ } from "./types"
23
+ import { Action, AnyAction, Dispatch } from "@reduxjs/toolkit"
24
+
25
+ export function isCardRef(p: any): boolean {
26
+ return (typeof p === "object" && p.cardType !== undefined)
27
+ }
28
+
29
+ type Mapping = {
30
+ cardType: string
31
+ props: { [k: string]: unknown }
32
+ eventMappers: { [k: string]: (ev: Action) => Action | null }
33
+ cardEvents: { [key: string]: string }
34
+ }
35
+
36
+ type MetaCard = {
37
+ type: string
38
+ registerCard: RegisterCardF
39
+ mapper: MetaCardMapperF
40
+ events?: { [key: string]: string }
41
+ }
42
+
43
+ const cardTypes: { [k: string]: PiRegisterComponent } = {}
44
+ const metacardTypes: { [k: string]: MetaCard } = {}
45
+
46
+ let framework: string // name of active UI framework
47
+ const cardMappings: { [k: string]: Mapping } = {}
48
+ const dispatch2registerReducer: [React.Dispatch<any>, PiRegisterReducerF][] = []
49
+ const logger = getLogger("card")
50
+
51
+ export function registerCardComponent(card: PiRegisterComponent): void {
52
+ if (cardTypes[card.name]) {
53
+ logger.warn(`Overwriting definition for card type "${card.name}"`)
54
+ }
55
+ logger.info(`Register card type "${card.name}"`)
56
+ if (!framework) {
57
+ // set default framework
58
+ const na = card.name.split("/")
59
+ if (na.length >= 2) {
60
+ framework = na[0]
61
+ logger.info(`Setting UI framework to "${framework}"`)
62
+ }
63
+ }
64
+ cardTypes[card.name] = card
65
+ }
66
+
67
+ export function registerMetacard(registerCard: RegisterCardF) {
68
+ function f<C>(declaration: PiRegisterMetaCard) {
69
+ const { type, mapper, events } = declaration
70
+ if (metacardTypes[type]) {
71
+ logger.warn(`Overwriting definition for meta card type "${type}"`)
72
+ }
73
+ logger.info(`Register meta card type "${type}"`)
74
+ metacardTypes[type] = { type, registerCard, mapper, events }
75
+ }
76
+ return f
77
+ }
78
+
79
+ export function registerCard(
80
+ registerReducer: PiRegisterReducerF,
81
+ dispatchF: React.Dispatch<any>,
82
+ ) {
83
+ // to be used by dynamically registered cards
84
+ dispatch2registerReducer.push([dispatchF, registerReducer])
85
+ return (name: string, parameters: PiCardDef): PiCardRef => {
86
+ return _registerCard(name, parameters, registerReducer)
87
+ }
88
+ }
89
+
90
+ function _registerCard(
91
+ name: string,
92
+ parameters: PiCardDef,
93
+ registerReducer: PiRegisterReducerF,
94
+ overrideEvents?: { [key: string]: string },
95
+ ): PiCardRef {
96
+ if (cardMappings[name]) {
97
+ logger.warn(`Overwriting definition for card "${name}"`)
98
+ }
99
+ let cardType = cardTypes[parameters.cardType]
100
+ if (!cardType) {
101
+ if (framework) {
102
+ cardType = cardTypes[`${framework}/${parameters.cardType}`]
103
+ }
104
+ if (!cardType) {
105
+ // maybe it's a metadata card
106
+ if (_registerMetadataCard(name, parameters, registerReducer)) {
107
+ return name
108
+ }
109
+ logger.warn("unknown card type", parameters.cardType)
110
+ return name
111
+ }
112
+ }
113
+
114
+ const events = overrideEvents || cardType.events || {}
115
+ _createCardMapping(name, parameters, registerReducer, events)
116
+ return name
117
+ // const props = {} as { [k: string]: unknown }
118
+ // const eventMappers = {} as { [k: string]: (ev: Action) => Action }
119
+
120
+ // const events = overrideEvents || cardType.events || {}
121
+ // Object.entries(parameters).forEach(([k, v]) => {
122
+ // if (k === "cardType") return
123
+ // if (typeof v === "object") {
124
+ // const cd = v as PiCardDef // speculative
125
+ // if (cd.cardType) {
126
+ // const cardName = `${name}/${k}`
127
+ // v = _registerCard(cardName, cd, registerReducer)
128
+ // }
129
+ // }
130
+ // if (
131
+ // k.startsWith("on") &&
132
+ // processEventParameter(k, v, events, eventMappers, registerReducer, name)
133
+ // ) {
134
+ // return
135
+ // }
136
+ // props[k] = v
137
+ // })
138
+ // cardMappings[name] = { cardType: parameters.cardType, props, eventMappers }
139
+ // return name
140
+ }
141
+
142
+ function _createCardMapping(
143
+ name: string,
144
+ parameters: PiCardDef,
145
+ registerReducer: PiRegisterReducerF,
146
+ cardEvents: { [key: string]: string },
147
+ ) {
148
+ const props = {} as { [k: string]: unknown }
149
+ const eventMappers = {} as { [k: string]: (ev: Action) => Action }
150
+
151
+ Object.entries(parameters).forEach(([k, v]) => {
152
+ if (k === "cardType") return
153
+ if (typeof v === "object") {
154
+ const cd = v as PiCardDef // speculative
155
+ if (cd.cardType) {
156
+ const cardName = `${name}/${k}`
157
+ v = _registerCard(cardName, cd, registerReducer)
158
+ }
159
+ }
160
+ if (
161
+ k.startsWith("on") &&
162
+ processEventParameter(k, v, cardEvents, eventMappers, registerReducer, name)
163
+ ) {
164
+ return
165
+ }
166
+ props[k] = v
167
+ })
168
+ cardMappings[name] = { cardType: parameters.cardType, props, eventMappers, cardEvents }
169
+ }
170
+
171
+ function _registerMetadataCard(
172
+ metaName: string,
173
+ parameters: PiCardDef,
174
+ registerReducer: PiRegisterReducerF,
175
+ ): boolean {
176
+ let mc = metacardTypes[parameters.cardType]
177
+ if (!mc) {
178
+ if (framework) {
179
+ mc = metacardTypes[`${framework}/${parameters.cardType}`]
180
+ }
181
+ if (!mc) {
182
+ return false
183
+ }
184
+ }
185
+ function registerCard(name: string, parameters: PiCardDef): PiCardRef {
186
+ const n = `${metaName}/${name}`
187
+ return mc.registerCard(n, parameters)
188
+ }
189
+ const top = mc.mapper(metaName, parameters, registerCard)
190
+ _registerCard(metaName, top, registerReducer, mc.events)
191
+ return true
192
+ }
193
+
194
+ export function createCardDeclaration<Props = {}, Events = {}>(
195
+ cardType: string,
196
+ ): <S extends ReduxState>(p: PiMapProps<Props, S, Events>) => PiCardDef {
197
+ return (p) => ({
198
+ ...p,
199
+ cardType,
200
+ })
201
+ }
202
+
203
+ function processEventParameter(
204
+ propName: string,
205
+ value: unknown,
206
+ events: { [key: string]: string },
207
+ eventMappers: { [k: string]: (ev: Action) => Action },
208
+ registerReducer: PiRegisterReducerF,
209
+ cardName: string,
210
+ ): boolean {
211
+ const eva = Object.entries(events).filter((e) => propName.startsWith(e[0]))
212
+ if (eva.length === 0) {
213
+ logger.warn(
214
+ `encountered property '${propName}' for card '${cardName}' which looks like an even but is not defined`,
215
+ )
216
+ return false
217
+ }
218
+ // eva could actually return more than one hit if we have similar named events
219
+ let handled = false
220
+ for (const [evName, actionType] of eva) {
221
+ if (propName === evName) {
222
+ const r = value as (state: ReduxState, action: CardAction, dispatch: DispatchF) => ReduxState
223
+ registerReducer(actionType, (s, a, d) => {
224
+ const ca = a as CardAction
225
+ if (ca.cardID === cardName) {
226
+ s = r(s, ca, d)
227
+ }
228
+ return s
229
+ })
230
+ handled = true
231
+ break
232
+ }
233
+ if (propName === `${evName}Mapper`) {
234
+ logger.debug("processEventParameter", cardName)
235
+
236
+ const m = value as (ev: Action) => Action
237
+ eventMappers[evName] = m
238
+ handled = true
239
+ break
240
+ }
241
+ }
242
+ return handled
243
+ }
244
+
245
+ export function memo<P, T, S extends ReduxState, C>(
246
+ filterF: (state: S, context: StateMapperContext<C>) => P,
247
+ mapperF: (partial: P, context: StateMapperContext<C>, state: S) => T,
248
+ ): (state: S, context: StateMapperContext<C>) => T {
249
+ const lastFilter: { [k: string]: P } = {}
250
+ const lastValue: { [k: string]: T } = {}
251
+ const isNotFirst: { [k: string]: boolean } = {}
252
+
253
+ return (state: S, context: StateMapperContext<C>): T => {
254
+ const k = context.cardKey || "-"
255
+ const fv = filterF(state, context)
256
+ if (isNotFirst[k] && equal(fv, lastFilter[k])) {
257
+ // nothing changed
258
+ return lastValue[k]
259
+ }
260
+ lastFilter[k] = fv
261
+ const v = mapperF(fv, context, state)
262
+ lastValue[k] = v
263
+ isNotFirst[k] = true
264
+ return v
265
+ }
266
+ }
267
+
268
+ // export type CardProp = {
269
+ // cardName: PiCardRef
270
+ // } & { [k: string]: any }
271
+
272
+ type CompProps = { [k: string]: any }
273
+ type CardInfo = {
274
+ mapping: Mapping
275
+ cardType: PiRegisterComponent
276
+ }
277
+
278
+ export function Card(props: CardProp): JSX.Element {
279
+ let cardName: string
280
+
281
+ const [id, _] = React.useState<number>(Math.floor(Math.random() * 10000))
282
+ const dispatch = useDispatch() // never change the order of hooks called
283
+
284
+ if (typeof props.cardName === "string") {
285
+ cardName = props.cardName
286
+ } else {
287
+ // lets fix it
288
+ cardName = checkForAnonymousCard(props, id, dispatch)
289
+ }
290
+ if (cardName === "") {
291
+ logger.error("card name is not of type string", props.cardName)
292
+ return ErrorCard(<div>Unknown type of cardName '{`${props.cardName}`}'</div>)
293
+ }
294
+
295
+ const [info, errCard] = getCardInfo(cardName)
296
+ if (errCard) {
297
+ return ErrorCard(errCard)
298
+ }
299
+ if (!info) {
300
+ throw new Error("info is empty, should never happen")
301
+ }
302
+ return GenericCard(cardName, props, info, id)
303
+ }
304
+
305
+ function checkForAnonymousCard(props: any, id: number, dispatch: Dispatch<AnyAction>): string {
306
+ // const [id, _] = React.useState<number>(Math.floor(Math.random() * 10000))
307
+ // const dispatch = useDispatch() // never change the order of hooks called
308
+
309
+ const cardType = props.cardName?.cardType
310
+ if (!cardType) {
311
+ return "" // not sure what that is
312
+ }
313
+ // looks like a potentially unregistered card
314
+ let cardName: string
315
+ if (props.parentCard) {
316
+ cardName = `${props.parentCard}/${cardType.split('/').pop()}`
317
+ } else {
318
+ cardName = cardType
319
+ }
320
+ if (props.cardKey) {
321
+ cardName = `${cardName}#${props.cardKey}-${id}`
322
+ } else {
323
+ cardName = `${cardName}#${id}`
324
+ }
325
+ // logger.debug("anonymous", cardName)
326
+
327
+ const mapping = cardMappings[cardName]
328
+ const parameters = props.cardName as PiCardDef
329
+ const el = dispatch2registerReducer.find(([d, _]) => d === dispatch)
330
+ if (!el) {
331
+ logger.warn("unexpected missing mapping between dispatcher and reducerF")
332
+ return ""
333
+ }
334
+ const regRed = el[1]
335
+ if (mapping) {
336
+ // looks like we already processed it
337
+ // do update props as they may have changed
338
+ _createCardMapping(cardName, parameters, regRed, mapping.cardEvents)
339
+ } else {
340
+ _registerCard(cardName, parameters, regRed)
341
+ }
342
+ return cardName
343
+ }
344
+
345
+ function GenericCard(cardName: string, props: CardProp, info: CardInfo, id: number) {
346
+ const cardProps = useSelector<ReduxState, CompProps>(
347
+ (s) => getCardProps(cardName, s, props, info?.mapping),
348
+ propEq,
349
+ )
350
+ const dispatch = useDispatch()
351
+
352
+ const extCardProps = appendEventHandlers(info, cardProps, cardName, dispatch)
353
+ extCardProps._cls = cls_f(cardName, info.mapping.cardType)
354
+ return React.createElement(info.cardType.component, extCardProps, props.children)
355
+ }
356
+
357
+ const EmptyCompProps = {} as CompProps
358
+
359
+ function ErrorCard(el: JSX.Element) {
360
+ // Note: call the EXACT same hooks as 'GenericCard'
361
+ useSelector<ReduxState, CompProps>((s) => EmptyCompProps, (a, b) => false)
362
+ useDispatch()
363
+ return el
364
+ }
365
+
366
+
367
+ function getCardInfo(cardName: string): [CardInfo?, JSX.Element?] {
368
+ const mapping = cardMappings[cardName]
369
+ if (!mapping) {
370
+ return [undefined, renderUnknownCard(cardName)]
371
+ }
372
+ let cardType = cardTypes[mapping.cardType]
373
+ if (!cardType) {
374
+ if (framework) {
375
+ cardType = cardTypes[`${framework}/${mapping.cardType}`]
376
+ }
377
+ if (!cardType) {
378
+ return [undefined, renderUnknownCardType(mapping.cardType)]
379
+ }
380
+ }
381
+ const info = { mapping, cardType }
382
+ return [info, undefined]
383
+ }
384
+
385
+
386
+ function getCardProps(
387
+ cardName: string,
388
+ state: ReduxState,
389
+ props: CardProp,
390
+ mapping: Mapping,
391
+ ): CompProps {
392
+ const ctxt: StateMapperContext<unknown> = {
393
+ cardName,
394
+ cardKey: props.cardKey,
395
+ ctxtProps: props,
396
+ }
397
+ const init: CompProps = {
398
+ cardName,
399
+ cardKey: props.cardKey,
400
+ }
401
+ const cprops = Object.entries(mapping.props).reduce((p, [key, vf]) => {
402
+ let v = vf
403
+ if (typeof vf === "function") {
404
+ const f = vf as StateMapper<unknown, ReduxState, any>
405
+ try {
406
+ v = f(state, ctxt)
407
+ } catch (ex) {
408
+ logger.error(`while resolving property '${key}'`, ex)
409
+ }
410
+ } else if (key in props) {
411
+ v = props[key]
412
+ }
413
+ p[key] = v
414
+ return p
415
+ }, init)
416
+ return cprops
417
+ }
418
+
419
+ function cls_f(
420
+ cardName: string,
421
+ cardComp: string,
422
+ prefix: string = "pi",
423
+ ): (nodeName: string | string[], styles?: CSSModuleClasses) => string {
424
+ const cn = cardName.replaceAll(/[/:]/g, "_")
425
+ const cp = cardComp.replaceAll(/[/:]/g, "_")
426
+ return (nodeName: string | string[], styles?: CSSModuleClasses): string => {
427
+ const na: string[] = typeof nodeName === "string" ? [nodeName] : nodeName
428
+ const ca = [] as string[]
429
+ na.forEach((n) => {
430
+ const s = styles?.[n]
431
+ if (s) ca.push(s)
432
+
433
+ const nn = n.replaceAll(/[/:]/g, "_")
434
+ ca.push(`${prefix}-${cn}-${nn}`)
435
+ ca.push(`${prefix}-${cp}-${nn}`)
436
+ })
437
+ return ca.join(" ")
438
+ }
439
+ }
440
+
441
+ function propEq(oldP: CompProps, newP: CompProps): boolean {
442
+ let isUnchanged = equal(oldP, newP)
443
+ // for (const [k, v] of Object.entries(newP)) {
444
+ // const ov = oldP[k]
445
+ // if (ov !== v) {
446
+ // // two empty arrays are considered to be different, but we don't agree :)
447
+ // if (!(Array.isArray(v) && !v.length && Array.isArray(ov) && !ov.length)) {
448
+ // isUnchanged = false
449
+ // break
450
+ // }
451
+ // }
452
+ // }
453
+ RegisterCardState.changed(newP.cardName, isUnchanged, newP)
454
+ return isUnchanged
455
+ }
456
+
457
+ function appendEventHandlers(
458
+ info: CardInfo,
459
+ cardProps: CompProps,
460
+ cardName: string,
461
+ dispatch: Dispatch<Action>,
462
+ ): CompProps {
463
+ RegisterCardState.props(cardName, cardProps, dispatch)
464
+ const cp: CompProps = {
465
+ ...cardProps,
466
+ _dispatch: (a: AnyAction) => dispatch(a)
467
+ }
468
+
469
+ const events = info.cardType?.events
470
+ if (!events) {
471
+ return cp
472
+ }
473
+ const eventMappers = info.mapping.eventMappers
474
+ Object.entries(events).forEach(([name, actionType]) => {
475
+ const m = eventMappers[name]
476
+ if (m) {
477
+ logger.debug("setup mapper", cardName)
478
+ cp[name] = (a: AnyAction) => {
479
+ a.cardID = cardName
480
+ const a2 = m(a)
481
+ if (a2) dispatch(a2)
482
+ }
483
+ } else {
484
+ cp[name] = (a: AnyAction) => {
485
+ a.type = actionType
486
+ a.cardID = cardName
487
+ dispatch(a)
488
+ }
489
+ }
490
+ })
491
+ return cp
492
+ }
493
+
494
+ function renderUnknownCard(cardName: string): JSX.Element {
495
+ return <div>Unknown card '{cardName}'</div>
496
+ }
497
+
498
+ function renderUnknownCardType(cardType: string): JSX.Element {
499
+ return <div>Unknown card type '{cardType}'</div>
500
+ }
501
+
502
+ // Adding card state to redux state for debugging
503
+
504
+ export const UPDATE_STATE_ACTION = "pi/card/update_state"
505
+
506
+ type CardState = {
507
+ props: (
508
+ cardName: string,
509
+ cardProps: CompProps,
510
+ dispatch: Dispatch<Action>,
511
+ ) => void
512
+ changed: (cardName: string, isUnchanged: boolean, props: CompProps) => void
513
+ reducer: (state: ReduxState, action: Action) => ReduxState
514
+ }
515
+
516
+ export const RegisterCardState = createCardState()
517
+
518
+ function createCardState(): CardState {
519
+ type S = {
520
+ cardProps?: CompProps
521
+ changedAt: number
522
+ reportedAt: number
523
+ }
524
+ const s: { [name: string]: S } = {}
525
+ let dispatch: Dispatch<Action>
526
+ let timer: number
527
+ let lastReport = 0
528
+
529
+ // const timer
530
+ const getS = (cardName: string, props: CompProps): S => {
531
+ const name = cardName
532
+ let e = s[name]
533
+ if (!e) {
534
+ const ts = Date.now()
535
+ e = {
536
+ changedAt: ts,
537
+ reportedAt: ts,
538
+ } as S
539
+ s[name] = e
540
+ resetTimer()
541
+ }
542
+ return e
543
+ }
544
+ const resetTimer = () => {
545
+ if (timer) {
546
+ clearTimeout(timer)
547
+ }
548
+ timer = window.setTimeout(() => {
549
+ //logger.debug("... timer went off") // , s, dispatch)
550
+ if (dispatch) {
551
+ const changed = Object.values(s).filter((s) => s.changedAt > lastReport)
552
+ if (changed.length > 0) {
553
+ clearTimeout(timer) // just in case
554
+ dispatch({ type: UPDATE_STATE_ACTION })
555
+ }
556
+ }
557
+ }, 1000)
558
+ }
559
+ const props = (
560
+ cardName: string,
561
+ cardProps: CompProps,
562
+ _dispatch: Dispatch<Action>,
563
+ ) => {
564
+ const e = getS(cardName, cardProps)
565
+ e.cardProps = cardProps
566
+ dispatch = _dispatch
567
+ }
568
+ const changed = (cardName: string, isUnchanged: boolean, props: CompProps) => {
569
+ const e = getS(cardName, props)
570
+ e.reportedAt = Date.now()
571
+ if (!isUnchanged) {
572
+ logger.debug("card has changed:", cardName)
573
+ e.changedAt = Date.now()
574
+ resetTimer()
575
+ }
576
+ }
577
+ const reducer = (state: ReduxState): ReduxState => {
578
+ const pi = Object.values(s)
579
+ .filter((s) => s.reportedAt > lastReport)
580
+ .reduce((p, s) => {
581
+ const cname = s.cardProps?.cardName
582
+ if (!cname) {
583
+ logger.warn("Unexpected missing card name", s)
584
+ return p
585
+ }
586
+ const name = cname
587
+ const props = copySafeProps(s.cardProps || {})
588
+ delete props.cardName
589
+ delete props._cls
590
+ p[name] = props
591
+ return p
592
+ }, {} as { [k: string]: any })
593
+ state.pihanga = pi
594
+ lastReport = Date.now()
595
+ return state
596
+ }
597
+ return { props, changed, reducer }
598
+ }
599
+
600
+ function copySafeProps(props: CompProps): CompProps {
601
+ return Object.entries(props).reduce((p, [k, v]) => {
602
+ // const ok = (typeof v === 'undefined' || typeof v === 'string' || typeof v === 'boolean' || typeof v === 'number' || Array.isArray(v));
603
+ const sv = makeSafe(v)
604
+ p[k] = sv
605
+ return p
606
+ }, {} as CompProps)
607
+ }
608
+
609
+ function makeSafe(v: any): any {
610
+ const t = typeof v
611
+ if (t === 'undefined' || t === 'string' || t === 'boolean' || t === 'number') {
612
+ return v
613
+ }
614
+ if (t === 'function') {
615
+ return "f(...)"
616
+ }
617
+ if (Array.isArray(v)) {
618
+ return v.map(makeSafe)
619
+ }
620
+ if (t === 'object') {
621
+ return Object.entries(v).reduce((p, [k, v]) => {
622
+ p[k] = makeSafe(v)
623
+ return p
624
+ }, {} as { [k: string]: any })
625
+ }
626
+ logger.warn(">>> reject", v, typeof v)
627
+ return "..."
628
+ }