@pie-lib/graphing-solution-set 3.1.1-next.0 → 3.2.0-next.0
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/lib/axis/arrow.js +0 -3
- package/lib/axis/arrow.js.map +1 -1
- package/lib/axis/axes.js +0 -16
- package/lib/axis/axes.js.map +1 -1
- package/lib/axis/index.js +0 -7
- package/lib/axis/index.js.map +1 -1
- package/lib/bg.js +0 -6
- package/lib/bg.js.map +1 -1
- package/lib/container/actions.js +0 -1
- package/lib/container/actions.js.map +1 -1
- package/lib/container/index.js +4 -10
- package/lib/container/index.js.map +1 -1
- package/lib/container/marks.js +0 -2
- package/lib/container/marks.js.map +1 -1
- package/lib/container/middleware.js +0 -1
- package/lib/container/middleware.js.map +1 -1
- package/lib/container/reducer.js +0 -1
- package/lib/container/reducer.js.map +1 -1
- package/lib/coordinates-label.js +0 -11
- package/lib/coordinates-label.js.map +1 -1
- package/lib/graph-with-controls.js +3 -22
- package/lib/graph-with-controls.js.map +1 -1
- package/lib/graph.js +7 -26
- package/lib/graph.js.map +1 -1
- package/lib/grid-setup.js +0 -11
- package/lib/grid-setup.js.map +1 -1
- package/lib/grid.js +0 -22
- package/lib/grid.js.map +1 -1
- package/lib/index.js +0 -7
- package/lib/index.js.map +1 -1
- package/lib/labels.js +0 -13
- package/lib/labels.js.map +1 -1
- package/lib/mark-label.js +0 -16
- package/lib/mark-label.js.map +1 -1
- package/lib/toggle-bar.js +0 -17
- package/lib/toggle-bar.js.map +1 -1
- package/lib/tool-menu.js +0 -3
- package/lib/tool-menu.js.map +1 -1
- package/lib/tools/index.js +0 -1
- package/lib/tools/index.js.map +1 -1
- package/lib/tools/line/component.js +0 -13
- package/lib/tools/line/component.js.map +1 -1
- package/lib/tools/line/index.js +0 -1
- package/lib/tools/line/index.js.map +1 -1
- package/lib/tools/polygon/component.js +5 -25
- package/lib/tools/polygon/component.js.map +1 -1
- package/lib/tools/polygon/index.js +0 -12
- package/lib/tools/polygon/index.js.map +1 -1
- package/lib/tools/polygon/line.js +0 -16
- package/lib/tools/polygon/line.js.map +1 -1
- package/lib/tools/polygon/polygon.js +0 -19
- package/lib/tools/polygon/polygon.js.map +1 -1
- package/lib/tools/shared/arrow-head.js +0 -3
- package/lib/tools/shared/arrow-head.js.map +1 -1
- package/lib/tools/shared/line/index.js +7 -22
- package/lib/tools/shared/line/index.js.map +1 -1
- package/lib/tools/shared/line/line-path.js +0 -16
- package/lib/tools/shared/line/line-path.js.map +1 -1
- package/lib/tools/shared/line/with-root-edge.js +0 -11
- package/lib/tools/shared/line/with-root-edge.js.map +1 -1
- package/lib/tools/shared/point/arrow-point.js +2 -5
- package/lib/tools/shared/point/arrow-point.js.map +1 -1
- package/lib/tools/shared/point/arrow.js +0 -3
- package/lib/tools/shared/point/arrow.js.map +1 -1
- package/lib/tools/shared/point/base-point.js +0 -11
- package/lib/tools/shared/point/base-point.js.map +1 -1
- package/lib/tools/shared/point/index.js +0 -16
- package/lib/tools/shared/point/index.js.map +1 -1
- package/lib/tools/shared/styles.js +0 -1
- package/lib/tools/shared/styles.js.map +1 -1
- package/lib/tools/shared/types.js +0 -1
- package/lib/tools/shared/types.js.map +1 -1
- package/lib/undo-redo.js +0 -2
- package/lib/undo-redo.js.map +1 -1
- package/lib/use-debounce.js +0 -2
- package/lib/use-debounce.js.map +1 -1
- package/lib/utils.js +8 -26
- package/lib/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/__tests__/bg.test.jsx +250 -0
- package/src/__tests__/coordinates-label.test.jsx +243 -0
- package/src/__tests__/graph-with-controls.test.jsx +9 -10
- package/src/__tests__/graph.test.jsx +0 -2
- package/src/__tests__/grid-setup.test.jsx +645 -0
- package/src/__tests__/mark-label.test.jsx +1 -1
- package/src/__tests__/tool-menu.test.jsx +422 -2
- package/src/__tests__/use-debounce.test.js +1 -1
- package/src/__tests__/utils.test.js +15 -61
- package/src/axis/__tests__/axes.test.jsx +1 -1
- package/src/axis/axes.jsx +7 -21
- package/src/axis/index.js +1 -0
- package/src/bg.jsx +1 -1
- package/src/container/__tests__/actions.test.js +105 -0
- package/src/container/__tests__/index.test.jsx +227 -0
- package/src/container/__tests__/marks.test.js +172 -0
- package/src/container/__tests__/middleware.test.js +235 -0
- package/src/container/__tests__/reducer.test.js +324 -0
- package/src/container/index.jsx +3 -4
- package/src/coordinates-label.jsx +1 -7
- package/src/graph-with-controls.jsx +7 -25
- package/src/graph.jsx +3 -4
- package/src/grid-setup.jsx +1 -1
- package/src/mark-label.jsx +2 -2
- package/src/toggle-bar.jsx +8 -1
- package/src/tool-menu.jsx +1 -1
- package/src/tools/line/__tests__/component.test.jsx +1 -0
- package/src/tools/line/component.jsx +2 -2
- package/src/tools/polygon/__tests__/component.test.jsx +417 -5
- package/src/tools/polygon/__tests__/polygon.test.jsx +1 -1
- package/src/tools/polygon/component.jsx +4 -14
- package/src/tools/polygon/line.jsx +1 -1
- package/src/tools/shared/line/__tests__/index.test.jsx +460 -17
- package/src/tools/shared/line/__tests__/line-path.test.jsx +7 -4
- package/src/tools/shared/line/__tests__/with-root-edge.test.jsx +439 -14
- package/src/tools/shared/line/index.jsx +4 -6
- package/src/tools/shared/line/line-path.jsx +2 -8
- package/src/tools/shared/point/__tests__/arrow.test.jsx +469 -0
- package/src/tools/shared/point/arrow-point.jsx +2 -2
- package/src/tools/shared/point/base-point.jsx +1 -1
- package/src/tools/shared/point/index.jsx +1 -1
- package/src/undo-redo.jsx +1 -3
- package/src/use-debounce.js +1 -1
- package/src/utils.js +1 -5
- package/NEXT.CHANGELOG.json +0 -16
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { getLastAction, lastActionMiddleware } from '../middleware';
|
|
2
|
+
|
|
3
|
+
describe('lastActionMiddleware', () => {
|
|
4
|
+
describe('middleware functionality', () => {
|
|
5
|
+
it('creates middleware function', () => {
|
|
6
|
+
const middleware = lastActionMiddleware();
|
|
7
|
+
expect(typeof middleware).toBe('function');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('middleware returns a function', () => {
|
|
11
|
+
const middleware = lastActionMiddleware();
|
|
12
|
+
const next = jest.fn();
|
|
13
|
+
const result = middleware(next);
|
|
14
|
+
expect(typeof result).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('calls next with action', () => {
|
|
18
|
+
const middleware = lastActionMiddleware();
|
|
19
|
+
const next = jest.fn();
|
|
20
|
+
const dispatch = middleware(next);
|
|
21
|
+
const action = { type: 'TEST_ACTION' };
|
|
22
|
+
|
|
23
|
+
dispatch(action);
|
|
24
|
+
expect(next).toHaveBeenCalledWith(action);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns result from next', () => {
|
|
28
|
+
const middleware = lastActionMiddleware();
|
|
29
|
+
const expectedResult = { status: 'success' };
|
|
30
|
+
const next = jest.fn(() => expectedResult);
|
|
31
|
+
const dispatch = middleware(next);
|
|
32
|
+
const action = { type: 'TEST_ACTION' };
|
|
33
|
+
|
|
34
|
+
const result = dispatch(action);
|
|
35
|
+
expect(result).toBe(expectedResult);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('stores action before calling next', () => {
|
|
39
|
+
const middleware = lastActionMiddleware();
|
|
40
|
+
const next = jest.fn(() => {
|
|
41
|
+
const lastAction = getLastAction();
|
|
42
|
+
expect(lastAction).toBeDefined();
|
|
43
|
+
expect(lastAction.type).toBe('TEST_ACTION');
|
|
44
|
+
});
|
|
45
|
+
const dispatch = middleware(next);
|
|
46
|
+
const action = { type: 'TEST_ACTION' };
|
|
47
|
+
|
|
48
|
+
dispatch(action);
|
|
49
|
+
expect(next).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('getLastAction', () => {
|
|
54
|
+
it('returns null initially', () => {
|
|
55
|
+
const lastAction = getLastAction();
|
|
56
|
+
expect(lastAction).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns last dispatched action', () => {
|
|
60
|
+
const middleware = lastActionMiddleware();
|
|
61
|
+
const next = jest.fn();
|
|
62
|
+
const dispatch = middleware(next);
|
|
63
|
+
const action = { type: 'MY_ACTION', payload: 'test' };
|
|
64
|
+
|
|
65
|
+
dispatch(action);
|
|
66
|
+
const lastAction = getLastAction();
|
|
67
|
+
|
|
68
|
+
expect(lastAction).toEqual(action);
|
|
69
|
+
expect(lastAction.type).toBe('MY_ACTION');
|
|
70
|
+
expect(lastAction.payload).toBe('test');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updates when new action is dispatched', () => {
|
|
74
|
+
const middleware = lastActionMiddleware();
|
|
75
|
+
const next = jest.fn();
|
|
76
|
+
const dispatch = middleware(next);
|
|
77
|
+
|
|
78
|
+
const action1 = { type: 'ACTION_1' };
|
|
79
|
+
dispatch(action1);
|
|
80
|
+
expect(getLastAction()).toEqual(action1);
|
|
81
|
+
|
|
82
|
+
const action2 = { type: 'ACTION_2' };
|
|
83
|
+
dispatch(action2);
|
|
84
|
+
expect(getLastAction()).toEqual(action2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('stores action with payload', () => {
|
|
88
|
+
const middleware = lastActionMiddleware();
|
|
89
|
+
const next = jest.fn();
|
|
90
|
+
const dispatch = middleware(next);
|
|
91
|
+
const action = {
|
|
92
|
+
type: 'CHANGE_MARKS',
|
|
93
|
+
marks: [{ id: 1, type: 'point' }],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
dispatch(action);
|
|
97
|
+
const lastAction = getLastAction();
|
|
98
|
+
|
|
99
|
+
expect(lastAction.marks).toEqual(action.marks);
|
|
100
|
+
expect(lastAction.marks.length).toBe(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('stores action with complex payload', () => {
|
|
104
|
+
const middleware = lastActionMiddleware();
|
|
105
|
+
const next = jest.fn();
|
|
106
|
+
const dispatch = middleware(next);
|
|
107
|
+
const action = {
|
|
108
|
+
type: 'COMPLEX_ACTION',
|
|
109
|
+
data: {
|
|
110
|
+
nested: {
|
|
111
|
+
value: 123,
|
|
112
|
+
array: [1, 2, 3],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
dispatch(action);
|
|
118
|
+
const lastAction = getLastAction();
|
|
119
|
+
|
|
120
|
+
expect(lastAction.data.nested.value).toBe(123);
|
|
121
|
+
expect(lastAction.data.nested.array).toEqual([1, 2, 3]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('action storage', () => {
|
|
126
|
+
it('overwrites previous action', () => {
|
|
127
|
+
const middleware = lastActionMiddleware();
|
|
128
|
+
const next = jest.fn();
|
|
129
|
+
const dispatch = middleware(next);
|
|
130
|
+
|
|
131
|
+
dispatch({ type: 'FIRST' });
|
|
132
|
+
dispatch({ type: 'SECOND' });
|
|
133
|
+
dispatch({ type: 'THIRD' });
|
|
134
|
+
|
|
135
|
+
const lastAction = getLastAction();
|
|
136
|
+
expect(lastAction.type).toBe('THIRD');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('stores reference to action object', () => {
|
|
140
|
+
const middleware = lastActionMiddleware();
|
|
141
|
+
const next = jest.fn();
|
|
142
|
+
const dispatch = middleware(next);
|
|
143
|
+
const action = { type: 'TEST', data: { value: 1 } };
|
|
144
|
+
|
|
145
|
+
dispatch(action);
|
|
146
|
+
const lastAction = getLastAction();
|
|
147
|
+
|
|
148
|
+
// Modify original action
|
|
149
|
+
action.data.value = 2;
|
|
150
|
+
|
|
151
|
+
// Last action should reflect the change (same reference)
|
|
152
|
+
expect(lastAction.data.value).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('integration scenarios', () => {
|
|
157
|
+
it('works with multiple middleware calls', () => {
|
|
158
|
+
const middleware = lastActionMiddleware();
|
|
159
|
+
const next1 = jest.fn();
|
|
160
|
+
const next2 = jest.fn();
|
|
161
|
+
|
|
162
|
+
const dispatch1 = middleware(next1);
|
|
163
|
+
const dispatch2 = middleware(next2);
|
|
164
|
+
|
|
165
|
+
dispatch1({ type: 'ACTION_1' });
|
|
166
|
+
expect(getLastAction().type).toBe('ACTION_1');
|
|
167
|
+
|
|
168
|
+
dispatch2({ type: 'ACTION_2' });
|
|
169
|
+
expect(getLastAction().type).toBe('ACTION_2');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('preserves action types', () => {
|
|
173
|
+
const middleware = lastActionMiddleware();
|
|
174
|
+
const next = jest.fn();
|
|
175
|
+
const dispatch = middleware(next);
|
|
176
|
+
|
|
177
|
+
const actionTypes = ['ADD_MARK', 'CHANGE_MARKS', 'DELETE_MARK', 'UPDATE_MARK'];
|
|
178
|
+
|
|
179
|
+
actionTypes.forEach((type) => {
|
|
180
|
+
dispatch({ type });
|
|
181
|
+
expect(getLastAction().type).toBe(type);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('handles actions without type', () => {
|
|
186
|
+
const middleware = lastActionMiddleware();
|
|
187
|
+
const next = jest.fn();
|
|
188
|
+
const dispatch = middleware(next);
|
|
189
|
+
const action = { payload: 'data' };
|
|
190
|
+
|
|
191
|
+
dispatch(action);
|
|
192
|
+
const lastAction = getLastAction();
|
|
193
|
+
|
|
194
|
+
expect(lastAction).toEqual(action);
|
|
195
|
+
expect(lastAction.type).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('handles empty action object', () => {
|
|
199
|
+
const middleware = lastActionMiddleware();
|
|
200
|
+
const next = jest.fn();
|
|
201
|
+
const dispatch = middleware(next);
|
|
202
|
+
const action = {};
|
|
203
|
+
|
|
204
|
+
dispatch(action);
|
|
205
|
+
const lastAction = getLastAction();
|
|
206
|
+
|
|
207
|
+
expect(lastAction).toEqual(action);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('error handling', () => {
|
|
212
|
+
it('stores action even if next throws error', () => {
|
|
213
|
+
const middleware = lastActionMiddleware();
|
|
214
|
+
const next = jest.fn(() => {
|
|
215
|
+
throw new Error('Next error');
|
|
216
|
+
});
|
|
217
|
+
const dispatch = middleware(next);
|
|
218
|
+
const action = { type: 'ERROR_ACTION' };
|
|
219
|
+
|
|
220
|
+
expect(() => dispatch(action)).toThrow('Next error');
|
|
221
|
+
expect(getLastAction()).toEqual(action);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('propagates errors from next', () => {
|
|
225
|
+
const middleware = lastActionMiddleware();
|
|
226
|
+
const errorMessage = 'Test error';
|
|
227
|
+
const next = jest.fn(() => {
|
|
228
|
+
throw new Error(errorMessage);
|
|
229
|
+
});
|
|
230
|
+
const dispatch = middleware(next);
|
|
231
|
+
|
|
232
|
+
expect(() => dispatch({ type: 'TEST' })).toThrow(errorMessage);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import reducer from '../reducer';
|
|
2
|
+
import { changeMarks } from '../actions';
|
|
3
|
+
|
|
4
|
+
describe('container reducer', () => {
|
|
5
|
+
describe('reducer creation', () => {
|
|
6
|
+
it('creates a reducer function', () => {
|
|
7
|
+
const rootReducer = reducer();
|
|
8
|
+
expect(typeof rootReducer).toBe('function');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns an object with marks property', () => {
|
|
12
|
+
const rootReducer = reducer();
|
|
13
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
14
|
+
expect(state).toHaveProperty('marks');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('wraps marks reducer with undoable', () => {
|
|
18
|
+
const rootReducer = reducer();
|
|
19
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
20
|
+
|
|
21
|
+
expect(state.marks).toHaveProperty('past');
|
|
22
|
+
expect(state.marks).toHaveProperty('present');
|
|
23
|
+
expect(state.marks).toHaveProperty('future');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('initial state', () => {
|
|
28
|
+
it('initializes with empty past', () => {
|
|
29
|
+
const rootReducer = reducer();
|
|
30
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
31
|
+
|
|
32
|
+
expect(state.marks.past).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('initializes with empty present', () => {
|
|
36
|
+
const rootReducer = reducer();
|
|
37
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
38
|
+
|
|
39
|
+
expect(state.marks.present).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('initializes with empty future', () => {
|
|
43
|
+
const rootReducer = reducer();
|
|
44
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
45
|
+
|
|
46
|
+
expect(state.marks.future).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('CHANGE_MARKS action', () => {
|
|
51
|
+
it('updates present with new marks', () => {
|
|
52
|
+
const rootReducer = reducer();
|
|
53
|
+
const newMarks = [{ id: 1, type: 'point', x: 5, y: 10 }];
|
|
54
|
+
const action = changeMarks(newMarks);
|
|
55
|
+
|
|
56
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
57
|
+
state = rootReducer(state, action);
|
|
58
|
+
|
|
59
|
+
expect(state.marks.present).toEqual(newMarks);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('adds previous state to past', () => {
|
|
63
|
+
const rootReducer = reducer();
|
|
64
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
65
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
66
|
+
|
|
67
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
68
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
69
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
70
|
+
|
|
71
|
+
expect(state.marks.past.length).toBeGreaterThan(0);
|
|
72
|
+
expect(state.marks.present).toEqual(secondMarks);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('clears future on new action', () => {
|
|
76
|
+
const rootReducer = reducer();
|
|
77
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
78
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
79
|
+
const thirdMarks = [{ id: 3, type: 'circle' }];
|
|
80
|
+
|
|
81
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
82
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
83
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
84
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
85
|
+
|
|
86
|
+
expect(state.marks.future.length).toBeGreaterThan(0);
|
|
87
|
+
|
|
88
|
+
state = rootReducer(state, changeMarks(thirdMarks));
|
|
89
|
+
expect(state.marks.future).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles empty marks array', () => {
|
|
93
|
+
const rootReducer = reducer();
|
|
94
|
+
const action = changeMarks([]);
|
|
95
|
+
|
|
96
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
97
|
+
state = rootReducer(state, action);
|
|
98
|
+
|
|
99
|
+
expect(state.marks.present).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('handles multiple marks', () => {
|
|
103
|
+
const rootReducer = reducer();
|
|
104
|
+
const marks = [
|
|
105
|
+
{ id: 1, type: 'point', x: 1, y: 2 },
|
|
106
|
+
{ id: 2, type: 'line', from: { x: 0, y: 0 }, to: { x: 5, y: 5 } },
|
|
107
|
+
{ id: 3, type: 'circle', center: { x: 3, y: 3 }, radius: 2 },
|
|
108
|
+
];
|
|
109
|
+
const action = changeMarks(marks);
|
|
110
|
+
|
|
111
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
112
|
+
state = rootReducer(state, action);
|
|
113
|
+
|
|
114
|
+
expect(state.marks.present).toEqual(marks);
|
|
115
|
+
expect(state.marks.present.length).toBe(3);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('undo functionality', () => {
|
|
120
|
+
it('supports undo action', () => {
|
|
121
|
+
const rootReducer = reducer();
|
|
122
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
123
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
124
|
+
|
|
125
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
126
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
127
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
128
|
+
|
|
129
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
130
|
+
|
|
131
|
+
expect(state.marks.present).toEqual(firstMarks);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('moves present to future on undo', () => {
|
|
135
|
+
const rootReducer = reducer();
|
|
136
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
137
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
138
|
+
|
|
139
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
140
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
141
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
142
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
143
|
+
|
|
144
|
+
expect(state.marks.future.length).toBeGreaterThan(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('handles multiple undos', () => {
|
|
148
|
+
const rootReducer = reducer();
|
|
149
|
+
const marks1 = [{ id: 1 }];
|
|
150
|
+
const marks2 = [{ id: 2 }];
|
|
151
|
+
const marks3 = [{ id: 3 }];
|
|
152
|
+
|
|
153
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
154
|
+
state = rootReducer(state, changeMarks(marks1));
|
|
155
|
+
state = rootReducer(state, changeMarks(marks2));
|
|
156
|
+
state = rootReducer(state, changeMarks(marks3));
|
|
157
|
+
|
|
158
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
159
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
160
|
+
|
|
161
|
+
expect(state.marks.present).toEqual(marks1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('does not undo beyond initial state', () => {
|
|
165
|
+
const rootReducer = reducer();
|
|
166
|
+
const marks = [{ id: 1, type: 'point' }];
|
|
167
|
+
|
|
168
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
169
|
+
state = rootReducer(state, changeMarks(marks));
|
|
170
|
+
|
|
171
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
172
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
173
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
174
|
+
|
|
175
|
+
expect(state.marks.present).toEqual([]);
|
|
176
|
+
expect(state.marks.past).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('redo functionality', () => {
|
|
181
|
+
it('supports redo action', () => {
|
|
182
|
+
const rootReducer = reducer();
|
|
183
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
184
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
185
|
+
|
|
186
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
187
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
188
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
189
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
190
|
+
|
|
191
|
+
// Redo
|
|
192
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
193
|
+
|
|
194
|
+
expect(state.marks.present).toEqual(secondMarks);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('moves future to past on redo', () => {
|
|
198
|
+
const rootReducer = reducer();
|
|
199
|
+
const firstMarks = [{ id: 1, type: 'point' }];
|
|
200
|
+
const secondMarks = [{ id: 2, type: 'line' }];
|
|
201
|
+
|
|
202
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
203
|
+
state = rootReducer(state, changeMarks(firstMarks));
|
|
204
|
+
state = rootReducer(state, changeMarks(secondMarks));
|
|
205
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
206
|
+
|
|
207
|
+
const futureBeforeRedo = state.marks.future.length;
|
|
208
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
209
|
+
|
|
210
|
+
expect(state.marks.future.length).toBe(futureBeforeRedo - 1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles multiple redos', () => {
|
|
214
|
+
const rootReducer = reducer();
|
|
215
|
+
const marks1 = [{ id: 1 }];
|
|
216
|
+
const marks2 = [{ id: 2 }];
|
|
217
|
+
const marks3 = [{ id: 3 }];
|
|
218
|
+
|
|
219
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
220
|
+
state = rootReducer(state, changeMarks(marks1));
|
|
221
|
+
state = rootReducer(state, changeMarks(marks2));
|
|
222
|
+
state = rootReducer(state, changeMarks(marks3));
|
|
223
|
+
|
|
224
|
+
// undo twice, then redo twice
|
|
225
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
226
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
227
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
228
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
229
|
+
|
|
230
|
+
expect(state.marks.present).toEqual(marks3);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('does not redo beyond last state', () => {
|
|
234
|
+
const rootReducer = reducer();
|
|
235
|
+
const marks = [{ id: 1, type: 'point' }];
|
|
236
|
+
|
|
237
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
238
|
+
state = rootReducer(state, changeMarks(marks));
|
|
239
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
240
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
241
|
+
|
|
242
|
+
// try to redo beyond the end
|
|
243
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
244
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' });
|
|
245
|
+
|
|
246
|
+
expect(state.marks.present).toEqual(marks);
|
|
247
|
+
expect(state.marks.future).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('state structure', () => {
|
|
252
|
+
it('maintains correct undoable structure', () => {
|
|
253
|
+
const rootReducer = reducer();
|
|
254
|
+
const state = rootReducer(undefined, { type: 'INIT' });
|
|
255
|
+
|
|
256
|
+
expect(state.marks).toHaveProperty('past');
|
|
257
|
+
expect(state.marks).toHaveProperty('present');
|
|
258
|
+
expect(state.marks).toHaveProperty('future');
|
|
259
|
+
expect(Array.isArray(state.marks.past)).toBe(true);
|
|
260
|
+
expect(Array.isArray(state.marks.future)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('preserves mark properties through undo/redo', () => {
|
|
264
|
+
const rootReducer = reducer();
|
|
265
|
+
const marks = [
|
|
266
|
+
{
|
|
267
|
+
id: 1,
|
|
268
|
+
type: 'point',
|
|
269
|
+
x: 10,
|
|
270
|
+
y: 20,
|
|
271
|
+
label: 'A',
|
|
272
|
+
correctness: { value: 'correct' },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
let state = rootReducer(undefined, { type: 'INIT' });
|
|
277
|
+
state = rootReducer(state, changeMarks(marks));
|
|
278
|
+
state = rootReducer(state, changeMarks([]));
|
|
279
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
280
|
+
|
|
281
|
+
expect(state.marks.present).toEqual(marks);
|
|
282
|
+
expect(state.marks.present[0]).toBeDefined();
|
|
283
|
+
if (state.marks.present[0]) {
|
|
284
|
+
expect(state.marks.present[0]).toEqual(marks[0]);
|
|
285
|
+
expect(state.marks.present[0].correctness).toBeDefined();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('integration', () => {
|
|
291
|
+
it('handles complex undo/redo sequences', () => {
|
|
292
|
+
const rootReducer = reducer();
|
|
293
|
+
const marks1 = [{ id: 1 }];
|
|
294
|
+
const marks2 = [{ id: 2 }];
|
|
295
|
+
const marks3 = [{ id: 3 }];
|
|
296
|
+
const marks4 = [{ id: 4 }];
|
|
297
|
+
|
|
298
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
299
|
+
state = rootReducer(state, changeMarks(marks1));
|
|
300
|
+
state = rootReducer(state, changeMarks(marks2));
|
|
301
|
+
state = rootReducer(state, changeMarks(marks3));
|
|
302
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' }); // back to marks2
|
|
303
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' }); // back to marks1
|
|
304
|
+
state = rootReducer(state, { type: '@@redux-undo/REDO' }); // forward to marks2
|
|
305
|
+
state = rootReducer(state, changeMarks(marks4));
|
|
306
|
+
|
|
307
|
+
expect(state.marks.present).toEqual(marks4);
|
|
308
|
+
expect(state.marks.future).toEqual([]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('works with empty state transitions', () => {
|
|
312
|
+
const rootReducer = reducer();
|
|
313
|
+
const marks = [{ id: 1 }];
|
|
314
|
+
|
|
315
|
+
let state = rootReducer(undefined, { type: '@@INIT' });
|
|
316
|
+
state = rootReducer(state, changeMarks([]));
|
|
317
|
+
state = rootReducer(state, changeMarks(marks));
|
|
318
|
+
state = rootReducer(state, changeMarks([]));
|
|
319
|
+
state = rootReducer(state, { type: '@@redux-undo/UNDO' });
|
|
320
|
+
|
|
321
|
+
expect(state.marks.present).toEqual(marks);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
package/src/container/index.jsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { connect } from 'react-redux';
|
|
1
|
+
import { connect, Provider } from 'react-redux';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Provider } from 'react-redux';
|
|
4
3
|
import { applyMiddleware, createStore } from 'redux';
|
|
5
4
|
import reducer from './reducer';
|
|
6
5
|
import { changeMarks } from './actions';
|
|
7
6
|
import PropTypes from 'prop-types';
|
|
8
|
-
import isEqual from 'lodash
|
|
7
|
+
import { isEqual } from 'lodash-es';
|
|
9
8
|
import { ActionCreators } from 'redux-undo';
|
|
10
9
|
import GraphWithControls from '../graph-with-controls';
|
|
11
|
-
import {
|
|
10
|
+
import { getLastAction, lastActionMiddleware } from './middleware';
|
|
12
11
|
|
|
13
12
|
const mapStateToProps = (s) => ({
|
|
14
13
|
marks: s.marks.present,
|
|
@@ -50,13 +50,7 @@ export const CoordinatesLabel = ({ x, y, graphProps }) => {
|
|
|
50
50
|
...labelPosition,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
return
|
|
54
|
-
<StyledInputBase
|
|
55
|
-
style={style}
|
|
56
|
-
value={label}
|
|
57
|
-
inputProps={{ ariaLabel: 'naked' }}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
53
|
+
return <StyledInputBase style={style} value={label} inputProps={{ ariaLabel: 'naked' }} />;
|
|
60
54
|
};
|
|
61
55
|
|
|
62
56
|
CoordinatesLabel.propTypes = {
|
|
@@ -2,19 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { styled } from '@mui/material/styles';
|
|
4
4
|
import classNames from 'classnames';
|
|
5
|
-
import uniq from 'lodash
|
|
6
|
-
import isString from 'lodash/isString';
|
|
5
|
+
import { isString, uniq } from 'lodash-es';
|
|
7
6
|
import { color } from '@pie-lib/render-ui';
|
|
8
7
|
import ToolMenu from './tool-menu';
|
|
9
8
|
import Graph, { graphPropTypes } from './graph';
|
|
10
9
|
import UndoRedo from './undo-redo';
|
|
11
10
|
import { allTools, toolsArr } from './tools';
|
|
12
|
-
import {
|
|
13
|
-
Accordion,
|
|
14
|
-
AccordionDetails,
|
|
15
|
-
AccordionSummary,
|
|
16
|
-
Typography,
|
|
17
|
-
} from '@mui/material';
|
|
11
|
+
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material';
|
|
18
12
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
19
13
|
|
|
20
14
|
export const setToolbarAvailability = (toolbarTools) =>
|
|
@@ -34,8 +28,7 @@ export const filterByValidToolTypes = (backgroundMarks) =>
|
|
|
34
28
|
export const filterByVisibleToolTypes = (toolbarTools, marks) =>
|
|
35
29
|
marks.filter((bM) => !!toolbarTools.find((tool) => tool === bM.type));
|
|
36
30
|
|
|
37
|
-
const getDefaultCurrentTool = (toolType) =>
|
|
38
|
-
toolsArr.find((tool) => tool.type === toolType) || null;
|
|
31
|
+
const getDefaultCurrentTool = (toolType) => toolsArr.find((tool) => tool.type === toolType) || null;
|
|
39
32
|
|
|
40
33
|
const GraphWithControlsRoot = styled('div')(() => ({
|
|
41
34
|
display: 'flex',
|
|
@@ -133,11 +126,9 @@ export class GraphWithControls extends React.Component {
|
|
|
133
126
|
}
|
|
134
127
|
}
|
|
135
128
|
|
|
136
|
-
changeCurrentTool = (tool, tools) =>
|
|
137
|
-
this.setState({ currentTool: tools.find((t) => t.type === tool) });
|
|
129
|
+
changeCurrentTool = (tool, tools) => this.setState({ currentTool: tools.find((t) => t.type === tool) });
|
|
138
130
|
|
|
139
|
-
toggleLabelMode = () =>
|
|
140
|
-
this.setState((state) => ({ labelModeEnabled: !state.labelModeEnabled }));
|
|
131
|
+
toggleLabelMode = () => this.setState((state) => ({ labelModeEnabled: !state.labelModeEnabled }));
|
|
141
132
|
|
|
142
133
|
render() {
|
|
143
134
|
let { currentTool, labelModeEnabled } = this.state;
|
|
@@ -200,12 +191,7 @@ export class GraphWithControls extends React.Component {
|
|
|
200
191
|
/>
|
|
201
192
|
{!disabled && (
|
|
202
193
|
<UndoRedoOuterDiv>
|
|
203
|
-
<UndoRedo
|
|
204
|
-
onUndo={onUndo}
|
|
205
|
-
onRedo={onRedo}
|
|
206
|
-
onReset={onCustomReset}
|
|
207
|
-
language={language}
|
|
208
|
-
/>
|
|
194
|
+
<UndoRedo onUndo={onUndo} onRedo={onRedo} onReset={onCustomReset} language={language} />
|
|
209
195
|
</UndoRedoOuterDiv>
|
|
210
196
|
)}
|
|
211
197
|
</>
|
|
@@ -215,11 +201,7 @@ export class GraphWithControls extends React.Component {
|
|
|
215
201
|
<GraphWithControlsRoot className={classNames(className)}>
|
|
216
202
|
{!disableToolbar && (
|
|
217
203
|
<Controls>
|
|
218
|
-
{collapsibleToolbar ?
|
|
219
|
-
<Collapsible title={collapsibleToolbarTitle}>{gssActions}</Collapsible>
|
|
220
|
-
) : (
|
|
221
|
-
gssActions
|
|
222
|
-
)}
|
|
204
|
+
{collapsibleToolbar ? <Collapsible title={collapsibleToolbarTitle}>{gssActions}</Collapsible> : gssActions}
|
|
223
205
|
</Controls>
|
|
224
206
|
)}
|
|
225
207
|
|
package/src/graph.jsx
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import isEqual from 'lodash
|
|
4
|
-
import
|
|
5
|
-
import { Root, types, createGraphProps } from '@pie-lib/plot';
|
|
3
|
+
import { cloneDeep, isEqual } from 'lodash-es';
|
|
4
|
+
import { createGraphProps, Root, types } from '@pie-lib/plot';
|
|
6
5
|
import debug from 'debug';
|
|
7
6
|
|
|
8
7
|
import { Axes, AxisPropTypes } from './axis';
|
|
9
8
|
import Grid from './grid';
|
|
10
9
|
import { LabelType } from './labels';
|
|
11
10
|
import Bg from './bg';
|
|
12
|
-
import {
|
|
11
|
+
import { areArraysOfObjectsEqual, isDuplicatedMark } from './utils';
|
|
13
12
|
|
|
14
13
|
const log = debug('pie-lib:graphing-solution-set:graph');
|
|
15
14
|
|
package/src/grid-setup.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { color, InputContainer } from '@pie-lib/render-ui';
|
|
4
|
-
import { Accordion,
|
|
4
|
+
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material';
|
|
5
5
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
6
6
|
import { NumberTextFieldCustom, Toggle } from '@pie-lib/config-ui';
|
|
7
7
|
import EditableHTML from '@pie-lib/editable-html-tip-tap';
|