Skip to content

Commit 33f971a

Browse files
gribnoysupAnemy
andauthored
chore(compass-crud, compass-components): Move Document editor to compass-components (#2915)
* chore(hadron-type-checker): Remove BSONMap mapper This type is never returned from the driver and changing type to BSONMap is not supported by hadron-document or any of the editors in the app * fix(hadron-document): Use correct insertEnd method for placeholder; Fix added / removed event bubbling; Pass element to all element events * chore(compass-components): Add editable document view * chore(compass-crud, compass-aggregations): Replace CRUD document with compass-components one * fix(hadron-document): Copy elements list before iterating Calling cancel on elements will remove them from LinkedList and so without cloning items in the iterator will shift before we can actually "cancel" them * fix(compass-components): Do not pass mouse down related event callbacks to inputs * chore(compass-components): Misc element editor fixes - Use css to toggle element actions (hooks are too slow for this use-case) - Limit min height of the text area by max height - Add spacing for type selector - Move Element to its own file * chore(compass-components): Make min width for textarea bigger Co-authored-by: Rhys <[email protected]> * chore(compass-crud): Fix failing tests * chore(compass-e2e-tests): Add a bnunch of testids and fix failing e2e tests * chore(compass-crud): Fix the testid selector in the test Co-authored-by: Rhys <[email protected]>
1 parent bfc9196 commit 33f971a

File tree

71 files changed

+1578
-3383
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+1578
-3383
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-aggregations/src/components/input-preview/input-preview.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ class InputPreview extends Component {
2828
<Document
2929
doc={new HadronDocument(doc)}
3030
editable={false}
31-
tz="UTC"
3231
key={i} />);
3332
});
3433
return (

packages/compass-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@react-aria/utils": "^3.9.0",
7171
"@react-stately/tooltip": "^3.0.5",
7272
"bson": "^4.6.1",
73+
"hadron-document": "^7.8.0",
7374
"hadron-type-checker": "^6.7.0",
7475
"react-window": "^1.8.6"
7576
},

packages/compass-components/src/components/bson-value.tsx

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { useMemo } from 'react';
2-
import type { TypeCastMap } from 'hadron-type-checker';
2+
import type { TypeCastMap, TypeCastTypes } from 'hadron-type-checker';
33
import { Binary } from 'bson';
44
import type { DBRef } from 'bson';
5-
import { css } from '..';
5+
import { css, cx } from '@leafygreen-ui/emotion';
66

77
type ValueProps =
88
| {
@@ -22,10 +22,47 @@ type PropsByValueType<V extends ValueTypes> = Omit<
2222
'type'
2323
>;
2424

25-
function getClassName(
26-
type: ValueTypes | Lowercase<ValueTypes> | string
27-
): string {
28-
return `element-value element-value-is-${type.toLowerCase()}`;
25+
export const VALUE_COLOR_BY_TYPE: Record<
26+
Extract<
27+
TypeCastTypes,
28+
| 'Int32'
29+
| 'Double'
30+
| 'Decimal128'
31+
| 'Date'
32+
| 'Boolean'
33+
| 'String'
34+
| 'ObjectId'
35+
>,
36+
string
37+
> = {
38+
Int32: '#145a32',
39+
Double: '#1e8449',
40+
Decimal128: '#229954',
41+
Date: 'firebrick',
42+
Boolean: 'purple',
43+
String: 'steelblue',
44+
ObjectId: 'orangered',
45+
};
46+
47+
export function hasCustomColor(
48+
type: ValueTypes | string
49+
): type is keyof typeof VALUE_COLOR_BY_TYPE {
50+
return type in VALUE_COLOR_BY_TYPE;
51+
}
52+
53+
const bsonValue = css({
54+
whiteSpace: 'pre-wrap',
55+
});
56+
57+
function getStyles(type: ValueTypes | string): string {
58+
return cx(
59+
bsonValue,
60+
hasCustomColor(type) &&
61+
css({
62+
color: VALUE_COLOR_BY_TYPE[type],
63+
}),
64+
`element-value element-value-is-${type.toLowerCase()}`
65+
);
2966
}
3067

3168
const nonSelectable = css({
@@ -40,7 +77,7 @@ export const ObjectIdValue: React.FunctionComponent<
4077
}, [value]);
4178

4279
return (
43-
<div className={getClassName('objectid')} title={stringifiedValue}>
80+
<div className={getStyles('ObjectId')} title={stringifiedValue}>
4481
<span className={nonSelectable}>ObjectId(&apos;</span>
4582
{stringifiedValue}
4683
<span className={nonSelectable}>&apos;)</span>
@@ -69,7 +106,7 @@ export const BinaryValue: React.FunctionComponent<PropsByValueType<'Binary'>> =
69106
}, [value]);
70107

71108
return (
72-
<div className={getClassName('binary')} title={title ?? stringifiedValue}>
109+
<div className={getStyles('Binary')} title={title ?? stringifiedValue}>
73110
{stringifiedValue}
74111
</div>
75112
);
@@ -85,7 +122,7 @@ export const CodeValue: React.FunctionComponent<PropsByValueType<'Code'>> = ({
85122
}, [value.code, value.scope]);
86123

87124
return (
88-
<div className={getClassName('code')} title={stringifiedValue}>
125+
<div className={getStyles('Code')} title={stringifiedValue}>
89126
{stringifiedValue}
90127
</div>
91128
);
@@ -95,11 +132,15 @@ export const DateValue: React.FunctionComponent<PropsByValueType<'Date'>> = ({
95132
value,
96133
}) => {
97134
const stringifiedValue = useMemo(() => {
98-
return new Date(value).toISOString().replace('Z', '+00:00');
135+
try {
136+
return new Date(value).toISOString().replace('Z', '+00:00');
137+
} catch {
138+
return String(value);
139+
}
99140
}, [value]);
100141

101142
return (
102-
<div className={getClassName('date')} title={stringifiedValue}>
143+
<div className={getStyles('Date')} title={stringifiedValue}>
103144
{stringifiedValue}
104145
</div>
105146
);
@@ -113,7 +154,7 @@ export const NumberValue: React.FunctionComponent<
113154
}, [value]);
114155

115156
return (
116-
<div className={getClassName(type)} title={stringifiedValue}>
157+
<div className={getStyles(type)} title={stringifiedValue}>
117158
{stringifiedValue}
118159
</div>
119160
);
@@ -126,7 +167,7 @@ export const StringValue: React.FunctionComponent<PropsByValueType<'String'>> =
126167
}, [value]);
127168

128169
return (
129-
<div className={getClassName('string')} title={value}>
170+
<div className={getStyles('String')} title={value}>
130171
&quot;{truncatedValue}&quot;
131172
</div>
132173
);
@@ -140,7 +181,7 @@ export const RegExpValue: React.FunctionComponent<
140181
}, [value.pattern, value.options]);
141182

142183
return (
143-
<div className={getClassName('BSONRegExp')} title={stringifiedValue}>
184+
<div className={getStyles('BSONRegExp')} title={stringifiedValue}>
144185
{stringifiedValue}
145186
</div>
146187
);
@@ -154,7 +195,7 @@ export const TimestampValue: React.FunctionComponent<
154195
}, [value]);
155196

156197
return (
157-
<div className={getClassName('timestamp')} title={stringifiedValue}>
198+
<div className={getStyles('Timestamp')} title={stringifiedValue}>
158199
{stringifiedValue}
159200
</div>
160201
);
@@ -167,7 +208,7 @@ export const KeyValue: React.FunctionComponent<{ type: 'MinKey' | 'MaxKey' }> =
167208
}, [type]);
168209

169210
return (
170-
<div className={getClassName(type)} title={stringifiedValue}>
211+
<div className={getStyles(type)} title={stringifiedValue}>
171212
{stringifiedValue}
172213
</div>
173214
);
@@ -183,7 +224,7 @@ export const DBRefValue: React.FunctionComponent<PropsByValueType<'DBRef'>> = ({
183224
}, [value.collection, value.oid, value.db]);
184225

185226
return (
186-
<div className={getClassName('dbref')} title={stringifiedValue}>
227+
<div className={getStyles('DBRef')} title={stringifiedValue}>
187228
{stringifiedValue}
188229
</div>
189230
);
@@ -197,7 +238,7 @@ export const SymbolValue: React.FunctionComponent<
197238
}, [value]);
198239

199240
return (
200-
<div className={getClassName('symbol')} title={stringifiedValue}>
241+
<div className={getStyles('Symbol')} title={stringifiedValue}>
201242
{stringifiedValue}
202243
</div>
203244
);
@@ -212,7 +253,7 @@ export const UnknownValue: React.FunctionComponent<{
212253
}, [value]);
213254

214255
return (
215-
<div className={getClassName(type)} title={stringifiedValue}>
256+
<div className={getStyles(type)} title={stringifiedValue}>
216257
{stringifiedValue}
217258
</div>
218259
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createContext, useContext } from 'react';
2+
3+
type AutoFocusInfo = {
4+
id: string;
5+
type: 'key' | 'value';
6+
} | null;
7+
8+
export const AutoFocusContext = createContext<AutoFocusInfo>(null);
9+
10+
export function useAutoFocusContext(): AutoFocusInfo {
11+
return useContext(AutoFocusContext);
12+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { css } from '@leafygreen-ui/emotion';
3+
import type {
4+
default as HadronDocumentType,
5+
Element as HadronElementType,
6+
} from 'hadron-document';
7+
import { ElementEvents } from 'hadron-document';
8+
import { fontFamilies, spacing } from '@leafygreen-ui/tokens';
9+
import { AutoFocusContext } from './auto-focus-context';
10+
import { useForceUpdate } from './use-force-update';
11+
import { HadronElement } from './element';
12+
13+
function useHadronDocument(doc: HadronDocumentType) {
14+
const forceUpdate = useForceUpdate();
15+
16+
const onDocumentFieldsAddedOrRemoved = useCallback(
17+
(
18+
_el: HadronElementType,
19+
parentEl: HadronElementType | HadronDocumentType | null
20+
) => {
21+
// We only care about elements added or removed to the root document here
22+
// so that the root elements list can be correctly re-rendered. Everything
23+
// else will be handled correctly by HadronElement component
24+
if (doc === parentEl) {
25+
forceUpdate();
26+
}
27+
},
28+
[doc, forceUpdate]
29+
);
30+
31+
useEffect(() => {
32+
doc.on(ElementEvents.Added, onDocumentFieldsAddedOrRemoved);
33+
doc.on(ElementEvents.Removed, onDocumentFieldsAddedOrRemoved);
34+
35+
return () => {
36+
doc.off(ElementEvents.Added, onDocumentFieldsAddedOrRemoved);
37+
doc.off(ElementEvents.Removed, onDocumentFieldsAddedOrRemoved);
38+
};
39+
}, [doc, onDocumentFieldsAddedOrRemoved]);
40+
41+
return {
42+
elements: [...doc.elements],
43+
};
44+
}
45+
46+
const hadronDocument = css({
47+
position: 'relative',
48+
fontFamily: fontFamilies.code,
49+
fontSize: '12px',
50+
lineHeight: `${spacing[3]}px`,
51+
counterReset: 'line-number',
52+
});
53+
54+
// TODO: This element should implement treegrid aria role to be accessible
55+
// https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html
56+
// https://jira.mongodb.org/browse/COMPASS-5614
57+
const HadronDocument: React.FunctionComponent<{
58+
value: HadronDocumentType;
59+
visibleFieldsCount?: number;
60+
expanded: boolean;
61+
editable: boolean;
62+
onEditStart?: () => void;
63+
editing: boolean;
64+
}> = ({
65+
value: document,
66+
visibleFieldsCount,
67+
expanded,
68+
editable,
69+
onEditStart,
70+
editing,
71+
}) => {
72+
const { elements } = useHadronDocument(document);
73+
const visibleElements = useMemo(() => {
74+
return elements.filter(Boolean).slice(0, visibleFieldsCount);
75+
}, [elements, visibleFieldsCount]);
76+
const [autoFocus, setAutoFocus] = useState<{
77+
id: string;
78+
type: 'key' | 'value';
79+
} | null>(null);
80+
81+
useEffect(() => {
82+
if (!editing) {
83+
setAutoFocus(null);
84+
}
85+
}, [editing]);
86+
87+
return (
88+
<div
89+
className={hadronDocument}
90+
data-testid="hadron-document"
91+
data-id={document.getStringId()}
92+
>
93+
<AutoFocusContext.Provider value={autoFocus}>
94+
{visibleElements.map((el) => {
95+
return (
96+
<HadronElement
97+
value={el}
98+
key={el.uuid}
99+
editingEnabled={editing}
100+
allExpanded={expanded}
101+
onEditStart={
102+
editable
103+
? (id, type) => {
104+
setAutoFocus({ id, type });
105+
onEditStart?.();
106+
}
107+
: undefined
108+
}
109+
lineNumberSize={visibleElements.length}
110+
onAddElement={(el) => {
111+
setAutoFocus({
112+
id: el.uuid,
113+
type: el.parent?.currentType === 'Array' ? 'value' : 'key',
114+
});
115+
}}
116+
></HadronElement>
117+
);
118+
})}
119+
</AutoFocusContext.Provider>
120+
</div>
121+
);
122+
};
123+
124+
export default HadronDocument;

0 commit comments

Comments
 (0)