mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Tweak navbar, breadcrumbs, and sign-in buttons
Summary: The changes are intended to smooth over some sharp edges when a signed-out user is using Grist (particularly while on the templates site). Test Plan: Browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3957
This commit is contained in:
@@ -13,7 +13,7 @@ import {SupportGristNudge} from 'app/client/ui/SupportGristNudge';
|
||||
import {attachCssThemeVars, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features, isLegacyPlan, Product} from 'app/common/Features';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {GristLoadConfig, IGristUrlState} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
|
||||
@@ -23,7 +23,7 @@ import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
|
||||
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
||||
import {getThemeColors} from 'app/common/Themes';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getUserPrefObs, getUserPrefsObs, markAsSeen, markAsUnSeen} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface AppModel {
|
||||
isPersonal: boolean; // Is it a personal site?
|
||||
isTeamSite: boolean; // Is it a team site?
|
||||
isLegacySite: boolean; // Is it a legacy site?
|
||||
isTemplatesSite: boolean; // Is it the templates site?
|
||||
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
||||
lastVisitedOrgDomain: Observable<string|null>;
|
||||
|
||||
@@ -249,6 +250,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
public readonly isPersonal = Boolean(this.currentOrg?.owner);
|
||||
public readonly isTeamSite = Boolean(this.currentOrg) && !this.isPersonal;
|
||||
public readonly isLegacySite = Boolean(this.currentProduct && isLegacyPlan(this.currentProduct.name));
|
||||
public readonly isTemplatesSite = isTemplatesOrg(this.currentOrg);
|
||||
|
||||
public readonly userPrefsObs = getUserPrefsObs(this);
|
||||
public readonly themePrefs = getUserPrefObs(this.userPrefsObs, 'theme', {
|
||||
@@ -325,12 +327,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
this.behavioralPromptsManager.reset();
|
||||
};
|
||||
|
||||
this.autoDispose(subscribe(urlState().state, async (_use, {doc, org}) => {
|
||||
// Keep track of the last valid org domain the user visited, ignoring those
|
||||
// with a document id in the URL.
|
||||
if (!this.currentOrg || doc) { return; }
|
||||
|
||||
this.lastVisitedOrgDomain.set(org ?? null);
|
||||
this.autoDispose(subscribe(urlState().state, this.topAppModel.orgs, async (_use, s, orgs) => {
|
||||
this._updateLastVisitedOrgDomain(s, orgs);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -404,6 +402,23 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
return computed;
|
||||
}
|
||||
|
||||
private _updateLastVisitedOrgDomain({doc, org}: IGristUrlState, availableOrgs: Organization[]) {
|
||||
if (
|
||||
!org ||
|
||||
// Invalid or inaccessible sites shouldn't be counted as visited.
|
||||
!this.currentOrg ||
|
||||
// Visits to a document shouldn't be counted either.
|
||||
doc
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only count sites that a user has access to (i.e. those listed in the Site Switcher).
|
||||
if (!availableOrgs.some(({domain}) => domain === org)) { return; }
|
||||
|
||||
this.lastVisitedOrgDomain.set(org);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current user is a new user, record a sign-up event via Google Tag Manager.
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface DocInfo extends Document {
|
||||
isSnapshot: boolean;
|
||||
isTutorialTrunk: boolean;
|
||||
isTutorialFork: boolean;
|
||||
isTemplate: boolean;
|
||||
idParts: UrlIdParts;
|
||||
openMode: OpenDocMode;
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export interface DocPageModel {
|
||||
isSnapshot: Observable<boolean>;
|
||||
isTutorialTrunk: Observable<boolean>;
|
||||
isTutorialFork: Observable<boolean>;
|
||||
isTemplate: Observable<boolean>;
|
||||
|
||||
importSources: ImportSource[];
|
||||
|
||||
@@ -131,6 +133,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
(use, doc) => doc ? doc.isTutorialTrunk : false);
|
||||
public readonly isTutorialFork = Computed.create(this, this.currentDoc,
|
||||
(use, doc) => doc ? doc.isTutorialFork : false);
|
||||
public readonly isTemplate = Computed.create(this, this.currentDoc,
|
||||
(use, doc) => doc ? doc.isTemplate : false);
|
||||
|
||||
public readonly importSources: ImportSource[] = [];
|
||||
|
||||
@@ -431,24 +435,33 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
|
||||
function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
|
||||
const idParts = parseUrlId(doc.urlId || doc.id);
|
||||
const isFork = Boolean(idParts.forkId || idParts.snapshotId);
|
||||
const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE;
|
||||
const isSnapshot = Boolean(idParts.snapshotId);
|
||||
const isTutorial = doc.type === 'tutorial';
|
||||
const isTutorialTrunk = isTutorial && !isFork && mode !== 'default';
|
||||
const isTutorialFork = isTutorial && isFork;
|
||||
|
||||
let openMode = mode;
|
||||
if (!openMode) {
|
||||
if (isFork) {
|
||||
// Ignore the document 'openMode' setting if the doc is an unsaved fork.
|
||||
if (isFork || isTutorialTrunk || isTutorialFork) {
|
||||
// Tutorials (if no explicit /m/default mode is set) automatically get or
|
||||
// create a fork on load, which then behaves as a document that is in default
|
||||
// mode. Since the document's 'openMode' has no effect, don't bother trying
|
||||
// to set it here, as it'll potentially be confusing for other code reading it.
|
||||
openMode = 'default';
|
||||
} else if (!isFork && doc.type === 'template') {
|
||||
// Templates should always open in fork mode by default.
|
||||
openMode = 'fork';
|
||||
} else {
|
||||
// Try to use the document's 'openMode' if it's set.
|
||||
openMode = doc.options?.openMode ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
const isPreFork = (openMode === 'fork');
|
||||
const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE;
|
||||
const isSnapshot = Boolean(idParts.snapshotId);
|
||||
const isTutorialTrunk = !isFork && doc.type === 'tutorial' && mode !== 'default';
|
||||
const isTutorialFork = isFork && doc.type === 'tutorial';
|
||||
const isPreFork = openMode === 'fork';
|
||||
const isTemplate = doc.type === 'template' && (isFork || isPreFork);
|
||||
const isEditable = !isSnapshot && (canEdit(doc.access) || isPreFork);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
isFork,
|
||||
@@ -459,6 +472,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
|
||||
isSnapshot,
|
||||
isTutorialTrunk,
|
||||
isTutorialFork,
|
||||
isTemplate,
|
||||
isReadonly: !isEditable,
|
||||
idParts,
|
||||
openMode,
|
||||
|
||||
@@ -11,6 +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 {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Document, Organization, Workspace} from 'app/common/UserAPI';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
import moment from 'moment';
|
||||
@@ -311,7 +312,9 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
// now, but it is good to show names to highlight the possibility of adding more.
|
||||
const nonSupportWss = Array.isArray(wss) ? wss.filter(ws => !ws.isSupportWorkspace) : null;
|
||||
this.singleWorkspace.set(
|
||||
!!nonSupportWss && nonSupportWss.length === 1 && _isSingleWorkspaceMode(this._app)
|
||||
// The anon personal site always has 0 non-support workspaces.
|
||||
nonSupportWss?.length === 0 ||
|
||||
nonSupportWss?.length === 1 && _isSingleWorkspaceMode(this._app)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -357,6 +360,9 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
* Only fetches featured (pinned) templates on the All Documents page.
|
||||
*/
|
||||
private async _maybeFetchTemplates(): Promise<Workspace[] | null> {
|
||||
const {templateOrg} = getGristConfig();
|
||||
if (!templateOrg) { return null; }
|
||||
|
||||
const currentPage = this.currentPage.get();
|
||||
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
|
||||
if (!shouldFetchTemplates) { return null; }
|
||||
@@ -366,10 +372,10 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
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.
|
||||
return null;
|
||||
reportError('Failed to load templates');
|
||||
}
|
||||
if (this.isDisposed()) { return null; }
|
||||
|
||||
for (const ws of templateWss) {
|
||||
for (const doc of ws.docs) {
|
||||
// Populate doc.workspace, which is used by DocMenu/PinnedDocs and
|
||||
|
||||
@@ -47,7 +47,7 @@ export class AccountPage extends Disposable {
|
||||
panelWidth: Observable.create(this, 240),
|
||||
panelOpen,
|
||||
hideOpener: true,
|
||||
header: dom.create(AppHeader, this._appModel.currentOrgName, this._appModel),
|
||||
header: dom.create(AppHeader, this._appModel),
|
||||
content: leftPanelBasic(this._appModel, panelOpen),
|
||||
},
|
||||
headerMain: this._buildHeaderMain(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {manageTeamUsers} from 'app/client/ui/OpenUserManager';
|
||||
import {createUserImage} from 'app/client/ui/UserImage';
|
||||
import * as viewport from 'app/client/ui/viewport';
|
||||
import {primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {bigPrimaryButtonLink, primaryButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {mediaDeviceNotSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||
@@ -20,8 +20,12 @@ import {getGristConfig} from 'app/common/urlUtils';
|
||||
const t = makeT('AccountWidget');
|
||||
|
||||
/**
|
||||
* Render the user-icon that opens the account menu. When no user is logged in, render a Sign-in
|
||||
* button instead.
|
||||
* Render the user-icon that opens the account menu.
|
||||
*
|
||||
* When no user is logged in, render "Sign In" and "Sign Up" buttons.
|
||||
*
|
||||
* When no user is logged in and a template document is open, render a "Use This Template"
|
||||
* button.
|
||||
*/
|
||||
export class AccountWidget extends Disposable {
|
||||
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
||||
@@ -30,20 +34,60 @@ export class AccountWidget extends Disposable {
|
||||
|
||||
public buildDom() {
|
||||
return cssAccountWidget(
|
||||
dom.domComputed(this._appModel.currentValidUser, (user) =>
|
||||
(user ?
|
||||
cssUserIcon(createUserImage(user, 'medium', testId('user-icon')),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
) :
|
||||
cssSignInButton(t("Sign in"), icon('Collapse'), testId('user-signin'),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
)
|
||||
)
|
||||
),
|
||||
dom.domComputed(use => {
|
||||
const isTemplate = Boolean(this._docPageModel && use(this._docPageModel.isTemplate));
|
||||
const user = this._appModel.currentValidUser;
|
||||
if (!user && isTemplate) {
|
||||
return this._buildUseThisTemplateButton();
|
||||
} else if (!user) {
|
||||
return this._buildSignInAndSignUpButtons();
|
||||
} else {
|
||||
return this._buildAccountMenuButton(user);
|
||||
}
|
||||
}),
|
||||
testId('dm-account'),
|
||||
);
|
||||
}
|
||||
|
||||
private _buildAccountMenuButton(user: FullUser|null) {
|
||||
return cssUserIcon(
|
||||
createUserImage(user, 'medium', testId('user-icon')),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
);
|
||||
}
|
||||
|
||||
private _buildSignInAndSignUpButtons() {
|
||||
return [
|
||||
cssSigninButton(t('Sign In'),
|
||||
cssSigninButton.cls('-secondary'),
|
||||
dom.attr('href', use => {
|
||||
// Keep the redirect param of the login URL fresh.
|
||||
use(urlState().state);
|
||||
return getLoginUrl();
|
||||
}),
|
||||
testId('user-sign-in'),
|
||||
),
|
||||
cssSigninButton(t('Sign Up'),
|
||||
dom.attr('href', use => {
|
||||
// Keep the redirect param of the signup URL fresh.
|
||||
use(urlState().state);
|
||||
return getSignupUrl();
|
||||
}),
|
||||
testId('user-sign-up'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _buildUseThisTemplateButton() {
|
||||
return cssUseThisTemplateButton(t('Use This Template'),
|
||||
dom.attr('href', use => {
|
||||
// Keep the redirect param of the login/signup URL fresh.
|
||||
use(urlState().state);
|
||||
return getLoginOrSignupUrl();
|
||||
}),
|
||||
testId('dm-account-use-this-template'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
|
||||
@@ -187,6 +231,7 @@ export class AccountWidget extends Disposable {
|
||||
}
|
||||
|
||||
const cssAccountWidget = styled('div', `
|
||||
display: flex;
|
||||
margin-right: 16px;
|
||||
white-space: nowrap;
|
||||
`);
|
||||
@@ -251,8 +296,22 @@ const cssSmallDeviceOnly = styled(menuItem, `
|
||||
}
|
||||
`);
|
||||
|
||||
const cssSignInButton = styled(primaryButton, `
|
||||
const cssSigninButton = styled(bigPrimaryButtonLink, `
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
min-height: unset;
|
||||
height: 36px;
|
||||
padding: 8px 16px 8px 16px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
|
||||
&-secondary, &-secondary:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: ${theme.text};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssUseThisTemplateButton = styled(primaryButtonLink, `
|
||||
margin: 8px;
|
||||
`);
|
||||
|
||||
@@ -4,14 +4,15 @@ import {cssLeftPane} from 'app/client/ui/PagePanels';
|
||||
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import * as version from 'app/common/version';
|
||||
import {menu, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||
import {isTemplatesOrg, Organization} from 'app/common/UserAPI';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {getOrgName, isTemplatesOrg, Organization} from 'app/common/UserAPI';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
||||
import {BindableValue, Disposable, dom, DomContents, styled} from 'grainjs';
|
||||
import {Computed, Disposable, dom, DomContents, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
|
||||
@@ -28,46 +29,95 @@ const productPills: {[name: string]: string|null} = {
|
||||
// Other plans are either personal, or grandfathered, or for testing.
|
||||
};
|
||||
|
||||
interface AppLogoOrgNameAndLink {
|
||||
name: string;
|
||||
link: AppLogoLink;
|
||||
org?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
type AppLogoLink = AppLogoOrgDomain | AppLogoHref;
|
||||
|
||||
interface AppLogoOrgDomain {
|
||||
type: 'domain';
|
||||
domain: string;
|
||||
}
|
||||
|
||||
interface AppLogoHref {
|
||||
type: 'href';
|
||||
href: string;
|
||||
}
|
||||
|
||||
export class AppHeader extends Disposable {
|
||||
constructor(private _orgName: BindableValue<string>, private _appModel: AppModel,
|
||||
private _docPageModel?: DocPageModel) {
|
||||
private _currentOrg = this._appModel.currentOrg;
|
||||
|
||||
/**
|
||||
* The name and link of the site shown next to the logo.
|
||||
*
|
||||
* The last visited site is used, if known. Otherwise, the current site is used.
|
||||
*/
|
||||
private _appLogoOrg = Computed.create<AppLogoOrgNameAndLink>(this, (use) => {
|
||||
const availableOrgs = use(this._appModel.topAppModel.orgs);
|
||||
const currentOrgName = (this._appModel.currentOrgName ||
|
||||
(this._docPageModel && use(this._docPageModel.currentOrgName))) ?? '';
|
||||
const lastVisitedOrgDomain = use(this._appModel.lastVisitedOrgDomain);
|
||||
return this._getAppLogoOrgNameAndLink({availableOrgs, currentOrgName, lastVisitedOrgDomain});
|
||||
});
|
||||
|
||||
private _appLogoOrgName = Computed.create(this, this._appLogoOrg, (_use, {name}) => name);
|
||||
|
||||
private _appLogoOrgLink = Computed.create(this, this._appLogoOrg, (_use, {link}) => link);
|
||||
|
||||
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
||||
super();
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
const productFlavor = getTheme(this._appModel.topAppModel.productFlavor);
|
||||
|
||||
const currentOrg = this._appModel.currentOrg;
|
||||
|
||||
return cssAppHeader(
|
||||
cssAppHeader.cls('-widelogo', productFlavor.wideLogo || false),
|
||||
// Show version when hovering over the application icon.
|
||||
// Include gitcommit when known. Cast version.gitcommit since, depending
|
||||
// on how Grist is compiled, tsc may believe it to be a constant and
|
||||
// believe that testing it is unnecessary.
|
||||
cssAppLogo(
|
||||
dom.domComputed(this._appLogoOrgLink, orgLink => cssAppLogo(
|
||||
// Show version when hovering over the application icon.
|
||||
// Include gitcommit when known. Cast version.gitcommit since, depending
|
||||
// on how Grist is compiled, tsc may believe it to be a constant and
|
||||
// believe that testing it is unnecessary.
|
||||
{title: `Version ${version.version}` +
|
||||
((version.gitcommit as string) !== 'unknown' ? ` (${version.gitcommit})` : '')},
|
||||
this._setHomePageUrl(),
|
||||
this._setHomePageUrl(orgLink),
|
||||
testId('dm-logo')
|
||||
),
|
||||
cssOrg(
|
||||
cssOrgName(dom.text(this._orgName), testId('dm-orgname')),
|
||||
productPill(currentOrg),
|
||||
this._orgName && cssDropdownIcon('Dropdown'),
|
||||
)),
|
||||
this._buildOrgLinkOrMenu(),
|
||||
);
|
||||
}
|
||||
|
||||
private _buildOrgLinkOrMenu() {
|
||||
const {currentValidUser, isPersonal, isTemplatesSite} = this._appModel;
|
||||
if (!currentValidUser && (isPersonal || isTemplatesSite)) {
|
||||
return cssOrgLink(
|
||||
cssOrgName(dom.text(this._appLogoOrgName), testId('dm-orgname')),
|
||||
{href: commonUrls.templates},
|
||||
testId('dm-org'),
|
||||
);
|
||||
} else {
|
||||
return cssOrg(
|
||||
cssOrgName(dom.text(this._appLogoOrgName), testId('dm-orgname')),
|
||||
productPill(this._currentOrg),
|
||||
dom.maybe(this._appLogoOrgName, () => cssDropdownIcon('Dropdown')),
|
||||
menu(() => [
|
||||
menuSubHeader(
|
||||
this._appModel.isTeamSite ? t("Team Site") : t("Personal Site")
|
||||
+ (this._appModel.isLegacySite ? ` (${t("Legacy")})` : ''),
|
||||
this._appModel.isPersonal
|
||||
? t("Personal Site") + (this._appModel.isLegacySite ? ` (${t("Legacy")})` : '')
|
||||
: t("Team Site"),
|
||||
testId('orgmenu-title'),
|
||||
),
|
||||
menuItemLink(urlState().setLinkUrl({}), t("Home Page"), testId('orgmenu-home-page')),
|
||||
|
||||
// Show 'Organization Settings' when on a home page of a valid org.
|
||||
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
||||
(!this._docPageModel && this._currentOrg && !this._currentOrg.owner ?
|
||||
menuItem(() => manageTeamUsersApp(this._appModel),
|
||||
'Manage Team', testId('orgmenu-manage-team'),
|
||||
dom.cls('disabled', !roles.canEditAccess(currentOrg.access))) :
|
||||
dom.cls('disabled', !roles.canEditAccess(this._currentOrg.access))) :
|
||||
// Don't show on doc pages, or for personal orgs.
|
||||
null),
|
||||
|
||||
@@ -77,16 +127,15 @@ export class AppHeader extends Disposable {
|
||||
maybeAddSiteSwitcherSection(this._appModel),
|
||||
], { placement: 'bottom-start' }),
|
||||
testId('dm-org'),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _setHomePageUrl() {
|
||||
const lastVisitedOrg = this._appModel.lastVisitedOrgDomain.get();
|
||||
if (lastVisitedOrg) {
|
||||
return urlState().setLinkUrl({org: lastVisitedOrg});
|
||||
private _setHomePageUrl(link: AppLogoLink) {
|
||||
if (link.type === 'href') {
|
||||
return {href: link.href};
|
||||
} else {
|
||||
return {href: getWelcomeHomeUrl()};
|
||||
return urlState().setLinkUrl({org: link.domain});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +172,50 @@ export class AppHeader extends Disposable {
|
||||
|
||||
return menuItemLink('Activation', urlState().setLinkUrl({activation: 'activation'}));
|
||||
}
|
||||
|
||||
private _getAppLogoOrgNameAndLink(params: {
|
||||
availableOrgs: Organization[],
|
||||
currentOrgName: string,
|
||||
lastVisitedOrgDomain: string|null,
|
||||
}): AppLogoOrgNameAndLink {
|
||||
const {
|
||||
currentValidUser,
|
||||
isPersonal,
|
||||
isTemplatesSite,
|
||||
} = this._appModel;
|
||||
if (!currentValidUser && (isPersonal || isTemplatesSite)) {
|
||||
// When signed out and not on a team site, link to the templates site.
|
||||
return {
|
||||
name: t('Grist Templates'),
|
||||
link: {
|
||||
type: 'href',
|
||||
href: commonUrls.templates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const {availableOrgs, currentOrgName, lastVisitedOrgDomain} = params;
|
||||
if (lastVisitedOrgDomain) {
|
||||
const lastVisitedOrg = availableOrgs.find(({domain}) => domain === lastVisitedOrgDomain);
|
||||
if (lastVisitedOrg) {
|
||||
return {
|
||||
name: getOrgName(lastVisitedOrg),
|
||||
link: {
|
||||
type: 'domain',
|
||||
domain: lastVisitedOrgDomain,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: currentOrgName ?? '',
|
||||
link: {
|
||||
type: 'href',
|
||||
href: getWelcomeHomeUrl(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function productPill(org: Organization|null, options: {large?: boolean} = {}): DomContents {
|
||||
@@ -198,6 +291,31 @@ const cssOrg = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
const cssOrgLink = styled('a', `
|
||||
display: none;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 48px);
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
font-weight: 500;
|
||||
color: ${theme.text};
|
||||
user-select: none;
|
||||
|
||||
&, &:hover, &:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${theme.text};
|
||||
background-color: ${theme.hover};
|
||||
}
|
||||
|
||||
.${cssLeftPane.className}-open & {
|
||||
display: flex;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssOrgName = styled('div', `
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
|
||||
@@ -106,7 +106,7 @@ function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) {
|
||||
panelWidth: Observable.create(owner, 240),
|
||||
panelOpen: leftPanelOpen,
|
||||
hideOpener: true,
|
||||
header: dom.create(AppHeader, appModel.currentOrgName, appModel),
|
||||
header: dom.create(AppHeader, appModel),
|
||||
content: createHomeLeftPane(leftPanelOpen, pageModel),
|
||||
},
|
||||
headerMain: createTopBarHome(appModel),
|
||||
@@ -153,7 +153,7 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App)
|
||||
leftPanel: {
|
||||
panelWidth: leftPanelWidth,
|
||||
panelOpen: leftPanelOpen,
|
||||
header: dom.create(AppHeader, appModel.currentOrgName || pageModel.currentOrgName, appModel, pageModel),
|
||||
header: dom.create(AppHeader, appModel, pageModel),
|
||||
content: pageModel.createLeftPane(leftPanelOpen),
|
||||
},
|
||||
rightPanel: {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/cli
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Workspace} from 'app/common/UserAPI';
|
||||
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
|
||||
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
||||
@@ -28,6 +29,7 @@ const t = makeT('HomeLeftPane');
|
||||
export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: HomeModel) {
|
||||
const creating = observable<boolean>(false);
|
||||
const renaming = observable<Workspace|null>(null);
|
||||
const isAnonymous = !home.app.currentValidUser;
|
||||
|
||||
return cssContent(
|
||||
dom.autoDispose(creating),
|
||||
@@ -109,14 +111,14 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
)),
|
||||
cssTools(
|
||||
cssPageEntry(
|
||||
dom.show(isFeatureEnabled("templates")),
|
||||
dom.show(isFeatureEnabled("templates") && Boolean(getGristConfig().templateOrg)),
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "templates"),
|
||||
cssPageLink(cssPageIcon('Board'), cssLinkText(t("Examples & Templates")),
|
||||
urlState().setLinkUrl({homePage: "templates"}),
|
||||
testId('dm-templates-page'),
|
||||
),
|
||||
),
|
||||
cssPageEntry(
|
||||
isAnonymous ? null : cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "trash"),
|
||||
cssPageLink(cssPageIcon('RemoveBig'), cssLinkText(t("Trash")),
|
||||
urlState().setLinkUrl({homePage: "trash"}),
|
||||
|
||||
@@ -47,7 +47,7 @@ export class SupportGristPage extends Disposable {
|
||||
panelWidth: Observable.create(this, 240),
|
||||
panelOpen,
|
||||
hideOpener: true,
|
||||
header: dom.create(AppHeader, this._appModel.currentOrgName, this._appModel),
|
||||
header: dom.create(AppHeader, this._appModel),
|
||||
content: leftPanelBasic(this._appModel, panelOpen),
|
||||
},
|
||||
headerMain: this._buildMainHeader(),
|
||||
|
||||
@@ -24,6 +24,8 @@ import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, style
|
||||
const t = makeT('TopBar');
|
||||
|
||||
export function createTopBarHome(appModel: AppModel) {
|
||||
const isAnonymous = !appModel.currentValidUser;
|
||||
|
||||
return [
|
||||
cssFlexSpace(),
|
||||
appModel.supportGristNudge.showButton(),
|
||||
@@ -40,7 +42,7 @@ export function createTopBarHome(appModel: AppModel) {
|
||||
),
|
||||
|
||||
buildLanguageMenu(appModel),
|
||||
buildNotifyMenuButton(appModel.notifier, appModel),
|
||||
isAnonymous ? null : buildNotifyMenuButton(appModel.notifier, appModel),
|
||||
dom('div', dom.create(AccountWidget, appModel)),
|
||||
];
|
||||
}
|
||||
@@ -78,6 +80,8 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
return !use(undoStack.isDisabled);
|
||||
});
|
||||
|
||||
const isAnonymous = !pageModel.appModel.currentValidUser;
|
||||
|
||||
return [
|
||||
// TODO Before gristDoc is loaded, we could show doc-name without the page. For now, we delay
|
||||
// showing of breadcrumbs until gristDoc is loaded.
|
||||
@@ -96,6 +100,8 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork)),
|
||||
isSnapshot: pageModel.isSnapshot,
|
||||
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
||||
isTemplate: pageModel.isTemplate,
|
||||
isAnonymous,
|
||||
})
|
||||
)
|
||||
),
|
||||
@@ -121,23 +127,21 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
const model = use(searchModelObs);
|
||||
return model && use(moduleObs)?.searchBar(model, makeTestId('test-tb-search-'));
|
||||
}),
|
||||
|
||||
buildShareMenuButton(pageModel),
|
||||
|
||||
dom.maybe(use =>
|
||||
(
|
||||
use(pageModel.gristDoc)
|
||||
&& !use(use(pageModel.gristDoc)!.isReadonly)
|
||||
&& use(COMMENTS())
|
||||
dom.maybe(use => !(use(pageModel.isTemplate) && isAnonymous), () => [
|
||||
buildShareMenuButton(pageModel),
|
||||
dom.maybe(use =>
|
||||
(
|
||||
use(pageModel.gristDoc)
|
||||
&& !use(use(pageModel.gristDoc)!.isReadonly)
|
||||
&& use(COMMENTS())
|
||||
),
|
||||
() => buildShowDiscussionButton(pageModel)),
|
||||
dom.update(
|
||||
buildNotifyMenuButton(appModel.notifier, appModel),
|
||||
cssHideForNarrowScreen.cls(''),
|
||||
),
|
||||
() => buildShowDiscussionButton(pageModel)),
|
||||
|
||||
dom.update(
|
||||
buildNotifyMenuButton(appModel.notifier, appModel),
|
||||
cssHideForNarrowScreen.cls(''),
|
||||
),
|
||||
|
||||
dom('div', dom.create(AccountWidget, appModel, pageModel))
|
||||
]),
|
||||
dom('div', dom.create(AccountWidget, appModel, pageModel)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export class WelcomePage extends Disposable {
|
||||
panelWidth: Observable.create(this, 240),
|
||||
panelOpen: Observable.create(this, false),
|
||||
hideOpener: true,
|
||||
header: dom.create(AppHeader, '', this._appModel),
|
||||
header: dom.create(AppHeader, this._appModel),
|
||||
content: null,
|
||||
},
|
||||
headerMain: [cssFlexSpace(), dom.create(AccountWidget, this._appModel)],
|
||||
|
||||
@@ -110,7 +110,7 @@ function pagePanelsError(appModel: AppModel, header: string, content: DomElement
|
||||
panelWidth: observable(240),
|
||||
panelOpen,
|
||||
hideOpener: true,
|
||||
header: dom.create(AppHeader, appModel.currentOrgName, appModel),
|
||||
header: dom.create(AppHeader, appModel),
|
||||
content: leftPanelBasic(appModel, panelOpen),
|
||||
},
|
||||
headerMain: createTopBarHome(appModel),
|
||||
|
||||
@@ -100,10 +100,13 @@ export function docBreadcrumbs(
|
||||
isRecoveryMode: Observable<boolean>,
|
||||
isSnapshot?: Observable<boolean>,
|
||||
isPublic?: Observable<boolean>,
|
||||
isTemplate?: Observable<boolean>,
|
||||
isAnonymous?: boolean,
|
||||
}
|
||||
): Element {
|
||||
const shouldShowWorkspace = !(options.isTemplate && options.isAnonymous);
|
||||
return cssBreadcrumbs(
|
||||
dom.domComputed<[boolean, PartialWorkspace|null]>(
|
||||
!shouldShowWorkspace ? null : dom.domComputed<[boolean, PartialWorkspace|null]>(
|
||||
(use) => [use(options.isBareFork), use(workspace)],
|
||||
([isBareFork, ws]) => {
|
||||
if (isBareFork || !ws) { return null; }
|
||||
|
||||
Reference in New Issue
Block a user