@things-factory/worklist 10.1.203 → 10.1.206
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/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/todo/done-list-calendar-page.d.ts +2 -0
- package/dist-client/pages/todo/done-list-calendar-page.js +63 -64
- package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/work-calendar-sources.d.ts +53 -0
- package/dist-client/work-calendar-sources.js +47 -0
- package/dist-client/work-calendar-sources.js.map +1 -0
- package/dist-server/controllers/activity-instance/pick.js +34 -1
- package/dist-server/controllers/activity-instance/pick.js.map +1 -1
- package/dist-server/controllers/activity-thread/check-authority.js +19 -4
- package/dist-server/controllers/activity-thread/check-authority.js.map +1 -1
- package/dist-server/controllers/common.d.ts +13 -0
- package/dist-server/controllers/common.js +26 -3
- package/dist-server/controllers/common.js.map +1 -1
- package/dist-server/service/activity-thread/my-open-threads.js +16 -2
- package/dist-server/service/activity-thread/my-open-threads.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/tests/pick-race.pg.test.ts +79 -1
- package/tests/pool-pick-waiting-thread.test.ts +254 -0
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/zh.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/worklist",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.206",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
55
55
|
"@operato/p13n": "^10.29.43",
|
|
56
56
|
"@operato/popup": "^10.29.43",
|
|
57
|
-
"@things-factory/attachment-base": "^10.1.
|
|
58
|
-
"@things-factory/auth-base": "^10.1.
|
|
59
|
-
"@things-factory/board-service": "^10.1.
|
|
60
|
-
"@things-factory/context-ui": "^10.1.
|
|
61
|
-
"@things-factory/i18n-base": "^10.1.
|
|
62
|
-
"@things-factory/organization": "^10.1.
|
|
63
|
-
"@things-factory/personalization": "^10.1.
|
|
64
|
-
"@things-factory/scheduler-client": "^10.1.
|
|
65
|
-
"@things-factory/shell": "^10.1.
|
|
57
|
+
"@things-factory/attachment-base": "^10.1.206",
|
|
58
|
+
"@things-factory/auth-base": "^10.1.206",
|
|
59
|
+
"@things-factory/board-service": "^10.1.206",
|
|
60
|
+
"@things-factory/context-ui": "^10.1.206",
|
|
61
|
+
"@things-factory/i18n-base": "^10.1.206",
|
|
62
|
+
"@things-factory/organization": "^10.1.206",
|
|
63
|
+
"@things-factory/personalization": "^10.1.206",
|
|
64
|
+
"@things-factory/scheduler-client": "^10.1.206",
|
|
65
|
+
"@things-factory/shell": "^10.1.206"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "18588a058a4a6cb21c379b2da5517e077c6442ba"
|
|
68
68
|
}
|
|
@@ -7,7 +7,8 @@ import { join } from 'path'
|
|
|
7
7
|
* between. Runs only when WORKLIST_PG_URL names a postgres server (see discard-versus-issue.pg.test.ts).
|
|
8
8
|
*
|
|
9
9
|
* A case waiting for one person must end with one thread, whoever wins; and one person pressing twice
|
|
10
|
-
* must not hold it twice.
|
|
10
|
+
* must not hold it twice. A case in a pool waits with one unassigned thread: whoever takes it, by pick or
|
|
11
|
+
* by claim, takes that thread, and nothing is left waiting once the case has its people.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const url = process.env.WORKLIST_PG_URL
|
|
@@ -23,6 +24,7 @@ maybe('picking at once, on postgres', () => {
|
|
|
23
24
|
const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
|
|
24
25
|
const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
|
|
25
26
|
const { pick } = require(join(DIST, 'controllers', 'activity-instance', 'pick.js'))
|
|
27
|
+
const { start } = require(join(DIST, 'controllers', 'activity-thread', 'start.js'))
|
|
26
28
|
const { Domain } = require('@things-factory/shell')
|
|
27
29
|
const { Role, User } = require('@things-factory/auth-base')
|
|
28
30
|
|
|
@@ -125,4 +127,80 @@ maybe('picking at once, on postgres', () => {
|
|
|
125
127
|
expect(await threadsOf(instance)).toBe(2)
|
|
126
128
|
}
|
|
127
129
|
}, 120000)
|
|
130
|
+
|
|
131
|
+
/** The shape issue() and a pool assign make: the pool, threadsMin, one unassigned thread. */
|
|
132
|
+
const aPoolCase = async (name: string, threadsMin: number) => {
|
|
133
|
+
const instance = await db.getRepository(ActivityInstance).save({
|
|
134
|
+
domain,
|
|
135
|
+
name,
|
|
136
|
+
state: 'issued',
|
|
137
|
+
activity,
|
|
138
|
+
assigneeRoles: [role],
|
|
139
|
+
threadsMin,
|
|
140
|
+
threadsMax: threadsMin
|
|
141
|
+
})
|
|
142
|
+
const waiting = await db.getRepository(ActivityThread).save({ domain, activityInstance: instance, state: 'unassigned' })
|
|
143
|
+
return { instance, waiting }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const statesOf = async (instance: any) =>
|
|
147
|
+
(await db.getRepository(ActivityThread).find({ where: { activityInstance: { id: instance.id } } }))
|
|
148
|
+
.map((thread: any) => [thread.state, thread.assigneeId || null])
|
|
149
|
+
.sort((a: any, b: any) => String(a).localeCompare(String(b)))
|
|
150
|
+
|
|
151
|
+
it('two members picking a pool case that waits for one: one holds the waiting thread, nothing is left waiting', async () => {
|
|
152
|
+
for (let round = 0; round < 20; round++) {
|
|
153
|
+
const { instance, waiting } = await aPoolCase(`pool${round}`, 1)
|
|
154
|
+
|
|
155
|
+
const results = await Promise.allSettled([
|
|
156
|
+
db.transaction((tx: any) => pick(instance.id, as(first)(tx))),
|
|
157
|
+
db.transaction((tx: any) => pick(instance.id, as(second)(tx)))
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
const won = results.findIndex(one => one.status === 'fulfilled')
|
|
161
|
+
expect(results.filter(one => one.status === 'fulfilled').length).toBe(1)
|
|
162
|
+
expect((results[won] as any).value.id).toBe(waiting.id)
|
|
163
|
+
expect(await statesOf(instance)).toEqual([['assigned', [first, second][won].id]])
|
|
164
|
+
}
|
|
165
|
+
}, 120000)
|
|
166
|
+
|
|
167
|
+
it('a pick and a claim of the waiting thread at once: exactly one wins, and it holds the one thread', async () => {
|
|
168
|
+
const endings: string[] = []
|
|
169
|
+
|
|
170
|
+
for (let round = 0; round < 20; round++) {
|
|
171
|
+
const { instance, waiting } = await aPoolCase(`claim${round}`, 1)
|
|
172
|
+
|
|
173
|
+
const [picked, claimed] = await Promise.allSettled([
|
|
174
|
+
db.transaction((tx: any) => pick(instance.id, as(first)(tx))),
|
|
175
|
+
db.transaction((tx: any) => start(waiting.id, {}, '', as(second)(tx)))
|
|
176
|
+
])
|
|
177
|
+
endings.push(`pick ${picked.status} · claim ${claimed.status}`)
|
|
178
|
+
|
|
179
|
+
expect([picked.status, claimed.status].filter(status => status === 'fulfilled').length).toBe(1)
|
|
180
|
+
expect(await statesOf(instance)).toEqual(
|
|
181
|
+
picked.status === 'fulfilled' ? [['assigned', first.id]] : [['started', second.id]]
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('endings', [...new Set(endings)].join(' · '))
|
|
186
|
+
}, 120000)
|
|
187
|
+
|
|
188
|
+
it('two members picking a pool case that waits for two: both hold a thread, nothing is left waiting', async () => {
|
|
189
|
+
for (let round = 0; round < 10; round++) {
|
|
190
|
+
const { instance } = await aPoolCase(`pool-two${round}`, 2)
|
|
191
|
+
|
|
192
|
+
const results = await Promise.allSettled([
|
|
193
|
+
db.transaction((tx: any) => pick(instance.id, as(first)(tx))),
|
|
194
|
+
db.transaction((tx: any) => pick(instance.id, as(second)(tx)))
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
expect(results.map(one => one.status)).toEqual(['fulfilled', 'fulfilled'])
|
|
198
|
+
expect(await statesOf(instance)).toEqual(
|
|
199
|
+
[
|
|
200
|
+
['assigned', first.id],
|
|
201
|
+
['assigned', second.id]
|
|
202
|
+
].sort((a: any, b: any) => String(a).localeCompare(String(b)))
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}, 120000)
|
|
128
206
|
})
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A pool's waiting thread, taken by pick or by claim (2026-10-20).
|
|
5
|
+
*
|
|
6
|
+
* A case in a role's pool carries one unassigned thread, and the to-do list of every pool holder reads that
|
|
7
|
+
* thread (my-open-threads.ts). Pick made a new assigned thread beside it and left it there, so a case one
|
|
8
|
+
* member had taken stayed on everyone else's list. The rule now, whichever door is used:
|
|
9
|
+
*
|
|
10
|
+
* - the taker gets the waiting thread itself;
|
|
11
|
+
* - while the case still waits for more people (fewer valid threads than threadsMin), one waiting thread is
|
|
12
|
+
* left for the rest of the pool, and none once it has enough;
|
|
13
|
+
* - one person holds one thread of a case - a second take is refused by name, and the waiting thread is off
|
|
14
|
+
* the list of whoever already holds one.
|
|
15
|
+
*
|
|
16
|
+
* Real entities on sqlite, as assign-role-pool.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/* Nobody here manages activity instances - the role is the only way in. */
|
|
20
|
+
jest.mock('@things-factory/auth-base', () => {
|
|
21
|
+
const actual = jest.requireActual('@things-factory/auth-base')
|
|
22
|
+
return { ...actual, checkPermission: async () => false }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
|
|
26
|
+
const DIST = join(__dirname, '..', 'dist-server')
|
|
27
|
+
const { Activity } = require(join(DIST, 'service', 'activity', 'activity.js'))
|
|
28
|
+
const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
|
|
29
|
+
const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
|
|
30
|
+
const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
|
|
31
|
+
const { myOpenThreadsWhere } = require(join(DIST, 'service', 'activity-thread', 'my-open-threads.js'))
|
|
32
|
+
const { assign } = require(join(DIST, 'controllers', 'activity-instance', 'assign.js'))
|
|
33
|
+
const { issue } = require(join(DIST, 'controllers', 'activity-instance', 'issue.js'))
|
|
34
|
+
const { pick } = require(join(DIST, 'controllers', 'activity-instance', 'pick.js'))
|
|
35
|
+
const { start } = require(join(DIST, 'controllers', 'activity-thread', 'start.js'))
|
|
36
|
+
const { Domain } = require('@things-factory/shell')
|
|
37
|
+
const { Role, User } = require('@things-factory/auth-base')
|
|
38
|
+
|
|
39
|
+
let db: any
|
|
40
|
+
let here: any
|
|
41
|
+
let manager: any
|
|
42
|
+
let first: any
|
|
43
|
+
let second: any
|
|
44
|
+
let third: any
|
|
45
|
+
let outsider: any
|
|
46
|
+
let pool: any
|
|
47
|
+
let elsewhere: any
|
|
48
|
+
let activity: any
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
db = createSqliteTestDataSource([Activity, ActivityInstance, ActivityThread, ActivityApproval])
|
|
52
|
+
await db.initialize()
|
|
53
|
+
|
|
54
|
+
here = await db.getRepository(Domain).save({ name: 'here', subdomain: 'here' })
|
|
55
|
+
|
|
56
|
+
const person = (name: string) =>
|
|
57
|
+
db.getRepository(User).save({ name, email: `${name}@example.com`, password: 'x', status: 'activated', userType: 'user', domains: [here] })
|
|
58
|
+
|
|
59
|
+
manager = await person('manager')
|
|
60
|
+
first = await person('first')
|
|
61
|
+
second = await person('second')
|
|
62
|
+
third = await person('third')
|
|
63
|
+
outsider = await person('outsider')
|
|
64
|
+
|
|
65
|
+
pool = await db.getRepository(Role).save({ domain: here, name: 'finance', users: [first, second, third] })
|
|
66
|
+
elsewhere = await db.getRepository(Role).save({ domain: here, name: 'support', users: [outsider] })
|
|
67
|
+
|
|
68
|
+
activity = await db.getRepository(Activity).save({ domain: here, name: 'tax-invoice', activityType: 'standard', startingType: 'issue' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
afterAll(async () => db?.destroy())
|
|
72
|
+
|
|
73
|
+
const as = (user: any, tx: any) => ({ state: { domain: here, user, tx }, t: (key: string) => key }) as any
|
|
74
|
+
|
|
75
|
+
const pickAs = (user: any, instance: any) => db.transaction((tx: any) => pick(instance.id, as(user, tx)))
|
|
76
|
+
const claimAs = (user: any, thread: any) => db.transaction((tx: any) => start(thread.id, {}, '', as(user, tx)))
|
|
77
|
+
|
|
78
|
+
const reread = (instance: any) => db.getRepository(ActivityInstance).findOneBy({ id: instance.id })
|
|
79
|
+
|
|
80
|
+
/** [state, holder] of every thread of the case, holders by name, in a fixed order. */
|
|
81
|
+
const threadsOf = async (instance: any) => {
|
|
82
|
+
const threads = await db.getRepository(ActivityThread).find({ where: { activityInstance: { id: instance.id } } })
|
|
83
|
+
const names = new Map([first, second, third, outsider, manager].map((user: any) => [user.id, user.name]))
|
|
84
|
+
return threads.map((thread: any) => [thread.state, names.get(thread.assigneeId) || null]).sort((a: any, b: any) => String(a).localeCompare(String(b)))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const waitingOf = async (instance: any) =>
|
|
88
|
+
(await db.getRepository(ActivityThread).find({ where: { activityInstance: { id: instance.id }, state: 'unassigned' } }))[0]
|
|
89
|
+
|
|
90
|
+
/** How many times the case is on this person's to-do list - the list's own condition (my-open-threads.ts). */
|
|
91
|
+
const onTheListOf = (user: any, roles: any[], instance: any) =>
|
|
92
|
+
db
|
|
93
|
+
.getRepository(ActivityThread)
|
|
94
|
+
.createQueryBuilder('at')
|
|
95
|
+
.innerJoin('at.activityInstance', 'ai')
|
|
96
|
+
.where(myOpenThreadsWhere({ userId: user.id, roles: roles.map(role => role.id) }))
|
|
97
|
+
.andWhere('ai.id = :id', { id: instance.id })
|
|
98
|
+
.getCount()
|
|
99
|
+
|
|
100
|
+
/** The case as each pool member and the outsider see it on their lists. */
|
|
101
|
+
const lists = async (instance: any) => ({
|
|
102
|
+
first: await onTheListOf(first, [pool], instance),
|
|
103
|
+
second: await onTheListOf(second, [pool], instance),
|
|
104
|
+
third: await onTheListOf(third, [pool], instance),
|
|
105
|
+
outsider: await onTheListOf(outsider, [elsewhere], instance)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const issued = (threadsMin: number) =>
|
|
109
|
+
db.transaction((tx: any) => issue({ activityId: activity.id, assignees: [], assigneeRoles: [pool], threadsMin }, as(manager, tx)))
|
|
110
|
+
|
|
111
|
+
const assigned = async () => {
|
|
112
|
+
const instance = await db.getRepository(ActivityInstance).save({ domain: here, name: 'case', state: 'issued', activity })
|
|
113
|
+
await db.transaction((tx: any) => assign({ id: instance.id, assigneeRoles: [pool.id] }, as(manager, tx)))
|
|
114
|
+
return instance
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('a pool waiting for one person', () => {
|
|
118
|
+
it.each([
|
|
119
|
+
['issued with an assignee role', () => issued(1)],
|
|
120
|
+
['put in a pool by assign', assigned]
|
|
121
|
+
])('%s, picked by a member: the waiting thread is theirs, and the rest of the pool no longer sees the case', async (_, made) => {
|
|
122
|
+
const instance = await made()
|
|
123
|
+
const [waiting] = await threadsOf(instance)
|
|
124
|
+
expect(waiting).toEqual(['unassigned', null])
|
|
125
|
+
|
|
126
|
+
await pickAs(first, instance)
|
|
127
|
+
|
|
128
|
+
expect(await threadsOf(instance)).toEqual([['assigned', 'first']])
|
|
129
|
+
expect((await reread(instance)).state).toBe('assigned')
|
|
130
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 0, third: 0, outsider: 0 })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('pick returns the waiting thread itself, now held', async () => {
|
|
134
|
+
const instance = await issued(1)
|
|
135
|
+
const waiting = await waitingOf(instance)
|
|
136
|
+
|
|
137
|
+
const picked = await pickAs(first, instance)
|
|
138
|
+
|
|
139
|
+
expect(picked.id).toBe(waiting.id)
|
|
140
|
+
expect([picked.state, picked.assigneeId]).toEqual(['assigned', first.id])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('a second member picking after it is refused by name and the case is left as it was', async () => {
|
|
144
|
+
const instance = await issued(1)
|
|
145
|
+
await pickAs(first, instance)
|
|
146
|
+
|
|
147
|
+
await expect(pickAs(second, instance)).rejects.toThrow('error.activity-instance should not be assigned')
|
|
148
|
+
expect(await threadsOf(instance)).toEqual([['assigned', 'first']])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('claimed by a member: as before, one thread, theirs, off the rest of the pool', async () => {
|
|
152
|
+
const instance = await issued(1)
|
|
153
|
+
|
|
154
|
+
await claimAs(first, await waitingOf(instance))
|
|
155
|
+
|
|
156
|
+
expect(await threadsOf(instance)).toEqual([['started', 'first']])
|
|
157
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 0, third: 0, outsider: 0 })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('a case with no waiting thread is picked as before - a new thread for the picker', async () => {
|
|
161
|
+
const instance = await db.getRepository(ActivityInstance).save({
|
|
162
|
+
domain: here,
|
|
163
|
+
name: 'bare',
|
|
164
|
+
state: 'issued',
|
|
165
|
+
activity,
|
|
166
|
+
assigneeRoles: [pool],
|
|
167
|
+
threadsMin: 1
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await pickAs(first, instance)
|
|
171
|
+
|
|
172
|
+
expect(await threadsOf(instance)).toEqual([['assigned', 'first']])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('a pool waiting for two people (threadsMin 2)', () => {
|
|
177
|
+
it('the first pick takes the waiting thread and leaves one for the rest; the second fills it and none is left', async () => {
|
|
178
|
+
const instance = await issued(2)
|
|
179
|
+
expect(await threadsOf(instance)).toEqual([['unassigned', null]])
|
|
180
|
+
|
|
181
|
+
await pickAs(first, instance)
|
|
182
|
+
|
|
183
|
+
expect(await threadsOf(instance)).toEqual([
|
|
184
|
+
['assigned', 'first'],
|
|
185
|
+
['unassigned', null]
|
|
186
|
+
])
|
|
187
|
+
expect((await reread(instance)).state).toBe('unassigned')
|
|
188
|
+
/* first sees their own thread once - not the pool's waiting thread beside it. */
|
|
189
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 1, third: 1, outsider: 0 })
|
|
190
|
+
|
|
191
|
+
await pickAs(second, instance)
|
|
192
|
+
|
|
193
|
+
expect(await threadsOf(instance)).toEqual([
|
|
194
|
+
['assigned', 'first'],
|
|
195
|
+
['assigned', 'second']
|
|
196
|
+
])
|
|
197
|
+
expect((await reread(instance)).state).toBe('assigned')
|
|
198
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 1, third: 0, outsider: 0 })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('the same by claiming the waiting thread each time', async () => {
|
|
202
|
+
const instance = await issued(2)
|
|
203
|
+
|
|
204
|
+
await claimAs(first, await waitingOf(instance))
|
|
205
|
+
|
|
206
|
+
expect(await threadsOf(instance)).toEqual([
|
|
207
|
+
['started', 'first'],
|
|
208
|
+
['unassigned', null]
|
|
209
|
+
])
|
|
210
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 1, third: 1, outsider: 0 })
|
|
211
|
+
|
|
212
|
+
await claimAs(second, await waitingOf(instance))
|
|
213
|
+
|
|
214
|
+
expect(await threadsOf(instance)).toEqual([
|
|
215
|
+
['started', 'first'],
|
|
216
|
+
['started', 'second']
|
|
217
|
+
])
|
|
218
|
+
expect(await lists(instance)).toEqual({ first: 1, second: 1, third: 0, outsider: 0 })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('one picks, the next claims: two holders, nothing left waiting', async () => {
|
|
222
|
+
const instance = await issued(2)
|
|
223
|
+
|
|
224
|
+
await pickAs(first, instance)
|
|
225
|
+
await claimAs(second, await waitingOf(instance))
|
|
226
|
+
|
|
227
|
+
expect(await threadsOf(instance)).toEqual([
|
|
228
|
+
['assigned', 'first'],
|
|
229
|
+
['started', 'second']
|
|
230
|
+
])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('someone who holds a thread already is refused by name a second one, by pick or by claim', async () => {
|
|
234
|
+
const instance = await issued(2)
|
|
235
|
+
await pickAs(first, instance)
|
|
236
|
+
|
|
237
|
+
await expect(pickAs(first, instance)).rejects.toThrow('error.activity-instance already yours')
|
|
238
|
+
await expect(claimAs(first, await waitingOf(instance))).rejects.toThrow('error.activity-instance already yours')
|
|
239
|
+
|
|
240
|
+
expect(await threadsOf(instance)).toEqual([
|
|
241
|
+
['assigned', 'first'],
|
|
242
|
+
['unassigned', null]
|
|
243
|
+
])
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('someone outside the pool is refused the waiting thread, and it stays waiting', async () => {
|
|
247
|
+
const instance = await issued(2)
|
|
248
|
+
|
|
249
|
+
await expect(pickAs(outsider, instance)).rejects.toThrow('error.activity-instance is addressed to another role')
|
|
250
|
+
await expect(claimAs(outsider, await waitingOf(instance))).rejects.toThrow('error.not authorized to handle this task')
|
|
251
|
+
|
|
252
|
+
expect(await threadsOf(instance)).toEqual([['unassigned', null]])
|
|
253
|
+
})
|
|
254
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "someone else handled this approval first. reload the list to see where it stands.",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id} is someone else's draft. Only the person who wrote it, or someone who manages activity instances, may edit or issue it.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} is assigned to another role. only someone with that role can pick it.",
|
|
37
|
+
"error.activity-instance already yours": "you already hold this task ({id}).",
|
|
37
38
|
"error.activity-instance is already issued": "this task ({id}) has already been issued with the {state} status.",
|
|
38
39
|
"error.activity-instance is already terminated": "this task ({id}) has already been terminated with the {state} status.",
|
|
39
40
|
"error.activity-instance approval line is set": "this task already has its approval line; it is set on the task's detail screen.",
|
package/translations/es.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "otra persona resolvió esta aprobación antes. vuelva a cargar la lista para ver su estado.",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id} es un borrador de otra persona. Solo quien lo redactó, o quien gestiona las instancias de actividad, puede editarlo o emitirlo.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} está asignado a otro rol. solo alguien con ese rol puede seleccionarlo.",
|
|
37
|
+
"error.activity-instance already yours": "ya tiene esta tarea ({id}).",
|
|
37
38
|
"error.activity-instance is already issued": "esta tarea ({id}) ya se emitió con el estado {state}.",
|
|
38
39
|
"error.activity-instance is already terminated": "esta tarea ({id}) ya finalizó con el estado {state}.",
|
|
39
40
|
"error.activity-instance approval line is set": "esta tarea ya tiene su línea de aprobación; se configura en la pantalla de detalle de la tarea.",
|
package/translations/ja.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "他の方が先に処理しました。一覧を再読み込みしてください。",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id} is someone else's draft. Only the person who wrote it, or someone who manages activity instances, may edit or issue it.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} は別の役割に割り当てられた業務です。その役割を持つ人だけが選択できます。",
|
|
37
|
+
"error.activity-instance already yours": "この業務({id})はすでに担当しています。",
|
|
37
38
|
"error.activity-instance is already issued": "このタスク({id})はすでに{state}状態で発行されています。",
|
|
38
39
|
"error.activity-instance is already terminated": "このタスク({id})はすでに{state}状態で終了しています。",
|
|
39
40
|
"error.activity-instance approval line is set": "この業務は決裁ラインが決まっています。決裁ラインは業務詳細で変更します。",
|
package/translations/ko.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "다른 분이 먼저 처리했습니다. 목록을 새로 읽어 주십시오.",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id}은(는) 다른 사용자가 작성한 초안입니다. 작성자 또는 업무 관리자만 수정하고 제출할 수 있습니다.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} 는 다른 역할에 할당된 업무입니다. 그 역할을 가진 사람만 선택할 수 있습니다.",
|
|
37
|
+
"error.activity-instance already yours": "이미 맡은 업무입니다({id}).",
|
|
37
38
|
"error.activity-instance is already issued": "이 작업({id})은 이미 {state} 상태로 제기되었습니다.",
|
|
38
39
|
"error.activity-instance is already terminated": "이 작업({id})는 이미 {state} 상태로 종료되었습니다.",
|
|
39
40
|
"error.activity-instance approval line is set": "이 업무에는 결재선이 지정되어 있습니다. 업무 상세 화면에서 결재선을 변경해 주세요.",
|
package/translations/ms.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "orang lain telah menguruskannya dahulu. sila muat semula senarai.",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id} is someone else's draft. Only the person who wrote it, or someone who manages activity instances, may edit or issue it.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} ditugaskan kepada peranan lain. hanya orang yang memegang peranan itu boleh memilihnya.",
|
|
37
|
+
"error.activity-instance already yours": "anda sudah memegang tugas ini ({id}).",
|
|
37
38
|
"error.activity-instance is already issued": "Tugas ini ({id}) telah dikeluarkan dengan status {state}.",
|
|
38
39
|
"error.activity-instance is already terminated": "Tugas ini ({id}) telah tamat dengan status {state}.",
|
|
39
40
|
"error.activity-instance approval line is set": "tugas ini sudah mempunyai laluan kelulusan; ia ditetapkan pada skrin butiran tugas.",
|
package/translations/zh.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"error.activity-approval taken by someone else": "他人已先行处理,请重新加载列表。",
|
|
35
35
|
"error.activity-instance belongs to someone else": "{id} is someone else's draft. Only the person who wrote it, or someone who manages activity instances, may edit or issue it.",
|
|
36
36
|
"error.activity-instance is addressed to another role": "{id} 已分配给其他角色。只有拥有该角色的人员才能选择。",
|
|
37
|
+
"error.activity-instance already yours": "您已负责此任务({id})。",
|
|
37
38
|
"error.activity-instance is already issued": "此任务({id})已被以{state}状态提出。",
|
|
38
39
|
"error.activity-instance is already terminated": "此任务({id})已以{state}状态终止。",
|
|
39
40
|
"error.activity-instance approval line is set": "此业务已设定审批线,请在业务详情中修改。",
|