[IMPROVE] Rewrite contextualbar Room Members - InviteUsers (#19694)

Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
pull/19721/head
Douglas Fabris 5 years ago committed by GitHub
parent 3ea8e7835c
commit 451dc84790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/ui-flextab/client/index.js
  2. 68
      app/ui-flextab/client/tabs/createInviteLink.html
  3. 109
      app/ui-flextab/client/tabs/createInviteLink.js
  4. 9
      app/ui-flextab/client/tabs/membersList.html
  5. 38
      app/ui-flextab/client/tabs/membersList.js
  6. 8
      client/channel/adapters.js
  7. 6
      client/components/basic/VerticalBar.js
  8. 26
      client/hooks/useClipboard.js
  9. 102
      client/views/room/RoomMembers/EditInvite/EditInvite.js
  10. 21
      client/views/room/RoomMembers/EditInvite/EditInvite.stories.js
  11. 3
      client/views/room/RoomMembers/EditInvite/index.js
  12. 138
      client/views/room/RoomMembers/InviteUsers/InviteUsers.js
  13. 19
      client/views/room/RoomMembers/InviteUsers/InviteUsers.stories.js
  14. 3
      client/views/room/RoomMembers/InviteUsers/index.js

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

@ -1,68 +0,0 @@
<template name="createInviteLink">
<form class="" role="form">
{{#if isEditing}}
<div class="rc-user-info__row">
<div class="rc-input">
<div class="rc-input__title">{{_ "Expiration_(Days)"}}</div>
<label class="rc-select">
<select class="rc-select__element js-type" name="expiration_days" id="expiration_days" aria-label="{{_ "Expiration_(Days)"}}">
<option value="1" selected>1</option>
<option value="7">7</option>
<option value="15">15</option>
<option value="30">30</option>
<option value="0">{{_ 'Never'}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</label>
</div>
</div>
<div class="rc-user-info__row">
<div class="rc-input">
<div class="rc-input__title">{{_ "Max_number_of_uses"}}</div>
<label class="rc-select">
<select class="rc-select__element js-type" name="max_uses" id="max_uses" aria-label="{{_ "Max_number_of_uses"}}">
<option value="1">1</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="0" selected>{{_ 'No Limit'}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</label>
</div>
</div>
<button class="rc-button rc-button--default js-confirm-invite" title="{{_ 'Generate_New_Link'}}">{{_ 'Generate_New_Link'}}</button>
{{else}}
<div class="rc-user-info__row">
<div class="rc-input">
<div class="rc-input__title">{{_ "Invite_Link"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon rc-input__icon--right rc-input__icon--clickable js-copy">
{{> icon block="rc-input__icon-svg" icon="copy" }}
</div>
<input type="text" class="rc-input__element rc-invite-link-input" name="link" id="link" readonly value="{{ url }}"/>
</div>
</div>
</div>
<div class="rc-user-info__row">
<div class="rc-input">
<div class="rc-input__title">{{linkExpirationText}}</div>
</div>
</div>
<div class="rc-user-info__row">
<button class="rc-button rc-button--outline js-edit-invite" title="{{_ 'Edit_Invite'}}">{{_ 'Edit_Invite'}}</button>
</div>
{{/if}}
</form>
<div class="rc-user-info__flex rc-user-info__row">
<button class="rc-button rc-button--outline js-close" title="{{_ 'Close'}}">{{_ 'Close'}}</button>
</div>
</template>

@ -1,109 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import Clipboard from 'clipboard';
import { Session } from 'meteor/session';
import { t, APIClient } from '../../../utils';
import { formatDateAndTime } from '../../../lib/client/lib/formatDate';
function getInviteLink(instance, rid, days, maxUses) {
APIClient.v1.post('findOrCreateInvite', {
rid,
days,
maxUses,
}).then((result) => {
if (!result) {
toastr.error(t('Failed_to_generate_invite_link'));
return;
}
instance.inviteData.set(result);
instance.url.set(result.url);
instance.isEditing.set(false);
}).catch(() => {
toastr.error(t('Failed_to_generate_invite_link'));
});
}
Template.createInviteLink.helpers({
isEditing() {
return Template.instance().isEditing.get();
},
isReady() {
return Template.instance().isReady.get();
},
url() {
return Template.instance().url.get();
},
linkExpirationText() {
const data = Template.instance().inviteData.get();
if (!data) {
return '';
}
if (data.expires) {
const expiration = new Date(data.expires);
if (data.maxUses) {
const usesLeft = data.maxUses - data.uses;
return TAPi18n.__('Your_invite_link_will_expire_on__date__or_after__usesLeft__uses', { date: formatDateAndTime(expiration), usesLeft });
}
return TAPi18n.__('Your_invite_link_will_expire_on__date__', { date: formatDateAndTime(expiration) });
}
if (data.maxUses) {
const usesLeft = data.maxUses - data.uses;
return TAPi18n.__('Your_invite_link_will_expire_after__usesLeft__uses', { usesLeft });
}
return t('Your_invite_link_will_never_expire');
},
});
Template.createInviteLink.events({
'click .js-copy'(event, i) {
$(event.currentTarget).attr('data-clipboard-text', i.url.get());
},
'click .js-edit-invite'(e, instance) {
e.preventDefault();
instance.isEditing.set(true);
},
'click .js-confirm-invite'(e, instance) {
e.preventDefault();
const { rid } = this;
const days = parseInt($('#expiration_days').val().trim());
const maxUses = parseInt($('#max_uses').val().trim());
getInviteLink(instance, rid, days, maxUses);
},
});
Template.createInviteLink.onCreated(function() {
this.isEditing = new ReactiveVar(false);
this.isReady = new ReactiveVar(false);
this.url = new ReactiveVar('');
this.inviteData = new ReactiveVar(null);
const { rid } = this.data;
getInviteLink(this, rid);
this.clipboard = new Clipboard('.js-copy');
this.clipboard.on('success', () => {
Session.get('openedRoom') === rid && toastr.success(TAPi18n.__('Copied'));
});
});
Template.createInviteLink.onDestroyed(function() {
this.clipboard.destroy();
});

@ -65,4 +65,13 @@
{{> UserInfoWithData (userInfoDetail)}}
</div>
{{/if}}
{{#if innerTab}}
<div class="rc-user-info-container flex-nav animated">
{{> Template.dynamic template=innerTab data=innerTabData }}
</div>
{{/if}}
</template>

@ -138,34 +138,29 @@ Template.membersList.helpers({
loadingMore() {
return Template.instance().loadingMore.get();
},
avatarUrl() {
const { user: { username, avatarETag } } = this;
return getUserAvatarURL(username, avatarETag);
},
innerTab() {
return Template.instance().innerTab.get();
},
innerTabData() {
const { tabBar, innerTab } = Template.instance();
return {
rid: this.rid,
tabBar,
onClickBack: () => innerTab.set(),
};
},
});
Template.membersList.events({
'click .js-add'() {
const { tabBar } = Template.currentData();
tabBar.setTemplate('inviteUsers');
tabBar.setData({
label: 'Add_users',
icon: 'user',
});
tabBar.open();
'click .js-add'(e, instance) {
instance.innerTab.set('inviteUsers');
},
'click .js-invite'() {
const { tabBar } = Template.currentData();
tabBar.setTemplate('createInviteLink');
tabBar.setData({
label: 'Invite_Users',
icon: 'user-plus',
});
tabBar.open();
'click .js-invite'(e, instance) {
instance.innerTab.set('InviteUsers');
},
'submit .js-search-form'(event) {
event.preventDefault();
@ -276,6 +271,9 @@ Template.membersList.onCreated(function() {
this.filter = new ReactiveVar('');
this.innerTab = new ReactiveVar();
this.users = new ReactiveVar([]);
this.total = new ReactiveVar();
this.loading = new ReactiveVar(true);

@ -26,6 +26,14 @@ createTemplateForComponent('AutoTranslate', () => import('./AutoTranslate'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('InviteUsers', () => import('../views/room/RoomMembers/InviteUsers'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('EditInvite', () => import('../views/room/RoomMembers/EditInvite'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});
createTemplateForComponent('OTR', () => import('../views/room/ContextualBar/OTR'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
});

@ -77,6 +77,10 @@ function VerticalBarAction({ name, ...props }) {
return <VerticalBarButton small square flexShrink={0} ghost {...props}><VerticalBarIcon name={name}/></VerticalBarButton>;
}
function VerticalBarActionBack(props) {
return <VerticalBarAction {...props} name='arrow-back' />;
}
function VerticalBarSkeleton(props) {
return <VerticalBar { ...props }>
<VerticalBarHeader><Skeleton width='100%'/></VerticalBarHeader>
@ -101,5 +105,7 @@ VerticalBar.Content = React.memo(VerticalBarContent);
VerticalBar.ScrollableContent = React.memo(VerticalBarScrollableContent);
VerticalBar.Skeleton = React.memo(VerticalBarSkeleton);
VerticalBar.Button = React.memo(VerticalBarButton);
VerticalBar.Back = React.memo(VerticalBarActionBack);
export default VerticalBar;

@ -0,0 +1,26 @@
import { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from '../contexts/TranslationContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
export default function useClipboard(text) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const onClick = useCallback((e) => {
e.preventDefault();
try {
navigator.clipboard.writeText(text);
dispatchToastMessage({ type: 'success', message: t('Copied') });
} catch (e) {
dispatchToastMessage({ type: 'error', message: e });
}
}, [dispatchToastMessage, t, text]);
return onClick;
}
useClipboard.propTypes = {
text: PropTypes.string.isRequired,
};

@ -0,0 +1,102 @@
import React, { useMemo, useState } from 'react';
import { Box, Field, Select, Button, InputBox } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../../../contexts/TranslationContext';
import VerticalBar from '../../../../components/basic/VerticalBar';
export const EditInvite = ({
onClickBack,
onClickClose,
onClickNewLink,
days,
setDays,
maxUses,
setMaxUses,
}) => {
const t = useTranslation();
const daysOptions = useMemo(() => [
[1, 1],
[7, 7],
[15, 15],
[30, 30],
[0, t('Never')],
], [t]);
const maxUsesOptions = useMemo(() => [
[5, 5],
[10, 10],
[25, 25],
[50, 50],
[100, 100],
[0, t('No_Limit')],
], [t]);
return (
<>
<VerticalBar.Header>
{onClickBack && <VerticalBar.Back onClick={onClickBack} />}
<VerticalBar.Text>{t('Invite_Users')}</VerticalBar.Text>
{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
</VerticalBar.Header>
<VerticalBar.ScrollableContent>
<Field>
<Field.Label flexGrow={0}>{t('Expiration_(Days)')}</Field.Label>
<Field.Row>
{days === undefined ? <InputBox.Skeleton /> : <Select value={days} onChange={setDays} options={daysOptions} />}
</Field.Row>
</Field>
<Field pb='x16'>
<Field.Label flexGrow={0}>{t('Max_number_of_uses')}</Field.Label>
<Field.Row>
{maxUses === undefined ? <InputBox.Skeleton /> : <Select value={maxUses} onChange={setMaxUses} options={maxUsesOptions} />}
</Field.Row>
</Field>
<Box pb='x16'>
<Button primary onClick={onClickNewLink}>{t('Generate_New_Link')}</Button>
</Box>
</VerticalBar.ScrollableContent>
</>
);
};
export default ({
tabBar,
onClickBack,
setParams,
linkText,
captionText,
days: _days,
maxUses: _maxUses,
}) => {
const onClickClose = useMutableCallback(() => tabBar && tabBar.close());
const [days, setDays] = useState(_days);
const [maxUses, setMaxUses] = useState(_maxUses);
const generateLink = useMutableCallback(() => {
setParams({
days,
maxUses,
});
});
return (
<EditInvite
onClickBack={onClickBack}
onClickClose={onClickClose}
onClickNewLink={generateLink}
setDays={setDays}
days={days}
maxUses={maxUses}
setMaxUses={setMaxUses}
linkText={linkText}
captionText={captionText}
/>
);
};

@ -0,0 +1,21 @@
import React from 'react';
import { EditInvite } from './EditInvite';
import VerticalBar from '../../../../components/basic/VerticalBar';
export default {
title: 'components/RoomMembers/EditInvite',
component: EditInvite,
};
export const Default = () => <VerticalBar>
<EditInvite
onClickBack={alert}
onClickClose={alert}
onClickNewLink={alert}
expirationDate={1}
setExpirtaionDate={alert}
maxUses={5}
setMaxUses={alert}
/>
</VerticalBar>;

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

@ -0,0 +1,138 @@
import React, { useState, useEffect } from 'react';
import { Box, Field, UrlInput, Icon, Button, InputBox, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import useClipboard from '../../../../hooks/useClipboard';
import VerticalBar from '../../../../components/basic/VerticalBar';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useEndpoint } from '../../../../contexts/ServerContext';
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';
import EditInvite from '../EditInvite';
export const InviteUsers = ({
onClickBack,
onClickClose,
onClickEdit,
captionText,
linkText,
error,
}) => {
const t = useTranslation();
const onClickCopy = useClipboard(linkText);
return (
<>
<VerticalBar.Header>
{onClickBack && <VerticalBar.Back onClick={onClickBack} />}
<VerticalBar.Text>{t('Invite_Users')}</VerticalBar.Text>
{onClickClose && <VerticalBar.Close onClick={onClickClose} />}
</VerticalBar.Header>
<VerticalBar.ScrollableContent>
<Field>
<Field.Label flexGrow={0}>{t('Invite_Link')}</Field.Label>
<Field.Row>
{linkText === undefined ? <InputBox.Skeleton /> : <UrlInput value={linkText} addon={<Icon onClick={onClickCopy} name='copy' size='x16'/>}/>}
</Field.Row>
</Field>
<Box pb='x8' color='neutral-600' fontScale='c2'>{captionText}</Box>
{ error && <Callout mi='x24' type='danger'>{error.toString()}</Callout>}
<Box pb='x16'>
{onClickEdit && <Button onClick={onClickEdit}>{t('Edit_Invite')}</Button>}
</Box>
</VerticalBar.ScrollableContent>
</>
);
};
export default ({
rid,
tabBar,
onClickBack,
}) => {
const [editing, setEditing] = useState(false);
const format = useFormatDateAndTime();
const t = useTranslation();
const onClickClose = useMutableCallback(() => tabBar && tabBar.close());
const handleEdit = useMutableCallback(() => setEditing(true));
const onClickBackEditing = useMutableCallback(() => setEditing(false));
const findOrCreateInvite = useEndpoint('POST', 'findOrCreateInvite');
const [{ days = 1, maxUses = 0 }, setDayAndMaxUses] = useState({});
const setParams = useMutableCallback((args) => {
setDayAndMaxUses(args);
setEditing(false);
});
const [state, setState] = useState();
const linkExpirationText = useMutableCallback((data) => {
if (!data) {
return '';
}
if (data.expires) {
const expiration = new Date(data.expires);
if (data.maxUses) {
const usesLeft = data.maxUses - data.uses;
return t('Your_invite_link_will_expire_on__date__or_after__usesLeft__uses', { date: format(expiration), usesLeft });
}
return t('Your_invite_link_will_expire_on__date__', { date: format(expiration) });
}
if (data.maxUses) {
const usesLeft = data.maxUses - data.uses;
return t('Your_invite_link_will_expire_after__usesLeft__uses', { usesLeft });
}
return t('Your_invite_link_will_never_expire');
});
useEffect(() => {
if (editing) {
return;
}
(async () => {
try {
const data = await findOrCreateInvite({ rid, days, maxUses });
setState({
url: data.url,
caption: linkExpirationText(data),
});
} catch (error) {
setState({ error });
}
})();
}, [findOrCreateInvite, editing, linkExpirationText, rid, days, maxUses]);
if (editing) {
return <EditInvite
onClickBack={onClickBackEditing}
linkText={state?.url}
captionText={state?.caption}
{...{ rid, tabBar, error: state?.error, setParams, days, maxUses }}
/>;
}
return (
<InviteUsers
error={state?.error}
onClickClose={onClickClose}
onClickBack={onClickBack}
onClickEdit={handleEdit}
linkText={state?.url}
captionText={state?.caption}
/>
);
};

@ -0,0 +1,19 @@
import React from 'react';
import { InviteUsers } from './InviteUsers';
import VerticalBar from '../../../../components/basic/VerticalBar';
export default {
title: 'components/RoomMembers/InviteUsers',
component: InviteUsers,
};
export const Default = () => <VerticalBar>
<InviteUsers
linkText='https://go.rocket.chat/invite?host=open.rocket.chat&path=invite%2F5sBs3a`'
captionText='Expire on February 4, 2020 4:45 PM.'
onClickBack={alert}
onClickClose={alert}
onClickEdit={alert}
/>
</VerticalBar>;

@ -0,0 +1,3 @@
import InviteUsers from './InviteUsers';
export default InviteUsers;
Loading…
Cancel
Save