diff --git a/src/components/cc-orga-member-list/cc-orga-member-list.js b/src/components/cc-orga-member-list/cc-orga-member-list.js index 62ec45928..046c0dc3b 100644 --- a/src/components/cc-orga-member-list/cc-orga-member-list.js +++ b/src/components/cc-orga-member-list/cc-orga-member-list.js @@ -3,26 +3,37 @@ import { classMap } from 'lit/directives/class-map.js'; import { createRef, ref } from 'lit/directives/ref.js'; import { repeat } from 'lit/directives/repeat.js'; import { LostFocusController } from '../../controllers/lost-focus-controller.js'; -import { validateEmailAddress } from '../../lib/email.js'; import { dispatchCustomEvent } from '../../lib/events.js'; +import { formSubmit } from '../../lib/form/form-submit-directive.js'; +import { Validation } from '../../lib/form/validation.js'; import { i18n } from '../../lib/i18n.js'; +import { linkStyles } from '../../templates/cc-link/cc-link.js'; import '../cc-block/cc-block.js'; import '../cc-block-section/cc-block-section.js'; -import '../cc-orga-member-card/cc-orga-member-card.js'; +import { CcOrgaMemberCard } from '../cc-orga-member-card/cc-orga-member-card.js'; import '../cc-notice/cc-notice.js'; import '../cc-input-text/cc-input-text.js'; import '../cc-loader/cc-loader.js'; import '../cc-button/cc-button.js'; import '../cc-badge/cc-badge.js'; import '../cc-select/cc-select.js'; -import { linkStyles } from '../../templates/cc-link/cc-link.js'; /** - * @typedef {import('./cc-orga-member-list.types.js').OrgaMemberInviteFormState} OrgaMemberInviteFormState * @typedef {import('./cc-orga-member-list.types.js').OrgaMemberListState} OrgaMemberListState + * @typedef {import('./cc-orga-member-list.types.js').OrgaMemberListStateLoaded} OrgaMemberListStateLoaded * @typedef {import('./cc-orga-member-list.types.js').Authorisations} Authorisations * @typedef {import('./cc-orga-member-list.types.js').InviteMember} InviteMember + * @typedef {import('./cc-orga-member-list.types.js').InviteMemberFormState} InviteMemberFormState * @typedef {import('../cc-orga-member-card/cc-orga-member-card.types.js').OrgaMemberCardState} OrgaMemberCardState + * @typedef {import('../cc-orga-member-card/cc-orga-member-card.types.js').OrgaMember} OrgaMember + * @typedef {import('../../lib/form/validation.types.js').Validator} Validator + * @typedef {import('../../lib/form/validation.types.js').Validity} Validity + * @typedef {import('../../lib/form/form.types.js').FormDataMap} FormDataMap + * @typedef {import('../cc-input-text/cc-input-text.js').CcInputText} CcInputText + * @typedef {import('lit').TemplateResult<1>} TemplateResult + * @typedef {import('lit').PropertyValues} CcOrgaMemberListPropertyValues + * @typedef {import('lit/directives/ref.js').Ref} HTMLElementRef + * @typedef {import('lit/directives/ref.js').Ref} HTMLFormElementRef */ /** @@ -48,23 +59,11 @@ export class CcOrgaMemberList extends LitElement { static get properties () { return { authorisations: { type: Object }, - inviteMemberForm: { type: Object }, + inviteMemberFormState: { type: Object, attribute: false }, members: { type: Object }, }; } - static get INIT_INVITE_FORM_STATE () { - return { - state: 'idle', - email: { - value: '', - }, - role: { - value: 'DEVELOPER', - }, - }; - }; - static get INIT_AUTHORISATIONS () { return { invite: false, @@ -79,15 +78,17 @@ export class CcOrgaMemberList extends LitElement { /** @type {Authorisations} Sets the authorisations that control the display of the invite form and the edit / delete buttons. */ this.authorisations = CcOrgaMemberList.INIT_AUTHORISATIONS; - /** @type {OrgaMemberInviteFormState} Sets the state of the member invite form. */ - this.inviteMemberForm = CcOrgaMemberList.INIT_INVITE_FORM_STATE; + /** @type {InviteMemberFormState} Invite member form state. */ + this.inviteMemberFormState = { type: 'idle' }; /** @type {OrgaMemberListState} Sets the state of the member list. */ this.members = { state: 'loading' }; - this._inviteMemberEmailRef = createRef(); - this._inviteMemberRoleRef = createRef(); + /** @type {HTMLFormElementRef} */ + this._inviteMemberFormRef = createRef(); + /** @type {HTMLElementRef} */ this._memberListHeadingRef = createRef(); + /** @type {HTMLElementRef} */ this._noResultMessageRef = createRef(); new LostFocusController(this, 'cc-orga-member-card', ({ suggestedElement }) => { @@ -97,16 +98,39 @@ export class CcOrgaMemberList extends LitElement { else if (suggestedElement == null) { this._memberListHeadingRef.value.focus(); } - else { + else if (suggestedElement instanceof CcOrgaMemberCard) { suggestedElement.focusDeleteBtn(); } }); + + /** @type {Validator} */ + this._memberEmailValidator = { + /** + * @param {string} value + * @param {Object} _formData + * @return {Validity} + */ + validate: (value, _formData) => { + const existingEmails = (this.members.state === 'loaded') + ? this.members.value.map((member) => member.email) + : []; + + return existingEmails.includes(value) ? Validation.invalid('duplicate') : Validation.VALID; + }, + }; + this._memberEmailErrorMessages = { + duplicate: i18n('cc-orga-member-list.invite.email.error-duplicate'), + }; + } + + resetInviteMemberForm () { + this._inviteMemberFormRef.value?.reset(); } /** * Check if the given member is the last admin of the organisation. * - * @param {OrgaMemberState} member - the member to check + * @param {OrgaMember} member - the member to check * @return {boolean} - true if the given member is an admin and there is only one admin left in the organisation */ isLastAdmin (member) { @@ -118,10 +142,13 @@ export class CcOrgaMemberList extends LitElement { } /** - * @return {OrgaMemberCardState[]} AdminMembers - the list of admins in the organisation. Used to ensure we don't allow deletion of the last admin. + * @return {OrgaMemberCardState[]} - The list of admins in the organisation. Used to ensure we don't allow deletion of the last admin. */ _getAdminList () { - return this.members.value.filter((member) => member.role === 'ADMIN'); + if (this.members.state === 'loaded') { + return this.members.value.filter((member) => member.role === 'ADMIN'); + } + return []; } _getRoleOptions () { @@ -157,7 +184,7 @@ export class CcOrgaMemberList extends LitElement { return matchIdentity && matchMfaDisabled; }); - const sortedFilteredMemberList = filteredMemberList.sort((a, b) => { + return filteredMemberList.sort((a, b) => { // currentUser first, then alphabetical order - case-insensitive if (a.isCurrentUser) { return -1; @@ -165,34 +192,23 @@ export class CcOrgaMemberList extends LitElement { if (b.isCurrentUser) { return 1; } - return a.email.localeCompare(b.email, { sensitivity: 'base' }); + return a.email.localeCompare(b.email, [], { sensitivity: 'base' }); }); - - return sortedFilteredMemberList; - } - - _onEmailInput ({ detail: value }) { - this.inviteMemberForm = { - ...this.inviteMemberForm, - email: { - ...this.inviteMemberForm.email, - value: value, - }, - }; } /** * This modifies the `members` prop, triggering a new render. * Everytime there is a new render, the list is filtered based on the filter values from the `member` prop. * - * @param {Event} - * @private + * @param {CustomEvent} e */ _onFilterIdentity ({ detail: value }) { - this.members = { - ...this.members, - identityFilter: value, - }; + if (this.members.state === 'loaded') { + this.members = { + ...this.members, + identityFilter: value, + }; + } } /** @@ -202,44 +218,28 @@ export class CcOrgaMemberList extends LitElement { * @private */ _onFilterMfaDisabledOnly () { - this.members = { - ...this.members, - mfaDisabledOnlyFilter: !this.members.mfaDisabledOnlyFilter, - }; + if (this.members.state === 'loaded') { + this.members = { + ...this.members, + mfaDisabledOnlyFilter: !this.members.mfaDisabledOnlyFilter, + }; + } } - _onInviteMember () { - const existingEmails = this.members.value.map((member) => member.email); - const email = this.inviteMemberForm.email.value.trim(); - const role = this._inviteMemberRoleRef.value.value; - - const duplicateEmailError = existingEmails.includes(email) ? 'duplicate' : null; - const emailError = validateEmailAddress(email) ?? duplicateEmailError; - - // We need to re-apply the value retrieved from the DOM via refs to the template (with potential errors) - this.inviteMemberForm = { - ...this.inviteMemberForm, - email: { - value: email, - error: emailError, - }, - role: { - value: role, - }, - }; - - if (emailError == null) { - dispatchCustomEvent(this, 'invite', { email, role }); - } - else { - this._inviteMemberEmailRef.value.focus(); + /** + * @param {FormDataMap} formData + */ + _onInviteMember (formData) { + if (typeof formData.email === 'string' && typeof formData.role === 'string') { + dispatchCustomEvent(this, 'invite', { email: formData.email, role: formData.role }); } } + /** + * @param {CustomEvent} e + */ _onLeaveFromCard ({ detail: currentUser }) { - const isLastAdminLeaving = this.isLastAdmin(currentUser); - - if (isLastAdminLeaving) { + if (this.members.state === 'loaded' && this.isLastAdmin(currentUser)) { this.members = { ...this.members, value: this.members.value.map((member) => { @@ -255,34 +255,29 @@ export class CcOrgaMemberList extends LitElement { } _onLeaveFromDangerZone () { - const currentUser = this.members.value.find((member) => member.isCurrentUser); - const isLastAdminLeaving = this.isLastAdmin(currentUser); - - if (isLastAdminLeaving) { - this.members = { - ...this.members, - dangerZoneState: 'error', - }; - } - else { - dispatchCustomEvent(this, 'leave', currentUser); + if (this.members.state === 'loaded') { + const currentUser = this.members.value.find((member) => member.isCurrentUser); + const isLastAdminLeaving = this.isLastAdmin(currentUser); + + if (isLastAdminLeaving) { + this.members = { + ...this.members, + dangerZoneState: 'error', + }; + } + else { + dispatchCustomEvent(this, 'leave', currentUser); + } } } - _onRoleInput ({ detail: value }) { - this.inviteMemberForm = { - ...this.inviteMemberForm, - role: { - ...this.inviteMemberForm.role, - value: value, - }, - }; - } - + /** + * @param {CustomEvent} e + */ _onUpdateFromCard ({ detail: memberToUpdate }) { const isLastAdmin = memberToUpdate.isCurrentUser && this.isLastAdmin(memberToUpdate); - if (isLastAdmin) { + if (this.members.state === 'loaded' && isLastAdmin) { this.members = { ...this.members, value: this.members.value.map((member) => { @@ -300,26 +295,30 @@ export class CcOrgaMemberList extends LitElement { /** * Close all cards and leave the one that fired the event * - * @param {Event} - * @private + * @param {CustomEvent} e */ _onToggleCardEditing ({ detail: { memberId, newState } }) { - this.members = { - ...this.members, - value: this.members.value.map((member) => { - return (member.id === memberId) - ? { ...member, state: newState } - : { ...member, state: 'loaded' }; - }), - }; + if (this.members.state === 'loaded') { + this.members = { + ...this.members, + value: this.members.value.map((member) => { + return (member.id === memberId) + ? { ...member, state: newState } + : { ...member, state: 'loaded' }; + }), + }; + } } - /* Everytime we render a new list, remove the "last-admin" error if the list contains more than 1 admin. */ + /** + * Everytime we render a new list, remove the "last-admin" error if the list contains more than 1 admin. + * + * @param {CcOrgaMemberListPropertyValues} changedProperties + */ willUpdate (changedProperties) { - const memberListNotLoaded = this.members.state !== 'loaded'; const updateNotRelatedToMembers = !changedProperties.has('members'); - if (updateNotRelatedToMembers || memberListNotLoaded) { + if (updateNotRelatedToMembers || this.members.state !== 'loaded') { return; } @@ -372,47 +371,46 @@ export class CcOrgaMemberList extends LitElement { ` : ''} - ${this.members.state === 'loaded' ? this._renderDangerZone() : ''} + ${this.members.state === 'loaded' ? this._renderDangerZone(this.members) : ''} `; } _renderInviteForm () { - const isFormDisabled = this.inviteMemberForm.state === 'inviting'; + const isFormDisabled = this.inviteMemberFormState.type === 'inviting'; return html`
${i18n('cc-orga-member-list.invite.heading')}

${i18n('cc-orga-member-list.invite.info')}

-
+

${i18n('cc-orga-member-list.invite.email.format')}

- ${this._renderFormEmailError()}
- + ${i18n('cc-orga-member-list.invite.submit')}
@@ -421,24 +419,11 @@ export class CcOrgaMemberList extends LitElement { `; } - _renderFormEmailError () { - switch (this.inviteMemberForm.email.error) { - case 'empty': - return html`

${i18n('cc-orga-member-list.invite.email.error-empty')}

`; - case 'invalid': - return html`

${i18n('cc-orga-member-list.invite.email.error-format')}

`; - case 'duplicate': - return html`

${i18n('cc-orga-member-list.invite.email.error-duplicate')}

`; - } - return html``; - } - /** * @param {OrgaMemberCardState[]} memberList * @param {string} identityFilter * @param {boolean} mfaDisabledOnlyFilter - * @return {TemplateResult<1>} - * @private + * @return {TemplateResult} */ _renderMemberList (memberList, identityFilter, mfaDisabledOnlyFilter) { @@ -482,7 +467,10 @@ export class CcOrgaMemberList extends LitElement { `; } - _renderDangerZone () { + /** + * @param {OrgaMemberListStateLoaded} members + */ + _renderDangerZone (members) { return html`
${i18n('cc-orga-member-list.leave.heading')}
@@ -494,13 +482,13 @@ export class CcOrgaMemberList extends LitElement { danger outlined @cc-button:click=${this._onLeaveFromDangerZone} - ?disabled="${this.members.dangerZoneState === 'error'}" - ?waiting="${this.members.dangerZoneState === 'leaving'}" + ?disabled="${members.dangerZoneState === 'error'}" + ?waiting="${members.dangerZoneState === 'leaving'}" >${i18n('cc-orga-member-list.leave.btn')}
- ${this.members.dangerZoneState === 'error' ? html` + ${members.dangerZoneState === 'error' ? html`
void) => void} settings.onEvent + * @param {function} settings.updateComponent + * @param {AbortSignal} settings.signal + */ onContextUpdate ({ component, context, onEvent, updateComponent, signal }) { const { apiConfig, ownerId } = context; @@ -31,16 +49,20 @@ defineSmartComponent({ * Checks if a manager is trying to edit an admin * * @param {OrgaMemberRole} role - the current role of the member to update + * @param {Array} members - the orga members * @return {boolean} - `true` if a manager is trying to edit and admin / `false` otherwise. */ - function isManagerEditingAdmin (role) { + function isManagerEditingAdmin (role, members) { if (role !== 'ADMIN') { return false; } - const currentUser = component.members.value.find((member) => member.isCurrentUser); + const currentUser = members.find((member) => member.isCurrentUser); return currentUser.role === 'MANAGER'; } + /** + * @param {OrgaMemberRole} [role] - the current role of the member to update + */ function updateAuthorisations (role) { const hasAdminRights = role === 'ADMIN' || role === 'MANAGER'; @@ -51,52 +73,60 @@ defineSmartComponent({ }); } + /** + * + * @param {string} memberId + * @param {(orgaMember: OrgaMemberCardState) => void} callback + */ function updateMember (memberId, callback) { - updateComponent('members', (members) => { - const member = members.value.find((member) => member.id === memberId); - if (member != null) { - callback(member); - } - }); + updateComponent('members', + /** @param {OrgaMemberListStateLoaded} members */ + (members) => { + const member = members.value.find((member) => member.id === memberId); + if (member != null) { + callback(member); + } + }); } onEvent('cc-orga-member-list:invite', ({ email, role }) => { + component.inviteMemberFormState = { type: 'inviting' }; - updateComponent('inviteMemberForm', (inviteMemberForm) => { - // Note: the UI component already resets the errors and sets the field values - inviteMemberForm.state = 'inviting'; - }); - - postNewMember({ apiConfig, ownerId, email, role }) + postNewMember({ apiConfig, ownerId, email: email.trim(), role }) .then(() => { notifySuccess(i18n('cc-orga-member-list.invite.submit.success', { userEmail: email })); - updateComponent('inviteMemberForm', CcOrgaMemberList.INIT_INVITE_FORM_STATE); + component.resetInviteMemberForm(); }) - .catch((error) => { - console.error(error); - if (error.id === UNAUTHORISED_ADMIN_ADDITION) { - notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); - } - else if (error.message === RATE_LIMIT_EXCEEDED) { - notifyError(i18n('cc-orga-member-list.invite.submit.error-rate-limit.message'), i18n('cc-orga-member-list.invite.submit.error-rate-limit.title')); - } - else { - notifyError(i18n('cc-orga-member-list.invite.submit.error', { userEmail: email })); - } - - updateComponent('inviteMemberForm', (inviteMemberForm) => { - inviteMemberForm.state = 'idle'; - }); + .catch( + /** @param {Error & {id: number}} error */ + (error) => { + console.error(error); + if (error.id === UNAUTHORISED_ADMIN_ADDITION) { + notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); + } + else if (error.message === RATE_LIMIT_EXCEEDED) { + notifyError(i18n('cc-orga-member-list.invite.submit.error-rate-limit.message'), i18n('cc-orga-member-list.invite.submit.error-rate-limit.title')); + } + else { + notifyError(i18n('cc-orga-member-list.invite.submit.error', { userEmail: email })); + } + }) + .finally(() => { + component.inviteMemberFormState = { type: 'idle' }; }); }); onEvent('cc-orga-member-list:update', ({ id, role, newRole, name, email, isCurrentUser }) => { + if (component.members.state !== 'loaded') { + return; + } + /** * The API does not prevent Managers from editing Admins yet. * We need to check if a Manager tries to edit an Admin and throw an error if that's the case. */ - if (isManagerEditingAdmin(role)) { + if (isManagerEditingAdmin(role, component.members.value)) { notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); return; } @@ -105,7 +135,7 @@ defineSmartComponent({ member.state = 'updating'; }); - editMember({ apiConfig, ownerId, id, newRole, name, email, isCurrentUser }) + editMember({ apiConfig, ownerId, id, newRole }) .then(() => { notifySuccess(i18n('cc-orga-member-list.edit.success', { memberIdentity: name ?? email })); updateMember(id, (member) => { @@ -117,22 +147,24 @@ defineSmartComponent({ updateAuthorisations(newRole); } }) - .catch((error) => { - console.error(error); - if (error.id === UNAUTHORISED_ADMIN_ADDITION || error.id === UNAUTHORISED_ADMIN_DELETION) { - notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); - } - else if (error.id === MEMBER_NOT_FOUND) { - notifyError(i18n('cc-orga-member-list.error-member-not-found.text'), i18n('cc-orga-member-list.error-member-not-found.heading')); - } - else { - notifyError(i18n('cc-orga-member-list.edit.error', { memberIdentity: name ?? email })); - } + .catch( + /** @param {Error & {id: number}} error */ + (error) => { + console.error(error); + if (error.id === UNAUTHORISED_ADMIN_ADDITION || error.id === UNAUTHORISED_ADMIN_DELETION) { + notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); + } + else if (error.id === MEMBER_NOT_FOUND) { + notifyError(i18n('cc-orga-member-list.error-member-not-found.text'), i18n('cc-orga-member-list.error-member-not-found.heading')); + } + else { + notifyError(i18n('cc-orga-member-list.edit.error', { memberIdentity: name ?? email })); + } - updateMember(id, (member) => { - member.state = 'editing'; + updateMember(id, (member) => { + member.state = 'editing'; + }); }); - }); }); onEvent('cc-orga-member-card:delete', ({ id, name, email }) => { @@ -143,26 +175,30 @@ defineSmartComponent({ deleteMember({ apiConfig, ownerId, id }) .then(() => { notifySuccess(i18n('cc-orga-member-list.delete.success', { memberIdentity: name ?? email })); - updateComponent('members', (members) => { - members.value = members.value.filter((member) => member.id !== id); - }); + updateComponent('members', + /** @param {OrgaMemberListStateLoaded} members */ + (members) => { + members.value = members.value.filter((member) => member.id !== id); + }); }) - .catch((error) => { - console.error(error); - if (error.id === UNAUTHORISED_ADMIN_DELETION) { - notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); - } - else if (error.id === MEMBER_NOT_FOUND) { - notifyError(i18n('cc-orga-member-list.error-member-not-found.text'), i18n('cc-orga-member-list.error-member-not-found.heading')); - } - else { - notifyError(i18n('cc-orga-member-list.delete.error', { memberIdentity: name ?? email })); - } + .catch( + /** @param {Error & {id: number}} error */ + (error) => { + console.error(error); + if (error.id === UNAUTHORISED_ADMIN_DELETION) { + notifyError(i18n('cc-orga-member-list.error.unauthorised.text'), i18n('cc-orga-member-list.error.unauthorised.heading')); + } + else if (error.id === MEMBER_NOT_FOUND) { + notifyError(i18n('cc-orga-member-list.error-member-not-found.text'), i18n('cc-orga-member-list.error-member-not-found.heading')); + } + else { + notifyError(i18n('cc-orga-member-list.delete.error', { memberIdentity: name ?? email })); + } - updateMember(id, (member) => { - member.state = 'loaded'; + updateMember(id, (member) => { + member.state = 'loaded'; + }); }); - }); }); onEvent('cc-orga-member-list:leave', ({ id }) => { @@ -170,9 +206,11 @@ defineSmartComponent({ member.state = 'deleting'; }); - updateComponent('members', (members) => { - members.dangerZoneState = 'leaving'; - }); + updateComponent('members', + /** @param {OrgaMemberListStateLoaded} members */ + (members) => { + members.dangerZoneState = 'leaving'; + }); deleteMember({ apiConfig, ownerId, id }) .then(() => { @@ -181,21 +219,26 @@ defineSmartComponent({ updateComponent('members', { state: 'error' }); window.dispatchEvent(new Event('orga-member-leave-success')); }) - .catch((error) => { - console.error(error); - notifyError(i18n('cc-orga-member-list.leave.error')); - updateMember(id, (member) => { - member.state = 'loaded'; + .catch( + /** @param {Error} error */ + (error) => { + console.error(error); + notifyError(i18n('cc-orga-member-list.leave.error')); + updateMember(id, (member) => { + member.state = 'loaded'; + }); + updateComponent('members', + /** @param {OrgaMemberListStateLoaded} members */ + (members) => { + members.dangerZoneState = 'idle'; + }); }); - updateComponent('members', (members) => { - members.dangerZoneState = 'idle'; - }); - }); }); // Reset the component before loading updateComponent('authorisations', CcOrgaMemberList.INIT_AUTHORISATIONS); - updateComponent('inviteMemberForm', CcOrgaMemberList.INIT_INVITE_FORM_STATE); + component.resetInviteMemberForm(); + component.inviteMemberFormState = { type: 'idle' }; updateComponent('members', { state: 'loading' }); getMemberList({ apiConfig, ownerId, signal }) @@ -217,6 +260,13 @@ defineSmartComponent({ }, }); +/** + * @param {Object} args + * @param {ApiConfig} args.apiConfig + * @param {string} args.ownerId + * @param {AbortSignal} args.signal + * @return {Promise>} + */ function getMemberList ({ apiConfig, ownerId, signal }) { return Promise .all([ @@ -225,30 +275,60 @@ function getMemberList ({ apiConfig, ownerId, signal }) { ]) .then(([{ ownerId }, memberList]) => { return memberList - .map(({ member, role, job }) => ({ - id: member.id, - avatar: member.avatar, - name: member.name, - jobTitle: job, - role: role, - newRole: role, - email: member.email, - isMfaEnabled: member.preferredMFA === 'TOTP', - isCurrentUser: member.id === ownerId, - })); + .map( + /** + * @param {Object} args + * @param {{id: string, avatar: string, name: string, email: string, preferredMFA: string}} args.member + * @param {OrgaMemberRole} args.role + * @param {string} args.job + * @return {OrgaMember} + */ + ({ member, role, job }) => ({ + id: member.id, + avatar: member.avatar, + name: member.name, + jobTitle: job, + role: role, + email: member.email, + isMfaEnabled: member.preferredMFA === 'TOTP', + isCurrentUser: member.id === ownerId, + })); }); } +/** + * @param {Object} args + * @param {ApiConfig} args.apiConfig + * @param {string} args.ownerId + * @param {string} args.email + * @param {OrgaMemberRole} args.role + * @return {Promise} + */ function postNewMember ({ apiConfig, ownerId, email, role }) { return addMember({ id: ownerId }, { email, role, job: null }) .then(sendToApi({ apiConfig })); } +/** + * @param {Object} args + * @param {ApiConfig} args.apiConfig + * @param {string} args.ownerId + * @param {string} args.id + * @return {Promise} + */ function deleteMember ({ apiConfig, ownerId, id }) { return removeMember({ id: ownerId, userId: id }) .then(sendToApi({ apiConfig })); } +/** + * @param {Object} args + * @param {ApiConfig} args.apiConfig + * @param {string} args.ownerId + * @param {string} args.id + * @param {OrgaMemberRole} args.newRole + * @return {Promise} + */ function editMember ({ apiConfig, ownerId, id, newRole }) { return updateMember({ id: ownerId, userId: id }, { role: newRole }) .then(sendToApi({ apiConfig })); diff --git a/src/components/cc-orga-member-list/cc-orga-member-list.stories.js b/src/components/cc-orga-member-list/cc-orga-member-list.stories.js index f72837b7e..b5d39b8d2 100644 --- a/src/components/cc-orga-member-list/cc-orga-member-list.stories.js +++ b/src/components/cc-orga-member-list/cc-orga-member-list.stories.js @@ -45,6 +45,16 @@ const authorisationsAdmin = { edit: true, delete: true, }; +const baseItem = { + authorisations: authorisationsAdmin, + members: { + state: 'loaded', + value: baseMemberList, + identityFilter: '', + mfaDisabledOnlyFilter: false, + dangerZoneState: 'idle', + }, +}; export default { tags: ['autodocs'], @@ -52,6 +62,10 @@ export default { component: 'cc-orga-member-list', }; +/** + * @typedef {import('./cc-orga-member-list.js').CcOrgaMemberList} CcOrgaMemberList + */ + const conf = { component: 'cc-orga-member-list', }; @@ -137,15 +151,6 @@ export const waitingWithLeavingAsAdmin = makeStory(conf, { export const waitingWithInvitingMember = makeStory(conf, { items: [{ authorisations: authorisationsAdmin, - inviteMemberForm: { - state: 'inviting', - email: { - value: 'jane.doe@example.com', - }, - role: { - value: 'ADMIN', - }, - }, members: { state: 'loaded', value: [baseMemberList[0]], @@ -153,7 +158,11 @@ export const waitingWithInvitingMember = makeStory(conf, { mfaDisabledOnlyFilter: false, dangerZoneState: 'idle', }, + inviteMemberFormState: { type: 'inviting' }, }], + onUpdateComplete: (component) => { + component._inviteMemberFormRef.value.email.value = 'june.doe@example.com'; + }, }); export const errorWithLoadingMemberList = makeStory(conf, { @@ -220,73 +229,30 @@ export const errorWithEditingYourselfAsLastAdmin = makeStory(conf, { }); export const errorWithInviteEmptyEmail = makeStory(conf, { - items: [{ - authorisations: authorisationsAdmin, - inviteMemberForm: { - state: 'idle', - email: { - value: '', - error: 'empty', - }, - role: { - value: 'ADMIN', - }, - }, - members: { - state: 'loaded', - value: baseMemberList, - identityFilter: '', - mfaDisabledOnlyFilter: false, - dangerZoneState: 'idle', - }, - }], + items: [baseItem], + onUpdateComplete: (component) => { + component._inviteMemberFormRef.value.email.value = ''; + component._inviteMemberFormRef.value.email.validate(); + component._inviteMemberFormRef.value.email.reportInlineValidity(); + }, }); export const errorWithInviteInvalidEmailFormat = makeStory(conf, { - items: [{ - authorisations: authorisationsAdmin, - inviteMemberForm: { - state: 'idle', - email: { - value: 'jane.doe', - error: 'invalid', - }, - role: { - value: 'ADMIN', - }, - }, - members: { - state: 'loaded', - value: baseMemberList, - identityFilter: '', - mfaDisabledOnlyFilter: false, - dangerZoneState: 'idle', - }, - }], + items: [baseItem], + onUpdateComplete: (component) => { + component._inviteMemberFormRef.value.email.value = 'jane.doe'; + component._inviteMemberFormRef.value.email.validate(); + component._inviteMemberFormRef.value.email.reportInlineValidity(); + }, }); export const errorWithInviteMemberAlreadyInsideOrganisation = makeStory(conf, { - items: [{ - authorisations: authorisationsAdmin, - inviteMemberForm: { - state: 'idle', - email: { - value: 'june.doe@example.com', - error: 'duplicate', - }, - role: { - value: 'ACCOUNTING', - }, - }, - members: { - state: 'loaded', - value: baseMemberList, - identityFilter: '', - mfaDisabledOnlyFilter: false, - dangerZoneState: 'idle', - }, + items: [baseItem], + onUpdateComplete: (component) => { + component._inviteMemberFormRef.value.email.value = 'june.doe@example.com'; + component._inviteMemberFormRef.value.email.validate(); + component._inviteMemberFormRef.value.email.reportInlineValidity(); }, - ], }); export const dataLoaded = makeStory(conf, { @@ -331,25 +297,10 @@ export const dataLoadedWithCurrentUserAdmin = makeStory(conf, { }); export const dataLoadedWithInviteFormWithLongEmail = makeStory(conf, { - items: [{ - authorisations: authorisationsAdmin, - inviteMemberForm: { - state: 'idle', - email: { - value: 'very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-email-address@very-very-very-very-very-very-very-long.example.com', - }, - role: { - value: 'ADMIN', - }, - }, - members: { - state: 'loaded', - value: baseMemberList, - identityFilter: '', - mfaDisabledOnlyFilter: false, - dangerZoneState: 'idle', - }, - }], + items: [baseItem], + onUpdateComplete: (component) => { + component._inviteMemberFormRef.value.email.value = 'very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-email-address@very-very-very-very-very-very-very-long.example.com'; + }, }); export const dataLoadedWithOnlyOneMember = makeStory(conf, { @@ -497,42 +448,17 @@ export const simulationWithInviteMember = makeStory(conf, { }], simulations: [ storyWait(1000, ([component]) => { - component.inviteMemberForm = { - ...component.inviteMemberForm, - email: { - ...component.inviteMemberForm.email, - value: 'john.doe@example.com', - }, - }; + component._inviteMemberFormRef.value.email.value = 'john.doe@example.com'; }), storyWait(1000, ([component]) => { - component.inviteMemberForm = { - ...component.inviteMemberForm, - role: { - ...component.inviteMemberForm.role, - value: 'ADMIN', - }, - }; + component._inviteMemberFormRef.value.role.value = 'ADMIN'; }), storyWait(500, ([component]) => { - component.inviteMemberForm = { - ...component.inviteMemberForm, - state: 'inviting', - }; + component.inviteMemberFormState = { type: 'inviting' }; }), storyWait(2000, ([component]) => { - component.inviteMemberForm = { - ...component.inviteMemberForm, - state: 'idle', - email: { - ...component.inviteMemberForm.email, - value: '', - }, - role: { - ...component.inviteMemberForm.role, - value: 'DEVELOPER', - }, - }; + component.resetInviteMemberForm(); + component.inviteMemberFormState = { type: 'idle' }; }), ], }); diff --git a/src/components/cc-orga-member-list/cc-orga-member-list.types.d.ts b/src/components/cc-orga-member-list/cc-orga-member-list.types.d.ts index 0f19d8884..f661e0205 100644 --- a/src/components/cc-orga-member-list/cc-orga-member-list.types.d.ts +++ b/src/components/cc-orga-member-list/cc-orga-member-list.types.d.ts @@ -1,19 +1,4 @@ -import { OrgaMemberCardState, OrgaMemberRole } from '../cc-orga-member-card/cc-orga-member-card.types'; - -export interface OrgaMemberInviteFormState { - state: 'idle' | 'inviting'; - email: EmailFormField; - role: RoleFormField; -} - -interface EmailFormField { - value: string; - error?: 'empty' | 'invalid' | 'duplicate' | null; -} - -interface RoleFormField { - value: OrgaMemberRole; -} +import { OrgaMemberCardState } from '../cc-orga-member-card/cc-orga-member-card.types'; export type OrgaMemberListState = OrgaMemberListStateLoading | OrgaMemberListStateLoaded | OrgaMemberListStateError; @@ -43,3 +28,7 @@ interface Authorisations { edit: boolean; delete: boolean; } + +export interface InviteMemberFormState { + type: "idle" | "inviting"; +} diff --git a/src/translations/translations.en.js b/src/translations/translations.en.js index d44f5b6cb..f78fd0dad 100644 --- a/src/translations/translations.en.js +++ b/src/translations/translations.en.js @@ -654,8 +654,6 @@ export const translations = { 'cc-orga-member-list.filter.mfa': `Accounts not secured with 2FA`, 'cc-orga-member-list.filter.name': `Filter by name or email address`, 'cc-orga-member-list.invite.email.error-duplicate': `This user is already a member of this organisation.`, - 'cc-orga-member-list.invite.email.error-empty': `Please enter an email address.`, - 'cc-orga-member-list.invite.email.error-format': () => sanitize`Invalid email address format.
Example: john.doe@example.com.`, 'cc-orga-member-list.invite.email.format': `name@example.com`, 'cc-orga-member-list.invite.email.label': `Email address`, 'cc-orga-member-list.invite.heading': `Invite a member`, diff --git a/src/translations/translations.fr.js b/src/translations/translations.fr.js index 7c1a4c68d..40843ad86 100644 --- a/src/translations/translations.fr.js +++ b/src/translations/translations.fr.js @@ -667,8 +667,6 @@ export const translations = { 'cc-orga-member-list.filter.mfa': `Comptes non sécurisés par 2FA`, 'cc-orga-member-list.filter.name': `Filtrer par nom ou adresse e-mail`, 'cc-orga-member-list.invite.email.error-duplicate': `Cet utilisateur fait déjà partie des membres de votre organisation.`, - 'cc-orga-member-list.invite.email.error-empty': `Veuillez saisir une adresse e-mail.`, - 'cc-orga-member-list.invite.email.error-format': () => sanitize`Format d'adresse e-mail invalide.
Exemple: john.doe@example.com.`, 'cc-orga-member-list.invite.email.format': `nom@example.com`, 'cc-orga-member-list.invite.email.label': `Adresse e-mail`, 'cc-orga-member-list.invite.heading': `Inviter un membre`,