@myparcel-dev/pdk-admin 2.2.2 → 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 +10 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/notificationBelongsToOrder.d.ts +6 -0
- package/package.json +1 -1
- package/src/actions/executors/executeHandler.spec.ts +99 -0
- package/src/actions/executors/executeHandler.ts +8 -1
- 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,16 @@
|
|
|
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
|
+
|
|
5
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)
|
|
6
16
|
|
|
7
17
|
|
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
|
@@ -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
|
|
|
@@ -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>
|