@webitel/api-services 26.8.42 → 26.8.43
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.
- package/package.json +1 -1
- package/src/api/clients//321/201ontacts/_internals/__tests__/stringifyContactExtensionFilters.spec.ts +80 -0
- package/src/api/clients//321/201ontacts/_internals/stringifyContactExtensionFilters.ts +88 -0
- package/src/api/clients//321/201ontacts/contacts.ts +27 -14
- package/src/api/transformers/translateError/__tests__/translateError.spec.ts +38 -0
- package/src/api/transformers/translateError/translateError.transformer.ts +17 -5
- package/src/locale/en/en.ts +8 -0
- package/src/locale/es/es.ts +8 -0
- package/src/locale/kz/kz.ts +8 -0
- package/src/locale/pl/pl.ts +8 -0
- package/src/locale/ro/ro.ts +8 -0
- package/src/locale/ru/ru.ts +8 -0
- package/src/locale/uk/uk.ts +8 -0
- package/src/locale/uz/uz.ts +8 -0
- package/src/locale/vi/vi.ts +8 -0
- package/types/.tsbuildinfo +1 -1
- package/types/api/clients//321/201ontacts/_internals/stringifyContactExtensionFilters.d.ts +3 -0
- package/types/api/transformers/translateError/translateError.transformer.d.ts +6 -4
- package/types/locale/en/en.d.ts +7 -0
- package/types/locale/es/es.d.ts +7 -0
- package/types/locale/index.d.ts +63 -0
- package/types/locale/kz/kz.d.ts +7 -0
- package/types/locale/pl/pl.d.ts +7 -0
- package/types/locale/ro/ro.d.ts +7 -0
- package/types/locale/ru/ru.d.ts +7 -0
- package/types/locale/uk/uk.d.ts +7 -0
- package/types/locale/uz/uz.d.ts +7 -0
- package/types/locale/vi/vi.d.ts +7 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,
|
package/src/locale/en/en.ts
CHANGED
|
@@ -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: {
|
package/src/locale/es/es.ts
CHANGED
|
@@ -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: {
|
package/src/locale/kz/kz.ts
CHANGED
|
@@ -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: {
|
package/src/locale/pl/pl.ts
CHANGED
|
@@ -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: {
|
package/src/locale/ro/ro.ts
CHANGED
|
@@ -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: {
|
package/src/locale/ru/ru.ts
CHANGED
|
@@ -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: {
|
package/src/locale/uk/uk.ts
CHANGED
|
@@ -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: {
|
package/src/locale/uz/uz.ts
CHANGED
|
@@ -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: {
|
package/src/locale/vi/vi.ts
CHANGED
|
@@ -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: {
|