@pdfme/ui 0.0.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 (97) hide show
  1. package/README.md +9 -0
  2. package/__mocks__/assetsTransformer.js +7 -0
  3. package/__mocks__/form-render.js +7 -0
  4. package/__mocks__/lucide-react.js +19 -0
  5. package/dist/index.es.js +159393 -0
  6. package/dist/index.umd.js +1041 -0
  7. package/dist/types/__tests__/assets/helper.d.ts +3 -0
  8. package/dist/types/__tests__/components/Designer.test.d.ts +1 -0
  9. package/dist/types/__tests__/components/PluginIcon.test.d.ts +1 -0
  10. package/dist/types/__tests__/components/Preview.test.d.ts +1 -0
  11. package/dist/types/__tests__/helper.test.d.ts +1 -0
  12. package/dist/types/src/Designer.d.ts +21 -0
  13. package/dist/types/src/Form.d.ts +24 -0
  14. package/dist/types/src/Viewer.d.ts +15 -0
  15. package/dist/types/src/class.d.ts +89 -0
  16. package/dist/types/src/components/AppContextProvider.d.ts +11 -0
  17. package/dist/types/src/components/CtlBar.d.ts +14 -0
  18. package/dist/types/src/components/Designer/Canvas/Guides.d.ts +9 -0
  19. package/dist/types/src/components/Designer/Canvas/Mask.d.ts +4 -0
  20. package/dist/types/src/components/Designer/Canvas/Moveable.d.ts +37 -0
  21. package/dist/types/src/components/Designer/Canvas/Padding.d.ts +6 -0
  22. package/dist/types/src/components/Designer/Canvas/Selecto.d.ts +10 -0
  23. package/dist/types/src/components/Designer/Canvas/index.d.ts +22 -0
  24. package/dist/types/src/components/Designer/LeftSidebar.d.ts +8 -0
  25. package/dist/types/src/components/Designer/PluginIcon.d.ts +10 -0
  26. package/dist/types/src/components/Designer/RightSidebar/DetailView/AlignWidget.d.ts +4 -0
  27. package/dist/types/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.d.ts +4 -0
  28. package/dist/types/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.d.ts +7 -0
  29. package/dist/types/src/components/Designer/RightSidebar/DetailView/index.d.ts +8 -0
  30. package/dist/types/src/components/Designer/RightSidebar/ListView/Item.d.ts +45 -0
  31. package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.d.ts +4 -0
  32. package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.d.ts +14 -0
  33. package/dist/types/src/components/Designer/RightSidebar/ListView/index.d.ts +4 -0
  34. package/dist/types/src/components/Designer/RightSidebar/index.d.ts +4 -0
  35. package/dist/types/src/components/Designer/RightSidebar/layout.d.ts +15 -0
  36. package/dist/types/src/components/Designer/index.d.ts +11 -0
  37. package/dist/types/src/components/ErrorScreen.d.ts +7 -0
  38. package/dist/types/src/components/Paper.d.ts +20 -0
  39. package/dist/types/src/components/Preview.d.ts +15 -0
  40. package/dist/types/src/components/Renderer.d.ts +13 -0
  41. package/dist/types/src/components/Root.d.ts +9 -0
  42. package/dist/types/src/components/Spinner.d.ts +3 -0
  43. package/dist/types/src/components/StaticSchema.d.ts +10 -0
  44. package/dist/types/src/components/UnitPager.d.ts +10 -0
  45. package/dist/types/src/constants.d.ts +11 -0
  46. package/dist/types/src/contexts.d.ts +10 -0
  47. package/dist/types/src/helper.d.ts +73 -0
  48. package/dist/types/src/hooks.d.ts +46 -0
  49. package/dist/types/src/i18n.d.ts +3 -0
  50. package/dist/types/src/index.d.ts +4 -0
  51. package/dist/types/src/theme.d.ts +2 -0
  52. package/dist/types/src/types.d.ts +19 -0
  53. package/eslint.config.mjs +41 -0
  54. package/package.json +127 -0
  55. package/src/Designer.tsx +107 -0
  56. package/src/Form.tsx +102 -0
  57. package/src/Viewer.tsx +59 -0
  58. package/src/class.ts +188 -0
  59. package/src/components/AppContextProvider.tsx +78 -0
  60. package/src/components/CtlBar.tsx +183 -0
  61. package/src/components/Designer/Canvas/Guides.tsx +49 -0
  62. package/src/components/Designer/Canvas/Mask.tsx +20 -0
  63. package/src/components/Designer/Canvas/Moveable.tsx +91 -0
  64. package/src/components/Designer/Canvas/Padding.tsx +56 -0
  65. package/src/components/Designer/Canvas/Selecto.tsx +45 -0
  66. package/src/components/Designer/Canvas/index.tsx +536 -0
  67. package/src/components/Designer/LeftSidebar.tsx +120 -0
  68. package/src/components/Designer/PluginIcon.tsx +87 -0
  69. package/src/components/Designer/RightSidebar/DetailView/AlignWidget.tsx +229 -0
  70. package/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.tsx +78 -0
  71. package/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.tsx +28 -0
  72. package/src/components/Designer/RightSidebar/DetailView/index.tsx +469 -0
  73. package/src/components/Designer/RightSidebar/ListView/Item.tsx +158 -0
  74. package/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.tsx +204 -0
  75. package/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.tsx +88 -0
  76. package/src/components/Designer/RightSidebar/ListView/index.tsx +116 -0
  77. package/src/components/Designer/RightSidebar/index.tsx +72 -0
  78. package/src/components/Designer/RightSidebar/layout.tsx +75 -0
  79. package/src/components/Designer/index.tsx +389 -0
  80. package/src/components/ErrorScreen.tsx +33 -0
  81. package/src/components/Paper.tsx +117 -0
  82. package/src/components/Preview.tsx +220 -0
  83. package/src/components/Renderer.tsx +165 -0
  84. package/src/components/Root.tsx +38 -0
  85. package/src/components/Spinner.tsx +45 -0
  86. package/src/components/StaticSchema.tsx +50 -0
  87. package/src/components/UnitPager.tsx +119 -0
  88. package/src/constants.ts +21 -0
  89. package/src/contexts.ts +14 -0
  90. package/src/helper.ts +534 -0
  91. package/src/hooks.ts +308 -0
  92. package/src/i18n.ts +903 -0
  93. package/src/index.ts +5 -0
  94. package/src/theme.ts +20 -0
  95. package/src/types.ts +20 -0
  96. package/tsconfig.json +48 -0
  97. package/vite.config.mts +22 -0
@@ -0,0 +1,469 @@
1
+ import { useForm } from 'form-render';
2
+ import React, { useRef, useContext, useState, useEffect, useCallback } from 'react';
3
+ import type {
4
+ Dict,
5
+ ChangeSchemaItem,
6
+ SchemaForUI,
7
+ PropPanelWidgetProps,
8
+ PropPanelSchema,
9
+ Schema,
10
+ } from '@pdfme/common';
11
+ import { isBlankPdf } from '@pdfme/common';
12
+ import type { SidebarProps } from '../../../../types.js';
13
+ import { Menu } from 'lucide-react';
14
+ import { I18nContext, PluginsRegistry, OptionsContext } from '../../../../contexts.js';
15
+ import { debounce } from '../../../../helper.js';
16
+ import { DESIGNER_CLASSNAME } from '../../../../constants.js';
17
+ import { theme, Typography, Button, Divider } from 'antd';
18
+ import AlignWidget from './AlignWidget.js';
19
+ import WidgetRenderer from './WidgetRenderer.js';
20
+ import ButtonGroupWidget from './ButtonGroupWidget.js';
21
+ import { InternalNamePath, ValidateErrorEntity } from 'rc-field-form/es/interface.js';
22
+ import { SidebarBody, SidebarFrame, SidebarHeader, SIDEBAR_H_PADDING_PX } from '../layout.js';
23
+
24
+ // Import FormRender as a default import
25
+ import FormRenderComponent from 'form-render';
26
+
27
+ const { Text } = Typography;
28
+
29
+ type DetailViewProps = Pick<
30
+ SidebarProps,
31
+ | 'size'
32
+ | 'schemas'
33
+ | 'schemasList'
34
+ | 'pageSize'
35
+ | 'basePdf'
36
+ | 'changeSchemas'
37
+ | 'activeElements'
38
+ | 'deselectSchema'
39
+ > & {
40
+ activeSchema: SchemaForUI;
41
+ };
42
+
43
+ const DetailView = (props: DetailViewProps) => {
44
+ const { token } = theme.useToken();
45
+
46
+ const { schemasList, changeSchemas, deselectSchema, activeSchema, pageSize, basePdf } = props;
47
+ const form = useForm();
48
+
49
+ const i18n = useContext(I18nContext);
50
+ const pluginsRegistry = useContext(PluginsRegistry);
51
+ const options = useContext(OptionsContext);
52
+
53
+ // Define a type-safe i18n function that accepts string keys
54
+ const typedI18n = useCallback(
55
+ (key: string): string => {
56
+ // Use a type assertion to handle the union type constraint
57
+ return typeof i18n === 'function' ? i18n(key as keyof Dict) : key;
58
+ },
59
+ [i18n],
60
+ );
61
+
62
+ const [widgets, setWidgets] = useState<{
63
+ [key: string]: (props: PropPanelWidgetProps) => React.JSX.Element;
64
+ }>({});
65
+
66
+ useEffect(() => {
67
+ const newWidgets: typeof widgets = {
68
+ AlignWidget: (p) => <AlignWidget {...p} {...props} options={options} />,
69
+ Divider: () => (
70
+ <Divider style={{ marginTop: token.marginXS, marginBottom: token.marginXS }} />
71
+ ),
72
+ ButtonGroup: (p) => <ButtonGroupWidget {...p} {...props} options={options} />,
73
+ };
74
+ for (const plugin of pluginsRegistry.values()) {
75
+ const widgets = plugin.propPanel.widgets || {};
76
+ Object.entries(widgets).forEach(([widgetKey, widgetValue]) => {
77
+ newWidgets[widgetKey] = (p) => (
78
+ <WidgetRenderer
79
+ {...p}
80
+ {...props}
81
+ options={options}
82
+ theme={token}
83
+ i18n={typedI18n}
84
+ widget={widgetValue}
85
+ />
86
+ );
87
+ });
88
+ }
89
+ setWidgets(newWidgets);
90
+ }, [activeSchema, pluginsRegistry, JSON.stringify(options)]);
91
+
92
+ useEffect(() => form.resetFields(), [activeSchema.id]);
93
+
94
+ useEffect(() => {
95
+ // Create a type-safe copy of the schema with editable property
96
+ const values: Record<string, unknown> = { ...activeSchema };
97
+ // Safely access and set properties
98
+ const readOnly = typeof values.readOnly === 'boolean' ? values.readOnly : false;
99
+ values.editable = !readOnly;
100
+ form.setValues(values);
101
+ }, [activeSchema]);
102
+
103
+ useEffect(() => {
104
+ uniqueSchemaName.current = (value: string): boolean => {
105
+ for (const page of schemasList) {
106
+ for (const s of Object.values(page)) {
107
+ if (s.name === value && s.id !== activeSchema.id) {
108
+ return false;
109
+ }
110
+ }
111
+ }
112
+ return true;
113
+ };
114
+ }, [schemasList, activeSchema]);
115
+
116
+ // Reference to a function that validates schema name uniqueness
117
+ const uniqueSchemaName = useRef(
118
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
+ (_unused: string): boolean => true,
120
+ );
121
+
122
+ // Use proper type for validator function parameter
123
+ const validateUniqueSchemaName = (_: unknown, value: string): boolean =>
124
+ uniqueSchemaName.current(value);
125
+
126
+ // Calculate padding values once
127
+ const [paddingTop, paddingRight, paddingBottom, paddingLeft] = isBlankPdf(basePdf)
128
+ ? basePdf.padding
129
+ : [0, 0, 0, 0];
130
+
131
+ // Cross-field validation: only checks when both fields are individually valid
132
+ const validatePosition = (_: unknown, value: number, fieldName: string): boolean => {
133
+ const formValues = form.getValues() as Record<string, unknown>;
134
+ const position = formValues.position as { x: number; y: number } | undefined;
135
+ const width = formValues.width as number | undefined;
136
+ const height = formValues.height as number | undefined;
137
+
138
+ if (!position || width === undefined || height === undefined) return true;
139
+
140
+ if (fieldName === 'x') {
141
+ if (value < paddingLeft || value > pageSize.width - paddingRight) return true;
142
+ if (width > 0 && value + width > pageSize.width - paddingRight) return false;
143
+ } else if (fieldName === 'y') {
144
+ if (value < paddingTop || value > pageSize.height - paddingBottom) return true;
145
+ if (height > 0 && value + height > pageSize.height - paddingBottom) return false;
146
+ } else if (fieldName === 'width') {
147
+ if (position.x < paddingLeft || position.x > pageSize.width - paddingRight) return true;
148
+ if (value > 0 && position.x + value > pageSize.width - paddingRight) return false;
149
+ } else if (fieldName === 'height') {
150
+ if (position.y < paddingTop || position.y > pageSize.height - paddingBottom) return true;
151
+ if (value > 0 && position.y + value > pageSize.height - paddingBottom) return false;
152
+ }
153
+
154
+ return true;
155
+ };
156
+
157
+ // Use explicit type for debounce function that matches the expected signature
158
+ const handleWatch = debounce(function (...args: unknown[]) {
159
+ const formSchema = args[0] as Record<string, unknown>;
160
+ const formAndSchemaValuesDiffer = (formValue: unknown, schemaValue: unknown): boolean => {
161
+ if (typeof formValue === 'object' && formValue !== null) {
162
+ return JSON.stringify(formValue) !== JSON.stringify(schemaValue);
163
+ }
164
+ return formValue !== schemaValue;
165
+ };
166
+
167
+ let changes: ChangeSchemaItem[] = [];
168
+ for (const key in formSchema) {
169
+ if (['id', 'content'].includes(key)) continue;
170
+
171
+ let value = formSchema[key];
172
+ if (formAndSchemaValuesDiffer(value, (activeSchema as Record<string, unknown>)[key])) {
173
+ // FIXME memo: https://github.com/pdfme/pdfme/pull/367#issuecomment-1857468274
174
+ if (value === null && ['rotate', 'opacity'].includes(key)) {
175
+ value = undefined;
176
+ }
177
+
178
+ if (key === 'editable') {
179
+ const readOnlyValue = !value;
180
+ changes.push({ key: 'readOnly', value: readOnlyValue, schemaId: activeSchema.id });
181
+ if (readOnlyValue) {
182
+ changes.push({ key: 'required', value: false, schemaId: activeSchema.id });
183
+ }
184
+ continue;
185
+ }
186
+
187
+ changes.push({ key, value, schemaId: activeSchema.id });
188
+ }
189
+ }
190
+
191
+ if (changes.length) {
192
+ // Only commit these schema changes if they have passed form validation
193
+ form
194
+ .validateFields()
195
+ .then(() => changeSchemas(changes))
196
+ .catch((reason: ValidateErrorEntity) => {
197
+ if (reason.errorFields.length) {
198
+ changes = changes.filter(
199
+ (change: ChangeSchemaItem) =>
200
+ !reason.errorFields.find((field: { name: InternalNamePath; errors: string[] }) =>
201
+ field.name.includes(change.key),
202
+ ),
203
+ );
204
+ }
205
+ if (changes.length) {
206
+ changeSchemas(changes);
207
+ }
208
+ });
209
+ }
210
+ }, 100);
211
+
212
+ const activePlugin = pluginsRegistry.findByType(activeSchema.type);
213
+ if (!activePlugin) {
214
+ throw Error(`[@pdfme/ui] Failed to find plugin used for ${activeSchema.type}`);
215
+ }
216
+
217
+ const activePropPanelSchema = activePlugin.propPanel.schema;
218
+ const typeOptions: Array<{ label: string; value: string | undefined }> = [];
219
+
220
+ pluginsRegistry.entries().forEach(([label, plugin]) => {
221
+ typeOptions.push({ label, value: plugin.propPanel.defaultSchema?.type ?? undefined });
222
+ });
223
+
224
+ // Create a safe empty schema as fallback
225
+ const emptySchema: Record<string, unknown> = {};
226
+
227
+ // Safely access the default schema with proper null checking
228
+ const defaultSchema: Record<string, unknown> = activePlugin?.propPanel?.defaultSchema
229
+ ? // Create a safe copy of the schema
230
+ (() => {
231
+ const result: Record<string, unknown> = {};
232
+
233
+ // Only copy properties that exist on the object
234
+ for (const key in activePlugin.propPanel.defaultSchema) {
235
+ if (Object.prototype.hasOwnProperty.call(activePlugin.propPanel.defaultSchema, key)) {
236
+ result[key] = (activePlugin.propPanel.defaultSchema as Record<string, unknown>)[key];
237
+ }
238
+ }
239
+
240
+ return result;
241
+ })()
242
+ : emptySchema;
243
+
244
+ // Calculate max values considering padding
245
+ const maxWidth = pageSize.width - paddingLeft - paddingRight;
246
+ const maxHeight = pageSize.height - paddingTop - paddingBottom;
247
+
248
+ // Create a type-safe schema object
249
+ const propPanelSchema: PropPanelSchema = {
250
+ type: 'object',
251
+ column: 2,
252
+ properties: {
253
+ type: {
254
+ title: typedI18n('type'),
255
+ type: 'string',
256
+ widget: 'select',
257
+ props: { options: typeOptions },
258
+ required: true,
259
+ span: 12,
260
+ },
261
+ name: {
262
+ title: typedI18n('fieldName'),
263
+ type: 'string',
264
+ required: true,
265
+ span: 12,
266
+ rules: [
267
+ {
268
+ validator: validateUniqueSchemaName,
269
+ message: typedI18n('validation.uniqueName'),
270
+ },
271
+ ],
272
+ props: { autoComplete: 'off' },
273
+ },
274
+ editable: {
275
+ title: typedI18n('editable'),
276
+ type: 'boolean',
277
+ span: 8,
278
+ hidden: typeof defaultSchema.readOnly !== 'undefined',
279
+ },
280
+ required: {
281
+ title: typedI18n('required'),
282
+ type: 'boolean',
283
+ span: 16,
284
+ hidden: '{{!formData.editable}}',
285
+ },
286
+ '-': { type: 'void', widget: 'Divider' },
287
+ align: { title: typedI18n('align'), type: 'void', widget: 'AlignWidget' },
288
+ position: {
289
+ type: 'object',
290
+ widget: 'card',
291
+ properties: {
292
+ x: {
293
+ title: 'X',
294
+ type: 'number',
295
+ widget: 'inputNumber',
296
+ required: true,
297
+ span: 8,
298
+ min: paddingLeft,
299
+ max: pageSize.width - paddingRight,
300
+ rules: [
301
+ {
302
+ validator: (_: unknown, value: number) => validatePosition(_, value, 'x'),
303
+ message: typedI18n('validation.outOfBounds'),
304
+ },
305
+ ],
306
+ },
307
+ y: {
308
+ title: 'Y',
309
+ type: 'number',
310
+ widget: 'inputNumber',
311
+ required: true,
312
+ span: 8,
313
+ min: paddingTop,
314
+ max: pageSize.height - paddingBottom,
315
+ rules: [
316
+ {
317
+ validator: (_: unknown, value: number) => validatePosition(_, value, 'y'),
318
+ message: typedI18n('validation.outOfBounds'),
319
+ },
320
+ ],
321
+ },
322
+ },
323
+ },
324
+ width: {
325
+ title: typedI18n('width'),
326
+ type: 'number',
327
+ widget: 'inputNumber',
328
+ required: true,
329
+ span: 6,
330
+ props: { min: 0, max: maxWidth },
331
+ rules: [
332
+ {
333
+ validator: (_: unknown, value: number) => validatePosition(_, value, 'width'),
334
+ message: typedI18n('validation.outOfBounds'),
335
+ },
336
+ ],
337
+ },
338
+ height: {
339
+ title: typedI18n('height'),
340
+ type: 'number',
341
+ widget: 'inputNumber',
342
+ required: true,
343
+ span: 6,
344
+ props: { min: 0, max: maxHeight },
345
+ rules: [
346
+ {
347
+ validator: (_: unknown, value: number) => validatePosition(_, value, 'height'),
348
+ message: typedI18n('validation.outOfBounds'),
349
+ },
350
+ ],
351
+ },
352
+ rotate: {
353
+ title: typedI18n('rotate'),
354
+ type: 'number',
355
+ widget: 'inputNumber',
356
+ disabled: typeof defaultSchema.rotate === 'undefined',
357
+ max: 360,
358
+ props: { min: 0 },
359
+ span: 6,
360
+ },
361
+ opacity: {
362
+ title: typedI18n('opacity'),
363
+ type: 'number',
364
+ widget: 'inputNumber',
365
+ disabled: typeof defaultSchema.opacity === 'undefined',
366
+ props: { step: 0.1, min: 0, max: 1 },
367
+ span: 6,
368
+ },
369
+ },
370
+ };
371
+
372
+ // Create a safe copy of the properties
373
+ const safeProperties = { ...propPanelSchema.properties };
374
+
375
+ if (typeof activePropPanelSchema === 'function') {
376
+ // Create a new object without the schemasList property
377
+ const { size, schemas, pageSize, changeSchemas, activeElements, deselectSchema, activeSchema } =
378
+ props;
379
+ const propPanelProps = {
380
+ size,
381
+ schemas,
382
+ pageSize,
383
+ changeSchemas,
384
+ activeElements,
385
+ deselectSchema,
386
+ activeSchema,
387
+ };
388
+
389
+ // Use the typedI18n function to avoid type issues
390
+ const functionResult = activePropPanelSchema({
391
+ ...propPanelProps,
392
+ options,
393
+ theme: token,
394
+ i18n: typedI18n,
395
+ });
396
+
397
+ // Safely handle the result
398
+ const apps = functionResult && typeof functionResult === 'object' ? functionResult : {};
399
+
400
+ // Create a divider if needed
401
+ const dividerObj =
402
+ Object.keys(apps).length === 0 ? {} : { '--': { type: 'void', widget: 'Divider' } };
403
+
404
+ // Assign properties safely - use type assertion to satisfy TypeScript
405
+ propPanelSchema.properties = {
406
+ ...safeProperties,
407
+ ...(dividerObj as Record<string, Partial<Schema>>),
408
+ ...(apps as Record<string, Partial<Schema>>),
409
+ };
410
+ } else {
411
+ // Handle non-function case
412
+ const apps =
413
+ activePropPanelSchema && typeof activePropPanelSchema === 'object'
414
+ ? activePropPanelSchema
415
+ : {};
416
+
417
+ // Create a divider if needed
418
+ const dividerObj =
419
+ Object.keys(apps).length === 0 ? {} : { '--': { type: 'void', widget: 'Divider' } };
420
+
421
+ // Assign properties safely - use type assertion to satisfy TypeScript
422
+ propPanelSchema.properties = {
423
+ ...safeProperties,
424
+ ...(dividerObj as Record<string, Partial<Schema>>),
425
+ ...(apps as Record<string, Partial<Schema>>),
426
+ };
427
+ }
428
+
429
+ return (
430
+ <SidebarFrame className={DESIGNER_CLASSNAME + 'detail-view'}>
431
+ <SidebarHeader>
432
+ <Button
433
+ className={DESIGNER_CLASSNAME + 'back-button'}
434
+ style={{
435
+ position: 'absolute',
436
+ left: SIDEBAR_H_PADDING_PX,
437
+ zIndex: 100,
438
+ display: 'flex',
439
+ alignItems: 'center',
440
+ justifyContent: 'center',
441
+ transform: 'translateY(-50%)',
442
+ top: '50%',
443
+ paddingTop: '3px',
444
+ }}
445
+ onClick={deselectSchema}
446
+ icon={<Menu strokeWidth={1.5} size={20} />}
447
+ />
448
+ <Text strong style={{ textAlign: 'center', width: '100%' }}>
449
+ {typedI18n('editField')}
450
+ </Text>
451
+ </SidebarHeader>
452
+ <SidebarBody>
453
+ <FormRenderComponent
454
+ form={form}
455
+ schema={propPanelSchema}
456
+ widgets={widgets}
457
+ watch={{ '#': handleWatch }}
458
+ locale="en-US"
459
+ />
460
+ </SidebarBody>
461
+ </SidebarFrame>
462
+ );
463
+ };
464
+
465
+ const propsAreUnchanged = (prevProps: DetailViewProps, nextProps: DetailViewProps) => {
466
+ return JSON.stringify(prevProps.activeSchema) == JSON.stringify(nextProps.activeSchema);
467
+ };
468
+
469
+ export default React.memo(DetailView, propsAreUnchanged);
@@ -0,0 +1,158 @@
1
+ import React, { useEffect, useContext } from 'react';
2
+ import { DraggableSyntheticListeners } from '@dnd-kit/core';
3
+ import { I18nContext } from '../../../../contexts.js';
4
+ import { GripVertical, CircleAlert, Lock } from 'lucide-react';
5
+ import { Button, Typography } from 'antd';
6
+
7
+ const { Text } = Typography;
8
+
9
+ // Define prop types for Item component
10
+ interface Props {
11
+ /** Content to display in the item */
12
+ value: React.ReactNode;
13
+ /** Optional icon to display */
14
+ icon?: React.ReactNode;
15
+ /** Custom styles for the item */
16
+ style?: React.CSSProperties;
17
+ /** Status indicator for the item */
18
+ status?: 'is-warning' | 'is-danger';
19
+ /** Title attribute for the item */
20
+ title?: string;
21
+ /** Whether the item is required */
22
+ required?: boolean;
23
+ /** Whether the item is read-only */
24
+ readOnly?: boolean;
25
+ /** Whether the item is being dragged as an overlay */
26
+ dragOverlay?: boolean;
27
+ /** Click handler for the item */
28
+ onClick?: () => void;
29
+ /** Mouse enter handler */
30
+ onMouseEnter?: () => void;
31
+ /** Mouse leave handler */
32
+ onMouseLeave?: () => void;
33
+ /** Whether the item is currently being dragged */
34
+ dragging?: boolean;
35
+ /** Whether items are being sorted */
36
+ sorting?: boolean;
37
+ /** CSS transition value */
38
+ transition?: string;
39
+ /** Transform data for the item */
40
+ transform?: { x: number; y: number; scaleX: number; scaleY: number } | null;
41
+ /** Whether to fade the item in */
42
+ fadeIn?: boolean;
43
+ /** Drag listeners from dnd-kit */
44
+ listeners?: DraggableSyntheticListeners;
45
+ }
46
+ // Using React.memo and forwardRef for optimized rendering
47
+ // Using TypeScript interface for prop validation instead of PropTypes
48
+ const Item = React.memo(
49
+ /* eslint-disable react/prop-types */
50
+ React.forwardRef<HTMLLIElement, Props>(function Item(
51
+ {
52
+ icon,
53
+ value,
54
+ status,
55
+ title,
56
+ required,
57
+ readOnly,
58
+ style,
59
+ dragOverlay,
60
+ onClick,
61
+ onMouseEnter,
62
+ onMouseLeave,
63
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
64
+ dragging,
65
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
66
+ fadeIn,
67
+ listeners,
68
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
69
+ sorting,
70
+ transition,
71
+ transform,
72
+ ...props
73
+ },
74
+ ref,
75
+ ) {
76
+ /* eslint-enable react/prop-types */
77
+ const i18n = useContext(I18nContext);
78
+
79
+ useEffect(() => {
80
+ if (!dragOverlay) {
81
+ return;
82
+ }
83
+
84
+ document.body.style.cursor = 'grabbing';
85
+
86
+ return () => {
87
+ document.body.style.cursor = '';
88
+ };
89
+ }, [dragOverlay]);
90
+
91
+ const { x, y, scaleX, scaleY } = transform || { x: 0, y: 0, scaleX: 1, scaleY: 1 };
92
+
93
+ return (
94
+ <li
95
+ style={{
96
+ marginTop: 10,
97
+ transition,
98
+ transform: `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`,
99
+ }}
100
+ onMouseEnter={onMouseEnter}
101
+ onMouseLeave={onMouseLeave}
102
+ ref={ref}
103
+ >
104
+ <div
105
+ style={{
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ cursor: 'pointer',
109
+ gap: '0.5rem',
110
+ ...style,
111
+ }}
112
+ {...props}
113
+ onClick={() => onClick && onClick()}
114
+ >
115
+ <Button
116
+ {...listeners}
117
+ style={{
118
+ display: 'flex',
119
+ alignItems: 'center',
120
+ background: 'none',
121
+ boxShadow: 'none',
122
+ border: 'none',
123
+ paddingLeft: '0.25rem',
124
+ }}
125
+ icon={<GripVertical size={15} style={{ cursor: 'grab' }} />}
126
+ />
127
+ {icon}
128
+ <Text
129
+ style={{
130
+ overflow: 'hidden',
131
+ whiteSpace: 'nowrap',
132
+ textOverflow: 'ellipsis',
133
+ width: '100%',
134
+ }}
135
+ title={title || ''}
136
+ >
137
+ {status === undefined ? (
138
+ value
139
+ ) : (
140
+ <span style={{ display: 'flex', alignItems: 'center' }}>
141
+ <CircleAlert size={15} style={{ marginRight: '0.25rem' }} />
142
+ {status === 'is-warning' ? i18n('noKeyName') : value}
143
+ {status === 'is-danger' ? i18n('notUniq') : ''}
144
+ </span>
145
+ )}
146
+ </Text>
147
+ {readOnly && <Lock size={15} style={{ marginRight: '0.5rem' }} />}
148
+ {required && <span style={{ color: 'red', marginRight: '0.5rem' }}>*</span>}
149
+ </div>
150
+ </li>
151
+ );
152
+ }),
153
+ );
154
+
155
+ // Set display name for debugging
156
+ Item.displayName = 'Item';
157
+
158
+ export default Item;