@open-mercato/core 0.6.7-develop.6834.1.e76f4b8cbc → 0.6.7-develop.6851.1.c1680b00d8
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 +1 -1
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +2 -3
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/dashboards/lib/aggregations.js +20 -6
- package/dist/modules/dashboards/lib/aggregations.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/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/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/sales/commands/documents.js +27 -0
- 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/package.json +8 -8
- package/src/modules/api_keys/i18n/ko.json +42 -0
- package/src/modules/attachments/i18n/ko.json +172 -0
- package/src/modules/audit_logs/i18n/ko.json +66 -0
- package/src/modules/auth/i18n/ko.json +255 -0
- package/src/modules/business_rules/i18n/ko.json +396 -0
- package/src/modules/catalog/i18n/ko.json +723 -0
- package/src/modules/communication_channels/i18n/ko.json +138 -0
- package/src/modules/configs/i18n/ko.json +197 -0
- package/src/modules/currencies/i18n/de.json +1 -0
- package/src/modules/currencies/i18n/en.json +1 -0
- package/src/modules/currencies/i18n/es.json +1 -0
- package/src/modules/currencies/i18n/ko.json +160 -0
- package/src/modules/currencies/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/i18n/ko.json +354 -0
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +2 -3
- package/src/modules/customers/i18n/de.json +10 -0
- package/src/modules/customers/i18n/en.json +10 -0
- package/src/modules/customers/i18n/es.json +10 -0
- package/src/modules/customers/i18n/ko.json +2855 -0
- package/src/modules/customers/i18n/pl.json +10 -0
- package/src/modules/dashboards/i18n/ko.json +118 -0
- package/src/modules/dashboards/lib/aggregations.ts +42 -6
- package/src/modules/data_sync/i18n/ko.json +125 -0
- package/src/modules/design_system/i18n/ko.json +58 -0
- package/src/modules/dictionaries/i18n/ko.json +130 -0
- package/src/modules/directory/i18n/ko.json +105 -0
- 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/components/RecordListRelationCell.tsx +90 -0
- package/src/modules/entities/components/recordListRelationDisplays.ts +109 -0
- package/src/modules/entities/i18n/ko.json +260 -0
- package/src/modules/feature_toggles/i18n/ko.json +96 -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/inbox_ops/i18n/ko.json +196 -0
- package/src/modules/integrations/i18n/ko.json +129 -0
- package/src/modules/messages/i18n/ko.json +263 -0
- package/src/modules/notifications/i18n/ko.json +55 -0
- package/src/modules/payment_gateways/i18n/ko.json +87 -0
- package/src/modules/planner/i18n/ko.json +565 -0
- package/src/modules/portal/i18n/ko.json +143 -0
- package/src/modules/progress/i18n/ko.json +20 -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/i18n/ko.json +69 -0
- package/src/modules/query_index/lib/coverage.ts +38 -8
- package/src/modules/query_index/lib/reindexer.ts +62 -40
- package/src/modules/resources/i18n/ko.json +770 -0
- package/src/modules/sales/commands/documents.ts +49 -0
- package/src/modules/sales/data/validators.ts +2 -0
- package/src/modules/sales/i18n/ko.json +1442 -0
- package/src/modules/shipping_carriers/i18n/ko.json +85 -0
- package/src/modules/staff/i18n/ko.json +1162 -0
- package/src/modules/sync_excel/i18n/ko.json +107 -0
- package/src/modules/translations/i18n/ko.json +45 -0
- package/src/modules/wms/i18n/ko.json +1053 -0
- package/src/modules/workflows/i18n/ko.json +1203 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"directory.audit.organizations.create": "조직 생성",
|
|
3
|
+
"directory.audit.organizations.delete": "조직 삭제",
|
|
4
|
+
"directory.audit.organizations.update": "조직 수정",
|
|
5
|
+
"directory.audit.tenants.create": "테넌트 생성",
|
|
6
|
+
"directory.audit.tenants.delete": "테넌트 삭제",
|
|
7
|
+
"directory.audit.tenants.update": "테넌트 수정",
|
|
8
|
+
"directory.branding.actions.reset": "기본 로고 사용",
|
|
9
|
+
"directory.branding.actions.save": "브랜딩 저장",
|
|
10
|
+
"directory.branding.aspectRatio.description": "비활성화되면 사이드바에서 로고가 표준 아이콘 모양으로 잘립니다.",
|
|
11
|
+
"directory.branding.aspectRatio.label": "가로 세로 비율 유지",
|
|
12
|
+
"directory.branding.currentScope": "현재 조직",
|
|
13
|
+
"directory.branding.description": "현재 선택된 조직에 대해 백엔드 사이드바에 사용되는 로고를 설정하세요.",
|
|
14
|
+
"directory.branding.errors.blocked": "브랜딩 저장이 차단되었습니다.",
|
|
15
|
+
"directory.branding.errors.invalidLogoUrl": "유효한 이미지 URL을 입력하세요.",
|
|
16
|
+
"directory.branding.errors.load": "조직 브랜딩을 불러오지 못했습니다",
|
|
17
|
+
"directory.branding.errors.notFound": "조직을 찾을 수 없습니다",
|
|
18
|
+
"directory.branding.errors.organizationRequired": "사이드바 브랜딩을 변경하기 전에 단일 조직을 선택하세요.",
|
|
19
|
+
"directory.branding.errors.save": "조직 브랜딩 업데이트에 실패했습니다",
|
|
20
|
+
"directory.branding.errors.upload": "로고 업로드에 실패했습니다",
|
|
21
|
+
"directory.branding.file.hint": "PNG, JPG, WebP 또는 SVG가 가장 적합합니다. 업로드된 파일은 조직 첨부 파일로 저장됩니다.",
|
|
22
|
+
"directory.branding.file.label": "로고 업로드",
|
|
23
|
+
"directory.branding.flash.saved": "조직 브랜딩이 업데이트되었습니다",
|
|
24
|
+
"directory.branding.loading": "조직 브랜딩을 불러오는 중...",
|
|
25
|
+
"directory.branding.nav": "조직 브랜딩",
|
|
26
|
+
"directory.branding.previewAlt": "{{name}} 로고 미리보기",
|
|
27
|
+
"directory.branding.title": "조직 브랜딩",
|
|
28
|
+
"directory.branding.url.hint": "외부 이미지 URL을 사용하거나 비워 두면 기본 Open Mercato 로고로 대체됩니다.",
|
|
29
|
+
"directory.branding.url.label": "로고 URL",
|
|
30
|
+
"directory.branding.url.placeholder": "https://example.com/logo.svg",
|
|
31
|
+
"directory.nav.group": "디렉터리",
|
|
32
|
+
"directory.nav.organizations": "조직",
|
|
33
|
+
"directory.nav.organizations.create": "조직 생성",
|
|
34
|
+
"directory.nav.tenants": "테넌트",
|
|
35
|
+
"directory.nav.tenants.create": "테넌트 생성",
|
|
36
|
+
"directory.organizations.common.none": "—",
|
|
37
|
+
"directory.organizations.errors.deleteFailed": "조직 삭제에 실패했습니다",
|
|
38
|
+
"directory.organizations.errors.orgIdRequired": "조직 식별자는 필수입니다",
|
|
39
|
+
"directory.organizations.errors.tenantRequired": "슈퍼 관리자는 테넌트 선택이 필요합니다",
|
|
40
|
+
"directory.organizations.flash.created": "조직이 생성되었습니다",
|
|
41
|
+
"directory.organizations.flash.deleted": "조직이 삭제되었습니다",
|
|
42
|
+
"directory.organizations.flash.updated": "조직이 수정되었습니다",
|
|
43
|
+
"directory.organizations.form.action.create": "생성",
|
|
44
|
+
"directory.organizations.form.action.save": "저장",
|
|
45
|
+
"directory.organizations.form.actions.backToList": "조직 목록으로 돌아가기",
|
|
46
|
+
"directory.organizations.form.children.empty": "직접 할당된 하위 조직이 없습니다.",
|
|
47
|
+
"directory.organizations.form.errors.load": "조직을 불러오지 못했습니다",
|
|
48
|
+
"directory.organizations.form.errors.missingId": "조직 식별자가 누락되었습니다.",
|
|
49
|
+
"directory.organizations.form.errors.notFound": "조직을 찾을 수 없습니다",
|
|
50
|
+
"directory.organizations.form.field.children": "하위 조직",
|
|
51
|
+
"directory.organizations.form.field.isActive": "활성",
|
|
52
|
+
"directory.organizations.form.field.name": "이름",
|
|
53
|
+
"directory.organizations.form.field.parent": "상위 조직",
|
|
54
|
+
"directory.organizations.form.field.slug": "슬러그",
|
|
55
|
+
"directory.organizations.form.field.slug.description": "고객 포털에 사용되는 URL 안전 식별자입니다 (소문자, 숫자, 하이픈, 밑줄).",
|
|
56
|
+
"directory.organizations.form.field.tenant": "테넌트",
|
|
57
|
+
"directory.organizations.form.field.tenantReadonlyDescription": "기존 조직의 테넌트를 변경할 수 없습니다.",
|
|
58
|
+
"directory.organizations.form.group.customFields": "커스텀 데이터",
|
|
59
|
+
"directory.organizations.form.group.details": "상세 정보",
|
|
60
|
+
"directory.organizations.form.loading": "조직을 불러오는 중...",
|
|
61
|
+
"directory.organizations.form.pathLabel": "경로: {path}",
|
|
62
|
+
"directory.organizations.form.rootOption": "— 최상위 레벨 —",
|
|
63
|
+
"directory.organizations.form.tenant.select": "테넌트 선택",
|
|
64
|
+
"directory.organizations.form.title.edit": "조직 편집",
|
|
65
|
+
"directory.organizations.list.actions.create": "생성",
|
|
66
|
+
"directory.organizations.list.actions.delete": "삭제",
|
|
67
|
+
"directory.organizations.list.actions.edit": "편집",
|
|
68
|
+
"directory.organizations.list.columns.active": "활성",
|
|
69
|
+
"directory.organizations.list.columns.children": "하위 조직",
|
|
70
|
+
"directory.organizations.list.columns.organization": "조직",
|
|
71
|
+
"directory.organizations.list.columns.parent": "상위 조직",
|
|
72
|
+
"directory.organizations.list.columns.path": "경로",
|
|
73
|
+
"directory.organizations.list.columns.tenant": "테넌트",
|
|
74
|
+
"directory.organizations.list.confirmDelete": "조직 \"{{name}}\"을(를) 보관하시겠습니까?",
|
|
75
|
+
"directory.organizations.list.error.delete": "조직 삭제에 실패했습니다",
|
|
76
|
+
"directory.organizations.list.error.load": "조직을 불러오지 못했습니다",
|
|
77
|
+
"directory.organizations.list.filters.active": "활성",
|
|
78
|
+
"directory.organizations.list.filters.all": "전체",
|
|
79
|
+
"directory.organizations.list.filters.inactive": "비활성",
|
|
80
|
+
"directory.organizations.list.filters.status": "상태",
|
|
81
|
+
"directory.organizations.list.searchPlaceholder": "조직 검색",
|
|
82
|
+
"directory.organizations.list.title": "조직",
|
|
83
|
+
"directory.tenants.form.actions.backToList": "테넌트 목록으로 돌아가기",
|
|
84
|
+
"directory.tenants.form.errors.delete": "테넌트 삭제에 실패했습니다",
|
|
85
|
+
"directory.tenants.form.errors.load": "테넌트를 불러오지 못했습니다",
|
|
86
|
+
"directory.tenants.form.errors.notFound": "테넌트를 찾을 수 없습니다",
|
|
87
|
+
"directory.tenants.form.fields.active": "활성",
|
|
88
|
+
"directory.tenants.form.fields.name": "이름",
|
|
89
|
+
"directory.tenants.form.groups.custom": "커스텀 데이터",
|
|
90
|
+
"directory.tenants.form.groups.details": "상세 정보",
|
|
91
|
+
"directory.tenants.form.loading": "테넌트를 불러오는 중…",
|
|
92
|
+
"directory.tenants.form.title.edit": "테넌트 편집",
|
|
93
|
+
"directory.tenants.list.actions.create": "생성",
|
|
94
|
+
"directory.tenants.list.columns.active": "활성",
|
|
95
|
+
"directory.tenants.list.columns.created": "생성일",
|
|
96
|
+
"directory.tenants.list.columns.tenant": "테넌트",
|
|
97
|
+
"directory.tenants.list.confirmDelete": "테넌트 \"{{name}}\"을(를) 삭제하시겠습니까? 보관 처리됩니다.",
|
|
98
|
+
"directory.tenants.list.error.delete": "테넌트 삭제에 실패했습니다",
|
|
99
|
+
"directory.tenants.list.error.load": "테넌트를 불러오지 못했습니다",
|
|
100
|
+
"directory.tenants.list.filters.active": "활성",
|
|
101
|
+
"directory.tenants.list.filters.inactive": "비활성",
|
|
102
|
+
"directory.tenants.list.filters.status": "상태",
|
|
103
|
+
"directory.tenants.list.success.delete": "테넌트가 삭제되었습니다",
|
|
104
|
+
"directory.tenants.list.title": "테넌트"
|
|
105
|
+
}
|
|
@@ -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>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
5
|
+
import {
|
|
6
|
+
getRecordListRelationDisplays,
|
|
7
|
+
resolveRecordListRelationDisplays,
|
|
8
|
+
type RelationDisplaysByField,
|
|
9
|
+
} from './recordListRelationDisplays'
|
|
10
|
+
|
|
11
|
+
const RelationDisplaysContext = React.createContext<RelationDisplaysByField>({})
|
|
12
|
+
|
|
13
|
+
export function RelationDisplaysProvider({
|
|
14
|
+
displaysByField,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
displaysByField: RelationDisplaysByField
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<RelationDisplaysContext.Provider value={displaysByField}>
|
|
22
|
+
{children}
|
|
23
|
+
</RelationDisplaysContext.Provider>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useRecordListRelationDisplays(
|
|
28
|
+
definitions: CustomFieldDefDto[],
|
|
29
|
+
records: Array<Record<string, unknown>>,
|
|
30
|
+
): RelationDisplaysByField {
|
|
31
|
+
const [displaysByField, setDisplaysByField] = React.useState<RelationDisplaysByField>({})
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const abortController = new AbortController()
|
|
35
|
+
setDisplaysByField((current) => (Object.keys(current).length ? {} : current))
|
|
36
|
+
if (!definitions.length || !records.length) {
|
|
37
|
+
return () => abortController.abort()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void resolveRecordListRelationDisplays(definitions, records, abortController.signal)
|
|
41
|
+
.then((resolvedDisplays) => {
|
|
42
|
+
if (!abortController.signal.aborted) setDisplaysByField(resolvedDisplays)
|
|
43
|
+
})
|
|
44
|
+
.catch(() => {
|
|
45
|
+
if (!abortController.signal.aborted) setDisplaysByField({})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return () => abortController.abort()
|
|
49
|
+
}, [definitions, records])
|
|
50
|
+
|
|
51
|
+
return displaysByField
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function RelationCell({
|
|
55
|
+
fieldKey,
|
|
56
|
+
value,
|
|
57
|
+
fallbackText,
|
|
58
|
+
}: {
|
|
59
|
+
fieldKey: string
|
|
60
|
+
value: unknown
|
|
61
|
+
fallbackText: string
|
|
62
|
+
}) {
|
|
63
|
+
const displaysByField = React.useContext(RelationDisplaysContext)
|
|
64
|
+
const resolvedDisplays = getRecordListRelationDisplays(value, displaysByField[fieldKey])
|
|
65
|
+
const rawValue = resolvedDisplays.map((display) => display.id).join(', ') || fallbackText
|
|
66
|
+
|
|
67
|
+
if (!resolvedDisplays.length) {
|
|
68
|
+
return <span className="inline-block max-w-60 truncate align-top" title={rawValue}>{rawValue}</span>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<span className="inline-block max-w-60 truncate align-top" title={rawValue}>
|
|
73
|
+
{resolvedDisplays.map((display, index) => (
|
|
74
|
+
<React.Fragment key={`${display.id}-${index}`}>
|
|
75
|
+
{index > 0 ? ', ' : null}
|
|
76
|
+
{display.href ? (
|
|
77
|
+
<Link
|
|
78
|
+
href={display.href}
|
|
79
|
+
className="font-medium text-primary underline-offset-2 hover:underline focus-visible:underline"
|
|
80
|
+
onClick={(event) => event.stopPropagation()}
|
|
81
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
82
|
+
>
|
|
83
|
+
{display.label}
|
|
84
|
+
</Link>
|
|
85
|
+
) : display.label}
|
|
86
|
+
</React.Fragment>
|
|
87
|
+
))}
|
|
88
|
+
</span>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
2
|
+
import {
|
|
3
|
+
buildRelationHref,
|
|
4
|
+
collectRelationValueIds,
|
|
5
|
+
fetchRelationRecordDisplays,
|
|
6
|
+
parseRelationOptionsMetadata,
|
|
7
|
+
type RelationOptionsMetadata,
|
|
8
|
+
type ResolvedValueDisplay,
|
|
9
|
+
} from '@open-mercato/ui/backend/utils/customFieldRelationDisplay'
|
|
10
|
+
|
|
11
|
+
export type RelationDisplaysByField = Record<string, Record<string, ResolvedValueDisplay>>
|
|
12
|
+
|
|
13
|
+
type RelationLookup = {
|
|
14
|
+
optionsUrl: string
|
|
15
|
+
relation: RelationOptionsMetadata
|
|
16
|
+
recordIds: Set<string>
|
|
17
|
+
recordIdsByField: Map<string, Set<string>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveRelationOptionsUrl(definition: CustomFieldDefDto): string | null {
|
|
21
|
+
if (definition.kind !== 'relation') return null
|
|
22
|
+
|
|
23
|
+
const configuredUrl = definition.optionsUrl?.trim()
|
|
24
|
+
if (configuredUrl && parseRelationOptionsMetadata(configuredUrl)) return configuredUrl
|
|
25
|
+
|
|
26
|
+
const relatedEntityId = definition.relatedEntityId?.trim()
|
|
27
|
+
if (!relatedEntityId) return null
|
|
28
|
+
return `/api/entities/relations/options?entityId=${encodeURIComponent(relatedEntityId)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectRelationLookups(
|
|
32
|
+
definitions: CustomFieldDefDto[],
|
|
33
|
+
records: Array<Record<string, unknown>>,
|
|
34
|
+
): RelationLookup[] {
|
|
35
|
+
const lookups = new Map<string, RelationLookup>()
|
|
36
|
+
|
|
37
|
+
definitions.forEach((definition) => {
|
|
38
|
+
const optionsUrl = resolveRelationOptionsUrl(definition)
|
|
39
|
+
if (!optionsUrl) return
|
|
40
|
+
const relation = parseRelationOptionsMetadata(optionsUrl)
|
|
41
|
+
if (!relation) return
|
|
42
|
+
|
|
43
|
+
const recordIds = new Set(
|
|
44
|
+
records.flatMap((record) => collectRelationValueIds(record[definition.key])),
|
|
45
|
+
)
|
|
46
|
+
if (!recordIds.size) return
|
|
47
|
+
|
|
48
|
+
const lookupKey = JSON.stringify([relation.entityId, optionsUrl])
|
|
49
|
+
const lookup = lookups.get(lookupKey) ?? {
|
|
50
|
+
optionsUrl,
|
|
51
|
+
relation,
|
|
52
|
+
recordIds: new Set<string>(),
|
|
53
|
+
recordIdsByField: new Map<string, Set<string>>(),
|
|
54
|
+
}
|
|
55
|
+
recordIds.forEach((recordId) => lookup.recordIds.add(recordId))
|
|
56
|
+
lookup.recordIdsByField.set(definition.key, recordIds)
|
|
57
|
+
lookups.set(lookupKey, lookup)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return Array.from(lookups.values())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function resolveRecordListRelationDisplays(
|
|
64
|
+
definitions: CustomFieldDefDto[],
|
|
65
|
+
records: Array<Record<string, unknown>>,
|
|
66
|
+
signal?: AbortSignal,
|
|
67
|
+
): Promise<RelationDisplaysByField> {
|
|
68
|
+
const displaysByField: RelationDisplaysByField = {}
|
|
69
|
+
const lookups = collectRelationLookups(definitions, records)
|
|
70
|
+
|
|
71
|
+
await Promise.all(lookups.map(async (lookup) => {
|
|
72
|
+
let fetchedDisplays: Record<string, ResolvedValueDisplay> = {}
|
|
73
|
+
try {
|
|
74
|
+
fetchedDisplays = await fetchRelationRecordDisplays(
|
|
75
|
+
lookup.optionsUrl,
|
|
76
|
+
lookup.relation,
|
|
77
|
+
Array.from(lookup.recordIds),
|
|
78
|
+
signal,
|
|
79
|
+
)
|
|
80
|
+
} catch {
|
|
81
|
+
fetchedDisplays = {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (signal?.aborted) return
|
|
85
|
+
lookup.recordIdsByField.forEach((recordIds, fieldKey) => {
|
|
86
|
+
const fieldDisplays: Record<string, ResolvedValueDisplay> = {}
|
|
87
|
+
recordIds.forEach((recordId) => {
|
|
88
|
+
fieldDisplays[recordId] = fetchedDisplays[recordId] ?? {
|
|
89
|
+
label: recordId,
|
|
90
|
+
href: buildRelationHref(lookup.relation.entityId, recordId),
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
displaysByField[fieldKey] = fieldDisplays
|
|
94
|
+
})
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
return displaysByField
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getRecordListRelationDisplays(
|
|
101
|
+
value: unknown,
|
|
102
|
+
displays: Record<string, ResolvedValueDisplay> | undefined,
|
|
103
|
+
): Array<{ id: string; label: string; href?: string }> {
|
|
104
|
+
return collectRelationValueIds(value).map((recordId) => ({
|
|
105
|
+
id: recordId,
|
|
106
|
+
label: displays?.[recordId]?.label ?? recordId,
|
|
107
|
+
href: displays?.[recordId]?.href,
|
|
108
|
+
}))
|
|
109
|
+
}
|