[NEW] Teams (#20966)
parent
499b110412
commit
b543d0c58a
@ -0,0 +1,321 @@ |
||||
import { Promise } from 'meteor/promise'; |
||||
|
||||
import { API } from '../api'; |
||||
import { Team } from '../../../../server/sdk'; |
||||
import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/server'; |
||||
import { Rooms, Subscriptions } from '../../../models/server'; |
||||
|
||||
API.v1.addRoute('teams.list', { authRequired: true }, { |
||||
get() { |
||||
const { offset, count } = this.getPaginationItems(); |
||||
const { sort, query } = this.parseJsonQuery(); |
||||
|
||||
const { records, total } = Promise.await(Team.list(this.userId, { offset, count }, { sort, query })); |
||||
|
||||
return API.v1.success({ |
||||
teams: records, |
||||
total, |
||||
count: records.length, |
||||
offset, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.listAll', { authRequired: true }, { |
||||
get() { |
||||
if (!hasPermission(this.userId, 'view-all-teams')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { offset, count } = this.getPaginationItems(); |
||||
|
||||
const { records, total } = Promise.await(Team.listAll({ offset, count })); |
||||
|
||||
return API.v1.success({ |
||||
teams: records, |
||||
total, |
||||
count: records.length, |
||||
offset, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.create', { authRequired: true }, { |
||||
post() { |
||||
if (!hasPermission(this.userId, 'create-team')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
const { name, type, members, room, owner } = this.bodyParams; |
||||
|
||||
if (!name) { |
||||
return API.v1.failure('Body param "name" is required'); |
||||
} |
||||
|
||||
const team = Promise.await(Team.create(this.userId, { |
||||
team: { |
||||
name, |
||||
type, |
||||
}, |
||||
room, |
||||
members, |
||||
owner, |
||||
})); |
||||
|
||||
return API.v1.success({ team }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.addRoom', { authRequired: true }, { |
||||
post() { |
||||
const { roomId, teamId, isDefault } = this.bodyParams; |
||||
|
||||
if (!hasPermission(this.userId, 'add-team-channel')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const room = Promise.await(Team.addRoom(this.userId, roomId, teamId, isDefault)); |
||||
|
||||
return API.v1.success({ room }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.addRooms', { authRequired: true }, { |
||||
post() { |
||||
const { rooms, teamId } = this.bodyParams; |
||||
|
||||
if (!hasPermission(this.userId, 'add-team-channel')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const validRooms = Promise.await(Team.addRooms(this.userId, rooms, teamId)); |
||||
|
||||
return API.v1.success({ rooms: validRooms }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.removeRoom', { authRequired: true }, { |
||||
post() { |
||||
const { roomId, teamId } = this.bodyParams; |
||||
|
||||
if (!hasPermission(this.userId, 'remove-team-channel')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels'); |
||||
|
||||
const room = Promise.await(Team.removeRoom(this.userId, roomId, teamId, canRemoveAny)); |
||||
|
||||
return API.v1.success({ room }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.updateRoom', { authRequired: true }, { |
||||
post() { |
||||
const { roomId, isDefault } = this.bodyParams; |
||||
|
||||
if (!hasPermission(this.userId, 'edit-team-channel')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
const canUpdateAny = !!hasPermission(this.userId, 'view-all-team-channels'); |
||||
|
||||
const room = Promise.await(Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny)); |
||||
|
||||
return API.v1.success({ room }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.listRooms', { authRequired: true }, { |
||||
get() { |
||||
const { teamId } = this.queryParams; |
||||
const { offset, count } = this.getPaginationItems(); |
||||
|
||||
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams'); |
||||
|
||||
let getAllRooms = false; |
||||
if (hasPermission(this.userId, 'view-all-team-channels')) { |
||||
getAllRooms = true; |
||||
} |
||||
|
||||
const { records, total } = Promise.await(Team.listRooms(this.userId, teamId, getAllRooms, allowPrivateTeam, { offset, count })); |
||||
|
||||
return API.v1.success({ |
||||
rooms: records, |
||||
total, |
||||
count: records.length, |
||||
offset, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { |
||||
get() { |
||||
const { offset, count } = this.getPaginationItems(); |
||||
const { teamId, userId } = this.queryParams; |
||||
|
||||
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams'); |
||||
|
||||
if (!hasPermission(this.userId, 'view-all-team-channels')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, teamId, userId, allowPrivateTeam, { offset, count })); |
||||
|
||||
return API.v1.success({ |
||||
rooms: records, |
||||
total, |
||||
count: records.length, |
||||
offset: 0, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.members', { authRequired: true }, { |
||||
get() { |
||||
const { offset, count } = this.getPaginationItems(); |
||||
const { teamId, teamName } = this.queryParams; |
||||
const { query } = this.parseJsonQuery(); |
||||
const canSeeAllMembers = hasPermission(this.userId, 'view-all-teams'); |
||||
|
||||
const { records, total } = Promise.await(Team.members(this.userId, teamId, teamName, canSeeAllMembers, { offset, count }, { query })); |
||||
|
||||
return API.v1.success({ |
||||
members: records, |
||||
total, |
||||
count: records.length, |
||||
offset, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.addMembers', { authRequired: true }, { |
||||
post() { |
||||
if (!hasAtLeastOnePermission(this.userId, ['add-team-member', 'edit-team-member'])) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { teamId, teamName, members } = this.bodyParams; |
||||
|
||||
Promise.await(Team.addMembers(this.userId, teamId, teamName, members)); |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.updateMember', { authRequired: true }, { |
||||
post() { |
||||
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { teamId, teamName, member } = this.bodyParams; |
||||
|
||||
Promise.await(Team.updateMember(teamId, teamName, member)); |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.removeMembers', { authRequired: true }, { |
||||
post() { |
||||
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { teamId, teamName, members, rooms } = this.bodyParams; |
||||
|
||||
Promise.await(Team.removeMembers(teamId, teamName, members)); |
||||
|
||||
if (rooms?.length) { |
||||
Subscriptions.removeByRoomIdsAndUserId(rooms, this.userId); |
||||
} |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.leave', { authRequired: true }, { |
||||
post() { |
||||
const { teamId, teamName, rooms } = this.bodyParams; |
||||
|
||||
Promise.await(Team.removeMembers(teamId, teamName, [{ |
||||
userId: this.userId, |
||||
}])); |
||||
|
||||
if (rooms?.length) { |
||||
Subscriptions.removeByRoomIdsAndUserId(rooms, this.userId); |
||||
} |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.info', { authRequired: true }, { |
||||
get() { |
||||
const { teamId, teamName } = this.queryParams; |
||||
|
||||
if (!teamId && !teamName) { |
||||
return API.v1.failure('Provide either the "teamId" or "teamName"'); |
||||
} |
||||
|
||||
const teamInfo = teamId |
||||
? Promise.await(Team.getInfoById(teamId)) |
||||
: Promise.await(Team.getInfoByName(teamName)); |
||||
|
||||
if (!teamInfo) { |
||||
return API.v1.failure('Team not found'); |
||||
} |
||||
|
||||
return API.v1.success({ teamInfo }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.delete', { authRequired: true }, { |
||||
post() { |
||||
if (!hasPermission(this.userId, 'delete-team')) { |
||||
return API.v1.unauthorized(); |
||||
} |
||||
|
||||
const { teamId, teamName, roomsToRemove } = this.bodyParams; |
||||
|
||||
if (!teamId && !teamName) { |
||||
return API.v1.failure('Provide either the "teamId" or "teamName"'); |
||||
} |
||||
|
||||
if (roomsToRemove && !Array.isArray(roomsToRemove)) { |
||||
return API.v1.failure('The list of rooms to remove is invalid.'); |
||||
} |
||||
|
||||
const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); |
||||
if (!team) { |
||||
return API.v1.failure('Team not found.'); |
||||
} |
||||
|
||||
const rooms = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove)); |
||||
|
||||
// Remove the team's main room
|
||||
Rooms.removeById(team.roomId); |
||||
|
||||
// If we got a list of rooms to delete along with the team, remove them first
|
||||
if (rooms.length) { |
||||
Rooms.removeByIds(rooms); |
||||
} |
||||
|
||||
// Move every other room back to the workspace
|
||||
Promise.await(Team.unsetTeamIdOfRooms(team._id)); |
||||
|
||||
// And finally delete the team itself
|
||||
Promise.await(Team.deleteById(team._id)); |
||||
|
||||
return API.v1.success(); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('teams.autocomplete', { authRequired: true }, { |
||||
get() { |
||||
const { name, userId } = this.queryParams; |
||||
|
||||
const teams = Promise.await(Team.autocomplete(userId, name)); |
||||
|
||||
return API.v1.success({ teams }); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,73 @@ |
||||
import { Collection, FindOneOptions, Cursor, UpdateWriteOpResult, DeleteWriteOpResultObject, FilterQuery } from 'mongodb'; |
||||
|
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { ITeam } from '../../../../definition/ITeam'; |
||||
|
||||
type T = ITeam; |
||||
export class TeamRaw extends BaseRaw<T> { |
||||
constructor( |
||||
public readonly col: Collection<T>, |
||||
public readonly trash?: Collection<T>, |
||||
) { |
||||
super(col, trash); |
||||
|
||||
this.col.createIndex({ name: 1 }, { unique: true }); |
||||
|
||||
// this.col.createIndexes([
|
||||
// { key: { status: 1, expireAt: 1 } },
|
||||
// ]);
|
||||
} |
||||
|
||||
findByNames(names: Array<string>, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ name: { $in: names } }, options); |
||||
} |
||||
|
||||
findByIds(ids: Array<string>, options?: FindOneOptions<T>, query?: FilterQuery<T>): Cursor<T> { |
||||
return this.col.find({ _id: { $in: ids }, ...query }, options); |
||||
} |
||||
|
||||
findByIdsAndType(ids: Array<string>, type: number, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ _id: { $in: ids }, type }, options); |
||||
} |
||||
|
||||
findByNameAndTeamIds(name: string | RegExp, teamIds: Array<string>, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ |
||||
name, |
||||
$or: [{ |
||||
type: 0, |
||||
}, { |
||||
_id: { |
||||
$in: teamIds, |
||||
}, |
||||
}], |
||||
}, options); |
||||
} |
||||
|
||||
findOneByName(name: string, options?: FindOneOptions<T>): Promise<T | null> { |
||||
return this.col.findOne({ name }, options); |
||||
} |
||||
|
||||
findOneByMainRoomId(roomId: string, options?: FindOneOptions<T>): Promise<T | null> { |
||||
return this.col.findOne({ roomId }, options); |
||||
} |
||||
|
||||
updateMainRoomForTeam(id: string, roomId: string): Promise<UpdateWriteOpResult> { |
||||
return this.col.updateOne({ |
||||
_id: id, |
||||
}, { |
||||
$set: { |
||||
roomId, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
deleteOneById(id: string): Promise<DeleteWriteOpResultObject> { |
||||
return this.col.deleteOne({ |
||||
_id: id, |
||||
}); |
||||
} |
||||
|
||||
deleteOneByName(name: string): Promise<DeleteWriteOpResultObject> { |
||||
return this.col.deleteOne({ name }); |
||||
} |
||||
} |
||||
@ -0,0 +1,105 @@ |
||||
import { Collection, FindOneOptions, Cursor, InsertOneWriteOpResult, UpdateWriteOpResult, DeleteWriteOpResultObject, FilterQuery } from 'mongodb'; |
||||
|
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { ITeamMember } from '../../../../definition/ITeam'; |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
|
||||
type T = ITeamMember; |
||||
export class TeamMemberRaw extends BaseRaw<T> { |
||||
constructor( |
||||
public readonly col: Collection<T>, |
||||
public readonly trash?: Collection<T>, |
||||
) { |
||||
super(col, trash); |
||||
|
||||
this.col.createIndexes([ |
||||
{ key: { teamId: 1 } }, |
||||
]); |
||||
|
||||
// teamId => userId should be unique
|
||||
this.col.createIndex({ teamId: 1, userId: 1 }, { unique: true }); |
||||
} |
||||
|
||||
findByUserId(userId: string, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ userId }, options); |
||||
} |
||||
|
||||
findOneByUserIdAndTeamId(userId: string, teamId: string, options?: FindOneOptions<T>): Promise<T | null> { |
||||
return this.col.findOne({ userId, teamId }, options); |
||||
} |
||||
|
||||
findByTeamId(teamId: string, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ teamId }, options); |
||||
} |
||||
|
||||
findByTeamIdAndRole(teamId: string, role?: string, options?: FindOneOptions<T>): Cursor<T> { |
||||
return this.col.find({ teamId, roles: role }, options); |
||||
} |
||||
|
||||
findByUserIdAndTeamIds(userId: string, teamIds: Array<string>, options: FindOneOptions<T> = {}): Cursor<T> { |
||||
const query = { |
||||
'u._id': userId, |
||||
teamId: { |
||||
$in: teamIds, |
||||
}, |
||||
}; |
||||
|
||||
return this.col.find(query, options); |
||||
} |
||||
|
||||
findMembersInfoByTeamId(teamId: string, limit: number, skip: number, query?: FilterQuery<T>): Cursor<T> { |
||||
return this.col.find({ teamId, ...query }, { |
||||
limit, |
||||
skip, |
||||
projection: { |
||||
userId: 1, |
||||
roles: 1, |
||||
createdBy: 1, |
||||
createdAt: 1, |
||||
}, |
||||
} as FindOneOptions<T>); |
||||
} |
||||
|
||||
updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial<T>): Promise<UpdateWriteOpResult> { |
||||
return this.col.updateOne({ userId, teamId }, { $set: update }); |
||||
} |
||||
|
||||
createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick<IUser, '_id' | 'username'>): Promise<InsertOneWriteOpResult<T>> { |
||||
return this.insertOne({ |
||||
teamId, |
||||
userId, |
||||
createdAt: new Date(), |
||||
_updatedAt: new Date(), |
||||
createdBy, |
||||
}); |
||||
} |
||||
|
||||
updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<string>): Promise<UpdateWriteOpResult> { |
||||
return this.col.updateOne({ |
||||
teamId, |
||||
userId, |
||||
}, { |
||||
$addToSet: { |
||||
roles: { $each: roles }, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<string>): Promise<UpdateWriteOpResult> { |
||||
return this.col.updateOne({ |
||||
teamId, |
||||
userId, |
||||
}, { |
||||
$pull: { |
||||
roles: { $in: roles }, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
deleteByUserIdAndTeamId(userId: string, teamId: string): Promise<DeleteWriteOpResultObject> { |
||||
return this.col.deleteOne({ |
||||
teamId, |
||||
userId, |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
|
||||
import GenericModal, { GenericModalDoNotAskAgain } from './GenericModal'; |
||||
|
||||
|
||||
export default { |
||||
title: 'components/GenericModal', |
||||
component: GenericModal, |
||||
}; |
||||
|
||||
const func = () => null; |
||||
const defaultProps = { onClose: func, onConfirm: func, onCancel: func }; |
||||
|
||||
export const _default = () => <GenericModal {...defaultProps} />; |
||||
export const Danger = () => <GenericModal {...defaultProps} variant='danger' />; |
||||
export const Warning = () => <GenericModal {...defaultProps} variant='warning' />; |
||||
export const Success = () => <GenericModal {...defaultProps} variant='success' />; |
||||
export const WithDontAskAgain = () => <GenericModalDoNotAskAgain dontAskAgain={{ action: '', label: '' }} {...defaultProps} />; |
||||
@ -0,0 +1,77 @@ |
||||
import { Box, Button, ButtonGroup, Icon, Modal, ButtonProps } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
import { withDoNotAskAgain, RequiredModalProps } from './withDoNotAskAgain'; |
||||
|
||||
type VariantType = 'danger' | 'warning' | 'info' | 'success'; |
||||
|
||||
type GenericModalProps = RequiredModalProps & { |
||||
variant?: VariantType; |
||||
cancelText?: string; |
||||
confirmText?: string; |
||||
title?: string; |
||||
icon?: string; |
||||
onCancel?: () => void; |
||||
onClose: () => void; |
||||
onConfirm: () => void; |
||||
}; |
||||
|
||||
const iconMap = { |
||||
danger: 'modal-warning', |
||||
warning: 'modal-warning', |
||||
info: 'info', |
||||
success: 'check', |
||||
}; |
||||
|
||||
const getButtonProps = (variant: VariantType): ButtonProps => { |
||||
switch (variant) { |
||||
case 'danger': |
||||
return { primary: true, danger: true }; |
||||
case 'warning': |
||||
return { primary: true }; |
||||
default: |
||||
return { }; |
||||
} |
||||
}; |
||||
|
||||
const GenericModal: FC<GenericModalProps> = ({ |
||||
variant = 'info', |
||||
children, |
||||
cancelText, |
||||
confirmText, |
||||
title, |
||||
icon, |
||||
onCancel, |
||||
onClose, |
||||
onConfirm, |
||||
dontAskAgain, |
||||
...props |
||||
}) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
{icon !== null && <Icon color={variant} name={icon ?? iconMap[variant]} size={24}/>} |
||||
<Modal.Title>{title ?? t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{children} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<Box display='flex' flexDirection='row' justifyContent='space-between' alignItems='center'> |
||||
{dontAskAgain} |
||||
<ButtonGroup align='end' flexGrow={1}> |
||||
{onCancel && <Button ghost onClick={onCancel}>{cancelText ?? t('Cancel')}</Button>} |
||||
<Button {...getButtonProps(variant)} onClick={onConfirm}>{confirmText ?? t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Box> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
// TODO update withDoNotAskAgain to use onConfirm istead of confirm
|
||||
export const GenericModalDoNotAskAgain = withDoNotAskAgain<GenericModalProps>(({ confirm, ...props }) => <GenericModal onConfirm={confirm} {...props}/>); |
||||
|
||||
export default GenericModal; |
||||
@ -1,14 +0,0 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Avatar, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
function BaseAvatar(props) { |
||||
const [error, setError] = useState(false); |
||||
|
||||
if (error) { |
||||
return <Skeleton variant='rect' {...props} />; |
||||
} |
||||
|
||||
return <Avatar onError={setError} {...props}/>; |
||||
} |
||||
|
||||
export default BaseAvatar; |
||||
@ -0,0 +1,16 @@ |
||||
import React, { FC, useState } from 'react'; |
||||
import { Avatar, AvatarProps, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
export type BaseAvatarProps = AvatarProps; |
||||
|
||||
const BaseAvatar: FC<BaseAvatarProps> = ({ size, ...props }) => { |
||||
const [error, setError] = useState<unknown>(false); |
||||
|
||||
if (error) { |
||||
return <Skeleton variant='rect' {...props} />; |
||||
} |
||||
|
||||
return <Avatar onError={setError} size={size} {...props}/>; |
||||
}; |
||||
|
||||
export default BaseAvatar; |
||||
@ -1,12 +0,0 @@ |
||||
import React, { memo } from 'react'; |
||||
|
||||
import BaseAvatar from './BaseAvatar'; |
||||
import { useUserAvatarPath } from '../../contexts/AvatarUrlContext'; |
||||
|
||||
function UserAvatar({ username, etag, ...rest }) { |
||||
const getUserAvatarPath = useUserAvatarPath(); |
||||
const { url = getUserAvatarPath(username, etag), ...props } = rest; |
||||
return <BaseAvatar url={url} title={username} {...props}/>; |
||||
} |
||||
|
||||
export default memo(UserAvatar); |
||||
@ -0,0 +1,22 @@ |
||||
import React, { FC, memo } from 'react'; |
||||
|
||||
import BaseAvatar, { BaseAvatarProps } from './BaseAvatar'; |
||||
import { useUserAvatarPath } from '../../contexts/AvatarUrlContext'; |
||||
|
||||
type UserAvatarProps = Omit<BaseAvatarProps, 'url' | 'title'> & { |
||||
username: string; |
||||
etag?: string; |
||||
url?: string; |
||||
}; |
||||
|
||||
const UserAvatar: FC<UserAvatarProps> = ({ username, etag, ...rest }) => { |
||||
const getUserAvatarPath = useUserAvatarPath(); |
||||
const { |
||||
url = getUserAvatarPath(username, etag), |
||||
...props |
||||
} = rest; |
||||
|
||||
return <BaseAvatar url={url} title={username} {...props}/>; |
||||
}; |
||||
|
||||
export default memo(UserAvatar); |
||||
@ -1,46 +0,0 @@ |
||||
import { createContext, useCallback, useContext, useMemo } from 'react'; |
||||
|
||||
type ServerContextValue = { |
||||
info: {}; |
||||
absoluteUrl: (path: string) => string; |
||||
callMethod: (methodName: string, ...args: any[]) => Promise<any>; |
||||
callEndpoint: (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string, ...args: any[]) => Promise<any>; |
||||
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>; |
||||
getStream: (streamName: string, options?: {}) => <T>(eventName: string, callback: (data: T) => void) => () => void; |
||||
}; |
||||
|
||||
export const ServerContext = createContext<ServerContextValue>({ |
||||
info: {}, |
||||
absoluteUrl: (path) => path, |
||||
callMethod: async () => undefined, |
||||
callEndpoint: async () => undefined, |
||||
uploadToEndpoint: async () => undefined, |
||||
getStream: () => () => (): void => undefined, |
||||
}); |
||||
|
||||
export const useServerInformation = (): {} => useContext(ServerContext).info; |
||||
|
||||
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl; |
||||
|
||||
export const useMethod = (methodName: string): (...args: any[]) => Promise<any> => { |
||||
const { callMethod } = useContext(ServerContext); |
||||
return useCallback((...args: any[]) => callMethod(methodName, ...args), [callMethod, methodName]); |
||||
}; |
||||
|
||||
export const useEndpoint = (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string): (...args: any[]) => Promise<any> => { |
||||
const { callEndpoint } = useContext(ServerContext); |
||||
return useCallback((...args: any[]) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]); |
||||
}; |
||||
|
||||
export const useUpload = (endpoint: string): (params: any, formData: any) => Promise<void> => { |
||||
const { uploadToEndpoint } = useContext(ServerContext); |
||||
return useCallback((params, formData: any) => uploadToEndpoint(endpoint, params, formData), [endpoint, uploadToEndpoint]); |
||||
}; |
||||
|
||||
export const useStream = ( |
||||
streamName: string, |
||||
options?: {}, |
||||
): <T>(eventName: string, callback: (data: T) => void) => (() => void) => { |
||||
const { getStream } = useContext(ServerContext); |
||||
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]); |
||||
}; |
||||
@ -0,0 +1,72 @@ |
||||
import { createContext, useCallback, useContext, useMemo } from 'react'; |
||||
|
||||
import { ServerEndpointMethodOf, ServerEndpointPath, ServerEndpointFunction, ServerEndpointRequestPayload, ServerEndpointFormData, ServerEndpointResponsePayload } from './endpoints'; |
||||
import { ServerMethodFunction, ServerMethodName, ServerMethodParameters, ServerMethodReturn, ServerMethods } from './methods'; |
||||
|
||||
type ServerContextValue = { |
||||
info: {}; |
||||
absoluteUrl: (path: string) => string; |
||||
callMethod?: <MethodName extends ServerMethodName>(methodName: MethodName, ...args: ServerMethodParameters<MethodName>) => Promise<ServerMethodReturn<MethodName>>; |
||||
callEndpoint?: < |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
>(httpMethod: Method, endpoint: Path, params: ServerEndpointRequestPayload<Method, Path>, formData?: ServerEndpointFormData<Method, Path>) => Promise<ServerEndpointResponsePayload<Method, Path>>; |
||||
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>; |
||||
getStream: (streamName: string, options?: {}) => <T>(eventName: string, callback: (data: T) => void) => () => void; |
||||
}; |
||||
|
||||
export const ServerContext = createContext<ServerContextValue>({ |
||||
info: {}, |
||||
absoluteUrl: (path) => path, |
||||
uploadToEndpoint: async () => undefined, |
||||
getStream: () => () => (): void => undefined, |
||||
}); |
||||
|
||||
export const useServerInformation = (): {} => useContext(ServerContext).info; |
||||
|
||||
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl; |
||||
|
||||
export const useMethod = <MethodName extends keyof ServerMethods>( |
||||
methodName: MethodName, |
||||
): ServerMethodFunction<MethodName> => { |
||||
const { callMethod } = useContext(ServerContext); |
||||
|
||||
return useCallback( |
||||
(...args: ServerMethodParameters<MethodName>) => { |
||||
if (!callMethod) { |
||||
throw new Error(`cannot use useMethod(${ methodName }) hook without a wrapping ServerContext`); |
||||
} |
||||
|
||||
return callMethod(methodName, ...args); |
||||
}, |
||||
[callMethod, methodName], |
||||
); |
||||
}; |
||||
|
||||
export const useEndpoint = < |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
>(httpMethod: Method, endpoint: Path): ServerEndpointFunction<Method, Path> => { |
||||
const { callEndpoint } = useContext(ServerContext); |
||||
|
||||
return useCallback((params: ServerEndpointRequestPayload<Method, Path>, formData?: ServerEndpointFormData<Method, Path>) => { |
||||
if (!callEndpoint) { |
||||
throw new Error(`cannot use useEndpoint(${ httpMethod }, ${ endpoint }) hook without a wrapping ServerContext`); |
||||
} |
||||
|
||||
return callEndpoint(httpMethod, endpoint, params, formData); |
||||
}, [callEndpoint, endpoint, httpMethod]); |
||||
}; |
||||
|
||||
export const useUpload = (endpoint: string): (params: any, formData: any) => Promise<void> => { |
||||
const { uploadToEndpoint } = useContext(ServerContext); |
||||
return useCallback((params, formData: any) => uploadToEndpoint(endpoint, params, formData), [endpoint, uploadToEndpoint]); |
||||
}; |
||||
|
||||
export const useStream = ( |
||||
streamName: string, |
||||
options?: {}, |
||||
): <T>(eventName: string, callback: (data: T) => void) => (() => void) => { |
||||
const { getStream } = useContext(ServerContext); |
||||
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]); |
||||
}; |
||||
@ -0,0 +1,70 @@ |
||||
import { FollowMessageEndpoint as ChatFollowMessageEndpoint } from './endpoints/v1/chat/followMessage'; |
||||
import { GetMessageEndpoint as ChatGetMessageEndpoint } from './endpoints/v1/chat/getMessage'; |
||||
import { UnfollowMessageEndpoint as ChatUnfollowMessageEndpoint } from './endpoints/v1/chat/unfollowMessage'; |
||||
import { AutocompleteEndpoint as UsersAutocompleteEndpoint } from './endpoints/v1/users/autocomplete'; |
||||
import { AutocompleteChannelAndPrivateEndpoint as RoomsAutocompleteEndpoint } from './endpoints/v1/rooms/autocompleteChannelAndPrivate'; |
||||
import { AppearanceEndpoint as LivechatAppearanceEndpoint } from './endpoints/v1/livechat/appearance'; |
||||
import { ListEndpoint as CustomUserStatusListEndpoint } from './endpoints/v1/custom-user-status/list'; |
||||
import { ExternalComponentsEndpoint as AppsExternalComponentsEndpoint } from './endpoints/apps/externalComponents'; |
||||
import { ManualRegisterEndpoint as CloudManualRegisterEndpoint } from './endpoints/v1/cloud/manualRegister'; |
||||
import { FilesEndpoint as GroupsFilesEndpoint } from './endpoints/v1/groups/files'; |
||||
import { FilesEndpoint as ImFilesEndpoint } from './endpoints/v1/im/files'; |
||||
import { AddRoomsEndpoint as TeamsAddRoomsEndpoint } from './endpoints/v1/teams/addRooms'; |
||||
import { FilesEndpoint as ChannelsFilesEndpoint } from './endpoints/v1/channels/files'; |
||||
import { ListEndpoint as EmojiCustomListEndpoint } from './endpoints/v1/emoji-custom/list'; |
||||
import { GetDiscussionsEndpoint as ChatGetDiscussionsEndpoint } from './endpoints/v1/chat/getDiscussions'; |
||||
import { GetThreadsListEndpoint as ChatGetThreadsListEndpoint } from './endpoints/v1/chat/getThreadsList'; |
||||
import { LivechatVisitorInfoEndpoint } from './endpoints/v1/livechat/visitorInfo'; |
||||
|
||||
export type ServerEndpoints = { |
||||
'chat.getMessage': ChatGetMessageEndpoint; |
||||
'chat.followMessage': ChatFollowMessageEndpoint; |
||||
'chat.unfollowMessage': ChatUnfollowMessageEndpoint; |
||||
'cloud.manualRegister': CloudManualRegisterEndpoint; |
||||
'chat.getDiscussions': ChatGetDiscussionsEndpoint; |
||||
'chat.getThreadsList': ChatGetThreadsListEndpoint; |
||||
'emoji-custom.list': EmojiCustomListEndpoint; |
||||
'channels.files': ChannelsFilesEndpoint; |
||||
'im.files': ImFilesEndpoint; |
||||
'groups.files': GroupsFilesEndpoint; |
||||
'users.autocomplete': UsersAutocompleteEndpoint; |
||||
'livechat/appearance': LivechatAppearanceEndpoint; |
||||
'custom-user-status.list': CustomUserStatusListEndpoint; |
||||
'/apps/externalComponents': AppsExternalComponentsEndpoint; |
||||
'rooms.autocomplete.channelAndPrivate': RoomsAutocompleteEndpoint; |
||||
'teams.addRooms': TeamsAddRoomsEndpoint; |
||||
'livechat/visitors.info': LivechatVisitorInfoEndpoint; |
||||
}; |
||||
|
||||
export type ServerEndpointPath = keyof ServerEndpoints; |
||||
export type ServerEndpointMethodOf<Path extends ServerEndpointPath> = keyof ServerEndpoints[Path] & ('GET' | 'POST' | 'DELETE'); |
||||
|
||||
type ServerEndpoint< |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
> = ServerEndpoints[Path][Method] extends (...args: any[]) => any |
||||
? ServerEndpoints[Path][Method] |
||||
: (...args: any[]) => any; |
||||
|
||||
export type ServerEndpointRequestPayload< |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
> = Parameters<ServerEndpoint<Method, Path>>[0]; |
||||
|
||||
export type ServerEndpointFormData< |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
> = Parameters<ServerEndpoint<Method, Path>>[1]; |
||||
|
||||
export type ServerEndpointResponsePayload< |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
> = ReturnType<ServerEndpoint<Method, Path>>; |
||||
|
||||
export type ServerEndpointFunction< |
||||
Method extends ServerEndpointMethodOf<Path>, |
||||
Path extends ServerEndpointPath |
||||
> = { |
||||
(params: ServerEndpointRequestPayload<Method, Path>): Promise<ServerEndpointResponsePayload<Method, Path>>; |
||||
(params: ServerEndpointRequestPayload<Method, Path>, formData: ServerEndpointFormData<Method, Path>): Promise<ServerEndpointResponsePayload<Method, Path>>; |
||||
}; |
||||
@ -0,0 +1,5 @@ |
||||
import { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; |
||||
|
||||
export type ExternalComponentsEndpoint = { |
||||
GET: (params: Record<string, never>) => { externalComponents: IExternalComponent[] }; |
||||
}; |
||||
@ -0,0 +1,15 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; |
||||
|
||||
export type FilesEndpoint = { |
||||
GET: (params: { |
||||
roomId: IRoom['_id']; |
||||
count: number; |
||||
sort: string; |
||||
query: string; |
||||
}) => { |
||||
files: ObjectFromApi<IMessage>[]; |
||||
total: number; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,9 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
|
||||
export type FollowMessageEndpoint = { |
||||
POST: (params: { mid: IMessage['_id'] }) => { |
||||
success: true; |
||||
statusCode: 200; |
||||
body: {}; |
||||
}; |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
|
||||
export type GetDiscussionsEndpoint = { |
||||
GET: (params: { |
||||
roomId: IRoom['_id']; |
||||
text?: string; |
||||
offset: number; |
||||
count: number; |
||||
}) => { |
||||
messages: ObjectFromApi<IMessage>[]; |
||||
total: number; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,7 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
|
||||
export type GetMessageEndpoint = { |
||||
GET: (params: { msgId: IMessage['_id'] }) => { |
||||
message: IMessage; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,16 @@ |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
|
||||
export type GetThreadsListEndpoint = { |
||||
GET: (params: { |
||||
rid: IRoom['_id']; |
||||
type: 'unread' | 'following' | 'all'; |
||||
text?: string; |
||||
offset: number; |
||||
count: number; |
||||
}) => { |
||||
threads: ObjectFromApi<IMessage>[]; |
||||
total: number; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,9 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
|
||||
export type UnfollowMessageEndpoint = { |
||||
POST: (params: { mid: IMessage['_id'] }) => { |
||||
success: true; |
||||
statusCode: 200; |
||||
body: {}; |
||||
}; |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
export type ManualRegisterEndpoint = { |
||||
POST: (params: Record<string, never>, formData: { cloudBlob: string }) => void; |
||||
}; |
||||
@ -0,0 +1,5 @@ |
||||
export type ListEndpoint = { |
||||
GET: (params: { query: string }) => { |
||||
statuses: unknown[]; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,14 @@ |
||||
type EmojiDescriptor = { |
||||
_id: string; |
||||
name: string; |
||||
aliases: string[]; |
||||
extension: string; |
||||
}; |
||||
|
||||
export type ListEndpoint = { |
||||
GET: (params: { query: string }) => { |
||||
emojis?: { |
||||
update: EmojiDescriptor[]; |
||||
}; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,15 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; |
||||
|
||||
export type FilesEndpoint = { |
||||
GET: (params: { |
||||
roomId: IRoom['_id']; |
||||
count: number; |
||||
sort: string; |
||||
query: string; |
||||
}) => { |
||||
files: ObjectFromApi<IMessage>[]; |
||||
total: number; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,15 @@ |
||||
import { IMessage } from '../../../../../../definition/IMessage'; |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi'; |
||||
|
||||
export type FilesEndpoint = { |
||||
GET: (params: { |
||||
roomId: IRoom['_id']; |
||||
count: number; |
||||
sort: string; |
||||
query: string; |
||||
}) => { |
||||
files: ObjectFromApi<IMessage>[]; |
||||
total: number; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,8 @@ |
||||
import { ISetting } from '../../../../../../definition/ISetting'; |
||||
|
||||
export type AppearanceEndpoint = { |
||||
GET: (params: Record<string, never>) => { |
||||
success: boolean; |
||||
appearance: ISetting[]; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,10 @@ |
||||
export type LivechatVisitorInfoEndpoint = { |
||||
GET: (visitorId: string) => { |
||||
success: boolean; |
||||
visitor: { |
||||
visitorEmails: Array<{ |
||||
address: string; |
||||
}>; |
||||
}; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,5 @@ |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
|
||||
export type AutocompleteChannelAndPrivateEndpoint = { |
||||
GET: (params: { selector: string }) => { items: IRoom[] }; |
||||
}; |
||||
@ -0,0 +1,9 @@ |
||||
import { IRoom } from '../../../../../../definition/IRoom'; |
||||
|
||||
export type AddRoomsEndpoint = { |
||||
POST: (params: { rooms: IRoom['_id'][]; teamId: string }) => { |
||||
success: true; |
||||
statusCode: 200; |
||||
body: IRoom[]; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,5 @@ |
||||
import { IUser } from '../../../../../../definition/IUser'; |
||||
|
||||
export type AutocompleteEndpoint = { |
||||
GET: (params: { selector: string }) => { items: IUser[] }; |
||||
}; |
||||
@ -0,0 +1,3 @@ |
||||
export * from './ServerContext'; |
||||
export * from './endpoints'; |
||||
export * from './methods'; |
||||
@ -0,0 +1,140 @@ |
||||
import { FollowMessageMethod } from './methods/followMessage'; |
||||
import { RoomNameExistsMethod } from './methods/roomNameExists'; |
||||
import { SaveRoomSettingsMethod } from './methods/saveRoomSettings'; |
||||
import { SaveSettingsMethod } from './methods/saveSettings'; |
||||
import { SaveUserPreferencesMethod } from './methods/saveUserPreferences'; |
||||
import { UnfollowMessageMethod } from './methods/unfollowMessage'; |
||||
|
||||
export type ServerMethods = { |
||||
'2fa:checkCodesRemaining': (...args: any[]) => any; |
||||
'2fa:disable': (...args: any[]) => any; |
||||
'2fa:enable': (...args: any[]) => any; |
||||
'2fa:regenerateCodes': (...args: any[]) => any; |
||||
'2fa:validateTempToken': (...args: any[]) => any; |
||||
'addOAuthApp': (...args: any[]) => any; |
||||
'addOAuthService': (...args: any[]) => any; |
||||
'addUsersToRoom': (...args: any[]) => any; |
||||
'apps/go-enable': (...args: any[]) => any; |
||||
'apps/is-enabled': (...args: any[]) => any; |
||||
'authorization:addPermissionToRole': (...args: any[]) => any; |
||||
'authorization:addUserToRole': (...args: any[]) => any; |
||||
'authorization:deleteRole': (...args: any[]) => any; |
||||
'authorization:removeRoleFromPermission': (...args: any[]) => any; |
||||
'authorization:removeUserFromRole': (...args: any[]) => any; |
||||
'authorization:saveRole': (...args: any[]) => any; |
||||
'bbbEnd': (...args: any[]) => any; |
||||
'bbbJoin': (...args: any[]) => any; |
||||
'blockUser': (...args: any[]) => any; |
||||
'checkUsernameAvailability': (...args: any[]) => any; |
||||
'cleanRoomHistory': (...args: any[]) => any; |
||||
'clearIntegrationHistory': (...args: any[]) => any; |
||||
'cloud:checkRegisterStatus': (...args: any[]) => any; |
||||
'cloud:checkUserLoggedIn': (...args: any[]) => any; |
||||
'cloud:connectWorkspace': (...args: any[]) => any; |
||||
'cloud:disconnectWorkspace': (...args: any[]) => any; |
||||
'cloud:finishOAuthAuthorization': (...args: any[]) => any; |
||||
'cloud:getOAuthAuthorizationUrl': (...args: any[]) => any; |
||||
'cloud:getWorkspaceRegisterData': (...args: any[]) => any; |
||||
'cloud:logout': (...args: any[]) => any; |
||||
'cloud:registerWorkspace': (...args: any[]) => any; |
||||
'cloud:syncWorkspace': (...args: any[]) => any; |
||||
'deleteCustomSound': (...args: any[]) => any; |
||||
'deleteCustomUserStatus': (...args: any[]) => any; |
||||
'deleteFileMessage': (...args: any[]) => any; |
||||
'deleteOAuthApp': (...args: any[]) => any; |
||||
'deleteUserOwnAccount': (...args: any[]) => any; |
||||
'e2e.resetOwnE2EKey': (...args: any[]) => any; |
||||
'eraseRoom': (...args: any[]) => any; |
||||
'followMessage': FollowMessageMethod; |
||||
'getAvatarSuggestion': (...args: any[]) => any; |
||||
'getSetupWizardParameters': (...args: any[]) => any; |
||||
'getUsersOfRoom': (...args: any[]) => any; |
||||
'hideRoom': (...args: any[]) => any; |
||||
'ignoreUser': (...args: any[]) => any; |
||||
'insertOrUpdateSound': (...args: any[]) => any; |
||||
'insertOrUpdateUserStatus': (...args: any[]) => any; |
||||
'instances/get': (...args: any[]) => any; |
||||
'jitsi:generateAccessToken': (...args: any[]) => any; |
||||
'jitsi:updateTimeout': (...args: any[]) => any; |
||||
'leaveRoom': (...args: any[]) => any; |
||||
'license:getTags': (...args: any[]) => any; |
||||
'livechat:addMonitor': (...args: any[]) => any; |
||||
'livechat:changeLivechatStatus': (...args: any[]) => any; |
||||
'livechat:closeRoom': (...args: any[]) => any; |
||||
'livechat:discardTranscript': (...args: any[]) => any; |
||||
'livechat:facebook': (...args: any[]) => any; |
||||
'livechat:getAgentOverviewData': (...args: any[]) => any; |
||||
'livechat:getAnalyticsChartData': (...args: any[]) => any; |
||||
'livechat:getAnalyticsOverviewData': (...args: any[]) => any; |
||||
'livechat:getRoutingConfig': (...args: any[]) => any; |
||||
'livechat:removeAllClosedRooms': (...args: any[]) => any; |
||||
'livechat:removeBusinessHour': (...args: any[]) => any; |
||||
'livechat:removeCustomField': (...args: any[]) => any; |
||||
'livechat:removeMonitor': (...args: any[]) => any; |
||||
'livechat:removePriority': (...args: any[]) => any; |
||||
'livechat:removeRoom': (...args: any[]) => any; |
||||
'livechat:removeTag': (...args: any[]) => any; |
||||
'livechat:removeTrigger': (...args: any[]) => any; |
||||
'livechat:removeUnit': (...args: any[]) => any; |
||||
'livechat:requestTranscript': (...args: any[]) => any; |
||||
'livechat:returnAsInquiry': (...args: any[]) => any; |
||||
'livechat:sendTranscript': (...args: any[]) => any; |
||||
'livechat:transfer': (...args: any[]) => any; |
||||
'livechat:saveAgentInfo': (...args: any[]) => any; |
||||
'livechat:saveAppearance': (...args: any[]) => any; |
||||
'livechat:saveBusinessHour': (...args: any[]) => any; |
||||
'livechat:saveCustomField': (...args: any[]) => any; |
||||
'livechat:saveDepartment': (...args: any[]) => any; |
||||
'livechat:saveIntegration': (...args: any[]) => any; |
||||
'livechat:savePriority': (...args: any[]) => any; |
||||
'livechat:saveTag': (...args: any[]) => any; |
||||
'livechat:saveTrigger': (...args: any[]) => any; |
||||
'livechat:saveUnit': (...args: any[]) => any; |
||||
'livechat:webhookTest': (...args: any[]) => any; |
||||
'logoutOtherClients': (...args: any[]) => any; |
||||
'Mailer.sendMail': (...args: any[]) => any; |
||||
'muteUserInRoom': (...args: any[]) => any; |
||||
'personalAccessTokens:generateToken': (...args: any[]) => any; |
||||
'personalAccessTokens:regenerateToken': (...args: any[]) => any; |
||||
'personalAccessTokens:removeToken': (...args: any[]) => any; |
||||
'readMessages': (...args: any[]) => any; |
||||
'refreshClients': (...args: any[]) => any; |
||||
'refreshOAuthService': (...args: any[]) => any; |
||||
'registerUser': (...args: any[]) => any; |
||||
'removeOAuthService': (...args: any[]) => any; |
||||
'removeWebdavAccount': (...args: any[]) => any; |
||||
'replayOutgoingIntegration': (...args: any[]) => any; |
||||
'requestDataDownload': (...args: any[]) => any; |
||||
'resetPassword': (...args: any[]) => any; |
||||
'roomNameExists': RoomNameExistsMethod; |
||||
'saveCannedResponse': (...args: any[]) => any; |
||||
'saveRoomSettings': SaveRoomSettingsMethod; |
||||
'saveSettings': SaveSettingsMethod; |
||||
'saveUserPreferences': SaveUserPreferencesMethod; |
||||
'saveUserProfile': (...args: any[]) => any; |
||||
'sendConfirmationEmail': (...args: any[]) => any; |
||||
'sendInvitationEmail': (...args: any[]) => any; |
||||
'setAdminStatus': (...args: any[]) => any; |
||||
'setAsset': (...args: any[]) => any; |
||||
'setAvatarFromService': (...args: any[]) => any; |
||||
'setUsername': (...args: any[]) => any; |
||||
'setUserPassword': (...args: any[]) => any; |
||||
'toggleFavorite': (...args: any[]) => any; |
||||
'unblockUser': (...args: any[]) => any; |
||||
'unfollowMessage': UnfollowMessageMethod; |
||||
'unmuteUserInRoom': (...args: any[]) => any; |
||||
'unreadMessages': (...args: any[]) => any; |
||||
'unsetAsset': (...args: any[]) => any; |
||||
'updateIncomingIntegration': (...args: any[]) => any; |
||||
'updateOAuthApp': (...args: any[]) => any; |
||||
'updateOutgoingIntegration': (...args: any[]) => any; |
||||
'uploadCustomSound': (...args: any[]) => any; |
||||
}; |
||||
|
||||
export type ServerMethodName = keyof ServerMethods; |
||||
|
||||
export type ServerMethodParameters<MethodName extends ServerMethodName> = Parameters<ServerMethods[MethodName]>; |
||||
|
||||
export type ServerMethodReturn<MethodName extends ServerMethodName> = ReturnType<ServerMethods[MethodName]>; |
||||
|
||||
export type ServerMethodFunction<MethodName extends ServerMethodName> = (...args: ServerMethodParameters<MethodName>) => Promise<ServerMethodReturn<MethodName>>; |
||||
@ -0,0 +1,3 @@ |
||||
import { IMessage } from '../../../../definition/IMessage'; |
||||
|
||||
export type FollowMessageMethod = (options: { mid: IMessage['_id'] }) => false | undefined; |
||||
@ -0,0 +1,3 @@ |
||||
import { IRoom } from '../../../../definition/IRoom'; |
||||
|
||||
export type RoomNameExistsMethod = (name: IRoom['name']) => boolean; |
||||
@ -0,0 +1,32 @@ |
||||
import { IRoom } from '../../../../definition/IRoom'; |
||||
|
||||
type RoomSettings = { |
||||
'roomAvatar': unknown; |
||||
'featured': unknown; |
||||
'roomName': unknown; |
||||
'roomTopic': unknown; |
||||
'roomAnnouncement': unknown; |
||||
'roomCustomFields': unknown; |
||||
'roomDescription': unknown; |
||||
'roomType': unknown; |
||||
'readOnly': unknown; |
||||
'reactWhenReadOnly': unknown; |
||||
'systemMessages': unknown; |
||||
'default': unknown; |
||||
'joinCode': unknown; |
||||
'tokenpass': unknown; |
||||
'streamingOptions': unknown; |
||||
'retentionEnabled': unknown; |
||||
'retentionMaxAge': unknown; |
||||
'retentionExcludePinned': unknown; |
||||
'retentionFilesOnly': unknown; |
||||
'retentionIgnoreThreads': unknown; |
||||
'retentionOverrideGlobal': unknown; |
||||
'encrypted': boolean; |
||||
'favorite': unknown; |
||||
}; |
||||
|
||||
export type SaveRoomSettingsMethod = { |
||||
(rid: IRoom['_id'], settings: Partial<RoomSettings>): { result: true; rid: IRoom['_id'] }; |
||||
<RoomSettingName extends keyof RoomSettings>(rid: IRoom['_id'], setting: RoomSettingName, value: RoomSettings[RoomSettingName]): { result: true; rid: IRoom['_id'] }; |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
import { ISetting } from '../../../../definition/ISetting'; |
||||
|
||||
type SettingChange = { |
||||
_id: ISetting['_id']; |
||||
value: unknown; |
||||
editor?: unknown; |
||||
}; |
||||
|
||||
export type SaveSettingsMethod = (changes: SettingChange[]) => true; |
||||
@ -0,0 +1,37 @@ |
||||
type UserPreferences = { |
||||
language: string; |
||||
newRoomNotification: string; |
||||
newMessageNotification: string; |
||||
clockMode: number; |
||||
useEmojis: boolean; |
||||
convertAsciiEmoji: boolean; |
||||
saveMobileBandwidth: boolean; |
||||
collapseMediaByDefault: boolean; |
||||
autoImageLoad: boolean; |
||||
emailNotificationMode: string; |
||||
unreadAlert: boolean; |
||||
notificationsSoundVolume: number; |
||||
desktopNotifications: string; |
||||
audioNotifications: string; |
||||
mobileNotifications: string; |
||||
enableAutoAway: boolean; |
||||
highlights: string[]; |
||||
messageViewMode: number; |
||||
hideUsernames: boolean; |
||||
hideRoles: boolean; |
||||
hideAvatars: boolean; |
||||
hideFlexTab: boolean; |
||||
sendOnEnter: string; |
||||
idleTimeLimit: number; |
||||
sidebarShowFavorites: boolean; |
||||
sidebarShowUnread: boolean; |
||||
sidebarSortby: string; |
||||
sidebarViewMode: string; |
||||
sidebarHideAvatar: boolean; |
||||
sidebarGroupByType: boolean; |
||||
sidebarShowDiscussion: boolean; |
||||
muteFocusedConversations: boolean; |
||||
dontAskAgainList: { action: string; label: string }[]; |
||||
}; |
||||
|
||||
export type SaveUserPreferencesMethod = (preferences: Partial<UserPreferences>) => boolean; |
||||
@ -0,0 +1,3 @@ |
||||
import { IMessage } from '../../../../definition/IMessage'; |
||||
|
||||
export type UnfollowMessageMethod = (options: { mid: IMessage['_id'] }) => false | undefined; |
||||
@ -0,0 +1,67 @@ |
||||
import React from 'react'; |
||||
|
||||
import InfoPanel, { RetentionPolicyCallout } from '.'; |
||||
|
||||
export default { |
||||
title: 'components/InfoPanel', |
||||
component: InfoPanel, |
||||
}; |
||||
|
||||
const room = { |
||||
fname: 'rocketchat-frontend-team', |
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero', |
||||
announcement: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero', |
||||
topic: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero', |
||||
}; |
||||
|
||||
export const Default = () => <InfoPanel> |
||||
<InfoPanel.Avatar /> |
||||
<InfoPanel.Section> |
||||
<InfoPanel.Title title={room.fname} icon={'hashtag'}/> |
||||
</InfoPanel.Section> |
||||
|
||||
<InfoPanel.Section> |
||||
<InfoPanel.Field> |
||||
<InfoPanel.Label>Description</InfoPanel.Label> |
||||
<InfoPanel.Text>{room.description}</InfoPanel.Text> |
||||
</InfoPanel.Field> |
||||
<InfoPanel.Field> |
||||
<InfoPanel.Label>Announcement</InfoPanel.Label> |
||||
<InfoPanel.Text>{room.announcement}</InfoPanel.Text> |
||||
</InfoPanel.Field> |
||||
<InfoPanel.Field> |
||||
<InfoPanel.Label>Topic</InfoPanel.Label> |
||||
<InfoPanel.Text>{room.topic}</InfoPanel.Text> |
||||
</InfoPanel.Field> |
||||
</InfoPanel.Section> |
||||
|
||||
<InfoPanel.Section> |
||||
<RetentionPolicyCallout maxAgeDefault={30} filesOnlyDefault={false} excludePinnedDefault={true} /> |
||||
</InfoPanel.Section> |
||||
</InfoPanel>; |
||||
|
||||
|
||||
// export const Archived = () => <VerticalBar height={800}>
|
||||
// <RoomInfo
|
||||
// {...room}
|
||||
// icon='lock'
|
||||
// onClickHide={alert}
|
||||
// onClickLeave={alert}
|
||||
// onClickEdit={alert}
|
||||
// onClickDelete={alert}
|
||||
// archived
|
||||
// />
|
||||
// </VerticalBar>;
|
||||
|
||||
|
||||
// export const Broadcast = () => <VerticalBar height={800}>
|
||||
// <RoomInfo
|
||||
// {...room}
|
||||
// icon='lock'
|
||||
// onClickHide={alert}
|
||||
// onClickLeave={alert}
|
||||
// onClickEdit={alert}
|
||||
// onClickDelete={alert}
|
||||
// broadcast
|
||||
// />
|
||||
// </VerticalBar>;
|
||||
@ -0,0 +1,62 @@ |
||||
import React, { FC, ReactNode } from 'react'; |
||||
import { Box, Icon, BoxProps, Button, ButtonProps, ButtonGroup, ButtonGroupProps } from '@rocket.chat/fuselage'; |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
|
||||
type TitleProps = { |
||||
title: string; |
||||
icon: string | ReactNode; |
||||
} |
||||
|
||||
const wordBreak = css` |
||||
word-break: break-word; |
||||
`;
|
||||
|
||||
const InfoPanel: FC = ({ children }) => <Box flexGrow={1} mb='neg-x24'>{children}</Box>; |
||||
|
||||
const Section: FC<BoxProps> = (props) => <Box mb='x24' {...props} />; |
||||
|
||||
const Title: FC<TitleProps> = ({ title, icon }) => <Box display='flex' title={title} flexShrink={0} alignItems='center' fontScale='s2' color='default' withTruncatedText> |
||||
{ |
||||
typeof icon === 'string' |
||||
? <Icon name={icon} size='x22' /> |
||||
: icon |
||||
} |
||||
<Box mis='x16' flexGrow={1} withTruncatedText>{title}</Box> |
||||
</Box>; |
||||
|
||||
const Label: FC<BoxProps> = (props) => <Box mb='x8' fontScale='p2' color='default' {...props} />; |
||||
|
||||
const Text: FC<BoxProps> = (props) => <Box |
||||
mb='x8' |
||||
fontScale='p1' |
||||
color='hint' |
||||
withTruncatedText |
||||
className={wordBreak} |
||||
{...props} |
||||
/>; |
||||
|
||||
const Action: FC<ButtonProps & { icon: string; label: string }> = ({ label, icon, ...props }) => <Button title={label} aria-label={label} {...props} mi='x4'> |
||||
<Icon name={icon} size='x20' mie='x4' /> |
||||
{label} |
||||
</Button>; |
||||
|
||||
const ActionGroup: FC<ButtonGroupProps> = (props) => <Section><ButtonGroup flexShrink={0} flexWrap='nowrap' withTruncatedText justifyContent='center' {...props}/></Section>; |
||||
|
||||
const Field: FC = ({ children }) => <Box mb='x12'>{children}</Box>; |
||||
|
||||
const Avatar: FC = ({ children }) => <Section display='flex' justifyContent='center'> |
||||
{children} |
||||
</Section>; |
||||
|
||||
Object.assign(InfoPanel, { |
||||
Title, |
||||
Label, |
||||
Text, |
||||
Avatar, |
||||
Field, |
||||
Action, |
||||
Section, |
||||
ActionGroup, |
||||
}); |
||||
|
||||
export default InfoPanel; |
||||
@ -0,0 +1,22 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Callout } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
type RetentionPolicyCalloutProps = { |
||||
filesOnlyDefault: boolean; |
||||
excludePinnedDefault: boolean; |
||||
maxAgeDefault: number; |
||||
} |
||||
|
||||
const RetentionPolicyCallout: FC<RetentionPolicyCalloutProps> = ({ filesOnlyDefault, excludePinnedDefault, maxAgeDefault }) => { |
||||
const t = useTranslation(); |
||||
return <Callout type='warning'> |
||||
{filesOnlyDefault && excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_FilesOnly', { time: maxAgeDefault })}</p>} |
||||
{filesOnlyDefault && !excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time: maxAgeDefault })}</p>} |
||||
{!filesOnlyDefault && excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning', { time: maxAgeDefault })}</p>} |
||||
{!filesOnlyDefault && !excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_Unpinned', { time: maxAgeDefault })}</p>} |
||||
</Callout>; |
||||
}; |
||||
|
||||
export default RetentionPolicyCallout; |
||||
@ -0,0 +1,6 @@ |
||||
import InfoPanel from './InfoPanel'; |
||||
import RetentionPolicyCallout from './RetentionPolicyCallout'; |
||||
|
||||
export { RetentionPolicyCallout }; |
||||
|
||||
export default InfoPanel; |
||||
@ -1,6 +0,0 @@ |
||||
export type EmojiDescriptor = { |
||||
_id: string; |
||||
name: string; |
||||
aliases: string[]; |
||||
extension: string; |
||||
}; |
||||
@ -0,0 +1,109 @@ |
||||
import { Box, Margins, Table, Avatar, Tag } from '@rocket.chat/fuselage'; |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo, useState, useCallback } from 'react'; |
||||
|
||||
import GenericTable from '../../components/GenericTable'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointData } from '../../hooks/useEndpointData'; |
||||
import { useFormatDate } from '../../hooks/useFormatDate'; |
||||
import { roomTypes } from '../../../app/utils/client'; |
||||
import { useQuery } from './hooks'; |
||||
|
||||
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; |
||||
|
||||
function RoomTags({ room }) { |
||||
const t = useTranslation(); |
||||
return <Box mi='x4' alignItems='center' display='flex'> |
||||
<Margins inline='x2'> |
||||
{room.default && <Tag variant='primary'>{t('default')}</Tag>} |
||||
{room.featured && <Tag variant='primary'>{t('featured')}</Tag>} |
||||
</Margins> |
||||
</Box>; |
||||
} |
||||
|
||||
function TeamsTable() { |
||||
const t = useTranslation(); |
||||
const [sort, setSort] = useState(['name', 'asc']); |
||||
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); |
||||
|
||||
const mediaQuery = useMediaQuery('(min-width: 768px)'); |
||||
|
||||
const onHeaderClick = useCallback((id) => { |
||||
const [sortBy, sortDirection] = sort; |
||||
|
||||
if (sortBy === id) { |
||||
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); |
||||
return; |
||||
} |
||||
setSort([id, 'asc']); |
||||
}, [sort]); |
||||
|
||||
const header = useMemo(() => [ |
||||
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'channelsCount'} direction={sort[1]} active={sort[0] === 'channelsCount'} onClick={onHeaderClick} sort='channelsCount' style={{ width: '100px' }}>{t('Channels')}</GenericTable.HeaderCell>, |
||||
mediaQuery && <GenericTable.HeaderCell key={'createdAt'} direction={sort[1]} active={sort[0] === 'createdAt'} onClick={onHeaderClick} sort='createdAt' style={{ width: '150px' }}>{t('Created_at')}</GenericTable.HeaderCell>, |
||||
].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]); |
||||
|
||||
const channelsRoute = useRoute('channel'); |
||||
const groupsRoute = useRoute('group'); |
||||
|
||||
const query = useQuery(params, sort); |
||||
|
||||
const { value: data = { result: [] } } = useEndpointData('teams.list', query); |
||||
|
||||
const onClick = useMemo(() => (name, type) => (e) => { |
||||
if (e.type === 'click' || e.key === 'Enter') { |
||||
type === 0 ? channelsRoute.push({ name }) : groupsRoute.push({ name }); |
||||
} |
||||
}, [channelsRoute, groupsRoute]); |
||||
|
||||
const formatDate = useFormatDate(); |
||||
const renderRow = useCallback((team) => { |
||||
const { _id, createdAt, name, type, rooms, roomId } = team; |
||||
const t = type === 0 ? 'c' : 'p'; |
||||
const avatarUrl = roomTypes.getConfig(t).getAvatarPath({ _id: roomId }); |
||||
|
||||
return <Table.Row key={_id} onKeyDown={onClick(name, type)} onClick={onClick(name, type)} tabIndex={0} role='link' action> |
||||
<Table.Cell> |
||||
<Box display='flex'> |
||||
<Box flexGrow={0}> |
||||
<Avatar size='x40' title={name} url={avatarUrl} /> |
||||
</Box> |
||||
<Box grow={1} mi='x8' style={style}> |
||||
<Box display='flex' alignItems='center'> |
||||
<Box fontScale='p2' mi='x4'>{name}</Box><RoomTags room={team} style={style} /> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p1' color='hint' style={style}> |
||||
{rooms} |
||||
</Table.Cell> |
||||
{ mediaQuery && <Table.Cell fontScale='p1' color='hint' style={style}> |
||||
{formatDate(createdAt)} |
||||
</Table.Cell>} |
||||
</Table.Row>; |
||||
} |
||||
, [formatDate, mediaQuery, onClick]); |
||||
|
||||
return <GenericTable |
||||
header={header} |
||||
renderRow={renderRow} |
||||
results={data.teams} |
||||
setParams={setParams} |
||||
total={data.total} |
||||
/>; |
||||
} |
||||
|
||||
export default function TeamsTab(props) { |
||||
const canViewPublicRooms = usePermission('view-c-room'); |
||||
|
||||
if (canViewPublicRooms) { |
||||
return <TeamsTable {...props} />; |
||||
} |
||||
|
||||
return <NotAuthorizedPage />; |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue