mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
1654a2681f
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
import {AppModel, reportError} from 'app/client/models/AppModel';
|
|
import {getResetPwdUrl} from 'app/client/models/gristUrlState';
|
|
import {ApiKey} from 'app/client/ui/ApiKey';
|
|
import * as billingPageCss from 'app/client/ui/BillingPageCss';
|
|
import {transientInput} from 'app/client/ui/transientInput';
|
|
import {buildNameWarningsDom, checkName} from 'app/client/ui/WelcomePage';
|
|
import {bigBasicButton, bigPrimaryButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
|
import {testId} from 'app/client/ui2018/cssVars';
|
|
import {cssModalBody, cssModalButtons, cssModalTitle, cssModalWidth} from 'app/client/ui2018/modals';
|
|
import {IModalControl, modal} from 'app/client/ui2018/modals';
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
|
import {Computed, dom, domComputed, DomElementArg, MultiHolder, Observable, styled} from 'grainjs';
|
|
|
|
|
|
/**
|
|
* Renders a modal with profile settings.
|
|
*/
|
|
export function showProfileModal(appModel: AppModel): void {
|
|
return modal((ctl, owner) => showProfileContent(ctl, owner, appModel));
|
|
}
|
|
|
|
function showProfileContent(ctl: IModalControl, owner: MultiHolder, appModel: AppModel): DomElementArg {
|
|
const apiKey = Observable.create<string>(owner, '');
|
|
const userObs = Observable.create<FullUser|null>(owner, null);
|
|
const isEditingName = Observable.create(owner, false);
|
|
const nameEdit = Observable.create<string>(owner, '');
|
|
const isNameValid = Computed.create(owner, nameEdit, (use, val) => checkName(val));
|
|
|
|
let needsReload = false;
|
|
let closeBtn: Element;
|
|
|
|
async function fetchApiKey() { apiKey.set(await appModel.api.fetchApiKey()); }
|
|
async function createApiKey() { apiKey.set(await appModel.api.createApiKey()); }
|
|
async function deleteApiKey() { await appModel.api.deleteApiKey(); apiKey.set(''); }
|
|
async function fetchUserProfile() { userObs.set(await appModel.api.getUserProfile()); }
|
|
|
|
async function fetchAll() {
|
|
await Promise.all([
|
|
fetchApiKey(),
|
|
fetchUserProfile()
|
|
]);
|
|
}
|
|
|
|
fetchAll().catch(reportError);
|
|
|
|
async function updateUserName(val: string) {
|
|
const user = userObs.get();
|
|
if (user && val && val !== user.name) {
|
|
closeBtn.toggleAttribute('disabled', true);
|
|
try {
|
|
await appModel.api.updateUserName(val);
|
|
await fetchAll();
|
|
needsReload = true;
|
|
} finally {
|
|
closeBtn.toggleAttribute('disabled', false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
cssModalTitle('User Profile'),
|
|
cssModalWidth('fixed-wide'),
|
|
domComputed(userObs, (user) => user && (
|
|
cssModalBody(
|
|
cssDataRow(cssSubHeader('Email'), user.email),
|
|
cssDataRow(
|
|
cssSubHeader('Name'),
|
|
domComputed(isEditingName, (isediting) => (
|
|
isediting ? [
|
|
transientInput(
|
|
{
|
|
initialValue: user.name,
|
|
save: (val) => isNameValid.get() && updateUserName(val),
|
|
close: () => { isEditingName.set(false); nameEdit.set(''); },
|
|
},
|
|
dom.on('input', (ev, el) => nameEdit.set(el.value)),
|
|
),
|
|
cssTextBtn(
|
|
cssBillingIcon('Settings'), 'Save',
|
|
// no need to save on 'click', the transient input already does it on close
|
|
),
|
|
] : [
|
|
user.name,
|
|
cssTextBtn(
|
|
cssBillingIcon('Settings'), 'Edit',
|
|
dom.on('click', () => isEditingName.set(true))
|
|
),
|
|
]
|
|
)),
|
|
testId('username')
|
|
),
|
|
// show warning for invalid name but not for the empty string
|
|
dom.maybe(use => use(nameEdit) && !use(isNameValid), cssWarnings),
|
|
cssDataRow(
|
|
cssSubHeader('Login Method'),
|
|
user.loginMethod,
|
|
// TODO: should show btn only when logged in with google
|
|
user.loginMethod === 'Email + Password' ? cssTextBtn(
|
|
// rename to remove mention of Billing in the css
|
|
cssBillingIcon('Settings'), 'Reset',
|
|
dom.on('click', () => confirmPwdResetModal(user.email))
|
|
) : null,
|
|
testId('login-method'),
|
|
),
|
|
cssDataRow(cssSubHeader('API Key'), cssContent(
|
|
dom.create(ApiKey, {
|
|
apiKey,
|
|
onCreate: createApiKey,
|
|
onDelete: deleteApiKey,
|
|
anonymous: false,
|
|
})
|
|
)),
|
|
)
|
|
)),
|
|
cssModalButtons(
|
|
closeBtn = bigPrimaryButton('Close',
|
|
dom.on('click', () => {
|
|
if (needsReload) {
|
|
appModel.topAppModel.initialize();
|
|
}
|
|
ctl.close();
|
|
}),
|
|
testId('modal-confirm')
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
// We cannot use the confirmModal here because of the button link that we need here.
|
|
function confirmPwdResetModal(userEmail: string) {
|
|
return modal((ctl, owner) => {
|
|
return [
|
|
cssModalTitle('Reset Password'),
|
|
cssModalBody(`Click continue to open the password reset form. Submit it for your email address: ${userEmail}`),
|
|
cssModalButtons(
|
|
bigPrimaryButtonLink(
|
|
{ href: getResetPwdUrl(), target: '_blank' },
|
|
'Continue',
|
|
dom.on('click', () => ctl.close())
|
|
),
|
|
bigBasicButton(
|
|
'Cancel',
|
|
dom.on('click', () => ctl.close())
|
|
),
|
|
),
|
|
];
|
|
});
|
|
}
|
|
|
|
|
|
const cssDataRow = styled('div', `
|
|
margin: 8px 0px;
|
|
display: flex;
|
|
align-items: baseline;
|
|
`);
|
|
|
|
const cssSubHeader = styled('div', `
|
|
width: 110px;
|
|
padding: 8px 0;
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
font-weight: bold;
|
|
`);
|
|
|
|
const cssContent = styled('div', `
|
|
flex: 1 1 300px;
|
|
`);
|
|
|
|
const cssTextBtn = styled(billingPageCss.billingTextBtn, `
|
|
width: 90px;
|
|
margin-left: auto;
|
|
`);
|
|
|
|
const cssBillingIcon = billingPageCss.billingIcon;
|
|
|
|
const cssWarnings = styled(buildNameWarningsDom, `
|
|
margin: -8px 0 0 110px;
|
|
`);
|