Skip to content

Commit 6d87a90

Browse files
MasonMJoibel
authored andcommitted
fix(ui): handle parsing errors properly in object editor (argoproj#13931)
Signed-off-by: Mason Malone <[email protected]> (cherry picked from commit fe8df34)
1 parent ebed7f9 commit 6d87a90

File tree

2 files changed

+49
-30
lines changed

2 files changed

+49
-30
lines changed

ui/src/shared/use-editable-object.test.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {createInitialState, reducer} from './use-editable-object';
1+
import {createInitialState, reducer, setObjectActionCreator} from './use-editable-object';
22

33
describe('createInitialState', () => {
44
test('without object', () => {
@@ -59,8 +59,8 @@ describe('reducer', () => {
5959
});
6060
});
6161

62-
test('setObject with string', () => {
63-
const newState = reducer(testState, {type: 'setObject', payload: 'a: 2'});
62+
test('setObject with object and string', () => {
63+
const newState = reducer(testState, {type: 'setObject', payload: {object: {a: 2}, serialization: 'a: 2'}});
6464
expect(newState).toEqual({
6565
object: {a: 2},
6666
serialization: 'a: 2',
@@ -70,8 +70,8 @@ describe('reducer', () => {
7070
});
7171
});
7272

73-
test('setObject with object', () => {
74-
const newState = reducer(testState, {type: 'setObject', payload: {a: 2}});
73+
test('setObject with only object', () => {
74+
const newState = reducer(testState, {type: 'setObject', payload: {object: {a: 2}}});
7575
expect(newState).toEqual({
7676
object: {a: 2},
7777
serialization: 'a: 2\n',
@@ -81,17 +81,6 @@ describe('reducer', () => {
8181
});
8282
});
8383

84-
test('resetObject with string', () => {
85-
const newState = reducer(testState, {type: 'resetObject', payload: 'a: 2'});
86-
expect(newState).toEqual({
87-
object: {a: 2},
88-
serialization: 'a: 2',
89-
lang: 'yaml',
90-
initialSerialization: 'a: 2',
91-
edited: false
92-
});
93-
});
94-
9584
test('resetObject with object', () => {
9685
const newState = reducer(testState, {type: 'resetObject', payload: {a: 2}});
9786
expect(newState).toEqual({
@@ -103,3 +92,23 @@ describe('reducer', () => {
10392
});
10493
});
10594
});
95+
96+
describe('setObjectActionCreator', () => {
97+
test('with object', () => {
98+
expect(setObjectActionCreator({a: 2})).toEqual({
99+
type: 'setObject',
100+
payload: {object: {a: 2}}
101+
});
102+
});
103+
104+
test('with string', () => {
105+
expect(setObjectActionCreator('a: 2')).toEqual({
106+
type: 'setObject',
107+
payload: {object: {a: 2}, serialization: 'a: 2'}
108+
});
109+
});
110+
111+
test("with string that can't be parsed", () => {
112+
expect(() => setObjectActionCreator('@')).toThrow('Plain value cannot start with reserved character @');
113+
});
114+
});

ui/src/shared/use-editable-object.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {useReducer} from 'react';
33
import {Lang, parse, stringify} from '../shared/components/object-parser';
44
import {ScopedLocalStorage} from '../shared/scoped-local-storage';
55

6-
type Action<T> = {type: 'setLang'; payload: Lang} | {type: 'setObject'; payload: string | T} | {type: 'resetObject'; payload: string | T};
6+
type SetObjectAction<T> = {type: 'setObject'; payload: {object: T; serialization?: string}};
7+
type Action<T> = {type: 'setLang'; payload: Lang} | SetObjectAction<T> | {type: 'resetObject'; payload: T};
78

89
interface State<T> {
910
/** The parsed form of the object, kept in sync with "serialization" */
@@ -24,20 +25,17 @@ const storage = new ScopedLocalStorage('object-editor');
2425
export function reducer<T>(state: State<T>, action: Action<T>) {
2526
const newState = {...state};
2627
switch (action.type) {
27-
case 'resetObject':
2828
case 'setObject':
29-
if (typeof action.payload === 'string') {
30-
newState.object = parse(action.payload);
31-
newState.serialization = action.payload;
32-
} else {
33-
newState.object = action.payload;
34-
newState.serialization = stringify(action.payload, newState.lang);
35-
}
36-
if (action.type === 'resetObject') {
37-
newState.initialSerialization = newState.serialization;
38-
}
29+
newState.object = action.payload.object;
30+
newState.serialization = action.payload.serialization ?? stringify(newState.object, newState.lang);
3931
newState.edited = newState.initialSerialization !== newState.serialization;
4032
return newState;
33+
case 'resetObject':
34+
newState.object = action.payload;
35+
newState.serialization = stringify(newState.object, newState.lang);
36+
newState.initialSerialization = newState.serialization;
37+
newState.edited = false;
38+
return newState;
4139
case 'setLang':
4240
newState.lang = action.payload;
4341
storage.setItem('lang', newState.lang, defaultLang);
@@ -61,19 +59,31 @@ export function createInitialState<T>(object?: T): State<T> {
6159
};
6260
}
6361

62+
/**
63+
* Action creator for setObject that can accept a string and parse it.
64+
* The reason the parsing logic isn't in the reducer is because we want parse
65+
* errors to be propagated to the caller.
66+
*/
67+
export function setObjectActionCreator<T>(value: string | T): SetObjectAction<T> {
68+
return {
69+
type: 'setObject',
70+
payload: typeof value === 'string' ? {object: parse<T>(value), serialization: value} : {object: value}
71+
};
72+
}
73+
6474
/**
6575
* useEditableObject is a React hook to manage the state of an object that can be serialized and edited, encapsulating the logic to
6676
* parse/stringify the object as necessary.
6777
*/
6878
export function useEditableObject<T>(object?: T): State<T> & {
6979
setObject: (value: string | T) => void;
70-
resetObject: (value: string | T) => void;
80+
resetObject: (value: T) => void;
7181
setLang: (lang: Lang) => void;
7282
} {
7383
const [state, dispatch] = useReducer(reducer<T>, object, createInitialState);
7484
return {
7585
...state,
76-
setObject: value => dispatch({type: 'setObject', payload: value}),
86+
setObject: value => dispatch(setObjectActionCreator<T>(value)),
7787
resetObject: value => dispatch({type: 'resetObject', payload: value}),
7888
setLang: value => dispatch({type: 'setLang', payload: value})
7989
};

0 commit comments

Comments
 (0)