mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding import from google drive to the home screen
Summary: Importing from google drive from home screen (also for anonymous users) Test Plan: Browser tests Reviewers: dsagal, paulfitz Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2943
This commit is contained in:
@@ -47,6 +47,7 @@ import {OpenLocalDocResult} from 'app/common/DocListAPI';
|
||||
import {HashLink, IDocPage} from 'app/common/gristUrls';
|
||||
import {RecalcWhen} from 'app/common/gristTypes';
|
||||
import {encodeQueryParams, undef, waitObs} from 'app/common/gutil';
|
||||
import {LocalPlugin} from "app/common/plugin";
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {TableData} from 'app/common/TableData';
|
||||
import {DocStateComparison} from 'app/common/UserAPI';
|
||||
@@ -143,6 +144,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
public readonly docComm: DocComm,
|
||||
public readonly docPageModel: DocPageModel,
|
||||
openDocResponse: OpenLocalDocResult,
|
||||
plugins: LocalPlugin[],
|
||||
options: {
|
||||
comparison?: DocStateComparison // initial comparison with another document
|
||||
} = {}
|
||||
@@ -152,8 +154,8 @@ export class GristDoc extends DisposableWithEvents {
|
||||
this.docData = new DocData(this.docComm, openDocResponse.doc);
|
||||
this.docModel = new DocModel(this.docData);
|
||||
this.querySetManager = QuerySetManager.create(this, this.docModel, this.docComm);
|
||||
this.docPluginManager = new DocPluginManager(openDocResponse.plugins, app.getUntrustedContentOrigin(),
|
||||
this.docComm, app.clientScope);
|
||||
this.docPluginManager = new DocPluginManager(plugins,
|
||||
app.topAppModel.getUntrustedContentOrigin(), this.docComm, app.clientScope);
|
||||
|
||||
// Maintain the MetaRowModel for the global document info, including docId and peers.
|
||||
this.docInfo = this.docModel.docInfo.getRowModel(1);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import {GristDoc} from "app/client/components/GristDoc";
|
||||
import {buildParseOptionsForm, ParseOptionValues} from 'app/client/components/ParseOptions';
|
||||
import {PluginScreen} from "app/client/components/PluginScreen";
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {fetchURL, isDriveUrl, selectFiles, uploadFiles} from 'app/client/lib/uploads';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
@@ -14,15 +15,13 @@ import {openFilePicker} from "app/client/ui/FileDialog";
|
||||
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||
import {IOptionFull, linkSelect} from 'app/client/ui2018/menus';
|
||||
import {cssModalButtons, cssModalTitle, IModalControl, modal} from 'app/client/ui2018/modals';
|
||||
import {cssModalButtons, cssModalTitle} from 'app/client/ui2018/modals';
|
||||
import {DataSourceTransformed, ImportResult, ImportTableResult} from "app/common/ActiveDocAPI";
|
||||
import {TransformColumn, TransformRule, TransformRuleMap} from "app/common/ActiveDocAPI";
|
||||
import {byteString} from "app/common/gutil";
|
||||
import {UploadResult} from 'app/common/uploads';
|
||||
import {ParseOptions, ParseOptionSchema} from 'app/plugin/FileParserAPI';
|
||||
import {RenderTarget} from 'app/plugin/RenderOptions';
|
||||
import {Computed, Disposable, dom, DomContents, IDisposable, Observable, styled} from 'grainjs';
|
||||
|
||||
// Special values for import destinations; null means "new table".
|
||||
@@ -118,9 +117,8 @@ export class Importer extends Disposable {
|
||||
|
||||
private _docComm = this._gristDoc.docComm;
|
||||
private _uploadResult?: UploadResult;
|
||||
private _openModalCtl: IModalControl|null = null;
|
||||
private _importerContent = Observable.create<DomContents>(this, null);
|
||||
|
||||
private _screen: PluginScreen;
|
||||
private _parseOptions = Observable.create<ParseOptions>(this, {});
|
||||
private _sourceInfoArray = Observable.create<SourceInfo[]>(this, []);
|
||||
private _sourceInfoSelected = Observable.create<SourceInfo|null>(this, null);
|
||||
@@ -143,6 +141,7 @@ export class Importer extends Disposable {
|
||||
constructor(private _gristDoc: GristDoc, private _importSourceElem: ImportSourceElement|null,
|
||||
private _createPreview: CreatePreviewFunc) {
|
||||
super();
|
||||
this._screen = PluginScreen.create(this, _importSourceElem?.importSource.label || "Import from file");
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -158,22 +157,10 @@ export class Importer extends Disposable {
|
||||
multiple: true, sizeLimit: 'import'});
|
||||
} else {
|
||||
const plugin = this._importSourceElem.plugin;
|
||||
|
||||
// registers a render target for plugin to render inline.
|
||||
const handle: RenderTarget = plugin.addRenderTarget((el, opt = {}) => {
|
||||
el.style.width = "100%";
|
||||
el.style.height = opt.height || "200px";
|
||||
this._showImportDialog();
|
||||
this._renderPlugin(el);
|
||||
});
|
||||
|
||||
const handle = this._screen.renderPlugin(plugin);
|
||||
const importSource = await this._importSourceElem.importSourceStub.getImportSource(handle);
|
||||
plugin.removeRenderTarget(handle);
|
||||
|
||||
if (!this._openModalCtl) {
|
||||
this._showImportDialog();
|
||||
}
|
||||
this._renderSpinner();
|
||||
this._screen.renderSpinner();
|
||||
|
||||
if (importSource) {
|
||||
// If data has been picked, upload it.
|
||||
@@ -202,10 +189,7 @@ export class Importer extends Disposable {
|
||||
await this._cancelImport();
|
||||
throw err;
|
||||
}
|
||||
if (!this._openModalCtl) {
|
||||
this._showImportDialog();
|
||||
}
|
||||
this._renderError(err.message);
|
||||
this._screen.renderError(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,8 +255,7 @@ export class Importer extends Disposable {
|
||||
}
|
||||
|
||||
private async _reImport(upload: UploadResult) {
|
||||
this._renderSpinner();
|
||||
this._showImportDialog();
|
||||
this._screen.renderSpinner();
|
||||
try {
|
||||
const parseOptions = {...this._parseOptions.get(), NUM_ROWS: 100};
|
||||
const importResult: ImportResult = await this._docComm.importFiles(
|
||||
@@ -300,12 +283,12 @@ export class Importer extends Disposable {
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Import failed", e);
|
||||
this._renderError(e.message);
|
||||
this._screen.renderError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _finishImport(upload: UploadResult) {
|
||||
this._renderSpinner();
|
||||
this._screen.renderSpinner();
|
||||
const parseOptions = {...this._parseOptions.get(), NUM_ROWS: 0};
|
||||
const importResult: ImportResult = await this._docComm.finishImportFiles(
|
||||
this._getTransformedDataSource(upload), parseOptions, this._getHiddenTableIds());
|
||||
@@ -314,7 +297,7 @@ export class Importer extends Disposable {
|
||||
const tableRowModel = this._gristDoc.docModel.dataTables[importResult.tables[0].hiddenTableId].tableMetaRow;
|
||||
await this._gristDoc.openDocPage(tableRowModel.primaryViewId());
|
||||
}
|
||||
this._openModalCtl?.close();
|
||||
this._screen.close();
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
@@ -323,60 +306,19 @@ export class Importer extends Disposable {
|
||||
await this._docComm.cancelImportFiles(
|
||||
this._getTransformedDataSource(this._uploadResult), this._getHiddenTableIds());
|
||||
}
|
||||
this._openModalCtl?.close();
|
||||
this._screen.close();
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private _showImportDialog() {
|
||||
if (this._openModalCtl) { return; }
|
||||
modal((ctl, owner) => {
|
||||
this._openModalCtl = ctl;
|
||||
return [
|
||||
cssModalOverrides.cls(''),
|
||||
dom.domComputed(this._importerContent),
|
||||
testId('importer-dialog'),
|
||||
];
|
||||
}, {
|
||||
noClickAway: true,
|
||||
noEscapeKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _buildModalTitle(rightElement?: DomContents) {
|
||||
const title = this._importSourceElem ? this._importSourceElem.importSource.label : 'Import from file';
|
||||
return cssModalHeader(cssModalTitle(title), rightElement);
|
||||
}
|
||||
|
||||
// The importer state showing just a spinner, when the user has to wait. We don't even let the
|
||||
// user cancel it, because the cleanup can only happen properly once the wait completes.
|
||||
private _renderSpinner() {
|
||||
this._importerContent.set([this._buildModalTitle(), cssSpinner(loadingSpinner())]);
|
||||
}
|
||||
|
||||
// The importer state showing the inline element from the plugin (e.g. to enter URL in case of
|
||||
// import-from-url).
|
||||
private _renderPlugin(inlineElement: HTMLElement) {
|
||||
this._importerContent.set([this._buildModalTitle(), inlineElement]);
|
||||
}
|
||||
|
||||
// The importer state showing just an error.
|
||||
private _renderError(message: string) {
|
||||
this._importerContent.set([
|
||||
this._buildModalTitle(),
|
||||
cssModalBody('Import failed: ', message, testId('importer-error')),
|
||||
cssModalButtons(
|
||||
bigBasicButton('Close',
|
||||
dom.on('click', () => this._cancelImport()),
|
||||
testId('modal-cancel'),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// The importer state showing import in progress, with a list of tables, and a preview.
|
||||
private _renderMain(upload: UploadResult) {
|
||||
const schema = this._parseOptions.get().SCHEMA;
|
||||
this._importerContent.set([
|
||||
this._screen.render([
|
||||
this._buildModalTitle(
|
||||
schema ? cssActionLink(cssLinkIcon('Settings'), 'Import options',
|
||||
testId('importer-options-link'),
|
||||
@@ -424,7 +366,7 @@ export class Importer extends Disposable {
|
||||
|
||||
// The importer state showing parse options that may be changed.
|
||||
private _renderParseOptions(schema: ParseOptionSchema[], upload: UploadResult) {
|
||||
this._importerContent.set([
|
||||
this._screen.render([
|
||||
this._buildModalTitle(),
|
||||
dom.create(buildParseOptionsForm, schema, this._parseOptions.get() as ParseOptionValues,
|
||||
(p: ParseOptions) => {
|
||||
@@ -466,28 +408,6 @@ const cssLinkIcon = styled(icon, `
|
||||
margin-right: 4px;
|
||||
`);
|
||||
|
||||
const cssModalOverrides = styled('div', `
|
||||
max-height: calc(100% - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
& > .${cssModalButtons.className} {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssModalBody = styled('div', `
|
||||
padding: 16px 0;
|
||||
overflow-y: auto;
|
||||
max-width: 470px;
|
||||
`);
|
||||
|
||||
const cssSpinner = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
margin: auto;
|
||||
`);
|
||||
|
||||
const cssModalHeader = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
115
app/client/components/PluginScreen.ts
Normal file
115
app/client/components/PluginScreen.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { bigBasicButton } from 'app/client/ui2018/buttons';
|
||||
import { testId } from 'app/client/ui2018/cssVars';
|
||||
import { loadingSpinner } from 'app/client/ui2018/loaders';
|
||||
import { cssModalButtons, cssModalTitle, IModalControl, modal } from 'app/client/ui2018/modals';
|
||||
import { PluginInstance } from 'app/common/PluginInstance';
|
||||
import { RenderTarget } from 'app/plugin/RenderOptions';
|
||||
import { Disposable, dom, DomContents, Observable, styled } from 'grainjs';
|
||||
|
||||
/**
|
||||
* Helper for showing plugin components during imports.
|
||||
*/
|
||||
export class PluginScreen extends Disposable {
|
||||
private _openModalCtl: IModalControl | null = null;
|
||||
private _importerContent = Observable.create<DomContents>(this, null);
|
||||
|
||||
constructor(private _title: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
// The importer state showing the inline element from the plugin (e.g. to enter URL in case of
|
||||
// import-from-url).
|
||||
public renderContent(inlineElement: HTMLElement) {
|
||||
this.render([this._buildModalTitle(), inlineElement]);
|
||||
}
|
||||
|
||||
// registers a render target for plugin to render inline.
|
||||
public renderPlugin(plugin: PluginInstance): RenderTarget {
|
||||
const handle: RenderTarget = plugin.addRenderTarget((el, opt = {}) => {
|
||||
el.style.width = "100%";
|
||||
el.style.height = opt.height || "200px";
|
||||
this.renderContent(el);
|
||||
});
|
||||
return handle;
|
||||
}
|
||||
|
||||
public render(content: DomContents) {
|
||||
this.showImportDialog();
|
||||
this._importerContent.set(content);
|
||||
}
|
||||
|
||||
// The importer state showing just an error.
|
||||
public renderError(message: string) {
|
||||
this.render([
|
||||
this._buildModalTitle(),
|
||||
cssModalBody('Import failed: ', message, testId('importer-error')),
|
||||
cssModalButtons(
|
||||
bigBasicButton('Close',
|
||||
dom.on('click', () => this.close()),
|
||||
testId('modal-cancel'))),
|
||||
]);
|
||||
}
|
||||
|
||||
// The importer state showing just a spinner, when the user has to wait. We don't even let the
|
||||
// user cancel it, because the cleanup can only happen properly once the wait completes.
|
||||
public renderSpinner() {
|
||||
this.render([this._buildModalTitle(), cssSpinner(loadingSpinner())]);
|
||||
}
|
||||
|
||||
public close() {
|
||||
this._openModalCtl?.close();
|
||||
this._openModalCtl = null;
|
||||
}
|
||||
|
||||
public showImportDialog() {
|
||||
if (this._openModalCtl) { return; }
|
||||
modal((ctl) => {
|
||||
this._openModalCtl = ctl;
|
||||
return [
|
||||
cssModalOverrides.cls(''),
|
||||
dom.domComputed(this._importerContent),
|
||||
testId('importer-dialog'),
|
||||
];
|
||||
}, {
|
||||
noClickAway: true,
|
||||
noEscapeKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _buildModalTitle(rightElement?: DomContents) {
|
||||
return cssModalHeader(cssModalTitle(this._title), rightElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const cssModalOverrides = styled('div', `
|
||||
max-height: calc(100% - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
& > .${cssModalButtons.className} {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssModalBody = styled('div', `
|
||||
padding: 16px 0;
|
||||
overflow-y: auto;
|
||||
max-width: 470px;
|
||||
`);
|
||||
|
||||
const cssModalHeader = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
& > .${cssModalTitle.className} {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssSpinner = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
margin: auto;
|
||||
`);
|
||||
Reference in New Issue
Block a user