@wildo-ai/presets-components 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/esm/.native.tsbuildinfo +1 -0
  2. package/dist/esm/components/actions/action-bar.d.ts.map +1 -1
  3. package/dist/esm/components/actions/action-bar.js +5 -2
  4. package/dist/esm/components/actions/action-bar.js.map +1 -1
  5. package/dist/esm/components/ai-elements/chat/suggestion.d.ts.map +1 -1
  6. package/dist/esm/components/ai-elements/chat/suggestion.js +8 -1
  7. package/dist/esm/components/ai-elements/chat/suggestion.js.map +1 -1
  8. package/dist/esm/components/ai-elements/decision/approval.d.ts +108 -13
  9. package/dist/esm/components/ai-elements/decision/approval.d.ts.map +1 -1
  10. package/dist/esm/components/ai-elements/decision/approval.js +60 -10
  11. package/dist/esm/components/ai-elements/decision/approval.js.map +1 -1
  12. package/dist/esm/components/ai-elements/graph/graph-runtime.d.ts +3 -1
  13. package/dist/esm/components/ai-elements/graph/graph-runtime.d.ts.map +1 -1
  14. package/dist/esm/components/ai-elements/graph/graph-runtime.js +3 -1
  15. package/dist/esm/components/ai-elements/graph/graph-runtime.js.map +1 -1
  16. package/dist/esm/components/app/app-layout-main-container.d.ts +9 -0
  17. package/dist/esm/components/app/app-layout-main-container.d.ts.map +1 -1
  18. package/dist/esm/components/app/app-layout-main-container.js +17 -23
  19. package/dist/esm/components/app/app-layout-main-container.js.map +1 -1
  20. package/dist/esm/components/app/app-layout-main-container.labels.d.ts.map +1 -1
  21. package/dist/esm/components/app/app-layout-main-container.labels.js +27 -0
  22. package/dist/esm/components/app/app-layout-main-container.labels.js.map +1 -1
  23. package/dist/esm/components/billing/billing-plan-change-preview.d.ts.map +1 -1
  24. package/dist/esm/components/billing/billing-plan-change-preview.js +5 -2
  25. package/dist/esm/components/billing/billing-plan-change-preview.js.map +1 -1
  26. package/dist/esm/components/billing/billing-plan-change-preview.labels.d.ts.map +1 -1
  27. package/dist/esm/components/billing/billing-plan-change-preview.labels.js +3 -0
  28. package/dist/esm/components/billing/billing-plan-change-preview.labels.js.map +1 -1
  29. package/dist/esm/components/billing/billing-pricing-table.d.ts +1 -1
  30. package/dist/esm/components/billing/billing-pricing-table.d.ts.map +1 -1
  31. package/dist/esm/components/billing/billing-pricing-table.js +8 -4
  32. package/dist/esm/components/billing/billing-pricing-table.js.map +1 -1
  33. package/dist/esm/components/billing/billing-pricing-table.labels.d.ts.map +1 -1
  34. package/dist/esm/components/billing/billing-pricing-table.labels.js +7 -1
  35. package/dist/esm/components/billing/billing-pricing-table.labels.js.map +1 -1
  36. package/dist/esm/components/billing/billing-usage-dashboard.d.ts.map +1 -1
  37. package/dist/esm/components/billing/billing-usage-dashboard.js +6 -3
  38. package/dist/esm/components/billing/billing-usage-dashboard.js.map +1 -1
  39. package/dist/esm/components/billing/billing-usage-dashboard.labels.d.ts.map +1 -1
  40. package/dist/esm/components/billing/billing-usage-dashboard.labels.js +2 -1
  41. package/dist/esm/components/billing/billing-usage-dashboard.labels.js.map +1 -1
  42. package/dist/esm/components/data-display/formatters/display-field-enum.d.ts.map +1 -1
  43. package/dist/esm/components/data-display/formatters/display-field-enum.js +18 -4
  44. package/dist/esm/components/data-display/formatters/display-field-enum.js.map +1 -1
  45. package/dist/esm/components/data-display/formatters/display-field-file.d.ts.map +1 -1
  46. package/dist/esm/components/data-display/formatters/display-field-file.js +13 -14
  47. package/dist/esm/components/data-display/formatters/display-field-file.js.map +1 -1
  48. package/dist/esm/components/data-display/formatters/display-field-file.labels.d.ts.map +1 -1
  49. package/dist/esm/components/data-display/formatters/display-field-file.labels.js +1 -0
  50. package/dist/esm/components/data-display/formatters/display-field-file.labels.js.map +1 -1
  51. package/dist/esm/components/data-display/formatters/display-field-number.d.ts.map +1 -1
  52. package/dist/esm/components/data-display/formatters/display-field-number.js +6 -1
  53. package/dist/esm/components/data-display/formatters/display-field-number.js.map +1 -1
  54. package/dist/esm/components/data-display/formatters/display-field-number.labels.d.ts.map +1 -1
  55. package/dist/esm/components/data-display/formatters/display-field-number.labels.js +5 -2
  56. package/dist/esm/components/data-display/formatters/display-field-number.labels.js.map +1 -1
  57. package/dist/esm/components/data-display/formatters/display-field-string.d.ts +1 -1
  58. package/dist/esm/components/data-display/formatters/display-field-string.d.ts.map +1 -1
  59. package/dist/esm/components/data-display/formatters/display-field-string.js +51 -12
  60. package/dist/esm/components/data-display/formatters/display-field-string.js.map +1 -1
  61. package/dist/esm/components/data-display/untrusted-html-content.d.ts +55 -0
  62. package/dist/esm/components/data-display/untrusted-html-content.d.ts.map +1 -0
  63. package/dist/esm/components/data-display/untrusted-html-content.js +67 -0
  64. package/dist/esm/components/data-display/untrusted-html-content.js.map +1 -0
  65. package/dist/esm/components/form/chromes/form-primitive.labels.d.ts.map +1 -1
  66. package/dist/esm/components/form/chromes/form-primitive.labels.js +9 -0
  67. package/dist/esm/components/form/chromes/form-primitive.labels.js.map +1 -1
  68. package/dist/esm/components/form/combobox.d.ts.map +1 -1
  69. package/dist/esm/components/form/combobox.js +10 -10
  70. package/dist/esm/components/form/combobox.js.map +1 -1
  71. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.adapter.d.ts +42 -0
  72. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.adapter.d.ts.map +1 -0
  73. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.adapter.js +120 -0
  74. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.adapter.js.map +1 -0
  75. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.d.ts +16 -0
  76. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.d.ts.map +1 -0
  77. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.js +8 -0
  78. package/dist/esm/components/form/controls/address/providers/google-places-address-lookup.js.map +1 -0
  79. package/dist/esm/components/navigation/app-level/activity-bar.d.ts.map +1 -1
  80. package/dist/esm/components/navigation/app-level/activity-bar.js +2 -2
  81. package/dist/esm/components/navigation/app-level/activity-bar.js.map +1 -1
  82. package/dist/esm/components/navigation/app-level/menu-user-org.d.ts.map +1 -1
  83. package/dist/esm/components/navigation/app-level/menu-user-org.js +10 -4
  84. package/dist/esm/components/navigation/app-level/menu-user-org.js.map +1 -1
  85. package/dist/esm/components/navigation/app-level/menu-user-org.labels.d.ts.map +1 -1
  86. package/dist/esm/components/navigation/app-level/menu-user-org.labels.js +7 -0
  87. package/dist/esm/components/navigation/app-level/menu-user-org.labels.js.map +1 -1
  88. package/dist/esm/components/navigation/app-level/menubar.d.ts.map +1 -1
  89. package/dist/esm/components/navigation/app-level/menubar.js +6 -6
  90. package/dist/esm/components/navigation/app-level/menubar.js.map +1 -1
  91. package/dist/esm/components/navigation/app-level/navigation-back.d.ts.map +1 -1
  92. package/dist/esm/components/navigation/app-level/navigation-back.js +27 -9
  93. package/dist/esm/components/navigation/app-level/navigation-back.js.map +1 -1
  94. package/dist/esm/components/navigation/app-level/sidebar.js +3 -3
  95. package/dist/esm/components/navigation/app-level/sidebar.js.map +1 -1
  96. package/dist/esm/components/navigation/app-level/status-bar.js +2 -2
  97. package/dist/esm/components/navigation/app-level/status-bar.js.map +1 -1
  98. package/dist/esm/components/navigation/app-level/toolbar.d.ts.map +1 -1
  99. package/dist/esm/components/navigation/app-level/toolbar.js +2 -2
  100. package/dist/esm/components/navigation/app-level/toolbar.js.map +1 -1
  101. package/dist/esm/components/navigation/app-level/utility-panel.js +2 -2
  102. package/dist/esm/components/navigation/app-level/utility-panel.js.map +1 -1
  103. package/dist/esm/components/navigation/breadcrumb.js +1 -1
  104. package/dist/esm/components/navigation/breadcrumb.js.map +1 -1
  105. package/dist/esm/components/sections/stepper.d.ts.map +1 -1
  106. package/dist/esm/components/sections/stepper.js +5 -3
  107. package/dist/esm/components/sections/stepper.js.map +1 -1
  108. package/dist/esm/config/preset-navigation.d.ts.map +1 -1
  109. package/dist/esm/config/preset-navigation.js +23 -2
  110. package/dist/esm/config/preset-navigation.js.map +1 -1
  111. package/dist/esm/config/resources/agent-work.resource-presets.d.ts +4 -0
  112. package/dist/esm/config/resources/agent-work.resource-presets.d.ts.map +1 -0
  113. package/dist/esm/config/resources/agent-work.resource-presets.js +115 -0
  114. package/dist/esm/config/resources/agent-work.resource-presets.js.map +1 -0
  115. package/dist/esm/config/resources/api-key-rotation-window.d.ts +22 -0
  116. package/dist/esm/config/resources/api-key-rotation-window.d.ts.map +1 -0
  117. package/dist/esm/config/resources/api-key-rotation-window.js +45 -0
  118. package/dist/esm/config/resources/api-key-rotation-window.js.map +1 -0
  119. package/dist/esm/config/resources/application.resource-presets.d.ts.map +1 -1
  120. package/dist/esm/config/resources/application.resource-presets.js +147 -61
  121. package/dist/esm/config/resources/application.resource-presets.js.map +1 -1
  122. package/dist/esm/config/resources/billing.resource-presets.d.ts +4 -3
  123. package/dist/esm/config/resources/billing.resource-presets.d.ts.map +1 -1
  124. package/dist/esm/config/resources/billing.resource-presets.js +240 -111
  125. package/dist/esm/config/resources/billing.resource-presets.js.map +1 -1
  126. package/dist/esm/config/resources/conversation-threads.resource-presets.d.ts +4 -0
  127. package/dist/esm/config/resources/conversation-threads.resource-presets.d.ts.map +1 -0
  128. package/dist/esm/config/resources/conversation-threads.resource-presets.js +105 -0
  129. package/dist/esm/config/resources/conversation-threads.resource-presets.js.map +1 -0
  130. package/dist/esm/config/resources/guidance.resource-presets.d.ts.map +1 -1
  131. package/dist/esm/config/resources/guidance.resource-presets.js +18 -11
  132. package/dist/esm/config/resources/guidance.resource-presets.js.map +1 -1
  133. package/dist/esm/config/resources/identity.resource-presets.d.ts +0 -13
  134. package/dist/esm/config/resources/identity.resource-presets.d.ts.map +1 -1
  135. package/dist/esm/config/resources/identity.resource-presets.js +48 -16
  136. package/dist/esm/config/resources/identity.resource-presets.js.map +1 -1
  137. package/dist/esm/config/resources/inbound-deliveries.resource-presets.d.ts +6 -0
  138. package/dist/esm/config/resources/inbound-deliveries.resource-presets.d.ts.map +1 -0
  139. package/dist/esm/config/resources/inbound-deliveries.resource-presets.js +112 -0
  140. package/dist/esm/config/resources/inbound-deliveries.resource-presets.js.map +1 -0
  141. package/dist/esm/config/resources/inbound-email.resource-presets.d.ts +4 -0
  142. package/dist/esm/config/resources/inbound-email.resource-presets.d.ts.map +1 -0
  143. package/dist/esm/config/resources/inbound-email.resource-presets.js +182 -0
  144. package/dist/esm/config/resources/inbound-email.resource-presets.js.map +1 -0
  145. package/dist/esm/config/resources/index.d.ts.map +1 -1
  146. package/dist/esm/config/resources/index.js +9 -2
  147. package/dist/esm/config/resources/index.js.map +1 -1
  148. package/dist/esm/config/resources/legal-holds.resource-presets.d.ts +4 -0
  149. package/dist/esm/config/resources/legal-holds.resource-presets.d.ts.map +1 -0
  150. package/dist/esm/config/resources/legal-holds.resource-presets.js +76 -0
  151. package/dist/esm/config/resources/legal-holds.resource-presets.js.map +1 -0
  152. package/dist/esm/config/resources/organizations.resource-presets.d.ts.map +1 -1
  153. package/dist/esm/config/resources/organizations.resource-presets.js +139 -36
  154. package/dist/esm/config/resources/organizations.resource-presets.js.map +1 -1
  155. package/dist/esm/config/resources/resource.config.utils.d.ts +20 -0
  156. package/dist/esm/config/resources/resource.config.utils.d.ts.map +1 -1
  157. package/dist/esm/config/resources/resource.config.utils.js +20 -0
  158. package/dist/esm/config/resources/resource.config.utils.js.map +1 -1
  159. package/dist/esm/config/resources/retention-enforcement.resource-presets.d.ts +5 -0
  160. package/dist/esm/config/resources/retention-enforcement.resource-presets.d.ts.map +1 -0
  161. package/dist/esm/config/resources/retention-enforcement.resource-presets.js +74 -0
  162. package/dist/esm/config/resources/retention-enforcement.resource-presets.js.map +1 -0
  163. package/dist/esm/config/resources/security.resource-presets.d.ts +3 -1
  164. package/dist/esm/config/resources/security.resource-presets.d.ts.map +1 -1
  165. package/dist/esm/config/resources/security.resource-presets.js +563 -133
  166. package/dist/esm/config/resources/security.resource-presets.js.map +1 -1
  167. package/dist/esm/config/resources/service-providers.resource-presets.d.ts +4 -0
  168. package/dist/esm/config/resources/service-providers.resource-presets.d.ts.map +1 -0
  169. package/dist/esm/config/resources/service-providers.resource-presets.js +321 -0
  170. package/dist/esm/config/resources/service-providers.resource-presets.js.map +1 -0
  171. package/dist/esm/config/resources/webhooks.resource-presets.d.ts +4 -3
  172. package/dist/esm/config/resources/webhooks.resource-presets.d.ts.map +1 -1
  173. package/dist/esm/config/resources/webhooks.resource-presets.js +76 -25
  174. package/dist/esm/config/resources/webhooks.resource-presets.js.map +1 -1
  175. package/dist/esm/index.d.ts +1 -0
  176. package/dist/esm/index.d.ts.map +1 -1
  177. package/dist/esm/index.js +1 -0
  178. package/dist/esm/index.js.map +1 -1
  179. package/dist/esm/registration/register-common-enums.d.ts.map +1 -1
  180. package/dist/esm/registration/register-common-enums.js +15 -0
  181. package/dist/esm/registration/register-common-enums.js.map +1 -1
  182. package/dist/esm/registration/register-provider-components.d.ts.map +1 -1
  183. package/dist/esm/registration/register-provider-components.js +6 -0
  184. package/dist/esm/registration/register-provider-components.js.map +1 -1
  185. package/dist/esm/themes/dense.theme.d.ts.map +1 -1
  186. package/dist/esm/themes/dense.theme.js +22 -2
  187. package/dist/esm/themes/dense.theme.js.map +1 -1
  188. package/dist/tsconfig.build.tsbuildinfo +1 -1
  189. package/package.json +8 -6
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { RelatedResourceSections, resourceUIBehavior, resourceUIBehaviorFrom, polymorphicResourceUIBehavior, StandardIconReference, SummaryFlow, SummaryMeta, SummaryTitle, } from '@wildo-ai/saas-frontend-lib';
3
- import { useFormTemplateContext, useDisplayTemplateContext, DisplayField, DisplayLayout, FormField, FormLayout, ObjectScope, Section, SummaryLayout, } from '@wildo-ai/saas-frontend-lib';
2
+ import { RelatedResourceSections, ResourceHeaderTitle, resourceUIBehavior, resourceUIBehaviorFrom, polymorphicResourceUIBehavior, StandardIconReference, SummaryFlow, SummaryMeta, SummaryTitle, unresolvedLabel, } from '@wildo-ai/saas-frontend-lib';
3
+ import { useDisplayTemplateContext, DisplayField, DisplayLayout, FormField, FormLayout, ObjectScope, Section, SummaryLayout, } from '@wildo-ai/saas-frontend-lib';
4
4
  import { BadgeSemanticVariant, BooleanDisplayMode, BooleanEditMode, BooleanLabellingValue, SectionMode, DisplayChromeMode, EditChromeMode, EnumDisplayMode, FormFieldComponent, NumberDisplayMode, StringDisplayMode, } from '@wildo-ai/zod-decorators';
5
- import { Package, Layers, Puzzle, Gauge, CircleDot, Coins, ShoppingBag, Hash, Layers2, TrendingUp, Package as PackageIcon, Calendar, CalendarDays, Ticket, KeyRound, Gift, Handshake, Percent, Banknote, Clock, UserCheck, CheckCircle2, TimerOff, CircleX, Building2, User, Ban, AlertTriangle, Wallet, Loader, OctagonX, PauseCircle, Sparkles, Square, Repeat, Infinity, FileText, BarChart3, BadgeCheck, PowerOff, ShoppingCart, ExternalLink, Info, CirclePlay, ArrowRightLeft, PackagePlus, PackageMinus, CirclePlus, } from 'lucide-react';
6
- import { CoreResourceType as R, CoreResourceOperation as Op, Promotion_Operations, BillingAccount_Operations, BillingSubscription_Operations, CreditPool_Operations, productsResourceConfiguration_InitializationFactory as productsFactory, pricesResourceConfiguration_InitializationFactory as pricesFactory, referralsResourceConfiguration_InitializationFactory as referralsFactory, promotionsResourceConfiguration_InitializationFactory as promotionsFactory, billingAccountsPolymorphicFactories, billingSubscriptionsPolymorphicFactories, creditPoolsPolymorphicFactories, invoiceRefsPolymorphicFactories, usageRecordsPolymorphicFactories, ProductSchema, PriceSchema, ReferralSchema, PromotionSchema, BillingAccountBaseSchema, BillingSubscriptionBaseSchema, CreditPoolSchema, InvoiceRefSchema, UsageRecordSchema, ReferralStatus, SubscriptionStatus, InvoiceStatus, BillingScopeType, PromotionDuration, ProductType, PricingModel, BillingInterval, PromotionType, DiscountType, } from '@wildo-ai/saas-models';
5
+ import { Package, Layers, Puzzle, Gauge, CircleDot, Coins, ShoppingBag, Hash, Layers2, TrendingUp, Package as PackageIcon, Calendar, CalendarDays, Ticket, KeyRound, Gift, Handshake, Percent, Banknote, Clock, UserCheck, CheckCircle2, TimerOff, CircleX, Building2, User, Ban, AlertTriangle, Wallet, Loader, OctagonX, PauseCircle, Sparkles, Square, Repeat, Infinity, FileText, BarChart3, BadgeCheck, PowerOff, ShoppingCart, ExternalLink, Info, CirclePlay, CalendarX2, ArrowRightLeft, PackagePlus, PackageMinus, CirclePlus, } from 'lucide-react';
6
+ import { CoreResourceType as R, CoreResourceOperation as Op, Promotion_Operations, BillingAccount_Operations, Referral_Operations, BillingSubscription_Operations, CreditPool_Operations, productsResourceConfiguration_InitializationFactory as productsFactory, pricesResourceConfiguration_InitializationFactory as pricesFactory, referralsResourceConfiguration_InitializationFactory as referralsFactory, promotionsResourceConfiguration_InitializationFactory as promotionsFactory, billingAccountsPolymorphicFactories, billingSubscriptionsPolymorphicFactories, creditPoolsPolymorphicFactories, invoiceRefsPolymorphicFactories, usageRecordsPolymorphicFactories, ProductSchema, PriceSchema, ReferralSchema, PromotionSchema, BillingAccountBaseSchema, BillingSubscriptionBaseSchema, CreditPoolSchema, InvoiceRefSchema, UsageRecordSchema, ReferralStatus, SubscriptionStatus, InvoiceStatus, BillingScopeType, PromotionDuration, ProductType, ProductAvailability, PricingModel, BillingInterval, PromotionType, DiscountType, } from '@wildo-ai/saas-models';
7
+ import { LabellingTypes } from '@wildo-ai/presets-components-models';
7
8
  import { notAddressable, apiOnly } from './resource.config.utils.js';
8
9
  export function buildBillingMainPresets() {
9
10
  /**
@@ -25,38 +26,61 @@ export function buildBillingMainPresets() {
25
26
  return {
26
27
  [R.PRODUCTS]: resourceUIBehavior(productsFactory, ProductSchema, ({ schema, schemaShape, opsVariantResolver }) => ({
27
28
  icon: Package,
28
- emptyState: { primaryAction: Op.CREATE },
29
29
  layout: {
30
+ // No `tax` section any more: it was a heading over one field. The tax display mode is a commercial
31
+ // term of the product, so it sits in `overview` with the other terms.
30
32
  refs: {
31
- sections: ['overview', 'tax', 'purchase', 'behavior', 'visibility'],
33
+ sections: ['overview', 'purchase', 'behavior', 'visibility'],
32
34
  },
35
+ // A catalogue entry is configuration: settings rows on read and edit, bare values on a card.
33
36
  defaultChromeMode: {
34
- display: DisplayChromeMode.STACKED,
35
- summary: DisplayChromeMode.INLINE,
37
+ display: DisplayChromeMode.SETTINGS_ROW,
38
+ summary: DisplayChromeMode.HIDDEN,
36
39
  edit: EditChromeMode.SETTINGS_ROW,
37
40
  },
38
- edit: () => {
39
- const { isExisting, operationType } = useFormTemplateContext();
40
- return (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "key" }), _jsx(FormField, { name: "type" }), _jsx(FormField, { name: "planTier" }), _jsx(FormField, { name: "planTierOrder" }), _jsx(FormField, { name: "requiredPlanTiers" }), _jsx(FormField, { name: "targetScopes" }), _jsx(FormField, { name: "grantedFeatures" }), _jsx(FormField, { name: "grantedLimits" }), _jsx(FormField, { name: "sortOrder" })] }), _jsx(Section, { name: "tax", children: _jsx(FormField, { name: "taxDisplayMode" }) }), _jsx(Section, { name: "purchase", appearance: SectionMode.CARD, children: _jsxs(ObjectScope, { name: "purchaseConfig", children: [_jsx(FormField, { name: "maxQuantityPerPurchase" }), _jsx(FormField, { name: "creditsPerUnit" }), _jsx(FormField, { name: "creditPoolKey" })] }) }), _jsx(Section, { name: "behavior", children: _jsx(FormField, { name: "behaviorPolicy" }) }), _jsxs(Section, { name: "visibility", children: [_jsx(FormField, { name: "isPublic" }), _jsx(FormField, { name: "isActive" }), _jsx(FormField, { name: "metadata" })] })] }));
41
- },
41
+ // The identity line: the product's name (the record's label, its word — #1851), what KIND of product it is, and whether new
42
+ // checkouts can buy it. None of the three is repeated below.
43
+ header: () => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-field", children: [_jsx(ResourceHeaderTitle, {}), _jsx(DisplayField, { name: "type", hideLabel: true, hideDescription: true, className: "w-auto" }), _jsx(DisplayField, { name: "isActive", hideLabel: true, hideDescription: true, className: "w-auto" })] })),
44
+ /*
45
+ * The form carries what can be CHANGED. `key`, `type`, `grantedFeatures` and `grantedLimits` are
46
+ * `isNotEditable` — they are set when the product is materialized from its definition — so on a form
47
+ * they were four read-only rows restating the read page. The first editable term takes the focus.
48
+ */
49
+ edit: () => (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "planTier", autoFocus: true }), _jsx(FormField, { name: "planTierOrder" }), _jsx(FormField, { name: "requiredPlanTiers" }), _jsx(FormField, { name: "targetScopes" }), _jsx(FormField, { name: "taxDisplayMode" })] }), _jsx(Section, { name: "purchase", appearance: SectionMode.CARD, children: _jsxs(ObjectScope, { name: "purchaseConfig", children: [_jsx(FormField, { name: "maxQuantityPerPurchase" }), _jsx(FormField, { name: "creditsPerUnit" }), _jsx(FormField, { name: "creditPoolKey" })] }) }), _jsx(Section, { name: "behavior", children: _jsx(FormField, { name: "behaviorPolicy" }) }), _jsxs(Section, { name: "visibility", children: [_jsx(FormField, { name: "isPublic" }), _jsx(FormField, { name: "isActive" }), _jsx(FormField, { name: "sortOrder" }), _jsx(FormField, { name: "metadata" })] })] })),
50
+ /*
51
+ * Read: the header names the product, its type and its availability, so the body starts at the terms.
52
+ * No read section carries a description — "Does not set legal tax rates", "Backend enforcement lives in
53
+ * services" were notes to an engineer, on a page an operator reads. The purchase and behaviour blocks only
54
+ * exist for some product types, so they render only when the product carries them; an add-on used to show
55
+ * a "Purchase settings" card of three dashes.
56
+ */
42
57
  display: () => {
43
- const { data, isLoading } = useDisplayTemplateContext();
44
- return (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(DisplayField, { name: "key" }), _jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "planTier" }), _jsx(DisplayField, { name: "planTierOrder" }), _jsx(DisplayField, { name: "sortOrder" }), _jsx(DisplayField, { name: "targetScopes" }), _jsx(DisplayField, { name: "grantedFeatures" }), _jsx(DisplayField, { name: "grantedLimits" })] }), _jsx(Section, { name: "tax", children: _jsx(DisplayField, { name: "taxDisplayMode" }) }), _jsx(Section, { name: "purchase", appearance: SectionMode.CARD, children: _jsxs(ObjectScope, { name: "purchaseConfig", children: [_jsx(DisplayField, { name: "maxQuantityPerPurchase" }), _jsx(DisplayField, { name: "creditsPerUnit" }), _jsx(DisplayField, { name: "creditPoolKey" })] }) }), _jsx(Section, { name: "behavior", children: _jsx(DisplayField, { name: "behaviorPolicy" }) }), _jsxs(Section, { name: "visibility", children: [_jsx(DisplayField, { name: "isPublic" }), _jsx(DisplayField, { name: "isActive" }), _jsx(DisplayField, { name: "metadata" })] }), _jsx(RelatedResourceSections, { config: { mode: SectionMode.TABS }, targets: [R.PRICES] })] }));
58
+ const { data } = useDisplayTemplateContext();
59
+ const purchase = data?.purchaseConfig;
60
+ const purchaseHas = (key) => purchase?.[key] !== undefined && purchase?.[key] !== null;
61
+ const hasPurchase = purchaseHas('maxQuantityPerPurchase') || purchaseHas('creditsPerUnit') || purchaseHas('creditPoolKey');
62
+ const behavior = data?.behaviorPolicy;
63
+ const hasBehavior = !!behavior && Object.keys(behavior).length > 0;
64
+ return (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, hideDescription: true, children: [_jsx(DisplayField, { name: "key" }), _jsx(DisplayField, { name: "planTier" }), _jsx(DisplayField, { name: "planTierOrder" }), _jsx(DisplayField, { name: "requiredPlanTiers" }), _jsx(DisplayField, { name: "targetScopes" }), _jsx(DisplayField, { name: "grantedFeatures" }), _jsx(DisplayField, { name: "grantedLimits" }), _jsx(DisplayField, { name: "taxDisplayMode" })] }), hasPurchase && (_jsx(Section, { name: "purchase", appearance: SectionMode.CARD, hideDescription: true, children: _jsxs(ObjectScope, { name: "purchaseConfig", children: [purchaseHas('maxQuantityPerPurchase') && _jsx(DisplayField, { name: "maxQuantityPerPurchase" }), purchaseHas('creditsPerUnit') && _jsx(DisplayField, { name: "creditsPerUnit" }), purchaseHas('creditPoolKey') && _jsx(DisplayField, { name: "creditPoolKey" })] }) })), hasBehavior && (_jsx(Section, { name: "behavior", chromeMode: DisplayChromeMode.HIDDEN, hideDescription: true, children: _jsx(DisplayField, { name: "behaviorPolicy" }) })), _jsxs(Section, { name: "visibility", hideDescription: true, children: [_jsx(DisplayField, { name: "isPublic" }), _jsx(DisplayField, { name: "sortOrder" }), _jsx(DisplayField, { name: "isRecommended" }), _jsx(DisplayField, { name: "priceDisclosure" }), _jsx(DisplayField, { name: "availability" }), _jsx(DisplayField, { name: "metadata" })] }), _jsx(RelatedResourceSections, { config: { mode: SectionMode.TABS }, targets: [R.PRICES] })] }));
45
65
  },
66
+ /*
67
+ * A catalogue card: the product's name, its key, the type badge, the tier. The title is the host-resolved name
68
+ * (the application's word for the product, #1851) — `name="key"` rendered the raw key where the word
69
+ * belonged. The key stays on the card as a meta value because an operator also identifies a product by it.
70
+ * Availability shows only when it is the EXCEPTION — nearly every product is active and public, and a
71
+ * checkmark on every card was a column of noise that hid the one product that is not. The record id is gone.
72
+ */
46
73
  summary: () => {
47
- const { data, isLoading } = useDisplayTemplateContext();
48
- return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "key" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "planTier" }), _jsx(DisplayField, { name: "isPublic" }), _jsx(DisplayField, { name: "isActive" })] }), _jsx(DisplayField, { name: "_id", hideLabel: true, className: "text-muted-foreground text-xs font-mono truncate max-w-full" })] }));
74
+ const { data } = useDisplayTemplateContext();
75
+ const product = data;
76
+ return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, {}), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "key" }), _jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "planTier" }), product?.isActive === false && _jsx(DisplayField, { name: "isActive" }), product?.isPublic === false && _jsx(DisplayField, { name: "isPublic" }), product?.availability === ProductAvailability.UPCOMING && _jsx(DisplayField, { name: "availability" }), product?.isRecommended === true && _jsx(DisplayField, { name: "isRecommended" })] })] }));
49
77
  },
50
78
  },
51
79
  fields: {
52
80
  _id: schemaShape._id.isHidden(),
53
- key: schemaShape.key.stringUI({
54
- display: { displayMode: StringDisplayMode.COPY, showDescription: true },
55
- edit: { showDescription: true },
56
- }),
81
+ key: schemaShape.key.stringUI({ display: { displayMode: StringDisplayMode.COPY } }),
57
82
  type: schemaShape.type.enumUI({
58
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
59
- edit: { showDescription: true },
83
+ display: { displayMode: EnumDisplayMode.BADGE },
60
84
  values: {
61
85
  [ProductType.PLAN]: { color: BadgeSemanticVariant.INFO, icon: Layers },
62
86
  [ProductType.ADDON]: { color: BadgeSemanticVariant.SECONDARY, icon: Puzzle },
@@ -66,57 +90,94 @@ export function buildBillingMainPresets() {
66
90
  [ProductType.ITEM]: { color: BadgeSemanticVariant.DEFAULT, icon: ShoppingBag },
67
91
  },
68
92
  }),
93
+ // Keeps its description on the FORM only: what a tier is for (plan comparison) is not obvious from the
94
+ // word. On a card the bare tier ("premium") beside the type badge read as a second product name, so the
95
+ // card keeps its label.
69
96
  planTier: schemaShape.planTier.stringUI({
70
- display: { displayMode: StringDisplayMode.TRUNCATE, truncateLength: 48, showDescription: true },
71
- edit: { showDescription: true },
72
- }),
73
- sortOrder: schemaShape.sortOrder.numberUI({
74
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
97
+ display: { displayMode: StringDisplayMode.TRUNCATE, truncateLength: 48, hideWhenEmpty: true },
98
+ summaryOverride: { chromeMode: DisplayChromeMode.INLINE },
75
99
  edit: { showDescription: true },
76
100
  }),
101
+ planTierOrder: schemaShape.planTierOrder.numberUI({ display: { hideWhenEmpty: true } }),
102
+ requiredPlanTiers: schemaShape.requiredPlanTiers.arrayUI({ display: { hideWhenEmpty: true } }),
103
+ grantedFeatures: schemaShape.grantedFeatures.arrayUI({ display: { hideWhenEmpty: true } }),
104
+ grantedLimits: schemaShape.grantedLimits.objectUI({ display: { hideWhenEmpty: true } }),
105
+ sortOrder: schemaShape.sortOrder.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
106
+ // Read as a word ("Active" / "Inactive") because it stands alone in the header and, when inactive, on a
107
+ // card: a bare checkmark there says nothing about what is checked. The form keeps its description — what
108
+ // "active" governs (new checkouts only) is not obvious.
77
109
  isActive: schemaShape.isActive.booleanUI({
78
110
  labellingValue: BooleanLabellingValue.ACTIVE_INACTIVE,
79
- display: { displayMode: BooleanDisplayMode.CHECKMARK, showDescription: true },
111
+ display: { displayMode: BooleanDisplayMode.RAW },
80
112
  edit: { editMode: BooleanEditMode.SWITCH, showDescription: true },
81
113
  }),
114
+ // A bare "No" on a card would not say what is not; the card keeps the label. The form keeps the description
115
+ // (public = listed in self-serve catalogues).
82
116
  isPublic: schemaShape.isPublic.booleanUI({
83
117
  labellingValue: BooleanLabellingValue.YES_NO,
84
- display: { displayMode: BooleanDisplayMode.CHECKMARK, showDescription: true },
118
+ display: { displayMode: BooleanDisplayMode.CHECKMARK },
119
+ summaryOverride: { chromeMode: DisplayChromeMode.INLINE },
85
120
  edit: { editMode: BooleanEditMode.SWITCH, showDescription: true },
86
121
  }),
87
- taxDisplayMode: schemaShape.taxDisplayMode.enumUI({ display: { tooltip: true }, edit: { tooltip: true } }),
122
+ // On a card only when true (see the summary above), and for the same reason as `isPublic` the card keeps the
123
+ // label: a bare checkmark beside the tier would not say WHAT is checked. Set by the catalogue sync, so there is
124
+ // no edit configuration.
125
+ isRecommended: schemaShape.isRecommended.booleanUI({
126
+ labellingValue: BooleanLabellingValue.YES_NO,
127
+ display: { displayMode: BooleanDisplayMode.CHECKMARK },
128
+ summaryOverride: { chromeMode: DisplayChromeMode.INLINE },
129
+ }),
130
+ taxDisplayMode: schemaShape.taxDisplayMode.enumUI({ display: { hideWhenEmpty: true }, edit: { tooltip: true } }),
131
+ metadata: schemaShape.metadata.objectUI({ display: { hideWhenEmpty: true } }),
132
+ },
133
+ /*
134
+ * A product is named by the application's word for it (`billing_products.<key>.name`), never by its key (#1851).
135
+ * The key stays on the page as a copyable row. A product with no authored word answers its key, which LOOKS
136
+ * missing and gets authored. The section is declared so the registry loads it for every surface that names a
137
+ * product — a list row, the header, a subscription line's product reference, the breadcrumb of a price.
138
+ */
139
+ getFrontendResourceLabel: ({ currentObject, t }) => {
140
+ const nameKey = `${LabellingTypes.BILLING_PRODUCTS}.${currentObject.key}.name`;
141
+ const name = t(nameKey);
142
+ return name === nameKey ? unresolvedLabel(currentObject.key) : name;
88
143
  },
89
- getFrontendResourceLabel: ({ currentObject }) => currentObject.key,
144
+ instanceLabelSections: [LabellingTypes.BILLING_PRODUCTS],
90
145
  views: [],
91
146
  })),
92
147
  [R.PRICES]: resourceUIBehavior(pricesFactory, PriceSchema, ({ schema, schemaShape, opsVariantResolver }) => ({
93
148
  icon: StandardIconReference.RECEIPT,
94
- emptyState: { primaryAction: Op.CREATE },
95
149
  layout: {
96
150
  refs: { sections: ['pricing', 'tiers', 'tax', 'availability'] },
97
151
  defaultChromeMode: {
98
- display: DisplayChromeMode.STACKED,
99
- summary: DisplayChromeMode.INLINE,
152
+ display: DisplayChromeMode.SETTINGS_ROW,
153
+ summary: DisplayChromeMode.HIDDEN,
100
154
  edit: EditChromeMode.SETTINGS_ROW,
101
155
  },
102
- edit: () => {
103
- const { isExisting, operationType } = useFormTemplateContext();
104
- return (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "pricing", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "productId" }), _jsx(FormField, { name: "model" }), _jsx(FormField, { name: "unitPrice" }), _jsx(FormField, { name: "interval" }), _jsx(FormField, { name: "intervalCount" })] }), _jsxs(Section, { name: "tiers", children: [_jsx(FormField, { name: "tiers" }), _jsx(FormField, { name: "packageSize" }), _jsx(FormField, { name: "usageAggregation" })] }), _jsx(Section, { name: "tax", children: _jsx(FormField, { name: "taxBehavior" }) }), _jsxs(Section, { name: "availability", chromeMode: EditChromeMode.INLINE, children: [_jsx(FormField, { name: "isDefault" }), _jsx(FormField, { name: "isActive" })] })] }));
105
- },
156
+ // The name of a price is its model and cadence (see `getFrontendResourceLabel`); the header adds whether it
157
+ // is the product's default and whether it is still sold, so neither is a body row.
158
+ header: () => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-field", children: [_jsx(ResourceHeaderTitle, {}), _jsx(DisplayField, { name: "isActive", hideLabel: true, hideDescription: true, className: "w-auto" })] })),
159
+ edit: () => (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "pricing", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "productId" }), _jsx(FormField, { name: "model", autoFocus: true }), _jsx(FormField, { name: "unitPrice" }), _jsx(FormField, { name: "interval" }), _jsx(FormField, { name: "intervalCount" })] }), _jsxs(Section, { name: "tiers", children: [_jsx(FormField, { name: "tiers" }), _jsx(FormField, { name: "packageSize" }), _jsx(FormField, { name: "usageAggregation" })] }), _jsx(Section, { name: "tax", children: _jsx(FormField, { name: "taxBehavior" }) }), _jsxs(Section, { name: "availability", chromeMode: EditChromeMode.INLINE, children: [_jsx(FormField, { name: "isDefault" }), _jsx(FormField, { name: "isActive" })] })] })),
160
+ /*
161
+ * Read: the amount first, then cadence. The product is not repeated — a price is read inside its product's
162
+ * page — and the model is the header. Tiers and tax only exist for some prices, so those rows hide when
163
+ * empty and their sections render only when they hold something; a flat monthly price used to show two
164
+ * headed blocks of dashes. No read section carries a description.
165
+ */
106
166
  display: () => {
107
- const { data, isLoading } = useDisplayTemplateContext();
108
- return (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "pricing", appearance: SectionMode.CARD, children: [_jsx(DisplayField, { name: "productId" }), _jsx(DisplayField, { name: "model" }), _jsx(DisplayField, { name: "unitPrice" }), _jsx(DisplayField, { name: "interval" }), _jsx(DisplayField, { name: "intervalCount" })] }), _jsxs(Section, { name: "tiers", children: [_jsx(DisplayField, { name: "tiers" }), _jsx(DisplayField, { name: "packageSize" }), _jsx(DisplayField, { name: "usageAggregation" })] }), _jsx(Section, { name: "tax", children: _jsx(DisplayField, { name: "taxBehavior" }) }), _jsxs(Section, { name: "availability", children: [_jsx(DisplayField, { name: "isDefault" }), _jsx(DisplayField, { name: "isActive" })] })] }));
109
- },
110
- summary: () => {
111
- const { data, isLoading } = useDisplayTemplateContext();
112
- return (_jsxs(SummaryLayout, { children: [_jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "model" }), _jsx(DisplayField, { name: "unitPrice" }), _jsx(DisplayField, { name: "interval" })] }), _jsx(DisplayField, { name: "_id", hideLabel: true, className: "text-muted-foreground text-xs font-mono truncate max-w-full" })] }));
167
+ const { data } = useDisplayTemplateContext();
168
+ const price = data;
169
+ const hasTiers = (price?.tiers?.length ?? 0) > 0 || price?.packageSize != null || price?.usageAggregation != null;
170
+ return (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "pricing", appearance: SectionMode.CARD, hideTitle: true, hideDescription: true, children: [_jsx(DisplayField, { name: "unitPrice" }), _jsx(DisplayField, { name: "interval" }), _jsx(DisplayField, { name: "intervalCount" }), _jsx(DisplayField, { name: "isDefault" })] }), hasTiers && (_jsxs(Section, { name: "tiers", hideDescription: true, children: [_jsx(DisplayField, { name: "tiers" }), _jsx(DisplayField, { name: "packageSize" }), _jsx(DisplayField, { name: "usageAggregation" })] })), price?.taxBehavior != null && (_jsx(Section, { name: "tax", hideDescription: true, children: _jsx(DisplayField, { name: "taxBehavior" }) }))] }));
113
171
  },
172
+ // A card inside the product's Prices tab: the amount, the cadence, the model — bare, each speaks for itself.
173
+ // The record id is gone.
174
+ summary: () => (_jsx(SummaryLayout, { children: _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "unitPrice" }), _jsx(DisplayField, { name: "interval" }), _jsx(DisplayField, { name: "model" })] }) })),
114
175
  },
115
176
  fields: {
116
177
  _id: schemaShape._id.isHidden(),
117
178
  model: schemaShape.model.enumUI({
118
179
  defaultColor: BadgeSemanticVariant.SECONDARY,
119
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
180
+ display: { displayMode: EnumDisplayMode.BADGE },
120
181
  edit: { showDescription: true },
121
182
  values: {
122
183
  [PricingModel.FLAT]: { color: BadgeSemanticVariant.SECONDARY, icon: Square },
@@ -126,48 +187,77 @@ export function buildBillingMainPresets() {
126
187
  [PricingModel.PACKAGE]: { color: BadgeSemanticVariant.OUTLINE, icon: PackageIcon },
127
188
  },
128
189
  }),
190
+ // Absent on a one-time price, which is exactly what its absence means; the header and the card say it by
191
+ // not showing a cadence.
129
192
  interval: schemaShape.interval.enumUI({
130
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
131
- edit: { showDescription: true },
193
+ display: { displayMode: EnumDisplayMode.BADGE, hideWhenEmpty: true },
132
194
  values: {
133
195
  [BillingInterval.MONTH]: { color: BadgeSemanticVariant.SECONDARY, icon: Calendar },
134
196
  [BillingInterval.YEAR]: { color: BadgeSemanticVariant.INFO, icon: CalendarDays },
135
197
  },
136
198
  }),
137
199
  intervalCount: schemaShape.intervalCount.numberUI({
138
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
200
+ display: { displayMode: NumberDisplayMode.FORMATTED, hideWhenEmpty: true },
139
201
  edit: { showDescription: true },
140
202
  }),
141
- packageSize: schemaShape.packageSize.numberUI({ display: { tooltip: true }, edit: { tooltip: true } }),
142
- usageAggregation: schemaShape.usageAggregation.enumUI({ display: { tooltip: true }, edit: { tooltip: true } }),
143
- taxBehavior: schemaShape.taxBehavior.enumUI({ display: { tooltip: true }, edit: { tooltip: true } }),
203
+ tiers: schemaShape.tiers.arrayUI({ display: { hideWhenEmpty: true } }),
204
+ packageSize: schemaShape.packageSize.numberUI({ display: { hideWhenEmpty: true }, edit: { tooltip: true } }),
205
+ usageAggregation: schemaShape.usageAggregation.enumUI({ display: { hideWhenEmpty: true }, edit: { tooltip: true } }),
206
+ taxBehavior: schemaShape.taxBehavior.enumUI({ display: { hideWhenEmpty: true }, edit: { tooltip: true } }),
207
+ // A default price is the exception worth reading; "not the default" is not information.
144
208
  isDefault: schemaShape.isDefault.booleanUI({
145
209
  labellingValue: BooleanLabellingValue.YES_NO,
146
- display: { displayMode: BooleanDisplayMode.CHECKMARK, showDescription: true },
210
+ display: { displayMode: BooleanDisplayMode.CHECKMARK },
147
211
  edit: { editMode: BooleanEditMode.SWITCH, showDescription: true },
148
212
  }),
213
+ // A word in the header, for the same reason as a product's.
149
214
  isActive: schemaShape.isActive.booleanUI({
150
215
  labellingValue: BooleanLabellingValue.ACTIVE_INACTIVE,
151
- display: { displayMode: BooleanDisplayMode.CHECKMARK, showDescription: true },
216
+ display: { displayMode: BooleanDisplayMode.RAW },
152
217
  edit: { editMode: BooleanEditMode.SWITCH, showDescription: true },
153
218
  }),
154
219
  },
155
- getFrontendResourceLabel: ({ currentObject }) => `${currentObject.model} — ${currentObject.interval ?? 'one-time'}`,
220
+ /*
221
+ * A price's name is its model and, when it recurs, its cadence — both in the application's own words. It used
222
+ * to join the raw wire values with an English "one-time", which no translator could reach
223
+ * (`display-text-comes-from-i18n.md`). A missing word answers the raw value, which LOOKS missing and gets
224
+ * authored. A one-time price is named by its model alone.
225
+ */
226
+ getFrontendResourceLabel: ({ currentObject, t }) => {
227
+ const word = (key, raw) => {
228
+ const resolved = t(key);
229
+ return resolved === key ? raw : resolved;
230
+ };
231
+ const model = word(`${LabellingTypes.SYSTEM_ENUMS}.PricingModel.${currentObject.model}`, String(currentObject.model));
232
+ if (!currentObject.interval)
233
+ return model;
234
+ const interval = word(`${LabellingTypes.RESOURCES}.${R.PRICES}.fields.interval.enum.${currentObject.interval}`, String(currentObject.interval));
235
+ return `${model} · ${interval}`;
236
+ },
156
237
  views: [],
157
238
  })),
158
239
  [R.REFERRALS]: resourceUIBehavior(referralsFactory, ReferralSchema, ({ schema, schemaShape, opsVariantResolver }) => ({
159
240
  icon: StandardIconReference.SHARE,
160
- emptyState: { primaryAction: Op.CREATE },
161
241
  layout: {
242
+ // No sections: a referral is one code, its state, the two sides and what each earned. It is an immutable
243
+ // ledger row (no edit form), read in a list.
162
244
  defaultChromeMode: {
163
- display: DisplayChromeMode.STACKED,
164
- summary: DisplayChromeMode.INLINE,
245
+ display: DisplayChromeMode.SETTINGS_ROW,
246
+ summary: DisplayChromeMode.HIDDEN,
165
247
  edit: EditChromeMode.SETTINGS_ROW,
166
248
  },
167
- summary: () => {
168
- const { data, isLoading } = useDisplayTemplateContext();
169
- return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "referralCode" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "status" }), _jsx(DisplayField, { name: "referrerScopeType" }), _jsx(DisplayField, { name: "referredScopeType" })] }), _jsxs(SummaryMeta, { className: "text-muted-foreground text-xs", children: [_jsx(DisplayField, { name: "expiresAt" }), _jsx(DisplayField, { name: "convertedAt" })] }), _jsx(DisplayField, { name: "referrerBillingAccountId", hideLabel: true, className: "text-xs font-mono truncate max-w-full" })] }));
170
- },
249
+ // The code is the record's name and the status its state; neither is repeated in the body.
250
+ header: () => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-field", children: [_jsx(ResourceHeaderTitle, {}), _jsx(DisplayField, { name: "status", hideLabel: true, hideDescription: true, className: "w-auto" })] })),
251
+ // The two sides, when each moved, and what each earned. Every optional row hides when empty — a referral
252
+ // that has not been accepted yet has no referred side, and a column of dashes said nothing.
253
+ display: () => (_jsxs(DisplayLayout, { children: [_jsx(DisplayField, { name: "referrerScopeType" }), _jsx(DisplayField, { name: "referredScopeType" }), _jsx(DisplayField, { name: "expiresAt" }), _jsx(DisplayField, { name: "acceptedAt" }), _jsx(DisplayField, { name: "convertedAt" }), _jsx(DisplayField, { name: "referrerReward" }), _jsx(DisplayField, { name: "referrerRewardApplied" }), _jsx(DisplayField, { name: "referredReward" }), _jsx(DisplayField, { name: "referredRewardApplied" })] })),
254
+ /*
255
+ * A card: the code and its status badge. The two scope badges and the two dates keep their labels — a bare
256
+ * "Organization" beside a bare "User" does not say which side referred, and a bare date does not say what
257
+ * happened on it. The referrer's billing-account id is gone: in an organization's own referrals it is that
258
+ * organization, and an opaque id told the reader nothing.
259
+ */
260
+ summary: () => (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "referralCode" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "status" }), _jsx(DisplayField, { name: "referrerScopeType" }), _jsx(DisplayField, { name: "referredScopeType" })] }), _jsxs(SummaryMeta, { className: "text-muted-foreground text-xs", children: [_jsx(DisplayField, { name: "expiresAt" }), _jsx(DisplayField, { name: "convertedAt" })] })] })),
171
261
  },
172
262
  fields: {
173
263
  _id: schemaShape._id.isHidden(),
@@ -184,18 +274,25 @@ export function buildBillingMainPresets() {
184
274
  referralCode: schemaShape.referralCode.stringUI({ display: { displayMode: StringDisplayMode.COPY } }),
185
275
  referrerScopeType: schemaShape.referrerScopeType.enumUI({
186
276
  display: { displayMode: EnumDisplayMode.BADGE },
277
+ summaryOverride: { chromeMode: DisplayChromeMode.INLINE },
187
278
  values: {
188
279
  [BillingScopeType.ORGANIZATIONS]: { color: BadgeSemanticVariant.SECONDARY, icon: Building2 },
189
280
  [BillingScopeType.USER_SELF]: { color: BadgeSemanticVariant.OUTLINE, icon: User },
190
281
  },
191
282
  }),
192
283
  referredScopeType: schemaShape.referredScopeType.enumUI({
193
- display: { displayMode: EnumDisplayMode.BADGE },
284
+ display: { displayMode: EnumDisplayMode.BADGE, hideWhenEmpty: true },
285
+ summaryOverride: { chromeMode: DisplayChromeMode.INLINE },
194
286
  values: {
195
287
  [BillingScopeType.ORGANIZATIONS]: { color: BadgeSemanticVariant.SECONDARY, icon: Building2 },
196
288
  [BillingScopeType.USER_SELF]: { color: BadgeSemanticVariant.OUTLINE, icon: User },
197
289
  },
198
290
  }),
291
+ expiresAt: schemaShape.expiresAt.dateUI({ display: { hideWhenEmpty: true }, summaryOverride: { chromeMode: DisplayChromeMode.INLINE } }),
292
+ acceptedAt: schemaShape.acceptedAt.dateUI({ display: { hideWhenEmpty: true } }),
293
+ convertedAt: schemaShape.convertedAt.dateUI({ display: { hideWhenEmpty: true }, summaryOverride: { chromeMode: DisplayChromeMode.INLINE } }),
294
+ referrerReward: schemaShape.referrerReward.objectUI({ display: { hideWhenEmpty: true } }),
295
+ referredReward: schemaShape.referredReward.objectUI({ display: { hideWhenEmpty: true } }),
199
296
  referrerRewardApplied: schemaShape.referrerRewardApplied.booleanUI({
200
297
  labellingValue: BooleanLabellingValue.YES_NO,
201
298
  display: { displayMode: BooleanDisplayMode.CHECKMARK },
@@ -206,45 +303,58 @@ export function buildBillingMainPresets() {
206
303
  }),
207
304
  },
208
305
  getFrontendResourceLabel: ({ currentObject }) => currentObject.referralCode,
209
- views: [],
306
+ // #1802: the organization owner issues a code from the list, and withdraws one that has not converted.
307
+ views: [
308
+ [Referral_Operations.ISSUE, notAddressable({ iconOverride: CirclePlus })],
309
+ [Referral_Operations.CANCEL, notAddressable({ iconOverride: CircleX })],
310
+ ],
210
311
  })),
211
312
  [R.PROMOTIONS]: resourceUIBehavior(promotionsFactory, PromotionSchema, ({ schema, schemaShape, opsVariantResolver }) => ({
212
313
  icon: StandardIconReference.PROMOTION,
213
- emptyState: { primaryAction: Op.CREATE },
214
314
  layout: {
215
315
  refs: { sections: ['overview', 'discount', 'duration', 'applicability', 'validity'] },
316
+ // An operator AUTHORS a promotion (a name, a description), so the form stacks; the read page is rows.
216
317
  defaultChromeMode: {
217
- display: DisplayChromeMode.STACKED,
218
- summary: DisplayChromeMode.INLINE,
219
- edit: EditChromeMode.SETTINGS_ROW,
220
- },
221
- edit: () => (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "name" }), _jsx(FormField, { name: "description" }), _jsx(FormField, { name: "type" }), _jsx(FormField, { name: "code" })] }), _jsxs(Section, { name: "discount", children: [_jsx(FormField, { name: "discountType" }), _jsx(FormField, { name: "discountPercentage" }), _jsx(FormField, { name: "discountFixedAmount" })] }), _jsxs(Section, { name: "duration", children: [_jsx(FormField, { name: "duration" }), _jsx(FormField, { name: "durationInMonths" })] }), _jsxs(Section, { name: "applicability", children: [_jsx(FormField, { name: "applicableProducts" }), _jsx(FormField, { name: "maxRedemptions" }), _jsx(FormField, { name: "timesRedeemed" })] }), _jsxs(Section, { name: "validity", children: [_jsx(FormField, { name: "validFrom" }), _jsx(FormField, { name: "validUntil" }), _jsx(FormField, { name: "isActive" }), _jsx(FormField, { name: "metadata" })] })] })),
222
- display: () => {
223
- const { data, isLoading } = useDisplayTemplateContext();
224
- return (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(DisplayField, { name: "name" }), _jsx(DisplayField, { name: "description" }), _jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "code" })] }), _jsxs(Section, { name: "discount", children: [_jsx(DisplayField, { name: "discountType" }), _jsx(DisplayField, { name: "discountPercentage" }), _jsx(DisplayField, { name: "discountFixedAmount" })] }), _jsxs(Section, { name: "duration", children: [_jsx(DisplayField, { name: "duration" }), _jsx(DisplayField, { name: "durationInMonths" })] }), _jsxs(Section, { name: "applicability", children: [_jsx(DisplayField, { name: "applicableProducts" }), _jsx(DisplayField, { name: "maxRedemptions" }), _jsx(DisplayField, { name: "timesRedeemed" })] }), _jsxs(Section, { name: "validity", children: [_jsx(DisplayField, { name: "validFrom" }), _jsx(DisplayField, { name: "validUntil" }), _jsx(DisplayField, { name: "isActive" }), _jsx(DisplayField, { name: "metadata" })] })] }));
225
- },
226
- summary: () => {
227
- const { data, isLoading } = useDisplayTemplateContext();
228
- return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "name" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "discountType" })] }), _jsx(DisplayField, { name: "_id", hideLabel: true, className: "text-muted-foreground text-xs font-mono truncate max-w-full" })] }));
318
+ display: DisplayChromeMode.SETTINGS_ROW,
319
+ summary: DisplayChromeMode.HIDDEN,
320
+ edit: EditChromeMode.STACKED,
229
321
  },
322
+ // The name, the kind of promotion and whether it still accepts redemptions — the three things the body no
323
+ // longer repeats.
324
+ header: () => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-field", children: [_jsx(ResourceHeaderTitle, {}), _jsx(DisplayField, { name: "type", hideLabel: true, hideDescription: true, className: "w-auto" }), _jsx(DisplayField, { name: "isActive", hideLabel: true, hideDescription: true, className: "w-auto" })] })),
325
+ /*
326
+ * The name first, focused; the description is prose, so a textarea. `timesRedeemed` is a counter the
327
+ * system advances (`isNotEditable`), so it is read on the record, not shown as a disabled input here. The
328
+ * discount and duration blocks keep their descriptions: which of the two amounts applies, and what
329
+ * "repeating" needs, are not obvious from the fields.
330
+ */
331
+ edit: () => (_jsxs(FormLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, children: [_jsx(FormField, { name: "name", autoFocus: true }), _jsx(FormField, { name: "description" }), _jsx(FormField, { name: "type" }), _jsx(FormField, { name: "code" })] }), _jsxs(Section, { name: "discount", children: [_jsx(FormField, { name: "discountType" }), _jsx(FormField, { name: "discountPercentage" }), _jsx(FormField, { name: "discountFixedAmount" })] }), _jsxs(Section, { name: "duration", children: [_jsx(FormField, { name: "duration" }), _jsx(FormField, { name: "durationInMonths" })] }), _jsxs(Section, { name: "applicability", children: [_jsx(FormField, { name: "applicableProducts" }), _jsx(FormField, { name: "maxRedemptions" })] }), _jsxs(Section, { name: "validity", children: [_jsx(FormField, { name: "validFrom" }), _jsx(FormField, { name: "validUntil" }), _jsx(FormField, { name: "isActive" }), _jsx(FormField, { name: "metadata" })] })] })),
332
+ /*
333
+ * Read: the header is the name, type and state. The first card is the promotion's own words and its code,
334
+ * with no heading — "Overview" over a description said nothing the description does not. No read section
335
+ * carries a description; those were notes about provider sync and API behaviour.
336
+ */
337
+ display: () => (_jsxs(DisplayLayout, { children: [_jsxs(Section, { name: "overview", appearance: SectionMode.CARD, hideTitle: true, hideDescription: true, children: [_jsx(DisplayField, { name: "description" }), _jsx(DisplayField, { name: "code" })] }), _jsxs(Section, { name: "discount", hideDescription: true, children: [_jsx(DisplayField, { name: "discountType" }), _jsx(DisplayField, { name: "discountPercentage" }), _jsx(DisplayField, { name: "discountFixedAmount" })] }), _jsxs(Section, { name: "duration", hideDescription: true, children: [_jsx(DisplayField, { name: "duration" }), _jsx(DisplayField, { name: "durationInMonths" })] }), _jsxs(Section, { name: "applicability", hideDescription: true, children: [_jsx(DisplayField, { name: "applicableProducts" }), _jsx(DisplayField, { name: "maxRedemptions" }), _jsx(DisplayField, { name: "timesRedeemed" })] }), _jsxs(Section, { name: "validity", hideDescription: true, children: [_jsx(DisplayField, { name: "validFrom" }), _jsx(DisplayField, { name: "validUntil" }), _jsx(DisplayField, { name: "metadata" })] })] })),
338
+ // A card: the name, then the promotion type and the discount type as badges. The record id is gone.
339
+ summary: () => (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "name" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "type" }), _jsx(DisplayField, { name: "discountType" })] })] })),
230
340
  },
231
341
  fields: {
232
342
  _id: schemaShape._id.isHidden(),
233
343
  name: schemaShape.name.stringUI({
234
- display: { displayMode: StringDisplayMode.TRUNCATE, truncateLength: 64, showDescription: true },
235
- edit: { showDescription: true },
344
+ display: { displayMode: StringDisplayMode.TRUNCATE, truncateLength: 64 },
236
345
  }),
237
346
  description: schemaShape.description.stringUI({
238
- display: { displayMode: StringDisplayMode.MULTILINE, showDescription: true },
239
- edit: { component: FormFieldComponent.TEXTAREA, showDescription: true },
347
+ display: { displayMode: StringDisplayMode.MULTILINE, hideWhenEmpty: true },
348
+ edit: { component: FormFieldComponent.TEXTAREA },
240
349
  }),
350
+ // Hidden when empty: a deal with no customer-facing code is a real kind of promotion, not a gap. The form
351
+ // keeps the description — what the code is for (typed at checkout) is not obvious.
241
352
  code: schemaShape.code.stringUI({
242
- display: { displayMode: StringDisplayMode.COPY, showDescription: true },
353
+ display: { displayMode: StringDisplayMode.COPY, hideWhenEmpty: true },
243
354
  edit: { showDescription: true },
244
355
  }),
245
356
  type: schemaShape.type.enumUI({
246
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
247
- edit: { showDescription: true },
357
+ display: { displayMode: EnumDisplayMode.BADGE },
248
358
  values: {
249
359
  [PromotionType.COUPON]: { color: BadgeSemanticVariant.SUCCESS, icon: Ticket },
250
360
  [PromotionType.PROMOTION_CODE]: { color: BadgeSemanticVariant.INFO, icon: KeyRound },
@@ -253,44 +363,43 @@ export function buildBillingMainPresets() {
253
363
  },
254
364
  }),
255
365
  discountType: schemaShape.discountType.enumUI({
256
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
257
- edit: { showDescription: true },
366
+ display: { displayMode: EnumDisplayMode.BADGE },
258
367
  values: {
259
368
  [DiscountType.PERCENTAGE]: { color: BadgeSemanticVariant.INFO, icon: Percent },
260
369
  [DiscountType.FIXED_AMOUNT]: { color: BadgeSemanticVariant.SECONDARY, icon: Banknote },
261
370
  },
262
371
  }),
372
+ // Only one of the two amounts applies (per `discountType`); the other is empty and hides.
263
373
  discountPercentage: schemaShape.discountPercentage.numberUI({
264
374
  suffix: '%',
265
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
266
- edit: { showDescription: true },
375
+ display: { displayMode: NumberDisplayMode.FORMATTED, hideWhenEmpty: true },
267
376
  }),
377
+ discountFixedAmount: schemaShape.discountFixedAmount.moneyUI({ display: { hideWhenEmpty: true } }),
268
378
  duration: schemaShape.duration.enumUI({
269
- display: { displayMode: EnumDisplayMode.BADGE, showDescription: true },
270
- edit: { showDescription: true },
379
+ display: { displayMode: EnumDisplayMode.BADGE },
271
380
  values: {
272
381
  [PromotionDuration.ONCE]: { color: BadgeSemanticVariant.SECONDARY, icon: CircleDot },
273
382
  [PromotionDuration.REPEATING]: { color: BadgeSemanticVariant.INFO, icon: Repeat },
274
383
  [PromotionDuration.FOREVER]: { color: BadgeSemanticVariant.SUCCESS, icon: Infinity },
275
384
  },
276
385
  }),
386
+ // Only a REPEATING promotion has a month count.
277
387
  durationInMonths: schemaShape.durationInMonths.numberUI({
278
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
279
- edit: { showDescription: true },
280
- }),
281
- maxRedemptions: schemaShape.maxRedemptions.numberUI({
282
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
283
- edit: { showDescription: true },
284
- }),
285
- timesRedeemed: schemaShape.timesRedeemed.numberUI({
286
- display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
287
- edit: { showDescription: true },
288
- }),
388
+ display: { displayMode: NumberDisplayMode.FORMATTED, hideWhenEmpty: true },
389
+ }),
390
+ // NOT hidden when empty, and neither are the redemption cap and the validity bounds: for these four an
391
+ // empty value MEANS something ("every product", "unlimited", "no start", "no end"). The dash is kept until
392
+ // those words are authored, rather than hiding a real answer.
393
+ maxRedemptions: schemaShape.maxRedemptions.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
394
+ timesRedeemed: schemaShape.timesRedeemed.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
395
+ // A word in the header ("Active" / "Inactive"); a bare checkmark beside the name said nothing about what
396
+ // was checked. The form keeps the description: deactivating stops NEW redemptions only.
289
397
  isActive: schemaShape.isActive.booleanUI({
290
398
  labellingValue: BooleanLabellingValue.ACTIVE_INACTIVE,
291
- display: { displayMode: BooleanDisplayMode.CHECKMARK, showDescription: true },
399
+ display: { displayMode: BooleanDisplayMode.RAW },
292
400
  edit: { editMode: BooleanEditMode.SWITCH, showDescription: true },
293
401
  }),
402
+ metadata: schemaShape.metadata.objectUI({ display: { hideWhenEmpty: true } }),
294
403
  },
295
404
  getFrontendResourceLabel: ({ currentObject }) => currentObject.name,
296
405
  views: [
@@ -319,14 +428,16 @@ export function buildBillingMainPresets() {
319
428
  display: { displayMode: BooleanDisplayMode.CHECKMARK },
320
429
  }),
321
430
  },
322
- // A billing account has no user-authored name — it is the singleton
323
- // provider link for its scope. Label it by its role + state instead of
324
- // leaking the raw `_id` as a title (same raw-composition convention as
325
- // the invoice label below).
326
- getFrontendResourceLabel: ({ currentObject }) => currentObject.isActive === false ? 'Billing account (inactive)' : 'Billing account',
431
+ // A billing account has no user-authored name — it is the singleton provider link for its scope — so it is
432
+ // named by its resource's own words (the READ title, "Billing account"), never by its raw `_id`. Whether it
433
+ // is active is its STATE, a badge, not part of the name; the name used to be English written in code
434
+ // ("Billing account (inactive)"), which no translator could reach. A missing word shows its key.
435
+ getFrontendResourceLabel: ({ t, resourceType }) => t(`${LabellingTypes.RESOURCES}.${resourceType}.operations.read.title`),
327
436
  views: [
328
437
  [BillingAccount_Operations.OPEN_CHECKOUT, notAddressable({ iconOverride: ShoppingCart })],
329
438
  [BillingAccount_Operations.OPEN_PORTAL, notAddressable({ iconOverride: ExternalLink })],
439
+ // #1802: a new customer redeems a referral code on its own account.
440
+ [BillingAccount_Operations.REDEEM_REFERRAL, notAddressable({ iconOverride: Handshake })],
330
441
  [BillingAccount_Operations.GET_STATE, notAddressable({ iconOverride: Info })],
331
442
  // Billing accounts are Stripe-managed (portal/checkout) — generic UPDATE/DELETE
332
443
  // are headless (no inline edit/delete UI).
@@ -416,7 +527,9 @@ export function buildBillingMainPresets() {
416
527
  edit: { component: FormFieldComponent.TEXTAREA },
417
528
  }),
418
529
  },
419
- getFrontendResourceLabel: ({ currentObject }) => String(currentObject.status),
530
+ // A subscription is named by the billing period it is in, in the viewer's date format. Its status is its state
531
+ // (the badge), and the raw status value was all the name used to say.
532
+ getFrontendResourceLabel: ({ currentObject, formatDate }) => `${formatDate(currentObject.currentPeriodStart)} – ${formatDate(currentObject.currentPeriodEnd)}`,
420
533
  views: [
421
534
  [BillingSubscription_Operations.CANCEL, notAddressable({ iconOverride: Ban })],
422
535
  [BillingSubscription_Operations.RESUME, notAddressable({ iconOverride: CirclePlay })],
@@ -424,6 +537,8 @@ export function buildBillingMainPresets() {
424
537
  [BillingSubscription_Operations.PREVIEW_PLAN_CHANGE, apiOnly()],
425
538
  [BillingSubscription_Operations.ADD_ADDON, notAddressable({ iconOverride: PackagePlus })],
426
539
  [BillingSubscription_Operations.REMOVE_ADDON, notAddressable({ iconOverride: PackageMinus })],
540
+ // Withdraws a plan change pending at the end of the period (#1868).
541
+ [BillingSubscription_Operations.CANCEL_PLAN_CHANGE, notAddressable({ iconOverride: CalendarX2 })],
427
542
  // Subscriptions are managed via the custom ops above + Stripe — generic UPDATE
428
543
  // is headless (no inline edit UI).
429
544
  [Op.UPDATE, apiOnly()],
@@ -450,16 +565,24 @@ export function buildBillingMainPresets() {
450
565
  },
451
566
  summary: () => {
452
567
  const { data, isLoading } = useDisplayTemplateContext();
453
- return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, { name: "poolKey" }), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "available" }), _jsx(DisplayField, { name: "totalPurchased" }), _jsx(DisplayField, { name: "totalConsumed" })] }), _jsx(DisplayField, { name: "billingAccountId", hideLabel: true, className: "text-muted-foreground text-xs font-mono truncate max-w-full" })] }));
568
+ return (_jsxs(SummaryLayout, { children: [_jsx(SummaryTitle, {}), _jsxs(SummaryMeta, { children: [_jsx(DisplayField, { name: "poolKey" }), _jsx(DisplayField, { name: "available" }), _jsx(DisplayField, { name: "totalPurchased" }), _jsx(DisplayField, { name: "totalGranted" }), _jsx(DisplayField, { name: "totalConsumed" })] }), _jsx(DisplayField, { name: "billingAccountId", hideLabel: true, className: "text-muted-foreground text-xs font-mono truncate max-w-full" })] }));
454
569
  },
455
570
  },
456
571
  fields: {
457
572
  _id: schema.shape._id.isHidden(),
458
573
  available: schema.shape.available.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
459
574
  totalPurchased: schema.shape.totalPurchased.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
575
+ totalGranted: schema.shape.totalGranted.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED, hideWhenEmpty: true } }),
460
576
  totalConsumed: schema.shape.totalConsumed.numberUI({ display: { displayMode: NumberDisplayMode.FORMATTED } }),
461
577
  },
462
- getFrontendResourceLabel: ({ currentObject }) => currentObject.poolKey,
578
+ // Named by the application's word for the pool (`billing_credit_pools.<poolKey>.name`), for the same reason as a
579
+ // product (#1851). An unworded pool answers its key.
580
+ getFrontendResourceLabel: ({ currentObject, t }) => {
581
+ const nameKey = `${LabellingTypes.BILLING_CREDIT_POOLS}.${currentObject.poolKey}.name`;
582
+ const name = t(nameKey);
583
+ return name === nameKey ? unresolvedLabel(currentObject.poolKey) : name;
584
+ },
585
+ instanceLabelSections: [LabellingTypes.BILLING_CREDIT_POOLS],
463
586
  views: [
464
587
  [CreditPool_Operations.TOP_UP, notAddressable({ iconOverride: CirclePlus })],
465
588
  ],
@@ -490,7 +613,9 @@ export function buildBillingMainPresets() {
490
613
  display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
491
614
  }),
492
615
  },
493
- getFrontendResourceLabel: ({ currentObject }) => `Invoice ${currentObject.status}`,
616
+ // An invoice is named by its date, in the viewer's format. Its status is the badge beside it; the name used to be
617
+ // English written in code around the raw status ("Invoice paid").
618
+ getFrontendResourceLabel: ({ currentObject, formatDate }) => formatDate(currentObject.createdAt),
494
619
  views: [
495
620
  [Op.LIST, notAddressable()],
496
621
  [Op.READ, notAddressable()],
@@ -514,7 +639,9 @@ export function buildBillingMainPresets() {
514
639
  display: { displayMode: NumberDisplayMode.FORMATTED, showDescription: true },
515
640
  }),
516
641
  },
517
- getFrontendResourceLabel: ({ currentObject }) => `${currentObject.productKey} · ${currentObject.quantity}`,
642
+ // A usage record is named by the instant it was recorded. The product key is an identifier and the quantity a
643
+ // value; both are rows, and the name used to be the two raw values joined.
644
+ getFrontendResourceLabel: ({ currentObject, formatDateTime }) => formatDateTime(currentObject.timestamp),
518
645
  views: [
519
646
  [Op.LIST, notAddressable()],
520
647
  [Op.READ, notAddressable()],
@@ -549,6 +676,8 @@ export function buildBillingAdminPresets(main) {
549
676
  [Op.READ, notAddressable()],
550
677
  // Referrals are an immutable ledger record — UPDATE is headless (no edit UI).
551
678
  [Op.UPDATE, apiOnly()],
679
+ [Referral_Operations.ISSUE, notAddressable({ iconOverride: CirclePlus })],
680
+ [Referral_Operations.CANCEL, notAddressable({ iconOverride: CircleX })],
552
681
  ],
553
682
  })),
554
683
  [R.PROMOTIONS]: resourceUIBehaviorFrom(promotionsFactory, main[R.PROMOTIONS], ({ base }) => ({