(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:
Jarosław Sadziński
2021-08-05 17:12:46 +02:00
parent bb55422d9c
commit 4ca47878ca
21 changed files with 348 additions and 142 deletions

View File

@@ -1,3 +1,4 @@
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {reportError, setErrorNotifier} from 'app/client/models/errors';
import {urlState} from 'app/client/models/gristUrlState';
import {Notifier} from 'app/client/models/NotifyModel';
@@ -5,12 +6,14 @@ import {getFlavor, ProductFlavor} from 'app/client/ui/CustomThemes';
import {Features} from 'app/common/Features';
import {GristLoadConfig} from 'app/common/gristUrls';
import {FullUser} from 'app/common/LoginSessionAPI';
import {LocalPlugin} from 'app/common/plugin';
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
import {Computed, Disposable, Observable, subscribe} from 'grainjs';
export {reportError} from 'app/client/models/errors';
export type PageType = "doc" | "home" | "billing" | "welcome";
const G = getBrowserGlobals('document', 'window');
// TopAppModel is the part of the app model that persists across org and user switches.
export interface TopAppModel {
@@ -20,6 +23,7 @@ export interface TopAppModel {
currentSubdomain: Observable<string|undefined>;
notifier: Notifier;
plugins: LocalPlugin[];
// Everything else gets fully rebuilt when the org/user changes. This is to ensure that
// different parts of the code aren't using different users/orgs while the switch is pending.
@@ -30,6 +34,11 @@ export interface TopAppModel {
// Rebuilds the AppModel and consequently the AppUI, without changing the user or the org.
reload(): void;
/**
* Returns the UntrustedContentOrigin use settings. Throws if not defined.
*/
getUntrustedContentOrigin(): string;
}
// AppModel is specific to the currently loaded organization and active user. It gets rebuilt when
@@ -59,6 +68,8 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
public readonly currentSubdomain = Computed.create(this, urlState().state, (use, s) => s.org);
public readonly notifier = Notifier.create(this);
public readonly appObs = Observable.create<AppModel|null>(this, null);
public readonly plugins: LocalPlugin[] = [];
private readonly _gristConfig?: GristLoadConfig;
constructor(
window: {gristConfig?: GristLoadConfig},
@@ -68,10 +79,12 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
setErrorNotifier(this.notifier);
this.isSingleOrg = Boolean(window.gristConfig && window.gristConfig.singleOrg);
this.productFlavor = getFlavor(window.gristConfig && window.gristConfig.org);
this._gristConfig = window.gristConfig;
// Initially, and on any change to subdomain, call initialize() to get the full Organization
// and the FullUser to use for it (the user may change when switching orgs).
this.autoDispose(subscribe(this.currentSubdomain, (use) => this.initialize()));
this.plugins = this._gristConfig?.plugins || [];
}
public initialize(): void {
@@ -87,6 +100,23 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
}
}
public getUntrustedContentOrigin() {
if (G.window.isRunningUnderElectron) {
// when loaded within webviews it is safe to serve plugin's content from the same domain
return "";
}
const origin = this._gristConfig?.pluginUrl;
if (!origin) {
throw new Error("Missing untrustedContentOrigin configuration");
}
if (origin.match(/:[0-9]+$/)) {
// Port number already specified, no need to add.
return origin;
}
return origin + ":" + G.window.location.port;
}
private async _doInitialize() {
this.appObs.set(null);
try {

View File

@@ -10,8 +10,8 @@ import {cssLeftPanel, cssScrollPane} from 'app/client/ui/LeftPanelCommon';
import {buildPagesDom} from 'app/client/ui/Pages';
import {openPageWidgetPicker} from 'app/client/ui/PageWidgetPicker';
import {tools} from 'app/client/ui/Tools';
import {testId} from 'app/client/ui2018/cssVars';
import {bigBasicButton} from 'app/client/ui2018/buttons';
import {testId} from 'app/client/ui2018/cssVars';
import {menu, menuDivider, menuIcon, menuItem, menuText} from 'app/client/ui2018/menus';
import {confirmModal} from 'app/client/ui2018/modals';
import {AsyncFlow, CancelledError, FlowRunner} from 'app/common/AsyncFlow';
@@ -21,8 +21,8 @@ import {IGristUrlState, parseUrlId, UrlIdParts} from 'app/common/gristUrls';
import {getReconnectTimeout} from 'app/common/gutil';
import {canEdit} from 'app/common/roles';
import {Document, NEW_DOCUMENT_CODE, Organization, UserAPI, Workspace} from 'app/common/UserAPI';
import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs';
import {Holder, Observable, subscribe} from 'grainjs';
import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs';
// tslint:disable:no-console
@@ -271,7 +271,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
await this._api.getDocAPI(urlId).compareDoc(comparisonUrlId, { detail: true }) : undefined;
const gristDoc = gdModule.GristDoc.create(flow, this._appObj, docComm, this, openDocResponse,
{comparison});
this.appModel.topAppModel.plugins, {comparison});
// Move ownership of docComm to GristDoc.
gristDoc.autoDispose(flow.release(docComm));

View File

@@ -1,4 +1,7 @@
import {ClientScope} from 'app/client/components/ClientScope';
import {guessTimezone} from 'app/client/lib/guessTimezone';
import {HomePluginManager} from 'app/client/lib/HomePluginManager';
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
import {localStorageObs} from 'app/client/lib/localStorageObs';
import {AppModel, reportError} from 'app/client/models/AppModel';
import {UserError} from 'app/client/models/errors';
@@ -10,9 +13,9 @@ import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
import * as roles from 'app/common/roles';
import {Document, Workspace} from 'app/common/UserAPI';
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
import * as moment from 'moment';
import flatten = require('lodash/flatten');
import sortBy = require('lodash/sortBy');
import * as moment from 'moment';
const DELAY_BEFORE_SPINNER_MS = 500;
@@ -62,6 +65,7 @@ export interface HomeModel {
currentSort: Observable<SortPref>;
currentView: Observable<ViewPref>;
importSources: Observable<ImportSourceElement[]>;
// The workspace for new docs, or "unsaved" to only allow unsaved-doc creation, or null if the
// user isn't allowed to create a doc.
@@ -96,6 +100,7 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
public readonly singleWorkspace = Observable.create(this, true);
public readonly trashWorkspaces = Observable.create<Workspace[]>(this, []);
public readonly templateWorkspaces = Observable.create<Workspace[]>(this, []);
public readonly importSources = Observable.create<ImportSourceElement[]>(this, []);
// Get the workspace details for the workspace with id of currentWSId.
public readonly currentWS = Computed.create(this, (use) =>
@@ -133,7 +138,7 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
constructor(private _app: AppModel) {
constructor(private _app: AppModel, clientScope: ClientScope) {
super();
if (!this.app.currentValidUser) {
@@ -155,6 +160,14 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
this.autoDispose(subscribe(this.currentPage, this.currentWSId, (use) =>
this._updateWorkspaces().catch(reportError)));
// Defer home plugin initialization
const pluginManager = new HomePluginManager(
_app.topAppModel.plugins,
_app.topAppModel.getUntrustedContentOrigin()!,
clientScope);
const importSources = ImportSourceElement.fromArray(pluginManager.pluginsList);
this.importSources.set(importSources);
}
// Accessor for the AppModel containing this HomeModel.