Compare commits

..

No commits in common. "f55a09ff1f8035640ed96dd7e7e5625da3c2a8e1" and "f97bdc364d4b0e5956d976519da8ce3bdd3d25de" have entirely different histories.

13 changed files with 86 additions and 450 deletions

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.935415,0,0,0.935415,2.292349,1.999999)">
<path d="M23.559,56.897C33.746,67.068 47.027,66.068 56.324,56.787C62.652,50.459 64.606,43.662 62.965,35.615C62.027,29.131 57.996,21.49 55.684,17.1C54.012,13.865 51.918,9.693 50.762,8.178C49.465,6.443 47.402,6.068 45.73,7.318C43.809,8.709 43.543,10.803 44.496,13.521L48.309,23.537C48.621,24.365 48.574,24.818 48.262,25.131C47.934,25.443 47.48,25.521 46.84,24.881L23.199,1.209C21.559,-0.4 19.137,-0.4 17.527,1.193C15.934,2.803 15.934,5.256 17.543,6.865L35.215,24.537C34.512,24.896 33.809,25.271 33.09,25.74L12.824,5.475C11.199,3.834 8.777,3.834 7.168,5.475C5.559,7.084 5.559,9.521 7.168,11.131L27.199,31.162C26.684,31.787 26.152,32.443 25.684,33.084L7.246,14.631C5.637,13.037 3.199,13.021 1.574,14.615C-0.004,16.225 -0.004,18.678 1.59,20.287L21.574,40.256C21.23,41.131 20.996,41.975 20.762,42.803L6.84,28.881C5.246,27.24 2.809,27.24 1.184,28.834C-0.395,30.475 -0.395,32.912 1.184,34.537L23.559,56.897Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.89584,0,0,0.89584,2.323959,1.951688)">
<path d="M58.333,59.078C67.13,50.281 68.599,39.468 62.739,27.046L55.505,11.796C54.911,10.546 54.208,9.531 53.38,8.734C50.583,5.921 46.38,5.843 43.489,8.718C41.864,10.343 41.536,12.781 42.083,15.265L43.192,17.953C43.224,18.062 43.224,18.156 43.177,18.234C43.067,18.343 42.942,18.281 42.864,18.203L27.052,2.39C23.927,-0.766 19.692,-0.704 16.552,2.453C15.802,3.171 15.333,4.078 15.099,5.046C12.271,3.484 9.239,3.953 6.817,6.359C4.521,8.64 4.083,11.765 5.521,14.546C4.63,14.718 3.817,15.171 3.161,15.828C0.239,18.734 0.224,22.812 3.192,25.765L4.536,27.125C3.692,27.265 2.833,27.687 2.146,28.359C-0.776,31.312 -0.714,35.593 2.333,38.593L22.224,58.515C33.458,69.765 47.614,69.781 58.333,59.078ZM53.302,54.328C45.224,62.406 35.177,62.281 26.411,53.484L7.505,34.562C6.833,33.89 6.802,32.984 7.458,32.343C8.099,31.703 9.036,31.687 9.724,32.359L21.239,43.874C22.395,45.031 24.192,44.968 25.302,43.859C26.427,42.734 26.505,40.937 25.349,39.765L8.005,22.406C7.317,21.734 7.286,20.843 7.958,20.218C8.567,19.546 9.505,19.562 10.192,20.218L26.349,36.39C27.505,37.546 29.317,37.468 30.442,36.359C31.536,35.234 31.614,33.406 30.474,32.265L11.411,13.218C10.724,12.546 10.724,11.624 11.364,10.968C11.989,10.343 12.927,10.328 13.599,10.999L31.974,29.39C33.114,30.5 34.911,30.468 36.036,29.343C37.177,28.25 37.224,26.39 36.083,25.265L20.599,9.781C19.942,9.109 19.927,8.234 20.567,7.578C21.208,6.921 22.13,6.921 22.786,7.593L45.27,30.031C46.802,31.593 48.755,31.39 49.974,30.109C50.989,29.078 51.474,27.562 50.77,25.75L46.802,15.374C46.38,14.343 46.817,13.531 47.567,13.187C48.411,12.828 49.208,13.187 49.677,14.187L56.864,29.843C61.536,40.093 60.067,47.546 53.302,54.328Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1680,7 +1680,3 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
margin: 0;
font-size: 95%;
}
.code-block-editor .simplebar-content-wrapper {
background-color: var(--layout-modal-bg);
}

View File

@ -9,7 +9,7 @@ import {
import { styleTags, tags as t } from '@lezer/highlight'
import { parseMixed } from '@lezer/common'
import { javascriptLanguage } from '@codemirror/lang-javascript'
import { javascriptCompletionSupport } from '../javascriptLang'
import { javascriptCompletionSources } from '../javascriptLang'
import {
xmlLanguage,
completeFromSchema,
@ -105,6 +105,8 @@ export function fcTemplateLang(options = {}) {
)
}),
autoCloseTags,
...javascriptCompletionSupport(jsScope)
javascriptLanguage.data.of({
autocomplete: javascriptCompletionSources(jsScope)
})
])
}

View File

@ -1,7 +1,10 @@
import { completeFromList } from '@codemirror/autocomplete'
import {
javascript,
javascriptLanguage,
scopeCompletionSource
localCompletionSource,
scopeCompletionSource,
snippets
} from '@codemirror/lang-javascript'
import { nodeJsScope } from './nodeScope.js'
@ -28,20 +31,14 @@ export function nodeScopeCompletionSource(autoCompleteObject) {
}
/**
* Support extensions for mixed languages that mount
* `javascriptLanguage` without `javascript()` itself (snippets,
* keywords, locals, and Node globals).
*
* Each source must be a separate `languageData` entry. CodeMirror
* treats an `autocomplete` *array* as `completeFromList` options, not
* as multiple completion sources.
* Completions for mixed languages that mount `javascriptLanguage`
* without `javascript()` (snippets + locals + Node globals).
*/
export function javascriptCompletionSupport(autoCompleteObject) {
export function javascriptCompletionSources(autoCompleteObject) {
return [
javascript().support,
javascriptLanguage.data.of({
autocomplete: nodeScopeCompletionSource(autoCompleteObject)
})
completeFromList(snippets),
localCompletionSource,
nodeScopeCompletionSource(autoCompleteObject)
]
}

View File

@ -7,15 +7,7 @@ const NewDocumentSize = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'documentSize'}
defaultValues={{
infiniteHeight: false,
printPadding: false,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
...defaultValues
}}
defaultValues={{ infiniteHeight: false, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [

View File

@ -868,35 +868,16 @@ const FilterInput = ({
// Keep the input focused while interacting with the popover.
event.preventDefault()
}}
className='filter-input-property-filter'
style={{
display: 'inline-block',
maxWidth: 280,
maxHeight: 220,
margin: -4,
verticalAlign: 'top'
}}
style={{ width: 280, height: 220, margin: -4 }}
>
<ScrollBox
inner
smallPadding
style={{ height: 'auto', maxHeight: 220, maxWidth: 280 }}
>
<div
style={{
padding: '12px 16px',
maxWidth: 280,
boxSizing: 'border-box'
}}
>
<div style={{ width: 'max-content', maxWidth: '100%' }}>
<SimplePropertyFilter
modelType={propertyFilter.modelType}
propertyName={propertyFilter.propertyName}
value={propertyFilterValue}
onChange={handlePropertyFilterChange}
/>
</div>
<ScrollBox inner smallPadding>
<div style={{ padding: '12px 16px', minWidth: 0 }}>
<SimplePropertyFilter
modelType={propertyFilter.modelType}
propertyName={propertyFilter.propertyName}
value={propertyFilterValue}
onChange={handlePropertyFilterChange}
/>
</div>
</ScrollBox>
</div>
@ -1033,12 +1014,6 @@ const FilterInput = ({
display: none;
height: 0;
}
.filter-input-property-filter .simplebar-content-wrapper {
max-width: none;
}
.filter-input-property-filter .simplebar-content {
display: inline-block;
}
`}</style>
{showPlaceholder && (
<span

View File

@ -187,24 +187,22 @@ const TemplateEditor = ({
<Splitter className={'farmcontrol-splitter'} vertical={true}>
{collapseState.preview == true && (
<Splitter.Panel style={{ height: '100%' }}>
<Spin spinning={loading} indicator={<LoadingOutlined />}>
<Card
spinning={loading}
<Card
spinning={loading}
style={style}
styles={{ body: { height: '100%', padding: '22px' } }}
>
<TemplatePreview
objectData={objectData?.testObject}
documentTemplate={objectData}
loading={loading}
isEditing={isEditing}
style={style}
styles={{ body: { height: '100%', padding: '22px' } }}
>
<TemplatePreview
objectData={objectData?.testObject}
documentTemplate={objectData}
loading={loading}
isEditing={isEditing}
style={style}
onTestObjectOpen={() => setTestObjectOpen(true)}
onPreviewMessage={handlePreviewMessage}
showTestObject={true}
/>
</Card>
</Spin>
onTestObjectOpen={() => setTestObjectOpen(true)}
onPreviewMessage={handlePreviewMessage}
showTestObject={true}
/>
</Card>
</Splitter.Panel>
)}
{collapseState.editor == true && (

View File

@ -1,20 +1,10 @@
import {
useState,
useContext,
useEffect,
useLayoutEffect,
useRef,
useCallback
} from 'react'
import { useState, useContext, useEffect, useRef, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Flex, Button, Input, Select } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
import PanIcon from '../../Icons/PanIcon.jsx'
import PanFilledIcon from '../../Icons/PanFilledIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx'
import ScrollBox from './ScrollBox.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx'
const noop = () => {}
@ -30,9 +20,6 @@ const TemplatePreview = ({
showPreviewSwitch = true
}) => {
const iframeRef = useRef(null)
const scrollElementRef = useRef(null)
const savedScrollRef = useRef({ top: 0, left: 0 })
const iframeSizeObserverRef = useRef(null)
const previewRequestIdRef = useRef(0)
const { fetchTemplatePreview, fetchTemplatePDF } =
useContext(ApiServerContext)
@ -41,115 +28,23 @@ const TemplatePreview = ({
const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML')
const [panMode, setPanMode] = useState(false)
const [isPanning, setIsPanning] = useState(false)
const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 })
const [iframeSize, setIframeSize] = useState({
width: '100%',
height: 'auto'
})
const applyIframeContentSize = useCallback(() => {
const iframe = iframeRef.current
const doc = iframe?.contentDocument
if (!iframe || !doc?.documentElement) {
return
}
const documentEl =
doc.querySelector('.previewDocument') ||
doc.querySelector('.previewWrapper') ||
doc.body
const container = doc.querySelector('.previewContainer')
const wrapper = doc.querySelector('.previewWrapper')
const visual = documentEl.getBoundingClientRect()
if (visual.width <= 0 && visual.height <= 0) {
return
}
if (wrapper) {
wrapper.style.width = `${visual.width}px`
wrapper.style.height = `${visual.height}px`
}
let padX = 0
let padY = 0
if (container) {
const containerStyle = doc.defaultView.getComputedStyle(container)
padX =
parseFloat(containerStyle.paddingLeft) +
parseFloat(containerStyle.paddingRight)
padY =
parseFloat(containerStyle.paddingTop) +
parseFloat(containerStyle.paddingBottom)
}
const contentWidth = Math.ceil(visual.width + padX)
const contentHeight = Math.ceil(visual.height + padY)
if (!doc.head?.querySelector('style[data-preview-outer-scroll]')) {
const styleEl = doc.createElement('style')
styleEl.setAttribute('data-preview-outer-scroll', 'true')
styleEl.textContent =
'html, body { overflow: hidden !important; height: auto !important; }'
doc.head?.appendChild(styleEl)
}
setIframeSize((prev) => {
const width = `${contentWidth}px`
const height = `${contentHeight}px`
if (prev.width === width && prev.height === height) {
return prev
}
return { width, height }
})
}, [])
const restoreScrollPosition = useCallback(() => {
const scrollEl = scrollElementRef.current
if (!scrollEl) {
return
}
scrollEl.scrollTo(savedScrollRef.current.left, savedScrollRef.current.top)
}, [])
const handleIframeLoad = useCallback(() => {
iframeSizeObserverRef.current?.disconnect()
iframeSizeObserverRef.current = null
if (previewType != 'HTML') {
return
}
applyIframeContentSize()
const doc = iframeRef.current?.contentDocument
const observed =
doc?.querySelector('.previewDocument') ||
doc?.querySelector('.previewContainer') ||
doc?.body
if (observed && typeof ResizeObserver === 'function') {
const observer = new ResizeObserver(() => {
applyIframeContentSize()
})
observer.observe(observed)
iframeSizeObserverRef.current = observer
}
}, [applyIframeContentSize, previewType])
useLayoutEffect(() => {
restoreScrollPosition()
}, [iframeSize, restoreScrollPosition])
const updatePreviewContentHTML = (html) => {
const scrollEl = scrollElementRef.current
if (scrollEl) {
savedScrollRef.current = {
top: scrollEl.scrollTop,
left: scrollEl.scrollLeft
if (iframeRef.current) {
// Save current scroll position
const scrollY = iframeRef.current.contentWindow.scrollY
const scrollX = iframeRef.current.contentWindow.scrollX
// Update srcDoc
setPreviewContentHTML(html)
// Restore scroll position after iframe loads new content
const handleLoad = () => {
iframeRef.current.contentWindow.scrollTo(scrollX, scrollY)
iframeRef.current.removeEventListener('load', handleLoad)
}
iframeRef.current.addEventListener('load', handleLoad)
}
setPreviewContentHTML(html)
}
const reloadPreviewPDF = useCallback(
@ -189,18 +84,24 @@ const TemplatePreview = ({
const requestId = ++previewRequestIdRef.current
setReloadLoading(true)
fetchTemplatePreview(templateId, content, testObject, scale, (result) => {
if (requestId !== previewRequestIdRef.current) {
return
fetchTemplatePreview(
templateId,
content,
testObject,
scale,
(result) => {
if (requestId !== previewRequestIdRef.current) {
return
}
setReloadLoading(false)
if (result?.error) {
onPreviewMessage(result.error, true)
} else {
updatePreviewContentHTML(result.html)
onPreviewMessage('No issues found.', false)
}
}
setReloadLoading(false)
if (result?.error) {
onPreviewMessage(result.error, true)
} else {
updatePreviewContentHTML(result.html)
onPreviewMessage('No issues found.', false)
}
})
)
},
[fetchTemplatePreview, onPreviewMessage, documentTemplate?._id]
)
@ -235,80 +136,6 @@ const TemplatePreview = ({
reloadPreviewPDF
])
useEffect(() => {
return () => {
iframeSizeObserverRef.current?.disconnect()
}
}, [])
useEffect(() => {
if (!panMode) {
setIsPanning(false)
}
}, [panMode])
useEffect(() => {
const scrollEl = scrollElementRef.current
if (!panMode || !scrollEl || previewType != 'HTML') {
return
}
const handlePointerDown = (event) => {
if (event.pointerType === 'mouse' && event.button !== 0) {
return
}
event.preventDefault()
panStartRef.current = {
x: event.clientX,
y: event.clientY,
scrollLeft: scrollEl.scrollLeft,
scrollTop: scrollEl.scrollTop
}
setIsPanning(true)
}
scrollEl.addEventListener('pointerdown', handlePointerDown)
return () => {
scrollEl.removeEventListener('pointerdown', handlePointerDown)
}
}, [panMode, previewType])
useEffect(() => {
if (!isPanning) {
return
}
const previousCursor = document.body.style.cursor
const previousUserSelect = document.body.style.userSelect
document.body.style.cursor = 'grabbing'
document.body.style.userSelect = 'none'
const handlePointerMove = (event) => {
const scrollEl = scrollElementRef.current
if (!scrollEl) {
return
}
const { x, y, scrollLeft, scrollTop } = panStartRef.current
scrollEl.scrollLeft = scrollLeft - (event.clientX - x)
scrollEl.scrollTop = scrollTop - (event.clientY - y)
}
const handlePointerUp = () => {
setIsPanning(false)
}
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
return () => {
document.body.style.cursor = previousCursor
document.body.style.userSelect = previousUserSelect
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}
}, [isPanning])
return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Flex gap={'small'}>
@ -322,7 +149,6 @@ const TemplatePreview = ({
objectData={objectData}
disabled={!isEditing || objectData?.global}
type={'object'}
style={{ minWidth: 0 }}
/>
) : (
<div style={{ flexGrow: 1 }}>
@ -339,19 +165,6 @@ const TemplatePreview = ({
</>
) : null}
<Button
icon={
panMode ? (
<PanFilledIcon style={{ color: 'var(--color-primary)' }} />
) : (
<PanIcon />
)
}
disabled={loading || reloadLoading || previewType == 'PDF'}
onClick={() => {
setPanMode((prev) => !prev)
}}
/>
<Button
icon={<PlusIcon />}
onClick={() => {
@ -392,62 +205,18 @@ const TemplatePreview = ({
) : null}
</Flex>
<div
<iframe
ref={iframeRef}
srcDoc={previewType == 'HTML' ? previewContentHTML : undefined}
src={previewType == 'PDF' ? pdfBlob : undefined}
frameBorder='0'
style={{
flex: '1 1 auto',
minHeight: 0,
border: '1px solid #85858541'
width: '100%',
flexGrow: 1,
border: '1px solid #85858541',
overflow: 'auto'
}}
>
{previewType == 'HTML' ? (
<ScrollBox
scrollableNodeProps={{ ref: scrollElementRef }}
style={{
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
touchAction: panMode ? 'none' : undefined
}}
>
<div
style={{
width: iframeSize.width,
height: iframeSize.height,
cursor: panMode
? isPanning
? 'grabbing'
: 'grab'
: undefined
}}
>
<iframe
ref={iframeRef}
srcDoc={previewContentHTML}
onLoad={handleIframeLoad}
scrolling='no'
frameBorder='0'
style={{
width: '100%',
height: iframeSize.height === 'auto' ? 150 : '100%',
display: 'block',
border: 0,
overflow: 'hidden',
pointerEvents: panMode ? 'none' : undefined
}}
/>
</div>
</ScrollBox>
) : (
<iframe
ref={iframeRef}
src={pdfBlob}
frameBorder='0'
style={{
width: '100%',
height: '100%',
border: 0
}}
/>
)}
</div>
/>
</Flex>
)
}

View File

@ -1854,8 +1854,7 @@ const ApiServerProvider = ({ children }) => {
{
content,
testObject,
scale,
padding: true
scale
},
{
headers: {
@ -1887,7 +1886,7 @@ const ApiServerProvider = ({ children }) => {
object
},
{
params: { type, padding: true },
params: { type },
responseType: 'arraybuffer',
headers: {
Authorization: `Bearer ${token}`

View File

@ -1,8 +0,0 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/panfilledicon.svg?react'
const PanFilledIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default PanFilledIcon

View File

@ -1,6 +0,0 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/panicon.svg?react'
const PanIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PanIcon

View File

@ -1,12 +1,10 @@
import { createElement, lazy } from 'react'
const DocumentSizeInfo = lazy(
() =>
import('../../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo')
() => import('../../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo')
)
const NewDocumentSize = lazy(
() =>
import('../../components/Dashboard/Management/DocumentSizes/NewDocumentSize')
() => import('../../components/Dashboard/Management/DocumentSizes/NewDocumentSize')
)
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
@ -32,11 +30,7 @@ export const DocumentSize = {
label: 'New Document Size',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
return createElement(NewDocumentSize, {
defaultValues: objectData,
onOk,
reset: true
})
return createElement(NewDocumentSize, { defaultValues: objectData, onOk, reset: true })
}
},
{
@ -85,7 +79,7 @@ export const DocumentSize = {
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
}
},
],
pages: [
{
@ -99,11 +93,6 @@ export const DocumentSize = {
'width',
'height',
'infiniteHeight',
'printPadding',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'createdAt',
'updatedAt'
],
@ -111,11 +100,6 @@ export const DocumentSize = {
'name',
'width',
'height',
'printPadding',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'createdAt',
'updatedAt',
'_reference'
@ -125,11 +109,6 @@ export const DocumentSize = {
'width',
'height',
'infiniteHeight',
'printPadding',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'createdAt',
'updatedAt'
],
@ -199,49 +178,6 @@ export const DocumentSize = {
visible: (objectData) => {
return !objectData.infiniteHeight
}
},
{
name: 'printPadding',
label: 'Print Padding',
required: true,
columnWidth: 160,
type: 'bool'
},
{
name: 'paddingLeft',
label: 'Padding Left',
required: true,
columnWidth: 150,
type: 'number',
suffix: 'mm',
min: 0
},
{
name: 'paddingRight',
label: 'Padding Right',
required: true,
columnWidth: 162,
type: 'number',
suffix: 'mm',
min: 0
},
{
name: 'paddingTop',
label: 'Padding Top',
required: true,
columnWidth: 155,
type: 'number',
suffix: 'mm',
min: 0
},
{
name: 'paddingBottom',
label: 'Padding Bottom',
required: true,
columnWidth: 180,
type: 'number',
suffix: 'mm',
min: 0
}
]
}