@modern-admin/react 0.1.0

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 (261) hide show
  1. package/dist/action-guard.d.ts +13 -0
  2. package/dist/action-guard.d.ts.map +1 -0
  3. package/dist/action-guard.js +15 -0
  4. package/dist/action-guard.js.map +1 -0
  5. package/dist/action-menu.d.ts +17 -0
  6. package/dist/action-menu.d.ts.map +1 -0
  7. package/dist/action-menu.jsx +80 -0
  8. package/dist/action-menu.jsx.map +1 -0
  9. package/dist/admin-app.d.ts +23 -0
  10. package/dist/admin-app.d.ts.map +1 -0
  11. package/dist/admin-app.jsx +407 -0
  12. package/dist/admin-app.jsx.map +1 -0
  13. package/dist/admin-router.d.ts +29 -0
  14. package/dist/admin-router.d.ts.map +1 -0
  15. package/dist/admin-router.jsx +215 -0
  16. package/dist/admin-router.jsx.map +1 -0
  17. package/dist/breadcrumbs.d.ts +17 -0
  18. package/dist/breadcrumbs.d.ts.map +1 -0
  19. package/dist/breadcrumbs.jsx +40 -0
  20. package/dist/breadcrumbs.jsx.map +1 -0
  21. package/dist/client.d.ts +526 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +582 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/component-loader.d.ts +10 -0
  26. package/dist/component-loader.d.ts.map +1 -0
  27. package/dist/component-loader.js +23 -0
  28. package/dist/component-loader.js.map +1 -0
  29. package/dist/components/ai-assistant-widget.d.ts +3 -0
  30. package/dist/components/ai-assistant-widget.d.ts.map +1 -0
  31. package/dist/components/ai-assistant-widget.jsx +390 -0
  32. package/dist/components/ai-assistant-widget.jsx.map +1 -0
  33. package/dist/components/ai-fill-dialog.d.ts +9 -0
  34. package/dist/components/ai-fill-dialog.d.ts.map +1 -0
  35. package/dist/components/ai-fill-dialog.jsx +105 -0
  36. package/dist/components/ai-fill-dialog.jsx.map +1 -0
  37. package/dist/components/chart-builder-dialog.d.ts +10 -0
  38. package/dist/components/chart-builder-dialog.d.ts.map +1 -0
  39. package/dist/components/chart-builder-dialog.jsx +433 -0
  40. package/dist/components/chart-builder-dialog.jsx.map +1 -0
  41. package/dist/components/chart-widget.d.ts +12 -0
  42. package/dist/components/chart-widget.d.ts.map +1 -0
  43. package/dist/components/chart-widget.jsx +365 -0
  44. package/dist/components/chart-widget.jsx.map +1 -0
  45. package/dist/components/global-search-dialog.d.ts +7 -0
  46. package/dist/components/global-search-dialog.d.ts.map +1 -0
  47. package/dist/components/global-search-dialog.jsx +187 -0
  48. package/dist/components/global-search-dialog.jsx.map +1 -0
  49. package/dist/components/group-settings-dialog.d.ts +13 -0
  50. package/dist/components/group-settings-dialog.d.ts.map +1 -0
  51. package/dist/components/group-settings-dialog.jsx +53 -0
  52. package/dist/components/group-settings-dialog.jsx.map +1 -0
  53. package/dist/components/move-chart-dialog.d.ts +18 -0
  54. package/dist/components/move-chart-dialog.d.ts.map +1 -0
  55. package/dist/components/move-chart-dialog.jsx +68 -0
  56. package/dist/components/move-chart-dialog.jsx.map +1 -0
  57. package/dist/components/reference-multi-table-dialog.d.ts +12 -0
  58. package/dist/components/reference-multi-table-dialog.d.ts.map +1 -0
  59. package/dist/components/reference-multi-table-dialog.jsx +126 -0
  60. package/dist/components/reference-multi-table-dialog.jsx.map +1 -0
  61. package/dist/components/related-records-tabs.d.ts +8 -0
  62. package/dist/components/related-records-tabs.d.ts.map +1 -0
  63. package/dist/components/related-records-tabs.jsx +75 -0
  64. package/dist/components/related-records-tabs.jsx.map +1 -0
  65. package/dist/components/revisions-button.d.ts +7 -0
  66. package/dist/components/revisions-button.d.ts.map +1 -0
  67. package/dist/components/revisions-button.jsx +152 -0
  68. package/dist/components/revisions-button.jsx.map +1 -0
  69. package/dist/components/wizard-form.d.ts +43 -0
  70. package/dist/components/wizard-form.d.ts.map +1 -0
  71. package/dist/components/wizard-form.jsx +136 -0
  72. package/dist/components/wizard-form.jsx.map +1 -0
  73. package/dist/dashboard/time-series.d.ts +20 -0
  74. package/dist/dashboard/time-series.d.ts.map +1 -0
  75. package/dist/dashboard/time-series.js +108 -0
  76. package/dist/dashboard/time-series.js.map +1 -0
  77. package/dist/dialogs.d.ts +35 -0
  78. package/dist/dialogs.d.ts.map +1 -0
  79. package/dist/dialogs.jsx +152 -0
  80. package/dist/dialogs.jsx.map +1 -0
  81. package/dist/export.d.ts +39 -0
  82. package/dist/export.d.ts.map +1 -0
  83. package/dist/export.js +114 -0
  84. package/dist/export.js.map +1 -0
  85. package/dist/extension-registry.d.ts +122 -0
  86. package/dist/extension-registry.d.ts.map +1 -0
  87. package/dist/extension-registry.js +93 -0
  88. package/dist/extension-registry.js.map +1 -0
  89. package/dist/header-controls.d.ts +4 -0
  90. package/dist/header-controls.d.ts.map +1 -0
  91. package/dist/header-controls.jsx +70 -0
  92. package/dist/header-controls.jsx.map +1 -0
  93. package/dist/hooks.d.ts +104 -0
  94. package/dist/hooks.d.ts.map +1 -0
  95. package/dist/hooks.js +374 -0
  96. package/dist/hooks.js.map +1 -0
  97. package/dist/hotkey-help.d.ts +3 -0
  98. package/dist/hotkey-help.d.ts.map +1 -0
  99. package/dist/hotkey-help.jsx +32 -0
  100. package/dist/hotkey-help.jsx.map +1 -0
  101. package/dist/hotkey-registry.d.ts +18 -0
  102. package/dist/hotkey-registry.d.ts.map +1 -0
  103. package/dist/hotkey-registry.jsx +34 -0
  104. package/dist/hotkey-registry.jsx.map +1 -0
  105. package/dist/i18n.d.ts +74 -0
  106. package/dist/i18n.d.ts.map +1 -0
  107. package/dist/i18n.jsx +127 -0
  108. package/dist/i18n.jsx.map +1 -0
  109. package/dist/index.d.ts +35 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +36 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/notify.d.ts +41 -0
  114. package/dist/notify.d.ts.map +1 -0
  115. package/dist/notify.jsx +58 -0
  116. package/dist/notify.jsx.map +1 -0
  117. package/dist/pages/ai-assistant-settings-section.d.ts +3 -0
  118. package/dist/pages/ai-assistant-settings-section.d.ts.map +1 -0
  119. package/dist/pages/ai-assistant-settings-section.jsx +126 -0
  120. package/dist/pages/ai-assistant-settings-section.jsx.map +1 -0
  121. package/dist/pages/audit-log-page.d.ts +3 -0
  122. package/dist/pages/audit-log-page.d.ts.map +1 -0
  123. package/dist/pages/audit-log-page.jsx +354 -0
  124. package/dist/pages/audit-log-page.jsx.map +1 -0
  125. package/dist/pages/edit-page.d.ts +7 -0
  126. package/dist/pages/edit-page.d.ts.map +1 -0
  127. package/dist/pages/edit-page.jsx +614 -0
  128. package/dist/pages/edit-page.jsx.map +1 -0
  129. package/dist/pages/export-dialog.d.ts +11 -0
  130. package/dist/pages/export-dialog.d.ts.map +1 -0
  131. package/dist/pages/export-dialog.jsx +102 -0
  132. package/dist/pages/export-dialog.jsx.map +1 -0
  133. package/dist/pages/home-page.d.ts +3 -0
  134. package/dist/pages/home-page.d.ts.map +1 -0
  135. package/dist/pages/home-page.jsx +211 -0
  136. package/dist/pages/home-page.jsx.map +1 -0
  137. package/dist/pages/list-page.d.ts +42 -0
  138. package/dist/pages/list-page.d.ts.map +1 -0
  139. package/dist/pages/list-page.jsx +1596 -0
  140. package/dist/pages/list-page.jsx.map +1 -0
  141. package/dist/pages/login-page.d.ts +11 -0
  142. package/dist/pages/login-page.d.ts.map +1 -0
  143. package/dist/pages/login-page.jsx +157 -0
  144. package/dist/pages/login-page.jsx.map +1 -0
  145. package/dist/pages/settings-page.d.ts +5 -0
  146. package/dist/pages/settings-page.d.ts.map +1 -0
  147. package/dist/pages/settings-page.jsx +787 -0
  148. package/dist/pages/settings-page.jsx.map +1 -0
  149. package/dist/pages/settings-shared.d.ts +51 -0
  150. package/dist/pages/settings-shared.d.ts.map +1 -0
  151. package/dist/pages/settings-shared.jsx +66 -0
  152. package/dist/pages/settings-shared.jsx.map +1 -0
  153. package/dist/pages/show-page.d.ts +7 -0
  154. package/dist/pages/show-page.d.ts.map +1 -0
  155. package/dist/pages/show-page.jsx +147 -0
  156. package/dist/pages/show-page.jsx.map +1 -0
  157. package/dist/pages/wizard-create-page.d.ts +14 -0
  158. package/dist/pages/wizard-create-page.d.ts.map +1 -0
  159. package/dist/pages/wizard-create-page.jsx +106 -0
  160. package/dist/pages/wizard-create-page.jsx.map +1 -0
  161. package/dist/property-renderer.d.ts +8 -0
  162. package/dist/property-renderer.d.ts.map +1 -0
  163. package/dist/property-renderer.jsx +690 -0
  164. package/dist/property-renderer.jsx.map +1 -0
  165. package/dist/provider.d.ts +20 -0
  166. package/dist/provider.d.ts.map +1 -0
  167. package/dist/provider.jsx +32 -0
  168. package/dist/provider.jsx.map +1 -0
  169. package/dist/realtime.d.ts +22 -0
  170. package/dist/realtime.d.ts.map +1 -0
  171. package/dist/realtime.js +38 -0
  172. package/dist/realtime.js.map +1 -0
  173. package/dist/reference.d.ts +52 -0
  174. package/dist/reference.d.ts.map +1 -0
  175. package/dist/reference.jsx +224 -0
  176. package/dist/reference.jsx.map +1 -0
  177. package/dist/relations.d.ts +11 -0
  178. package/dist/relations.d.ts.map +1 -0
  179. package/dist/relations.js +36 -0
  180. package/dist/relations.js.map +1 -0
  181. package/dist/router.d.ts +82 -0
  182. package/dist/router.d.ts.map +1 -0
  183. package/dist/router.jsx +187 -0
  184. package/dist/router.jsx.map +1 -0
  185. package/dist/show-when.d.ts +7 -0
  186. package/dist/show-when.d.ts.map +1 -0
  187. package/dist/show-when.js +77 -0
  188. package/dist/show-when.js.map +1 -0
  189. package/dist/types.d.ts +194 -0
  190. package/dist/types.d.ts.map +1 -0
  191. package/dist/types.js +18 -0
  192. package/dist/types.js.map +1 -0
  193. package/dist/use-dashboard-charts.d.ts +93 -0
  194. package/dist/use-dashboard-charts.d.ts.map +1 -0
  195. package/dist/use-dashboard-charts.js +263 -0
  196. package/dist/use-dashboard-charts.js.map +1 -0
  197. package/dist/use-hotkey.d.ts +17 -0
  198. package/dist/use-hotkey.d.ts.map +1 -0
  199. package/dist/use-hotkey.js +103 -0
  200. package/dist/use-hotkey.js.map +1 -0
  201. package/dist/user-directory.d.ts +18 -0
  202. package/dist/user-directory.d.ts.map +1 -0
  203. package/dist/user-directory.js +51 -0
  204. package/dist/user-directory.js.map +1 -0
  205. package/dist/validation.d.ts +22 -0
  206. package/dist/validation.d.ts.map +1 -0
  207. package/dist/validation.js +338 -0
  208. package/dist/validation.js.map +1 -0
  209. package/package.json +59 -0
  210. package/src/action-guard.ts +20 -0
  211. package/src/action-menu.tsx +161 -0
  212. package/src/admin-app.tsx +630 -0
  213. package/src/admin-router.tsx +273 -0
  214. package/src/breadcrumbs.tsx +75 -0
  215. package/src/client.ts +1093 -0
  216. package/src/component-loader.ts +33 -0
  217. package/src/components/ai-assistant-widget.tsx +565 -0
  218. package/src/components/ai-fill-dialog.tsx +143 -0
  219. package/src/components/chart-builder-dialog.tsx +618 -0
  220. package/src/components/chart-widget.tsx +654 -0
  221. package/src/components/global-search-dialog.tsx +272 -0
  222. package/src/components/group-settings-dialog.tsx +93 -0
  223. package/src/components/move-chart-dialog.tsx +130 -0
  224. package/src/components/reference-multi-table-dialog.tsx +196 -0
  225. package/src/components/related-records-tabs.tsx +130 -0
  226. package/src/components/revisions-button.tsx +237 -0
  227. package/src/components/wizard-form.tsx +302 -0
  228. package/src/dashboard/time-series.ts +125 -0
  229. package/src/dialogs.tsx +265 -0
  230. package/src/export.ts +140 -0
  231. package/src/extension-registry.ts +195 -0
  232. package/src/header-controls.tsx +125 -0
  233. package/src/hooks.ts +509 -0
  234. package/src/hotkey-help.tsx +56 -0
  235. package/src/hotkey-registry.tsx +60 -0
  236. package/src/i18n.tsx +267 -0
  237. package/src/index.ts +192 -0
  238. package/src/notify.tsx +94 -0
  239. package/src/pages/ai-assistant-settings-section.tsx +167 -0
  240. package/src/pages/audit-log-page.tsx +580 -0
  241. package/src/pages/edit-page.tsx +743 -0
  242. package/src/pages/export-dialog.tsx +154 -0
  243. package/src/pages/home-page.tsx +318 -0
  244. package/src/pages/list-page.tsx +2645 -0
  245. package/src/pages/login-page.tsx +242 -0
  246. package/src/pages/settings-page.tsx +1143 -0
  247. package/src/pages/settings-shared.tsx +134 -0
  248. package/src/pages/show-page.tsx +223 -0
  249. package/src/pages/wizard-create-page.tsx +164 -0
  250. package/src/property-renderer.tsx +1143 -0
  251. package/src/provider.tsx +70 -0
  252. package/src/realtime.ts +55 -0
  253. package/src/reference.tsx +386 -0
  254. package/src/relations.ts +55 -0
  255. package/src/router.tsx +211 -0
  256. package/src/show-when.ts +76 -0
  257. package/src/types.ts +198 -0
  258. package/src/use-dashboard-charts.ts +362 -0
  259. package/src/use-hotkey.ts +128 -0
  260. package/src/user-directory.ts +56 -0
  261. package/src/validation.ts +361 -0
@@ -0,0 +1,690 @@
1
+ // Property-type rendering: maps a PropertyJSON.type to display + form widgets.
2
+ // Custom components registered via ComponentLoader take precedence.
3
+ import * as React from 'react';
4
+ import { Button, Input, PasswordInput, Textarea, Badge, FileInput, MultiFileInput, Switch, DatePicker, JsonEditor, JsonView, KeyValueEditor, KeyValueView, MediaPreview, RichtextEditor, RichtextRender, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tooltip, TooltipContent, TooltipTrigger, } from '@modern-admin/ui';
5
+ import { Check, Copy } from 'lucide-react';
6
+ import { uuidv7 } from '@modern-admin/core';
7
+ import { useQueries } from '@tanstack/react-query';
8
+ import { getPropertyExtension } from './extension-registry.js';
9
+ import { useAdminContext, useAdminClient } from './provider.js';
10
+ import { useI18n } from './i18n.js';
11
+ import { useNotify } from './notify.js';
12
+ import { ReferenceCombobox, ReferenceLink, ReferenceLinkList, ReferenceMultiCombobox, } from './reference.js';
13
+ import { ReferenceMultiTableDialog } from './components/reference-multi-table-dialog.js';
14
+ import { useResource } from './hooks.js';
15
+ const formatDate = (value) => {
16
+ if (value == null)
17
+ return '';
18
+ if (value instanceof Date)
19
+ return value.toISOString().slice(0, 10);
20
+ const d = new Date(String(value));
21
+ if (Number.isNaN(d.getTime()))
22
+ return String(value);
23
+ return d.toISOString().slice(0, 10);
24
+ };
25
+ export const formatMoneyValue = (value, currency, locale) => {
26
+ const amount = typeof value === 'number' ? value : Number(value);
27
+ if (!Number.isFinite(amount))
28
+ return String(value ?? '');
29
+ try {
30
+ if (!currency) {
31
+ return new Intl.NumberFormat(locale, {
32
+ minimumFractionDigits: 2,
33
+ maximumFractionDigits: 2,
34
+ }).format(amount);
35
+ }
36
+ return new Intl.NumberFormat(locale, {
37
+ style: 'currency',
38
+ currency,
39
+ minimumFractionDigits: 2,
40
+ maximumFractionDigits: 2,
41
+ }).format(amount);
42
+ }
43
+ catch {
44
+ return amount.toFixed(2);
45
+ }
46
+ };
47
+ const normalizeHexColor = (value) => {
48
+ if (typeof value !== 'string')
49
+ return null;
50
+ const trimmed = value.trim();
51
+ return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed) ? trimmed : null;
52
+ };
53
+ function CopiableDisplay({ text, children, }) {
54
+ const { t } = useI18n();
55
+ const notify = useNotify();
56
+ const [copied, setCopied] = React.useState(false);
57
+ React.useEffect(() => {
58
+ if (!copied)
59
+ return;
60
+ const timer = window.setTimeout(() => setCopied(false), 3_000);
61
+ return () => window.clearTimeout(timer);
62
+ }, [copied]);
63
+ const onCopy = async () => {
64
+ try {
65
+ await navigator.clipboard.writeText(text);
66
+ setCopied(true);
67
+ }
68
+ catch {
69
+ notify.error({ key: 'settings:apiKeys.notice.copyFailed' });
70
+ }
71
+ };
72
+ return (<span className="inline-flex max-w-full items-center gap-2 align-middle">
73
+ <span className="min-w-0">{children}</span>
74
+ <Tooltip>
75
+ <TooltipTrigger asChild>
76
+ <Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={() => void onCopy()} aria-label={copied ? t('settings:apiKeys.created.copied') : t('settings:apiKeys.created.copy')}>
77
+ {copied ? <Check className="size-4 text-emerald-600"/> : <Copy className="size-4"/>}
78
+ </Button>
79
+ </TooltipTrigger>
80
+ <TooltipContent>
81
+ {copied ? t('settings:apiKeys.created.copied') : t('settings:apiKeys.created.copy')}
82
+ </TooltipContent>
83
+ </Tooltip>
84
+ </span>);
85
+ }
86
+ function ListCellText({ children }) {
87
+ return (<span className="max-w-full overflow-hidden break-words text-foreground" style={{
88
+ display: '-webkit-box',
89
+ WebkitBoxOrient: 'vertical',
90
+ WebkitLineClamp: 5,
91
+ whiteSpace: 'pre-wrap',
92
+ }}>
93
+ {children}
94
+ </span>);
95
+ }
96
+ export function PropertyDisplay({ property, value, view = 'list', populated }) {
97
+ const { components } = useAdminContext();
98
+ const { t, locale } = useI18n();
99
+ const copiable = view === 'show' && (property.isId === true || property.custom?.copiable === true);
100
+ const withCopy = (content) => copiable ? <CopiableDisplay text={String(value)}>{content}</CopiableDisplay> : content;
101
+ const componentName = property.components?.[view];
102
+ if (componentName && components?.has(componentName)) {
103
+ const Custom = components.get(componentName);
104
+ return <Custom property={property} value={value} view={view}/>;
105
+ }
106
+ if (value == null || value === '')
107
+ return <span className="text-muted-foreground">—</span>;
108
+ switch (property.type) {
109
+ case 'boolean':
110
+ return <Badge variant={value ? 'default' : 'outline'}>{value ? 'true' : 'false'}</Badge>;
111
+ case 'date':
112
+ case 'datetime':
113
+ return withCopy(view === 'list' ? <ListCellText>{formatDate(value)}</ListCellText> : <span>{formatDate(value)}</span>);
114
+ case 'money': {
115
+ const currency = typeof property.custom?.currency === 'string'
116
+ ? property.custom.currency
117
+ : undefined;
118
+ return withCopy(view === 'list'
119
+ ? <ListCellText>{formatMoneyValue(value, currency, locale)}</ListCellText>
120
+ : <span>{formatMoneyValue(value, currency, locale)}</span>);
121
+ }
122
+ case 'json':
123
+ case 'mixed':
124
+ case 'key-value':
125
+ if (property.keyValueFields?.length) {
126
+ return (<KeyValueView fields={property.keyValueFields} value={value} variant={view === 'list' ? 'inline' : 'block'} labels={{
127
+ emptyValue: '—',
128
+ trueLabel: t('common:yes'),
129
+ falseLabel: t('common:no'),
130
+ }}/>);
131
+ }
132
+ return <JsonView value={value} inline={view === 'list'}/>;
133
+ case 'reference':
134
+ if (property.reference) {
135
+ if (property.isArray) {
136
+ const ids = Array.isArray(value)
137
+ ? value
138
+ : [];
139
+ return (<ReferenceLinkList resourceId={property.reference} recordIds={ids} populated={populated} populatedKeyPrefix={property.path}/>);
140
+ }
141
+ const populatedRecord = populated?.[property.path];
142
+ return (<ReferenceLink resourceId={property.reference} recordId={value} showIcon={view === 'show'} populated={populatedRecord}/>);
143
+ }
144
+ return <Badge variant="secondary">{String(value)}</Badge>;
145
+ case 'm2m': {
146
+ const items = Array.isArray(value) ? value : [];
147
+ const m2m = property.custom?.m2m;
148
+ const reference = m2m?.reference ?? property.reference;
149
+ const ids = items.map((i) => String(i.id ?? ''));
150
+ if (!reference)
151
+ return <span className="text-muted-foreground">—</span>;
152
+ if (items.length === 0)
153
+ return <span className="text-muted-foreground">—</span>;
154
+ const extras = m2m?.extraFields ?? [];
155
+ if (view === 'list' || extras.length === 0) {
156
+ return (<ReferenceLinkList resourceId={reference} recordIds={ids} populated={populated} populatedKeyPrefix={property.path}/>);
157
+ }
158
+ return (<div className="space-y-1">
159
+ {items.map((it) => {
160
+ const populatedRef = populated?.[`${property.path}.${it.id}`];
161
+ return (<div key={String(it.id)} className="flex flex-wrap items-center gap-x-3 gap-y-1">
162
+ <ReferenceLink resourceId={reference} recordId={String(it.id)} populated={populatedRef}/>
163
+ {extras.map((f) => it[f] != null && it[f] !== '' ? (<span key={f} className="text-xs text-muted-foreground">
164
+ {f}:{' '}
165
+ <span className="text-foreground">{String(it[f])}</span>
166
+ </span>) : null)}
167
+ </div>);
168
+ })}
169
+ </div>);
170
+ }
171
+ case 'richtext':
172
+ if (view === 'show') {
173
+ return <RichtextRender value={String(value)} format="html"/>;
174
+ }
175
+ // List view: strip HTML tags for a compact preview.
176
+ return (<ListCellText>
177
+ {String(value).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()}
178
+ </ListCellText>);
179
+ case 'markdown':
180
+ if (view === 'show') {
181
+ return <RichtextRender value={String(value)} format="markdown"/>;
182
+ }
183
+ return (<ListCellText>
184
+ {String(value).replace(/[#>*_`~-]/g, '').replace(/\s+/g, ' ').trim()}
185
+ </ListCellText>);
186
+ case 'textarea':
187
+ return withCopy(view === 'show'
188
+ ? <span className="whitespace-pre-wrap text-foreground">{String(value)}</span>
189
+ : <ListCellText>{String(value)}</ListCellText>);
190
+ case 'color': {
191
+ const color = normalizeHexColor(value);
192
+ if (!color) {
193
+ return withCopy(view === 'list' ? <ListCellText>{String(value)}</ListCellText> : <span>{String(value)}</span>);
194
+ }
195
+ return withCopy(<span className="inline-flex items-center gap-2">
196
+ <span className="size-3 rounded-full border border-border" style={{ backgroundColor: color }}/>
197
+ <span>{color.toUpperCase()}</span>
198
+ </span>);
199
+ }
200
+ case 'previewMedia': {
201
+ const url = String(value);
202
+ const labels = {
203
+ preview: t('common:preview'),
204
+ download: t('common:download'),
205
+ openInNewTab: t('common:openInNewTab'),
206
+ title: property.label || t('common:preview'),
207
+ };
208
+ return (<MediaPreview url={url} labels={labels} showUrl={view === 'show'} triggerSize="sm" triggerVariant="outline"/>);
209
+ }
210
+ case 'file': {
211
+ const template = property.custom?.uploadUrlTemplate;
212
+ const renderOne = (rawKey, idx) => {
213
+ const url = template
214
+ ? template.replace('{key}', rawKey)
215
+ : rawKey.startsWith('http')
216
+ ? rawKey
217
+ : null;
218
+ const filename = rawKey.split('/').pop() ?? rawKey;
219
+ if (url) {
220
+ const labels = {
221
+ preview: t('common:preview'),
222
+ download: t('common:download'),
223
+ openInNewTab: t('common:openInNewTab'),
224
+ title: filename,
225
+ };
226
+ return (<MediaPreview key={idx ?? rawKey} url={url} downloadName={filename} labels={labels} showUrl={view === 'show'} triggerSize="sm" triggerVariant="outline"/>);
227
+ }
228
+ return (<span key={idx ?? rawKey} className="text-sm text-muted-foreground">
229
+ {filename}
230
+ </span>);
231
+ };
232
+ if (Array.isArray(value)) {
233
+ const arr = value;
234
+ if (arr.length === 0)
235
+ return <span className="text-muted-foreground">—</span>;
236
+ return (<div className="flex flex-wrap items-center gap-2">
237
+ {arr.map((v, i) => renderOne(String(v), i))}
238
+ </div>);
239
+ }
240
+ return renderOne(String(value));
241
+ }
242
+ default: {
243
+ // Check the extension registry for a custom type before falling back to plain text.
244
+ const ext = getPropertyExtension(property.type);
245
+ if (ext)
246
+ return <ext.display property={property} value={value} view={view} populated={populated}/>;
247
+ return withCopy(view === 'list' ? <ListCellText>{String(value)}</ListCellText> : <span>{String(value)}</span>);
248
+ }
249
+ }
250
+ }
251
+ /** Build the public URL for a stored key, using `{key}` substitution. */
252
+ const urlForKey = (key, template) => {
253
+ if (template)
254
+ return template.replace('{key}', key);
255
+ if (key.startsWith('http'))
256
+ return key;
257
+ return null;
258
+ };
259
+ const newPendingId = () => uuidv7();
260
+ function FilePropertyEditor({ property, value, onChange, disabled, resourceId, }) {
261
+ const client = useAdminClient();
262
+ const { t } = useI18n();
263
+ const isArray = Boolean(property.isArray);
264
+ const [pending, setPending] = React.useState([]);
265
+ const [uploadError, setUploadError] = React.useState(null);
266
+ // Map of key → freshly-uploaded URL (so we can render previews without
267
+ // waiting for the form to re-fetch), and the set of keys that were uploaded
268
+ // in this editing session and have not yet been "saved" by submitting the
269
+ // form. The latter is used to fire `cancelUpload` when the user removes a
270
+ // pending file before saving.
271
+ const [uploadedUrls, setUploadedUrls] = React.useState({});
272
+ const pendingKeysRef = React.useRef(new Set());
273
+ const template = property.custom?.uploadUrlTemplate;
274
+ const accept = property.custom?.uploadMimeTypes?.join(',') ?? undefined;
275
+ // Normalise current value into an array of keys for uniform handling.
276
+ const currentKeys = React.useMemo(() => {
277
+ if (isArray) {
278
+ return Array.isArray(value)
279
+ ? value.flatMap((v) => (v == null || v === '' ? [] : [String(v)]))
280
+ : [];
281
+ }
282
+ return value == null || value === '' ? [] : [String(value)];
283
+ }, [value, isArray]);
284
+ const currentKeysRef = React.useRef(currentKeys);
285
+ React.useEffect(() => {
286
+ currentKeysRef.current = currentKeys;
287
+ }, [currentKeys]);
288
+ const cancelIfPending = React.useCallback((key) => {
289
+ if (!resourceId)
290
+ return;
291
+ if (!pendingKeysRef.current.has(key))
292
+ return;
293
+ pendingKeysRef.current.delete(key);
294
+ void client.cancelUpload(resourceId, property.path, key).catch(() => {
295
+ // Best-effort — server-side TTL sweeper handles missed cancellations.
296
+ });
297
+ }, [client, resourceId, property.path]);
298
+ const startUploads = async (files) => {
299
+ if (!resourceId) {
300
+ setUploadError('resourceId is required for file upload');
301
+ return;
302
+ }
303
+ setUploadError(null);
304
+ // For single-value fields, only the first file matters; the rest are
305
+ // dropped before they ever hit the network.
306
+ const accepted = isArray ? files : files.slice(0, 1);
307
+ if (accepted.length === 0)
308
+ return;
309
+ // Pre-allocate one pending row per file; the index correlates with the
310
+ // upload index used by per-item callbacks.
311
+ const ids = accepted.map(() => newPendingId());
312
+ setPending((prev) => [
313
+ ...prev,
314
+ ...accepted.map((f, i) => ({
315
+ id: ids[i],
316
+ name: f.name,
317
+ progress: 0,
318
+ status: 'queued',
319
+ })),
320
+ ]);
321
+ await client.uploadFiles(resourceId, property.path, accepted, {
322
+ concurrency: 3,
323
+ onItemStart: (i) => {
324
+ setPending((prev) => prev.map((p) => (p.id === ids[i] ? { ...p, status: 'uploading' } : p)));
325
+ },
326
+ onItemProgress: (i, _f, p) => {
327
+ setPending((prev) => prev.map((row) => (row.id === ids[i] ? { ...row, progress: p.percent } : row)));
328
+ },
329
+ onItemComplete: (i, _f, info) => {
330
+ setPending((prev) => prev.filter((p) => p.id !== ids[i]));
331
+ setUploadedUrls((u) => ({ ...u, [info.key]: info.url }));
332
+ pendingKeysRef.current.add(info.key);
333
+ if (isArray) {
334
+ const next = [...currentKeysRef.current, info.key];
335
+ currentKeysRef.current = next;
336
+ onChange(next);
337
+ }
338
+ else {
339
+ // Single value: cancel any previously-staged key being replaced.
340
+ for (const old of currentKeysRef.current)
341
+ cancelIfPending(old);
342
+ currentKeysRef.current = [info.key];
343
+ onChange(info.key);
344
+ }
345
+ },
346
+ onItemError: (i, _f, err) => {
347
+ setPending((prev) => prev.map((p) => p.id === ids[i] ? { ...p, status: 'error', error: err.message } : p));
348
+ },
349
+ });
350
+ };
351
+ const dismissPending = (id) => {
352
+ setPending((prev) => prev.filter((p) => p.id !== id));
353
+ };
354
+ const removeAt = (index) => {
355
+ const key = currentKeys[index];
356
+ if (key)
357
+ cancelIfPending(key);
358
+ if (isArray) {
359
+ const next = currentKeys.filter((_, i) => i !== index);
360
+ onChange(next);
361
+ }
362
+ else {
363
+ onChange(null);
364
+ }
365
+ setUploadError(null);
366
+ };
367
+ const stillUploading = pending.some((p) => p.status !== 'error');
368
+ if (isArray) {
369
+ const items = currentKeys.map((key) => ({
370
+ value: key,
371
+ previewUrl: uploadedUrls[key] ?? urlForKey(key, template),
372
+ }));
373
+ const pendingItems = pending.map((p) => ({
374
+ id: p.id,
375
+ name: p.name,
376
+ progress: p.status === 'error' ? undefined : p.progress,
377
+ status: p.status,
378
+ error: p.error,
379
+ }));
380
+ return (<MultiFileInput items={items} pendingItems={pendingItems} accept={accept} error={uploadError ?? undefined} disabled={disabled} labels={{
381
+ chooseFiles: t('common:chooseFiles'),
382
+ dragAndDrop: t('common:dragAndDrop'),
383
+ chooseLink: t('common:chooseAFile'),
384
+ addMoreLink: t('common:addMoreFiles'),
385
+ uploading: t('common:uploading'),
386
+ removeFile: t('common:removeFile'),
387
+ uploadFailed: t('common:uploadFailed'),
388
+ dismiss: t('common:dismiss'),
389
+ }} onFilesSelect={(files) => {
390
+ void startUploads(files);
391
+ }} onRemove={removeAt} onPendingDismiss={dismissPending}/>);
392
+ }
393
+ const storedKey = currentKeys[0] ?? null;
394
+ const previewUrl = storedKey ? (uploadedUrls[storedKey] ?? urlForKey(storedKey, template)) : null;
395
+ // For single-value fields we surface the latest in-flight upload's progress
396
+ // through the simple FileInput's `uploading` flag. The detailed progress UI
397
+ // lives in MultiFileInput.
398
+ const activePending = pending.find((p) => p.status === 'uploading') ?? pending.find((p) => p.status === 'queued');
399
+ const erroredPending = pending.find((p) => p.status === 'error');
400
+ return (<FileInput value={storedKey} previewUrl={previewUrl} accept={accept} uploading={stillUploading} uploadProgress={activePending?.progress} uploadingName={activePending?.name} error={uploadError ?? erroredPending?.error ?? undefined} disabled={disabled} labels={{
401
+ chooseFile: t('common:chooseFile'),
402
+ dragAndDrop: t('common:dragAndDrop'),
403
+ chooseAFile: t('common:chooseAFile'),
404
+ uploading: t('common:uploading'),
405
+ uploadingFile: t('common:uploadingFile'),
406
+ removeFile: t('common:removeFile'),
407
+ }} onFileSelect={(f) => {
408
+ void startUploads([f]);
409
+ }} onRemove={() => removeAt(0)}/>);
410
+ }
411
+ /**
412
+ * Editor for many-to-many properties registered by `m2mFeature`. Wraps the
413
+ * existing `ReferenceMultiCombobox` for picking referenced records, then
414
+ * (when the relation has extra junction columns) renders a per-item row of
415
+ * nested `PropertyEditor`s — one per extra field, typed from the junction
416
+ * resource's own property declarations.
417
+ */
418
+ function M2MPropertyEditor({ property, value, onChange, disabled, }) {
419
+ const m2m = property.custom?.m2m;
420
+ const junction = useResource(m2m?.through);
421
+ if (!m2m?.reference)
422
+ return <span className="text-muted-foreground">—</span>;
423
+ const items = Array.isArray(value)
424
+ ? value.flatMap((entry) => {
425
+ if (entry == null)
426
+ return [];
427
+ if (typeof entry === 'string' || typeof entry === 'number') {
428
+ return [{ id: String(entry) }];
429
+ }
430
+ if (typeof entry === 'object' && entry.id != null) {
431
+ return [{ ...entry, id: String(entry.id) }];
432
+ }
433
+ return [];
434
+ })
435
+ : [];
436
+ const ids = items.map((i) => String(i.id));
437
+ const extras = m2m.extraFields ?? [];
438
+ const setIds = (nextIds) => {
439
+ const byId = new Map(items.map((i) => [String(i.id), i]));
440
+ const next = nextIds.map((rawId) => {
441
+ const id = String(rawId);
442
+ return byId.get(id) ?? { id };
443
+ });
444
+ onChange(next);
445
+ };
446
+ const updateItem = (id, field, val) => {
447
+ onChange(items.map((it) => (String(it.id) === id ? { ...it, [field]: val } : it)));
448
+ };
449
+ // m2m relations are typically large tables, so default to the table-driven
450
+ // dialog picker. Opt back into the combobox via `m2m.picker = 'combobox'`.
451
+ const Picker = m2m?.picker === 'combobox'
452
+ ? ReferenceMultiCombobox
453
+ : ReferenceMultiTableDialog;
454
+ return (<div className="space-y-3">
455
+ <Picker referenceResourceId={m2m.reference} value={ids} onChange={setIds} disabled={disabled}/>
456
+ {extras.length > 0 && items.length > 0 ? (<div className="space-y-1.5">
457
+ {items.map((item) => (<div key={item.id} className="rounded-md border border-border bg-muted/30 px-2.5 py-2">
458
+ {/* Mobile: stack reference link above the extras row.
459
+ ≥sm: reference link gets a fixed-width slot on the left,
460
+ extras flow inline on the right so we don't waste space. */}
461
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
462
+ <div className="min-w-0 sm:w-32 sm:shrink-0 sm:pt-1.5">
463
+ <ReferenceLink resourceId={m2m.reference} recordId={item.id} showIcon/>
464
+ </div>
465
+ <div className={'grid min-w-0 flex-1 gap-2 ' +
466
+ (extras.length > 1 ? 'sm:grid-cols-2' : '')}>
467
+ {extras.map((f) => {
468
+ const junctionProp = junction?.properties.find((p) => p.path === f);
469
+ const synthetic = junctionProp ?? {
470
+ path: f,
471
+ label: f,
472
+ type: 'string',
473
+ isId: false,
474
+ isSortable: false,
475
+ isRequired: false,
476
+ isDisabled: false,
477
+ isArray: false,
478
+ reference: null,
479
+ availableValues: null,
480
+ components: {},
481
+ visibility: { list: false, show: true, edit: true, filter: false },
482
+ position: 1,
483
+ custom: {},
484
+ };
485
+ return (<div key={f} className="flex items-center gap-2">
486
+ <label className="w-16 shrink-0 text-xs font-medium text-muted-foreground sm:w-auto">
487
+ {synthetic.label}
488
+ </label>
489
+ <div className="min-w-0 flex-1">
490
+ <PropertyEditor property={synthetic} value={item[f]} onChange={(v) => updateItem(String(item.id), f, v)} disabled={disabled}/>
491
+ </div>
492
+ </div>);
493
+ })}
494
+ </div>
495
+ </div>
496
+ </div>))}
497
+ </div>) : null}
498
+ </div>);
499
+ }
500
+ // ─── KeyValue editor wrapper that resolves DB-bound autocomplete sources ────
501
+ /**
502
+ * Loads dynamic autocomplete suggestions for `keyValueFields[i].type ===
503
+ * 'autocomplete'` fields that declare a `suggestionsResource` +
504
+ * `suggestionsField` binding, then forwards everything to KeyValueEditor.
505
+ *
506
+ * Static suggestions (declared via `availableValues` on the field) are
507
+ * already supported inside the editor itself — this wrapper only handles
508
+ * the network-fetch side, so the UI primitive stays i18n- and
509
+ * client-unaware.
510
+ */
511
+ function KeyValueEditorWithSuggestions({ fields, value, onChange, disabled, }) {
512
+ const client = useAdminClient();
513
+ const { t } = useI18n();
514
+ // Identify just the fields that need a network fetch. The order is
515
+ // stable across renders (driven by the `fields` prop array) so the
516
+ // `useQueries` array length is stable too.
517
+ const dynamic = React.useMemo(() => fields.filter((f) => f.type === 'autocomplete' &&
518
+ !!f.suggestionsResource &&
519
+ !!f.suggestionsField), [fields]);
520
+ const queries = useQueries({
521
+ queries: dynamic.map((f) => ({
522
+ queryKey: [
523
+ 'modern-admin',
524
+ 'fieldSuggestions',
525
+ f.suggestionsResource,
526
+ f.suggestionsField,
527
+ 200,
528
+ ],
529
+ queryFn: async () => {
530
+ const res = await client.list(f.suggestionsResource, { perPage: 200 });
531
+ const seen = new Set();
532
+ const out = [];
533
+ for (const r of res.records) {
534
+ const raw = r.params?.[f.suggestionsField];
535
+ if (raw == null || raw === '')
536
+ continue;
537
+ const v = String(raw);
538
+ if (seen.has(v))
539
+ continue;
540
+ seen.add(v);
541
+ out.push(v);
542
+ }
543
+ out.sort((a, b) => a.localeCompare(b));
544
+ return out;
545
+ },
546
+ staleTime: 60_000,
547
+ })),
548
+ });
549
+ const suggestionsByKey = {};
550
+ const suggestionsLoadingByKey = {};
551
+ dynamic.forEach((f, i) => {
552
+ suggestionsByKey[f.key] = queries[i]?.data ?? [];
553
+ suggestionsLoadingByKey[f.key] = queries[i]?.isLoading ?? false;
554
+ });
555
+ return (<KeyValueEditor fields={fields} value={value} onChange={onChange} disabled={disabled} suggestionsByKey={suggestionsByKey} suggestionsLoadingByKey={suggestionsLoadingByKey} labels={{
556
+ combobox: {
557
+ loading: t('common:loading'),
558
+ // KeyValueEditor's combobox label inherits the field label; this
559
+ // is the empty-state message inside the dropdown.
560
+ noMatches: t('keyValue:noMatches'),
561
+ },
562
+ }}/>);
563
+ }
564
+ // ─── Generic property editor ──────────────────────────────────────────────────
565
+ export function PropertyEditor({ property, value, onChange, disabled, resourceId, }) {
566
+ const { components } = useAdminContext();
567
+ const { t } = useI18n();
568
+ const componentName = property.components?.edit;
569
+ if (componentName && components?.has(componentName)) {
570
+ const Custom = components.get(componentName);
571
+ return <Custom property={property} value={value} onChange={onChange} disabled={disabled}/>;
572
+ }
573
+ const stringValue = value == null ? '' : String(value);
574
+ if (property.type === 'm2m') {
575
+ return (<M2MPropertyEditor property={property} value={value} onChange={onChange} disabled={disabled} resourceId={resourceId}/>);
576
+ }
577
+ if (property.reference) {
578
+ if (property.isArray) {
579
+ const arr = Array.isArray(value)
580
+ ? value
581
+ : [];
582
+ // Opt into the table-driven dialog picker via `custom.picker = 'dialog'`;
583
+ // default stays as the compact combobox for plain reference arrays.
584
+ const pickerKind = property.custom?.picker;
585
+ const ArrayPicker = pickerKind === 'dialog' ? ReferenceMultiTableDialog : ReferenceMultiCombobox;
586
+ return (<ArrayPicker referenceResourceId={property.reference} value={arr} onChange={(next) => onChange(next)} disabled={disabled}/>);
587
+ }
588
+ return (<ReferenceCombobox referenceResourceId={property.reference} value={value} onChange={(next) => onChange(next)} disabled={disabled}/>);
589
+ }
590
+ if (property.availableValues?.length) {
591
+ return (<Select value={stringValue} onValueChange={(v) => onChange(v === '_empty_' ? '' : v)} disabled={disabled}>
592
+ <SelectTrigger>
593
+ <SelectValue placeholder="—"/>
594
+ </SelectTrigger>
595
+ <SelectContent>
596
+ <SelectItem value="_empty_">—</SelectItem>
597
+ {property.availableValues.map((opt) => (<SelectItem key={opt.value} value={opt.value}>
598
+ {opt.label}
599
+ </SelectItem>))}
600
+ </SelectContent>
601
+ </Select>);
602
+ }
603
+ switch (property.type) {
604
+ case 'boolean':
605
+ return (<Switch checked={Boolean(value)} onCheckedChange={(v) => onChange(Boolean(v))} disabled={disabled}/>);
606
+ case 'json':
607
+ case 'mixed':
608
+ case 'key-value':
609
+ if (property.keyValueFields?.length) {
610
+ return (<KeyValueEditorWithSuggestions fields={property.keyValueFields} value={value} onChange={(next) => onChange(next)} disabled={disabled}/>);
611
+ }
612
+ return (<JsonEditor value={value} onChange={onChange} disabled={disabled} formatLabel={t('common:format')} invalidLabel={t('common:invalidJson')}/>);
613
+ case 'number':
614
+ case 'float':
615
+ case 'currency':
616
+ case 'money':
617
+ return (<Input type="number" inputMode="decimal" step="0.01" value={stringValue} onChange={(e) => onChange(e.target.value === '' ? null : Number(e.target.value))} disabled={disabled}/>);
618
+ case 'date':
619
+ return (<DatePicker mode="date" value={value == null ? '' : String(value)} onChange={(v) => onChange(v === '' ? null : v)} disabled={disabled} ariaLabel={property.label} openCalendarLabel={t('common:openCalendar')} timeLabel={t('common:time')}/>);
620
+ case 'datetime':
621
+ case 'datetime-local':
622
+ return (<DatePicker mode="datetime" value={value == null ? '' : String(value)} onChange={(v) => onChange(v === '' ? null : v)} disabled={disabled} ariaLabel={property.label} openCalendarLabel={t('common:openCalendar')} timeLabel={t('common:time')}/>);
623
+ case 'richtext':
624
+ return (<RichtextEditor value={stringValue} onChange={(v) => onChange(v)} format="html" disabled={disabled} ariaLabelledBy={property.label} labels={{
625
+ bold: t('richtext:bold'),
626
+ italic: t('richtext:italic'),
627
+ strikethrough: t('richtext:strikethrough'),
628
+ inlineCode: t('richtext:inlineCode'),
629
+ heading: t('richtext:heading'),
630
+ bulletList: t('richtext:bulletList'),
631
+ numberedList: t('richtext:numberedList'),
632
+ blockquote: t('richtext:blockquote'),
633
+ horizontalRule: t('richtext:horizontalRule'),
634
+ insertLink: t('richtext:insertLink'),
635
+ undo: t('richtext:undo'),
636
+ redo: t('richtext:redo'),
637
+ source: t('richtext:source'),
638
+ splitView: t('richtext:splitView'),
639
+ visualEditor: t('richtext:visualEditor'),
640
+ fullscreen: t('richtext:fullscreen'),
641
+ exitFullscreen: t('richtext:exitFullscreen'),
642
+ urlPrompt: t('richtext:urlPrompt'),
643
+ }}/>);
644
+ case 'markdown':
645
+ return (<RichtextEditor value={stringValue} onChange={(v) => onChange(v)} format="markdown" disabled={disabled} ariaLabelledBy={property.label} labels={{
646
+ bold: t('richtext:bold'),
647
+ italic: t('richtext:italic'),
648
+ strikethrough: t('richtext:strikethrough'),
649
+ inlineCode: t('richtext:inlineCode'),
650
+ heading: t('richtext:heading'),
651
+ bulletList: t('richtext:bulletList'),
652
+ numberedList: t('richtext:numberedList'),
653
+ blockquote: t('richtext:blockquote'),
654
+ horizontalRule: t('richtext:horizontalRule'),
655
+ insertLink: t('richtext:insertLink'),
656
+ undo: t('richtext:undo'),
657
+ redo: t('richtext:redo'),
658
+ source: t('richtext:source'),
659
+ splitView: t('richtext:splitView'),
660
+ visualEditor: t('richtext:visualEditor'),
661
+ fullscreen: t('richtext:fullscreen'),
662
+ exitFullscreen: t('richtext:exitFullscreen'),
663
+ urlPrompt: t('richtext:urlPrompt'),
664
+ }}/>);
665
+ case 'textarea':
666
+ return (<Textarea value={stringValue} onChange={(e) => onChange(e.target.value)} disabled={disabled} rows={5}/>);
667
+ case 'password':
668
+ return (<PasswordInput value={stringValue} onChange={(e) => onChange(e.target.value)} disabled={disabled} toggleLabel={{
669
+ show: t('common:showPassword'),
670
+ hide: t('common:hidePassword'),
671
+ }}/>);
672
+ case 'file':
673
+ return (<FilePropertyEditor property={property} value={value} onChange={onChange} disabled={disabled} resourceId={resourceId}/>);
674
+ case 'previewMedia':
675
+ return (<Input type="url" inputMode="url" placeholder="https://…" value={stringValue} onChange={(e) => onChange(e.target.value)} disabled={disabled}/>);
676
+ case 'color':
677
+ return (<div className="flex items-center gap-3">
678
+ <Input type="color" className="h-10 w-14 rounded-md p-1" value={normalizeHexColor(value) ?? '#000000'} onChange={(e) => onChange(e.target.value)} disabled={disabled}/>
679
+ <Input value={stringValue} placeholder="#000000" onChange={(e) => onChange(e.target.value)} disabled={disabled}/>
680
+ </div>);
681
+ default: {
682
+ // Check the extension registry for a custom type before falling back to a plain text input.
683
+ const ext = getPropertyExtension(property.type);
684
+ if (ext)
685
+ return <ext.editor property={property} value={value} onChange={onChange} disabled={disabled} resourceId={resourceId}/>;
686
+ return (<Input value={stringValue} onChange={(e) => onChange(e.target.value)} disabled={disabled}/>);
687
+ }
688
+ }
689
+ }
690
+ //# sourceMappingURL=property-renderer.jsx.map