@open-mercato/core 0.8.1-develop.7254.1.ab24f26208 → 0.8.1-develop.7263.1.bbc6db3437

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 (46) hide show
  1. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +11 -3
  2. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  3. package/dist/modules/customer_accounts/api/admin/users.js +27 -14
  4. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  5. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +9 -0
  6. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
  7. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +8 -8
  8. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +2 -2
  9. package/dist/modules/customers/backend/customers/deals/map/page.js +10 -2
  10. package/dist/modules/customers/backend/customers/deals/map/page.js.map +2 -2
  11. package/dist/modules/customers/backend/customers/deals/page.js +10 -2
  12. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  13. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +41 -66
  14. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
  15. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -2
  16. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  17. package/dist/modules/customers/components/formConfig.js +3 -3
  18. package/dist/modules/customers/components/formConfig.js.map +2 -2
  19. package/dist/modules/customers/data/validators.js +3 -1
  20. package/dist/modules/customers/data/validators.js.map +2 -2
  21. package/package.json +7 -7
  22. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +19 -5
  23. package/src/modules/catalog/i18n/de.json +1 -0
  24. package/src/modules/catalog/i18n/en.json +1 -0
  25. package/src/modules/catalog/i18n/es.json +1 -0
  26. package/src/modules/catalog/i18n/ko.json +1 -0
  27. package/src/modules/catalog/i18n/pl.json +1 -0
  28. package/src/modules/customer_accounts/api/admin/users.ts +30 -14
  29. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +11 -0
  30. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +11 -11
  31. package/src/modules/customer_accounts/i18n/de.json +1 -0
  32. package/src/modules/customer_accounts/i18n/en.json +1 -0
  33. package/src/modules/customer_accounts/i18n/es.json +1 -0
  34. package/src/modules/customer_accounts/i18n/ko.json +1 -0
  35. package/src/modules/customer_accounts/i18n/pl.json +1 -0
  36. package/src/modules/customers/backend/customers/deals/map/page.tsx +13 -2
  37. package/src/modules/customers/backend/customers/deals/page.tsx +13 -2
  38. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +69 -56
  39. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +8 -2
  40. package/src/modules/customers/components/formConfig.tsx +3 -3
  41. package/src/modules/customers/data/validators.ts +2 -1
  42. package/src/modules/customers/i18n/de.json +1 -0
  43. package/src/modules/customers/i18n/en.json +1 -0
  44. package/src/modules/customers/i18n/es.json +1 -0
  45. package/src/modules/customers/i18n/ko.json +1 -0
  46. package/src/modules/customers/i18n/pl.json +1 -0
@@ -14,11 +14,22 @@ import {
14
14
  import { SearchInput } from '@open-mercato/ui/primitives/search-input'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
16
  import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
17
- import { ViewTabsRow } from '../pipeline/components/ViewTabsRow'
17
+ import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
18
+ import {
19
+ ViewTabsRow,
20
+ VIEW_TABS_ROW_COMPONENT_ID,
21
+ type ViewTabsRowProps,
22
+ } from '../pipeline/components/ViewTabsRow'
18
23
  import { DealsMapView } from './components/DealsMapView'
19
24
 
20
25
  export default function DealsMapPage(): React.ReactElement {
21
26
  const t = useT()
27
+ // Resolved through the component registry so downstream apps can hide a view
28
+ // (or replace the whole switcher) without forking this page.
29
+ const DealsViewTabsRow = useRegisteredComponent<ViewTabsRowProps>(
30
+ VIEW_TABS_ROW_COMPONENT_ID,
31
+ ViewTabsRow,
32
+ )
22
33
  const [search, setSearch] = React.useState('')
23
34
 
24
35
  return (
@@ -68,7 +79,7 @@ export default function DealsMapPage(): React.ReactElement {
68
79
  </div>
69
80
  </div>
70
81
 
71
- <ViewTabsRow active="map" className="mt-4" />
82
+ <DealsViewTabsRow active="map" className="mt-4" />
72
83
 
73
84
  <DealsMapView search={search} />
74
85
  </PageBody>
@@ -30,7 +30,12 @@ import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
30
30
  import { Briefcase, AlertTriangle, X } from 'lucide-react'
31
31
  import { isLostDealStatus, isWonDealStatus } from '../../../lib/dealStatus'
32
32
  import { formatRelativeTime } from '@open-mercato/shared/lib/time'
33
- import { ViewTabsRow } from './pipeline/components/ViewTabsRow'
33
+ import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
34
+ import {
35
+ ViewTabsRow,
36
+ VIEW_TABS_ROW_COMPONENT_ID,
37
+ type ViewTabsRowProps,
38
+ } from './pipeline/components/ViewTabsRow'
34
39
  import { DealsKpiStrip } from '../../../components/DealsKpiStrip'
35
40
  import { E } from '#generated/entities.ids.generated'
36
41
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -195,6 +200,12 @@ function formatGroupedAmount(amount: number | null | undefined): string | null {
195
200
 
196
201
  export default function CustomersDealsPage() {
197
202
  const t = useT()
203
+ // Resolved through the component registry so downstream apps can hide a view
204
+ // (or replace the whole switcher) without forking this page.
205
+ const DealsViewTabsRow = useRegisteredComponent<ViewTabsRowProps>(
206
+ VIEW_TABS_ROW_COMPONENT_ID,
207
+ ViewTabsRow,
208
+ )
198
209
  const locale = useLocale()
199
210
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
200
211
  const router = useRouter()
@@ -1064,7 +1075,7 @@ export default function CustomersDealsPage() {
1064
1075
  return (
1065
1076
  <Page>
1066
1077
  <PageBody>
1067
- <ViewTabsRow active="list" className="mb-4" />
1078
+ <DealsViewTabsRow active="list" className="mb-4" />
1068
1079
  <DealsKpiStrip
1069
1080
  ownerNames={ownerNames}
1070
1081
  stageDictionary={dictionaryMaps['pipeline-stages'] ?? {}}
@@ -7,24 +7,76 @@ import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
7
7
 
8
8
  export type KanbanView = 'kanban' | 'list' | 'map'
9
9
 
10
- type ViewTabsRowProps = {
10
+ /**
11
+ * Canonical left-to-right order of the deals views. `visibleViews` selects a
12
+ * subset of these; it never reorders them, so a host that hides one view keeps
13
+ * the same tab order as every other deployment.
14
+ */
15
+ export const DEALS_VIEWS: readonly KanbanView[] = ['kanban', 'list', 'map']
16
+
17
+ export type ViewTabsRowProps = {
11
18
  active: KanbanView
12
19
  className?: string
20
+ /**
21
+ * Views exposed in the tab row. Defaults to all of `DEALS_VIEWS`; pass a
22
+ * subset to hide a view a deployment has no use for (e.g. omit `'map'` when
23
+ * deal addresses are never geocoded). Unknown and duplicated entries are
24
+ * ignored, and `active` is always rendered — a tab row that omits the page it
25
+ * is sitting on would show no selected tab at all.
26
+ */
27
+ visibleViews?: readonly KanbanView[]
28
+ }
29
+
30
+ /**
31
+ * Registered component id for the deals view switcher. Downstream apps can
32
+ * target it from `widgets/components.ts` to replace or wrap the row, or — the
33
+ * reason it is resolved through the registry at all — to transform its props
34
+ * from one place instead of forking each of the three pages that render it:
35
+ *
36
+ * ```ts
37
+ * export const componentOverrides: ComponentOverride[] = [{
38
+ * target: { componentId: 'section:customers.deals.viewTabs' },
39
+ * priority: 50,
40
+ * metadata: { module: 'my_module' },
41
+ * propsTransform: (props) => ({ ...props, visibleViews: ['kanban', 'list'] }),
42
+ * }]
43
+ * ```
44
+ *
45
+ * Hiding a tab does not unpublish its route; an app that also wants the page
46
+ * gone disables it through `entry.overrides.pages` (#5923).
47
+ */
48
+ export const VIEW_TABS_ROW_COMPONENT_ID = 'section:customers.deals.viewTabs'
49
+
50
+ const VIEW_HREFS: Record<KanbanView, string> = {
51
+ kanban: '/backend/customers/deals/pipeline',
52
+ list: '/backend/customers/deals',
53
+ map: '/backend/customers/deals/map',
13
54
  }
14
55
 
15
- const VIEW_KANBAN_HREF = '/backend/customers/deals/pipeline'
16
- const VIEW_LIST_HREF = '/backend/customers/deals'
17
- const VIEW_MAP_HREF = '/backend/customers/deals/map'
56
+ /**
57
+ * Narrows `visibleViews` to the tabs this row renders, in canonical order.
58
+ * Exported so the contract — canonical order, deduplication, unknown entries
59
+ * dropped, `active` always kept — is testable without rendering.
60
+ */
61
+ export function resolveVisibleViews(
62
+ active: KanbanView,
63
+ visibleViews?: readonly KanbanView[],
64
+ ): KanbanView[] {
65
+ if (!visibleViews) return [...DEALS_VIEWS]
66
+ const requested = new Set<KanbanView>(visibleViews.filter((view) => DEALS_VIEWS.includes(view)))
67
+ requested.add(active)
68
+ return DEALS_VIEWS.filter((view) => requested.has(view))
69
+ }
18
70
 
19
- export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.ReactElement {
71
+ export function ViewTabsRow({ active, className, visibleViews }: ViewTabsRowProps): React.ReactElement {
20
72
  const t = useT()
21
- const labels = {
73
+ const labels: Record<KanbanView, string> = {
22
74
  kanban: translateWithFallback(t, 'customers.deals.kanban.view.kanban', 'Kanban'),
23
75
  list: translateWithFallback(t, 'customers.deals.kanban.view.list', 'List'),
24
76
  map: translateWithFallback(t, 'customers.deals.kanban.view.map', 'Map'),
25
77
  }
26
78
 
27
- // Link-based tab row (two routes), so the Tabs primitive (state-driven,
79
+ // Link-based tab row (three routes), so the Tabs primitive (state-driven,
28
80
  // onValueChange) does not fit — real <Link> semantics must stay. Classes
29
81
  // mirror the Tabs underline variant: accent-indigo active border,
30
82
  // shadow-focus halo.
@@ -33,14 +85,9 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
33
85
  const activeTab = 'border-b-2 border-accent-indigo font-semibold text-foreground'
34
86
  const inactiveTab = 'border-b-2 border-transparent font-normal text-muted-foreground hover:text-foreground'
35
87
 
36
- // Renders the active tab as a non-navigating `<span>` and the inactive tab as a `<Link>`,
37
- // so the user can always round-trip between kanban and list from either page. Previously
38
- // the kanban tab was a hardcoded `<span>` because the row was only rendered on the kanban
39
- // page — adding it to the list page (item 7 of the SPEC-048 UX review) requires it to
40
- // navigate back to /pipeline.
41
- const isKanbanActive = active === 'kanban'
42
- const isListActive = active === 'list'
43
- const isMapActive = active === 'map'
88
+ // The active tab renders as a non-navigating `<span>` and every other tab as a
89
+ // `<Link>`, so the user can always round-trip between the views from any page.
90
+ const views = resolveVisibleViews(active, visibleViews)
44
91
 
45
92
  return (
46
93
  <div
@@ -48,60 +95,26 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
48
95
  aria-label={translateWithFallback(t, 'customers.deals.kanban.view.tablistLabel', 'Deals views')}
49
96
  className={`flex items-end gap-1 border-b border-border ${className ?? ''}`.trim()}
50
97
  >
51
- {isKanbanActive ? (
52
- <span
53
- role="tab"
54
- aria-selected={true}
55
- className={`${baseTab} ${activeTab}`}
56
- >
57
- {labels.kanban}
58
- </span>
59
- ) : (
60
- <Link
61
- href={VIEW_KANBAN_HREF}
62
- role="tab"
63
- aria-selected={false}
64
- className={`${baseTab} ${inactiveTab}`}
65
- >
66
- {labels.kanban}
67
- </Link>
68
- )}
69
- {isListActive ? (
70
- <span
71
- role="tab"
72
- aria-selected={true}
73
- className={`${baseTab} ${activeTab}`}
74
- >
75
- {labels.list}
76
- </span>
77
- ) : (
78
- <Link
79
- href={VIEW_LIST_HREF}
80
- role="tab"
81
- aria-selected={false}
82
- className={`${baseTab} ${inactiveTab}`}
83
- >
84
- {labels.list}
85
- </Link>
86
- )}
87
- {isMapActive ? (
98
+ {views.map((view) => (view === active ? (
88
99
  <span
100
+ key={view}
89
101
  role="tab"
90
102
  aria-selected={true}
91
103
  className={`${baseTab} ${activeTab}`}
92
104
  >
93
- {labels.map}
105
+ {labels[view]}
94
106
  </span>
95
107
  ) : (
96
108
  <Link
97
- href={VIEW_MAP_HREF}
109
+ key={view}
110
+ href={VIEW_HREFS[view]}
98
111
  role="tab"
99
112
  aria-selected={false}
100
113
  className={`${baseTab} ${inactiveTab}`}
101
114
  >
102
- {labels.map}
115
+ {labels[view]}
103
116
  </Link>
104
- )}
117
+ )))}
105
118
  </div>
106
119
  )
107
120
  }
@@ -54,7 +54,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
54
54
  import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
55
55
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
56
56
  import type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'
57
- import { ViewTabsRow } from './components/ViewTabsRow'
57
+ import { ViewTabsRow, VIEW_TABS_ROW_COMPONENT_ID, type ViewTabsRowProps } from './components/ViewTabsRow'
58
58
  import { LANE_WIDTH_CLASS } from './components/constants'
59
59
  import { useCurrencyDictionary } from '../../../../components/detail/hooks/useCurrencyDictionary'
60
60
  import { FilterBarRow, type KanbanFilterChip } from './components/FilterBarRow'
@@ -419,6 +419,12 @@ export default function DealsKanbanPage(): React.ReactElement {
419
419
  QUICK_DEAL_DIALOG_COMPONENT_ID,
420
420
  DefaultQuickDealDialog,
421
421
  )
422
+ // Same reason: an app that does not use one of the three views hides it with a
423
+ // props override on this handle instead of forking all three deals pages.
424
+ const DealsViewTabsRow = useRegisteredComponent<ViewTabsRowProps>(
425
+ VIEW_TABS_ROW_COMPONENT_ID,
426
+ ViewTabsRow,
427
+ )
422
428
  const router = useRouter()
423
429
  const scopeVersion = useOrganizationScopeVersion()
424
430
  const queryClient = useQueryClient()
@@ -2558,7 +2564,7 @@ export default function DealsKanbanPage(): React.ReactElement {
2558
2564
  return (
2559
2565
  <Page>
2560
2566
  <PageBody>
2561
- <ViewTabsRow active="kanban" className="mb-4" />
2567
+ <DealsViewTabsRow active="kanban" className="mb-4" />
2562
2568
  <div className="flex flex-col gap-2">
2563
2569
  <Breadcrumb>
2564
2570
  <BreadcrumbList>
@@ -59,7 +59,7 @@ import {
59
59
  type CustomerDictionaryKind,
60
60
  } from '../lib/dictionaries'
61
61
  import { normalizeCustomFieldSubmitValue } from './detail/customFieldUtils'
62
- import { CUSTOMER_PHONE_INVALID_MESSAGE_KEY } from '../data/validators'
62
+ import { CUSTOMER_PHONE_INVALID_MESSAGE_KEY, CUSTOMER_URL_INVALID_MESSAGE_KEY } from '../data/validators'
63
63
 
64
64
  export const metadata = {
65
65
  navHidden: true,
@@ -1199,7 +1199,7 @@ export const createCompanyFormSchema = () =>
1199
1199
  websiteUrl: z
1200
1200
  .string()
1201
1201
  .trim()
1202
- .url()
1202
+ .url(CUSTOMER_URL_INVALID_MESSAGE_KEY)
1203
1203
  .optional()
1204
1204
  .or(z.literal(''))
1205
1205
  .transform((val) => (val === '' ? undefined : val))
@@ -1559,7 +1559,7 @@ const clearableUrlField = () =>
1559
1559
  z
1560
1560
  .string()
1561
1561
  .trim()
1562
- .url()
1562
+ .url(CUSTOMER_URL_INVALID_MESSAGE_KEY)
1563
1563
  .optional()
1564
1564
  .or(z.literal(''))
1565
1565
  .transform((val) => (val === '' ? null : val))
@@ -6,6 +6,7 @@ import { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictio
6
6
  const uuid = () => z.string().uuid()
7
7
 
8
8
  export const CUSTOMER_PHONE_INVALID_MESSAGE_KEY = 'customers.people.form.primaryPhone.invalid'
9
+ export const CUSTOMER_URL_INVALID_MESSAGE_KEY = 'customers.people.form.websiteUrl.invalid'
9
10
  export const ACTIVITY_DATE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.dateRequired'
10
11
  export const ACTIVITY_TIME_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.timeRequired'
11
12
  export const ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.phoneRequired'
@@ -41,7 +42,7 @@ const clearableEmailSchema = z.preprocess(
41
42
 
42
43
  const clearableUrlSchema = z.preprocess(
43
44
  emptyStringToNull,
44
- z.string().url().max(300).nullable().optional(),
45
+ z.string().url(CUSTOMER_URL_INVALID_MESSAGE_KEY).max(300).nullable().optional(),
45
46
  )
46
47
 
47
48
  // Domain is a plain (non-URL) string that maps to a nullable column, so blanking
@@ -2352,6 +2352,7 @@
2352
2352
  "customers.people.form.twitterUrl": "X / Twitter",
2353
2353
  "customers.people.form.twitterUrl.placeholder": "https://x.com/beispiel",
2354
2354
  "customers.people.form.updateSuccess": "Person aktualisiert.",
2355
+ "customers.people.form.websiteUrl.invalid": "Geben Sie eine gültige URL ein (z. B. https://example.com)",
2355
2356
  "customers.people.list.actions.delete": "Löschen",
2356
2357
  "customers.people.list.actions.new": "Neue Person",
2357
2358
  "customers.people.list.actions.openInNewTab": "In neuem Tab öffnen",
@@ -2352,6 +2352,7 @@
2352
2352
  "customers.people.form.twitterUrl": "X / Twitter",
2353
2353
  "customers.people.form.twitterUrl.placeholder": "https://x.com/example",
2354
2354
  "customers.people.form.updateSuccess": "Person updated.",
2355
+ "customers.people.form.websiteUrl.invalid": "Enter a valid URL (e.g. https://example.com)",
2355
2356
  "customers.people.list.actions.delete": "Delete",
2356
2357
  "customers.people.list.actions.new": "New Person",
2357
2358
  "customers.people.list.actions.openInNewTab": "Open in new tab",
@@ -2352,6 +2352,7 @@
2352
2352
  "customers.people.form.twitterUrl": "X / Twitter",
2353
2353
  "customers.people.form.twitterUrl.placeholder": "https://x.com/ejemplo",
2354
2354
  "customers.people.form.updateSuccess": "Persona actualizada.",
2355
+ "customers.people.form.websiteUrl.invalid": "Introduce una URL válida (p. ej., https://example.com)",
2355
2356
  "customers.people.list.actions.delete": "Eliminar",
2356
2357
  "customers.people.list.actions.new": "Nueva persona",
2357
2358
  "customers.people.list.actions.openInNewTab": "Abrir en nueva pestaña",
@@ -2352,6 +2352,7 @@
2352
2352
  "customers.people.form.twitterUrl": "X / Twitter",
2353
2353
  "customers.people.form.twitterUrl.placeholder": "https://x.com/example",
2354
2354
  "customers.people.form.updateSuccess": "연락처가 업데이트되었습니다.",
2355
+ "customers.people.form.websiteUrl.invalid": "올바른 URL을 입력하세요 (예: https://example.com)",
2355
2356
  "customers.people.list.actions.delete": "삭제",
2356
2357
  "customers.people.list.actions.new": "새 연락처",
2357
2358
  "customers.people.list.actions.openInNewTab": "새 탭에서 열기",
@@ -2352,6 +2352,7 @@
2352
2352
  "customers.people.form.twitterUrl": "X / Twitter",
2353
2353
  "customers.people.form.twitterUrl.placeholder": "https://x.com/przyklad",
2354
2354
  "customers.people.form.updateSuccess": "Osoba została zaktualizowana.",
2355
+ "customers.people.form.websiteUrl.invalid": "Podaj prawidłowy adres URL (np. https://example.com)",
2355
2356
  "customers.people.list.actions.delete": "Usuń",
2356
2357
  "customers.people.list.actions.new": "Nowa osoba",
2357
2358
  "customers.people.list.actions.openInNewTab": "Otwórz w nowej karcie",