@things-factory/worklist 10.1.221 → 10.1.222

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 (26) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/controllers/activity-approval/approve.js +3 -0
  3. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  4. package/dist-server/controllers/activity-approval/create-approval.js +8 -2
  5. package/dist-server/controllers/activity-approval/create-approval.js.map +1 -1
  6. package/dist-server/controllers/activity-approval/delegate.js +4 -1
  7. package/dist-server/controllers/activity-approval/delegate.js.map +1 -1
  8. package/dist-server/controllers/activity-approval/reject.js +3 -0
  9. package/dist-server/controllers/activity-approval/reject.js.map +1 -1
  10. package/dist-server/service/activity-approval/index.d.ts +1 -1
  11. package/dist-server/service/approval-notice/approval-notice-rules.d.ts +67 -0
  12. package/dist-server/service/approval-notice/approval-notice-rules.js +67 -0
  13. package/dist-server/service/approval-notice/approval-notice-rules.js.map +1 -0
  14. package/dist-server/service/approval-notice/approval-notice.d.ts +11 -0
  15. package/dist-server/service/approval-notice/approval-notice.js +76 -0
  16. package/dist-server/service/approval-notice/approval-notice.js.map +1 -0
  17. package/dist-server/service/index.d.ts +3 -3
  18. package/dist-server/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +4 -3
  20. package/tests/approval-notice-db.test.ts +180 -0
  21. package/translations/en.json +15 -1
  22. package/translations/es.json +15 -1
  23. package/translations/ja.json +15 -1
  24. package/translations/ko.json +15 -1
  25. package/translations/ms.json +15 -1
  26. package/translations/zh.json +15 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/worklist",
3
- "version": "10.1.221",
3
+ "version": "10.1.222",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "exports": {
@@ -59,10 +59,11 @@
59
59
  "@things-factory/board-service": "^10.1.218",
60
60
  "@things-factory/context-ui": "^10.1.221",
61
61
  "@things-factory/i18n-base": "^10.1.218",
62
- "@things-factory/organization": "^10.1.218",
62
+ "@things-factory/notification": "^10.1.222",
63
+ "@things-factory/organization": "^10.1.222",
63
64
  "@things-factory/personalization": "^10.1.218",
64
65
  "@things-factory/scheduler-client": "^10.1.218",
65
66
  "@things-factory/shell": "^10.1.208"
66
67
  },
67
- "gitHead": "90e211d7a2c99216e017bd19a62e1703b20f2710"
68
+ "gitHead": "afdd6a9b1c8b440e39d4fec48bfbad1a0c02fccc"
68
69
  }
@@ -0,0 +1,180 @@
1
+ import { join } from 'path'
2
+
3
+ /**
4
+ * The approval notice through the real controllers on sqlite (ADR-0164).
5
+ *
6
+ * What the pure rules (test/approval-notice.test.ts) cannot show: the four controllers hand notification a notice
7
+ * for the right people once the transaction has committed - nothing before, and nothing when it rolls back. The
8
+ * channels themselves are notification's (packages/notification/tests/notice-delivery.test.ts); here the in-app
9
+ * `notify` of the request is a fake and the domain has no mail or Teams turned on.
10
+ *
11
+ * Reads dist-server - run the package's build:server first. The jest env has no translations, so the words are keys.
12
+ */
13
+
14
+ const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
15
+ const DIST = join(__dirname, '..', 'dist-server')
16
+ const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
17
+ const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
18
+ const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
19
+ const { approve } = require(join(DIST, 'controllers', 'activity-approval', 'approve.js'))
20
+ const { reject } = require(join(DIST, 'controllers', 'activity-approval', 'reject.js'))
21
+ const { delegate } = require(join(DIST, 'controllers', 'activity-approval', 'delegate.js'))
22
+ const { Activity } = require(join(DIST, 'service', 'activity', 'activity.js'))
23
+ const { Domain } = require('@things-factory/shell')
24
+ const { Role, User } = require('@things-factory/auth-base')
25
+ const { Employee } = require('@things-factory/organization')
26
+
27
+ let db: any
28
+ let here: any
29
+ let first: any
30
+ let second: any
31
+ let requester: any
32
+ let holderA: any
33
+ let holderB: any
34
+ let asleep: any
35
+ let reviewers: any
36
+ let leave: any
37
+ const seats: Record<string, any> = {}
38
+
39
+ beforeAll(async () => {
40
+ db = createSqliteTestDataSource([ActivityInstance, ActivityThread, ActivityApproval, Activity, Employee])
41
+ await db.initialize()
42
+
43
+ here = await db.getRepository(Domain).save({ name: 'here', subdomain: 'here' })
44
+
45
+ const person = (name: string, status = 'activated') =>
46
+ db.getRepository(User).save({ name, email: `${name}@example.com`, password: 'x', status, userType: 'user', locale: 'ko-KR', domains: [here] })
47
+
48
+ first = await person('first')
49
+ second = await person('second')
50
+ requester = await person('requester')
51
+ holderA = await person('holderA')
52
+ holderB = await person('holderB')
53
+ asleep = await person('asleep', 'inactive')
54
+ reviewers = await db.getRepository(Role).save({ name: 'reviewers', domain: here, users: [holderA, holderB, asleep] })
55
+ leave = await db.getRepository(Activity).save({ domain: here, name: 'Leave' })
56
+
57
+ /* the approval line names people as employees of this domain, and a role by its id */
58
+ const employee = async (user: any, controlNo: string) =>
59
+ ({ type: 'Employee', value: (await db.getRepository(Employee).save({ domain: here, controlNo, name: user.name, user, active: true })).id })
60
+ seats.first = await employee(first, 'E1')
61
+ seats.second = await employee(second, 'E2')
62
+ seats.reviewers = { type: 'Role', value: reviewers.id }
63
+ })
64
+
65
+ afterAll(async () => db?.destroy())
66
+
67
+ /** A submitted case whose line is the named seats, with `first`'s seat pending. */
68
+ async function aCase(line = ['first', 'second']) {
69
+ const instance = await db.getRepository(ActivityInstance).save({
70
+ domain: here,
71
+ name: '휴가 신청 — requester',
72
+ state: 'started',
73
+ activity: leave,
74
+ issuer: requester,
75
+ approvalLine: line.map(name => seats[name])
76
+ })
77
+ const thread = await db.getRepository(ActivityThread).save({
78
+ domain: here,
79
+ name: 't',
80
+ round: 1,
81
+ state: 'submitted',
82
+ activityInstance: instance,
83
+ assignee: requester
84
+ })
85
+ const seat = await db.getRepository(ActivityApproval).save({
86
+ domain: here,
87
+ activityThread: thread,
88
+ approver: first,
89
+ order: 1,
90
+ round: 1,
91
+ judgment: '',
92
+ participation: 'approval'
93
+ })
94
+ return { instance, thread, seat }
95
+ }
96
+
97
+ /** Runs `work` as `user` in a transaction with an after-commit list, then the list - as the @transaction directive does. */
98
+ async function asRequest(user: any, work: (context: any) => Promise<any>, { rollback = false } = {}) {
99
+ const told: any[] = []
100
+ const afterCommit: { name: string; run: () => unknown }[] = []
101
+ let toldBeforeCommit = -1
102
+
103
+ try {
104
+ await db.transaction(async (tx: any) => {
105
+ const context = { state: { domain: here, user, tx, afterCommit, notify: (payload: any) => void told.push(payload) }, t: (key: string) => key }
106
+ await work(context)
107
+ toldBeforeCommit = told.length
108
+ if (rollback) throw new Error('rolled back')
109
+ })
110
+ } catch (error) {
111
+ if (!rollback) throw error
112
+ return { told, toldBeforeCommit, afterCommit }
113
+ }
114
+
115
+ for (const step of afterCommit) await step.run()
116
+ /* sending does not hold the request - let it finish */
117
+ for (let i = 0; i < 20 && told.length === 0; i++) await new Promise(resolve => setTimeout(resolve, 5))
118
+ await new Promise(resolve => setTimeout(resolve, 20))
119
+
120
+ return { told, toldBeforeCommit, afterCommit }
121
+ }
122
+
123
+ const foreground = (told: any[]) => told.filter(one => one.mode === 'foreground')
124
+
125
+ it('an approval with someone after it tells the next approver their turn, after the commit', async () => {
126
+ const { seat } = await aCase()
127
+
128
+ const { told, toldBeforeCommit } = await asRequest(first, context => approve({ id: seat.id, comment: 'ok' }, context))
129
+
130
+ expect(toldBeforeCommit).toBe(0)
131
+ expect(foreground(told)).toHaveLength(1)
132
+ expect(foreground(told)[0]).toMatchObject({ receivers: [second.id], type: 'approval', subject: 'info', title: 'approval-notice.request.subject' })
133
+ expect(foreground(told)[0].url).toMatch(/^activity-approval\/[0-9a-f-]+$/)
134
+ expect(told.filter(one => one.mode === 'background')).toHaveLength(1)
135
+ })
136
+
137
+ it('the last approval tells the requester it is approved', async () => {
138
+ const { seat, thread } = await aCase(['first'])
139
+
140
+ const { told } = await asRequest(first, context => approve({ id: seat.id, comment: 'ok' }, context))
141
+
142
+ expect(foreground(told)).toEqual([
143
+ expect.objectContaining({ receivers: [requester.id], title: 'approval-notice.approved.subject', url: `activity-thread-view/${thread.id}` })
144
+ ])
145
+ })
146
+
147
+ it('a rejection tells the requester, as a warning, and opens the thread to revise', async () => {
148
+ const { seat, thread } = await aCase()
149
+
150
+ const { told } = await asRequest(first, context => reject({ id: seat.id, comment: 'no' }, context))
151
+
152
+ expect(foreground(told)).toEqual([
153
+ expect.objectContaining({ receivers: [requester.id], subject: 'warn', title: 'approval-notice.rejected.subject', url: `activity-thread/${thread.id}` })
154
+ ])
155
+ })
156
+
157
+ it('a handed seat tells the person it was handed to', async () => {
158
+ const { seat } = await aCase()
159
+
160
+ const { told } = await asRequest(first, context => delegate({ id: seat.id, to: { id: second.id }, comment: 'away' }, context))
161
+
162
+ expect(foreground(told)).toEqual([expect.objectContaining({ receivers: [second.id], title: 'approval-notice.delegated.subject' })])
163
+ })
164
+
165
+ it('a seat held by a role tells every active holder, each alone', async () => {
166
+ const { seat } = await aCase(['first', 'reviewers'])
167
+
168
+ const { told } = await asRequest(first, context => approve({ id: seat.id, comment: 'ok' }, context))
169
+
170
+ expect(foreground(told).map(one => one.receivers).sort()).toEqual([[holderA.id], [holderB.id]].sort())
171
+ })
172
+
173
+ it('a rolled-back judgment tells nobody', async () => {
174
+ const { seat } = await aCase()
175
+
176
+ const { told, afterCommit } = await asRequest(first, context => reject({ id: seat.id, comment: 'no' }, context), { rollback: true })
177
+
178
+ expect(afterCommit.length).toBeGreaterThan(0)
179
+ expect(told).toEqual([])
180
+ })
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "Putting up approvals",
381
381
  "task.worklist.submit-approvals.description": "Reads the kinds of work and puts a case up for approval. Opens only the cases one put up or stands on the line of.",
382
382
  "task.worklist.all-cases-reading.name": "Reading every approval case",
383
- "task.worklist.all-cases-reading.description": "Reads every case and its input, whoever put it up."
383
+ "task.worklist.all-cases-reading.description": "Reads every case and its input, whoever put it up.",
384
+ "approval-notice.request.subject": "[Approval request] {name}",
385
+ "approval-notice.request.body": "It is your turn to approve \"{name}\", raised by {requester}. Step {step} of {steps} on the approval line.",
386
+ "approval-notice.delegated.subject": "[Approval handed to you] {name}",
387
+ "approval-notice.delegated.body": "{actor} handed you the approval of \"{name}\", raised by {requester}. Step {step} of {steps} on the approval line.",
388
+ "approval-notice.reference.subject": "[For reference] {name}",
389
+ "approval-notice.reference.body": "You were added as a reference to \"{name}\", raised by {requester}.",
390
+ "approval-notice.approved.subject": "[Approved] {name}",
391
+ "approval-notice.approved.body": "The approval of \"{name}\" is complete. {actor} gave the last approval.",
392
+ "approval-notice.rejected.subject": "[Rejected] {name}",
393
+ "approval-notice.rejected.body": "{actor} rejected \"{name}\". You can revise and raise it again, or abort it.",
394
+ "approval-notice.open-approval": "Open the approval",
395
+ "approval-notice.open-request": "Open the request",
396
+ "approval-notice.see-approval-box": "You can find it in your approval box.",
397
+ "approval-notice.see-request-box": "You can find it in your to-do list."
384
398
  }
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "Enviar aprobaciones",
381
381
  "task.worklist.submit-approvals.description": "Lee los tipos de trabajo y envía casos a aprobación. Solo abre los casos que uno envió o en cuya línea figura.",
382
382
  "task.worklist.all-cases-reading.name": "Leer todos los casos de aprobación",
383
- "task.worklist.all-cases-reading.description": "Lee todos los casos y sus datos, sin importar quién los envió."
383
+ "task.worklist.all-cases-reading.description": "Lee todos los casos y sus datos, sin importar quién los envió.",
384
+ "approval-notice.request.subject": "[Solicitud de aprobación] {name}",
385
+ "approval-notice.request.body": "Le toca aprobar \"{name}\", enviado por {requester}. Paso {step} de {steps} de la línea de aprobación.",
386
+ "approval-notice.delegated.subject": "[Aprobación delegada] {name}",
387
+ "approval-notice.delegated.body": "{actor} le delegó la aprobación de \"{name}\", enviado por {requester}. Paso {step} de {steps} de la línea de aprobación.",
388
+ "approval-notice.reference.subject": "[Para referencia] {name}",
389
+ "approval-notice.reference.body": "Se le añadió como referencia en \"{name}\", enviado por {requester}.",
390
+ "approval-notice.approved.subject": "[Aprobado] {name}",
391
+ "approval-notice.approved.body": "La aprobación de \"{name}\" ha terminado. {actor} dio la última aprobación.",
392
+ "approval-notice.rejected.subject": "[Rechazado] {name}",
393
+ "approval-notice.rejected.body": "{actor} rechazó \"{name}\". Puede corregirlo y enviarlo de nuevo, o cancelarlo.",
394
+ "approval-notice.open-approval": "Abrir la aprobación",
395
+ "approval-notice.open-request": "Abrir la solicitud",
396
+ "approval-notice.see-approval-box": "Puede verlo en su bandeja de aprobaciones.",
397
+ "approval-notice.see-request-box": "Puede verlo en su lista de tareas."
384
398
  }
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "決裁の上申",
381
381
  "task.worklist.submit-approvals.description": "業務の種類を見て決裁を上申する。自分が上申したか決裁線に入っている決裁だけを開く。",
382
382
  "task.worklist.all-cases-reading.name": "全決裁の閲覧",
383
- "task.worklist.all-cases-reading.description": "誰が上申したかに関係なく、すべての決裁とその入力値を読む。"
383
+ "task.worklist.all-cases-reading.description": "誰が上申したかに関係なく、すべての決裁とその入力値を読む。",
384
+ "approval-notice.request.subject": "[承認依頼] {name}",
385
+ "approval-notice.request.body": "{requester}さんが申請した「{name}」の承認順になりました。承認ライン {steps} 段階中 {step} 段階目です。",
386
+ "approval-notice.delegated.subject": "[承認の委任] {name}",
387
+ "approval-notice.delegated.body": "{actor}さんが「{name}」の承認を委任しました。申請者は{requester}さん、承認ライン {steps} 段階中 {step} 段階目です。",
388
+ "approval-notice.reference.subject": "[参照] {name}",
389
+ "approval-notice.reference.body": "{requester}さんが申請した「{name}」の参照者に指定されました。",
390
+ "approval-notice.approved.subject": "[承認完了] {name}",
391
+ "approval-notice.approved.body": "「{name}」の承認が完了しました。最終承認は{actor}さんです。",
392
+ "approval-notice.rejected.subject": "[差し戻し] {name}",
393
+ "approval-notice.rejected.body": "{actor}さんが「{name}」を差し戻しました。修正して再申請するか、中止できます。",
394
+ "approval-notice.open-approval": "承認画面を開く",
395
+ "approval-notice.open-request": "申請を開く",
396
+ "approval-notice.see-approval-box": "承認ボックスで確認できます。",
397
+ "approval-notice.see-request-box": "やることリストで確認できます。"
384
398
  }
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "결재 올리기",
381
381
  "task.worklist.submit-approvals.description": "업무 종류를 보고 결재를 올린다. 자기가 올렸거나 결재선에 든 결재만 연다.",
382
382
  "task.worklist.all-cases-reading.name": "모든 결재 열람",
383
- "task.worklist.all-cases-reading.description": "누가 올렸든 모든 결재 사례와 그 입력값을 읽는다."
383
+ "task.worklist.all-cases-reading.description": "누가 올렸든 모든 결재 사례와 그 입력값을 읽는다.",
384
+ "approval-notice.request.subject": "[결재 요청] {name}",
385
+ "approval-notice.request.body": "{requester}님이 올린 「{name}」을(를) 결재할 차례입니다. 결재선 {steps}단계 중 {step}단계입니다.",
386
+ "approval-notice.delegated.subject": "[결재 위임] {name}",
387
+ "approval-notice.delegated.body": "{actor}님이 「{name}」 결재를 넘겼습니다. 상신자는 {requester}님이고, 결재선 {steps}단계 중 {step}단계입니다.",
388
+ "approval-notice.reference.subject": "[참조] {name}",
389
+ "approval-notice.reference.body": "{requester}님이 올린 「{name}」에 참조로 지정되었습니다.",
390
+ "approval-notice.approved.subject": "[승인 완료] {name}",
391
+ "approval-notice.approved.body": "「{name}」의 결재가 끝났습니다. 마지막 승인은 {actor}님입니다.",
392
+ "approval-notice.rejected.subject": "[반려] {name}",
393
+ "approval-notice.rejected.body": "{actor}님이 「{name}」을(를) 반려했습니다. 고쳐서 다시 올리거나 중단할 수 있습니다.",
394
+ "approval-notice.open-approval": "결재 화면 열기",
395
+ "approval-notice.open-request": "요청 열기",
396
+ "approval-notice.see-approval-box": "내 결재함에서 확인할 수 있습니다.",
397
+ "approval-notice.see-request-box": "할 일에서 확인할 수 있습니다."
384
398
  }
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "Menghantar kelulusan",
381
381
  "task.worklist.submit-approvals.description": "Membaca jenis kerja dan menghantar kes untuk kelulusan. Hanya membuka kes yang dihantar sendiri atau yang ada dalam laluannya.",
382
382
  "task.worklist.all-cases-reading.name": "Membaca semua kes kelulusan",
383
- "task.worklist.all-cases-reading.description": "Membaca semua kes dan inputnya, tidak kira siapa yang menghantarnya."
383
+ "task.worklist.all-cases-reading.description": "Membaca semua kes dan inputnya, tidak kira siapa yang menghantarnya.",
384
+ "approval-notice.request.subject": "[Permintaan kelulusan] {name}",
385
+ "approval-notice.request.body": "Giliran anda untuk meluluskan \"{name}\" yang dihantar oleh {requester}. Langkah {step} daripada {steps} dalam laluan kelulusan.",
386
+ "approval-notice.delegated.subject": "[Kelulusan diserahkan] {name}",
387
+ "approval-notice.delegated.body": "{actor} menyerahkan kelulusan \"{name}\" kepada anda. Pemohon {requester}, langkah {step} daripada {steps} dalam laluan kelulusan.",
388
+ "approval-notice.reference.subject": "[Untuk rujukan] {name}",
389
+ "approval-notice.reference.body": "Anda ditambah sebagai rujukan untuk \"{name}\" yang dihantar oleh {requester}.",
390
+ "approval-notice.approved.subject": "[Diluluskan] {name}",
391
+ "approval-notice.approved.body": "Kelulusan \"{name}\" telah selesai. Kelulusan terakhir oleh {actor}.",
392
+ "approval-notice.rejected.subject": "[Ditolak] {name}",
393
+ "approval-notice.rejected.body": "{actor} menolak \"{name}\". Anda boleh membetulkan dan menghantarnya semula, atau membatalkannya.",
394
+ "approval-notice.open-approval": "Buka kelulusan",
395
+ "approval-notice.open-request": "Buka permintaan",
396
+ "approval-notice.see-approval-box": "Anda boleh melihatnya dalam kotak kelulusan anda.",
397
+ "approval-notice.see-request-box": "Anda boleh melihatnya dalam senarai tugasan anda."
384
398
  }
@@ -380,5 +380,19 @@
380
380
  "task.worklist.submit-approvals.name": "提交审批",
381
381
  "task.worklist.submit-approvals.description": "查看工作类型并提交审批。只打开自己提交或在审批线上的案件。",
382
382
  "task.worklist.all-cases-reading.name": "查看全部审批",
383
- "task.worklist.all-cases-reading.description": "无论由谁提交,读取所有审批案件及其输入。"
383
+ "task.worklist.all-cases-reading.description": "无论由谁提交,读取所有审批案件及其输入。",
384
+ "approval-notice.request.subject": "[审批请求] {name}",
385
+ "approval-notice.request.body": "轮到您审批 {requester} 提交的“{name}”。审批流程共 {steps} 步,这是第 {step} 步。",
386
+ "approval-notice.delegated.subject": "[审批转交] {name}",
387
+ "approval-notice.delegated.body": "{actor} 将“{name}”的审批转交给您。提交人 {requester},审批流程共 {steps} 步,这是第 {step} 步。",
388
+ "approval-notice.reference.subject": "[抄送] {name}",
389
+ "approval-notice.reference.body": "您被设为 {requester} 提交的“{name}”的抄送人。",
390
+ "approval-notice.approved.subject": "[已批准] {name}",
391
+ "approval-notice.approved.body": "“{name}”的审批已完成。最后批准人是 {actor}。",
392
+ "approval-notice.rejected.subject": "[已驳回] {name}",
393
+ "approval-notice.rejected.body": "{actor} 驳回了“{name}”。您可以修改后重新提交,或终止。",
394
+ "approval-notice.open-approval": "打开审批页面",
395
+ "approval-notice.open-request": "打开申请",
396
+ "approval-notice.see-approval-box": "可在我的审批箱中查看。",
397
+ "approval-notice.see-request-box": "可在待办事项中查看。"
384
398
  }