@dainamite/cpq 0.2.2
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 +70 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +236 -0
- package/README.md +108 -0
- package/dist/modules/cpq/acl.js +30 -0
- package/dist/modules/cpq/acl.js.map +7 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js +37 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js.map +7 -0
- package/dist/modules/cpq/api/attribute-options/route.js +47 -0
- package/dist/modules/cpq/api/attribute-options/route.js.map +7 -0
- package/dist/modules/cpq/api/bundle-slots/route.js +101 -0
- package/dist/modules/cpq/api/bundle-slots/route.js.map +7 -0
- package/dist/modules/cpq/api/bundle-tree/route.js +28 -0
- package/dist/modules/cpq/api/bundle-tree/route.js.map +7 -0
- package/dist/modules/cpq/api/context.js +29 -0
- package/dist/modules/cpq/api/context.js.map +7 -0
- package/dist/modules/cpq/api/inventory/assets/route.js +113 -0
- package/dist/modules/cpq/api/inventory/assets/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/assets/status/route.js +35 -0
- package/dist/modules/cpq/api/inventory/assets/status/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js +30 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js +31 -0
- package/dist/modules/cpq/api/inventory/customer/[customerId]/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscription-items/route.js +99 -0
- package/dist/modules/cpq/api/inventory/subscription-items/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js +57 -0
- package/dist/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js +62 -0
- package/dist/modules/cpq/api/inventory/subscriptions/expiring/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/route.js +116 -0
- package/dist/modules/cpq/api/inventory/subscriptions/route.js.map +7 -0
- package/dist/modules/cpq/api/inventory/subscriptions/status/route.js +35 -0
- package/dist/modules/cpq/api/inventory/subscriptions/status/route.js.map +7 -0
- package/dist/modules/cpq/api/offering-components/route.js +91 -0
- package/dist/modules/cpq/api/offering-components/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +39 -0
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/route.js +28 -0
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js +38 -0
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +7 -0
- package/dist/modules/cpq/api/orders/route.js +71 -0
- package/dist/modules/cpq/api/orders/route.js.map +7 -0
- package/dist/modules/cpq/api/price-rules/route.js +180 -0
- package/dist/modules/cpq/api/price-rules/route.js.map +7 -0
- package/dist/modules/cpq/api/pricing-table-entries/route.js +142 -0
- package/dist/modules/cpq/api/pricing-table-entries/route.js.map +7 -0
- package/dist/modules/cpq/api/pricing-tables/route.js +179 -0
- package/dist/modules/cpq/api/pricing-tables/route.js.map +7 -0
- package/dist/modules/cpq/api/product-attributes/route.js +160 -0
- package/dist/modules/cpq/api/product-attributes/route.js.map +7 -0
- package/dist/modules/cpq/api/product-charges/route.js +166 -0
- package/dist/modules/cpq/api/product-charges/route.js.map +7 -0
- package/dist/modules/cpq/api/product-offerings/route.js +294 -0
- package/dist/modules/cpq/api/product-offerings/route.js.map +7 -0
- package/dist/modules/cpq/api/product-relationships/route.js +144 -0
- package/dist/modules/cpq/api/product-relationships/route.js.map +7 -0
- package/dist/modules/cpq/api/product-specifications/route.js +292 -0
- package/dist/modules/cpq/api/product-specifications/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js +71 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +29 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +54 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js +27 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/items/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js +72 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/merge-meta/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +35 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js +102 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js +27 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/status/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js +79 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js +95 -0
- package/dist/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/attributes/route.js +52 -0
- package/dist/modules/cpq/api/quotes/attributes/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/from-subscription/route.js +72 -0
- package/dist/modules/cpq/api/quotes/from-subscription/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/price/route.js +69 -0
- package/dist/modules/cpq/api/quotes/price/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/route.js +97 -0
- package/dist/modules/cpq/api/quotes/route.js.map +7 -0
- package/dist/modules/cpq/api/quotes/validate-relationships/route.js +86 -0
- package/dist/modules/cpq/api/quotes/validate-relationships/route.js.map +7 -0
- package/dist/modules/cpq/api/resolveQuotingService.js +7 -0
- package/dist/modules/cpq/api/resolveQuotingService.js.map +7 -0
- package/dist/modules/cpq/api/wizards/resolve/route.js +38 -0
- package/dist/modules/cpq/api/wizards/resolve/route.js.map +7 -0
- package/dist/modules/cpq/api/wizards/route.js +111 -0
- package/dist/modules/cpq/api/wizards/route.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js +136 -0
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js +185 -0
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js +343 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.js +35 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/inventory/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/shared.js +20 -0
- package/dist/modules/cpq/backend/cpq/inventory/shared.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js +794 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +917 -0
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.js +405 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/offerings/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js +677 -0
- package/dist/modules/cpq/backend/cpq/orders/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/page.js +134 -0
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/orders/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js +272 -0
- package/dist/modules/cpq/backend/cpq/price-rules/PriceRuleForm.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js +407 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js +13 -0
- package/dist/modules/cpq/backend/cpq/price-rules/[id]/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.js +211 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/price-rules/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +275 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js +112 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js +492 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js +50 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js +249 -0
- package/dist/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.js +87 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/pricing/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js +652 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js +1289 -0
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.js +283 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/quotes/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js +214 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +1399 -0
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.js +148 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/specifications/page.meta.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js +204 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js +191 -0
- package/dist/modules/cpq/backend/cpq/wizards/[definitionCode]/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.js +119 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +7 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.meta.js +18 -0
- package/dist/modules/cpq/backend/cpq/wizards/page.meta.js.map +7 -0
- package/dist/modules/cpq/ce.js +151 -0
- package/dist/modules/cpq/ce.js.map +7 -0
- package/dist/modules/cpq/cli.js +11 -0
- package/dist/modules/cpq/cli.js.map +7 -0
- package/dist/modules/cpq/components/CpqListView.js +265 -0
- package/dist/modules/cpq/components/CpqListView.js.map +7 -0
- package/dist/modules/cpq/components/NumberInput.js +95 -0
- package/dist/modules/cpq/components/NumberInput.js.map +7 -0
- package/dist/modules/cpq/components/StatusPath.js +60 -0
- package/dist/modules/cpq/components/StatusPath.js.map +7 -0
- package/dist/modules/cpq/components/statusMaps.js +66 -0
- package/dist/modules/cpq/components/statusMaps.js.map +7 -0
- package/dist/modules/cpq/components/useCpqRowActions.js +125 -0
- package/dist/modules/cpq/components/useCpqRowActions.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +1556 -0
- package/dist/modules/cpq/data/entities.js.map +7 -0
- package/dist/modules/cpq/data/validators.js +575 -0
- package/dist/modules/cpq/data/validators.js.map +7 -0
- package/dist/modules/cpq/di.js +57 -0
- package/dist/modules/cpq/di.js.map +7 -0
- package/dist/modules/cpq/events.js +50 -0
- package/dist/modules/cpq/events.js.map +7 -0
- package/dist/modules/cpq/index.js +16 -0
- package/dist/modules/cpq/index.js.map +7 -0
- package/dist/modules/cpq/lib/seeds/api.js +32 -0
- package/dist/modules/cpq/lib/seeds/api.js.map +7 -0
- package/dist/modules/cpq/lib/seeds/tenant-provisioning.js +158 -0
- package/dist/modules/cpq/lib/seeds/tenant-provisioning.js.map +7 -0
- package/dist/modules/cpq/lib/seeds.js +6 -0
- package/dist/modules/cpq/lib/seeds.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js +40 -0
- package/dist/modules/cpq/migrations/Migration20260220220000_cpq.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js +54 -0
- package/dist/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js +15 -0
- package/dist/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js +62 -0
- package/dist/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js +56 -0
- package/dist/modules/cpq/migrations/Migration20260402000000_cpq_workflows.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js +26 -0
- package/dist/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js +37 -0
- package/dist/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js +35 -0
- package/dist/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js +21 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js +46 -0
- package/dist/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js +118 -0
- package/dist/modules/cpq/migrations/Migration20260410100000_cpq_inventory.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js +63 -0
- package/dist/modules/cpq/migrations/Migration20260411000000_cpq_orders.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js +28 -0
- package/dist/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js +36 -0
- package/dist/modules/cpq/migrations/Migration20260504064621_cpq_arc.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js +14 -0
- package/dist/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js +14 -0
- package/dist/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js +58 -0
- package/dist/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js +29 -0
- package/dist/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.js.map +7 -0
- package/dist/modules/cpq/services/cpqBundleService.js +447 -0
- package/dist/modules/cpq/services/cpqBundleService.js.map +7 -0
- package/dist/modules/cpq/services/cpqInventoryService.js +1371 -0
- package/dist/modules/cpq/services/cpqInventoryService.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +901 -0
- package/dist/modules/cpq/services/cpqOrderService.js.map +7 -0
- package/dist/modules/cpq/services/cpqPricingService.js +289 -0
- package/dist/modules/cpq/services/cpqPricingService.js.map +7 -0
- package/dist/modules/cpq/services/cpqProductService.js +204 -0
- package/dist/modules/cpq/services/cpqProductService.js.map +7 -0
- package/dist/modules/cpq/services/cpqQuotingService.js +1867 -0
- package/dist/modules/cpq/services/cpqQuotingService.js.map +7 -0
- package/dist/modules/cpq/services/cpqValidationService.js +234 -0
- package/dist/modules/cpq/services/cpqValidationService.js.map +7 -0
- package/dist/modules/cpq/services/cpqWizardService.js +137 -0
- package/dist/modules/cpq/services/cpqWizardService.js.map +7 -0
- package/dist/modules/cpq/services/types.js +114 -0
- package/dist/modules/cpq/services/types.js.map +7 -0
- package/dist/modules/cpq/setup.js +49 -0
- package/dist/modules/cpq/setup.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-amended-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-amended-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-cancelled-log.js +19 -0
- package/dist/modules/cpq/subscribers/subscription-cancelled-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-merged-log.js +20 -0
- package/dist/modules/cpq/subscribers/subscription-merged-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-renewed-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-renewed-log.js.map +7 -0
- package/dist/modules/cpq/subscribers/subscription-superseded-log.js +18 -0
- package/dist/modules/cpq/subscribers/subscription-superseded-log.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js +192 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/customer-inventory/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js +79 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/order-cpq-tab/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js +181 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.client.js.map +7 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js +17 -0
- package/dist/modules/cpq/widgets/injection/quote-configurator/widget.js.map +7 -0
- package/dist/modules/cpq/widgets/injection-table.js +44 -0
- package/dist/modules/cpq/widgets/injection-table.js.map +7 -0
- package/dist/modules/cpq/workflows/WizardRunner.js +113 -0
- package/dist/modules/cpq/workflows/WizardRunner.js.map +7 -0
- package/dist/modules/cpq/workflows/WizardTrigger.js +116 -0
- package/dist/modules/cpq/workflows/WizardTrigger.js.map +7 -0
- package/dist/modules/cpq/workflows/engine.js +188 -0
- package/dist/modules/cpq/workflows/engine.js.map +7 -0
- package/dist/modules/cpq/workflows/registry.js +16 -0
- package/dist/modules/cpq/workflows/registry.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js +119 -0
- package/dist/modules/cpq/workflows/steps/CustomerSelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js +24 -0
- package/dist/modules/cpq/workflows/steps/InventorySelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ItemListStep.js +115 -0
- package/dist/modules/cpq/workflows/steps/ItemListStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js +327 -0
- package/dist/modules/cpq/workflows/steps/OfferingSelectStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js +231 -0
- package/dist/modules/cpq/workflows/steps/ProductConfigureStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/ReviewStep.js +152 -0
- package/dist/modules/cpq/workflows/steps/ReviewStep.js.map +7 -0
- package/dist/modules/cpq/workflows/steps/index.js +49 -0
- package/dist/modules/cpq/workflows/steps/index.js.map +7 -0
- package/dist/modules/cpq/workflows/types.js +1 -0
- package/dist/modules/cpq/workflows/types.js.map +7 -0
- package/package.json +150 -0
- package/src/modules/cpq/__integration__/TC-CPQ-001.spec.ts +177 -0
- package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +123 -0
- package/src/modules/cpq/__integration__/TC-CPQ-003.spec.ts +115 -0
- package/src/modules/cpq/__integration__/TC-CPQ-004.spec.ts +184 -0
- package/src/modules/cpq/__integration__/TC-CPQ-005.spec.ts +43 -0
- package/src/modules/cpq/__integration__/TC-CPQ-006.spec.ts +87 -0
- package/src/modules/cpq/__integration__/TC-CPQ-007.spec.ts +79 -0
- package/src/modules/cpq/__integration__/TC-CPQ-008.spec.ts +55 -0
- package/src/modules/cpq/__integration__/TC-CPQ-009.spec.ts +45 -0
- package/src/modules/cpq/__integration__/TC-CPQ-010.spec.ts +54 -0
- package/src/modules/cpq/__integration__/TC-CPQ-011.spec.ts +24 -0
- package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-001.spec.ts +52 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-002.spec.ts +65 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-003.spec.ts +107 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-004.spec.ts +74 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-005.spec.ts +86 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-006.spec.ts +79 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-007.spec.ts +63 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-008.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-009.spec.ts +62 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-010.spec.ts +59 -0
- package/src/modules/cpq/__integration__/TC-CPQ-ARC-011.spec.ts +128 -0
- package/src/modules/cpq/__integration__/_helpers/cpqArcFixtures.ts +208 -0
- package/src/modules/cpq/__tests__/package-isolation.test.ts +232 -0
- package/src/modules/cpq/acl.ts +26 -0
- package/src/modules/cpq/api/_helpers/__tests__/handleArcError.test.ts +117 -0
- package/src/modules/cpq/api/_helpers/handleArcError.ts +81 -0
- package/src/modules/cpq/api/attribute-options/route.ts +55 -0
- package/src/modules/cpq/api/bundle-slots/route.ts +112 -0
- package/src/modules/cpq/api/bundle-tree/route.ts +31 -0
- package/src/modules/cpq/api/context.ts +39 -0
- package/src/modules/cpq/api/inventory/assets/route.ts +123 -0
- package/src/modules/cpq/api/inventory/assets/status/route.ts +34 -0
- package/src/modules/cpq/api/inventory/customer/[customerId]/parent-items/route.ts +30 -0
- package/src/modules/cpq/api/inventory/customer/[customerId]/route.ts +31 -0
- package/src/modules/cpq/api/inventory/subscription-items/route.ts +107 -0
- package/src/modules/cpq/api/inventory/subscriptions/[subscriptionId]/change-log/route.ts +64 -0
- package/src/modules/cpq/api/inventory/subscriptions/expiring/route.ts +66 -0
- package/src/modules/cpq/api/inventory/subscriptions/route.ts +126 -0
- package/src/modules/cpq/api/inventory/subscriptions/status/route.ts +34 -0
- package/src/modules/cpq/api/offering-components/route.ts +103 -0
- package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +48 -0
- package/src/modules/cpq/api/orders/[orderId]/route.ts +28 -0
- package/src/modules/cpq/api/orders/[orderId]/status/route.ts +38 -0
- package/src/modules/cpq/api/orders/route.ts +77 -0
- package/src/modules/cpq/api/price-rules/route.ts +210 -0
- package/src/modules/cpq/api/pricing-table-entries/route.ts +159 -0
- package/src/modules/cpq/api/pricing-tables/route.ts +202 -0
- package/src/modules/cpq/api/product-attributes/route.ts +175 -0
- package/src/modules/cpq/api/product-charges/route.ts +181 -0
- package/src/modules/cpq/api/product-offerings/route.ts +329 -0
- package/src/modules/cpq/api/product-relationships/route.ts +156 -0
- package/src/modules/cpq/api/product-specifications/route.ts +325 -0
- package/src/modules/cpq/api/quotes/[quoteId]/cancel-meta/route.ts +77 -0
- package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +28 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +55 -0
- package/src/modules/cpq/api/quotes/[quoteId]/items/route.ts +26 -0
- package/src/modules/cpq/api/quotes/[quoteId]/merge-meta/route.ts +78 -0
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +34 -0
- package/src/modules/cpq/api/quotes/[quoteId]/route.ts +109 -0
- package/src/modules/cpq/api/quotes/[quoteId]/status/route.ts +26 -0
- package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/[targetId]/route.ts +91 -0
- package/src/modules/cpq/api/quotes/[quoteId]/target-subscriptions/route.ts +107 -0
- package/src/modules/cpq/api/quotes/attributes/route.ts +61 -0
- package/src/modules/cpq/api/quotes/from-subscription/route.ts +85 -0
- package/src/modules/cpq/api/quotes/price/route.ts +81 -0
- package/src/modules/cpq/api/quotes/route.ts +114 -0
- package/src/modules/cpq/api/quotes/validate-relationships/route.ts +100 -0
- package/src/modules/cpq/api/resolveQuotingService.ts +11 -0
- package/src/modules/cpq/api/wizards/resolve/route.ts +39 -0
- package/src/modules/cpq/api/wizards/route.ts +127 -0
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +166 -0
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +251 -0
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/inventory/assets/[id]/page.tsx +440 -0
- package/src/modules/cpq/backend/cpq/inventory/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/inventory/page.tsx +49 -0
- package/src/modules/cpq/backend/cpq/inventory/shared.tsx +32 -0
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/inventory/subscriptions/[id]/page.tsx +1085 -0
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +1165 -0
- package/src/modules/cpq/backend/cpq/offerings/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +468 -0
- package/src/modules/cpq/backend/cpq/orders/[id]/page.tsx +921 -0
- package/src/modules/cpq/backend/cpq/orders/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/orders/page.tsx +182 -0
- package/src/modules/cpq/backend/cpq/price-rules/PriceRuleForm.tsx +310 -0
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.meta.ts +9 -0
- package/src/modules/cpq/backend/cpq/price-rules/[id]/page.tsx +472 -0
- package/src/modules/cpq/backend/cpq/price-rules/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +253 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +431 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/ImportPreview.tsx +122 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/page.tsx +588 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxExport.ts +90 -0
- package/src/modules/cpq/backend/cpq/pricing/[id]/xlsxImport.ts +338 -0
- package/src/modules/cpq/backend/cpq/pricing/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +111 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/_components/ArcQuoteConfigurator.tsx +737 -0
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +1721 -0
- package/src/modules/cpq/backend/cpq/quotes/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +370 -0
- package/src/modules/cpq/backend/cpq/specifications/[id]/DependsOnRulesEditor.tsx +300 -0
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +1722 -0
- package/src/modules/cpq/backend/cpq/specifications/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +191 -0
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/detail/page.tsx +319 -0
- package/src/modules/cpq/backend/cpq/wizards/[definitionCode]/page.tsx +234 -0
- package/src/modules/cpq/backend/cpq/wizards/page.meta.ts +16 -0
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +141 -0
- package/src/modules/cpq/ce.ts +150 -0
- package/src/modules/cpq/cli.ts +10 -0
- package/src/modules/cpq/components/CpqListView.tsx +431 -0
- package/src/modules/cpq/components/NumberInput.tsx +166 -0
- package/src/modules/cpq/components/StatusPath.tsx +116 -0
- package/src/modules/cpq/components/statusMaps.ts +148 -0
- package/src/modules/cpq/components/useCpqRowActions.tsx +183 -0
- package/src/modules/cpq/data/__tests__/validators.arc.test.ts +271 -0
- package/src/modules/cpq/data/__tests__/validators.test.ts +782 -0
- package/src/modules/cpq/data/entities.ts +1467 -0
- package/src/modules/cpq/data/validators.ts +663 -0
- package/src/modules/cpq/di.ts +70 -0
- package/src/modules/cpq/events.ts +62 -0
- package/src/modules/cpq/index.ts +15 -0
- package/src/modules/cpq/lib/seeds/__tests__/api.test.ts +86 -0
- package/src/modules/cpq/lib/seeds/__tests__/tenant-provisioning.test.ts +349 -0
- package/src/modules/cpq/lib/seeds/api.ts +119 -0
- package/src/modules/cpq/lib/seeds/tenant-provisioning.ts +239 -0
- package/src/modules/cpq/lib/seeds.ts +18 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +7473 -0
- package/src/modules/cpq/migrations/Migration20260220220000_cpq.ts +51 -0
- package/src/modules/cpq/migrations/Migration20260330000000_cpq_product_domain.ts +70 -0
- package/src/modules/cpq/migrations/Migration20260331000000_cpq_charge_unique_per_offering.ts +15 -0
- package/src/modules/cpq/migrations/Migration20260401000000_cpq_quoting_v2.ts +73 -0
- package/src/modules/cpq/migrations/Migration20260402000000_cpq_workflows.ts +59 -0
- package/src/modules/cpq/migrations/Migration20260407000000_cpq_pricing_tables_v2.ts +30 -0
- package/src/modules/cpq/migrations/Migration20260407100000_cpq_price_rules.ts +36 -0
- package/src/modules/cpq/migrations/Migration20260408000000_cpq_price_rules.ts +33 -0
- package/src/modules/cpq/migrations/Migration20260410000000_cpq_drop_datacentres_inventory.ts +22 -0
- package/src/modules/cpq/migrations/Migration20260410000000_cpq_wizard_rename.ts +56 -0
- package/src/modules/cpq/migrations/Migration20260410100000_cpq_inventory.ts +118 -0
- package/src/modules/cpq/migrations/Migration20260411000000_cpq_orders.ts +62 -0
- package/src/modules/cpq/migrations/Migration20260412000000_cpq_bundled_offerings.ts +33 -0
- package/src/modules/cpq/migrations/Migration20260504064621_cpq_arc.ts +68 -0
- package/src/modules/cpq/migrations/Migration20260514000000_cpq_spec_merge_retired_deprecated.ts +30 -0
- package/src/modules/cpq/migrations/Migration20260514100000_cpq_offering_merge_retired_deprecated.ts +27 -0
- package/src/modules/cpq/migrations/Migration20260514110000_cpq_list_search_indexes.ts +82 -0
- package/src/modules/cpq/migrations/Migration20260515000000_cpq_inventory_subscriptions_search_indexes.ts +52 -0
- package/src/modules/cpq/services/__tests__/_helpers/mockEm.ts +59 -0
- package/src/modules/cpq/services/__tests__/cpqBundleService.test.ts +661 -0
- package/src/modules/cpq/services/__tests__/cpqInventoryService.arc.test.ts +357 -0
- package/src/modules/cpq/services/__tests__/cpqInventoryService.test.ts +718 -0
- package/src/modules/cpq/services/__tests__/cpqOrderService.test.ts +603 -0
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +1012 -0
- package/src/modules/cpq/services/__tests__/cpqProductService.test.ts +603 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.arc.test.ts +143 -0
- package/src/modules/cpq/services/__tests__/cpqQuotingService.test.ts +658 -0
- package/src/modules/cpq/services/__tests__/cpqValidationService.test.ts +811 -0
- package/src/modules/cpq/services/__tests__/cpqWizardService.test.ts +389 -0
- package/src/modules/cpq/services/__tests__/types.test.ts +359 -0
- package/src/modules/cpq/services/cpqBundleService.ts +671 -0
- package/src/modules/cpq/services/cpqInventoryService.ts +1929 -0
- package/src/modules/cpq/services/cpqOrderService.ts +1230 -0
- package/src/modules/cpq/services/cpqPricingService.ts +412 -0
- package/src/modules/cpq/services/cpqProductService.ts +311 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +2638 -0
- package/src/modules/cpq/services/cpqValidationService.ts +323 -0
- package/src/modules/cpq/services/cpqWizardService.ts +204 -0
- package/src/modules/cpq/services/types.ts +303 -0
- package/src/modules/cpq/setup.ts +63 -0
- package/src/modules/cpq/subscribers/subscription-amended-log.ts +20 -0
- package/src/modules/cpq/subscribers/subscription-cancelled-log.ts +19 -0
- package/src/modules/cpq/subscribers/subscription-merged-log.ts +22 -0
- package/src/modules/cpq/subscribers/subscription-renewed-log.ts +18 -0
- package/src/modules/cpq/subscribers/subscription-superseded-log.ts +19 -0
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.client.tsx +339 -0
- package/src/modules/cpq/widgets/injection/customer-inventory/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.client.tsx +96 -0
- package/src/modules/cpq/widgets/injection/order-cpq-tab/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.client.tsx +265 -0
- package/src/modules/cpq/widgets/injection/quote-configurator/widget.ts +16 -0
- package/src/modules/cpq/widgets/injection-table.ts +42 -0
- package/src/modules/cpq/workflows/WizardRunner.tsx +158 -0
- package/src/modules/cpq/workflows/WizardTrigger.tsx +142 -0
- package/src/modules/cpq/workflows/engine.tsx +278 -0
- package/src/modules/cpq/workflows/registry.ts +15 -0
- package/src/modules/cpq/workflows/steps/CustomerSelectStep.tsx +136 -0
- package/src/modules/cpq/workflows/steps/InventorySelectStep.tsx +34 -0
- package/src/modules/cpq/workflows/steps/ItemListStep.tsx +146 -0
- package/src/modules/cpq/workflows/steps/OfferingSelectStep.tsx +438 -0
- package/src/modules/cpq/workflows/steps/ProductConfigureStep.tsx +287 -0
- package/src/modules/cpq/workflows/steps/ReviewStep.tsx +219 -0
- package/src/modules/cpq/workflows/steps/index.ts +51 -0
- package/src/modules/cpq/workflows/types.ts +102 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import type { ColumnDef, SortingState } from '@tanstack/react-table'
|
|
4
|
+
import { DataTable, type BulkAction } from '@open-mercato/ui/backend/DataTable'
|
|
5
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
|
+
import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
7
|
+
import { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'
|
|
8
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
|
+
import { useCpqRowActions, type UseCpqRowActionsOptions } from './useCpqRowActions'
|
|
12
|
+
|
|
13
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
type PaginatedResponse<T> = {
|
|
16
|
+
items?: T[]
|
|
17
|
+
total?: number
|
|
18
|
+
totalPages?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CpqListData<T> = {
|
|
22
|
+
rows: T[]
|
|
23
|
+
total: number
|
|
24
|
+
totalPages: number
|
|
25
|
+
page: number
|
|
26
|
+
isLoading: boolean
|
|
27
|
+
/** Last successful fetch duration in milliseconds. Surfaces in the
|
|
28
|
+
* pagination footer as `... in 1.5s`, matching OM's standard list views. */
|
|
29
|
+
durationMs: number | null
|
|
30
|
+
search: string
|
|
31
|
+
sorting: SortingState
|
|
32
|
+
filterValues: FilterValues
|
|
33
|
+
setPage: (page: number) => void
|
|
34
|
+
setSearch: (value: string) => void
|
|
35
|
+
setSorting: React.Dispatch<React.SetStateAction<SortingState>>
|
|
36
|
+
setFilterValues: (values: FilterValues) => void
|
|
37
|
+
clearFilters: () => void
|
|
38
|
+
reload: () => void
|
|
39
|
+
queryString: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type UseCpqListDataOptions = {
|
|
43
|
+
/** API endpoint without query string, e.g. `/api/cpq/product-offerings` */
|
|
44
|
+
endpoint: string
|
|
45
|
+
/** Default 50 — keep at or below 100. */
|
|
46
|
+
pageSize?: number
|
|
47
|
+
/** Initial sort state. Default `[{ id: 'createdAt', desc: true }]`. */
|
|
48
|
+
initialSorting?: SortingState
|
|
49
|
+
/** Map filter values to URL params (only call set on the params arg). */
|
|
50
|
+
buildFilterParams?: (values: FilterValues, params: URLSearchParams) => void
|
|
51
|
+
/** Override the flash message shown when the load fails. */
|
|
52
|
+
loadErrorMessage?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Server-side paginated list state for CPQ list views.
|
|
57
|
+
*
|
|
58
|
+
* Designed for 100k+ records:
|
|
59
|
+
* - search / sort / filter all go to the server
|
|
60
|
+
* - default pageSize stays at or below 100
|
|
61
|
+
* - debouncing is handled by FilterBar's search input (1s)
|
|
62
|
+
*/
|
|
63
|
+
export function useCpqListData<T extends { id: string }>(
|
|
64
|
+
options: UseCpqListDataOptions,
|
|
65
|
+
): CpqListData<T> {
|
|
66
|
+
const {
|
|
67
|
+
endpoint,
|
|
68
|
+
pageSize = 50,
|
|
69
|
+
initialSorting = [{ id: 'createdAt', desc: true }],
|
|
70
|
+
buildFilterParams,
|
|
71
|
+
loadErrorMessage,
|
|
72
|
+
} = options
|
|
73
|
+
const t = useT()
|
|
74
|
+
|
|
75
|
+
const [rows, setRows] = React.useState<T[]>([])
|
|
76
|
+
const [total, setTotal] = React.useState(0)
|
|
77
|
+
const [totalPages, setTotalPages] = React.useState(1)
|
|
78
|
+
const [page, setPageState] = React.useState(1)
|
|
79
|
+
const [isLoading, setIsLoading] = React.useState(true)
|
|
80
|
+
const [durationMs, setDurationMs] = React.useState<number | null>(null)
|
|
81
|
+
const [reloadToken, setReloadToken] = React.useState(0)
|
|
82
|
+
const [search, setSearchState] = React.useState('')
|
|
83
|
+
const [sorting, setSorting] = React.useState<SortingState>(initialSorting)
|
|
84
|
+
const [filterValues, setFilterValuesState] = React.useState<FilterValues>({})
|
|
85
|
+
|
|
86
|
+
const queryString = React.useMemo(() => {
|
|
87
|
+
const params = new URLSearchParams()
|
|
88
|
+
params.set('page', String(page))
|
|
89
|
+
params.set('pageSize', String(pageSize))
|
|
90
|
+
if (search.trim()) params.set('search', search.trim())
|
|
91
|
+
const sort = sorting[0]
|
|
92
|
+
if (sort?.id) {
|
|
93
|
+
params.set('sortField', sort.id)
|
|
94
|
+
params.set('sortDir', sort.desc ? 'desc' : 'asc')
|
|
95
|
+
}
|
|
96
|
+
if (buildFilterParams) buildFilterParams(filterValues, params)
|
|
97
|
+
return params.toString()
|
|
98
|
+
}, [buildFilterParams, filterValues, page, pageSize, search, sorting])
|
|
99
|
+
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
let cancelled = false
|
|
102
|
+
async function load() {
|
|
103
|
+
setIsLoading(true)
|
|
104
|
+
// Use `performance.now()` so the wall-clock change during the request
|
|
105
|
+
// (NTP, DST, manual clock skew) can never produce a negative duration.
|
|
106
|
+
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
107
|
+
try {
|
|
108
|
+
const fallback: PaginatedResponse<T> = { items: [], total: 0, totalPages: 1 }
|
|
109
|
+
const call = await apiCall<PaginatedResponse<T>>(
|
|
110
|
+
`${endpoint}?${queryString}`,
|
|
111
|
+
undefined,
|
|
112
|
+
{ fallback },
|
|
113
|
+
)
|
|
114
|
+
if (cancelled) return
|
|
115
|
+
if (!call.ok) {
|
|
116
|
+
flash(loadErrorMessage ?? t('cpq.list.error.load', 'Failed to load'), 'error')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
const payload = call.result ?? fallback
|
|
120
|
+
const items = Array.isArray(payload.items) ? payload.items : []
|
|
121
|
+
setRows(items)
|
|
122
|
+
setTotal(typeof payload.total === 'number' ? payload.total : items.length)
|
|
123
|
+
setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)
|
|
124
|
+
const finishedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
125
|
+
setDurationMs(Math.max(0, finishedAt - startedAt))
|
|
126
|
+
} finally {
|
|
127
|
+
if (!cancelled) setIsLoading(false)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
load()
|
|
131
|
+
return () => {
|
|
132
|
+
cancelled = true
|
|
133
|
+
}
|
|
134
|
+
}, [endpoint, loadErrorMessage, queryString, reloadToken, t])
|
|
135
|
+
|
|
136
|
+
const setSearch = React.useCallback((value: string) => {
|
|
137
|
+
setSearchState(value)
|
|
138
|
+
setPageState(1)
|
|
139
|
+
}, [])
|
|
140
|
+
|
|
141
|
+
const setFilterValues = React.useCallback((values: FilterValues) => {
|
|
142
|
+
setFilterValuesState(values)
|
|
143
|
+
setPageState(1)
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
const clearFilters = React.useCallback(() => {
|
|
147
|
+
setFilterValuesState({})
|
|
148
|
+
setPageState(1)
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
const reload = React.useCallback(() => {
|
|
152
|
+
setReloadToken((token) => token + 1)
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
rows,
|
|
157
|
+
total,
|
|
158
|
+
totalPages,
|
|
159
|
+
page,
|
|
160
|
+
isLoading,
|
|
161
|
+
durationMs,
|
|
162
|
+
search,
|
|
163
|
+
sorting,
|
|
164
|
+
filterValues,
|
|
165
|
+
setPage: setPageState,
|
|
166
|
+
setSearch,
|
|
167
|
+
setSorting,
|
|
168
|
+
setFilterValues,
|
|
169
|
+
clearFilters,
|
|
170
|
+
reload,
|
|
171
|
+
queryString,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Component ───────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Declarative shortcut for the standard CRUD-list row actions.
|
|
179
|
+
*
|
|
180
|
+
* When provided, CpqListView:
|
|
181
|
+
* - registers `useCpqRowActions` internally
|
|
182
|
+
* - renders Edit + Delete in every row's `⋯` menu (with optional
|
|
183
|
+
* `extraRowItems` per row prepended / appended)
|
|
184
|
+
* - adds a "Delete selected" bulk action (after any `bulkActions` you
|
|
185
|
+
* pass explicitly, so module-specific bulk ops come first)
|
|
186
|
+
* - mounts the confirm-dialog portal in the page footer
|
|
187
|
+
*
|
|
188
|
+
* Pass `rowActions` / `bulkActions` directly instead when the page needs
|
|
189
|
+
* fully custom semantics (e.g. Quotes / Orders have no DELETE endpoint).
|
|
190
|
+
*/
|
|
191
|
+
export type CpqListViewCrudConfig<T extends { id: string }> = Omit<
|
|
192
|
+
UseCpqRowActionsOptions<T>,
|
|
193
|
+
'onReload'
|
|
194
|
+
> & {
|
|
195
|
+
/**
|
|
196
|
+
* Extra items added to each row's actions menu.
|
|
197
|
+
* `prepend` renders before Edit, `append` between Edit and Delete.
|
|
198
|
+
*/
|
|
199
|
+
extraRowItems?: (row: T) => { prepend?: RowActionItem[]; append?: RowActionItem[] }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type CpqListViewProps<T extends { id: string }> = {
|
|
203
|
+
/** Page + table title */
|
|
204
|
+
title: React.ReactNode
|
|
205
|
+
/** Stable id for perspectives (e.g. `cpq.offerings.list`) */
|
|
206
|
+
tableId: string
|
|
207
|
+
/** Stable id for DataTable's selection state (defaults to tableId) */
|
|
208
|
+
selectionScopeKey?: string
|
|
209
|
+
/** List state — typically built from `useCpqListData` in the parent */
|
|
210
|
+
data: CpqListData<T>
|
|
211
|
+
/** Table columns */
|
|
212
|
+
columns: ColumnDef<T>[]
|
|
213
|
+
/** Filter definitions for the FilterBar overlay */
|
|
214
|
+
filters?: FilterDef[]
|
|
215
|
+
/** Search input placeholder */
|
|
216
|
+
searchPlaceholder?: string
|
|
217
|
+
/** Header actions (e.g. Create button) */
|
|
218
|
+
actions?: React.ReactNode
|
|
219
|
+
/** Bulk actions toolbar shown when rows are selected */
|
|
220
|
+
bulkActions?: BulkAction<T>[]
|
|
221
|
+
/** Per-row action menu */
|
|
222
|
+
rowActions?: (row: T) => React.ReactNode
|
|
223
|
+
/**
|
|
224
|
+
* Declarative CRUD config. When set, CpqListView produces the standard
|
|
225
|
+
* `⋯ → Edit / Delete` menu, a "Delete selected" bulk action, and mounts
|
|
226
|
+
* the confirm-dialog portal — replacing the ~30 lines of boilerplate
|
|
227
|
+
* each list page used to repeat. Override `rowActions` / `bulkActions`
|
|
228
|
+
* to take full control instead.
|
|
229
|
+
*/
|
|
230
|
+
crud?: CpqListViewCrudConfig<T>
|
|
231
|
+
/** Click handler for whole row (typically navigates to detail) */
|
|
232
|
+
onRowClick?: (row: T) => void
|
|
233
|
+
/** Custom empty state */
|
|
234
|
+
emptyState?: React.ReactNode
|
|
235
|
+
/** Pagination size (must match `useCpqListData` pageSize) */
|
|
236
|
+
pageSize?: number
|
|
237
|
+
/**
|
|
238
|
+
* Custom content rendered ABOVE the DataTable inside the same Page wrapper.
|
|
239
|
+
* Use for inline forms, customer pickers, tab switchers, alerts.
|
|
240
|
+
*/
|
|
241
|
+
toolbarContent?: React.ReactNode
|
|
242
|
+
/**
|
|
243
|
+
* Custom content rendered BELOW the DataTable (e.g. confirm dialog portals).
|
|
244
|
+
*/
|
|
245
|
+
footerContent?: React.ReactNode
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Generic CPQ list view. Wraps DataTable + FilterBar + Page in the
|
|
250
|
+
* Open Mercato standard, with stronger row-hover affordance so users
|
|
251
|
+
* always know what's clickable.
|
|
252
|
+
*/
|
|
253
|
+
export function CpqListView<T extends { id: string }>({
|
|
254
|
+
title,
|
|
255
|
+
tableId,
|
|
256
|
+
selectionScopeKey,
|
|
257
|
+
data,
|
|
258
|
+
columns,
|
|
259
|
+
filters,
|
|
260
|
+
searchPlaceholder,
|
|
261
|
+
actions,
|
|
262
|
+
bulkActions,
|
|
263
|
+
rowActions,
|
|
264
|
+
crud,
|
|
265
|
+
onRowClick,
|
|
266
|
+
emptyState,
|
|
267
|
+
pageSize = 50,
|
|
268
|
+
toolbarContent,
|
|
269
|
+
footerContent,
|
|
270
|
+
}: CpqListViewProps<T>) {
|
|
271
|
+
const t = useT()
|
|
272
|
+
|
|
273
|
+
// Wire `crud` into RowActions + bulkActions + footer confirm dialog so the
|
|
274
|
+
// call-site stays a single declarative config block.
|
|
275
|
+
// The hook is always called (Rules of Hooks); the helpers below decide
|
|
276
|
+
// whether to actually use its output based on the presence of `crud`.
|
|
277
|
+
const crudActions = useCpqRowActions<T>({
|
|
278
|
+
endpoint: crud?.endpoint ?? '',
|
|
279
|
+
entityName: crud?.entityName ?? 'item',
|
|
280
|
+
editHref: crud?.editHref ?? (() => '#'),
|
|
281
|
+
onReload: data.reload,
|
|
282
|
+
canDelete: crud?.canDelete,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const resolvedRowActions = React.useMemo(() => {
|
|
286
|
+
if (rowActions) return rowActions
|
|
287
|
+
if (!crud) return undefined
|
|
288
|
+
return (row: T) => (
|
|
289
|
+
<RowActions items={crudActions.buildItems(row, crud.extraRowItems?.(row))} />
|
|
290
|
+
)
|
|
291
|
+
}, [crud, crudActions, rowActions])
|
|
292
|
+
|
|
293
|
+
const resolvedBulkActions = React.useMemo<BulkAction<T>[] | undefined>(() => {
|
|
294
|
+
if (!crud) return bulkActions
|
|
295
|
+
const deleteAction: BulkAction<T> = {
|
|
296
|
+
id: 'delete',
|
|
297
|
+
label: t('cpq.list.bulk.deleteSelected', 'Delete selected'),
|
|
298
|
+
destructive: true,
|
|
299
|
+
onExecute: crudActions.bulkDelete,
|
|
300
|
+
}
|
|
301
|
+
return bulkActions ? [...bulkActions, deleteAction] : [deleteAction]
|
|
302
|
+
}, [bulkActions, crud, crudActions.bulkDelete, t])
|
|
303
|
+
|
|
304
|
+
const resolvedFooter = crud ? (
|
|
305
|
+
<>
|
|
306
|
+
{footerContent}
|
|
307
|
+
{crudActions.ConfirmDialogElement}
|
|
308
|
+
</>
|
|
309
|
+
) : (
|
|
310
|
+
footerContent
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Page>
|
|
315
|
+
<PageBody className="cpq-list-view space-y-6">
|
|
316
|
+
<CpqInteractionStyles />
|
|
317
|
+
{toolbarContent}
|
|
318
|
+
<DataTable<T>
|
|
319
|
+
title={title}
|
|
320
|
+
actions={actions}
|
|
321
|
+
refreshButton={{
|
|
322
|
+
label: t('cpq.list.actions.refresh', 'Refresh'),
|
|
323
|
+
onRefresh: data.reload,
|
|
324
|
+
isRefreshing: data.isLoading,
|
|
325
|
+
}}
|
|
326
|
+
columns={columns}
|
|
327
|
+
data={data.rows}
|
|
328
|
+
searchValue={data.search}
|
|
329
|
+
onSearchChange={data.setSearch}
|
|
330
|
+
searchPlaceholder={searchPlaceholder ?? t('cpq.list.search.placeholder', 'Search...')}
|
|
331
|
+
filters={filters}
|
|
332
|
+
filterValues={data.filterValues}
|
|
333
|
+
onFiltersApply={data.setFilterValues}
|
|
334
|
+
onFiltersClear={data.clearFilters}
|
|
335
|
+
sorting={data.sorting}
|
|
336
|
+
onSortingChange={data.setSorting}
|
|
337
|
+
bulkActions={resolvedBulkActions}
|
|
338
|
+
selectionScopeKey={selectionScopeKey ?? tableId}
|
|
339
|
+
onRowClick={onRowClick}
|
|
340
|
+
rowActions={resolvedRowActions}
|
|
341
|
+
perspective={{ tableId }}
|
|
342
|
+
columnChooser={{ auto: true }}
|
|
343
|
+
pagination={{
|
|
344
|
+
page: data.page,
|
|
345
|
+
pageSize,
|
|
346
|
+
total: data.total,
|
|
347
|
+
totalPages: data.totalPages,
|
|
348
|
+
onPageChange: data.setPage,
|
|
349
|
+
durationMs: data.durationMs,
|
|
350
|
+
}}
|
|
351
|
+
isLoading={data.isLoading}
|
|
352
|
+
emptyState={emptyState}
|
|
353
|
+
/>
|
|
354
|
+
{resolvedFooter}
|
|
355
|
+
</PageBody>
|
|
356
|
+
</Page>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── Shared interaction styles ───────────────────────────────────
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Inline style block applied inside `.cpq-list-view` (and any `.cpq-page`)
|
|
364
|
+
* wrappers. Strengthens the OM hover affordance so users can clearly tell
|
|
365
|
+
* which row their mouse is on, and forces `cursor: pointer` on every
|
|
366
|
+
* clickable element inside CPQ pages.
|
|
367
|
+
*
|
|
368
|
+
* Kept inline to avoid coupling the package to a global stylesheet.
|
|
369
|
+
*/
|
|
370
|
+
export function CpqInteractionStyles() {
|
|
371
|
+
return (
|
|
372
|
+
<style>{`
|
|
373
|
+
.cpq-list-view tbody tr.cursor-pointer,
|
|
374
|
+
.cpq-page tbody tr.cursor-pointer {
|
|
375
|
+
cursor: pointer;
|
|
376
|
+
}
|
|
377
|
+
.cpq-list-view tbody tr.cursor-pointer:hover,
|
|
378
|
+
.cpq-page tbody tr.cursor-pointer:hover {
|
|
379
|
+
background-color: rgba(0, 0, 0, 0.045);
|
|
380
|
+
}
|
|
381
|
+
.dark .cpq-list-view tbody tr.cursor-pointer:hover,
|
|
382
|
+
.dark .cpq-page tbody tr.cursor-pointer:hover {
|
|
383
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
384
|
+
}
|
|
385
|
+
.cpq-list-view button:not(:disabled),
|
|
386
|
+
.cpq-list-view [role="button"]:not([aria-disabled="true"]),
|
|
387
|
+
.cpq-list-view a[href],
|
|
388
|
+
.cpq-list-view [data-clickable="true"],
|
|
389
|
+
.cpq-page button:not(:disabled),
|
|
390
|
+
.cpq-page [role="button"]:not([aria-disabled="true"]),
|
|
391
|
+
.cpq-page a[href],
|
|
392
|
+
.cpq-page [data-clickable="true"] {
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
}
|
|
395
|
+
/* Row actions \`⋯\` trigger: OM's hover:bg-accent is barely visible in
|
|
396
|
+
the light theme. Make hover (and the open state) clearly distinct so
|
|
397
|
+
users see what they're aiming at. */
|
|
398
|
+
.cpq-list-view [aria-haspopup="menu"]:hover,
|
|
399
|
+
.cpq-page [aria-haspopup="menu"]:hover,
|
|
400
|
+
.cpq-list-view [aria-haspopup="menu"][aria-expanded="true"],
|
|
401
|
+
.cpq-page [aria-haspopup="menu"][aria-expanded="true"] {
|
|
402
|
+
background-color: rgba(0, 0, 0, 0.08);
|
|
403
|
+
}
|
|
404
|
+
.dark .cpq-list-view [aria-haspopup="menu"]:hover,
|
|
405
|
+
.dark .cpq-page [aria-haspopup="menu"]:hover,
|
|
406
|
+
.dark .cpq-list-view [aria-haspopup="menu"][aria-expanded="true"],
|
|
407
|
+
.dark .cpq-page [aria-haspopup="menu"][aria-expanded="true"] {
|
|
408
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
409
|
+
}
|
|
410
|
+
/* Items inside the row-actions dropdown menu — strengthen hover so the
|
|
411
|
+
item under the pointer is unmistakable. The menu portal renders at the
|
|
412
|
+
document body, so we can't scope it to .cpq-list-view; targeting the
|
|
413
|
+
role attribute is enough. */
|
|
414
|
+
[role="menu"] [role="menuitem"]:hover,
|
|
415
|
+
[role="menu"] [role="menuitem"]:focus-visible {
|
|
416
|
+
background-color: rgba(0, 0, 0, 0.06);
|
|
417
|
+
}
|
|
418
|
+
.dark [role="menu"] [role="menuitem"]:hover,
|
|
419
|
+
.dark [role="menu"] [role="menuitem"]:focus-visible {
|
|
420
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
421
|
+
}
|
|
422
|
+
/* Align padding between <a href> menu items (OM's px-2) and the <button>
|
|
423
|
+
variant used for onSelect items (OM's Button size=sm = px-3). Without
|
|
424
|
+
this the Delete label visibly indents past the Edit label. */
|
|
425
|
+
[role="menu"] button[role="menuitem"] {
|
|
426
|
+
padding-left: 0.5rem;
|
|
427
|
+
padding-right: 0.5rem;
|
|
428
|
+
}
|
|
429
|
+
`}</style>
|
|
430
|
+
)
|
|
431
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Input, type InputProps } from '@open-mercato/ui/primitives/input'
|
|
4
|
+
|
|
5
|
+
// ─── Why this exists ─────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Plain `<input type="number" value={state}>` is hostile when `state === 0`:
|
|
8
|
+
// the field renders "0", the caret lands AFTER it, and typing "5" produces
|
|
9
|
+
// "05" which most browsers normalise back to `5` — the user typed five but
|
|
10
|
+
// the field swallowed the digit they wanted to keep. Same trap when editing
|
|
11
|
+
// any single-digit existing value.
|
|
12
|
+
//
|
|
13
|
+
// NumberInput fixes this with two layered behaviours:
|
|
14
|
+
//
|
|
15
|
+
// 1. **Select on focus.** Tabbing or clicking into the field selects the
|
|
16
|
+
// whole text, so the next keystroke replaces the existing value
|
|
17
|
+
// instead of prepending to it. This single change removes ~95 % of the
|
|
18
|
+
// friction without changing the controlled-component contract.
|
|
19
|
+
//
|
|
20
|
+
// 2. **Internal string state.** The input is rendered with a string we
|
|
21
|
+
// own, not `String(numericState)`. That lets intermediate edits like
|
|
22
|
+
// `"-"`, `"1."`, or empty stay visible while the parent only ever sees
|
|
23
|
+
// a parsed `number | null`. Parent updates (programmatic reset after
|
|
24
|
+
// save, form initial load) still flow in via the effect below.
|
|
25
|
+
//
|
|
26
|
+
// It also drops `type="number"` in favour of `type="text"` +
|
|
27
|
+
// `inputMode={'numeric' | 'decimal'}` so we get the right mobile keyboard
|
|
28
|
+
// without the spinner buttons and the silent step/min/max coercion that
|
|
29
|
+
// HTML number inputs apply on blur.
|
|
30
|
+
|
|
31
|
+
export type NumberInputProps = Omit<
|
|
32
|
+
InputProps,
|
|
33
|
+
'value' | 'onChange' | 'type' | 'inputMode' | 'defaultValue'
|
|
34
|
+
> & {
|
|
35
|
+
/**
|
|
36
|
+
* Numeric value. `null`/`undefined` renders the field empty — that's how
|
|
37
|
+
* callers signal "no value yet" without colliding with `0` as a real
|
|
38
|
+
* default.
|
|
39
|
+
*/
|
|
40
|
+
value: number | null | undefined
|
|
41
|
+
/** Called with the parsed number, or `null` when the field is cleared. */
|
|
42
|
+
onChange: (value: number | null) => void
|
|
43
|
+
/** Selects the input contents on focus. Default true. */
|
|
44
|
+
selectOnFocus?: boolean
|
|
45
|
+
/** Restrict input to integers (rejects `.`, `e`, etc). Default false. */
|
|
46
|
+
integer?: boolean
|
|
47
|
+
/** Optional minimum (enforced on commit, not while typing). */
|
|
48
|
+
min?: number
|
|
49
|
+
/** Optional maximum (enforced on commit, not while typing). */
|
|
50
|
+
max?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isIntermediateInput(raw: string, integer: boolean): boolean {
|
|
54
|
+
// Allow the user to type these without us replacing the field mid-edit.
|
|
55
|
+
// - empty / lone `-` → not parseable yet
|
|
56
|
+
// - trailing `.` for decimals → not parseable yet
|
|
57
|
+
// - trailing `e` / `e-` → scientific notation in progress
|
|
58
|
+
if (raw === '' || raw === '-') return true
|
|
59
|
+
if (!integer && (raw.endsWith('.') || raw.endsWith('e') || raw.endsWith('e-'))) return true
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
64
|
+
function NumberInput(
|
|
65
|
+
{
|
|
66
|
+
value,
|
|
67
|
+
onChange,
|
|
68
|
+
selectOnFocus = true,
|
|
69
|
+
integer = false,
|
|
70
|
+
min,
|
|
71
|
+
max,
|
|
72
|
+
onFocus,
|
|
73
|
+
onBlur,
|
|
74
|
+
...rest
|
|
75
|
+
},
|
|
76
|
+
ref,
|
|
77
|
+
) {
|
|
78
|
+
const [text, setText] = React.useState<string>(value == null ? '' : String(value))
|
|
79
|
+
const lastEmittedRef = React.useRef<number | null | undefined>(value)
|
|
80
|
+
|
|
81
|
+
// Sync external updates (form reset after save, initial load, etc.) into
|
|
82
|
+
// the local text state — but only when the parent's value actually
|
|
83
|
+
// diverges from what we last emitted, so we don't clobber intermediate
|
|
84
|
+
// text like "1." while the user is mid-edit.
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (lastEmittedRef.current === value) return
|
|
87
|
+
setText(value == null ? '' : String(value))
|
|
88
|
+
lastEmittedRef.current = value
|
|
89
|
+
}, [value])
|
|
90
|
+
|
|
91
|
+
const clamp = React.useCallback(
|
|
92
|
+
(n: number): number => {
|
|
93
|
+
let result = n
|
|
94
|
+
if (typeof min === 'number' && result < min) result = min
|
|
95
|
+
if (typeof max === 'number' && result > max) result = max
|
|
96
|
+
return result
|
|
97
|
+
},
|
|
98
|
+
[max, min],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const handleChange = React.useCallback(
|
|
102
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
103
|
+
const raw = e.currentTarget.value
|
|
104
|
+
|
|
105
|
+
// Strip characters that are never valid in our numeric formats; this
|
|
106
|
+
// lets us keep `type="text"` while still rejecting alphabetic input.
|
|
107
|
+
const allowed = integer ? /[^\d-]/g : /[^\d.\-eE]/g
|
|
108
|
+
const cleaned = raw.replace(allowed, '')
|
|
109
|
+
|
|
110
|
+
if (isIntermediateInput(cleaned, integer)) {
|
|
111
|
+
setText(cleaned)
|
|
112
|
+
// Treat intermediates as "no committed value yet" — emit null so
|
|
113
|
+
// the parent doesn't store a half-typed number. The full value
|
|
114
|
+
// commits on the next valid keystroke or on blur.
|
|
115
|
+
if (lastEmittedRef.current !== null) {
|
|
116
|
+
lastEmittedRef.current = null
|
|
117
|
+
onChange(null)
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parsed = integer ? parseInt(cleaned, 10) : parseFloat(cleaned)
|
|
123
|
+
if (Number.isNaN(parsed)) return // Reject — keep previous state.
|
|
124
|
+
|
|
125
|
+
setText(cleaned)
|
|
126
|
+
const next = clamp(parsed)
|
|
127
|
+
if (lastEmittedRef.current !== next) {
|
|
128
|
+
lastEmittedRef.current = next
|
|
129
|
+
onChange(next)
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[clamp, integer, onChange],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const handleBlur = React.useCallback(
|
|
136
|
+
(e: React.FocusEvent<HTMLInputElement>) => {
|
|
137
|
+
// On blur, normalise the displayed text to match the committed value
|
|
138
|
+
// (e.g. "01" → "1", "1." → "1"). Empty stays empty.
|
|
139
|
+
if (lastEmittedRef.current == null) {
|
|
140
|
+
if (text !== '') setText('')
|
|
141
|
+
} else {
|
|
142
|
+
const normalised = String(lastEmittedRef.current)
|
|
143
|
+
if (text !== normalised) setText(normalised)
|
|
144
|
+
}
|
|
145
|
+
onBlur?.(e)
|
|
146
|
+
},
|
|
147
|
+
[onBlur, text],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Input
|
|
152
|
+
ref={ref}
|
|
153
|
+
{...rest}
|
|
154
|
+
type="text"
|
|
155
|
+
inputMode={integer ? 'numeric' : 'decimal'}
|
|
156
|
+
value={text}
|
|
157
|
+
onFocus={(e) => {
|
|
158
|
+
if (selectOnFocus) e.currentTarget.select()
|
|
159
|
+
onFocus?.(e)
|
|
160
|
+
}}
|
|
161
|
+
onChange={handleChange}
|
|
162
|
+
onBlur={handleBlur}
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
},
|
|
166
|
+
)
|