diff --git a/core/actions/_lib/types.ts b/core/actions/_lib/zodTypes.ts similarity index 100% rename from core/actions/_lib/types.ts rename to core/actions/_lib/zodTypes.ts diff --git a/core/actions/email/action.ts b/core/actions/email/action.ts index 24a034596..05ee43c09 100644 --- a/core/actions/email/action.ts +++ b/core/actions/email/action.ts @@ -2,7 +2,7 @@ import * as z from "zod"; import { Mail } from "ui/icon"; -import { markdown } from "../_lib/types"; +import { markdown } from "../_lib/zodTypes"; import * as corePubFields from "../corePubFields"; import { defineAction } from "../types"; @@ -34,7 +34,7 @@ export const action = defineAction({ .optional(), }) .optional(), - pubFields: [corePubFields.title], + pubFields: [], icon: Mail, }); diff --git a/packages/ui/package.json b/packages/ui/package.json index 5f56d7fff..def5e75ee 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -250,6 +250,7 @@ "@lexical/markdown": "^0.15.0", "@lexical/react": "^0.15.0", "@lexical/rich-text": "^0.15.0", + "@lexical/utils": "^0.15.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.3", diff --git a/packages/ui/src/auto-form/fields/markdown/MarkdownEditor.tsx b/packages/ui/src/auto-form/fields/markdown/MarkdownEditor.tsx index 4dae135d3..aae96c94d 100644 --- a/packages/ui/src/auto-form/fields/markdown/MarkdownEditor.tsx +++ b/packages/ui/src/auto-form/fields/markdown/MarkdownEditor.tsx @@ -19,9 +19,20 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; import { EditorState } from "lexical"; +import { cn } from "utils"; + +import { FormControl, FormItem, FormMessage } from "../../../form"; +import AutoFormDescription from "../../common/description"; +import AutoFormLabel from "../../common/label"; +import AutoFormTooltip from "../../common/tooltip"; import { AutoFormInputComponentProps } from "../../types"; +import { TokenProvider } from "./TokenContext"; +import { TokenNode } from "./TokenNode"; +import { TokenPlugin } from "./TokenPlugin"; -const theme = {}; +const theme = { + token: "token", +}; function onError(error: unknown) { console.error(error); @@ -35,6 +46,7 @@ const NODES = [ ListNode, ListItemNode, QuoteNode, + TokenNode, ]; const makeSyntheticChangeEvent = (value: string) => { @@ -46,6 +58,8 @@ const makeSyntheticChangeEvent = (value: string) => { }; export const MarkdownEditor = (props: AutoFormInputComponentProps) => { + const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = props.fieldProps; + const showLabel = _showLabel === undefined ? true : _showLabel; const initialValue = React.useMemo(() => props.field.value ?? "", []); const initialConfig = React.useMemo(() => { return { @@ -61,23 +75,60 @@ export const MarkdownEditor = (props: AutoFormInputComponentProps) => { (editorState: EditorState) => { editorState.read(() => { const markdown = $convertToMarkdownString(TRANSFORMERS); - props.fieldProps.onChange(makeSyntheticChangeEvent(markdown)); + fieldPropsWithoutShowLabel.onChange(makeSyntheticChangeEvent(markdown)); }); }, - [props.fieldProps.onChange] + [fieldPropsWithoutShowLabel.onChange] ); return ( - - } - placeholder={
Enter some text...
} - ErrorBoundary={LexicalErrorBoundary} - /> - - - - -
+
+ + {showLabel && ( + <> + + {props.description && ( + + )} + + )} + + + + + } + placeholder={null} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + + + + + +
); }; diff --git a/packages/ui/src/auto-form/fields/markdown/TokenContext.tsx b/packages/ui/src/auto-form/fields/markdown/TokenContext.tsx new file mode 100644 index 000000000..0f9bf720a --- /dev/null +++ b/packages/ui/src/auto-form/fields/markdown/TokenContext.tsx @@ -0,0 +1,19 @@ +import React, { createContext, PropsWithChildren, useContext } from "react"; + +export type TokenContext = { + staticTokens: string[]; + dynamicTokens: RegExp | null; +}; + +export const TokenContext = createContext({ + staticTokens: [], + dynamicTokens: /^.$/, +}); + +export const useTokenContext = () => { + return useContext(TokenContext); +}; + +export function TokenProvider(props: PropsWithChildren) { + return {props.children}; +} diff --git a/packages/ui/src/auto-form/fields/markdown/TokenNode.ts b/packages/ui/src/auto-form/fields/markdown/TokenNode.ts new file mode 100644 index 000000000..1f790ec9b --- /dev/null +++ b/packages/ui/src/auto-form/fields/markdown/TokenNode.ts @@ -0,0 +1,57 @@ +import type { EditorConfig, LexicalNode, NodeKey, SerializedTextNode } from "lexical"; + +import { addClassNamesToElement } from "@lexical/utils"; +import { $applyNodeReplacement, TextNode } from "lexical"; + +export class TokenNode extends TextNode { + static getType(): string { + return "token"; + } + + static clone(node: TokenNode): TokenNode { + return new TokenNode(node.__text, node.__key); + } + + constructor(text: string, key?: NodeKey) { + super(text, key); + console.log(text, key); + } + + createDOM(config: EditorConfig): HTMLElement { + const element = super.createDOM(config); + addClassNamesToElement(element, config.theme.token); + return element; + } + + static importJSON(serializedNode: SerializedTextNode): TokenNode { + const node = $createTokenNode(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + exportJSON(): SerializedTextNode { + return { + ...super.exportJSON(), + type: "token", + }; + } + + canInsertTextBefore(): boolean { + return false; + } + + isTextEntity(): true { + return true; + } +} + +export function $createTokenNode(text = ""): TokenNode { + return $applyNodeReplacement(new TokenNode(text)); +} + +export function $isTokenNode(node: LexicalNode | null | undefined): node is TokenNode { + return node instanceof TokenNode; +} diff --git a/packages/ui/src/auto-form/fields/markdown/TokenPlugin.tsx b/packages/ui/src/auto-form/fields/markdown/TokenPlugin.tsx new file mode 100644 index 000000000..4dc2af690 --- /dev/null +++ b/packages/ui/src/auto-form/fields/markdown/TokenPlugin.tsx @@ -0,0 +1,50 @@ +import { useCallback, useEffect, useMemo } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity"; +import { TextNode } from "lexical"; + +import { useTokenContext } from "./TokenContext"; +import { $createTokenNode, TokenNode } from "./TokenNode"; + +const boundary = "^|$|[^&/" + "*" + "]"; + +const $createTokenNode_ = (textNode: TextNode): TokenNode => { + return $createTokenNode(textNode.getTextContent()); +}; + +export function TokenPlugin() { + const [editor] = useLexicalComposerContext(); + const { staticTokens, dynamicTokens } = useTokenContext(); + + const REGEX = useMemo( + () => new RegExp(`(${boundary})\{(${staticTokens.join("|")})\}`, "i"), + [staticTokens] + ); + + useEffect(() => { + if (!editor.hasNodes([TokenNode])) { + throw new Error("TokenPlugin: TokenNode not registered on editor"); + } + }, [editor]); + + const getTokenMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text); + + if (matchArr === null) { + return null; + } + + const tokenLength = matchArr[2].length + 2; // add two for the curly braces + const startOffset = matchArr.index + matchArr[1].length; + const endOffset = startOffset + tokenLength; + + return { + end: endOffset, + start: startOffset, + }; + }, []); + + useLexicalTextEntity(getTokenMatch, TokenNode, $createTokenNode_); + + return null; +} diff --git a/packages/ui/styles.css b/packages/ui/styles.css index 6a310cad4..f830cc6d9 100644 --- a/packages/ui/styles.css +++ b/packages/ui/styles.css @@ -49,3 +49,11 @@ @tailwind base; @tailwind components; @tailwind utilities; + +.editor .token { + color: blue; +} + +.editor.markdown { + min-height: 200px; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dae963e2..bd6bb76ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -697,6 +697,9 @@ importers: '@lexical/rich-text': specifier: ^0.15.0 version: 0.15.0 + '@lexical/utils': + specifier: ^0.15.0 + version: 0.15.0 '@radix-ui/react-accordion': specifier: ^1.1.2 version: 1.1.2(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)