Skip to content

Commit

Permalink
Merge branch 'develop' into feat/configurable-visitor-chat-closing
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman authored Aug 23, 2024
2 parents d43f387 + eaf9c8d commit 74b5ee2
Show file tree
Hide file tree
Showing 22 changed files with 614 additions and 31 deletions.
9 changes: 9 additions & 0 deletions .changeset/sixty-spoons-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/models": minor
"@rocket.chat/rest-typings": minor
---

Introduced "create contacts" endpoint to omnichannel
4 changes: 4 additions & 0 deletions apps/meteor/app/authorization/server/constant/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export const permissions = [
_id: 'view-l-room',
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
},
{
_id: 'create-livechat-contact',
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
},
{ _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
{
_id: 'view-omnichannel-contact-center',
Expand Down
13 changes: 6 additions & 7 deletions apps/meteor/app/livechat/client/lib/chartHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,9 @@ export const drawDoughnutChart = async (
chartContext: { destroy: () => void } | undefined,
dataLabels: string[],
dataPoints: number[],
): Promise<ChartType<'doughnut', number[], string> | void> => {
): Promise<ChartType> => {
if (!chart) {
console.error('No chart element');
return;
throw new Error('No chart element');
}
if (chartContext) {
chartContext.destroy();
Expand All @@ -200,7 +199,7 @@ export const drawDoughnutChart = async (
],
},
options: doughnutChartConfiguration(title),
});
}) as ChartType;
};

/**
Expand All @@ -209,12 +208,12 @@ export const drawDoughnutChart = async (
* @param {String} label [chart label]
* @param {Array(Double)} data [updated data]
*/
export const updateChart = async (c: ChartType, label: string, data: { [x: string]: number }): Promise<void> => {
export const updateChart = async (c: ChartType, label: string, data: number[]): Promise<void> => {
const chart = await c;
if (chart.data?.labels?.indexOf(label) === -1) {
// insert data
chart.data.labels.push(label);
chart.data.datasets.forEach((dataset: { data: any[] }, idx: string | number) => {
chart.data.datasets.forEach((dataset: { data: any[] }, idx: number) => {
dataset.data.push(data[idx]);
});
} else {
Expand All @@ -224,7 +223,7 @@ export const updateChart = async (c: ChartType, label: string, data: { [x: strin
return;
}

chart.data.datasets.forEach((dataset: { data: { [x: string]: any } }, idx: string | number) => {
chart.data.datasets.forEach((dataset: { data: { [x: string]: any } }, idx: number) => {
dataset.data[index] = data[idx];
});
}
Expand Down
23 changes: 21 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models';
import { isPOSTOmnichannelContactsProps } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { API } from '../../../../api/server';
import { Contacts } from '../../lib/Contacts';
import { Contacts, createContact } from '../../lib/Contacts';

API.v1.addRoute(
'omnichannel/contact',
{ authRequired: true, permissionsRequired: ['view-l-room'] },
{
authRequired: true,
permissionsRequired: ['view-l-room'],
},
{
async post() {
check(this.bodyParams, {
Expand Down Expand Up @@ -82,3 +86,18 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'omnichannel/contacts',
{ authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps },
{
async post() {
if (!process.env.TEST_MODE) {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
}
const contactId = await createContact({ ...this.bodyParams, unknown: false });

return API.v1.success({ contactId });
},
},
);
85 changes: 83 additions & 2 deletions apps/meteor/app/livechat/server/lib/Contacts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users, LivechatRooms, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models';
import type { ILivechatContactChannel, ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import {
LivechatVisitors,
Users,
LivechatRooms,
LivechatCustomField,
LivechatInquiry,
Rooms,
Subscriptions,
LivechatContacts,
} from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb';
Expand All @@ -26,6 +35,16 @@ type RegisterContactProps = {
};
};

type CreateContactParams = {
name: string;
emails: string[];
phones: string[];
unknown: boolean;
customFields?: Record<string, string | unknown>;
contactManager?: string;
channels?: ILivechatContactChannel[];
};

export const Contacts = {
async registerContact({
token,
Expand Down Expand Up @@ -165,3 +184,65 @@ export const Contacts = {
return contactId;
},
};

export async function createContact(params: CreateContactParams): Promise<string> {
const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params;

if (contactManager) {
const contactManagerUser = await Users.findOneAgentById<Pick<IUser, 'roles'>>(contactManager, { projection: { roles: 1 } });
if (!contactManagerUser) {
throw new Error('error-contact-manager-not-found');
}
}

const allowedCustomFields = await getAllowedCustomFields();
validateCustomFields(allowedCustomFields, customFields);

const { insertedId } = await LivechatContacts.insertOne({
name,
emails,
phones,
contactManager,
channels,
customFields,
unknown,
});

return insertedId;
}

async function getAllowedCustomFields(): Promise<ILivechatCustomField[]> {
return LivechatCustomField.findByScope(
'visitor',
{
projection: { _id: 1, label: 1, regexp: 1, required: 1 },
},
false,
).toArray();
}

export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record<string, string | unknown>) {
for (const cf of allowedCustomFields) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}
const cfValue: string = trim(customFields[cf._id]);

if (!cfValue || typeof cfValue !== 'string') {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}

if (cf.regexp) {
const regex = new RegExp(cf.regexp);
if (!regex.test(cfValue)) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { OperationParams } from '@rocket.chat/rest-typings';
import type { TranslationContextValue, TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { Chart as ChartType } from 'chart.js';
import type { MutableRefObject } from 'react';
import React, { useRef, useEffect } from 'react';

import { drawDoughnutChart } from '../../../../../app/livechat/client/lib/chartHandler';
Expand All @@ -16,20 +20,25 @@ const initialData = {
offline: 0,
};

const init = (canvas, context, t) =>
const init = (canvas: HTMLCanvasElement, context: ChartType | undefined, t: TranslationContextValue['translate']): Promise<ChartType> =>
drawDoughnutChart(
canvas,
t('Agents'),
context,
labels.map((l) => t(l)),
labels.map((l) => t(l as TranslationKey)),
Object.values(initialData),
);

const AgentStatusChart = ({ params, reloadRef, ...props }) => {
type AgentStatusChartsProps = {
params: OperationParams<'GET', '/v1/livechat/analytics/dashboards/charts/agents-status'>;
reloadRef: MutableRefObject<{ [x: string]: () => void }>;
};

const AgentStatusChart = ({ params, reloadRef, ...props }: AgentStatusChartsProps) => {
const t = useTranslation();

const canvas = useRef();
const context = useRef();
const canvas: MutableRefObject<HTMLCanvasElement | null> = useRef(null);
const context: MutableRefObject<ChartType | undefined> = useRef();

const updateChartData = useUpdateChartData({
context,
Expand All @@ -46,7 +55,9 @@ const AgentStatusChart = ({ params, reloadRef, ...props }) => {

useEffect(() => {
const initChart = async () => {
context.current = await init(canvas.current, context.current, t);
if (canvas?.current) {
context.current = await init(canvas.current, context.current, t);
}
};
initChart();
}, [t]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { OperationParams } from '@rocket.chat/rest-typings';
import type { TranslationContextValue, TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { Chart as ChartType } from 'chart.js';
import type { MutableRefObject } from 'react';
import React, { useRef, useEffect } from 'react';

import { drawDoughnutChart } from '../../../../../app/livechat/client/lib/chartHandler';
Expand All @@ -16,20 +20,25 @@ const initialData = {
closed: 0,
};

const init = (canvas, context, t) =>
const init = (canvas: HTMLCanvasElement, context: ChartType | undefined, t: TranslationContextValue['translate']) =>
drawDoughnutChart(
canvas,
t('Chats'),
context,
labels.map((l) => t(l)),
labels.map((l) => t(l as TranslationKey)),
Object.values(initialData),
);

const ChatsChart = ({ params, reloadRef, ...props }) => {
type ChatsChartProps = {
params: OperationParams<'GET', '/v1/livechat/analytics/dashboards/charts/chats'>;
reloadRef: MutableRefObject<{ [x: string]: () => void }>;
};

const ChatsChart = ({ params, reloadRef, ...props }: ChatsChartProps) => {
const t = useTranslation();

const canvas = useRef();
const context = useRef();
const canvas: MutableRefObject<HTMLCanvasElement | null> = useRef(null);
const context: MutableRefObject<ChartType | undefined> = useRef();

const updateChartData = useUpdateChartData({
context,
Expand All @@ -46,7 +55,9 @@ const ChatsChart = ({ params, reloadRef, ...props }) => {

useEffect(() => {
const initChart = async () => {
context.current = await init(canvas.current, context.current, t);
if (canvas?.current) {
context.current = await init(canvas.current, context.current, t);
}
};
initChart();
}, [t]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import type { TranslationContextValue } from '@rocket.chat/ui-contexts';
import { type Chart } from 'chart.js';
import { type TFunction } from 'i18next';
import { type RefObject } from 'react';
import { type MutableRefObject } from 'react';

import { updateChart } from '../../../../../app/livechat/client/lib/chartHandler';

type UseUpdateChartDataOptions = {
context: RefObject<Chart | undefined>;
canvas: RefObject<HTMLCanvasElement | null>;
init: (canvas: HTMLCanvasElement, context: undefined, t: TFunction) => Promise<Chart>;
t: TFunction;
context: MutableRefObject<Chart | undefined>;
canvas: MutableRefObject<HTMLCanvasElement | null>;
init: (canvas: HTMLCanvasElement, context: undefined, t: TranslationContextValue['translate']) => Promise<Chart>;
t: TranslationContextValue['translate'];
};

export const useUpdateChartData = ({ context: contextRef, canvas: canvasRef, init, t }: UseUpdateChartDataOptions) =>
useMutableCallback(async (label: string, data: { [x: string]: number }) => {
useMutableCallback(async (label: string, data: number[]) => {
const canvas = canvasRef.current;

if (!canvas) {
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/server/models/LivechatContacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerModel } from '@rocket.chat/models';

import { db } from '../database/utils';
import { LivechatContactsRaw } from './raw/LivechatContacts';

registerModel('ILivechatContactsModel', new LivechatContactsRaw(db));
11 changes: 11 additions & 0 deletions apps/meteor/server/models/raw/LivechatContacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ILivechatContact, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { ILivechatContactsModel } from '@rocket.chat/model-typings';
import type { Collection, Db } from 'mongodb';

import { BaseRaw } from './BaseRaw';

export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements ILivechatContactsModel {
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatContact>>) {
super(db, 'livechat_contact', trash);
}
}
1 change: 1 addition & 0 deletions apps/meteor/server/models/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import './Integrations';
import './Invites';
import './LivechatAgentActivity';
import './LivechatBusinessHours';
import './LivechatContacts';
import './LivechatCustomField';
import './LivechatDepartment';
import './LivechatDepartmentAgents';
Expand Down
Loading

0 comments on commit 74b5ee2

Please sign in to comment.