(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

@@ -63,7 +63,7 @@ export class App extends DisposableWithEvents {
this.autoDispose(Clipboard.create(this));
} else {
// On mobile, we do not want to keep focus on a special textarea (which would cause unwanted
// scrolling and showing of mobile keyboard). But we still rely on 'cliboard_focus' and
// scrolling and showing of mobile keyboard). But we still rely on 'clipboard_focus' and
// 'clipboard_blur' events to know when the "app" has a focus (rather than a particular
// input), by making document.body focusable and using a FocusLayer with it as the default.
document.body.setAttribute('tabindex', '-1');
@@ -172,7 +172,7 @@ export class App extends DisposableWithEvents {
this.autoDispose(createAppUI(this.topAppModel, this));
}
// We want to test erors from Selenium, but errors we can trigger using driver.executeScript()
// We want to test errors from Selenium, but errors we can trigger using driver.executeScript()
// will be impossible for the application to report properly (they seem to be considered not of
// "same-origin"). So this silly callback is for tests to generate a fake error.
public testTriggerError(msg: string) { throw new Error(msg); }
@@ -184,7 +184,7 @@ export class App extends DisposableWithEvents {
// When called as a dom method, adds the "newui" class when ?newui=1 is set. For example
// dom('div.some-old-class', this.app.addNewUIClass(), ...)
// Then you may overridde newui styles in CSS by using selectors like:
// Then you may override newui styles in CSS by using selectors like:
// .some-old-class.newui { ... }
public addNewUIClass(): DomElementMethod {
return (elem) => { if (this.useNewUI) { elem.classList.add('newui'); } };
@@ -206,28 +206,6 @@ export class App extends DisposableWithEvents {
return true;
}
/**
* Returns the UntrustedContentOrigin use settings. Throws if not defined. The configured
* UntrustedContentOrign should not include the port, it is defined at runtime.
*/
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 = G.window.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;
}
// Get the user profile for testing purposes
public async testGetProfile(): Promise<any> {
const resp = await fetchFromHome('/api/profile/user', {credentials: 'include'});

View File

@@ -66,7 +66,7 @@ function createMainPage(appModel: AppModel, appObj: App) {
}
return dom.domComputed(appModel.pageType, (pageType) => {
if (pageType === 'home') {
return dom.create(pagePanelsHome, appModel);
return dom.create(pagePanelsHome, appModel, appObj);
} else if (pageType === 'billing') {
return domAsync(loadBillingPage().then(bp => dom.create(bp.BillingPage, appModel)));
} else if (pageType === 'welcome') {
@@ -77,8 +77,8 @@ function createMainPage(appModel: AppModel, appObj: App) {
});
}
function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel) {
const pageModel = HomeModelImpl.create(owner, appModel);
function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) {
const pageModel = HomeModelImpl.create(owner, appModel, app.clientScope);
const leftPanelOpen = Observable.create(owner, true);
// Set document title to strings like "Home - Grist" or "Org Name - Grist".

View File

@@ -1,4 +1,6 @@
import {PluginScreen} from 'app/client/components/PluginScreen';
import {guessTimezone} from 'app/client/lib/guessTimezone';
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
import {IMPORTABLE_EXTENSIONS, uploadFiles} from 'app/client/lib/uploads';
import {AppModel, reportError} from 'app/client/models/AppModel';
import {IProgress} from 'app/client/models/NotifyModel';
@@ -22,6 +24,14 @@ export async function docImport(app: AppModel, workspaceId: number|"unsaved"): P
if (!files.length) { return null; }
return await fileImport(files, app, workspaceId);
}
/**
* Imports a document from a file and returns its docId.
*/
export async function fileImport(
files: File[], app: AppModel, workspaceId: number | "unsaved"): Promise<string | null> {
// There is just one file (thanks to {multiple: false} above).
const progressUI = app.notifier.createProgressIndicator(files[0].name, byteString(files[0].size));
const progress = ImportProgress.create(progressUI, progressUI, files[0]);
@@ -110,3 +120,44 @@ export class ImportProgress extends Disposable {
this._progressUI.setProgress(100 * progress);
}
}
/**
* Imports document through a plugin from a home/welcome screen.
*/
export async function importFromPlugin(
app: AppModel,
workspaceId: number | "unsaved",
importSourceElem: ImportSourceElement
) {
const screen = PluginScreen.create(null, importSourceElem.importSource.label);
try {
const plugin = importSourceElem.plugin;
const handle = screen.renderPlugin(plugin);
const importSource = await importSourceElem.importSourceStub.getImportSource(handle);
plugin.removeRenderTarget(handle);
if (importSource) {
// If data has been picked, upload it.
const item = importSource.item;
if (item.kind === "fileList") {
const files = item.files.map(({ content, name }) => new File([content], name));
const docId = await fileImport(files, app, workspaceId);
screen.close();
return docId;
} else if (item.kind === "url") {
//TODO: importing from url is not yet implemented.
//uploadResult = await fetchURL(this._docComm, item.url);
throw new Error("Url is not supported yet");
} else {
throw new Error(`Import source of kind ${(item as any).kind} are not yet supported!`);
}
} else {
screen.close();
return null;
}
} catch (err) {
screen.renderError(err.message);
return null;
}
}

View File

@@ -1,11 +1,11 @@
import {loadUserManager} from 'app/client/lib/imports';
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
import {reportError} from 'app/client/models/AppModel';
import {docUrl, urlState} from 'app/client/models/gristUrlState';
import {HomeModel} from 'app/client/models/HomeModel';
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
import {docImport} from 'app/client/ui/HomeImports';
import {createHelpTools, cssLeftPanel, cssScrollPane, cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
import {transientInput} from 'app/client/ui/transientInput';
import {colors, testId} from 'app/client/ui2018/cssVars';
@@ -14,7 +14,9 @@ import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/cli
import {confirmModal} from 'app/client/ui2018/modals';
import * as roles from 'app/common/roles';
import {Workspace} from 'app/common/UserAPI';
import {computed, dom, DomElementArg, Observable, observable, styled} from 'grainjs';
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
import {createHelpTools, cssLeftPanel, cssScrollPane,
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: HomeModel) {
const creating = observable<boolean>(false);
@@ -138,6 +140,23 @@ export async function importDocAndOpen(home: HomeModel) {
}
}
export async function importFromPluginAndOpen(home: HomeModel, source: ImportSourceElement) {
try {
const destWS = home.newDocWorkspace.get();
if (!destWS) { return; }
const docId = await importFromPlugin(
home.app,
destWS === "unsaved" ? "unsaved" : destWS.id,
source);
if (docId) {
const doc = await home.app.api.getDoc(docId);
await urlState().pushUrl(docUrl(doc));
}
} catch (err) {
reportError(err);
}
}
function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[] {
const org = home.app.currentOrg;
const orgAccess: roles.Role|null = org ? org.access : null;
@@ -152,6 +171,14 @@ function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[
dom.cls('disabled', !home.newDocWorkspace.get()),
testId("dm-import")
),
domComputed(home.importSources, importSources => ([
...importSources.map((source, i) =>
menuItem(() => importFromPluginAndOpen(home, source),
menuIcon('Import'),
source.importSource.label,
testId(`dm-import-plugin`)
))
])),
// For workspaces: if ACL says we can create them, but product says we can't,
// then offer an upgrade link.
upgradableMenuItem(needUpgrade, () => creating.set(true), menuIcon('Folder'), "Create Workspace",