@things-factory/worklist 9.1.19 → 10.0.0-beta.2

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 (103) hide show
  1. package/dist-client/components/activity-thread-timeline.d.ts +1 -9
  2. package/dist-client/components/activity-thread-timeline.js +1 -3
  3. package/dist-client/components/activity-thread-timeline.js.map +1 -1
  4. package/dist-client/pages/activity/activity-list-page.d.ts +1 -7
  5. package/dist-client/pages/activity/activity-list-page.js +125 -234
  6. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  7. package/dist-client/pages/activity/activity-page.d.ts +1 -7
  8. package/dist-client/pages/activity/activity-page.js +51 -93
  9. package/dist-client/pages/activity/activity-page.js.map +1 -1
  10. package/dist-client/pages/activity/starter-list-page.d.ts +1 -7
  11. package/dist-client/pages/activity/starter-list-page.js +33 -62
  12. package/dist-client/pages/activity/starter-list-page.js.map +1 -1
  13. package/dist-client/pages/activity-approval/activity-approval-list-page.d.ts +1 -7
  14. package/dist-client/pages/activity-approval/activity-approval-list-page.js +50 -95
  15. package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
  16. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +1 -7
  17. package/dist-client/pages/activity-approval/activity-approval-page.js +73 -119
  18. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  19. package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -6
  20. package/dist-client/pages/activity-instance/activity-instance-list-page.js +63 -120
  21. package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
  22. package/dist-client/pages/activity-instance/activity-instance-search-page.d.ts +1 -7
  23. package/dist-client/pages/activity-instance/activity-instance-search-page.js +55 -101
  24. package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
  25. package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -7
  26. package/dist-client/pages/activity-instance/activity-instance-start-page.js +65 -109
  27. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  28. package/dist-client/pages/activity-stats/activity-stats-list-page.d.ts +1 -7
  29. package/dist-client/pages/activity-stats/activity-stats-list-page.js +50 -95
  30. package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
  31. package/dist-client/pages/activity-store/activity-store-page.d.ts +1 -7
  32. package/dist-client/pages/activity-store/activity-store-page.js +2 -3
  33. package/dist-client/pages/activity-store/activity-store-page.js.map +1 -1
  34. package/dist-client/pages/activity-supervisor/reporter-list-page.d.ts +1 -7
  35. package/dist-client/pages/activity-supervisor/reporter-list-page.js +36 -66
  36. package/dist-client/pages/activity-supervisor/reporter-list-page.js.map +1 -1
  37. package/dist-client/pages/activity-template/activity-template-list-page.d.ts +1 -7
  38. package/dist-client/pages/activity-template/activity-template-list-page.js +70 -134
  39. package/dist-client/pages/activity-template/activity-template-list-page.js.map +1 -1
  40. package/dist-client/pages/activity-thread/activity-thread-list-page.d.ts +1 -7
  41. package/dist-client/pages/activity-thread/activity-thread-list-page.js +49 -93
  42. package/dist-client/pages/activity-thread/activity-thread-list-page.js.map +1 -1
  43. package/dist-client/pages/activity-thread/activity-thread-page.d.ts +1 -7
  44. package/dist-client/pages/activity-thread/activity-thread-page.js +80 -135
  45. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  46. package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +1 -7
  47. package/dist-client/pages/activity-thread/activity-thread-view-page.js +54 -80
  48. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  49. package/dist-client/pages/activity-thread/activity-thread-view.js +4 -0
  50. package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
  51. package/dist-client/pages/dashboard/dashboard-home.js +3 -5
  52. package/dist-client/pages/dashboard/dashboard-home.js.map +1 -1
  53. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +0 -6
  54. package/dist-client/pages/installable-activity/installable-activity-list-page.js +68 -130
  55. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  56. package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -7
  57. package/dist-client/pages/todo/approval-done-list-page.js +53 -100
  58. package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
  59. package/dist-client/pages/todo/approval-pending-list-page.d.ts +0 -6
  60. package/dist-client/pages/todo/approval-pending-list-page.js +63 -119
  61. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  62. package/dist-client/pages/todo/done-list-calendar-page.d.ts +1 -7
  63. package/dist-client/pages/todo/done-list-calendar-page.js +2 -3
  64. package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
  65. package/dist-client/pages/todo/done-list-page.d.ts +1 -7
  66. package/dist-client/pages/todo/done-list-page.js +56 -106
  67. package/dist-client/pages/todo/done-list-page.js.map +1 -1
  68. package/dist-client/pages/todo/draft-list-page.d.ts +1 -7
  69. package/dist-client/pages/todo/draft-list-page.js +49 -88
  70. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  71. package/dist-client/pages/todo/pickable-list-page.d.ts +1 -7
  72. package/dist-client/pages/todo/pickable-list-page.js +48 -91
  73. package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
  74. package/dist-client/pages/todo/todo-list-page.d.ts +0 -6
  75. package/dist-client/pages/todo/todo-list-page.js +56 -106
  76. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  77. package/dist-client/pages/worklist-home.js +2 -3
  78. package/dist-client/pages/worklist-home.js.map +1 -1
  79. package/dist-client/route.d.ts +1 -1
  80. package/dist-client/templates/activity-approval-context-template.js +8 -12
  81. package/dist-client/templates/activity-approval-context-template.js.map +1 -1
  82. package/dist-client/templates/activity-instance-context-template.js +8 -12
  83. package/dist-client/templates/activity-instance-context-template.js.map +1 -1
  84. package/dist-client/templates/activity-thread-context-template.js +8 -12
  85. package/dist-client/templates/activity-thread-context-template.js.map +1 -1
  86. package/dist-client/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/controllers/activity-approval/approve.js +2 -2
  88. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  89. package/dist-server/controllers/activity-thread/submit.js +2 -2
  90. package/dist-server/controllers/activity-thread/submit.js.map +1 -1
  91. package/dist-server/service/index.d.ts +2 -2
  92. package/dist-server/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +16 -16
  94. package/spec/integration/approval-mixed-types.spec.ts +491 -0
  95. package/spec/integration/approval-role-based.spec.ts +389 -0
  96. package/spec/integration/instance-lifecycle.spec.ts +406 -0
  97. package/spec/integration/role-approval-edge-cases.spec.ts +581 -0
  98. package/spec/unit/controllers/activity-instance-issue.spec.ts +360 -0
  99. package/spec/unit/controllers/activity-thread-submit.spec.ts +384 -0
  100. package/spec/unit/role-approval-escalate-logic.spec.ts +499 -0
  101. package/spec/unit/role-approval-submit-logic.spec.ts +481 -0
  102. package/spec/unit/thread-state-helpers.spec.ts +253 -0
  103. package/translations/en.json +1 -1
@@ -0,0 +1,384 @@
1
+ /**
2
+ * ActivityThread Submit Controller Tests
3
+ * ActivityThread 제출 컨트롤러 테스트
4
+ */
5
+
6
+ import { TestDatabase } from '../../../../../test/test-database'
7
+ import { withTestTransaction } from '../../../../../test/test-context'
8
+ import {
9
+ domainFactory,
10
+ userFactory,
11
+ roleFactory,
12
+ activityInstanceFactory,
13
+ activityThreadFactory,
14
+ activityApprovalFactory
15
+ } from '../../../../../test/factories'
16
+ import {
17
+ ActivityInstanceStatus,
18
+ ActivityThreadStatus,
19
+ ActivityApprovalJudgment
20
+ } from '../../../../../test/entities/schemas'
21
+
22
+ describe('ActivityThread Submit Controller', () => {
23
+ let testDb: TestDatabase
24
+
25
+ beforeAll(async () => {
26
+ testDb = TestDatabase.getInstance()
27
+ })
28
+
29
+ describe('Submit 기본 동작', () => {
30
+ it('Started 상태의 Thread를 Submit할 수 있어야 한다', async () => {
31
+ await withTestTransaction(async (context) => {
32
+ const { tx, domain, user } = context.state
33
+
34
+ // Given: Started 상태의 Thread
35
+ const instance = await activityInstanceFactory.createWithActivity(
36
+ { state: ActivityInstanceStatus.Started },
37
+ undefined,
38
+ domain,
39
+ tx
40
+ )
41
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
42
+ {
43
+ state: ActivityThreadStatus.Started,
44
+ startedAt: new Date()
45
+ },
46
+ instance,
47
+ user,
48
+ tx
49
+ )
50
+
51
+ // When: Submit 시뮬레이션
52
+ const output = { action: 'Temperature adjusted to normal range' }
53
+ thread.output = output
54
+ thread.state = ActivityThreadStatus.Ended
55
+ thread.terminatedAt = new Date()
56
+ const submitted = await tx.save('ActivityThread', thread)
57
+
58
+ // Then
59
+ expect(submitted.state).toBe(ActivityThreadStatus.Ended)
60
+ expect(submitted.output).toEqual(output)
61
+ expect(submitted.terminatedAt).toBeDefined()
62
+ })
63
+ })
64
+
65
+ it('output이 Thread에 저장되어야 한다', async () => {
66
+ await withTestTransaction(async (context) => {
67
+ const { tx, domain, user } = context.state
68
+
69
+ // Given
70
+ const instance = await activityInstanceFactory.createWithActivity(
71
+ { state: ActivityInstanceStatus.Started },
72
+ undefined,
73
+ domain,
74
+ tx
75
+ )
76
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
77
+ { state: ActivityThreadStatus.Started },
78
+ instance,
79
+ user,
80
+ tx
81
+ )
82
+
83
+ // When
84
+ const output = {
85
+ instruction: 'Adjust temperature',
86
+ result: 'success',
87
+ notes: 'Completed without issues'
88
+ }
89
+ thread.output = output
90
+ const updated = await tx.save('ActivityThread', thread)
91
+
92
+ // Then
93
+ expect(updated.output).toEqual(output)
94
+ expect(updated.output?.result).toBe('success')
95
+ })
96
+ })
97
+ })
98
+
99
+ describe('결재라인 없는 경우', () => {
100
+ it('결재라인이 없으면 바로 Ended 상태가 되어야 한다', async () => {
101
+ await withTestTransaction(async (context) => {
102
+ const { tx, domain, user } = context.state
103
+
104
+ // Given: 결재라인 없는 Instance
105
+ const instance = await activityInstanceFactory.createWithActivity(
106
+ {
107
+ state: ActivityInstanceStatus.Started,
108
+ approvalLine: [] // 빈 결재라인
109
+ },
110
+ undefined,
111
+ domain,
112
+ tx
113
+ )
114
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
115
+ { state: ActivityThreadStatus.Started },
116
+ instance,
117
+ user,
118
+ tx
119
+ )
120
+
121
+ // When: Submit
122
+ thread.state = ActivityThreadStatus.Ended
123
+ thread.terminatedAt = new Date()
124
+ const submitted = await tx.save('ActivityThread', thread)
125
+
126
+ // Then: 바로 Ended
127
+ expect(submitted.state).toBe(ActivityThreadStatus.Ended)
128
+ })
129
+ })
130
+ })
131
+
132
+ describe('결재라인 있는 경우', () => {
133
+ it('결재라인이 있으면 Submitted 상태가 되어야 한다', async () => {
134
+ await withTestTransaction(async (context) => {
135
+ const { tx, domain, user } = context.state
136
+
137
+ // Given: 결재라인 있는 Instance
138
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
139
+ const instance = await activityInstanceFactory.createWithActivity(
140
+ {
141
+ state: ActivityInstanceStatus.Started,
142
+ approvalLine: [
143
+ { type: 'Role', value: approverRole.id, approver: { id: approverRole.id, name: approverRole.name } }
144
+ ]
145
+ },
146
+ undefined,
147
+ domain,
148
+ tx
149
+ )
150
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
151
+ { state: ActivityThreadStatus.Started },
152
+ instance,
153
+ user,
154
+ tx
155
+ )
156
+
157
+ // When: Submit (결재 필요)
158
+ thread.state = ActivityThreadStatus.Submitted
159
+ const submitted = await tx.save('ActivityThread', thread)
160
+
161
+ // Then: Submitted 상태로 대기
162
+ expect(submitted.state).toBe(ActivityThreadStatus.Submitted)
163
+ })
164
+ })
165
+
166
+ it('Submit 시 ActivityApproval이 생성되어야 한다', async () => {
167
+ await withTestTransaction(async (context) => {
168
+ const { tx, domain, user } = context.state
169
+
170
+ // Given
171
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
172
+ const instance = await activityInstanceFactory.createWithActivity(
173
+ {
174
+ state: ActivityInstanceStatus.Started,
175
+ approvalLine: [
176
+ { type: 'Role', value: approverRole.id, approver: { id: approverRole.id, name: approverRole.name } }
177
+ ]
178
+ },
179
+ undefined,
180
+ domain,
181
+ tx
182
+ )
183
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
184
+ { state: ActivityThreadStatus.Started },
185
+ instance,
186
+ user,
187
+ tx
188
+ )
189
+
190
+ // When: Submit 후 Approval 생성 시뮬레이션
191
+ thread.state = ActivityThreadStatus.Submitted
192
+ await tx.save('ActivityThread', thread)
193
+
194
+ const approval = await activityApprovalFactory.createWithRole(
195
+ {
196
+ round: 1,
197
+ order: 1,
198
+ judgment: ActivityApprovalJudgment.Pending
199
+ },
200
+ thread,
201
+ approverRole,
202
+ tx
203
+ )
204
+
205
+ // Then
206
+ expect(approval).toBeDefined()
207
+ expect(approval.judgment).toBe(ActivityApprovalJudgment.Pending)
208
+ expect(approval.approverRole?.id).toBe(approverRole.id)
209
+ })
210
+ })
211
+ })
212
+
213
+ describe('다중 레벨 결재', () => {
214
+ it('첫 번째 결재자에게만 Approval이 생성되어야 한다', async () => {
215
+ await withTestTransaction(async (context) => {
216
+ const { tx, domain, user } = context.state
217
+
218
+ // Given: 2단계 결재라인
219
+ const firstApproverRole = await roleFactory.create({ name: 'Team Lead', domain }, tx)
220
+ const secondApproverRole = await roleFactory.create({ name: 'Manager', domain }, tx)
221
+
222
+ const instance = await activityInstanceFactory.createWithActivity(
223
+ {
224
+ state: ActivityInstanceStatus.Started,
225
+ approvalLine: [
226
+ { type: 'Role', value: firstApproverRole.id, approver: { id: firstApproverRole.id, name: firstApproverRole.name } },
227
+ { type: 'Role', value: secondApproverRole.id, approver: { id: secondApproverRole.id, name: secondApproverRole.name } }
228
+ ]
229
+ },
230
+ undefined,
231
+ domain,
232
+ tx
233
+ )
234
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
235
+ { state: ActivityThreadStatus.Started },
236
+ instance,
237
+ user,
238
+ tx
239
+ )
240
+
241
+ // When: Submit 시 첫 번째 결재자만 생성
242
+ thread.state = ActivityThreadStatus.Submitted
243
+ await tx.save('ActivityThread', thread)
244
+
245
+ const firstApproval = await activityApprovalFactory.createWithRole(
246
+ {
247
+ round: 1,
248
+ order: 1,
249
+ judgment: ActivityApprovalJudgment.Pending
250
+ },
251
+ thread,
252
+ firstApproverRole,
253
+ tx
254
+ )
255
+
256
+ // Then
257
+ expect(firstApproval.order).toBe(1)
258
+
259
+ // 조회해서 1개만 있는지 확인
260
+ const approvals = await tx.getRepository('ActivityApproval').find({
261
+ where: { activityThread: { id: thread.id } }
262
+ })
263
+ expect(approvals.length).toBe(1)
264
+ })
265
+ })
266
+ })
267
+
268
+ describe('Instance 상태 동기화', () => {
269
+ it('모든 Thread가 Ended되면 Instance도 Ended가 되어야 한다', async () => {
270
+ await withTestTransaction(async (context) => {
271
+ const { tx, domain, user } = context.state
272
+
273
+ // Given: 단일 Thread Instance
274
+ const instance = await activityInstanceFactory.createWithActivity(
275
+ {
276
+ state: ActivityInstanceStatus.Started,
277
+ threadsMin: 1,
278
+ threadsMax: 1
279
+ },
280
+ undefined,
281
+ domain,
282
+ tx
283
+ )
284
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
285
+ { state: ActivityThreadStatus.Started },
286
+ instance,
287
+ user,
288
+ tx
289
+ )
290
+
291
+ // When: Thread Ended
292
+ thread.state = ActivityThreadStatus.Ended
293
+ thread.terminatedAt = new Date()
294
+ await tx.save('ActivityThread', thread)
295
+
296
+ // Instance도 Ended로 업데이트 시뮬레이션
297
+ instance.state = ActivityInstanceStatus.Ended
298
+ instance.terminatedAt = new Date()
299
+ const endedInstance = await tx.save('ActivityInstance', instance)
300
+
301
+ // Then
302
+ expect(endedInstance.state).toBe(ActivityInstanceStatus.Ended)
303
+ })
304
+ })
305
+
306
+ it('일부 Thread만 Ended되면 Instance는 Started 유지되어야 한다', async () => {
307
+ await withTestTransaction(async (context) => {
308
+ const { tx, domain } = context.state
309
+
310
+ // Given: 복수 Thread Instance
311
+ const user1 = await userFactory.create({ name: 'User 1' }, tx)
312
+ const user2 = await userFactory.create({ name: 'User 2' }, tx)
313
+
314
+ const instance = await activityInstanceFactory.createWithActivity(
315
+ {
316
+ state: ActivityInstanceStatus.Started,
317
+ threadsMin: 2,
318
+ threadsMax: 2
319
+ },
320
+ undefined,
321
+ domain,
322
+ tx
323
+ )
324
+
325
+ const thread1 = await activityThreadFactory.createWithInstanceAndAssignee(
326
+ { state: ActivityThreadStatus.Started },
327
+ instance,
328
+ user1,
329
+ tx
330
+ )
331
+ const thread2 = await activityThreadFactory.createWithInstanceAndAssignee(
332
+ { state: ActivityThreadStatus.Started },
333
+ instance,
334
+ user2,
335
+ tx
336
+ )
337
+
338
+ // When: thread1만 Ended
339
+ thread1.state = ActivityThreadStatus.Ended
340
+ thread1.terminatedAt = new Date()
341
+ await tx.save('ActivityThread', thread1)
342
+
343
+ // Then: Instance는 아직 Started
344
+ // (thread2가 아직 진행 중)
345
+ expect(instance.state).toBe(ActivityInstanceStatus.Started)
346
+ })
347
+ })
348
+ })
349
+
350
+ describe('Output 집계', () => {
351
+ it('Thread의 output이 Instance의 output으로 집계되어야 한다', async () => {
352
+ await withTestTransaction(async (context) => {
353
+ const { tx, domain, user } = context.state
354
+
355
+ // Given
356
+ const instance = await activityInstanceFactory.createWithActivity(
357
+ { state: ActivityInstanceStatus.Started },
358
+ undefined,
359
+ domain,
360
+ tx
361
+ )
362
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
363
+ { state: ActivityThreadStatus.Started },
364
+ instance,
365
+ user,
366
+ tx
367
+ )
368
+
369
+ // When: Thread output 설정 후 Instance에 집계
370
+ const threadOutput = { action: 'Fixed', result: 'success' }
371
+ thread.output = threadOutput
372
+ thread.state = ActivityThreadStatus.Ended
373
+ await tx.save('ActivityThread', thread)
374
+
375
+ // Instance output 업데이트
376
+ instance.output = threadOutput
377
+ const updated = await tx.save('ActivityInstance', instance)
378
+
379
+ // Then
380
+ expect(updated.output).toEqual(threadOutput)
381
+ })
382
+ })
383
+ })
384
+ })