Skip to content

Commit bf7a4ff

Browse files
authored
Merge pull request #278 from zendesk/victor.piolin/views
feat: Implement zendesk views in toolbox
2 parents 0e50f22 + 1ec72ca commit bf7a4ff

5 files changed

Lines changed: 366 additions & 2 deletions

File tree

__tests__/services/zendesk-api-service.spec.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,4 +977,194 @@ describe("ZendeskService", () => {
977977
expect(requestMock).toHaveBeenCalledTimes(1);
978978
});
979979
});
980+
981+
describe("getActiveViews", () => {
982+
const viewSample = {
983+
active: true,
984+
conditions: {
985+
group_id: 1
986+
},
987+
created_at: "2023-01-01T00:00:00Z",
988+
default: false,
989+
description: "Test View",
990+
execution: {
991+
group_by: "group_by",
992+
sort_by: "sort_by",
993+
group_order: "asc",
994+
sort_order: "asc"
995+
}
996+
};
997+
998+
it("should fetch active views with the correct data", async () => {
999+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1000+
1001+
const result = await service.getActiveViews();
1002+
1003+
expect(requestMock).toHaveBeenCalledWith({
1004+
url: `/api/v2/views/active`
1005+
});
1006+
expect(result).toEqual([viewSample]);
1007+
});
1008+
});
1009+
1010+
describe("getViews", () => {
1011+
const viewSample = {
1012+
active: true,
1013+
conditions: {
1014+
group_id: 1
1015+
},
1016+
created_at: "2023-01-01T00:00:00Z",
1017+
default: false,
1018+
description: "Test View",
1019+
execution: {
1020+
group_by: "group_by",
1021+
sort_by: "sort_by",
1022+
group_order: "asc",
1023+
sort_order: "asc"
1024+
}
1025+
};
1026+
1027+
it("should fetch views with the correct data", async () => {
1028+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1029+
1030+
const result = await service.getViews();
1031+
1032+
expect(requestMock).toHaveBeenCalledWith({
1033+
url: `/api/v2/views`
1034+
});
1035+
expect(result).toEqual([viewSample]);
1036+
});
1037+
});
1038+
1039+
describe("searchViews", () => {
1040+
const viewSample = {
1041+
active: true,
1042+
conditions: {},
1043+
created_at: "2023-01-01T00:00:00Z",
1044+
default: false,
1045+
description: "View for recent tickets",
1046+
execution: {},
1047+
id: 25,
1048+
position: 3,
1049+
restriction: {},
1050+
title: "Tickets updated less than 12 Hours",
1051+
updated_at: "2023-01-01T00:00:00Z"
1052+
};
1053+
1054+
const inactiveViewSample = {
1055+
active: false,
1056+
conditions: {},
1057+
created_at: "2023-01-01T00:00:00Z",
1058+
default: false,
1059+
description: "View for tickets that are not assigned",
1060+
execution: {},
1061+
id: 23,
1062+
position: 7,
1063+
restriction: {},
1064+
title: "Unassigned tickets",
1065+
updated_at: "2023-01-01T00:00:00Z"
1066+
};
1067+
1068+
it("should search views with query string", async () => {
1069+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1070+
1071+
const result = await service.searchViews("tickets");
1072+
1073+
expect(requestMock).toHaveBeenCalledWith({
1074+
url: `/api/v2/views/search?query=tickets`
1075+
});
1076+
expect(result).toEqual([viewSample]);
1077+
});
1078+
1079+
it("should search views with query string and access filter", async () => {
1080+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1081+
1082+
const result = await service.searchViews("tickets", { access: "shared" });
1083+
1084+
expect(requestMock).toHaveBeenCalledWith({
1085+
url: `/api/v2/views/search?query=tickets&access=shared`
1086+
});
1087+
expect(result).toEqual([viewSample]);
1088+
});
1089+
1090+
it("should search views with query string and active filter", async () => {
1091+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1092+
1093+
const result = await service.searchViews("tickets", { active: true });
1094+
1095+
expect(requestMock).toHaveBeenCalledWith({
1096+
url: `/api/v2/views/search?query=tickets&active=true`
1097+
});
1098+
expect(result).toEqual([viewSample]);
1099+
});
1100+
1101+
it("should search views with query string and group_id filter", async () => {
1102+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1103+
1104+
const result = await service.searchViews("tickets", { group_id: 123 });
1105+
1106+
expect(requestMock).toHaveBeenCalledWith({
1107+
url: `/api/v2/views/search?query=tickets&group_id=123`
1108+
});
1109+
expect(result).toEqual([viewSample]);
1110+
});
1111+
1112+
it("should search views with query string and sort parameters", async () => {
1113+
requestMock.mockResolvedValueOnce({ views: [viewSample] });
1114+
1115+
const result = await service.searchViews("tickets", { sort_by: "alphabetical", sort_order: "asc" });
1116+
1117+
expect(requestMock).toHaveBeenCalledWith({
1118+
url: `/api/v2/views/search?query=tickets&sort_by=alphabetical&sort_order=asc`
1119+
});
1120+
expect(result).toEqual([viewSample]);
1121+
});
1122+
1123+
it("should search views with all options", async () => {
1124+
requestMock.mockResolvedValueOnce({ views: [viewSample, inactiveViewSample] });
1125+
1126+
const result = await service.searchViews("tickets", {
1127+
access: "personal",
1128+
active: false,
1129+
group_id: 456,
1130+
include: "sideload",
1131+
sort_by: "updated_at",
1132+
sort_order: "desc"
1133+
});
1134+
1135+
expect(requestMock).toHaveBeenCalledWith({
1136+
url: `/api/v2/views/search?query=tickets&access=personal&active=false&group_id=456&include=sideload&sort_by=updated_at&sort_order=desc`
1137+
});
1138+
expect(result).toEqual([viewSample, inactiveViewSample]);
1139+
});
1140+
1141+
it("should continue calling the API until next_page disappears", async () => {
1142+
requestMock
1143+
.mockResolvedValueOnce({ views: [viewSample], next_page: "next_page" })
1144+
.mockResolvedValueOnce({ views: [inactiveViewSample] });
1145+
1146+
const result = await service.searchViews("tickets");
1147+
1148+
expect(requestMock).toHaveBeenCalledTimes(2);
1149+
expect(requestMock).toHaveBeenNthCalledWith(1, {
1150+
url: `/api/v2/views/search?query=tickets`
1151+
});
1152+
expect(requestMock).toHaveBeenNthCalledWith(2, {
1153+
url: "next_page"
1154+
});
1155+
expect(result).toEqual([viewSample, inactiveViewSample]);
1156+
});
1157+
1158+
it("should only call the API one time when fetchAllViews is false", async () => {
1159+
requestMock.mockResolvedValueOnce({ views: [viewSample], next_page: "next_page" });
1160+
1161+
const result = await service.searchViews("tickets", undefined, false);
1162+
1163+
expect(requestMock).toHaveBeenCalledTimes(1);
1164+
expect(requestMock).toHaveBeenCalledWith({
1165+
url: `/api/v2/views/search?query=tickets`
1166+
});
1167+
expect(result).toEqual([viewSample]);
1168+
});
1169+
});
9801170
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zendesk/zaf-toolbox",
3-
"version": "1.3.1",
3+
"version": "1.4.0",
44
"description": "A toolbox for ZAF application built with 🩷 by Zendesk Labs",
55
"main": "lib/src/index.js",
66
"types": "lib/src/index.d.ts",

src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from "@models/whats-app-template";
99
export * from "@models/zendesk-user";
1010
export * from "@models/custom-objects";
1111
export * from "@models/zendesk-api";
12+
export * from "@models/zendesk-view";

src/models/zendesk-view.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { IZendeskResponse } from "./zendesk-api";
2+
3+
interface IViewCondition {
4+
field: string;
5+
operator: string;
6+
value: string | number | boolean;
7+
}
8+
9+
export interface IViewConditions {
10+
all?: IViewCondition[];
11+
// eslint-disable-next-line id-denylist
12+
any?: IViewCondition[];
13+
}
14+
15+
export interface IViewColumn {
16+
id: number | string;
17+
title: string;
18+
type: string;
19+
url?: string;
20+
}
21+
22+
export interface IViewGroup {
23+
id: string;
24+
title: string;
25+
order: "asc" | "desc";
26+
}
27+
28+
export interface IViewSort {
29+
id: string;
30+
title: string;
31+
order: "asc" | "desc";
32+
}
33+
34+
export interface IViewExecution {
35+
group_by?: string;
36+
sort_by?: string;
37+
group_order?: "asc" | "desc";
38+
sort_order?: "asc" | "desc";
39+
columns: IViewColumn[];
40+
group: IViewGroup;
41+
sort: IViewSort;
42+
}
43+
44+
export interface IViewRestriction {
45+
type: string;
46+
id: number;
47+
}
48+
49+
export interface IZendeskView {
50+
active: boolean;
51+
conditions: IViewConditions;
52+
created_at: string;
53+
default: boolean;
54+
description: string;
55+
execution: IViewExecution;
56+
id: number;
57+
position: number;
58+
restriction: IViewRestriction | null;
59+
title: string;
60+
updated_at: string;
61+
}
62+
63+
export interface IViewsResponse extends IZendeskResponse {
64+
views: IZendeskView[];
65+
}

src/services/zendesk-api-service.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ import {
3030
ICreateAccessTokenResponse,
3131
IZendeskTicket,
3232
IBulkJobResponse,
33-
ITicketsResults
33+
ITicketsResults,
34+
IZendeskView,
35+
IViewsResponse
3436
} from "@models/index";
37+
import { buildUrlParams } from "@utils/build-url-params";
3538
import {
3639
ICreateConnectionResponse,
3740
ICreateInboundWebhookResponse,
@@ -615,4 +618,109 @@ export class ZendeskApiService {
615618
data: JSON.stringify(bundle)
616619
});
617620
}
621+
622+
/**
623+
* VIEWS SECTION
624+
*/
625+
626+
/**
627+
* Fetch views with optional filters
628+
*
629+
* @param options Query parameters for filtering views
630+
* @param options.access Only views with given access. May be "personal", "shared", or "account"
631+
* @param options.active Only active views if true, inactive views if false
632+
* @param options.group_id Only views belonging to given group
633+
* @param options.sort The sort parameter used with cursor pagination. Defaults to "created_at". Prefix with '-' for descending order
634+
* @param options.sort_by The sort_by parameter used with offset pagination. Possible values are "alphabetical", "created_at", or "updated_at". Defaults to "position"
635+
* @param options.sort_order The sort_order parameter used with offset pagination. One of "asc" or "desc". Defaults to "asc" for alphabetical and position sort, "desc" for all others
636+
* @param fetchAllViews Whether to fetch all pages or just the first. Defaults to true
637+
* @returns Promise resolving to array of views
638+
*/
639+
public async getViews(
640+
options?: {
641+
access?: "personal" | "shared" | "account";
642+
active?: boolean;
643+
group_id?: number;
644+
sort?: string;
645+
sort_by?: "alphabetical" | "created_at" | "updated_at";
646+
sort_order?: "asc" | "desc";
647+
},
648+
fetchAllViews = true
649+
): Promise<IZendeskView[]> {
650+
const queryParams = options ? buildUrlParams(options) : "";
651+
const url = `/api/v2/views${queryParams ? `?${queryParams}` : ""}`;
652+
653+
return this.fetchAllPaginatedResults<IViewsResponse, IZendeskView>(
654+
url,
655+
fetchAllViews,
656+
(response) => response.views
657+
);
658+
}
659+
660+
/**
661+
* Fetch active views with optional filters
662+
*
663+
* @param options Query parameters for filtering active views
664+
* @param options.access Only views with given access. May be "personal", "shared", or "account"
665+
* @param options.group_id Only views belonging to given group
666+
* @param options.sort_by Possible values are "alphabetical", "created_at", or "updated_at". Defaults to "position"
667+
* @param options.sort_order One of "asc" or "desc". Defaults to "asc" for alphabetical and position sort, "desc" for all others
668+
* @param fetchAllViews Whether to fetch all pages or just the first. Defaults to true
669+
* @returns Promise resolving to array of active views
670+
*/
671+
public async getActiveViews(
672+
options?: {
673+
access?: "personal" | "shared" | "account";
674+
group_id?: number;
675+
sort_by?: "alphabetical" | "created_at" | "updated_at";
676+
sort_order?: "asc" | "desc";
677+
},
678+
fetchAllViews = true
679+
): Promise<IZendeskView[]> {
680+
const queryParams = options ? buildUrlParams(options) : "";
681+
const url = `/api/v2/views/active${queryParams ? `?${queryParams}` : ""}`;
682+
683+
return this.fetchAllPaginatedResults<IViewsResponse, IZendeskView>(
684+
url,
685+
fetchAllViews,
686+
(response) => response.views
687+
);
688+
}
689+
690+
/**
691+
* Search views by query string with optional filters
692+
*
693+
* @param query Query string used to find all views with matching title
694+
* @param options Query parameters for filtering search results
695+
* @param options.access Filter views by access. May be "personal", "shared", or "account"
696+
* @param options.active Filter by active views if true or inactive views if false
697+
* @param options.group_id Filter views by group
698+
* @param options.include A sideload to include in the response
699+
* @param options.sort_by Possible values are "alphabetical", "created_at", "updated_at", and "position". If unspecified, views are sorted by relevance
700+
* @param options.sort_order One of "asc" or "desc". Defaults to "asc" for alphabetical and position sort, "desc" for all others
701+
* @param fetchAllViews Whether to fetch all pages or just the first. Defaults to true
702+
* @returns Promise resolving to array of views matching the search query
703+
*/
704+
public async searchViews(
705+
query: string,
706+
options?: {
707+
access?: "personal" | "shared" | "account";
708+
active?: boolean;
709+
group_id?: number;
710+
include?: string;
711+
sort_by?: "alphabetical" | "created_at" | "updated_at" | "position";
712+
sort_order?: "asc" | "desc";
713+
},
714+
fetchAllViews = true
715+
): Promise<IZendeskView[]> {
716+
const params = { query, ...options };
717+
const queryParams = buildUrlParams(params);
718+
const url = `/api/v2/views/search?${queryParams}`;
719+
720+
return this.fetchAllPaginatedResults<IViewsResponse, IZendeskView>(
721+
url,
722+
fetchAllViews,
723+
(response) => response.views
724+
);
725+
}
618726
}

0 commit comments

Comments
 (0)