[IMPROVE] Rewrite contextualbar RoomMembers - AddUsers as React Component (#19803)

Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
header_user
Douglas Fabris 5 years ago committed by GitHub
parent 4e72f02a6b
commit edcc10d59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/ui-flextab/client/index.js
  2. 26
      app/ui-flextab/client/tabs/inviteUsers.html
  3. 164
      app/ui-flextab/client/tabs/inviteUsers.js
  4. 2
      app/ui-flextab/client/tabs/membersList.js
  5. 6
      client/components/AutoComplete.stories.js
  6. 4
      client/views/room/adapters.js
  7. 99
      client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.js
  8. 29
      client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.stories.js
  9. 3
      client/views/room/contextualBar/RoomMembers/AddUsers/index.js
  10. 1
      client/views/room/contextualBar/RoomMembers/EditInvite/EditInvite.js
  11. 5
      ee/client/audit/UserAutoCompleteMultiple.js
  12. 2
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -1,8 +1,6 @@
import './flexTabBar.html';
import './tabs/inviteUsers.html';
import './tabs/membersList.html';
import './tabs/uploadedFilesList.html';
import './flexTabBar';
import './tabs/inviteUsers';
import './tabs/membersList';
import './tabs/uploadedFilesList';

@ -1,26 +0,0 @@
<template name="inviteUsers">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Invite_Users"}}</div>
<div class="rc-input__wrapper">
<div class="rc-tags rc-tags--no-icon">
{{#each user in selectedUsers}}
{{> tag user}}
{{/each}}
<input type="text" class="rc-tags__input" placeholder="{{_ "Username_Placeholder"}}"
name="users" autocomplete="off"/>
</div>
</div>
{{#with config}}
{{#if autocomplete 'isShowing'}}
{{> popupList data=config items=items ready=(autocomplete 'isLoaded')}}
{{/if}}
{{/with}}
</label>
</div>
<div class="rc-user-info__flex rc-user-info__row">
<button class="rc-button rc-button--outline js-close" title="{{_ 'Cancel'}}">{{_ 'Cancel'}}</button>
<button class="rc-button rc-button--primary js-add" disabled={{disabled}}>{{> icon block="rc-input__icon-svg" icon="plus"}}{{tAddUsers}}</button>
</div>
</template>

@ -1,164 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Blaze } from 'meteor/blaze';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { Deps } from 'meteor/deps';
import toastr from 'toastr';
import { settings } from '../../../settings';
import { t, handleError } from '../../../utils/client';
import { AutoComplete } from '../../../meteor-autocomplete/client';
const acEvents = {
'click .rc-popup-list__item'(e, t) {
t.ac.onItemClick(this, e);
},
'keydown [name="users"]'(e, t) {
if ([8, 46].includes(e.keyCode) && e.target.value === '') {
const users = t.selectedUsers;
const usersArr = users.get();
usersArr.pop();
return users.set(usersArr);
}
t.ac.onKeyDown(e);
},
'keyup [name="users"]'(e, t) {
t.ac.onKeyUp(e);
},
'focus [name="users"]'(e, t) {
t.ac.onFocus(e);
},
'blur [name="users"]'(e, t) {
t.ac.onBlur(e);
},
};
const filterNames = (old) => {
if (settings.get('UI_Allow_room_names_with_special_chars')) {
return old;
}
const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`);
return [...old.replace(' ', '').toLocaleLowerCase()].filter((f) => reg.test(f)).join('');
};
Template.inviteUsers.helpers({
disabled() {
return Template.instance().selectedUsers.get().length === 0;
},
tAddUsers() {
return t('Add_users');
},
autocomplete(key) {
const instance = Template.instance();
const param = instance.ac[key];
return typeof param === 'function' ? param.apply(instance.ac) : param;
},
items() {
return Template.instance().ac.filteredList();
},
config() {
const filter = Template.instance().userFilter.get();
return {
filter,
noMatchTemplate: 'userSearchEmpty',
modifier(text) {
const f = filter;
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter), function(part) {
return `<strong>${ part }</strong>`;
}) }`;
},
};
},
selectedUsers() {
return Template.instance().selectedUsers.get();
},
});
Template.inviteUsers.events({
...acEvents,
'click .rc-tags__tag'({ target }, t) {
const { username } = Blaze.getData(target);
t.selectedUsers.set(t.selectedUsers.get().filter((user) => user.username !== username));
},
'click .rc-tags__tag-icon'(e, t) {
const { username } = Blaze.getData(t.find('.rc-tags__tag-text'));
t.selectedUsers.set(t.selectedUsers.get().filter((user) => user.username !== username));
},
'input [name="users"]'(e, t) {
const input = e.target;
const position = input.selectionEnd || input.selectionStart;
const { length } = input.value;
const modified = filterNames(input.value);
input.value = modified;
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length);
t.userFilter.set(modified);
},
'click .js-add'(e, instance) {
const users = instance.selectedUsers.get().map(({ username }) => username);
Meteor.call('addUsersToRoom', {
rid: Session.get('openedRoom'),
users,
}, function(err) {
if (err) {
return handleError(err);
}
toastr.success(t('Users_added'));
instance.selectedUsers.set([]);
});
},
});
Template.inviteUsers.onRendered(function() {
const users = this.selectedUsers;
this.firstNode.querySelector('[name="users"]').focus();
this.ac.element = this.firstNode.querySelector('[name="users"]');
this.ac.$element = $(this.ac.element);
this.ac.$element.on('autocompleteselect', function(e, { item }) {
const usersArr = users.get();
usersArr.push(item);
users.set(usersArr);
});
});
Template.inviteUsers.onCreated(function() {
this.selectedUsers = new ReactiveVar([]);
const filter = { exceptions: [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username)) };
Deps.autorun(() => {
filter.exceptions = [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username));
});
this.userFilter = new ReactiveVar('');
this.ac = new AutoComplete({
selector: {
anchor: '.rc-input__label',
item: '.rc-popup-list__item',
container: '.rc-popup-list__list',
},
position: 'fixed',
limit: 10,
inputDelay: 300,
rules: [
{
// @TODO maybe change this 'collection' and/or template
collection: 'UserAndRoom',
endpoint: 'users.autocomplete',
field: 'username',
matchAll: true,
filter,
doNotChangeWidth: false,
selector(match) {
return { term: match };
},
sort: 'username',
},
],
});
this.ac.tmplInst = this;
});

@ -157,7 +157,7 @@ Template.membersList.helpers({
Template.membersList.events({
'click .js-add'(e, instance) {
instance.tabBar.setTemplate('inviteUsers');
instance.innerTab.set('AddUsers');
},
'click .js-invite'(e, instance) {
instance.innerTab.set('InviteUsers');

@ -1,10 +1,10 @@
import React from 'react';
import AutoComplete from './AutoComplete';
import { UserAutoComplete } from './AutoComplete';
export default {
title: 'components/AutoComplete',
component: AutoComplete,
component: UserAutoComplete,
};
export const Example = () => <AutoComplete/>;
export const Example = () => <UserAutoComplete/>;

@ -38,6 +38,10 @@ createTemplateForComponent('EditInvite', () => import('./contextualBar/RoomMembe
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('AddUsers', () => import('./contextualBar/RoomMembers/AddUsers'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('OTR', () => import('./contextualBar/OTR'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});

@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Field, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import UserAutoCompleteMultiple from '../../../../../../ee/client/audit/UserAutoCompleteMultiple';
import VerticalBar from '../../../../../components/VerticalBar';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useForm } from '../../../../../hooks/useForm';
import { useMethod } from '../../../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
export const AddUsers = ({
onClickClose,
onClickBack,
onClickSave,
value,
onChange,
errors,
}) => {
const t = useTranslation();
return (
<>
<VerticalBar.Header>
{onClickBack && <VerticalBar.Back onClick={onClickBack} />}
<VerticalBar.Text>{t('Add_users')}</VerticalBar.Text>
{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
</VerticalBar.Header>
<VerticalBar.ScrollableContent>
<Field >
<Field.Label flexGrow={0}>{t('Choose_users')}</Field.Label>
<UserAutoCompleteMultiple errors={errors.users} value={value} onChange={onChange} placeholder={t('Choose_users')} />
{errors.users && <Field.Error>
{errors.users}
</Field.Error>}
</Field>
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<Button primary disabled={!value || value.length === 0} onClick={onClickSave}>{t('Add_users')}</Button>
</VerticalBar.Footer>
</>
);
};
export default ({
rid,
tabBar,
onClickBack,
}) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [errors, setErrors] = useState({});
const onClickClose = useMutableCallback(() => tabBar && tabBar.close());
const saveAction = useMethod('addUsersToRoom');
const { values, handlers } = useForm({ users: [] });
const { users } = values;
const { handleUsers } = handlers;
const onChangeUsers = useMutableCallback((value, action) => {
if (!action) {
if (users.includes(value)) {
return;
}
return handleUsers([...users, value]);
}
handleUsers(users.filter((current) => current !== value));
});
const handleSave = useMutableCallback(async () => {
if (users.length < 1) {
return setErrors({
users: t('Select_at_least_one_user'),
});
}
try {
await saveAction({ rid, users });
dispatchToastMessage({ type: 'success', message: t('Users_added') });
onClickBack();
} catch (e) {
dispatchToastMessage({ type: 'error', message: e });
}
setErrors({});
});
return (
<AddUsers
onClickClose={onClickClose}
onClickBack={onClickBack}
onClickSave={handleSave}
value={users}
onChange={onChangeUsers}
errors={errors}
/>
);
};

@ -0,0 +1,29 @@
import React from 'react';
import { AddUsers } from './AddUsers';
import VerticalBar from '../../../../../components/VerticalBar';
export default {
title: 'components/RoomMembers/AddUsers',
component: AddUsers,
};
export const Default = () => <VerticalBar>
<AddUsers
onClickBack={alert}
onClickClose={alert}
onClickSave={alert}
value={[]}
errors={{}}
/>
</VerticalBar>;
export const Error = () => <VerticalBar>
<AddUsers
onClickBack={alert}
onClickClose={alert}
onClickSave={alert}
value={[]}
errors={{ users: 'With Test Error' }}
/>
</VerticalBar>;

@ -0,0 +1,3 @@
import AddUsers from './AddUsers';
export default AddUsers;

@ -72,7 +72,6 @@ export default ({
captionText,
days: _days,
maxUses: _maxUses,
}) => {
const onClickClose = useMutableCallback(() => tabBar && tabBar.close());

@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react';
import { AutoComplete, Option, Options, Chip } from '@rocket.chat/fuselage';
import { AutoComplete, Box, Option, Options, Chip } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import UserAvatar from '../../../client/components/avatar/UserAvatar';
@ -18,11 +18,12 @@ const UserAutoCompleteMultiple = React.memo((props) => {
e.preventDefault();
props.onChange(e.currentTarget.value, 'remove');
});
return <AutoComplete
{...props}
filter={filter}
setFilter={setFilter}
renderSelected={({ value: selected }) => selected?.map((value) => <Chip key={value} {...props} height='x20' value={value} onClick={onClickRemove} mie='x4'><UserAvatar size='x20' username={value} />{value}</Chip>)}
renderSelected={({ value: selected }) => selected?.map((value) => <Chip key={value} {...props} height='x20' value={value} onClick={onClickRemove} mie='x4'><UserAvatar size='x20' username={value} /><Box is='span' margin='none' mis='x4'>{value}</Box></Chip>)}
renderItem={({ value, ...props }) => <Option key={value} {...props} avatar={<Avatar value={value} />} />}
options={ options }
/>;

@ -746,6 +746,7 @@
"Choose_messages": "Choose messages",
"Choose_the_alias_that_will_appear_before_the_username_in_messages": "Choose the alias that will appear before the username in messages.",
"Choose_the_username_that_this_integration_will_post_as": "Choose the username that this integration will post as.",
"Choose_users": "Choose users",
"Clean_Usernames": "Clear usernames",
"clean-channel-history": "Clean Channel History",
"clean-channel-history_description": "Permission to Clear the history from channels",
@ -3307,6 +3308,7 @@
"Select_a_user": "Select a user",
"Select_an_avatar": "Select an avatar",
"Select_an_option": "Select an option",
"Select_at_least_one_user": "Select at least one user",
"Select_at_least_two_users": "Select at least two users",
"Select_department": "Select a department",
"Select_file": "Select file",

Loading…
Cancel
Save