@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.
- package/README.md +9 -0
- package/__mocks__/assetsTransformer.js +7 -0
- package/__mocks__/form-render.js +7 -0
- package/__mocks__/lucide-react.js +19 -0
- package/dist/index.es.js +159393 -0
- package/dist/index.umd.js +1041 -0
- package/dist/types/__tests__/assets/helper.d.ts +3 -0
- package/dist/types/__tests__/components/Designer.test.d.ts +1 -0
- package/dist/types/__tests__/components/PluginIcon.test.d.ts +1 -0
- package/dist/types/__tests__/components/Preview.test.d.ts +1 -0
- package/dist/types/__tests__/helper.test.d.ts +1 -0
- package/dist/types/src/Designer.d.ts +21 -0
- package/dist/types/src/Form.d.ts +24 -0
- package/dist/types/src/Viewer.d.ts +15 -0
- package/dist/types/src/class.d.ts +89 -0
- package/dist/types/src/components/AppContextProvider.d.ts +11 -0
- package/dist/types/src/components/CtlBar.d.ts +14 -0
- package/dist/types/src/components/Designer/Canvas/Guides.d.ts +9 -0
- package/dist/types/src/components/Designer/Canvas/Mask.d.ts +4 -0
- package/dist/types/src/components/Designer/Canvas/Moveable.d.ts +37 -0
- package/dist/types/src/components/Designer/Canvas/Padding.d.ts +6 -0
- package/dist/types/src/components/Designer/Canvas/Selecto.d.ts +10 -0
- package/dist/types/src/components/Designer/Canvas/index.d.ts +22 -0
- package/dist/types/src/components/Designer/LeftSidebar.d.ts +8 -0
- package/dist/types/src/components/Designer/PluginIcon.d.ts +10 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/AlignWidget.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.d.ts +7 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/index.d.ts +8 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/Item.d.ts +45 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.d.ts +14 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/index.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/index.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/layout.d.ts +15 -0
- package/dist/types/src/components/Designer/index.d.ts +11 -0
- package/dist/types/src/components/ErrorScreen.d.ts +7 -0
- package/dist/types/src/components/Paper.d.ts +20 -0
- package/dist/types/src/components/Preview.d.ts +15 -0
- package/dist/types/src/components/Renderer.d.ts +13 -0
- package/dist/types/src/components/Root.d.ts +9 -0
- package/dist/types/src/components/Spinner.d.ts +3 -0
- package/dist/types/src/components/StaticSchema.d.ts +10 -0
- package/dist/types/src/components/UnitPager.d.ts +10 -0
- package/dist/types/src/constants.d.ts +11 -0
- package/dist/types/src/contexts.d.ts +10 -0
- package/dist/types/src/helper.d.ts +73 -0
- package/dist/types/src/hooks.d.ts +46 -0
- package/dist/types/src/i18n.d.ts +3 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/theme.d.ts +2 -0
- package/dist/types/src/types.d.ts +19 -0
- package/eslint.config.mjs +41 -0
- package/package.json +127 -0
- package/src/Designer.tsx +107 -0
- package/src/Form.tsx +102 -0
- package/src/Viewer.tsx +59 -0
- package/src/class.ts +188 -0
- package/src/components/AppContextProvider.tsx +78 -0
- package/src/components/CtlBar.tsx +183 -0
- package/src/components/Designer/Canvas/Guides.tsx +49 -0
- package/src/components/Designer/Canvas/Mask.tsx +20 -0
- package/src/components/Designer/Canvas/Moveable.tsx +91 -0
- package/src/components/Designer/Canvas/Padding.tsx +56 -0
- package/src/components/Designer/Canvas/Selecto.tsx +45 -0
- package/src/components/Designer/Canvas/index.tsx +536 -0
- package/src/components/Designer/LeftSidebar.tsx +120 -0
- package/src/components/Designer/PluginIcon.tsx +87 -0
- package/src/components/Designer/RightSidebar/DetailView/AlignWidget.tsx +229 -0
- package/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.tsx +78 -0
- package/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.tsx +28 -0
- package/src/components/Designer/RightSidebar/DetailView/index.tsx +469 -0
- package/src/components/Designer/RightSidebar/ListView/Item.tsx +158 -0
- package/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.tsx +204 -0
- package/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.tsx +88 -0
- package/src/components/Designer/RightSidebar/ListView/index.tsx +116 -0
- package/src/components/Designer/RightSidebar/index.tsx +72 -0
- package/src/components/Designer/RightSidebar/layout.tsx +75 -0
- package/src/components/Designer/index.tsx +389 -0
- package/src/components/ErrorScreen.tsx +33 -0
- package/src/components/Paper.tsx +117 -0
- package/src/components/Preview.tsx +220 -0
- package/src/components/Renderer.tsx +165 -0
- package/src/components/Root.tsx +38 -0
- package/src/components/Spinner.tsx +45 -0
- package/src/components/StaticSchema.tsx +50 -0
- package/src/components/UnitPager.tsx +119 -0
- package/src/constants.ts +21 -0
- package/src/contexts.ts +14 -0
- package/src/helper.ts +534 -0
- package/src/hooks.ts +308 -0
- package/src/i18n.ts +903 -0
- package/src/index.ts +5 -0
- package/src/theme.ts +20 -0
- package/src/types.ts +20 -0
- package/tsconfig.json +48 -0
- 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;
|