Add MagicWandIcon component and integrate template formatting functionality
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced MagicWandIcon component using SVG for enhanced visual representation in the TemplateEditor.
- Updated TemplateEditor to include a button for formatting template content, utilizing the new fetchTemplateFormat function from ApiServerContext.
- Enhanced DocumentTemplateDesign to pass necessary props to TemplateEditor for improved functionality.
- Refactored CodeBlockEditor to support an additional value prop for better code handling.
This commit is contained in:
Tom Butcher 2026-08-19 20:02:35 +01:00
parent ec0e6245b3
commit 6d0e9a7abc
6 changed files with 122 additions and 19 deletions

View File

@ -0,0 +1,8 @@
<?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.885551,0,0,0.885551,3,2.046843)">
<path d="M50.547,29.572C51.295,29.572 51.877,29.045 51.995,28.338C53.37,19.038 54.594,17.459 64.024,16.253C64.81,16.143 65.361,15.53 65.361,14.782C65.361,14.043 64.81,13.453 64.024,13.367C54.904,12.161 53.559,10.64 51.995,1.321C51.829,0.503 51.295,0 50.547,0C49.808,0 49.226,0.503 49.132,1.314C47.805,10.551 46.39,12.113 37.086,13.367C36.316,13.453 35.765,14.043 35.765,14.782C35.765,15.53 36.316,16.112 37.086,16.253C46.127,17.794 47.513,18.895 49.132,28.33C49.242,29.045 49.817,29.572 50.547,29.572ZM9.329,55.436C9.964,55.436 10.42,54.98 10.507,54.376C11.325,48.334 11.42,48.159 17.652,47.278C18.28,47.168 18.712,46.696 18.712,46.108C18.712,45.496 18.304,45.023 17.652,44.937C11.349,43.794 11.349,43.667 10.507,37.815C10.42,37.187 9.941,36.731 9.329,36.731C8.724,36.731 8.268,37.187 8.158,37.839C7.364,43.714 7.292,43.937 1.108,44.937C0.384,45.023 0,45.448 0,46.108C0,46.696 0.384,47.144 1.013,47.278C7.34,48.398 7.388,48.501 8.158,54.376C8.268,54.98 8.724,55.436 9.329,55.436ZM20.339,65.06C20.762,65.06 21.06,64.755 21.13,64.332C21.788,60.84 21.727,60.803 25.481,60.013C25.903,59.919 26.177,59.661 26.177,59.246C26.177,58.831 25.927,58.518 25.505,58.432C21.734,57.665 21.802,57.643 21.13,54.081C21.06,53.665 20.731,53.36 20.339,53.36C19.916,53.36 19.595,53.665 19.525,54.096C18.87,57.643 18.968,57.688 15.205,58.432C14.727,58.542 14.453,58.823 14.453,59.246C14.453,59.661 14.727,59.919 15.15,60.013C18.968,60.803 18.87,60.84 19.525,64.309C19.595,64.755 19.916,65.06 20.339,65.06Z" style="fill-rule:nonzero;"/>
<path d="M55.838,66.006C57.977,68.2 61.69,68.193 63.862,66.006C66.042,63.788 66.042,60.206 63.855,57.981L20.002,13.818C17.839,11.624 14.149,11.624 11.954,13.818C9.774,16.06 9.774,19.641 11.961,21.859L55.838,66.006ZM28.346,32.791L15.017,19.432C14.341,18.732 14.317,17.622 15.017,16.922C15.71,16.205 16.851,16.198 17.575,16.922L30.815,30.241L28.346,32.791Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -135,7 +135,7 @@ const DocumentTemplateDesign = () => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
{({ loading, isEditing, objectData, form, setObjectData }) => {
return (
<TemplateEditor
objectData={objectData}
@ -143,6 +143,8 @@ const DocumentTemplateDesign = () => {
style={{ height: '72vh' }}
collapseState={collapseState}
isEditing={isEditing}
form={form}
setObjectData={setObjectData}
/>
)
}}

View File

@ -82,6 +82,7 @@ export function getCodeLanguageExtension(
export default function CodeBlockEditor({
code = '',
value,
language = 'javascript',
style = {},
className = '',
@ -95,7 +96,8 @@ export default function CodeBlockEditor({
}) {
const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
const editorCode = toEditorCode(code, language)
const sourceCode = value !== undefined ? value : code
const editorCode = toEditorCode(sourceCode, language)
const languageExtension = useMemo(
() => getCodeLanguageExtension(language, autoCompleteObject),
@ -192,7 +194,8 @@ export default function CodeBlockEditor({
}
CodeBlockEditor.propTypes = {
code: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
code: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
language: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)

View File

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'
import { useState, useCallback, useContext } from 'react'
import PropTypes from 'prop-types'
import {
Flex,
@ -14,9 +14,11 @@ import {
import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import MagicWandIcon from '../../Icons/MagicWandIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx'
import TemplatePreview from './TemplatePreview.jsx'
import DataTree from './DataTree.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx'
//import { useMediaQuery } from 'react-responsive'
const TemplateEditor = ({
@ -24,12 +26,16 @@ const TemplateEditor = ({
loading,
collapseState,
isEditing,
style
style,
form,
setObjectData
}) => {
const { fetchTemplateFormat } = useContext(ApiServerContext)
const [testObjectOpen, setTestObjectOpen] = useState(false)
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
const [previewMessage, setPreviewMessage] = useState('No issues found.')
const [previewError, setPreviewError] = useState(false)
const [formatLoading, setFormatLoading] = useState(false)
//const isMobile = useMediaQuery({ maxWidth: 768 })
const handlePreviewMessage = useCallback((message, isError) => {
@ -67,6 +73,40 @@ const TemplateEditor = ({
setPreviewError(isError)
}, [])
const handleFormatCode = useCallback(async () => {
const content = form?.getFieldValue?.('content') ?? objectData?.content
if (content == null || typeof content !== 'string') {
return
}
setFormatLoading(true)
try {
const result = await fetchTemplateFormat(content)
if (result?.error) {
handlePreviewMessage(result.error, true)
return
}
if (result?.content == null) {
return
}
form?.setFieldValue?.('content', result.content)
setObjectData?.((prev) => ({
...prev,
content: result.content
}))
} finally {
setFormatLoading(false)
}
}, [
form,
objectData?.content,
fetchTemplateFormat,
handlePreviewMessage,
setObjectData
])
return (
<>
<Splitter className={'farmcontrol-splitter'} vertical={true}>
@ -95,19 +135,33 @@ const TemplateEditor = ({
<Spin spinning={loading} indicator={<LoadingOutlined />}>
<Card style={style} styles={{ body: { height: '100%' } }}>
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Alert
message={previewMessage}
showIcon
style={{ padding: '4px 5px 4px 8px' }}
icon={
previewError ? (
<ExclamationOctagonIcon />
) : (
<CheckCircleIcon />
)
}
type={previewError ? 'error' : 'success'}
/>
<Flex gap={'small'} align={'stretch'}>
<Alert
message={previewMessage}
showIcon
style={{
padding: '4px 5px 4px 8px',
flex: '1 1 auto',
minWidth: 0
}}
icon={
previewError ? (
<ExclamationOctagonIcon />
) : (
<CheckCircleIcon />
)
}
type={previewError ? 'error' : 'success'}
/>
{isEditing && (
<Button
icon={<MagicWandIcon />}
loading={formatLoading}
disabled={loading || formatLoading}
onClick={handleFormatCode}
/>
)}
</Flex>
<div
style={{
@ -190,7 +244,9 @@ TemplateEditor.propTypes = {
objectData: PropTypes.object,
collapseState: PropTypes.object,
isEditing: PropTypes.bool,
style: PropTypes.object
style: PropTypes.object,
form: PropTypes.object,
setObjectData: PropTypes.func
}
export default TemplateEditor

View File

@ -1774,6 +1774,33 @@ const ApiServerProvider = ({ children }) => {
URL.revokeObjectURL(fileUrl)
}
const fetchTemplateFormat = async (content, callback) => {
logger.debug('Formatting template content...')
try {
const response = await axios.post(
`${config.backendUrl}/documenttemplates/format`,
{ content },
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
if (typeof callback === 'function') {
callback(response.data)
}
return response.data
} catch (err) {
const error = getTemplateErrorFromResponse(err)
logger.error('Error formatting template content:', error)
if (typeof callback === 'function') {
callback({ error })
}
return { error }
}
}
const fetchTemplatePreview = async (
id,
content,
@ -2346,6 +2373,7 @@ const ApiServerProvider = ({ children }) => {
exportToExcel,
exportToCsv,
fetchTemplatePreview,
fetchTemplateFormat,
fetchTemplatePDF,
fetchNotes,
downloadTemplate,

View File

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