@myparcel-dev/pdk-admin 2.2.1 → 2.2.3
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/CHANGELOG.md +20 -0
- package/dist/forms/helpers/useFormCapabilities.d.ts +2 -2
- package/dist/forms/shipmentOptions/fieldFactoryRegistry.d.ts +2 -2
- package/dist/forms/shipmentOptions/fields/createInsuranceField.d.ts +1 -1
- package/dist/forms/shipmentOptions/wireProxyCapabilities.d.ts +2 -2
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/notificationBelongsToOrder.d.ts +6 -0
- package/package.json +2 -2
- package/src/actions/executors/executeHandler.spec.ts +99 -0
- package/src/actions/executors/executeHandler.ts +8 -1
- package/src/forms/helpers/useFormCapabilities.spec.ts +23 -11
- package/src/forms/helpers/useFormCapabilities.ts +8 -17
- package/src/forms/shipmentOptions/createShipmentOptionsForm.ts +1 -1
- package/src/forms/shipmentOptions/fieldFactoryRegistry.ts +2 -2
- package/src/forms/shipmentOptions/fields/createInsuranceField.ts +2 -2
- package/src/forms/shipmentOptions/wireProxyCapabilities.ts +2 -2
- package/src/services/actions/createActionContext.ts +3 -0
- package/src/services/createNotification.spec.ts +66 -0
- package/src/services/createNotification.ts +7 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/notificationBelongsToOrder.spec.ts +28 -0
- package/src/utils/notificationBelongsToOrder.ts +18 -0
- package/src/views/OrderListItem.vue +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- MONOWEAVE:BELOW -->
|
|
4
4
|
|
|
5
|
+
## [2.2.3](https://github.com/myparcelnl/js-pdk/compare/@myparcel-dev/pdk-admin@2.2.2...@myparcel-dev/pdk-admin@2.2.3) "@myparcel-dev/pdk-admin" (2026-09-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **notifications:** show action errors and match bulk order tags ([#432](https://github.com/myparcelnl/js-pdk/issues/432)) ([81a476e](https://github.com/myparcelnl/js-pdk/commit/81a476ea092bc3b0bcdc7da03715b5b6a82362bc)), closes [myparcelnl/pdk#528](https://github.com/myparcelnl/pdk/issues/528)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [2.2.2](https://github.com/myparcelnl/js-pdk/compare/@myparcel-dev/pdk-admin@2.2.1...@myparcel-dev/pdk-admin@2.2.2) "@myparcel-dev/pdk-admin" (2026-08-19)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **insurance:** read insurance limits from the flat capabilities format ([#383](https://github.com/myparcelnl/js-pdk/issues/383)) ([8794c46](https://github.com/myparcelnl/js-pdk/commit/8794c46ee0ffce36fdad9b3e7dc95e44ee56af23)), closes [myparcelnl/pdk#511](https://github.com/myparcelnl/pdk/issues/511)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
5
25
|
## [2.2.1](https://github.com/myparcelnl/js-pdk/compare/@myparcel-dev/pdk-admin@2.2.0...@myparcel-dev/pdk-admin@2.2.1) "@myparcel-dev/pdk-admin" (2026-08-07)
|
|
6
26
|
|
|
7
27
|
|
|
@@ -37,8 +37,8 @@ export type FormCapabilities = {
|
|
|
37
37
|
*/
|
|
38
38
|
hasShipmentOption: (form: FormInstance, option: string) => boolean;
|
|
39
39
|
/**
|
|
40
|
-
* Insurance amount bracket options derived from the currently selected carrier's
|
|
41
|
-
* `
|
|
40
|
+
* Insurance amount bracket options derived from the currently selected carrier's insurance
|
|
41
|
+
* limits (`min` / `max`). Amounts in the context are in cents; emitted values are strings
|
|
42
42
|
* representing cent amounts so they round-trip through form select inputs unchanged.
|
|
43
43
|
*/
|
|
44
44
|
getInsuranceOptions: (form: FormInstance, formatter: Formatter) => SelectOption[];
|
|
@@ -15,7 +15,7 @@ export type FieldFactory = (refs: ShipmentOptionsRefs, fieldName: string) => Int
|
|
|
15
15
|
* `carrier.options`) to its custom factory function.
|
|
16
16
|
*
|
|
17
17
|
* Current exceptions:
|
|
18
|
-
* - `insurance`: renders as a select/dropdown with amount brackets derived from
|
|
19
|
-
* `
|
|
18
|
+
* - `insurance`: renders as a select/dropdown with amount brackets derived from the
|
|
19
|
+
* `min` / `max` limits in the option data.
|
|
20
20
|
*/
|
|
21
21
|
export declare const fieldFactoryRegistry: Record<string, FieldFactory>;
|
|
@@ -4,7 +4,7 @@ import { type ShipmentOptionsRefs } from '../types';
|
|
|
4
4
|
* Custom field factory for the insurance shipment option.
|
|
5
5
|
*
|
|
6
6
|
* Renders as a select/dropdown instead of a TriState toggle. Insurance amount brackets are
|
|
7
|
-
* derived from the currently selected carrier's shipment-scoped
|
|
7
|
+
* derived from the currently selected carrier's shipment-scoped insurance limits (min/max)
|
|
8
8
|
* and refresh automatically whenever that data changes — i.e. on carrier change AND on
|
|
9
9
|
* packageType / deliveryType / weight / cc change, since those all narrow the shipment-scoped
|
|
10
10
|
* capabilities response.
|
|
@@ -13,8 +13,8 @@ import { type CapabilitiesSelection } from '../../actions/composables/queries/ac
|
|
|
13
13
|
* - **Order query** drives the carrier / packageType / deliveryType dropdowns from each
|
|
14
14
|
* carrier's flat union arrays. Refetches only when destination, weight or the recipient
|
|
15
15
|
* business flag change.
|
|
16
|
-
* - **Shipment query** drives per-option metadata (`isRequired`, `requires`, `excludes`,
|
|
17
|
-
* `
|
|
16
|
+
* - **Shipment query** drives per-option metadata (`isRequired`, `requires`, `excludes`, and the
|
|
17
|
+
* insurance `min` / `max`) for the chosen combination. Refetches when any axis of the selection
|
|
18
18
|
* changes; the empty-result case is the invalid-combo signal consumed by
|
|
19
19
|
* `useCapabilitiesAutoClear`.
|
|
20
20
|
*
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './forms';
|
|
|
10
10
|
export * from './generateLabelFilename';
|
|
11
11
|
export * from './getOrderId';
|
|
12
12
|
export * from './getOrderShipmentIds';
|
|
13
|
+
export * from './notificationBelongsToOrder';
|
|
13
14
|
export * from './openUrlInNewTab';
|
|
14
15
|
export * from './prefixComponent';
|
|
15
16
|
export * from './query';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type PdkNotification } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* A backend notification is tagged with every order the action ran for, so an order list column
|
|
4
|
+
* matches on that list instead of on the whole tag.
|
|
5
|
+
*/
|
|
6
|
+
export declare const notificationBelongsToOrder: (notification: Pick<PdkNotification, "tags">, externalIdentifier?: string) => boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myparcel-dev/pdk-admin",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/myparcelnl/js-pdk.git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@myparcel-dev/constants": "^2.9.0",
|
|
40
|
-
"@myparcel-dev/pdk-common": "^2.1.
|
|
40
|
+
"@myparcel-dev/pdk-common": "^2.1.2",
|
|
41
41
|
"@myparcel-dev/sdk": "^5.1.0",
|
|
42
42
|
"@myparcel-dev/vue-form-builder": "1.0.0-beta.42.6",
|
|
43
43
|
"@tanstack/vue-query": "~4.43.0",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
4
|
+
import {ApiException} from '@myparcel-dev/sdk';
|
|
5
|
+
import {Variant} from '@myparcel-dev/pdk-common';
|
|
6
|
+
import {useNotificationStore} from '../../stores';
|
|
7
|
+
import {NotificationCategory} from '../../data';
|
|
8
|
+
import {doComponentTestSetup, doComponentTestTeardown} from '../../__tests__';
|
|
9
|
+
import {type ActionContext} from './types';
|
|
10
|
+
import {executeHandler} from './executeHandler';
|
|
11
|
+
|
|
12
|
+
const ERROR_NOTIFICATION = {
|
|
13
|
+
category: NotificationCategory.Action,
|
|
14
|
+
title: 'Something went wrong',
|
|
15
|
+
variant: Variant.Error,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const BACKEND_NOTIFICATION = {
|
|
19
|
+
category: NotificationCategory.Action,
|
|
20
|
+
content: ['Street is required.'],
|
|
21
|
+
title: 'The given data was invalid.',
|
|
22
|
+
variant: Variant.Error,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createContext = (handler: () => Promise<unknown>): ActionContext =>
|
|
26
|
+
({
|
|
27
|
+
action: {name: 'ordersPrint', handler},
|
|
28
|
+
instance: {logger: {debug: vi.fn(), error: vi.fn()}},
|
|
29
|
+
notifications: {[Variant.Error]: ERROR_NOTIFICATION},
|
|
30
|
+
parameters: {},
|
|
31
|
+
} as unknown as ActionContext);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PdkFetchClient adds the notifications of an error response to the store and then throws.
|
|
35
|
+
*/
|
|
36
|
+
const createApiException = (notifications?: unknown[]): ApiException => {
|
|
37
|
+
const store = useNotificationStore();
|
|
38
|
+
|
|
39
|
+
notifications?.forEach((notification) => store.add(notification as never));
|
|
40
|
+
|
|
41
|
+
return new ApiException({
|
|
42
|
+
errors: [{code: 400, message: 'Request failed.'}],
|
|
43
|
+
...(notifications ? {notifications} : {}),
|
|
44
|
+
} as never);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('executeHandler', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
doComponentTestSetup();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
doComponentTestTeardown();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('reports a failing action to the user', async () => {
|
|
57
|
+
const context = createContext(() => Promise.reject(new Error('Printing failed')));
|
|
58
|
+
|
|
59
|
+
await executeHandler(context);
|
|
60
|
+
|
|
61
|
+
expect(useNotificationStore().notifications).toMatchObject([
|
|
62
|
+
{category: NotificationCategory.Action, content: 'Printing failed', variant: Variant.Error},
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not report a failure the backend already reported', async () => {
|
|
67
|
+
const store = useNotificationStore();
|
|
68
|
+
const context = createContext(() => Promise.reject(createApiException([BACKEND_NOTIFICATION])));
|
|
69
|
+
|
|
70
|
+
await executeHandler(context);
|
|
71
|
+
|
|
72
|
+
expect(store.notifications).toHaveLength(1);
|
|
73
|
+
expect(store.notifications[0].title).toBe('The given data was invalid.');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('reports a failure the backend did not report itself', async () => {
|
|
77
|
+
const store = useNotificationStore();
|
|
78
|
+
const context = createContext(() => Promise.reject(createApiException()));
|
|
79
|
+
|
|
80
|
+
await executeHandler(context);
|
|
81
|
+
|
|
82
|
+
expect(store.notifications).toMatchObject([{title: 'Something went wrong', variant: Variant.Error}]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('still reports the failure when an unrelated error notification is in the store', async () => {
|
|
86
|
+
const store = useNotificationStore();
|
|
87
|
+
|
|
88
|
+
const context = createContext(() => {
|
|
89
|
+
store.add({category: NotificationCategory.Api, content: 'Unrelated', variant: Variant.Error});
|
|
90
|
+
|
|
91
|
+
return Promise.reject(new Error('Printing failed'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await executeHandler(context);
|
|
95
|
+
|
|
96
|
+
expect(store.notifications).toHaveLength(2);
|
|
97
|
+
expect(store.notifications[1]).toMatchObject({content: 'Printing failed', variant: Variant.Error});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {ApiException} from '@myparcel-dev/sdk';
|
|
1
2
|
import {type ActionResponse, type MaybeAdminAction} from '../../types';
|
|
2
3
|
import {useNotificationStore} from '../../stores';
|
|
3
4
|
import {NotificationCategory} from '../../data';
|
|
@@ -25,7 +26,13 @@ export async function executeHandler<A extends MaybeAdminAction>(
|
|
|
25
26
|
|
|
26
27
|
return response as ActionResponse<A>;
|
|
27
28
|
} catch (error) {
|
|
28
|
-
|
|
29
|
+
// The backend reports a failure through the notifications in its error response, which
|
|
30
|
+
// PdkFetchClient adds to the store. Only fall back to a generic message when the response
|
|
31
|
+
// carried none, for example because the request never reached the backend.
|
|
32
|
+
const reported =
|
|
33
|
+
error instanceof ApiException && Boolean((error.data as {notifications?: unknown[]}).notifications?.length);
|
|
34
|
+
|
|
35
|
+
if (!reported && notifications?.error && error instanceof Error) {
|
|
29
36
|
store.add({...notifications.error, timeout: false, content: error.message}, context.parameters);
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -182,11 +182,9 @@ describe('useFormCapabilities', () => {
|
|
|
182
182
|
carrier: 'POSTNL',
|
|
183
183
|
options: {
|
|
184
184
|
insurance: {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
default: {amount: 0, currency: 'EUR'},
|
|
189
|
-
},
|
|
185
|
+
min: {amount: 0, currency: 'EUR'},
|
|
186
|
+
max: {amount: 0, currency: 'EUR'},
|
|
187
|
+
default: {amount: 0, currency: 'EUR'},
|
|
190
188
|
} as never,
|
|
191
189
|
},
|
|
192
190
|
});
|
|
@@ -202,12 +200,10 @@ describe('useFormCapabilities', () => {
|
|
|
202
200
|
carrier: 'POSTNL',
|
|
203
201
|
options: {
|
|
204
202
|
insurance: {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
default: {amount: 0, currency: 'EUR'},
|
|
210
|
-
},
|
|
203
|
+
min: {amount: 0, currency: 'EUR'},
|
|
204
|
+
// max is 1000€ in cents
|
|
205
|
+
max: {amount: 100_000, currency: 'EUR'},
|
|
206
|
+
default: {amount: 0, currency: 'EUR'},
|
|
211
207
|
} as never,
|
|
212
208
|
},
|
|
213
209
|
});
|
|
@@ -223,5 +219,21 @@ describe('useFormCapabilities', () => {
|
|
|
223
219
|
// the boundary semantics — replacing `<` with `<=` on LOW_THRESHOLD would shift these.
|
|
224
220
|
expect(values.slice(1)).toEqual(['0', '25000', '50000', '100000']);
|
|
225
221
|
});
|
|
222
|
+
|
|
223
|
+
it('returns [] when the carrier advertises insurance without limits', async () => {
|
|
224
|
+
// Stored carrier data that predates the flat limits: insurance is offered, but there is
|
|
225
|
+
// no max to build brackets up to, so there is nothing to enumerate into the dropdown.
|
|
226
|
+
const shipment = buildCarrier({
|
|
227
|
+
carrier: 'POSTNL',
|
|
228
|
+
options: {
|
|
229
|
+
insurance: {isRequired: false, isSelectedByDefault: false} as never,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
|
|
233
|
+
|
|
234
|
+
const {useFormCapabilities} = await import('./useFormCapabilities');
|
|
235
|
+
|
|
236
|
+
expect(useFormCapabilities().getInsuranceOptions(formStub('POSTNL'), formatter)).toEqual([]);
|
|
237
|
+
});
|
|
226
238
|
});
|
|
227
239
|
});
|
|
@@ -7,14 +7,6 @@ import {type AdminContext, type SelectOption} from '../../types';
|
|
|
7
7
|
import {useQueryStore, type ResolvedQuery} from '../../stores';
|
|
8
8
|
import {Format, type Formatter} from '../../composables';
|
|
9
9
|
|
|
10
|
-
interface InsuranceAmountData {
|
|
11
|
-
insuredAmount?: {
|
|
12
|
-
min: {amount: number; currency: string};
|
|
13
|
-
max: {amount: number; currency: string};
|
|
14
|
-
default: {amount: number; currency: string};
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
/**
|
|
19
11
|
* Bag of capability resolvers, each pre-bound to a Pinia store + orderId snapshot captured at
|
|
20
12
|
* the composable's invocation time. Returned by {@link useFormCapabilities}.
|
|
@@ -53,8 +45,8 @@ export type FormCapabilities = {
|
|
|
53
45
|
hasShipmentOption: (form: FormInstance, option: string) => boolean;
|
|
54
46
|
|
|
55
47
|
/**
|
|
56
|
-
* Insurance amount bracket options derived from the currently selected carrier's
|
|
57
|
-
* `
|
|
48
|
+
* Insurance amount bracket options derived from the currently selected carrier's insurance
|
|
49
|
+
* limits (`min` / `max`). Amounts in the context are in cents; emitted values are strings
|
|
58
50
|
* representing cent amounts so they round-trip through form select inputs unchanged.
|
|
59
51
|
*/
|
|
60
52
|
getInsuranceOptions: (form: FormInstance, formatter: Formatter) => SelectOption[];
|
|
@@ -80,11 +72,10 @@ export type FormCapabilities = {
|
|
|
80
72
|
* window focus, so we read `query.data` live on every resolver call rather than snapshotting
|
|
81
73
|
* the carriers array at setup time.
|
|
82
74
|
*/
|
|
75
|
+
// eslint-disable-next-line max-lines-per-function
|
|
83
76
|
export const useFormCapabilities = (): FormCapabilities => {
|
|
84
77
|
const queryStore = useQueryStore();
|
|
85
|
-
const dynamicContextQuery = queryStore.get(BackendEndpoint.FetchContext, AdminContextKey.Dynamic)
|
|
86
|
-
BackendEndpoint.FetchContext
|
|
87
|
-
>;
|
|
78
|
+
const dynamicContextQuery = queryStore.get(BackendEndpoint.FetchContext, AdminContextKey.Dynamic);
|
|
88
79
|
const orderId = getOrderId();
|
|
89
80
|
|
|
90
81
|
const orderModifier = typeof orderId === 'string' ? `${orderId}.order` : undefined;
|
|
@@ -97,7 +88,7 @@ export const useFormCapabilities = (): FormCapabilities => {
|
|
|
97
88
|
|
|
98
89
|
if (toValue(query.status) !== 'success') return undefined;
|
|
99
90
|
|
|
100
|
-
return
|
|
91
|
+
return toValue(query.data) ?? [];
|
|
101
92
|
};
|
|
102
93
|
|
|
103
94
|
const liveDynamicCarriers = (): CarrierModel[] => {
|
|
@@ -145,10 +136,10 @@ export const useFormCapabilities = (): FormCapabilities => {
|
|
|
145
136
|
const STEP_THRESHOLD = 500;
|
|
146
137
|
|
|
147
138
|
const carrier = getCarrierCapabilitiesForShipment(form);
|
|
148
|
-
const insuranceData =
|
|
139
|
+
const insuranceData = carrier?.options?.insurance;
|
|
149
140
|
|
|
150
|
-
const min = insuranceData
|
|
151
|
-
const max = insuranceData
|
|
141
|
+
const min = insuranceData?.min?.amount ? insuranceData.min.amount / 100 : 0;
|
|
142
|
+
const max = insuranceData?.max?.amount ? insuranceData.max.amount / 100 : 0;
|
|
152
143
|
|
|
153
144
|
if (max === 0) return [];
|
|
154
145
|
|
|
@@ -86,7 +86,7 @@ export const createShipmentOptionsForm = (orders?: OneOrMore<Plugin.ModelPdkOrde
|
|
|
86
86
|
* `hasShipmentOption`, which checks whether the currently selected carrier
|
|
87
87
|
* supports each option.
|
|
88
88
|
*
|
|
89
|
-
* Runtime carrier-specific data (isRequired,
|
|
89
|
+
* Runtime carrier-specific data (isRequired, insurance limits, etc.) is read
|
|
90
90
|
* from the currently selected carrier via `getCarrier(form)`, not from
|
|
91
91
|
* creation-time option data.
|
|
92
92
|
*/
|
|
@@ -18,8 +18,8 @@ export type FieldFactory = (refs: ShipmentOptionsRefs, fieldName: string) => Int
|
|
|
18
18
|
* `carrier.options`) to its custom factory function.
|
|
19
19
|
*
|
|
20
20
|
* Current exceptions:
|
|
21
|
-
* - `insurance`: renders as a select/dropdown with amount brackets derived from
|
|
22
|
-
* `
|
|
21
|
+
* - `insurance`: renders as a select/dropdown with amount brackets derived from the
|
|
22
|
+
* `min` / `max` limits in the option data.
|
|
23
23
|
*/
|
|
24
24
|
export const fieldFactoryRegistry: Record<string, FieldFactory> = {
|
|
25
25
|
insurance: createInsuranceField,
|
|
@@ -13,7 +13,7 @@ import {createRef} from './createRef';
|
|
|
13
13
|
* Custom field factory for the insurance shipment option.
|
|
14
14
|
*
|
|
15
15
|
* Renders as a select/dropdown instead of a TriState toggle. Insurance amount brackets are
|
|
16
|
-
* derived from the currently selected carrier's shipment-scoped
|
|
16
|
+
* derived from the currently selected carrier's shipment-scoped insurance limits (min/max)
|
|
17
17
|
* and refresh automatically whenever that data changes — i.e. on carrier change AND on
|
|
18
18
|
* packageType / deliveryType / weight / cc change, since those all narrow the shipment-scoped
|
|
19
19
|
* capabilities response.
|
|
@@ -39,7 +39,7 @@ export const createInsuranceField = (
|
|
|
39
39
|
|
|
40
40
|
// Watch the shipment-scoped carrier capabilities directly. The resolver tracks both the
|
|
41
41
|
// selected carrier (via `form.getValue(FIELD_CARRIER)`) and the shipment-capabilities
|
|
42
|
-
// query data, so any axis change that updates
|
|
42
|
+
// query data, so any axis change that updates the insurance limits triggers a refresh.
|
|
43
43
|
stopWatcher = watch(
|
|
44
44
|
() => capabilities.getCarrierCapabilitiesForShipment(field.form),
|
|
45
45
|
() => {
|
|
@@ -47,8 +47,8 @@ const resolveOrderInput = (form: FormInstance, order: Plugin.ModelContextOrderDa
|
|
|
47
47
|
* - **Order query** drives the carrier / packageType / deliveryType dropdowns from each
|
|
48
48
|
* carrier's flat union arrays. Refetches only when destination, weight or the recipient
|
|
49
49
|
* business flag change.
|
|
50
|
-
* - **Shipment query** drives per-option metadata (`isRequired`, `requires`, `excludes`,
|
|
51
|
-
* `
|
|
50
|
+
* - **Shipment query** drives per-option metadata (`isRequired`, `requires`, `excludes`, and the
|
|
51
|
+
* insurance `min` / `max`) for the chosen combination. Refetches when any axis of the selection
|
|
52
52
|
* changes; the empty-result case is the invalid-combo signal consumed by
|
|
53
53
|
* `useCapabilitiesAutoClear`.
|
|
54
54
|
*
|
|
@@ -38,6 +38,9 @@ export const createActionContext = <A extends MaybeAdminAction>(
|
|
|
38
38
|
(acc, variant) => ({
|
|
39
39
|
...acc,
|
|
40
40
|
[variant]: createNotification(variant, {
|
|
41
|
+
// Action notifications are the only ones rendered next to the order and in the order
|
|
42
|
+
// box, which is where a failing action has to report itself.
|
|
43
|
+
category: NotificationCategory.Action,
|
|
41
44
|
tags: {
|
|
42
45
|
action: identifier,
|
|
43
46
|
},
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import {mount} from '@vue/test-utils';
|
|
5
|
+
import {Variant} from '@myparcel-dev/pdk-common';
|
|
6
|
+
import {type PdkNotification} from '../types';
|
|
7
|
+
import {useQueryStore} from '../stores';
|
|
8
|
+
import {NotificationCategory} from '../data';
|
|
9
|
+
import {doComponentTestSetup, doComponentTestTeardown, mockDefaultTranslations} from '../__tests__';
|
|
10
|
+
import {createNotification} from './createNotification';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* createNotification needs the translations, which are only available inside a setup context.
|
|
14
|
+
*/
|
|
15
|
+
const create = (options: Parameters<typeof createNotification>[1]): undefined | PdkNotification => {
|
|
16
|
+
let notification: undefined | PdkNotification;
|
|
17
|
+
|
|
18
|
+
mount({
|
|
19
|
+
setup() {
|
|
20
|
+
useQueryStore().registerContextQueries();
|
|
21
|
+
notification = createNotification(Variant.Error, options);
|
|
22
|
+
|
|
23
|
+
return () => null;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return notification;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('createNotification', () => {
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
mockDefaultTranslations.mockReturnValue({
|
|
33
|
+
notification_action_error: 'Something went wrong',
|
|
34
|
+
notification_action_error_body: 'Try again later.',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
doComponentTestSetup();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
doComponentTestTeardown();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
mockDefaultTranslations.mockReset();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('translates the title it derives from the category', () => {
|
|
51
|
+
expect(create({category: NotificationCategory.Action})).toMatchObject({
|
|
52
|
+
category: NotificationCategory.Action,
|
|
53
|
+
content: 'Try again later.',
|
|
54
|
+
title: 'Something went wrong',
|
|
55
|
+
variant: Variant.Error,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('leaves a title that was passed in untouched', () => {
|
|
60
|
+
expect(create({title: 'Street is required.'})?.title).toBe('Street is required.');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns undefined when there is nothing to show', () => {
|
|
64
|
+
expect(create({category: NotificationCategory.General})).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -22,14 +22,18 @@ export const createNotification = (
|
|
|
22
22
|
return notification;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
const contentKey = `${
|
|
25
|
+
const titleKey = `${PREFIX}${identifier ?? options?.category ?? NotificationCategory.General}_${variant}`;
|
|
26
|
+
const contentKey = `${titleKey}_body`;
|
|
27
27
|
|
|
28
28
|
const content = language.has(contentKey) ? language.translate(contentKey) : undefined;
|
|
29
29
|
|
|
30
|
-
if (!language.has(
|
|
30
|
+
if (!language.has(titleKey) && !content) {
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// No notification component translates the title, so resolve it here instead of handing them a
|
|
35
|
+
// translation key to render verbatim.
|
|
36
|
+
const title = language.has(titleKey) ? language.translate(titleKey) : undefined;
|
|
37
|
+
|
|
34
38
|
return {...notification, title, content};
|
|
35
39
|
};
|
package/src/utils/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './forms';
|
|
|
10
10
|
export * from './generateLabelFilename';
|
|
11
11
|
export * from './getOrderId';
|
|
12
12
|
export * from './getOrderShipmentIds';
|
|
13
|
+
export * from './notificationBelongsToOrder';
|
|
13
14
|
export * from './openUrlInNewTab';
|
|
14
15
|
export * from './prefixComponent';
|
|
15
16
|
export * from './query';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {notificationBelongsToOrder} from './notificationBelongsToOrder';
|
|
3
|
+
|
|
4
|
+
describe('notificationBelongsToOrder', () => {
|
|
5
|
+
it('matches the order the notification was tagged with', () => {
|
|
6
|
+
expect(notificationBelongsToOrder({tags: {orderIds: '123'}}, '123')).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('matches an order inside a bulk action tag', () => {
|
|
10
|
+
expect(notificationBelongsToOrder({tags: {orderIds: '123,456,789'}}, '456')).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not match another order', () => {
|
|
14
|
+
expect(notificationBelongsToOrder({tags: {orderIds: '123,456'}}, '999')).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('does not match a partial order id', () => {
|
|
18
|
+
expect(notificationBelongsToOrder({tags: {orderIds: '1234'}}, '123')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not match an untagged notification', () => {
|
|
22
|
+
expect(notificationBelongsToOrder({tags: {}}, '123')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not match when there is no order', () => {
|
|
26
|
+
expect(notificationBelongsToOrder({tags: {orderIds: '123'}}, undefined)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {type PdkNotification} from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A backend notification is tagged with every order the action ran for, so an order list column
|
|
5
|
+
* matches on that list instead of on the whole tag.
|
|
6
|
+
*/
|
|
7
|
+
export const notificationBelongsToOrder = (
|
|
8
|
+
notification: Pick<PdkNotification, 'tags'>,
|
|
9
|
+
externalIdentifier?: string,
|
|
10
|
+
): boolean => {
|
|
11
|
+
const {orderIds} = notification.tags ?? {};
|
|
12
|
+
|
|
13
|
+
if (!orderIds || !externalIdentifier) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return orderIds.split(',').includes(externalIdentifier);
|
|
18
|
+
};
|
|
@@ -21,6 +21,7 @@ import {useActionStore, useQueryStore} from '../stores';
|
|
|
21
21
|
import {NotificationCategory, OrderMode} from '../data';
|
|
22
22
|
import {useOrderData, useOrderMode} from '../composables';
|
|
23
23
|
import {NotificationContainer} from '../components';
|
|
24
|
+
import {notificationBelongsToOrder} from '../utils';
|
|
24
25
|
|
|
25
26
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
26
27
|
const OrderModeOrderListItem = defineAsyncComponent(() => {
|
|
@@ -46,6 +47,6 @@ const orderMode = useOrderMode();
|
|
|
46
47
|
const {query} = useOrderData();
|
|
47
48
|
|
|
48
49
|
const notificationFilter: NotificationFilter = (notification) => {
|
|
49
|
-
return notification
|
|
50
|
+
return notificationBelongsToOrder(notification, toValue(query.data)?.externalIdentifier);
|
|
50
51
|
};
|
|
51
52
|
</script>
|