@yanqirenshi/table.wbs 0.0.17 → 0.0.19
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/.claude/launch.json +11 -0
- package/README.md +102 -0
- package/dist/Core.js +187 -29
- package/dist/Supporter.js +191 -0
- package/dist/WBSTable.js +55 -8
- package/dist/components/ChooserColumn.js +43 -1
- package/dist/components/Controller.js +101 -15
- package/dist/components/TBody.js +3 -1
- package/dist/components/TBodyTr.js +39 -10
- package/dist/components/THead.js +14 -2
- package/dist/index.js +8 -0
- package/example/.env +4 -0
- package/example/package-lock.json +377 -157
- package/example/package.json +6 -3
- package/example/yanqirenshi-table.wbs-0.0.18.tgz +0 -0
- package/package.json +1 -1
- package/tests/Core.filter.test.js +51 -0
- package/tests/Core.selection.test.js +49 -0
- package/tests/Core.width.test.js +70 -0
- package/tests/Supporter.test.js +115 -0
- package/.claude/settings.local.json +0 -17
package/example/package.json
CHANGED
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
"@testing-library/jest-dom": "^6.4.6",
|
|
7
7
|
"@testing-library/react": "^16.0.0",
|
|
8
8
|
"@testing-library/user-event": "^14.5.2",
|
|
9
|
-
"@yanqirenshi/table.wbs": "file
|
|
9
|
+
"@yanqirenshi/table.wbs": "file:yanqirenshi-table.wbs-0.0.18.tgz",
|
|
10
10
|
"moment": "^2.27.0",
|
|
11
11
|
"react": "^19.0.0",
|
|
12
12
|
"react-dom": "^19.0.0",
|
|
13
13
|
"react-router-dom": "^5.2.0",
|
|
14
14
|
"react-scripts": "3.4.1"
|
|
15
15
|
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"cross-env": "^7.0.3"
|
|
18
|
+
},
|
|
16
19
|
"scripts": {
|
|
17
|
-
"start": "react-scripts start",
|
|
18
|
-
"build": "react-scripts build",
|
|
20
|
+
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
|
|
21
|
+
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-scripts build",
|
|
19
22
|
"test": "react-scripts test",
|
|
20
23
|
"eject": "react-scripts eject"
|
|
21
24
|
},
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Core from '../src/Core.js';
|
|
2
|
+
|
|
3
|
+
const core = new Core();
|
|
4
|
+
|
|
5
|
+
// 1
|
|
6
|
+
// ├ 20
|
|
7
|
+
// │ ├ 22
|
|
8
|
+
// │ │ └ 220 (wp)
|
|
9
|
+
// │ └ 21
|
|
10
|
+
// │ ├ 210 (wp)
|
|
11
|
+
// │ └ 211 (wp)
|
|
12
|
+
// └ 10
|
|
13
|
+
// └ 100 (wp)
|
|
14
|
+
const wp = (_id, _level, label) => ({ _id, _level, _class: 'WORKPACKAGE', label, _core: { label } });
|
|
15
|
+
const wbs = (_id, _level, label) => ({ _id, _level, _class: 'WBS', label, _core: { label } });
|
|
16
|
+
|
|
17
|
+
const records = [
|
|
18
|
+
wbs(1, 0, 'project'),
|
|
19
|
+
wbs(20, 1, 'design'),
|
|
20
|
+
wbs(22, 2, 'detail'),
|
|
21
|
+
wp(220, 3, 'api'),
|
|
22
|
+
wbs(21, 2, 'basic'),
|
|
23
|
+
wp(210, 3, 'screen'),
|
|
24
|
+
wp(211, 3, 'db'),
|
|
25
|
+
wbs(10, 1, 'requirements'),
|
|
26
|
+
wp(100, 2, 'survey'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const ids = (list) => list.map(d => d._id);
|
|
30
|
+
|
|
31
|
+
describe('applyFilter (collapse)', () => {
|
|
32
|
+
test('shows everything when nothing is closed', () => {
|
|
33
|
+
expect(ids(core.applyFilter('', {}, records))).toEqual([1, 20, 22, 220, 21, 210, 211, 10, 100]);
|
|
34
|
+
});
|
|
35
|
+
test('hides the descendants of a closed node but keeps the node itself', () => {
|
|
36
|
+
expect(ids(core.applyFilter('', { 21: true }, records))).toEqual([1, 20, 22, 220, 21, 10, 100]);
|
|
37
|
+
});
|
|
38
|
+
test('a closed node inside a closed ancestor stays hidden', () => {
|
|
39
|
+
// 21 を閉じてから 20 を閉じる → 21 は 20 の中なので見えてはいけない
|
|
40
|
+
expect(ids(core.applyFilter('', { 21: true, 20: true }, records))).toEqual([1, 20, 10, 100]);
|
|
41
|
+
});
|
|
42
|
+
test('siblings after a closed subtree are shown again', () => {
|
|
43
|
+
expect(ids(core.applyFilter('', { 20: true }, records))).toEqual([1, 20, 10, 100]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('applyFilter (workpackage filter)', () => {
|
|
48
|
+
test('keeps only matching workpackages, case-insensitively, and all WBS rows', () => {
|
|
49
|
+
expect(ids(core.applyFilter('API', {}, records))).toEqual([1, 20, 22, 220, 21, 10]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Core from '../src/Core.js';
|
|
2
|
+
|
|
3
|
+
const core = new Core();
|
|
4
|
+
|
|
5
|
+
const records = [
|
|
6
|
+
{ _id: 1, _class: 'PROJECT', _level: 0 },
|
|
7
|
+
{ _id: 10, _class: 'WBS', _level: 1 },
|
|
8
|
+
{ _id: 100, _class: 'WORKPACKAGE', _level: 2 },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe('toggleSelected (single)', () => {
|
|
12
|
+
test('selects the clicked row and replaces any previous selection', () => {
|
|
13
|
+
expect(core.toggleSelected(10, {}, 'single')).toEqual({ 10: true });
|
|
14
|
+
expect(core.toggleSelected(100, { 10: true }, 'single')).toEqual({ 100: true });
|
|
15
|
+
});
|
|
16
|
+
test('clicking the selected row again clears the selection', () => {
|
|
17
|
+
expect(core.toggleSelected(10, { 10: true }, 'single')).toEqual({});
|
|
18
|
+
});
|
|
19
|
+
test('does not mutate the given object', () => {
|
|
20
|
+
const before = { 10: true };
|
|
21
|
+
core.toggleSelected(100, before, 'single');
|
|
22
|
+
expect(before).toEqual({ 10: true });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('toggleSelected (multiple)', () => {
|
|
27
|
+
test('adds rows to the selection', () => {
|
|
28
|
+
expect(core.toggleSelected(10, {}, 'multiple')).toEqual({ 10: true });
|
|
29
|
+
expect(core.toggleSelected(100, { 10: true }, 'multiple')).toEqual({ 10: true, 100: true });
|
|
30
|
+
});
|
|
31
|
+
test('removes a row that is already selected', () => {
|
|
32
|
+
expect(core.toggleSelected(10, { 10: true, 100: true }, 'multiple')).toEqual({ 100: true });
|
|
33
|
+
});
|
|
34
|
+
test('does not mutate the given object', () => {
|
|
35
|
+
const before = { 10: true };
|
|
36
|
+
core.toggleSelected(100, before, 'multiple');
|
|
37
|
+
expect(before).toEqual({ 10: true });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('selectedIds / selectedRecords', () => {
|
|
42
|
+
test('selectedIds returns numeric ids', () => {
|
|
43
|
+
expect(core.selectedIds({ 10: true, 100: true }).sort((a, b) => a - b)).toEqual([10, 100]);
|
|
44
|
+
expect(core.selectedIds({})).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
test('selectedRecords keeps record order', () => {
|
|
47
|
+
expect(core.selectedRecords({ 100: true, 1: true }, records).map(d => d._id)).toEqual([1, 100]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Core from '../src/Core.js';
|
|
2
|
+
|
|
3
|
+
const core = new Core();
|
|
4
|
+
|
|
5
|
+
const columns = core.makeColumns([
|
|
6
|
+
{ label: 'Code', leveling: true },
|
|
7
|
+
{ label: 'Name', width: { min: 100 } },
|
|
8
|
+
{ label: 'Description' },
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
describe('makeColumns', () => {
|
|
12
|
+
test('gives every column a width with min/max defaulting to null', () => {
|
|
13
|
+
expect(columns[0].width).toEqual({ min: null, max: null });
|
|
14
|
+
expect(columns[2].width).toEqual({ min: null, max: null });
|
|
15
|
+
});
|
|
16
|
+
test('keeps width given in the seed', () => {
|
|
17
|
+
expect(columns[1].width).toEqual({ min: 100, max: null });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('changeColumnWidth', () => {
|
|
22
|
+
test('sets min/max on the target column only and does not mutate the input', () => {
|
|
23
|
+
const out = core.changeColumnWidth(2, 'max', 300, columns);
|
|
24
|
+
|
|
25
|
+
expect(out[2].width).toEqual({ min: null, max: 300 });
|
|
26
|
+
expect(out[0]).toBe(columns[0]);
|
|
27
|
+
expect(out[1]).toBe(columns[1]);
|
|
28
|
+
expect(columns[2].width).toEqual({ min: null, max: null });
|
|
29
|
+
});
|
|
30
|
+
test('accepts numeric strings from inputs', () => {
|
|
31
|
+
expect(core.changeColumnWidth(1, 'max', '250', columns)[1].width).toEqual({ min: 100, max: 250 });
|
|
32
|
+
});
|
|
33
|
+
test('treats empty, NaN and non-positive values as unset', () => {
|
|
34
|
+
expect(core.changeColumnWidth(1, 'min', '', columns)[1].width.min).toBeNull();
|
|
35
|
+
expect(core.changeColumnWidth(1, 'min', 'abc', columns)[1].width.min).toBeNull();
|
|
36
|
+
expect(core.changeColumnWidth(1, 'min', 0, columns)[1].width.min).toBeNull();
|
|
37
|
+
expect(core.changeColumnWidth(1, 'min', -5, columns)[1].width.min).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('widthCellStyle / fitsFullWidth', () => {
|
|
42
|
+
test('widthCellStyle pins the column width to max, nothing otherwise', () => {
|
|
43
|
+
expect(core.widthCellStyle(columns[1])).toEqual({});
|
|
44
|
+
expect(core.widthCellStyle(core.changeColumnWidth(1, 'max', 120, columns)[1])).toEqual({ width: 120 });
|
|
45
|
+
});
|
|
46
|
+
test('fitsFullWidth is false only when every column has a max', () => {
|
|
47
|
+
expect(core.fitsFullWidth(columns)).toBe(true);
|
|
48
|
+
|
|
49
|
+
let all = columns;
|
|
50
|
+
for (const c of columns)
|
|
51
|
+
all = core.changeColumnWidth(c.number, 'max', 100, all);
|
|
52
|
+
|
|
53
|
+
expect(core.fitsFullWidth(all)).toBe(false);
|
|
54
|
+
expect(core.fitsFullWidth(core.changeColumnWidth(2, 'max', '', all))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('widthStyle', () => {
|
|
59
|
+
test('returns an empty style when nothing is set', () => {
|
|
60
|
+
expect(core.widthStyle(columns[0])).toEqual({});
|
|
61
|
+
expect(core.widthStyle({ label: 'x' })).toEqual({});
|
|
62
|
+
});
|
|
63
|
+
test('maps min to minWidth', () => {
|
|
64
|
+
expect(core.widthStyle(columns[1])).toEqual({ minWidth: 100 });
|
|
65
|
+
});
|
|
66
|
+
test('maps max to maxWidth and enables word breaking', () => {
|
|
67
|
+
const c = core.changeColumnWidth(2, 'max', 300, columns)[2];
|
|
68
|
+
expect(core.widthStyle(c)).toEqual({ maxWidth: 300, overflowWrap: 'break-word' });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import Supporter from '../src/Supporter.js';
|
|
4
|
+
|
|
5
|
+
const wbs = { _id: 10, _class: 'WBS', _level: 1, label: 'b', description: 'desc-b' };
|
|
6
|
+
const wp = { _id: 100, _class: 'WORKPACKAGE', _node_type: 'DB_TABLE', _level: 2,
|
|
7
|
+
label: 'c', _core: { physical_name: 'T_C', use: true } };
|
|
8
|
+
|
|
9
|
+
describe('margin', () => {
|
|
10
|
+
test('default indent is a full-width space per level', () => {
|
|
11
|
+
const s = new Supporter();
|
|
12
|
+
expect(s.margin(0)).toBe('');
|
|
13
|
+
expect(s.margin(3)).toBe(' ');
|
|
14
|
+
});
|
|
15
|
+
test('indent can be customized', () => {
|
|
16
|
+
const s = new Supporter({ indent: '--' });
|
|
17
|
+
expect(s.margin(2)).toBe('----');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('path / to', () => {
|
|
22
|
+
test('falls back to defaultPath when the node type is unknown', () => {
|
|
23
|
+
const s = new Supporter();
|
|
24
|
+
expect(s.to(wbs)).toBe('/wbs/10');
|
|
25
|
+
});
|
|
26
|
+
test('uses _node_type before _class to look up paths', () => {
|
|
27
|
+
const s = new Supporter({ paths: { DB_TABLE: 'database/tables', WORKPACKAGE: 'wp' } });
|
|
28
|
+
expect(s.to(wp)).toBe('/database/tables/100');
|
|
29
|
+
});
|
|
30
|
+
test('defaultPath can be customized', () => {
|
|
31
|
+
const s = new Supporter({ defaultPath: 'nodes' });
|
|
32
|
+
expect(s.to(wbs)).toBe('/nodes/10');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('cellID', () => {
|
|
37
|
+
test('returns the raw _id when no link renderer is given', () => {
|
|
38
|
+
const s = new Supporter();
|
|
39
|
+
expect(s.cellID({}, wbs)).toBe(10);
|
|
40
|
+
expect(s.cellIDNotLink({}, wbs)).toBe(10);
|
|
41
|
+
});
|
|
42
|
+
test('delegates to the link renderer with (to, label, data)', () => {
|
|
43
|
+
const link = jest.fn((to, label) => `${label}@${to}`);
|
|
44
|
+
const s = new Supporter({ link: link });
|
|
45
|
+
|
|
46
|
+
expect(s.cellID({}, wbs)).toBe('10@/wbs/10');
|
|
47
|
+
expect(link).toHaveBeenCalledWith('/wbs/10', 10, wbs);
|
|
48
|
+
});
|
|
49
|
+
test('methods are bound so they can be passed as contents directly', () => {
|
|
50
|
+
const s = new Supporter({ link: (to, label) => `${label}@${to}` });
|
|
51
|
+
const contents = s.cellID;
|
|
52
|
+
expect(contents({}, wbs)).toBe('10@/wbs/10');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('cellLabel / cellDescription / cellValue / cellCoreValue', () => {
|
|
57
|
+
const s = new Supporter({ empty: '-' });
|
|
58
|
+
|
|
59
|
+
test('cellLabel returns label', () => {
|
|
60
|
+
expect(s.cellLabel({}, wbs)).toBe('b');
|
|
61
|
+
});
|
|
62
|
+
test('cellDescription returns description or empty mark', () => {
|
|
63
|
+
expect(s.cellDescription({}, wbs)).toBe('desc-b');
|
|
64
|
+
expect(s.cellDescription({}, wp)).toBe('-');
|
|
65
|
+
});
|
|
66
|
+
test('cellValue reads data[key]', () => {
|
|
67
|
+
expect(s.cellValue('label')({}, wp)).toBe('c');
|
|
68
|
+
expect(s.cellValue('nothing')({}, wp)).toBe('-');
|
|
69
|
+
});
|
|
70
|
+
test('cellCoreValue reads data._core[key]', () => {
|
|
71
|
+
expect(s.cellCoreValue('physical_name')({}, wp)).toBe('T_C');
|
|
72
|
+
expect(s.cellCoreValue('physical_name')({}, wbs)).toBe('-');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('cellFlag', () => {
|
|
77
|
+
test('shows ○ for true and -- otherwise, looking in data then _core', () => {
|
|
78
|
+
const s = new Supporter();
|
|
79
|
+
const f = s.cellFlag('use');
|
|
80
|
+
expect(f({}, wp)).toBe('○');
|
|
81
|
+
expect(f({}, wbs)).toBe('--');
|
|
82
|
+
expect(f({}, { use: true })).toBe('○');
|
|
83
|
+
expect(f({}, { use: 'yes' })).toBe('--');
|
|
84
|
+
});
|
|
85
|
+
test('marks can be customized globally and per cell', () => {
|
|
86
|
+
const s = new Supporter({ marks: { true: 'Y', false: 'N' } });
|
|
87
|
+
expect(s.cellFlag('use')({}, wp)).toBe('Y');
|
|
88
|
+
expect(s.cellFlag('use', { false: '×' })({}, wbs)).toBe('×');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('cellByType', () => {
|
|
93
|
+
test('switches contents by node type and falls back to cellLabel', () => {
|
|
94
|
+
const s = new Supporter();
|
|
95
|
+
const f = s.cellByType({ DB_TABLE: s.cellCoreValue('physical_name') });
|
|
96
|
+
expect(f({}, wp)).toBe('T_C');
|
|
97
|
+
expect(f({}, wbs)).toBe('b');
|
|
98
|
+
});
|
|
99
|
+
test('custom fallback is used for unmatched types', () => {
|
|
100
|
+
const s = new Supporter();
|
|
101
|
+
const f = s.cellByType({}, () => '---');
|
|
102
|
+
expect(f({}, wbs)).toBe('---');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('withMargin', () => {
|
|
107
|
+
test('wraps contents with a level-based indent', () => {
|
|
108
|
+
const s = new Supporter();
|
|
109
|
+
const el = s.withMargin(s.cellLabel)({}, wp, 0);
|
|
110
|
+
const [margin, body] = el.props.children;
|
|
111
|
+
|
|
112
|
+
expect(margin.props.children).toBe(' ');
|
|
113
|
+
expect(body.props.children).toBe('c');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(node -e \"console.log\\(require\\('./node_modules/react/package.json'\\).version\\)\")",
|
|
5
|
-
"Bash(npm install *)",
|
|
6
|
-
"Bash(npm test *)",
|
|
7
|
-
"Bash(git stash *)",
|
|
8
|
-
"Bash(npm view *)",
|
|
9
|
-
"Bash(npx react-scripts *)",
|
|
10
|
-
"Bash(SKIP_PREFLIGHT_CHECK=true CI=true npx react-scripts test --watchAll=false)",
|
|
11
|
-
"Bash(SKIP_PREFLIGHT_CHECK=true CI=true npx react-scripts build)",
|
|
12
|
-
"Bash(SKIP_PREFLIGHT_CHECK=true BROWSER=none npx react-scripts start)",
|
|
13
|
-
"Bash(kill %1)",
|
|
14
|
-
"Bash(wait)"
|
|
15
|
-
]
|
|
16
|
-
}
|
|
17
|
-
}
|