mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 06:55:50 +00:00
feat: markdown & iframe html support handlebars as rendering engin (#4946)
* feat: markdown support setting rendering engin * refactor: markdown * refactor: markdown * refactor: markdown * refactor: markdown * fix: bug * fix: bug * fix: bug * fix: markdown style * fix: markdown style * fix: bug
This commit is contained in:
parent
e358ce378e
commit
0f4dd0b3ae
@ -2,9 +2,7 @@
|
||||
"version": "1.3.0-alpha",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -966,5 +966,6 @@
|
||||
"Search": "搜索",
|
||||
"Clear default value": "清除默认值",
|
||||
"Open in new window": "新窗口打开",
|
||||
"Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。"
|
||||
"Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。",
|
||||
"Set Template Engine": "设置模板引擎"
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export const MarkdownBlockInitializer = () => {
|
||||
'x-decorator': 'CardItem',
|
||||
'x-decorator-props': {
|
||||
name: 'markdown',
|
||||
engine: 'handlebars',
|
||||
},
|
||||
'x-component': 'Markdown.Void',
|
||||
'x-editable': false,
|
||||
|
@ -11,7 +11,7 @@ import { useField } from '@formily/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
|
||||
import { SchemaSettingsRenderEngine } from '../../../../schema-settings/SchemaSettingsRenderEngine';
|
||||
export const markdownBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:markdown',
|
||||
items: [
|
||||
@ -30,6 +30,10 @@ export const markdownBlockSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setBlockTemplate',
|
||||
Component: SchemaSettingsRenderEngine,
|
||||
},
|
||||
{
|
||||
name: 'setTheBlockHeight',
|
||||
Component: SchemaSettingsBlockHeightItem,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
SchemaSettingsDivider,
|
||||
SchemaSettingsItem,
|
||||
SchemaSettingsRemove,
|
||||
SchemaSettingsRenderEngine,
|
||||
} from '../../../schema-settings';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
|
||||
@ -29,6 +30,7 @@ export const MarkdownVoidDesigner = () => {
|
||||
field.editable = true;
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsRenderEngine />
|
||||
<SchemaSettingsBlockHeightItem />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
|
@ -13,20 +13,22 @@ import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import cls from 'classnames';
|
||||
import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useGlobalTheme } from '../../../global-theme';
|
||||
import { useDesignable } from '../../hooks/useDesignable';
|
||||
import { MarkdownVoidDesigner } from './Markdown.Void.Designer';
|
||||
import { useStyles } from './style';
|
||||
import { parseMarkdown } from './util';
|
||||
import { TextAreaProps } from 'antd/es/input';
|
||||
import { useBlockHeight } from '../../hooks/useBlockSize';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { useCollectionRecord } from '../../../data-source';
|
||||
import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
|
||||
import { VariableSelect } from '../variable/VariableSelect';
|
||||
import { replaceVariableValue } from '../../../block-provider/hooks';
|
||||
import { useLocalVariables, useVariables } from '../../../variables';
|
||||
import { registerQrcodeWebComponent } from './qrcode-webcom';
|
||||
import { getRenderContent } from '../../common/utils/uitls';
|
||||
import { parseMarkdown } from './util';
|
||||
import { useCompile } from '../../';
|
||||
export interface MarkdownEditorProps extends Omit<TextAreaProps, 'onSubmit'> {
|
||||
scope: any[];
|
||||
defaultValue?: string;
|
||||
@ -82,11 +84,19 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
|
||||
}}
|
||||
style={{ paddingBottom: '40px' }}
|
||||
/>
|
||||
<>
|
||||
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
|
||||
{t('Syntax references')}:
|
||||
</span>
|
||||
<a href="https://handlebarsjs.com/guide/" target="_blank" rel="noreferrer">
|
||||
Handlebars.js
|
||||
</a>
|
||||
</>
|
||||
<div style={{ position: 'absolute', top: 21, right: 1 }}>
|
||||
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} />
|
||||
</div>
|
||||
|
||||
<Space style={{ position: 'absolute', bottom: 5, right: 5 }}>
|
||||
<Space style={{ position: 'absolute', bottom: 25, right: 5 }}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
props.onCancel?.(e);
|
||||
@ -129,17 +139,27 @@ export const MarkdownVoid: any = withDynamicSchemaProps(
|
||||
const [html, setHtml] = useState('');
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables();
|
||||
const { engine } = schema?.['x-decorator-props'] || {};
|
||||
const [loading, setLoading] = useState(false);
|
||||
const compile = useCompile();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const cvtContentToHTML = async () => {
|
||||
const replacedContent = await replaceVariableValue(content, variables, localVariables);
|
||||
const html = await parseMarkdown(replacedContent);
|
||||
setHtml(html);
|
||||
const replacedContent = await getRenderContent(
|
||||
engine,
|
||||
content,
|
||||
compile(variables),
|
||||
compile(localVariables),
|
||||
parseMarkdown,
|
||||
);
|
||||
const sanitizedHtml = DOMPurify.sanitize(replacedContent);
|
||||
setHtml(sanitizedHtml);
|
||||
setLoading(false);
|
||||
};
|
||||
cvtContentToHTML();
|
||||
}, [content, variables, localVariables]);
|
||||
}, [content, variables, localVariables, engine]);
|
||||
|
||||
const height = useMarkdownHeight();
|
||||
const scope = useVariableOptions({
|
||||
collectionField: { uiSchema: schema },
|
||||
|
@ -22,10 +22,12 @@ export function useParseMarkdown(text: string) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
parseMarkdown(text).then((r) => {
|
||||
setHtml(r);
|
||||
setLoading(false);
|
||||
});
|
||||
parseMarkdown(text)
|
||||
.then((r) => {
|
||||
setHtml(r);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
}, [text]);
|
||||
return { html, loading };
|
||||
}
|
||||
|
@ -10,3 +10,4 @@
|
||||
export * from './dnd-context';
|
||||
export * from './sortable-item';
|
||||
export * from './show-form-data';
|
||||
export { getRenderContent } from './utils/uitls';
|
||||
|
@ -8,10 +8,12 @@
|
||||
*/
|
||||
|
||||
import _, { every, findIndex, some } from 'lodash';
|
||||
import Handlebars from 'handlebars';
|
||||
import { VariableOption, VariablesContextType } from '../../../variables/types';
|
||||
import { isVariable } from '../../../variables/utils/isVariable';
|
||||
import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
|
||||
import { getJsonLogic } from '../../common/utils/logic';
|
||||
import { replaceVariableValue } from '../../../block-provider/hooks';
|
||||
|
||||
type VariablesCtx = {
|
||||
/** 当前登录的用户 */
|
||||
@ -138,3 +140,32 @@ export function targetFieldToVariableString(targetField: string[]) {
|
||||
// Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单
|
||||
return `{{ $nForm.${targetField.join('.')} }}`;
|
||||
}
|
||||
|
||||
const getVariablesData = (localVariables) => {
|
||||
const data = {};
|
||||
localVariables.map((v) => {
|
||||
data[v.name] = v.ctx;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export async function getRenderContent(templateEngine, content, variables, localVariables, defaultParse) {
|
||||
if (content && templateEngine === 'handlebars') {
|
||||
const renderedContent = Handlebars.compile(content);
|
||||
// 处理渲染后的内容
|
||||
try {
|
||||
const data = getVariablesData(localVariables);
|
||||
return renderedContent({ ...variables.ctxRef.current, ...data });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return content;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const html = await replaceVariableValue(content, variables, localVariables);
|
||||
return await defaultParse(html);
|
||||
} catch (error) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Field } from '@formily/core';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTableBlockContext } from '../block-provider';
|
||||
import { useDesignable } from '../schema-component/hooks/useDesignable';
|
||||
import { SchemaSettingsSelectItem } from './SchemaSettings';
|
||||
|
||||
export function SchemaSettingsRenderEngine() {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const { service } = useTableBlockContext();
|
||||
const options = [
|
||||
{
|
||||
value: 'string',
|
||||
label: t('String template'),
|
||||
},
|
||||
{
|
||||
value: 'handlebars',
|
||||
label: t('Handlebars'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<SchemaSettingsSelectItem
|
||||
key="render-template"
|
||||
title={t('Set Template Engine')}
|
||||
options={options}
|
||||
value={field.decoratorProps.engine || 'string'}
|
||||
onChange={(engine) => {
|
||||
fieldSchema['x-decorator-props'].engine = engine;
|
||||
field.decoratorProps.engine = engine;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -22,6 +22,7 @@ export * from './SchemaSettingsTemplate';
|
||||
export * from './SchemaSettingsBlockHeightItem';
|
||||
export * from './setDefaultSortingRulesSchemaSettingsItem';
|
||||
export * from './setTheDataScopeSchemaSettingsItem';
|
||||
export * from './SchemaSettingsRenderEngine';
|
||||
export * from './hooks/useGetAriaLabelOfDesigner';
|
||||
export * from './hooks/useIsAllowToSetDefaultValue';
|
||||
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
|
||||
|
@ -29,7 +29,7 @@ export const IframeDesigner = () => {
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const api = useAPIClient();
|
||||
const { mode, url, htmlId, height = '60vh' } = fieldSchema['x-component-props'] || {};
|
||||
const { mode, url, htmlId, height = '60vh', engine } = fieldSchema['x-component-props'] || {};
|
||||
|
||||
const saveHtml = async (html: string) => {
|
||||
const options = {
|
||||
@ -46,11 +46,13 @@ export const IframeDesigner = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async ({ mode, url, html, height }) => {
|
||||
const submitHandler = async ({ mode, url, html, height, engine }) => {
|
||||
const componentProps = fieldSchema['x-component-props'] || {};
|
||||
componentProps['mode'] = mode;
|
||||
componentProps['height'] = height;
|
||||
componentProps['url'] = url;
|
||||
componentProps['engine'] = engine || 'string';
|
||||
|
||||
if (mode === 'html') {
|
||||
const data = await saveHtml(html);
|
||||
componentProps['htmlId'] = data.id;
|
||||
@ -125,6 +127,23 @@ export const IframeDesigner = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
engine: {
|
||||
title: '{{t("Template engine")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [
|
||||
{ value: 'string', label: t('String template') },
|
||||
{ value: 'handlebars', label: t('Handlebars') },
|
||||
],
|
||||
'x-reactions': {
|
||||
dependencies: ['mode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
hidden: '{{$deps[0] === "url"}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
html: {
|
||||
title: t('html'),
|
||||
type: 'string',
|
||||
|
@ -9,12 +9,13 @@
|
||||
|
||||
import { observer, useField } from '@formily/react';
|
||||
import {
|
||||
replaceVariableValue,
|
||||
useCompile,
|
||||
useBlockHeight,
|
||||
useLocalVariables,
|
||||
useParseURLAndParams,
|
||||
useRequest,
|
||||
useVariables,
|
||||
getRenderContent,
|
||||
} from '@nocobase/client';
|
||||
import { Card, Spin } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@ -32,13 +33,14 @@ function isNumeric(str: string | undefined) {
|
||||
}
|
||||
|
||||
export const Iframe: any = observer(
|
||||
(props: IIframe & { html?: string; htmlId?: number; mode: string; params?: any }) => {
|
||||
const { url, htmlId, mode = 'url', height, html, params, ...others } = props;
|
||||
(props: IIframe & { html?: string; htmlId?: number; mode: string; params?: any; engine?: string }) => {
|
||||
const { url, htmlId, mode = 'url', height, html, params, engine, ...others } = props;
|
||||
const field = useField();
|
||||
const { t } = useTranslation();
|
||||
const targetHeight = useBlockHeight() || height;
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables();
|
||||
const compile = useCompile();
|
||||
const { loading, data: htmlContent } = useRequest<string>(
|
||||
{
|
||||
url: `iframeHtml:getHtml/${htmlId}`,
|
||||
@ -50,13 +52,21 @@ export const Iframe: any = observer(
|
||||
);
|
||||
const { parseURLAndParams } = useParseURLAndParams();
|
||||
const [src, setSrc] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const generateSrc = async () => {
|
||||
if (mode === 'html') {
|
||||
const targetHtmlContent = await replaceVariableValue(htmlContent, variables, localVariables);
|
||||
const targetHtmlContent = await getRenderContent(
|
||||
engine,
|
||||
htmlContent,
|
||||
compile(variables),
|
||||
compile(localVariables),
|
||||
(data) => {
|
||||
return data;
|
||||
},
|
||||
);
|
||||
const encodedHtml = encodeURIComponent(targetHtmlContent);
|
||||
const dataUrl = 'data:text/html;charset=utf-8,' + encodedHtml;
|
||||
|
||||
setSrc(dataUrl);
|
||||
} else {
|
||||
try {
|
||||
|
@ -50,7 +50,7 @@ const commonOptions: any = {
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const api = useAPIClient();
|
||||
const { mode, url, params, htmlId, height = '60vh' } = fieldSchema['x-component-props'] || {};
|
||||
const { mode, url, params, htmlId, height = '60vh', engine } = fieldSchema['x-component-props'] || {};
|
||||
const saveHtml = async (html: string) => {
|
||||
const options = {
|
||||
values: { html },
|
||||
@ -66,10 +66,11 @@ const commonOptions: any = {
|
||||
}
|
||||
};
|
||||
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||
const submitHandler = async ({ mode, url, html, height, params }) => {
|
||||
const submitHandler = async ({ mode, url, html, height, params, engine }) => {
|
||||
const componentProps = fieldSchema['x-component-props'] || {};
|
||||
componentProps['mode'] = mode;
|
||||
componentProps['height'] = height;
|
||||
componentProps['engine'] = engine || 'string';
|
||||
componentProps['params'] = params;
|
||||
componentProps['url'] = url;
|
||||
if (mode === 'html') {
|
||||
@ -94,6 +95,7 @@ const commonOptions: any = {
|
||||
mode,
|
||||
url,
|
||||
height,
|
||||
engine,
|
||||
params,
|
||||
};
|
||||
if (htmlId) {
|
||||
@ -123,15 +125,42 @@ const commonOptions: any = {
|
||||
required: true,
|
||||
},
|
||||
params: paramsSchema,
|
||||
html: {
|
||||
title: t('html'),
|
||||
type: 'string',
|
||||
engine: {
|
||||
title: '{{t("Template engine")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': getVariableComponentWithScope(Variable.RawTextArea),
|
||||
'x-component-props': {
|
||||
rows: 10,
|
||||
},
|
||||
required: true,
|
||||
enum: [
|
||||
{ value: 'string', label: t('String template') },
|
||||
{ value: 'handlebars', label: t('Handlebars') },
|
||||
],
|
||||
'x-reactions': {
|
||||
dependencies: ['mode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
hidden: '{{$deps[0] === "url"}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
html: {
|
||||
title: t('html'),
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': getVariableComponentWithScope(Variable.RawTextArea),
|
||||
'x-component-props': {
|
||||
rows: 10,
|
||||
},
|
||||
required: true,
|
||||
description: (
|
||||
<>
|
||||
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
|
||||
{t('Syntax references')}:
|
||||
</span>
|
||||
<a href="https://handlebarsjs.com/guide/" target="_blank" rel="noreferrer">
|
||||
Handlebars.js
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
'x-reactions': {
|
||||
dependencies: ['mode'],
|
||||
fulfill: {
|
||||
@ -141,13 +170,6 @@ const commonOptions: any = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// height: {
|
||||
// title: t('Height'),
|
||||
// type: 'string',
|
||||
// 'x-decorator': 'FormItem',
|
||||
// 'x-component': 'Input',
|
||||
// required: true,
|
||||
// },
|
||||
},
|
||||
} as ISchema,
|
||||
onSubmit: submitHandler,
|
||||
|
Loading…
Reference in New Issue
Block a user