Skip to content

Commit 1ce40e7

Browse files
authored
Merge pull request #1790 from merico-dev/rich-text
rich text
2 parents af1a814 + 75a7feb commit 1ce40e7

File tree

14 files changed

+412
-18
lines changed

14 files changed

+412
-18
lines changed

dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtable/dashboard",
3-
"version": "14.58.1",
3+
"version": "14.58.2",
44
"license": "Apache-2.0",
55
"repository": {
66
"url": "https://github.com/merico-dev/table"

dashboard/src/components/plugins/viz-components/rich-text/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DEFAULT_CONFIG, IRichTextConf } from './type';
55
import { VizRichText } from './viz-rich-text';
66
import { VizRichTextEditor } from './viz-rich-text-editor';
77
import { translation } from './translation';
8+
import { ClickRichTextBlock } from './triggers';
89

910
class VizRichTextMigrator extends VersionBasedMigrator {
1011
readonly VERSION = 1;
@@ -36,5 +37,6 @@ export const RichTextVizComponent: VizComponent = {
3637
config: cloneDeep(DEFAULT_CONFIG) as IRichTextConf,
3738
};
3839
},
40+
triggers: [ClickRichTextBlock],
3941
translation,
4042
};

dashboard/src/components/plugins/viz-components/rich-text/translation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,26 @@ import { TranslationPatch } from '~/types/plugin';
33
const en = {
44
rich_text: {
55
viz_name: 'Rich Text',
6+
click: {
7+
label: 'Click Rich Text Block',
8+
block_id_label: 'Block ID',
9+
block_id_description: 'Optional identifier for the clicked block',
10+
block_id_placeholder: 'Enter block ID',
11+
click_block_with_id: 'Click Rich Text Block: {{blockId}}',
12+
},
613
},
714
};
815

916
const zh = {
1017
rich_text: {
1118
viz_name: '富文本',
19+
click: {
20+
label: '点击富文本块',
21+
block_id_label: '块ID',
22+
block_id_description: '被点击块的可选标识符',
23+
block_id_placeholder: '输入块ID',
24+
click_block_with_id: '点击富文本块: {{blockId}}',
25+
},
1226
},
1327
};
1428

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Text, TextInput } from '@mantine/core';
2+
import { defaults } from 'lodash';
3+
import { useTranslation } from 'react-i18next';
4+
import { useStorageData } from '~/components/plugins';
5+
import { ITriggerConfigProps, ITriggerSchema } from '~/types/plugin';
6+
7+
export const ClickRichTextBlock: ITriggerSchema = {
8+
id: 'builtin:rich-text:click-rich-text-block',
9+
displayName: 'viz.rich_text.click.label',
10+
nameRender: ClickRichTextBlockName,
11+
configRender: ClickRichTextBlockSettings,
12+
payload: [
13+
{
14+
name: 'variables',
15+
description: 'Panel variables',
16+
valueType: 'object',
17+
},
18+
],
19+
};
20+
21+
export interface IClickRichTextBlockConfig {
22+
blockId: string;
23+
}
24+
25+
const DEFAULT_CONFIG: IClickRichTextBlockConfig = {
26+
blockId: '',
27+
};
28+
29+
export function ClickRichTextBlockSettings(props: ITriggerConfigProps) {
30+
const { t } = useTranslation();
31+
const { value: config, set: setConfig } = useStorageData<IClickRichTextBlockConfig>(
32+
props.trigger.triggerData,
33+
'config',
34+
);
35+
const { blockId } = defaults({}, config, DEFAULT_CONFIG);
36+
37+
const handleBlockIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
38+
void setConfig({ blockId: e.currentTarget.value });
39+
};
40+
41+
return (
42+
<TextInput
43+
label={t('viz.rich_text.click.block_id_label')}
44+
description={t('viz.rich_text.click.block_id_description')}
45+
value={blockId}
46+
onChange={handleBlockIdChange}
47+
placeholder={t('viz.rich_text.click.block_id_placeholder')}
48+
/>
49+
);
50+
}
51+
52+
function ClickRichTextBlockName(props: Omit<ITriggerConfigProps, 'sampleData'>) {
53+
const { t } = useTranslation();
54+
const { value: config } = useStorageData<IClickRichTextBlockConfig>(props.trigger.triggerData, 'config');
55+
const { blockId } = defaults({}, config, DEFAULT_CONFIG);
56+
57+
if (blockId) {
58+
return <Text size="sm">{t('viz.rich_text.click.click_block_with_id', { blockId })}</Text>;
59+
}
60+
61+
return <Text size="sm">{t('viz.rich_text.click.label')}</Text>;
62+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './click-rich-text-block';

dashboard/src/components/plugins/viz-components/rich-text/viz-rich-text.tsx

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@ import { useMemo } from 'react';
44
import { useStorageData } from '~/components/plugins/hooks';
55
import { ReadonlyRichText } from '~/components/widgets/rich-text-editor/readonly-rich-text-editor';
66
import { useRenderContentModelContext, useRenderPanelContext } from '~/contexts';
7+
import { useCurrentInteractionManager, useTriggerSnapshotList } from '~/interactions';
78
import { VizViewProps } from '~/types/plugin';
89
import { parseRichTextContent } from '~/utils';
10+
import { ClickRichTextBlock } from './triggers';
11+
import { IClickRichTextBlockConfig } from './triggers/click-rich-text-block';
912
import { DEFAULT_CONFIG, IRichTextConf } from './type';
1013

11-
export const VizRichText = observer(({ context }: VizViewProps) => {
14+
export const VizRichText = observer(({ context, instance }: VizViewProps) => {
15+
const interactionManager = useCurrentInteractionManager({
16+
vizManager: context.vizManager,
17+
instance,
18+
});
19+
const triggers = useTriggerSnapshotList<IClickRichTextBlockConfig>(
20+
interactionManager.triggerManager,
21+
ClickRichTextBlock.id,
22+
);
1223
const contentModel = useRenderContentModelContext();
1324
const { panel } = useRenderPanelContext();
1425
const { value: confValue } = useStorageData<IRichTextConf>(context.instanceData, 'config');
@@ -26,20 +37,59 @@ export const VizRichText = observer(({ context }: VizViewProps) => {
2637
return null;
2738
}
2839

40+
function handleClick(ev: React.MouseEvent) {
41+
// implement the interaction block click handler with event delegation
42+
const target = ev.target as HTMLElement;
43+
const interactionBlock = target.closest('[data-interaction-block-id]') as HTMLElement;
44+
45+
const payload = { variables: panel.variableValueMap };
46+
47+
if (interactionBlock) {
48+
const blockId = interactionBlock.getAttribute('data-interaction-block-id');
49+
50+
// Find triggers that match this blockId
51+
const matchingTriggers = triggers.filter((t) => {
52+
return t.config?.blockId === blockId;
53+
});
54+
55+
// Run matching triggers
56+
matchingTriggers.forEach((t) => {
57+
void interactionManager.runInteraction(t.id, payload);
58+
});
59+
60+
// If there are matching triggers, prevent default and stop propagation
61+
if (matchingTriggers.length > 0) {
62+
ev.preventDefault();
63+
ev.stopPropagation();
64+
}
65+
} else {
66+
// Run triggers without blockId (global click handlers)
67+
const globalTriggers = triggers.filter((t) => {
68+
return !t.config?.blockId;
69+
});
70+
71+
globalTriggers.forEach((t) => {
72+
void interactionManager.runInteraction(t.id, payload);
73+
});
74+
}
75+
}
76+
2977
return (
30-
<ReadonlyRichText
31-
value={content}
32-
styles={{
33-
root: {
34-
border: 'none',
35-
height: '100%',
36-
},
37-
content: {
38-
padding: 0,
39-
},
40-
}}
41-
dashboardStateValues={contentModel.dashboardStateValues}
42-
variableAggValueMap={panel.variableAggValueMap}
43-
/>
78+
<div onClick={handleClick}>
79+
<ReadonlyRichText
80+
value={content}
81+
styles={{
82+
root: {
83+
border: 'none',
84+
height: '100%',
85+
},
86+
content: {
87+
padding: 0,
88+
},
89+
}}
90+
dashboardStateValues={contentModel.dashboardStateValues}
91+
variableAggValueMap={panel.variableAggValueMap}
92+
/>
93+
</div>
4494
);
4595
});

dashboard/src/components/widgets/rich-text-editor/custom-rich-text-editor.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import { ColorMappingControl, ColorMappingMark } from './color-mapping-mark';
2525
import { ColorPickerControl } from './color-picker-control';
2626
import { DynamicColorControl, DynamicColorMark } from './dynamic-color-mark';
2727
import { ChooseFontSize, FontSize } from './font-size-extension';
28+
import { InteractionBlock } from './interaction-block-node';
29+
import {
30+
ClearInteractionBlockControl,
31+
InteractionBlockControl,
32+
} from './interaction-block-node/interaction-block-control';
2833

2934
const RTEContentStyle: EmotionSx = {
3035
'dynamic-color': {
@@ -78,7 +83,7 @@ interface ICustomRichTextEditor {
7883
}
7984

8085
export const CustomRichTextEditor = forwardRef(
81-
({ value, onChange, styles = {}, label, onSubmit, onCancel }: ICustomRichTextEditor, ref: any) => {
86+
({ value, onChange, styles = {}, label, onSubmit, onCancel }: ICustomRichTextEditor) => {
8287
const { t } = useTranslation();
8388
const inPanelContext = useIsInEditPanelContext();
8489
const extensions: Extensions = useMemo(() => {
@@ -104,6 +109,7 @@ export const CustomRichTextEditor = forwardRef(
104109
Color,
105110
FontSize,
106111
DynamicColorMark,
112+
InteractionBlock,
107113
];
108114
if (inPanelContext) {
109115
ret.push(ColorMappingMark);
@@ -177,6 +183,10 @@ export const CustomRichTextEditor = forwardRef(
177183
<RichTextEditor.ControlsGroup>
178184
<DynamicColorControl editor={editor} />
179185
</RichTextEditor.ControlsGroup>
186+
<RichTextEditor.ControlsGroup>
187+
<InteractionBlockControl />
188+
<ClearInteractionBlockControl />
189+
</RichTextEditor.ControlsGroup>
180190
<RichTextEditor.ControlsGroup>
181191
<RichTextEditor.Bold />
182192
<RichTextEditor.Italic />
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// create custom block node for tiptap editor
2+
// it will wrap its content with a div[data-interaction-block-id=xxxx]
3+
4+
import { Node, mergeAttributes } from '@tiptap/core';
5+
6+
export interface InteractionBlockOptions {
7+
HTMLAttributes: Record<string, any>;
8+
}
9+
10+
declare module '@tiptap/core' {
11+
interface Commands<ReturnType> {
12+
interactionBlock: {
13+
/**
14+
* Set an interaction block with a specific ID
15+
*/
16+
setInteractionBlock: (blockId: string) => ReturnType;
17+
/**
18+
* Toggle an interaction block
19+
*/
20+
toggleInteractionBlock: (blockId: string) => ReturnType;
21+
/**
22+
* Unset an interaction block
23+
*/
24+
unsetInteractionBlock: () => ReturnType;
25+
};
26+
}
27+
}
28+
29+
export const InteractionBlock = Node.create<InteractionBlockOptions>({
30+
name: 'interactionBlock',
31+
32+
group: 'block',
33+
34+
content: 'block+',
35+
36+
defining: true,
37+
38+
addOptions() {
39+
return {
40+
HTMLAttributes: {},
41+
};
42+
},
43+
44+
addAttributes() {
45+
return {
46+
blockId: {
47+
default: null,
48+
parseHTML: (element) => element.getAttribute('data-interaction-block-id'),
49+
renderHTML: (attributes) => {
50+
if (!attributes.blockId) {
51+
return {};
52+
}
53+
return {
54+
'data-interaction-block-id': attributes.blockId,
55+
};
56+
},
57+
},
58+
};
59+
},
60+
61+
parseHTML() {
62+
return [
63+
{
64+
tag: 'div[data-interaction-block-id]',
65+
},
66+
];
67+
},
68+
69+
renderHTML({ HTMLAttributes }) {
70+
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
71+
},
72+
73+
addCommands() {
74+
return {
75+
setInteractionBlock:
76+
(blockId: string) =>
77+
({ commands }) => {
78+
return commands.wrapIn(this.name, { blockId });
79+
},
80+
toggleInteractionBlock:
81+
(blockId: string) =>
82+
({ commands }) => {
83+
return commands.toggleWrap(this.name, { blockId });
84+
},
85+
unsetInteractionBlock:
86+
() =>
87+
({ commands }) => {
88+
return commands.lift(this.name);
89+
},
90+
};
91+
},
92+
93+
addKeyboardShortcuts() {
94+
return {
95+
'Mod-Alt-i': () => this.editor.commands.toggleInteractionBlock(''),
96+
};
97+
},
98+
});

0 commit comments

Comments
 (0)