gristlabs_grist-core/app/client/lib/MultiUserManager.ts

180 lines
4.9 KiB
TypeScript
Raw Normal View History

import {computed, Computed, dom, DomElementArg, IDisposableOwner, Observable, styled} from "grainjs";
2022-12-27 18:35:03 +00:00
import {cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
IModalControl, modal} from 'app/client/ui2018/modals';
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {mediaXSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
2022-12-27 18:35:03 +00:00
import {IOrgMemberSelectOption, UserManagerModel} from 'app/client/models/UserManagerModel';
import {icon} from 'app/client/ui2018/icons';
import {textarea} from "app/client/ui/inputs";
2022-12-27 18:35:03 +00:00
import {BasicRole, isBasicRole, NonGuestRole, VIEWER} from "app/common/roles";
import {menu, menuItem} from 'app/client/ui2018/menus';
function parseEmailList(emailListRaw: string): Array<string> {
return emailListRaw
.split('\n')
.map(email => email.trim().toLowerCase())
.filter(email => email !== "");
}
function validateEmail(email: string): boolean {
const mailformat = /\S+@\S+\.\S+/;
return mailformat.test(email);
}
export function buildMultiUserManagerModal(
owner: IDisposableOwner,
model: UserManagerModel,
onAdd: (email: string, role: NonGuestRole) => void,
) {
const emailListObs = Observable.create(owner, "");
const rolesObs = Observable.create<BasicRole>(owner, VIEWER);
const isValidObs = Observable.create(owner, true);
2022-12-27 18:35:03 +00:00
const enableAdd: Computed<boolean> = computed(
(use) => Boolean(use(emailListObs) && use(rolesObs) && use(isValidObs))
);
const save = (ctl: IModalControl) => {
const emailList = parseEmailList(emailListObs.get());
const role = rolesObs.get();
if (emailList.some(email => !validateEmail(email))) {
isValidObs.set(false);
} else {
emailList.forEach(email => onAdd(email, role));
ctl.close();
}
2022-12-27 18:35:03 +00:00
};
return modal(ctl => [
{ style: 'padding: 0;' },
dom.cls(cssAnimatedModal.className),
cssTitle(
'Invite Users',
testId('um-header'),
),
cssModalBody(
cssUserManagerBody(
buildEmailsTextarea(emailListObs, isValidObs),
dom.maybe(use => !use(isValidObs), () => cssErrorMessage('At least one email is invalid')),
cssInheritRoles(
dom('span', 'Access: '),
buildRolesSelect(rolesObs, model)
)
),
),
cssModalButtons(
{ style: 'margin: 32px 64px; display: flex;' },
bigPrimaryButton('Confirm',
dom.boolAttr('disabled', (use) => !use(enableAdd)),
2022-12-27 18:35:03 +00:00
dom.on('click', () => { save(ctl); }),
testId('um-confirm')
),
bigBasicButton(
'Cancel',
dom.on('click', () => ctl.close()),
testId('um-cancel')
),
)
]);
}
function buildRolesSelect(
roleSelectedObs: Observable<BasicRole>,
model: UserManagerModel,
) {
const allRoles = (model.isOrg ? model.orgUserSelectOptions : model.userSelectOptions)
.filter((x): x is {value: BasicRole, label: string} => isBasicRole(x.value));
return cssOptionBtn(
menu(() => [
dom.forEach(allRoles, (_role) =>
menuItem(() => roleSelectedObs.set(_role.value), _role.label,
testId(`um-role-option`)
)
)
]),
dom.text((use) => {
// Get the label of the active role.
const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(roleSelectedObs) === _role.value);
return activeRole ? activeRole.label : "";
}),
cssCollapseIcon('Collapse'),
testId('um-role-select')
);
}
function buildEmailsTextarea(
emailListObs: Observable<string>,
isValidObs: Observable<boolean>,
...args: DomElementArg[]
) {
return cssTextarea(emailListObs,
{onInput: true, isValid: isValidObs},
{placeholder: "Enter one email address per line"},
dom.on('change', (_ev) => isValidObs.set(true)),
...args,
);
}
const cssTitle = styled(cssModalTitle, `
margin: 40px 64px 0 64px;
@media ${mediaXSmall} {
& {
margin: 16px;
}
}
`);
const cssInheritRoles = styled('span', `
margin: 13px 63px 42px;
`);
const cssErrorMessage = styled('span', `
margin: 0 63px;
color: ${theme.errorText};
`);
const cssOptionBtn = styled('span', `
display: inline-flex;
font-size: ${vars.mediumFontSize};
color: ${theme.controlFg};
cursor: pointer;
`);
const cssCollapseIcon = styled(icon, `
margin-top: 1px;
background-color: ${theme.controlFg};
`);
const cssAccessDetailsBody = styled('div', `
display: flex;
flex-direction: column;
width: 600px;
font-size: ${vars.mediumFontSize};
`);
const cssUserManagerBody = styled(cssAccessDetailsBody, `
height: 374px;
border-bottom: 1px solid ${theme.modalBorderDark};
`);
const cssTextarea = styled(textarea, `
margin: 16px 63px;
padding: 12px 10px;
border-radius: 3px;
resize: none;
border: 1px solid ${theme.inputBorder};
color: ${theme.inputFg};
background-color: ${theme.inputBg};
flex: 1 1 0;
font-size: ${vars.mediumFontSize};
font-family: ${vars.fontFamily};
outline: none;
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
`);