@things-factory/worklist 10.1.5 → 10.1.6

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 (71) hide show
  1. package/dist-client/components/activity-approval-ribon.js +17 -2
  2. package/dist-client/components/activity-approval-ribon.js.map +1 -1
  3. package/dist-client/components/decision-slots.d.ts +89 -0
  4. package/dist-client/components/decision-slots.js +111 -0
  5. package/dist-client/components/decision-slots.js.map +1 -0
  6. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +72 -0
  7. package/dist-client/pages/activity-approval/activity-approval-page.js +425 -43
  8. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  9. package/dist-client/pages/activity-catalog/activity-catalog-page.d.ts +11 -0
  10. package/dist-client/pages/activity-catalog/activity-catalog-page.js +48 -1
  11. package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
  12. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +27 -0
  13. package/dist-client/pages/installable-activity/installable-activity-list-page.js +70 -11
  14. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  15. package/dist-client/route.d.ts +1 -1
  16. package/dist-client/tsconfig.tsbuildinfo +1 -1
  17. package/dist-server/controllers/activity-approval/approval-line.d.ts +32 -0
  18. package/dist-server/controllers/activity-approval/approval-line.js +35 -0
  19. package/dist-server/controllers/activity-approval/approval-line.js.map +1 -1
  20. package/dist-server/controllers/activity-approval/approve.js +17 -0
  21. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  22. package/dist-server/controllers/activity-approval/reject.js +21 -1
  23. package/dist-server/controllers/activity-approval/reject.js.map +1 -1
  24. package/dist-server/controllers/activity-installation/activity-installation-controller.js +5 -9
  25. package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
  26. package/dist-server/controllers/activity-installation/installable-to-activity.d.ts +49 -0
  27. package/dist-server/controllers/activity-installation/installable-to-activity.js +40 -0
  28. package/dist-server/controllers/activity-installation/installable-to-activity.js.map +1 -0
  29. package/dist-server/controllers/activity-installation/registry-conflict.d.ts +32 -0
  30. package/dist-server/controllers/activity-installation/registry-conflict.js +21 -0
  31. package/dist-server/controllers/activity-installation/registry-conflict.js.map +1 -0
  32. package/dist-server/service/activity/activity-decision-key-item-type.d.ts +160 -0
  33. package/dist-server/service/activity/activity-decision-key-item-type.js +176 -0
  34. package/dist-server/service/activity/activity-decision-key-item-type.js.map +1 -0
  35. package/dist-server/service/activity/activity-type.d.ts +3 -0
  36. package/dist-server/service/activity/activity-type.js +8 -0
  37. package/dist-server/service/activity/activity-type.js.map +1 -1
  38. package/dist-server/service/activity/activity.d.ts +30 -0
  39. package/dist-server/service/activity/activity.js +14 -0
  40. package/dist-server/service/activity/activity.js.map +1 -1
  41. package/dist-server/service/activity/decision-key-declaration.d.ts +33 -0
  42. package/dist-server/service/activity/decision-key-declaration.js +67 -0
  43. package/dist-server/service/activity/decision-key-declaration.js.map +1 -0
  44. package/dist-server/service/activity/{credential-guard-subscriber.d.ts → declaration-guard-subscriber.d.ts} +6 -1
  45. package/dist-server/service/activity/declaration-guard-subscriber.js +46 -0
  46. package/dist-server/service/activity/declaration-guard-subscriber.js.map +1 -0
  47. package/dist-server/service/activity/index.d.ts +3 -3
  48. package/dist-server/service/activity/index.js +2 -2
  49. package/dist-server/service/activity/index.js.map +1 -1
  50. package/dist-server/service/activity/search-key-declaration.d.ts +38 -0
  51. package/dist-server/service/activity/search-key-declaration.js +49 -0
  52. package/dist-server/service/activity/search-key-declaration.js.map +1 -0
  53. package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
  54. package/dist-server/service/activity-instance/index.d.ts +1 -1
  55. package/dist-server/service/activity-template/index.d.ts +1 -1
  56. package/dist-server/service/activity-thread/index.d.ts +1 -1
  57. package/dist-server/service/index.d.ts +3 -3
  58. package/dist-server/service/installable-activity/installable-activity-mutation.js +24 -0
  59. package/dist-server/service/installable-activity/installable-activity-mutation.js.map +1 -1
  60. package/dist-server/service/installable-activity/installable-activity.d.ts +9 -0
  61. package/dist-server/service/installable-activity/installable-activity.js +5 -0
  62. package/dist-server/service/installable-activity/installable-activity.js.map +1 -1
  63. package/dist-server/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +9 -9
  65. package/translations/en.json +28 -6
  66. package/translations/ja.json +22 -0
  67. package/translations/ko.json +28 -6
  68. package/translations/ms.json +22 -0
  69. package/translations/zh.json +22 -0
  70. package/dist-server/service/activity/credential-guard-subscriber.js +0 -35
  71. package/dist-server/service/activity/credential-guard-subscriber.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
2
3
  import '@operato/property-editor/ox-properties-dynamic-view.js';
3
4
  import '@operato/board/ox-board-viewer.js';
4
5
  import '../../components/activity-approval-ribon.js';
@@ -13,7 +14,9 @@ import { i18next, localize } from '@operato/i18n';
13
14
  import { PageView } from '@operato/shell';
14
15
  import { CommonButtonStyles, ScrollbarStyles } from '@operato/styles';
15
16
  import { provider } from '@things-factory/board-ui/dist-client/board-provider.js';
16
- import { ActivityFieldsStyles, inputSpecOf, outputSpecOf, renderGiven } from '../../components/activity-fields.js';
17
+ import { isReferenceStep } from '@things-factory/organization/dist-client';
18
+ import { ActivityFieldsStyles, fieldValue, inputSpecOf, outputSpecOf, renderGiven } from '../../components/activity-fields.js';
19
+ import { decisionSlots, reversibilityOf, slotOfRole, slotsOfRole } from '../../components/decision-slots.js';
17
20
  const ActivityApprovalFetchResult = `\
18
21
  { id
19
22
  judgment
@@ -57,6 +60,12 @@ const ActivityApprovalFetchResult = `\
57
60
  description
58
61
  state
59
62
  activity { id
63
+ label
64
+ decisionKeys { role
65
+ inputPath
66
+ tKey
67
+ unit
68
+ }
60
69
  model { name
61
70
  description
62
71
  active
@@ -72,6 +81,7 @@ const ActivityApprovalFetchResult = `\
72
81
  }
73
82
  approvalLine { type
74
83
  value
84
+ participation
75
85
  approver { id
76
86
  name
77
87
  description
@@ -214,8 +224,154 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
214
224
  color: var(--ui-faint, #8a959c);
215
225
  word-break: keep-all;
216
226
  }
227
+
228
+ /*
229
+ * The decision, drawn from the activity's declaration.
230
+ *
231
+ * The order is the declaration's and the weight descends with it: what is being asked reads
232
+ * first at title size, then what it is about, then the words a person wrote, then what changes
233
+ * on approval, and last why it came up at all.
234
+ */
235
+ section[decision] .headline {
236
+ font-size: var(--ui-t-headline, 19px);
237
+ font-weight: 600;
238
+ line-height: 1.35;
239
+ color: var(--ui-ink, #17242c);
240
+ word-break: keep-all;
241
+ }
242
+
243
+ section[decision] .subject {
244
+ margin-top: 3px;
245
+ font-family: var(--ui-mono, ui-monospace, Menlo, monospace);
246
+ font-size: var(--ui-t-body, 13px);
247
+ color: var(--ui-muted, #57676f);
248
+ }
249
+
250
+ section[decision] .block {
251
+ margin-top: 14px;
252
+ }
253
+
254
+ section[decision] .block > .label {
255
+ margin-bottom: 4px;
256
+ font-size: var(--ui-t-small, 11.5px);
257
+ color: var(--ui-muted, #57676f);
258
+ }
259
+
260
+ /* What a person wrote is set apart, because it is the only part nobody computed. */
261
+ section[decision] .said {
262
+ padding: 9px 11px;
263
+ border-left: 3px solid var(--ui-line, #d5dce0);
264
+ background: var(--ui-line-soft, #f2f5f7);
265
+ color: var(--ui-ink, #17242c);
266
+ word-break: break-word;
267
+ }
268
+
269
+ section[decision] .none {
270
+ color: var(--ui-faint, #8a959c);
271
+ }
272
+
273
+ section[decision] .consequence {
274
+ margin-top: 8px;
275
+ padding: 9px 11px;
276
+ border: 1px solid var(--ui-line, #d5dce0);
277
+ border-radius: var(--ui-r-small, 6px);
278
+ }
279
+
280
+ section[decision] .consequence > .label {
281
+ margin-bottom: 3px;
282
+ font-size: var(--ui-t-small, 11.5px);
283
+ color: var(--ui-muted, #57676f);
284
+ }
285
+
286
+ /*
287
+ * Whether approving can be undone reads as the frame around the decision, not as one of its
288
+ * consequences. Among them it carries the same weight as "four orders get pushed back", and
289
+ * on an approval that moves equipment it is the heaviest line on the screen.
290
+ */
291
+ section[decision] .reversibility {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 6px;
295
+
296
+ margin-top: 12px;
297
+ padding: 8px 11px;
298
+ border-radius: var(--ui-r-small, 6px);
299
+
300
+ background: var(--ui-line-soft, #f2f5f7);
301
+ color: var(--ui-muted, #57676f);
302
+ font-size: var(--ui-t-small, 11.5px);
303
+ }
304
+
305
+ section[decision] .reversibility[irreversible] {
306
+ background: var(--md-sys-color-error-container, #ffdad6);
307
+ color: var(--md-sys-color-on-error-container, #410002);
308
+ font-weight: 600;
309
+ }
310
+
311
+ section[decision] .reversibility md-icon {
312
+ font-size: 1.15em;
313
+ }
314
+
315
+ /*
316
+ * What the decision was made from. Set flatter than the consequences — it is the record the
317
+ * approver checks, not something the approval brings about.
318
+ */
319
+ section[decision] .evidence {
320
+ margin-top: 8px;
321
+ padding: 7px 0 7px 11px;
322
+ border-left: 2px solid var(--ui-line, #d5dce0);
323
+ }
324
+
325
+ section[decision] .evidence > .label {
326
+ margin-bottom: 2px;
327
+ font-size: var(--ui-t-small, 11.5px);
328
+ color: var(--ui-muted, #57676f);
329
+ }
330
+
331
+ /*
332
+ * What is true right now sits apart from what the approval would change. Sharing the box the
333
+ * consequences use would say the approval causes these readings.
334
+ */
335
+ section[decision] .current-state {
336
+ margin-top: 14px;
337
+ padding: 9px 11px;
338
+ border: 1px dashed var(--ui-line, #d5dce0);
339
+ border-radius: var(--ui-r-small, 6px);
340
+ }
341
+
342
+ section[decision] .current-state > .label {
343
+ margin-bottom: 3px;
344
+ font-size: var(--ui-t-small, 11.5px);
345
+ color: var(--ui-muted, #57676f);
346
+ }
347
+
348
+ section[decision] .policy {
349
+ margin-top: 14px;
350
+ padding-top: 11px;
351
+ border-top: 1px solid var(--ui-line, #d5dce0);
352
+ font-size: var(--ui-t-small, 11.5px);
353
+ color: var(--ui-muted, #57676f);
354
+ }
217
355
  `
218
356
  ]; }
357
+ /**
358
+ * Is the reader seated here as a reference — someone who comments and does not decide?
359
+ *
360
+ * The rule comes from organization's client types, the same function its own approval-line editor
361
+ * and list use, so the two wire spellings of the enum are handled in one place. The approval line
362
+ * answers rather than ActivityApproval.participation, matching what the server refuses on
363
+ * (isReferenceOrder in approval-line.ts): the line decides who comes next, so letting the row
364
+ * decide who may judge would leave two authorities with nothing to settle a disagreement.
365
+ *
366
+ * The value comes back false while the query is still out. That is the same answer as for an
367
+ * approver, so the buttons show for a moment on a reference's screen — pressing one is refused by
368
+ * the server, which is why the server closed first.
369
+ */
370
+ get isReference() {
371
+ const approvalLine = this.activityThread?.activityInstance?.approvalLine;
372
+ const order = Number(this.activityApproval?.order ?? 0);
373
+ return isReferenceStep(approvalLine?.[order - 1]);
374
+ }
219
375
  get context() {
220
376
  const judgment = this.activityApproval?.judgment;
221
377
  return { title: this.lifecycle?.params?.['title'] || i18next.t('title.activity approval'),
@@ -227,6 +383,17 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
227
383
  * 어느 목록에서 열리는지는 이 화면이 안다. 그것을 적어 둔다.
228
384
  */
229
385
  menu: 'approval-pending-list',
386
+ /*
387
+ * A reference is offered neither approve nor reject.
388
+ *
389
+ * ActivityApprovalJudgment has carried Closed for this seat from the start — "the case ended"
390
+ * rather than a judgment — so a reference row was never meant to end in approved or rejected.
391
+ * The buttons were offered anyway, and the server accepted them until 2026-09-09.
392
+ *
393
+ * What stays is the note, because a comment is the whole point of the seat, and close. The
394
+ * hint under the textarea says which seat this is; buttons that simply vanish leave the
395
+ * reader looking for them.
396
+ */
230
397
  actions: judgment
231
398
  ? [
232
399
  { title: i18next.t('button.close'),
@@ -236,48 +403,16 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
236
403
  ...CommonButtonStyles.back
237
404
  }
238
405
  ]
239
- : /*
240
- * ── 세 단추의 순서와 무게 (2026-09-06) ──────────────────────────
241
- * 전에는 반려가 첫 자리였고, 승인 · 반려 · 저장이 모두 같은 모양이었다. 되돌리기 어려운
242
- * 것이 앞에 서고 무엇이 주된 흐름인지 화면이 말하지 않았다.
243
- *
244
- * 승인이 주 동작이다 — 채운 단추 하나. 반려는 대등한 선택이지만 예외 경로라 테두리
245
- * 단추로 그 옆에 둔다.
246
- *
247
- * 「저장」은 이름이 뜻을 말하지 않았다. 이 단추가 저장하는 것은 **의견 하나**이고
248
- * (saveActivityApproval 은 id 와 comment 만 보낸다), 판단은 미뤄 둔다. 그리고 그 결과는
249
- * 목록에서 「메모 있음」 표식으로 나타난다(activity-cells 의 approvalMarks). 목록이
250
- * 부르는 이름과 같은 낱말로 맞춘다.
251
- */
252
- [
253
- { title: i18next.t('button.approve'),
254
- action: this.approveActivityApproval.bind(this),
255
- icon: 'approval',
256
- emphasis: { raised: true,
257
- outlined: false,
258
- dense: false,
259
- danger: false
260
- }
261
- },
262
- { title: i18next.t('button.reject'),
263
- action: this.rejectActivityApproval.bind(this),
264
- icon: 'assignment_return',
265
- emphasis: { raised: false,
266
- outlined: true,
267
- dense: false,
268
- danger: false
269
- }
270
- },
271
- { title: i18next.t('button.save-note'),
406
+ : this.isReference
407
+ ? [
408
+ {
409
+ title: i18next.t('button.save-note'),
272
410
  action: this.saveActivityApproval.bind(this),
273
411
  icon: 'edit_note',
274
- emphasis: { raised: false,
275
- outlined: true,
276
- dense: false,
277
- danger: false
278
- }
412
+ emphasis: { raised: true, outlined: false, dense: false, danger: false }
279
413
  },
280
- { title: i18next.t('button.close'),
414
+ {
415
+ title: i18next.t('button.close'),
281
416
  action: () => {
282
417
  history.back();
283
418
  },
@@ -285,6 +420,55 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
285
420
  icon: 'close'
286
421
  }
287
422
  ]
423
+ : /*
424
+ * ── 세 단추의 순서와 무게 (2026-09-06) ──────────────────────────
425
+ * 전에는 반려가 첫 자리였고, 승인 · 반려 · 저장이 모두 같은 모양이었다. 되돌리기 어려운
426
+ * 것이 앞에 서고 무엇이 주된 흐름인지 화면이 말하지 않았다.
427
+ *
428
+ * 승인이 주 동작이다 — 채운 단추 하나. 반려는 대등한 선택이지만 예외 경로라 테두리
429
+ * 단추로 그 옆에 둔다.
430
+ *
431
+ * 「저장」은 이름이 뜻을 말하지 않았다. 이 단추가 저장하는 것은 **의견 하나**이고
432
+ * (saveActivityApproval 은 id 와 comment 만 보낸다), 판단은 미뤄 둔다. 그리고 그 결과는
433
+ * 목록에서 「메모 있음」 표식으로 나타난다(activity-cells 의 approvalMarks). 목록이
434
+ * 부르는 이름과 같은 낱말로 맞춘다.
435
+ */
436
+ [
437
+ { title: i18next.t('button.approve'),
438
+ action: this.approveActivityApproval.bind(this),
439
+ icon: 'approval',
440
+ emphasis: { raised: true,
441
+ outlined: false,
442
+ dense: false,
443
+ danger: false
444
+ }
445
+ },
446
+ { title: i18next.t('button.reject'),
447
+ action: this.rejectActivityApproval.bind(this),
448
+ icon: 'assignment_return',
449
+ emphasis: { raised: false,
450
+ outlined: true,
451
+ dense: false,
452
+ danger: false
453
+ }
454
+ },
455
+ { title: i18next.t('button.save-note'),
456
+ action: this.saveActivityApproval.bind(this),
457
+ icon: 'edit_note',
458
+ emphasis: { raised: false,
459
+ outlined: true,
460
+ dense: false,
461
+ danger: false
462
+ }
463
+ },
464
+ { title: i18next.t('button.close'),
465
+ action: () => {
466
+ history.back();
467
+ },
468
+ ...CommonButtonStyles.back,
469
+ icon: 'close'
470
+ }
471
+ ]
288
472
  // activityApproval: this.activityApproval
289
473
  };
290
474
  }
@@ -317,7 +501,9 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
317
501
  <section>
318
502
  <h2>${i18next.t('field.wl-comment')}</h2>
319
503
  <textarea
320
- placeholder=${String(i18next.t('text.explain the reason for approval/rejection'))}
504
+ placeholder=${String(i18next.t(this.isReference
505
+ ? 'text.write-your-comment-as-reference'
506
+ : 'text.explain the reason for approval/rejection'))}
321
507
  .value=${this.activityApproval?.comment}
322
508
  @change=${(e) => {
323
509
  this.activityApproval.comment = e.target.value;
@@ -326,8 +512,14 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
326
512
  <!--
327
513
  반려는 의견이 필수다(rejectActivityApproval). 전에는 그 규칙을 **누른 뒤에** 토스트로
328
514
  알렸다. 되돌릴 수 없는 것과 뜻이 다른 동작은 하기 전에 말한다.
515
+
516
+ A reference reads a different line: this one names the seat, because the approve
517
+ and reject buttons are absent for them and a reader whose buttons are missing
518
+ goes looking for them.
329
519
  -->
330
- <p class="hint">${i18next.t('text.approval-comment-hint')}</p>
520
+ <p class="hint">
521
+ ${i18next.t(this.isReference ? 'text.reference-comment-hint' : 'text.approval-comment-hint')}
522
+ </p>
331
523
  </section>
332
524
  </div>
333
525
  `
@@ -387,12 +579,163 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
387
579
  * approveActivityApproval / rejectActivityApproval 은 id 와 comment 만 보낸다 — 그 값은 어디에도
388
580
  * 가지 않는다. 결재자가 적을 수 있는 것은 의견 하나이고, 그것은 화면 아래 칸이 받는다.
389
581
  */
582
+ /**
583
+ * The decision the activity declared, or nothing when it declared none.
584
+ *
585
+ * Returning nothing is how generatedContent knows to fall back to the model. An activity with no
586
+ * decisionKeys draws exactly what it drew before.
587
+ *
588
+ * A slot whose path led nowhere is skipped rather than drawn empty. Older instances of an activity
589
+ * predate a field the declaration names — plant hit that with its `policy` value — and an empty
590
+ * bar on the screen would read as "there is no reason" when the truth is that this record has no
591
+ * such value. What cannot be shown is left out; what is shown is only what the payload actually
592
+ * carries.
593
+ *
594
+ * `reason` is the exception. Missing and blank mean the same thing to whoever is reading — nobody
595
+ * wrote one — so both draw the grey line rather than disappearing. A reason that silently vanishes
596
+ * would let an approver assume it was never asked for.
597
+ */
598
+ decisionContent() {
599
+ const { activityInstance } = this.activityThread || {};
600
+ const slots = decisionSlots(activityInstance?.activity?.decisionKeys, activityInstance?.input);
601
+ if (!slots.length) {
602
+ return nothing;
603
+ }
604
+ const headline = slotOfRole(slots, 'headline');
605
+ const subject = slotOfRole(slots, 'subject');
606
+ const reason = slotOfRole(slots, 'reason');
607
+ const consequences = slotsOfRole(slots, 'consequence').filter(slot => slot.found);
608
+ const policy = slotOfRole(slots, 'policy');
609
+ const evidence = slotsOfRole(slots, 'evidence').filter(slot => slot.found);
610
+ const currentState = slotOfRole(slots, 'currentState');
611
+ const reversibility = slotOfRole(slots, 'reversibility');
612
+ const drawable = [headline, subject, policy, currentState].some(slot => slot?.found) ||
613
+ !!reason ||
614
+ consequences.length ||
615
+ evidence.length ||
616
+ reversibilityOf(reversibility) !== undefined;
617
+ /*
618
+ * Every declared slot missed this payload. Saying so is not this screen's job — falling back to
619
+ * the model at least shows the values that are there, and the reader is no worse off than before
620
+ * the declaration existed.
621
+ */
622
+ if (!drawable) {
623
+ return nothing;
624
+ }
625
+ return keyed(this.activityApproval.id, html `
626
+ <section decision>
627
+ ${headline?.found ? html `<div class="headline">${this.slotText(headline)}</div>` : ''}
628
+ ${subject?.found ? html `<div class="subject">${this.slotText(subject)}</div>` : ''}
629
+ ${reason
630
+ ? html `
631
+ <div class="block">
632
+ <div class="label">${i18next.t('field.reason')}</div>
633
+ <div class="said ${this.hasWords(reason) ? '' : 'none'}">
634
+ ${this.hasWords(reason) ? this.slotText(reason) : i18next.t('text.no-reason-given')}
635
+ </div>
636
+ </div>
637
+ `
638
+ : ''}
639
+ ${this.renderReversibility(reversibility)}
640
+ ${evidence.length
641
+ ? html `
642
+ <div class="block">
643
+ <div class="label">${i18next.t('label.what-it-was-judged-from')}</div>
644
+ ${evidence.map(slot => html `
645
+ <div class="evidence">
646
+ ${slot.tKey ? html `<div class="label">${i18next.t(slot.tKey)}</div>` : ''}
647
+ <div>${fieldValue(slot.value)}</div>
648
+ </div>
649
+ `)}
650
+ </div>
651
+ `
652
+ : ''}
653
+ ${currentState?.found
654
+ ? html `
655
+ <div class="current-state">
656
+ <div class="label">${i18next.t(currentState.tKey || 'label.current-state')}</div>
657
+ <div>${fieldValue(currentState.value)}</div>
658
+ </div>
659
+ `
660
+ : ''}
661
+ ${consequences.length
662
+ ? html `
663
+ <div class="block">
664
+ <div class="label">${i18next.t('label.what-changes')}</div>
665
+ ${consequences.map(slot => html `
666
+ <div class="consequence">
667
+ ${slot.tKey ? html `<div class="label">${i18next.t(slot.tKey)}</div>` : ''}
668
+ <div>${fieldValue(slot.value)}</div>
669
+ </div>
670
+ `)}
671
+ </div>
672
+ `
673
+ : ''}
674
+ ${policy?.found
675
+ ? html `
676
+ <div class="policy">
677
+ ${i18next.t('label.why-approval')} — ${this.slotText(policy)}
678
+ </div>
679
+ `
680
+ : ''}
681
+ </section>
682
+ `);
683
+ }
684
+ /**
685
+ * Whether approving can be undone, drawn as the frame around the decision.
686
+ *
687
+ * Only an explicit true or false draws. Anything else — the path led nowhere, or the value is not
688
+ * a boolean — draws nothing, because saying "this can be undone" on a screen that does not know is
689
+ * the one mistake this line must never make.
690
+ *
691
+ * This is not rejectionIsFinal. That one says whether **rejecting** ends the case for good; this
692
+ * says whether **approving** can be taken back. The twin actuation carries both, which is exactly
693
+ * why they must not share a sentence.
694
+ */
695
+ renderReversibility(slot) {
696
+ const reversible = reversibilityOf(slot);
697
+ if (reversible === undefined) {
698
+ return '';
699
+ }
700
+ return html `
701
+ <div class="reversibility" ?irreversible=${!reversible}>
702
+ <md-icon>${reversible ? 'undo' : 'lock'}</md-icon>
703
+ <span>${i18next.t(reversible ? 'text.approval-is-reversible' : 'text.approval-is-irreversible')}</span>
704
+ </div>
705
+ `;
706
+ }
707
+ /**
708
+ * One slot's value as something to read.
709
+ *
710
+ * A scalar prints with its unit when one was declared. Anything shaped — an object, a list — goes
711
+ * through fieldValue, the same laying-out the model sections use, so a payload reads the same way
712
+ * wherever it appears and a four-level object does not become one long line.
713
+ */
714
+ slotText(slot) {
715
+ const scalar = slot.value !== null && slot.value !== undefined && typeof slot.value !== 'object';
716
+ if (scalar && slot.unit) {
717
+ return `${slot.value} ${slot.unit}`;
718
+ }
719
+ return scalar ? String(slot.value) : fieldValue(slot.value);
720
+ }
721
+ /** Did a person actually write something here. Whitespace is not words. */
722
+ hasWords(slot) {
723
+ return typeof slot.value === 'string' ? slot.value.trim().length > 0 : slot.found && slot.value != null;
724
+ }
390
725
  generatedContent() {
391
726
  if (!this.activityApproval) {
392
727
  return html ``;
393
728
  }
394
729
  const { output, activityInstance } = this.activityThread || {};
395
730
  const { input } = activityInstance || {};
731
+ /*
732
+ * The declaration goes first. It answers "what happens if I approve", which is the question this
733
+ * screen exists for; the model answers "what are the values", which is the fallback.
734
+ */
735
+ const decision = this.decisionContent();
736
+ if (decision !== nothing) {
737
+ return decision;
738
+ }
396
739
  const model = activityInstance?.activity?.model;
397
740
  const inputSpec = inputSpecOf(model);
398
741
  const outputSpec = outputSpecOf(model);
@@ -449,12 +792,37 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
449
792
  ></ox-board-viewer>
450
793
  `);
451
794
  }
795
+ /**
796
+ * The element the activity names, or a plain statement that it could not be loaded.
797
+ *
798
+ * document.createElement answers something for any name, so an activity pointing at an element
799
+ * nobody registered used to produce an empty card. The approver got a screen with nothing on it
800
+ * and the approve button still worked — plant shipped two such forms in one day and found them by
801
+ * reading the file list, not by opening the screen.
802
+ *
803
+ * The reader is told what to judge by instead. Same shape as the empty generated screen: what is
804
+ * missing, and where to look — the submission reason and the history in the timeline below.
805
+ *
806
+ * ── Waiting for a late definition ─────────────────────────────────────────
807
+ * An element can be registered after this page renders, and answering "not loaded" forever in
808
+ * that case would be a screen inventing a defect. whenDefined resolves whenever it arrives and
809
+ * the page redraws; a name nobody ever registers simply never resolves, and the statement stands.
810
+ */
452
811
  customElementContent() {
453
812
  if (!this.activityApproval) {
454
813
  return html ``;
455
814
  }
456
815
  const { output, activityInstance } = this.activityThread;
457
816
  const { viewSource: tagName, input } = activityInstance || {};
817
+ if (!tagName || !customElements.get(tagName)) {
818
+ if (tagName) {
819
+ customElements.whenDefined(tagName).then(() => this.requestUpdate());
820
+ }
821
+ return html `<section empty>
822
+ <span class="title">${i18next.t('text.approval-view-not-loaded')}</span>
823
+ <span class="why">${i18next.t('text.no-approval-content-why')}</span>
824
+ </section>`;
825
+ }
458
826
  const element = document.createElement(tagName);
459
827
  element.id = 'custom-content';
460
828
  element.activityId = activityInstance.activity.id;
@@ -586,7 +954,21 @@ __decorate([
586
954
  ], ActivityApprovalPage.prototype, "board", void 0);
587
955
  __decorate([
588
956
  query('textarea'),
589
- __metadata("design:type", HTMLTextAreaElement)
957
+ __metadata("design:type", HTMLTextAreaElement
958
+ /**
959
+ * Is the reader seated here as a reference — someone who comments and does not decide?
960
+ *
961
+ * The rule comes from organization's client types, the same function its own approval-line editor
962
+ * and list use, so the two wire spellings of the enum are handled in one place. The approval line
963
+ * answers rather than ActivityApproval.participation, matching what the server refuses on
964
+ * (isReferenceOrder in approval-line.ts): the line decides who comes next, so letting the row
965
+ * decide who may judge would leave two authorities with nothing to settle a disagreement.
966
+ *
967
+ * The value comes back false while the query is still out. That is the same answer as for an
968
+ * approver, so the buttons show for a moment on a reference's screen — pressing one is refused by
969
+ * the server, which is why the server closed first.
970
+ */
971
+ )
590
972
  ], ActivityApprovalPage.prototype, "commentInput", void 0);
591
973
  __decorate([
592
974
  query('ox-board-viewer'),