diff --git a/client/web/src/components/Markdown.tsx b/client/web/src/components/Markdown.tsx index d7ce6ed3..eb5a1135 100644 --- a/client/web/src/components/Markdown.tsx +++ b/client/web/src/components/Markdown.tsx @@ -41,6 +41,7 @@ export const Markdown: React.FC<{ className="tailchat-markdown" transformImageUri={(src) => transformUrl(src)} transformLinkUri={(href) => transformUrl(href)} + linkTarget="_blank" skipHtml={true} components={components} > diff --git a/client/web/src/components/Modal.tsx b/client/web/src/components/Modal.tsx index 87a80e46..afc1f4a4 100644 --- a/client/web/src/components/Modal.tsx +++ b/client/web/src/components/Modal.tsx @@ -141,17 +141,20 @@ export function closeModal(key?: number): void { export function openModal( content: React.ReactNode, props?: Pick & { - onCloseModal?: () => void; + onCloseModal?: () => void | Promise; } ): number { const key = PortalAdd( { + onChangeVisible={async (visible) => { if (visible === false) { + if (typeof props?.onCloseModal === 'function') { + await props.onCloseModal(); + } + closeModal(key); - props?.onCloseModal?.(); } }} > diff --git a/client/web/src/components/Panel/group/GroupExtraDataPanel.tsx b/client/web/src/components/Panel/group/GroupExtraDataPanel.tsx new file mode 100644 index 00000000..4f473911 --- /dev/null +++ b/client/web/src/components/Panel/group/GroupExtraDataPanel.tsx @@ -0,0 +1,149 @@ +import { useGroupPanelContext } from '@/context/GroupPanelContext'; +import React, { useReducer, useRef } from 'react'; +import { + model, + PERMISSION, + showSuccessToasts, + showToasts, + t, + useAsync, + useHasGroupPermission, + useMemoizedFn, +} from 'tailchat-shared'; +import { Problem } from '@/components/Problem'; +import { ErrorView } from '@/components/ErrorView'; +import { Loading } from '@/components/Loading'; +import { GroupPanelContainer } from './shared/GroupPanelContainer'; +import { IconBtn } from '@/components/IconBtn'; +import { openModal } from '@/components/Modal'; +import _isEqual from 'lodash/isEqual'; + +type GroupExtraDataPanelRenderInfo = Record; // + +interface GroupExtraDataPanelInnerProps extends GroupExtraDataPanelProps { + groupId: string; + panelId: string; +} + +/** + * A Component which can edit panel and display with group extra data + */ +const GroupExtraDataPanelInner: React.FC = + React.memo((props) => { + const { groupId, panelId, names } = props; + const [updateIndex, updateInfo] = useReducer((state) => state + 1, 0); + const { + value: info = {}, + loading, + error, + } = useAsync(async () => { + const list = await Promise.all( + names.map((name) => + model.group + .getGroupPanelExtraData(props.groupId, props.panelId, name) + .then((data) => ({ + name, + data, + })) + ) + ); + + return list.reduce((prev, curr) => { + return { + ...prev, + [curr.name]: curr.data, + }; + }, {}); + }, [groupId, panelId, names, updateIndex]); + + const [hasPermission] = useHasGroupPermission(groupId, [ + PERMISSION.core.managePanel, + ]); + + const savingRef = useRef(false); + const handleEdit = useMemoizedFn(() => { + const dataMap = { ...info }; + if (!props.renderEdit) { + console.warn('[GroupExtraDataPanel] Please set renderEdit'); + return; + } + + const handleSave = async () => { + if (savingRef.current === true) { + showToasts(t('正在保存, 请稍后')); + return; + } + + if (!_isEqual(dataMap, info)) { + savingRef.current = true; + await Promise.all( + Object.entries(dataMap).map(([name, data]) => + model.group.saveGroupPanelExtraData(groupId, panelId, name, data) + ) + ); + + savingRef.current = false; + updateInfo(); + + showSuccessToasts(); + } + }; + + openModal(props.renderEdit(dataMap), { + onCloseModal: handleSave, + }); + }); + + if (error) { + return ; + } + + return ( + + hasPermission + ? [ + , + ] + : [] + } + > + +
{props.render(info)}
+
+
+ ); + }); +GroupExtraDataPanelInner.displayName = 'GroupExtraDataPanelInner'; + +interface GroupExtraDataPanelProps { + names: string[]; + renderEdit: (dataMap: Record) => React.ReactNode; + render: (info: GroupExtraDataPanelRenderInfo) => React.ReactNode; +} +export const GroupExtraDataPanel: React.FC = + React.memo((props) => { + const context = useGroupPanelContext(); + + if (context === null) { + return ; + } + + return ( + + ); + }); +GroupExtraDataPanel.displayName = 'GroupExtraDataPanel'; diff --git a/client/web/src/components/Panel/group/PluginPanel.tsx b/client/web/src/components/Panel/group/PluginPanel.tsx index 8477087e..60945946 100644 --- a/client/web/src/components/Panel/group/PluginPanel.tsx +++ b/client/web/src/components/Panel/group/PluginPanel.tsx @@ -1,3 +1,4 @@ +import { Problem } from '@/components/Problem'; import { findPluginPanelInfoByName } from '@/utils/plugin-helper'; import { Alert } from 'antd'; import React, { useMemo } from 'react'; @@ -64,7 +65,7 @@ export const GroupPluginPanel: React.FC = React.memo( if (!Component) { // 没有找到插件组件 // TODO: Fallback - return null; + return ; } return ; diff --git a/client/web/src/plugin/builtin.ts b/client/web/src/plugin/builtin.ts index 557306eb..12462c0d 100644 --- a/client/web/src/plugin/builtin.ts +++ b/client/web/src/plugin/builtin.ts @@ -59,6 +59,17 @@ export const builtinPlugins: PluginManifest[] = _compact([ 'description.zh-CN': '为应用首次打开介绍应用的能力', requireRestart: true, }, + { + label: 'Markdown Panel', + 'label.zh-CN': 'Markdown 面板', + name: 'com.msgbyte.mdpanel', + url: '/plugins/com.msgbyte.mdpanel/index.js', + version: '0.0.0', + author: 'moonrailgun', + description: 'Add markdown panel support', + 'description.zh-CN': '增加 Markdown 面板支持', + requireRestart: true, + }, // isOffical isOffical && { label: 'Posthog', diff --git a/client/web/src/plugin/component/index.tsx b/client/web/src/plugin/component/index.tsx index 90ad1356..bad4e637 100644 --- a/client/web/src/plugin/component/index.tsx +++ b/client/web/src/plugin/component/index.tsx @@ -30,6 +30,7 @@ export { export { Link } from 'react-router-dom'; export { MessageAckContainer } from '@/components/ChatBox/ChatMessageList/MessageAckContainer'; +export { GroupExtraDataPanel } from '@/components/Panel/group/GroupExtraDataPanel'; export { Image } from '@/components/Image'; export { IconBtn } from '@/components/IconBtn'; export { PillTabs, PillTabPane } from '@/components/PillTabs'; diff --git a/client/web/src/routes/Main/Content/Group/Panel.tsx b/client/web/src/routes/Main/Content/Group/Panel.tsx index b582aa95..0db5e5b1 100644 --- a/client/web/src/routes/Main/Content/Group/Panel.tsx +++ b/client/web/src/routes/Main/Content/Group/Panel.tsx @@ -74,7 +74,7 @@ export const GroupPanelRender: React.FC = React.memo( if (panelInfo.type === GroupPanelType.PLUGIN) { return ( - ; + ); } diff --git a/server/services/core/group/groupExtra.service.ts b/server/services/core/group/groupExtra.service.ts index e840719e..2777d0bc 100644 --- a/server/services/core/group/groupExtra.service.ts +++ b/server/services/core/group/groupExtra.service.ts @@ -48,7 +48,7 @@ class GroupExtraService extends TcService { name, }); - return res?.data ?? null; + return { data: res?.data ?? null }; } async savePanelData( @@ -69,6 +69,9 @@ class GroupExtraService extends TcService { }, { data: String(data), + }, + { + upsert: true, // Create if not exist } );