@things-factory/worklist 10.1.18 → 10.1.20
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/dist-client/components/activity-fields.d.ts +20 -0
- package/dist-client/components/activity-fields.js +23 -0
- package/dist-client/components/activity-fields.js.map +1 -1
- package/dist-client/components/decision-slots.d.ts +26 -0
- package/dist-client/components/decision-slots.js +24 -0
- package/dist-client/components/decision-slots.js.map +1 -1
- package/dist-client/pages/activity-approval/activity-approval-page.d.ts +13 -0
- package/dist-client/pages/activity-approval/activity-approval-page.js +29 -7
- package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-search-page.js +2 -1
- package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-view.js +2 -1
- package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/check-authority.d.ts +17 -0
- package/dist-server/controllers/activity-approval/check-authority.js +53 -0
- package/dist-server/controllers/activity-approval/check-authority.js.map +1 -1
- package/dist-server/controllers/activity-approval/own-request.d.ts +162 -0
- package/dist-server/controllers/activity-approval/own-request.js +135 -0
- package/dist-server/controllers/activity-approval/own-request.js.map +1 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +21 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +59 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -0
- package/dist-server/controllers/activity-installation/activity-installation-controller.js +2 -0
- package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
- package/dist-server/controllers/activity-installation/registry-conflict.d.ts +15 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js +26 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js.map +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.d.ts +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.js +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.js.map +1 -1
- package/dist-server/controllers/activity-instance/issue.js +38 -0
- package/dist-server/controllers/activity-instance/issue.js.map +1 -1
- package/dist-server/service/activity/activity-decision-key-item-type.d.ts +49 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js +9 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js.map +1 -1
- package/dist-server/service/activity/decision-key-declaration.d.ts +32 -0
- package/dist-server/service/activity/decision-key-declaration.js +81 -0
- package/dist-server/service/activity/decision-key-declaration.js.map +1 -1
- package/dist-server/service/activity-approval/my-pending-approvals.d.ts +16 -4
- package/dist-server/service/activity-approval/my-pending-approvals.js +19 -4
- package/dist-server/service/activity-approval/my-pending-approvals.js.map +1 -1
- package/dist-server/service/activity-summary/activity-summary-query.js +35 -14
- package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
|
@@ -35,6 +35,26 @@ export declare function renderGiven(specs: any[], values: any): import("lit-html
|
|
|
35
35
|
* 이름이 낫다.
|
|
36
36
|
*/
|
|
37
37
|
export declare function fieldLabel(spec: any): string;
|
|
38
|
+
/**
|
|
39
|
+
* 선언이 준 이름 — `tKey` 로 찾되, **없으면 키를 보이지 않는다.**
|
|
40
|
+
*
|
|
41
|
+
* 찾는 축(`searchKeys`)과 판단 자리(`decisionKeys`) 둘 다 이것을 쓴다. 두 선언이 같은 모양으로
|
|
42
|
+
* 이름을 나른다 — `tKey` 가 있으면 그 키로, 없으면 `name` 으로.
|
|
43
|
+
*
|
|
44
|
+
* `fieldLabel` 과 규칙이 하나 다르다. 그쪽은 `label.` 을 스스로 붙이지만 여기는 안 붙인다 — 이 두
|
|
45
|
+
* 선언의 `tKey` 는 선언한 모듈이 온전한 키로 적는다(plant 는 `field.lot`, twin 은 `twin.instance`).
|
|
46
|
+
* 접두사를 붙이면 맞게 적은 키가 오히려 안 찾아진다.
|
|
47
|
+
*
|
|
48
|
+
* ── 왜 물러날 자리가 필요한가 (2026-09-16) ────────────────────────────────
|
|
49
|
+
* i18next 는 없는 키를 **키 그대로** 돌려준다. 선언된 tKey 를 세어 보니 plant 는 서른 중 열아홉이,
|
|
50
|
+
* twin 은 셋 다 다섯 언어 어디에도 없었다. 그래서 그 표가 「어느 로트」 자리에 `lotName` 을, 「누가
|
|
51
|
+
* 냈나」 자리에 `twin.command-origin` 을 그리고 있다. 사용자에게 뜻이 없는 글자다.
|
|
52
|
+
*
|
|
53
|
+
* 없는 번역을 이 자리에서 지어낼 수는 없다. 대신 선언이 이미 준 `name` 으로 물러난다 — 개발자가 적은
|
|
54
|
+
* 영어지만 점과 낙타등이 섞인 키보다는 읽힌다. 같은 규율이 `fieldLabel` 에 이미 있었고, 이 규칙이
|
|
55
|
+
* 다섯 자리에 흩어져 있어서 한 자리만 고쳐서는 나머지 넷이 그대로 남았다.
|
|
56
|
+
*/
|
|
57
|
+
export declare function declaredLabel(item: any): string;
|
|
38
58
|
/**
|
|
39
59
|
* 값 한 칸.
|
|
40
60
|
*
|
|
@@ -272,6 +272,29 @@ export function fieldLabel(spec) {
|
|
|
272
272
|
const full = key.indexOf('label.') >= 0 ? key : 'label.' + key;
|
|
273
273
|
return i18next.t(full, { defaultValue: spec?.name || key });
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* 선언이 준 이름 — `tKey` 로 찾되, **없으면 키를 보이지 않는다.**
|
|
277
|
+
*
|
|
278
|
+
* 찾는 축(`searchKeys`)과 판단 자리(`decisionKeys`) 둘 다 이것을 쓴다. 두 선언이 같은 모양으로
|
|
279
|
+
* 이름을 나른다 — `tKey` 가 있으면 그 키로, 없으면 `name` 으로.
|
|
280
|
+
*
|
|
281
|
+
* `fieldLabel` 과 규칙이 하나 다르다. 그쪽은 `label.` 을 스스로 붙이지만 여기는 안 붙인다 — 이 두
|
|
282
|
+
* 선언의 `tKey` 는 선언한 모듈이 온전한 키로 적는다(plant 는 `field.lot`, twin 은 `twin.instance`).
|
|
283
|
+
* 접두사를 붙이면 맞게 적은 키가 오히려 안 찾아진다.
|
|
284
|
+
*
|
|
285
|
+
* ── 왜 물러날 자리가 필요한가 (2026-09-16) ────────────────────────────────
|
|
286
|
+
* i18next 는 없는 키를 **키 그대로** 돌려준다. 선언된 tKey 를 세어 보니 plant 는 서른 중 열아홉이,
|
|
287
|
+
* twin 은 셋 다 다섯 언어 어디에도 없었다. 그래서 그 표가 「어느 로트」 자리에 `lotName` 을, 「누가
|
|
288
|
+
* 냈나」 자리에 `twin.command-origin` 을 그리고 있다. 사용자에게 뜻이 없는 글자다.
|
|
289
|
+
*
|
|
290
|
+
* 없는 번역을 이 자리에서 지어낼 수는 없다. 대신 선언이 이미 준 `name` 으로 물러난다 — 개발자가 적은
|
|
291
|
+
* 영어지만 점과 낙타등이 섞인 키보다는 읽힌다. 같은 규율이 `fieldLabel` 에 이미 있었고, 이 규칙이
|
|
292
|
+
* 다섯 자리에 흩어져 있어서 한 자리만 고쳐서는 나머지 넷이 그대로 남았다.
|
|
293
|
+
*/
|
|
294
|
+
export function declaredLabel(item) {
|
|
295
|
+
const name = String(item?.name ?? '');
|
|
296
|
+
return item?.tKey ? i18next.t(item.tKey, { defaultValue: name || item.tKey }) : name;
|
|
297
|
+
}
|
|
275
298
|
/**
|
|
276
299
|
* 얼마나 깊이 펼치나.
|
|
277
300
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-fields.js","sourceRoot":"","sources":["../../client/components/activity-fields.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html } from 'lit'\n\nimport { i18next } from '@operato/i18n'\n\n/**\n * 업무 항목을 화면에 내는 방식 — **두 화면이 한 벌을 쓴다.**\n *\n * 업무 상세(activity-thread-page)와 결재 상세(activity-approval-page)는 거의 복사본이다. 항목을\n * 고르는 함수, 화면 종류에 따라 그리는 분기, 절을 그리는 부분이 두 파일에 같은 코드로 있었다.\n * 그래서 한쪽만 고치면 나머지가 그대로 남는다 — 실제로 그랬다(2026-09-06, 상세 화면의\n * Input · Output 영문 legend 를 고쳤는데 결재 화면에는 그대로 있었다).\n *\n * 화면마다 다른 것은 남긴다 — 상세는 결과를 적고, 결재는 읽기만 한다.\n */\n\n/** 절과 값 표의 모양. 카드 값은 SurfaceStyles 의 .ui-card 와 같다. */\nexport const ActivityFieldsStyles = css`\n section {\n margin: var(--spacing-large, 16px) var(--spacing-large, 16px) 0;\n padding: 14px;\n background: var(--ui-panel, #ffffff);\n border: 1px solid var(--ui-line, #d5dce0);\n border-radius: var(--ui-r, 10px);\n }\n\n section > h2 {\n margin: 0 0 10px;\n font-size: var(--ui-t-title, 15px);\n font-weight: 600;\n color: var(--ui-ink, #17242c);\n }\n\n /* 읽기만 하는 절 — 값을 바꿀 수 없다는 것이 보여야 한다. */\n section[readonly] > h2 {\n color: var(--ui-muted, #57676f);\n }\n\n /* 받은 값 — 이름과 값을 두 칸으로. 이름은 오른쪽 정렬이라 값의 왼쪽 끝이 한 줄로 선다. */\n dl.given {\n display: grid;\n grid-template-columns: minmax(80px, max-content) 1fr;\n gap: 7px var(--spacing-large, 16px);\n margin: 0;\n }\n\n dl.given dt {\n text-align: right;\n color: var(--ui-muted, #57676f);\n word-break: keep-all;\n }\n\n dl.given dd {\n margin: 0;\n color: var(--ui-ink, #17242c);\n word-break: break-word;\n }\n\n dl.given .none {\n color: var(--ui-faint, #8a959c);\n }\n\n dl.given code {\n font-family: var(--ui-mono, ui-monospace, Menlo, monospace);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 안쪽으로 펼친 오브젝트 · 배열.\n *\n * 바깥과 같은 두 칸 구조를 쓰되 **한 단 들여쓰고 왼쪽에 선을 긋는다.** 선이 없으면 세 단이 겹칠 때\n * 어느 이름이 어느 값에 딸린 것인지 눈으로 못 가른다.\n *\n * 안쪽 이름은 왼쪽 정렬이다. 바깥은 오른쪽 정렬이라 값의 왼쪽 끝이 한 줄로 서는데(dl.given dt),\n * 안쪽까지 그러면 단이 깊어질수록 이름이 값 쪽으로 밀려 붙는다.\n */\n dl.given .nested {\n margin: 2px 0 0;\n padding-left: 10px;\n border-left: 1px solid var(--ui-line, #d5dce0);\n }\n\n dl.given dl.nested {\n display: grid;\n grid-template-columns: minmax(60px, max-content) 1fr;\n gap: 4px var(--spacing-medium, 8px);\n }\n\n dl.given dl.nested dt {\n text-align: left;\n color: var(--ui-faint, #8a959c);\n font-family: var(--ui-mono, ui-monospace, Menlo, monospace);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 페이로드를 그대로 가져가는 자리.\n *\n * 화면은 값을 읽기 좋게 펴 놓지만, 이 값을 **다른 도구에 넣어 볼 일**이 있다 — 트윈 명령 하나가\n * 네 단짜리 오브젝트이고, 그것을 손으로 옮겨 적을 수는 없다. 화면이 편 글자가 아니라 원문 JSON 을\n * 준다. 붙여 넣는 쪽이 기대하는 것은 원문이다.\n *\n * 값 위에 얹지 않고 오른쪽 위 구석에 둔다 — 값이 한 줄일 때도 두 단 깊을 때도 자리가 같다.\n */\n dl.given dd:has(> .payload) {\n position: relative;\n }\n\n .copy {\n position: absolute;\n top: 0;\n right: 0;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 1px 5px;\n border: 1px solid var(--ui-line, #d5dce0);\n border-radius: 999px;\n background: var(--ui-panel, #ffffff);\n color: var(--ui-muted, #57676f);\n font-size: var(--ui-t-micro, 11px);\n line-height: 16px;\n cursor: pointer;\n }\n\n .copy md-icon {\n font-size: 13px;\n width: 13px;\n height: 13px;\n }\n\n .copy:hover {\n color: var(--ui-ink, #17242c);\n border-color: var(--ui-muted, #57676f);\n }\n\n .copy:focus-visible {\n outline: 2px solid var(--ui-accent, #00727e);\n outline-offset: 1px;\n }\n\n /* 복사한 직후 — 눌린 것이 보여야 한다. 소리도 움직임도 없는 조작이라 이 표시가 유일한 대답이다. */\n .copy[done] {\n border-color: var(--ui-good, #297d39);\n color: var(--ui-good, #297d39);\n }\n\n /*\n * 못 했을 때도 **같은 크기로** 말한다.\n *\n * 처음에는 실패를 title 로만 적었다. 그러면 누른 사람 눈에는 아무 일도 안 일어난 것과 같고,\n * 이유를 알려면 그 자리에 마우스를 올려 두어야 한다 — 손가락으로 쓰는 화면에는 그 길이 없다.\n * 클립보드는 브라우저가 막을 수 있는 자리라(보안 컨텍스트가 아니거나 권한이 없으면) 실제로 나는 일이다.\n */\n .copy[failed] {\n border-color: var(--ui-crit, #d14946);\n color: var(--ui-crit, #d14946);\n }\n\n /* 배열 — 번호로 몇 번째인지 말한다. 순서가 뜻을 갖는 값이 온다(주의 목록 · 권장 조치). */\n dl.given ol.nested {\n list-style: decimal;\n padding-left: 24px;\n }\n\n dl.given ol.nested li {\n margin: 2px 0;\n }\n\n dl.given ol.nested li::marker {\n color: var(--ui-faint, #8a959c);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 빈 화면은 가장 중요한 자리다. 없다고만 하지 않고 왜 비어 있는지 적는다.\n *\n * 두 가지가 있다. 화면 전체가 비는 것(건을 못 읽었다)은 세로로 늘려 가운데에 두고, 절 자리가\n * 비는 것(항목이 정의되지 않았다)은 **다른 절과 같은 카드**로 둔다. 절 하나가 없다고 해서\n * 화면 높이를 다 쓰면 그 아래의 타임라인이 화면 밖으로 밀린다.\n */\n section[empty] {\n display: flex;\n flex: none;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n padding: 28px 14px;\n text-align: center;\n color: var(--ui-muted, #57676f);\n }\n\n div[empty] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: var(--spacing-large, 16px);\n text-align: center;\n color: var(--ui-muted, #57676f);\n }\n\n [empty] .title {\n font-size: var(--ui-t-title, 15px);\n font-weight: 600;\n color: var(--ui-ink, #17242c);\n }\n\n [empty] .why {\n max-width: 40em;\n font-size: var(--ui-t-body, 13px);\n color: var(--ui-faint, #8a959c);\n word-break: keep-all;\n }\n`\n\n/**\n * 화면에 낼 항목을 고른다.\n *\n * hidden 을 지킨다. 전에는 이 선언을 보지 않았다 — 액티비티가 「이 항목은 보이지 않는다」고 적어\n * 두어도 화면에 나왔다. 편집기가 없는 타입(object 등)이라 조용히 빠졌을 뿐이고, 편집기가 있는\n * 타입이면 그대로 보였다. 값을 표로 그리기 시작하면서 실제로 드러났다 — 처분 승인 화면에\n * disposition 객체의 JSON 원문이 나왔다.\n */\nfunction specsOf(model: any[] | null | undefined, inouts: string[]) {\n return (model || [])\n .filter(item => !item.hidden)\n .filter(item => inouts.includes(item.inout))\n .map(item => ({\n ...item,\n name: item.tag,\n label: item.tKey || item.name,\n property: item.options\n }))\n}\n\n/** 받은 값 — 업무를 시작할 때 함께 온 것. */\nexport function inputSpecOf(model: any[] | null | undefined) {\n return specsOf(model, ['in', 'inout'])\n}\n\n/** 적어 낼 값 — 수행자가 채우는 것. */\nexport function outputSpecOf(model: any[] | null | undefined) {\n return specsOf(model, ['out', 'inout'])\n}\n\n/**\n * 값을 **표로** 보인다. 입력칸으로 그리지 않는다.\n *\n * 전에는 읽기만 하는 절도 ox-properties-dynamic-view 로 그렸다. 그러면 값마다 흰 입력 상자가\n * 생기고 사용자는 고쳐 쓸 수 있는 것으로 읽는다. 그런데 그 절에는 변경을 받는 자리가 없다 —\n * 결재 화면은 아예 output 을 mutation 에 싣지 않는다(approveActivityApproval 은 id 와 comment 만\n * 보낸다). 「적을 수 있게 보이는데 적히지 않는 칸」이라 가장 나쁜 쪽이다.\n */\nexport function renderGiven(specs: any[], values: any) {\n return html`\n <dl class=\"given\">\n ${specs.map(\n spec => html`\n <dt>${fieldLabel(spec)}</dt>\n <dd>${fieldValue(values?.[spec.name])}</dd>\n `\n )}\n </dl>\n `\n}\n\n/**\n * 항목 이름 — ox-property-editor 와 같은 규칙으로 찾는다(그쪽은 label. 접두사를 스스로 붙인다).\n *\n * 번역이 없으면 **키를 그대로 보이지 않는다.** 전에는 label.because 가 없어서 화면에\n * Label.Because 가 나왔다. 사용자에게 뜻이 없는 글자다.\n *\n * description 으로 물러나지도 않는다 — 액티비티의 description 은 개발자가 영어로 적어 둔 설명이다\n * (예: \"The disposition waiting for approval\"). 한국어 화면에 영어 문장이 나오는 것보다 항목\n * 이름이 낫다.\n */\nexport function fieldLabel(spec: any): string {\n const key = String(spec?.label ?? spec?.name ?? '')\n const full = key.indexOf('label.') >= 0 ? key : 'label.' + key\n\n return i18next.t(full, { defaultValue: spec?.name || key })\n}\n\n/**\n * 얼마나 깊이 펼치나.\n *\n * 이 화면들이 받는 값은 액티비티가 정하므로 얼마나 깊을지 알 수 없다. 트윈의 작동 명령은 네 단이다\n * (twinNow → attentions[0] → params → 값). 끝없이 펼치면 어떤 페이로드에서 화면이 통째로 페이로드가\n * 되므로, 이 깊이를 넘으면 원문을 그대로 보인다 — 접는 것이 아니라 **다른 방식으로 보이는 것**이다.\n */\nconst MAX_DEPTH = 4\n\n/**\n * 값 한 칸.\n *\n * ── 오브젝트를 한 줄 JSON 으로 내지 않는다 (2026-09-07) ─────────────────────\n * `JSON.stringify` 한 줄이었다. 트윈의 작동 명령을 열면 그 한 줄이 700자가 넘어서, 「업무 내용」이\n * 읽을 수 없는 글자 덩어리 하나가 됐다. 값은 다 있는데 아무것도 읽히지 않는다.\n *\n * 화면은 이미 이름과 값을 두 칸으로 보이는 말을 갖고 있다(dl.given). 오브젝트도 그 말로 편다 —\n * 새 표현을 만들지 않고, 안쪽이든 바깥쪽이든 같은 모양으로 읽힌다.\n *\n * 값이 없으면 빈 칸으로 두지 않는다 — 빈 칸은 값이 없는 것인지 화면이 못 낸 것인지 모른다.\n */\nexport function fieldValue(value: any, depth = 0): unknown {\n if (value === null || value === undefined || value === '') {\n return html`<span class=\"none\">${i18next.t('text.no-value')}</span>`\n }\n\n if (typeof value === 'boolean') {\n return i18next.t(value ? 'label.yes' : 'label.no', { defaultValue: String(value) })\n }\n\n if (typeof value !== 'object') {\n return String(value)\n }\n\n /* 너무 깊으면 펼치기를 그만두고 원문을 보인다 — 없는 척하지 않는다. */\n if (depth >= MAX_DEPTH) {\n return html`<code>${JSON.stringify(value)}</code>`\n }\n\n const laid = layOut(value, depth)\n\n /*\n * 맨 바깥 값에만 복사를 붙인다.\n *\n * 안쪽 단마다 달면 네 단짜리 페이로드에 단추가 열 개 넘게 붙어, 정작 읽으라고 편 값이 단추에\n * 묻힌다. 사람이 가져가려는 것은 「이 항목」이다.\n */\n return depth === 0 ? html`<span class=\"payload\">${copyButton(value)}${laid}</span>` : laid\n}\n\n/**\n * 이 값을 원문 그대로 가져간다.\n *\n * 화면이 편 글자가 아니라 JSON 을 준다 — 붙여 넣는 쪽(질의 · 로그 검색 · 문의 글)이 기대하는 것은\n * 원문이다. 들여쓰기를 넣어 그대로 읽을 수 있게 한다.\n *\n * 눌러도 화면이 안 바뀌면 눌린 줄을 모른다. 그래서 잠깐 표시를 남긴다 — 이 조작에는 그것 말고\n * 대답이 없다.\n */\nfunction copyButton(value: any) {\n const onCopy = async (e: Event) => {\n const button = e.currentTarget as HTMLElement\n\n const mark = (attr: 'done' | 'failed') => {\n button.toggleAttribute(attr, true)\n setTimeout(() => button.toggleAttribute(attr, false), 1500)\n }\n\n try {\n await navigator.clipboard.writeText(JSON.stringify(value, null, 2))\n } catch {\n /*\n * 클립보드는 브라우저가 막을 수 있다(보안 컨텍스트가 아니거나 권한이 없으면). 안 된 것을 된\n * 것처럼 말하지 않고, 성공과 같은 크기로 보인다 — title 만 바꾸면 누른 사람 눈에는 아무 일도\n * 안 일어난 것과 같다.\n */\n button.setAttribute('title', String(i18next.t('text.copy-failed')))\n mark('failed')\n return\n }\n\n mark('done')\n }\n\n return html`\n <button class=\"copy\" type=\"button\" title=${String(i18next.t('text.copy-payload'))} @click=${onCopy}>\n <md-icon>content_copy</md-icon>${i18next.t('button.copy')}\n </button>\n `\n}\n\n/** 값을 이름·값 두 칸으로 편다. 복사 단추는 여기 끼지 않는다 — 편 결과만 만든다. */\nfunction layOut(value: any, depth: number): unknown {\n if (Array.isArray(value)) {\n /* 빈 배열은 「값 없음」이 아니다 — 「비어 있다」는 그 자체로 사실이다. */\n if (value.length === 0) {\n return html`<span class=\"none\">${i18next.t('text.empty-list')}</span>`\n }\n\n /*\n * 배열은 깊이를 먹지 않는다 — **같은 것이 여러 개**이지 새로운 단이 아니다.\n *\n * 배열도 한 단으로 세었더니 트윈 작동 명령의 마지막 줄이 잘렸다: twinNow → attentions →\n * attentions[0] → recommendedActions → { code } 에서 recommendedActions 의 항목이 다섯 번째가\n * 되어, 정작 가장 단순한 { code: 'advice.raise-priority' } 하나가 JSON 원문으로 남았다.\n * 사람이 세는 단은 넷이다.\n */\n return html`\n <ol class=\"nested\">\n ${value.map(item => html`<li>${fieldValue(item, depth)}</li>`)}\n </ol>\n `\n }\n\n const keys = Object.keys(value)\n\n if (keys.length === 0) {\n return html`<span class=\"none\">${i18next.t('text.empty-object')}</span>`\n }\n\n /*\n * 안쪽 이름은 번역하지 않는다. 액티비티가 정한 키라 번역이 있을 리 없고, 없는 번역을 찾으면\n * 「Label.MaterialDefinitionId」 같은 글자가 나온다 — 사용자에게 뜻이 없는 글자다.\n */\n return html`\n <dl class=\"given nested\">\n ${keys.map(\n key => html`\n <dt>${key}</dt>\n <dd>${fieldValue(value[key], depth + 1)}</dd>\n `\n )}\n </dl>\n `\n}\n"]}
|
|
1
|
+
{"version":3,"file":"activity-fields.js","sourceRoot":"","sources":["../../client/components/activity-fields.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html } from 'lit'\n\nimport { i18next } from '@operato/i18n'\n\n/**\n * 업무 항목을 화면에 내는 방식 — **두 화면이 한 벌을 쓴다.**\n *\n * 업무 상세(activity-thread-page)와 결재 상세(activity-approval-page)는 거의 복사본이다. 항목을\n * 고르는 함수, 화면 종류에 따라 그리는 분기, 절을 그리는 부분이 두 파일에 같은 코드로 있었다.\n * 그래서 한쪽만 고치면 나머지가 그대로 남는다 — 실제로 그랬다(2026-09-06, 상세 화면의\n * Input · Output 영문 legend 를 고쳤는데 결재 화면에는 그대로 있었다).\n *\n * 화면마다 다른 것은 남긴다 — 상세는 결과를 적고, 결재는 읽기만 한다.\n */\n\n/** 절과 값 표의 모양. 카드 값은 SurfaceStyles 의 .ui-card 와 같다. */\nexport const ActivityFieldsStyles = css`\n section {\n margin: var(--spacing-large, 16px) var(--spacing-large, 16px) 0;\n padding: 14px;\n background: var(--ui-panel, #ffffff);\n border: 1px solid var(--ui-line, #d5dce0);\n border-radius: var(--ui-r, 10px);\n }\n\n section > h2 {\n margin: 0 0 10px;\n font-size: var(--ui-t-title, 15px);\n font-weight: 600;\n color: var(--ui-ink, #17242c);\n }\n\n /* 읽기만 하는 절 — 값을 바꿀 수 없다는 것이 보여야 한다. */\n section[readonly] > h2 {\n color: var(--ui-muted, #57676f);\n }\n\n /* 받은 값 — 이름과 값을 두 칸으로. 이름은 오른쪽 정렬이라 값의 왼쪽 끝이 한 줄로 선다. */\n dl.given {\n display: grid;\n grid-template-columns: minmax(80px, max-content) 1fr;\n gap: 7px var(--spacing-large, 16px);\n margin: 0;\n }\n\n dl.given dt {\n text-align: right;\n color: var(--ui-muted, #57676f);\n word-break: keep-all;\n }\n\n dl.given dd {\n margin: 0;\n color: var(--ui-ink, #17242c);\n word-break: break-word;\n }\n\n dl.given .none {\n color: var(--ui-faint, #8a959c);\n }\n\n dl.given code {\n font-family: var(--ui-mono, ui-monospace, Menlo, monospace);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 안쪽으로 펼친 오브젝트 · 배열.\n *\n * 바깥과 같은 두 칸 구조를 쓰되 **한 단 들여쓰고 왼쪽에 선을 긋는다.** 선이 없으면 세 단이 겹칠 때\n * 어느 이름이 어느 값에 딸린 것인지 눈으로 못 가른다.\n *\n * 안쪽 이름은 왼쪽 정렬이다. 바깥은 오른쪽 정렬이라 값의 왼쪽 끝이 한 줄로 서는데(dl.given dt),\n * 안쪽까지 그러면 단이 깊어질수록 이름이 값 쪽으로 밀려 붙는다.\n */\n dl.given .nested {\n margin: 2px 0 0;\n padding-left: 10px;\n border-left: 1px solid var(--ui-line, #d5dce0);\n }\n\n dl.given dl.nested {\n display: grid;\n grid-template-columns: minmax(60px, max-content) 1fr;\n gap: 4px var(--spacing-medium, 8px);\n }\n\n dl.given dl.nested dt {\n text-align: left;\n color: var(--ui-faint, #8a959c);\n font-family: var(--ui-mono, ui-monospace, Menlo, monospace);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 페이로드를 그대로 가져가는 자리.\n *\n * 화면은 값을 읽기 좋게 펴 놓지만, 이 값을 **다른 도구에 넣어 볼 일**이 있다 — 트윈 명령 하나가\n * 네 단짜리 오브젝트이고, 그것을 손으로 옮겨 적을 수는 없다. 화면이 편 글자가 아니라 원문 JSON 을\n * 준다. 붙여 넣는 쪽이 기대하는 것은 원문이다.\n *\n * 값 위에 얹지 않고 오른쪽 위 구석에 둔다 — 값이 한 줄일 때도 두 단 깊을 때도 자리가 같다.\n */\n dl.given dd:has(> .payload) {\n position: relative;\n }\n\n .copy {\n position: absolute;\n top: 0;\n right: 0;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 1px 5px;\n border: 1px solid var(--ui-line, #d5dce0);\n border-radius: 999px;\n background: var(--ui-panel, #ffffff);\n color: var(--ui-muted, #57676f);\n font-size: var(--ui-t-micro, 11px);\n line-height: 16px;\n cursor: pointer;\n }\n\n .copy md-icon {\n font-size: 13px;\n width: 13px;\n height: 13px;\n }\n\n .copy:hover {\n color: var(--ui-ink, #17242c);\n border-color: var(--ui-muted, #57676f);\n }\n\n .copy:focus-visible {\n outline: 2px solid var(--ui-accent, #00727e);\n outline-offset: 1px;\n }\n\n /* 복사한 직후 — 눌린 것이 보여야 한다. 소리도 움직임도 없는 조작이라 이 표시가 유일한 대답이다. */\n .copy[done] {\n border-color: var(--ui-good, #297d39);\n color: var(--ui-good, #297d39);\n }\n\n /*\n * 못 했을 때도 **같은 크기로** 말한다.\n *\n * 처음에는 실패를 title 로만 적었다. 그러면 누른 사람 눈에는 아무 일도 안 일어난 것과 같고,\n * 이유를 알려면 그 자리에 마우스를 올려 두어야 한다 — 손가락으로 쓰는 화면에는 그 길이 없다.\n * 클립보드는 브라우저가 막을 수 있는 자리라(보안 컨텍스트가 아니거나 권한이 없으면) 실제로 나는 일이다.\n */\n .copy[failed] {\n border-color: var(--ui-crit, #d14946);\n color: var(--ui-crit, #d14946);\n }\n\n /* 배열 — 번호로 몇 번째인지 말한다. 순서가 뜻을 갖는 값이 온다(주의 목록 · 권장 조치). */\n dl.given ol.nested {\n list-style: decimal;\n padding-left: 24px;\n }\n\n dl.given ol.nested li {\n margin: 2px 0;\n }\n\n dl.given ol.nested li::marker {\n color: var(--ui-faint, #8a959c);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /*\n * 빈 화면은 가장 중요한 자리다. 없다고만 하지 않고 왜 비어 있는지 적는다.\n *\n * 두 가지가 있다. 화면 전체가 비는 것(건을 못 읽었다)은 세로로 늘려 가운데에 두고, 절 자리가\n * 비는 것(항목이 정의되지 않았다)은 **다른 절과 같은 카드**로 둔다. 절 하나가 없다고 해서\n * 화면 높이를 다 쓰면 그 아래의 타임라인이 화면 밖으로 밀린다.\n */\n section[empty] {\n display: flex;\n flex: none;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n padding: 28px 14px;\n text-align: center;\n color: var(--ui-muted, #57676f);\n }\n\n div[empty] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: var(--spacing-large, 16px);\n text-align: center;\n color: var(--ui-muted, #57676f);\n }\n\n [empty] .title {\n font-size: var(--ui-t-title, 15px);\n font-weight: 600;\n color: var(--ui-ink, #17242c);\n }\n\n [empty] .why {\n max-width: 40em;\n font-size: var(--ui-t-body, 13px);\n color: var(--ui-faint, #8a959c);\n word-break: keep-all;\n }\n`\n\n/**\n * 화면에 낼 항목을 고른다.\n *\n * hidden 을 지킨다. 전에는 이 선언을 보지 않았다 — 액티비티가 「이 항목은 보이지 않는다」고 적어\n * 두어도 화면에 나왔다. 편집기가 없는 타입(object 등)이라 조용히 빠졌을 뿐이고, 편집기가 있는\n * 타입이면 그대로 보였다. 값을 표로 그리기 시작하면서 실제로 드러났다 — 처분 승인 화면에\n * disposition 객체의 JSON 원문이 나왔다.\n */\nfunction specsOf(model: any[] | null | undefined, inouts: string[]) {\n return (model || [])\n .filter(item => !item.hidden)\n .filter(item => inouts.includes(item.inout))\n .map(item => ({\n ...item,\n name: item.tag,\n label: item.tKey || item.name,\n property: item.options\n }))\n}\n\n/** 받은 값 — 업무를 시작할 때 함께 온 것. */\nexport function inputSpecOf(model: any[] | null | undefined) {\n return specsOf(model, ['in', 'inout'])\n}\n\n/** 적어 낼 값 — 수행자가 채우는 것. */\nexport function outputSpecOf(model: any[] | null | undefined) {\n return specsOf(model, ['out', 'inout'])\n}\n\n/**\n * 값을 **표로** 보인다. 입력칸으로 그리지 않는다.\n *\n * 전에는 읽기만 하는 절도 ox-properties-dynamic-view 로 그렸다. 그러면 값마다 흰 입력 상자가\n * 생기고 사용자는 고쳐 쓸 수 있는 것으로 읽는다. 그런데 그 절에는 변경을 받는 자리가 없다 —\n * 결재 화면은 아예 output 을 mutation 에 싣지 않는다(approveActivityApproval 은 id 와 comment 만\n * 보낸다). 「적을 수 있게 보이는데 적히지 않는 칸」이라 가장 나쁜 쪽이다.\n */\nexport function renderGiven(specs: any[], values: any) {\n return html`\n <dl class=\"given\">\n ${specs.map(\n spec => html`\n <dt>${fieldLabel(spec)}</dt>\n <dd>${fieldValue(values?.[spec.name])}</dd>\n `\n )}\n </dl>\n `\n}\n\n/**\n * 항목 이름 — ox-property-editor 와 같은 규칙으로 찾는다(그쪽은 label. 접두사를 스스로 붙인다).\n *\n * 번역이 없으면 **키를 그대로 보이지 않는다.** 전에는 label.because 가 없어서 화면에\n * Label.Because 가 나왔다. 사용자에게 뜻이 없는 글자다.\n *\n * description 으로 물러나지도 않는다 — 액티비티의 description 은 개발자가 영어로 적어 둔 설명이다\n * (예: \"The disposition waiting for approval\"). 한국어 화면에 영어 문장이 나오는 것보다 항목\n * 이름이 낫다.\n */\nexport function fieldLabel(spec: any): string {\n const key = String(spec?.label ?? spec?.name ?? '')\n const full = key.indexOf('label.') >= 0 ? key : 'label.' + key\n\n return i18next.t(full, { defaultValue: spec?.name || key })\n}\n\n/**\n * 선언이 준 이름 — `tKey` 로 찾되, **없으면 키를 보이지 않는다.**\n *\n * 찾는 축(`searchKeys`)과 판단 자리(`decisionKeys`) 둘 다 이것을 쓴다. 두 선언이 같은 모양으로\n * 이름을 나른다 — `tKey` 가 있으면 그 키로, 없으면 `name` 으로.\n *\n * `fieldLabel` 과 규칙이 하나 다르다. 그쪽은 `label.` 을 스스로 붙이지만 여기는 안 붙인다 — 이 두\n * 선언의 `tKey` 는 선언한 모듈이 온전한 키로 적는다(plant 는 `field.lot`, twin 은 `twin.instance`).\n * 접두사를 붙이면 맞게 적은 키가 오히려 안 찾아진다.\n *\n * ── 왜 물러날 자리가 필요한가 (2026-09-16) ────────────────────────────────\n * i18next 는 없는 키를 **키 그대로** 돌려준다. 선언된 tKey 를 세어 보니 plant 는 서른 중 열아홉이,\n * twin 은 셋 다 다섯 언어 어디에도 없었다. 그래서 그 표가 「어느 로트」 자리에 `lotName` 을, 「누가\n * 냈나」 자리에 `twin.command-origin` 을 그리고 있다. 사용자에게 뜻이 없는 글자다.\n *\n * 없는 번역을 이 자리에서 지어낼 수는 없다. 대신 선언이 이미 준 `name` 으로 물러난다 — 개발자가 적은\n * 영어지만 점과 낙타등이 섞인 키보다는 읽힌다. 같은 규율이 `fieldLabel` 에 이미 있었고, 이 규칙이\n * 다섯 자리에 흩어져 있어서 한 자리만 고쳐서는 나머지 넷이 그대로 남았다.\n */\nexport function declaredLabel(item: any): string {\n const name = String(item?.name ?? '')\n\n return item?.tKey ? i18next.t(item.tKey, { defaultValue: name || item.tKey }) : name\n}\n\n/**\n * 얼마나 깊이 펼치나.\n *\n * 이 화면들이 받는 값은 액티비티가 정하므로 얼마나 깊을지 알 수 없다. 트윈의 작동 명령은 네 단이다\n * (twinNow → attentions[0] → params → 값). 끝없이 펼치면 어떤 페이로드에서 화면이 통째로 페이로드가\n * 되므로, 이 깊이를 넘으면 원문을 그대로 보인다 — 접는 것이 아니라 **다른 방식으로 보이는 것**이다.\n */\nconst MAX_DEPTH = 4\n\n/**\n * 값 한 칸.\n *\n * ── 오브젝트를 한 줄 JSON 으로 내지 않는다 (2026-09-07) ─────────────────────\n * `JSON.stringify` 한 줄이었다. 트윈의 작동 명령을 열면 그 한 줄이 700자가 넘어서, 「업무 내용」이\n * 읽을 수 없는 글자 덩어리 하나가 됐다. 값은 다 있는데 아무것도 읽히지 않는다.\n *\n * 화면은 이미 이름과 값을 두 칸으로 보이는 말을 갖고 있다(dl.given). 오브젝트도 그 말로 편다 —\n * 새 표현을 만들지 않고, 안쪽이든 바깥쪽이든 같은 모양으로 읽힌다.\n *\n * 값이 없으면 빈 칸으로 두지 않는다 — 빈 칸은 값이 없는 것인지 화면이 못 낸 것인지 모른다.\n */\nexport function fieldValue(value: any, depth = 0): unknown {\n if (value === null || value === undefined || value === '') {\n return html`<span class=\"none\">${i18next.t('text.no-value')}</span>`\n }\n\n if (typeof value === 'boolean') {\n return i18next.t(value ? 'label.yes' : 'label.no', { defaultValue: String(value) })\n }\n\n if (typeof value !== 'object') {\n return String(value)\n }\n\n /* 너무 깊으면 펼치기를 그만두고 원문을 보인다 — 없는 척하지 않는다. */\n if (depth >= MAX_DEPTH) {\n return html`<code>${JSON.stringify(value)}</code>`\n }\n\n const laid = layOut(value, depth)\n\n /*\n * 맨 바깥 값에만 복사를 붙인다.\n *\n * 안쪽 단마다 달면 네 단짜리 페이로드에 단추가 열 개 넘게 붙어, 정작 읽으라고 편 값이 단추에\n * 묻힌다. 사람이 가져가려는 것은 「이 항목」이다.\n */\n return depth === 0 ? html`<span class=\"payload\">${copyButton(value)}${laid}</span>` : laid\n}\n\n/**\n * 이 값을 원문 그대로 가져간다.\n *\n * 화면이 편 글자가 아니라 JSON 을 준다 — 붙여 넣는 쪽(질의 · 로그 검색 · 문의 글)이 기대하는 것은\n * 원문이다. 들여쓰기를 넣어 그대로 읽을 수 있게 한다.\n *\n * 눌러도 화면이 안 바뀌면 눌린 줄을 모른다. 그래서 잠깐 표시를 남긴다 — 이 조작에는 그것 말고\n * 대답이 없다.\n */\nfunction copyButton(value: any) {\n const onCopy = async (e: Event) => {\n const button = e.currentTarget as HTMLElement\n\n const mark = (attr: 'done' | 'failed') => {\n button.toggleAttribute(attr, true)\n setTimeout(() => button.toggleAttribute(attr, false), 1500)\n }\n\n try {\n await navigator.clipboard.writeText(JSON.stringify(value, null, 2))\n } catch {\n /*\n * 클립보드는 브라우저가 막을 수 있다(보안 컨텍스트가 아니거나 권한이 없으면). 안 된 것을 된\n * 것처럼 말하지 않고, 성공과 같은 크기로 보인다 — title 만 바꾸면 누른 사람 눈에는 아무 일도\n * 안 일어난 것과 같다.\n */\n button.setAttribute('title', String(i18next.t('text.copy-failed')))\n mark('failed')\n return\n }\n\n mark('done')\n }\n\n return html`\n <button class=\"copy\" type=\"button\" title=${String(i18next.t('text.copy-payload'))} @click=${onCopy}>\n <md-icon>content_copy</md-icon>${i18next.t('button.copy')}\n </button>\n `\n}\n\n/** 값을 이름·값 두 칸으로 편다. 복사 단추는 여기 끼지 않는다 — 편 결과만 만든다. */\nfunction layOut(value: any, depth: number): unknown {\n if (Array.isArray(value)) {\n /* 빈 배열은 「값 없음」이 아니다 — 「비어 있다」는 그 자체로 사실이다. */\n if (value.length === 0) {\n return html`<span class=\"none\">${i18next.t('text.empty-list')}</span>`\n }\n\n /*\n * 배열은 깊이를 먹지 않는다 — **같은 것이 여러 개**이지 새로운 단이 아니다.\n *\n * 배열도 한 단으로 세었더니 트윈 작동 명령의 마지막 줄이 잘렸다: twinNow → attentions →\n * attentions[0] → recommendedActions → { code } 에서 recommendedActions 의 항목이 다섯 번째가\n * 되어, 정작 가장 단순한 { code: 'advice.raise-priority' } 하나가 JSON 원문으로 남았다.\n * 사람이 세는 단은 넷이다.\n */\n return html`\n <ol class=\"nested\">\n ${value.map(item => html`<li>${fieldValue(item, depth)}</li>`)}\n </ol>\n `\n }\n\n const keys = Object.keys(value)\n\n if (keys.length === 0) {\n return html`<span class=\"none\">${i18next.t('text.empty-object')}</span>`\n }\n\n /*\n * 안쪽 이름은 번역하지 않는다. 액티비티가 정한 키라 번역이 있을 리 없고, 없는 번역을 찾으면\n * 「Label.MaterialDefinitionId」 같은 글자가 나온다 — 사용자에게 뜻이 없는 글자다.\n */\n return html`\n <dl class=\"given nested\">\n ${keys.map(\n key => html`\n <dt>${key}</dt>\n <dd>${fieldValue(value[key], depth + 1)}</dd>\n `\n )}\n </dl>\n `\n}\n"]}
|
|
@@ -18,12 +18,38 @@ export type DecisionKeyDeclaration = {
|
|
|
18
18
|
name?: string;
|
|
19
19
|
tKey?: string;
|
|
20
20
|
unit?: string;
|
|
21
|
+
/** i18n key for the value **sentence** — `approval.<door>.<inputPath>`, computed by the server. */
|
|
22
|
+
valueTKey?: string;
|
|
23
|
+
/** What the sentence puts in its blanks: name in the sentence → inputPath to read it from. */
|
|
24
|
+
params?: {
|
|
25
|
+
[name: string]: string;
|
|
26
|
+
};
|
|
21
27
|
};
|
|
22
28
|
export type DecisionSlot = {
|
|
23
29
|
role: string;
|
|
24
30
|
inputPath: string;
|
|
31
|
+
/**
|
|
32
|
+
* 번역이 없을 때 화면이 보일 수 있는 단 하나의 글.
|
|
33
|
+
*
|
|
34
|
+
* 선언은 늘 이것을 주는데 슬롯이 버리고 있었다. 그래서 `tKey` 가 다섯 언어 어디에도 없으면 화면에
|
|
35
|
+
* 남는 것이 점 섞인 키뿐이었다 — 보일 것이 없는 게 아니라 **가진 것을 안 나른 것**이다.
|
|
36
|
+
*/
|
|
37
|
+
name?: string;
|
|
25
38
|
tKey?: string;
|
|
26
39
|
unit?: string;
|
|
40
|
+
/** i18n key for the value sentence. Absent means the frozen `value` is what this slot has. */
|
|
41
|
+
valueTKey?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The sentence's blanks, **already read out of the input.**
|
|
44
|
+
*
|
|
45
|
+
* The declaration says where each value lives; this says what was there. Resolving here rather
|
|
46
|
+
* than on the screen keeps the path language in one place — the same `valueAtPath` the slot's own
|
|
47
|
+
* value goes through, so a params path that leads nowhere behaves the way an inputPath does
|
|
48
|
+
* instead of inventing a second rule.
|
|
49
|
+
*/
|
|
50
|
+
params?: {
|
|
51
|
+
[name: string]: unknown;
|
|
52
|
+
};
|
|
27
53
|
/** What the path led to. Undefined both when the path leads nowhere and when the value is unset. */
|
|
28
54
|
value: unknown;
|
|
29
55
|
/**
|
|
@@ -73,13 +73,37 @@ export function decisionSlots(decisionKeys, input) {
|
|
|
73
73
|
return {
|
|
74
74
|
role: String(key?.role ?? ''),
|
|
75
75
|
inputPath: String(key?.inputPath ?? ''),
|
|
76
|
+
name: key?.name,
|
|
76
77
|
tKey: key?.tKey,
|
|
77
78
|
unit: key?.unit,
|
|
79
|
+
valueTKey: key?.valueTKey,
|
|
80
|
+
params: paramsFrom(key?.params, input),
|
|
78
81
|
value,
|
|
79
82
|
found
|
|
80
83
|
};
|
|
81
84
|
});
|
|
82
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the sentence's blanks out of the input.
|
|
88
|
+
*
|
|
89
|
+
* A path that leads nowhere lands as `undefined`, the same as a slot's own value — i18next prints
|
|
90
|
+
* an empty blank rather than the word `undefined`, and the sentence still says the rest. Folding it
|
|
91
|
+
* into an empty string here would hide which of the two it was from anyone looking later.
|
|
92
|
+
*
|
|
93
|
+
* Returns undefined when nothing was declared, so a slot without a sentence carries no empty object
|
|
94
|
+
* for a reader to puzzle over.
|
|
95
|
+
*/
|
|
96
|
+
function paramsFrom(declared, input) {
|
|
97
|
+
const names = Object.keys(declared || {});
|
|
98
|
+
if (!names.length) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const read = {};
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
read[name] = valueAtPath(input, String(declared[name] ?? '')).value;
|
|
104
|
+
}
|
|
105
|
+
return read;
|
|
106
|
+
}
|
|
83
107
|
/** The slots for one role, in declared order. Empty when the activity declared none. */
|
|
84
108
|
export function slotsOfRole(slots, role) {
|
|
85
109
|
return slots.filter(slot => slot.role === role);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-slots.js","sourceRoot":"","sources":["../../client/components/decision-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,aAAa,EAAE,UAAU,CAAU,CAAA;AAEpE,4FAA4F;AAC5F,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,UAAU;IACV,SAAS;IACT,eAAe;IACf,QAAQ;IACR,UAAU;IACV,cAAc;IACd,aAAa;IACb,QAAQ;CACA,CAAA;AA4BV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,KAAU,EAAE,IAAY;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SAC7B,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAElC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IAC3C,CAAC;IAED,IAAI,EAAE,GAAQ,KAAK,CAAA;IAEnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,SAAS,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;YAC9D,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;QAC3C,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YACpD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;QAC3C,CAAC;QAED,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACf,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AACnC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,YAAyD,EACzD,KAAU;IAEV,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC5B,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,CAAA;QAEzE,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC;YAC7B,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC;YACvC,IAAI,EAAE,GAAG,EAAE,IAAI;YACf,IAAI,EAAE,GAAG,EAAE,IAAI;YACf,KAAK;YACL,KAAK;SACN,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,KAAqB,EAAE,IAAY;IAC7D,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAqB,EAAE,IAAY;IAC5D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC/C,CAAC;AAwBD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,KAAK,GAAQ,IAAI,EAAE,KAAK,CAAA;IAE9B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACjF,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,GAAG,CAAC,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7F,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAmB;IAC1D,OAAO,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,SAAS,CAAA;AAC1F,CAAC","sourcesContent":["/**\n * Turns an activity's decisionKeys declaration plus an instance's input into the slots a decision\n * screen draws.\n *\n * ── Nothing is imported ───────────────────────────────────────────────────\n * No lit, no i18next. The rule is a mapping from a declaration and a payload to an ordered list,\n * and keeping it free of imports is what lets the test runner hold it: that runner parses\n * TypeScript by stripping types and reads neither decorators nor enums, so one heavy import takes\n * a whole test file down with it. Labels and markup are the caller's half.\n */\n\n/** The roles that may appear more than once. Every other role names one value. */\nexport const REPEATABLE_ROLES = ['consequence', 'evidence'] as const\n\n/** Listed in the order the screen reads them: what is asked, then past, present, future. */\nexport const DECISION_ROLES = [\n 'headline',\n 'subject',\n 'reversibility',\n 'reason',\n 'evidence',\n 'currentState',\n 'consequence',\n 'policy'\n] as const\n\nexport type DecisionKeyDeclaration = {\n role?: string\n inputPath?: string\n name?: string\n tKey?: string\n unit?: string\n}\n\nexport type DecisionSlot = {\n role: string\n inputPath: string\n tKey?: string\n unit?: string\n /** What the path led to. Undefined both when the path leads nowhere and when the value is unset. */\n value: unknown\n /**\n * Whether the path led anywhere at all.\n *\n * A reason nobody wrote and a reason whose path is wrong look identical in `value`, and the screen\n * owes different answers: the first is 「사유 없음」 in grey, the second is a declaration that does\n * not match the payload and must not be drawn as if the person left it blank. Folding them into\n * one value is the fallback that shifts meaning.\n */\n found: boolean\n}\n\n/**\n * Reads a dotted path out of the input.\n *\n * Dots only. An array index or a wildcard is not part of the path language — drawing a list is the\n * slot's job — so 'shift.passes[0]' looks for a key spelled \"passes[0]\", finds nothing, and reports\n * found: false. The declaration guard refuses such a path when it arrives, so this is the second\n * line rather than the first.\n *\n * A path that runs into a non-object partway reports found: false rather than throwing. The screen\n * has to draw something either way, and a thrown error here would take the whole approval down over\n * one slot.\n */\nexport function valueAtPath(input: any, path: string): { value: unknown; found: boolean } {\n const steps = String(path ?? '')\n .split('.')\n .filter(step => step.length > 0)\n\n if (!steps.length) {\n return { value: undefined, found: false }\n }\n\n let at: any = input\n\n for (const step of steps) {\n if (at === null || at === undefined || typeof at !== 'object') {\n return { value: undefined, found: false }\n }\n\n if (!Object.prototype.hasOwnProperty.call(at, step)) {\n return { value: undefined, found: false }\n }\n\n at = at[step]\n }\n\n return { value: at, found: true }\n}\n\n/**\n * The slots to draw, in the order they were declared.\n *\n * Declaration order is the screen's hierarchy: plant wrote four of these screens by hand and all\n * four put the headline first, then the subject, the reason, what changes, and why it came up. That\n * agreement is what makes the order part of the contract rather than a preference, so this function\n * neither sorts nor groups.\n *\n * Every declared key produces a slot, including one whose path leads nowhere. Dropping those would\n * leave the screen unable to say that a declaration and a payload disagree, and a reader would see\n * a screen that looks complete.\n */\nexport function decisionSlots(\n decisionKeys: DecisionKeyDeclaration[] | null | undefined,\n input: any\n): DecisionSlot[] {\n if (!decisionKeys?.length) {\n return []\n }\n\n return decisionKeys.map(key => {\n const { value, found } = valueAtPath(input, String(key?.inputPath ?? ''))\n\n return {\n role: String(key?.role ?? ''),\n inputPath: String(key?.inputPath ?? ''),\n tKey: key?.tKey,\n unit: key?.unit,\n value,\n found\n }\n })\n}\n\n/** The slots for one role, in declared order. Empty when the activity declared none. */\nexport function slotsOfRole(slots: DecisionSlot[], role: string): DecisionSlot[] {\n return slots.filter(slot => slot.role === role)\n}\n\n/**\n * The single slot for a role that names one value, or undefined.\n *\n * Reaching for a second one would be reading a declaration the guard refuses, so this answers the\n * first and does not quietly merge.\n */\nexport function slotOfRole(slots: DecisionSlot[], role: string): DecisionSlot | undefined {\n return slots.find(slot => slot.role === role)\n}\n\n/**\n * What the declaration says about undoing this approval.\n *\n * The value is an object: `reversible` is required and `caveat` is the module's own sentence about\n * it. The asymmetry is the point — the boolean decides how heavily the screen draws the band, and\n * a sentence alone cannot decide that.\n *\n * { reversible: false, caveat: '버린 자재는 돌아오지 않습니다' }\n *\n * ── A condition is written as false ───────────────────────────────────────\n * 「되돌릴 수 있습니다 — 단, 실적이 마감되기 전까지」 is written `false` with the condition in the\n * caveat. Written `true` the screen draws it lightly and the reader walks past the condition. There\n * is no third value: a middle one always draws ambiguously, and the reader has to guess which side\n * it is on. (2026-09-11, architect's ruling)\n *\n * ── Not `note` ───────────────────────────────────────────────────────────\n * ops-contract already has CommandApproval.note, and that one is **the approver's own comment** —\n * a person's words, in the same neighbourhood as this screen. Same word for a module's sentence\n * would be read as the person's. `caveat` is used nowhere in the three repositories.\n */\nexport type Reversibility = { reversible: boolean; caveat?: string }\n\n/**\n * What the slot says, or undefined when it does not say.\n *\n * Undefined covers both \"the path led nowhere\" and \"nobody declared the role\". Drawing nothing is\n * right for both: telling an approver an action can be taken back when the payload never said so\n * is the one mistake this line cannot make, and a truthy read would make it — the string\n * '되돌릴 수 없습니다…' is truthy.\n */\nexport function reversibilityOf(slot?: DecisionSlot): Reversibility | undefined {\n const value: any = slot?.value\n\n if (!value || typeof value !== 'object' || typeof value.reversible !== 'boolean') {\n return undefined\n }\n\n return {\n reversible: value.reversible,\n ...(typeof value.caveat === 'string' && value.caveat.trim() ? { caveat: value.caveat } : {})\n }\n}\n\n/**\n * The value is there but not the shape this role takes.\n *\n * plant declared the role and put a sentence at that path, so the band drew nothing at all and the\n * heaviest line on the screen was silently missing. Reporting nothing to the reader is right — the\n * screen cannot tell which way it goes — but a developer should not have to find it by hand. The\n * caller logs this; the rule stays free of side effects so a test can hold it.\n */\nexport function reversibilityIsMalformed(slot?: DecisionSlot): boolean {\n return !!slot && slot.found && slot.value != null && reversibilityOf(slot) === undefined\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-slots.js","sourceRoot":"","sources":["../../client/components/decision-slots.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Turns an activity's decisionKeys declaration plus an instance's input into the slots a decision\n * screen draws.\n *\n * ── Nothing is imported ───────────────────────────────────────────────────\n * No lit, no i18next. The rule is a mapping from a declaration and a payload to an ordered list,\n * and keeping it free of imports is what lets the test runner hold it: that runner parses\n * TypeScript by stripping types and reads neither decorators nor enums, so one heavy import takes\n * a whole test file down with it. Labels and markup are the caller's half.\n */\n\n/** The roles that may appear more than once. Every other role names one value. */\nexport const REPEATABLE_ROLES = ['consequence', 'evidence'] as const\n\n/** Listed in the order the screen reads them: what is asked, then past, present, future. */\nexport const DECISION_ROLES = [\n 'headline',\n 'subject',\n 'reversibility',\n 'reason',\n 'evidence',\n 'currentState',\n 'consequence',\n 'policy'\n] as const\n\nexport type DecisionKeyDeclaration = {\n role?: string\n inputPath?: string\n name?: string\n tKey?: string\n unit?: string\n /** i18n key for the value **sentence** — `approval.<door>.<inputPath>`, computed by the server. */\n valueTKey?: string\n /** What the sentence puts in its blanks: name in the sentence → inputPath to read it from. */\n params?: { [name: string]: string }\n}\n\nexport type DecisionSlot = {\n role: string\n inputPath: string\n /**\n * 번역이 없을 때 화면이 보일 수 있는 단 하나의 글.\n *\n * 선언은 늘 이것을 주는데 슬롯이 버리고 있었다. 그래서 `tKey` 가 다섯 언어 어디에도 없으면 화면에\n * 남는 것이 점 섞인 키뿐이었다 — 보일 것이 없는 게 아니라 **가진 것을 안 나른 것**이다.\n */\n name?: string\n tKey?: string\n unit?: string\n /** i18n key for the value sentence. Absent means the frozen `value` is what this slot has. */\n valueTKey?: string\n /**\n * The sentence's blanks, **already read out of the input.**\n *\n * The declaration says where each value lives; this says what was there. Resolving here rather\n * than on the screen keeps the path language in one place — the same `valueAtPath` the slot's own\n * value goes through, so a params path that leads nowhere behaves the way an inputPath does\n * instead of inventing a second rule.\n */\n params?: { [name: string]: unknown }\n /** What the path led to. Undefined both when the path leads nowhere and when the value is unset. */\n value: unknown\n /**\n * Whether the path led anywhere at all.\n *\n * A reason nobody wrote and a reason whose path is wrong look identical in `value`, and the screen\n * owes different answers: the first is 「사유 없음」 in grey, the second is a declaration that does\n * not match the payload and must not be drawn as if the person left it blank. Folding them into\n * one value is the fallback that shifts meaning.\n */\n found: boolean\n}\n\n/**\n * Reads a dotted path out of the input.\n *\n * Dots only. An array index or a wildcard is not part of the path language — drawing a list is the\n * slot's job — so 'shift.passes[0]' looks for a key spelled \"passes[0]\", finds nothing, and reports\n * found: false. The declaration guard refuses such a path when it arrives, so this is the second\n * line rather than the first.\n *\n * A path that runs into a non-object partway reports found: false rather than throwing. The screen\n * has to draw something either way, and a thrown error here would take the whole approval down over\n * one slot.\n */\nexport function valueAtPath(input: any, path: string): { value: unknown; found: boolean } {\n const steps = String(path ?? '')\n .split('.')\n .filter(step => step.length > 0)\n\n if (!steps.length) {\n return { value: undefined, found: false }\n }\n\n let at: any = input\n\n for (const step of steps) {\n if (at === null || at === undefined || typeof at !== 'object') {\n return { value: undefined, found: false }\n }\n\n if (!Object.prototype.hasOwnProperty.call(at, step)) {\n return { value: undefined, found: false }\n }\n\n at = at[step]\n }\n\n return { value: at, found: true }\n}\n\n/**\n * The slots to draw, in the order they were declared.\n *\n * Declaration order is the screen's hierarchy: plant wrote four of these screens by hand and all\n * four put the headline first, then the subject, the reason, what changes, and why it came up. That\n * agreement is what makes the order part of the contract rather than a preference, so this function\n * neither sorts nor groups.\n *\n * Every declared key produces a slot, including one whose path leads nowhere. Dropping those would\n * leave the screen unable to say that a declaration and a payload disagree, and a reader would see\n * a screen that looks complete.\n */\nexport function decisionSlots(\n decisionKeys: DecisionKeyDeclaration[] | null | undefined,\n input: any\n): DecisionSlot[] {\n if (!decisionKeys?.length) {\n return []\n }\n\n return decisionKeys.map(key => {\n const { value, found } = valueAtPath(input, String(key?.inputPath ?? ''))\n\n return {\n role: String(key?.role ?? ''),\n inputPath: String(key?.inputPath ?? ''),\n name: key?.name,\n tKey: key?.tKey,\n unit: key?.unit,\n valueTKey: key?.valueTKey,\n params: paramsFrom(key?.params, input),\n value,\n found\n }\n })\n}\n\n/**\n * Read the sentence's blanks out of the input.\n *\n * A path that leads nowhere lands as `undefined`, the same as a slot's own value — i18next prints\n * an empty blank rather than the word `undefined`, and the sentence still says the rest. Folding it\n * into an empty string here would hide which of the two it was from anyone looking later.\n *\n * Returns undefined when nothing was declared, so a slot without a sentence carries no empty object\n * for a reader to puzzle over.\n */\nfunction paramsFrom(\n declared: { [name: string]: string } | undefined,\n input: { [key: string]: any } | undefined | null\n): { [name: string]: unknown } | undefined {\n const names = Object.keys(declared || {})\n\n if (!names.length) {\n return undefined\n }\n\n const read: { [name: string]: unknown } = {}\n\n for (const name of names) {\n read[name] = valueAtPath(input, String(declared![name] ?? '')).value\n }\n\n return read\n}\n\n/** The slots for one role, in declared order. Empty when the activity declared none. */\nexport function slotsOfRole(slots: DecisionSlot[], role: string): DecisionSlot[] {\n return slots.filter(slot => slot.role === role)\n}\n\n/**\n * The single slot for a role that names one value, or undefined.\n *\n * Reaching for a second one would be reading a declaration the guard refuses, so this answers the\n * first and does not quietly merge.\n */\nexport function slotOfRole(slots: DecisionSlot[], role: string): DecisionSlot | undefined {\n return slots.find(slot => slot.role === role)\n}\n\n/**\n * What the declaration says about undoing this approval.\n *\n * The value is an object: `reversible` is required and `caveat` is the module's own sentence about\n * it. The asymmetry is the point — the boolean decides how heavily the screen draws the band, and\n * a sentence alone cannot decide that.\n *\n * { reversible: false, caveat: '버린 자재는 돌아오지 않습니다' }\n *\n * ── A condition is written as false ───────────────────────────────────────\n * 「되돌릴 수 있습니다 — 단, 실적이 마감되기 전까지」 is written `false` with the condition in the\n * caveat. Written `true` the screen draws it lightly and the reader walks past the condition. There\n * is no third value: a middle one always draws ambiguously, and the reader has to guess which side\n * it is on. (2026-09-11, architect's ruling)\n *\n * ── Not `note` ───────────────────────────────────────────────────────────\n * ops-contract already has CommandApproval.note, and that one is **the approver's own comment** —\n * a person's words, in the same neighbourhood as this screen. Same word for a module's sentence\n * would be read as the person's. `caveat` is used nowhere in the three repositories.\n */\nexport type Reversibility = { reversible: boolean; caveat?: string }\n\n/**\n * What the slot says, or undefined when it does not say.\n *\n * Undefined covers both \"the path led nowhere\" and \"nobody declared the role\". Drawing nothing is\n * right for both: telling an approver an action can be taken back when the payload never said so\n * is the one mistake this line cannot make, and a truthy read would make it — the string\n * '되돌릴 수 없습니다…' is truthy.\n */\nexport function reversibilityOf(slot?: DecisionSlot): Reversibility | undefined {\n const value: any = slot?.value\n\n if (!value || typeof value !== 'object' || typeof value.reversible !== 'boolean') {\n return undefined\n }\n\n return {\n reversible: value.reversible,\n ...(typeof value.caveat === 'string' && value.caveat.trim() ? { caveat: value.caveat } : {})\n }\n}\n\n/**\n * The value is there but not the shape this role takes.\n *\n * plant declared the role and put a sentence at that path, so the band drew nothing at all and the\n * heaviest line on the screen was silently missing. Reporting nothing to the reader is right — the\n * screen cannot tell which way it goes — but a developer should not have to find it by hand. The\n * caller logs this; the rule stays free of side effects so a test can hold it.\n */\nexport function reversibilityIsMalformed(slot?: DecisionSlot): boolean {\n return !!slot && slot.found && slot.value != null && reversibilityOf(slot) === undefined\n}\n"]}
|
|
@@ -124,8 +124,21 @@ export declare class ActivityApprovalPage extends ActivityApprovalPage_base {
|
|
|
124
124
|
* A scalar prints with its unit when one was declared. Anything shaped — an object, a list — goes
|
|
125
125
|
* through fieldValue, the same laying-out the model sections use, so a payload reads the same way
|
|
126
126
|
* wherever it appears and a four-level object does not become one long line.
|
|
127
|
+
*
|
|
128
|
+
* ── The sentence is built here, not frozen when the request was raised ──
|
|
129
|
+
* Eighteen doors write a Korean sentence at issue and the issue snapshots it, so it stands in the
|
|
130
|
+
* language of whoever raised it and an approver reading another language never catches up. Where
|
|
131
|
+
* a door declares `valueTKey`, the sentence is made at reading time instead, out of the values
|
|
132
|
+
* the request did freeze.
|
|
133
|
+
*
|
|
134
|
+
* **A door that has not moved yet draws exactly what it drew before.** That is what lets plant
|
|
135
|
+
* move them one at a time instead of eighteen at once, and it covers a door that moved into a
|
|
136
|
+
* locale nobody has written yet: an empty `defaultValue` means a missing key falls through to the
|
|
137
|
+
* frozen sentence rather than printing `approval.…` on the card.
|
|
127
138
|
*/
|
|
128
139
|
private slotText;
|
|
140
|
+
/** The sentence this slot declares, when it declares one and the locale has it. */
|
|
141
|
+
private sentenceOf;
|
|
129
142
|
/** Did a person actually write something here. Whitespace is not words. */
|
|
130
143
|
private hasWords;
|
|
131
144
|
generatedContent(): DirectiveResult;
|
|
@@ -16,7 +16,7 @@ import { PageView } from '@operato/shell';
|
|
|
16
16
|
import { CommonButtonStyles, ScrollbarStyles } from '@operato/styles';
|
|
17
17
|
import { provider } from '@things-factory/board-ui/dist-client/board-provider.js';
|
|
18
18
|
import { isReferenceStep } from '@things-factory/organization/dist-client';
|
|
19
|
-
import { ActivityFieldsStyles, fieldValue, inputSpecOf, outputSpecOf, renderGiven } from '../../components/activity-fields.js';
|
|
19
|
+
import { ActivityFieldsStyles, declaredLabel, fieldValue, inputSpecOf, outputSpecOf, renderGiven } from '../../components/activity-fields.js';
|
|
20
20
|
import { decisionSlots, reversibilityIsMalformed, reversibilityOf, slotOfRole, slotsOfRole } from '../../components/decision-slots.js';
|
|
21
21
|
const ActivityApprovalFetchResult = `\
|
|
22
22
|
{ id
|
|
@@ -672,8 +672,8 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
|
|
|
672
672
|
<div class="label">${i18next.t('label.what-it-was-judged-from')}</div>
|
|
673
673
|
${evidence.map(slot => html `
|
|
674
674
|
<div class="evidence">
|
|
675
|
-
${slot
|
|
676
|
-
<div>${
|
|
675
|
+
${declaredLabel(slot) ? html `<div class="label">${declaredLabel(slot)}</div>` : ''}
|
|
676
|
+
<div>${this.slotText(slot)}</div>
|
|
677
677
|
</div>
|
|
678
678
|
`)}
|
|
679
679
|
</div>
|
|
@@ -682,8 +682,8 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
|
|
|
682
682
|
${currentState?.found
|
|
683
683
|
? html `
|
|
684
684
|
<div class="current-state">
|
|
685
|
-
<div class="label">${
|
|
686
|
-
<div>${
|
|
685
|
+
<div class="label">${declaredLabel(currentState) || i18next.t('label.current-state')}</div>
|
|
686
|
+
<div>${this.slotText(currentState)}</div>
|
|
687
687
|
</div>
|
|
688
688
|
`
|
|
689
689
|
: ''}
|
|
@@ -693,8 +693,8 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
|
|
|
693
693
|
<div class="label">${i18next.t('label.what-changes')}</div>
|
|
694
694
|
${consequences.map(slot => html `
|
|
695
695
|
<div class="consequence">
|
|
696
|
-
${slot
|
|
697
|
-
<div>${
|
|
696
|
+
${declaredLabel(slot) ? html `<div class="label">${declaredLabel(slot)}</div>` : ''}
|
|
697
|
+
<div>${this.slotText(slot)}</div>
|
|
698
698
|
</div>
|
|
699
699
|
`)}
|
|
700
700
|
</div>
|
|
@@ -755,14 +755,36 @@ let ActivityApprovalPage = class ActivityApprovalPage extends localize(i18next)(
|
|
|
755
755
|
* A scalar prints with its unit when one was declared. Anything shaped — an object, a list — goes
|
|
756
756
|
* through fieldValue, the same laying-out the model sections use, so a payload reads the same way
|
|
757
757
|
* wherever it appears and a four-level object does not become one long line.
|
|
758
|
+
*
|
|
759
|
+
* ── The sentence is built here, not frozen when the request was raised ──
|
|
760
|
+
* Eighteen doors write a Korean sentence at issue and the issue snapshots it, so it stands in the
|
|
761
|
+
* language of whoever raised it and an approver reading another language never catches up. Where
|
|
762
|
+
* a door declares `valueTKey`, the sentence is made at reading time instead, out of the values
|
|
763
|
+
* the request did freeze.
|
|
764
|
+
*
|
|
765
|
+
* **A door that has not moved yet draws exactly what it drew before.** That is what lets plant
|
|
766
|
+
* move them one at a time instead of eighteen at once, and it covers a door that moved into a
|
|
767
|
+
* locale nobody has written yet: an empty `defaultValue` means a missing key falls through to the
|
|
768
|
+
* frozen sentence rather than printing `approval.…` on the card.
|
|
758
769
|
*/
|
|
759
770
|
slotText(slot) {
|
|
771
|
+
const said = this.sentenceOf(slot);
|
|
772
|
+
if (said) {
|
|
773
|
+
return said;
|
|
774
|
+
}
|
|
760
775
|
const scalar = slot.value !== null && slot.value !== undefined && typeof slot.value !== 'object';
|
|
761
776
|
if (scalar && slot.unit) {
|
|
762
777
|
return `${slot.value} ${slot.unit}`;
|
|
763
778
|
}
|
|
764
779
|
return scalar ? String(slot.value) : fieldValue(slot.value);
|
|
765
780
|
}
|
|
781
|
+
/** The sentence this slot declares, when it declares one and the locale has it. */
|
|
782
|
+
sentenceOf(slot) {
|
|
783
|
+
if (!slot.valueTKey) {
|
|
784
|
+
return '';
|
|
785
|
+
}
|
|
786
|
+
return String(i18next.t(slot.valueTKey, { ...(slot.params || {}), defaultValue: '' }) || '');
|
|
787
|
+
}
|
|
766
788
|
/** Did a person actually write something here. Whitespace is not words. */
|
|
767
789
|
hasWords(slot) {
|
|
768
790
|
return typeof slot.value === 'string' ? slot.value.trim().length > 0 : slot.found && slot.value != null;
|