@open-mercato/core 0.7.1-develop.7106.1.53fe9dd1b1 → 0.7.1-develop.7112.1.d486e6f1b2

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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/users/resend-invite/route.js +1 -1
  3. package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
  4. package/dist/modules/business_rules/backend/logs/[id]/page.js +3 -9
  5. package/dist/modules/business_rules/backend/logs/[id]/page.js.map +2 -2
  6. package/dist/modules/business_rules/backend/rules/[id]/page.js +3 -9
  7. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  8. package/dist/modules/business_rules/backend/sets/[id]/page.js +3 -9
  9. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  10. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +6 -4
  11. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +2 -2
  12. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  13. package/dist/modules/customers/components/detail/ActivitiesSection.js +4 -12
  14. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  15. package/dist/modules/customers/components/detail/authorUserLookup.js +46 -0
  16. package/dist/modules/customers/components/detail/authorUserLookup.js.map +7 -0
  17. package/dist/modules/query_index/lib/engine.js +2 -1
  18. package/dist/modules/query_index/lib/engine.js.map +2 -2
  19. package/dist/modules/workflows/backend/definitions/[id]/page.js +3 -9
  20. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  21. package/dist/modules/workflows/backend/events/[id]/page.js +3 -9
  22. package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
  23. package/package.json +7 -7
  24. package/src/modules/auth/api/users/resend-invite/route.ts +1 -1
  25. package/src/modules/business_rules/backend/logs/[id]/page.tsx +3 -11
  26. package/src/modules/business_rules/backend/rules/[id]/page.tsx +3 -11
  27. package/src/modules/business_rules/backend/sets/[id]/page.tsx +3 -11
  28. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +7 -5
  29. package/src/modules/communication_channels/i18n/de.json +1 -0
  30. package/src/modules/communication_channels/i18n/en.json +1 -0
  31. package/src/modules/communication_channels/i18n/es.json +1 -0
  32. package/src/modules/communication_channels/i18n/ko.json +1 -0
  33. package/src/modules/communication_channels/i18n/pl.json +1 -0
  34. package/src/modules/currencies/backend/currencies/[id]/page.tsx +1 -1
  35. package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -18
  36. package/src/modules/customers/components/detail/authorUserLookup.ts +85 -0
  37. package/src/modules/query_index/lib/engine.ts +9 -0
  38. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -11
  39. package/src/modules/workflows/backend/events/[id]/page.tsx +3 -12
@@ -0,0 +1,85 @@
1
+ "use client"
2
+
3
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
4
+ import { MAX_USER_LOOKUP_IDS } from '@open-mercato/core/modules/auth/lib/userIdFilter'
5
+
6
+ type AuthUserItem = {
7
+ id?: unknown
8
+ name?: unknown
9
+ display_name?: unknown
10
+ email?: unknown
11
+ }
12
+
13
+ // Imported rather than restated: `parseIdsParam` slices past the route's cap without complaining,
14
+ // so a client batch larger than the server accepts loses the overflow ids silently.
15
+ const MAX_IDS_PER_LOOKUP = MAX_USER_LOOKUP_IDS
16
+
17
+ /**
18
+ * The outcome of a batch lookup. `resolvedIds` lists the ids the server actually answered for —
19
+ * an id that came back without a row (deleted user) still counts as resolved, an id whose request
20
+ * failed does not. Callers cache on `resolvedIds`, so a transient failure is retried rather than
21
+ * remembered as a permanent blank.
22
+ */
23
+ export type AuthorUserLookup = {
24
+ names: Map<string, string>
25
+ resolvedIds: string[]
26
+ }
27
+
28
+ function toName(item: AuthUserItem | null | undefined): [string, string] | null {
29
+ if (!item || typeof item.id !== 'string' || !item.id.trim()) return null
30
+ const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null
31
+ const displayName = typeof item.display_name === 'string' && item.display_name.trim()
32
+ ? item.display_name.trim()
33
+ : null
34
+ const email = typeof item.email === 'string' && item.email.trim() ? item.email.trim() : null
35
+ const label = name ?? displayName ?? email
36
+ return label ? [item.id.trim(), label] : null
37
+ }
38
+
39
+ // A viewer of a customer record does not necessarily hold `auth.users.list`, so this lookup must be
40
+ // allowed to fail: `x-om-forbidden-redirect: 0` keeps a 403 from bouncing the whole detail page to
41
+ // /login, and `null` signals that the call itself failed. Callers must not confuse that with a
42
+ // successful call that matched nobody — caching the former would remember a transient error forever.
43
+ async function fetchBatch(ids: string[], signal?: AbortSignal): Promise<AuthUserItem[] | null> {
44
+ const params = new URLSearchParams()
45
+ params.set('page', '1')
46
+ // The route's pageSize defaults to 50, so a batch larger than that would be truncated by
47
+ // pagination even after it survived the `?ids=` cap.
48
+ params.set('pageSize', String(ids.length))
49
+ // URLSearchParams encodes on toString(); pre-encoding here would double-escape the commas.
50
+ params.set('ids', ids.join(','))
51
+ const call = await apiCall<{ items?: AuthUserItem[] }>(
52
+ `/api/auth/users?${params.toString()}`,
53
+ { headers: { 'x-om-forbidden-redirect': '0' }, signal },
54
+ { fallback: null },
55
+ ).catch(() => null)
56
+ if (!call || !call.ok) return null
57
+ return call.result?.items ?? []
58
+ }
59
+
60
+ /**
61
+ * Batch id → display-name resolution for activity authors already on screen, so an author whose
62
+ * name was not denormalized onto the activity still renders a name instead of a bare UUID.
63
+ */
64
+ export async function resolveAuthorUserNames(
65
+ ids: readonly string[],
66
+ signal?: AbortSignal,
67
+ ): Promise<AuthorUserLookup> {
68
+ const unique = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)))
69
+ const names = new Map<string, string>()
70
+ const resolvedIds: string[] = []
71
+ for (let offset = 0; offset < unique.length; offset += MAX_IDS_PER_LOOKUP) {
72
+ if (signal?.aborted) break
73
+ const batch = unique.slice(offset, offset + MAX_IDS_PER_LOOKUP)
74
+ const items = await fetchBatch(batch, signal)
75
+ // One failed batch must not cost the batches that did answer, and its ids stay unresolved so
76
+ // the next render retries them.
77
+ if (items === null) continue
78
+ for (const item of items) {
79
+ const entry = toName(item)
80
+ if (entry) names.set(entry[0], entry[1])
81
+ }
82
+ resolvedIds.push(...batch)
83
+ }
84
+ return { names, resolvedIds }
85
+ }
@@ -352,10 +352,19 @@ export class HybridQueryEngine implements QueryEngine {
352
352
 
353
353
  const normalizedFilters = normalizeFilters(opts.filters)
354
354
  const cfFilters = normalizedFilters.filter((filter) => filter.field.startsWith('cf:') || filter.field.startsWith('l10n:'))
355
+ // `applySort` orders `cf:` sorts off the left-joined index doc, so a
356
+ // sort-only custom-field query depends on the index just as much as a cf
357
+ // filter does — without this disjunct it would skip the coverage guards
358
+ // below and silently order by a NULL expression on an empty index (#5521).
359
+ // Only `cf:` qualifies: neither engine ever applies an `l10n:` ordering —
360
+ // both drop it while resolving sorts — so counting it here would pay the
361
+ // coverage probes and the ORM diversion for an ordering that is discarded.
362
+ const hasCfSort = (opts.sort || []).some((sort) => String(sort.field).startsWith('cf:'))
355
363
  const coverageScope = this.resolveCoverageSnapshotScope(opts)
356
364
  const wantsCf = (
357
365
  (opts.fields || []).some((field) => typeof field === 'string' && (field.startsWith('cf:') || field.startsWith('l10n:'))) ||
358
366
  cfFilters.length > 0 ||
367
+ hasCfSort ||
359
368
  (Array.isArray(opts.includeCustomFields) && opts.includeCustomFields.length > 0)
360
369
  )
361
370
 
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useParams, usePathname } from 'next/navigation'
4
+ import { useRouter, usePathname } from 'next/navigation'
5
5
  import { useQuery } from '@tanstack/react-query'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
@@ -33,20 +33,12 @@ import { MobileDefinitionDetail } from '../../../components/mobile/MobileDefinit
33
33
  import { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'
34
34
  import type { WorkflowDefinitionTrigger } from '../../../data/entities'
35
35
 
36
- export default function EditWorkflowDefinitionPage() {
36
+ export default function EditWorkflowDefinitionPage({ params }: { params?: { id?: string } }) {
37
37
  const router = useRouter()
38
- const params = useParams()
39
38
  const pathname = usePathname()
40
39
  const t = useT()
41
40
  const isMobile = useIsMobile()
42
-
43
- // Handle catch-all route: params.slug = ['definitions', 'uuid']
44
- let definitionId: string | undefined
45
- if (params?.slug && Array.isArray(params.slug)) {
46
- definitionId = params.slug[1] // Second element is the ID
47
- } else if (params?.id) {
48
- definitionId = Array.isArray(params.id) ? params.id[0] : params.id
49
- }
41
+ const definitionId = params?.id
50
42
 
51
43
  const { data: definition, isLoading, error } = useQuery({
52
44
  queryKey: ['workflow-definition', definitionId],
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useParams, useRouter } from 'next/navigation'
5
+ import { useRouter } from 'next/navigation'
6
6
  import { useQuery } from '@tanstack/react-query'
7
7
  import { apiFetch } from '@open-mercato/ui/backend/utils/api'
8
8
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -36,19 +36,10 @@ type WorkflowEvent = {
36
36
  } | null
37
37
  }
38
38
 
39
- export default function WorkflowEventDetailPage() {
39
+ export default function WorkflowEventDetailPage({ params }: { params?: { id?: string } }) {
40
40
  const router = useRouter()
41
- const params = useParams()
42
41
  const t = useT()
43
-
44
- // Handle both {id: '17'} and {slug: ['events', '17']} formats
45
- let eventId: string | undefined
46
- if (params?.id) {
47
- eventId = Array.isArray(params.id) ? params.id[0] : params.id
48
- } else if (params?.slug && Array.isArray(params.slug)) {
49
- // If slug is ['events', '17'], extract '17'
50
- eventId = params.slug[params.slug.length - 1]
51
- }
42
+ const eventId = params?.id
52
43
 
53
44
  const { data: event, isLoading, error } = useQuery({
54
45
  queryKey: ['workflows', 'events', eventId],