mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Improve the look and behavior of /welcome/teams page (also shown for /welcome/start)
Summary: - Move css module for the login page css to core/, to be reusable in core/ pages. - Move /welcome/teams implementation to WelcomeSitePicker.ts - List users for personal sites, as well as team sites. - Add org param to setSessionActive() API method and end endpoint, to allow switching the specified org to another user. - Add a little safety to getOrgUrl() function. Test Plan: Added a test case for the new behaviors of the /welcome/teams page. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3914
This commit is contained in:
parent
812cded291
commit
2740884e3c
216
app/client/ui/LoginPagesCss.ts
Normal file
216
app/client/ui/LoginPagesCss.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import {bigPrimaryButton as gristBigPrimaryButton,
|
||||||
|
bigPrimaryButtonLink as gristBigPrimaryButtonLink,
|
||||||
|
textButton as gristTextButton} from 'app/client/ui2018/buttons';
|
||||||
|
import {colors, mediaXSmall, theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {textInput} from 'app/client/ui/inputs';
|
||||||
|
import {styled} from 'grainjs';
|
||||||
|
|
||||||
|
export const text = styled('div', `
|
||||||
|
color: ${theme.text};
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const lightText = styled(text, `
|
||||||
|
color: ${theme.lightText};
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const lightColor = styled('span', `
|
||||||
|
color: ${theme.lightText};
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const centeredText = styled(text, `
|
||||||
|
text-align: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const lightlyBolded = styled('span', `
|
||||||
|
font-weight: 500;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const input = textInput;
|
||||||
|
|
||||||
|
export const codeInput = styled(input, `
|
||||||
|
width: 200px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const label = styled('label', `
|
||||||
|
color: ${theme.text};
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formLabel = styled(label, `
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const googleButton = styled('button', `
|
||||||
|
/* Resets */
|
||||||
|
position: relative;
|
||||||
|
border-style: none;
|
||||||
|
|
||||||
|
/* Vars */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
color: ${colors.dark};
|
||||||
|
background-color: ${colors.lightGrey};
|
||||||
|
border: 1px solid ${colors.darkGrey};
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${colors.mediumGrey};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const image = styled('div', `
|
||||||
|
display: inline-block;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const gristLogo = styled(image, `
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
background-image: var(--icon-GristLogo);
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const googleLogo = styled(image, `
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-image: var(--icon-GoogleLogo);
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const loginMethodsSeparator = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 24px 0px 24px 0px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const horizontalLine = styled('hr', `
|
||||||
|
border: 1px solid ${theme.loginPageLine};
|
||||||
|
flex-grow: 1;
|
||||||
|
`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Consider using our own outline.
|
||||||
|
*
|
||||||
|
* We revert here to improve accessibility on the login pages. We could also
|
||||||
|
* leave the default outline alone, since it doesn't seem to appear on
|
||||||
|
* click anymore (in modern browsers, at least).
|
||||||
|
*/
|
||||||
|
export const bigPrimaryButton = styled(gristBigPrimaryButton, `
|
||||||
|
outline: revert;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const bigPrimaryButtonLink = styled(gristBigPrimaryButtonLink, `
|
||||||
|
outline: revert;
|
||||||
|
padding: 16px 32px 16px 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const textButton = styled(gristTextButton, `
|
||||||
|
outline: revert;
|
||||||
|
font-size: 14px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const pageContainer = styled('div', `
|
||||||
|
min-height: 100%;
|
||||||
|
background-color: ${theme.loginPageBackdrop};
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
background-color: ${theme.loginPageBg};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const centeredFlexContainer = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formContainer = styled('div', `
|
||||||
|
background-color: ${theme.loginPageBg};
|
||||||
|
max-width: 576px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 60px 25px 60px 25px;
|
||||||
|
padding: 40px 56px 40px 56px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 25px 20px 25px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formHeading = styled('div', `
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: ${theme.text};
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formInstructions = styled('div', `
|
||||||
|
margin-bottom: 32px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formError = styled(text, `
|
||||||
|
color: ${theme.errorText};
|
||||||
|
margin-bottom: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const centeredFormError = styled(formError, `
|
||||||
|
text-align: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formButtons = styled('div', `
|
||||||
|
margin: 32px 0px 0px 0px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formFooter = styled(text, `
|
||||||
|
margin-top: 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const formBody = styled('div', ``);
|
||||||
|
|
||||||
|
export const resendCode = styled(text, `
|
||||||
|
margin-top: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const spinner = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
`);
|
@ -1,7 +1,7 @@
|
|||||||
import { Disposable, dom, domComputed, DomContents, MultiHolder, Observable, styled } from "grainjs";
|
import { Disposable, dom, domComputed, DomContents, MultiHolder, Observable, styled } from "grainjs";
|
||||||
|
|
||||||
import { handleSubmit, submitForm } from "app/client/lib/formUtils";
|
import { handleSubmit, submitForm } from "app/client/lib/formUtils";
|
||||||
import { AppModel, reportError } from "app/client/models/AppModel";
|
import { AppModel } 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';
|
||||||
@ -10,11 +10,11 @@ import { pagePanels } from "app/client/ui/PagePanels";
|
|||||||
import { createUserImage } from 'app/client/ui/UserImage';
|
import { createUserImage } from 'app/client/ui/UserImage';
|
||||||
import { cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
import { cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
||||||
cssMemberSecondary, cssMemberText } from 'app/client/ui/UserItem';
|
cssMemberSecondary, cssMemberText } from 'app/client/ui/UserItem';
|
||||||
import { basicButtonLink, bigBasicButtonLink, bigPrimaryButton, bigPrimaryButtonLink,
|
import { buildWelcomeSitePicker } from 'app/client/ui/WelcomeSitePicker';
|
||||||
cssButton } from "app/client/ui2018/buttons";
|
import { basicButtonLink, bigBasicButtonLink, bigPrimaryButton } from "app/client/ui2018/buttons";
|
||||||
import { mediaSmall, testId, theme, vars } from "app/client/ui2018/cssVars";
|
import { mediaSmall, testId, theme, vars } from "app/client/ui2018/cssVars";
|
||||||
import { cssLink } from "app/client/ui2018/links";
|
import { cssLink } from "app/client/ui2018/links";
|
||||||
import { getOrgName, Organization } from "app/common/UserAPI";
|
import { WelcomePage as WelcomePageEnum } from 'app/common/gristUrls';
|
||||||
|
|
||||||
// Redirect from ..../welcome/thing to .../welcome/${name}
|
// Redirect from ..../welcome/thing to .../welcome/${name}
|
||||||
function _redirectToSiblingPage(name: string) {
|
function _redirectToSiblingPage(name: string) {
|
||||||
@ -36,14 +36,15 @@ function handleSubmitForm(
|
|||||||
|
|
||||||
export class WelcomePage extends Disposable {
|
export class WelcomePage extends Disposable {
|
||||||
|
|
||||||
private _orgs: Organization[];
|
|
||||||
private _orgsLoaded = Observable.create(this, false);
|
|
||||||
|
|
||||||
constructor(private _appModel: AppModel) {
|
constructor(private _appModel: AppModel) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
|
return domComputed(urlState().state, state => this._buildDomInPagePanels(state.welcome));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildDomInPagePanels(page?: WelcomePageEnum) {
|
||||||
return pagePanels({
|
return pagePanels({
|
||||||
leftPanel: {
|
leftPanel: {
|
||||||
panelWidth: Observable.create(this, 240),
|
panelWidth: Observable.create(this, 240),
|
||||||
@ -53,22 +54,21 @@ export class WelcomePage extends Disposable {
|
|||||||
content: null,
|
content: null,
|
||||||
},
|
},
|
||||||
headerMain: [cssFlexSpace(), dom.create(AccountWidget, this._appModel)],
|
headerMain: [cssFlexSpace(), dom.create(AccountWidget, this._appModel)],
|
||||||
contentMain: this.buildPageContent()
|
contentMain: (
|
||||||
|
page === 'teams' ? dom.create(buildWelcomeSitePicker, this._appModel) :
|
||||||
|
this._buildPageContent(page)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildPageContent(): Element {
|
private _buildPageContent(page?: WelcomePageEnum): Element {
|
||||||
return cssScrollContainer(cssContainer(
|
return cssScrollContainer(cssContainer(
|
||||||
cssTitle('Welcome to Grist'),
|
cssTitle('Welcome to Grist'),
|
||||||
testId('welcome-page'),
|
testId('welcome-page'),
|
||||||
|
page === 'signup' ? dom.create(this._buildSignupForm.bind(this)) :
|
||||||
domComputed(urlState().state, (state) => (
|
page === 'verify' ? dom.create(this._buildVerifyForm.bind(this)) :
|
||||||
state.welcome === 'signup' ? dom.create(this._buildSignupForm.bind(this)) :
|
page === 'select-account' ? dom.create(this._buildAccountPicker.bind(this)) :
|
||||||
state.welcome === 'verify' ? dom.create(this._buildVerifyForm.bind(this)) :
|
|
||||||
state.welcome === 'teams' ? dom.create(this._buildOrgPicker.bind(this)) :
|
|
||||||
state.welcome === 'select-account' ? dom.create(this._buildAccountPicker.bind(this)) :
|
|
||||||
null
|
null
|
||||||
)),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,44 +189,6 @@ export class WelcomePage extends Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchOrgs() {
|
|
||||||
this._orgs = await this._appModel.api.getOrgs(true);
|
|
||||||
this._orgsLoaded.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private _buildOrgPicker(): DomContents {
|
|
||||||
this._fetchOrgs().catch(reportError);
|
|
||||||
return dom.maybe(this._orgsLoaded, () => {
|
|
||||||
let orgs = this._orgs;
|
|
||||||
if (orgs && orgs.length > 1) {
|
|
||||||
|
|
||||||
// Let's make sure that the first org is not the personal org.
|
|
||||||
if (orgs[0].owner) {
|
|
||||||
orgs = [...orgs.slice(1), orgs[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
cssParagraph(
|
|
||||||
"You've been added to a team. ",
|
|
||||||
"Go to the team site, or to your personal site."
|
|
||||||
),
|
|
||||||
cssParagraph(
|
|
||||||
"You can always switch sites using the account menu in the top-right corner."
|
|
||||||
),
|
|
||||||
orgs.map((org, i) => (
|
|
||||||
cssOrgButton(
|
|
||||||
getOrgName(org),
|
|
||||||
urlState().setLinkUrl({org: org.domain || undefined}),
|
|
||||||
testId('org'),
|
|
||||||
i ? cssButton.cls('-primary', false) : null
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _buildAccountPicker(): DomContents {
|
private _buildAccountPicker(): DomContents {
|
||||||
function addUserToLink(email: string): string {
|
function addUserToLink(email: string): string {
|
||||||
const next = new URLSearchParams(location.search).get('next') || '';
|
const next = new URLSearchParams(location.search).get('next') || '';
|
||||||
@ -333,15 +295,3 @@ const cssInput = styled(textInput, `
|
|||||||
padding: 13px;
|
padding: 13px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssOrgButton = styled(bigPrimaryButtonLink, `
|
|
||||||
margin: 0 0 8px;
|
|
||||||
width: 200px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
116
app/client/ui/WelcomeSitePicker.ts
Normal file
116
app/client/ui/WelcomeSitePicker.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { makeT } from 'app/client/lib/localization';
|
||||||
|
import { AppModel } from "app/client/models/AppModel";
|
||||||
|
import { urlState} from "app/client/models/gristUrlState";
|
||||||
|
import { createUserImage } from 'app/client/ui/UserImage';
|
||||||
|
import { bigBasicButtonLink } from "app/client/ui2018/buttons";
|
||||||
|
import { testId, theme } from "app/client/ui2018/cssVars";
|
||||||
|
import { FullUser } from 'app/common/LoginSessionAPI';
|
||||||
|
import { getOrgName } from "app/common/UserAPI";
|
||||||
|
import * as css from 'app/client/ui/LoginPagesCss';
|
||||||
|
import { Computed, dom, DomContents, IDisposableOwner, styled } from "grainjs";
|
||||||
|
|
||||||
|
const t = makeT('WelcomeSitePicker');
|
||||||
|
|
||||||
|
export function buildWelcomeSitePicker(owner: IDisposableOwner, appModel: AppModel): DomContents {
|
||||||
|
// We assume that there is a single domain for personal orgs, and will show a button to open
|
||||||
|
// that domain with each of the currently signed-in users.
|
||||||
|
const personalOrg = Computed.create(owner, (use) =>
|
||||||
|
use(appModel.topAppModel.orgs).find(o => Boolean(o.owner))?.domain || undefined);
|
||||||
|
|
||||||
|
return cssPageContainer(
|
||||||
|
testId('welcome-page'),
|
||||||
|
css.centeredFlexContainer(
|
||||||
|
css.formContainer(
|
||||||
|
css.gristLogo(),
|
||||||
|
cssHeading(t('Welcome back')),
|
||||||
|
cssMessage(t('You have access to the following Grist sites.')),
|
||||||
|
cssColumns(
|
||||||
|
cssColumn(
|
||||||
|
cssColumnLabel(css.horizontalLine(), css.lightText('Personal'), css.horizontalLine()),
|
||||||
|
dom.forEach(appModel.topAppModel.users, (user) => (
|
||||||
|
cssOrgButton(
|
||||||
|
cssPersonalOrg(
|
||||||
|
createUserImage(user, 'small'),
|
||||||
|
dom('div', user.email, testId('personal-org-email')),
|
||||||
|
),
|
||||||
|
dom.attr('href', (use) => urlState().makeUrl({org: use(personalOrg)})),
|
||||||
|
dom.on('click', (ev) => { void(switchToPersonalUrl(ev, appModel, personalOrg.get(), user)); }),
|
||||||
|
testId('personal-org'),
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
cssColumn(
|
||||||
|
cssColumnLabel(css.horizontalLine(), css.lightText('Team'), css.horizontalLine()),
|
||||||
|
dom.forEach(appModel.topAppModel.orgs, (org) => (
|
||||||
|
org.owner || !org.domain ? null : cssOrgButton(
|
||||||
|
getOrgName(org),
|
||||||
|
urlState().setLinkUrl({org: org.domain}),
|
||||||
|
testId('org'),
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cssMessage(t("You can always switch sites using the account menu.")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This works but not for opening a link in a new tab. We currently lack and endpoint that
|
||||||
|
// would enable opening a link as a particular user, or to switch user and open as them.
|
||||||
|
async function switchToPersonalUrl(ev: MouseEvent, appModel: AppModel, org: string|undefined, user: FullUser) {
|
||||||
|
// Only handle plain-vanilla clicks.
|
||||||
|
if (ev.shiftKey || ev.metaKey || ev.ctrlKey || ev.altKey) { return; }
|
||||||
|
ev.preventDefault();
|
||||||
|
// Set the active session for the given org, then load its home page.
|
||||||
|
await appModel.api.setSessionActive(user.email, org);
|
||||||
|
window.location.assign(urlState().makeUrl({org}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssPageContainer = styled(css.pageContainer, `
|
||||||
|
overflow: auto;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssHeading = styled(css.formHeading, `
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssMessage = styled(css.centeredText, `
|
||||||
|
margin: 24px 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssColumns = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 32px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssColumn = styled('div', `
|
||||||
|
flex: 1 0 0px;
|
||||||
|
min-width: 200px;
|
||||||
|
position: relative;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssColumnLabel = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssOrgButton = styled(bigBasicButtonLink, `
|
||||||
|
display: block;
|
||||||
|
margin: 8px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssPersonalOrg = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: -8px;
|
||||||
|
gap: 8px;
|
||||||
|
color: ${theme.lightText};
|
||||||
|
`);
|
@ -788,6 +788,11 @@ export const theme = {
|
|||||||
undefined, colors.darkGrey),
|
undefined, colors.darkGrey),
|
||||||
highlightedCodeBgDisabled: new CustomProp('theme-highlighted-code-bg-disabled',
|
highlightedCodeBgDisabled: new CustomProp('theme-highlighted-code-bg-disabled',
|
||||||
undefined, colors.mediumGreyOpaque),
|
undefined, colors.mediumGreyOpaque),
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
loginPageBg: new CustomProp('theme-login-page-bg', undefined, 'white'),
|
||||||
|
loginPageBackdrop: new CustomProp('theme-login-page-backdrop', undefined, '#F5F8FA'),
|
||||||
|
loginPageLine: new CustomProp('theme-login-page-line', undefined, colors.lightGrey),
|
||||||
};
|
};
|
||||||
|
|
||||||
const cssColors = values(colors).map(v => v.decl()).join('\n');
|
const cssColors = values(colors).map(v => v.decl()).join('\n');
|
||||||
|
@ -386,6 +386,9 @@ export const ThemeColors = t.iface([], {
|
|||||||
"highlighted-code-fg": "string",
|
"highlighted-code-fg": "string",
|
||||||
"highlighted-code-border": "string",
|
"highlighted-code-border": "string",
|
||||||
"highlighted-code-bg-disabled": "string",
|
"highlighted-code-bg-disabled": "string",
|
||||||
|
"login-page-bg": "string",
|
||||||
|
"login-page-backdrop": "string",
|
||||||
|
"login-page-line": "string",
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportedTypeSuite: t.ITypeSuite = {
|
const exportedTypeSuite: t.ITypeSuite = {
|
||||||
|
@ -504,6 +504,11 @@ export interface ThemeColors {
|
|||||||
'highlighted-code-fg': string;
|
'highlighted-code-fg': string;
|
||||||
'highlighted-code-border': string;
|
'highlighted-code-border': string;
|
||||||
'highlighted-code-bg-disabled': string;
|
'highlighted-code-bg-disabled': string;
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
'login-page-bg': string;
|
||||||
|
'login-page-backdrop': string;
|
||||||
|
'login-page-line': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;
|
export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;
|
||||||
|
@ -333,7 +333,7 @@ export interface DocStateComparisonDetails {
|
|||||||
|
|
||||||
export interface UserAPI {
|
export interface UserAPI {
|
||||||
getSessionActive(): Promise<ActiveSessionInfo>;
|
getSessionActive(): Promise<ActiveSessionInfo>;
|
||||||
setSessionActive(email: string): Promise<void>;
|
setSessionActive(email: string, org?: string): Promise<void>;
|
||||||
getSessionAll(): Promise<{users: FullUser[], orgs: Organization[]}>;
|
getSessionAll(): Promise<{users: FullUser[], orgs: Organization[]}>;
|
||||||
getOrgs(merged?: boolean): Promise<Organization[]>;
|
getOrgs(merged?: boolean): Promise<Organization[]>;
|
||||||
getWorkspace(workspaceId: number): Promise<Workspace>;
|
getWorkspace(workspaceId: number): Promise<Workspace>;
|
||||||
@ -487,8 +487,8 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
|
|||||||
return this.requestJson(`${this._url}/api/session/access/active`, {method: 'GET'});
|
return this.requestJson(`${this._url}/api/session/access/active`, {method: 'GET'});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setSessionActive(email: string): Promise<void> {
|
public async setSessionActive(email: string, org?: string): Promise<void> {
|
||||||
const body = JSON.stringify({ email });
|
const body = JSON.stringify({ email, org });
|
||||||
return this.requestJson(`${this._url}/api/session/access/active`, {method: 'POST', body});
|
return this.requestJson(`${this._url}/api/session/access/active`, {method: 'POST', body});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,4 +483,9 @@ export const GristDark: ThemeColors = {
|
|||||||
'highlighted-code-fg': '#A4A4A4',
|
'highlighted-code-fg': '#A4A4A4',
|
||||||
'highlighted-code-border': '#69697D',
|
'highlighted-code-border': '#69697D',
|
||||||
'highlighted-code-bg-disabled': '#555563',
|
'highlighted-code-bg-disabled': '#555563',
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
'login-page-bg': '#32323F',
|
||||||
|
'login-page-backdrop': '#404150',
|
||||||
|
'login-page-line': '#57575F',
|
||||||
};
|
};
|
||||||
|
@ -483,4 +483,9 @@ export const GristLight: ThemeColors = {
|
|||||||
'highlighted-code-fg': '#929299',
|
'highlighted-code-fg': '#929299',
|
||||||
'highlighted-code-border': '#D9D9D9',
|
'highlighted-code-border': '#D9D9D9',
|
||||||
'highlighted-code-bg-disabled': '#E8E8E8',
|
'highlighted-code-bg-disabled': '#E8E8E8',
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
'login-page-bg': 'white',
|
||||||
|
'login-page-backdrop': '#F5F8FA',
|
||||||
|
'login-page-line': '#F7F7F7',
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ import {expressWrap} from 'app/server/lib/expressWrap';
|
|||||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
|
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
|
||||||
isParameterOn, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils';
|
isParameterOn, optStringParam, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils';
|
||||||
import {IWidgetRepository} from 'app/server/lib/WidgetRepository';
|
import {IWidgetRepository} from 'app/server/lib/WidgetRepository';
|
||||||
|
|
||||||
import {User} from './entity/User';
|
import {User} from './entity/User';
|
||||||
@ -487,16 +487,20 @@ export class ApiServer {
|
|||||||
|
|
||||||
// POST /api/session/access/active
|
// POST /api/session/access/active
|
||||||
// Body params: email (required)
|
// Body params: email (required)
|
||||||
|
// Body params: org (optional) - string subdomain or 'current', for which org's active user to modify.
|
||||||
// Sets active user for active org
|
// Sets active user for active org
|
||||||
this._app.post('/api/session/access/active', expressWrap(async (req, res) => {
|
this._app.post('/api/session/access/active', expressWrap(async (req, res) => {
|
||||||
const mreq = req as RequestWithLogin;
|
const mreq = req as RequestWithLogin;
|
||||||
const domain = getOrgFromRequest(mreq);
|
let domain = optStringParam(req.body.org);
|
||||||
|
if (!domain || domain === 'current') {
|
||||||
|
domain = getOrgFromRequest(mreq) || '';
|
||||||
|
}
|
||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
if (!email) { throw new ApiError('email required', 400); }
|
if (!email) { throw new ApiError('email required', 400); }
|
||||||
try {
|
try {
|
||||||
// Modify session copy in request. Will be saved to persistent storage before responding
|
// Modify session copy in request. Will be saved to persistent storage before responding
|
||||||
// by express-session middleware.
|
// by express-session middleware.
|
||||||
linkOrgWithEmail(mreq.session, req.body.email, domain || '');
|
linkOrgWithEmail(mreq.session, req.body.email, domain);
|
||||||
clearSessionCacheIfNeeded(req, {sessionID: mreq.sessionID});
|
clearSessionCacheIfNeeded(req, {sessionID: mreq.sessionID});
|
||||||
return sendOkReply(req, res, {email});
|
return sendOkReply(req, res, {email});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {ApiError} from 'app/common/ApiError';
|
import {ApiError} from 'app/common/ApiError';
|
||||||
import {DEFAULT_HOME_SUBDOMAIN, isOrgInPathOnly, parseSubdomain} from 'app/common/gristUrls';
|
import {DEFAULT_HOME_SUBDOMAIN, isOrgInPathOnly, parseSubdomain, sanitizePathTail} from 'app/common/gristUrls';
|
||||||
import * as gutil from 'app/common/gutil';
|
import * as gutil from 'app/common/gutil';
|
||||||
import {DocScope, QueryResult, Scope} from 'app/gen-server/lib/HomeDBManager';
|
import {DocScope, QueryResult, Scope} from 'app/gen-server/lib/HomeDBManager';
|
||||||
import {getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
import {getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||||
@ -70,7 +70,8 @@ export function addOrgToPath(req: RequestWithOrg, path: string): string {
|
|||||||
* Get url to the org associated with the request.
|
* Get url to the org associated with the request.
|
||||||
*/
|
*/
|
||||||
export function getOrgUrl(req: Request, path: string = '/') {
|
export function getOrgUrl(req: Request, path: string = '/') {
|
||||||
return getOriginUrl(req) + addOrgToPathIfNeeded(req, path);
|
// Be careful to include a leading slash in path, to ensure we don't modify the origin or org.
|
||||||
|
return getOriginUrl(req) + addOrgToPathIfNeeded(req, sanitizePathTail(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1030,5 +1030,10 @@
|
|||||||
},
|
},
|
||||||
"GridView": {
|
"GridView": {
|
||||||
"Click to insert": "Click to insert"
|
"Click to insert": "Click to insert"
|
||||||
|
},
|
||||||
|
"WelcomeSitePicker": {
|
||||||
|
"Welcome back": "Welcome back",
|
||||||
|
"You can always switch sites using the account menu.": "You can always switch sites using the account menu.",
|
||||||
|
"You have access to the following Grist sites.": "You have access to the following Grist sites."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user