(core) Adding Skip options when importing multiple tables.

Summary:
Adding new destination "Skip" for multiple table imports.
Selecting this destination skips the import and makes the preview grayed out.

Test Plan: New Tests

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3181
This commit is contained in:
Jarosław Sadziński 2021-12-13 10:11:18 +01:00
parent d99db8d016
commit 1ae586cf42
3 changed files with 41 additions and 15 deletions

View File

@ -22,9 +22,9 @@ import {IOptionFull, linkSelect, menu, menuDivider, menuItem, multiSelect} from
import {cssModalButtons, cssModalTitle} from 'app/client/ui2018/modals'; import {cssModalButtons, cssModalTitle} from 'app/client/ui2018/modals';
import {loadingSpinner} from 'app/client/ui2018/loaders'; import {loadingSpinner} from 'app/client/ui2018/loaders';
import {openFormulaEditor} from 'app/client/widgets/FieldEditor'; import {openFormulaEditor} from 'app/client/widgets/FieldEditor';
import {DataSourceTransformed, ImportResult, ImportTableResult, MergeOptions, import {DataSourceTransformed, DestId, ImportResult, ImportTableResult, MergeOptions,
MergeOptionsMap, MergeOptionsMap, MergeStrategy, NEW_TABLE, SKIP_TABLE,
MergeStrategy, TransformColumn, TransformRule, TransformRuleMap} from 'app/common/ActiveDocAPI'; TransformColumn, TransformRule, TransformRuleMap} from 'app/common/ActiveDocAPI';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {byteString} from 'app/common/gutil'; import {byteString} from 'app/common/gutil';
import {FetchUrlOptions, UploadResult} from 'app/common/uploads'; import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
@ -35,10 +35,6 @@ import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {ACCESS_DENIED, AUTH_INTERRUPTED, canReadPrivateFiles, getGoogleCodeForReading} from 'app/client/ui/googleAuth'; import {ACCESS_DENIED, AUTH_INTERRUPTED, canReadPrivateFiles, getGoogleCodeForReading} from 'app/client/ui/googleAuth';
import debounce = require('lodash/debounce'); import debounce = require('lodash/debounce');
// Special values for import destinations; null means "new table".
// TODO We should also support "skip table" (needs server support), so that one can open, say,
// an Excel file with many tabs, and import only some of them.
type DestId = string | null;
// We expect a function for creating the preview GridView, to avoid the need to require the // We expect a function for creating the preview GridView, to avoid the need to require the
// GridView module here. That brings many dependencies, making a simple test fixture difficult. // GridView module here. That brings many dependencies, making a simple test fixture difficult.
@ -182,9 +178,10 @@ export class Importer extends DisposableWithEvents {
private _hasScheduledDiffUpdate = false; private _hasScheduledDiffUpdate = false;
// destTables is a list of options for import destinations, and includes all tables in the // destTables is a list of options for import destinations, and includes all tables in the
// document, plus two values: to import as a new table, and to skip an import table entirely. // document, plus two values: to import as a new table, and to skip a table.
private _destTables = Computed.create<Array<IOptionFull<DestId>>>(this, (use) => [ private _destTables = Computed.create<Array<IOptionFull<DestId>>>(this, (use) => [
{value: null, label: 'New Table'}, {value: NEW_TABLE, label: 'New Table'},
...(use(this._sourceInfoArray).length > 1 ? [{value: SKIP_TABLE, label: 'Skip'}] : []),
...use(this._gristDoc.docModel.allTableIds.getObservable()).map((t) => ({value: t, label: t})), ...use(this._gristDoc.docModel.allTableIds.getObservable()).map((t) => ({value: t, label: t})),
]); ]);
@ -393,7 +390,7 @@ export class Importer extends DisposableWithEvents {
origTableName: info.origTableName, origTableName: info.origTableName,
sourceSection: this._getPrimaryViewSection(info.hiddenTableId)!, sourceSection: this._getPrimaryViewSection(info.hiddenTableId)!,
transformSection: Observable.create(null, this._getSectionByRef(info.transformSectionRef)), transformSection: Observable.create(null, this._getSectionByRef(info.transformSectionRef)),
destTableId: Observable.create<DestId>(null, info.destTableId), destTableId: Observable.create<DestId>(null, info.destTableId ?? NEW_TABLE),
isLoadingSection: Observable.create(null, false), isLoadingSection: Observable.create(null, false),
lastGenImporterViewPromise: null lastGenImporterViewPromise: null
}))); })));
@ -436,7 +433,7 @@ export class Importer extends DisposableWithEvents {
const importResult: ImportResult = await this._docComm.finishImportFiles( const importResult: ImportResult = await this._docComm.finishImportFiles(
this._getTransformedDataSource(upload), this._getHiddenTableIds(), {mergeOptionMaps, parseOptions}); this._getTransformedDataSource(upload), this._getHiddenTableIds(), {mergeOptionMaps, parseOptions});
if (importResult.tables[0].hiddenTableId) { if (importResult.tables[0]?.hiddenTableId) {
const tableRowModel = this._gristDoc.docModel.dataTables[importResult.tables[0].hiddenTableId].tableMetaRow; const tableRowModel = this._gristDoc.docModel.dataTables[importResult.tables[0].hiddenTableId].tableMetaRow;
await this._gristDoc.openDocPage(tableRowModel.primaryViewId()); await this._gristDoc.openDocPage(tableRowModel.primaryViewId());
} }
@ -580,7 +577,9 @@ export class Importer extends DisposableWithEvents {
info.destTableId.set(destId); info.destTableId.set(destId);
this._resetTableMergeOptions(info.hiddenTableId); this._resetTableMergeOptions(info.hiddenTableId);
if (destId !== SKIP_TABLE) {
await this._updateTransformSection(info); await this._updateTransformSection(info);
}
}); });
return cssTableInfo( return cssTableInfo(
dom.autoDispose(destTableId), dom.autoDispose(destTableId),
@ -734,6 +733,8 @@ export class Importer extends DisposableWithEvents {
}); });
return cssPreviewGrid( return cssPreviewGrid(
dom.maybe(use1 => SKIP_TABLE === use1(info.destTableId),
() => cssOverlay(testId("importer-preview-overlay"))),
dom.autoDispose(gridView), dom.autoDispose(gridView),
gridView.viewPane, gridView.viewPane,
testId('importer-preview'), testId('importer-preview'),
@ -746,7 +747,10 @@ export class Importer extends DisposableWithEvents {
cssModalButtons( cssModalButtons(
bigPrimaryButton('Import', bigPrimaryButton('Import',
dom.on('click', () => this._maybeFinishImport(upload)), dom.on('click', () => this._maybeFinishImport(upload)),
dom.boolAttr('disabled', use => use(this._previewViewSection) === null), dom.boolAttr('disabled', use => {
return use(this._previewViewSection) === null ||
use(this._sourceInfoArray).every(i => use(i.destTableId) === SKIP_TABLE);
}),
testId('modal-confirm'), testId('modal-confirm'),
), ),
bigBasicButton('Cancel', bigBasicButton('Cancel',
@ -1032,8 +1036,19 @@ const cssPreviewSpinner = styled(cssPreview, `
justify-content: center; justify-content: center;
`); `);
const cssOverlay = styled('div', `
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
z-index: 10;
background: ${colors.mediumGrey};
`);
const cssPreviewGrid = styled(cssPreview, ` const cssPreviewGrid = styled(cssPreview, `
border: 1px solid ${colors.darkGrey}; border: 1px solid ${colors.darkGrey};
position: relative;
`); `);
const cssMergeOptions = styled('div', ` const cssMergeOptions = styled('div', `

View File

@ -31,8 +31,14 @@ export interface TransformRuleMap {
[origTableName: string]: TransformRule; [origTableName: string]: TransformRule;
} }
// Special values for import destinations; null means "new table", "" means skip table.
// Both special options exposed as consts.
export type DestId = string | null;
export const NEW_TABLE = null;
export const SKIP_TABLE = "";
export interface TransformRule { export interface TransformRule {
destTableId: string|null; destTableId: DestId;
destCols: TransformColumn[]; destCols: TransformColumn[];
sourceCols: string[]; sourceCols: string[];
} }

View File

@ -5,7 +5,8 @@ import * as _ from 'underscore';
import {ColumnDelta, createEmptyActionSummary} from 'app/common/ActionSummary'; import {ColumnDelta, createEmptyActionSummary} from 'app/common/ActionSummary';
import {ApplyUAResult, DataSourceTransformed, ImportOptions, ImportResult, ImportTableResult, import {ApplyUAResult, DataSourceTransformed, ImportOptions, ImportResult, ImportTableResult,
MergeOptions, MergeOptionsMap, MergeStrategy, TransformColumn, TransformRule, MergeOptions, MergeOptionsMap, MergeStrategy, SKIP_TABLE, TransformColumn,
TransformRule,
TransformRuleMap} from 'app/common/ActiveDocAPI'; TransformRuleMap} from 'app/common/ActiveDocAPI';
import {ApiError} from 'app/common/ApiError'; import {ApiError} from 'app/common/ApiError';
import {BulkColValues, CellValue, fromTableDataAction, TableRecordValue} from 'app/common/DocActions'; import {BulkColValues, CellValue, fromTableDataAction, TableRecordValue} from 'app/common/DocActions';
@ -329,6 +330,10 @@ export class ActiveDocImport {
createdTableId = hiddenTableId; createdTableId = hiddenTableId;
} else { } else {
if (destTableId === SKIP_TABLE) {
await this._activeDoc.applyUserActions(docSession, [['RemoveTable', hiddenTableId]]);
continue;
}
// Do final import // Do final import
const mergeOptions = mergeOptionsMap[origTableName] ?? null; const mergeOptions = mergeOptionsMap[origTableName] ?? null;
const intoNewTable: boolean = destTableId ? false : true; const intoNewTable: boolean = destTableId ? false : true;