mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Improve input team member (#268)
* Autocomplete for email * Remove old MemberEmail input and styled correctly the new autocomplete one * Add validation on autocomplete input * fix selected item styling * Add prompt feature on ACUserManager * Add sort for result in autocomplete * Add attach option to autocomplete Co-authored-by: Ronan Amicel <ronan.amicel.prestataire@anct.gouv.fr>
This commit is contained in:
parent
d55b5110ac
commit
1bff046a3b
@ -100,7 +100,7 @@ const cssSelectBtn = styled('div', `
|
|||||||
--icon-color: ${theme.selectButtonFg};
|
--icon-color: ${theme.selectButtonFg};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssSelectItem = styled('li', `
|
export const cssSelectItem = styled('li', `
|
||||||
color: ${theme.menuItemFg};
|
color: ${theme.menuItemFg};
|
||||||
display: block;
|
display: block;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
204
app/client/lib/ACUserManager.ts
Normal file
204
app/client/lib/ACUserManager.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import {ACResults, ACIndex, ACItem, buildHighlightedDom, normalizeText} from "app/client/lib/ACIndex";
|
||||||
|
import {cssSelectItem} from "app/client/lib/ACSelect";
|
||||||
|
import {Autocomplete, IAutocompleteOptions} from "app/client/lib/autocomplete";
|
||||||
|
import {cssMenuItem} from "popweasel";
|
||||||
|
|
||||||
|
import {testId, colors, theme} from "app/client/ui2018/cssVars";
|
||||||
|
import {menuCssClass} from "app/client/ui2018/menus";
|
||||||
|
import {dom, DomElementArg, Holder, IDisposableOwner, Observable, styled, computed, Computed} from "grainjs";
|
||||||
|
import {
|
||||||
|
cssEmailInput,
|
||||||
|
cssEmailInputContainer,
|
||||||
|
cssMailIcon,
|
||||||
|
cssMemberImage,
|
||||||
|
cssMemberListItem,
|
||||||
|
cssMemberPrimary,
|
||||||
|
cssMemberSecondary,
|
||||||
|
cssMemberText,
|
||||||
|
} from "app/client/ui/UserItem";
|
||||||
|
import {createUserImage, cssUserImage} from "app/client/ui/UserImage";
|
||||||
|
|
||||||
|
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) => Promise<void> | void;
|
||||||
|
isInputValid: Observable<boolean>;
|
||||||
|
prompt?: {email: string},
|
||||||
|
},
|
||||||
|
...args: DomElementArg[]
|
||||||
|
) {
|
||||||
|
const { acIndex, emailObs, save, isInputValid, prompt } = options;
|
||||||
|
const acHolder = Holder.create<Autocomplete<ACUserItem>>(owner);
|
||||||
|
let emailInput: HTMLInputElement;
|
||||||
|
|
||||||
|
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 openOrCommit = () => {
|
||||||
|
isOpen() ? commitIfValid() : acOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitIfValid = () => {
|
||||||
|
const item = acHolder.get()?.getSelectedItem();
|
||||||
|
if (item) {
|
||||||
|
emailObs.set(item.value);
|
||||||
|
}
|
||||||
|
emailInput.setCustomValidity("");
|
||||||
|
const selectedEmail = item?.value || emailObs.get();
|
||||||
|
try {
|
||||||
|
if (selectedEmail && isInputValid.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) {
|
||||||
|
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("Invite new member"),
|
||||||
|
cssMemberSecondaryPlus(
|
||||||
|
dom.text(use => `We'll email an invite to ${use(emailObs)}`)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
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(isInputValid)));
|
||||||
|
|
||||||
|
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: isInputValid},
|
||||||
|
{type: "email", placeholder: "Enter email address"},
|
||||||
|
dom.on("input", acOpen),
|
||||||
|
dom.on("focus", acOpen),
|
||||||
|
dom.on("click", acOpen),
|
||||||
|
dom.on("blur", acClose),
|
||||||
|
dom.onKeyDown({
|
||||||
|
Escape: finish,
|
||||||
|
Enter: openOrCommit,
|
||||||
|
ArrowDown: acOpen,
|
||||||
|
Tab: commitIfValid,
|
||||||
|
}),
|
||||||
|
...args
|
||||||
|
)),
|
||||||
|
cssEmailInputContainer.cls('-green', enableAdd),
|
||||||
|
);
|
||||||
|
|
||||||
|
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};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
@ -32,6 +32,10 @@ export interface IAutocompleteOptions<Item extends ACItem> {
|
|||||||
|
|
||||||
// A callback triggered when user clicks one of the choices.
|
// A callback triggered when user clicks one of the choices.
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
|
|
||||||
|
// To which element to append the popup content. Null means triggerElem.parentNode and is the
|
||||||
|
// default; string is a selector for the closest matching ancestor of triggerElem, e.g. 'body'.
|
||||||
|
attach?: Element|string|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,8 +90,10 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
|
|||||||
this.search();
|
this.search();
|
||||||
this.autoDispose(dom.onElem(_triggerElem, 'input', () => this.search()));
|
this.autoDispose(dom.onElem(_triggerElem, 'input', () => this.search()));
|
||||||
|
|
||||||
// Attach the content to the page.
|
const attachElem = _options.attach === undefined ? document.body : _options.attach;
|
||||||
document.body.appendChild(content);
|
const containerElem = getContainer(_triggerElem, attachElem) ?? document.body;
|
||||||
|
containerElem.appendChild(content);
|
||||||
|
|
||||||
this.onDispose(() => { dom.domDispose(content); content.remove(); });
|
this.onDispose(() => { dom.domDispose(content); content.remove(); });
|
||||||
|
|
||||||
// Prepare and create the Popper instance, which places the content according to the options.
|
// Prepare and create the Popper instance, which places the content according to the options.
|
||||||
@ -198,6 +204,15 @@ export const defaultPopperOptions: Partial<PopperOptions> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that finds the container according to attachElem. Null means
|
||||||
|
* elem.parentNode; string is a selector for the closest matching ancestor, e.g. 'body'.
|
||||||
|
*/
|
||||||
|
function getContainer(elem: Element, attachElem: Element|string|null): Node|null {
|
||||||
|
return (typeof attachElem === 'string') ? elem.closest(attachElem) :
|
||||||
|
(attachElem || elem.parentNode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function which returns the direct child of ancestor which is an ancestor of elem, or
|
* Helper function which returns the direct child of ancestor which is an ancestor of elem, or
|
||||||
* null if elem is not a descendant of ancestor.
|
* null if elem is not a descendant of ancestor.
|
||||||
|
@ -137,7 +137,6 @@ export const cssEmailInput = styled(input, `
|
|||||||
color: ${theme.inputFg};
|
color: ${theme.inputFg};
|
||||||
background-color: ${theme.inputBg};
|
background-color: ${theme.inputBg};
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
line-height: 42px;
|
|
||||||
font-size: ${vars.mediumFontSize};
|
font-size: ${vars.mediumFontSize};
|
||||||
font-family: ${vars.fontFamily};
|
font-family: ${vars.fontFamily};
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -10,13 +10,13 @@ import {isLongerThan} from 'app/common/gutil';
|
|||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {Organization, PermissionData, UserAPI} from 'app/common/UserAPI';
|
import {Organization, PermissionData, UserAPI} from 'app/common/UserAPI';
|
||||||
import {computed, Computed, Disposable, keyframes, observable, Observable} from 'grainjs';
|
import {Computed, Disposable, keyframes, observable, Observable, dom, DomElementArg, styled} from 'grainjs';
|
||||||
import {dom, DomElementArg, styled} from 'grainjs';
|
|
||||||
import pick = require('lodash/pick');
|
import pick = require('lodash/pick');
|
||||||
import {cssMenuItem} from 'popweasel';
|
|
||||||
|
|
||||||
|
import {ACIndexImpl, normalizeText} from 'app/client/lib/ACIndex';
|
||||||
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
||||||
import {setTestState} from 'app/client/lib/testState';
|
import {setTestState} from 'app/client/lib/testState';
|
||||||
|
import {ACUserItem, buildACMemberEmail} from 'app/client/lib/ACUserManager';
|
||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {reportError} from 'app/client/models/errors';
|
import {reportError} from 'app/client/models/errors';
|
||||||
@ -27,16 +27,16 @@ import {UserManagerModel, UserManagerModelImpl} from 'app/client/models/UserMana
|
|||||||
import {getResourceParent, ResourceType} from 'app/client/models/UserManagerModel';
|
import {getResourceParent, ResourceType} from 'app/client/models/UserManagerModel';
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
||||||
import {createUserImage, cssUserImage} from 'app/client/ui/UserImage';
|
import {createUserImage} from 'app/client/ui/UserImage';
|
||||||
import {cssEmailInput, cssEmailInputContainer, cssMailIcon, cssMemberBtn, cssMemberImage, cssMemberListItem,
|
import {cssMemberBtn, cssMemberImage, cssMemberListItem,
|
||||||
cssMemberPrimary, cssMemberSecondary, cssMemberText, cssMemberType, cssMemberTypeProblem,
|
cssMemberPrimary, cssMemberSecondary, cssMemberText, cssMemberType, cssMemberTypeProblem,
|
||||||
cssRemoveIcon} from 'app/client/ui/UserItem';
|
cssRemoveIcon} from 'app/client/ui/UserItem';
|
||||||
import {basicButton, bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {colors, mediaXSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
import {mediaXSmall, testId, theme, 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 {inputMenu, menu, menuItem, menuText} from 'app/client/ui2018/menus';
|
import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
|
||||||
import {confirmModal, cssModalBody, cssModalButtons, cssModalTitle, IModalControl,
|
import {confirmModal, cssModalBody, cssModalButtons, cssModalTitle, IModalControl,
|
||||||
modal} from 'app/client/ui2018/modals';
|
modal} from 'app/client/ui2018/modals';
|
||||||
|
|
||||||
@ -204,8 +204,12 @@ export class UserManager extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
const memberEmail = this.autoDispose(new MemberEmail(this._onAdd.bind(this),
|
const acmemberEmail = this.autoDispose(new ACMemberEmail(
|
||||||
this._options.prompt));
|
this._onAdd.bind(this),
|
||||||
|
(member) => this._model.isActiveUser(member),
|
||||||
|
this._model.membersEdited.get(),
|
||||||
|
this._options.prompt,
|
||||||
|
));
|
||||||
if (this._model.isPublicMember) {
|
if (this._model.isPublicMember) {
|
||||||
return this._buildSelfPublicAccessDom();
|
return this._buildSelfPublicAccessDom();
|
||||||
}
|
}
|
||||||
@ -215,7 +219,7 @@ export class UserManager extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
memberEmail.buildDom(),
|
acmemberEmail.buildDom(),
|
||||||
this._buildOptionsDom(),
|
this._buildOptionsDom(),
|
||||||
this._dom = shadowScroll(
|
this._dom = shadowScroll(
|
||||||
testId('um-members'),
|
testId('um-members'),
|
||||||
@ -518,90 +522,60 @@ export class UserManager extends Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserItem(member: IEditableMember): ACUserItem {
|
||||||
|
return {
|
||||||
|
value: member.email,
|
||||||
|
label: member.email,
|
||||||
|
cleanText: normalizeText(member.email),
|
||||||
|
email: member.email,
|
||||||
|
name: member.name,
|
||||||
|
picture: member?.picture,
|
||||||
|
id: member.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the widget that allows typing in an email and adding it.
|
* Represents the widget that allows typing in an email and adding it.
|
||||||
* The border of the input turns green when the email is considered valid.
|
|
||||||
*/
|
*/
|
||||||
export class MemberEmail extends Disposable {
|
export class ACMemberEmail extends Disposable {
|
||||||
public email = this.autoDispose(observable<string>(""));
|
private _email = this.autoDispose(observable<string>(""));
|
||||||
public isEmpty = this.autoDispose(computed<boolean>((use) => !use(this.email)));
|
|
||||||
|
|
||||||
private _isValid = this.autoDispose(observable<boolean>(false));
|
private _isValid = this.autoDispose(observable<boolean>(false));
|
||||||
private _emailElem: HTMLInputElement;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _onAdd: (email: string, role: roles.NonGuestRole) => void,
|
private _onAdd: (email: string, role: roles.NonGuestRole) => void,
|
||||||
private _prompt?: {email: string},
|
private _isActiveUser: (member: IEditableMember) => boolean,
|
||||||
|
private _members: Array<IEditableMember>,
|
||||||
|
private _prompt?: {email: string}
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
if (_prompt) {
|
if (_prompt) {
|
||||||
this.email.set(_prompt.email);
|
this._email.set(_prompt.email);
|
||||||
}
|
}
|
||||||
// Reset custom validity that we sometimes set.
|
|
||||||
this.email.addListener(() => this._emailElem.setCustomValidity(""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom(): Element {
|
public buildDom() {
|
||||||
const enableAdd: Computed<boolean> = computed((use) => Boolean(use(this.email) && use(this._isValid)));
|
const acUserItem = this._members.map((member: IEditableMember) => getUserItem(member));
|
||||||
const result = cssEmailInputContainer(
|
const acIndex = new ACIndexImpl<ACUserItem>(acUserItem);
|
||||||
dom.autoDispose(enableAdd),
|
|
||||||
cssMailIcon('Mail'),
|
return buildACMemberEmail(this,
|
||||||
this._emailElem = cssEmailInput(this.email, {onInput: true, isValid: this._isValid},
|
{
|
||||||
{type: "email", placeholder: "Enter email address"},
|
acIndex,
|
||||||
dom.onKeyPress({Enter: () => this._commit()}),
|
emailObs: this._email,
|
||||||
inputMenu(() => [
|
save: this._handleSave.bind(this),
|
||||||
cssInputMenuItem(() => this._commit(),
|
isInputValid: this._isValid,
|
||||||
cssUserImagePlus('+',
|
prompt: this._prompt,
|
||||||
cssUserImage.cls('-large'),
|
},
|
||||||
cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd))
|
|
||||||
),
|
|
||||||
cssMemberText(
|
|
||||||
cssMemberPrimary('Invite new member'),
|
|
||||||
cssMemberSecondary(
|
|
||||||
dom.text((use) => `We'll email an invite to ${use(this.email)}`)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
testId('um-add-email')
|
|
||||||
)
|
|
||||||
], {
|
|
||||||
// NOTE: An offset of -40px is used to center the input button across the
|
|
||||||
// input container (including an envelope icon) rather than the input inside.
|
|
||||||
modifiers: {
|
|
||||||
offset: { enabled: true, offset: -40 }
|
|
||||||
},
|
|
||||||
stretchToSelector: `.${cssEmailInputContainer.className}`,
|
|
||||||
attach: null,
|
|
||||||
boundaries: 'document' as any, // TODO: Update weasel.js types to allow 'document'.
|
|
||||||
})
|
|
||||||
),
|
|
||||||
cssEmailInputContainer.cls('-green', enableAdd),
|
|
||||||
testId('um-member-new')
|
testId('um-member-new')
|
||||||
);
|
);
|
||||||
if (this._prompt) {
|
|
||||||
this._emailElem.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the currently entered email if valid, or trigger a validation message if not.
|
private _handleSave(selectedEmail: string) {
|
||||||
private _commit() {
|
const member = this._members.find(member => member.email === selectedEmail);
|
||||||
this._emailElem.setCustomValidity("");
|
if (!member) {
|
||||||
this._isValid.set(this._emailElem.checkValidity());
|
this._onAdd(selectedEmail, roles.VIEWER);
|
||||||
if (this.email.get() && this._isValid.get()) {
|
} else if (!this._isActiveUser(member)) {
|
||||||
try {
|
member?.effectiveAccess.set(roles.VIEWER);
|
||||||
this._onAdd(this.email.get(), roles.VIEWER);
|
|
||||||
this._reset();
|
|
||||||
} catch (e) {
|
|
||||||
this._emailElem.setCustomValidity(e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this._emailElem.reportValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the widget.
|
|
||||||
private _reset() {
|
|
||||||
this.email.set("");
|
|
||||||
this._emailElem.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,25 +708,6 @@ const cssCollapseIcon = styled(icon, `
|
|||||||
background-color: ${theme.controlFg};
|
background-color: ${theme.controlFg};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssInputMenuItem = styled(menuItem, `
|
|
||||||
height: 64px;
|
|
||||||
padding: 8px 15px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
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 cssAccessLink = styled(cssLink, `
|
const cssAccessLink = styled(cssLink, `
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
Loading…
Reference in New Issue
Block a user