Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/late-papayas-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Adds deprecation warning on `removeCannedResponse`;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useId, memo, useCallback } from 'react';
import { FormProvider, useForm } from 'react-hook-form';

import CannedResponseForm from './components/CannedResponseForm';
import { useRemoveCannedResponse } from './useRemoveCannedResponse';
import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../components/Page';

export type CannedResponseEditFormData = {
Expand All @@ -21,6 +20,7 @@ export type CannedResponseEditFormData = {
type CannedResponseEditProps = {
cannedResponseData?: Serialized<IOmnichannelCannedResponse>;
departmentData?: Serialized<ILivechatDepartment>;
onDelete?: () => void;
};

const getInitialData = (cannedResponseData: Serialized<IOmnichannelCannedResponse> | undefined) => ({
Expand All @@ -32,7 +32,7 @@ const getInitialData = (cannedResponseData: Serialized<IOmnichannelCannedRespons
departmentId: cannedResponseData?.departmentId || '',
});

const CannedResponseEdit = ({ cannedResponseData }: CannedResponseEditProps) => {
const CannedResponseEdit = ({ cannedResponseData, onDelete }: CannedResponseEditProps) => {
const t = useTranslation();
const router = useRouter();
const dispatchToastMessage = useToastMessageDispatch();
Expand All @@ -48,8 +48,6 @@ const CannedResponseEdit = ({ cannedResponseData }: CannedResponseEditProps) =>
formState: { isDirty },
} = methods;

const handleDelete = useRemoveCannedResponse();

const handleSave = useCallback(
async ({ departmentId, ...data }: CannedResponseEditFormData) => {
try {
Expand Down Expand Up @@ -82,7 +80,7 @@ const CannedResponseEdit = ({ cannedResponseData }: CannedResponseEditProps) =>
>
{cannedResponseData?._id && (
<ButtonGroup>
<Button danger onClick={() => handleDelete(cannedResponseData._id)}>
<Button danger onClick={onDelete}>
{t('Delete')}
</Button>
</ButtonGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { useTranslation } from 'react-i18next';

import CannedResponseEdit from './CannedResponseEdit';
import CannedResponseEditWithDepartmentData from './CannedResponseEditWithDepartmentData';
import { useRemoveCannedResponse } from './useRemoveCannedResponse';
import { FormSkeleton } from '../../components/Skeleton';

const CannedResponseEditWithData = ({ cannedResponseId }: { cannedResponseId: IOmnichannelCannedResponse['_id'] }) => {
const { t } = useTranslation();

const getCannedResponseById = useEndpoint('GET', '/v1/canned-responses/:_id', { _id: cannedResponseId });
const handleDelete = useRemoveCannedResponse(cannedResponseId);

const { data, isPending, isError } = useQuery({
queryKey: ['getCannedResponseById', cannedResponseId],
queryFn: async () => getCannedResponseById(),
Expand All @@ -30,10 +33,10 @@ const CannedResponseEditWithData = ({ cannedResponseId }: { cannedResponseId: IO
}

if (data?.cannedResponse?.scope === 'department') {
return <CannedResponseEditWithDepartmentData cannedResponseData={data.cannedResponse} />;
return <CannedResponseEditWithDepartmentData cannedResponseData={data.cannedResponse} onDelete={handleDelete} />;
}

return <CannedResponseEdit cannedResponseData={data?.cannedResponse} />;
return <CannedResponseEdit cannedResponseData={data?.cannedResponse} onDelete={handleDelete} />;
};

export default CannedResponseEditWithData;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import CannedResponseEdit from './CannedResponseEdit';
import { FormSkeleton } from '../../components/Skeleton';
import { omnichannelQueryKeys } from '../../lib/queryKeys';

const CannedResponseEditWithDepartmentData = ({ cannedResponseData }: { cannedResponseData: Serialized<IOmnichannelCannedResponse> }) => {
const CannedResponseEditWithDepartmentData = ({
cannedResponseData,
onDelete,
}: {
cannedResponseData: Serialized<IOmnichannelCannedResponse>;
onDelete: () => void;
}) => {
const departmentId = useMemo(() => cannedResponseData?.departmentId, [cannedResponseData]) as string;

const getDepartment = useEndpoint('GET', '/v1/livechat/department/:_id', { _id: departmentId });
Expand Down Expand Up @@ -39,7 +45,7 @@ const CannedResponseEditWithDepartmentData = ({ cannedResponseData }: { cannedRe
);
}

return <CannedResponseEdit cannedResponseData={cannedResponseData} departmentData={departmentData.department} />;
return <CannedResponseEdit cannedResponseData={cannedResponseData} departmentData={departmentData.department} onDelete={onDelete} />;
};

export default CannedResponseEditWithDepartmentData;
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Box, IconButton, Pagination } from '@rocket.chat/fuselage';
import { Box, Pagination } from '@rocket.chat/fuselage';
import { useDebouncedValue, useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useTranslation, usePermission, useToastMessageDispatch, useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
import { hashKey, useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';

import CannedResponseFilter from './CannedResponseFilter';
import { useRemoveCannedResponse } from './useRemoveCannedResponse';
import RemoveCannedResponseButton from './RemoveCannedResponseButton';
import GenericNoResults from '../../components/GenericNoResults';
import {
GenericTable,
Expand Down Expand Up @@ -75,8 +75,6 @@ const CannedResponsesTable = () => {
router.navigate(`/omnichannel/canned-responses/edit/${id}`);
});

const handleDelete = useRemoveCannedResponse();

const defaultOptions = useMemo(
() => ({
global: t('Public'),
Expand Down Expand Up @@ -170,19 +168,7 @@ const CannedResponsesTable = () => {
</GenericTableCell>
<GenericTableCell withTruncatedText>{getTime(_createdAt)}</GenericTableCell>
<GenericTableCell withTruncatedText>{tags.join(', ')}</GenericTableCell>
{!(scope === 'global' && isMonitor && !isManager) && (
<GenericTableCell withTruncatedText>
<IconButton
icon='trash'
small
title={t('Remove')}
onClick={(e) => {
e.stopPropagation();
handleDelete(_id);
}}
/>
</GenericTableCell>
)}
{!(scope === 'global' && isMonitor && !isManager) && <RemoveCannedResponseButton id={_id} />}
</GenericTableRow>
))}
</GenericTableBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { IconButton } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';

import { useRemoveCannedResponse } from './useRemoveCannedResponse';
import { GenericTableCell } from '../../components/GenericTable';

const RemoveCannedResponseButton = ({ id }: { id: IOmnichannelCannedResponse['_id'] }) => {
const { t } = useTranslation();

const handleDelete = useRemoveCannedResponse(id);

return (
<GenericTableCell withTruncatedText>
<IconButton
icon='trash'
small
title={t('Remove')}
onClick={(e) => {
e.stopPropagation();
handleDelete();
}}
/>
</GenericTableCell>
);
};

export default RemoveCannedResponseButton;
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useRouter, useMethod } from '@rocket.chat/ui-contexts';
import { useSetModal, useToastMessageDispatch, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';

export const useRemoveCannedResponse = () => {
export const useRemoveCannedResponse = (id: IOmnichannelCannedResponse['_id']) => {
const { t } = useTranslation();
const setModal = useSetModal();
const router = useRouter();
const queryClient = useQueryClient();

const dispatchToastMessage = useToastMessageDispatch();
const removeCannedResponse = useMethod('removeCannedResponse');
const removeCannedResponse = useEndpoint('DELETE', '/v1/canned-responses/:_id', { _id: id });

const handleDelete = useEffectEvent((id: string) => {
const handleDelete = useEffectEvent(() => {
const onDeleteCannedResponse: () => Promise<void> = async () => {
try {
await removeCannedResponse(id);
await removeCannedResponse();
queryClient.invalidateQueries({
queryKey: ['getCannedResponses'],
});
Expand Down
63 changes: 61 additions & 2 deletions apps/meteor/ee/app/api-enterprise/server/canned-responses.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ILivechatDepartment, IOmnichannelCannedResponse, IUser } from '@rocket.chat/core-typings';
import { isPOSTCannedResponsesProps, isDELETECannedResponsesProps, isCannedResponsesProps } from '@rocket.chat/rest-typings';
import { isPOSTCannedResponsesProps, isCannedResponsesProps, isDELETECannedResponsesProps } from '@rocket.chat/rest-typings';
import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings';

import { findAllCannedResponses, findAllCannedResponsesFilter, findOneCannedResponse } from './lib/canned-responses';
Expand Down Expand Up @@ -38,6 +38,7 @@ declare module '@rocket.chat/rest-typings' {
GET: () => {
cannedResponse: IOmnichannelCannedResponse;
};
DELETE: () => void;
};
}
}
Expand All @@ -54,13 +55,61 @@ API.v1.addRoute(
},
);

/**
* @deprecated
* @openapi
* /api/v1/canned-responses:
* delete:
* deprecated: true
* security:
* $ref: '#/security/authenticated'
* parameters:
* - in: body
* name: body
* description: |
* **_id** (required): Canned Response ID to be removed.
* schema:
* type: object
* required:
* - _id
* properties:
* _id:
* type: string
* tags:
* - Canned_Responses
* responses:
* 200:
* description: Successful Response
* schema:
* type: object
* properties:
* status:
* type: string
* example: success
* data:
* type: object
* description: The response data
* properties:
* success:
* type: boolean
* example: true
* 401:
* $ref: '#/responses/Unauthorized'
* 403:
* $ref: '#/responses/Forbidden'
* 404:
* $ref: '#/responses/NotFound'
* 500:
* $ref: '#/responses/InternalServerError'
*/
API.v1.addRoute(
'canned-responses',
{
authRequired: true,
permissionsRequired: { GET: ['view-canned-responses'], POST: ['save-canned-responses'], DELETE: ['remove-canned-responses'] },
validateParams: { POST: isPOSTCannedResponsesProps, DELETE: isDELETECannedResponsesProps, GET: isCannedResponsesProps },
license: ['canned-responses'],
deprecations: { DELETE: { version: '8.0.0', alternatives: ['/v1/canned-responses/:_id'] } },
},
{
async get() {
Expand Down Expand Up @@ -104,6 +153,7 @@ API.v1.addRoute(
);
return API.v1.success();
},
// deprecated
async delete() {
const { _id } = this.bodyParams;
await removeCannedResponse(this.userId, _id);
Expand All @@ -114,7 +164,11 @@ API.v1.addRoute(

API.v1.addRoute(
'canned-responses/:_id',
{ authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] },
{
authRequired: true,
permissionsRequired: { GET: ['view-canned-responses'], DELETE: ['remove-canned-responses'] },
license: ['canned-responses'],
},
{
async get() {
const { _id } = this.urlParams;
Expand All @@ -129,5 +183,10 @@ API.v1.addRoute(

return API.v1.success({ cannedResponse });
},
async delete() {
const { _id } = this.urlParams;
await removeCannedResponse(this.userId, _id);
return API.v1.success();
},
},
);
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 notifications from '../../../../../app/notifications/server/lib/Notifications';

declare module '@rocket.chat/ddp-client' {
Expand Down Expand Up @@ -36,6 +37,7 @@ export const removeCannedResponse = async (uid: string, _id: string): Promise<vo

Meteor.methods<ServerMethods>({
async removeCannedResponse(_id) {
methodDeprecationLogger.method('removeCannedResponse', '8.0.0', 'DELETE /v1/canned-responses/:_id');
const uid = Meteor.userId();

if (!uid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ import { IS_EE } from '../../../e2e/config/constants';
describe('[DELETE] canned-responses', () => {
it('should fail if user dont have remove-canned-responses permission', async () => {
await updatePermission('remove-canned-responses', []);
return request.delete(api('canned-responses')).send({ _id: 'sfdads' }).set(credentials).expect(403);
return request.delete(api('canned-responses/sfdads')).set(credentials).expect(403);
});
it('should fail if _id is not on the request', async () => {
await updatePermission('remove-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']);
Expand All @@ -259,7 +259,10 @@ import { IS_EE } from '../../../e2e/config/constants';
it('should delete a canned response', async () => {
const response = await createCannedResponse();
const { body: cr } = await request.get(api('canned-responses')).set(credentials).query({ shortcut: response.shortcut }).expect(200);
const { body } = await request.delete(api('canned-responses')).send({ _id: cr.cannedResponses[0]._id }).set(credentials).expect(200);
const { body } = await request
.delete(api(`canned-responses/${cr.cannedResponses[0]._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('success', true);
});
});
Expand Down
Loading