(core) Fixing UserManger releted tests

Summary: Some tests were not compatible with the new ACUser search component.

Test Plan: Existing

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3643
This commit is contained in:
Jarosław Sadziński 2022-09-27 23:06:02 +02:00
parent a5744dadfb
commit 0af379db7d
4 changed files with 43 additions and 57 deletions

View File

@ -1,11 +1,8 @@
import {ACResults, ACIndex, ACItem, buildHighlightedDom, normalizeText} from "app/client/lib/ACIndex"; import {ACIndex, ACItem, ACResults, buildHighlightedDom, normalizeText} from "app/client/lib/ACIndex";
import {cssSelectItem} from "app/client/lib/ACSelect"; import {cssSelectItem} from "app/client/lib/ACSelect";
import {Autocomplete, IAutocompleteOptions} from "app/client/lib/autocomplete"; import {Autocomplete, IAutocompleteOptions} from "app/client/lib/autocomplete";
import {cssMenuItem} from "popweasel"; import {colors, testId, theme} from "app/client/ui2018/cssVars";
import {testId, colors, theme} from "app/client/ui2018/cssVars";
import {menuCssClass} from "app/client/ui2018/menus"; import {menuCssClass} from "app/client/ui2018/menus";
import {dom, DomElementArg, Holder, IDisposableOwner, Observable, styled, computed, Computed} from "grainjs";
import { import {
cssEmailInput, cssEmailInput,
cssEmailInputContainer, cssEmailInputContainer,
@ -17,6 +14,8 @@ import {
cssMemberText, cssMemberText,
} from "app/client/ui/UserItem"; } from "app/client/ui/UserItem";
import {createUserImage, cssUserImage} from "app/client/ui/UserImage"; import {createUserImage, cssUserImage} from "app/client/ui/UserImage";
import {Computed, computed, dom, DomElementArg, Holder, IDisposableOwner, Observable, styled} from "grainjs";
import {cssMenuItem} from "popweasel";
export interface ACUserItem extends ACItem { export interface ACUserItem extends ACItem {
value: string; value: string;
@ -33,16 +32,17 @@ export function buildACMemberEmail(
options: { options: {
acIndex: ACIndex<ACUserItem>; acIndex: ACIndex<ACUserItem>;
emailObs: Observable<string>; emailObs: Observable<string>;
save: (value: string) => Promise<void> | void; save: (value: string) => void;
isInputValid: Observable<boolean>;
prompt?: {email: string}, prompt?: {email: string},
}, },
...args: DomElementArg[] ...args: DomElementArg[]
) { ) {
const { acIndex, emailObs, save, isInputValid, prompt } = options; const {acIndex, emailObs, save, prompt} = options;
const acHolder = Holder.create<Autocomplete<ACUserItem>>(owner); const acHolder = Holder.create<Autocomplete<ACUserItem>>(owner);
let emailInput: HTMLInputElement; let emailInput: HTMLInputElement;
const isValid = Observable.create(owner, true);
const isOpen = () => !acHolder.isEmpty(); const isOpen = () => !acHolder.isEmpty();
const acOpen = () => acHolder.isEmpty() && Autocomplete.create(acHolder, emailInput, acOptions); const acOpen = () => acHolder.isEmpty() && Autocomplete.create(acHolder, emailInput, acOptions);
const acClose = () => acHolder.clear(); const acClose = () => acHolder.clear();
@ -52,7 +52,7 @@ export function buildACMemberEmail(
emailInput.value = emailObs.get(); emailInput.value = emailObs.get();
emailInput.focus(); emailInput.focus();
}; };
const openOrCommit = () => { const onEnter = () => {
isOpen() ? commitIfValid() : acOpen(); isOpen() ? commitIfValid() : acOpen();
}; };
@ -62,14 +62,16 @@ export function buildACMemberEmail(
emailObs.set(item.value); emailObs.set(item.value);
} }
emailInput.setCustomValidity(""); emailInput.setCustomValidity("");
isValid.set(emailInput.checkValidity());
const selectedEmail = item?.value || emailObs.get(); const selectedEmail = item?.value || emailObs.get();
try { try {
if (selectedEmail && isInputValid.get()) { if (selectedEmail && isValid.get()) {
save(emailObs.get()); save(emailObs.get());
finish(); finish();
} }
} catch (e) { } catch (e) {
emailInput.setCustomValidity(e.message); emailInput.setCustomValidity(e.message);
} finally { } finally {
emailInput.reportValidity(); emailInput.reportValidity();
} }
@ -79,7 +81,7 @@ export function buildACMemberEmail(
const cleanText = normalizeText(text); const cleanText = normalizeText(text);
const items = results.items const items = results.items
.filter(item => item.cleanText.includes(cleanText)) .filter(item => item.cleanText.includes(cleanText))
.sort((a,b) => a.cleanText.localeCompare(b.cleanText)); .sort((a, b) => a.cleanText.localeCompare(b.cleanText));
results.items = items; results.items = items;
if (!results.items.length) { if (!results.items.length) {
const newObject = { const newObject = {
@ -102,7 +104,7 @@ export function buildACMemberEmail(
"+", "+",
cssUserImage.cls("-large"), cssUserImage.cls("-large"),
cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd), cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd),
)), )),
cssMemberText( cssMemberText(
cssMemberPrimaryPlus("Invite new member"), cssMemberPrimaryPlus("Invite new member"),
cssMemberSecondaryPlus( cssMemberSecondaryPlus(
@ -121,7 +123,7 @@ export function buildACMemberEmail(
) )
)); ));
const enableAdd: Computed<boolean> = computed((use) => Boolean(use(emailObs) && use(isInputValid))); const enableAdd: Computed<boolean> = computed((use) => Boolean(use(emailObs) && use(isValid)));
const acOptions: IAutocompleteOptions<ACUserItem> = { const acOptions: IAutocompleteOptions<ACUserItem> = {
attach: null, attach: null,
@ -136,7 +138,7 @@ export function buildACMemberEmail(
cssMailIcon("Mail"), cssMailIcon("Mail"),
(emailInput = cssEmailInput( (emailInput = cssEmailInput(
emailObs, emailObs,
{onInput: true, isValid: isInputValid}, {onInput: true, isValid},
{type: "email", placeholder: "Enter email address"}, {type: "email", placeholder: "Enter email address"},
dom.on("input", acOpen), dom.on("input", acOpen),
dom.on("focus", acOpen), dom.on("focus", acOpen),
@ -144,51 +146,43 @@ export function buildACMemberEmail(
dom.on("blur", acClose), dom.on("blur", acClose),
dom.onKeyDown({ dom.onKeyDown({
Escape: finish, Escape: finish,
Enter: openOrCommit, Enter: onEnter,
ArrowDown: acOpen, ArrowDown: acOpen,
Tab: commitIfValid, Tab: commitIfValid,
}), }),
...args )),
)),
cssEmailInputContainer.cls('-green', enableAdd), cssEmailInputContainer.cls('-green', enableAdd),
...args
); );
// Reset custom validity that we sometimes set.
owner.autoDispose(emailObs.addListener(() => emailInput.setCustomValidity("")));
if (prompt) { setTimeout(() => emailInput.focus(), 0); } if (prompt) { setTimeout(() => emailInput.focus(), 0); }
return result; return result;
} }
const cssMemberPrimaryPlus = styled( const cssMemberPrimaryPlus = styled(cssMemberPrimary, `
cssMemberPrimary,
`
.${cssSelectItem.className}.selected & { .${cssSelectItem.className}.selected & {
color: ${theme.menuItemSelectedFg}; color: ${theme.menuItemSelectedFg};
} }
` `);
);
const cssMemberSecondaryPlus = styled( const cssMemberSecondaryPlus = styled(cssMemberSecondary, `
cssMemberSecondary,
`
.${cssSelectItem.className}.selected & { .${cssSelectItem.className}.selected & {
color: ${theme.menuItemSelectedFg}; color: ${theme.menuItemSelectedFg};
} }
` `);
);
const cssMatchText = styled( const cssMatchText = styled("span", `
"span",
`
color: ${theme.autocompleteMatchText}; color: ${theme.autocompleteMatchText};
.${cssSelectItem.className}.selected & { .${cssSelectItem.className}.selected & {
color: ${theme.autocompleteSelectedMatchText}; color: ${theme.autocompleteSelectedMatchText};
} }
` `);
);
const cssUserImagePlus = styled( const cssUserImagePlus = styled(cssUserImage, `
cssUserImage,
`
background-color: ${colors.lightGreen}; background-color: ${colors.lightGreen};
margin: auto 0; margin: auto 0;
@ -200,5 +194,4 @@ const cssUserImagePlus = styled(
background-color: ${theme.menuItemIconSelectedFg}; background-color: ${theme.menuItemIconSelectedFg};
color: ${theme.menuItemSelectedBg}; color: ${theme.menuItemSelectedBg};
} }
` `);
);

View File

@ -21,6 +21,11 @@ export interface IAutocompleteOptions<Item extends ACItem> {
// Popper options for positioning the popup. // Popper options for positioning the popup.
popperOptions?: Partial<PopperOptions>; popperOptions?: Partial<PopperOptions>;
// To which element to append the popup content. Null means triggerElem.parentNode; string is
// a selector for the closest matching ancestor of triggerElem, e.g. 'body'.
// Defaults to the document body.
attach?: Element|string|null;
// Given a search term, return the list of Items to render. // Given a search term, return the list of Items to render.
search(searchText: string): Promise<ACResults<Item>>; search(searchText: string): Promise<ACResults<Item>>;
@ -32,10 +37,6 @@ 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;
} }
/** /**

View File

@ -32,6 +32,8 @@ export interface UserManagerModel {
activeUser: FullUser|null; // Populated if current user is logged in. activeUser: FullUser|null; // Populated if current user is logged in.
gristDoc: GristDoc|null; // Populated if there is an open document. gristDoc: GristDoc|null; // Populated if there is an open document.
// Analyze the relation that users have to the resource or site.
annotate(): void;
// Resets all unsaved changes // Resets all unsaved changes
reset(): void; reset(): void;
// Recreate annotations, factoring in any changes on the back-end. // Recreate annotations, factoring in any changes on the back-end.
@ -253,7 +255,6 @@ export class UserManagerModelImpl extends Disposable implements UserManagerModel
return member.email === this.activeUser?.email; return member.email === this.activeUser?.email;
} }
// Analyze the relation that users have to the resource or site.
public annotate() { public annotate() {
// Only attempt for documents for now. // Only attempt for documents for now.
// TODO: extend to workspaces. // TODO: extend to workspaces.

View File

@ -10,7 +10,7 @@ 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, Disposable, keyframes, observable, Observable, dom, DomElementArg, styled} from 'grainjs'; import {Computed, Disposable, dom, DomElementArg, keyframes, Observable, observable, styled} from 'grainjs';
import pick = require('lodash/pick'); import pick = require('lodash/pick');
import {ACIndexImpl, normalizeText} from 'app/client/lib/ACIndex'; import {ACIndexImpl, normalizeText} from 'app/client/lib/ACIndex';
@ -204,9 +204,8 @@ export class UserManager extends Disposable {
} }
public buildDom() { public buildDom() {
const acmemberEmail = this.autoDispose(new ACMemberEmail( const acMemberEmail = this.autoDispose(new ACMemberEmail(
this._onAdd.bind(this), this._onAdd.bind(this),
(member) => this._model.isActiveUser(member),
this._model.membersEdited.get(), this._model.membersEdited.get(),
this._options.prompt, this._options.prompt,
)); ));
@ -219,7 +218,7 @@ export class UserManager extends Disposable {
} }
return [ return [
acmemberEmail.buildDom(), acMemberEmail.buildDom(),
this._buildOptionsDom(), this._buildOptionsDom(),
this._dom = shadowScroll( this._dom = shadowScroll(
testId('um-members'), testId('um-members'),
@ -531,7 +530,7 @@ function getUserItem(member: IEditableMember): ACUserItem {
name: member.name, name: member.name,
picture: member?.picture, picture: member?.picture,
id: member.id, id: member.id,
} };
} }
/** /**
@ -539,11 +538,9 @@ function getUserItem(member: IEditableMember): ACUserItem {
*/ */
export class ACMemberEmail extends Disposable { export class ACMemberEmail extends Disposable {
private _email = this.autoDispose(observable<string>("")); private _email = this.autoDispose(observable<string>(""));
private _isValid = this.autoDispose(observable<boolean>(false));
constructor( constructor(
private _onAdd: (email: string, role: roles.NonGuestRole) => void, private _onAdd: (email: string, role: roles.NonGuestRole) => void,
private _isActiveUser: (member: IEditableMember) => boolean,
private _members: Array<IEditableMember>, private _members: Array<IEditableMember>,
private _prompt?: {email: string} private _prompt?: {email: string}
) { ) {
@ -562,7 +559,6 @@ export class ACMemberEmail extends Disposable {
acIndex, acIndex,
emailObs: this._email, emailObs: this._email,
save: this._handleSave.bind(this), save: this._handleSave.bind(this),
isInputValid: this._isValid,
prompt: this._prompt, prompt: this._prompt,
}, },
testId('um-member-new') testId('um-member-new')
@ -570,12 +566,7 @@ export class ACMemberEmail extends Disposable {
} }
private _handleSave(selectedEmail: string) { private _handleSave(selectedEmail: string) {
const member = this._members.find(member => member.email === selectedEmail); this._onAdd(selectedEmail, roles.VIEWER);
if (!member) {
this._onAdd(selectedEmail, roles.VIEWER);
} else if (!this._isActiveUser(member)) {
member?.effectiveAccess.set(roles.VIEWER);
}
} }
} }