diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index d1f1ef031..c32b04747 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -117,7 +117,6 @@ impl ApiHandler for MonoApiService { Ok(()) } - fn strip_relative(&self, path: &Path) -> Result { Ok(path.to_path_buf()) } @@ -326,6 +325,23 @@ impl MonoApiService { Ok(()) } + pub async fn comment(&self, mr_link: &str, comment: String) -> Result<(), MegaError> { + let storage = self.context.services.mono_storage.clone(); + if let Some(model) = storage.get_mr(mr_link).await.unwrap() { + storage + .add_mr_conversation(&model.mr_link, 0, ConvType::Comment, Some(comment)) + .await + .unwrap(); + } + Ok(()) + } + + pub async fn delete_comment(&self, id: i64) -> Result<(), MegaError> { + let storage = self.context.services.mono_storage.clone(); + storage.remove_mr_conversation(id).await.unwrap(); + Ok(()) + } + async fn update_parent_tree( &self, mut path: PathBuf, diff --git a/ceres/src/model/mr.rs b/ceres/src/model/mr.rs index f441b257a..74b240562 100644 --- a/ceres/src/model/mr.rs +++ b/ceres/src/model/mr.rs @@ -38,6 +38,7 @@ pub struct MRDetail { #[derive(Serialize, Deserialize)] pub struct MRConversion { + pub id: i64, pub user_id: i64, pub conv_type: String, pub comment: Option, @@ -62,6 +63,7 @@ impl From for MRDetail { impl From for MRConversion { fn from(value: mega_mr_conv::Model) -> Self { Self { + id: value.id, user_id: value.user_id, conv_type: value.conv_type.to_string(), comment: value.comment, diff --git a/docker/mono-pg-dockerfile b/docker/mono-pg-dockerfile index 4804d1199..26fd0bb5f 100644 --- a/docker/mono-pg-dockerfile +++ b/docker/mono-pg-dockerfile @@ -17,6 +17,6 @@ EXPOSE 5432 # Add the database initialization script to the container # When the container starts, PostgreSQL will automatically execute all .sql files in the docker-entrypoint-initdb.d/ directory -COPY ./sql/postgres/pg_20240912__init.sql /docker-entrypoint-initdb.d/ +COPY ./sql/postgres/pg_20240923__init.sql /docker-entrypoint-initdb.d/ CMD ["postgres"] diff --git a/jupiter/src/storage/mono_storage.rs b/jupiter/src/storage/mono_storage.rs index dfe66730c..11b9a7ad3 100644 --- a/jupiter/src/storage/mono_storage.rs +++ b/jupiter/src/storage/mono_storage.rs @@ -9,8 +9,7 @@ use sea_orm::{ use callisto::db_enums::{ConvType, MergeStatus}; use callisto::{ - mega_blob, mega_commit, mega_mr, mega_mr_conv, mega_refs, mega_tag, mega_tree, - raw_blob, + mega_blob, mega_commit, mega_mr, mega_mr_conv, mega_refs, mega_tag, mega_tree, raw_blob, }; use common::errors::MegaError; use common::utils::generate_id; @@ -103,7 +102,10 @@ impl MonoStorage { Ok(()) } - pub async fn get_open_mr_by_path(&self, path: &str) -> Result, MegaError> { + pub async fn get_open_mr_by_path( + &self, + path: &str, + ) -> Result, MegaError> { let model = mega_mr::Entity::find() .filter(mega_mr::Column::Path.eq(path)) .filter(mega_mr::Column::Status.eq(MergeStatus::Open)) @@ -173,6 +175,14 @@ impl MonoStorage { Ok(model?) } + pub async fn remove_mr_conversation(&self, id: i64) -> Result<(), MegaError> { + mega_mr_conv::Entity::delete_by_id(id) + .exec(self.get_connection()) + .await + .unwrap(); + Ok(()) + } + pub async fn add_mr_conversation( &self, mr_link: &str, diff --git a/mono/src/api/mr_router.rs b/mono/src/api/mr_router.rs index 93d645855..e01bcb06e 100644 --- a/mono/src/api/mr_router.rs +++ b/mono/src/api/mr_router.rs @@ -6,6 +6,8 @@ use axum::{ Json, Router, }; +use bytes::Bytes; + use ceres::model::mr::{MRDetail, MrInfoItem}; use common::model::CommonResult; use taurus::event::api_request::{ApiRequestEvent, ApiType}; @@ -19,6 +21,8 @@ pub fn routers() -> Router { .route("/mr/:mr_link/detail", get(mr_detail)) .route("/mr/:mr_link/merge", post(merge)) .route("/mr/:mr_link/files", get(get_mr_files)) + .route("/mr/:mr_link/comment", post(save_comment)) + .route("/mr/comment/:conv_id/delete", post(delete_comment)) } async fn merge( @@ -75,3 +79,30 @@ async fn get_mr_files( }; Ok(Json(res)) } + +async fn save_comment( + Path(mr_link): Path, + state: State, + body: Bytes, +) -> Result>, ApiError> { + let json_string = + String::from_utf8(body.to_vec()).unwrap_or_else(|_| "Invalid UTF-8".to_string()); + let res = state.monorepo().comment(&mr_link, json_string).await; + let res = match res { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn delete_comment( + Path(conv_id): Path, + state: State, +) -> Result>, ApiError> { + let res = state.monorepo().delete_comment(conv_id).await; + let res = match res { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} \ No newline at end of file diff --git a/moon/package.json b/moon/package.json index ea9f73b75..c1d3140e2 100644 --- a/moon/package.json +++ b/moon/package.json @@ -9,21 +9,21 @@ "lint": "next lint" }, "dependencies": { - "@ant-design/icons": "^5.4.0", + "@ant-design/icons": "^5.5.1", "@ant-design/nextjs-registry": "^1.0.1", - "@headlessui/react": "^2.1.6", + "@headlessui/react": "^2.1.8", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.1.5", "@lexical/react": "^0.17.1", + "lexical": "^0.17.1", "@tailwindcss/forms": "^0.5.9", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", - "date-fns": "^3.6.0", - "framer-motion": "^11.5.3", - "github-markdown-css": "^5.6.1", + "date-fns": "^4.1.0", + "framer-motion": "^11.7.0", + "github-markdown-css": "^5.7.0", "highlight.js": "^11.10.0", - "lexical": "^0.17.1", - "next": "^14.2.9", + "next": "^14.2.13", "prism-react-renderer": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/moon/src/app/(dashboard)/mr/[id]/page.tsx b/moon/src/app/(dashboard)/mr/[id]/page.tsx index 5dabba8c2..7fc1f6262 100644 --- a/moon/src/app/(dashboard)/mr/[id]/page.tsx +++ b/moon/src/app/(dashboard)/mr/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client' import { useEffect, useState } from "react"; -import { Card, Button, List, Tabs, TabsProps, Space, Timeline } from 'antd/lib'; -import { useRouter } from 'next/navigation'; +import { Card, Button, List, Tabs, TabsProps, Space, Timeline, Flex } from 'antd/lib'; import { CommentOutlined, MergeOutlined } from '@ant-design/icons'; import { formatDistance, fromUnixTime } from 'date-fns'; +import RichEditor from "@/components/rich-editor/RichEditor"; +import MRComment from "@/components/MRComment"; interface MRDetail { status: string, @@ -11,38 +12,44 @@ interface MRDetail { title: string, } interface Conversation { + id: number, user_id: number, - conv_type: String, - comment: String, + conv_type: string, + comment: string, created_at: number, } export default function MRDetailPage({ params }: { params: { id: string } }) { + const [editorState, setEditorState] = useState(""); const [mrDetail, setMrDetail] = useState( { - status: "", - conversions: [], + status: "", + conversions: [], title: "", - } + } ); - const router = useRouter(); const [filedata, setFileData] = useState([]); const [loadings, setLoadings] = useState([]); + const fetchDetail = async () => { + const detail = await fetch(`/api/mr/${params.id}/detail`); + const detail_json = await detail.json(); + setMrDetail(detail_json.data.data); + }; + + const fetchFileList = async () => { + set_to_loading(2) + try { + const res = await fetch(`/api/mr/${params.id}/files`); + const result = await res.json(); + setFileData(result.data.data); + } finally { + cancel_loading(2) + } + }; + useEffect(() => { - const fetchFileList = async () => { - set_to_loading(2) - try { - const detail = await fetch(`/api/mr/${params.id}/detail`); - const detail_json = await detail.json(); - setMrDetail(detail_json.data.data); - const res = await fetch(`/api/mr/${params.id}/files`); - const result = await res.json(); - setFileData(result.data.data); - } finally { - cancel_loading(2) - } - }; + fetchDetail() fetchFileList(); }, [params.id]); @@ -62,21 +69,34 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { }); } - const approve_mr = async (index: number, mr_link: string) => { - set_to_loading(index); - const res = await fetch(`/api/mr/${mr_link}/merge`, { + async function approve_mr() { + set_to_loading(1); + const res = await fetch(`/api/mr/${params.id}/merge`, { method: 'POST', }); if (res) { - cancel_loading(index); + cancel_loading(1); } }; + async function save_comment(comment) { + set_to_loading(3); + const res = await fetch(`/api/mr/${params.id}/comment`, { + method: 'POST', + body: comment, + }); + if (res) { + setEditorState(""); + fetchDetail(); + cancel_loading(3); + } + } + let conv_items = mrDetail?.conversions.map(conv => { let icon; let children; switch (conv.conv_type) { - case "Comment": icon = ; children = conv.comment; break; + case "Comment": icon = ; children = ; break case "Merged": icon = ; children = "Merged via the queue into main " + formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true }); break; // default: icon = ; children = conv.comment; }; @@ -94,8 +114,13 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { key: '1', label: 'Conversation', children: - + +

Add a comment

+ + + +
}, { @@ -122,9 +147,8 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { {mrDetail && mrDetail.status === "open" && diff --git a/moon/src/app/api/mr/[id]/comment/route.ts b/moon/src/app/api/mr/[id]/comment/route.ts new file mode 100644 index 000000000..75bc8a958 --- /dev/null +++ b/moon/src/app/api/mr/[id]/comment/route.ts @@ -0,0 +1,24 @@ +import { verifySession } from "@/app/lib/dal"; + +export async function POST(request: Request, { params }: { params: { id: string } }) { + const session = await verifySession() + const jsonData = await request.json(); + + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/mr/${params.id}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(jsonData), + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/api/mr/comment/[id]/delete/route.ts b/moon/src/app/api/mr/comment/[id]/delete/route.ts new file mode 100644 index 000000000..e20dd1976 --- /dev/null +++ b/moon/src/app/api/mr/comment/[id]/delete/route.ts @@ -0,0 +1,19 @@ +import { verifySession } from "@/app/lib/dal"; + +export async function POST(request: Request, { params }: { params: { id: string } }) { + const session = await verifySession() + + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/mr/comment/${params.id}/delete`, { + method: 'POST', + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/host/route.ts b/moon/src/app/host/route.ts index 5e1c8dc2e..49f1a1328 100644 --- a/moon/src/app/host/route.ts +++ b/moon/src/app/host/route.ts @@ -1,3 +1,4 @@ +export const revalidate = 0 export async function GET() { const endpoint = process.env.MEGA_HOST; diff --git a/moon/src/components/MRComment.tsx b/moon/src/components/MRComment.tsx new file mode 100644 index 000000000..7bbc55ba5 --- /dev/null +++ b/moon/src/components/MRComment.tsx @@ -0,0 +1,61 @@ +import { Card, Dropdown } from 'antd/lib'; +import type { MenuProps } from 'antd'; +import { MoreOutlined } from '@ant-design/icons'; +import { formatDistance, fromUnixTime } from 'date-fns'; +import LexicalContent from "@/components/rich-editor/LexicalContent"; + + +const Comment = ({ conv, fetchDetail }) => { + + const delete_comment = async () => { + await fetch(`/api/mr/comment/${conv.id}/delete`, { + method: 'POST', + }); + }; + const handleMenuClick: MenuProps['onClick'] = ({ key }) => { + if (key === '3') { + delete_comment() + fetchDetail() + } + }; + + const items: MenuProps['items'] = [ + { + label: 'Edit', + key: '1', + disabled: true + }, + { + label: 'Hide', + key: '2', + disabled: true + }, + { + type: 'divider', + }, + { + label: 'Delete', + key: '3', + danger: true, + } + ]; + const menuProps = { + items, + onClick: handleMenuClick, + }; + + let time = formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true }); + + return ( + + + + }> + + + ) + +} + +export default Comment; \ No newline at end of file diff --git a/moon/src/components/rich-editor/ExampleTheme.ts b/moon/src/components/rich-editor/ExampleTheme.ts new file mode 100644 index 000000000..bbd871b65 --- /dev/null +++ b/moon/src/components/rich-editor/ExampleTheme.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +export default { + code: 'editor-code', + heading: { + h1: 'editor-heading-h1', + h2: 'editor-heading-h2', + h3: 'editor-heading-h3', + h4: 'editor-heading-h4', + h5: 'editor-heading-h5', + }, + image: 'editor-image', + link: 'editor-link', + list: { + listitem: 'editor-listitem', + nested: { + listitem: 'editor-nested-listitem', + }, + ol: 'editor-list-ol', + ul: 'editor-list-ul', + }, + ltr: 'ltr', + paragraph: 'editor-paragraph', + placeholder: 'editor-placeholder', + quote: 'editor-quote', + rtl: 'rtl', + text: { + bold: 'editor-text-bold', + code: 'editor-text-code', + hashtag: 'editor-text-hashtag', + italic: 'editor-text-italic', + overflowed: 'editor-text-overflowed', + strikethrough: 'editor-text-strikethrough', + underline: 'editor-text-underline', + underlineStrikethrough: 'editor-text-underlineStrikethrough', + }, +}; diff --git a/moon/src/components/rich-editor/LexicalContent.tsx b/moon/src/components/rich-editor/LexicalContent.tsx new file mode 100644 index 000000000..94521e665 --- /dev/null +++ b/moon/src/components/rich-editor/LexicalContent.tsx @@ -0,0 +1,46 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import ExampleTheme from './ExampleTheme'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; + +const LexicalContent = ({ lexicalJson }: { lexicalJson: string }) => { + const editorConfig = { + namespace: '', + nodes: [], + onError(error: Error) { + throw error; + }, + theme: ExampleTheme, + editable: false, + editorState: lexicalJson, + }; + + const placeholder = 'No description provided.'; + + return ( + +
+
+ {placeholder}
+ } + /> + } + ErrorBoundary={LexicalErrorBoundary} + /> +
+ +
+ ); +}; + + + +export default LexicalContent; \ No newline at end of file diff --git a/moon/src/components/rich-editor/RichEditor.tsx b/moon/src/components/rich-editor/RichEditor.tsx new file mode 100644 index 000000000..e6a1d1b14 --- /dev/null +++ b/moon/src/components/rich-editor/RichEditor.tsx @@ -0,0 +1,82 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'; +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; + +import ExampleTheme from './ExampleTheme'; +import ToolbarPlugin from './plugins/ToolbarPlugin'; +import TreeViewPlugin from './plugins/TreeViewPlugin'; + +import './styles.css'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { useEffect, useState } from 'react'; + +const placeholder = 'Add your comment here...'; + +const editorConfig = { + namespace: '', + nodes: [], + // Handling of errors during update + onError(error: Error) { + throw error; + }, + // The editor theme + theme: ExampleTheme, +}; + +function OnChangePlugin({ onChange }) { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + onChange(editorState); + }); + }, [editor, onChange]); + return null; +} + + +export default function RichEditor({ setEditorState }) { + + function onChange(editorState) { + // Call toJSON on the EditorState object, which produces a serialization safe string + const editorStateJSON = editorState.toJSON(); + // However, we still have a JavaScript object, so we need to convert it to an actual string with JSON.stringify + setEditorState(JSON.stringify(editorStateJSON)); + } + + return ( + +
+ +
+ {placeholder}
+ } + /> + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + {/* */} +
+ + + +
+ ); +} diff --git a/moon/src/components/rich-editor/icons/LICENSE.md b/moon/src/components/rich-editor/icons/LICENSE.md new file mode 100644 index 000000000..ce74f6abe --- /dev/null +++ b/moon/src/components/rich-editor/icons/LICENSE.md @@ -0,0 +1,5 @@ +Bootstrap Icons +https://icons.getbootstrap.com + +Licensed under MIT license +https://github.com/twbs/icons/blob/main/LICENSE.md diff --git a/moon/src/components/rich-editor/icons/arrow-clockwise.svg b/moon/src/components/rich-editor/icons/arrow-clockwise.svg new file mode 100644 index 000000000..b072eb097 --- /dev/null +++ b/moon/src/components/rich-editor/icons/arrow-clockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/arrow-counterclockwise.svg b/moon/src/components/rich-editor/icons/arrow-counterclockwise.svg new file mode 100644 index 000000000..b0b23b9bb --- /dev/null +++ b/moon/src/components/rich-editor/icons/arrow-counterclockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/journal-text.svg b/moon/src/components/rich-editor/icons/journal-text.svg new file mode 100644 index 000000000..9b66f43aa --- /dev/null +++ b/moon/src/components/rich-editor/icons/journal-text.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/justify.svg b/moon/src/components/rich-editor/icons/justify.svg new file mode 100644 index 000000000..009bd7214 --- /dev/null +++ b/moon/src/components/rich-editor/icons/justify.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/text-center.svg b/moon/src/components/rich-editor/icons/text-center.svg new file mode 100644 index 000000000..2887a99f2 --- /dev/null +++ b/moon/src/components/rich-editor/icons/text-center.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/text-left.svg b/moon/src/components/rich-editor/icons/text-left.svg new file mode 100644 index 000000000..045261164 --- /dev/null +++ b/moon/src/components/rich-editor/icons/text-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/text-paragraph.svg b/moon/src/components/rich-editor/icons/text-paragraph.svg new file mode 100644 index 000000000..9779beabf --- /dev/null +++ b/moon/src/components/rich-editor/icons/text-paragraph.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/text-right.svg b/moon/src/components/rich-editor/icons/text-right.svg new file mode 100644 index 000000000..34686b0f1 --- /dev/null +++ b/moon/src/components/rich-editor/icons/text-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/type-bold.svg b/moon/src/components/rich-editor/icons/type-bold.svg new file mode 100644 index 000000000..276d133c2 --- /dev/null +++ b/moon/src/components/rich-editor/icons/type-bold.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/type-italic.svg b/moon/src/components/rich-editor/icons/type-italic.svg new file mode 100644 index 000000000..3ac6b09f0 --- /dev/null +++ b/moon/src/components/rich-editor/icons/type-italic.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/type-strikethrough.svg b/moon/src/components/rich-editor/icons/type-strikethrough.svg new file mode 100644 index 000000000..1c940e42a --- /dev/null +++ b/moon/src/components/rich-editor/icons/type-strikethrough.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/icons/type-underline.svg b/moon/src/components/rich-editor/icons/type-underline.svg new file mode 100644 index 000000000..c299b8bf2 --- /dev/null +++ b/moon/src/components/rich-editor/icons/type-underline.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/moon/src/components/rich-editor/plugins/ToolbarPlugin.tsx b/moon/src/components/rich-editor/plugins/ToolbarPlugin.tsx new file mode 100644 index 000000000..c4357bd67 --- /dev/null +++ b/moon/src/components/rich-editor/plugins/ToolbarPlugin.tsx @@ -0,0 +1,172 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {mergeRegister} from '@lexical/utils'; +import { + $getSelection, + $isRangeSelection, + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + FORMAT_ELEMENT_COMMAND, + FORMAT_TEXT_COMMAND, + REDO_COMMAND, + SELECTION_CHANGE_COMMAND, + UNDO_COMMAND, +} from 'lexical'; +import {useCallback, useEffect, useRef, useState} from 'react'; + +const LowPriority = 1; + +function Divider() { + return
; +} + +export default function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + + const $updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + // Update text format + setIsBold(selection.hasFormat('bold')); + setIsItalic(selection.hasFormat('italic')); + setIsUnderline(selection.hasFormat('underline')); + setIsStrikethrough(selection.hasFormat('strikethrough')); + } + }, []); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({editorState}) => { + editorState.read(() => { + $updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, _newEditor) => { + $updateToolbar(); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority, + ), + ); + }, [editor, $updateToolbar]); + + return ( +
+ + + + + + + + + + + + {' '} +
+ ); +} diff --git a/moon/src/components/rich-editor/plugins/TreeViewPlugin.tsx b/moon/src/components/rich-editor/plugins/TreeViewPlugin.tsx new file mode 100644 index 000000000..3f8980b7e --- /dev/null +++ b/moon/src/components/rich-editor/plugins/TreeViewPlugin.tsx @@ -0,0 +1,25 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {TreeView} from '@lexical/react/LexicalTreeView'; + +export default function TreeViewPlugin(): JSX.Element { + const [editor] = useLexicalComposerContext(); + return ( + + ); +} diff --git a/moon/src/components/rich-editor/styles.css b/moon/src/components/rich-editor/styles.css new file mode 100644 index 000000000..75e5dba2d --- /dev/null +++ b/moon/src/components/rich-editor/styles.css @@ -0,0 +1,396 @@ + +.editor-container { + margin: 20px auto 20px auto; + border-radius: 10px; + border: 1px solid #d1d9e0; + color: #000; + position: relative; + line-height: 20px; + font-weight: 400; + text-align: left; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.editor-inner { + position: relative; +} + +.editor-input { + min-height: 150px; + resize: none; + font-size: 15px; + caret-color: rgb(5, 5, 5); + position: relative; + tab-size: 1; + outline: 0; + padding: 15px 10px; + caret-color: #444; +} + +.editor-placeholder { + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 15px; + left: 10px; + font-size: 15px; + user-select: none; + display: inline-block; + pointer-events: none; +} + +.editor-text-bold { + font-weight: bold; +} + +.editor-text-italic { + font-style: italic; +} + +.editor-text-underline { + text-decoration: underline; +} + +.editor-text-strikethrough { + text-decoration: line-through; +} + +.editor-text-underlineStrikethrough { + text-decoration: underline line-through; +} + +.editor-text-code { + background-color: rgb(240, 242, 245); + padding: 1px 0.25rem; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 94%; +} + +.editor-link { + color: rgb(33, 111, 219); + text-decoration: none; +} + +.tree-view-output { + display: block; + background: #222; + color: #fff; + padding: 5px; + font-size: 12px; + white-space: pre-wrap; + margin: 1px auto 10px auto; + max-height: 250px; + position: relative; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + overflow: auto; + line-height: 14px; +} + +.editor-code { + background-color: rgb(240, 242, 245); + font-family: Menlo, Consolas, Monaco, monospace; + display: block; + padding: 8px 8px 8px 52px; + line-height: 1.53; + font-size: 13px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; + tab-size: 2; + /* white-space: pre; */ + overflow-x: auto; + position: relative; +} + +.editor-code:before { + content: attr(data-gutter); + position: absolute; + background-color: #eee; + left: 0; + top: 0; + border-right: 1px solid #ccc; + padding: 8px; + color: #777; + white-space: pre-wrap; + text-align: right; + min-width: 25px; +} +.editor-code:after { + content: attr(data-highlight-language); + top: 0; + right: 3px; + padding: 3px; + font-size: 10px; + text-transform: uppercase; + position: absolute; + color: rgba(0, 0, 0, 0.5); +} + +.editor-tokenComment { + color: slategray; +} + +.editor-tokenPunctuation { + color: #999; +} + +.editor-tokenProperty { + color: #905; +} + +.editor-tokenSelector { + color: #690; +} + +.editor-tokenOperator { + color: #9a6e3a; +} + +.editor-tokenAttr { + color: #07a; +} + +.editor-tokenVariable { + color: #e90; +} + +.editor-tokenFunction { + color: #dd4a68; +} + +.editor-paragraph { + margin: 0; + margin-bottom: 8px; + position: relative; +} + +.editor-paragraph:last-child { + margin-bottom: 0; +} + +.editor-heading-h1 { + font-size: 24px; + color: rgb(5, 5, 5); + font-weight: 400; + margin: 0; + margin-bottom: 12px; + padding: 0; +} + +.editor-heading-h2 { + font-size: 15px; + color: rgb(101, 103, 107); + font-weight: 700; + margin: 0; + margin-top: 10px; + padding: 0; + text-transform: uppercase; +} + +.editor-quote { + margin: 0; + margin-left: 20px; + font-size: 15px; + color: rgb(101, 103, 107); + border-left-color: rgb(206, 208, 212); + border-left-width: 4px; + border-left-style: solid; + padding-left: 16px; +} + +.editor-list-ol { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-list-ul { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-listitem { + margin: 8px 32px 8px 32px; +} + +.editor-nested-listitem { + list-style-type: none; +} + +pre::-webkit-scrollbar { + background: transparent; + width: 10px; +} + +pre::-webkit-scrollbar-thumb { + background: #999; +} + +.debug-timetravel-panel { + overflow: hidden; + padding: 0 0 10px 0; + margin: auto; + display: flex; +} + +.debug-timetravel-panel-slider { + padding: 0; + flex: 8; +} + +.debug-timetravel-panel-button { + padding: 0; + border: 0; + background: none; + flex: 1; + color: #fff; + font-size: 12px; +} + +.debug-timetravel-panel-button:hover { + text-decoration: underline; +} + +.debug-timetravel-button { + border: 0; + padding: 0; + font-size: 12px; + top: 10px; + right: 15px; + position: absolute; + background: none; + color: #fff; +} + +.debug-timetravel-button:hover { + text-decoration: underline; +} + +.toolbar { + display: flex; + margin-bottom: 1px; + background: #fff; + padding: 4px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + vertical-align: middle; +} + +.toolbar button.toolbar-item { + border: 0; + display: flex; + background: none; + border-radius: 10px; + padding: 8px; + cursor: pointer; + vertical-align: middle; +} + +.toolbar button.toolbar-item:disabled { + cursor: not-allowed; +} + +.toolbar button.toolbar-item.spaced { + margin-right: 2px; +} + +.toolbar button.toolbar-item i.format { + background-size: contain; + display: inline-block; + height: 18px; + width: 18px; + margin-top: 2px; + vertical-align: -0.25em; + display: flex; + opacity: 0.6; +} + +.toolbar button.toolbar-item:disabled i.format { + opacity: 0.2; +} + +.toolbar button.toolbar-item.active { + background-color: rgba(223, 232, 250, 0.3); +} + +.toolbar button.toolbar-item.active i { + opacity: 1; +} + +.toolbar .toolbar-item:hover:not([disabled]) { + background-color: #eee; +} + +.toolbar .divider { + width: 1px; + background-color: #eee; + margin: 0 4px; +} + +.toolbar .toolbar-item .text { + display: flex; + line-height: 20px; + width: 200px; + vertical-align: middle; + font-size: 14px; + color: #777; + text-overflow: ellipsis; + width: 70px; + overflow: hidden; + height: 20px; + text-align: left; +} + +.toolbar .toolbar-item .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 8px; + line-height: 16px; + background-size: contain; +} + +i.undo { + background-image: url(icons/arrow-counterclockwise.svg); +} + +i.redo { + background-image: url(icons/arrow-clockwise.svg); +} + +i.bold { + background-image: url(icons/type-bold.svg); +} + +i.italic { + background-image: url(icons/type-italic.svg); +} + +i.underline { + background-image: url(icons/type-underline.svg); +} + +i.strikethrough { + background-image: url(icons/type-strikethrough.svg); +} + +i.left-align { + background-image: url(icons/text-left.svg); +} + +i.center-align { + background-image: url(icons/text-center.svg); +} + +i.right-align { + background-image: url(icons/text-right.svg); +} + +i.justify-align { + background-image: url(icons/justify.svg); +}