Skip to content

Commit 215d4b0

Browse files
lerouxbAnemy
andauthored
feat(import): analyse CSV fields and auto-select the correct type COMPASS-6536 (#4155)
* use analyzeCSVFields() * test auto-detecting number * add/fix tests * file type select weirdness * tooltips for mixed & number * obvs clickVisible is not available on an element.. * feedback * more cleanup * Update packages/compass-import-export/src/components/import-preview.tsx Co-authored-by: Rhys <[email protected]> * Update packages/compass-import-export/src/components/select-field-type.tsx Co-authored-by: Rhys <[email protected]> * merge select-field-type into import-preview * async/await * css tweaks * yellow column hilight for mixed fields * tweaks --------- Co-authored-by: Rhys <[email protected]>
1 parent 9c83b39 commit 215d4b0

File tree

15 files changed

+628
-235
lines changed

15 files changed

+628
-235
lines changed

packages/compass-components/src/components/placeholder.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const Placeholder: React.FunctionComponent<
5757
maxChar?: number;
5858
width?: CSSProperties['width'];
5959
height?: CSSProperties['height'];
60+
'data-testid'?: string;
6061
}
6162
> = ({
6263
className,
@@ -76,7 +77,7 @@ const Placeholder: React.FunctionComponent<
7677
<div
7778
{...props}
7879
role="presentation"
79-
data-testid="placeholder"
80+
data-testid={props['data-testid'] ?? 'placeholder'}
8081
className={cx(placeholder, className, darkMode && placeholderDarkMode)}
8182
style={{ width, height: propsHeight }}
8283
></div>

packages/compass-e2e-tests/helpers/selectors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,9 @@ export const ImportDone =
561561
'[data-testid="import-modal"] [data-testid="done-button"]';
562562
export const ImportErrorBox = '[data-testid="import-error-box"]';
563563

564+
export const importPreviewFieldHeaderField = (fieldName: string): string => {
565+
return `[data-testid="import-preview-field-type-select-menu-${fieldName}"]`;
566+
};
564567
export const importPreviewFieldHeaderSelect = (fieldName: string): string => {
565568
return `[data-testid="preview-field-header-${fieldName}"] button`;
566569
};

packages/compass-e2e-tests/tests/collection-import.test.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ async function selectFieldType(
5454

5555
const fieldTypeSelectSpan = await fieldTypeSelectMenu.$(`span=${fieldType}`);
5656
await fieldTypeSelectSpan.waitForDisplayed();
57+
await fieldTypeSelectSpan.scrollIntoView();
58+
await browser.pause(1000);
5759
await fieldTypeSelectSpan.click();
5860

5961
// Wait so that the menu animation can complete.
@@ -454,12 +456,21 @@ describe('Collection import', function () {
454456
await browser.clickVisible(Selectors.ImportFileOption);
455457

456458
// Select the file.
459+
// Unfortunately this opens a second open dialog and the one that got
460+
// automatically opened when clicking on import file sticks around on top of
461+
// everything :(
457462
await browser.selectFile(Selectors.ImportFileInput, csvPath);
458463

459464
// Wait for the modal to appear.
460465
const importModal = await browser.$(Selectors.ImportModal);
461466
await importModal.waitForDisplayed();
462467

468+
// wait for it to finish analyzing
469+
await browser.$(Selectors.ImportConfirm).waitForDisplayed();
470+
await browser
471+
.$(Selectors.importPreviewFieldHeaderField('id'))
472+
.waitForDisplayed();
473+
463474
// pick some types
464475
const typeMapping = {
465476
id: 'Number',
@@ -560,23 +571,11 @@ describe('Collection import', function () {
560571
);
561572
expect(await importDelimiterSelectButton.getText()).to.equal('semicolon');
562573

563-
/*
564-
const importDelimiterSelectButton = await browser.$(
565-
Selectors.ImportDelimiterSelect
566-
);
567-
await importDelimiterSelectButton.waitForDisplayed();
568-
await importDelimiterSelectButton.click();
569-
570-
const importDelimiterSelectMenu = await browser.$(
571-
Selectors.ImportDelimiterMenu
572-
);
573-
await importDelimiterSelectMenu.waitForDisplayed();
574-
const delimiterSelectSpan = await importDelimiterSelectMenu.$(
575-
'span=semicolon'
576-
);
577-
await delimiterSelectSpan.waitForDisplayed();
578-
await delimiterSelectSpan.click();
579-
*/
574+
// wait for it to finish analyzing
575+
await browser.$(Selectors.ImportConfirm).waitForDisplayed();
576+
await browser
577+
.$(Selectors.importPreviewFieldHeaderField('amount'))
578+
.waitForDisplayed();
580579

581580
// pick some types
582581
const typeMapping = {
@@ -648,6 +647,12 @@ describe('Collection import', function () {
648647
const importModal = await browser.$(Selectors.ImportModal);
649648
await importModal.waitForDisplayed();
650649

650+
// wait for it to finish analyzing
651+
await browser.$(Selectors.ImportConfirm).waitForDisplayed();
652+
await browser
653+
.$(Selectors.importPreviewFieldHeaderField('id'))
654+
.waitForDisplayed();
655+
651656
// pick an incompatible type
652657
await selectFieldType(browser, 'id', 'ObjectId');
653658

packages/compass-import-export/src/components/import-file-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function ImportFileInput({
2020
const handleChooseFile = useCallback(
2121
(files: string[]) => {
2222
if (files.length > 0) {
23-
selectImportFileName(files[0]);
23+
void selectImportFileName(files[0]);
2424
} else if (typeof onCancel === 'function') {
2525
onCancel();
2626
}

packages/compass-import-export/src/components/import-modal.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { ImportErrorList } from './import-error-list';
4343
import type { RootImportState } from '../stores/import-store';
4444
import type { CSVDelimiter, FieldFromCSV } from '../modules/import';
4545
import { ImportFileInput } from './import-file-input';
46+
import type { CSVParsableFieldType } from '../utils/csv';
4647

4748
/**
4849
* Progress messages.
@@ -96,13 +97,14 @@ type ImportModalProps = {
9697
*/
9798
fields: {
9899
path: string;
99-
checked: boolean;
100-
type?: string; // Only on csv imports.
100+
checked?: boolean; // CSV placeholder fields don't have checked
101+
type?: CSVParsableFieldType | 'placeholder'; // Only on csv imports.
101102
}[];
102103
values: string[][];
103104
toggleIncludeField: (path: string) => void;
104105
setFieldType: (path: string, bsonType: string) => void;
105106
previewLoaded: boolean;
107+
csvAnalyzed: boolean;
106108
};
107109

108110
function ImportModal({
@@ -136,6 +138,7 @@ function ImportModal({
136138
toggleIncludeField,
137139
setFieldType,
138140
previewLoaded,
141+
csvAnalyzed,
139142
}: ImportModalProps) {
140143
const modalBodyRef = useRef<HTMLDivElement>(null);
141144
const handleCancel = useCallback(() => {
@@ -194,7 +197,12 @@ function ImportModal({
194197
}
195198

196199
return (
197-
<Modal open={isOpen} setOpen={handleClose} data-testid="import-modal">
200+
<Modal
201+
open={isOpen}
202+
setOpen={handleClose}
203+
data-testid="import-modal"
204+
size="large"
205+
>
198206
<ModalHeader title="Import" subtitle={`To Collection ${ns}`} />
199207
<ModalBody ref={modalBodyRef}>
200208
<ImportOptions
@@ -212,6 +220,7 @@ function ImportModal({
212220
<FormFieldContainer>
213221
<ImportPreview
214222
loaded={previewLoaded}
223+
analyzed={csvAnalyzed}
215224
onFieldCheckedChanged={toggleIncludeField}
216225
setFieldType={setFieldType}
217226
values={values}
@@ -261,7 +270,11 @@ function ImportModal({
261270
<Button
262271
data-testid="import-button"
263272
onClick={handleImportBtnClicked}
264-
disabled={!fileName || status === STARTED}
273+
disabled={
274+
!fileName ||
275+
status === STARTED ||
276+
(fileType === 'csv' && !csvAnalyzed)
277+
}
265278
variant="primary"
266279
>
267280
{status === STARTED ? 'Importing\u2026' : 'Import'}
@@ -301,6 +314,7 @@ const mapStateToProps = (state: RootImportState) => ({
301314
fields: state.importData.fields,
302315
values: state.importData.values,
303316
previewLoaded: state.importData.previewLoaded,
317+
csvAnalyzed: state.importData.analyzeStatus === 'COMPLETED',
304318
});
305319

306320
/**

packages/compass-import-export/src/components/import-options.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function ImportOptions({
9090
id="import-delimiter-select"
9191
data-testid="import-delimiter-select"
9292
onChange={(delimiter: string) =>
93-
setDelimiter(delimiter as CSVDelimiter)
93+
void setDelimiter(delimiter as CSVDelimiter)
9494
}
9595
value={delimiter}
9696
allowDeselect={false}

packages/compass-import-export/src/components/import-preview.spec.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import sinon from 'sinon';
44
import { expect } from 'chai';
55

66
import { ImportPreview } from './import-preview';
7+
import type { CSVParsableFieldType } from '../utils/csv';
78

89
const testText = 'Specify Fields and Types';
910

1011
const testField = {
1112
path: '_id',
1213
checked: true,
13-
type: 'String',
14+
type: 'string' as CSVParsableFieldType,
1415
};
1516

1617
describe('ImportPreview [Component]', function () {
@@ -31,6 +32,7 @@ describe('ImportPreview [Component]', function () {
3132
loaded={false}
3233
onFieldCheckedChanged={onFieldCheckedChangedSpy}
3334
setFieldType={setFieldTypeSpy}
35+
analyzed={false}
3436
/>
3537
);
3638
});
@@ -49,6 +51,7 @@ describe('ImportPreview [Component]', function () {
4951
loaded
5052
onFieldCheckedChanged={onFieldCheckedChangedSpy}
5153
setFieldType={setFieldTypeSpy}
54+
analyzed={true}
5255
/>
5356
);
5457
});
@@ -67,6 +70,7 @@ describe('ImportPreview [Component]', function () {
6770
loaded
6871
onFieldCheckedChanged={onFieldCheckedChangedSpy}
6972
setFieldType={setFieldTypeSpy}
73+
analyzed={true}
7074
/>
7175
);
7276
});
@@ -91,6 +95,7 @@ describe('ImportPreview [Component]', function () {
9195
loaded
9296
onFieldCheckedChanged={onFieldCheckedChangedSpy}
9397
setFieldType={setFieldTypeSpy}
98+
analyzed={true}
9499
/>
95100
);
96101
});
@@ -99,4 +104,51 @@ describe('ImportPreview [Component]', function () {
99104
expect(screen.queryByText(testText)).to.be.visible;
100105
});
101106
});
107+
108+
it('renders placeholders when not analyzed', function () {
109+
render(
110+
<ImportPreview
111+
fields={[testField]}
112+
values={
113+
[
114+
{
115+
_id: 25,
116+
},
117+
] as any
118+
}
119+
loaded
120+
onFieldCheckedChanged={onFieldCheckedChangedSpy}
121+
setFieldType={setFieldTypeSpy}
122+
analyzed={false}
123+
/>
124+
);
125+
126+
expect(screen.queryByTestId('import-preview-placeholder-_id')).to.be
127+
.visible;
128+
expect(screen.queryByTestId('import-preview-field-type-select-menu-_id')).to
129+
.not.exist;
130+
});
131+
132+
it('renders field type selects once analyzed', function () {
133+
render(
134+
<ImportPreview
135+
fields={[testField]}
136+
values={
137+
[
138+
{
139+
_id: 25,
140+
},
141+
] as any
142+
}
143+
loaded
144+
onFieldCheckedChanged={onFieldCheckedChangedSpy}
145+
setFieldType={setFieldTypeSpy}
146+
analyzed={true}
147+
/>
148+
);
149+
150+
expect(screen.queryByTestId('import-preview-placeholder-_id')).to.not.exist;
151+
expect(screen.queryByTestId('import-preview-field-type-select-menu-_id')).to
152+
.be.visible;
153+
});
102154
});

0 commit comments

Comments
 (0)