mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
4ed90faf79
Summary: 1. Unclear why Embed fails often. Locally, it fails for me every time, and this tweak makes it pass (while still keeping the test useful). 2. Reduced back main bundle's size by removing dependency of some common elements on the full AdminPanel. Updated expected size of errorPages bundle to the new reduced size. Test Plan: No changes to functionality; relying on existing tests to verify that. Reviewers: jordigh Reviewed By: jordigh Subscribers: georgegevoian, jordigh Differential Revision: https://phab.getgrist.com/D4315
325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import {AppModel} from 'app/client/models/AppModel';
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
|
import {getAdminPanelName} from 'app/client/ui/AdminPanelName';
|
|
import {manageTeamUsers} from 'app/client/ui/OpenUserManager';
|
|
import {createUserImage} from 'app/client/ui/UserImage';
|
|
import * as viewport from 'app/client/ui/viewport';
|
|
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';
|
|
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
|
import * as roles from 'app/common/roles';
|
|
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
|
import {cssMenuItem} from 'popweasel';
|
|
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
|
import {makeT} from 'app/client/lib/localization';
|
|
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 "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) {
|
|
super();
|
|
}
|
|
|
|
public buildDom() {
|
|
return cssAccountWidget(
|
|
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.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
|
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.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
|
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 => {
|
|
const {doc: srcDocId} = use(urlState().state);
|
|
return getLoginOrSignupUrl({srcDocId});
|
|
}),
|
|
dom.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
|
testId('dm-account-use-this-template'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
|
|
* Note that `user` should NOT be anonymous (none of the items are really relevant).
|
|
*/
|
|
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
|
|
const currentOrg = this._appModel.currentOrg;
|
|
|
|
// The 'Document Settings' item, when there is an open document.
|
|
const documentSettingsItem = this._docPageModel ? menuItemLink(
|
|
urlState().setLinkUrl({docPage: 'settings'}),
|
|
t("Document Settings"),
|
|
testId('dm-doc-settings')
|
|
) : null;
|
|
|
|
// The item to toggle mobile mode (presence of viewport meta tag).
|
|
const mobileModeToggle = menuItem(viewport.toggleViewport,
|
|
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
|
|
t("Toggle Mobile Mode"),
|
|
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
|
|
testId('usermenu-toggle-mobile'),
|
|
);
|
|
|
|
if (!user) {
|
|
return [
|
|
menuItemLink({href: getLoginOrSignupUrl()}, t("Sign in")),
|
|
menuDivider(),
|
|
documentSettingsItem,
|
|
menuItemLink({href: commonUrls.plans}, t("Pricing")),
|
|
mobileModeToggle,
|
|
];
|
|
}
|
|
|
|
const users = this._appModel.topAppModel.users;
|
|
const isExternal = user?.loginMethod === 'External';
|
|
return [
|
|
cssUserInfo(
|
|
createUserImage(user, 'large'),
|
|
cssUserName(dom('span', user.name, testId('usermenu-name')),
|
|
cssEmail(user.email, testId('usermenu-email'))
|
|
)
|
|
),
|
|
menuItemLink(urlState().setLinkUrl({account: 'account'}), t("Profile Settings"), testId('dm-account-settings')),
|
|
|
|
documentSettingsItem,
|
|
|
|
// Show 'Organization Settings' when on a home page of a valid org.
|
|
(!this._docPageModel && currentOrg && this._appModel.isTeamSite ?
|
|
menuItem(() => manageTeamUsers({org: currentOrg, user, api: this._appModel.api}),
|
|
roles.canEditAccess(currentOrg.access) ? t("Manage Team") : t("Access Details"),
|
|
testId('dm-org-access')) :
|
|
// Don't show on doc pages, or for personal orgs.
|
|
null),
|
|
|
|
this._maybeBuildBillingPageMenuItem(),
|
|
this._maybeBuildActivationPageMenuItem(),
|
|
this._maybeBuildAdminPanelMenuItem(),
|
|
this._maybeBuildSupportGristButton(),
|
|
mobileModeToggle,
|
|
|
|
// TODO Add section ("Here right now") listing icons of other users currently on this doc.
|
|
// (See Invision "Panels" near the bottom.)
|
|
|
|
// In case of a single-org setup, skip all the account-switching UI. We'll also skip the
|
|
// org-listing UI below.
|
|
this._appModel.topAppModel.isSingleOrg || !isFeatureEnabled("multiAccounts") ? [] : [
|
|
menuDivider(),
|
|
menuSubHeader(dom.text((use) => use(users).length > 1 ? t("Switch Accounts") : t("Accounts"))),
|
|
dom.forEach(users, (_user) => {
|
|
if (_user.id === user.id) { return null; }
|
|
return menuItem(() => this._switchAccount(_user),
|
|
cssSmallIconWrap(createUserImage(_user, 'small')),
|
|
cssOtherEmail(_user.email, testId('usermenu-other-email')),
|
|
);
|
|
}),
|
|
isExternal ? null : menuItemLink({href: getLoginUrl()}, t("Add Account"), testId('dm-add-account')),
|
|
],
|
|
|
|
menuItemLink({href: getLogoutUrl()}, t("Sign Out"), testId('dm-log-out')),
|
|
|
|
maybeAddSiteSwitcherSection(this._appModel),
|
|
];
|
|
}
|
|
|
|
// Switch BrowserSession to use the given user for the currently loaded org.
|
|
private async _switchAccount(user: FullUser) {
|
|
await this._appModel.switchUser(user);
|
|
if (urlState().state.get().doc) {
|
|
// Document access level may have changed.
|
|
// If it was not accessible but now is, we currently need to reload the page to get
|
|
// a complete gristConfig for the document from the server.
|
|
// If it was accessible but now is not, it would suffice to reconnect the web socket.
|
|
// For simplicity, just reload from server in either case.
|
|
// TODO: get fancier here to avoid reload.
|
|
window.location.reload(true);
|
|
return;
|
|
}
|
|
this._appModel.topAppModel.initialize();
|
|
}
|
|
|
|
private _maybeBuildBillingPageMenuItem() {
|
|
const {deploymentType} = getGristConfig();
|
|
if (deploymentType !== 'saas') { return null; }
|
|
|
|
const {currentValidUser, currentOrg, isTeamSite} = this._appModel;
|
|
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
|
(currentOrg.billingAccount.isManager || currentValidUser?.isSupport));
|
|
|
|
return isTeamSite ?
|
|
// For links, disabling with just a class is hard; easier to just not make it a link.
|
|
// TODO weasel menus should support disabling menuItemLink.
|
|
(isBillingManager ?
|
|
menuItemLink(urlState().setLinkUrl({billing: 'billing'}), t('Billing Account')) :
|
|
menuItem(() => null, t('Billing Account'), dom.cls('disabled', true))
|
|
) :
|
|
menuItem(() => this._appModel.showUpgradeModal(), t('Upgrade Plan'));
|
|
}
|
|
|
|
private _maybeBuildActivationPageMenuItem() {
|
|
const {deploymentType} = getGristConfig();
|
|
if (deploymentType !== 'enterprise' || !this._appModel.isInstallAdmin()) {
|
|
return null;
|
|
}
|
|
|
|
return menuItemLink(t('Activation'), urlState().setLinkUrl({activation: 'activation'}));
|
|
}
|
|
|
|
private _maybeBuildAdminPanelMenuItem() {
|
|
// Only show Admin Panel item to the installation admins.
|
|
if (this._appModel.currentUser?.isInstallAdmin) {
|
|
return menuItemLink(
|
|
getAdminPanelName(),
|
|
urlState().setLinkUrl({adminPanel: 'admin'}),
|
|
testId('usermenu-admin-panel'),
|
|
);
|
|
}
|
|
}
|
|
|
|
private _maybeBuildSupportGristButton() {
|
|
const {deploymentType} = getGristConfig();
|
|
const isEnabled = (deploymentType === 'core') && isFeatureEnabled("supportGrist");
|
|
if (isEnabled) {
|
|
return menuItemLink(t('Support Grist'), ' 💛',
|
|
{href: commonUrls.githubSponsorGristLabs, target: '_blank'},
|
|
testId('usermenu-support-grist'),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const cssAccountWidget = styled('div', `
|
|
display: flex;
|
|
margin-right: 16px;
|
|
white-space: nowrap;
|
|
`);
|
|
|
|
export const cssUserIcon = styled('div', `
|
|
height: 48px;
|
|
width: 48px;
|
|
padding: 8px;
|
|
cursor: pointer;
|
|
`);
|
|
|
|
const cssUserInfo = styled('div', `
|
|
padding: 12px 24px 12px 16px;
|
|
min-width: 200px;
|
|
display: flex;
|
|
align-items: center;
|
|
`);
|
|
|
|
const cssUserName = styled('div', `
|
|
margin-left: 8px;
|
|
font-size: ${vars.mediumFontSize};
|
|
font-weight: ${vars.headerControlTextWeight};
|
|
color: ${theme.text};
|
|
`);
|
|
|
|
const cssEmail = styled('div', `
|
|
margin-top: 4px;
|
|
font-size: ${vars.smallFontSize};
|
|
font-weight: initial;
|
|
color: ${theme.lightText};
|
|
`);
|
|
|
|
const cssSmallIconWrap = styled('div', `
|
|
flex: none;
|
|
margin: -4px 8px -4px 0px;
|
|
`);
|
|
|
|
const cssOtherEmail = styled('div', `
|
|
color: ${theme.lightText};
|
|
.${cssMenuItem.className}-sel & {
|
|
color: ${theme.menuItemSelectedFg};
|
|
}
|
|
`);
|
|
|
|
const cssCheckmark = styled(icon, `
|
|
flex: none;
|
|
margin-left: 16px;
|
|
--icon-color: ${theme.accentIcon};
|
|
`);
|
|
|
|
// Note that this css class hides the item when the device width is small (not based on viewport
|
|
// width, which may be larger). This only appropriate for when to enable the "mobile mode" toggle.
|
|
const cssSmallDeviceOnly = styled(menuItem, `
|
|
@media ${mediaDeviceNotSmall} {
|
|
& {
|
|
display: none;
|
|
}
|
|
}
|
|
`);
|
|
|
|
const cssSigninButton = styled(bigPrimaryButtonLink, `
|
|
display: flex;
|
|
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;
|
|
`);
|