(core) Make new account page mobile-friendly

Summary:
Tweaks CSS of account page, ApiKey and MFAConfig to work better
on narrow-screen devices.

Test Plan: Tested manually.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3234
This commit is contained in:
George Gevoian 2022-01-24 12:38:32 -08:00
parent dbde9e459c
commit ddb67ff44e
3 changed files with 57 additions and 29 deletions

View File

@ -53,7 +53,10 @@ export class AccountPage extends Disposable {
return domComputed(this._userObs, (user) => user && ( return domComputed(this._userObs, (user) => user && (
cssContainer(cssAccountPage( cssContainer(cssAccountPage(
cssHeader('Account settings'), cssHeader('Account settings'),
cssDataRow(cssSubHeader('Email'), user.email), cssDataRow(
cssSubHeader('Email'),
cssEmail(user.email),
),
cssDataRow( cssDataRow(
cssSubHeader('Name'), cssSubHeader('Name'),
domComputed(this._isEditingName, (isEditing) => ( domComputed(this._isEditingName, (isEditing) => (
@ -64,14 +67,16 @@ export class AccountPage extends Disposable {
save: (val) => this._isNameValid.get() && this._updateUserName(val), save: (val) => this._isNameValid.get() && this._updateUserName(val),
close: () => { this._isEditingName.set(false); this._nameEdit.set(''); }, close: () => { this._isEditingName.set(false); this._nameEdit.set(''); },
}, },
{ size: '5' }, // Lower size so that input can shrink below ~152px.
dom.on('input', (_ev, el) => this._nameEdit.set(el.value)), dom.on('input', (_ev, el) => this._nameEdit.set(el.value)),
cssFlexGrow.cls(''),
), ),
cssTextBtn( cssTextBtn(
cssIcon('Settings'), 'Save', cssIcon('Settings'), 'Save',
// No need to save on 'click'. The transient input already does it on close. // No need to save on 'click'. The transient input already does it on close.
), ),
] : [ ] : [
user.name, cssName(user.name),
cssTextBtn( cssTextBtn(
cssIcon('Settings'), 'Edit', cssIcon('Settings'), 'Edit',
dom.on('click', () => this._isEditingName.set(true)), dom.on('click', () => this._isEditingName.set(true)),
@ -85,7 +90,7 @@ export class AccountPage extends Disposable {
cssHeader('Password & Security'), cssHeader('Password & Security'),
cssDataRow( cssDataRow(
cssSubHeader('Login Method'), cssSubHeader('Login Method'),
user.loginMethod, cssLoginMethod(user.loginMethod),
user.loginMethod === 'Email + Password' ? cssTextBtn( user.loginMethod === 'Email + Password' ? cssTextBtn(
cssIcon('Settings'), 'Reset', cssIcon('Settings'), 'Reset',
dom.on('click', () => confirmPwdResetModal(user.email)), dom.on('click', () => confirmPwdResetModal(user.email)),
@ -111,6 +116,7 @@ export class AccountPage extends Disposable {
onCreate: () => this._createApiKey(), onCreate: () => this._createApiKey(),
onDelete: () => this._deleteApiKey(), onDelete: () => this._deleteApiKey(),
anonymous: false, anonymous: false,
inputArgs: [{ size: '5' }], // Lower size so that input can shrink below ~152px.
}) })
)), )),
), ),
@ -209,7 +215,7 @@ const cssHeader = styled('div', `
const cssAccountPage = styled('div', ` const cssAccountPage = styled('div', `
max-width: 600px; max-width: 600px;
padding: 32px 64px 24px 64px; padding: 16px;
`); `);
const cssDataRow = styled('div', ` const cssDataRow = styled('div', `
@ -226,7 +232,7 @@ const cssSubHeaderFullWidth = styled('div', `
`); `);
const cssSubHeader = styled(cssSubHeaderFullWidth, ` const cssSubHeader = styled(cssSubHeaderFullWidth, `
width: 110px; min-width: 110px;
`); `);
const cssContent = styled('div', ` const cssContent = styled('div', `
@ -237,12 +243,12 @@ const cssTextBtn = styled('button', `
font-size: ${vars.mediumFontSize}; font-size: ${vars.mediumFontSize};
color: ${colors.lightGreen}; color: ${colors.lightGreen};
cursor: pointer; cursor: pointer;
margin-left: auto; margin-left: 16px;
background-color: transparent; background-color: transparent;
border: none; border: none;
padding: 0; padding: 0;
text-align: left; text-align: left;
width: 90px; min-width: 90px;
&:hover { &:hover {
color: ${colors.darkGreen}; color: ${colors.darkGreen};
@ -266,3 +272,19 @@ const cssDescription = styled('div', `
color: #8a8a8a; color: #8a8a8a;
font-size: 13px; font-size: 13px;
`); `);
const cssFlexGrow = styled('div', `
flex-grow: 1;
`);
const cssName = styled(cssFlexGrow, `
word-break: break-word;
`);
const cssEmail = styled('div', `
word-break: break-word;
`);
const cssLoginMethod = styled(cssFlexGrow, `
word-break: break-word;
`);

View File

@ -2,7 +2,7 @@ import * as billingPageCss from 'app/client/ui/BillingPageCss';
import { basicButton } from 'app/client/ui2018/buttons'; import { basicButton } from 'app/client/ui2018/buttons';
import { confirmModal } from 'app/client/ui2018/modals'; import { confirmModal } from 'app/client/ui2018/modals';
import { Disposable, dom, makeTestId, Observable, observable, styled } from "grainjs"; import { Disposable, dom, IDomArgs, makeTestId, Observable, observable, styled } from "grainjs";
interface IWidgetOptions { interface IWidgetOptions {
apiKey: Observable<string>; apiKey: Observable<string>;
@ -11,6 +11,7 @@ interface IWidgetOptions {
anonymous?: boolean; // Configure appearance and available options for anonymous use. anonymous?: boolean; // Configure appearance and available options for anonymous use.
// When anonymous, no modifications are permitted to profile information. // When anonymous, no modifications are permitted to profile information.
// TODO: add browser test for this option. // TODO: add browser test for this option.
inputArgs?: IDomArgs<HTMLInputElement>;
} }
const testId = makeTestId('test-apikey-'); const testId = makeTestId('test-apikey-');
@ -27,6 +28,7 @@ export class ApiKey extends Disposable {
private _onDeleteCB: () => Promise<void>; private _onDeleteCB: () => Promise<void>;
private _onCreateCB: () => Promise<void>; private _onCreateCB: () => Promise<void>;
private _anonymous: boolean; private _anonymous: boolean;
private _inputArgs: IDomArgs<HTMLInputElement>;
private _loading = observable(false); private _loading = observable(false);
constructor(options: IWidgetOptions) { constructor(options: IWidgetOptions) {
@ -35,6 +37,7 @@ export class ApiKey extends Disposable {
this._onDeleteCB = options.onDelete; this._onDeleteCB = options.onDelete;
this._onCreateCB = options.onCreate; this._onCreateCB = options.onCreate;
this._anonymous = Boolean(options.anonymous); this._anonymous = Boolean(options.anonymous);
this._inputArgs = options.inputArgs ?? [];
} }
public buildDom() { public buildDom() {
@ -42,8 +45,13 @@ export class ApiKey extends Disposable {
dom.maybe(this._apiKey, (apiKey) => dom('div', dom.maybe(this._apiKey, (apiKey) => dom('div',
cssRow( cssRow(
cssInput( cssInput(
{readonly: true, value: this._apiKey.get()}, testId('key'), {
dom.on('click', (ev, el) => el.select()) readonly: true,
value: this._apiKey.get(),
},
testId('key'),
dom.on('click', (ev, el) => el.select()),
this._inputArgs
), ),
cssTextBtn( cssTextBtn(
cssBillingIcon('Remove'), 'Remove', cssBillingIcon('Remove'), 'Remove',

View File

@ -6,8 +6,8 @@ import {colors, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links'; import {cssLink} from 'app/client/ui2018/links';
import {loadingSpinner} from 'app/client/ui2018/loaders'; import {loadingSpinner} from 'app/client/ui2018/loaders';
import {cssModalBody, cssModalTitle, IModalControl, modal, import {cssModalBody, cssModalTitle, cssModalWidth, IModalControl,
cssModalButtons as modalButtons} from 'app/client/ui2018/modals'; modal, cssModalButtons as modalButtons} from 'app/client/ui2018/modals';
import {ApiError} from 'app/common/ApiError'; import {ApiError} from 'app/common/ApiError';
import {FullUser} from 'app/common/LoginSessionAPI'; import {FullUser} from 'app/common/LoginSessionAPI';
import {AuthMethod, ChallengeRequired, UserMFAPreferences} from 'app/common/UserAPI'; import {AuthMethod, ChallengeRequired, UserMFAPreferences} from 'app/common/UserAPI';
@ -85,7 +85,7 @@ export class MFAConfig extends Disposable {
private _buildButtons() { private _buildButtons() {
return cssButtons( return cssButtons(
dom.domComputed(this._mfaPrefs, mfaPrefs => { dom.domComputed(this._mfaPrefs, mfaPrefs => {
if (!mfaPrefs) { return cssCenteredDiv(cssSmallLoadingSpinner()); } if (!mfaPrefs) { return cssSmallSpinner(cssSmallLoadingSpinner()); }
const {isSmsMfaEnabled, isSoftwareTokenMfaEnabled, phoneNumber} = mfaPrefs; const {isSmsMfaEnabled, isSoftwareTokenMfaEnabled, phoneNumber} = mfaPrefs;
return [ return [
@ -282,7 +282,7 @@ export class MFAConfig extends Disposable {
} }
} }
}), }),
cssModal.cls(''), cssModalWidth('fixed-wide'),
]; ];
}); });
} }
@ -339,11 +339,11 @@ export class MFAConfig extends Disposable {
.catch(reportError) .catch(reportError)
.finally(() => ctl.close()); .finally(() => ctl.close());
return cssCenteredDivFixedHeight(loadingSpinner()); return cssSpinner(loadingSpinner());
} }
} }
}), }),
cssModal.cls(''), cssModalWidth('fixed-wide'),
]; ];
}); });
} }
@ -371,7 +371,7 @@ export class MFAConfig extends Disposable {
dom.domComputed(securityStep, (step) => { dom.domComputed(securityStep, (step) => {
switch (step) { switch (step) {
case 'loading': { case 'loading': {
return cssCenteredDivFixedHeight(loadingSpinner()); return cssSpinner(loadingSpinner());
} }
case 'password': { case 'password': {
let formElement: HTMLFormElement; let formElement: HTMLFormElement;
@ -510,7 +510,7 @@ export class MFAConfig extends Disposable {
dom.autoDispose(resendingListener), dom.autoDispose(resendingListener),
dom.autoDispose(multiHolder), dom.autoDispose(multiHolder),
dom.domComputed(isResendingCode, isLoading => { dom.domComputed(isResendingCode, isLoading => {
if (isLoading) { return cssCenteredDivFixedHeight(loadingSpinner()); } if (isLoading) { return cssSpinner(loadingSpinner()); }
return [ return [
cssModalTitle('Almost there!', testId('title')), cssModalTitle('Almost there!', testId('title')),
@ -618,7 +618,7 @@ export class MFAConfig extends Disposable {
dom.autoDispose(pending.addListener(isPending => isPending && errorObs.set(null))), dom.autoDispose(pending.addListener(isPending => isPending && errorObs.set(null))),
dom.autoDispose(holder), dom.autoDispose(holder),
dom.domComputed(qrCode, code => { dom.domComputed(qrCode, code => {
if (code === null) { return cssCenteredDivFixedHeight(loadingSpinner()); } if (code === null) { return cssSpinner(loadingSpinner()); }
return [ return [
cssModalTitle('Configure authenticator app', testId('title')), cssModalTitle('Configure authenticator app', testId('title')),
@ -748,7 +748,7 @@ export class MFAConfig extends Disposable {
dom.autoDispose(resendingListener), dom.autoDispose(resendingListener),
dom.autoDispose(multiHolder), dom.autoDispose(multiHolder),
dom.domComputed(isResendingCode, isLoading => { dom.domComputed(isResendingCode, isLoading => {
if (isLoading) { return cssCenteredDivFixedHeight(loadingSpinner()); } if (isLoading) { return cssSpinner(loadingSpinner()); }
return [ return [
cssModalTitle('Confirm your phone', testId('title')), cssModalTitle('Confirm your phone', testId('title')),
@ -939,14 +939,13 @@ const cssBackBtn = styled(cssTextBtn, `
`); `);
const cssAuthMethods = styled('div', ` const cssAuthMethods = styled('div', `
display: flex; display: grid;
flex-direction: column; grid-auto-rows: 1fr;
margin-top: 16px; margin-top: 16px;
gap: 8px; gap: 8px;
`); `);
const cssAuthMethod = styled('div', ` const cssAuthMethod = styled('div', `
height: 120px;
border: 1px solid ${colors.mediumGreyOpaque}; border: 1px solid ${colors.mediumGreyOpaque};
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
@ -965,7 +964,7 @@ const cssAuthMethodTitle = styled(cssIconAndText, `
const cssAuthMethodDesc = styled('div', ` const cssAuthMethodDesc = styled('div', `
color: #8a8a8a; color: #8a8a8a;
padding-left: 40px; padding: 0px 16px 16px 40px;
`); `);
const cssInput = styled(input, ` const cssInput = styled(input, `
@ -992,10 +991,6 @@ const cssCodeInput = styled(cssInput, `
width: 200px; width: 200px;
`); `);
const cssModal = styled('div', `
width: 600px;
`);
const cssSmallLoadingSpinner = styled(loadingSpinner, ` const cssSmallLoadingSpinner = styled(loadingSpinner, `
width: ${spinnerSizePixels}; width: ${spinnerSizePixels};
height: ${spinnerSizePixels}; height: ${spinnerSizePixels};
@ -1008,8 +1003,11 @@ const cssCenteredDiv = styled('div', `
align-items: center; align-items: center;
`); `);
const cssCenteredDivFixedHeight = styled(cssCenteredDiv, ` const cssSmallSpinner = cssCenteredDiv;
const cssSpinner = styled(cssCenteredDiv, `
height: 200px; height: 200px;
min-width: 200px;
`); `);
const cssBoldSubHeading = styled('div', ` const cssBoldSubHeading = styled('div', `