Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make changes required for Desktop FS updates #1099

Merged
merged 22 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading