feat: add `sidepanel` to teams api (#32868)

feat/single-contact-apis^2
Júlia Jaeger Foresti 1 year ago committed by GitHub
parent 9939508bb3
commit e28be46db7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      .changeset/swift-maps-tickle.md
  2. 9
      apps/meteor/app/api/server/v1/teams.ts
  3. 27
      apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts
  4. 2
      apps/meteor/app/lib/server/functions/createRoom.ts
  5. 4
      apps/meteor/server/models/raw/Rooms.ts
  6. 4
      apps/meteor/server/services/room/service.ts
  7. 6
      apps/meteor/server/services/team/service.ts
  8. 70
      apps/meteor/tests/end-to-end/api/rooms.ts
  9. 78
      apps/meteor/tests/end-to-end/api/teams.ts
  10. 1
      packages/core-services/src/types/IRoomService.ts
  11. 1
      packages/core-services/src/types/ITeamService.ts
  12. 22
      packages/core-typings/src/IRoom.ts
  13. 2
      packages/model-typings/src/models/IRoomsModel.ts
  14. 1
      packages/rest-typings/src/v1/teams/index.ts

@ -0,0 +1,9 @@
---
'@rocket.chat/core-services': minor
'@rocket.chat/model-typings': minor
'@rocket.chat/core-typings': minor
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---
Added `sidepanel` field to `teams.create` and `rooms.saveRoomSettings` endpoints

@ -1,6 +1,6 @@
import { Team } from '@rocket.chat/core-services';
import type { ITeam, UserStatus } from '@rocket.chat/core-typings';
import { TEAM_TYPE } from '@rocket.chat/core-typings';
import { TEAM_TYPE, isValidSidepanel } from '@rocket.chat/core-typings';
import { Users, Rooms } from '@rocket.chat/models';
import {
isTeamsConvertToChannelProps,
@ -85,7 +85,11 @@ API.v1.addRoute(
}),
);
const { name, type, members, room, owner } = this.bodyParams;
const { name, type, members, room, owner, sidepanel } = this.bodyParams;
if (sidepanel?.items && !isValidSidepanel(sidepanel)) {
throw new Error('error-invalid-sidepanel');
}
const team = await Team.create(this.userId, {
team: {
@ -95,6 +99,7 @@ API.v1.addRoute(
room,
members,
owner,
sidepanel,
});
return API.v1.success({ team });

@ -1,6 +1,6 @@
import { Team } from '@rocket.chat/core-services';
import type { IRoom, IRoomWithRetentionPolicy, IUser, MessageTypesValues } from '@rocket.chat/core-typings';
import { TEAM_TYPE } from '@rocket.chat/core-typings';
import { TEAM_TYPE, isValidSidepanel } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Rooms, Users } from '@rocket.chat/models';
import { Match } from 'meteor/check';
@ -49,6 +49,7 @@ type RoomSettings = {
favorite: boolean;
defaultValue: boolean;
};
sidepanel?: IRoom['sidepanel'];
};
type RoomSettingsValidators = {
@ -80,6 +81,24 @@ const validators: RoomSettingsValidators = {
});
}
},
async sidepanel({ room, userId, value }) {
if (!room.teamMain) {
throw new Meteor.Error('error-action-not-allowed', 'Invalid room', {
method: 'saveRoomSettings',
});
}
if (!(await hasPermissionAsync(userId, 'edit-team', room._id))) {
throw new Meteor.Error('error-action-not-allowed', 'You do not have permission to change sidepanel items', {
method: 'saveRoomSettings',
});
}
if (!isValidSidepanel(value)) {
throw new Meteor.Error('error-invalid-sidepanel');
}
},
async roomType({ userId, room, value }) {
if (value === room.t) {
return;
@ -213,6 +232,11 @@ const settingSavers: RoomSettingsSavers = {
await saveRoomTopic(rid, value, user);
}
},
async sidepanel({ value, rid, room }) {
if (JSON.stringify(value) !== JSON.stringify(room.sidepanel)) {
await Rooms.setSidepanelById(rid, value);
}
},
async roomAnnouncement({ value, room, rid, user }) {
if (!value && !room.announcement) {
return;
@ -339,6 +363,7 @@ const fields: (keyof RoomSettings)[] = [
'retentionOverrideGlobal',
'encrypted',
'favorite',
'sidepanel',
];
const validate = <TRoomSetting extends keyof RoomSettings>(

@ -112,6 +112,7 @@ export const createRoom = async <T extends RoomType>(
readOnly?: boolean,
roomExtraData?: Partial<IRoom>,
options?: ICreateRoomParams['options'],
sidepanel?: ICreateRoomParams['sidepanel'],
): Promise<
ICreatedRoom & {
rid: string;
@ -187,6 +188,7 @@ export const createRoom = async <T extends RoomType>(
},
ts: now,
ro: readOnly === true,
sidepanel,
};
if (teamId) {

@ -662,6 +662,10 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
return this.updateOne({ _id: roomId }, { $set: { name } });
}
setSidepanelById(roomId: IRoom['_id'], sidepanel: IRoom['sidepanel']): Promise<UpdateResult> {
return this.updateOne({ _id: roomId }, { $set: { sidepanel } });
}
setFnameById(_id: IRoom['_id'], fname: IRoom['fname']): Promise<UpdateResult> {
const query: Filter<IRoom> = { _id };

@ -16,7 +16,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
protected name = 'room';
async create(uid: string, params: ICreateRoomParams): Promise<IRoom> {
const { type, name, members = [], readOnly, extraData, options } = params;
const { type, name, members = [], readOnly, extraData, options, sidepanel } = params;
const hasPermission = await Authorization.hasPermission(uid, `create-${type}`);
if (!hasPermission) {
@ -29,7 +29,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
}
// TODO convert `createRoom` function to "raw" and move to here
return createRoom(type, name, user, members, false, readOnly, extraData, options) as unknown as IRoom;
return createRoom(type, name, user, members, false, readOnly, extraData, options, sidepanel) as unknown as IRoom;
}
async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> {

@ -37,7 +37,10 @@ import { settings } from '../../../app/settings/server';
export class TeamService extends ServiceClassInternal implements ITeamService {
protected name = 'team';
async create(uid: string, { team, room = { name: team.name, extraData: {} }, members, owner }: ITeamCreateParams): Promise<ITeam> {
async create(
uid: string,
{ team, room = { name: team.name, extraData: {} }, members, owner, sidepanel }: ITeamCreateParams,
): Promise<ITeam> {
if (!(await checkUsernameAvailability(team.name))) {
throw new Error('team-name-already-exists');
}
@ -120,6 +123,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
teamId,
teamMain: true,
},
sidepanel,
};
const createdRoom = await Room.create(owner || uid, newRoom);

@ -2129,12 +2129,15 @@ describe('[Rooms]', () => {
describe('rooms.saveRoomSettings', () => {
let testChannel: IRoom;
const randomString = `randomString${Date.now()}`;
const teamName = `team-${Date.now()}`;
let discussion: IRoom;
let testTeam: ITeam;
before(async () => {
const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` });
testChannel = result.body.channel;
const resTeam = await request.post(api('teams.create')).set(credentials).send({ name: teamName, type: 0 });
const resDiscussion = await request
.post(api('rooms.createDiscussion'))
.set(credentials)
@ -2143,10 +2146,17 @@ describe('[Rooms]', () => {
t_name: `discussion-create-from-tests-${testChannel.name}`,
});
testTeam = resTeam.body.team;
discussion = resDiscussion.body.discussion;
});
after(() => Promise.all([deleteRoom({ type: 'p', roomId: discussion._id }), deleteRoom({ type: 'p', roomId: testChannel._id })]));
after(() =>
Promise.all([
deleteRoom({ type: 'p', roomId: discussion._id }),
deleteTeam(credentials, testTeam.name),
deleteRoom({ type: 'p', roomId: testChannel._id }),
]),
);
it('should update the room settings', (done) => {
const imageDataUri = `data:image/png;base64,${fs.readFileSync(path.join(process.cwd(), imgURL)).toString('base64')}`;
@ -2290,6 +2300,64 @@ describe('[Rooms]', () => {
expect(res.body.room).to.not.have.property('favorite');
});
});
it('should update the team sidepanel items to channels and discussions', async () => {
const sidepanelItems = ['channels', 'discussions'];
const response = await request
.post(api('rooms.saveRoomSettings'))
.set(credentials)
.send({
rid: testTeam.roomId,
sidepanel: { items: sidepanelItems },
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
const channelInfoResponse = await request
.get(api('channels.info'))
.set(credentials)
.query({ roomId: response.body.rid })
.expect('Content-Type', 'application/json')
.expect(200);
expect(channelInfoResponse.body).to.have.property('success', true);
expect(channelInfoResponse.body.channel).to.have.property('sidepanel');
expect(channelInfoResponse.body.channel.sidepanel).to.have.property('items').that.is.an('array').to.have.deep.members(sidepanelItems);
});
it('should throw error when updating team sidepanel with incorrect items', async () => {
const sidepanelItems = ['wrong'];
await request
.post(api('rooms.saveRoomSettings'))
.set(credentials)
.send({
rid: testTeam.roomId,
sidepanel: { items: sidepanelItems },
})
.expect(400);
});
it('should throw error when updating team sidepanel with more than 2 items', async () => {
const sidepanelItems = ['channels', 'discussions', 'extra'];
await request
.post(api('rooms.saveRoomSettings'))
.set(credentials)
.send({
rid: testTeam.roomId,
sidepanel: { items: sidepanelItems },
})
.expect(400);
});
it('should throw error when updating team sidepanel with duplicated items', async () => {
const sidepanelItems = ['channels', 'channels'];
await request
.post(api('rooms.saveRoomSettings'))
.set(credentials)
.send({
rid: testTeam.roomId,
sidepanel: { items: sidepanelItems },
})
.expect(400);
});
});
describe('rooms.images', () => {

@ -172,6 +172,84 @@ describe('[Teams]', () => {
})
.end(done);
});
it('should create a team with sidepanel items containing channels', async () => {
const teamName = `test-team-with-sidepanel-${Date.now()}`;
const sidepanelItems = ['channels'];
const response = await request
.post(api('teams.create'))
.set(credentials)
.send({
name: teamName,
type: 0,
sidepanel: {
items: sidepanelItems,
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('channels.info'))
.set(credentials)
.query({ roomId: response.body.team.roomId })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((response) => {
expect(response.body).to.have.property('success', true);
expect(response.body.channel).to.have.property('sidepanel');
expect(response.body.channel.sidepanel).to.have.property('items').that.is.an('array').to.have.deep.members(sidepanelItems);
});
await deleteTeam(credentials, teamName);
});
it('should throw error when creating a team with sidepanel with more than 2 items', async () => {
await request
.post(api('teams.create'))
.set(credentials)
.send({
name: `test-team-with-sidepanel-error-${Date.now()}`,
type: 0,
sidepanel: {
items: ['channels', 'discussion', 'other'],
},
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should throw error when creating a team with sidepanel with incorrect items', async () => {
await request
.post(api('teams.create'))
.set(credentials)
.send({
name: `test-team-with-sidepanel-error-${Date.now()}`,
type: 0,
sidepanel: {
items: ['other'],
},
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should throw error when creating a team with sidepanel with duplicated items', async () => {
await request
.post(api('teams.create'))
.set(credentials)
.send({
name: `test-team-with-sidepanel-error-${Date.now()}`,
type: 0,
sidepanel: {
items: ['channels', 'channels'],
},
})
.expect('Content-Type', 'application/json')
.expect(400);
});
});
describe('/teams.convertToChannel', () => {

@ -24,6 +24,7 @@ export interface ICreateRoomParams {
readOnly?: boolean;
extraData?: Partial<ICreateRoomExtraData>;
options?: ICreateRoomOptions;
sidepanel?: IRoom['sidepanel'];
}
export interface IRoomService {
addMember(uid: string, rid: string): Promise<boolean>;

@ -23,6 +23,7 @@ export interface ITeamCreateParams {
room: ITeamCreateRoom;
members?: Array<string> | null; // list of user _ids
owner?: string | null; // the team owner. If not present, owner = requester
sidepanel?: IRoom['sidepanel'];
}
export interface ITeamMemberParams {

@ -7,6 +7,8 @@ import type { IUser, Username } from './IUser';
import type { RoomType } from './RoomType';
type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing';
const sidepanelItemValues = ['channels', 'discussions'] as const;
export type SidepanelItem = (typeof sidepanelItemValues)[number];
export type RoomID = string;
export type ChannelName = string;
@ -95,8 +97,28 @@ export interface IRoom extends IRocketChatRecord {
customFields?: Record<string, any>;
usersWaitingForE2EKeys?: { userId: IUser['_id']; ts: Date }[];
sidepanel?: {
items: [SidepanelItem, SidepanelItem?];
};
}
export const isSidepanelItem = (item: any): item is SidepanelItem => {
return sidepanelItemValues.includes(item);
};
export const isValidSidepanel = (sidepanel: IRoom['sidepanel']) => {
if (!sidepanel?.items) {
return false;
}
return (
Array.isArray(sidepanel.items) &&
sidepanel.items.length &&
sidepanel.items.every(isSidepanelItem) &&
sidepanel.items.length === new Set(sidepanel.items).size
);
};
export const isRoomWithJoinCode = (room: Partial<IRoom>): room is IRoomWithJoinCode =>
'joinCodeRequired' in room && (room as any).joinCodeRequired === true;

@ -119,6 +119,8 @@ export interface IRoomsModel extends IBaseModel<IRoom> {
setRoomNameById(roomId: IRoom['_id'], name: IRoom['name']): Promise<UpdateResult>;
setSidepanelById(roomId: IRoom['_id'], sidepanel: IRoom['sidepanel']): Promise<UpdateResult>;
setFnameById(_id: IRoom['_id'], fname: IRoom['fname']): Promise<UpdateResult>;
setRoomTopicById(roomId: IRoom['_id'], topic: IRoom['description']): Promise<UpdateResult>;

@ -89,6 +89,7 @@ export type TeamsEndpoints = {
};
};
owner?: IUser['_id'];
sidepanel?: IRoom['sidepanel'];
}) => {
team: ITeam;
};

Loading…
Cancel
Save