@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.
- package/CHANGELOG.md +19 -0
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/exports.spec.ts.snap +8 -1
- package/src/__tests__/doComponentTestTeardown.ts +2 -0
- package/src/__tests__/mocks/index.ts +1 -1
- package/src/__tests__/mocks/mockDefaultAccount.ts +1 -0
- package/src/__tests__/mocks/mockDefaultGlobalContext.ts +2 -2
- package/src/__tests__/mocks/{mockDefaultPlatform.ts → mockDefaultProposition.ts} +1 -3
- package/src/__tests__/mocks/mockDefaultShop.ts +1 -0
- package/src/__tests__/utils/getActionIds.ts +12 -0
- package/src/__tests__/utils/mockOrderData.ts +20 -0
- package/src/actions/composables/mutations/orders/useExportOrdersMutation.ts +1 -1
- package/src/actions/composables/queries/account/useOrderCapabilitiesQuery.ts +67 -0
- package/src/actions/composables/queries/account/useShipmentCapabilitiesQuery.ts +96 -0
- package/src/actions/definitions/orders.ts +6 -8
- package/src/actions/index.ts +1 -0
- package/src/actions/orderModeActions.spec.ts +150 -0
- package/src/actions/orderModeActions.ts +90 -0
- package/src/components/OrderBox/ShipmentOptionsBox.spec.ts +109 -0
- package/src/components/OrderBox/ShipmentOptionsBox.vue +9 -25
- package/src/components/OrderListItem/OrderModeOrderListItem.spec.ts +125 -0
- package/src/components/OrderListItem/OrderModeOrderListItem.vue +10 -6
- package/src/components/PluginSettings/PluginSettingsForms.vue +7 -4
- package/src/components/common/DeliveryOptionsExcerptCarrierName.vue +3 -3
- package/src/components/modals/ShipmentOptionsModal.spec.ts +79 -0
- package/src/components/modals/ShipmentOptionsModal.vue +4 -21
- package/src/composables/components/useToggleInputContext.ts +2 -2
- package/src/composables/components/useTriStateInputContext.ts +2 -2
- package/src/composables/context/index.ts +1 -0
- package/src/composables/context/useOrderMode.spec.ts +75 -0
- package/src/composables/context/useOrderMode.ts +26 -0
- package/src/composables/language/useLanguage.spec.ts +2 -2
- package/src/composables/shipments/useShipmentData.ts +2 -2
- package/src/data/index.ts +1 -0
- package/src/data/orderMode.spec.ts +22 -0
- package/src/data/orderMode.ts +50 -0
- package/src/forms/form-builder/builders/buildAfterUpdate.spec.ts +5 -8
- package/src/forms/form-builder/operations/executeOperations.spec.ts +4 -7
- package/src/forms/form-builder/operations/executeSetPropOperation.spec.ts +4 -7
- package/src/forms/form-builder/operations/executeSetValueOperation.spec.ts +3 -2
- package/src/forms/form-builder/utils/createValueSetter.ts +21 -8
- package/src/forms/helpers/addCapabilitiesClearNotification.ts +39 -0
- package/src/forms/helpers/getDeliveryTypes.ts +5 -10
- package/src/forms/helpers/getFieldLabel.ts +7 -2
- package/src/forms/helpers/getPackageTypes.ts +7 -12
- package/src/forms/helpers/index.ts +3 -10
- package/src/forms/helpers/useFormCapabilities.spec.ts +227 -0
- package/src/forms/helpers/useFormCapabilities.ts +176 -0
- package/src/forms/pluginSettings/createPluginSettingsForm.ts +2 -2
- package/src/forms/pluginSettings/filterPluginSettingsView.spec.ts +158 -0
- package/src/forms/pluginSettings/filterPluginSettingsView.ts +96 -0
- package/src/forms/pluginSettings/index.ts +1 -0
- package/src/forms/pluginSettings/resolveSettingsValues.spec.ts +62 -0
- package/src/forms/pluginSettings/resolveSettingsValues.ts +16 -0
- package/src/forms/shipmentOptions/createShipmentOptionsForm.spec.ts +108 -0
- package/src/forms/shipmentOptions/createShipmentOptionsForm.ts +114 -52
- package/src/forms/shipmentOptions/field.ts +6 -66
- package/src/forms/shipmentOptions/fieldFactoryRegistry.ts +26 -0
- package/src/forms/shipmentOptions/fields/createCarrierField.ts +119 -30
- package/src/forms/shipmentOptions/fields/createDigitalStampRangeField.ts +3 -3
- package/src/forms/shipmentOptions/fields/createInsuranceField.ts +43 -22
- package/src/forms/shipmentOptions/fields/createPackageTypeField.ts +1 -2
- package/src/forms/shipmentOptions/fields/createShipmentOptionField.spec.ts +177 -0
- package/src/forms/shipmentOptions/fields/createShipmentOptionField.ts +90 -8
- package/src/forms/shipmentOptions/fields/index.ts +0 -9
- package/src/forms/shipmentOptions/useCapabilitiesAutoClear.spec.ts +347 -0
- package/src/forms/shipmentOptions/useCapabilitiesAutoClear.ts +452 -0
- package/src/forms/shipmentOptions/useCapabilitiesWatcher.spec.ts +14 -0
- package/src/forms/shipmentOptions/useCapabilitiesWatcher.ts +29 -0
- package/src/forms/shipmentOptions/wireProxyCapabilities.spec.ts +160 -0
- package/src/forms/shipmentOptions/wireProxyCapabilities.ts +102 -0
- package/src/index.ts +2 -0
- package/src/pdk/instance/plugins/createRegisterComponentsPlugin.ts +2 -0
- package/src/sdk/composables/api/useFetchCarrier.ts +39 -4
- package/src/sdk/composables/usePdkAdminApi.ts +8 -3
- package/src/stores/useActionStore.spec.ts +101 -0
- package/src/stores/useActionStore.ts +4 -28
- package/src/stores/useQueryStore.ts +15 -0
- package/src/types/actions/endpoints.types.ts +2 -1
- package/src/types/sdk.types.ts +54 -1
- package/src/utils/forms/formToBody.ts +2 -1
- package/src/utils/forms/getEnabledValues.spec.ts +66 -0
- package/src/utils/forms/getEnabledValues.ts +25 -0
- package/src/utils/forms/index.ts +1 -0
- package/src/utils/translations/getDynamicTranslation.ts +1 -1
- package/src/views/OrderBox.vue +11 -5
- package/src/views/OrderListItem.spec.ts +71 -0
- package/src/views/OrderListItem.vue +6 -7
- package/src/forms/helpers/createHasShipmentOptionWatcher.ts +0 -12
- package/src/forms/helpers/getCarrier.ts +0 -11
- package/src/forms/helpers/getInsuranceOptions.ts +0 -23
- package/src/forms/helpers/hasPostNlAgeCheck.ts +0 -11
- package/src/forms/helpers/hasShipmentOption.ts +0 -8
- package/src/forms/helpers/isPackageTypeMailbox.ts +0 -7
- package/src/forms/helpers/isPackageTypePackage.ts +0 -7
- package/src/forms/helpers/isPackageTypePackageOrSmall.ts +0 -7
- package/src/forms/helpers/isPackageTypeSmallPackage.ts +0 -7
- package/src/forms/helpers/setPostNlAgeCheckSubtext.ts +0 -17
- package/src/forms/shipmentOptions/fields/createAgeCheckField.ts +0 -33
- package/src/forms/shipmentOptions/fields/createDirectReturnField.ts +0 -8
- package/src/forms/shipmentOptions/fields/createHideSenderField.ts +0 -8
- package/src/forms/shipmentOptions/fields/createLargeFormatField.ts +0 -8
- package/src/forms/shipmentOptions/fields/createOnlyRecipientField.ts +0 -21
- package/src/forms/shipmentOptions/fields/createPriorityDeliveryField.ts +0 -12
- package/src/forms/shipmentOptions/fields/createReceiptCodeField.ts +0 -21
- package/src/forms/shipmentOptions/fields/createSameDayDeliveryField.ts +0 -8
- package/src/forms/shipmentOptions/fields/createSignatureField.ts +0 -11
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {ref} from 'vue';
|
|
3
|
+
import {TriState} from '@myparcel-dev/pdk-common';
|
|
4
|
+
import {type InteractiveElementInstance} from '@myparcel-dev/vue-form-builder';
|
|
5
|
+
import {createShipmentOptionField} from './createShipmentOptionField';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mutable carrier context — tests set this to control what the bag's `getCarrierCapabilitiesForShipment`
|
|
9
|
+
* and `hasShipmentOption` return without needing per-test mock overrides.
|
|
10
|
+
*/
|
|
11
|
+
let mockCarrier: Record<string, unknown> | undefined;
|
|
12
|
+
|
|
13
|
+
// Full mock (no importOriginal) to avoid the Vue file import chain
|
|
14
|
+
// that triggers "Install @vitejs/plugin-vue" errors.
|
|
15
|
+
vi.mock('../../helpers', () => ({
|
|
16
|
+
resolveFormComponent: () => 'MockTriStateInput',
|
|
17
|
+
defineFormField: (config: Record<string, unknown>) => config,
|
|
18
|
+
getFieldLabel: (name: string) => name,
|
|
19
|
+
useFormCapabilities: () => ({
|
|
20
|
+
getCarrierCapabilitiesForShipment: () => mockCarrier,
|
|
21
|
+
getCarrierCapabilitiesForOrder: () => mockCarrier,
|
|
22
|
+
hasShipmentOption: (_form: unknown, option: string) => {
|
|
23
|
+
return Object.hasOwn((mockCarrier?.options as Record<string, unknown>) ?? {}, option);
|
|
24
|
+
},
|
|
25
|
+
getInsuranceOptions: () => [],
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
/** Create a minimal mock field instance with a reactive ref and mutable props. */
|
|
30
|
+
const createMockField = (name: string, fieldValue = TriState.Inherit) => {
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
ref: ref(fieldValue),
|
|
34
|
+
form: {} as never,
|
|
35
|
+
props: {readOnly: false, disabled: false},
|
|
36
|
+
} as unknown as InteractiveElementInstance;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Create a mock form that resolves fields by name from the given list. */
|
|
40
|
+
const createMockForm = (fields: InteractiveElementInstance[]) => {
|
|
41
|
+
return {
|
|
42
|
+
getField: (name: string) => fields.find((f) => f.name === name),
|
|
43
|
+
getValue: () => undefined,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('createShipmentOptionField', () => {
|
|
48
|
+
const createRefs = (fieldName: string, value?: unknown) => ({[fieldName]: value});
|
|
49
|
+
|
|
50
|
+
it('creates a field with TriState.Inherit default', () => {
|
|
51
|
+
const fieldName = 'deliveryOptions.shipmentOptions.testOption';
|
|
52
|
+
const refs = createRefs(fieldName);
|
|
53
|
+
|
|
54
|
+
const field = createShipmentOptionField(refs, fieldName);
|
|
55
|
+
|
|
56
|
+
expect(field.name).toBe(fieldName);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('afterUpdate dependency logic', () => {
|
|
60
|
+
/**
|
|
61
|
+
* Helper that sets up a POSTNL carrier with `requires` / `excludes` declared on the
|
|
62
|
+
* `requiresAgeVerification` option (mirroring what the live capabilities response carries),
|
|
63
|
+
* the dependent fields, and a triggerAfterUpdate to invoke the just-toggled age-check field.
|
|
64
|
+
*
|
|
65
|
+
* The `requires` / `excludes` rules live directly on the option entry in the carrier model
|
|
66
|
+
* — sourced from the shipment-scoped capabilities response on the live path.
|
|
67
|
+
*
|
|
68
|
+
* @param optionOverrides - per-option overrides merged into the default carrier options
|
|
69
|
+
* (e.g. `{ requiresSignature: { isRequired: true } }`)
|
|
70
|
+
*/
|
|
71
|
+
const setupAgeVerificationScenario = (optionOverrides: Record<string, Record<string, unknown>> = {}) => {
|
|
72
|
+
const defaultOptions: Record<string, Record<string, unknown>> = {
|
|
73
|
+
requiresAgeVerification: {
|
|
74
|
+
isRequired: false,
|
|
75
|
+
isSelectedByDefault: false,
|
|
76
|
+
requires: ['recipientOnlyDelivery', 'requiresSignature'],
|
|
77
|
+
excludes: ['requiresReceiptCode'],
|
|
78
|
+
},
|
|
79
|
+
requiresSignature: {isRequired: false, isSelectedByDefault: false},
|
|
80
|
+
recipientOnlyDelivery: {isRequired: false, isSelectedByDefault: false},
|
|
81
|
+
requiresReceiptCode: {isRequired: false, isSelectedByDefault: false},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
mockCarrier = {
|
|
85
|
+
carrier: 'POSTNL',
|
|
86
|
+
options: {...defaultOptions, ...optionOverrides},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const signatureField = createMockField('deliveryOptions.shipmentOptions.requiresSignature');
|
|
90
|
+
const onlyRecipientField = createMockField('deliveryOptions.shipmentOptions.recipientOnlyDelivery');
|
|
91
|
+
const receiptCodeField = createMockField('deliveryOptions.shipmentOptions.requiresReceiptCode');
|
|
92
|
+
const form = createMockForm([signatureField, onlyRecipientField, receiptCodeField]);
|
|
93
|
+
|
|
94
|
+
const fieldName = 'deliveryOptions.shipmentOptions.requiresAgeVerification';
|
|
95
|
+
const config = createShipmentOptionField(createRefs(fieldName), fieldName);
|
|
96
|
+
|
|
97
|
+
const triggerAfterUpdate = (value: TriState) => {
|
|
98
|
+
const ageCheckField = {
|
|
99
|
+
...createMockField(fieldName, value),
|
|
100
|
+
form,
|
|
101
|
+
} as unknown as InteractiveElementInstance;
|
|
102
|
+
|
|
103
|
+
config.afterUpdate!(ageCheckField, value, undefined);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {signatureField, onlyRecipientField, receiptCodeField, triggerAfterUpdate};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Enabling 18+ check should force signature + only-recipient ON and lock them.
|
|
110
|
+
it('forces required-dependency fields ON and readOnly when source is enabled', () => {
|
|
111
|
+
const {signatureField, onlyRecipientField, triggerAfterUpdate} = setupAgeVerificationScenario();
|
|
112
|
+
|
|
113
|
+
triggerAfterUpdate(TriState.On);
|
|
114
|
+
|
|
115
|
+
expect(signatureField.ref.value).toBe(TriState.On);
|
|
116
|
+
expect(signatureField.props.readOnly).toBe(true);
|
|
117
|
+
expect(onlyRecipientField.ref.value).toBe(TriState.On);
|
|
118
|
+
expect(onlyRecipientField.props.readOnly).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Enabling 18+ check should force receipt code OFF and lock it.
|
|
122
|
+
it('forces excluded-dependency fields OFF and readOnly when source is enabled', () => {
|
|
123
|
+
const {receiptCodeField, triggerAfterUpdate} = setupAgeVerificationScenario();
|
|
124
|
+
|
|
125
|
+
triggerAfterUpdate(TriState.On);
|
|
126
|
+
|
|
127
|
+
expect(receiptCodeField.ref.value).toBe(TriState.Off);
|
|
128
|
+
expect(receiptCodeField.props.readOnly).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Disabling 18+ check should unlock dependent fields so the user can edit them again.
|
|
132
|
+
it('clears readOnly on dependent fields when source is disabled', () => {
|
|
133
|
+
const {signatureField, onlyRecipientField, receiptCodeField, triggerAfterUpdate} =
|
|
134
|
+
setupAgeVerificationScenario();
|
|
135
|
+
|
|
136
|
+
// First enable to lock the fields…
|
|
137
|
+
triggerAfterUpdate(TriState.On);
|
|
138
|
+
// …then disable to unlock them.
|
|
139
|
+
triggerAfterUpdate(TriState.Off);
|
|
140
|
+
|
|
141
|
+
expect(signatureField.props.readOnly).toBe(false);
|
|
142
|
+
expect(onlyRecipientField.props.readOnly).toBe(false);
|
|
143
|
+
expect(receiptCodeField.props.readOnly).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// When the carrier independently requires an option (isRequired: true), disabling the
|
|
147
|
+
// dependency source must NOT clear that field's readOnly or change its value. Regression:
|
|
148
|
+
// previously, turning off 18+ check would blindly set readOnly = false on all dependents,
|
|
149
|
+
// overriding the carrier's isRequired lock.
|
|
150
|
+
it('preserves readOnly and value on carrier-required fields when dependency source is disabled', () => {
|
|
151
|
+
const {signatureField, onlyRecipientField, receiptCodeField, triggerAfterUpdate} =
|
|
152
|
+
setupAgeVerificationScenario({
|
|
153
|
+
// Signature is independently required by the carrier
|
|
154
|
+
requiresSignature: {isRequired: true, isSelectedByDefault: false},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// First enable to lock all dependent fields…
|
|
158
|
+
triggerAfterUpdate(TriState.On);
|
|
159
|
+
|
|
160
|
+
// Verify signature was forced ON by the dependency.
|
|
161
|
+
expect(signatureField.ref.value).toBe(TriState.On);
|
|
162
|
+
expect(signatureField.props.readOnly).toBe(true);
|
|
163
|
+
|
|
164
|
+
// …then disable — signature should stay locked and ON, others should unlock.
|
|
165
|
+
triggerAfterUpdate(TriState.Off);
|
|
166
|
+
|
|
167
|
+
// Signature must remain ON and readOnly because the carrier requires it, regardless of
|
|
168
|
+
// the 18+ dependency being turned off.
|
|
169
|
+
expect(signatureField.ref.value).toBe(TriState.On);
|
|
170
|
+
expect(signatureField.props.readOnly).toBe(true);
|
|
171
|
+
// Only-recipient is not independently required, so it should unlock.
|
|
172
|
+
expect(onlyRecipientField.props.readOnly).toBe(false);
|
|
173
|
+
// Receipt code is an excluded dep, also not independently required.
|
|
174
|
+
expect(receiptCodeField.props.readOnly).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -1,25 +1,107 @@
|
|
|
1
|
-
import {type
|
|
2
|
-
import {
|
|
1
|
+
import {type InteractiveElementConfiguration, type InteractiveElementInstance} from '@myparcel-dev/vue-form-builder';
|
|
2
|
+
import {TriState} from '@myparcel-dev/pdk-common';
|
|
3
3
|
import {type ShipmentOptionsRefs} from '../types';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {FIELD_SHIPMENT_OPTIONS_PREFIX} from '../field';
|
|
5
|
+
import {defineFormField, getFieldLabel, resolveFormComponent, useFormCapabilities} from '../../helpers';
|
|
6
|
+
import {setFieldRef} from '../../form-builder/utils/createValueSetter';
|
|
6
7
|
import {AdminComponent} from '../../../data';
|
|
7
8
|
import {createRef} from './createRef';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Creates a generic shipment option field as a TriState toggle.
|
|
12
|
+
*
|
|
13
|
+
* Default factory for any option from `carrier.options` that doesn't have a custom factory in
|
|
14
|
+
* `fieldFactoryRegistry`. `requires` / `excludes` are sourced from the shipment-scoped
|
|
15
|
+
* response (`getCarrierCapabilitiesForShipment`); they're absent while the shipment query is loading or
|
|
16
|
+
* falling back to order-level data — locking doesn't run during those windows.
|
|
17
|
+
*
|
|
18
|
+
* `useFormCapabilities()` is called at factory invocation (setup-time) so visibility, disabled,
|
|
19
|
+
* readOnly and afterUpdate handlers close over a captured Pinia store + orderId — no
|
|
20
|
+
* `inject()` calls during form-builder's watchEffect-driven re-evaluators.
|
|
21
|
+
*/
|
|
9
22
|
export const createShipmentOptionField = (
|
|
10
23
|
refs: ShipmentOptionsRefs,
|
|
11
|
-
fieldName:
|
|
24
|
+
fieldName: string,
|
|
12
25
|
config?: Partial<InteractiveElementConfiguration>,
|
|
13
26
|
): InteractiveElementConfiguration => {
|
|
14
|
-
const name =
|
|
27
|
+
const name = fieldName.split('.').pop() ?? fieldName;
|
|
28
|
+
const capabilities = useFormCapabilities();
|
|
15
29
|
|
|
16
30
|
return defineFormField({
|
|
17
31
|
name: fieldName,
|
|
18
32
|
component: resolveFormComponent(AdminComponent.TriStateInput),
|
|
19
33
|
ref: createRef(refs, fieldName, TriState.Inherit),
|
|
20
34
|
label: getFieldLabel(name),
|
|
21
|
-
visibleWhen:
|
|
22
|
-
|
|
35
|
+
visibleWhen: ({form}) => capabilities.hasShipmentOption(form, name),
|
|
36
|
+
// Disabled when the carrier doesn't support this option.
|
|
37
|
+
// Required options use readOnlyWhen instead, keeping them enabled
|
|
38
|
+
// so their value is included in the form body by getEnabledValues().
|
|
39
|
+
disabledWhen: ({form}) => !capabilities.hasShipmentOption(form, name),
|
|
40
|
+
// Read-only prevents the TriState "inherit" toggle from being used,
|
|
41
|
+
// fully locking the field when the carrier requires this option.
|
|
42
|
+
readOnlyWhen: ({form}) => {
|
|
43
|
+
return capabilities.getCarrierCapabilitiesForShipment(form)?.options?.[name]?.isRequired === true;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
afterUpdate(field, value) {
|
|
47
|
+
const carrier = capabilities.getCarrierCapabilitiesForShipment(field.form);
|
|
48
|
+
const option = carrier?.options?.[name];
|
|
49
|
+
|
|
50
|
+
if (!option) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Enforce isRequired: revert any user change back to TriState.On.
|
|
55
|
+
if (option.isRequired === true && value !== TriState.On) {
|
|
56
|
+
setFieldRef(field, TriState.On);
|
|
57
|
+
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isEnabled = TriState.On === value;
|
|
62
|
+
|
|
63
|
+
for (const requiredOption of option.requires ?? []) {
|
|
64
|
+
const targetFieldName = `${FIELD_SHIPMENT_OPTIONS_PREFIX}.${requiredOption}`;
|
|
65
|
+
const targetField = field.form.getField(targetFieldName);
|
|
66
|
+
|
|
67
|
+
if (!targetField) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isEnabled) {
|
|
72
|
+
setFieldRef(targetField as InteractiveElementInstance, TriState.On);
|
|
73
|
+
targetField.props.readOnly = true;
|
|
74
|
+
} else {
|
|
75
|
+
// Only clear readOnly if the carrier doesn't independently require this option.
|
|
76
|
+
const isRequiredByCarrier = carrier?.options?.[requiredOption]?.isRequired === true;
|
|
77
|
+
|
|
78
|
+
if (!isRequiredByCarrier) {
|
|
79
|
+
targetField.props.readOnly = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const excludedOption of option.excludes ?? []) {
|
|
85
|
+
const targetFieldName = `${FIELD_SHIPMENT_OPTIONS_PREFIX}.${excludedOption}`;
|
|
86
|
+
const targetField = field.form.getField(targetFieldName);
|
|
87
|
+
|
|
88
|
+
if (!targetField) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isEnabled) {
|
|
93
|
+
setFieldRef(targetField as InteractiveElementInstance, TriState.Off);
|
|
94
|
+
targetField.props.readOnly = true;
|
|
95
|
+
} else {
|
|
96
|
+
const isRequiredByCarrier = carrier?.options?.[excludedOption]?.isRequired === true;
|
|
97
|
+
|
|
98
|
+
if (!isRequiredByCarrier) {
|
|
99
|
+
targetField.props.readOnly = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
23
105
|
...config,
|
|
24
106
|
});
|
|
25
107
|
};
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
export * from './createAgeCheckField';
|
|
2
|
-
export * from './createCarrierField';
|
|
3
1
|
export * from './createDigitalStampRangeField';
|
|
4
|
-
export * from './createDirectReturnField';
|
|
5
2
|
export * from './createInsuranceField';
|
|
6
3
|
export * from './createLabelAmountField';
|
|
7
|
-
export * from './createLargeFormatField';
|
|
8
|
-
export * from './createOnlyRecipientField';
|
|
9
4
|
export * from './createPackageTypeField';
|
|
10
|
-
export * from './createPriorityDeliveryField';
|
|
11
5
|
export * from './createRef';
|
|
12
|
-
export * from './createSameDayDeliveryField';
|
|
13
6
|
export * from './createShipmentOptionField';
|
|
14
|
-
export * from './createSignatureField';
|
|
15
|
-
export * from './createReceiptCodeField';
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest';
|
|
2
|
+
import {effectScope, nextTick, ref, type Ref} from 'vue';
|
|
3
|
+
import {BackendEndpoint, type CarrierModel, TriState} from '@myparcel-dev/pdk-common';
|
|
4
|
+
import {FIELD_CARRIER, FIELD_DELIVERY_TYPE, FIELD_PACKAGE_TYPE, FIELD_SHIPMENT_OPTIONS_PREFIX} from './field';
|
|
5
|
+
|
|
6
|
+
type FakeQuery = {
|
|
7
|
+
status: ReturnType<typeof ref<string>>;
|
|
8
|
+
data: ReturnType<typeof ref<unknown>>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const queries = new Map<string, FakeQuery>();
|
|
12
|
+
|
|
13
|
+
const queryStoreMock = {
|
|
14
|
+
has: (endpoint: BackendEndpoint, modifier: string) => queries.has(`${endpoint}.${modifier}`),
|
|
15
|
+
get: (endpoint: BackendEndpoint, modifier: string) =>
|
|
16
|
+
queries.get(`${endpoint}.${modifier}`) ?? {status: ref('idle'), data: ref(undefined)},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create the shipment-query entry up front (in `loading` state) so the auto-clear's watcher
|
|
21
|
+
* sees a registered query at setup; subsequent state transitions mutate the same refs and
|
|
22
|
+
* are reactively visible to the watcher.
|
|
23
|
+
*/
|
|
24
|
+
const registerShipmentQuery = (orderId: string): FakeQuery => {
|
|
25
|
+
const entry: FakeQuery = {status: ref('loading'), data: ref(undefined)};
|
|
26
|
+
|
|
27
|
+
queries.set(`${BackendEndpoint.ProxyCapabilities}.${orderId}.shipment`, entry);
|
|
28
|
+
|
|
29
|
+
return entry;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const notificationStoreMock = {
|
|
33
|
+
add: vi.fn(),
|
|
34
|
+
remove: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const translateMock = vi.fn((key: unknown) => {
|
|
38
|
+
if (typeof key === 'string') return key;
|
|
39
|
+
const k = (key as {key: string}).key;
|
|
40
|
+
const a = (key as {args?: Record<string, string>}).args;
|
|
41
|
+
|
|
42
|
+
return a ? `${k}(${Object.entries(a).map(([k2, v]) => `${k2}=${v}`).join(',')})` : k;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const getCarrierCapabilitiesForOrderMock = vi.fn();
|
|
46
|
+
const addCapabilitiesClearNotificationMock = vi.fn();
|
|
47
|
+
|
|
48
|
+
vi.mock('../../stores', () => ({
|
|
49
|
+
useQueryStore: () => queryStoreMock,
|
|
50
|
+
useNotificationStore: () => notificationStoreMock,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
vi.mock('../../composables', () => ({
|
|
54
|
+
useLanguage: () => ({translate: translateMock}),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('../helpers', () => ({
|
|
58
|
+
addCapabilitiesClearNotification: addCapabilitiesClearNotificationMock,
|
|
59
|
+
CAPABILITIES_CLEARED_NOTIFICATION_ID: 'capabilities_cleared',
|
|
60
|
+
useFormCapabilities: () => ({getCarrierCapabilitiesForOrder: getCarrierCapabilitiesForOrderMock}),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const setFieldRefMock = vi.fn();
|
|
64
|
+
|
|
65
|
+
vi.mock('../form-builder/utils/createValueSetter', () => ({
|
|
66
|
+
setFieldRef: setFieldRefMock,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const buildForm = (initial: Record<string, unknown>) => {
|
|
70
|
+
const fieldByName = new Map<string, {name: string; ref: Ref<unknown>}>();
|
|
71
|
+
|
|
72
|
+
Object.keys(initial).forEach((name) => {
|
|
73
|
+
fieldByName.set(name, {name, ref: ref(initial[name])});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
// `state` proxies into each field's ref so test assertions read the same source the
|
|
78
|
+
// auto-clear is watching — `state[name]` reads `fieldByName.get(name).ref.value`.
|
|
79
|
+
state: new Proxy({} as Record<string, unknown>, {
|
|
80
|
+
get: (_, name: string) => fieldByName.get(name)?.ref.value,
|
|
81
|
+
}),
|
|
82
|
+
getValue: <T,>(name: string): T => fieldByName.get(name)?.ref.value as T,
|
|
83
|
+
getField: (name: string) => {
|
|
84
|
+
if (!fieldByName.has(name)) {
|
|
85
|
+
fieldByName.set(name, {name, ref: ref(undefined)});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return fieldByName.get(name);
|
|
89
|
+
},
|
|
90
|
+
setExternally: (name: string, value: unknown) => {
|
|
91
|
+
const field = fieldByName.get(name);
|
|
92
|
+
|
|
93
|
+
if (field) {
|
|
94
|
+
(field.ref as Ref<unknown>).value = value;
|
|
95
|
+
} else {
|
|
96
|
+
fieldByName.set(name, {name, ref: ref(value)});
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
queries.clear();
|
|
104
|
+
notificationStoreMock.add.mockClear();
|
|
105
|
+
notificationStoreMock.remove.mockClear();
|
|
106
|
+
translateMock.mockClear();
|
|
107
|
+
getCarrierCapabilitiesForOrderMock.mockReset();
|
|
108
|
+
addCapabilitiesClearNotificationMock.mockClear();
|
|
109
|
+
setFieldRefMock.mockReset();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const buildCarrier = (overrides: Partial<CarrierModel>): CarrierModel =>
|
|
113
|
+
({
|
|
114
|
+
carrier: 'POSTNL',
|
|
115
|
+
packageTypes: [],
|
|
116
|
+
deliveryTypes: [],
|
|
117
|
+
options: {},
|
|
118
|
+
...overrides,
|
|
119
|
+
}) as CarrierModel;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wire `setFieldRef` to mutate the form state so the auto-clear's silent rewrites surface
|
|
123
|
+
* via subsequent `form.getValue(...)` reads.
|
|
124
|
+
*/
|
|
125
|
+
const wireSetFieldRef = (form: ReturnType<typeof buildForm>) => {
|
|
126
|
+
setFieldRefMock.mockImplementation((field: {name: string}, value: unknown) => {
|
|
127
|
+
form.setExternally(field.name, value);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
describe('useCapabilitiesAutoClear', () => {
|
|
132
|
+
it('on carrier change with shipment-empty: applies inherited defaults silently (no notification)', async () => {
|
|
133
|
+
const form = buildForm({
|
|
134
|
+
[FIELD_CARRIER]: 'DPD',
|
|
135
|
+
[FIELD_PACKAGE_TYPE]: 'PACKAGE',
|
|
136
|
+
[FIELD_DELIVERY_TYPE]: 'PICKUP',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
wireSetFieldRef(form);
|
|
140
|
+
// Order-query "available carriers" — getCarrierCapabilitiesForOrder resolves through form.getValue(FIELD_CARRIER)
|
|
141
|
+
// so the order watcher sees fresh data for whichever carrier is currently selected.
|
|
142
|
+
const orderCarriers = [
|
|
143
|
+
buildCarrier({carrier: 'DPD', packageTypes: ['PACKAGE'], deliveryTypes: ['PICKUP']}),
|
|
144
|
+
buildCarrier({carrier: 'POSTNL', packageTypes: ['PACKAGE'], deliveryTypes: ['STANDARD']}),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
getCarrierCapabilitiesForOrderMock.mockImplementation((f: {getValue: (n: string) => unknown}) =>
|
|
148
|
+
orderCarriers.find((c) => c.carrier === f.getValue(FIELD_CARRIER)),
|
|
149
|
+
);
|
|
150
|
+
const shipmentEntry = registerShipmentQuery('order-1');
|
|
151
|
+
|
|
152
|
+
const selection = ref({carrier: 'DPD', packageType: 'PACKAGE', deliveryType: 'PICKUP'}) as Ref<{
|
|
153
|
+
carrier?: string;
|
|
154
|
+
packageType?: string;
|
|
155
|
+
deliveryType?: string;
|
|
156
|
+
}>;
|
|
157
|
+
|
|
158
|
+
const scope = effectScope();
|
|
159
|
+
const {useCapabilitiesAutoClear} = await import('./useCapabilitiesAutoClear');
|
|
160
|
+
|
|
161
|
+
scope.run(() => {
|
|
162
|
+
useCapabilitiesAutoClear(
|
|
163
|
+
form as never,
|
|
164
|
+
['signature'],
|
|
165
|
+
'order-1',
|
|
166
|
+
{POSTNL: {packageType: 'PACKAGE', deliveryType: 'STANDARD'}} as never,
|
|
167
|
+
selection,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// User switches DPD → POSTNL; new combo not in shipment results.
|
|
172
|
+
form.setExternally(FIELD_CARRIER, 'POSTNL');
|
|
173
|
+
selection.value = {carrier: 'POSTNL', packageType: 'PACKAGE', deliveryType: 'PICKUP'};
|
|
174
|
+
shipmentEntry.status.value = 'success';
|
|
175
|
+
shipmentEntry.data.value = [];
|
|
176
|
+
await nextTick();
|
|
177
|
+
|
|
178
|
+
expect(form.state[FIELD_PACKAGE_TYPE]).toBe('PACKAGE');
|
|
179
|
+
expect(form.state[FIELD_DELIVERY_TYPE]).toBe('STANDARD');
|
|
180
|
+
expect(addCapabilitiesClearNotificationMock).not.toHaveBeenCalled();
|
|
181
|
+
|
|
182
|
+
scope.stop();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('on packageType change with shipment-empty: reverts that axis to its prior value WITH notification', async () => {
|
|
186
|
+
const form = buildForm({
|
|
187
|
+
[FIELD_CARRIER]: 'POSTNL',
|
|
188
|
+
[FIELD_PACKAGE_TYPE]: 'PACKAGE',
|
|
189
|
+
[FIELD_DELIVERY_TYPE]: 'STANDARD',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
wireSetFieldRef(form);
|
|
193
|
+
getCarrierCapabilitiesForOrderMock.mockReturnValue(
|
|
194
|
+
buildCarrier({carrier: 'POSTNL', packageTypes: ['PACKAGE', 'MAILBOX'], deliveryTypes: ['STANDARD']}),
|
|
195
|
+
);
|
|
196
|
+
const shipmentEntry = registerShipmentQuery('order-1');
|
|
197
|
+
|
|
198
|
+
const selection = ref({carrier: 'POSTNL', packageType: 'PACKAGE', deliveryType: 'STANDARD'}) as Ref<{
|
|
199
|
+
carrier?: string;
|
|
200
|
+
packageType?: string;
|
|
201
|
+
deliveryType?: string;
|
|
202
|
+
}>;
|
|
203
|
+
|
|
204
|
+
const scope = effectScope();
|
|
205
|
+
const {useCapabilitiesAutoClear} = await import('./useCapabilitiesAutoClear');
|
|
206
|
+
|
|
207
|
+
scope.run(() => {
|
|
208
|
+
useCapabilitiesAutoClear(form as never, ['signature'], 'order-1', {} as never, selection);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// User changes packageType to MAILBOX. Shipment query confirms invalid combo.
|
|
212
|
+
form.setExternally(FIELD_PACKAGE_TYPE, 'MAILBOX');
|
|
213
|
+
selection.value = {carrier: 'POSTNL', packageType: 'MAILBOX', deliveryType: 'STANDARD'};
|
|
214
|
+
shipmentEntry.status.value = 'success';
|
|
215
|
+
shipmentEntry.data.value = [];
|
|
216
|
+
await nextTick();
|
|
217
|
+
|
|
218
|
+
expect(form.state[FIELD_PACKAGE_TYPE]).toBe('PACKAGE');
|
|
219
|
+
expect(addCapabilitiesClearNotificationMock).toHaveBeenCalledTimes(1);
|
|
220
|
+
|
|
221
|
+
const lines = addCapabilitiesClearNotificationMock.mock.calls[0][2] as string[];
|
|
222
|
+
|
|
223
|
+
expect(lines[0]).toContain('capabilities_cleared_field_reverted');
|
|
224
|
+
|
|
225
|
+
scope.stop();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('on packageType change with no prior valid value: clears the field and notifies cleared (not reverted)', async () => {
|
|
229
|
+
const form = buildForm({
|
|
230
|
+
[FIELD_CARRIER]: 'POSTNL',
|
|
231
|
+
[FIELD_PACKAGE_TYPE]: undefined,
|
|
232
|
+
[FIELD_DELIVERY_TYPE]: 'STANDARD',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
wireSetFieldRef(form);
|
|
236
|
+
getCarrierCapabilitiesForOrderMock.mockReturnValue(
|
|
237
|
+
buildCarrier({carrier: 'POSTNL', packageTypes: ['MAILBOX'], deliveryTypes: ['STANDARD']}),
|
|
238
|
+
);
|
|
239
|
+
const shipmentEntry = registerShipmentQuery('order-1');
|
|
240
|
+
|
|
241
|
+
const selection = ref({carrier: 'POSTNL', packageType: undefined, deliveryType: 'STANDARD'}) as Ref<{
|
|
242
|
+
carrier?: string;
|
|
243
|
+
packageType?: string;
|
|
244
|
+
deliveryType?: string;
|
|
245
|
+
}>;
|
|
246
|
+
|
|
247
|
+
const scope = effectScope();
|
|
248
|
+
const {useCapabilitiesAutoClear} = await import('./useCapabilitiesAutoClear');
|
|
249
|
+
|
|
250
|
+
scope.run(() => {
|
|
251
|
+
useCapabilitiesAutoClear(form as never, [], 'order-1', {} as never, selection);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// First-ever packageType pick is MAILBOX → invalid combo, no prior valid value to revert to.
|
|
255
|
+
form.setExternally(FIELD_PACKAGE_TYPE, 'MAILBOX');
|
|
256
|
+
selection.value = {carrier: 'POSTNL', packageType: 'MAILBOX', deliveryType: 'STANDARD'};
|
|
257
|
+
shipmentEntry.status.value = 'success';
|
|
258
|
+
shipmentEntry.data.value = [];
|
|
259
|
+
await nextTick();
|
|
260
|
+
|
|
261
|
+
expect(form.state[FIELD_PACKAGE_TYPE]).toBeUndefined();
|
|
262
|
+
const lines = addCapabilitiesClearNotificationMock.mock.calls[0][2] as string[];
|
|
263
|
+
|
|
264
|
+
expect(lines[0]).toContain('capabilities_cleared_field_cleared');
|
|
265
|
+
|
|
266
|
+
scope.stop();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('on shipment success+matched: clears orphaned active options silently when carrier changed', async () => {
|
|
270
|
+
const optionField = `${FIELD_SHIPMENT_OPTIONS_PREFIX}.signature`;
|
|
271
|
+
const form = buildForm({
|
|
272
|
+
[FIELD_CARRIER]: 'DPD',
|
|
273
|
+
[FIELD_PACKAGE_TYPE]: 'PACKAGE',
|
|
274
|
+
[FIELD_DELIVERY_TYPE]: 'PICKUP',
|
|
275
|
+
[optionField]: TriState.On,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
wireSetFieldRef(form);
|
|
279
|
+
getCarrierCapabilitiesForOrderMock.mockReturnValue(buildCarrier({carrier: 'DPD'}));
|
|
280
|
+
const shipmentEntry = registerShipmentQuery('order-1');
|
|
281
|
+
|
|
282
|
+
const selection = ref({carrier: 'DPD', packageType: 'PACKAGE', deliveryType: 'PICKUP'}) as Ref<{
|
|
283
|
+
carrier?: string;
|
|
284
|
+
packageType?: string;
|
|
285
|
+
deliveryType?: string;
|
|
286
|
+
}>;
|
|
287
|
+
|
|
288
|
+
const scope = effectScope();
|
|
289
|
+
const {useCapabilitiesAutoClear} = await import('./useCapabilitiesAutoClear');
|
|
290
|
+
|
|
291
|
+
scope.run(() => {
|
|
292
|
+
useCapabilitiesAutoClear(form as never, ['signature'], 'order-1', {} as never, selection);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// User switches to POSTNL; shipment response success but POSTNL.options has no signature.
|
|
296
|
+
form.setExternally(FIELD_CARRIER, 'POSTNL');
|
|
297
|
+
selection.value = {carrier: 'POSTNL', packageType: 'PACKAGE', deliveryType: 'PICKUP'};
|
|
298
|
+
shipmentEntry.status.value = 'success';
|
|
299
|
+
shipmentEntry.data.value = [buildCarrier({carrier: 'POSTNL', options: {}})];
|
|
300
|
+
await nextTick();
|
|
301
|
+
|
|
302
|
+
expect(form.state[optionField]).toBe(TriState.Inherit);
|
|
303
|
+
// Silent — option clearing during a carrier change does not produce a notification.
|
|
304
|
+
expect(addCapabilitiesClearNotificationMock).not.toHaveBeenCalled();
|
|
305
|
+
|
|
306
|
+
scope.stop();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('readShipmentSnapshot uses the debounced selection.carrier (not form.getValue) to avoid the race', async () => {
|
|
310
|
+
const form = buildForm({
|
|
311
|
+
[FIELD_CARRIER]: 'POSTNL',
|
|
312
|
+
[FIELD_PACKAGE_TYPE]: 'PACKAGE',
|
|
313
|
+
[FIELD_DELIVERY_TYPE]: 'STANDARD',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
wireSetFieldRef(form);
|
|
317
|
+
getCarrierCapabilitiesForOrderMock.mockReturnValue(
|
|
318
|
+
buildCarrier({carrier: 'POSTNL', packageTypes: ['PACKAGE'], deliveryTypes: ['STANDARD']}),
|
|
319
|
+
);
|
|
320
|
+
const shipmentEntry = registerShipmentQuery('order-1');
|
|
321
|
+
|
|
322
|
+
// Form (live) is on POSTNL; debounced selection still on DPD; shipment results contain only DPD.
|
|
323
|
+
const selection = ref({carrier: 'DPD', packageType: 'PACKAGE', deliveryType: 'PICKUP'}) as Ref<{
|
|
324
|
+
carrier?: string;
|
|
325
|
+
packageType?: string;
|
|
326
|
+
deliveryType?: string;
|
|
327
|
+
}>;
|
|
328
|
+
|
|
329
|
+
const scope = effectScope();
|
|
330
|
+
const {useCapabilitiesAutoClear} = await import('./useCapabilitiesAutoClear');
|
|
331
|
+
|
|
332
|
+
scope.run(() => {
|
|
333
|
+
useCapabilitiesAutoClear(form as never, [], 'order-1', {} as never, selection);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
shipmentEntry.status.value = 'success';
|
|
337
|
+
shipmentEntry.data.value = [buildCarrier({carrier: 'DPD', options: {}})];
|
|
338
|
+
await nextTick();
|
|
339
|
+
|
|
340
|
+
// Form's live POSTNL doesn't match DPD-only results, but the snapshot is computed against
|
|
341
|
+
// the debounced selection (DPD), which matches → state === 'matched', no rollback fires.
|
|
342
|
+
expect(addCapabilitiesClearNotificationMock).not.toHaveBeenCalled();
|
|
343
|
+
expect(form.state[FIELD_PACKAGE_TYPE]).toBe('PACKAGE');
|
|
344
|
+
|
|
345
|
+
scope.stop();
|
|
346
|
+
});
|
|
347
|
+
});
|