Skip to content

Commit

Permalink
Make changes required for Desktop FS updates (#1099)
Browse files Browse the repository at this point in the history
Make a set of changes required for Desktop FS improvements, see
gristlabs/grist-desktop#42

---------

Co-authored-by: Spoffy <contact@spoffy.net>
Co-authored-by: Spoffy <4805393+Spoffy@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 17, 2024
1 parent 938bb06 commit 02cfcee
Show file tree
Hide file tree
Showing 29 changed files with 552 additions and 447 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
/sandbox_venv*
/.vscode/

# Files created by grist-desktop setup
/cpython.tar.gz
/python
/static_ext

# Build helper files.
/.build*

Expand Down Expand Up @@ -82,7 +87,8 @@ xunit.xml
**/_build

# ext directory can be overwritten
ext/**
/ext
/ext/**

# Docker compose examples - persistent values and secrets
/docker-compose-examples/*/persist
Expand Down
66 changes: 5 additions & 61 deletions app/client/ui/HomeImports.ts → app/client/ui/CoreHomeImports.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
47 changes: 47 additions & 0 deletions app/client/ui/CoreNewDocMethods.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 3 additions & 3 deletions app/client/ui/HomeIntro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)),
),
);
}
Expand Down
72 changes: 14 additions & 58 deletions app/client/ui/HomeLeftPane.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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()),
Expand Down
58 changes: 58 additions & 0 deletions app/client/ui/ImportProgress.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}

Loading

0 comments on commit 02cfcee

Please sign in to comment.