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

Desktop user experience overhaul #42

Merged
merged 64 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8783694
Enable opening Grist documents anywhere directly
SleepyLeslie Jul 10, 2024
fa7610b
Point core temporarily to the nativefs branch
SleepyLeslie Jul 11, 2024
f97587a
Update setup script for core changes
SleepyLeslie Jul 11, 2024
8d62d24
Fix wrong import paths
SleepyLeslie Jul 11, 2024
3fb71b8
Fix loading before FlexServer fully bootstraps
SleepyLeslie Jul 11, 2024
a389edd
Handle menu File/New properly
SleepyLeslie Jul 15, 2024
70c3f50
Add .grist extension automatically
SleepyLeslie Jul 15, 2024
0273ff3
Move DocStorageManager decorator to ICreate
SleepyLeslie Jul 16, 2024
d4a50d1
Rename docStorageManagerDecorator
SleepyLeslie Jul 17, 2024
466372f
Implement in-app doc creation with electron IPC
SleepyLeslie Jul 18, 2024
1701ee2
Add a more user-friendly error message
SleepyLeslie Jul 18, 2024
e344727
Stub the login system
SleepyLeslie Jul 18, 2024
dc696fc
Move login system and implement WindowManager
SleepyLeslie Jul 19, 2024
ab5e55d
Fix DocRegistry "garbage collector"
SleepyLeslie Jul 19, 2024
692bc0f
Move logfile setup into a separate file
SleepyLeslie Jul 19, 2024
f2ab8db
Implement home page import button
SleepyLeslie Jul 26, 2024
db4771d
Rewrite the openFile function
SleepyLeslie Jul 26, 2024
3092c0e
Implement import from file menu
SleepyLeslie Jul 26, 2024
bba46ab
Fix window management not reusing existing windows
SleepyLeslie Jul 27, 2024
792c738
Add comments
SleepyLeslie Jul 27, 2024
e8cfa3f
Fix linting issues
SleepyLeslie Jul 27, 2024
1f88340
Bump Python version to 3.11.9
SleepyLeslie Jul 29, 2024
ab313bb
Use v3 for Linux x86_64
SleepyLeslie Jul 29, 2024
a9283ca
Fix page loading issue for external visitors
SleepyLeslie Jul 30, 2024
8d5245d
Change desktop.package.json import path
SleepyLeslie Jul 30, 2024
e0ec5b9
Fixes resolve-tspaths using the wrong tsconfig file.
Spoffy Aug 2, 2024
dc497b4
Replace minimal test
SleepyLeslie Aug 5, 2024
1831a8e
Adapt to core stubs updates
SleepyLeslie Aug 7, 2024
4da9d63
Remove import from core/stubs
SleepyLeslie Aug 7, 2024
7b88549
Attach save dialogs to initiating window
SleepyLeslie Aug 7, 2024
ee22692
Add importable file associations
SleepyLeslie Aug 7, 2024
417b46f
Upgrade electron-builder to v25.0.3
SleepyLeslie Aug 7, 2024
9ab22bf
Upgrade pipeline Node to v20
SleepyLeslie Aug 7, 2024
267c076
Fix build error for Linux
SleepyLeslie Aug 7, 2024
b357417
Remove icons for importable documents
SleepyLeslie Aug 8, 2024
20dbd50
Add APPLE_APP_SPECIFIC_PASSWORD env var
SleepyLeslie Aug 8, 2024
868f250
Address PR remarks
SleepyLeslie Aug 21, 2024
1d06fe1
Dismiss the FileToOpen class
SleepyLeslie Aug 21, 2024
7086885
Make importDocAndOpen signature same as in core
SleepyLeslie Aug 21, 2024
997694d
Replace window.location.assign with urlState
SleepyLeslie Aug 21, 2024
1f0ee7e
Bumps core to track indev nativefs core changes
Spoffy Aug 27, 2024
7927c13
Switches to using DesktopDocStorageManager
Spoffy Aug 27, 2024
ca54ee2
WIP
Spoffy Aug 27, 2024
d56b163
Extracts methods from DocRegistry
Spoffy Aug 30, 2024
98b9655
Migrates DocRegistry to DesktopDocStorageManager (WIP)
Spoffy Aug 31, 2024
db20234
Bumps core to use async storage manager, won't build
Spoffy Sep 10, 2024
46f6545
Makes desktop storage manager creation async
Spoffy Sep 10, 2024
ac42a7e
Re-adds syncing of doc path cache with home DB
Spoffy Sep 10, 2024
7ea1b97
Cleans up some DocRegistry mentions and streamlines an error case
Spoffy Sep 10, 2024
63f319a
Adds missing `await`
Spoffy Sep 10, 2024
1c91a16
Adds a comments explaining `electronOnly()` error
Spoffy Sep 10, 2024
109f692
Splits docImport into docImport and fileImport
Spoffy Sep 10, 2024
9f6bde8
Uses window.gristApp instead of HomeModel
Spoffy Sep 11, 2024
c69fac6
Removes potential circular dependency
Spoffy Sep 11, 2024
cd5af1d
Improves WindowManager mapping consistency
Spoffy Sep 11, 2024
3ba4aee
Merge branch 'main' into sleepyleslie/nativefs
Spoffy Sep 18, 2024
d5a3781
Makes core submodule latest grist-core/main
Spoffy Sep 18, 2024
f016be6
Fixes DesktopDocStorageManager imports
Spoffy Sep 18, 2024
a1b023b
Fixes bug with opening previous version documents
Spoffy Sep 19, 2024
d535880
Makes opening a soft deleted file undelete it.
Spoffy Sep 20, 2024
e02aeb7
Attempts to fix signing on OSX
Spoffy Sep 20, 2024
0346390
Bumps MacOS DMG build to 14
Spoffy Sep 20, 2024
441ca55
Makes Recent Documents function with menu "Open" option
Spoffy Sep 21, 2024
cd97f98
Changes session secret from "something" to "no-longer-needed"
Spoffy Sep 21, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
target:
- x64
node:
- 18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we safely bump core to 20 if Desktop runs happily under 20? Are there any specific changes needed to make this run?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there'll be much trouble if any at all. I did this because latest electron-builder requires Node v20+. No other changes were needed and it just worked.

I have been using v22.6.0 for development. From my experience, Desktop worked perfectly with v22, so you might even want to try that.

- 20
name: ${{ matrix.os }} (node=${{ matrix.node }}, host=${{ matrix.host }}, target=${{ matrix.target }})
steps:
- uses: actions/checkout@v3
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ jobs:
target:
- x64
node:
- 18
- 20
include:
- os: macos-14
node: 18
node: 20
host: arm64
target: arm64
- os: windows-2022
node: 18
node: 20
host: x64
target: x86
name: ${{ matrix.os }} (node=${{ matrix.node }}, host=${{ matrix.host }}, target=${{ matrix.target }})
Expand Down Expand Up @@ -117,6 +117,7 @@ jobs:
DEBUG: electron-builder
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[submodule "core"]
path = core
url = https://github.com/gristlabs/grist-core
branch = main
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫We'll need to switch this back before merging (marking it as a blocker)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but remember to merge that grist-core PR first!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can happen now right @Spoffy ?

branch = nativefs
ignore = dirty
2 changes: 1 addition & 1 deletion core
Submodule core updated 243 files
30 changes: 30 additions & 0 deletions ext/app/client/electronAPI.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HomeModel } from "app/client/models/HomeModel";

export type NewDocument = {
path: string,
id: string
}

/**
* Allows the Grist client to call into electron.
* See https://www.electronjs.org/docs/latest/tutorial/ipc
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
*/
interface IElectronAPI {

// The Grist client can use these interfaces to request the electron main process to perform
// certain tasks.
createDoc: () => Promise<NewDocument>,
importDoc: (uploadId: number) => Promise<NewDocument>,

// The Grist client needs to call these interfaces to register callback functions for certain
// events coming from the electron main process.
onMainProcessImportDoc: (callback: (fileContents: Buffer, fileName: string) => void) => void

}

declare global {
interface Window {
electronAPI: IElectronAPI,
gristHomeModel: HomeModel,
}
}
5 changes: 5 additions & 0 deletions ext/app/client/electronOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function electronOnly() {
if (window.electronAPI === undefined) {
throw Error("Sorry, this must be done from within the app.");
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
}
}
41 changes: 41 additions & 0 deletions ext/app/client/ui/HomeImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { AppModel, reportError } from 'app/client/models/AppModel';
import { IMPORTABLE_EXTENSIONS } from 'app/client/lib/uploads';
import { ImportProgress } from 'app/client/ui/ImportProgress';
import { byteString } from 'app/common/gutil';
import { openFilePicker } from 'app/client/ui/FileDialog';
import { uploadFiles } from 'app/client/lib/uploads';

/**
* Imports a document and returns its upload ID, or null if no files were selected.
*/
async function docImport(app: AppModel, fileToImport?: File): Promise<number|null> {
let files: File[];

if (fileToImport === undefined) {
files = await openFilePicker({
multiple: false,
accept: IMPORTABLE_EXTENSIONS.filter((extension) => extension !== ".grist").join(","),
});
if (!files.length) { return null; }
} else {
files = [fileToImport];
}
Spoffy marked this conversation as resolved.
Show resolved Hide resolved

const progressUI = app.notifier.createProgressIndicator(files[0].name, byteString(files[0].size));
const progress = ImportProgress.create(progressUI, progressUI, files[0]);
try {
const docWorker = await app.api.getWorkerAPI('import');
const uploadResult = await uploadFiles(files, {docWorkerUrl: docWorker.url, sizeLimit: 'import'},
(p) => progress.setUploadProgress(p));

return uploadResult!.uploadId;
} catch (err) {
reportError(err);
return null;
} finally {
progress.finish();
progressUI.dispose();
}
}

export const homeImports = {docImport};
34 changes: 34 additions & 0 deletions ext/app/client/ui/NewDocMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HomeModel } from 'app/client/models/HomeModel';
import { electronOnly } from "app/client/electronOnly";
import { homeImports } from 'app/client/ui/HomeImports';

async function createDocAndOpen() {
electronOnly();
const doc = await window.electronAPI.createDoc();
if (doc) {
window.location.assign("/o/docs/" + doc.id);
}
}

async function importDocAndOpen(home: HomeModel, fileToImport: File) {
electronOnly();
const uploadId = await homeImports.docImport(home.app, fileToImport);
if (uploadId === null) { return; }
const doc = await window.electronAPI.importDoc(uploadId);
if (doc) {
window.location.assign("/o/docs/" + doc.id);
}
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These function signatures differ from the ones in core, which could cause us problems with compatibility.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For createDocAndOpen, I ignore the home parameter anyway, and I think it's safe to not declare any parameter, so I didn't change it. For importDocAndOpen, you are right and I made some changes.

Note that there should also be an importFromPluginAndOpen but I left it out. Added a comment in the code. See PM for details.

// The ? is for external visitors over the network. electronAPI is set by electron's preload script
// and is undefined for non-electron visitors. An error here will make the entire page fail to load.
window.electronAPI?.onMainProcessImportDoc((fileContents: Buffer, fileName: string) => {
(async() => {
while (!window.gristHomeModel || !window.gristHomeModel.app) {
await new Promise(resolve => setTimeout(resolve, 100));
}
importDocAndOpen(window.gristHomeModel, new File([fileContents], fileName));
})();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is window.gristHomeModel set? I was a little worried this might be an infinite loop so went to investigate, and I can't find an assignment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can you recommend a comment, for why we need to wait for this here? As I'm not sure when onMainProcessImportDoc is triggered.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is window.gristHomeModel set?

Here, as a part of the core PR. https://github.com/gristlabs/grist-core/blob/2f84093129c02767a83330811bd330bb2732ee55/app/client/ui/AppUI.ts#L99-L100

There is also has some contextual information about this (I PM'd you). I hate this polling approach myself, but I wasn't able to find a better way to ensure gristHomeModel.app is fully initialized.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, I'll look deeper into this! Thank you :)


export const newDocMethods = { createDocAndOpen, importDocAndOpen };
4 changes: 2 additions & 2 deletions ext/app/electron/AppMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ class AppMenu extends events.EventEmitter {
submenu: [{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => this.emit('menu-file-new')
click: (item, focusedWindow) => this.emit('menu-file-new', focusedWindow)
}, {
label: 'Open...',
accelerator: 'CmdOrCtrl+O',
click: () => this.emit('menu-file-open')
click: (item, focusedWindow) => this.emit('menu-file-open', focusedWindow)
}, {
label: 'Open Recent',
submenu: this.buildOpenRecentSubmenu()
Expand Down
74 changes: 74 additions & 0 deletions ext/app/electron/DocRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as path from "path";
import { HomeDBManager } from "app/gen-server/lib/homedb/HomeDBManager";
import { fileExists } from "./utils";

export class DocRegistry {

private idToPathMap: Map<string, string>;
private pathToIdMap: Map<string, string>;
private db: HomeDBManager;

Spoffy marked this conversation as resolved.
Show resolved Hide resolved
private constructor(dbManager: HomeDBManager) {
this.db = dbManager;
}

public static async create(dbManager: HomeDBManager) {
// Allocate space.
const dr = new DocRegistry(dbManager);
dr.idToPathMap = new Map<string, string>;
dr.pathToIdMap = new Map<string, string>;
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved

// Go over all documents we know about.
for (const doc of await dr.db.getAllDocs()) {
// All documents are supposed to have externalId set.
const docPath = doc.options?.externalId;
if (docPath && fileExists(docPath)) {
// Cache the two-way mapping docID <-> path.
dr.idToPathMap.set(doc.id, docPath);
dr.pathToIdMap.set(docPath, doc.id);
} else {
// Remove this document - it should not appear in a DB for Grist Desktop.
await dr.db.deleteDocument({
userId: (await dr.getDefaultUser()).id,
urlId: doc.id
});
}
}
return dr;
Spoffy marked this conversation as resolved.
Show resolved Hide resolved
}
SleepyLeslie marked this conversation as resolved.
Show resolved Hide resolved

public lookupById(docId: string): string | null {
return this.idToPathMap.get(docId) ?? null;
}

public lookupByPath(docPath: string): string | null {
return this.pathToIdMap.get(docPath) ?? null;
}

public async getDefaultUser() {
const user = await this.db.getUserByLogin(process.env.GRIST_DEFAULT_EMAIL as string);
if (!user) { throw new Error('cannot find default user'); }
return user;
}

public async registerDoc(docPath: string): Promise<string> {
const defaultUser = await this.getDefaultUser();
const wss = this.db.unwrapQueryResult(await this.db.getOrgWorkspaces({userId: defaultUser.id}, 0));
for (const doc of wss[0].docs) {
if (doc.options?.externalId === docPath) {
// We might be able to do better.
throw Error("DocRegistry cache incoherent. Please try restarting the app.");
}
}
const docId = this.db.unwrapQueryResult(await this.db.addDocument({
userId: defaultUser.id,
}, wss[0].id, {
name: path.basename(docPath, '.grist'),
options: { externalId: docPath },
}));
this.pathToIdMap.set(docPath, docId);
this.idToPathMap.set(docId, docPath);
return docId;
}

}
Loading
Loading