chore: Adds deprecation warning for livechat:saveTag and new endpoint to replace it (#37281)

This commit is contained in:
Lucas Pelegrino 2025-12-04 15:00:13 -03:00 committed by GitHub
parent 7809e0401a
commit dc67590d14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 201 additions and 114 deletions

View File

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---
Adds deprecation warning for `livechat:saveTag` and new endpoint to replace it; `livechat/tags.save`

View File

@ -8,7 +8,7 @@ import {
ContextualbarHeader,
ContextualbarClose,
} from '@rocket.chat/ui-client';
import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQueryClient } from '@tanstack/react-query';
import { useId } from 'react';
import { useForm, Controller } from 'react-hook-form';
@ -35,7 +35,7 @@ const TagEdit = ({ tagData, currentDepartments, onClose }: TagEditProps) => {
const handleDeleteTag = useRemoveTag();
const dispatchToastMessage = useToastMessageDispatch();
const saveTag = useMethod('livechat:saveTag');
const saveTag = useEndpoint('POST', '/v1/livechat/tags.save');
const { _id, name, description } = tagData || {};
@ -56,7 +56,11 @@ const TagEdit = ({ tagData, currentDepartments, onClose }: TagEditProps) => {
const departmentsId = departments?.map((dep) => dep.value) || [''];
try {
await saveTag(_id as unknown as string, { name, description }, departmentsId);
await saveTag({
_id,
tagData: { name, description },
...(departmentsId.length > 0 && { tagDepartments: departmentsId }),
});
dispatchToastMessage({ type: 'success', message: t('Saved') });
queryClient.invalidateQueries({
queryKey: ['livechat-tags'],

View File

@ -1,4 +1,4 @@
import type { ILivechatTag } from '@rocket.chat/core-typings';
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';
import { LivechatTag } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { Filter, FindOptions } from 'mongodb';
@ -19,13 +19,6 @@ type FindTagsParams = {
viewAll?: boolean;
};
type FindTagsResult = {
tags: ILivechatTag[];
count: number;
offset: number;
total: number;
};
type FindTagsByIdParams = {
userId: string;
tagId: string;

View File

@ -1,6 +1,8 @@
import {
isPOSTLivechatTagsRemoveParams,
POSTLivechatTagsRemoveSuccessResponse,
isPOSTLivechatTagsSaveParams,
POSTLivechatTagsSaveSuccessResponse,
isPOSTLivechatTagsDeleteParams,
POSTLivechatTagsDeleteSuccessResponse,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
@ -67,35 +69,51 @@ API.v1.addRoute(
},
);
const livechatTagsEndpoints = API.v1.post(
'livechat/tags.delete',
{
response: {
200: POSTLivechatTagsRemoveSuccessResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
const livechatTagsEndpoints = API.v1
.post(
'livechat/tags.save',
{
response: {
200: POSTLivechatTagsSaveSuccessResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
authRequired: true,
permissions: ['manage-livechat-tags'],
license: ['livechat-enterprise'],
body: isPOSTLivechatTagsSaveParams,
},
authRequired: true,
permissions: ['manage-livechat-tags'],
license: ['livechat-enterprise'],
body: isPOSTLivechatTagsRemoveParams,
},
async function action() {
const { id } = this.bodyParams;
try {
async function action() {
const { _id, tagData, tagDepartments } = this.bodyParams;
const result = await LivechatEnterprise.saveTag(_id, tagData, tagDepartments);
return API.v1.success(result);
},
)
.post(
'livechat/tags.delete',
{
response: {
200: POSTLivechatTagsDeleteSuccessResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
authRequired: true,
permissions: ['manage-livechat-tags'],
license: ['livechat-enterprise'],
body: isPOSTLivechatTagsDeleteParams,
},
async function action() {
const { id } = this.bodyParams;
await LivechatEnterprise.removeTag(id);
return API.v1.success();
} catch (error: unknown) {
if (error instanceof Meteor.Error) {
return API.v1.failure(error.reason);
}
return API.v1.failure('error-removing-tag');
}
},
);
},
);
type LivechatTagsEndpoints = ExtractRoutesFromAPI<typeof livechatTagsEndpoints>;

View File

@ -141,7 +141,7 @@ export const LivechatEnterprise = {
return LivechatTag.removeById(_id);
},
async saveTag(_id: string | undefined, tagData: { name: string; description?: string }, tagDepartments: string[]) {
async saveTag(_id: string | undefined, tagData: { name: string; description?: string }, tagDepartments: string[] | undefined) {
return LivechatTag.createOrUpdateTag(_id, tagData, tagDepartments);
},

View File

@ -4,6 +4,7 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../../../app/lib/server/lib/deprecationWarningLogger';
import { LivechatEnterprise } from '../lib/LivechatEnterprise';
declare module '@rocket.chat/ddp-client' {
@ -15,9 +16,10 @@ declare module '@rocket.chat/ddp-client' {
Meteor.methods<ServerMethods>({
async 'livechat:saveTag'(_id, tagData, tagDepartments) {
methodDeprecationLogger.method('livechat:saveTag', '8.0.0', 'POST /v1/livechat/tags.save');
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-tags'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveTags' });
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveTag' });
}
check(_id, Match.Maybe(String));

View File

@ -1,49 +1,40 @@
import { faker } from '@faker-js/faker';
import type { ILivechatTag } from '@rocket.chat/core-typings';
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';
import { credentials, methodCall, request } from '../api-data';
import type { DummyResponse } from './utils';
import { credentials, request, api } from '../api-data';
export const saveTags = (departments: string[] = []): Promise<ILivechatTag> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall(`livechat:saveTag`))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveTag',
params: [undefined, { name: faker.string.uuid(), description: faker.lorem.sentence() }, departments],
id: '101',
msg: 'method',
}),
})
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
export const listTags = async (): Promise<FindTagsResult> => {
const { body } = await request.get(api('livechat/tags')).set(credentials).query({ viewAll: 'true' });
return body;
};
export const removeTag = (id: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall(`livechat:removeTag`))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:removeTag',
params: [id],
id: '101',
msg: 'method',
}),
})
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
export const saveTags = async (departments: string[] = []): Promise<ILivechatTag> => {
const { body } = await request
.post(api('livechat/tags.save'))
.set(credentials)
.send({
tagData: {
name: faker.string.uuid(),
description: faker.lorem.sentence(),
},
...(departments.length > 0 && { tagDepartments: departments }),
});
return body;
};
export const removeTag = async (id: string): Promise<boolean> => {
const res = await request.post(api('livechat/tags.delete')).set(credentials).send({ id });
return res.status === 200;
};
export const removeAllTags = async (): Promise<boolean> => {
const tagsList = await listTags();
await Promise.all(tagsList.tags.map((tag) => removeTag(tag._id)));
const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(200);
return response.body.tags.length === 0;
};

View File

@ -1,32 +1,35 @@
import type { ILivechatTag } from '@rocket.chat/core-typings';
import { parseMeteorResponse } from '../parseMeteorResponse';
import type { BaseTest } from '../test';
type CreateTagParams = {
id?: string | null;
name?: string;
description?: string;
departments?: { departmentId: string }[];
departments?: string[];
};
const removeTag = async (api: BaseTest['api'], id: string) => api.post('/livechat/tags.delete', { id });
export const createTag = async (api: BaseTest['api'], { id = null, name, description = '', departments = [] }: CreateTagParams = {}) => {
const response = await api.post('/method.call/livechat:saveTag', {
message: JSON.stringify({
msg: 'method',
id: '33',
method: 'livechat:saveTag',
params: [id, { name, description }, departments],
}),
const response = await api.post('/livechat/tags.save', {
_id: id,
tagData: {
name,
description,
},
...(departments.length > 0 && { tagDepartments: departments }),
});
const tag = await parseMeteorResponse<ILivechatTag>(response);
if (response.status() !== 200) {
throw new Error(`Failed to create tag [http status: ${response.status()}]`);
}
const data: ILivechatTag = await response.json();
return {
response,
data: tag,
delete: async () => removeTag(api, tag?._id),
data,
delete: async () => removeTag(api, data._id),
};
};

View File

@ -4,7 +4,7 @@ import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department';
import { removeTag, saveTags } from '../../../data/livechat/tags';
import { saveTags, removeAllTags } from '../../../data/livechat/tags';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import type { IUserWithCredentials } from '../../../data/user';
@ -38,19 +38,7 @@ import { IS_EE } from '../../../e2e/config/constants';
};
// remove all existing tags
const allTags = await request
.get(api('livechat/tags'))
.set(credentials)
.query({ viewAll: 'true' })
.expect('Content-Type', 'application/json')
.expect(200);
const { tags } = allTags.body;
for await (const tag of tags) {
await removeTag(tag._id);
}
const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('tags').and.to.be.an('array').that.is.empty;
await removeAllTags();
// should add 3 tags
const { department: dA, agent: agentA } = await createDepartmentWithAnOnlineAgent();
@ -67,6 +55,7 @@ import { IS_EE } from '../../../e2e/config/constants';
after(async () => {
await deleteUser(monitor.user);
await removeAllTags();
});
it('should throw unauthorized error when the user does not have the necessary permission', async () => {

View File

@ -5,3 +5,10 @@ export interface ILivechatTag {
numDepartments: number;
departments: Array<string>;
}
export type FindTagsResult = {
tags: ILivechatTag[];
count: number;
offset: number;
total: number;
};

View File

@ -575,11 +575,85 @@ const POSTLivechatMonitorsDeleteSuccessSchema = {
export const POSTLivechatMonitorsDeleteSuccessResponse = ajv.compile<void>(POSTLivechatMonitorsDeleteSuccessSchema);
type POSTLivechatTagsRemoveParams = {
type POSTLivechatTagsSaveParams = {
_id?: string;
tagData: {
name: string;
description?: string;
};
tagDepartments?: string[];
};
const POSTLivechatTagsSaveParamsSchema = {
type: 'object',
properties: {
_id: {
type: 'string',
nullable: true,
},
tagData: {
type: 'object',
properties: {
name: {
type: 'string',
},
description: {
type: 'string',
},
},
},
tagDepartments: {
type: 'array',
items: {
type: 'string',
},
minItems: 1,
nullable: true,
},
},
required: ['tagData'],
additionalProperties: false,
};
export const isPOSTLivechatTagsSaveParams = ajv.compile<POSTLivechatTagsSaveParams>(POSTLivechatTagsSaveParamsSchema);
const POSTLivechatTagsSaveSuccessResponseSchema = {
type: 'object',
properties: {
_id: {
type: 'string',
},
name: {
type: 'string',
},
description: {
type: 'string',
},
numDepartments: {
type: 'number',
},
departments: {
type: 'array',
items: {
type: 'string',
},
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['_id', 'name', 'description', 'numDepartments', 'departments', 'success'],
additionalProperties: false,
};
export const POSTLivechatTagsSaveSuccessResponse = ajv.compile<ILivechatTag>(POSTLivechatTagsSaveSuccessResponseSchema);
type POSTLivechatTagsDeleteParams = {
id: string;
};
const POSTLivechatTagsRemoveSchema = {
const POSTLivechatTagsDeleteSchema = {
type: 'object',
properties: {
id: {
@ -590,9 +664,9 @@ const POSTLivechatTagsRemoveSchema = {
additionalProperties: false,
};
export const isPOSTLivechatTagsRemoveParams = ajv.compile<POSTLivechatTagsRemoveParams>(POSTLivechatTagsRemoveSchema);
export const isPOSTLivechatTagsDeleteParams = ajv.compile<POSTLivechatTagsDeleteParams>(POSTLivechatTagsDeleteSchema);
const POSTLivechatTagsRemoveSuccessResponseSchema = {
const POSTLivechatTagsDeleteSuccessResponseSchema = {
type: 'object',
properties: {
success: {
@ -603,7 +677,7 @@ const POSTLivechatTagsRemoveSuccessResponseSchema = {
additionalProperties: false,
};
export const POSTLivechatTagsRemoveSuccessResponse = ajv.compile<void>(POSTLivechatTagsRemoveSuccessResponseSchema);
export const POSTLivechatTagsDeleteSuccessResponse = ajv.compile<void>(POSTLivechatTagsDeleteSuccessResponseSchema);
type LivechatTagsListProps = PaginatedRequest<{ text: string; viewAll?: 'true' | 'false'; department?: string }, 'name'>;