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/D2943pull/71/head
parent
bb55422d9c
commit
4ca47878ca
@ -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;
|
||||
`);
|
@ -0,0 +1,57 @@
|
||||
import {ClientScope} from 'app/client/components/ClientScope';
|
||||
import {SafeBrowser} from 'app/client/lib/SafeBrowser';
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import {createRpcLogger, PluginInstance} from 'app/common/PluginInstance';
|
||||
|
||||
/**
|
||||
* Home plugins are all plugins that contributes to a general Grist management tasks.
|
||||
* They operate on Grist as a whole, without current document context.
|
||||
* TODO: currently it is used primary for importing documents on home screen and supports
|
||||
* only safeBrowser components without any access to Grist.
|
||||
*/
|
||||
export class HomePluginManager {
|
||||
|
||||
public pluginsList: PluginInstance[];
|
||||
|
||||
constructor(localPlugins: LocalPlugin[],
|
||||
_untrustedContentOrigin: string,
|
||||
_clientScope: ClientScope) {
|
||||
this.pluginsList = [];
|
||||
for (const plugin of localPlugins) {
|
||||
try {
|
||||
const components = plugin.manifest.components || {};
|
||||
// Home plugins supports only safeBrowser components
|
||||
if (components.safePython || components.unsafeNode) {
|
||||
continue;
|
||||
}
|
||||
// and currently implements only safe imports
|
||||
const importSources = plugin.manifest.contributions.importSources;
|
||||
if (!importSources?.some(i => i.safeHome)) {
|
||||
continue;
|
||||
}
|
||||
const pluginInstance = new PluginInstance(plugin, createRpcLogger(console, `HOME PLUGIN ${plugin.id}:`));
|
||||
const safeBrowser = pluginInstance.safeBrowser = new SafeBrowser(pluginInstance,
|
||||
_clientScope, _untrustedContentOrigin, components.safeBrowser);
|
||||
if (components.safeBrowser) {
|
||||
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);
|
||||
}
|
||||
const forwarder = new NotAvailableForwarder();
|
||||
// Block any calls to internal apis.
|
||||
pluginInstance.rpc.registerForwarder('*', {
|
||||
forwardCall: (call) => forwarder.forwardPluginRpc(plugin.id, call),
|
||||
forwardMessage: (msg) => forwarder.forwardPluginRpc(plugin.id, msg),
|
||||
});
|
||||
this.pluginsList.push(pluginInstance);
|
||||
} catch (err) {
|
||||
console.error( // tslint:disable-line:no-console
|
||||
`HomePluginManager: failed to instantiate ${plugin.id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotAvailableForwarder {
|
||||
public async forwardPluginRpc(pluginId: string, msg: any) {
|
||||
throw new Error("This api is not available");
|
||||
}
|
||||
}
|
Loading…
Reference in new issue