create-procode 0.0.0-stage → 1.0.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 (54) hide show
  1. package/README.md +240 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +139 -0
  5. package/src/cli.mjs +392 -0
  6. package/src/deps.mjs +195 -0
  7. package/src/log.mjs +23 -0
  8. package/src/prompt.mjs +60 -0
  9. package/src/scaffold.mjs +104 -0
  10. package/templates/base/README.md.tmpl +183 -0
  11. package/templates/base/_dotenv.local.example.tmpl +8 -0
  12. package/templates/base/_dotenv.tmpl +41 -0
  13. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  14. package/templates/base/_dotgitignore.tmpl +32 -0
  15. package/templates/base/_dotnvmrc.tmpl +1 -0
  16. package/templates/base/_dotvscode/extensions.json +3 -0
  17. package/templates/base/index.html.tmpl +18 -0
  18. package/templates/base/package.json.tmpl +15 -0
  19. package/templates/base/public/favicon.svg +4 -0
  20. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  21. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  22. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  23. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  24. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  25. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  26. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  27. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  28. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  29. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  30. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  31. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  32. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  33. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  34. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  35. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  37. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  38. package/templates/base/src/Module/index.ts.tmpl +26 -0
  39. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  40. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  41. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  42. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  43. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  44. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  45. package/templates/base/src/assets/styles/index.scss.tmpl +146 -0
  46. package/templates/base/src/constant.ts.tmpl +53 -0
  47. package/templates/base/src/icons.ts.tmpl +17 -0
  48. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  49. package/templates/base/src/main.tsx.tmpl +136 -0
  50. package/templates/base/src/routes.ts.tmpl +20 -0
  51. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  52. package/templates/base/tsconfig.json.tmpl +30 -0
  53. package/templates/base/tsconfig.node.json.tmpl +11 -0
  54. package/templates/base/vite.config.ts.tmpl +61 -0
@@ -0,0 +1,965 @@
1
+ {
2
+ "_readme": [
3
+ "SAMPLE SCREEN — one JSON document, no backend. Everything on this page is declared here:",
4
+ "a heading and a permission-gated button; three BLOCK summary tiles; a GRID with sorting,",
5
+ "column search, checkbox selection and three bulk actions that open the framework's",
6
+ "confirmation dialog; a click on a name that opens an inline editor; an editor with seven",
7
+ "widget types, standard validators and a rule-driven conditional field; a Save that validates",
8
+ "and then opens the information dialog. The rows come from the adapter's defaultViewModel",
9
+ "(src/AdapterProvider/SampleContacts.ts). Five small custom actions in",
10
+ "src/Factory/Actions/sampleContacts.ts stand in for the API-backed standard actions.",
11
+ "",
12
+ "GOING LIVE — the three swaps:",
13
+ " 1. Add `apiConfig` with `get` / `create` / `update` / `delete` verb blocks, point the grid's",
14
+ " `onScreenDataLoad` at LOAD_SCREEN_DATA and add `paginationConfig` — the server now",
15
+ " sorts, searches and pages; the seed rows and contactsSource stop being used.",
16
+ " 2. The bulk actions become DELETE_ASYNC / UPDATE_ASYNC with `refresh: true`.",
17
+ " 3. Save becomes FORMSUBMIT_CREATE_ASYNC / FORMSUBMIT_UPDATE_ASYNC and the row click",
18
+ " becomes NAVIGATE to an edit screen with a STATE_SELECTED_DATA param.",
19
+ "Keys beginning with an underscore are comments; the core ignores them."
20
+ ],
21
+ "screenName": "Contacts",
22
+ "screenId": "sample-contacts",
23
+ "slug": "sample",
24
+ "enableHeader": false,
25
+ "enableFooter": false,
26
+ "rootScreenElement": {
27
+ "uiElementType": "FORM",
28
+ "children": [
29
+ {
30
+ "_comment": "CONTAINER is the grouping element type. CONTAINER/FORM/WIDGET are uiElementType values; BLOCK, GRIDTOOLBAR and MENU are widgetType values on a WIDGET. Writing uiElementType: BLOCK renders nothing, silently.",
31
+ "uiElementType": "CONTAINER",
32
+ "widgetStyle": {
33
+ "className": "prc-flx-container prc-verticle qo-gap-y-lg qo-padding-lg"
34
+ },
35
+ "children": [
36
+ {
37
+ "_comment": "Page header: an H1 widget (level = element, look = theme class) and a button gated by a permission key through CustomAuthorizationProvider.",
38
+ "uiElementType": "CONTAINER",
39
+ "widgetStyle": {
40
+ "className": "prc-flx-container prc-space-between prc-center-cross-axis prc-nowrap"
41
+ },
42
+ "children": [
43
+ {
44
+ "uiElementType": "CONTAINER",
45
+ "widgetStyle": {
46
+ "className": "prc-flx-container prc-verticle qo-gap-y-sm"
47
+ },
48
+ "children": [
49
+ {
50
+ "uiElementType": "WIDGET",
51
+ "widgetType": "H1",
52
+ "title": "Contacts",
53
+ "skeletonType": "PARAGRAPH"
54
+ },
55
+ {
56
+ "uiElementType": "WIDGET",
57
+ "widgetType": "LABEL",
58
+ "title": "Select rows for bulk actions, click a name to edit, or add a new contact. Every behaviour on this page is declared in src/Module/Sample/Contacts.json.",
59
+ "widgetStyle": {
60
+ "className": "qo-font-sm qo-text-secondary"
61
+ },
62
+ "skeletonType": "PARAGRAPH"
63
+ }
64
+ ]
65
+ },
66
+ {
67
+ "_comment": "A BUTTON raises onClick with no arguments, so it cannot reach MODIFY_MODEL directly; CUS_SET_FIELD turns the event config into MODIFY_MODEL's (field, value) arguments. permissionKey gates the click: denied => blocked, and the provider's message is toasted.",
68
+ "uiElementType": "WIDGET",
69
+ "widgetType": "BUTTON",
70
+ "title": "Add contact",
71
+ "widgetStyle": {
72
+ "className": "qo-btn qo-btn-primary"
73
+ },
74
+ "events": {
75
+ "onClick": {
76
+ "key": "CUS_SET_FIELD::MODIFY_MODEL",
77
+ "permissionKey": "sample.add",
78
+ "config": {
79
+ "field": "screenData.editor",
80
+ "value": {
81
+ "mode": "add",
82
+ "id": null,
83
+ "name": "",
84
+ "email": "",
85
+ "department": null,
86
+ "joined": null,
87
+ "active": true,
88
+ "notes": "",
89
+ "region": null,
90
+ "referenceCode": ""
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ]
97
+ },
98
+ {
99
+ "_comment": "Summary tiles: BLOCK is a vs-template widget — caption over a value bound to the view model. The custom actions recompute screenData.summary after every change, so these stay live.",
100
+ "uiElementType": "CONTAINER",
101
+ "widgetStyle": {
102
+ "className": "prc-flx-container prc-nowrap qo-gap-x-md"
103
+ },
104
+ "children": [
105
+ {
106
+ "uiElementType": "WIDGET",
107
+ "widgetType": "BLOCK",
108
+ "title": "Total contacts",
109
+ "screenDataField": "screenData.summary.total",
110
+ "enableHeaderBorder": true,
111
+ "skeletonType": "SQUARE_LARGE",
112
+ "widgetStyle": {
113
+ "className": "prc-cl-3"
114
+ }
115
+ },
116
+ {
117
+ "uiElementType": "WIDGET",
118
+ "widgetType": "BLOCK",
119
+ "title": "Active",
120
+ "screenDataField": "screenData.summary.active",
121
+ "enableHeaderBorder": true,
122
+ "skeletonType": "SQUARE_LARGE",
123
+ "widgetStyle": {
124
+ "className": "prc-cl-3"
125
+ }
126
+ },
127
+ {
128
+ "uiElementType": "WIDGET",
129
+ "widgetType": "BLOCK",
130
+ "title": "Departments",
131
+ "screenDataField": "screenData.summary.departments",
132
+ "enableHeaderBorder": true,
133
+ "skeletonType": "SQUARE_LARGE",
134
+ "widgetStyle": {
135
+ "className": "prc-cl-3"
136
+ }
137
+ }
138
+ ]
139
+ },
140
+ {
141
+ "_comment": "The GRID. Data is { records, total } at screenDataField. The grid raises onScreenDataLoad on mount and whenever its sort or column-search state changes; with an API that event is LOAD_SCREEN_DATA + apiConfig.get and the adapter's generateQueryParams turns the grid state into the query string — here CUS_SAMPLE_LOAD_ROWS applies the same state to the in-memory set. selectedField adds the checkbox column; headerActionBar lists the bulk actions — each names an event below and the alert widget its confirmation opens. The name column is a NAVIGATIONCELL: clicking it raises `navigate` with { dataItemKey, value }. Every cell renders HTML, which is how the status badge is drawn. Column search comes from `filterable` (type, then Enter); sorting from the arrow at the header's right edge. Paging is server-side by design and is enabled with paginationConfig once an API pages the data.",
142
+ "_comment_navigate": "With a real edit screen the navigate event is { key: NAVIGATE, config: { path: /sample/edit, params: [{ name: id, rightOperand: { valueReplacePolicy: STATE_SELECTED_DATA, value: screenData.contacts.records::id } }] } }.",
143
+ "_comment_bulk": "With an API each bulk event is DELETE_ASYNC / UPDATE_ASYNC carrying its own apiConfig verb block, and the action sets refresh: true so the grid re-fetches.",
144
+ "uiElementType": "WIDGET",
145
+ "widgetType": "GRID",
146
+ "applicableOn": "contactsGrid",
147
+ "permissionKey": "sample.grid",
148
+ "screenDataField": "screenData.contacts",
149
+ "config": {
150
+ "uiElementGroupId": "screenData.contactsGridState"
151
+ },
152
+ "dataItemKey": "id",
153
+ "selectedField": "selected",
154
+ "lazyLoading": false,
155
+ "skeletonType": "SQUARE_LARGE",
156
+ "sortConfig": {
157
+ "sortable": true,
158
+ "sortMode": "single",
159
+ "defaultSortFields": [
160
+ {
161
+ "field": "name",
162
+ "dir": "asc"
163
+ }
164
+ ]
165
+ },
166
+ "localSearchConfig": {
167
+ "logic": "and",
168
+ "filters": []
169
+ },
170
+ "_comment_paging": "No defaultPagination on purpose: in data-load mode the grid also slices locally with Kendo's process(), and a take of -1 there is slice(0, -1) — the last row silently disappears.",
171
+ "paginationConfig": {
172
+ "pageable": false
173
+ },
174
+ "columns": [
175
+ {
176
+ "id": 1,
177
+ "order": 1,
178
+ "field": "id",
179
+ "title": "#",
180
+ "selected": true,
181
+ "primary": true,
182
+ "width": 64,
183
+ "widgetStyle": {
184
+ "className": "qo-text-secondary qo-font-sm"
185
+ }
186
+ },
187
+ {
188
+ "id": 2,
189
+ "order": 2,
190
+ "field": "name",
191
+ "title": "Name",
192
+ "selected": true,
193
+ "sortable": true,
194
+ "filterable": true,
195
+ "resizable": true,
196
+ "reorderable": true,
197
+ "width": 190,
198
+ "cellType": "NAVIGATIONCELL",
199
+ "widgetStyle": {
200
+ "className": "qo-text-primary qo-font-weight-medium"
201
+ },
202
+ "tooltip": {
203
+ "enabled": true,
204
+ "field": "email"
205
+ }
206
+ },
207
+ {
208
+ "id": 3,
209
+ "order": 3,
210
+ "field": "email",
211
+ "title": "Email",
212
+ "selected": true,
213
+ "sortable": true,
214
+ "filterable": true,
215
+ "resizable": true,
216
+ "reorderable": true,
217
+ "width": "auto"
218
+ },
219
+ {
220
+ "id": 4,
221
+ "order": 4,
222
+ "field": "department",
223
+ "title": "Department",
224
+ "selected": true,
225
+ "sortable": true,
226
+ "filterable": true,
227
+ "resizable": true,
228
+ "reorderable": true,
229
+ "width": 140
230
+ },
231
+ {
232
+ "id": 5,
233
+ "order": 5,
234
+ "field": "city",
235
+ "title": "City",
236
+ "selected": true,
237
+ "sortable": true,
238
+ "filterable": true,
239
+ "resizable": true,
240
+ "reorderable": true,
241
+ "width": 130
242
+ },
243
+ {
244
+ "id": 6,
245
+ "order": 6,
246
+ "field": "joined",
247
+ "title": "Joined",
248
+ "selected": true,
249
+ "sortable": true,
250
+ "resizable": true,
251
+ "reorderable": true,
252
+ "width": 120
253
+ },
254
+ {
255
+ "id": 7,
256
+ "order": 7,
257
+ "field": "statusBadge",
258
+ "title": "Status",
259
+ "selected": true,
260
+ "resizable": true,
261
+ "reorderable": true,
262
+ "width": 130
263
+ }
264
+ ],
265
+ "headerActionBar": {
266
+ "actions": [
267
+ {
268
+ "actionName": "onRecordDeactivate",
269
+ "label": "Deactivate",
270
+ "value": "Deactivate",
271
+ "refresh": false,
272
+ "targetScreenDataField": "screenData.statusConfirm"
273
+ },
274
+ {
275
+ "actionName": "onRecordActivate",
276
+ "label": "Activate",
277
+ "value": "Activate",
278
+ "refresh": false,
279
+ "targetScreenDataField": "screenData.statusConfirm"
280
+ },
281
+ {
282
+ "actionName": "onRecordDelete",
283
+ "label": "Delete",
284
+ "value": "Delete",
285
+ "refresh": false,
286
+ "targetScreenDataField": "screenData.deleteConfirm"
287
+ }
288
+ ]
289
+ },
290
+ "events": {
291
+ "onModelUpdate": {
292
+ "key": "MODIFY_MODEL"
293
+ },
294
+ "onScreenDataLoad": {
295
+ "key": "CUS_SAMPLE_LOAD_ROWS"
296
+ },
297
+ "navigate": {
298
+ "key": "CUS_SAMPLE_OPEN_ROW::MODIFY_MODEL"
299
+ },
300
+ "onRecordDelete": {
301
+ "key": "CUS_SAMPLE_UPDATE_ROWS",
302
+ "config": {
303
+ "remove": true
304
+ }
305
+ },
306
+ "onRecordDeactivate": {
307
+ "key": "CUS_SAMPLE_UPDATE_ROWS",
308
+ "config": {
309
+ "set": {
310
+ "status": "Inactive"
311
+ }
312
+ }
313
+ },
314
+ "onRecordActivate": {
315
+ "key": "CUS_SAMPLE_UPDATE_ROWS",
316
+ "config": {
317
+ "set": {
318
+ "status": "Active"
319
+ }
320
+ }
321
+ }
322
+ }
323
+ },
324
+ {
325
+ "_comment": "The inline editor. Hidden until screenData.editor.mode is 'add' or 'edit' — see ruleInterpreter[applicableOn=editor]. Field width comes from rootStyle (the wrapper), not widgetStyle (the control).",
326
+ "uiElementType": "CONTAINER",
327
+ "applicableOn": "editor",
328
+ "widgetStyle": {
329
+ "className": "prc-flx-container prc-verticle qo-gap-y-md qo-padding-lg qo-bg-light qo-border-radius-md qo-boxshadow-sm"
330
+ },
331
+ "children": [
332
+ {
333
+ "uiElementType": "WIDGET",
334
+ "widgetType": "H3",
335
+ "title": "Contact details",
336
+ "skeletonType": "PARAGRAPH"
337
+ },
338
+ {
339
+ "uiElementType": "CONTAINER",
340
+ "widgetStyle": {
341
+ "className": "prc-flx-container prc-wrap qo-gap-x-md qo-gap-y-md"
342
+ },
343
+ "children": [
344
+ {
345
+ "uiElementType": "WIDGET",
346
+ "widgetType": "TEXTBOX",
347
+ "applicableOn": "editorName",
348
+ "screenDataField": "screenData.editor.name",
349
+ "name": "name",
350
+ "placeHolder": "Full name",
351
+ "rootStyle": {
352
+ "className": "prc-cl-3"
353
+ },
354
+ "widgetStyle": {
355
+ "className": "qo-h-control"
356
+ },
357
+ "skeletonType": "FIELD",
358
+ "label": {
359
+ "title": "Name",
360
+ "isApply": true,
361
+ "position": "top",
362
+ "widgetStyle": {
363
+ "className": "qo-label prc-blc-inline-container"
364
+ }
365
+ },
366
+ "validations": [
367
+ {
368
+ "name": "REQUIRED",
369
+ "message": "Please enter a name",
370
+ "executeOrder": 1,
371
+ "executeMode": "ONVALUECHANGE|ONSUBMIT"
372
+ },
373
+ {
374
+ "name": "MAX_CHAR_COUNT",
375
+ "message": "Keep the name under 40 characters",
376
+ "executeOrder": 2,
377
+ "executeMode": "ONVALUECHANGE|ONSUBMIT",
378
+ "params": {
379
+ "maxCharCount": 40
380
+ }
381
+ }
382
+ ],
383
+ "events": {
384
+ "onChange": {
385
+ "key": "MODIFY_MODEL"
386
+ }
387
+ }
388
+ },
389
+ {
390
+ "uiElementType": "WIDGET",
391
+ "widgetType": "TEXTBOX",
392
+ "applicableOn": "editorEmail",
393
+ "screenDataField": "screenData.editor.email",
394
+ "name": "email",
395
+ "placeHolder": "name@company.com",
396
+ "rootStyle": {
397
+ "className": "prc-cl-3"
398
+ },
399
+ "widgetStyle": {
400
+ "className": "qo-h-control"
401
+ },
402
+ "skeletonType": "FIELD",
403
+ "label": {
404
+ "title": "Email",
405
+ "isApply": true,
406
+ "position": "top",
407
+ "widgetStyle": {
408
+ "className": "qo-label prc-blc-inline-container"
409
+ }
410
+ },
411
+ "validations": [
412
+ {
413
+ "name": "REQUIRED",
414
+ "message": "Please enter an email address",
415
+ "executeOrder": 1,
416
+ "executeMode": "ONVALUECHANGE|ONSUBMIT"
417
+ },
418
+ {
419
+ "name": "EMAIL",
420
+ "message": "That does not look like an email address",
421
+ "executeOrder": 2,
422
+ "executeMode": "ONVALUECHANGE|ONSUBMIT"
423
+ }
424
+ ],
425
+ "events": {
426
+ "onChange": {
427
+ "key": "MODIFY_MODEL"
428
+ }
429
+ }
430
+ },
431
+ {
432
+ "_comment": "A DROPDOWN with inline options (valueLoadType SELF). It writes the whole { label, value } object to the model, which is why the rule below reads screenData.editor.department.value. With an API the options come from apiConfig.loadSupportiveData + LOAD_SUPPORTIVE_DATA.",
433
+ "uiElementType": "WIDGET",
434
+ "widgetType": "DROPDOWN",
435
+ "applicableOn": "editorDepartment",
436
+ "screenDataField": "screenData.editor.department",
437
+ "supportiveDataField": "supportiveData.departments",
438
+ "textField": "label",
439
+ "dataItemKey": "value",
440
+ "defaultItem": {
441
+ "label": "Select a department",
442
+ "value": null
443
+ },
444
+ "dataProvider": {
445
+ "valueLoadType": "SELF",
446
+ "value": [
447
+ {
448
+ "label": "Sales",
449
+ "value": "Sales"
450
+ },
451
+ {
452
+ "label": "Support",
453
+ "value": "Support"
454
+ },
455
+ {
456
+ "label": "Engineering",
457
+ "value": "Engineering"
458
+ },
459
+ {
460
+ "label": "Finance",
461
+ "value": "Finance"
462
+ }
463
+ ]
464
+ },
465
+ "rootStyle": {
466
+ "className": "prc-cl-3"
467
+ },
468
+ "widgetStyle": {
469
+ "className": "qo-h-control"
470
+ },
471
+ "skeletonType": "FIELD",
472
+ "label": {
473
+ "title": "Department",
474
+ "isApply": true,
475
+ "position": "top",
476
+ "widgetStyle": {
477
+ "className": "qo-label prc-blc-inline-container"
478
+ }
479
+ },
480
+ "validations": [
481
+ {
482
+ "name": "REQUIRED",
483
+ "message": "Please choose a department",
484
+ "executeOrder": 1,
485
+ "executeMode": "ONVALUECHANGE|ONSUBMIT"
486
+ }
487
+ ],
488
+ "events": {
489
+ "onChange": {
490
+ "key": "MODIFY_MODEL"
491
+ },
492
+ "onModelUpdate": {
493
+ "key": "MODIFY_MODEL"
494
+ }
495
+ }
496
+ },
497
+ {
498
+ "_comment": "Shown only while the department is Sales — see ruleInterpreter[applicableOn=editorRegion]. Rules are the framework's conditional mechanism; never local state that hides an element.",
499
+ "uiElementType": "WIDGET",
500
+ "widgetType": "DROPDOWN",
501
+ "applicableOn": "editorRegion",
502
+ "screenDataField": "screenData.editor.region",
503
+ "supportiveDataField": "supportiveData.regions",
504
+ "textField": "label",
505
+ "dataItemKey": "value",
506
+ "defaultItem": {
507
+ "label": "Select a region",
508
+ "value": null
509
+ },
510
+ "dataProvider": {
511
+ "valueLoadType": "SELF",
512
+ "value": [
513
+ {
514
+ "label": "North America",
515
+ "value": "NA"
516
+ },
517
+ {
518
+ "label": "Europe",
519
+ "value": "EU"
520
+ },
521
+ {
522
+ "label": "Asia Pacific",
523
+ "value": "APAC"
524
+ }
525
+ ]
526
+ },
527
+ "rootStyle": {
528
+ "className": "prc-cl-3"
529
+ },
530
+ "widgetStyle": {
531
+ "className": "qo-h-control"
532
+ },
533
+ "skeletonType": "FIELD",
534
+ "label": {
535
+ "title": "Sales region",
536
+ "isApply": true,
537
+ "position": "top",
538
+ "widgetStyle": {
539
+ "className": "qo-label prc-blc-inline-container"
540
+ }
541
+ },
542
+ "helperText": {
543
+ "title": "Only asked for the Sales department",
544
+ "isApply": true
545
+ },
546
+ "events": {
547
+ "onChange": {
548
+ "key": "MODIFY_MODEL"
549
+ },
550
+ "onModelUpdate": {
551
+ "key": "MODIFY_MODEL"
552
+ }
553
+ }
554
+ },
555
+ {
556
+ "uiElementType": "WIDGET",
557
+ "widgetType": "DATE",
558
+ "applicableOn": "editorJoined",
559
+ "screenDataField": "screenData.editor.joined",
560
+ "format": "yyyy-MM-dd",
561
+ "placeHolder": "yyyy-mm-dd",
562
+ "rootStyle": {
563
+ "className": "prc-cl-3"
564
+ },
565
+ "widgetStyle": {
566
+ "className": "qo-h-control"
567
+ },
568
+ "skeletonType": "FIELD",
569
+ "label": {
570
+ "title": "Joined",
571
+ "isApply": true,
572
+ "position": "top",
573
+ "widgetStyle": {
574
+ "className": "qo-label prc-blc-inline-container"
575
+ }
576
+ },
577
+ "events": {
578
+ "onChange": {
579
+ "key": "MODIFY_MODEL"
580
+ }
581
+ }
582
+ },
583
+ {
584
+ "_comment": "The custom widget from src/Factory/Widgets/MyWidget — composed from the library's TextBox and Button. Its onChange is chained through CUS_MY_ACTION (normalise) into MODIFY_MODEL; its Reload button raises onReload(null, fieldPath) into CUS_SUGGEST_CODE. MY_VALIDATION is the custom validator.",
585
+ "uiElementType": "WIDGET",
586
+ "widgetType": "MY_WIDGET",
587
+ "title": "Reference code",
588
+ "applicableOn": "editorReferenceCode",
589
+ "screenDataField": "screenData.editor.referenceCode",
590
+ "placeholder": "e.g. CT_A1B2",
591
+ "rootStyle": {
592
+ "className": "prc-cl-3"
593
+ },
594
+ "skeletonType": "FIELD",
595
+ "validations": [
596
+ {
597
+ "name": "MY_VALIDATION",
598
+ "message": "Use capitals, digits and underscore only",
599
+ "executeOrder": 1,
600
+ "executeMode": "ONVALUECHANGE|ONSUBMIT"
601
+ }
602
+ ],
603
+ "events": {
604
+ "onChange": {
605
+ "key": "CUS_MY_ACTION::MODIFY_MODEL"
606
+ },
607
+ "onReload": {
608
+ "key": "CUS_SUGGEST_CODE::MODIFY_MODEL"
609
+ }
610
+ }
611
+ },
612
+ {
613
+ "uiElementType": "WIDGET",
614
+ "widgetType": "SWITCH",
615
+ "applicableOn": "editorActive",
616
+ "screenDataField": "screenData.editor.active",
617
+ "size": "medium",
618
+ "rootStyle": {
619
+ "className": "prc-cl-3"
620
+ },
621
+ "skeletonType": "FIELD",
622
+ "label": {
623
+ "title": "Active",
624
+ "isApply": true,
625
+ "position": "top",
626
+ "widgetStyle": {
627
+ "className": "qo-label prc-blc-inline-container"
628
+ }
629
+ },
630
+ "events": {
631
+ "onChange": {
632
+ "key": "MODIFY_MODEL"
633
+ }
634
+ }
635
+ },
636
+ {
637
+ "uiElementType": "WIDGET",
638
+ "widgetType": "TEXTAREA",
639
+ "applicableOn": "editorNotes",
640
+ "screenDataField": "screenData.editor.notes",
641
+ "rows": 3,
642
+ "placeHolder": "Anything the team should know",
643
+ "rootStyle": {
644
+ "className": "prc-cl-1"
645
+ },
646
+ "widgetStyle": {
647
+ "className": "qo-mw-100"
648
+ },
649
+ "skeletonType": "FIELD",
650
+ "label": {
651
+ "title": "Notes",
652
+ "isApply": true,
653
+ "position": "top",
654
+ "widgetStyle": {
655
+ "className": "qo-label prc-blc-inline-container"
656
+ }
657
+ },
658
+ "validations": [
659
+ {
660
+ "name": "MAX_CHAR_COUNT",
661
+ "message": "Notes are limited to 200 characters",
662
+ "executeOrder": 1,
663
+ "executeMode": "ONVALUECHANGE|ONSUBMIT",
664
+ "params": {
665
+ "maxCharCount": 200
666
+ }
667
+ }
668
+ ],
669
+ "events": {
670
+ "onChange": {
671
+ "key": "MODIFY_MODEL"
672
+ }
673
+ }
674
+ }
675
+ ]
676
+ },
677
+ {
678
+ "uiElementType": "CONTAINER",
679
+ "widgetStyle": {
680
+ "className": "prc-flx-container prc-nowrap qo-gap-x-sm"
681
+ },
682
+ "children": [
683
+ {
684
+ "_comment": "With an API: { key: FORMSUBMIT_CREATE_ASYNC, config: { apiConfig: { create: { endPoint: ..., responseConfig: { 201: { enableMessage: true, message: 'Contact added', alertType: 'toast' } } } } } }. The sample validates with the same validators, then hands the updated screenData to MODIFY_MODEL and opens the information dialog.",
685
+ "uiElementType": "WIDGET",
686
+ "widgetType": "BUTTON",
687
+ "title": "Save",
688
+ "widgetStyle": {
689
+ "className": "qo-btn qo-btn-primary"
690
+ },
691
+ "events": {
692
+ "onClick": {
693
+ "key": "CUS_SAMPLE_SAVE::MODIFY_MODEL",
694
+ "permissionKey": "sample.save"
695
+ }
696
+ }
697
+ },
698
+ {
699
+ "uiElementType": "WIDGET",
700
+ "widgetType": "BUTTON",
701
+ "title": "Cancel",
702
+ "widgetStyle": {
703
+ "className": "qo-btn qo-btn-outline-default qo-text-primary"
704
+ },
705
+ "events": {
706
+ "onClick": {
707
+ "key": "CUS_SET_FIELD::MODIFY_MODEL",
708
+ "config": {
709
+ "field": "screenData.editor",
710
+ "value": {
711
+ "mode": ""
712
+ }
713
+ }
714
+ }
715
+ }
716
+ }
717
+ ]
718
+ }
719
+ ]
720
+ },
721
+ {
722
+ "_comment": "The three dialogs. The grid writes { visibleMode: 'SHOW', onConfirm, onCancel, selectedData } to the two confirmation fields when a bulk action is picked; a SHOW rule on visibleMode opens each one, Yes runs the named grid event, either button nulls the field through onModelUpdate -> MODIFY_MODEL, which closes it. handleBody lets a custom action put the selection count into the text. The two alert widget names are misspelled in the library — write them exactly as they are.",
723
+ "uiElementType": "WIDGET",
724
+ "widgetType": "CONFOMATIONALERTBOX",
725
+ "visible": false,
726
+ "applicableOn": "deleteConfirm",
727
+ "screenDataField": "screenData.deleteConfirm",
728
+ "title": "Confirmation",
729
+ "messageHeader": "Delete the selected contacts?",
730
+ "messageBody": "You are about to delete {count} contact{s}. This cannot be undone.",
731
+ "iconName": "delete",
732
+ "confirmText": "Delete",
733
+ "cancelText": "Keep them",
734
+ "events": {
735
+ "onModelUpdate": {
736
+ "key": "MODIFY_MODEL"
737
+ },
738
+ "handleBody": {
739
+ "key": "CUS_SAMPLE_CONFIRM_BODY"
740
+ }
741
+ }
742
+ },
743
+ {
744
+ "uiElementType": "WIDGET",
745
+ "widgetType": "CONFOMATIONALERTBOX",
746
+ "visible": false,
747
+ "applicableOn": "statusConfirm",
748
+ "screenDataField": "screenData.statusConfirm",
749
+ "title": "Confirmation",
750
+ "messageHeader": "Change the status of the selected contacts?",
751
+ "messageBody": "{count} contact{s} will be updated.",
752
+ "confirmText": "Yes, change it",
753
+ "cancelText": "No",
754
+ "events": {
755
+ "onModelUpdate": {
756
+ "key": "MODIFY_MODEL"
757
+ },
758
+ "handleBody": {
759
+ "key": "CUS_SAMPLE_CONFIRM_BODY"
760
+ }
761
+ }
762
+ },
763
+ {
764
+ "_comment": "The success dialog. CUS_SAMPLE_SAVE writes { visibleMode: 'SHOW', message } to screenData.saveNotice; handleBody copies the message in; OK nulls the field and closes it.",
765
+ "uiElementType": "WIDGET",
766
+ "widgetType": "INFOMATIONALERTBOX",
767
+ "visible": false,
768
+ "applicableOn": "saveNotice",
769
+ "screenDataField": "screenData.saveNotice",
770
+ "title": "Saved",
771
+ "messageHeader": "Contact saved",
772
+ "messageBody": "{message}",
773
+ "confirmText": "OK",
774
+ "events": {
775
+ "onModelUpdate": {
776
+ "key": "MODIFY_MODEL"
777
+ },
778
+ "handleBody": {
779
+ "key": "CUS_SAMPLE_NOTICE_BODY"
780
+ }
781
+ }
782
+ }
783
+ ]
784
+ }
785
+ ]
786
+ },
787
+ "_ruleInterpreter_comment": "Conditional behaviour is declared, never coded. Each entry targets an element by its applicableOn (exact, case-sensitive match); an effect is one of the core's standard ones (SHOW, HIDE, DISABLE_ELEMENT, SET_PROPS, COMPUTE_VALUE, …) or a custom one registered in src/Factory/CustomRuleEffectFactory.ts — editorNotes below uses the custom CUS_READONLY. EQUAL compares as strings after String(). An entry that omits applicableOn matches the ROOT and can blank the whole screen.",
788
+ "ruleInterpreter": [
789
+ {
790
+ "applicableOn": "editor",
791
+ "rules": [
792
+ {
793
+ "effect": "SHOW",
794
+ "conditionList": [
795
+ {
796
+ "expressions": [
797
+ {
798
+ "id": 1,
799
+ "leftOperand": {
800
+ "field": "screenData.editor.mode",
801
+ "valueLoadType": "VALUE"
802
+ },
803
+ "operator": "EQUAL",
804
+ "rightOperand": {
805
+ "valueReplacePolicy": "SELF",
806
+ "value": "add"
807
+ }
808
+ },
809
+ {
810
+ "id": 2,
811
+ "leftOperand": {
812
+ "field": "screenData.editor.mode",
813
+ "valueLoadType": "VALUE"
814
+ },
815
+ "operator": "EQUAL",
816
+ "rightOperand": {
817
+ "valueReplacePolicy": "SELF",
818
+ "value": "edit"
819
+ }
820
+ }
821
+ ],
822
+ "criteria": "{{1}} || {{2}}"
823
+ }
824
+ ]
825
+ }
826
+ ]
827
+ },
828
+ {
829
+ "applicableOn": "editorRegion",
830
+ "rules": [
831
+ {
832
+ "effect": "SHOW",
833
+ "conditionList": [
834
+ {
835
+ "expressions": [
836
+ {
837
+ "id": 1,
838
+ "leftOperand": {
839
+ "field": "screenData.editor.department.value",
840
+ "valueLoadType": "VALUE"
841
+ },
842
+ "operator": "EQUAL",
843
+ "rightOperand": {
844
+ "valueReplacePolicy": "SELF",
845
+ "value": "Sales"
846
+ }
847
+ }
848
+ ],
849
+ "criteria": "{{1}}"
850
+ }
851
+ ]
852
+ }
853
+ ]
854
+ },
855
+ {
856
+ "applicableOn": "deleteConfirm",
857
+ "rules": [
858
+ {
859
+ "effect": "SHOW",
860
+ "conditionList": [
861
+ {
862
+ "expressions": [
863
+ {
864
+ "id": 1,
865
+ "leftOperand": {
866
+ "field": "screenData.deleteConfirm.visibleMode",
867
+ "valueLoadType": "VALUE"
868
+ },
869
+ "operator": "EQUAL",
870
+ "rightOperand": {
871
+ "valueReplacePolicy": "SELF",
872
+ "value": "SHOW"
873
+ }
874
+ }
875
+ ],
876
+ "criteria": "{{1}}"
877
+ }
878
+ ]
879
+ }
880
+ ]
881
+ },
882
+ {
883
+ "applicableOn": "statusConfirm",
884
+ "rules": [
885
+ {
886
+ "effect": "SHOW",
887
+ "conditionList": [
888
+ {
889
+ "expressions": [
890
+ {
891
+ "id": 1,
892
+ "leftOperand": {
893
+ "field": "screenData.statusConfirm.visibleMode",
894
+ "valueLoadType": "VALUE"
895
+ },
896
+ "operator": "EQUAL",
897
+ "rightOperand": {
898
+ "valueReplacePolicy": "SELF",
899
+ "value": "SHOW"
900
+ }
901
+ }
902
+ ],
903
+ "criteria": "{{1}}"
904
+ }
905
+ ]
906
+ }
907
+ ]
908
+ },
909
+ {
910
+ "applicableOn": "saveNotice",
911
+ "rules": [
912
+ {
913
+ "effect": "SHOW",
914
+ "conditionList": [
915
+ {
916
+ "expressions": [
917
+ {
918
+ "id": 1,
919
+ "leftOperand": {
920
+ "field": "screenData.saveNotice.visibleMode",
921
+ "valueLoadType": "VALUE"
922
+ },
923
+ "operator": "EQUAL",
924
+ "rightOperand": {
925
+ "valueReplacePolicy": "SELF",
926
+ "value": "SHOW"
927
+ }
928
+ }
929
+ ],
930
+ "criteria": "{{1}}"
931
+ }
932
+ ]
933
+ }
934
+ ]
935
+ },
936
+ {
937
+ "applicableOn": "editorNotes",
938
+ "rules": [
939
+ {
940
+ "effect": "CUS_READONLY",
941
+ "config": { "otherwise": false },
942
+ "conditionList": [
943
+ {
944
+ "expressions": [
945
+ {
946
+ "id": 1,
947
+ "leftOperand": {
948
+ "field": "screenData.editor.active",
949
+ "valueLoadType": "VALUE"
950
+ },
951
+ "operator": "EQUAL",
952
+ "rightOperand": {
953
+ "valueReplacePolicy": "SELF",
954
+ "value": "false"
955
+ }
956
+ }
957
+ ],
958
+ "criteria": "{{1}}"
959
+ }
960
+ ]
961
+ }
962
+ ]
963
+ }
964
+ ]
965
+ }