gristlabs_grist-core/app/client/components/PluginScreen.ts
George Gevoian 08b1286f4f (core) Add column matching to Importer
Summary:
The Importer dialog is now maximized, showing additional column
matching options and information on the left, with the preview
table shown on the right. Columns can be mapped via a select menu
listing all source columns, or by clicking a formula field next to
the menu and directly editing the transform formula.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3096
2021-11-09 12:30:52 -08:00

132 lines
3.6 KiB
TypeScript

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';
/**
* Rendering options for the PluginScreen modal.
*/
export interface RenderOptions {
// Maximizes modal to fill the viewport.
fullscreen?: boolean;
}
/**
* Helper for showing plugin components during imports.
*/
export class PluginScreen extends Disposable {
private _openModalCtl: IModalControl | null = null;
private _importerContent = Observable.create<DomContents>(this, null);
private _fullscreen = Observable.create(this, false);
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, options?: RenderOptions) {
this.showImportDialog();
this._importerContent.set(content);
this._fullscreen.set(Boolean(options?.fullscreen));
}
// 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(''),
cssModalOverrides.cls('-fullscreen', this._fullscreen),
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;
}
&-fullscreen {
height: 100%;
margin: 32px;
}
`);
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;
`);