import {makeT} from 'app/client/lib/localization'; import {ACIndex, ACItem, ACResults, buildHighlightedDom, normalizeText} from "app/client/lib/ACIndex"; import {cssSelectItem} from "app/client/lib/ACSelect"; import {Autocomplete, IAutocompleteOptions} from "app/client/lib/autocomplete"; import {colors, testId, theme} from "app/client/ui2018/cssVars"; import {icon} from "app/client/ui2018/icons"; import {menuCssClass} from "app/client/ui2018/menus"; import { cssEmailInput, cssEmailInputContainer, cssMailIcon, cssMemberImage, cssMemberListItem, cssMemberPrimary, cssMemberSecondary, cssMemberText, } from "app/client/ui/UserItem"; import {createUserImage, cssUserImage} from "app/client/ui/UserImage"; import {getGristConfig} from 'app/common/urlUtils'; import {Computed, computed, dom, DomElementArg, Holder, IDisposableOwner, Observable, styled} from "grainjs"; import {cssMenuItem} from "popweasel"; const t = makeT('ACUserManager'); export interface ACUserItem extends ACItem { value: string; label: string; name: string; email: string; id: number; picture?: string | null; // when present, a url to a public image of unspecified dimensions. isNew?: boolean; } export function buildACMemberEmail( owner: IDisposableOwner, options: { acIndex: ACIndex; emailObs: Observable; save: (value: string) => void; prompt?: {email: string}, }, ...args: DomElementArg[] ) { const {acIndex, emailObs, save, prompt} = options; const acHolder = Holder.create>(owner); let emailInput: HTMLInputElement; const isValid = Observable.create(owner, true); const isOpen = () => !acHolder.isEmpty(); const acOpen = () => acHolder.isEmpty() && Autocomplete.create(acHolder, emailInput, acOptions); const acClose = () => acHolder.clear(); const finish = () => { acClose(); emailObs.set(""); emailInput.value = emailObs.get(); emailInput.focus(); }; const onEnter = () => { isOpen() ? commitIfValid() : acOpen(); }; const commitIfValid = () => { const item = acHolder.get()?.getSelectedItem(); if (item) { emailObs.set(item.value); } emailInput.setCustomValidity(""); isValid.set(emailInput.checkValidity()); const selectedEmail = item?.value || emailObs.get(); try { if (selectedEmail && isValid.get()) { save(emailObs.get()); finish(); } } catch (e) { emailInput.setCustomValidity(e.message); } finally { emailInput.reportValidity(); } }; const maybeShowAddNew = async (results: ACResults, text: string): Promise> => { const cleanText = normalizeText(text); const items = results.items .filter(item => item.cleanText.includes(cleanText)) .sort((a, b) => a.cleanText.localeCompare(b.cleanText)); results.items = items; if (!results.items.length && cleanText) { const newObject = { value: text, cleanText, name: "", email: "", isNew: true, label: text, id: 0, }; results.items.push(newObject); } return results; }; const renderSearchItem = (item: ACUserItem, highlightFunc: any): HTMLLIElement => (item?.isNew ? cssSelectItem( cssMemberListItem( cssUserImagePlus( cssPlusIcon('Plus'), cssUserImage.cls("-large"), cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd), )), cssMemberText( cssMemberPrimaryPlus(t("Invite new member")), getGristConfig().notifierEnabled ? cssMemberSecondaryPlus( dom.text(use => t("We'll email an invite to {{email}}", {email: use(emailObs)})) ) : null, ), testId("um-add-email") ) ) : cssSelectItem( cssMemberListItem( cssMemberImage(createUserImage(item, "large")), cssMemberText( cssMemberPrimaryPlus(item.name, testId("um-member-name")), cssMemberSecondaryPlus(buildHighlightedDom(item.label, highlightFunc, cssMatchText)) ) ) )); const enableAdd: Computed = computed((use) => Boolean(use(emailObs) && use(isValid))); const acOptions: IAutocompleteOptions = { attach: null, menuCssClass: `${menuCssClass} test-acselect-dropdown`, search: (term) => maybeShowAddNew(acIndex.search(term), term), renderItem: renderSearchItem, getItemText: (item) => item.value, onClick: commitIfValid, }; const result = cssEmailInputContainer( cssMailIcon("Mail"), (emailInput = cssEmailInput( emailObs, {onInput: true, isValid}, {type: "email", placeholder: t("Enter email address")}, dom.on("input", acOpen), dom.on("focus", acOpen), dom.on("click", acOpen), dom.on("blur", acClose), dom.onKeyDown({ Escape: finish, Enter: onEnter, ArrowDown: acOpen, Tab: commitIfValid, }), )), cssEmailInputContainer.cls('-green', enableAdd), ...args ); // Reset custom validity that we sometimes set. owner.autoDispose(emailObs.addListener(() => emailInput.setCustomValidity(""))); if (prompt) { setTimeout(() => emailInput.focus(), 0); } return result; } const cssMemberPrimaryPlus = styled(cssMemberPrimary, ` .${cssSelectItem.className}.selected & { color: ${theme.menuItemSelectedFg}; } `); const cssMemberSecondaryPlus = styled(cssMemberSecondary, ` .${cssSelectItem.className}.selected & { color: ${theme.menuItemSelectedFg}; } `); const cssMatchText = styled("span", ` color: ${theme.autocompleteMatchText}; .${cssSelectItem.className}.selected & { color: ${theme.autocompleteSelectedMatchText}; } `); const cssUserImagePlus = styled(cssUserImage, ` background-color: ${colors.lightGreen}; margin: auto 0; &-invalid { background-color: ${colors.mediumGrey}; } .${cssMenuItem.className}-sel & { background-color: ${theme.menuItemIconSelectedFg}; color: ${theme.menuItemSelectedBg}; } `); const cssPlusIcon = styled(icon, ` width: 20px; height: 20px; `);