@open-mercato/core 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7-develop.6869.1.d1ce9999ea

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 (110) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/backend/config/attachments/page.js +1 -1
  3. package/dist/modules/attachments/backend/config/attachments/page.js.map +2 -2
  4. package/dist/modules/audit_logs/components/AuditLogsActions.js +1 -1
  5. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
  6. package/dist/modules/auth/frontend/login.js +2 -2
  7. package/dist/modules/auth/frontend/login.js.map +2 -2
  8. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js +10 -5
  9. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js.map +2 -2
  10. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +4 -4
  11. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
  12. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +2 -2
  13. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  14. package/dist/modules/customers/api/utils.js +9 -6
  15. package/dist/modules/customers/api/utils.js.map +2 -2
  16. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  17. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -1
  19. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
  20. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +1 -1
  21. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  22. package/dist/modules/customers/components/detail/MiniWeekCalendar.js +7 -52
  23. package/dist/modules/customers/components/detail/MiniWeekCalendar.js.map +2 -2
  24. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -1
  25. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  26. package/dist/modules/customers/components/detail/legacyActivities.js +69 -0
  27. package/dist/modules/customers/components/detail/legacyActivities.js.map +7 -0
  28. package/dist/modules/customers/utils/phoneDuplicates.js +1 -2
  29. package/dist/modules/customers/utils/phoneDuplicates.js.map +2 -2
  30. package/dist/modules/data_sync/backend/data-sync/page.js +3 -3
  31. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  32. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +5 -5
  33. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  34. package/dist/modules/entities/api/records.js +8 -4
  35. package/dist/modules/entities/api/records.js.map +2 -2
  36. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +1 -1
  37. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  38. package/dist/modules/feature_toggles/components/FeatureToggleOverrideCard.js +1 -1
  39. package/dist/modules/feature_toggles/components/FeatureToggleOverrideCard.js.map +2 -2
  40. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +1 -1
  41. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +2 -2
  42. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -1
  43. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  44. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +1 -1
  45. package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +2 -2
  46. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +1 -1
  47. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +2 -2
  48. package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.js +1 -1
  49. package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.js.map +2 -2
  50. package/dist/modules/sales/components/documents/ItemsSection.js +11 -1
  51. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  52. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js +1 -1
  53. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js.map +2 -2
  54. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -2
  55. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  56. package/dist/modules/workflows/backend/definitions/create/page.js +1 -1
  57. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  58. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +3 -3
  59. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  60. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
  61. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  62. package/dist/modules/workflows/components/NodeEditDialog.js +3 -3
  63. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  64. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +3 -3
  65. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  66. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -1
  67. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  68. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +1 -1
  69. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  70. package/package.json +7 -7
  71. package/src/modules/attachments/backend/config/attachments/page.tsx +1 -1
  72. package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
  73. package/src/modules/auth/frontend/login.tsx +2 -2
  74. package/src/modules/catalog/widgets/injection/product-bulk-delete/widget.ts +15 -5
  75. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +4 -4
  76. package/src/modules/communication_channels/i18n/de.json +2 -2
  77. package/src/modules/communication_channels/i18n/en.json +2 -2
  78. package/src/modules/communication_channels/i18n/es.json +2 -2
  79. package/src/modules/communication_channels/i18n/ko.json +2 -2
  80. package/src/modules/communication_channels/i18n/pl.json +2 -2
  81. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +2 -2
  82. package/src/modules/customers/api/utils.ts +13 -6
  83. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -1
  84. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -1
  85. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +1 -1
  86. package/src/modules/customers/components/detail/MiniWeekCalendar.tsx +8 -57
  87. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -1
  88. package/src/modules/customers/components/detail/legacyActivities.ts +79 -0
  89. package/src/modules/customers/utils/phoneDuplicates.ts +6 -3
  90. package/src/modules/data_sync/backend/data-sync/page.tsx +3 -3
  91. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +5 -5
  92. package/src/modules/entities/api/records.ts +12 -4
  93. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +1 -1
  94. package/src/modules/feature_toggles/components/FeatureToggleOverrideCard.tsx +1 -1
  95. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +1 -1
  96. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -1
  97. package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +1 -1
  98. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +1 -1
  99. package/src/modules/portal/frontend/[orgSlug]/portal/verify/page.tsx +1 -1
  100. package/src/modules/sales/components/documents/ItemsSection.tsx +13 -0
  101. package/src/modules/sales/components/documents/lineItemTypes.ts +1 -0
  102. package/src/modules/sync_excel/widgets/injection/upload-config/widget.client.tsx +1 -1
  103. package/src/modules/workflows/backend/definitions/[id]/page.tsx +2 -2
  104. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -1
  105. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +3 -3
  106. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
  107. package/src/modules/workflows/components/NodeEditDialog.tsx +3 -3
  108. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +3 -3
  109. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -1
  110. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +1 -1
@@ -13,7 +13,7 @@ export function DnsDiagnostics({ mapping }: DiagnosticsProps) {
13
13
  const t = useT()
14
14
  if (mapping.status !== 'dns_failed') return null
15
15
  return (
16
- <Alert variant="destructive">
16
+ <Alert status="error">
17
17
  <AlertTitle>
18
18
  {t('customer_accounts.domainMapping.dns.diagnostics.title', 'DNS configuration issue')}
19
19
  </AlertTitle>
@@ -38,7 +38,7 @@ export function TlsDiagnostics({ mapping }: DiagnosticsProps) {
38
38
  const t = useT()
39
39
  if (mapping.status !== 'tls_failed') return null
40
40
  return (
41
- <Alert variant="warning">
41
+ <Alert status="warning">
42
42
  <AlertTitle>
43
43
  {t('customer_accounts.domainMapping.tls.diagnostics.title', 'SSL certificate issue')}
44
44
  </AlertTitle>
@@ -280,10 +280,14 @@ export async function findMatchingEntityIdsWithQueryEngine({
280
280
  const qe = ctx.container.resolve('queryEngine') as QueryEngine
281
281
  const ids = new Set<string>()
282
282
  const pageSize = 100
283
+ const maxPages = 1000
283
284
  let page = 1
284
- let total = 0
285
285
 
286
- do {
286
+ // Short-page termination: `total` is a display value, not a loop bound. The previous
287
+ // `while (ids.size < total)` could spin forever — `ids` is a Set, so duplicate ids across
288
+ // pages keep its size permanently below an inflated `total`. Fail closed at the page
289
+ // ceiling: a partial id set fed into advanced filtering would silently drop matches.
290
+ for (;;) {
287
291
  const result = await qe.query(entityId, {
288
292
  fields: ['id'],
289
293
  filters,
@@ -296,16 +300,19 @@ export async function findMatchingEntityIdsWithQueryEngine({
296
300
  joins,
297
301
  })
298
302
 
299
- total = result.total ?? 0
300
- for (const item of result.items ?? []) {
303
+ const items = result.items ?? []
304
+ for (const item of items) {
301
305
  const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null
302
306
  if (typeof id === 'string' && id.length > 0) {
303
307
  ids.add(id)
304
308
  }
305
309
  }
306
- if (!result.items?.length) break
310
+ if (items.length < pageSize) break
311
+ if (page >= maxPages) {
312
+ throw new Error(`[internal] advanced-filter id resolution exceeded ${maxPages} pages; refusing to return a partial id set`)
313
+ }
307
314
  page += 1
308
- } while (ids.size < total)
315
+ }
309
316
 
310
317
  return Array.from(ids)
311
318
  }
@@ -2750,7 +2750,7 @@ export default function DealsKanbanPage(): React.ReactElement {
2750
2750
  </div>
2751
2751
  ) : firstError ? (
2752
2752
  <div className="max-w-xl">
2753
- <Alert variant="destructive">
2753
+ <Alert status="error">
2754
2754
  <AlertTitle>{translateWithFallback(t, 'ui.errors.defaultTitle', 'Something went wrong')}</AlertTitle>
2755
2755
  <AlertDescription>
2756
2756
  {firstError instanceof Error
@@ -189,7 +189,7 @@ function EditorBody({
189
189
  // and the type switcher span both columns.
190
190
  <div className="grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6">
191
191
  {conflict ? (
192
- <Alert variant="warning" className="rounded-lg lg:col-span-2">
192
+ <Alert status="warning" className="rounded-lg lg:col-span-2">
193
193
  <AlertTitle>{t('customers.calendar.editor.conflictTitle', 'Calendar conflict')}</AlertTitle>
194
194
  <AlertDescription>{conflict}</AlertDescription>
195
195
  </Alert>
@@ -151,7 +151,7 @@ export function ConfirmDealLostDialog({
151
151
  </DialogHeader>
152
152
 
153
153
  <div className="space-y-6 px-7 py-6">
154
- <Alert variant="warning" className="rounded-md">
154
+ <Alert status="warning" className="rounded-md">
155
155
  <AlertTitle>
156
156
  {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}
157
157
  </AlertTitle>
@@ -7,7 +7,8 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
9
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
10
- import type { InteractionSummary, ActivitySummary } from './types'
10
+ import type { InteractionSummary } from './types'
11
+ import { loadLegacyActivitiesInRange } from './legacyActivities'
11
12
 
12
13
  interface MiniWeekCalendarProps {
13
14
  entityId: string
@@ -132,61 +133,6 @@ export function MiniWeekCalendar({ entityId, useCanonicalInteractions = true, re
132
133
  return items.filter(isWithinRange)
133
134
  }
134
135
 
135
- async function loadLegacyActivities(): Promise<InteractionSummary[]> {
136
- const items: InteractionSummary[] = []
137
- let page = 1
138
- let totalPages = 1
139
- do {
140
- const payload = await readApiResultOrThrow<{
141
- items?: ActivitySummary[]
142
- totalPages?: number
143
- }>(
144
- `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=100&sortField=occurredAt&sortDir=desc`,
145
- { signal: controller.signal },
146
- )
147
- const pageItems = Array.isArray(payload?.items) ? payload.items : []
148
- const mappedItems: InteractionSummary[] = pageItems.map((activity) => ({
149
- id: activity.id,
150
- interactionType: activity.activityType,
151
- title: activity.subject ?? null,
152
- body: activity.body ?? null,
153
- status: 'done',
154
- scheduledAt: null,
155
- occurredAt: activity.occurredAt ?? null,
156
- priority: null,
157
- authorUserId: activity.authorUserId ?? null,
158
- ownerUserId: null,
159
- appearanceIcon: activity.appearanceIcon ?? null,
160
- appearanceColor: activity.appearanceColor ?? null,
161
- source: 'legacy-activity',
162
- entityId: activity.entityId ?? null,
163
- dealId: activity.dealId ?? null,
164
- organizationId: null,
165
- tenantId: null,
166
- authorName: activity.authorName ?? null,
167
- authorEmail: activity.authorEmail ?? null,
168
- dealTitle: activity.dealTitle ?? null,
169
- customValues: null,
170
- createdAt: activity.createdAt,
171
- updatedAt: activity.createdAt,
172
- }))
173
- items.push(...mappedItems.filter(isWithinRange))
174
- totalPages = typeof payload?.totalPages === 'number' ? payload.totalPages : totalPages
175
- if (!pageItems.length) break
176
- const oldestPageTimestamp = pageItems.reduce<number>((oldest, activity) => {
177
- const rawDate = activity.occurredAt ?? activity.createdAt
178
- const timestamp = rawDate ? new Date(rawDate).getTime() : Number.NaN
179
- if (!Number.isFinite(timestamp)) return oldest
180
- return Math.min(oldest, timestamp)
181
- }, Number.POSITIVE_INFINITY)
182
- if (Number.isFinite(oldestPageTimestamp) && oldestPageTimestamp < rangeStart.getTime()) {
183
- break
184
- }
185
- page += 1
186
- } while (page <= totalPages)
187
- return items
188
- }
189
-
190
136
  void (async () => {
191
137
  try {
192
138
  const canonicalItems = await loadCanonicalInteractions()
@@ -194,7 +140,12 @@ export function MiniWeekCalendar({ entityId, useCanonicalInteractions = true, re
194
140
  setEvents(canonicalItems)
195
141
  return
196
142
  }
197
- const legacyItems = await loadLegacyActivities()
143
+ const legacyItems = await loadLegacyActivitiesInRange({
144
+ entityId,
145
+ rangeStart,
146
+ isWithinRange,
147
+ signal: controller.signal,
148
+ })
198
149
  const seen = new Set<string>()
199
150
  const merged: InteractionSummary[] = []
200
151
  for (const item of [...canonicalItems, ...legacyItems]) {
@@ -511,7 +511,7 @@ export function ScheduleActivityDialog({
511
511
 
512
512
  {/* Conflict warning */}
513
513
  {state.conflict && (
514
- <Alert variant="warning" className="rounded-lg">
514
+ <Alert status="warning" className="rounded-lg">
515
515
  <AlertTitle>
516
516
  {t('customers.schedule.conflict.title', 'Calendar conflict')}
517
517
  </AlertTitle>
@@ -0,0 +1,79 @@
1
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
2
+ import type { ActivitySummary, InteractionSummary } from './types'
3
+
4
+ export const LEGACY_ACTIVITIES_PAGE_SIZE = 100
5
+ export const LEGACY_ACTIVITIES_MAX_PAGES = 1000
6
+
7
+ function mapActivityToInteraction(activity: ActivitySummary): InteractionSummary {
8
+ return {
9
+ id: activity.id,
10
+ interactionType: activity.activityType,
11
+ title: activity.subject ?? null,
12
+ body: activity.body ?? null,
13
+ status: 'done',
14
+ scheduledAt: null,
15
+ occurredAt: activity.occurredAt ?? null,
16
+ priority: null,
17
+ authorUserId: activity.authorUserId ?? null,
18
+ ownerUserId: null,
19
+ appearanceIcon: activity.appearanceIcon ?? null,
20
+ appearanceColor: activity.appearanceColor ?? null,
21
+ source: 'legacy-activity',
22
+ entityId: activity.entityId ?? null,
23
+ dealId: activity.dealId ?? null,
24
+ organizationId: null,
25
+ tenantId: null,
26
+ authorName: activity.authorName ?? null,
27
+ authorEmail: activity.authorEmail ?? null,
28
+ dealTitle: activity.dealTitle ?? null,
29
+ customValues: null,
30
+ createdAt: activity.createdAt,
31
+ updatedAt: activity.createdAt,
32
+ }
33
+ }
34
+
35
+ export async function loadLegacyActivitiesInRange({
36
+ entityId,
37
+ rangeStart,
38
+ isWithinRange,
39
+ signal,
40
+ }: {
41
+ entityId: string
42
+ rangeStart: Date
43
+ isWithinRange: (item: InteractionSummary) => boolean
44
+ signal?: AbortSignal
45
+ }): Promise<InteractionSummary[]> {
46
+ const items: InteractionSummary[] = []
47
+ let page = 1
48
+ // Short-page termination: server-reported `totalPages` is a display value, not a loop
49
+ // bound — it can under-report (capped counts) or drift mid-enumeration. Pages are sorted
50
+ // by `occurredAt` desc, so the scan also stops once a page reaches past the requested
51
+ // range. Fail closed at the page ceiling rather than rendering a partial week silently.
52
+ for (;;) {
53
+ const payload = await readApiResultOrThrow<{
54
+ items?: ActivitySummary[]
55
+ totalPages?: number
56
+ }>(
57
+ `/api/customers/activities?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${LEGACY_ACTIVITIES_PAGE_SIZE}&sortField=occurredAt&sortDir=desc`,
58
+ { signal },
59
+ )
60
+ const pageItems = Array.isArray(payload?.items) ? payload.items : []
61
+ items.push(...pageItems.map(mapActivityToInteraction).filter(isWithinRange))
62
+ if (!pageItems.length) break
63
+ const oldestPageTimestamp = pageItems.reduce<number>((oldest, activity) => {
64
+ const rawDate = activity.occurredAt ?? activity.createdAt
65
+ const timestamp = rawDate ? new Date(rawDate).getTime() : Number.NaN
66
+ if (!Number.isFinite(timestamp)) return oldest
67
+ return Math.min(oldest, timestamp)
68
+ }, Number.POSITIVE_INFINITY)
69
+ if (Number.isFinite(oldestPageTimestamp) && oldestPageTimestamp < rangeStart.getTime()) {
70
+ break
71
+ }
72
+ if (pageItems.length < LEGACY_ACTIVITIES_PAGE_SIZE) break
73
+ if (page >= LEGACY_ACTIVITIES_MAX_PAGES) {
74
+ throw new Error(`[internal] legacy activity enumeration exceeded ${LEGACY_ACTIVITIES_MAX_PAGES} pages; refusing to render a partial set`)
75
+ }
76
+ page += 1
77
+ }
78
+ return items
79
+ }
@@ -22,7 +22,7 @@ export async function lookupPhoneDuplicate(
22
22
  for (let page = 1; page <= MAX_PAGES; page += 1) {
23
23
  try {
24
24
  const url = `/api/customers/people?hasPhone=true&page=${page}&pageSize=${PAGE_SIZE}&sortField=createdAt&sortDir=desc`
25
- const call = await apiCall<{ items?: unknown[]; total?: number }>(url)
25
+ const call = await apiCall<{ items?: unknown[] }>(url)
26
26
  if (!call.ok) continue
27
27
  const payload = call.result ?? {}
28
28
  const items = Array.isArray(payload?.items) ? payload.items : []
@@ -45,8 +45,11 @@ export async function lookupPhoneDuplicate(
45
45
  }
46
46
  }
47
47
  }
48
- const total = typeof payload?.total === 'number' ? payload.total : null
49
- if (total !== null && page * PAGE_SIZE >= total) {
48
+ // Short-page termination: a page shorter than requested is the end of the result
49
+ // set. `total` is a display value and may under-report (capped counts), so it is
50
+ // no longer consulted. MAX_PAGES stays as the deliberate best-effort bound — this
51
+ // is a duplicate hint, not an exhaustive check.
52
+ if (items.length < PAGE_SIZE) {
50
53
  break
51
54
  }
52
55
  } catch {
@@ -950,21 +950,21 @@ export default function SyncRunsDashboardPage() {
950
950
  </div>
951
951
 
952
952
  {selectedIntegration && !selectedIntegration.isEnabled ? (
953
- <Alert variant="warning">
953
+ <Alert status="warning">
954
954
  <AlertDescription>
955
955
  {t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}
956
956
  </AlertDescription>
957
957
  </Alert>
958
958
  ) : null}
959
959
  {selectedIntegration && !selectedIntegration.hasCredentials ? (
960
- <Alert variant="warning">
960
+ <Alert status="warning">
961
961
  <AlertDescription>
962
962
  {t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}
963
963
  </AlertDescription>
964
964
  </Alert>
965
965
  ) : null}
966
966
  {selectedIntegration && selectedIntegration.canStartRun === false ? (
967
- <Alert variant="info">
967
+ <Alert status="information">
968
968
  <AlertDescription>
969
969
  {t('data_sync.dashboard.start.providerManaged', 'This integration starts sync runs from its own setup flow. Open the integration settings page to continue.')}
970
970
  </AlertDescription>
@@ -393,7 +393,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
393
393
 
394
394
  if (!option) {
395
395
  return (
396
- <Alert variant="warning">
396
+ <Alert status="warning">
397
397
  <AlertDescription>
398
398
  {t('data_sync.integrationTab.notAvailable', 'This integration is not registered as a data sync provider.')}
399
399
  </AlertDescription>
@@ -433,7 +433,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
433
433
  </div>
434
434
 
435
435
  {!props.isEnabled ? (
436
- <Alert variant="warning">
436
+ <Alert status="warning">
437
437
  <AlertDescription>
438
438
  {t('data_sync.integrationTab.integrationDisabledNotice', 'The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.')}
439
439
  </AlertDescription>
@@ -441,7 +441,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
441
441
  ) : null}
442
442
 
443
443
  {!props.hasCredentials ? (
444
- <Alert variant="warning">
444
+ <Alert status="warning">
445
445
  <AlertDescription>
446
446
  {t('data_sync.integrationTab.credentialsMissingNotice', 'Credentials are still missing. Save schedules first if you want, but manual and scheduled runs will fail until credentials are configured.')}
447
447
  </AlertDescription>
@@ -449,7 +449,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
449
449
  ) : null}
450
450
 
451
451
  {option.canStartRun === false ? (
452
- <Alert variant="info">
452
+ <Alert status="information">
453
453
  <AlertDescription>
454
454
  {t('data_sync.integrationTab.providerManagedNotice', 'This provider needs its own setup flow before a run can start. Use the provider tab on this page instead of generic schedules.')}
455
455
  </AlertDescription>
@@ -457,7 +457,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
457
457
  ) : null}
458
458
 
459
459
  {rows.length === 0 ? (
460
- <Alert variant="info">
460
+ <Alert status="information">
461
461
  <AlertDescription>
462
462
  {t('data_sync.integrationTab.empty', 'This provider does not expose any schedulable sync entities yet.')}
463
463
  </AlertDescription>
@@ -120,6 +120,7 @@ export const metadata = {
120
120
  }
121
121
 
122
122
  const DEFAULT_EXPORT_PAGE_SIZE = 1000
123
+ const EXPORT_MAX_PAGES = 1000
123
124
 
124
125
  const listRecordsQuerySchema = z
125
126
  .object({
@@ -330,10 +331,14 @@ export async function GET(req: Request) {
330
331
  }
331
332
 
332
333
  if (requestedExport) {
333
- let exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]
334
- if (total > exportItems.length) {
334
+ const exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]
335
+ // Short-page termination: `total` is a display value, not a loop bound — it can
336
+ // under-report (capped counts) or drift while rows are inserted/deleted mid-export.
337
+ // Keep fetching while pages come back full; fail closed at the page ceiling rather
338
+ // than serializing a partial export.
339
+ if (rawItems.length >= pageSize) {
335
340
  let nextPage = 2
336
- while (exportItems.length < total) {
341
+ for (;;) {
337
342
  const nextRes = await qe.query(entityId as any, {
338
343
  ...qopts,
339
344
  page: { page: nextPage, pageSize },
@@ -344,7 +349,10 @@ export async function GET(req: Request) {
344
349
  const nextFullItems = nextRawItems.map(mapFullRow)
345
350
  const nextBatch = exportFullRequested ? nextFullItems : nextViewItems
346
351
  exportItems.push(...nextBatch)
347
- if (nextBatch.length < pageSize) break
352
+ if (nextRawItems.length < pageSize) break
353
+ if (nextPage >= EXPORT_MAX_PAGES) {
354
+ throw new Error(`[internal] export exceeded ${EXPORT_MAX_PAGES} pages; refusing to return a partial export`)
355
+ }
348
356
  nextPage += 1
349
357
  }
350
358
  }
@@ -574,7 +574,7 @@ export default function EditDefinitionsPage({ params }: { params?: { entityId?:
574
574
  <Page>
575
575
  <PageBody>
576
576
  <div className="p-6">
577
- <Alert variant="destructive">
577
+ <Alert status="error">
578
578
  <AlertTitle>{t('entities.userEntities.edit.errors.invalidEntityTitle', 'Invalid entity')}</AlertTitle>
579
579
  <AlertDescription>{t('entities.userEntities.edit.errors.invalidEntityDescription', 'The requested entity ID is missing or invalid.')}</AlertDescription>
580
580
  </Alert>
@@ -69,7 +69,7 @@ export function FeatureToggleOverrideCard({ toggleId }: { toggleId: string }) {
69
69
  <CardTitle>{t('feature_toggles.override.title', 'Override')}</CardTitle>
70
70
  </CardHeader>
71
71
  <CardContent>
72
- <Alert variant="destructive">
72
+ <Alert status="error">
73
73
  <AlertTitle>{t('ui.errors.defaultTitle', 'Something went wrong')}</AlertTitle>
74
74
  <AlertDescription>{error.message}</AlertDescription>
75
75
  </Alert>
@@ -126,7 +126,7 @@ export default function PortalInvitePage({ params }: Props) {
126
126
 
127
127
  <form onSubmit={handleSubmit} className="flex flex-col gap-4">
128
128
  {error ? (
129
- <Alert variant="destructive">
129
+ <Alert status="error">
130
130
  <AlertDescription>{error}</AlertDescription>
131
131
  </Alert>
132
132
  ) : null}
@@ -101,7 +101,7 @@ export default function PortalLoginPage({ params }: Props) {
101
101
 
102
102
  <form onSubmit={handleSubmit} className="flex flex-col gap-4">
103
103
  {error ? (
104
- <Alert variant="destructive">
104
+ <Alert status="error">
105
105
  <AlertDescription>{error}</AlertDescription>
106
106
  </Alert>
107
107
  ) : null}
@@ -140,7 +140,7 @@ export default function PortalResetPasswordPage({ params }: Props) {
140
140
 
141
141
  <form onSubmit={handleSubmit} className="flex flex-col gap-4">
142
142
  {error ? (
143
- <Alert variant="destructive">
143
+ <Alert status="error">
144
144
  <AlertDescription>{error}</AlertDescription>
145
145
  </Alert>
146
146
  ) : null}
@@ -138,7 +138,7 @@ export default function PortalSignupPage({ params }: Props) {
138
138
 
139
139
  <form onSubmit={handleSubmit} className="flex flex-col gap-4">
140
140
  {error ? (
141
- <Alert variant="destructive">
141
+ <Alert status="error">
142
142
  <AlertDescription>{error}</AlertDescription>
143
143
  </Alert>
144
144
  ) : null}
@@ -81,7 +81,7 @@ export default function PortalVerifyPage({ params }: Props) {
81
81
 
82
82
  return (
83
83
  <div className="mx-auto flex max-w-sm flex-col gap-4 py-12">
84
- <Alert variant="destructive">
84
+ <Alert status="error">
85
85
  <AlertDescription>{error || t('portal.verify.error.generic', 'Email verification failed. Please try again.')}</AlertDescription>
86
86
  </Alert>
87
87
  <Button asChild variant="outline" className="rounded-lg">
@@ -239,6 +239,10 @@ export function SalesDocumentItemsSection({
239
239
  typeof (item.catalog_snapshot as Record<string, unknown>).name === "string"
240
240
  ? (item.catalog_snapshot as Record<string, unknown>).name as string
241
241
  : null;
242
+ const description =
243
+ typeof item.description === "string" && item.description.trim()
244
+ ? item.description
245
+ : null;
242
246
  const quantity = normalizeNumber(item.quantity, 0);
243
247
  const uomFields = getUomFields(item);
244
248
  const quantityUnit = canonicalizeUnitCode(uomFields.quantityUnit);
@@ -303,6 +307,7 @@ export function SalesDocumentItemsSection({
303
307
  const record: SalesLineRecord = {
304
308
  id,
305
309
  name,
310
+ description,
306
311
  productId:
307
312
  typeof item.product_id === "string" ? item.product_id : null,
308
313
  productVariantId:
@@ -725,6 +730,14 @@ export function SalesDocumentItemsSection({
725
730
  {showProductSku}
726
731
  </div>
727
732
  ) : null}
733
+ {item.description ? (
734
+ <div
735
+ className="text-xs text-muted-foreground line-clamp-2"
736
+ title={item.description}
737
+ >
738
+ {item.description}
739
+ </div>
740
+ ) : null}
728
741
  </div>
729
742
  </div>
730
743
  </td>
@@ -3,6 +3,7 @@ import type { SalesLineUomSnapshot } from '../../lib/types'
3
3
  export type SalesLineRecord = {
4
4
  id: string
5
5
  name: string | null
6
+ description?: string | null
6
7
  productId: string | null
7
8
  productVariantId: string | null
8
9
  quantity: number
@@ -1156,7 +1156,7 @@ function StatusAlert({
1156
1156
  message: string
1157
1157
  }) {
1158
1158
  return (
1159
- <Alert variant={variant === 'error' ? 'destructive' : variant}>
1159
+ <Alert status={variant === 'info' ? 'information' : variant}>
1160
1160
  <AlertTitle>{title}</AlertTitle>
1161
1161
  <AlertDescription>{message}</AlertDescription>
1162
1162
  </Alert>
@@ -288,7 +288,7 @@ export default function EditWorkflowDefinitionPage() {
288
288
  <Page>
289
289
  <PageBody>
290
290
  {isCodeOnly && (
291
- <Alert variant="info" className="mb-4">
291
+ <Alert status="information" className="mb-4">
292
292
  <AlertDescription className="flex items-center justify-between">
293
293
  <span>{t('workflows.source.code.readonlyBanner')}</span>
294
294
  <Button variant="outline" size="sm" onClick={handleCustomize}>
@@ -298,7 +298,7 @@ export default function EditWorkflowDefinitionPage() {
298
298
  </Alert>
299
299
  )}
300
300
  {isCodeOverride && (
301
- <Alert variant="warning" className="mb-4">
301
+ <Alert status="warning" className="mb-4">
302
302
  <AlertDescription className="flex items-center justify-between">
303
303
  <span>{t('workflows.source.code_override.banner')}</span>
304
304
  <Button variant="outline" size="sm" onClick={handleResetToCode}>
@@ -53,7 +53,7 @@ export default function CreateWorkflowDefinitionPage() {
53
53
  return (
54
54
  <Page>
55
55
  <PageBody>
56
- <Alert variant="info" icon={<Zap aria-hidden="true" />} className="mb-6">
56
+ <Alert status="information" icon={<Zap aria-hidden="true" />} className="mb-6">
57
57
  <AlertTitle>{t('workflows.create.eventTriggersTitle')}</AlertTitle>
58
58
  <AlertDescription>
59
59
  {t('workflows.create.eventTriggersDescription')}
@@ -837,12 +837,12 @@ export default function VisualEditorPage() {
837
837
  {(isCodeOnly || isCodeOverride) && (
838
838
  <div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
839
839
  {isCodeOnly && (
840
- <Alert variant="info">
840
+ <Alert status="information">
841
841
  <AlertTitle>{t('workflows.source.code.readonlyBanner')}</AlertTitle>
842
842
  </Alert>
843
843
  )}
844
844
  {isCodeOverride && (
845
- <Alert variant="warning">
845
+ <Alert status="warning">
846
846
  <AlertTitle>{t('workflows.source.code_override.banner')}</AlertTitle>
847
847
  </Alert>
848
848
  )}
@@ -1170,7 +1170,7 @@ export default function VisualEditorPage() {
1170
1170
  </div>
1171
1171
 
1172
1172
  {/* Instructions */}
1173
- <Alert variant="info" className="mt-6">
1173
+ <Alert status="information" className="mt-6">
1174
1174
  <AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
1175
1175
  <div className="mt-2">
1176
1176
  <ul className="list-inside list-disc space-y-1 text-xs">
@@ -301,7 +301,7 @@ export function DefinitionTriggersEditor({
301
301
  </p>
302
302
 
303
303
  {value.length === 0 ? (
304
- <Alert variant="info">
304
+ <Alert status="information">
305
305
  <AlertTitle>{t('workflows.triggers.empty.title', 'No triggers configured')}</AlertTitle>
306
306
  <AlertDescription>
307
307
  {t('workflows.triggers.empty.description', 'Click "Add Trigger" to create an event trigger that automatically starts this workflow.')}
@@ -540,7 +540,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
540
540
 
541
541
  <div className="space-y-4">
542
542
  {!isEditable ? (
543
- <Alert variant="info">
543
+ <Alert status="information">
544
544
  <AlertDescription>
545
545
  {t('workflows.nodeEditor.endStepsNotEditable')}
546
546
  </AlertDescription>
@@ -548,7 +548,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
548
548
  ) : isStartNode ? (
549
549
  <div className="space-y-4">
550
550
  {/* Info Alert for START nodes */}
551
- <Alert variant="info">
551
+ <Alert status="information">
552
552
  <AlertDescription>
553
553
  {t('workflows.nodeEditor.startStepsInfo')}
554
554
  </AlertDescription>
@@ -691,7 +691,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
691
691
 
692
692
  {/* JSON Schema Format Notice */}
693
693
  {isJsonSchemaFormat && (
694
- <Alert variant="info" className="mb-3">
694
+ <Alert status="information" className="mb-3">
695
695
  <AlertDescription>
696
696
  {t('workflows.nodeEditor.jsonSchemaFormat')}
697
697
  </AlertDescription>