@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 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
- * `insuredAmount` data. Amounts in the context are in cents; emitted values are strings
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
- * `insuredAmount` in the option data.
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 `insuredAmount` data (min/max)
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
- * `insuredAmount`) for the chosen combination. Refetches when any axis of the selection
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
  *
@@ -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.1",
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.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
- if (notifications?.error && error instanceof Error) {
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
- insuredAmount: {
186
- min: {amount: 0, currency: 'EUR'},
187
- max: {amount: 0, currency: 'EUR'},
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
- insuredAmount: {
206
- min: {amount: 0, currency: 'EUR'},
207
- // max is 1000€ in cents
208
- max: {amount: 100_000, currency: 'EUR'},
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
- * `insuredAmount` data. Amounts in the context are in cents; emitted values are strings
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) as ResolvedQuery<
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 (toValue(query.data) ?? []) as CarrierModel[];
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 = (carrier?.options?.insurance ?? {}) as InsuranceAmountData;
139
+ const insuranceData = carrier?.options?.insurance;
149
140
 
150
- const min = insuranceData.insuredAmount?.min.amount ? insuranceData.insuredAmount.min.amount / 100 : 0;
151
- const max = insuranceData.insuredAmount?.max.amount ? insuranceData.insuredAmount.max.amount / 100 : 0;
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, insuredAmount, etc.) is read
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
- * `insuredAmount` in the option data.
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 `insuredAmount` data (min/max)
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 `insuredAmount` triggers a refresh.
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
- * `insuredAmount`) for the chosen combination. Refetches when any axis of the selection
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 title = `${PREFIX}${identifier ?? options?.category ?? NotificationCategory.General}_${variant}`;
26
- const contentKey = `${title}_body`;
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(title) && !content) {
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
  };
@@ -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.tags?.orderIds === toValue(query.data)?.externalIdentifier;
50
+ return notificationBelongsToOrder(notification, toValue(query.data)?.externalIdentifier);
50
51
  };
51
52
  </script>