@myparcel-dev/pdk-admin 1.14.2 → 2.0.1

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 (107) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +6 -6
  3. package/src/__tests__/__snapshots__/exports.spec.ts.snap +8 -1
  4. package/src/__tests__/doComponentTestTeardown.ts +2 -0
  5. package/src/__tests__/mocks/index.ts +1 -1
  6. package/src/__tests__/mocks/mockDefaultAccount.ts +1 -0
  7. package/src/__tests__/mocks/mockDefaultGlobalContext.ts +2 -2
  8. package/src/__tests__/mocks/{mockDefaultPlatform.ts → mockDefaultProposition.ts} +1 -3
  9. package/src/__tests__/mocks/mockDefaultShop.ts +1 -0
  10. package/src/__tests__/utils/getActionIds.ts +12 -0
  11. package/src/__tests__/utils/mockOrderData.ts +20 -0
  12. package/src/actions/composables/mutations/orders/useExportOrdersMutation.ts +1 -1
  13. package/src/actions/composables/queries/account/useOrderCapabilitiesQuery.ts +67 -0
  14. package/src/actions/composables/queries/account/useShipmentCapabilitiesQuery.ts +96 -0
  15. package/src/actions/definitions/orders.ts +6 -8
  16. package/src/actions/index.ts +1 -0
  17. package/src/actions/orderModeActions.spec.ts +150 -0
  18. package/src/actions/orderModeActions.ts +90 -0
  19. package/src/components/OrderBox/ShipmentOptionsBox.spec.ts +109 -0
  20. package/src/components/OrderBox/ShipmentOptionsBox.vue +9 -25
  21. package/src/components/OrderListItem/OrderModeOrderListItem.spec.ts +125 -0
  22. package/src/components/OrderListItem/OrderModeOrderListItem.vue +10 -6
  23. package/src/components/PluginSettings/PluginSettingsForms.vue +7 -4
  24. package/src/components/common/DeliveryOptionsExcerptCarrierName.vue +3 -3
  25. package/src/components/modals/ShipmentOptionsModal.spec.ts +79 -0
  26. package/src/components/modals/ShipmentOptionsModal.vue +4 -21
  27. package/src/composables/components/useToggleInputContext.ts +2 -2
  28. package/src/composables/components/useTriStateInputContext.ts +2 -2
  29. package/src/composables/context/index.ts +1 -0
  30. package/src/composables/context/useOrderMode.spec.ts +75 -0
  31. package/src/composables/context/useOrderMode.ts +26 -0
  32. package/src/composables/language/useLanguage.spec.ts +2 -2
  33. package/src/composables/shipments/useShipmentData.ts +2 -2
  34. package/src/data/index.ts +1 -0
  35. package/src/data/orderMode.spec.ts +22 -0
  36. package/src/data/orderMode.ts +50 -0
  37. package/src/forms/form-builder/builders/buildAfterUpdate.spec.ts +5 -8
  38. package/src/forms/form-builder/operations/executeOperations.spec.ts +4 -7
  39. package/src/forms/form-builder/operations/executeSetPropOperation.spec.ts +4 -7
  40. package/src/forms/form-builder/operations/executeSetValueOperation.spec.ts +3 -2
  41. package/src/forms/form-builder/utils/createValueSetter.ts +21 -8
  42. package/src/forms/helpers/addCapabilitiesClearNotification.ts +39 -0
  43. package/src/forms/helpers/getDeliveryTypes.ts +5 -10
  44. package/src/forms/helpers/getFieldLabel.ts +7 -2
  45. package/src/forms/helpers/getPackageTypes.ts +7 -12
  46. package/src/forms/helpers/index.ts +3 -10
  47. package/src/forms/helpers/useFormCapabilities.spec.ts +227 -0
  48. package/src/forms/helpers/useFormCapabilities.ts +176 -0
  49. package/src/forms/pluginSettings/createPluginSettingsForm.ts +2 -2
  50. package/src/forms/pluginSettings/filterPluginSettingsView.spec.ts +158 -0
  51. package/src/forms/pluginSettings/filterPluginSettingsView.ts +96 -0
  52. package/src/forms/pluginSettings/index.ts +1 -0
  53. package/src/forms/pluginSettings/resolveSettingsValues.spec.ts +62 -0
  54. package/src/forms/pluginSettings/resolveSettingsValues.ts +16 -0
  55. package/src/forms/shipmentOptions/createShipmentOptionsForm.spec.ts +108 -0
  56. package/src/forms/shipmentOptions/createShipmentOptionsForm.ts +114 -52
  57. package/src/forms/shipmentOptions/field.ts +6 -66
  58. package/src/forms/shipmentOptions/fieldFactoryRegistry.ts +26 -0
  59. package/src/forms/shipmentOptions/fields/createCarrierField.ts +119 -30
  60. package/src/forms/shipmentOptions/fields/createDigitalStampRangeField.ts +3 -3
  61. package/src/forms/shipmentOptions/fields/createInsuranceField.ts +43 -22
  62. package/src/forms/shipmentOptions/fields/createPackageTypeField.ts +1 -2
  63. package/src/forms/shipmentOptions/fields/createShipmentOptionField.spec.ts +177 -0
  64. package/src/forms/shipmentOptions/fields/createShipmentOptionField.ts +90 -8
  65. package/src/forms/shipmentOptions/fields/index.ts +0 -9
  66. package/src/forms/shipmentOptions/useCapabilitiesAutoClear.spec.ts +347 -0
  67. package/src/forms/shipmentOptions/useCapabilitiesAutoClear.ts +452 -0
  68. package/src/forms/shipmentOptions/useCapabilitiesWatcher.spec.ts +14 -0
  69. package/src/forms/shipmentOptions/useCapabilitiesWatcher.ts +29 -0
  70. package/src/forms/shipmentOptions/wireProxyCapabilities.spec.ts +160 -0
  71. package/src/forms/shipmentOptions/wireProxyCapabilities.ts +102 -0
  72. package/src/index.ts +2 -0
  73. package/src/pdk/instance/plugins/createRegisterComponentsPlugin.ts +2 -0
  74. package/src/sdk/composables/api/useFetchCarrier.ts +39 -4
  75. package/src/sdk/composables/usePdkAdminApi.ts +8 -3
  76. package/src/stores/useActionStore.spec.ts +101 -0
  77. package/src/stores/useActionStore.ts +4 -28
  78. package/src/stores/useQueryStore.ts +15 -0
  79. package/src/types/actions/endpoints.types.ts +2 -1
  80. package/src/types/sdk.types.ts +54 -1
  81. package/src/utils/forms/formToBody.ts +2 -1
  82. package/src/utils/forms/getEnabledValues.spec.ts +66 -0
  83. package/src/utils/forms/getEnabledValues.ts +25 -0
  84. package/src/utils/forms/index.ts +1 -0
  85. package/src/utils/translations/getDynamicTranslation.ts +1 -1
  86. package/src/views/OrderBox.vue +11 -5
  87. package/src/views/OrderListItem.spec.ts +71 -0
  88. package/src/views/OrderListItem.vue +6 -7
  89. package/src/forms/helpers/createHasShipmentOptionWatcher.ts +0 -12
  90. package/src/forms/helpers/getCarrier.ts +0 -11
  91. package/src/forms/helpers/getInsuranceOptions.ts +0 -23
  92. package/src/forms/helpers/hasPostNlAgeCheck.ts +0 -11
  93. package/src/forms/helpers/hasShipmentOption.ts +0 -8
  94. package/src/forms/helpers/isPackageTypeMailbox.ts +0 -7
  95. package/src/forms/helpers/isPackageTypePackage.ts +0 -7
  96. package/src/forms/helpers/isPackageTypePackageOrSmall.ts +0 -7
  97. package/src/forms/helpers/isPackageTypeSmallPackage.ts +0 -7
  98. package/src/forms/helpers/setPostNlAgeCheckSubtext.ts +0 -17
  99. package/src/forms/shipmentOptions/fields/createAgeCheckField.ts +0 -33
  100. package/src/forms/shipmentOptions/fields/createDirectReturnField.ts +0 -8
  101. package/src/forms/shipmentOptions/fields/createHideSenderField.ts +0 -8
  102. package/src/forms/shipmentOptions/fields/createLargeFormatField.ts +0 -8
  103. package/src/forms/shipmentOptions/fields/createOnlyRecipientField.ts +0 -21
  104. package/src/forms/shipmentOptions/fields/createPriorityDeliveryField.ts +0 -12
  105. package/src/forms/shipmentOptions/fields/createReceiptCodeField.ts +0 -21
  106. package/src/forms/shipmentOptions/fields/createSameDayDeliveryField.ts +0 -8
  107. package/src/forms/shipmentOptions/fields/createSignatureField.ts +0 -11
@@ -3,33 +3,23 @@
3
3
 
4
4
  import {toRaw} from 'vue';
5
5
  import {get} from 'lodash-unified';
6
- import {defineForm, type FormInstance} from '@myparcel-dev/vue-form-builder';
6
+ import {defineForm, type FormInstance, type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
7
7
  import {type OneOrMore, toArray} from '@myparcel-dev/ts-utils';
8
- import {type Plugin} from '@myparcel-dev/pdk-common';
8
+ import {AdminContextKey, type Plugin} from '@myparcel-dev/pdk-common';
9
9
  import {addBulkEditNotification} from '../helpers';
10
10
  import {createShipmentFormName} from '../../utils';
11
11
  import {useModalStore} from '../../stores';
12
12
  import {AdminModalKey} from '../../data';
13
- import {useAdminConfig} from '../../composables';
13
+ import {useAdminConfig, useContext} from '../../composables';
14
+ import {useCapabilitiesAutoClear} from './useCapabilitiesAutoClear';
15
+ import {wireProxyCapabilities} from './wireProxyCapabilities';
14
16
  import {type ShipmentOptionsRefs} from './types';
15
- import {createHideSenderField} from './fields/createHideSenderField';
17
+ import {createShipmentOptionField} from './fields/createShipmentOptionField';
16
18
  import {createDeliveryTypeField} from './fields/createDeliveryTypeField';
17
- import {
18
- createAgeCheckField,
19
- createCarrierField,
20
- createDigitalStampRangeField,
21
- createDirectReturnField,
22
- createInsuranceField,
23
- createPriorityDeliveryField,
24
- createLabelAmountField,
25
- createLargeFormatField,
26
- createOnlyRecipientField,
27
- createPackageTypeField,
28
- createSameDayDeliveryField,
29
- createSignatureField,
30
- createReceiptCodeField,
31
- } from './fields';
32
- import {ALL_FIELDS, FIELD_CARRIER} from './field';
19
+ import {createCarrierField} from './fields/createCarrierField';
20
+ import {createDigitalStampRangeField, createLabelAmountField, createPackageTypeField} from './fields';
21
+ import {fieldFactoryRegistry} from './fieldFactoryRegistry';
22
+ import {FIELD_CARRIER, FIELD_DELIVERY_TYPE, FIELD_LABEL_AMOUNT, FIELD_MANUAL_WEIGHT, FIELD_PACKAGE_TYPE, FIELD_SHIPMENT_OPTIONS_PREFIX} from './field';
33
23
 
34
24
  export const createShipmentOptionsForm = (orders?: OneOrMore<Plugin.ModelPdkOrder>): FormInstance => {
35
25
  const ordersArray = toArray(orders ?? []).map(toRaw);
@@ -46,40 +36,112 @@ export const createShipmentOptionsForm = (orders?: OneOrMore<Plugin.ModelPdkOrde
46
36
 
47
37
  const order = ((isBulk ? undefined : ordersArray[0]) ?? {}) as Plugin.ModelContextOrderDataContext;
48
38
 
49
- const refs = ALL_FIELDS.reduce((acc, fieldName) => {
50
- acc[fieldName] = get(order, fieldName);
39
+ const dynamicContext = useContext(AdminContextKey.Dynamic);
40
+ const allOptionKeys = collectAllOptionKeys(dynamicContext.carriers);
41
+ const refs = buildDynamicRefs(order, allOptionKeys);
51
42
 
52
- return acc;
53
- }, {} as ShipmentOptionsRefs);
54
-
55
- refs[FIELD_CARRIER] = get(order, `${FIELD_CARRIER}.externalIdentifier`);
56
-
57
- return defineForm(createShipmentFormName(order.externalIdentifier), {
43
+ const form = defineForm(createShipmentFormName(order.externalIdentifier), {
58
44
  ...(isModal ? config.formConfigOverrides?.modal : null),
59
45
  ...config.formConfigOverrides?.shipmentOptions,
60
- fields: [
61
- // General delivery options
62
- createCarrierField(refs, order.inheritedDeliveryOptions),
63
-
64
- createPackageTypeField(refs),
65
-
66
- createDeliveryTypeField(refs),
67
-
68
- createLabelAmountField(refs),
69
-
70
- createDigitalStampRangeField(refs, order),
71
-
72
- // Actual shipment options
73
- createAgeCheckField(refs),
74
- createSignatureField(refs),
75
- createOnlyRecipientField(refs),
76
- createDirectReturnField(refs),
77
- createLargeFormatField(refs),
78
- createHideSenderField(refs),
79
- createSameDayDeliveryField(refs),
80
- createReceiptCodeField(refs),
81
- createInsuranceField(refs),
82
- createPriorityDeliveryField(refs),
83
- ],
46
+ fields: createShipmentOptionsFields(refs, order, allOptionKeys),
84
47
  });
48
+
49
+ if (!isBulk && order.externalIdentifier) {
50
+ const wired = wireProxyCapabilities(form, order);
51
+
52
+ if (wired) {
53
+ useCapabilitiesAutoClear(
54
+ form,
55
+ allOptionKeys,
56
+ order.externalIdentifier,
57
+ order.inheritedDeliveryOptions,
58
+ wired.selection,
59
+ );
60
+ }
61
+ }
62
+
63
+ return form;
64
+ };
65
+
66
+ /**
67
+ * Collect the union of all option keys across all carriers from the dynamic context.
68
+ *
69
+ * Fields are created for every unique option key so they exist when the user
70
+ * switches carriers. Visibility and disabled state are controlled by
71
+ * `hasShipmentOption`, which checks whether the currently selected carrier
72
+ * supports each option.
73
+ *
74
+ * Runtime carrier-specific data (isRequired, insuredAmount, etc.) is read
75
+ * from the currently selected carrier via `getCarrier(form)`, not from
76
+ * creation-time option data.
77
+ */
78
+ const collectAllOptionKeys = (carriers: {options?: Record<string, unknown> | null}[]): string[] => {
79
+ const keys = new Set<string>();
80
+
81
+ for (const carrier of carriers) {
82
+ if (!carrier.options) continue;
83
+
84
+ for (const key of Object.keys(carrier.options)) {
85
+ keys.add(key);
86
+ }
87
+ }
88
+
89
+ return [...keys];
90
+ };
91
+
92
+ /**
93
+ * Build refs dynamically from carrier options and static fields.
94
+ *
95
+ * Instead of a hardcoded ALL_FIELDS list, refs are built from the union of
96
+ * all carrier options in the dynamic context.
97
+ */
98
+ const buildDynamicRefs = (
99
+ order: Plugin.ModelContextOrderDataContext,
100
+ optionKeys: string[],
101
+ ): ShipmentOptionsRefs => {
102
+ const refs: ShipmentOptionsRefs = {};
103
+
104
+ // Static field refs
105
+ refs[FIELD_CARRIER] = get(order, `${FIELD_CARRIER}.carrier`);
106
+ refs[FIELD_LABEL_AMOUNT] = get(order, FIELD_LABEL_AMOUNT);
107
+ refs[FIELD_PACKAGE_TYPE] = get(order, FIELD_PACKAGE_TYPE);
108
+ refs[FIELD_DELIVERY_TYPE] = get(order, FIELD_DELIVERY_TYPE);
109
+ refs[FIELD_MANUAL_WEIGHT] = get(order, FIELD_MANUAL_WEIGHT);
110
+
111
+ // Dynamic shipment option refs from carrier options
112
+ for (const key of optionKeys) {
113
+ const fieldName = `${FIELD_SHIPMENT_OPTIONS_PREFIX}.${key}`;
114
+ refs[fieldName] = get(order, fieldName);
115
+ }
116
+
117
+ return refs;
118
+ };
119
+
120
+ const createShipmentOptionsFields = (
121
+ refs: ShipmentOptionsRefs,
122
+ order: Plugin.ModelContextOrderDataContext,
123
+ optionKeys: string[],
124
+ ): InteractiveElementConfiguration[] => {
125
+ // Static fields — always present
126
+ const staticFields = [
127
+ createCarrierField(refs, order.inheritedDeliveryOptions),
128
+ createPackageTypeField(refs),
129
+ createDeliveryTypeField(refs),
130
+ createLabelAmountField(refs),
131
+ createDigitalStampRangeField(refs, order),
132
+ ];
133
+
134
+ // Dynamic shipment option fields — driven by carrier.options
135
+ const dynamicFields = optionKeys.map((key) => {
136
+ const fieldName = `${FIELD_SHIPMENT_OPTIONS_PREFIX}.${key}`;
137
+ const factory = fieldFactoryRegistry[key];
138
+
139
+ if (factory) {
140
+ return factory(refs, fieldName);
141
+ }
142
+
143
+ return createShipmentOptionField(refs, fieldName);
144
+ });
145
+
146
+ return [...staticFields, ...dynamicFields];
85
147
  };
@@ -1,42 +1,18 @@
1
- import {type Shipment} from '@myparcel-dev/pdk-common';
2
-
3
1
  export type FieldName = string;
4
2
 
5
3
  const DELIVERY_OPTIONS_PREFIX = 'deliveryOptions';
6
4
 
7
5
  const PHYSICAL_PROPERTIES_PREFIX = 'physicalProperties';
8
6
 
9
- export const SHIPMENT_OPTIONS = `shipmentOptions`;
10
-
11
- const SHIPMENT_OPTIONS_PREFIX = `${DELIVERY_OPTIONS_PREFIX}.${SHIPMENT_OPTIONS}`;
12
-
13
- export const CARRIER = 'carrier' satisfies keyof Shipment.ModelDeliveryOptions;
14
-
15
- export const LABEL_AMOUNT = 'labelAmount' satisfies keyof Shipment.ModelDeliveryOptions;
16
-
17
- export const PACKAGE_TYPE = 'packageType' satisfies keyof Shipment.ModelDeliveryOptions;
18
-
19
- export const DELIVERY_TYPE = 'deliveryType' satisfies keyof Shipment.ModelDeliveryOptions;
20
-
21
- export const AGE_CHECK = 'ageCheck' satisfies keyof Shipment.ModelShipmentOptions;
22
-
23
- export const RECEIPT_CODE = 'receiptCode' satisfies keyof Shipment.ModelShipmentOptions;
24
-
25
- export const DIRECT_RETURN = 'return' satisfies keyof Shipment.ModelShipmentOptions;
26
-
27
- export const HIDE_SENDER = 'hideSender' satisfies keyof Shipment.ModelShipmentOptions;
28
-
29
- export const LARGE_FORMAT = 'largeFormat' satisfies keyof Shipment.ModelShipmentOptions;
7
+ export const SHIPMENT_OPTIONS = 'shipmentOptions';
30
8
 
31
- export const ONLY_RECIPIENT = 'onlyRecipient' satisfies keyof Shipment.ModelShipmentOptions;
9
+ export const CARRIER = 'carrier';
32
10
 
33
- export const SAME_DAY_DELIVERY = 'sameDayDelivery' satisfies keyof Shipment.ModelShipmentOptions;
11
+ export const LABEL_AMOUNT = 'labelAmount';
34
12
 
35
- export const SIGNATURE = 'signature' satisfies keyof Shipment.ModelShipmentOptions;
13
+ export const PACKAGE_TYPE = 'packageType';
36
14
 
37
- export const INSURANCE = 'insurance' satisfies keyof Shipment.ModelShipmentOptions;
38
-
39
- export const PRIORITY_DELIVERY = 'priorityDelivery' satisfies keyof Shipment.ModelShipmentOptions;
15
+ export const DELIVERY_TYPE = 'deliveryType';
40
16
 
41
17
  export const MANUAL_WEIGHT = 'manualWeight';
42
18
 
@@ -54,42 +30,6 @@ export const FIELD_PACKAGE_TYPE: FieldName = `${DELIVERY_OPTIONS_PREFIX}.${PACKA
54
30
 
55
31
  export const FIELD_DELIVERY_TYPE: FieldName = `${DELIVERY_OPTIONS_PREFIX}.${DELIVERY_TYPE}`;
56
32
 
57
- export const FIELD_AGE_CHECK: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${AGE_CHECK}`;
58
-
59
- export const FIELD_DIRECT_RETURN: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${DIRECT_RETURN}`;
60
-
61
- export const FIELD_HIDE_SENDER: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${HIDE_SENDER}`;
62
-
63
- export const FIELD_LARGE_FORMAT: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${LARGE_FORMAT}`;
64
-
65
- export const FIELD_ONLY_RECIPIENT: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${ONLY_RECIPIENT}`;
66
-
67
- export const FIELD_SAME_DAY_DELIVERY: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${SAME_DAY_DELIVERY}`;
68
-
69
- export const FIELD_SIGNATURE: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${SIGNATURE}`;
70
-
71
- export const FIELD_INSURANCE: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${INSURANCE}`;
72
-
73
- export const FIELD_PRIORITY_DELIVERY: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${PRIORITY_DELIVERY}`;
74
-
75
33
  export const FIELD_MANUAL_WEIGHT: FieldName = `${PHYSICAL_PROPERTIES_PREFIX}.${MANUAL_WEIGHT}`;
76
34
 
77
- export const FIELD_RECEIPT_CODE: FieldName = `${SHIPMENT_OPTIONS_PREFIX}.${RECEIPT_CODE}`;
78
-
79
- export const ALL_FIELDS = [
80
- FIELD_AGE_CHECK,
81
- FIELD_CARRIER,
82
- FIELD_DIRECT_RETURN,
83
- FIELD_HIDE_SENDER,
84
- FIELD_INSURANCE,
85
- FIELD_PRIORITY_DELIVERY,
86
- FIELD_LABEL_AMOUNT,
87
- FIELD_LARGE_FORMAT,
88
- FIELD_ONLY_RECIPIENT,
89
- FIELD_MANUAL_WEIGHT,
90
- FIELD_PACKAGE_TYPE,
91
- FIELD_DELIVERY_TYPE,
92
- FIELD_SAME_DAY_DELIVERY,
93
- FIELD_SIGNATURE,
94
- FIELD_RECEIPT_CODE,
95
- ] as const;
35
+ export const FIELD_SHIPMENT_OPTIONS_PREFIX: FieldName = `${DELIVERY_OPTIONS_PREFIX}.${SHIPMENT_OPTIONS}`;
@@ -0,0 +1,26 @@
1
+ import {type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
2
+ import {type ShipmentOptionsRefs} from './types';
3
+ import {createInsuranceField} from './fields/createInsuranceField';
4
+
5
+ /**
6
+ * Factory function signature for custom shipment option field creators.
7
+ *
8
+ * Uses `string` for fieldName because option names are determined at runtime
9
+ * from the carrier context and are not a fixed compile-time set.
10
+ */
11
+ export type FieldFactory = (refs: ShipmentOptionsRefs, fieldName: string) => InteractiveElementConfiguration;
12
+
13
+ /**
14
+ * Registry of shipment option names that require custom field rendering.
15
+ *
16
+ * Options NOT listed here are rendered as generic TriState toggles by
17
+ * `createShipmentOptionField`. Each entry maps an option name (as it appears in
18
+ * `carrier.options`) to its custom factory function.
19
+ *
20
+ * Current exceptions:
21
+ * - `insurance`: renders as a select/dropdown with amount brackets derived from
22
+ * `insuredAmount` in the option data.
23
+ */
24
+ export const fieldFactoryRegistry: Record<string, FieldFactory> = {
25
+ insurance: createInsuranceField,
26
+ };
@@ -1,69 +1,159 @@
1
- import {toValue} from 'vue';
1
+ import {toValue, watch} from 'vue';
2
+ import {snakeCase} from 'lodash-unified';
2
3
  import {type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
3
- import {AdminContextKey, type Plugin} from '@myparcel-dev/pdk-common';
4
- import {PackageTypeName} from '@myparcel-dev/constants';
4
+ import {AdminContextKey, BackendEndpoint, type CarrierModel, type Plugin} from '@myparcel-dev/pdk-common';
5
5
  import {type ShipmentOptionsRefs} from '../types';
6
- import {FIELD_CARRIER, FIELD_DELIVERY_TYPE, FIELD_INSURANCE, FIELD_PACKAGE_TYPE, PROP_OPTIONS} from '../field';
7
- import {getDeliveryTypes} from '../../helpers/getDeliveryTypes';
6
+ import {FIELD_CARRIER, FIELD_DELIVERY_TYPE, FIELD_PACKAGE_TYPE, PROP_OPTIONS} from '../field';
8
7
  import {
9
8
  defineFormField,
10
- getInsuranceOptions,
9
+ getDeliveryTypes,
11
10
  getPackageTypes,
12
11
  resolveFormComponent,
13
12
  setFieldProp,
14
- setPostNlAgeCheckSubtext,
15
13
  updateFieldsDefaults,
14
+ useFormCapabilities,
16
15
  } from '../../helpers';
17
- import {createAssetUrl} from '../../../utils';
16
+ import {createAssetUrl, getOrderId} from '../../../utils';
18
17
  import {type RadioGroupOption} from '../../../types';
18
+ import {useQueryStore} from '../../../stores';
19
19
  import {useFetchCarrier} from '../../../sdk';
20
20
  import {AdminComponent} from '../../../data';
21
- import {useContext, useLanguage, useLocalizedFormatter} from '../../../composables';
21
+ import {useContext, useLanguage} from '../../../composables';
22
22
  import {createRef} from './createRef';
23
23
 
24
+ type CarrierMeta = {label: string; image?: string};
25
+
26
+ type OrderCarriersState =
27
+ | undefined // No query registered (bulk or before wireProxyCapabilities runs) — show all.
28
+ | 'loading' // Order query is loading — show no options yet.
29
+ | CarrierModel[]; // Success — only carriers in this list are available.
30
+
24
31
  // eslint-disable-next-line max-lines-per-function
25
32
  export const createCarrierField = (
26
33
  refs: ShipmentOptionsRefs,
27
34
  inheritedDeliveryOptions: Plugin.ModelContextOrderDataContext['inheritedDeliveryOptions'],
28
35
  ): InteractiveElementConfiguration => {
29
36
  const dynamicContext = useContext(AdminContextKey.Dynamic);
30
-
31
- const formatter = useLocalizedFormatter();
37
+ const capabilities = useFormCapabilities();
38
+ const queryStore = useQueryStore();
39
+ const orderId = getOrderId();
40
+ const orderModifier = typeof orderId === 'string' ? `${orderId}.order` : undefined;
32
41
 
33
42
  const {translate} = useLanguage();
34
43
 
44
+ const staticMeta = new Map<string, CarrierMeta>();
45
+
46
+ /**
47
+ * Read the order-capabilities query state. Returns:
48
+ * - `undefined` when there's no query (bulk path, or pre-wire) → fallback: render all carriers.
49
+ * - `undefined` when the query is registered but disabled (e.g. order has no country code, so
50
+ * `useOrderCapabilitiesQuery`'s `enabled` gate is false). TanStack reports `status: 'pending'`
51
+ * indefinitely in that state, so we detect it via `fetchStatus === 'idle'` and fall back to
52
+ * the unfiltered carrier list instead of trapping the user in a permanent loading state.
53
+ * - `'loading'` while the query hasn't resolved → render no options to avoid a flash where
54
+ * carriers appear and then a subset disappears once the response lands.
55
+ * - `CarrierModel[]` on success → render only the carriers in this list (others are hidden).
56
+ * - On error: treated as `undefined` so the merchant can still operate the form, accepting
57
+ * that a stale or empty filter is better than an empty radio.
58
+ */
59
+ const readOrderCarriers = (): OrderCarriersState => {
60
+ if (!orderModifier || !queryStore.has(BackendEndpoint.ProxyCapabilities, orderModifier)) {
61
+ return undefined;
62
+ }
63
+
64
+ const query = queryStore.get(BackendEndpoint.ProxyCapabilities, orderModifier);
65
+ const status = toValue(query.status);
66
+ const fetchStatus = toValue(query.fetchStatus);
67
+
68
+ if (status === 'success') {
69
+ return (toValue(query.data) ?? []) as CarrierModel[];
70
+ }
71
+
72
+ if (status === 'error') {
73
+ return undefined;
74
+ }
75
+
76
+ if (fetchStatus === 'idle') {
77
+ return undefined;
78
+ }
79
+
80
+ return 'loading';
81
+ };
82
+
83
+ const computeOptions = (state: OrderCarriersState): RadioGroupOption[] => {
84
+ if (state === 'loading') return [];
85
+
86
+ const validCarrierNames = Array.isArray(state) ? new Set(state.map((c) => c.carrier)) : null;
87
+ const carriersToRender = validCarrierNames
88
+ ? dynamicContext.carriers.filter((carrier) => validCarrierNames.has(carrier.carrier ?? ''))
89
+ : dynamicContext.carriers;
90
+
91
+ return carriersToRender.map((carrier): RadioGroupOption => {
92
+ const name = carrier.carrier ?? '';
93
+ const meta = staticMeta.get(name);
94
+
95
+ return {
96
+ plainLabel: meta?.label ?? name,
97
+ value: name,
98
+ image: meta?.image,
99
+ };
100
+ });
101
+ };
102
+
35
103
  return defineFormField({
36
104
  name: FIELD_CARRIER,
37
105
  label: 'carrier',
38
- ref: createRef<string>(refs, FIELD_CARRIER, PackageTypeName.Package),
106
+ ref: createRef<string>(refs, FIELD_CARRIER, dynamicContext.shop.defaultCarrier ?? undefined),
39
107
  component: resolveFormComponent(AdminComponent.RadioGroup),
40
108
  props: {
41
109
  options: [],
42
110
  },
43
111
 
44
112
  onBeforeMount: async (field) => {
45
- const carrierSelectOptions = await Promise.all(
46
- dynamicContext.carriers.map(async (carrier): Promise<RadioGroupOption> => {
47
- const query = useFetchCarrier(carrier.name);
113
+ // Fetch per-carrier metadata (logo + localized human name) once. Stored in `staticMeta`
114
+ // so the reactive options watcher below can stitch label/image together with the
115
+ // current order-capabilities filter without re-fetching.
116
+ await Promise.all(
117
+ dynamicContext.carriers.map(async (carrier) => {
118
+ const query = useFetchCarrier(carrier.carrier);
119
+
48
120
  await query.suspense();
49
121
 
50
122
  const apiCarrier = toValue(query.data);
51
123
 
52
- let plainLabel = apiCarrier?.human ?? carrier.human ?? '';
53
-
54
- if (!carrier.isDefault) {
55
- plainLabel += ` ${carrier.label ?? translate(`carrier_type_${carrier.type}`)}`;
56
- }
57
-
58
- return {
59
- plainLabel,
60
- value: carrier.externalIdentifier ?? apiCarrier?.name ?? '',
124
+ staticMeta.set(carrier.carrier ?? '', {
125
+ label: apiCarrier?.human ?? translate(`carrier_${snakeCase(carrier.carrier)}`) ?? '',
61
126
  image: apiCarrier?.meta.logo_svg ? createAssetUrl(apiCarrier.meta.logo_svg) : undefined,
62
- };
127
+ });
63
128
  }),
64
129
  );
65
130
 
66
- setFieldProp(field, PROP_OPTIONS, carrierSelectOptions);
131
+ // Reactively rebuild the carrier radio options whenever the order-capabilities query
132
+ // changes state (loading → success, or weight/cc edits triggering a refetch). Carriers
133
+ // that aren't valid for the current order context (e.g. weight below the minimum) are
134
+ // hidden from the radio entirely — preventing the dead-end scenario where the form
135
+ // populates with packageType / deliveryType radios that have no valid combo. The
136
+ // RadioGroup component doesn't honor per-option `disabled` flags, so filter is the
137
+ // only way to express "not available" today.
138
+ //
139
+ // Also refresh package/delivery type dropdowns from the same response: weight/cc edits
140
+ // can shrink the available types for a carrier without changing the chosen carrier, and
141
+ // `afterUpdate` only runs on carrier change. Without this refresh those dropdowns would
142
+ // keep stale options after the order-capabilities response narrows.
143
+ watch(
144
+ () => readOrderCarriers(),
145
+ (state) => {
146
+ setFieldProp(field, PROP_OPTIONS, computeOptions(state));
147
+
148
+ const carrier = capabilities.getCarrierCapabilitiesForOrder(field.form);
149
+
150
+ if (carrier) {
151
+ setFieldProp(field.form, FIELD_PACKAGE_TYPE, PROP_OPTIONS, getPackageTypes(carrier));
152
+ setFieldProp(field.form, FIELD_DELIVERY_TYPE, PROP_OPTIONS, getDeliveryTypes(carrier));
153
+ }
154
+ },
155
+ {immediate: true},
156
+ );
67
157
 
68
158
  await field?.afterUpdate?.(field, toValue(field.ref), undefined);
69
159
 
@@ -73,11 +163,10 @@ export const createCarrierField = (
73
163
  afterUpdate: (field, newCarrier: string) => {
74
164
  updateFieldsDefaults(newCarrier, field, inheritedDeliveryOptions);
75
165
 
76
- setFieldProp(field.form, FIELD_INSURANCE, PROP_OPTIONS, getInsuranceOptions(field, formatter));
77
- setFieldProp(field.form, FIELD_PACKAGE_TYPE, PROP_OPTIONS, getPackageTypes(field.form));
78
- setFieldProp(field.form, FIELD_DELIVERY_TYPE, PROP_OPTIONS, getDeliveryTypes(field.form));
166
+ const carrier = capabilities.getCarrierCapabilitiesForOrder(field.form);
79
167
 
80
- setPostNlAgeCheckSubtext(field);
168
+ setFieldProp(field.form, FIELD_PACKAGE_TYPE, PROP_OPTIONS, getPackageTypes(carrier));
169
+ setFieldProp(field.form, FIELD_DELIVERY_TYPE, PROP_OPTIONS, getDeliveryTypes(carrier));
81
170
  },
82
171
  });
83
172
  };
@@ -2,7 +2,6 @@ import {ref, toValue} from 'vue';
2
2
  import {get} from 'lodash-unified';
3
3
  import {type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
4
4
  import {type Plugin, TriState} from '@myparcel-dev/pdk-common';
5
- import {PackageTypeName} from '@myparcel-dev/constants';
6
5
  import {type ShipmentOptionsRefs} from '../types';
7
6
  import {FIELD_MANUAL_WEIGHT, FIELD_PACKAGE_TYPE} from '../field';
8
7
  import {createDefaultOption, defineFormField, resolveFormComponent} from '../../helpers';
@@ -34,8 +33,9 @@ export const createDigitalStampRangeField = (
34
33
  label: 'weight_range',
35
34
  ref: ref(selectedValue ?? TriState.Inherit),
36
35
  component: resolveFormComponent(AdminComponent.SelectInput),
37
- visibleWhen: (field) => PackageTypeName.DigitalStamp === field.form.getValue(FIELD_PACKAGE_TYPE),
38
- disabledWhen: (field) => PackageTypeName.DigitalStamp !== field.form.getValue(FIELD_PACKAGE_TYPE),
36
+ // @TODO Remove package type checks when the context provides visibility restrictions per option.
37
+ visibleWhen: (field) => field.form.getValue(FIELD_PACKAGE_TYPE) === 'DIGITAL_STAMP',
38
+ disabledWhen: (field) => field.form.getValue(FIELD_PACKAGE_TYPE) !== 'DIGITAL_STAMP',
39
39
  props: {
40
40
  subtext: {
41
41
  key: 'order_weight',
@@ -1,31 +1,52 @@
1
- import { type InteractiveElementConfiguration } from '@myparcel-dev/vue-form-builder';
2
- import { type ShipmentOptionsRefs } from '../types';
3
- import { FIELD_INSURANCE, PROP_OPTIONS, INSURANCE } from '../field';
4
- import {
5
- createHasShipmentOptionWatcher,
6
- getInsuranceOptions,
7
- isPackageTypePackageOrSmall,
8
- resolveFormComponent,
9
- setFieldProp,
10
- } from '../../helpers';
11
- import { type ElementInstance } from '../../../types';
12
- import { AdminComponent } from '../../../data';
13
- import { useLocalizedFormatter } from '../../../composables';
14
- import { createShipmentOptionField } from './createShipmentOptionField';
15
- import { createRef } from './createRef';
1
+ import {watch} from 'vue';
2
+ import {type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
3
+ import {type ShipmentOptionsRefs} from '../types';
4
+ import {PROP_OPTIONS} from '../field';
5
+ import {resolveFormComponent, setFieldProp, useFormCapabilities} from '../../helpers';
6
+ import {type ElementInstance} from '../../../types';
7
+ import {AdminComponent} from '../../../data';
8
+ import {useLocalizedFormatter} from '../../../composables';
9
+ import {createShipmentOptionField} from './createShipmentOptionField';
10
+ import {createRef} from './createRef';
16
11
 
17
- export const createInsuranceField = (refs: ShipmentOptionsRefs): InteractiveElementConfiguration => {
12
+ /**
13
+ * Custom field factory for the insurance shipment option.
14
+ *
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)
17
+ * and refresh automatically whenever that data changes — i.e. on carrier change AND on
18
+ * packageType / deliveryType / weight / cc change, since those all narrow the shipment-scoped
19
+ * capabilities response.
20
+ *
21
+ * Visibility / disabled state is inherited from {@link createShipmentOptionField} — driven
22
+ * purely by `hasShipmentOption` (i.e. by the capabilities response). No manual package-type
23
+ * gating: if insurance isn't valid for the current combination, the API won't include it in
24
+ * `carrier.options` and the field will hide.
25
+ */
26
+ export const createInsuranceField = (
27
+ refs: ShipmentOptionsRefs,
28
+ fieldName: string,
29
+ ): InteractiveElementConfiguration => {
18
30
  const formatter = useLocalizedFormatter();
31
+ const capabilities = useFormCapabilities();
32
+ let stopWatcher: (() => void) | undefined;
19
33
 
20
- return createShipmentOptionField(refs, FIELD_INSURANCE, {
21
- ref: createRef(refs, FIELD_INSURANCE, 0),
34
+ return createShipmentOptionField(refs, fieldName, {
35
+ ref: createRef(refs, fieldName, 0),
22
36
  component: resolveFormComponent(AdminComponent.SelectInput),
23
- visibleWhen: createHasShipmentOptionWatcher(INSURANCE, false, isPackageTypePackageOrSmall),
24
- disabledWhen: createHasShipmentOptionWatcher(INSURANCE, true, isPackageTypePackageOrSmall),
25
37
  onBeforeMount(field: ElementInstance) {
26
- const insurancePossibilities = getInsuranceOptions(field, formatter);
38
+ stopWatcher?.();
27
39
 
28
- setFieldProp(field, PROP_OPTIONS, insurancePossibilities);
40
+ // Watch the shipment-scoped carrier capabilities directly. The resolver tracks both the
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.
43
+ stopWatcher = watch(
44
+ () => capabilities.getCarrierCapabilitiesForShipment(field.form),
45
+ () => {
46
+ setFieldProp(field, PROP_OPTIONS, capabilities.getInsuranceOptions(field.form, formatter));
47
+ },
48
+ {immediate: true},
49
+ );
29
50
  },
30
51
  });
31
52
  };
@@ -1,6 +1,5 @@
1
1
  import {snakeCase} from 'lodash-unified';
2
2
  import {type InteractiveElementConfiguration} from '@myparcel-dev/vue-form-builder';
3
- import {PackageTypeName} from '@myparcel-dev/constants';
4
3
  import {type ShipmentOptionsRefs} from '../types';
5
4
  import {FIELD_PACKAGE_TYPE, SHIPMENT_OPTIONS} from '../field';
6
5
  import {defineFormField, resolveFormComponent} from '../../helpers';
@@ -11,7 +10,7 @@ export const createPackageTypeField = (refs: ShipmentOptionsRefs): InteractiveEl
11
10
  return defineFormField({
12
11
  name: FIELD_PACKAGE_TYPE,
13
12
  label: snakeCase(`${SHIPMENT_OPTIONS}_package_type`),
14
- ref: createRef<PackageTypeName>(refs, FIELD_PACKAGE_TYPE, PackageTypeName.Package),
13
+ ref: createRef<string>(refs, FIELD_PACKAGE_TYPE, 'PACKAGE'),
15
14
  component: resolveFormComponent(AdminComponent.RadioGroup),
16
15
  // options defined in afterUpdate of `createCarrierField`
17
16
  });