gristlabs_grist-core/app/client/lib/ACUserManager.ts
2023-01-03 15:46:19 +01:00

202 lines
5.8 KiB
TypeScript

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 {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 {Computed, computed, dom, DomElementArg, Holder, IDisposableOwner, Observable, styled} from "grainjs";
import {cssMenuItem} from "popweasel";
const t = makeT('lib.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<ACUserItem>;
emailObs: Observable<string>;
save: (value: string) => void;
prompt?: {email: string},
},
...args: DomElementArg[]
) {
const {acIndex, emailObs, save, prompt} = options;
const acHolder = Holder.create<Autocomplete<ACUserItem>>(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<ACUserItem>, text: string): Promise<ACResults<ACUserItem>> => {
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(
"+",
cssUserImage.cls("-large"),
cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd),
)),
cssMemberText(
cssMemberPrimaryPlus(t("Invite new member")),
cssMemberSecondaryPlus(
// dom.text(use => `We'll email an invite to ${use(emailObs)}`)
dom.text(use => t("We'll email an invite to {{email}}", {email: use(emailObs)})) // TODO i18next
)
),
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<boolean> = computed((use) => Boolean(use(emailObs) && use(isValid)));
const acOptions: IAutocompleteOptions<ACUserItem> = {
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};
}
`);