Skip to content

Commit 04ebe56

Browse files
committed
jschema: removed 'Clear' button; added 'Reset' button
1 parent 4c5a257 commit 04ebe56

File tree

17 files changed

+277
-118
lines changed

17 files changed

+277
-118
lines changed

jschema/__tests__/jschema_initial_data.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,22 @@ describe('jschema_intial_data', () => {
466466
);
467467
expect(data).deep.eq({ b1: false, b2: null });
468468
});
469+
470+
it('Handle tuple size/items mismatch (minItems = 2; items.length = 1)', () => {
471+
const data = getJsonSchemaData({
472+
title: 'test',
473+
type: 'object',
474+
properties: {
475+
foo: {
476+
default: [1, 2],
477+
type: 'array',
478+
minItems: 2,
479+
maxItems: 2,
480+
items: [{ type: 'integer' }]
481+
}
482+
},
483+
required: ['foo']
484+
});
485+
expect(data).deep.eq({ foo: [1, null] });
486+
});
469487
});

jschema/__tests__/properties/array.test.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,20 @@ describe('Array properties', () => {
3535
checkBold(screen.getByText('ArrayProperty'), true);
3636
let inputs = screen.getAllByRole('textbox');
3737
expect(inputs.length).eq(3);
38-
const clearBtns = screen.getAllByRole('button', { name: 'Clear' });
39-
expect(clearBtns.length).eq(3);
38+
expect(screen.queryAllByRole('button', { name: 'Remove' })).toHaveLength(0);
4039
expect(component.getArguments()).deep.eq({ testProp: [null, null, null] });
4140
await fireEvent.input(inputs[0], { target: { value: 'foo' } });
4241
expect(onChange).toHaveBeenCalledWith({ testProp: ['foo', null, null] });
43-
await fireEvent.click(clearBtns[0]);
44-
expect(inputs[0]).toHaveValue('');
45-
expect(onChange).toHaveBeenCalledWith({ testProp: [null, null, null] });
4642
const addBtn = screen.getByRole('button', { name: 'Add argument to list' });
4743
expect(addBtn.disabled).toBe(false);
4844
await fireEvent.click(addBtn);
49-
expect(onChange).toHaveBeenCalledWith({ testProp: [null, null, null, null] });
45+
expect(onChange).toHaveBeenCalledWith({ testProp: ['foo', null, null, null] });
5046
expect(screen.getAllByRole('button', { name: 'Remove' }).length).eq(4);
5147
await fireEvent.click(addBtn);
5248
inputs = screen.getAllByRole('textbox');
5349
expect(inputs.length).eq(5);
5450
expect(onChange).toHaveBeenCalledWith({
55-
testProp: [null, null, null, null, null]
51+
testProp: ['foo', null, null, null, null]
5652
});
5753
expect(addBtn.disabled).toBe(true);
5854
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { fireEvent, screen } from '@testing-library/svelte';
3+
import { renderSchemaWithSingleProperty } from './property_test_utils';
4+
5+
describe('Reset properties to their default values', async () => {
6+
it('Reset object with default value on top-level property', async () => {
7+
const { component, onChange } = renderSchemaWithSingleProperty(
8+
{
9+
type: 'object',
10+
default: { key1: { key2: 'foo' } },
11+
properties: { key1: { type: 'object', properties: { key2: { type: 'string' } } } },
12+
required: ['key1']
13+
},
14+
true
15+
);
16+
expect(component.getArguments()).deep.eq({ testProp: { key1: { key2: 'foo' } } });
17+
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'bar' } });
18+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'bar' } } });
19+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
20+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'foo' } } });
21+
});
22+
23+
it('Object with default on nested object does not have the Reset button', async () => {
24+
const { component, onChange } = renderSchemaWithSingleProperty(
25+
{
26+
type: 'object',
27+
properties: {
28+
key1: {
29+
type: 'object',
30+
default: { key2: 'foo' },
31+
properties: { key2: { type: 'string' } }
32+
}
33+
},
34+
required: ['key1']
35+
},
36+
true
37+
);
38+
expect(component.getArguments()).deep.eq({ testProp: { key1: { key2: 'foo' } } });
39+
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'bar' } });
40+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'bar' } } });
41+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
42+
});
43+
44+
it('Reset tuple with default value on top-level property', async () => {
45+
const { component, onChange } = renderSchemaWithSingleProperty(
46+
{
47+
type: 'array',
48+
minItems: 2,
49+
maxItems: 2,
50+
items: [{ type: 'integer' }, { type: 'integer' }],
51+
default: [1, 2]
52+
},
53+
true
54+
);
55+
expect(component.getArguments()).deep.eq({ testProp: [1, 2] });
56+
const [input1, input2] = screen.getAllByRole('spinbutton');
57+
expect(input1).toHaveValue(1);
58+
expect(input2).toHaveValue(2);
59+
await fireEvent.input(input1, { target: { value: '3' } });
60+
expect(onChange).toHaveBeenCalledWith({ testProp: [3, 2] });
61+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
62+
expect(onChange).toHaveBeenCalledWith({ testProp: [1, 2] });
63+
});
64+
65+
it('Tuple with default on nested items does not have the Reset button', async () => {
66+
const { component, onChange } = renderSchemaWithSingleProperty(
67+
{
68+
type: 'array',
69+
minItems: 2,
70+
maxItems: 2,
71+
items: [
72+
{ type: 'integer', default: 1 },
73+
{ type: 'integer', default: 2 }
74+
]
75+
},
76+
true
77+
);
78+
expect(component.getArguments()).deep.eq({ testProp: [1, 2] });
79+
const [input1, input2] = screen.getAllByRole('spinbutton');
80+
expect(input1).toHaveValue(1);
81+
expect(input2).toHaveValue(2);
82+
await fireEvent.input(input1, { target: { value: '3' } });
83+
expect(onChange).toHaveBeenCalledWith({ testProp: [3, 2] });
84+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
85+
});
86+
87+
it('Reset array with default value on top-level property', async () => {
88+
const { component, onChange } = renderSchemaWithSingleProperty(
89+
{
90+
type: 'array',
91+
items: { type: 'string' },
92+
default: ['a', 'b']
93+
},
94+
true
95+
);
96+
expect(component.getArguments()).deep.eq({ testProp: ['a', 'b'] });
97+
const [input1, input2] = screen.getAllByRole('textbox');
98+
expect(input1).toHaveValue('a');
99+
expect(input2).toHaveValue('b');
100+
await fireEvent.input(input1, { target: { value: 'x' } });
101+
expect(onChange).toHaveBeenCalledWith({ testProp: ['x', 'b'] });
102+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
103+
expect(onChange).toHaveBeenCalledWith({ testProp: ['a', 'b'] });
104+
});
105+
106+
it('Array with default on nested items does not have the Reset button', async () => {
107+
const { component, onChange } = renderSchemaWithSingleProperty(
108+
{
109+
type: 'array',
110+
items: { type: 'string', default: 'a' },
111+
minItems: 2
112+
},
113+
true
114+
);
115+
expect(component.getArguments()).deep.eq({ testProp: ['a', 'a'] });
116+
const [input1, input2] = screen.getAllByRole('textbox');
117+
expect(input1).toHaveValue('a');
118+
expect(input2).toHaveValue('a');
119+
await fireEvent.input(input1, { target: { value: 'x' } });
120+
expect(onChange).toHaveBeenCalledWith({ testProp: ['x', 'a'] });
121+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
122+
});
123+
});

jschema/__tests__/properties/tuple.test.js

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ describe('Tuple properties', () => {
5151

5252
// verify that value has been reset to default
5353
expect(onChange).toHaveBeenCalledWith({ patch_size: [1300, 'foo', 1] });
54-
55-
expect(screen.getAllByRole('button', { name: 'Clear' }).length).eq(3);
56-
inputs = screen.getAllByRole('spinbutton');
57-
expect(inputs.length).eq(2);
58-
expect(inputs[0]).toHaveValue(1300);
59-
expect(screen.getByRole('textbox').value).eq('foo');
60-
expect(inputs[1]).toHaveValue(1);
61-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
62-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 'foo', 1] });
63-
expect(inputs[0]).toHaveValue(null);
6454
});
6555

6656
it('optional tuple without default values', async function () {
@@ -114,13 +104,6 @@ describe('Tuple properties', () => {
114104
expect(inputs[1].value).eq('');
115105

116106
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
117-
118-
await fireEvent.input(inputs[1], { target: { value: 'bar' } });
119-
expect(inputs[1].value).eq('bar');
120-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 'bar'] });
121-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[1]);
122-
expect(inputs[1].value).eq('');
123-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
124107
});
125108

126109
it('required tuple with default values', async function () {
@@ -158,10 +141,10 @@ describe('Tuple properties', () => {
158141
expect(inputs[1].value).eq('1500');
159142
expect(inputs[2].value).eq('1');
160143
expect(screen.queryAllByRole('button', { name: 'Remove tuple' }).length).eq(0);
161-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
162-
expect(inputs[0].value).eq('');
163-
expect(screen.getAllByRole('spinbutton').length).eq(3);
164-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 1500, 1] });
144+
await fireEvent.input(screen.getAllByRole('spinbutton')[0], { target: { value: 10 } });
145+
expect(onChange).toHaveBeenCalledWith({ patch_size: [10, 1500, 1] });
146+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
147+
expect(onChange).toHaveBeenCalledWith({ patch_size: [1300, 1500, 1] });
165148
});
166149

167150
it('required tuple without default values', async function () {
@@ -197,9 +180,7 @@ describe('Tuple properties', () => {
197180
await fireEvent.input(inputs[0], { target: { value: 'foo' } });
198181
expect(inputs[0].value).eq('foo');
199182
expect(onChange).toHaveBeenCalledWith({ patch_size: ['foo', null] });
200-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
201-
expect(inputs[0].value).eq('');
202-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
183+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
203184
});
204185

205186
it('nested tuple', async function () {
@@ -247,8 +228,6 @@ describe('Tuple properties', () => {
247228
expect(screen.getAllByRole('button', { name: 'Remove tuple' }).length).toEqual(2);
248229
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'foo' } });
249230
expect(onChange).toHaveBeenCalledWith({ arg_A: [[['foo']]] });
250-
await fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
251-
expect(onChange).toHaveBeenCalledWith({ arg_A: [[[null]]] });
252231
});
253232

254233
it('referenced tuple', async function () {

jschema/src/lib/components/JSchema.svelte

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@
4343
4444
function initFormManager() {
4545
if (schema) {
46-
formManager = new FormManager(schema, dispatch, propertiesToIgnore, schemaData);
46+
try {
47+
formManager = new FormManager(schema, dispatch, propertiesToIgnore, schemaData);
48+
} catch (err) {
49+
console.error(err);
50+
formManager = undefined;
51+
}
4752
} else {
4853
formManager = undefined;
4954
}
@@ -53,7 +58,7 @@
5358
{#if formManager}
5459
{#key formManager}
5560
<div id={componentId}>
56-
<ObjectProperty formElement={formManager.root} />
61+
<ObjectProperty formElement={formManager.root} isRoot={true} />
5762
</div>
5863
{/key}
5964
{/if}

jschema/src/lib/components/form_element.js

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export class BaseFormElement {
2222
*/
2323
removable;
2424
/**
25-
* The default value of the field
25+
* @type {import("../types/jschema").JSONSchemaProperty}
2626
*/
27-
default;
27+
property;
2828

2929
/**
3030
* @param {import("../types/form").BaseFormElementFields} fields
@@ -38,7 +38,7 @@ export class BaseFormElement {
3838
this.required = fields.required;
3939
this.description = fields.description;
4040
this.removable = fields.removable;
41-
this.default = fields.default;
41+
this.property = fields.property;
4242
}
4343

4444
notifyChange() {
@@ -175,6 +175,28 @@ export class ObjectFormElement extends BaseFormElement {
175175
this.children = this.children.filter((c) => c.key !== key);
176176
this.notifyChange();
177177
}
178+
179+
/**
180+
* Reset a child to its default value (if it exists)
181+
* @param {number} index
182+
*/
183+
resetChild(index) {
184+
const child = this.children[index];
185+
const defaultValue = child.property.default;
186+
if (defaultValue === undefined) {
187+
return;
188+
}
189+
const newChild = this.manager.createFormElement({
190+
key: child.key,
191+
property: child.property,
192+
required: child.required,
193+
removable: child.removable,
194+
value: defaultValue
195+
});
196+
newChild.collapsed = child.collapsed;
197+
this.children[index] = newChild;
198+
this.notifyChange();
199+
}
178200
}
179201

180202
export class ArrayFormElement extends BaseFormElement {
@@ -257,17 +279,6 @@ export class ArrayFormElement extends BaseFormElement {
257279
this.notifyChange();
258280
}
259281
}
260-
261-
/**
262-
* @param {number} index
263-
*/
264-
clearChild(index) {
265-
const child = this.children[index];
266-
if ('value' in child) {
267-
child.value = null;
268-
this.notifyChange();
269-
}
270-
}
271282
}
272283

273284
export class TupleFormElement extends BaseFormElement {
@@ -290,15 +301,20 @@ export class TupleFormElement extends BaseFormElement {
290301
let value;
291302
if (Array.isArray(this.items)) {
292303
value = this.items.map((p, i) =>
293-
getPropertyData(p, false, Array.isArray(this.default) ? this.default[i] : undefined, false)
304+
getPropertyData(
305+
p,
306+
false,
307+
Array.isArray(this.property.default) ? this.property.default[i] : undefined,
308+
false
309+
)
294310
);
295311
} else {
296312
const property = this.items;
297313
value = Array(this.size).map((i) =>
298314
getPropertyData(
299315
property,
300316
false,
301-
Array.isArray(this.default) ? this.default[i] : undefined,
317+
Array.isArray(this.property.default) ? this.property.default[i] : undefined,
302318
false
303319
)
304320
);
@@ -311,15 +327,4 @@ export class TupleFormElement extends BaseFormElement {
311327
this.children = [];
312328
this.notifyChange();
313329
}
314-
315-
/**
316-
* @param {number} index
317-
*/
318-
clearChild(index) {
319-
const child = this.children[index];
320-
if ('value' in child) {
321-
child.value = null;
322-
this.notifyChange();
323-
}
324-
}
325330
}

jschema/src/lib/components/form_manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,8 @@ export class FormManager {
329329
id: this.getUniqueId(),
330330
type: property.type || null,
331331
title: property.title || key || '',
332-
default: deepCopy(property.default),
333332
description: property.description || '',
333+
property: deepCopy(property),
334334
notifyChange: this.notifyChange
335335
};
336336
}

0 commit comments

Comments
 (0)