Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
437fad8
chore: adds deprecation warning for livechat:saveTag
lucas-a-pelegrino Oct 22, 2025
3e34045
chore: adds a new endpoint to handle the saving of tags
lucas-a-pelegrino Oct 22, 2025
bf1c7c1
chore: updates test helpers to use the new endpoint instead of livech…
lucas-a-pelegrino Oct 22, 2025
e9ad5e1
chore: updates the client to use the new endpoint instead of livechat…
lucas-a-pelegrino Oct 22, 2025
d800086
docs: adds changeset
lucas-a-pelegrino Oct 22, 2025
2a41bf9
fix: linting errors
lucas-a-pelegrino Oct 22, 2025
e4c953a
fix: prettier formatting
lucas-a-pelegrino Oct 23, 2025
181c258
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into chor…
lucas-a-pelegrino Oct 23, 2025
e552717
fix: CreateTagParams types; request body params
lucas-a-pelegrino Oct 24, 2025
2ac8925
fix: livechat method name
lucas-a-pelegrino Nov 6, 2025
2882d40
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into chor…
lucas-a-pelegrino Nov 6, 2025
b784926
fix: Meteor.Error with wrong method name
lucas-a-pelegrino Nov 6, 2025
4fd53d6
chore: adds minor improvement to the livechat/tags.save endpoint layer
lucas-a-pelegrino Nov 7, 2025
34d69ad
Merge remote-tracking branch 'origin/chore/v7/CORE-1411' into chore/v…
lucas-a-pelegrino Nov 7, 2025
f16d490
chore: adds improvements suggested from code reviews
lucas-a-pelegrino Nov 21, 2025
2534616
chore: applies code review suggestions
lucas-a-pelegrino Dec 1, 2025
0f8a0a2
fix: merge conflicts
lucas-a-pelegrino Dec 1, 2025
e555f46
chore: adds minor improvements to tags testing functions
lucas-a-pelegrino Dec 1, 2025
97c3113
chore: adds minor improvements to tag saving logic
lucas-a-pelegrino Dec 1, 2025
a22519f
fix: linting error
lucas-a-pelegrino Dec 1, 2025
d20dea5
chore: adds minor improvement to createTag helper
lucas-a-pelegrino Dec 1, 2025
714ac3b
fix: merge conflicts
lucas-a-pelegrino Dec 3, 2025
65a4fae
fix: merge conflicts in omnichannel.ts
lucas-a-pelegrino Dec 3, 2025
ec0848b
Merge branch 'develop' into chore/v7/CORE-1411
tassoevan Dec 4, 2025
e2da75e
Merge branch 'develop' into chore/v7/CORE-1411
kodiakhq[bot] Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/seven-otters-turn.md
Original file line number Diff line number Diff line change
@@ -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`
10 changes: 7 additions & 3 deletions apps/meteor/client/views/omnichannel/tags/TagEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 || {};

Expand All @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,13 +19,6 @@ type FindTagsParams = {
viewAll?: boolean;
};

type FindTagsResult = {
tags: ILivechatTag[];
count: number;
offset: number;
total: number;
};

type FindTagsByIdParams = {
userId: string;
tagId: string;
Expand Down
72 changes: 45 additions & 27 deletions apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
isPOSTLivechatTagsRemoveParams,
POSTLivechatTagsRemoveSuccessResponse,
isPOSTLivechatTagsSaveParams,
POSTLivechatTagsSaveSuccessResponse,
isPOSTLivechatTagsDeleteParams,
POSTLivechatTagsDeleteSuccessResponse,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
Expand Down Expand Up @@ -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>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand All @@ -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));
Expand Down
81 changes: 36 additions & 45 deletions apps/meteor/tests/data/livechat/tags.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import { faker } from '@faker-js/faker';
import type { ILivechatTag } from '@rocket.chat/core-typings';

import { credentials, methodCall, request } from '../api-data';
import type { DummyResponse } from './utils';

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);
});
});
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';

import { credentials, request, api } from '../api-data';

export const listTags = async (): Promise<FindTagsResult> => {
const { body } = await request.get(api('livechat/tags')).set(credentials).query({ viewAll: 'true' });

return body;
};

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 = (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 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;
};
27 changes: 15 additions & 12 deletions apps/meteor/tests/e2e/utils/omnichannel/tags.ts
Original file line number Diff line number Diff line change
@@ -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),
};
};
17 changes: 3 additions & 14 deletions apps/meteor/tests/end-to-end/api/livechat/13-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/core-typings/src/ILivechatTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export interface ILivechatTag {
numDepartments: number;
departments: Array<string>;
}

export type FindTagsResult = {
tags: ILivechatTag[];
count: number;
offset: number;
total: number;
};
Loading
Loading