@things-factory/organization 10.1.165 → 10.1.168

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/organization",
3
- "version": "10.1.165",
3
+ "version": "10.1.168",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -36,10 +36,10 @@
36
36
  "@operato/p13n": "^10.0.0",
37
37
  "@operato/popup": "^10.15.0",
38
38
  "@operato/shell": "^10.0.0",
39
- "@things-factory/auth-base": "^10.1.165",
40
- "@things-factory/code-ui": "^10.1.165",
41
- "@things-factory/contact": "^10.1.165",
39
+ "@things-factory/auth-base": "^10.1.167",
40
+ "@things-factory/code-ui": "^10.1.167",
41
+ "@things-factory/contact": "^10.1.167",
42
42
  "@things-factory/shell": "^10.1.165"
43
43
  },
44
- "gitHead": "d9e3774d70561b063e4f1d10c387caf721d99b61"
44
+ "gitHead": "fe067ff11d90effffc2523374453d9b8313b489f"
45
45
  }
@@ -0,0 +1,78 @@
1
+ import {
2
+ lineAfterCopy,
3
+ lineFromRows,
4
+ participationChoice,
5
+ PARTICIPATION_CHOICES
6
+ } from '../client/component/approval-line-edit'
7
+
8
+ const ROLE = { id: 'role-1', name: '대표' }
9
+ const PERSON = { id: 'employee-1', name: '홍길동' }
10
+
11
+ describe('the line the editor hands on is read off the rows on screen', () => {
12
+ it('keeps type, approver, participation and notifyAfter, in row order', () => {
13
+ expect(
14
+ lineFromRows([
15
+ { type: 'Role', approver: ROLE, participation: '' },
16
+ { type: 'Employee', approver: PERSON, participation: 'reference', notifyAfter: '2' }
17
+ ])
18
+ ).toEqual([
19
+ { type: 'Role', approver: ROLE, participation: undefined, notifyAfter: undefined },
20
+ { type: 'Employee', approver: PERSON, participation: 'reference', notifyAfter: 2 }
21
+ ])
22
+ })
23
+
24
+ it('drops a row with no type - an added row nobody filled', () => {
25
+ expect(lineFromRows([{ type: 'Role', approver: ROLE }, { approver: PERSON }, {}])).toHaveLength(1)
26
+ })
27
+
28
+ it('reads the API spelling of a reference', () => {
29
+ expect(lineFromRows([{ type: 'Role', approver: ROLE, participation: 'Reference' }])[0].participation).toBe('reference')
30
+ })
31
+
32
+ it('no rows: an empty line', () => {
33
+ expect(lineFromRows(undefined)).toEqual([])
34
+ })
35
+ })
36
+
37
+ describe('copying a saved line', () => {
38
+ const current = [{ type: 'Role', approver: ROLE }] as any
39
+
40
+ it('nothing picked: the line being edited stays as it is', () => {
41
+ expect(lineAfterCopy(current, undefined)).toBe(current)
42
+ })
43
+
44
+ it('emptied on purpose: an empty line', () => {
45
+ expect(lineAfterCopy(current, null)).toEqual([])
46
+ })
47
+
48
+ it('a picked line comes over with its reference steps and their notify point', () => {
49
+ const picked = {
50
+ model: [
51
+ { type: 'Role', approver: ROLE },
52
+ { type: 'Employee', approver: PERSON, participation: 'Reference', notifyAfter: 1 }
53
+ ]
54
+ } as any
55
+
56
+ expect(lineAfterCopy(current, picked)).toEqual([
57
+ { type: 'Role', approver: ROLE, participation: undefined, notifyAfter: undefined },
58
+ { type: 'Employee', approver: PERSON, participation: 'reference', notifyAfter: 1 }
59
+ ])
60
+ })
61
+ })
62
+
63
+ describe('the participation cell names approval too', () => {
64
+ it('a step with nothing stored reads as approval', () => {
65
+ expect(participationChoice({}).key).toBe('label.participation-approval')
66
+ expect(participationChoice({ participation: undefined }).key).toBe('label.participation-approval')
67
+ expect(participationChoice({ participation: null }).key).toBe('label.participation-approval')
68
+ })
69
+
70
+ it('a reference reads as reference, in either spelling', () => {
71
+ expect(participationChoice({ participation: 'reference' }).key).toBe('label.participation-reference')
72
+ expect(participationChoice({ participation: 'Reference' }).key).toBe('label.participation-reference')
73
+ })
74
+
75
+ it('every choice the cell can draw is a choice the select offers', () => {
76
+ expect(PARTICIPATION_CHOICES.map(one => one.value)).toEqual(['', 'reference'])
77
+ })
78
+ })
@@ -0,0 +1,108 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * Every screen that reads an approval line step asks for every field the step carries.
6
+ *
7
+ * The approval line editor keeps `type`, `approver`, `participation` and `notifyAfter` of a step
8
+ * (approval-line-edit's lineFromRows; `value` is left to `approver` on purpose). Screens asked for
9
+ * some of them. A screen that reopens a line and saves it again
10
+ * writes back only what it read, so a missing `participation` turns a reference step into an
11
+ * approver and a missing `notifyAfter` wipes the notify point - with no error. Found so far: the
12
+ * worklist screens (notifyAfter, twelve selections), the copy-from selector and the two template
13
+ * pages here (both), and meta-ui's activity definition (both).
14
+ *
15
+ * A gql`` document is a template literal, so type checking cannot see a missing field. This reads the
16
+ * documents of every package's client: any `approvalLine { }` or `model { }` selection that asks for
17
+ * `approver` is a step selection.
18
+ */
19
+
20
+ const packagesDir = path.resolve(__dirname, '../..')
21
+
22
+ function sources(dir: string): string[] {
23
+ return readdirSync(dir).flatMap(name => {
24
+ if (name === 'node_modules' || name.startsWith('dist')) {
25
+ return []
26
+ }
27
+
28
+ const file = path.join(dir, name)
29
+
30
+ if (statSync(file).isDirectory()) {
31
+ return sources(file)
32
+ }
33
+
34
+ return file.endsWith('.ts') ? [file] : []
35
+ })
36
+ }
37
+
38
+ /** The top-level field names of each `approvalLine { }` / `model { }` selection in a source. */
39
+ function selections(source: string): string[][] {
40
+ const found: string[][] = []
41
+
42
+ for (const match of source.matchAll(/\b(?:approvalLine|model)\s*\{/g)) {
43
+ const fields: string[] = []
44
+ let depth = 0
45
+ let word = ''
46
+
47
+ const take = () => {
48
+ if (depth === 1 && word) fields.push(word)
49
+ word = ''
50
+ }
51
+
52
+ for (let at = match.index! + match[0].length - 1; at < source.length; at++) {
53
+ const char = source[at]
54
+
55
+ if (char === '{') {
56
+ take()
57
+ depth++
58
+ } else if (char === '}') {
59
+ take()
60
+ if (!--depth) break
61
+ } else if (/\w/.test(char)) {
62
+ word += char
63
+ } else {
64
+ take()
65
+ }
66
+ }
67
+
68
+ found.push(fields)
69
+ }
70
+
71
+ return found
72
+ }
73
+
74
+ /** What the editor keeps of a step - read less and the next save loses the rest. */
75
+ const STEP_FIELDS = ['type', 'participation', 'notifyAfter', 'approver']
76
+
77
+ const clients = readdirSync(packagesDir)
78
+ .map(name => path.join(packagesDir, name, 'client'))
79
+ .filter(dir => statSync(path.dirname(dir)).isDirectory() && readdirSync(path.dirname(dir)).includes('client'))
80
+
81
+ const screens = clients
82
+ .flatMap(sources)
83
+ .map(file => ({
84
+ file: path.relative(packagesDir, file),
85
+ steps: selections(readFileSync(file, 'utf-8')).filter(fields => fields.includes('approver'))
86
+ }))
87
+ .filter(one => one.steps.length)
88
+
89
+ describe('every screen that reads an approval line step asks for all of its fields', () => {
90
+ it('the scan finds the screens', () => {
91
+ expect(screens.map(one => one.file)).toEqual(
92
+ expect.arrayContaining([
93
+ 'organization/client/component/approval-line-selector.ts',
94
+ 'organization/client/pages/approval-line/my-approval-line-templates-page.ts',
95
+ 'worklist/client/pages/activity-catalog/activity-detail-page.ts'
96
+ ])
97
+ )
98
+ })
99
+
100
+ it.each(screens.map(one => [one.file, one.steps] as [string, string[][]]))('%s', (file, steps) => {
101
+ for (const fields of steps) {
102
+ expect({ asks: fields, missing: STEP_FIELDS.filter(field => !fields.includes(field)) }).toEqual({
103
+ asks: fields,
104
+ missing: []
105
+ })
106
+ }
107
+ })
108
+ })