@webitel/api-services 26.8.42 → 26.8.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/api/clients/cases/_internals/types.ts +11 -0
  3. package/src/api/clients/cases/cases.ts +54 -5
  4. package/src/api/clients//321/201ontacts/_internals/__tests__/stringifyContactExtensionFilters.spec.ts +80 -0
  5. package/src/api/clients//321/201ontacts/_internals/stringifyContactExtensionFilters.ts +88 -0
  6. package/src/api/clients//321/201ontacts/contacts.ts +27 -14
  7. package/src/api/transformers/translateError/__tests__/translateError.spec.ts +38 -0
  8. package/src/api/transformers/translateError/translateError.transformer.ts +17 -5
  9. package/src/locale/en/en.ts +8 -0
  10. package/src/locale/es/es.ts +8 -0
  11. package/src/locale/kz/kz.ts +8 -0
  12. package/src/locale/pl/pl.ts +8 -0
  13. package/src/locale/ro/ro.ts +8 -0
  14. package/src/locale/ru/ru.ts +8 -0
  15. package/src/locale/uk/uk.ts +8 -0
  16. package/src/locale/uz/uz.ts +8 -0
  17. package/src/locale/vi/vi.ts +8 -0
  18. package/types/.tsbuildinfo +1 -1
  19. package/types/api/clients/cases/_internals/types.d.ts +9 -0
  20. package/types/api/clients/cases/cases.d.ts +4 -2
  21. package/types/api/clients//321/201ontacts/_internals/stringifyContactExtensionFilters.d.ts +3 -0
  22. package/types/api/transformers/translateError/translateError.transformer.d.ts +6 -4
  23. package/types/locale/en/en.d.ts +7 -0
  24. package/types/locale/es/es.d.ts +7 -0
  25. package/types/locale/index.d.ts +63 -0
  26. package/types/locale/kz/kz.d.ts +7 -0
  27. package/types/locale/pl/pl.d.ts +7 -0
  28. package/types/locale/ro/ro.d.ts +7 -0
  29. package/types/locale/ru/ru.d.ts +7 -0
  30. package/types/locale/uk/uk.d.ts +7 -0
  31. package/types/locale/uz/uz.d.ts +7 -0
  32. package/types/locale/vi/vi.d.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/api-services",
3
- "version": "26.8.42",
3
+ "version": "26.8.44",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,11 @@
1
+ import type { LocateCaseNeighborDirection } from '@webitel/api-services/gen/models';
2
+ import type { ApiParams, GetItemParams } from '../../_shared/types';
3
+
4
+ export interface GetCaseParams extends GetItemParams {
5
+ listParams?: ApiParams;
6
+ }
7
+
8
+ export interface GetCaseNeighborParams extends GetItemParams {
9
+ direction: LocateCaseNeighborDirection;
10
+ listParams: ApiParams;
11
+ }
@@ -21,10 +21,10 @@ import type {
21
21
  ApiId,
22
22
  ApiParams,
23
23
  DeleteItemParams,
24
- GetItemParams,
25
24
  } from '../_shared/types';
26
25
  import { FTSServiceAPI } from './_internals/ftsService';
27
26
  import { stringifyCaseFilters } from './_internals/stringifyCaseFilters';
27
+ import type { GetCaseNeighborParams, GetCaseParams } from './_internals/types';
28
28
 
29
29
  const casesService = getCases();
30
30
 
@@ -174,11 +174,36 @@ const caseFieldsToSend = [
174
174
  'custom',
175
175
  ];
176
176
 
177
- const getCase = async ({ itemId: id }: GetItemParams) => {
177
+ const caseNeighborFields = [
178
+ 'has_prev',
179
+ 'has_next',
180
+ ];
181
+
182
+ const getCaseListContext = ({ sort, ...filters }: ApiParams) => ({
183
+ q: filters.search,
184
+ sort,
185
+ filters: stringifyCaseFilters(filters),
186
+ });
187
+
188
+ const getLocateCaseParams = (listParams?: ApiParams) =>
189
+ listParams
190
+ ? {
191
+ ...getCaseListContext(listParams),
192
+ fields: [
193
+ ...caseFieldsToSend,
194
+ ...caseNeighborFields,
195
+ ],
196
+ }
197
+ : {
198
+ fields: caseFieldsToSend,
199
+ };
200
+
201
+ const getCase = async ({ itemId: id, listParams }: GetCaseParams) => {
178
202
  try {
179
- const response = await casesService.locateCase(String(id), {
180
- fields: caseFieldsToSend,
181
- });
203
+ const response = await casesService.locateCase(
204
+ String(id),
205
+ getLocateCaseParams(listParams),
206
+ );
182
207
  return applyTransform(response.data, [
183
208
  snakeToCamel([
184
209
  'custom',
@@ -193,6 +218,29 @@ const getCase = async ({ itemId: id }: GetItemParams) => {
193
218
  }
194
219
  };
195
220
 
221
+ const getCaseNeighbor = async ({
222
+ itemId: id,
223
+ direction,
224
+ listParams,
225
+ }: GetCaseNeighborParams) => {
226
+ try {
227
+ const response = await casesService.locateCaseNeighbor(String(id), {
228
+ ...getCaseListContext(listParams),
229
+ direction,
230
+ fields: [
231
+ 'id',
232
+ ],
233
+ });
234
+ return applyTransform(response.data, [
235
+ snakeToCamel(),
236
+ ]);
237
+ } catch (err) {
238
+ throw applyTransform(err, [
239
+ notify,
240
+ ]);
241
+ }
242
+ };
243
+
196
244
  const deleteCase = async ({ id }: DeleteItemParams) => {
197
245
  try {
198
246
  const response = await casesService.deleteCase(String(id));
@@ -364,6 +412,7 @@ export const CasesAPI = {
364
412
  getList: getCasesList,
365
413
  getLookup: getCasesLookup,
366
414
  get: getCase,
415
+ getNeighbor: getCaseNeighbor,
367
416
  delete: deleteCase,
368
417
  update: updateCase,
369
418
  add: addCase,
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { DataField } from '../../../../../gen/_models';
4
+ import { stringifyContactExtensionFilters } from '../stringifyContactExtensionFilters';
5
+
6
+ const fields: DataField[] = [
7
+ {
8
+ id: 'tee',
9
+ kind: 'string',
10
+ },
11
+ {
12
+ id: 'test',
13
+ kind: 'int32',
14
+ },
15
+ {
16
+ id: 'my_bool',
17
+ kind: 'bool',
18
+ },
19
+ {
20
+ id: 'ca',
21
+ kind: 'datetime',
22
+ },
23
+ {
24
+ id: 'my_priority',
25
+ kind: 'lookup',
26
+ },
27
+ {
28
+ id: 'tags',
29
+ kind: 'list',
30
+ },
31
+ ];
32
+
33
+ describe('stringifyContactExtensionFilters', () => {
34
+ it('builds a CEL expression by field kind', () => {
35
+ expect(
36
+ stringifyContactExtensionFilters(
37
+ {
38
+ tee: '123',
39
+ test: 42,
40
+ my_bool: false,
41
+ ca: {
42
+ from: 1700000000000,
43
+ },
44
+ my_priority: '1',
45
+ tags: [
46
+ '1',
47
+ '2',
48
+ ],
49
+ },
50
+ fields,
51
+ ),
52
+ ).toBe(
53
+ 'tee == "123" && test == 42 && my_bool == false && ca >= 1700000000000 && my_priority.id == "1" && (tags.exists(x, x.id == "1") || tags.exists(x, x.id == "2"))',
54
+ );
55
+ });
56
+
57
+ it('skips a list filter with no selected items', () => {
58
+ expect(
59
+ stringifyContactExtensionFilters(
60
+ {
61
+ tee: '123',
62
+ tags: [],
63
+ },
64
+ fields,
65
+ ),
66
+ ).toBe('tee == "123"');
67
+ });
68
+
69
+ it('returns undefined when no extension filter is applied', () => {
70
+ expect(
71
+ stringifyContactExtensionFilters(
72
+ {
73
+ page: 1,
74
+ tee: null,
75
+ },
76
+ fields,
77
+ ),
78
+ ).toBeUndefined();
79
+ });
80
+ });
@@ -0,0 +1,88 @@
1
+ import type { DataField } from '../../../../gen/_models';
2
+ import { TypeKind } from '../../../../gen/_models/typeKind';
3
+ import {
4
+ type DatetimeRangeValue,
5
+ type NormalizeDatetimeValueParam,
6
+ normalizeDatetimeRange,
7
+ } from '../../../../scripts';
8
+ import type { ApiParams } from '../../_shared/types';
9
+
10
+ type ScalarFilterValue = string | number | boolean;
11
+
12
+ type LookupFilterValue =
13
+ | string
14
+ | number
15
+ | {
16
+ id: string | number;
17
+ };
18
+
19
+ type DatetimeFilterValue = NormalizeDatetimeValueParam | DatetimeRangeValue;
20
+
21
+ type ExtensionFilterValue =
22
+ | ScalarFilterValue
23
+ | LookupFilterValue
24
+ | LookupFilterValue[]
25
+ | DatetimeFilterValue;
26
+
27
+ const quote = (value: ScalarFilterValue) => JSON.stringify(String(value));
28
+
29
+ const lookupId = (value: LookupFilterValue) =>
30
+ typeof value === 'object' ? value.id : value;
31
+
32
+ const toListCondition = (
33
+ id: string,
34
+ value: LookupFilterValue | LookupFilterValue[],
35
+ ) => {
36
+ const items = [
37
+ value,
38
+ ].flat();
39
+ if (!items.length) return '';
40
+
41
+ const conditions = items.map(
42
+ (item) => `${id}.exists(x, x.id == ${quote(lookupId(item))})`,
43
+ );
44
+ return `(${conditions.join(' || ')})`;
45
+ };
46
+
47
+ const toDatetimeCondition = (id: string, value: DatetimeFilterValue) => {
48
+ const { from, to } = normalizeDatetimeRange(value) ?? {};
49
+ return [
50
+ from && `${id} >= ${from}`,
51
+ to && `${id} <= ${to}`,
52
+ ]
53
+ .filter(Boolean)
54
+ .join(' && ');
55
+ };
56
+
57
+ const toCondition = (
58
+ id: string,
59
+ kind: DataField['kind'],
60
+ value: ExtensionFilterValue,
61
+ ) => {
62
+ switch (kind) {
63
+ case TypeKind.String:
64
+ case TypeKind.Richtext:
65
+ return `${id} == ${quote(value as ScalarFilterValue)}`;
66
+ case TypeKind.Lookup:
67
+ return `${id}.id == ${quote(lookupId(value as LookupFilterValue))}`;
68
+ case TypeKind.List:
69
+ return toListCondition(
70
+ id,
71
+ value as LookupFilterValue | LookupFilterValue[],
72
+ );
73
+ case TypeKind.Datetime:
74
+ return toDatetimeCondition(id, value as DatetimeFilterValue);
75
+ default:
76
+ return `${id} == ${value}`;
77
+ }
78
+ };
79
+
80
+ export const stringifyContactExtensionFilters = (
81
+ params: ApiParams,
82
+ extensionFields: DataField[] = [],
83
+ ) =>
84
+ extensionFields
85
+ .filter(({ id = '' }) => params[id] != null)
86
+ .map(({ id = '', kind }) => toCondition(id, kind, params[id]))
87
+ .filter(Boolean)
88
+ .join(' && ') || undefined;
@@ -12,6 +12,7 @@ import {
12
12
  notify,
13
13
  sanitizeToWire,
14
14
  snakeToCamel,
15
+ translateError,
15
16
  } from '../../transformers';
16
17
  import { generatePermissionsApi } from '../_shared/generatePermissionsApi';
17
18
  import type {
@@ -20,6 +21,7 @@ import type {
20
21
  DeleteItemParams,
21
22
  GetItemParams,
22
23
  } from '../_shared/types';
24
+ import { stringifyContactExtensionFilters } from './_internals/stringifyContactExtensionFilters';
23
25
  import { ContactsSearchMode } from './enums/ContactsSearchMode';
24
26
 
25
27
  const baseUrl = '/contacts';
@@ -118,20 +120,30 @@ const getList = async (params: ApiParams) => {
118
120
  ]);
119
121
 
120
122
  try {
121
- const response = await getContacts().searchContacts({
122
- ...searchParams,
123
- sort: searchParams.sort
124
- ? [
125
- searchParams.sort,
126
- ]
127
- : [
128
- '+name',
129
- ],
130
- fields: [
131
- 'mode',
132
- ...(searchParams.fields || []),
133
- ],
134
- });
123
+ const response = await getContacts().searchContacts(
124
+ {
125
+ ...searchParams,
126
+ sort: searchParams.sort
127
+ ? [
128
+ searchParams.sort,
129
+ ]
130
+ : [
131
+ '+name',
132
+ ],
133
+ fields: [
134
+ 'mode',
135
+ ...(searchParams.fields || []),
136
+ ],
137
+ },
138
+ {
139
+ params: {
140
+ filters: stringifyContactExtensionFilters(
141
+ params,
142
+ params.extensionFields,
143
+ ),
144
+ },
145
+ },
146
+ );
135
147
 
136
148
  const { items, next } = applyTransform(
137
149
  {
@@ -152,6 +164,7 @@ const getList = async (params: ApiParams) => {
152
164
  };
153
165
  } catch (err) {
154
166
  throw applyTransform(err, [
167
+ translateError,
155
168
  notify,
156
169
  ]);
157
170
  }
@@ -0,0 +1,38 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { createI18n } from 'vue-i18n';
3
+
4
+ import { config, setConfig } from '../../../../config/config';
5
+ import translateError from '../translateError.transformer';
6
+
7
+ const reservedFieldError = () => ({
8
+ response: {
9
+ data: {
10
+ id: 'contacts.search.filters.reserved_field',
11
+ detail:
12
+ "contacts: field 'list' has a reserved name and cannot be used in filters; rename it",
13
+ } as {
14
+ id: string;
15
+ detail: string;
16
+ translation?: string;
17
+ },
18
+ },
19
+ });
20
+
21
+ describe('translateError', () => {
22
+ afterEach(() => {
23
+ config.i18n = null;
24
+ });
25
+
26
+ it('translates contacts reserved field error with the field code', () => {
27
+ setConfig({
28
+ i18n: createI18n({
29
+ legacy: false,
30
+ locale: 'uk',
31
+ }),
32
+ });
33
+
34
+ expect(translateError(reservedFieldError()).response.data.translation).toBe(
35
+ 'Поле "list" має зарезервоване ім\'я. Перейменуйте його в розділі Персоналізація, щоб увімкнути фільтрацію',
36
+ );
37
+ });
38
+ });
@@ -9,15 +9,27 @@ const BACKEND_ERRORS_PREFIX = 'backendErrors';
9
9
  // Converts snake_case error IDs from backend to camelCase keys
10
10
  // Looks up translations in api-services locale files under 'backendErrors' prefix
11
11
  // Adds the translated message to err.response.data.translation for use by notify transformer
12
+ interface TranslatableErrorData {
13
+ id?: string;
14
+ detail?: string;
15
+ translation?: string;
16
+ }
17
+
12
18
  interface TranslatableError {
13
19
  response?: {
14
- data?: {
15
- id?: string;
16
- translation?: string;
17
- };
20
+ data?: TranslatableErrorData;
18
21
  };
19
22
  }
20
23
 
24
+ const errorTranslationParams: Record<
25
+ string,
26
+ (data: TranslatableErrorData) => Record<string, unknown>
27
+ > = {
28
+ 'contacts.search.filters.reserved_field': ({ detail }) => ({
29
+ field: detail?.match(/'([^']+)'/)?.[1],
30
+ }),
31
+ };
32
+
21
33
  const translateError = <T extends TranslatableError>(err: T): T => {
22
34
  const errorId = err?.response?.data?.id;
23
35
  if (!errorId) return err;
@@ -33,7 +45,7 @@ const translateError = <T extends TranslatableError>(err: T): T => {
33
45
  // unify, so narrow to the composition-API Composer we run with.
34
46
  const translation = (i18n.global as Composer).t(
35
47
  fullKey,
36
- {},
48
+ errorTranslationParams[errorId]?.(err.response?.data ?? {}) ?? {},
37
49
  {
38
50
  missingWarn: false,
39
51
  fallbackWarn: false,
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" is a reserved name. Rename the field in Personalization to enable filtering',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" es un nombre reservado. Cambie el nombre del campo en Personalización para habilitar el filtrado',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" — резервтелген атау. Сүзгілеуді қосу үшін өрісті Жекелендіру бөлімінде қайта атаңыз',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" to zarezerwowana nazwa. Zmień nazwę pola w sekcji Personalizacja, aby włączyć filtrowanie',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" este un nume rezervat. Redenumiți câmpul în Personalizare pentru a activa filtrarea',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ 'Поле "{field}" имеет зарезервированное имя. Переименуйте его в разделе Персонализация, чтобы включить фильтрацию',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ 'Поле "{field}" має зарезервоване ім\'я. Перейменуйте його в розділі Персоналізація, щоб увімкнути фільтрацію',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" zaxiralangan nom. Filtrlashni yoqish uchun maydonni Shaxsiylashtirish bo\'limida qayta nomlang',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {
@@ -2,6 +2,14 @@ import type { MessageContext } from 'vue-i18n';
2
2
 
3
3
  export default {
4
4
  backendErrors: {
5
+ contacts: {
6
+ search: {
7
+ filters: {
8
+ reservedField:
9
+ '"{field}" là tên dành riêng. Hãy đổi tên trường trong mục Cá nhân hóa để bật lọc',
10
+ },
11
+ },
12
+ },
5
13
  app: {
6
14
  auditForm: {
7
15
  isValid: {