mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
c561dad22d
commit
d83d734b75
@ -8,7 +8,7 @@ import {GristLoadConfig} from 'app/common/gristUrls';
|
|||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import {LocalPlugin} from 'app/common/plugin';
|
import {LocalPlugin} from 'app/common/plugin';
|
||||||
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
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';
|
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.
|
// different parts of the code aren't using different users/orgs while the switch is pending.
|
||||||
appObs: Observable<AppModel|null>;
|
appObs: Observable<AppModel|null>;
|
||||||
|
|
||||||
|
orgs: Observable<Organization[]>;
|
||||||
|
users: Observable<FullUser[]>;
|
||||||
|
|
||||||
// Reinitialize the app. This is called when org or user changes.
|
// Reinitialize the app. This is called when org or user changes.
|
||||||
initialize(): void;
|
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 currentSubdomain = Computed.create(this, urlState().state, (use, s) => s.org);
|
||||||
public readonly notifier = Notifier.create(this);
|
public readonly notifier = Notifier.create(this);
|
||||||
public readonly appObs = Observable.create<AppModel|null>(this, null);
|
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[] = [];
|
public readonly plugins: LocalPlugin[] = [];
|
||||||
private readonly _gristConfig?: GristLoadConfig;
|
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).
|
// and the FullUser to use for it (the user may change when switching orgs).
|
||||||
this.autoDispose(subscribe(this.currentSubdomain, (use) => this.initialize()));
|
this.autoDispose(subscribe(this.currentSubdomain, (use) => this.initialize()));
|
||||||
this.plugins = this._gristConfig?.plugins || [];
|
this.plugins = this._gristConfig?.plugins || [];
|
||||||
|
|
||||||
|
this._fetchUsersAndOrgs().catch(reportError);
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
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});
|
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 {
|
export class AppModelImpl extends Disposable implements AppModel {
|
||||||
|
@ -11,7 +11,7 @@ import {IHomePage} from 'app/common/gristUrls';
|
|||||||
import {isLongerThan} from 'app/common/gutil';
|
import {isLongerThan} from 'app/common/gutil';
|
||||||
import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
|
import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
|
||||||
import * as roles from 'app/common/roles';
|
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 {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import flatten = require('lodash/flatten');
|
import flatten = require('lodash/flatten');
|
||||||
@ -63,6 +63,10 @@ export interface HomeModel {
|
|||||||
// List of featured templates from templateWorkspaces.
|
// List of featured templates from templateWorkspaces.
|
||||||
featuredTemplates: Observable<Document[]>;
|
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>;
|
currentSort: Observable<SortPref>;
|
||||||
currentView: Observable<ViewPref>;
|
currentView: Observable<ViewPref>;
|
||||||
importSources: Observable<ImportSourceElement[]>;
|
importSources: Observable<ImportSourceElement[]>;
|
||||||
@ -118,6 +122,21 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
|||||||
return sortBy(featuredTemplates, (t) => t.name.toLowerCase());
|
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 currentSort: Observable<SortPref>;
|
||||||
public readonly currentView: Observable<ViewPref>;
|
public readonly currentView: Observable<ViewPref>;
|
||||||
|
|
||||||
@ -255,16 +274,10 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
|||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
const currentPage = this.currentPage.get();
|
const currentPage = this.currentPage.get();
|
||||||
const promises = [
|
const promises = [
|
||||||
this._fetchWorkspaces(org.id, false).catch(reportError), // workspaces
|
this._fetchWorkspaces(org.id, false).catch(reportError),
|
||||||
currentPage === 'trash' ? this._fetchWorkspaces(org.id, true).catch(reportError) : null, // trash
|
currentPage === 'trash' ? this._fetchWorkspaces(org.id, true).catch(reportError) : null,
|
||||||
null // templates
|
this._maybeFetchTemplates(),
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
|
|
||||||
if (shouldFetchTemplates) {
|
|
||||||
const onlyFeatured = currentPage === 'all';
|
|
||||||
promises[2] = this._fetchTemplates(onlyFeatured);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = Promise.all(promises);
|
const promise = Promise.all(promises);
|
||||||
if (await isLongerThan(promise, DELAY_BEFORE_SPINNER_MS)) {
|
if (await isLongerThan(promise, DELAY_BEFORE_SPINNER_MS)) {
|
||||||
@ -327,9 +340,19 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
|||||||
ws.name.toLowerCase()]);
|
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[] = [];
|
let templateWss: Workspace[] = [];
|
||||||
try {
|
try {
|
||||||
|
const onlyFeatured = currentPage === 'all';
|
||||||
templateWss = await this._app.api.getTemplates(onlyFeatured);
|
templateWss = await this._app.api.getTemplates(onlyFeatured);
|
||||||
} catch {
|
} catch {
|
||||||
// If the org doesn't exist (404), return nothing and don't report error to user.
|
// If the org doesn't exist (404), return nothing and don't report error to user.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {loadGristDoc, loadUserManager} from 'app/client/lib/imports';
|
import {loadGristDoc, loadUserManager} from 'app/client/lib/imports';
|
||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/client/models/gristUrlState';
|
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {showProfileModal} from 'app/client/ui/ProfileDialog';
|
import {showProfileModal} from 'app/client/ui/ProfileDialog';
|
||||||
@ -13,22 +13,17 @@ import {commonUrls} from 'app/common/gristUrls';
|
|||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {getOrgName, Organization, SUPPORT_EMAIL} from 'app/common/UserAPI';
|
import {getOrgName, Organization, SUPPORT_EMAIL} from 'app/common/UserAPI';
|
||||||
import {bundleChanges, Disposable, dom, DomElementArg, Observable, styled} from 'grainjs';
|
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
||||||
import {cssMenuItem} from 'popweasel';
|
import {cssMenuItem} from 'popweasel';
|
||||||
|
import {cssOrgCheckmark, cssOrgSelected} from 'app/client/ui/AppHeader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the user-icon that opens the account menu. When no user is logged in, render a Sign-in
|
* Render the user-icon that opens the account menu. When no user is logged in, render a Sign-in
|
||||||
* button instead.
|
* button instead.
|
||||||
*/
|
*/
|
||||||
export class AccountWidget extends Disposable {
|
export class AccountWidget extends Disposable {
|
||||||
private _users = Observable.create<FullUser[]>(this, []);
|
|
||||||
private _orgs = Observable.create<Organization[]>(this, []);
|
|
||||||
|
|
||||||
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
||||||
super();
|
super();
|
||||||
// We initialize users and orgs asynchronously when we create the menu, so it will *probably* be
|
|
||||||
// available by the time the user opens it. Even if not, we do not delay the opening of the menu.
|
|
||||||
this._fetchUsersAndOrgs().catch(reportError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
@ -47,16 +42,6 @@ export class AccountWidget extends Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchUsersAndOrgs() {
|
|
||||||
if (!this._appModel.topAppModel.isSingleOrg) {
|
|
||||||
const data = await this._appModel.api.getSessionAll();
|
|
||||||
if (this.isDisposed()) { return; }
|
|
||||||
bundleChanges(() => {
|
|
||||||
this._users.set(data.users);
|
|
||||||
this._orgs.set(data.orgs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
|
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
|
||||||
@ -104,6 +89,9 @@ export class AccountWidget extends Disposable {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const users = this._appModel.topAppModel.users;
|
||||||
|
const orgs = this._appModel.topAppModel.orgs;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
cssUserInfo(
|
cssUserInfo(
|
||||||
createUserImage(user, 'large'),
|
createUserImage(user, 'large'),
|
||||||
@ -141,8 +129,8 @@ export class AccountWidget extends Disposable {
|
|||||||
// org-listing UI below.
|
// org-listing UI below.
|
||||||
this._appModel.topAppModel.isSingleOrg ? [] : [
|
this._appModel.topAppModel.isSingleOrg ? [] : [
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
menuSubHeader(dom.text((use) => use(this._users).length > 1 ? 'Switch Accounts' : 'Accounts')),
|
menuSubHeader(dom.text((use) => use(users).length > 1 ? 'Switch Accounts' : 'Accounts')),
|
||||||
dom.forEach(this._users, (_user) => {
|
dom.forEach(users, (_user) => {
|
||||||
if (_user.id === user.id) { return null; }
|
if (_user.id === user.id) { return null; }
|
||||||
return menuItem(() => this._switchAccount(_user),
|
return menuItem(() => this._switchAccount(_user),
|
||||||
cssSmallIconWrap(createUserImage(_user, 'small')),
|
cssSmallIconWrap(createUserImage(_user, 'small')),
|
||||||
@ -154,11 +142,11 @@ export class AccountWidget extends Disposable {
|
|||||||
|
|
||||||
menuItemLink({href: getLogoutUrl()}, "Sign Out", testId('dm-log-out')),
|
menuItemLink({href: getLogoutUrl()}, "Sign Out", testId('dm-log-out')),
|
||||||
|
|
||||||
dom.maybe((use) => use(this._orgs).length > 0, () => [
|
dom.maybe((use) => use(orgs).length > 0, () => [
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
menuSubHeader('Switch Sites'),
|
menuSubHeader('Switch Sites'),
|
||||||
]),
|
]),
|
||||||
dom.forEach(this._orgs, (org) =>
|
dom.forEach(orgs, (org) =>
|
||||||
menuItemLink(urlState().setLinkUrl({org: org.domain || undefined}),
|
menuItemLink(urlState().setLinkUrl({org: org.domain || undefined}),
|
||||||
cssOrgSelected.cls('', this._appModel.currentOrg ? org.id === this._appModel.currentOrg.id : false),
|
cssOrgSelected.cls('', this._appModel.currentOrg ? org.id === this._appModel.currentOrg.id : false),
|
||||||
getOrgName(org),
|
getOrgName(org),
|
||||||
@ -231,21 +219,6 @@ const cssOtherEmail = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssOrgSelected = styled('div', `
|
|
||||||
background-color: ${colors.dark};
|
|
||||||
color: ${colors.light};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssOrgCheckmark = styled(icon, `
|
|
||||||
flex: none;
|
|
||||||
margin-left: 16px;
|
|
||||||
--icon-color: ${colors.light};
|
|
||||||
display: none;
|
|
||||||
.${cssOrgSelected.className} > & {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssCheckmark = styled(icon, `
|
const cssCheckmark = styled(icon, `
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
@ -1,23 +1,75 @@
|
|||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes';
|
import {getTheme} from 'app/client/ui/CustomThemes';
|
||||||
import {cssLeftPane} from 'app/client/ui/PagePanels';
|
import {cssLeftPane} from 'app/client/ui/PagePanels';
|
||||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||||
import * as version from 'app/common/version';
|
import * as version from 'app/common/version';
|
||||||
import {BindableValue, dom, styled} from "grainjs";
|
import {BindableValue, Disposable, dom, styled} from "grainjs";
|
||||||
|
import {menu, menuDivider, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||||
|
import {getOrgName} from 'app/common/UserAPI';
|
||||||
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
|
||||||
|
|
||||||
|
export class AppHeader extends Disposable {
|
||||||
|
constructor(private _orgName: BindableValue<string>, private _appModel: AppModel) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
const theme = getTheme(this._appModel.topAppModel.productFlavor);
|
||||||
|
|
||||||
export function appHeader(orgName: BindableValue<string>, productFlavor: ProductFlavor) {
|
|
||||||
const theme = getTheme(productFlavor);
|
|
||||||
return cssAppHeader(
|
return cssAppHeader(
|
||||||
urlState().setLinkUrl({}),
|
|
||||||
cssAppHeader.cls('-widelogo', theme.wideLogo || false),
|
cssAppHeader.cls('-widelogo', theme.wideLogo || false),
|
||||||
// Show version when hovering over the application icon.
|
// Show version when hovering over the application icon.
|
||||||
cssAppLogo({title: `Ver ${version.version} (${version.gitcommit})`}),
|
cssAppLogo(
|
||||||
cssOrgName(dom.text(orgName)),
|
{title: `Ver ${version.version} (${version.gitcommit})`},
|
||||||
|
urlState().setLinkUrl({}),
|
||||||
|
testId('dm-logo')
|
||||||
|
),
|
||||||
|
cssOrg(
|
||||||
|
cssOrgName(dom.text(this._orgName)),
|
||||||
|
this._orgName && cssDropdownIcon('Dropdown'),
|
||||||
|
menu(() => this._makeOrgMenu(), {placement: 'bottom-start'}),
|
||||||
testId('dm-org'),
|
testId('dm-org'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeOrgMenu() {
|
||||||
|
const orgs = this._appModel.topAppModel.orgs;
|
||||||
|
|
||||||
|
return [
|
||||||
|
menuItemLink(urlState().setLinkUrl({}), 'Go to Home Page', testId('orgmenu-home-page')),
|
||||||
|
menuDivider(),
|
||||||
|
menuSubHeader('Switch Sites'),
|
||||||
|
dom.forEach(orgs, (org) =>
|
||||||
|
menuItemLink(urlState().setLinkUrl({org: org.domain || undefined}),
|
||||||
|
cssOrgSelected.cls('', this._appModel.currentOrg ? org.id === this._appModel.currentOrg.id : false),
|
||||||
|
getOrgName(org),
|
||||||
|
cssOrgCheckmark('Tick', testId('orgmenu-org-tick')),
|
||||||
|
testId('orgmenu-org'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssAppHeader = styled('a', `
|
export const cssOrgSelected = styled('div', `
|
||||||
|
background-color: ${colors.dark};
|
||||||
|
color: ${colors.light};
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const cssOrgCheckmark = styled(icon, `
|
||||||
|
flex: none;
|
||||||
|
margin-left: 16px;
|
||||||
|
--icon-color: ${colors.light};
|
||||||
|
display: none;
|
||||||
|
.${cssOrgSelected.className} > & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssAppHeader = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -29,7 +81,7 @@ const cssAppHeader = styled('a', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssAppLogo = styled('div', `
|
const cssAppLogo = styled('a', `
|
||||||
flex: none;
|
flex: none;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
@ -49,8 +101,23 @@ const cssAppLogo = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssDropdownIcon = styled(icon, `
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOrg = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
max-width: calc(100% - 48px);
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
const cssOrgName = styled('div', `
|
const cssOrgName = styled('div', `
|
||||||
padding: 0px 16px;
|
padding-left: 16px;
|
||||||
|
padding-right: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -5,7 +5,7 @@ import {AppModel, TopAppModel} from 'app/client/models/AppModel';
|
|||||||
import {DocPageModelImpl} from 'app/client/models/DocPageModel';
|
import {DocPageModelImpl} from 'app/client/models/DocPageModel';
|
||||||
import {HomeModelImpl} from 'app/client/models/HomeModel';
|
import {HomeModelImpl} from 'app/client/models/HomeModel';
|
||||||
import {App} from 'app/client/ui/App';
|
import {App} from 'app/client/ui/App';
|
||||||
import {appHeader} from 'app/client/ui/AppHeader';
|
import {AppHeader} from 'app/client/ui/AppHeader';
|
||||||
import {createBottomBarDoc} from 'app/client/ui/BottomBar';
|
import {createBottomBarDoc} from 'app/client/ui/BottomBar';
|
||||||
import {createDocMenu} from 'app/client/ui/DocMenu';
|
import {createDocMenu} from 'app/client/ui/DocMenu';
|
||||||
import {createForbiddenPage, createNotFoundPage, createOtherErrorPage} from 'app/client/ui/errorPages';
|
import {createForbiddenPage, createNotFoundPage, createOtherErrorPage} from 'app/client/ui/errorPages';
|
||||||
@ -96,7 +96,7 @@ function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) {
|
|||||||
panelWidth: Observable.create(owner, 240),
|
panelWidth: Observable.create(owner, 240),
|
||||||
panelOpen: leftPanelOpen,
|
panelOpen: leftPanelOpen,
|
||||||
hideOpener: true,
|
hideOpener: true,
|
||||||
header: appHeader(appModel.currentOrgName, appModel.topAppModel.productFlavor),
|
header: dom.create(AppHeader, appModel.currentOrgName, appModel),
|
||||||
content: createHomeLeftPane(leftPanelOpen, pageModel),
|
content: createHomeLeftPane(leftPanelOpen, pageModel),
|
||||||
},
|
},
|
||||||
headerMain: createTopBarHome(appModel),
|
headerMain: createTopBarHome(appModel),
|
||||||
@ -136,7 +136,7 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App)
|
|||||||
leftPanel: {
|
leftPanel: {
|
||||||
panelWidth: leftPanelWidth,
|
panelWidth: leftPanelWidth,
|
||||||
panelOpen: leftPanelOpen,
|
panelOpen: leftPanelOpen,
|
||||||
header: appHeader(appModel.currentOrgName || pageModel.currentOrgName, appModel.topAppModel.productFlavor),
|
header: dom.create(AppHeader, appModel.currentOrgName || pageModel.currentOrgName, appModel),
|
||||||
content: pageModel.createLeftPane(leftPanelOpen),
|
content: pageModel.createLeftPane(leftPanelOpen),
|
||||||
},
|
},
|
||||||
rightPanel: {
|
rightPanel: {
|
||||||
|
@ -2,7 +2,7 @@ import {beaconOpenMessage} from 'app/client/lib/helpScout';
|
|||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||||
import {BillingModel, BillingModelImpl, ISubscriptionModel} from 'app/client/models/BillingModel';
|
import {BillingModel, BillingModelImpl, ISubscriptionModel} from 'app/client/models/BillingModel';
|
||||||
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {appHeader} from 'app/client/ui/AppHeader';
|
import {AppHeader} from 'app/client/ui/AppHeader';
|
||||||
import {BillingForm, IFormData} from 'app/client/ui/BillingForm';
|
import {BillingForm, IFormData} from 'app/client/ui/BillingForm';
|
||||||
import * as css from 'app/client/ui/BillingPageCss';
|
import * as css from 'app/client/ui/BillingPageCss';
|
||||||
import {BillingPlanManagers} from 'app/client/ui/BillingPlanManagers';
|
import {BillingPlanManagers} from 'app/client/ui/BillingPlanManagers';
|
||||||
@ -64,7 +64,7 @@ export class BillingPage extends Disposable {
|
|||||||
panelWidth: Observable.create(this, 240),
|
panelWidth: Observable.create(this, 240),
|
||||||
panelOpen,
|
panelOpen,
|
||||||
hideOpener: true,
|
hideOpener: true,
|
||||||
header: appHeader(this._appModel.currentOrgName, this._appModel.topAppModel.productFlavor),
|
header: dom.create(AppHeader, this._appModel.currentOrgName, this._appModel),
|
||||||
content: leftPanelBasic(this._appModel, panelOpen),
|
content: leftPanelBasic(this._appModel, panelOpen),
|
||||||
},
|
},
|
||||||
headerMain: this._createTopBarBilling(),
|
headerMain: this._createTopBarBilling(),
|
||||||
|
@ -84,6 +84,7 @@ function createLoadedDocMenu(home: HomeModel) {
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
dom.maybe(home.available, () => [
|
dom.maybe(home.available, () => [
|
||||||
|
buildOtherSites(home),
|
||||||
(showIntro && page === 'all' ?
|
(showIntro && page === 'all' ?
|
||||||
null :
|
null :
|
||||||
css.docListHeader(
|
css.docListHeader(
|
||||||
@ -232,6 +233,48 @@ function buildAllTemplates(home: HomeModel, templateWorkspaces: Observable<Works
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the Other Sites section if there are any to show. Otherwise, builds nothing.
|
||||||
|
*/
|
||||||
|
function buildOtherSites(home: HomeModel) {
|
||||||
|
return dom.domComputed(home.otherSites, sites => {
|
||||||
|
if (sites.length === 0) { return null; }
|
||||||
|
|
||||||
|
const hideOtherSitesObs = Observable.create(null, false);
|
||||||
|
return css.otherSitesBlock(
|
||||||
|
dom.autoDispose(hideOtherSitesObs),
|
||||||
|
css.otherSitesHeader(
|
||||||
|
'Other Sites',
|
||||||
|
dom.domComputed(hideOtherSitesObs, (collapsed) =>
|
||||||
|
collapsed ? css.otherSitesHeaderIcon('Expand') : css.otherSitesHeaderIcon('Collapse')
|
||||||
|
),
|
||||||
|
dom.on('click', () => hideOtherSitesObs.set(!hideOtherSitesObs.get())),
|
||||||
|
testId('other-sites-header'),
|
||||||
|
),
|
||||||
|
dom.maybe((use) => !use(hideOtherSitesObs), () => {
|
||||||
|
const onPersonalSite = Boolean(home.app.currentOrg?.owner);
|
||||||
|
const siteName = onPersonalSite ? 'your personal site' : `the ${home.app.currentOrgName} site`;
|
||||||
|
return [
|
||||||
|
dom('div',
|
||||||
|
`You are on ${siteName}. You also have access to the following sites:`,
|
||||||
|
testId('other-sites-message')
|
||||||
|
),
|
||||||
|
css.otherSitesButtons(
|
||||||
|
dom.forEach(sites, s =>
|
||||||
|
css.siteButton(
|
||||||
|
s.name,
|
||||||
|
urlState().setLinkUrl({org: s.domain ?? undefined}),
|
||||||
|
testId('other-sites-button')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
testId('other-sites-buttons')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the widget for selecting sort and view mode options.
|
* Build the widget for selecting sort and view mode options.
|
||||||
* If hideSort is true, will hide the sort dropdown: it has no effect on the list of examples, so
|
* If hideSort is true, will hide the sort dropdown: it has no effect on the list of examples, so
|
||||||
|
@ -2,6 +2,7 @@ import {transientInput} from 'app/client/ui/transientInput';
|
|||||||
import {colors, mediaSmall, vars} from 'app/client/ui2018/cssVars';
|
import {colors, mediaSmall, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {styled} from 'grainjs';
|
import {styled} from 'grainjs';
|
||||||
|
import {bigBasicButton} from 'app/client/ui2018/buttons';
|
||||||
|
|
||||||
// The "&:after" clause forces some padding below all docs.
|
// The "&:after" clause forces some padding below all docs.
|
||||||
export const docList = styled('div', `
|
export const docList = styled('div', `
|
||||||
@ -40,6 +41,8 @@ export const featuredTemplatesHeader = styled(docListHeader, `
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const otherSitesHeader = templatesHeader;
|
||||||
|
|
||||||
export const docBlock = styled('div', `
|
export const docBlock = styled('div', `
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
@ -54,6 +57,23 @@ export const templatesDocBlock = styled(docBlock, `
|
|||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const otherSitesBlock = styled('div', `
|
||||||
|
margin-bottom: 32px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const otherSitesButtons = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
gap: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const siteButton = styled(bigBasicButton, `
|
||||||
|
flex: 0 0 auto;
|
||||||
|
`);
|
||||||
|
|
||||||
export const docHeaderIconDark = styled(icon, `
|
export const docHeaderIconDark = styled(icon, `
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
@ -74,6 +94,8 @@ export const templatesHeaderIcon = styled(docHeaderIcon, `
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const otherSitesHeaderIcon = templatesHeaderIcon;
|
||||||
|
|
||||||
const docBlockHeader = `
|
const docBlockHeader = `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -4,7 +4,7 @@ import { submitForm } from "app/client/lib/uploads";
|
|||||||
import { AppModel, reportError } from "app/client/models/AppModel";
|
import { AppModel, reportError } from "app/client/models/AppModel";
|
||||||
import { getLoginUrl, getSignupUrl, urlState } from "app/client/models/gristUrlState";
|
import { getLoginUrl, getSignupUrl, urlState } from "app/client/models/gristUrlState";
|
||||||
import { AccountWidget } from "app/client/ui/AccountWidget";
|
import { AccountWidget } from "app/client/ui/AccountWidget";
|
||||||
import { appHeader } from 'app/client/ui/AppHeader';
|
import { AppHeader } from 'app/client/ui/AppHeader';
|
||||||
import * as BillingPageCss from "app/client/ui/BillingPageCss";
|
import * as BillingPageCss from "app/client/ui/BillingPageCss";
|
||||||
import * as forms from "app/client/ui/forms";
|
import * as forms from "app/client/ui/forms";
|
||||||
import { pagePanels } from "app/client/ui/PagePanels";
|
import { pagePanels } from "app/client/ui/PagePanels";
|
||||||
@ -73,7 +73,7 @@ export class WelcomePage extends Disposable {
|
|||||||
panelWidth: Observable.create(this, 240),
|
panelWidth: Observable.create(this, 240),
|
||||||
panelOpen: Observable.create(this, false),
|
panelOpen: Observable.create(this, false),
|
||||||
hideOpener: true,
|
hideOpener: true,
|
||||||
header: appHeader('', this._appModel.topAppModel.productFlavor),
|
header: dom.create(AppHeader, '', this._appModel),
|
||||||
content: null,
|
content: null,
|
||||||
},
|
},
|
||||||
headerMain: [cssFlexSpace(), dom.create(AccountWidget, this._appModel)],
|
headerMain: [cssFlexSpace(), dom.create(AccountWidget, this._appModel)],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {appHeader} from 'app/client/ui/AppHeader';
|
import {AppHeader} from 'app/client/ui/AppHeader';
|
||||||
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
|
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
|
||||||
import {pagePanels} from 'app/client/ui/PagePanels';
|
import {pagePanels} from 'app/client/ui/PagePanels';
|
||||||
import {createTopBarHome} from 'app/client/ui/TopBar';
|
import {createTopBarHome} from 'app/client/ui/TopBar';
|
||||||
@ -105,7 +105,7 @@ function pagePanelsError(appModel: AppModel, header: string, content: DomElement
|
|||||||
panelWidth: observable(240),
|
panelWidth: observable(240),
|
||||||
panelOpen,
|
panelOpen,
|
||||||
hideOpener: true,
|
hideOpener: true,
|
||||||
header: appHeader(appModel.currentOrgName, appModel.topAppModel.productFlavor),
|
header: dom.create(AppHeader, appModel.currentOrgName, appModel),
|
||||||
content: leftPanelBasic(appModel, panelOpen),
|
content: leftPanelBasic(appModel, panelOpen),
|
||||||
},
|
},
|
||||||
headerMain: createTopBarHome(appModel),
|
headerMain: createTopBarHome(appModel),
|
||||||
|
Loading…
Reference in New Issue
Block a user