mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
dbde9e459c
commit
ddb67ff44e
@ -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;
|
||||||
|
`);
|
||||||
|
@ -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',
|
||||||
|
@ -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', `
|
||||||
|
Loading…
Reference in New Issue
Block a user