Enhance Template and Object Management with Resource Support
- Updated fcTemplateLang to include resourceNames for improved autocompletion in template languages. - Added resource management capabilities to DocumentTemplate and EmailTemplate models, allowing for dynamic handling of resources. - Enhanced DocumentTemplateInfo and EmailTemplateInfo components to display and manage resources, improving user interaction and data visibility. - Refactored related components, including CodeBlockEditor and ObjectProperty, to support resource integration, ensuring consistent functionality across the application.
This commit is contained in:
parent
db23e685ff
commit
e610290465
@ -4,7 +4,8 @@ import {
|
||||
indentNodeProp,
|
||||
delimitedIndent,
|
||||
foldNodeProp,
|
||||
foldInside
|
||||
foldInside,
|
||||
syntaxTree
|
||||
} from '@codemirror/language'
|
||||
import { styleTags, tags as t } from '@lezer/highlight'
|
||||
import { parseMixed } from '@lezer/common'
|
||||
@ -81,13 +82,64 @@ function buildJsScope(autoCompleteObject, templateType) {
|
||||
return typed
|
||||
}
|
||||
|
||||
function isResourceValueContext(state, position) {
|
||||
let node = syntaxTree(state).resolveInner(position, -1)
|
||||
let insideTemplateInterpolation = false
|
||||
|
||||
while (node) {
|
||||
if (node.name === 'Interpolation') {
|
||||
insideTemplateInterpolation = true
|
||||
}
|
||||
if (node.name === 'AttributeValue' || node.name === 'String') {
|
||||
return true
|
||||
}
|
||||
if (node.name === 'TemplateString') {
|
||||
return !insideTemplateInterpolation
|
||||
}
|
||||
node = node.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resourceCompletionSource(resourceNames) {
|
||||
const options = [...new Set(resourceNames)]
|
||||
.filter((name) => typeof name === 'string' && name.length > 0)
|
||||
.map((name) => ({
|
||||
label: `@[${name}]`,
|
||||
apply: `@[${name}]`,
|
||||
type: 'variable',
|
||||
detail: 'Template resource'
|
||||
}))
|
||||
|
||||
return (context) => {
|
||||
if (options.length === 0) return null
|
||||
|
||||
const before = context.state.sliceDoc(0, context.pos)
|
||||
const match = before.match(/@(?:\[[^\]\r\n]*)?$/)
|
||||
if (!match || !isResourceValueContext(context.state, context.pos)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
from: context.pos - match[0].length,
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Language support for FarmControl EJS+XML document templates.
|
||||
* @param {{ autoCompleteObject?: object|string, templateType?: string }} [options]
|
||||
* @param {{ autoCompleteObject?: object|string, templateType?: string, resourceNames?: string[] }} [options]
|
||||
*/
|
||||
export function fcTemplateLang(options = {}) {
|
||||
const { autoCompleteObject, templateType = 'documentTemplate' } = options
|
||||
const {
|
||||
autoCompleteObject,
|
||||
templateType = 'documentTemplate',
|
||||
resourceNames = []
|
||||
} = options
|
||||
const jsScope = buildJsScope(autoCompleteObject, templateType)
|
||||
const completeResources = resourceCompletionSource(resourceNames)
|
||||
|
||||
return new LanguageSupport(fcTemplateLanguage, [
|
||||
xmlLanguage.data.of({
|
||||
@ -96,6 +148,8 @@ export function fcTemplateLang(options = {}) {
|
||||
fcTemplateAttributes
|
||||
)
|
||||
}),
|
||||
xmlLanguage.data.of({ autocomplete: completeResources }),
|
||||
javascriptLanguage.data.of({ autocomplete: completeResources }),
|
||||
autoCloseTags,
|
||||
...javascriptCompletionSupport(jsScope)
|
||||
])
|
||||
|
||||
@ -8,10 +8,12 @@ import useCollapseState from '../../hooks/useCollapseState.jsx'
|
||||
import NotesPanel from '../../common/NotesPanel.jsx'
|
||||
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
||||
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||
import ViewButton from '../../common/ViewButton.jsx'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import FileIcon from '../../../Icons/FileIcon.jsx'
|
||||
import ObjectForm from '../../common/ObjectForm.jsx'
|
||||
import EditButtons from '../../common/EditButtons.jsx'
|
||||
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
|
||||
@ -23,7 +25,10 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import InfoActionButtons from '../../common/InfoActionButtons.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import { getModelByName } from '../../../../database/ObjectModels.js'
|
||||
import {
|
||||
getModelByName,
|
||||
getModelProperty
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
|
||||
const log = loglevel.getLogger('DocumentTemplateInfo')
|
||||
@ -40,6 +45,7 @@ const DocumentTemplateInfo = () => {
|
||||
'DocumentTemplateInfo',
|
||||
{
|
||||
info: true,
|
||||
resources: true,
|
||||
stocks: true,
|
||||
notes: true,
|
||||
auditLogs: false
|
||||
@ -108,6 +114,7 @@ const DocumentTemplateInfo = () => {
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'DocumentTemplate Information' },
|
||||
{ key: 'resources', label: 'Resources' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
@ -161,25 +168,27 @@ const DocumentTemplateInfo = () => {
|
||||
loading={objectFormState.loading}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<InfoCollapse
|
||||
title='Document Template Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
collapseKey='info'
|
||||
<ObjectForm
|
||||
id={documentTemplateId}
|
||||
type='documentTemplate'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
setCurrentObject={true}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
<ObjectForm
|
||||
id={documentTemplateId}
|
||||
type='documentTemplate'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
setCurrentObject={true}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
{({ loading, isEditing, objectData }) => {
|
||||
return (
|
||||
{({ loading, isEditing, objectData }) => (
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Document Template Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
collapseKey='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
@ -188,14 +197,35 @@ const DocumentTemplateInfo = () => {
|
||||
objectData={objectData}
|
||||
visibleProperties={{
|
||||
content: false,
|
||||
testObject: false
|
||||
testObject: false,
|
||||
resources: false
|
||||
}}
|
||||
labelWidth='190px'
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</ObjectForm>
|
||||
</InfoCollapse>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Resources'
|
||||
icon={<FileIcon />}
|
||||
active={collapseState.resources}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('resources', expanded)
|
||||
}
|
||||
collapseKey='resources'
|
||||
>
|
||||
<ObjectProperty
|
||||
{...getModelProperty(
|
||||
'documentTemplate',
|
||||
'resources'
|
||||
)}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
loading={loading}
|
||||
size='medium'
|
||||
/>
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
)}
|
||||
</ObjectForm>
|
||||
</ActionHandler>
|
||||
|
||||
<InfoCollapse
|
||||
|
||||
@ -42,6 +42,7 @@ const EmailTemplateInfo = () => {
|
||||
'EmailTemplateInfo',
|
||||
{
|
||||
info: true,
|
||||
resources: true,
|
||||
attachments: true,
|
||||
notes: true,
|
||||
auditLogs: false
|
||||
@ -107,6 +108,7 @@ const EmailTemplateInfo = () => {
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'Email Template Information' },
|
||||
{ key: 'resources', label: 'Resources' },
|
||||
{ key: 'attachments', label: 'Attachments' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
@ -191,11 +193,29 @@ const EmailTemplateInfo = () => {
|
||||
visibleProperties={{
|
||||
content: false,
|
||||
testObject: false,
|
||||
resources: false,
|
||||
attachments: false
|
||||
}}
|
||||
labelWidth='210px'
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Resources'
|
||||
icon={<FileIcon />}
|
||||
active={collapseState.resources}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('resources', expanded)
|
||||
}
|
||||
collapseKey='resources'
|
||||
>
|
||||
<ObjectProperty
|
||||
{...getModelProperty('emailTemplate', 'resources')}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
loading={loading}
|
||||
size='medium'
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Attachments'
|
||||
icon={<FileIcon />}
|
||||
|
||||
@ -166,6 +166,7 @@ export default function CodeBlockEditor({
|
||||
disabled = false,
|
||||
minimal = false,
|
||||
autoCompleteObject = null,
|
||||
resourceNames = [],
|
||||
errorLines = [],
|
||||
templateType,
|
||||
onCursorChange = null
|
||||
@ -181,8 +182,14 @@ export default function CodeBlockEditor({
|
||||
}, [onCursorChange])
|
||||
|
||||
const languageExtension = useMemo(
|
||||
() => getCodeLanguageExtension(language, autoCompleteObject, templateType),
|
||||
[language, autoCompleteObject, templateType]
|
||||
() =>
|
||||
getCodeLanguageExtension(
|
||||
language,
|
||||
autoCompleteObject,
|
||||
templateType,
|
||||
resourceNames
|
||||
),
|
||||
[language, autoCompleteObject, templateType, resourceNames]
|
||||
)
|
||||
|
||||
const errorLineExtension = useMemo(() => {
|
||||
@ -326,6 +333,7 @@ CodeBlockEditor.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
resourceNames: PropTypes.arrayOf(PropTypes.string),
|
||||
errorLines: PropTypes.arrayOf(PropTypes.number),
|
||||
templateType: PropTypes.string,
|
||||
onCursorChange: PropTypes.func
|
||||
|
||||
@ -116,6 +116,7 @@ const ObjectProperty = ({
|
||||
height = 'auto',
|
||||
minimal = false,
|
||||
autoCompleteObject = null,
|
||||
resourceNames = [],
|
||||
errorLines = [],
|
||||
templateType,
|
||||
onCursorChange,
|
||||
@ -437,6 +438,7 @@ const ObjectProperty = ({
|
||||
readOnly={true}
|
||||
minimal={minimal}
|
||||
autoCompleteObject={autoCompleteObject}
|
||||
resourceNames={resourceNames}
|
||||
errorLines={errorLines}
|
||||
templateType={templateType}
|
||||
onCursorChange={onCursorChange}
|
||||
@ -990,6 +992,7 @@ const ObjectProperty = ({
|
||||
height={height}
|
||||
minimal={minimal}
|
||||
autoCompleteObject={autoCompleteObject}
|
||||
resourceNames={resourceNames}
|
||||
errorLines={errorLines}
|
||||
templateType={templateType}
|
||||
onCursorChange={onCursorChange}
|
||||
@ -1145,6 +1148,7 @@ ObjectProperty.propTypes = {
|
||||
name: PropTypes.string,
|
||||
language: PropTypes.string,
|
||||
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
resourceNames: PropTypes.arrayOf(PropTypes.string),
|
||||
errorLines: PropTypes.arrayOf(PropTypes.number),
|
||||
templateType: PropTypes.string,
|
||||
onCursorChange: PropTypes.func,
|
||||
|
||||
@ -94,6 +94,16 @@ const parseEjsErrorLines = (message) => {
|
||||
return [...lines]
|
||||
}
|
||||
|
||||
const normalizeResourceNames = (resources) => {
|
||||
if (!Array.isArray(resources)) return []
|
||||
|
||||
return resources
|
||||
.map((resource) =>
|
||||
typeof resource === 'string' ? resource : resource?.name
|
||||
)
|
||||
.filter((name) => typeof name === 'string' && name.length > 0)
|
||||
}
|
||||
|
||||
const TemplateEditor = ({
|
||||
objectData,
|
||||
template,
|
||||
@ -118,11 +128,15 @@ const TemplateEditor = ({
|
||||
const [errorLines, setErrorLines] = useState([])
|
||||
const [formatLoading, setFormatLoading] = useState(false)
|
||||
const [intellisense, setIntellisense] = useState(null)
|
||||
const [responseResources, setResponseResources] = useState(null)
|
||||
const intellisenseRequestIdRef = useRef(0)
|
||||
const intellisenseTimerRef = useRef(null)
|
||||
const resourceNames =
|
||||
responseResources ?? normalizeResourceNames(currentTemplate?.resources)
|
||||
|
||||
useEffect(() => {
|
||||
setIntellisense(null)
|
||||
setResponseResources(null)
|
||||
}, [currentTemplate?._id])
|
||||
|
||||
useEffect(() => {
|
||||
@ -168,6 +182,9 @@ const TemplateEditor = ({
|
||||
setIntellisense(result.intellisense)
|
||||
}
|
||||
}
|
||||
if (Array.isArray(result?.resources)) {
|
||||
setResponseResources(normalizeResourceNames(result.resources))
|
||||
}
|
||||
}
|
||||
)
|
||||
}, 200)
|
||||
@ -369,6 +386,7 @@ const TemplateEditor = ({
|
||||
autoCompleteObject={
|
||||
intellisense ?? currentTemplate?.testObject
|
||||
}
|
||||
resourceNames={resourceNames}
|
||||
errorLines={errorLines}
|
||||
templateType={templateType}
|
||||
onCursorChange={handleCursorChange}
|
||||
|
||||
@ -26,7 +26,8 @@ export function toEditorCode(code, language = 'javascript') {
|
||||
export function getCodeLanguageExtension(
|
||||
language = 'javascript',
|
||||
autoCompleteObject = null,
|
||||
templateType
|
||||
templateType,
|
||||
resourceNames = []
|
||||
) {
|
||||
const lang = Array.isArray(language) ? language[0] : language
|
||||
|
||||
@ -43,7 +44,11 @@ export function getCodeLanguageExtension(
|
||||
return xml()
|
||||
case 'fctemplatelang':
|
||||
case 'ejs':
|
||||
return fcTemplateLang({ autoCompleteObject, templateType })
|
||||
return fcTemplateLang({
|
||||
autoCompleteObject,
|
||||
templateType,
|
||||
resourceNames
|
||||
})
|
||||
case 'html':
|
||||
return html()
|
||||
case 'css':
|
||||
|
||||
@ -2240,12 +2240,14 @@ const ApiServerProvider = ({ children }) => {
|
||||
} catch (err) {
|
||||
const error = getTemplateErrorFromResponse(err)
|
||||
const intellisense = err?.response?.data?.intellisense
|
||||
const resources = err?.response?.data?.resources
|
||||
logger.error('Error fetching template intellisense:', error)
|
||||
const payload = {
|
||||
error,
|
||||
...(intellisense != null && typeof intellisense === 'object'
|
||||
? { intellisense }
|
||||
: {})
|
||||
: {}),
|
||||
...(Array.isArray(resources) ? { resources } : {})
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback(payload)
|
||||
|
||||
@ -22,6 +22,34 @@ import ListIcon from '../../components/Icons/ListIcon'
|
||||
import DuplicateIcon from '../../components/Icons/DuplicateIcon'
|
||||
import BinIcon from '../../components/Icons/BinIcon'
|
||||
|
||||
const computeTemplateResources = (resources) => {
|
||||
if (!Array.isArray(resources)) return []
|
||||
|
||||
const usedNames = new Set()
|
||||
const nextSuffix = new Map()
|
||||
|
||||
return resources.map((resource) => {
|
||||
const filename = resource?.file?.filename || resource?.file?.name || ''
|
||||
const baseName =
|
||||
filename
|
||||
.replace(/\.[^.]*$/, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'resource'
|
||||
let name = baseName
|
||||
let suffix = nextSuffix.get(baseName) || 1
|
||||
|
||||
while (usedNames.has(name)) {
|
||||
name = `${baseName}-${suffix}`
|
||||
suffix += 1
|
||||
}
|
||||
|
||||
usedNames.add(name)
|
||||
nextSuffix.set(baseName, suffix)
|
||||
return { ...resource, name }
|
||||
})
|
||||
}
|
||||
|
||||
export const DocumentTemplate = {
|
||||
name: 'documentTemplate',
|
||||
label: 'Document Template',
|
||||
@ -288,6 +316,36 @@ export const DocumentTemplate = {
|
||||
type: 'codeBlock',
|
||||
language: 'fcTemplateLang'
|
||||
},
|
||||
{
|
||||
name: 'resources',
|
||||
label: 'Resources',
|
||||
type: 'objectChildren',
|
||||
required: false,
|
||||
canAddRemove: true,
|
||||
size: 'medium',
|
||||
columns: ['name', 'file'],
|
||||
value: (documentTemplate) =>
|
||||
computeTemplateResources(documentTemplate?.resources),
|
||||
properties: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
readOnly: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'file',
|
||||
label: 'File',
|
||||
type: 'object',
|
||||
objectType: 'file',
|
||||
required: true,
|
||||
showHyperlink: true,
|
||||
columnWidth: 230
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'testObject',
|
||||
label: 'Test Object',
|
||||
|
||||
@ -22,6 +22,34 @@ import EmailTemplateIcon from '../../components/Icons/EmailTemplateIcon'
|
||||
import DuplicateIcon from '../../components/Icons/DuplicateIcon'
|
||||
import BinIcon from '../../components/Icons/BinIcon'
|
||||
|
||||
const computeTemplateResources = (resources) => {
|
||||
if (!Array.isArray(resources)) return []
|
||||
|
||||
const usedNames = new Set()
|
||||
const nextSuffix = new Map()
|
||||
|
||||
return resources.map((resource) => {
|
||||
const filename = resource?.file?.filename || resource?.file?.name || ''
|
||||
const baseName =
|
||||
filename
|
||||
.replace(/\.[^.]*$/, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'resource'
|
||||
let name = baseName
|
||||
let suffix = nextSuffix.get(baseName) || 1
|
||||
|
||||
while (usedNames.has(name)) {
|
||||
name = `${baseName}-${suffix}`
|
||||
suffix += 1
|
||||
}
|
||||
|
||||
usedNames.add(name)
|
||||
nextSuffix.set(baseName, suffix)
|
||||
return { ...resource, name }
|
||||
})
|
||||
}
|
||||
|
||||
export const EmailTemplate = {
|
||||
name: 'emailTemplate',
|
||||
label: 'Email Template',
|
||||
@ -270,6 +298,36 @@ export const EmailTemplate = {
|
||||
objectType: (data) => data?.objectType,
|
||||
showHyperlink: true
|
||||
},
|
||||
{
|
||||
name: 'resources',
|
||||
label: 'Resources',
|
||||
type: 'objectChildren',
|
||||
required: false,
|
||||
canAddRemove: true,
|
||||
size: 'medium',
|
||||
columns: ['name', 'file'],
|
||||
value: (emailTemplate) =>
|
||||
computeTemplateResources(emailTemplate?.resources),
|
||||
properties: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
readOnly: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'file',
|
||||
label: 'File',
|
||||
type: 'object',
|
||||
objectType: 'file',
|
||||
required: true,
|
||||
showHyperlink: true,
|
||||
columnWidth: 230
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user