@things-factory/worklist 10.1.147 → 10.1.150
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/decision-slots.d.ts +5 -18
- package/dist-client/components/decision-slots.js +6 -54
- package/dist-client/components/decision-slots.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.d.ts +3 -0
- package/dist-server/index.js +7 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/activity/freeze-decision-sentences-for.d.ts +11 -0
- package/dist-server/service/activity/freeze-decision-sentences-for.js +18 -0
- package/dist-server/service/activity/freeze-decision-sentences-for.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -11
- package/shared/decision-sentences.d.ts +30 -0
- package/shared/decision-sentences.js +146 -0
package/package.json
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/worklist",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.150",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"browser": "./dist-client/index.js",
|
|
9
|
+
"default": "./dist-server/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./issue": "./dist-server/controllers/activity-instance/issue.js",
|
|
12
|
+
"./dist-client": "./dist-client/index.js",
|
|
13
|
+
"./*": "./*"
|
|
14
|
+
},
|
|
15
|
+
"typesVersions": {
|
|
16
|
+
"*": {
|
|
17
|
+
"issue": [
|
|
18
|
+
"dist-server/controllers/activity-instance/issue.d.ts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
6
22
|
"things-factory": true,
|
|
7
23
|
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
8
24
|
"description": "Module to manage the todo list of business participants",
|
|
@@ -25,7 +41,7 @@
|
|
|
25
41
|
"clean:server": "npx rimraf dist-server",
|
|
26
42
|
"clean": "npm run clean:server && npm run clean:client",
|
|
27
43
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration",
|
|
28
|
-
"test": "node --import ./test/resolve-ts-siblings.mjs --test
|
|
44
|
+
"test": "node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts"
|
|
29
45
|
},
|
|
30
46
|
"dependencies": {
|
|
31
47
|
"@material/web": "^2.0.0",
|
|
@@ -38,14 +54,15 @@
|
|
|
38
54
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
39
55
|
"@operato/p13n": "^10.0.0",
|
|
40
56
|
"@operato/popup": "^10.15.0",
|
|
41
|
-
"@things-factory/attachment-base": "^10.1.
|
|
42
|
-
"@things-factory/auth-base": "^10.1.
|
|
43
|
-
"@things-factory/board-service": "^10.1.
|
|
44
|
-
"@things-factory/context-ui": "^10.1.
|
|
45
|
-
"@things-factory/
|
|
46
|
-
"@things-factory/
|
|
47
|
-
"@things-factory/
|
|
48
|
-
"@things-factory/
|
|
57
|
+
"@things-factory/attachment-base": "^10.1.150",
|
|
58
|
+
"@things-factory/auth-base": "^10.1.150",
|
|
59
|
+
"@things-factory/board-service": "^10.1.150",
|
|
60
|
+
"@things-factory/context-ui": "^10.1.150",
|
|
61
|
+
"@things-factory/i18n-base": "^10.1.150",
|
|
62
|
+
"@things-factory/organization": "^10.1.150",
|
|
63
|
+
"@things-factory/personalization": "^10.1.150",
|
|
64
|
+
"@things-factory/scheduler-client": "^10.1.150",
|
|
65
|
+
"@things-factory/shell": "^10.1.150"
|
|
49
66
|
},
|
|
50
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "39c135863f5f7455dcd80b8907550a1c91a86118"
|
|
51
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The declaration fields a sentence reads. The full declaration type lives with each side. */
|
|
2
|
+
export interface DecisionSentenceKey {
|
|
3
|
+
inputPath?: string
|
|
4
|
+
/** i18n key for the value sentence. */
|
|
5
|
+
valueTKey?: string
|
|
6
|
+
/** What the sentence puts in its blanks: name in the sentence → inputPath to read it from. */
|
|
7
|
+
params?: { [name: string]: string }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Translate = (key: string, options?: { [name: string]: unknown }) => string
|
|
11
|
+
|
|
12
|
+
export function valueAtPath(input: any, path: string): { value: unknown; found: boolean }
|
|
13
|
+
|
|
14
|
+
export function paramsFrom(
|
|
15
|
+
declared: { [name: string]: string } | undefined,
|
|
16
|
+
input: { [key: string]: any } | undefined | null
|
|
17
|
+
): { [name: string]: unknown } | undefined
|
|
18
|
+
|
|
19
|
+
export function freezeDecisionSentences<T>(
|
|
20
|
+
decisionKeys: DecisionSentenceKey[] | null | undefined,
|
|
21
|
+
input: T,
|
|
22
|
+
t: Translate,
|
|
23
|
+
language?: string
|
|
24
|
+
): T
|
|
25
|
+
|
|
26
|
+
export class DecisionSentenceMissing extends Error {
|
|
27
|
+
readonly code: 'decision-sentence-missing'
|
|
28
|
+
readonly params: { key: string; language: string }
|
|
29
|
+
constructor(key: string, language?: string)
|
|
30
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The value sentences of an approval — read by the decision screen and frozen by the server. No UI, i18n or DB
|
|
3
|
+
* imports: both sides pass in what they have.
|
|
4
|
+
*
|
|
5
|
+
* ── Why one file (2026-10-11) ────────────────────────────────────────────
|
|
6
|
+
* A door declares, for each sentence, its key (`valueTKey`) and where its blanks come from (`params`). The
|
|
7
|
+
* screen read the blanks out of the input here; the server kept its own copy of every sentence in Korean and
|
|
8
|
+
* froze that. When the translations were reworded (plant 8d073d11, 39 sentences) the server copies were not,
|
|
9
|
+
* and the stored sentence no longer said what the screen said. Now the server freezes what the translation
|
|
10
|
+
* says, through the same reading of the blanks the screen uses.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The value at a dotted path, and whether the path led anywhere.
|
|
15
|
+
*
|
|
16
|
+
* A path that runs into a non-object partway reports found: false rather than throwing. The screen has to
|
|
17
|
+
* draw something either way, and a thrown error would take the whole approval down over one slot.
|
|
18
|
+
*/
|
|
19
|
+
function valueAtPath(input, path) {
|
|
20
|
+
const steps = String(path == null ? '' : path)
|
|
21
|
+
.split('.')
|
|
22
|
+
.filter(step => step.length > 0)
|
|
23
|
+
|
|
24
|
+
if (!steps.length) {
|
|
25
|
+
return { value: undefined, found: false }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let at = input
|
|
29
|
+
|
|
30
|
+
for (const step of steps) {
|
|
31
|
+
if (at === null || at === undefined || typeof at !== 'object') {
|
|
32
|
+
return { value: undefined, found: false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(at, step)) {
|
|
36
|
+
return { value: undefined, found: false }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
at = at[step]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { value: at, found: true }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read a sentence's blanks out of the input.
|
|
47
|
+
*
|
|
48
|
+
* A path that leads nowhere lands as `undefined` — i18next prints an empty blank rather than the word
|
|
49
|
+
* `undefined`. Returns undefined when nothing was declared, so a slot without blanks carries no empty object.
|
|
50
|
+
*/
|
|
51
|
+
function paramsFrom(declared, input) {
|
|
52
|
+
const names = Object.keys(declared || {})
|
|
53
|
+
|
|
54
|
+
if (!names.length) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const read = {}
|
|
59
|
+
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
read[name] = valueAtPath(input, String(declared[name] == null ? '' : declared[name])).value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return read
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a translator answers for a key it does not have. Not a sentence anyone could write. */
|
|
68
|
+
const MISSING = '\u0000decision-sentence-missing'
|
|
69
|
+
|
|
70
|
+
/** Put in a blank that has no value, to see whether the chosen wording uses it. */
|
|
71
|
+
const NO_VALUE = '\u0000decision-sentence-no-value'
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A sentence key with no translation in the language being frozen. Thrown rather than stored: a stored key
|
|
75
|
+
* reads as gibberish on every screen that shows the record, and nobody can tell why later.
|
|
76
|
+
*/
|
|
77
|
+
class DecisionSentenceMissing extends Error {
|
|
78
|
+
constructor(key, language) {
|
|
79
|
+
super(`there is no ${language ? language + ' ' : ''}sentence for ${key}.`)
|
|
80
|
+
this.code = 'decision-sentence-missing'
|
|
81
|
+
this.params = { key, language: language || '' }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A copy of `input` with `value` at `path` — only the objects along the path are copied. */
|
|
86
|
+
function withValueAt(input, path, value) {
|
|
87
|
+
const steps = String(path).split('.').filter(step => step.length > 0)
|
|
88
|
+
const root = { ...(input || {}) }
|
|
89
|
+
let at = root
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
92
|
+
const next = at[steps[i]]
|
|
93
|
+
at[steps[i]] = next && typeof next === 'object' && !Array.isArray(next) ? { ...next } : {}
|
|
94
|
+
at = at[steps[i]]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
at[steps[steps.length - 1]] = value
|
|
98
|
+
return root
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The input with every declared value sentence written in, read from the translation through `t`.
|
|
103
|
+
*
|
|
104
|
+
* A sentence is written only when **the wording its branch picks** has a value for every blank it uses — a
|
|
105
|
+
* sentence is not made without its values (a maintenance order with no due date has no "the date it carries
|
|
106
|
+
* now"). A blank the declaration names but the chosen wording does not use may be empty: the no-lot branch of
|
|
107
|
+
* a substitution declares `{lot}` and does not say it. `context` picks the wording and is never a blank.
|
|
108
|
+
*
|
|
109
|
+
* `t` should not fall back to another language; a key it does not have throws `DecisionSentenceMissing`.
|
|
110
|
+
* `language` only names that language in the error.
|
|
111
|
+
*/
|
|
112
|
+
function freezeDecisionSentences(decisionKeys, input, t, language) {
|
|
113
|
+
let frozen = input
|
|
114
|
+
|
|
115
|
+
for (const key of decisionKeys || []) {
|
|
116
|
+
if (!key || !key.valueTKey || !key.inputPath) {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const params = paramsFrom(key.params, input) || {}
|
|
121
|
+
const filled = {}
|
|
122
|
+
|
|
123
|
+
for (const [name, value] of Object.entries(params)) {
|
|
124
|
+
filled[name] = value === undefined && name !== 'context' ? NO_VALUE : value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sentence = t(key.valueTKey, { ...filled, defaultValue: MISSING })
|
|
128
|
+
|
|
129
|
+
if (typeof sentence !== 'string' || sentence === MISSING) {
|
|
130
|
+
throw new DecisionSentenceMissing(key.valueTKey, language)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (sentence.includes(NO_VALUE)) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
frozen = withValueAt(frozen, key.inputPath, sentence)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return frozen
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
exports.valueAtPath = valueAtPath
|
|
144
|
+
exports.paramsFrom = paramsFrom
|
|
145
|
+
exports.freezeDecisionSentences = freezeDecisionSentences
|
|
146
|
+
exports.DecisionSentenceMissing = DecisionSentenceMissing
|