Skip to content

Commit f6dcd61

Browse files
authored
feat(compass-crud): use type from last array element when inserting new element to array COMPASS-6432 (#3965)
1 parent cb3371c commit f6dcd61

File tree

5 files changed

+263
-35
lines changed

5 files changed

+263
-35
lines changed

packages/compass-crud/src/components/table-view/cell-editor.tsx

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
// TODO: COMPASS-5847 Fix accessibility issues and remove lint disables.
2-
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
3-
/* eslint-disable jsx-a11y/click-events-have-key-events */
4-
/* eslint-disable jsx-a11y/no-static-element-interactions */
5-
61
import React from 'react';
72
import PropTypes from 'prop-types';
83
import type { TypeCastTypes } from 'hadron-type-checker';
9-
import TypeChecker from 'hadron-type-checker';
104
import type { Editor, Element } from 'hadron-document';
115
import type Document from 'hadron-document';
12-
import { ElementEditor as initEditors } from 'hadron-document';
6+
import {
7+
ElementEditor as initEditors,
8+
getDefaultValueForType,
9+
} from 'hadron-document';
1310
import TypesDropdown from './types-dropdown';
1411
import AddFieldButton from './add-field-button';
1512
import {
@@ -32,30 +29,6 @@ import type { GridActions, TableHeaderType } from '../../stores/grid-store';
3229
import type { CrudActions } from '../../stores/crud-store';
3330
import type { GridContext } from './document-table-view';
3431

35-
const EMPTY_TYPE: {
36-
[T in TypeCastTypes]: unknown;
37-
} = {
38-
Array: [],
39-
Object: {},
40-
Decimal128: 0,
41-
Int32: 0,
42-
Int64: 0,
43-
Double: 0,
44-
MaxKey: 0,
45-
MinKey: 0,
46-
Timestamp: 0,
47-
Date: 0,
48-
String: '',
49-
Code: '',
50-
Binary: '',
51-
ObjectId: '',
52-
BSONRegExp: '',
53-
BSONSymbol: '',
54-
Boolean: false,
55-
Undefined: undefined,
56-
Null: null,
57-
};
58-
5932
/**
6033
* BEM BASE
6134
*/
@@ -165,7 +138,7 @@ class CellEditor
165138
type = 'String';
166139
}
167140

168-
const value = TypeChecker.cast(EMPTY_TYPE[type], type);
141+
const value = getDefaultValueForType(type);
169142
this.element = parent.insertEnd(String(key), value);
170143
this.element.edit(value);
171144
} else {

packages/hadron-document/src/element.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type Document from './document';
1515
import type { TypeCastTypes } from 'hadron-type-checker';
1616
import type { ObjectId } from 'bson';
1717
import type { BSONArray, BSONObject, BSONValue } from './utils';
18+
import { getDefaultValueForType } from './utils';
1819
import { ElementEvents } from '.';
1920

2021
export const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
@@ -327,11 +328,25 @@ export class Element extends EventEmitter {
327328
* @returns The placeholder element.
328329
*/
329330
insertPlaceholder(): Element {
330-
return this.insertEnd('', '');
331+
// When adding a placeholder value to an array we default to the type
332+
// of the last value currently in the array. Otherwise empty string.
333+
const placeholderValue: BSONValue =
334+
this.currentType === 'Array' && this.elements?.lastElement
335+
? getDefaultValueForType(this.elements?.lastElement.currentType)
336+
: '';
337+
338+
return this.insertEnd('', placeholderValue);
331339
}
332340

333341
insertSiblingPlaceholder(): Element {
334-
return this.parent!.insertAfter(this, '', '')!;
342+
// When adding a sibling placeholder value to an array we default the
343+
// new values' type to the preceding element's type to hopefully make
344+
// it so folks don't have to change the type later. Otherwise empty string.
345+
const placeholderValue: BSONValue =
346+
this.parent?.currentType === 'Array'
347+
? getDefaultValueForType(this.currentType)
348+
: '';
349+
return this.parent!.insertAfter(this, '', placeholderValue)!;
335350
}
336351

337352
/**

packages/hadron-document/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Element, {
55
} from './element';
66
import ElementEditor from './editor';
77
import type { Editor } from './editor';
8+
import { getDefaultValueForType } from './utils';
89

910
export default Document;
1011
export type { Editor };
@@ -15,4 +16,5 @@ export {
1516
ElementEvents,
1617
ElementEditor,
1718
isInternalFieldPath,
19+
getDefaultValueForType,
1820
};

packages/hadron-document/src/utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import { EJSON } from 'bson';
2+
import TypeChecker from 'hadron-type-checker';
23
import type { TypeCastMap, TypeCastTypes } from 'hadron-type-checker';
34

5+
const UNCASTED_EMPTY_TYPE_VALUE: {
6+
[T in TypeCastTypes]: unknown;
7+
} = {
8+
Array: [],
9+
Object: {},
10+
Decimal128: 0,
11+
Int32: 0,
12+
Int64: 0,
13+
Double: 0,
14+
MaxKey: 0,
15+
MinKey: 0,
16+
Timestamp: 0,
17+
Date: 0,
18+
String: '',
19+
Code: '',
20+
Binary: '',
21+
ObjectId: '',
22+
BSONRegExp: '',
23+
BSONSymbol: '',
24+
Boolean: false,
25+
Undefined: undefined,
26+
Null: null,
27+
};
28+
429
const maxFourYearDate = new Date('9999-12-31T23:59:59.999Z').valueOf();
530

631
export function fieldStringLen(value: unknown): number {
@@ -85,3 +110,10 @@ function makeEJSONIdiomatic(value: EJSON.SerializableTypes): void {
85110
makeEJSONIdiomatic(entry);
86111
}
87112
}
113+
114+
/**
115+
* Returns a default value for the BSON type passed in.
116+
*/
117+
export function getDefaultValueForType(type: keyof TypeCastMap) {
118+
return TypeChecker.cast(UNCASTED_EMPTY_TYPE_VALUE[type], type);
119+
}

packages/hadron-document/test/element.test.ts

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1890,7 +1890,7 @@ describe('Element', function () {
18901890
before(function () {
18911891
element.insertPlaceholder();
18921892
});
1893-
it('array has one element', function () {
1893+
it('array has one empty string element', function () {
18941894
expect(element.at(0)?.currentKey).to.equal(0);
18951895
expect(element.at(0)?.value).to.equal('');
18961896
expect(element.elements?.size).to.equal(1);
@@ -1922,6 +1922,89 @@ describe('Element', function () {
19221922
}
19231923
});
19241924
});
1925+
context('into a number array', function () {
1926+
const doc = new Document({});
1927+
const element = new Element('emails', [25, 123], doc);
1928+
before(function () {
1929+
element.insertPlaceholder();
1930+
});
1931+
it('inserts a number type element into the end', function () {
1932+
expect(element.at(2)?.value).to.deep.equal(new Int32(0));
1933+
});
1934+
});
1935+
context('into a date array', function () {
1936+
const doc = new Document({});
1937+
const element = new Element('emails', [new Date(), new Date()], doc);
1938+
before(function () {
1939+
element.insertPlaceholder();
1940+
});
1941+
it('inserts a date type element into the end', function () {
1942+
expect(element.at(2)?.value?.toString()).to.equal(
1943+
new Date(0).toString()
1944+
);
1945+
expect(element.at(2)?.value).to.deep.equal(new Date(0));
1946+
});
1947+
});
1948+
context('into an array of arrays', function () {
1949+
const doc = new Document({});
1950+
const element = new Element(
1951+
'emails',
1952+
[
1953+
['a', 'b'],
1954+
['c', 'd'],
1955+
],
1956+
doc
1957+
);
1958+
before(function () {
1959+
element.insertPlaceholder();
1960+
});
1961+
it('inserts an array type element into the end', function () {
1962+
expect(element.elements?.size).to.equal(3);
1963+
expect(element.elements?.at(1)?.elements?.at(0)?.value).to.equal('c');
1964+
expect(element.elements?.at(2)?.currentType).to.equal('Array');
1965+
expect(element.elements?.at(2)?.elements?.size).to.equal(0);
1966+
expect(element.generateObject()).to.deep.equal([
1967+
['a', 'b'],
1968+
['c', 'd'],
1969+
[],
1970+
]);
1971+
});
1972+
});
1973+
context('into an array of objects', function () {
1974+
const doc = new Document({});
1975+
const element = new Element(
1976+
'fruits',
1977+
[
1978+
{
1979+
name: 'pineapple',
1980+
},
1981+
{
1982+
name: 'orange',
1983+
},
1984+
],
1985+
doc
1986+
);
1987+
before(function () {
1988+
element.insertPlaceholder();
1989+
});
1990+
it('inserts an object type element into the end', function () {
1991+
expect(element.elements?.size).to.equal(3);
1992+
expect(
1993+
element.elements?.at(1)?.elements?.get('name')?.value
1994+
).to.equal('orange');
1995+
expect(element.elements?.at(2)?.currentType).to.equal('Object');
1996+
expect(element.elements?.at(2)?.elements?.size).to.equal(0);
1997+
expect(element.generateObject()).to.deep.equal([
1998+
{
1999+
name: 'pineapple',
2000+
},
2001+
{
2002+
name: 'orange',
2003+
},
2004+
{},
2005+
]);
2006+
});
2007+
});
19252008
context('insert after placeholders', function () {
19262009
context('insertAfter', function () {
19272010
context('with only placeholders', function () {
@@ -2040,6 +2123,129 @@ describe('Element', function () {
20402123
});
20412124
});
20422125

2126+
describe('#insertSiblingPlaceholder', function () {
2127+
context('into an empty array', function () {
2128+
const doc = new Document({});
2129+
const element = new Element('emails', [], doc);
2130+
before(function () {
2131+
element.insertPlaceholder();
2132+
element.at(0)?.insertSiblingPlaceholder();
2133+
});
2134+
it('array has two empty string element', function () {
2135+
expect(element.at(0)?.currentKey).to.equal(0);
2136+
expect(element.at(0)?.value).to.equal('');
2137+
expect(element.at(1)?.currentKey).to.equal(1);
2138+
expect(element.at(1)?.value).to.equal('');
2139+
expect(element.elements?.size).to.equal(2);
2140+
});
2141+
it('element is modified', function () {
2142+
expect(element.at(0)?.isModified()).to.equal(true);
2143+
expect(element.at(1)?.isModified()).to.equal(true);
2144+
});
2145+
});
2146+
context('into a full array', function () {
2147+
const doc = new Document({});
2148+
const element = new Element('emails', ['item0', 'item1', 'item2'], doc);
2149+
before(function () {
2150+
element.at(2)?.insertSiblingPlaceholder();
2151+
});
2152+
it('inserts the element into the end', function () {
2153+
expect(element.at(3)?.currentKey).to.equal(3);
2154+
expect(element.at(3)?.value).to.equal('');
2155+
});
2156+
it('keeps the other elements the same', function () {
2157+
expect(element.elements?.size).to.equal(4);
2158+
for (let i = 0; i < 3; i++) {
2159+
expect(element.at(i)?.currentKey).to.equal(i);
2160+
expect(element.at(i)?.value).to.equal(`item${i}`);
2161+
}
2162+
});
2163+
it('element is modified', function () {
2164+
for (let i = 0; i < 4; i++) {
2165+
expect(element.at(i)?.isModified()).to.equal(i === 3);
2166+
}
2167+
});
2168+
});
2169+
context('into a number array', function () {
2170+
const doc = new Document({});
2171+
const element = new Element('emails', [25, 123], doc);
2172+
before(function () {
2173+
element.at(1)?.insertSiblingPlaceholder();
2174+
});
2175+
it('inserts a number type element into the end', function () {
2176+
expect(element.at(2)?.value).to.deep.equal(new Int32(0));
2177+
});
2178+
});
2179+
context('into a date array', function () {
2180+
const doc = new Document({});
2181+
const element = new Element('emails', [new Date(), new Date()], doc);
2182+
before(function () {
2183+
element.at(1)?.insertSiblingPlaceholder();
2184+
});
2185+
it('inserts a date type element into the end', function () {
2186+
expect(element.at(2)?.value).to.deep.equal(new Date(0));
2187+
});
2188+
});
2189+
context('into an array of arrays', function () {
2190+
const doc = new Document({});
2191+
const element = new Element(
2192+
'emails',
2193+
[
2194+
[1, 2],
2195+
[3, 4],
2196+
],
2197+
doc
2198+
);
2199+
before(function () {
2200+
element.at(1)?.insertSiblingPlaceholder();
2201+
});
2202+
it('inserts an array type element into the end', function () {
2203+
expect(element.elements?.size).to.equal(3);
2204+
expect(element.elements?.at(1)?.elements?.at(0)?.value).to.deep.equal(
2205+
new Int32(3)
2206+
);
2207+
expect(element.elements?.at(2)?.currentType).to.equal('Array');
2208+
expect(element.elements?.at(2)?.elements?.size).to.equal(0);
2209+
expect(element.elements?.at(2)?.generateObject()).to.deep.equal([]);
2210+
});
2211+
context('into an array of objects', function () {
2212+
const doc = new Document({});
2213+
const element = new Element(
2214+
'fruits',
2215+
[
2216+
{
2217+
name: 'pineapple',
2218+
},
2219+
{
2220+
name: 'orange',
2221+
},
2222+
],
2223+
doc
2224+
);
2225+
before(function () {
2226+
element.at(1)?.insertSiblingPlaceholder();
2227+
});
2228+
it('inserts an object type element into the end', function () {
2229+
expect(element.elements?.size).to.equal(3);
2230+
expect(
2231+
element.elements?.at(1)?.elements?.get('name')?.value
2232+
).to.equal('orange');
2233+
expect(element.elements?.at(2)?.currentType).to.equal('Object');
2234+
expect(element.elements?.at(2)?.elements?.size).to.equal(0);
2235+
expect(element.generateObject()).to.deep.equal([
2236+
{
2237+
name: 'pineapple',
2238+
},
2239+
{
2240+
name: 'orange',
2241+
},
2242+
{},
2243+
]);
2244+
});
2245+
});
2246+
});
2247+
});
2248+
20432249
describe('#remove', function () {
20442250
context('element to be removed is top-level', function () {
20452251
context('and is added', function () {

0 commit comments

Comments
 (0)