2021-08-18 17:49:34 +00:00
|
|
|
import {AppModel} from 'app/client/models/AppModel';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
2023-07-26 22:31:02 +00:00
|
|
|
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
2024-03-23 17:11:06 +00:00
|
|
|
import {getAdminPanelName} from 'app/client/ui/AdminPanel';
|
2022-06-03 14:58:07 +00:00
|
|
|
import {manageTeamUsers} from 'app/client/ui/OpenUserManager';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {createUserImage} from 'app/client/ui/UserImage';
|
2021-01-21 19:12:24 +00:00
|
|
|
import * as viewport from 'app/client/ui/viewport';
|
2023-07-26 22:31:02 +00:00
|
|
|
import {bigPrimaryButtonLink, primaryButtonLink} from 'app/client/ui2018/buttons';
|
2022-09-06 01:51:57 +00:00
|
|
|
import {mediaDeviceNotSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
|
|
import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
2023-05-20 23:58:41 +00:00
|
|
|
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
|
|
|
import * as roles from 'app/common/roles';
|
2021-08-18 17:49:34 +00:00
|
|
|
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {cssMenuItem} from 'popweasel';
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
2022-10-28 16:11:08 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2023-06-06 17:08:50 +00:00
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2022-10-28 16:11:08 +00:00
|
|
|
|
|
|
|
const t = makeT('AccountWidget');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
/**
|
2023-07-26 22:31:02 +00:00
|
|
|
* 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.
|
2020-10-02 15:10:00 +00:00
|
|
|
*/
|
|
|
|
export class AccountWidget extends Disposable {
|
|
|
|
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
public buildDom() {
|
|
|
|
return cssAccountWidget(
|
2023-07-26 22:31:02 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}),
|
2020-10-02 15:10:00 +00:00
|
|
|
testId('dm-account'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
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'),
|
2023-10-10 13:49:36 +00:00
|
|
|
dom.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
2023-07-26 22:31:02 +00:00
|
|
|
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'),
|
2023-10-10 13:49:36 +00:00
|
|
|
dom.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
2023-07-26 22:31:02 +00:00
|
|
|
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 => {
|
2023-09-06 18:35:46 +00:00
|
|
|
const {doc: srcDocId} = use(urlState().state);
|
|
|
|
return getLoginOrSignupUrl({srcDocId});
|
2023-07-26 22:31:02 +00:00
|
|
|
}),
|
2023-09-06 18:35:46 +00:00
|
|
|
dom.on('click', () => { this._docPageModel?.clearUnsavedChanges(); }),
|
2023-07-26 22:31:02 +00:00
|
|
|
testId('dm-account-use-this-template'),
|
|
|
|
);
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
*/
|
2021-02-25 16:11:59 +00:00
|
|
|
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
|
2020-10-02 15:10:00 +00:00
|
|
|
const currentOrg = this._appModel.currentOrg;
|
2021-02-25 16:11:59 +00:00
|
|
|
|
|
|
|
// The 'Document Settings' item, when there is an open document.
|
2023-01-05 08:11:54 +00:00
|
|
|
const documentSettingsItem = this._docPageModel ? menuItemLink(
|
|
|
|
urlState().setLinkUrl({docPage: 'settings'}),
|
|
|
|
t("Document Settings"),
|
|
|
|
testId('dm-doc-settings')
|
|
|
|
) : null;
|
2021-02-25 16:11:59 +00:00
|
|
|
|
|
|
|
// 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.
|
2022-12-06 13:57:29 +00:00
|
|
|
t("Toggle Mobile Mode"),
|
2021-02-25 16:11:59 +00:00
|
|
|
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
|
|
|
|
testId('usermenu-toggle-mobile'),
|
2023-09-06 18:35:46 +00:00
|
|
|
);
|
2021-02-25 16:11:59 +00:00
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
return [
|
2022-12-06 13:57:29 +00:00
|
|
|
menuItemLink({href: getLoginOrSignupUrl()}, t("Sign in")),
|
2021-02-25 16:11:59 +00:00
|
|
|
menuDivider(),
|
|
|
|
documentSettingsItem,
|
2022-12-06 13:57:29 +00:00
|
|
|
menuItemLink({href: commonUrls.plans}, t("Pricing")),
|
2021-02-25 16:11:59 +00:00
|
|
|
mobileModeToggle,
|
|
|
|
];
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-08-18 17:49:34 +00:00
|
|
|
const users = this._appModel.topAppModel.users;
|
2022-05-18 10:25:14 +00:00
|
|
|
const isExternal = user?.loginMethod === 'External';
|
2020-10-02 15:10:00 +00:00
|
|
|
return [
|
|
|
|
cssUserInfo(
|
|
|
|
createUserImage(user, 'large'),
|
2022-01-07 18:11:52 +00:00
|
|
|
cssUserName(dom('span', user.name, testId('usermenu-name')),
|
2020-10-02 15:10:00 +00:00
|
|
|
cssEmail(user.email, testId('usermenu-email'))
|
|
|
|
)
|
|
|
|
),
|
2023-01-24 13:13:18 +00:00
|
|
|
menuItemLink(urlState().setLinkUrl({account: 'account'}), t("Profile Settings"), testId('dm-account-settings')),
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-02-25 16:11:59 +00:00
|
|
|
documentSettingsItem,
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// Show 'Organization Settings' when on a home page of a valid org.
|
2022-06-03 14:58:07 +00:00
|
|
|
(!this._docPageModel && currentOrg && this._appModel.isTeamSite ?
|
|
|
|
menuItem(() => manageTeamUsers(currentOrg, user, this._appModel.api),
|
2022-12-06 13:57:29 +00:00
|
|
|
roles.canEditAccess(currentOrg.access) ? t("Manage Team") : t("Access Details"),
|
2022-04-12 19:31:41 +00:00
|
|
|
testId('dm-org-access')) :
|
2020-10-02 15:10:00 +00:00
|
|
|
// Don't show on doc pages, or for personal orgs.
|
|
|
|
null),
|
|
|
|
|
2023-06-06 17:08:50 +00:00
|
|
|
this._maybeBuildBillingPageMenuItem(),
|
|
|
|
this._maybeBuildActivationPageMenuItem(),
|
2024-03-23 17:11:06 +00:00
|
|
|
this._maybeBuildAdminPanelMenuItem(),
|
|
|
|
this._maybeBuildSupportGristButton(),
|
2021-02-25 16:11:59 +00:00
|
|
|
mobileModeToggle,
|
2021-01-21 19:12:24 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// 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.
|
2023-05-20 23:58:41 +00:00
|
|
|
this._appModel.topAppModel.isSingleOrg || !isFeatureEnabled("multiAccounts") ? [] : [
|
2020-10-02 15:10:00 +00:00
|
|
|
menuDivider(),
|
2022-12-06 13:57:29 +00:00
|
|
|
menuSubHeader(dom.text((use) => use(users).length > 1 ? t("Switch Accounts") : t("Accounts"))),
|
2021-08-18 17:49:34 +00:00
|
|
|
dom.forEach(users, (_user) => {
|
2020-10-02 15:10:00 +00:00
|
|
|
if (_user.id === user.id) { return null; }
|
|
|
|
return menuItem(() => this._switchAccount(_user),
|
|
|
|
cssSmallIconWrap(createUserImage(_user, 'small')),
|
|
|
|
cssOtherEmail(_user.email, testId('usermenu-other-email')),
|
|
|
|
);
|
|
|
|
}),
|
2022-12-06 13:57:29 +00:00
|
|
|
isExternal ? null : menuItemLink({href: getLoginUrl()}, t("Add Account"), testId('dm-add-account')),
|
2020-10-02 15:10:00 +00:00
|
|
|
],
|
|
|
|
|
2022-12-06 13:57:29 +00:00
|
|
|
menuItemLink({href: getLogoutUrl()}, t("Sign Out"), testId('dm-log-out')),
|
2020-10-02 15:10:00 +00:00
|
|
|
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
maybeAddSiteSwitcherSection(this._appModel),
|
2020-10-02 15:10:00 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Switch BrowserSession to use the given user for the currently loaded org.
|
|
|
|
private async _switchAccount(user: FullUser) {
|
2023-09-19 05:31:22 +00:00
|
|
|
await this._appModel.switchUser(user);
|
2020-10-02 15:10:00 +00:00
|
|
|
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();
|
|
|
|
}
|
2023-06-06 17:08:50 +00:00
|
|
|
|
|
|
|
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 ?
|
2023-07-04 21:21:34 +00:00
|
|
|
menuItemLink(urlState().setLinkUrl({billing: 'billing'}), t('Billing Account')) :
|
|
|
|
menuItem(() => null, t('Billing Account'), dom.cls('disabled', true))
|
2023-06-06 17:08:50 +00:00
|
|
|
) :
|
2023-07-04 21:21:34 +00:00
|
|
|
menuItem(() => this._appModel.showUpgradeModal(), t('Upgrade Plan'));
|
2023-06-06 17:08:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private _maybeBuildActivationPageMenuItem() {
|
2024-03-23 17:11:06 +00:00
|
|
|
const {deploymentType} = getGristConfig();
|
|
|
|
if (deploymentType !== 'enterprise' || !this._appModel.isInstallAdmin()) {
|
2023-06-06 17:08:50 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-07-04 21:21:34 +00:00
|
|
|
return menuItemLink(t('Activation'), urlState().setLinkUrl({activation: 'activation'}));
|
|
|
|
}
|
|
|
|
|
2024-03-23 17:11:06 +00:00
|
|
|
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'),
|
|
|
|
);
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
2024-03-23 17:11:06 +00:00
|
|
|
}
|
2023-07-04 21:21:34 +00:00
|
|
|
|
2024-03-23 17:11:06 +00:00
|
|
|
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'),
|
|
|
|
);
|
|
|
|
}
|
2023-06-06 17:08:50 +00:00
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const cssAccountWidget = styled('div', `
|
2023-07-26 22:31:02 +00:00
|
|
|
display: flex;
|
2020-10-02 15:10:00 +00:00
|
|
|
margin-right: 16px;
|
|
|
|
white-space: nowrap;
|
|
|
|
`);
|
|
|
|
|
2022-10-17 09:47:16 +00:00
|
|
|
export const cssUserIcon = styled('div', `
|
2020-10-02 15:10:00 +00:00
|
|
|
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};
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.text};
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssEmail = styled('div', `
|
|
|
|
margin-top: 4px;
|
|
|
|
font-size: ${vars.smallFontSize};
|
|
|
|
font-weight: initial;
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.lightText};
|
2020-10-02 15:10:00 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
const cssSmallIconWrap = styled('div', `
|
|
|
|
flex: none;
|
|
|
|
margin: -4px 8px -4px 0px;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssOtherEmail = styled('div', `
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.lightText};
|
2020-10-02 15:10:00 +00:00
|
|
|
.${cssMenuItem.className}-sel & {
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.menuItemSelectedFg};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2021-01-21 19:12:24 +00:00
|
|
|
const cssCheckmark = styled(icon, `
|
|
|
|
flex: none;
|
|
|
|
margin-left: 16px;
|
2022-09-06 01:51:57 +00:00
|
|
|
--icon-color: ${theme.accentIcon};
|
2021-01-21 19:12:24 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`);
|
2021-02-25 16:11:59 +00:00
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
const cssSigninButton = styled(bigPrimaryButtonLink, `
|
2021-02-25 16:11:59 +00:00
|
|
|
display: flex;
|
2023-07-26 22:31:02 +00:00
|
|
|
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, `
|
2021-02-25 16:11:59 +00:00
|
|
|
margin: 8px;
|
|
|
|
`);
|