@open-mercato/core 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a

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 (115) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/bootstrap.js +16 -1
  3. package/dist/bootstrap.js.map +2 -2
  4. package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
  5. package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
  6. package/dist/generated/entities.ids.generated.js +2 -1
  7. package/dist/generated/entities.ids.generated.js.map +2 -2
  8. package/dist/generated/entity-fields-registry.js +17 -0
  9. package/dist/generated/entity-fields-registry.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +104 -8
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/attachments/data/entities.js +67 -1
  13. package/dist/modules/attachments/data/entities.js.map +2 -2
  14. package/dist/modules/attachments/di.js +7 -1
  15. package/dist/modules/attachments/di.js.map +2 -2
  16. package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
  17. package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
  18. package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
  19. package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
  20. package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
  21. package/dist/modules/attachments/lib/quota-service.js +224 -0
  22. package/dist/modules/attachments/lib/quota-service.js.map +7 -0
  23. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
  24. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
  25. package/dist/modules/attachments/workers/quota-recovery.js +45 -0
  26. package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
  27. package/dist/modules/auth/lib/setup-app.js +16 -2
  28. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  29. package/dist/modules/dashboards/services/widgetDataService.js +3 -4
  30. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  31. package/dist/modules/entities/api/definitions.js +2 -0
  32. package/dist/modules/entities/api/definitions.js.map +2 -2
  33. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +18 -3
  34. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  35. package/dist/modules/entities/cli.js +193 -19
  36. package/dist/modules/entities/cli.js.map +2 -2
  37. package/dist/modules/entities/components/RecordListRelationCell.js +63 -0
  38. package/dist/modules/entities/components/RecordListRelationCell.js.map +7 -0
  39. package/dist/modules/entities/components/recordListRelationDisplays.js +80 -0
  40. package/dist/modules/entities/components/recordListRelationDisplays.js.map +7 -0
  41. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +14 -1
  42. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  43. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +22 -17
  44. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  45. package/dist/modules/messages/api/[id]/route.js +15 -0
  46. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  47. package/dist/modules/query_index/api/reindex.js +10 -0
  48. package/dist/modules/query_index/api/reindex.js.map +2 -2
  49. package/dist/modules/query_index/cli.js +125 -1
  50. package/dist/modules/query_index/cli.js.map +2 -2
  51. package/dist/modules/query_index/lib/coverage.js +19 -7
  52. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  53. package/dist/modules/query_index/lib/reindexer.js +43 -37
  54. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  55. package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
  56. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  57. package/dist/modules/sales/commands/documents.js +137 -22
  58. package/dist/modules/sales/commands/documents.js.map +2 -2
  59. package/dist/modules/sales/data/validators.js +2 -0
  60. package/dist/modules/sales/data/validators.js.map +2 -2
  61. package/dist/modules/sales/workflows.js +10 -7
  62. package/dist/modules/sales/workflows.js.map +2 -2
  63. package/dist/modules/workflows/lib/activity-executor.js +27 -0
  64. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  65. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
  66. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
  67. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
  68. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  69. package/generated/entities/attachment_quota_reservation/index.ts +15 -0
  70. package/generated/entities.ids.generated.ts +2 -1
  71. package/generated/entity-fields-registry.ts +17 -0
  72. package/package.json +7 -7
  73. package/src/bootstrap.ts +17 -2
  74. package/src/modules/attachments/api/route.ts +114 -7
  75. package/src/modules/attachments/data/entities.ts +59 -0
  76. package/src/modules/attachments/di.ts +9 -1
  77. package/src/modules/attachments/i18n/de.json +3 -0
  78. package/src/modules/attachments/i18n/en.json +3 -0
  79. package/src/modules/attachments/i18n/es.json +3 -0
  80. package/src/modules/attachments/i18n/pl.json +3 -0
  81. package/src/modules/attachments/lib/drivers/index.ts +1 -1
  82. package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
  83. package/src/modules/attachments/lib/drivers/types.ts +5 -0
  84. package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
  85. package/src/modules/attachments/lib/quota-service.ts +442 -0
  86. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
  87. package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
  88. package/src/modules/attachments/workers/quota-recovery.ts +68 -0
  89. package/src/modules/auth/lib/setup-app.ts +20 -2
  90. package/src/modules/dashboards/services/widgetDataService.ts +3 -6
  91. package/src/modules/entities/api/definitions.ts +4 -0
  92. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +94 -77
  93. package/src/modules/entities/cli.ts +238 -21
  94. package/src/modules/entities/components/RecordListRelationCell.tsx +90 -0
  95. package/src/modules/entities/components/recordListRelationDisplays.ts +109 -0
  96. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +17 -1
  97. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +21 -16
  98. package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
  99. package/src/modules/messages/api/[id]/route.ts +21 -0
  100. package/src/modules/query_index/api/reindex.ts +15 -0
  101. package/src/modules/query_index/cli.ts +180 -1
  102. package/src/modules/query_index/lib/coverage.ts +38 -8
  103. package/src/modules/query_index/lib/reindexer.ts +62 -40
  104. package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
  105. package/src/modules/sales/commands/documents.ts +237 -29
  106. package/src/modules/sales/data/validators.ts +2 -0
  107. package/src/modules/sales/workflows.ts +10 -7
  108. package/src/modules/workflows/i18n/de.json +1 -0
  109. package/src/modules/workflows/i18n/en.json +1 -0
  110. package/src/modules/workflows/i18n/es.json +1 -0
  111. package/src/modules/workflows/i18n/ko.json +1 -0
  112. package/src/modules/workflows/i18n/pl.json +1 -0
  113. package/src/modules/workflows/lib/activity-executor.ts +40 -1
  114. package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
  115. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +15 -6
@@ -0,0 +1,12 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260709234741_attachments extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`create table "attachment_quota_reservations" ("id" uuid not null, "tenant_id" uuid not null, "organization_id" uuid not null, "reserved_bytes" bigint not null, "actual_bytes" bigint null, "status" text not null default 'reserved', "source" text not null, "storage_driver" text not null, "partition_code" text null, "storage_path" text not null, "lease_token" uuid not null, "upload_token_hash" text null, "expires_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
7
+ this.addSql(`create index "attachment_quota_reservations_expires_idx" on "attachment_quota_reservations" ("expires_at");`);
8
+ this.addSql(`create index "attachment_quota_reservations_tenant_status_idx" on "attachment_quota_reservations" ("tenant_id", "status");`);
9
+ this.addSql(`alter table "attachment_quota_reservations" add constraint "attachment_quota_reservations_scope_path_unique" unique ("tenant_id", "storage_driver", "storage_path");`);
10
+ }
11
+
12
+ }
@@ -0,0 +1,68 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import type { AttachmentQuotaService } from '../lib/quota-service'
4
+ import type { StorageDriverFactory } from '../lib/drivers'
5
+ import { scheduleAttachmentQuotaRecovery, type AttachmentQuotaRecoveryJob } from '../lib/quota-recovery-queue'
6
+
7
+ export const metadata: WorkerMeta = {
8
+ queue: 'attachments-quota-recovery',
9
+ id: 'attachments:quota-recovery',
10
+ concurrency: 2,
11
+ }
12
+
13
+ type HandlerContext = JobContext & { resolve: <T = unknown>(name: string) => T }
14
+
15
+ type AttachmentLookupDatabase = {
16
+ attachments: {
17
+ id: string
18
+ tenant_id: string
19
+ storage_driver: string
20
+ storage_path: string
21
+ }
22
+ }
23
+
24
+ export default async function handle(
25
+ job: QueuedJob<AttachmentQuotaRecoveryJob>,
26
+ ctx: HandlerContext,
27
+ ): Promise<void> {
28
+ const quotaService = ctx.resolve<AttachmentQuotaService>('attachmentQuotaService')
29
+ const scope = {
30
+ tenantId: job.payload.tenantId,
31
+ organizationId: job.payload.organizationId,
32
+ }
33
+ const record = await quotaService.getReservation(job.payload.reservationId, scope)
34
+ if (!record || record.status === 'committed') return
35
+ if (record.expiresAt && record.expiresAt.getTime() > Date.now()) {
36
+ await scheduleAttachmentQuotaRecovery(job.payload, record.expiresAt.getTime() - Date.now())
37
+ return
38
+ }
39
+
40
+ const claimed = await quotaService.claimExpired(record.id, scope)
41
+ if (!claimed) return
42
+ try {
43
+ const em = ctx.resolve<EntityManager>('em')
44
+ const existingAttachment = await em.getKysely<AttachmentLookupDatabase>()
45
+ .selectFrom('attachments')
46
+ .select('id')
47
+ .where('tenant_id', '=', claimed.tenantId)
48
+ .where('storage_driver', '=', claimed.storageDriver)
49
+ .where('storage_path', '=', claimed.storagePath)
50
+ .executeTakeFirst()
51
+ if (!existingAttachment) {
52
+ if (!claimed.partitionCode) throw new Error('Attachment quota recovery is missing its partition code.')
53
+ const factory = ctx.resolve<StorageDriverFactory>('storageDriverFactory')
54
+ const driver = await factory.resolveForPartition(claimed.partitionCode, {
55
+ tenantId: claimed.tenantId,
56
+ organizationId: claimed.organizationId,
57
+ })
58
+ await (driver.deleteStrict?.(claimed.partitionCode, claimed.storagePath)
59
+ ?? driver.delete(claimed.partitionCode, claimed.storagePath))
60
+ }
61
+ await quotaService.release(claimed.id, claimed.leaseToken)
62
+ } catch (error) {
63
+ if (claimed.expiresAt) {
64
+ await scheduleAttachmentQuotaRecovery(job.payload, claimed.expiresAt.getTime() - Date.now())
65
+ }
66
+ throw error
67
+ }
68
+ }
@@ -5,7 +5,7 @@ import { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/d
5
5
  import { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'
6
6
  import { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'
7
7
  import { normalizeTenantId } from './tenantAccess'
8
- import { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'
8
+ import { computeEmailHash, emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'
9
9
  import { getDefaultEncryptionMaps, type Module } from '@open-mercato/shared/modules/registry'
10
10
  import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
11
11
  import { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'
@@ -190,7 +190,20 @@ export async function setupInitialTenant(
190
190
  const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules)
191
191
 
192
192
  const mainEmail = primaryUser.email
193
- const existingUser = await findOneWithDecryption(em, User, { email: mainEmail }, {}, { tenantId: null, organizationId: null })
193
+ const existingUserByHash = await findOneWithDecryption(
194
+ em,
195
+ User,
196
+ { emailHash: { $in: emailHashLookupValues(mainEmail) }, deletedAt: null },
197
+ {},
198
+ { tenantId: null, organizationId: null },
199
+ )
200
+ const existingUser = existingUserByHash ?? await findOneWithDecryption(
201
+ em,
202
+ User,
203
+ { email: mainEmail, deletedAt: null },
204
+ {},
205
+ { tenantId: null, organizationId: null },
206
+ )
194
207
  if (existingUser && failIfUserExists) {
195
208
  throw new Error('USER_EXISTS')
196
209
  }
@@ -358,7 +371,12 @@ export async function setupInitialTenant(
358
371
  await tem.flush()
359
372
 
360
373
  if (isTenantDataEncryptionEnabled()) {
374
+ // System-scoped maps are resolved from module code, never from a tenant row.
375
+ // Persisting one here would make the tenant-scoped encryption CLIs believe they
376
+ // own that entity and re-wrap its `system:<entityId>` ciphertext under the tenant
377
+ // DEK, which runtime decryption can no longer read.
361
378
  for (const spec of defaultEncryptionMaps) {
379
+ if (spec.keyScope === 'system') continue
362
380
  const existing = await findOneWithDecryption(tem, EncryptionMap, { entityId: spec.entityId, tenantId: tenant.id, organizationId: organization.id, deletedAt: null }, {}, { tenantId: String(tenant.id), organizationId: String(organization.id) })
363
381
  if (!existing) {
364
382
  tem.persist(tem.create(EncryptionMap, {
@@ -5,6 +5,7 @@ import { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
5
5
  import { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'
6
6
  import { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'
7
7
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
+ import { listEntityMetadata } from '@open-mercato/shared/lib/db/entityMetadata'
8
9
  import {
9
10
  type DateRangePreset,
10
11
  resolveDateRange,
@@ -865,12 +866,8 @@ export class WidgetDataService {
865
866
  }
866
867
 
867
868
  private resolveEntityMetadata(tableName: string): Record<string, any> | null {
868
- const registry = (this.em as any)?.getMetadata?.()
869
- if (!registry) return null
870
- const entries =
871
- (typeof registry.getAll === 'function' && registry.getAll()) ||
872
- (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))
873
- const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})
869
+ const metas = listEntityMetadata(this.em)
870
+ if (!metas.length) return null
874
871
  const match = metas.find((meta: any) => {
875
872
  const table = meta?.tableName ?? meta?.collection
876
873
  if (typeof table !== 'string') return false
@@ -441,6 +441,9 @@ export async function GET(req: Request) {
441
441
  label: d.configJson?.label || d.key,
442
442
  description: d.configJson?.description || undefined,
443
443
  multi: Boolean(d.configJson?.multi),
444
+ relatedEntityId: d.kind === 'relation' && typeof d.configJson?.relatedEntityId === 'string'
445
+ ? d.configJson.relatedEntityId.trim() || undefined
446
+ : undefined,
444
447
  options: (() => {
445
448
  if (d.kind === 'currency') return undefined
446
449
  const normalizedOptions = normalizeCustomFieldOptions(d.configJson?.options)
@@ -707,6 +710,7 @@ const customFieldDefinitionSchema = z.object({
707
710
  label: z.string(),
708
711
  description: z.string().optional(),
709
712
  multi: z.boolean().optional(),
713
+ relatedEntityId: z.string().optional(),
710
714
  options: z.array(customFieldOptionValueSchema).optional(),
711
715
  optionsUrl: z.string().optional(),
712
716
  filterable: z.boolean().optional(),
@@ -4,7 +4,7 @@ import { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widge
4
4
  import { extensionPoints } from '@open-mercato/core/modules/entities/extension-points'
5
5
  import { useSearchParams } from 'next/navigation'
6
6
  import type { ColumnDef, SortingState } from '@tanstack/react-table'
7
- import { filterCustomFieldDefs, useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'
7
+ import { filterCustomFieldDefs, useCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
8
8
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
9
9
  import { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'
10
10
  import type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'
@@ -23,6 +23,11 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
23
23
  import { useT } from '@open-mercato/shared/lib/i18n/context'
24
24
  import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
25
25
  import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
26
+ import {
27
+ RelationCell,
28
+ RelationDisplaysProvider,
29
+ useRecordListRelationDisplays,
30
+ } from '@open-mercato/core/modules/entities/components/RecordListRelationCell'
26
31
 
27
32
  type RecordsResponse = {
28
33
  items: any[]
@@ -98,6 +103,13 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
98
103
  enabled: Boolean(entityId),
99
104
  keyExtras: [scopeVersion],
100
105
  })
106
+ const relationDefinitions = React.useMemo(
107
+ () => filterCustomFieldDefs(cfDefs, 'list').filter(
108
+ (definition): definition is CustomFieldDefDto => definition.kind === 'relation',
109
+ ),
110
+ [cfDefs],
111
+ )
112
+ const relationDisplaysByField = useRecordListRelationDisplays(relationDefinitions, rawData)
101
113
 
102
114
  // Fields searched server-side: every visible column plus the base `id`.
103
115
  const searchableFields = React.useMemo(() => {
@@ -182,7 +194,10 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
182
194
  meta: { priority: idx < 4 ? 1 : idx < 6 ? 2 : idx < 8 ? 3 : idx < maxVisible ? 4 : 5 },
183
195
  cell: ({ getValue }: { getValue: () => unknown }) => {
184
196
  const v = getValue() as any
185
- return <span className="truncate max-w-[24ch] inline-block align-top" title={normalizeCell(v)}>{normalizeCell(v)}</span>
197
+ if (d.kind === 'relation') {
198
+ return <RelationCell fieldKey={d.key} value={v} fallbackText={normalizeCell(v)} />
199
+ }
200
+ return <span className="inline-block max-w-60 truncate align-top" title={normalizeCell(v)}>{normalizeCell(v)}</span>
186
201
  },
187
202
  }))
188
203
  // Ensure hidden 'id' column exists for sorting/state
@@ -378,81 +393,83 @@ export RECORD_ID="<record uuid>"`}</code></pre>
378
393
  </div>
379
394
  </div>
380
395
  </ContextHelp>
381
- <DataTable
382
- stickyActionsColumn
383
- title={`Records: ${entityId}`}
384
- entityId={entityId}
385
- actions={actions}
386
- columns={columns}
387
- data={data}
388
- perspective={{
389
- tableId: resolveExtensionPointPattern(extensionPoints.hosts.userRecordsTable.pattern, { entityId }),
390
- }}
391
- exporter={exportConfig}
392
- filters={baseFilters}
393
- filterValues={filterValues}
394
- rowActions={(row) => (
395
- <RowActions
396
- items={[
397
- { id: 'edit', label: 'Edit', href: `/backend/entities/user/${encodeURIComponent(entityId)}/records/${encodeURIComponent(String((row as any).id))}` },
398
- { id: 'delete', label: 'Delete', destructive: true, onSelect: async () => {
399
- const recordId = String((row as any).id)
400
- try {
401
- const confirmed = await confirm({
402
- title: 'Delete this record?',
403
- variant: 'destructive',
404
- })
405
- if (!confirmed) return
406
- await runDeleteMutation({
407
- operation: async () => {
408
- const deleteCall = await withScopedApiRequestHeaders(
409
- buildOptimisticLockHeader((row as any).updatedAt),
410
- () => apiCall(
411
- `/api/entities/records?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,
412
- { method: 'DELETE' },
413
- ),
414
- )
415
- if (!deleteCall.ok) {
416
- await raiseCrudError(deleteCall.response, 'Failed to delete record')
417
- }
418
- },
419
- context: {
420
- formId: deleteMutationContextId,
421
- resourceKind: 'entities.record',
422
- resourceId: recordId,
423
- retryLastMutation: retryDeleteMutation,
424
- },
425
- })
426
- const j = await readApiResultOrThrow<RecordsResponse>(
427
- `/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${pageSize}`,
428
- undefined,
429
- {
430
- errorMessage: 'Failed to reload records',
431
- fallback: { items: [], total: 0, page, pageSize, totalPages: 1 },
432
- },
433
- )
434
- setRawData(j.items || [])
435
- setTotal(j.total || 0)
436
- setTotalPages(j.totalPages || 1)
437
- flash('Record has been removed', 'success')
438
- } catch (error) {
439
- const message = error instanceof Error ? error.message : 'Failed to delete record'
440
- flash(message, 'error')
441
- }
442
- } },
443
- ]}
444
- />
445
- )}
446
- sortable
447
- sorting={sorting}
448
- onSortingChange={setSorting}
449
- searchValue={search}
450
- onSearchChange={(v) => { setSearch(v); setPage(1) }}
451
- onFiltersApply={(vals) => { setFilterValues(vals); setPage(1) }}
452
- onFiltersClear={() => { setFilterValues({}); setPage(1) }}
453
- pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}
454
- isLoading={loading}
455
- />
396
+ <RelationDisplaysProvider displaysByField={relationDisplaysByField}>
397
+ <DataTable
398
+ stickyActionsColumn
399
+ title={`Records: ${entityId}`}
400
+ entityId={entityId}
401
+ actions={actions}
402
+ columns={columns}
403
+ data={data}
404
+ perspective={{
405
+ tableId: resolveExtensionPointPattern(extensionPoints.hosts.userRecordsTable.pattern, { entityId }),
406
+ }}
407
+ exporter={exportConfig}
408
+ filters={baseFilters}
409
+ filterValues={filterValues}
410
+ rowActions={(row) => (
411
+ <RowActions
412
+ items={[
413
+ { id: 'edit', label: 'Edit', href: `/backend/entities/user/${encodeURIComponent(entityId)}/records/${encodeURIComponent(String((row as any).id))}` },
414
+ { id: 'delete', label: 'Delete', destructive: true, onSelect: async () => {
415
+ const recordId = String((row as any).id)
416
+ try {
417
+ const confirmed = await confirm({
418
+ title: 'Delete this record?',
419
+ variant: 'destructive',
420
+ })
421
+ if (!confirmed) return
422
+ await runDeleteMutation({
423
+ operation: async () => {
424
+ const deleteCall = await withScopedApiRequestHeaders(
425
+ buildOptimisticLockHeader((row as any).updatedAt),
426
+ () => apiCall(
427
+ `/api/entities/records?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,
428
+ { method: 'DELETE' },
429
+ ),
430
+ )
431
+ if (!deleteCall.ok) {
432
+ await raiseCrudError(deleteCall.response, 'Failed to delete record')
433
+ }
434
+ },
435
+ context: {
436
+ formId: deleteMutationContextId,
437
+ resourceKind: 'entities.record',
438
+ resourceId: recordId,
439
+ retryLastMutation: retryDeleteMutation,
440
+ },
441
+ })
442
+ const j = await readApiResultOrThrow<RecordsResponse>(
443
+ `/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${pageSize}`,
444
+ undefined,
445
+ {
446
+ errorMessage: 'Failed to reload records',
447
+ fallback: { items: [], total: 0, page, pageSize, totalPages: 1 },
448
+ },
449
+ )
450
+ setRawData(j.items || [])
451
+ setTotal(j.total || 0)
452
+ setTotalPages(j.totalPages || 1)
453
+ flash('Record has been removed', 'success')
454
+ } catch (error) {
455
+ const message = error instanceof Error ? error.message : 'Failed to delete record'
456
+ flash(message, 'error')
457
+ }
458
+ } },
459
+ ]}
460
+ />
461
+ )}
462
+ sortable
463
+ sorting={sorting}
464
+ onSortingChange={setSorting}
465
+ searchValue={search}
466
+ onSearchChange={(v) => { setSearch(v); setPage(1) }}
467
+ onFiltersApply={(vals) => { setFilterValues(vals); setPage(1) }}
468
+ onFiltersClear={() => { setFilterValues({}); setPage(1) }}
469
+ pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}
470
+ isLoading={loading}
471
+ />
472
+ </RelationDisplaysProvider>
456
473
  </PageBody>
457
474
  {ConfirmDialogElement}
458
475
  </Page>