@pdfme/ui 4.2.4 → 4.2.5-dev.1

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.
@@ -37,6 +37,7 @@ export declare abstract class BaseUIClass {
37
37
  rotate: import("zod").ZodOptional<import("zod").ZodNumber>;
38
38
  opacity: import("zod").ZodOptional<import("zod").ZodNumber>;
39
39
  readOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
40
+ required: import("zod").ZodOptional<import("zod").ZodBoolean>;
40
41
  }, "passthrough", import("zod").ZodTypeAny, import("zod").objectOutputType<{
41
42
  type: import("zod").ZodString;
42
43
  content: import("zod").ZodOptional<import("zod").ZodString>;
@@ -55,6 +56,7 @@ export declare abstract class BaseUIClass {
55
56
  rotate: import("zod").ZodOptional<import("zod").ZodNumber>;
56
57
  opacity: import("zod").ZodOptional<import("zod").ZodNumber>;
57
58
  readOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
59
+ required: import("zod").ZodOptional<import("zod").ZodBoolean>;
58
60
  }, import("zod").ZodTypeAny, "passthrough">, import("zod").objectInputType<{
59
61
  type: import("zod").ZodString;
60
62
  content: import("zod").ZodOptional<import("zod").ZodString>;
@@ -73,6 +75,7 @@ export declare abstract class BaseUIClass {
73
75
  rotate: import("zod").ZodOptional<import("zod").ZodNumber>;
74
76
  opacity: import("zod").ZodOptional<import("zod").ZodNumber>;
75
77
  readOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
78
+ required: import("zod").ZodOptional<import("zod").ZodBoolean>;
76
79
  }, import("zod").ZodTypeAny, "passthrough">>>, "many">;
77
80
  basePdf: import("zod").ZodUnion<[import("zod").ZodUnion<[import("zod").ZodString, import("zod").ZodType<ArrayBuffer, import("zod").ZodTypeDef, ArrayBuffer>, import("zod").ZodType<Uint8Array, import("zod").ZodTypeDef, Uint8Array>]>, import("zod").ZodObject<{
78
81
  width: import("zod").ZodNumber;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { Plugin } from "@pdfme/common";
3
+ interface PluginIconProps {
4
+ plugin: Plugin<any>;
5
+ label: string;
6
+ size?: number;
7
+ styles?: React.CSSProperties;
8
+ }
9
+ declare const PluginIcon: (props: PluginIconProps) => React.JSX.Element;
10
+ export default PluginIcon;
@@ -2,9 +2,11 @@ import React from 'react';
2
2
  import { DraggableSyntheticListeners } from '@dnd-kit/core';
3
3
  interface Props {
4
4
  value: React.ReactNode;
5
+ icon?: React.ReactNode;
5
6
  style?: React.CSSProperties;
6
7
  status?: 'is-warning' | 'is-danger';
7
8
  title?: string;
9
+ required?: boolean;
8
10
  dragOverlay?: boolean;
9
11
  onClick?: () => void;
10
12
  onMouseEnter?: () => void;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
2
  import { Plugins, UIOptions } from '@pdfme/common';
3
- export declare const I18nContext: import("react").Context<(key: "type" | "width" | "height" | "rotate" | "opacity" | "cancel" | "field" | "fieldName" | "align" | "edit" | "plsInputName" | "fieldMustUniq" | "notUniq" | "noKeyName" | "fieldsList" | "editField" | "errorOccurred" | "errorBulkUpdateFieldName" | "commitBulkUpdateFieldName" | "bulkUpdateFieldName" | "addPageAfter" | "removePage" | "removePageConfirm" | "hexColorPrompt" | "schemas.color" | "schemas.borderWidth" | "schemas.borderColor" | "schemas.backgroundColor" | "schemas.textColor" | "schemas.bgColor" | "schemas.horizontal" | "schemas.vertical" | "schemas.left" | "schemas.center" | "schemas.right" | "schemas.top" | "schemas.middle" | "schemas.bottom" | "schemas.padding" | "schemas.text.fontName" | "schemas.text.size" | "schemas.text.spacing" | "schemas.text.textAlign" | "schemas.text.verticalAlign" | "schemas.text.lineHeight" | "schemas.text.min" | "schemas.text.max" | "schemas.text.fit" | "schemas.text.dynamicFontSize" | "schemas.text.format" | "schemas.mvt.typingInstructions" | "schemas.mvt.sampleField" | "schemas.mvt.variablesSampleData" | "schemas.barcodes.barColor" | "schemas.barcodes.includetext" | "schemas.table.alternateBackgroundColor" | "schemas.table.tableStyle" | "schemas.table.headStyle" | "schemas.table.bodyStyle" | "schemas.table.columnStyle", dict?: {
3
+ export declare const I18nContext: import("react").Context<(key: "type" | "width" | "height" | "rotate" | "opacity" | "required" | "cancel" | "field" | "fieldName" | "align" | "edit" | "plsInputName" | "fieldMustUniq" | "notUniq" | "noKeyName" | "fieldsList" | "editField" | "errorOccurred" | "errorBulkUpdateFieldName" | "commitBulkUpdateFieldName" | "bulkUpdateFieldName" | "addPageAfter" | "removePage" | "removePageConfirm" | "validation.uniqueName" | "validation.hexColor" | "schemas.color" | "schemas.borderWidth" | "schemas.borderColor" | "schemas.backgroundColor" | "schemas.textColor" | "schemas.bgColor" | "schemas.horizontal" | "schemas.vertical" | "schemas.left" | "schemas.center" | "schemas.right" | "schemas.top" | "schemas.middle" | "schemas.bottom" | "schemas.padding" | "schemas.text.fontName" | "schemas.text.size" | "schemas.text.spacing" | "schemas.text.textAlign" | "schemas.text.verticalAlign" | "schemas.text.lineHeight" | "schemas.text.min" | "schemas.text.max" | "schemas.text.fit" | "schemas.text.dynamicFontSize" | "schemas.text.format" | "schemas.mvt.typingInstructions" | "schemas.mvt.sampleField" | "schemas.mvt.variablesSampleData" | "schemas.barcodes.barColor" | "schemas.barcodes.includetext" | "schemas.table.alternateBackgroundColor" | "schemas.table.tableStyle" | "schemas.table.headStyle" | "schemas.table.bodyStyle" | "schemas.table.columnStyle", dict?: {
4
4
  cancel: string;
5
5
  field: string;
6
6
  fieldName: string;
@@ -10,6 +10,7 @@ export declare const I18nContext: import("react").Context<(key: "type" | "width"
10
10
  height: string;
11
11
  rotate: string;
12
12
  edit: string;
13
+ required: string;
13
14
  plsInputName: string;
14
15
  fieldMustUniq: string;
15
16
  notUniq: string;
@@ -24,7 +25,8 @@ export declare const I18nContext: import("react").Context<(key: "type" | "width"
24
25
  addPageAfter: string;
25
26
  removePage: string;
26
27
  removePageConfirm: string;
27
- hexColorPrompt: string;
28
+ 'validation.uniqueName': string;
29
+ 'validation.hexColor': string;
28
30
  'schemas.color': string;
29
31
  'schemas.borderWidth': string;
30
32
  'schemas.borderColor': string;
@@ -36,6 +36,7 @@ export declare const template2SchemasList: (_template: Template) => Promise<{
36
36
  key: string;
37
37
  opacity?: number | undefined;
38
38
  rotate?: number | undefined;
39
+ required?: boolean | undefined;
39
40
  content?: string | undefined;
40
41
  readOnly?: boolean | undefined;
41
42
  }[][]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdfme/ui",
3
- "version": "4.2.4",
3
+ "version": "4.2.5-dev.1",
4
4
  "sideEffects": false,
5
5
  "author": "hand-dot",
6
6
  "license": "MIT",
package/src/Designer.tsx CHANGED
@@ -65,6 +65,7 @@ class Designer extends BaseUIClass {
65
65
  template={this.template}
66
66
  onSaveTemplate={(template) => {
67
67
  this.template = template;
68
+ this.template.pdfmeVersion = PDFME_VERSION;
68
69
  if (this.onSaveTemplateCallback) {
69
70
  this.onSaveTemplateCallback(template);
70
71
  }
@@ -8,7 +8,8 @@ import { theme, Button } from 'antd';
8
8
  import { useDraggable } from '@dnd-kit/core';
9
9
  import { CSS } from "@dnd-kit/utilities";
10
10
  import Renderer from '../Renderer';
11
- import { PluginsRegistry, OptionsContext } from '../../contexts';
11
+ import { PluginsRegistry } from '../../contexts';
12
+ import PluginIcon from "./PluginIcon";
12
13
 
13
14
  const Draggable = (props: { plugin: Plugin<any>, scale: number, basePdf: BasePdf, children: React.ReactNode }) => {
14
15
  const { scale, basePdf, plugin } = props;
@@ -44,8 +45,6 @@ const Draggable = (props: { plugin: Plugin<any>, scale: number, basePdf: BasePdf
44
45
  const LeftSidebar = ({ height, scale, basePdf }: { height: number, scale: number, basePdf: BasePdf }) => {
45
46
  const { token } = theme.useToken();
46
47
  const pluginsRegistry = useContext(PluginsRegistry);
47
- const options = useContext(OptionsContext);
48
-
49
48
  const [isDragging, setIsDragging] = useState(false);
50
49
 
51
50
  useEffect(() => {
@@ -77,7 +76,6 @@ const LeftSidebar = ({ height, scale, basePdf }: { height: number, scale: number
77
76
  >
78
77
  {Object.entries(pluginsRegistry).map(([label, plugin]) => {
79
78
  if (!plugin?.propPanel.defaultSchema) return null;
80
- const icon = options.icons?.[plugin.propPanel.defaultSchema.type] ?? plugin.icon;
81
79
 
82
80
  return <Draggable
83
81
  key={label}
@@ -85,16 +83,10 @@ const LeftSidebar = ({ height, scale, basePdf }: { height: number, scale: number
85
83
  basePdf={basePdf}
86
84
  plugin={plugin}>
87
85
  <Button
88
- title={label}
89
- onMouseDown={() => {
90
- setIsDragging(true);
91
- }}
92
- style={{ width: 35, height: 35, marginTop: '0.25rem', padding: '0.25rem' }}>
93
- {icon ?
94
- <div dangerouslySetInnerHTML={{ __html: icon }} />
95
- :
96
- <div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</div>
97
- }
86
+ onMouseDown={() => setIsDragging(true)}
87
+ style={{ width: 35, height: 35, marginTop: '0.25rem', padding: '0.25rem' }}
88
+ >
89
+ <PluginIcon plugin={plugin} label={label} />
98
90
  </Button>
99
91
  </Draggable>
100
92
  })}
@@ -0,0 +1,47 @@
1
+ import React, { useContext } from 'react';
2
+ import { Plugin } from "@pdfme/common";
3
+ import { OptionsContext } from '../../contexts';
4
+
5
+ interface PluginIconProps {
6
+ plugin: Plugin<any>;
7
+ label: string;
8
+ size?: number;
9
+ styles?: React.CSSProperties;
10
+ }
11
+
12
+ const getWithModifiedSize = (htmlString: string, label: string, size: number, styles?: React.CSSProperties) => {
13
+ const parser = new DOMParser();
14
+ const doc = parser.parseFromString(htmlString, 'text/html');
15
+
16
+ const modifyNode = (node: HTMLElement) => {
17
+ if (node.tagName === 'SVG' || node.tagName === 'svg') {
18
+ node.setAttribute('width', size.toString());
19
+ node.setAttribute('height', size.toString());
20
+ }
21
+ Array.from(node.children).forEach(child => modifyNode(child as HTMLElement));
22
+ };
23
+
24
+ Array.from(doc.body.children).forEach(child => modifyNode(child as HTMLElement));
25
+
26
+ return (
27
+ <div style={styles} title={label} dangerouslySetInnerHTML={{ __html: doc.body.innerHTML }} />
28
+ );
29
+ };
30
+
31
+ const PluginIcon = (props: PluginIconProps) => {
32
+ const { plugin, label, size, styles } = props;
33
+ const options = useContext(OptionsContext);
34
+ const icon = options.icons?.[plugin.propPanel.defaultSchema.type] ?? plugin.icon;
35
+ const iconStyles = { ...styles, display: 'flex', justifyContent: 'center' };
36
+
37
+ if (icon) {
38
+ if (size) {
39
+ return getWithModifiedSize(icon, label, size, iconStyles);
40
+ }
41
+ return <div style={iconStyles} title={label} dangerouslySetInnerHTML={{__html: icon}} />
42
+ }
43
+
44
+ return <div style={{...styles, overflow: 'hidden', fontSize: 10, }}>{label}</div>
45
+ };
46
+
47
+ export default PluginIcon;
@@ -6,7 +6,7 @@ interface ButtonConfig {
6
6
  key: string;
7
7
  icon: string;
8
8
  type: 'boolean' | 'select';
9
- value?: String;
9
+ value?: string;
10
10
  }
11
11
 
12
12
  const ButtonGroupWidget = (props: PropPanelWidgetProps) => {
@@ -1,10 +1,10 @@
1
1
  import FormRender, { useForm } from 'form-render';
2
- import React, { useContext, useEffect } from 'react';
2
+ import React, { useRef, useContext, useState, useEffect } from 'react';
3
3
  import type { ChangeSchemaItem, Dict, SchemaForUI, PropPanelWidgetProps, PropPanelSchema } from '@pdfme/common';
4
4
  import type { SidebarProps } from '../../../../types';
5
5
  import { MenuOutlined } from '@ant-design/icons';
6
6
  import { I18nContext, PluginsRegistry, OptionsContext } from '../../../../contexts';
7
- import { getSidebarContentHeight } from '../../../../helper';
7
+ import { getSidebarContentHeight, debounce } from '../../../../helper';
8
8
  import { theme, Typography, Button, Divider } from 'antd';
9
9
  import AlignWidget from './AlignWidget';
10
10
  import WidgetRenderer from './WidgetRenderer';
@@ -22,25 +22,72 @@ type DetailViewProps = Pick<SidebarProps,
22
22
  const DetailView = (props: DetailViewProps) => {
23
23
  const { token } = theme.useToken();
24
24
 
25
- const { size, changeSchemas, deselectSchema, activeSchema } = props;
25
+ const { size, schemas, changeSchemas, deselectSchema, activeSchema } = props;
26
26
  const form = useForm();
27
27
 
28
28
  const i18n = useContext(I18nContext);
29
29
  const pluginsRegistry = useContext(PluginsRegistry);
30
30
  const options = useContext(OptionsContext);
31
31
 
32
+ const [widgets, setWidgets] = useState<{
33
+ [key: string]: (props: PropPanelWidgetProps) => React.JSX.Element;
34
+ }>({});
35
+
36
+ useEffect(() => {
37
+ const newWidgets: typeof widgets = {
38
+ AlignWidget: (p) => <AlignWidget {...p} {...props} options={options} />,
39
+ Divider: () => (
40
+ <Divider style={{ marginTop: token.marginXS, marginBottom: token.marginXS }} />
41
+ ),
42
+ ButtonGroup: (p) => <ButtonGroupWidget {...p} {...props} options={options} />,
43
+ };
44
+ for (const plugin of Object.values(pluginsRegistry)) {
45
+ const widgets = plugin?.propPanel.widgets || {};
46
+ Object.entries(widgets).forEach(([widgetKey, widgetValue]) => {
47
+ newWidgets[widgetKey] = (p) => (
48
+ <WidgetRenderer
49
+ {...p}
50
+ {...props}
51
+ options={options}
52
+ theme={token}
53
+ i18n={i18n as (key: keyof Dict | string) => string}
54
+ widget={widgetValue}
55
+ />
56
+ );
57
+ });
58
+ }
59
+ setWidgets(newWidgets);
60
+ }, [activeSchema, pluginsRegistry, JSON.stringify(options)]);
61
+
32
62
  useEffect(() => {
33
63
  const values: any = { ...activeSchema };
34
64
  // [position] Change the nested position object into a flat, as a three-column layout is difficult to implement
35
65
  values.x = values.position.x;
36
66
  values.y = values.position.y;
37
67
  delete values.position;
68
+
38
69
  form.setValues(values);
39
70
 
40
71
  }, [activeSchema, form]);
41
72
 
73
+ useEffect(() => form.resetFields(), [activeSchema.id])
74
+
75
+ useEffect(() => {
76
+ uniqueSchemaKey.current = (value: string): boolean => {
77
+ for (const s of Object.values(schemas)) {
78
+ if (s.key === value && s.id !== activeSchema.id) {
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ };
84
+ }, [schemas, activeSchema]);
85
+
86
+ const uniqueSchemaKey = useRef((value: string): boolean => true);
42
87
 
43
- const handleWatch = (formSchema: any) => {
88
+ const validateUniqueSchemaKey = (_: any, value: string): boolean => uniqueSchemaKey.current(value)
89
+
90
+ const handleWatch = debounce((formSchema: any) => {
44
91
  const formAndSchemaValuesDiffer = (formValue: any, schemaValue: any): boolean => {
45
92
  if (typeof formValue === 'object') {
46
93
  return JSON.stringify(formValue) !== JSON.stringify(schemaValue);
@@ -88,7 +135,7 @@ const DetailView = (props: DetailViewProps) => {
88
135
  }
89
136
  });
90
137
  }
91
- };
138
+ }, 500);
92
139
 
93
140
  const activePlugin = Object.values(pluginsRegistry).find(
94
141
  (plugin) => plugin?.propPanel.defaultSchema.type === activeSchema.type
@@ -118,7 +165,18 @@ Check this document: https://pdfme.com/docs/custom-schemas`);
118
165
  required: true,
119
166
  span: 12,
120
167
  },
121
- key: { title: i18n('fieldName'), type: 'string', required: true, span: 12 },
168
+ key: {
169
+ title: i18n('fieldName'),
170
+ type: 'string',
171
+ required: true,
172
+ span: 12,
173
+ rules: [{
174
+ validator: validateUniqueSchemaKey,
175
+ message: i18n('validation.uniqueName'),
176
+ }],
177
+ props: { autoComplete: "off" }
178
+ },
179
+ required: { title: i18n('required'), type: 'boolean', span: 8, hidden: defaultSchema?.readOnly },
122
180
  '-': { type: 'void', widget: 'Divider' },
123
181
  align: { title: i18n('align'), type: 'void', widget: 'AlignWidget' },
124
182
  x: { title: 'X', type: 'number', widget: 'inputNumber', required: true, span: 8, min: 0 },
@@ -181,31 +239,6 @@ Check this document: https://pdfme.com/docs/custom-schemas`);
181
239
  };
182
240
  }
183
241
 
184
- const allWidgets: {
185
- [key: string]: (props: PropPanelWidgetProps) => React.JSX.Element;
186
- } = {
187
- AlignWidget: (p) => <AlignWidget {...p} {...props} options={options} />,
188
- Divider: () => (
189
- <Divider style={{ marginTop: token.marginXS, marginBottom: token.marginXS }} />
190
- ),
191
- ButtonGroup: (p) => <ButtonGroupWidget {...p} {...props} options={options} />,
192
- };
193
- for (const plugin of Object.values(pluginsRegistry)) {
194
- const widgets = plugin?.propPanel.widgets || {};
195
- Object.entries(widgets).forEach(([widgetKey, widgetValue]) => {
196
- allWidgets[widgetKey] = (p) => (
197
- <WidgetRenderer
198
- {...p}
199
- {...props}
200
- options={options}
201
- theme={token}
202
- i18n={i18n as (key: keyof Dict | string) => string}
203
- widget={widgetValue}
204
- />
205
- );
206
- });
207
- }
208
-
209
242
  return (
210
243
  <div>
211
244
  <div style={{ height: 40, display: 'flex', alignItems: 'center' }}>
@@ -235,7 +268,7 @@ Check this document: https://pdfme.com/docs/custom-schemas`);
235
268
  <FormRender
236
269
  form={form}
237
270
  schema={propPanelSchema}
238
- widgets={allWidgets}
271
+ widgets={widgets}
239
272
  watch={{ '#': handleWatch }}
240
273
  locale="en-US"
241
274
  />
@@ -245,7 +278,7 @@ Check this document: https://pdfme.com/docs/custom-schemas`);
245
278
  };
246
279
 
247
280
  const propsAreUnchanged = (prevProps: DetailViewProps, nextProps: DetailViewProps) => {
248
- return JSON.stringify(prevProps.activeSchema) == JSON.stringify(nextProps.activeSchema)
281
+ return JSON.stringify(prevProps.activeSchema) == JSON.stringify(nextProps.activeSchema);
249
282
  };
250
283
 
251
284
  export default React.memo(DetailView, propsAreUnchanged);
@@ -8,9 +8,11 @@ const { Text } = Typography;
8
8
 
9
9
  interface Props {
10
10
  value: React.ReactNode;
11
+ icon?: React.ReactNode;
11
12
  style?: React.CSSProperties;
12
13
  status?: 'is-warning' | 'is-danger';
13
14
  title?: string;
15
+ required?: boolean;
14
16
  dragOverlay?: boolean;
15
17
  onClick?: () => void;
16
18
  onMouseEnter?: () => void;
@@ -26,9 +28,11 @@ const Item = React.memo(
26
28
  React.forwardRef<HTMLLIElement, Props>(
27
29
  (
28
30
  {
31
+ icon,
29
32
  value,
30
33
  status,
31
34
  title,
35
+ required,
32
36
  style,
33
37
  dragOverlay,
34
38
  onClick,
@@ -93,6 +97,7 @@ const Item = React.memo(
93
97
  }}
94
98
  icon={<HolderOutlined style={{ cursor: 'grab' }} />}
95
99
  />
100
+ {icon}
96
101
  <Text
97
102
  style={{
98
103
  overflow: 'hidden',
@@ -105,12 +110,13 @@ const Item = React.memo(
105
110
  {status === undefined ? (
106
111
  value
107
112
  ) : (
108
- <span style={{ display: 'flex', alignItems: 'center' }}>
113
+ <span>
109
114
  <ExclamationCircleOutlined width={15} style={{ marginRight: '0.5rem' }} />
110
115
  {status === 'is-warning' ? i18n('noKeyName') : value}
111
116
  {status === 'is-danger' ? i18n('notUniq') : ''}
112
117
  </span>
113
118
  )}
119
+ {required && <span style={{ color: 'red', marginLeft: '0.5rem' }}>*</span>}
114
120
  </Text>
115
121
  </div>
116
122
  </li>
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useContext, ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import {
4
4
  closestCorners,
@@ -17,9 +17,11 @@ import {
17
17
  } from '@dnd-kit/sortable';
18
18
  import { SchemaForUI } from '@pdfme/common';
19
19
  import type { SidebarProps } from '../../../../types';
20
+ import { PluginsRegistry } from '../../../../contexts';
20
21
  import Item from './Item';
21
22
  import SelectableSortableItem from './SelectableSortableItem';
22
23
  import { theme } from 'antd';
24
+ import PluginIcon from "../../PluginIcon";
23
25
 
24
26
  const SelectableSortableContainer = (
25
27
  props: Pick<
@@ -28,11 +30,11 @@ const SelectableSortableContainer = (
28
30
  >
29
31
  ) => {
30
32
  const { token } = theme.useToken();
31
-
32
33
  const { schemas, onEdit, onSortEnd, hoveringSchemaId, onChangeHoveringSchemaId } = props;
33
34
  const [selectedSchemas, setSelectedSchemas] = useState<SchemaForUI[]>([]);
34
- const [dragOverlaydItems, setClonedItems] = useState<SchemaForUI[] | null>(null);
35
+ const [dragOverlaidItems, setClonedItems] = useState<SchemaForUI[] | null>(null);
35
36
  const [activeId, setActiveId] = useState<string | null>(null);
37
+ const pluginsRegistry = useContext(PluginsRegistry);
36
38
  const sensors = useSensors(
37
39
  useSensor(PointerSensor, { activationConstraint: { distance: 15 } }),
38
40
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
@@ -56,6 +58,20 @@ const SelectableSortableContainer = (
56
58
  }
57
59
  };
58
60
 
61
+ const getPluginIcon = (inSchema: string|SchemaForUI): ReactNode => {
62
+ const thisSchema = (typeof inSchema === 'string') ? schemas.find((schema) => schema.id === inSchema) : inSchema;
63
+
64
+ const [pluginLabel, activePlugin] = Object.entries(pluginsRegistry).find(
65
+ ([label, plugin]) => plugin?.propPanel.defaultSchema.type === thisSchema?.type
66
+ )!;
67
+
68
+ if (!activePlugin) {
69
+ return <></>
70
+ }
71
+
72
+ return <PluginIcon plugin={activePlugin} label={pluginLabel} size={20} styles={{marginRight: '0.5rem'}}/>
73
+ };
74
+
59
75
  return (
60
76
  <DndContext
61
77
  sensors={sensors}
@@ -101,8 +117,8 @@ const SelectableSortableContainer = (
101
117
  setActiveId(null);
102
118
  }}
103
119
  onDragCancel={() => {
104
- if (dragOverlaydItems) {
105
- onSortEnd(dragOverlaydItems);
120
+ if (dragOverlaidItems) {
121
+ onSortEnd(dragOverlaidItems);
106
122
  }
107
123
 
108
124
  setActiveId(null);
@@ -139,7 +155,9 @@ const SelectableSortableContainer = (
139
155
  <>
140
156
  <ul style={{ margin: 0, padding: 0, listStyle: 'none' }}>
141
157
  <Item
158
+ icon={getPluginIcon(activeId)}
142
159
  value={schemas.find((schema) => schema.id === activeId)!.key}
160
+ required={schemas.find((schema) => schema.id === activeId)!.required}
143
161
  style={{ background: token.colorPrimary }}
144
162
  dragOverlay
145
163
  />
@@ -149,8 +167,10 @@ const SelectableSortableContainer = (
149
167
  .filter((item) => item.id !== activeId)
150
168
  .map((item) => (
151
169
  <Item
170
+ icon={getPluginIcon(item)}
152
171
  key={item.id}
153
172
  value={item.key}
173
+ required={item.required}
154
174
  style={{ background: token.colorPrimary }}
155
175
  dragOverlay
156
176
  />
@@ -1,10 +1,11 @@
1
1
  import React, { useContext } from 'react';
2
2
  import { useSortable } from '@dnd-kit/sortable';
3
3
  import { SchemaForUI } from '@pdfme/common';
4
- import { I18nContext } from '../../../../contexts';
4
+ import { PluginsRegistry, I18nContext } from '../../../../contexts';
5
5
  import Item from './Item';
6
6
  import { useMountStatus } from '../../../../hooks';
7
7
  import { theme } from 'antd';
8
+ import PluginIcon from "../../PluginIcon";
8
9
 
9
10
  interface Props {
10
11
  isSelected: boolean;
@@ -29,6 +30,7 @@ const SelectableSortableItem = ({
29
30
  const { token } = theme.useToken();
30
31
 
31
32
  const i18n = useContext(I18nContext);
33
+ const pluginsRegistry = useContext(PluginsRegistry);
32
34
  const { setNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({
33
35
  id: schema.id,
34
36
  });
@@ -40,6 +42,12 @@ const SelectableSortableItem = ({
40
42
  onClick: (event: any) => onSelect(schema.id, event.shiftKey),
41
43
  };
42
44
 
45
+ const [pluginLabel, thisPlugin] = Object.entries(pluginsRegistry).find(
46
+ ([label, plugin]) => plugin?.propPanel.defaultSchema.type === schema.type
47
+ )!;
48
+
49
+ const iconStyles = { width: 20, marginRight: '0.5rem' };
50
+
43
51
  let status: undefined | 'is-warning' | 'is-danger';
44
52
  if (!schema.key) {
45
53
  status = 'is-warning';
@@ -64,9 +72,11 @@ const SelectableSortableItem = ({
64
72
  onMouseEnter={onMouseEnter}
65
73
  onMouseLeave={onMouseLeave}
66
74
  onClick={() => onEdit(schema.id)}
75
+ icon={thisPlugin && <PluginIcon plugin={thisPlugin} label={pluginLabel} size={20} styles={iconStyles}/>}
67
76
  value={schema.key}
68
77
  status={status}
69
78
  title={title}
79
+ required={schema.required}
70
80
  style={{ ...selectedStyle, ...style }}
71
81
  dragging={isDragging}
72
82
  sorting={isSorting}
@@ -15,7 +15,7 @@ import RightSidebar from './RightSidebar/index';
15
15
  import LeftSidebar from './LeftSidebar';
16
16
  import Canvas from './Canvas/index';
17
17
  import { RULER_HEIGHT, RIGHT_SIDEBAR_WIDTH } from '../../constants';
18
- import { I18nContext, PluginsRegistry } from '../../contexts';
18
+ import { I18nContext, OptionsContext, PluginsRegistry } from '../../contexts';
19
19
  import {
20
20
  schemasList2template,
21
21
  uuid,
@@ -62,6 +62,7 @@ const TemplateEditor = ({
62
62
 
63
63
  const i18n = useContext(I18nContext);
64
64
  const pluginsRegistry = useContext(PluginsRegistry);
65
+ const options = useContext(OptionsContext);
65
66
 
66
67
  const [hoveringSchemaId, setHoveringSchemaId] = useState<string | null>(null);
67
68
  const [activeElements, setActiveElements] = useState<HTMLElement[]>([]);
@@ -161,16 +162,26 @@ const TemplateEditor = ({
161
162
  const [paddingTop, paddingRight, paddingBottom, paddingLeft] = isBlankPdf(template.basePdf) ? template.basePdf.padding : [0, 0, 0, 0];
162
163
  const pageSize = pageSizes[pageCursor];
163
164
 
165
+ const newSchemaKey = (prefix: string) => {
166
+ let keyNum = schemasList[pageCursor].length + 1;
167
+ let newKey = prefix + keyNum;
168
+ while (schemasList[pageCursor].find((s) => s.key === newKey)) {
169
+ keyNum++;
170
+ newKey = prefix + keyNum;
171
+ }
172
+ return newKey;
173
+ };
164
174
  const ensureMiddleValue = (min: number, value: number, max: number) => Math.min(Math.max(min, value), max)
165
175
 
166
176
  const s = {
167
177
  id: uuid(),
168
- key: `${i18n('field')}${schemasList[pageCursor].length + 1}`,
178
+ key: newSchemaKey(i18n('field')),
169
179
  ...defaultSchema,
170
180
  position: {
171
181
  x: ensureMiddleValue(paddingLeft, defaultSchema.position.x, pageSize.width - paddingRight - defaultSchema.width),
172
182
  y: ensureMiddleValue(paddingTop, defaultSchema.position.y, pageSize.height - paddingBottom - defaultSchema.height),
173
183
  },
184
+ required: defaultSchema.readOnly ? false : options.requiredByDefault || defaultSchema.required || false,
174
185
  } as SchemaForUI;
175
186
 
176
187
  if (defaultSchema.position.y === 0) {
@@ -62,6 +62,16 @@ const Wrapper = ({
62
62
  outline,
63
63
  }}
64
64
  >
65
+ {schema.required &&
66
+ <span style={{
67
+ color: 'red',
68
+ position: 'absolute',
69
+ top: -12,
70
+ left: -12,
71
+ fontSize: 18,
72
+ fontWeight: 700,
73
+ }}>*</span>
74
+ }
65
75
  {children}
66
76
  </div>
67
77
  );
package/src/helper.ts CHANGED
@@ -448,7 +448,7 @@ const handleTypeChange = (
448
448
  pluginsRegistry: Plugins
449
449
  ) => {
450
450
  if (key !== 'type') return;
451
- const keysToKeep = ['id', 'key', 'type', 'position'];
451
+ const keysToKeep = ['id', 'key', 'type', 'position', 'required'];
452
452
  Object.keys(schema).forEach((key) => {
453
453
  if (!keysToKeep.includes(key)) {
454
454
  delete schema[key as keyof typeof schema];
@@ -463,6 +463,9 @@ const handleTypeChange = (
463
463
  (schema as any)[key] = propPanel?.defaultSchema[key];
464
464
  }
465
465
  });
466
+ if (schema.readOnly) {
467
+ schema.required = false;
468
+ }
466
469
  };
467
470
 
468
471
  export const changeSchemas = (args: {