@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,102 @@
|
|
|
1
|
+
import {computed, onScopeDispose, type Ref} from 'vue';
|
|
2
|
+
import {type FormInstance} from '@myparcel-dev/vue-form-builder';
|
|
3
|
+
import {BackendEndpoint, type Plugin} from '@myparcel-dev/pdk-common';
|
|
4
|
+
import {globalLogger} from '../../services';
|
|
5
|
+
import {useQueryStore} from '../../stores';
|
|
6
|
+
import {
|
|
7
|
+
useShipmentCapabilitiesQuery,
|
|
8
|
+
type CapabilitiesSelection,
|
|
9
|
+
} from '../../actions/composables/queries/account/useShipmentCapabilitiesQuery';
|
|
10
|
+
import {
|
|
11
|
+
useOrderCapabilitiesQuery,
|
|
12
|
+
type OrderCapabilitiesInput,
|
|
13
|
+
} from '../../actions/composables/queries/account/useOrderCapabilitiesQuery';
|
|
14
|
+
import {useCapabilitiesWatcher, type FormInput, type OrderInput} from './useCapabilitiesWatcher';
|
|
15
|
+
import {FIELD_CARRIER, FIELD_DELIVERY_TYPE, FIELD_MANUAL_WEIGHT, FIELD_PACKAGE_TYPE} from './field';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wire BOTH capabilities queries — order-scoped (cc + weight) and shipment-scoped (full
|
|
19
|
+
* selection) — into the shipment-options form for a single order, register them in the query
|
|
20
|
+
* store under composite-string modifiers (`${orderId}.order` and `${orderId}.shipment`), and
|
|
21
|
+
* tear them down when the form's scope disposes (modal close).
|
|
22
|
+
*
|
|
23
|
+
* The two queries serve different concerns:
|
|
24
|
+
*
|
|
25
|
+
* - **Order query** drives the carrier / packageType / deliveryType dropdowns from each
|
|
26
|
+
* carrier's flat union arrays. Refetches only when destination or weight change.
|
|
27
|
+
* - **Shipment query** drives per-option metadata (`isRequired`, `requires`, `excludes`,
|
|
28
|
+
* `insuredAmount`) for the chosen combination. Refetches when any axis of the selection
|
|
29
|
+
* changes; the empty-result case is the invalid-combo signal consumed by
|
|
30
|
+
* `useCapabilitiesAutoClear`.
|
|
31
|
+
*
|
|
32
|
+
* Manual-weight reactivity: weight is sourced from `FIELD_MANUAL_WEIGHT` when the user has set
|
|
33
|
+
* one, otherwise from `order.physicalProperties.initialWeight`. Both queries refetch when
|
|
34
|
+
* weight changes through the slider.
|
|
35
|
+
*
|
|
36
|
+
* Skipped for bulk forms (no single orderId) and for orders without an externalIdentifier (no
|
|
37
|
+
* key to register under).
|
|
38
|
+
*/
|
|
39
|
+
export const wireProxyCapabilities = (
|
|
40
|
+
form: FormInstance,
|
|
41
|
+
order: Plugin.ModelContextOrderDataContext,
|
|
42
|
+
): {selection: Readonly<Ref<CapabilitiesSelection>>} | undefined => {
|
|
43
|
+
const orderId = order.externalIdentifier;
|
|
44
|
+
|
|
45
|
+
if (!orderId) {
|
|
46
|
+
globalLogger.debug('wireProxyCapabilities', 'skipped — order has no externalIdentifier', {order});
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const orderModifier = `${orderId}.order`;
|
|
52
|
+
const shipmentModifier = `${orderId}.shipment`;
|
|
53
|
+
|
|
54
|
+
// Use `form.getValue(name)` (which goes through `q(field.ref)` / `toValue` internally) rather
|
|
55
|
+
// than `form.values[name]` so reactive deps are tracked correctly inside computed/watchers.
|
|
56
|
+
const orderInput = computed<OrderInput>(() => {
|
|
57
|
+
// FIELD_MANUAL_WEIGHT carries `TriState.Inherit` (-1) when the user hasn't entered a manual
|
|
58
|
+
// override; only positive numbers represent an actual weight. Treat anything else as "unset"
|
|
59
|
+
// and fall back to the order's initial weight.
|
|
60
|
+
const manualWeightRaw = form.getValue(FIELD_MANUAL_WEIGHT);
|
|
61
|
+
const manualWeight = typeof manualWeightRaw === 'number' && manualWeightRaw > 0 ? manualWeightRaw : undefined;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
cc: order.shippingAddress?.cc,
|
|
65
|
+
weight: manualWeight ?? order.physicalProperties?.initialWeight,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const formInput = computed<FormInput>(() => ({
|
|
70
|
+
carrier: form.getValue<string | undefined>(FIELD_CARRIER),
|
|
71
|
+
packageType: form.getValue<string | undefined>(FIELD_PACKAGE_TYPE),
|
|
72
|
+
deliveryType: form.getValue<string | undefined>(FIELD_DELIVERY_TYPE),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const selection = useCapabilitiesWatcher(orderInput, formInput);
|
|
76
|
+
|
|
77
|
+
const orderInputForQuery = computed<OrderCapabilitiesInput>(() => ({
|
|
78
|
+
cc: selection.value.cc,
|
|
79
|
+
weight: selection.value.weight,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const orderQuery = useOrderCapabilitiesQuery(orderInputForQuery);
|
|
83
|
+
const shipmentQuery = useShipmentCapabilitiesQuery(selection);
|
|
84
|
+
|
|
85
|
+
const queryStore = useQueryStore();
|
|
86
|
+
|
|
87
|
+
queryStore.register(BackendEndpoint.ProxyCapabilities, orderModifier, orderQuery);
|
|
88
|
+
queryStore.register(BackendEndpoint.ProxyCapabilities, shipmentModifier, shipmentQuery);
|
|
89
|
+
|
|
90
|
+
// Drop both store entries when the form scope disposes (modal close). Without this, reopening
|
|
91
|
+
// the modal for the same order would surface the prior session's stale data because
|
|
92
|
+
// `register` returns the existing entry on second registration.
|
|
93
|
+
onScopeDispose(() => {
|
|
94
|
+
queryStore.unregister(BackendEndpoint.ProxyCapabilities, orderModifier);
|
|
95
|
+
queryStore.unregister(BackendEndpoint.ProxyCapabilities, shipmentModifier);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {selection};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Re-exported for the auto-clear composable so it can refer to the same selection-shape type.
|
|
102
|
+
export type {CapabilitiesSelection};
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
AdminModalKey,
|
|
43
43
|
AdminView,
|
|
44
44
|
NotificationCategory,
|
|
45
|
+
OrderMode,
|
|
45
46
|
allAdminComponentNames,
|
|
46
47
|
optionalAdminActionContainerComponentNames,
|
|
47
48
|
optionalAdminComponentNames,
|
|
@@ -99,6 +100,7 @@ export {
|
|
|
99
100
|
useModalElementContext,
|
|
100
101
|
useMultiSelectInputContext,
|
|
101
102
|
useOrderData,
|
|
103
|
+
useOrderMode,
|
|
102
104
|
useOrdersData,
|
|
103
105
|
useProductData,
|
|
104
106
|
useRadioGroupContext,
|
|
@@ -67,6 +67,8 @@ export const createRegisterComponentsPlugin: PdkAppPlugin = ({config, logger}) =
|
|
|
67
67
|
Object.entries(getOptionalComponents(app)).forEach(([componentName, component]) => {
|
|
68
68
|
const componentToRegister = typeof component === 'string' ? componentsFromConfig[component] : component;
|
|
69
69
|
|
|
70
|
+
if (!componentToRegister) return;
|
|
71
|
+
|
|
70
72
|
app.component(prefixComponent(componentName), markRaw(componentToRegister));
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -1,18 +1,53 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
2
2
|
import {useQuery, useQueryClient} from '@tanstack/vue-query';
|
|
3
3
|
import {type EndpointResponse, type GetCarrier} from '@myparcel-dev/sdk';
|
|
4
|
-
import {type
|
|
4
|
+
import {CarrierId, CarrierName, type CarrierNameOrId} from '@myparcel-dev/constants';
|
|
5
5
|
import {useMyParcelApi} from '../useMyParcelApi';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const KNOWN_CARRIER_NAMES = new Set<string>(Object.values(CarrierName));
|
|
8
|
+
const KNOWN_CARRIER_IDS = new Set<number>(
|
|
9
|
+
Object.values(CarrierId).filter((value): value is number => typeof value === 'number'),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const resolveCarrierIdentifier = (input: string | number): CarrierNameOrId => {
|
|
13
|
+
if (typeof input === 'number') {
|
|
14
|
+
if (KNOWN_CARRIER_IDS.has(input)) {
|
|
15
|
+
return input as CarrierNameOrId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.warn(`[useFetchCarrier] Unknown carrier ID "${input}", it may cause issues.`);
|
|
20
|
+
|
|
21
|
+
return input as CarrierNameOrId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (KNOWN_CARRIER_NAMES.has(input)) {
|
|
25
|
+
return input as CarrierNameOrId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalized = input.replace(/_/g, '').toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (KNOWN_CARRIER_NAMES.has(normalized)) {
|
|
31
|
+
return normalized as CarrierNameOrId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line no-console
|
|
35
|
+
console.warn(`[useFetchCarrier] Unknown carrier identifier "${input}", it may cause issues.`);
|
|
36
|
+
|
|
37
|
+
// Cast input as CarrierNameOrId, even if it's not valid, to allow the query to run and return an error from the API if it's truly invalid.
|
|
38
|
+
return input as CarrierNameOrId;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useFetchCarrier = (carrier?: string | number) => {
|
|
42
|
+
const resolved = carrier === undefined ? undefined : resolveCarrierIdentifier(carrier);
|
|
8
43
|
const queryClient = useQueryClient();
|
|
9
|
-
const queryKey = ['carriers', {name:
|
|
44
|
+
const queryKey = ['carriers', {name: resolved}] as const;
|
|
10
45
|
|
|
11
46
|
return useQuery<EndpointResponse<GetCarrier>[number]>(
|
|
12
47
|
queryKey,
|
|
13
48
|
async () => {
|
|
14
49
|
const sdk = useMyParcelApi();
|
|
15
|
-
const carriers = await sdk.getCarrier(
|
|
50
|
+
const carriers = await sdk.getCarrier(resolved === undefined ? undefined : {path: {carrier: resolved}});
|
|
16
51
|
|
|
17
52
|
return carriers[0];
|
|
18
53
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {createGlobalState} from '@vueuse/core';
|
|
2
|
-
import {type BackendEndpoint} from '@myparcel-dev/pdk-common';
|
|
3
2
|
import {createMyParcelSdk, type HttpMethod, type MyParcelSdk} from '@myparcel-dev/sdk';
|
|
3
|
+
import {type BackendEndpoint} from '@myparcel-dev/pdk-common';
|
|
4
4
|
import {AbstractPdkEndpoint} from '../endpoints';
|
|
5
5
|
import {PdkFetchClient} from '../PdkFetchClient';
|
|
6
6
|
import {useGlobalContext} from '../../composables';
|
|
@@ -16,12 +16,17 @@ export const usePdkAdminApi = createGlobalState((): MyParcelSdk<AbstractPdkEndpo
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const pdkEndpoints = Object.entries(globalContext.endpoints).map(([endpointName, options]) => {
|
|
19
|
+
// Effectively cast null as 'undefined' to prevent the SDK casting the prefix property to the string 'null'.
|
|
20
|
+
const property = typeof options.property === 'string' ? options.property : undefined;
|
|
21
|
+
const responseProperty = typeof options.responseProperty === 'string' ? options.responseProperty : undefined;
|
|
19
22
|
class PdkEndpoint extends AbstractPdkEndpoint {
|
|
20
23
|
public readonly method = options.method.toUpperCase() as HttpMethod;
|
|
21
24
|
public readonly name = endpointName as BackendEndpoint;
|
|
22
25
|
public readonly path = options.path;
|
|
23
|
-
public readonly property =
|
|
24
|
-
|
|
26
|
+
public readonly property = property;
|
|
27
|
+
// If this is undefined, the SDK falls back to the endpoint instance's `property` value above.
|
|
28
|
+
public readonly responseProperty = responseProperty;
|
|
29
|
+
public readonly useDataEnvelope = typeof options.useDataEnvelope === 'undefined' ? true : options.useDataEnvelope;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
return new PdkEndpoint({
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import {computed, defineComponent, h} from 'vue';
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
5
|
+
import {mount} from '@vue/test-utils';
|
|
6
|
+
import {AdminAction, OrderMode} from '../data';
|
|
7
|
+
import {useOrderMode} from '../composables/context/useOrderMode';
|
|
8
|
+
import {ORDER_VIEW_IN_BACKOFFICE_ID} from '../actions';
|
|
9
|
+
import {doComponentTestSetup, doComponentTestTeardown} from '../__tests__';
|
|
10
|
+
import {useActionStore} from './useActionStore';
|
|
11
|
+
|
|
12
|
+
vi.mock('../composables/context/useOrderMode', () => ({
|
|
13
|
+
useOrderMode: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const mockedUseOrderMode = vi.mocked(useOrderMode);
|
|
17
|
+
|
|
18
|
+
describe('useActionStore', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
doComponentTestSetup();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
doComponentTestTeardown();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('registerOrderActions', () => {
|
|
28
|
+
const mountWithStore = (mode: OrderMode) => {
|
|
29
|
+
mockedUseOrderMode.mockReturnValue(computed(() => mode));
|
|
30
|
+
|
|
31
|
+
let store!: ReturnType<typeof useActionStore>;
|
|
32
|
+
|
|
33
|
+
mount(
|
|
34
|
+
defineComponent({
|
|
35
|
+
setup() {
|
|
36
|
+
store = useActionStore();
|
|
37
|
+
store.registerOrderActions();
|
|
38
|
+
|
|
39
|
+
return () => h('div');
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return store;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const commonActions = [AdminAction.OrdersEdit, AdminAction.OrdersFetch, AdminAction.OrdersUpdate];
|
|
48
|
+
|
|
49
|
+
it.each(commonActions)('registers common action %s regardless of mode', (action) => {
|
|
50
|
+
for (const mode of [OrderMode.Shipments, OrderMode.OrderV1, OrderMode.OrderV2]) {
|
|
51
|
+
const store = mountWithStore(mode);
|
|
52
|
+
expect(store.get(action), `${action} should be registered in ${mode} mode`).toBeDefined();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('registers shipment-specific actions in Shipments mode', () => {
|
|
57
|
+
const store = mountWithStore(OrderMode.Shipments);
|
|
58
|
+
|
|
59
|
+
expect(store.get(AdminAction.OrdersPrint)).toBeDefined();
|
|
60
|
+
expect(store.get(AdminAction.OrdersExport)).toBeDefined();
|
|
61
|
+
expect(store.get(AdminAction.OrdersExportPrint)).toBeDefined();
|
|
62
|
+
expect(store.get(AdminAction.ShipmentsExportReturn)).toBeDefined();
|
|
63
|
+
expect(store.get(AdminAction.ShipmentsDelete)).toBeDefined();
|
|
64
|
+
expect(store.get(AdminAction.ShipmentsUpdate)).toBeDefined();
|
|
65
|
+
expect(store.get(AdminAction.ShipmentsPrint)).toBeDefined();
|
|
66
|
+
expect(store.get(ORDER_VIEW_IN_BACKOFFICE_ID)).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('registers order export and backoffice actions in OrderV1 mode', () => {
|
|
70
|
+
const store = mountWithStore(OrderMode.OrderV1);
|
|
71
|
+
|
|
72
|
+
expect(store.get(AdminAction.OrdersExport)).toBeDefined();
|
|
73
|
+
expect(store.get(ORDER_VIEW_IN_BACKOFFICE_ID)).toBeDefined();
|
|
74
|
+
expect(store.get(AdminAction.OrdersExportPrint)).toBeUndefined();
|
|
75
|
+
expect(store.get(AdminAction.ShipmentsExportReturn)).toBeUndefined();
|
|
76
|
+
expect(store.get(AdminAction.ShipmentsDelete)).toBeUndefined();
|
|
77
|
+
expect(store.get(AdminAction.ShipmentsUpdate)).toBeUndefined();
|
|
78
|
+
expect(store.get(AdminAction.ShipmentsPrint)).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('registers the same shipment-specific action set as Shipments in OrderV2 mode (hybrid)', () => {
|
|
82
|
+
const store = mountWithStore(OrderMode.OrderV2);
|
|
83
|
+
|
|
84
|
+
expect(store.get(AdminAction.OrdersPrint)).toBeDefined();
|
|
85
|
+
expect(store.get(AdminAction.OrdersExport)).toBeDefined();
|
|
86
|
+
expect(store.get(AdminAction.OrdersExportPrint)).toBeDefined();
|
|
87
|
+
expect(store.get(AdminAction.ShipmentsExportReturn)).toBeDefined();
|
|
88
|
+
expect(store.get(AdminAction.ShipmentsDelete)).toBeDefined();
|
|
89
|
+
expect(store.get(AdminAction.ShipmentsUpdate)).toBeDefined();
|
|
90
|
+
expect(store.get(AdminAction.ShipmentsPrint)).toBeDefined();
|
|
91
|
+
// The backoffice view is V1-only and should still not appear in V2.
|
|
92
|
+
expect(store.get(ORDER_VIEW_IN_BACKOFFICE_ID)).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('registers shipmentsExportReturn exactly once in Shipments mode', () => {
|
|
96
|
+
const store = mountWithStore(OrderMode.Shipments);
|
|
97
|
+
|
|
98
|
+
expect(store.get(AdminAction.ShipmentsExportReturn)).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -4,22 +4,14 @@ import {type OneOrMore, type PromiseOr, toArray} from '@myparcel-dev/ts-utils';
|
|
|
4
4
|
import {type ActionParameters, type AnyActionDefinition, type ResolvedAction} from '../types';
|
|
5
5
|
import {createAction, getActionIdentifier} from '../services';
|
|
6
6
|
import {type AdminAction} from '../data';
|
|
7
|
-
import {
|
|
7
|
+
import {useOrderMode} from '../composables';
|
|
8
8
|
import {
|
|
9
9
|
modalCloseAction,
|
|
10
10
|
modalSubmitFormAction,
|
|
11
|
-
orderExportAction,
|
|
12
|
-
orderExportToShipmentsAction,
|
|
13
11
|
ordersEditAction,
|
|
14
|
-
ordersExportPrintShipmentsAction,
|
|
15
12
|
ordersFetchAction,
|
|
16
|
-
ordersPrintAction,
|
|
17
13
|
ordersUpdateAction,
|
|
18
|
-
|
|
19
|
-
shipmentsDeleteAction,
|
|
20
|
-
shipmentsExportReturnAction,
|
|
21
|
-
shipmentsPrintAction,
|
|
22
|
-
shipmentsUpdateAction,
|
|
14
|
+
STORE_MODE_ACTIONS,
|
|
23
15
|
webhooksCreateAction,
|
|
24
16
|
webhooksDeleteAction,
|
|
25
17
|
webhooksFetchAction,
|
|
@@ -62,25 +54,9 @@ export const useActionStore = defineStore('actions', () => {
|
|
|
62
54
|
dispatch,
|
|
63
55
|
|
|
64
56
|
registerOrderActions: () => {
|
|
65
|
-
const
|
|
57
|
+
const orderMode = useOrderMode();
|
|
66
58
|
|
|
67
|
-
register([
|
|
68
|
-
ordersEditAction,
|
|
69
|
-
ordersFetchAction,
|
|
70
|
-
ordersPrintAction,
|
|
71
|
-
ordersUpdateAction,
|
|
72
|
-
...(pluginSettings.order.orderMode
|
|
73
|
-
? [orderExportAction, orderViewInBackofficeAction]
|
|
74
|
-
: [
|
|
75
|
-
orderExportToShipmentsAction,
|
|
76
|
-
ordersExportPrintShipmentsAction,
|
|
77
|
-
shipmentsExportReturnAction,
|
|
78
|
-
shipmentsExportReturnAction,
|
|
79
|
-
shipmentsDeleteAction,
|
|
80
|
-
shipmentsUpdateAction,
|
|
81
|
-
shipmentsPrintAction,
|
|
82
|
-
]),
|
|
83
|
-
]);
|
|
59
|
+
register([ordersEditAction, ordersFetchAction, ordersUpdateAction, ...STORE_MODE_ACTIONS[orderMode.value]]);
|
|
84
60
|
},
|
|
85
61
|
|
|
86
62
|
registerModalActions: () => {
|
|
@@ -80,6 +80,20 @@ export const useQueryStore = defineStore('query', () => {
|
|
|
80
80
|
return query;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Drop the entry for {endpoint, modifier?} from the store. Used when a feature that registered
|
|
85
|
+
* its own query is unmounted (e.g. shipment-options modal closing) so a fresh registration on
|
|
86
|
+
* the next open isn't shadowed by stale data.
|
|
87
|
+
*/
|
|
88
|
+
const unregister = <E extends BackendEndpoint>(endpoint: E, modifier?: PlainModifier): void => {
|
|
89
|
+
const key = createQueryCacheKey(endpoint, modifier);
|
|
90
|
+
|
|
91
|
+
if (queries.value[key] === undefined) return;
|
|
92
|
+
|
|
93
|
+
globalLogger.debug(`Unregistering query ${key}`);
|
|
94
|
+
delete queries.value[key];
|
|
95
|
+
};
|
|
96
|
+
|
|
83
97
|
const registerShipmentQueries = (
|
|
84
98
|
shipmentIds: MaybeRef<OneOrMore<number>>,
|
|
85
99
|
orderIds?: MaybeRef<OneOrMore<string>>,
|
|
@@ -125,6 +139,7 @@ export const useQueryStore = defineStore('query', () => {
|
|
|
125
139
|
get,
|
|
126
140
|
has,
|
|
127
141
|
register,
|
|
142
|
+
unregister,
|
|
128
143
|
|
|
129
144
|
registerShipmentQueries,
|
|
130
145
|
|
package/src/types/sdk.types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import {type RecursivePartial} from '@myparcel-dev/ts-utils';
|
|
1
2
|
import {
|
|
2
3
|
type AdminContextKey,
|
|
3
4
|
type BackendEndpoint,
|
|
5
|
+
type CarrierModel,
|
|
4
6
|
type ExtractEndpointDefinition,
|
|
5
7
|
type LabelFormat,
|
|
6
8
|
type LabelOutput,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
type Settings,
|
|
11
13
|
type Shipment,
|
|
12
14
|
} from '@myparcel-dev/pdk-common';
|
|
13
|
-
import {type RecursivePartial} from '@myparcel-dev/ts-utils';
|
|
14
15
|
import {type AdminContextObject} from './context.types';
|
|
15
16
|
import {type WebhookDefinition} from './common.types';
|
|
16
17
|
|
|
@@ -185,6 +186,57 @@ interface DebugSwitchToProductionApiDefinition extends PdkEndpointDefinition {
|
|
|
185
186
|
response: void;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
interface ProxyCapabilitiesDefinition extends PdkEndpointDefinition {
|
|
190
|
+
name: BackendEndpoint.ProxyCapabilities;
|
|
191
|
+
/**
|
|
192
|
+
* Action-control parameters live in the query string so the request body stays a clean
|
|
193
|
+
* mirror of the actual capabilities API contract.
|
|
194
|
+
*
|
|
195
|
+
* `filterSupported` (opt-in): when `true`, the action applies the registered-option allowlist
|
|
196
|
+
* server-side (`Carrier::filterRegisteredOptions`). When `false` or `undefined`, the backend
|
|
197
|
+
* should treat it as not opted in, preserving the existing unfiltered SDK passthrough
|
|
198
|
+
* behavior for callers.
|
|
199
|
+
*/
|
|
200
|
+
parameters?: {filterSupported?: boolean};
|
|
201
|
+
/**
|
|
202
|
+
* Body shape mirrors the actual capabilities API request: camelCase keys at every level
|
|
203
|
+
* matching the SDK's attribute maps (`countryCode`, `physicalProperties`, `packageType`,
|
|
204
|
+
* `deliveryType`). The PHP action translates these top-level camelCase keys to the
|
|
205
|
+
* snake_case names the SDK PHP model uses internally — generic translation read from the
|
|
206
|
+
* SDK's own attribute map.
|
|
207
|
+
*
|
|
208
|
+
* `physicalProperties.weight` is an object `{value, unit}`, not a primitive — that's how the
|
|
209
|
+
* SDK's `PhysicalPropertiesWeightV2` model defines it on the wire.
|
|
210
|
+
*/
|
|
211
|
+
body: {
|
|
212
|
+
recipient: {countryCode: string};
|
|
213
|
+
physicalProperties?: {
|
|
214
|
+
weight?: {value: number; unit: 'g' | 'kg'};
|
|
215
|
+
};
|
|
216
|
+
carrier?: string;
|
|
217
|
+
packageType?: string;
|
|
218
|
+
deliveryType?: string;
|
|
219
|
+
options?: string[];
|
|
220
|
+
};
|
|
221
|
+
response: {results: CarrierModel[]};
|
|
222
|
+
formattedResponse: CarrierModel[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Body / parameters / call shape for `pdk.proxyCapabilities`. Both order-scoped and
|
|
227
|
+
* shipment-scoped query composables share these — the order-scoped call simply omits
|
|
228
|
+
* `parameters` at the call site. Derived from {@link ProxyCapabilitiesDefinition} so any
|
|
229
|
+
* change to the endpoint contract flows through automatically.
|
|
230
|
+
*/
|
|
231
|
+
export type ProxyCapabilitiesBody = NonNullable<ProxyCapabilitiesDefinition['body']>;
|
|
232
|
+
|
|
233
|
+
export type ProxyCapabilitiesParameters = NonNullable<ProxyCapabilitiesDefinition['parameters']>;
|
|
234
|
+
|
|
235
|
+
export type ProxyCapabilitiesCall = (options: {
|
|
236
|
+
body: ProxyCapabilitiesBody;
|
|
237
|
+
parameters?: ProxyCapabilitiesParameters;
|
|
238
|
+
}) => Promise<ProxyCapabilitiesDefinition['response']>;
|
|
239
|
+
|
|
188
240
|
export type BackendEndpointDefinition =
|
|
189
241
|
| CreateWebhooksDefinition
|
|
190
242
|
| DebugDownloadLogsDefinition
|
|
@@ -201,6 +253,7 @@ export type BackendEndpointDefinition =
|
|
|
201
253
|
| FetchWebhooksDefinition
|
|
202
254
|
| PrintOrdersDefinition
|
|
203
255
|
| PrintShipmentsDefinition
|
|
256
|
+
| ProxyCapabilitiesDefinition
|
|
204
257
|
| UpdateAccountDefinition
|
|
205
258
|
| UpdateOrdersDefinition
|
|
206
259
|
| UpdatePluginSettingsDefinition
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type FormInstance} from '@myparcel-dev/vue-form-builder';
|
|
2
2
|
import {isOfType} from '@myparcel-dev/ts-utils';
|
|
3
3
|
import {convertDotNotationToObject} from '../convertDotNotationToObject';
|
|
4
|
+
import {getEnabledValues} from './getEnabledValues';
|
|
4
5
|
|
|
5
6
|
export const formToBody = <T extends Record<string, unknown> = Record<string, unknown>>(
|
|
6
7
|
form?: false | FormInstance | T,
|
|
@@ -9,7 +10,7 @@ export const formToBody = <T extends Record<string, unknown> = Record<string, un
|
|
|
9
10
|
return form ? form : ({} as T);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const formValues = form
|
|
13
|
+
const formValues = getEnabledValues(form);
|
|
13
14
|
return convertDotNotationToObject(formValues, (value) => {
|
|
14
15
|
return typeof value === 'boolean' ? Number(value) : value;
|
|
15
16
|
}) as T;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {ref, computed} from 'vue';
|
|
3
|
+
import {type FormInstance} from '@myparcel-dev/vue-form-builder';
|
|
4
|
+
import {getEnabledValues} from './getEnabledValues';
|
|
5
|
+
|
|
6
|
+
const createMockField = (name: string, value: unknown, isDisabled: boolean) => ({
|
|
7
|
+
name,
|
|
8
|
+
ref: ref(value),
|
|
9
|
+
isDisabled: ref(isDisabled),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createMockForm = (fields: ReturnType<typeof createMockField>[]): FormInstance => {
|
|
13
|
+
const allValues: Record<string, unknown> = {};
|
|
14
|
+
|
|
15
|
+
for (const field of fields) {
|
|
16
|
+
allValues[field.name] = field.ref.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
interactiveFields: computed(() => fields),
|
|
21
|
+
getValues: () => allValues,
|
|
22
|
+
} as unknown as FormInstance;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('getEnabledValues', () => {
|
|
26
|
+
it('returns values from enabled fields only', () => {
|
|
27
|
+
const form = createMockForm([
|
|
28
|
+
createMockField('deliveryOptions.carrier', 'POSTNL', false),
|
|
29
|
+
createMockField('deliveryOptions.shipmentOptions.requiresSignature', true, false),
|
|
30
|
+
createMockField('deliveryOptions.shipmentOptions.requiresAgeVerification', true, true),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const values = getEnabledValues(form);
|
|
34
|
+
|
|
35
|
+
expect(values).toEqual({
|
|
36
|
+
'deliveryOptions.carrier': 'POSTNL',
|
|
37
|
+
'deliveryOptions.shipmentOptions.requiresSignature': true,
|
|
38
|
+
});
|
|
39
|
+
expect(values).not.toHaveProperty('deliveryOptions.shipmentOptions.requiresAgeVerification');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns all values when no fields are disabled', () => {
|
|
43
|
+
const form = createMockForm([
|
|
44
|
+
createMockField('deliveryOptions.carrier', 'POSTNL', false),
|
|
45
|
+
createMockField('deliveryOptions.shipmentOptions.requiresSignature', true, false),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const values = getEnabledValues(form);
|
|
49
|
+
|
|
50
|
+
expect(values).toEqual({
|
|
51
|
+
'deliveryOptions.carrier': 'POSTNL',
|
|
52
|
+
'deliveryOptions.shipmentOptions.requiresSignature': true,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns empty object when all fields are disabled', () => {
|
|
57
|
+
const form = createMockForm([
|
|
58
|
+
createMockField('deliveryOptions.shipmentOptions.requiresSignature', true, true),
|
|
59
|
+
createMockField('deliveryOptions.shipmentOptions.requiresAgeVerification', false, true),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const values = getEnabledValues(form);
|
|
63
|
+
|
|
64
|
+
expect(values).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {toValue} from 'vue';
|
|
2
|
+
import {type FormInstance} from '@myparcel-dev/vue-form-builder';
|
|
3
|
+
|
|
4
|
+
type FormValues = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns form values excluding disabled fields.
|
|
8
|
+
*
|
|
9
|
+
* Works around a vue-form-builder bug where `getValues()` retains stale
|
|
10
|
+
* values from fields that became disabled after initialisation (e.g. when
|
|
11
|
+
* switching carriers hides unsupported shipment options).
|
|
12
|
+
*/
|
|
13
|
+
export const getEnabledValues = <T extends FormValues = FormValues>(form: FormInstance): T => {
|
|
14
|
+
const values: FormValues = {};
|
|
15
|
+
|
|
16
|
+
for (const field of toValue(form.interactiveFields)) {
|
|
17
|
+
if (toValue(field.isDisabled)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
values[field.name] = toValue(field.ref);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return values as T;
|
|
25
|
+
};
|
package/src/utils/forms/index.ts
CHANGED
package/src/views/OrderBox.vue
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<ShipmentOptionsBox />
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
<!--
|
|
5
|
+
Shipments mode plus V2 hybrid both surface manual shipments here.
|
|
6
|
+
@TODO INT-1590: restrict to OrderMode.Shipments once V2 with an active sales
|
|
7
|
+
channel hides the manual paths again. Original gate preserved as a
|
|
8
|
+
comment below for easy restoration.
|
|
9
|
+
-->
|
|
10
|
+
<!-- <ShipmentTableBox v-if="orderMode === OrderMode.Shipments && data?.shipments.some((item) => !item.deleted)" /> -->
|
|
11
|
+
<ShipmentTableBox v-if="orderMode !== OrderMode.OrderV1 && data?.shipments.some((item) => !item.deleted)" />
|
|
5
12
|
</template>
|
|
6
13
|
|
|
7
14
|
<script lang="ts" setup>
|
|
8
15
|
import {computed, defineAsyncComponent, toValue} from 'vue';
|
|
9
16
|
import {useActionStore, useQueryStore} from '../stores';
|
|
10
|
-
import {
|
|
17
|
+
import {OrderMode} from '../data';
|
|
18
|
+
import {useOrderData, useOrderMode} from '../composables';
|
|
11
19
|
import ShipmentOptionsBox from '../components/OrderBox/ShipmentOptionsBox.vue';
|
|
12
20
|
|
|
13
21
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
@@ -22,11 +30,9 @@ const actionStore = useActionStore();
|
|
|
22
30
|
|
|
23
31
|
actionStore.registerOrderActions();
|
|
24
32
|
|
|
25
|
-
const
|
|
33
|
+
const orderMode = useOrderMode();
|
|
26
34
|
|
|
27
35
|
const {query} = useOrderData();
|
|
28
36
|
|
|
29
37
|
const data = computed(() => toValue(query.data));
|
|
30
|
-
|
|
31
|
-
const {orderMode} = pluginSettings.order;
|
|
32
38
|
</script>
|