@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/bootstrap.js +16 -1
- package/dist/bootstrap.js.map +2 -2
- package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
- package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +17 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/api/route.js +104 -8
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +67 -1
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/di.js +7 -1
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
- package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
- package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
- package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
- package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
- package/dist/modules/attachments/lib/quota-service.js +224 -0
- package/dist/modules/attachments/lib/quota-service.js.map +7 -0
- package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
- package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
- package/dist/modules/attachments/workers/quota-recovery.js +45 -0
- package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
- package/dist/modules/auth/lib/setup-app.js +16 -2
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/dashboards/services/widgetDataService.js +3 -4
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +2 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +18 -3
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/cli.js +193 -19
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/components/RecordListRelationCell.js +63 -0
- package/dist/modules/entities/components/RecordListRelationCell.js.map +7 -0
- package/dist/modules/entities/components/recordListRelationDisplays.js +80 -0
- package/dist/modules/entities/components/recordListRelationDisplays.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +14 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +22 -17
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +15 -0
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +10 -0
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/cli.js +125 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +19 -7
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +43 -37
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
- package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +137 -22
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +2 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/workflows.js +10 -7
- package/dist/modules/sales/workflows.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +27 -0
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
- package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/attachment_quota_reservation/index.ts +15 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +17 -0
- package/package.json +7 -7
- package/src/bootstrap.ts +17 -2
- package/src/modules/attachments/api/route.ts +114 -7
- package/src/modules/attachments/data/entities.ts +59 -0
- package/src/modules/attachments/di.ts +9 -1
- package/src/modules/attachments/i18n/de.json +3 -0
- package/src/modules/attachments/i18n/en.json +3 -0
- package/src/modules/attachments/i18n/es.json +3 -0
- package/src/modules/attachments/i18n/pl.json +3 -0
- package/src/modules/attachments/lib/drivers/index.ts +1 -1
- package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
- package/src/modules/attachments/lib/drivers/types.ts +5 -0
- package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
- package/src/modules/attachments/lib/quota-service.ts +442 -0
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
- package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
- package/src/modules/attachments/workers/quota-recovery.ts +68 -0
- package/src/modules/auth/lib/setup-app.ts +20 -2
- package/src/modules/dashboards/services/widgetDataService.ts +3 -6
- package/src/modules/entities/api/definitions.ts +4 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +94 -77
- package/src/modules/entities/cli.ts +238 -21
- package/src/modules/entities/components/RecordListRelationCell.tsx +90 -0
- package/src/modules/entities/components/recordListRelationDisplays.ts +109 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +17 -1
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +21 -16
- package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
- package/src/modules/messages/api/[id]/route.ts +21 -0
- package/src/modules/query_index/api/reindex.ts +15 -0
- package/src/modules/query_index/cli.ts +180 -1
- package/src/modules/query_index/lib/coverage.ts +38 -8
- package/src/modules/query_index/lib/reindexer.ts +62 -40
- package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
- package/src/modules/sales/commands/documents.ts +237 -29
- package/src/modules/sales/data/validators.ts +2 -0
- package/src/modules/sales/workflows.ts +10 -7
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/activity-executor.ts +40 -1
- package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
- 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
|
|
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
|
|
869
|
-
if (!
|
|
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
|
-
|
|
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
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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>
|