mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
Make changes required for Desktop FS updates (#1099)
Make a set of changes required for Desktop FS improvements, see https://github.com/gristlabs/grist-desktop/pull/42 --------- Co-authored-by: Spoffy <contact@spoffy.net> Co-authored-by: Spoffy <4805393+Spoffy@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {AxiosProgressEvent} from 'axios';
|
||||
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';
|
||||
import {ImportProgress} from 'app/client/ui/ImportProgress';
|
||||
import {IMPORTABLE_EXTENSIONS} from 'app/client/lib/uploads';
|
||||
import {openFilePicker} from 'app/client/ui/FileDialog';
|
||||
import {byteString} from 'app/common/gutil';
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
import {Disposable} from 'grainjs';
|
||||
import {uploadFiles} from 'app/client/lib/uploads';
|
||||
|
||||
/**
|
||||
* Imports a document and returns its docId, or null if no files were selected.
|
||||
@@ -66,62 +66,6 @@ export async function fileImport(
|
||||
progressUI.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportProgress extends Disposable {
|
||||
// Import does upload first, then import. We show a single indicator, estimating which fraction
|
||||
// of the time should be given to upload (whose progress we can report well), and which to the
|
||||
// subsequent import (whose progress indicator is mostly faked).
|
||||
private _uploadFraction: number;
|
||||
private _estImportSeconds: number;
|
||||
|
||||
private _importTimer: null | ReturnType<typeof setInterval> = null;
|
||||
private _importStart: number = 0;
|
||||
|
||||
constructor(private _progressUI: IProgress, file: File) {
|
||||
super();
|
||||
// We'll assume that for .grist files, the upload takes 90% of the total time, and for other
|
||||
// files, 40%.
|
||||
this._uploadFraction = file.name.endsWith(".grist") ? 0.9 : 0.4;
|
||||
|
||||
// TODO: Import step should include a progress callback, to be combined with upload progress.
|
||||
// Without it, we estimate import to take 2s per MB (non-scientific unreliable estimate), and
|
||||
// use an asymptotic indicator which keeps moving without ever finishing. Not terribly useful,
|
||||
// but does slow down for larger files, and is more comforting than a stuck indicator.
|
||||
this._estImportSeconds = file.size / 1024 / 1024 * 2;
|
||||
|
||||
this._progressUI.setProgress(0);
|
||||
this.onDispose(() => this._importTimer && clearInterval(this._importTimer));
|
||||
}
|
||||
|
||||
// Once this reaches 100, the import stage begins.
|
||||
public setUploadProgress(percentage: number) {
|
||||
this._progressUI.setProgress(percentage * this._uploadFraction);
|
||||
if (percentage >= 100 && !this._importTimer) {
|
||||
this._importStart = Date.now();
|
||||
this._importTimer = setInterval(() => this._onImportTimer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
public finish() {
|
||||
if (this._importTimer) {
|
||||
clearInterval(this._importTimer);
|
||||
}
|
||||
this._progressUI.setProgress(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls _progressUI.setProgress(percent) with percentage increasing from 0 and asymptotically
|
||||
* approaching 100, reaching 50% after estSeconds. It's intended to look reasonable when the
|
||||
* estimate is good, and to keep showing slowing progress even if it's not.
|
||||
*/
|
||||
private _onImportTimer() {
|
||||
const elapsedSeconds = (Date.now() - this._importStart) / 1000;
|
||||
const importProgress = elapsedSeconds / (elapsedSeconds + this._estImportSeconds);
|
||||
const progress = this._uploadFraction + importProgress * (1 - this._uploadFraction);
|
||||
this._progressUI.setProgress(100 * progress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports document through a plugin from a home/welcome screen.
|
||||
*/
|
||||
47
app/client/ui/CoreNewDocMethods.ts
Normal file
47
app/client/ui/CoreNewDocMethods.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {homeImports} from 'app/client/ui/HomeImports';
|
||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
|
||||
export async function createDocAndOpen(home: HomeModel) {
|
||||
const destWS = home.newDocWorkspace.get();
|
||||
if (!destWS) { return; }
|
||||
try {
|
||||
const docId = await home.createDoc("Untitled document", destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||
// Fetch doc information including urlId.
|
||||
// TODO: consider changing API to return same response as a GET when creating an
|
||||
// object, which is a semi-standard.
|
||||
const doc = await home.app.api.getDoc(docId);
|
||||
await urlState().pushUrl(docUrl(doc));
|
||||
} catch (err) {
|
||||
reportError(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function importDocAndOpen(home: HomeModel) {
|
||||
const destWS = home.newDocWorkspace.get();
|
||||
if (!destWS) { return; }
|
||||
const docId = await homeImports.docImport(home.app, destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||
if (docId) {
|
||||
const doc = await home.app.api.getDoc(docId);
|
||||
await urlState().pushUrl(docUrl(doc));
|
||||
}
|
||||
}
|
||||
|
||||
export async function importFromPluginAndOpen(home: HomeModel, source: ImportSourceElement) {
|
||||
try {
|
||||
const destWS = home.newDocWorkspace.get();
|
||||
if (!destWS) { return; }
|
||||
const docId = await homeImports.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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {getLoginOrSignupUrl, getLoginUrl, getSignupUrl, urlState} from 'app/clie
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {productPill} from 'app/client/ui/AppHeader';
|
||||
import * as css from 'app/client/ui/DocMenuCss';
|
||||
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
|
||||
import {newDocMethods} from 'app/client/ui/NewDocMethods';
|
||||
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||
import {bigBasicButton, cssButton} from 'app/client/ui2018/buttons';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
@@ -177,11 +177,11 @@ function buildButtons(homeModel: HomeModel, options: {
|
||||
),
|
||||
!options.import ? null :
|
||||
cssBtn(cssBtnIcon('Import'), t("Import Document"), testId('intro-import-doc'),
|
||||
dom.on('click', () => importDocAndOpen(homeModel)),
|
||||
dom.on('click', () => newDocMethods.importDocAndOpen(homeModel)),
|
||||
),
|
||||
!options.empty ? null :
|
||||
cssBtn(cssBtnIcon('Page'), t("Create Empty Document"), testId('intro-create-doc'),
|
||||
dom.on('click', () => createDocAndOpen(homeModel)),
|
||||
dom.on('click', () => newDocMethods.createDocAndOpen(homeModel)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
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 {urlState} from 'app/client/models/gristUrlState';
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {getAdminPanelName} from 'app/client/ui/AdminPanelName';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
||||
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
|
||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
|
||||
import {newDocMethods} from 'app/client/ui/NewDocMethods';
|
||||
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
||||
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||
import {
|
||||
cssLinkText, cssMenuTrigger, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer
|
||||
} from 'app/client/ui/LeftPanelCommon';
|
||||
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
||||
import {transientInput} from 'app/client/ui/transientInput';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/client/ui2018/menus';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {transientInput} from 'app/client/ui/transientInput';
|
||||
import {Workspace} from 'app/common/UserAPI';
|
||||
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
|
||||
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
||||
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||
|
||||
const t = makeT('HomeLeftPane');
|
||||
|
||||
@@ -160,65 +158,23 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
);
|
||||
}
|
||||
|
||||
export async function createDocAndOpen(home: HomeModel) {
|
||||
const destWS = home.newDocWorkspace.get();
|
||||
if (!destWS) { return; }
|
||||
try {
|
||||
const docId = await home.createDoc("Untitled document", destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||
// Fetch doc information including urlId.
|
||||
// TODO: consider changing API to return same response as a GET when creating an
|
||||
// object, which is a semi-standard.
|
||||
const doc = await home.app.api.getDoc(docId);
|
||||
await urlState().pushUrl(docUrl(doc));
|
||||
} catch (err) {
|
||||
reportError(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function importDocAndOpen(home: HomeModel) {
|
||||
const destWS = home.newDocWorkspace.get();
|
||||
if (!destWS) { return; }
|
||||
const docId = await docImport(home.app, destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||
if (docId) {
|
||||
const doc = await home.app.api.getDoc(docId);
|
||||
await urlState().pushUrl(docUrl(doc));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const needUpgrade = home.app.currentFeatures?.maxWorkspacesPerOrg === 1;
|
||||
|
||||
return [
|
||||
menuItem(() => createDocAndOpen(home), menuIcon('Page'), t("Create Empty Document"),
|
||||
menuItem(() => newDocMethods.createDocAndOpen(home), menuIcon('Page'), t("Create Empty Document"),
|
||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||
testId("dm-new-doc")
|
||||
),
|
||||
menuItem(() => importDocAndOpen(home), menuIcon('Import'), t("Import Document"),
|
||||
menuItem(() => newDocMethods.importDocAndOpen(home), menuIcon('Import'), t("Import Document"),
|
||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||
testId("dm-import")
|
||||
),
|
||||
domComputed(home.importSources, importSources => ([
|
||||
...importSources.map((source, i) =>
|
||||
menuItem(() => importFromPluginAndOpen(home, source),
|
||||
menuItem(() => newDocMethods.importFromPluginAndOpen(home, source),
|
||||
menuIcon('Import'),
|
||||
source.importSource.label,
|
||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||
|
||||
58
app/client/ui/ImportProgress.ts
Normal file
58
app/client/ui/ImportProgress.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {IProgress} from 'app/client/models/NotifyModel';
|
||||
import {Disposable} from 'grainjs';
|
||||
|
||||
export class ImportProgress extends Disposable {
|
||||
// Import does upload first, then import. We show a single indicator, estimating which fraction
|
||||
// of the time should be given to upload (whose progress we can report well), and which to the
|
||||
// subsequent import (whose progress indicator is mostly faked).
|
||||
private _uploadFraction: number;
|
||||
private _estImportSeconds: number;
|
||||
|
||||
private _importTimer: null | ReturnType<typeof setInterval> = null;
|
||||
private _importStart: number = 0;
|
||||
|
||||
constructor(private _progressUI: IProgress, file: File) {
|
||||
super();
|
||||
// We'll assume that for .grist files, the upload takes 90% of the total time, and for other
|
||||
// files, 40%.
|
||||
this._uploadFraction = file.name.endsWith(".grist") ? 0.9 : 0.4;
|
||||
|
||||
// TODO: Import step should include a progress callback, to be combined with upload progress.
|
||||
// Without it, we estimate import to take 2s per MB (non-scientific unreliable estimate), and
|
||||
// use an asymptotic indicator which keeps moving without ever finishing. Not terribly useful,
|
||||
// but does slow down for larger files, and is more comforting than a stuck indicator.
|
||||
this._estImportSeconds = file.size / 1024 / 1024 * 2;
|
||||
|
||||
this._progressUI.setProgress(0);
|
||||
this.onDispose(() => this._importTimer && clearInterval(this._importTimer));
|
||||
}
|
||||
|
||||
// Once this reaches 100, the import stage begins.
|
||||
public setUploadProgress(percentage: number) {
|
||||
this._progressUI.setProgress(percentage * this._uploadFraction);
|
||||
if (percentage >= 100 && !this._importTimer) {
|
||||
this._importStart = Date.now();
|
||||
this._importTimer = setInterval(() => this._onImportTimer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
public finish() {
|
||||
if (this._importTimer) {
|
||||
clearInterval(this._importTimer);
|
||||
}
|
||||
this._progressUI.setProgress(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls _progressUI.setProgress(percent) with percentage increasing from 0 and asymptotically
|
||||
* approaching 100, reaching 50% after estSeconds. It's intended to look reasonable when the
|
||||
* estimate is good, and to keep showing slowing progress even if it's not.
|
||||
*/
|
||||
private _onImportTimer() {
|
||||
const elapsedSeconds = (Date.now() - this._importStart) / 1000;
|
||||
const importProgress = elapsedSeconds / (elapsedSeconds + this._estImportSeconds);
|
||||
const progress = this._uploadFraction + importProgress * (1 - this._uploadFraction);
|
||||
this._progressUI.setProgress(100 * progress);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user