(core) Site Switcher and Other Sites

Summary:
A new section, Other Sites, will now be shown on the All Documents page when:

 - A user is on a personal site and has access to other team sites.
 - A user is on a public site with view access only.

In addition, a site switcher is now available by clicking
the site name in the top-left section of the UI next to the
Grist logo. It works much like the switcher in the Account
menu.

Test Plan: Browser tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2979
This commit is contained in:
George Gevoian
2021-08-18 10:49:34 -07:00
parent c561dad22d
commit d83d734b75
10 changed files with 218 additions and 74 deletions

View File

@@ -8,7 +8,7 @@ import {GristLoadConfig} from 'app/common/gristUrls';
import {FullUser} from 'app/common/LoginSessionAPI';
import {LocalPlugin} from 'app/common/plugin';
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
import {Computed, Disposable, Observable, subscribe} from 'grainjs';
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
export {reportError} from 'app/client/models/errors';
@@ -29,6 +29,9 @@ export interface TopAppModel {
// different parts of the code aren't using different users/orgs while the switch is pending.
appObs: Observable<AppModel|null>;
orgs: Observable<Organization[]>;
users: Observable<FullUser[]>;
// Reinitialize the app. This is called when org or user changes.
initialize(): void;
@@ -68,6 +71,8 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
public readonly currentSubdomain = Computed.create(this, urlState().state, (use, s) => s.org);
public readonly notifier = Notifier.create(this);
public readonly appObs = Observable.create<AppModel|null>(this, null);
public readonly orgs = Observable.create<Organization[]>(this, []);
public readonly users = Observable.create<FullUser[]>(this, []);
public readonly plugins: LocalPlugin[] = [];
private readonly _gristConfig?: GristLoadConfig;
@@ -85,6 +90,8 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
// and the FullUser to use for it (the user may change when switching orgs).
this.autoDispose(subscribe(this.currentSubdomain, (use) => this.initialize()));
this.plugins = this._gristConfig?.plugins || [];
this._fetchUsersAndOrgs().catch(reportError);
}
public initialize(): void {
@@ -146,6 +153,15 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
AppModelImpl.create(this.appObs, this, null, null, {error: err.message, status: err.status || 500});
}
}
private async _fetchUsersAndOrgs() {
const data = await this.api.getSessionAll();
if (this.isDisposed()) { return; }
bundleChanges(() => {
this.users.set(data.users);
this.orgs.set(data.orgs);
});
}
}
export class AppModelImpl extends Disposable implements AppModel {

View File

@@ -11,7 +11,7 @@ import {IHomePage} from 'app/common/gristUrls';
import {isLongerThan} from 'app/common/gutil';
import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
import * as roles from 'app/common/roles';
import {Document, Workspace} from 'app/common/UserAPI';
import {Document, Organization, Workspace} from 'app/common/UserAPI';
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
import * as moment from 'moment';
import flatten = require('lodash/flatten');
@@ -63,6 +63,10 @@ export interface HomeModel {
// List of featured templates from templateWorkspaces.
featuredTemplates: Observable<Document[]>;
// List of other sites (orgs) user can access. Only populated on All Documents, and only when
// the current org is a personal org, or the current org is view access only.
otherSites: Observable<Organization[]>;
currentSort: Observable<SortPref>;
currentView: Observable<ViewPref>;
importSources: Observable<ImportSourceElement[]>;
@@ -118,6 +122,21 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
return sortBy(featuredTemplates, (t) => t.name.toLowerCase());
});
public readonly otherSites = Computed.create(this, this.currentPage, this.app.topAppModel.orgs,
(_use, page, orgs) => {
if (page !== 'all') { return []; }
const currentOrg = this._app.currentOrg;
if (!currentOrg) { return []; }
const isPersonalOrg = currentOrg.owner;
if (!isPersonalOrg && (currentOrg.access !== 'viewers' || !currentOrg.public)) {
return [];
}
return orgs.filter(org => org.id !== currentOrg.id);
});
public readonly currentSort: Observable<SortPref>;
public readonly currentView: Observable<ViewPref>;
@@ -255,16 +274,10 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
this.loading.set(true);
const currentPage = this.currentPage.get();
const promises = [
this._fetchWorkspaces(org.id, false).catch(reportError), // workspaces
currentPage === 'trash' ? this._fetchWorkspaces(org.id, true).catch(reportError) : null, // trash
null // templates
];
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
if (shouldFetchTemplates) {
const onlyFeatured = currentPage === 'all';
promises[2] = this._fetchTemplates(onlyFeatured);
}
this._fetchWorkspaces(org.id, false).catch(reportError),
currentPage === 'trash' ? this._fetchWorkspaces(org.id, true).catch(reportError) : null,
this._maybeFetchTemplates(),
] as const;
const promise = Promise.all(promises);
if (await isLongerThan(promise, DELAY_BEFORE_SPINNER_MS)) {
@@ -327,9 +340,19 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
ws.name.toLowerCase()]);
}
private async _fetchTemplates(onlyFeatured: boolean) {
/**
* Fetches templates if on the Templates or All Documents page.
*
* Only fetches featured (pinned) templates on the All Documents page.
*/
private async _maybeFetchTemplates(): Promise<Workspace[] | null> {
const currentPage = this.currentPage.get();
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
if (!shouldFetchTemplates) { return null; }
let templateWss: Workspace[] = [];
try {
const onlyFeatured = currentPage === 'all';
templateWss = await this._app.api.getTemplates(onlyFeatured);
} catch {
// If the org doesn't exist (404), return nothing and don't report error to user.