Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Capability to search visitors by custom fields #26312

Merged
merged 42 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0f5fb61
[IMPROVE] Capability to search visitors by custom fields
cauefcr Jul 20, 2022
152638b
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 21, 2022
76c6ca6
reverting i18n change
cauefcr Jul 21, 2022
9267b2f
Update apps/meteor/server/models/raw/LivechatVisitors.ts
cauefcr Jul 25, 2022
51fcb54
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 25, 2022
181b23b
removing useless code
cauefcr Jul 25, 2022
bbaba3e
Merge branch 'fix/custom-field-search2' of ssh://github.com/RocketCha…
cauefcr Jul 25, 2022
d879b33
some extra changes to regex and queries and permissions
KevLehman Jul 26, 2022
0f92df4
fix new custom fields searchable, fix deprecated field usage, and val…
cauefcr Jul 27, 2022
fb1a23a
Merge branch 'fix/custom-field-search2' of ssh://github.com/RocketCha…
cauefcr Jul 27, 2022
8ce26b5
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 27, 2022
274909a
livechat/visitor.search tests
cauefcr Jul 27, 2022
92678e2
better tests, thanks @kevin!
cauefcr Jul 29, 2022
8f15363
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Jul 29, 2022
04bebf4
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 1, 2022
b8f0558
proper test end
cauefcr Aug 1, 2022
511e42b
Merge branch 'develop' into fix/custom-field-search2
KevLehman Aug 1, 2022
3b414ee
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 3, 2022
b70de71
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 3, 2022
f67c01c
Merge branch 'fix/custom-field-search2' of ssh://github.com/RocketCha…
cauefcr Aug 3, 2022
6097ad7
linting
cauefcr Aug 4, 2022
641da30
Merge branch 'develop' into fix/custom-field-search2
KevLehman Aug 15, 2022
6c83b8e
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 15, 2022
8c35f64
Merge branch 'fix/custom-field-search2' of ssh://github.com/RocketCha…
cauefcr Aug 15, 2022
018742b
Merge branch 'develop' into fix/custom-field-search2
KevLehman Aug 18, 2022
3f97b3e
Merge branch 'develop' into fix/custom-field-search2
KevLehman Aug 18, 2022
cca0adb
Merge branch 'develop' into fix/custom-field-search2
KevLehman Aug 19, 2022
f953ae8
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 23, 2022
c1d8685
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 24, 2022
4eda605
refactoring queries to proper models and other fixes
cauefcr Aug 24, 2022
120332f
Merge branch 'fix/custom-field-search2' of ssh://github.com/RocketCha…
cauefcr Aug 24, 2022
7f403b5
fixing type error
cauefcr Aug 24, 2022
5f947f0
Merge branch 'develop' of ssh://github.com/RocketChat/Rocket.Chat int…
cauefcr Aug 24, 2022
9094b7b
Merge branch 'develop' into fix/custom-field-search2
casalsgh Aug 25, 2022
404f63a
Merge branch 'fix/custom-field-search2' of github.com:RocketChat/Rock…
KevLehman Aug 25, 2022
ae4ee00
small improvements
KevLehman Aug 25, 2022
a5d2c52
Merge remote-tracking branch 'origin/develop' into fix/custom-field-s…
sampaiodiego Aug 25, 2022
04b77de
small refactor
sampaiodiego Aug 25, 2022
31b73ac
remove that urlsearchparams
KevLehman Aug 25, 2022
48e26ab
forgotten changes
sampaiodiego Aug 25, 2022
9cd1fe3
Merge branch 'fix/custom-field-search2' of github.com:RocketChat/Rock…
sampaiodiego Aug 25, 2022
d142246
await but different
sampaiodiego Aug 25, 2022
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
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/imports/server/rest/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
findChatHistory,
searchChats,
findVisitorsToAutocomplete,
findVisitorsByEmailOrPhoneOrNameOrUsername,
findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField,
} from '../../../server/api/lib/visitors';

API.v1.addRoute(
Expand Down Expand Up @@ -144,7 +144,7 @@ API.v1.addRoute(
const nameOrUsername = new RegExp(escapeRegExp(term), 'i');

return API.v1.success(
await findVisitorsByEmailOrPhoneOrNameOrUsername({
await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({
userId: this.userId,
emailOrPhone: term,
nameOrUsername,
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/lib/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function findRooms({
const departmentsIds = [...new Set(rooms.map((room) => room.departmentId).filter(Boolean))];
if (departmentsIds.length) {
const departments = await LivechatDepartment.findInIds(departmentsIds, {
fields: { name: 1 },
projection: { name: 1 },
}).toArray();

rooms.forEach((room) => {
Expand Down
39 changes: 25 additions & 14 deletions apps/meteor/app/livechat/server/api/lib/visitors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LivechatVisitors, Messages, LivechatRooms } from '@rocket.chat/models';
import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models';

import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom';

Expand Down Expand Up @@ -129,20 +129,31 @@ export async function findVisitorsToAutocomplete({ selector }) {
};
}

export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ emailOrPhone, nameOrUsername, pagination: { offset, count, sort } }) {
const { cursor, totalCount } = LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername(emailOrPhone, nameOrUsername, {
sort: sort || { ts: -1 },
skip: offset,
limit: count,
projection: {
username: 1,
name: 1,
phone: 1,
livechatData: 1,
visitorEmails: 1,
lastChat: 1,
export async function findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({
emailOrPhone,
nameOrUsername,
pagination: { offset, count, sort },
}) {
const allowedCF = LivechatCustomField.findMatchingCustomFields('visitor', true, { projection: { _id: 1 } });

const { cursor, totalCount } = await LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField(
emailOrPhone,
nameOrUsername,
{
sort: sort || { ts: -1 },
skip: offset,
limit: count,
projection: {
username: 1,
name: 1,
phone: 1,
livechatData: 1,
visitorEmails: 1,
lastChat: 1,
},
},
});
(await allowedCF).map((cf) => cf._id),
);

const [visitors, total] = await Promise.all([cursor.toArray(), totalCount]);

Expand Down
29 changes: 15 additions & 14 deletions apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { LivechatVisitors } from '@rocket.chat/models';
import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models';

import { API } from '../../../../api/server';
import { Contacts } from '../../lib/Contacts';
Expand Down Expand Up @@ -47,23 +47,24 @@ API.v1.addRoute(
check(this.queryParams, {
email: Match.Maybe(String),
phone: Match.Maybe(String),
custom: Match.Maybe(String),
});
const { email, phone, custom } = this.queryParams;

const { email, phone } = this.queryParams;

if (!email && !phone) {
if (!email && !phone && !custom) {
throw new Meteor.Error('error-invalid-params');
}

const query = Object.assign(
{},
{
...(email && { visitorEmails: { address: email } }),
...(phone && { phone: { phoneNumber: phone } }),
},
);

const contact = await LivechatVisitors.findOne(query);
let foundCF = {};
if (custom) {
const customObj = Object.fromEntries(Array.from(new URLSearchParams(custom)));
foundCF = Object.fromEntries(
cauefcr marked this conversation as resolved.
Show resolved Hide resolved
(await LivechatCustomField.findMatchingCustomFieldsNames('visitor', true, Object.keys(customObj))).map((id: string) => [
id,
customObj[id],
]),
);
}
const contact = await LivechatVisitors.findOneByEmailAndPhoneAndCustomField(email, phone, foundCF);
return API.v1.success({ contact });
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Meteor.methods({
scope: String,
visibility: String,
regexp: String,
searchable: Boolean,
}),
);

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class LivechatRooms extends Base {
},
},
);
this.tryEnsureIndex({ 'livechatData.$**': 1 });
}

findOneByIdOrName(_idOrName, options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import React, { useMemo } from 'react';
const CustomFieldsForm = ({ values = {}, handlers = {}, className }) => {
const t = useTranslation();

const { id, field, label, scope, visibility, regexp } = values;
const { id, field, label, scope, visibility, searchable, regexp } = values;

const { handleField, handleLabel, handleScope, handleVisibility, handleRegexp } = handlers;
const { handleField, handleLabel, handleScope, handleVisibility, handleSearchable, handleRegexp } = handlers;

const scopeOptions = useMemo(
() => [
Expand Down Expand Up @@ -45,6 +45,14 @@ const CustomFieldsForm = ({ values = {}, handlers = {}, className }) => {
</Field.Row>
</Box>
</Field>
<Field className={className}>
<Box display='flex' flexDirection='row'>
<Field.Label htmlFor='searchable'>{t('Searchable')}</Field.Label>
<Field.Row>
<ToggleSwitch id='searchable' checked={searchable} onChange={handleSearchable} />
</Field.Row>
</Box>
</Field>
<Field className={className}>
<Field.Label>{t('Validation')}</Field.Label>
<Field.Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
const useQuery = ({ text, itemsPerPage, current }, [column, direction]) =>
useMemo(
() => ({
fields: JSON.stringify({ label: 1 }),
text,
sort: JSON.stringify({ [column]: sortDir(direction) }),
...(itemsPerPage && { count: itemsPerPage }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const getInitialValues = (cf) => ({
label: cf.label,
scope: cf.scope,
visibility: cf.visibility === 'visible',
searchable: !!cf.searchable,
regexp: cf.regexp,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const initialValues = {
scope: 'visitor',
visibility: true,
regexp: '',
searchable: true,
};

const NewCustomFieldsPage = ({ reload }) => {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4123,6 +4123,7 @@
"Script": "Script",
"Script_Enabled": "Script Enabled",
"Search": "Search",
"Searchable": "Searchable",
"Search_Apps": "Search Apps",
"Search_by_file_name": "Search by file name",
"Search_by_username": "Search by username",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3819,6 +3819,7 @@
"Screen_Share": "Compartilhamento de tela",
"Script_Enabled": "Script ativado",
"Search": "Pesquisar",
"Searchable": "Pesquisável",
"Search_Apps": "Pesquisar aplicativos",
"Search_by_file_name": "Pesquisar por nome de arquivo",
"Search_by_username": "Pesquisar por nome de usuário",
Expand Down
21 changes: 21 additions & 0 deletions apps/meteor/server/models/raw/LivechatCustomField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ export class LivechatCustomFieldRaw extends BaseRaw<ILivechatCustomField> implem
return this.find({ scope }, options || {});
}

async findMatchingCustomFields(
scope: ILivechatCustomField['scope'],
searchable = true,
options: FindOptions<ILivechatCustomField> = {},
extraFilter: { [key: string]: string | string[] | { [key: string]: string | string[] } } = {},
): Promise<ILivechatCustomField[]> {
const query = {
scope,
searchable,
...extraFilter,
};

return this.find(query, options).toArray();
}

async findMatchingCustomFieldsNames(scope: ILivechatCustomField['scope'], searchable = true, names: string[]) {
return (await this.findMatchingCustomFields(scope, searchable, { projection: { _id: 1 } }, { _id: { $in: names } })).map(
({ _id }) => _id,
);
}

async createOrUpdateCustomField(
_id: string,
field: string,
Expand Down
33 changes: 31 additions & 2 deletions apps/meteor/server/models/raw/LivechatVisitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL
{ key: { name: 1 }, sparse: true },
{ key: { username: 1 } },
{ key: { 'contactMananger.username': 1 }, sparse: true },
{ key: { 'livechatData.$**': 1 } },
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
];
}

Expand Down Expand Up @@ -158,11 +159,12 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL
/**
* Find visitors by their email or phone or username or name
*/
findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername(
async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField(
emailOrPhone: string,
nameOrUsername: RegExp,
options: FindOptions<ILivechatVisitor>,
): FindPaginated<FindCursor<ILivechatVisitor>> {
allowedCustomFields: string[] = [],
): Promise<FindPaginated<FindCursor<ILivechatVisitor>>> {
const query = {
$or: [
{
Expand All @@ -177,12 +179,39 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL
{
username: nameOrUsername,
},
// nameorusername is a clean regex, so we should be good

...allowedCustomFields.map((c: string) => ({ [`livechatData.${c}`]: nameOrUsername })),
],
};

return this.findPaginated(query, options);
}

async findOneByEmailAndPhoneAndCustomField(
email: string | null | undefined,
phone: string | null | undefined,
customFields?: { [key: string]: string },
): Promise<ILivechatVisitor | null> {
const query = Object.assign(
{},
{
...(email && { visitorEmails: { address: email } }),
...(phone && { phone: { phoneNumber: phone } }),
...(customFields &&
Object.keys(customFields).length &&
Object.fromEntries(
Object.keys(customFields).map((fieldName: string) => [
`livechatData.${fieldName}`,
new RegExp(escapeRegExp(customFields[fieldName]), 'i'),
]),
)),
},
);

return this.findOne(query);
}

async updateLivechatDataByToken(
token: string,
key: string,
Expand Down
52 changes: 52 additions & 0 deletions apps/meteor/tests/data/livechat/custom-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Response } from 'supertest';
import { ILivechatCustomField } from '@rocket.chat/core-typings';
import { credentials, request, methodCall, api } from './../api-data';

export const createCustomField = (customField: ILivechatCustomField) => new Promise((resolve, reject) => {
request
.get(api('livechat/custom-fields/'+customField.label))
.set(credentials)
.send()
.end((err: Error, res:Response) => {
if (err) {
reject(err);
} else {
if (res.body.customField != null && res.body.customField != undefined) {
resolve(res.body.customField);
}else{
request
.post(methodCall('livechat:saveCustomField'))
.send({
message: JSON.stringify({
method: 'livechat:saveCustomField',
params: [null,customField],
id: 'id',
msg: 'method',
}),
})
.set(credentials)
.end((err: Error, res: Response): void => {
if (err) {
return reject(err);
}
resolve(res.body);
});
}
}
});

});

export const deleteCustomField = (customFieldID: string) => new Promise((resolve, reject) => {
request
.post(methodCall('livechat:saveCustomField'))
.send(customFieldID)
.set(credentials)
.end((err: Error, res: Response): void => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});

Loading