diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts index 7e4ac9e636..d3af2903cf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -2,7 +2,8 @@ import { Op } from 'quill-delta'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend'; -import { YXmlText } from 'yjs/dist/src/types/YXmlText'; +import { PageCover } from '$app_reducers/pages/slice'; +import * as Y from 'yjs'; export interface EditorNode { id: string; @@ -162,14 +163,22 @@ export interface MentionPage { } export interface EditorProps { - id: string; - sharedType?: YXmlText; title?: string; + cover?: PageCover; onTitleChange?: (title: string) => void; + onCoverChange?: (cover?: PageCover) => void; showTitle?: boolean; + id: string; disableFocus?: boolean; } +export interface LocalEditorProps { + disableFocus?: boolean; + sharedType: Y.XmlText; + id: string; + caretColor?: string; +} + export enum EditorNodeType { Text = 'text', Paragraph = 'paragraph', @@ -221,7 +230,9 @@ export enum MentionType { export interface Mention { // inline page ref id - page?: string; + page_id?: string; // reminder date ref id date?: string; + + type: MentionType; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg index 3e86e21b8d..0739605066 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg new file mode 100644 index 0000000000..aeaa6a0f29 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx deleted file mode 100644 index af3a91b3b9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export function Colors() { - return
; -} - -export default Colors; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx index 40a46fed81..84f73c8ebe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx @@ -1,13 +1,22 @@ import React, { useCallback, useState } from 'react'; import TextField from '@mui/material/TextField'; import { useTranslation } from 'react-i18next'; -import { pattern } from '$app/utils/open_url'; import Button from '@mui/material/Button'; -export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => void; onEscape?: () => void }) { +const urlPattern = /^https?:\/\/.+/; + +export function EmbedLink({ + onDone, + onEscape, + defaultLink, +}: { + defaultLink?: string; + onDone?: (value: string) => void; + onEscape?: () => void; +}) { const { t } = useTranslation(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(defaultLink ?? ''); const [error, setError] = useState(false); const handleChange = useCallback( @@ -15,7 +24,7 @@ export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => vo const value = e.target.value; setValue(value); - setError(!pattern.test(value)); + setError(!urlPattern.test(value)); }, [setValue, setError] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx new file mode 100644 index 0000000000..65eb58b7ae --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx @@ -0,0 +1,123 @@ +import React, { SyntheticEvent, useCallback, useState } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs'; +import SwipeableViews from 'react-swipeable-views'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; + +export enum TAB_KEY { + Colors = 'colors', + UPLOAD = 'upload', + EMBED_LINK = 'embed_link', + UNSPLASH = 'unsplash', +} + +export type TabOption = { + key: TAB_KEY; + label: string; + Component: React.ComponentType<{ + onDone?: (value: string) => void; + onEscape?: () => void; + }>; + onDone?: (value: string) => void; +}; + +export function UploadTabs({ + tabOptions, + popoverProps, + containerStyle, +}: { + containerStyle?: React.CSSProperties; + tabOptions: TabOption[]; + popoverProps?: PopoverProps; +}) { + const [tabValue, setTabValue] = useState(() => { + return tabOptions[0].key; + }); + + const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => { + setTabValue(newValue as TAB_KEY); + }, []); + + const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + popoverProps?.onClose?.({}, 'escapeKeyDown'); + } + + if (e.key === 'Tab') { + e.preventDefault(); + e.stopPropagation(); + setTabValue((prev) => { + const currentIndex = tabOptions.findIndex((tab) => tab.key === prev); + let nextIndex = currentIndex + 1; + + if (e.shiftKey) { + nextIndex = currentIndex - 1; + } + + return tabOptions[nextIndex % tabOptions.length]?.key ?? tabOptions[0].key; + }); + } + }, + [popoverProps, tabOptions] + ); + + return ( + +
+ + {tabOptions.map((tab) => { + const { key, label } = tab; + + return ; + })} + + +
+ + {tabOptions.map((tab, index) => { + const { key, Component, onDone } = tab; + + return ( + + popoverProps?.onClose?.({}, 'escapeKeyDown')} /> + + ); + })} + +
+
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts index f2eab1116b..c2b4c5552d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts @@ -1,4 +1,4 @@ export * from './Unsplash'; export * from './UploadImage'; export * from './EmbedLink'; -export * from './Colors'; +export * from './UploadTabs'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx index 99f444ac26..db179ffb7f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx @@ -1,33 +1,52 @@ import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup'; -import { PageIcon } from '$app_reducers/pages/slice'; +import { PageCover, PageIcon } from '$app_reducers/pages/slice'; import ViewIcon from '$app/components/_shared/view_title/ViewIcon'; +import { ViewCover } from '$app/components/_shared/view_title/cover'; function ViewBanner({ icon, hover, onUpdateIcon, + showCover, + cover, + onUpdateCover, }: { icon?: PageIcon; hover: boolean; onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover?: PageCover) => void; }) { return ( - <> -
- +
+ {showCover && cover && } + +
+
+ +
+
+ +
-
- -
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx index ec38130c05..66827bca56 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx @@ -29,7 +29,7 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon if (!icon) return null; return ( <> -
+
{icon.value}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx index 68377951e8..7c23e0587a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx @@ -1,31 +1,42 @@ import { useTranslation } from 'react-i18next'; -import { PageIcon } from '$app_reducers/pages/slice'; +import { CoverType, PageCover, PageIcon } from '$app_reducers/pages/slice'; import React, { useCallback } from 'react'; import { randomEmoji } from '$app/utils/emoji'; import { EmojiEmotionsOutlined } from '@mui/icons-material'; import Button from '@mui/material/Button'; +import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; +import { ImageType } from '$app/application/document/document.types'; interface Props { icon?: PageIcon; - // onUpdateCover: (coverType: CoverType, cover: string) => void; onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover: PageCover) => void; } -function ViewIconGroup({ icon, onUpdateIcon }: Props) { + +const defaultCover = { + cover_selection_type: CoverType.Asset, + cover_selection: 'app_flowy_abstract_cover_2.jpeg', + image_type: ImageType.Internal, +}; + +function ViewIconGroup({ icon, onUpdateIcon, showCover, cover, onUpdateCover }: Props) { const { t } = useTranslation(); const showAddIcon = !icon?.value; + const showAddCover = !cover && showCover; + const onAddIcon = useCallback(() => { const emoji = randomEmoji(); onUpdateIcon(emoji); }, [onUpdateIcon]); - // const onAddCover = useCallback(() => { - // const color = randomColor(); - // - // onUpdateCover(CoverType.Color, color); - // }, []); + const onAddCover = useCallback(() => { + onUpdateCover?.(defaultCover); + }, [onUpdateCover]); return (
@@ -34,11 +45,11 @@ function ViewIconGroup({ icon, onUpdateIcon }: Props) { {t('document.plugins.cover.addIcon')} )} - {/*{showAddCover && (*/} - {/* */} - {/*)}*/} + {showAddCover && ( + + )}
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx index 26f83ac921..8d81b6d4b7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import ViewBanner from '$app/components/_shared/view_title/ViewBanner'; -import { Page, PageIcon } from '$app_reducers/pages/slice'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; import { ViewIconTypePB } from '@/services/backend'; import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput'; @@ -9,9 +9,20 @@ interface Props { showTitle?: boolean; onTitleChange?: (title: string) => void; onUpdateIcon?: (icon: PageIcon) => void; + forceHover?: boolean; + showCover?: boolean; + onUpdateCover?: (cover?: PageCover) => void; } -function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpdateIconProp }: Props) { +function ViewTitle({ + view, + forceHover = false, + onTitleChange, + showTitle = true, + onUpdateIcon: onUpdateIconProp, + showCover = false, + onUpdateCover, +}: Props) { const [hover, setHover] = useState(false); const [icon, setIcon] = useState(view.icon); @@ -38,7 +49,14 @@ function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpda onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} > - + {showTitle && (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx new file mode 100644 index 0000000000..78b8bbcc46 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { colorMap } from '$app/utils/color'; + +const colors = Object.entries(colorMap); + +function Colors({ onDone }: { onDone?: (value: string) => void }) { + return ( +
+ {colors.map(([name, value]) => ( +
onDone?.(name)} + /> + ))} +
+ ); +} + +export default Colors; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx new file mode 100644 index 0000000000..01da528a06 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx @@ -0,0 +1,91 @@ +import React, { useMemo } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { PopoverOrigin } from '@mui/material/Popover'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload'; +import { useTranslation } from 'react-i18next'; +import Colors from '$app/components/_shared/view_title/cover/Colors'; +import { ImageType } from '$app/application/document/document.types'; + +const initialOrigin: { + anchorOrigin: PopoverOrigin; + transformOrigin: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, +}; + +function CoverPopover({ + anchorEl, + open, + onClose, + onUpdateCover, +}: { + anchorEl: HTMLElement | null; + open: boolean; + onClose: () => void; + onUpdateCover?: (cover?: PageCover) => void; +}) { + const { t } = useTranslation(); + const tabOptions: TabOption[] = useMemo(() => { + return [ + { + label: t('document.plugins.cover.colors'), + key: TAB_KEY.Colors, + Component: Colors, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Color, + cover_selection: value, + image_type: ImageType.Internal, + }); + }, + }, + { + label: t('document.imageBlock.embedLink.label'), + key: TAB_KEY.EMBED_LINK, + Component: EmbedLink, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + onClose(); + }, + }, + { + key: TAB_KEY.UNSPLASH, + label: t('document.imageBlock.unsplash.label'), + Component: Unsplash, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + }, + }, + ]; + }, [onClose, onUpdateCover, t]); + + return ( + + ); +} + +export default CoverPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx new file mode 100644 index 0000000000..a86eef6ca5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx @@ -0,0 +1,66 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { renderColor } from '$app/utils/color'; +import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions'; +import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover'; +import DefaultImage from '$app/assets/images/default_cover.jpg'; + +export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) { + const { cover_selection_type: type, cover_selection: value = '' } = useMemo(() => cover || {}, [cover]); + const [showAction, setShowAction] = useState(false); + const actionRef = useRef(null); + const [showPopover, setShowPopover] = useState(false); + + const renderCoverColor = useCallback((color: string) => { + return ( +
+ ); + }, []); + + const renderCoverImage = useCallback((url: string) => { + return {''}; + }, []); + + const handleRemoveCover = useCallback(() => { + onUpdateCover?.(null); + }, [onUpdateCover]); + + const handleClickChange = useCallback(() => { + setShowPopover(true); + }, []); + + return ( +
{ + setShowAction(true); + }} + onMouseLeave={() => { + setShowAction(false); + }} + className={'relative flex h-[255px] w-full'} + > + {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} + {type === CoverType.Color ? renderCoverColor(value) : null} + {type === CoverType.Image ? renderCoverImage(value) : null} + + {showPopover && ( + setShowPopover(false)} + anchorEl={actionRef.current} + onUpdateCover={onUpdateCover} + /> + )} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx new file mode 100644 index 0000000000..97615804fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx @@ -0,0 +1,42 @@ +import React, { forwardRef } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; + +function ViewCoverActions( + { show, onRemove, onClickChange }: { show: boolean; onRemove: () => void; onClickChange: () => void }, + ref: React.ForwardedRef +) { + const { t } = useTranslation(); + + return ( +
+
+ +
+
+ ); +} + +export default forwardRef(ViewCoverActions); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts new file mode 100644 index 0000000000..8df50bb41e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts @@ -0,0 +1 @@ +export * from './ViewCover'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx index eeb8b85904..079a6fd75f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx @@ -1,12 +1,14 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Editor from '$app/components/editor/Editor'; import { DocumentHeader } from 'src/appflowy_app/components/document/document_header'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { updatePageName } from '$app_reducers/pages/async_actions'; +import { PageCover } from '$app_reducers/pages/slice'; export function Document({ id }: { id: string }) { const page = useAppSelector((state) => state.pages.pageMap[id]); + const [cover, setCover] = useState(undefined); const dispatch = useAppDispatch(); const onTitleChange = useCallback( @@ -21,12 +23,29 @@ export function Document({ id }: { id: string }) { [dispatch, id] ); + const view = useMemo(() => { + return { + ...page, + cover, + }; + }, [page, cover]); + + useEffect(() => { + return () => { + setCover(undefined); + }; + }, [id]); + if (!page) return null; return ( -
- - +
+ +
+
+ +
+
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx index a944547870..f6e8736c54 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx @@ -1,15 +1,18 @@ -import React, { memo, useCallback } from 'react'; -import { Page, PageIcon } from '$app_reducers/pages/slice'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; import ViewTitle from '$app/components/_shared/view_title/ViewTitle'; import { updatePageIcon } from '$app/application/folder/page.service'; interface DocumentHeaderProps { page: Page; + onUpdateCover: (cover?: PageCover) => void; } -export function DocumentHeader({ page }: DocumentHeaderProps) { +export function DocumentHeader({ page, onUpdateCover }: DocumentHeaderProps) { const pageId = page.id; + const ref = useRef(null); + const [forceHover, setForceHover] = useState(false); const onUpdateIcon = useCallback( async (icon: PageIcon) => { await updatePageIcon(pageId, icon.value ? icon : undefined); @@ -17,10 +20,39 @@ export function DocumentHeader({ page }: DocumentHeaderProps) { [pageId] ); + useEffect(() => { + const parent = ref.current?.parentElement; + + if (!parent) return; + + const documentDom = parent.querySelector('.appflowy-editor') as HTMLElement; + + if (!documentDom) return; + + const handleMouseMove = (e: MouseEvent) => { + const isMoveInTitle = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-title')); + const isMoveInHeader = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-header')); + + setForceHover(isMoveInTitle || isMoveInHeader); + }; + + documentDom.addEventListener('mousemove', handleMouseMove); + return () => { + documentDom.removeEventListener('mousemove', handleMouseMove); + }; + }, []); + if (!page) return null; return ( -
- +
+
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts index d5a0e0c976..04a2e7c0f1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts @@ -49,6 +49,12 @@ export function wrapFormula(editor: ReactEditor, formula?: string) { Transforms.insertNodes(editor, formulaElement, { select: true, }); + + const path = editor.selection?.anchor.path; + + if (path) { + editor.select(path); + } } export function unwrapFormula(editor: ReactEditor) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts index a8135da1cc..91e6ecd76e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts @@ -254,17 +254,23 @@ export const CustomEditor = { }, insertMention(editor: ReactEditor, mention: Mention) { - const mentionElement = { - type: EditorInlineNodeType.Mention, - children: [{ text: '@' }], - data: { - ...mention, + const mentionElement = [ + { + type: EditorInlineNodeType.Mention, + children: [{ text: '$' }], + data: { + ...mention, + }, }, - }; + ]; Transforms.insertNodes(editor, mentionElement, { select: true, }); + + editor.collapse({ + edge: 'end', + }); }, toggleTodo(editor: ReactEditor, node: TodoListNode) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx index 0966edf384..a20300bbc2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx @@ -9,10 +9,8 @@ export const Callout = memo(
-
-
+
+
{children}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx index 6d3a7cfd22..9d2b4fdac0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx @@ -24,9 +24,9 @@ export const ImageBlock = memo( onClick={() => { if (!selected) onFocusNode(); }} - className={`${className} image-block relative w-full cursor-pointer py-1`} + className={`${className} image-block relative w-full cursor-pointer py-1`} > -
+
{children}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx index 1c46776063..b6d882ec7b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx @@ -1,20 +1,13 @@ -import React, { useCallback, useMemo, SyntheticEvent, useState } from 'react'; -import Popover, { PopoverOrigin } from '@mui/material/Popover/Popover'; +import React, { useMemo } from 'react'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; -import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; -import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs'; + import { useTranslation } from 'react-i18next'; -import { EmbedLink, Unsplash } from '$app/components/_shared/image_upload'; -import SwipeableViews from 'react-swipeable-views'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload'; import { CustomEditor } from '$app/components/editor/command'; import { useSlateStatic } from 'slate-react'; import { ImageNode, ImageType } from '$app/application/document/document.types'; -enum TAB_KEY { - UPLOAD = 'upload', - EMBED_LINK = 'embed_link', - UNSPLASH = 'unsplash', -} const initialOrigin: { transformOrigin: PopoverOrigin; anchorOrigin: PopoverOrigin; @@ -53,7 +46,7 @@ function UploadPopover({ open, }); - const tabOptions = useMemo(() => { + const tabOptions: TabOption[] = useMemo(() => { return [ // { // label: t('button.upload'), @@ -87,102 +80,25 @@ function UploadPopover({ ]; }, [editor, node, onClose, t]); - const [tabValue, setTabValue] = useState(tabOptions[0].key); - - const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => { - setTabValue(newValue as TAB_KEY); - }, []); - - const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - e.stopPropagation(); - - if (e.key === 'Escape') { - e.preventDefault(); - e.stopPropagation(); - onClose(); - } - - if (e.key === 'Tab') { - e.preventDefault(); - e.stopPropagation(); - setTabValue((prev) => { - const currentIndex = tabOptions.findIndex((tab) => tab.key === prev); - const nextIndex = (currentIndex + 1) % tabOptions.length; - - return tabOptions[nextIndex]?.key ?? tabOptions[0].key; - }); - } - }, - [onClose, tabOptions] - ); - return ( - { - e.stopPropagation(); - }} - onKeyDown={onKeyDown} - PaperProps={{ - style: { - padding: 0, + { + e.stopPropagation(); }, }} - > -
- - {tabOptions.map((tab) => { - const { key, label } = tab; - - return ; - })} - - -
- - {tabOptions.map((tab, index) => { - const { key, Component, onDone } = tab; - - return ( - - - - ); - })} - -
-
-
+ containerStyle={{ + maxWidth: paperWidth, + maxHeight: paperHeight, + overflow: 'hidden', + }} + tabOptions={tabOptions} + /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx index f376f9cd75..6d04a77c2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx @@ -4,7 +4,7 @@ import { EditorElementProps, PageNode } from '$app/application/document/document export const Page = memo( forwardRef>(({ node: _, children, ...attributes }, ref) => { const className = useMemo(() => { - return `${attributes.className ?? ''} pb-3 text-4xl font-bold`; + return `${attributes.className ?? ''} document-title pb-3 text-4xl font-bold`; }, [attributes.className]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx index b05000c6e2..83af8fdbd1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx @@ -5,62 +5,87 @@ import { EditorProps } from '$app/application/document/document.types'; import { Provider } from '$app/components/editor/provider'; import { YXmlText } from 'yjs/dist/src/types/YXmlText'; import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation'; +import isEqual from 'lodash-es/isEqual'; -export const CollaborativeEditor = memo(({ id, title, showTitle = true, onTitleChange, disableFocus }: EditorProps) => { - const [sharedType, setSharedType] = useState(null); - const provider = useMemo(() => { - setSharedType(null); - return new Provider(id, showTitle); - }, [id, showTitle]); +export const CollaborativeEditor = memo( + ({ id, title, cover, showTitle = true, onTitleChange, onCoverChange, ...props }: EditorProps) => { + const [sharedType, setSharedType] = useState(null); + const provider = useMemo(() => { + setSharedType(null); + return new Provider(id, showTitle); + }, [id, showTitle]); - const root = useMemo(() => { - if (!showTitle || !sharedType || !sharedType.doc) return null; + const root = useMemo(() => { + if (!showTitle || !sharedType || !sharedType.doc) return null; - return getYTarget(sharedType?.doc, [0]); - }, [sharedType, showTitle]); + return getYTarget(sharedType?.doc, [0]); + }, [sharedType, showTitle]); - const rootText = useMemo(() => { - if (!root) return null; - return getInsertTarget(root, [0]); - }, [root]); + const rootText = useMemo(() => { + if (!root) return null; + return getInsertTarget(root, [0]); + }, [root]); - useEffect(() => { - if (!rootText || rootText.toString() === title) return; + useEffect(() => { + if (!rootText || rootText.toString() === title) return; - if (rootText.length > 0) { - rootText.delete(0, rootText.length); + if (rootText.length > 0) { + rootText.delete(0, rootText.length); + } + + rootText.insert(0, title || ''); + }, [title, rootText]); + + useEffect(() => { + if (!root) return; + + const originalCover = root.getAttribute('data')?.cover; + + if (cover === undefined) return; + if (isEqual(originalCover, cover)) return; + root.setAttribute('data', { cover: cover ? cover : undefined }); + }, [cover, root]); + + useEffect(() => { + if (!root) return; + const rootId = root.getAttribute('blockId'); + + if (!rootId) return; + + const getCover = () => { + const data = root.getAttribute('data'); + + onCoverChange?.(data?.cover); + }; + + getCover(); + const onChange = () => { + onTitleChange?.(root.toString()); + getCover(); + }; + + root.observeDeep(onChange); + return () => root.unobserveDeep(onChange); + }, [onTitleChange, root, onCoverChange]); + + useEffect(() => { + provider.connect(); + const handleConnected = () => { + setSharedType(provider.sharedType); + }; + + provider.on('ready', handleConnected); + return () => { + setSharedType(null); + provider.off('ready', handleConnected); + provider.disconnect(); + }; + }, [provider]); + + if (!sharedType || id !== provider.id) { + return null; } - rootText.insert(0, title || ''); - }, [title, rootText]); - - useEffect(() => { - if (!root) return; - const onChange = () => { - onTitleChange?.(root.toString()); - }; - - root.observeDeep(onChange); - return () => root.unobserveDeep(onChange); - }, [onTitleChange, root]); - - useEffect(() => { - provider.connect(); - const handleConnected = () => { - setSharedType(provider.sharedType); - }; - - provider.on('ready', handleConnected); - return () => { - setSharedType(null); - provider.off('ready', handleConnected); - provider.disconnect(); - }; - }, [provider]); - - if (!sharedType || id !== provider.id) { - return null; + return ; } - - return ; -}); +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx index 3ac4f1e5c6..12b198b23e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx @@ -11,7 +11,6 @@ import { useShortcuts } from 'src/appflowy_app/components/editor/plugins/shortcu import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions'; import { CircularProgress } from '@mui/material'; -import * as Y from 'yjs'; import { NodeEntry } from 'slate'; import { DecorateStateProvider, @@ -22,8 +21,9 @@ import { } from '$app/components/editor/stores'; import CommandPanel from '../tools/command_panel/CommandPanel'; import { EditorBlockStateProvider } from '$app/components/editor/stores/block'; +import { LocalEditorProps } from '$app/application/document/document.types'; -function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: string; disableFocus?: boolean }) { +function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) { const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType); const decorateCodeHighlight = useDecorateCodeHighlight(editor); const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor); @@ -74,7 +74,10 @@ function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: strin disableFocus={disableFocus} onKeyDown={onKeyDown} decorate={decorate} - className={'px-16 caret-text-title outline-none focus:outline-none'} + style={{ + caretColor, + }} + className={`px-16 outline-none focus:outline-none`} />
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx index 903ae5741f..1824d8a590 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx @@ -27,6 +27,7 @@ import { Text as TextComponent } from '../blocks/text'; import { Page } from '../blocks/page'; import { useElementState } from '$app/components/editor/components/editor/Element.hooks'; import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock'; +import { renderColor } from '$app/utils/color'; function Element({ element, attributes, children }: RenderElementProps) { const node = element; @@ -98,8 +99,8 @@ function Element({ element, attributes, children }: RenderElementProps) { const data = (node.data as BlockData) || {}; return { - backgroundColor: data.bg_color, - color: data.font_color, + backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, }; }, [node.data]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx index 468cc3d380..188ac33361 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties } from 'react'; import { RenderLeafProps } from 'slate-react'; import { Link } from '$app/components/editor/components/inline_nodes/link'; +import { renderColor } from '$app/utils/color'; export function Leaf({ attributes, children, leaf }: RenderLeafProps) { let newChildren = children; @@ -39,11 +40,11 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) { const style: CSSProperties = {}; if (leaf.font_color) { - style['color'] = leaf.font_color.replace('0x', '#'); + style['color'] = renderColor(leaf.font_color); } if (leaf.bg_color) { - style['backgroundColor'] = leaf.bg_color.replace('0x', '#'); + style['backgroundColor'] = renderColor(leaf.bg_color); } if (leaf.href) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx index 47aada1143..fb32eb18a9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx @@ -3,10 +3,10 @@ import React from 'react'; // Put this at the start and end of an inline component to work around this Chromium bug: // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405 -export const InlineChromiumBugfix = () => ( +export const InlineChromiumBugfix = ({ className }: { className?: string }) => ( { + if (selected && isCollapsed && !open) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, open]); + const handleClick = useCallback( (e: MouseEvent) => { const target = e.currentTarget; const path = getNodePath(editor, target); - ReactEditor.focus(editor); - Transforms.select(editor, path); - if (editor.selection) { - setRange(editor.selection); - openPopover(); - } + setRange(path); + openPopover(); }, [editor, openPopover, setRange] ); @@ -103,9 +113,9 @@ export const InlineFormula = memo( selected ? 'selected' : '' }`} > - + {children} - + {open && ( >(({ node, children, ...attributes }, ref) => { return ( - <> - - - {children} - - - + + + {children} + + + ); }) ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx index eba7c77169..12e5bab14d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx @@ -5,23 +5,44 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { pageTypeMap } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; -import { useSelected } from 'slate-react'; +import { useSelected, useSlate } from 'slate-react'; import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; import { notify } from 'src/appflowy_app/components/_shared/notify'; import { subscribeNotifications } from '$app/application/notification'; import { FolderNotification } from '@/services/backend'; +import { Editor, Range } from 'slate'; -export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) { +export function MentionLeaf({ mention }: { mention: Mention }) { const { t } = useTranslation(); const [page, setPage] = useState(null); const [error, setError] = useState(false); const navigate = useNavigate(); + const editor = useSlate(); const selected = useSelected(); + const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); + + useEffect(() => { + if (selected && isCollapsed && page) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, page]); + const loadPage = useCallback(async () => { setError(true); - if (!mention.page) return; + // keep old field for backward compatibility + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const pageId = mention.page_id ?? mention.page; + + if (!pageId) return; try { - const page = await getPage(mention.page); + const page = await getPage(pageId); setPage(page); setError(false); @@ -29,7 +50,7 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: setPage(null); setError(true); } - }, [mention.page]); + }, [mention]); useEffect(() => { void loadPage(); @@ -94,31 +115,27 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: }, [page]); return ( - - - {page && ( + + {error ? ( + <> + + {t('document.mention.deleted')} + + ) : ( + page && ( <> - {page.icon?.value || } - {page.name || t('document.title.placeholder')} + {page.icon?.value || } + {page.name || t('document.title.placeholder')} - )} - {error && ( - <> - - - - {t('document.mention.deleted')} - - )} - - - {children} + ) + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx index 10b86e503a..b3bb665702 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx @@ -6,6 +6,7 @@ import KeyboardNavigation, { import { useTranslation } from 'react-i18next'; import { TitleOutlined } from '@mui/icons-material'; import { EditorMarkFormat } from '$app/application/document/document.types'; +import { ColorEnum, renderColor } from '$app/utils/color'; export interface ColorPickerProps { onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void; @@ -39,8 +40,8 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro >
@@ -118,40 +119,40 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro content: renderColorItem(t('editor.backgroundColorDefault'), '', ''), }, { - key: `bg-gray-rgba(161,161,159,0.61)`, - content: renderColorItem(t('editor.backgroundColorGray'), '', 'rgba(161,161,159,0.61)'), + key: `bg-lime-${ColorEnum.Lime}`, + content: renderColorItem(t('editor.backgroundColorLime'), '', ColorEnum.Lime), }, { - key: `bg-brown-rgba(178,93,37,0.65)`, - content: renderColorItem(t('editor.backgroundColorBrown'), '', 'rgba(178,93,37,0.65)'), + key: `bg-aqua-${ColorEnum.Aqua}`, + content: renderColorItem(t('editor.backgroundColorAqua'), '', ColorEnum.Aqua), }, { - key: `bg-orange-rgba(248,156,71,0.65)`, - content: renderColorItem(t('editor.backgroundColorOrange'), '', 'rgba(248,156,71,0.65)'), + key: `bg-orange-${ColorEnum.Orange}`, + content: renderColorItem(t('editor.backgroundColorOrange'), '', ColorEnum.Orange), }, { - key: `bg-yellow-rgba(229,197,137,0.6)`, - content: renderColorItem(t('editor.backgroundColorYellow'), '', 'rgba(229,197,137,0.6)'), + key: `bg-yellow-${ColorEnum.Yellow}`, + content: renderColorItem(t('editor.backgroundColorYellow'), '', ColorEnum.Yellow), }, { - key: `bg-green-rgba(124,189,111,0.65)`, - content: renderColorItem(t('editor.backgroundColorGreen'), '', 'rgba(124,189,111,0.65)'), + key: `bg-green-${ColorEnum.Green}`, + content: renderColorItem(t('editor.backgroundColorGreen'), '', ColorEnum.Green), }, { - key: `bg-blue-rgba(100,174,199,0.71)`, - content: renderColorItem(t('editor.backgroundColorBlue'), '', 'rgba(100,174,199,0.71)'), + key: `bg-blue-${ColorEnum.Blue}`, + content: renderColorItem(t('editor.backgroundColorBlue'), '', ColorEnum.Blue), }, { - key: `bg-purple-rgba(182,114,234,0.63)`, - content: renderColorItem(t('editor.backgroundColorPurple'), '', 'rgba(182,114,234,0.63)'), + key: `bg-purple-${ColorEnum.Purple}`, + content: renderColorItem(t('editor.backgroundColorPurple'), '', ColorEnum.Purple), }, { - key: `bg-pink-rgba(238,142,179,0.6)`, - content: renderColorItem(t('editor.backgroundColorPink'), '', 'rgba(238,142,179,0.6)'), + key: `bg-pink-${ColorEnum.Pink}`, + content: renderColorItem(t('editor.backgroundColorPink'), '', ColorEnum.Pink), }, { - key: `bg-red-rgba(238,88,98,0.64)`, - content: renderColorItem(t('editor.backgroundColorRed'), '', 'rgba(238,88,98,0.64)'), + key: `bg-red-${ColorEnum.LightPink}`, + content: renderColorItem(t('editor.backgroundColorRed'), '', ColorEnum.LightPink), }, ], }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx index 20c326d640..ade9817503 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx @@ -26,6 +26,7 @@ export const canSetColorBlocks: EditorNodeType[] = [ EditorNodeType.NumberedListBlock, EditorNodeType.ToggleListBlock, EditorNodeType.QuoteBlock, + EditorNodeType.CalloutBlock, ]; export function BlockOperationMenu({ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx index 76ba3d3404..806a2a3788 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx @@ -52,7 +52,8 @@ export function useMentionPanel({ closePanel(true); CustomEditor.insertMention(editor, { - page: id, + page_id: id, + type: MentionType.PageRef, }); }, [closePanel, editor] diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx index 1ebb783871..ddab776abc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx @@ -17,7 +17,6 @@ import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material'; import { CustomEditor } from '$app/components/editor/command'; -import { randomEmoji } from '$app/utils/emoji'; import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { YjsEditor } from '@slate-yjs/core'; import { useEditorBlockDispatch } from '$app/components/editor/stores/block'; @@ -133,7 +132,7 @@ export function useSlashCommandPanel({ if (nodeType === EditorNodeType.CalloutBlock) { Object.assign(data, { - icon: randomEmoji(), + icon: '📌', }); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx index 38f18a1d30..c7bfc11352 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx @@ -27,6 +27,8 @@ export function Formula() { } requestAnimationFrame(() => { + const selection = editor.selection; + if (!selection) return; setRange(selection); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index bd9ef76e9e..b555b978c9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -83,7 +83,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } -.text-content { +.text-content, [data-dark-mode="true"] .text-content { &.empty-content { @apply min-w-[1px]; span { @@ -94,7 +94,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } } -.text-element:has(.text-placeholder), .divider-node { +.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node { ::selection { @apply bg-transparent; } @@ -151,8 +151,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } } -.image-block, .math-equation-block { +.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block { ::selection { @apply bg-transparent; } +} + +.mention-inline { + &:hover { + @apply bg-fill-list-active rounded; + } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts index c74f0e993b..ead982006c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts @@ -28,8 +28,11 @@ export class Provider extends EventEmitter { sharedType.applyDelta(delta); const rootId = this.dataClient.rootId as string; + const root = delta[0].insert as Y.XmlText; + const data = root.getAttribute('data'); sharedType.setAttribute('blockId', rootId); + sharedType.setAttribute('data', data); this.sharedType = sharedType; this.sharedType?.observeDeep(this.onChange); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts index 42d0f372c7..b4da4b3ca7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts @@ -7,76 +7,90 @@ export function generateId() { return nanoid(10); } -export function transformToInlineElement(op: Op): Element | null { +export function transformToInlineElement(op: Op): Element[] { const attributes = op.attributes; - if (!attributes) return null; + if (!attributes) return []; const { formula, mention, ...attrs } = attributes; if (formula) { - return { - type: EditorInlineNodeType.Formula, - data: formula, - children: [ - { - text: op.insert as string, - ...attrs, - }, - ], - }; + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Formula, + data: formula, + children: [ + { + text, + ...attrs, + }, + ], + }; + }); } if (mention) { - return { - type: EditorInlineNodeType.Mention, - children: [ - { - text: op.insert as string, - ...attrs, + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Mention, + children: [ + { + text, + ...attrs, + }, + ], + data: { + ...(mention as Mention), }, - ], - data: { - ...(mention as Mention), - }, - }; + }; + }); } - return null; + return []; } export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] { - return delta && delta.length > 0 - ? delta.map((op) => { - const matchInline = transformToInlineElement(op); + const newDelta: (Text | Element)[] = []; - if (matchInline) { - return matchInline; - } + if (!delta || !delta.length) + return [ + { + text: '', + }, + ]; - if (op.attributes) { - if ('font_color' in op.attributes && op.attributes['font_color'] === '') { - delete op.attributes['font_color']; - } + delta.forEach((op) => { + const matchInlines = transformToInlineElement(op); - if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') { - delete op.attributes['bg_color']; - } + if (matchInlines.length > 0) { + newDelta.push(...matchInlines); + return; + } - if ('code' in op.attributes && !op.attributes['code']) { - delete op.attributes['code']; - } - } + if (op.attributes) { + if ('font_color' in op.attributes && op.attributes['font_color'] === '') { + delete op.attributes['font_color']; + } - return { - text: op.insert as string, - ...op.attributes, - }; - }) - : [ - { - text: '', - }, - ]; + if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') { + delete op.attributes['bg_color']; + } + + if ('code' in op.attributes && !op.attributes['code']) { + delete op.attributes['code']; + } + } + + newDelta.push({ + text: op.insert as string, + ...op.attributes, + }); + }); + + return newDelta; } export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss index a708777326..e844bd6729 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -1,12 +1,31 @@ +* { + margin: 0; + padding: 0; +} + +.appflowy-scroll-container { + &::-webkit-scrollbar { + width: 0px; + } +} + .workspaces { ::-webkit-scrollbar { width: 0px; } } + + .MuiPopover-root, .MuiPaper-root { ::-webkit-scrollbar { width: 0; height: 0; } +} + +.view-icon { + &:hover { + background-color: rgba(156, 156, 156, 0.20); + } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index 8d3f07507e..57fe941acc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -1,6 +1,8 @@ import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import isEqual from 'lodash-es/isEqual'; +import { ImageType } from '$app/application/document/document.types'; +import { Nullable } from 'unsplash-js/dist/helpers/typescript'; export const pageTypeMap = { [ViewLayoutPB.Document]: 'document', @@ -14,6 +16,7 @@ export interface Page { name: string; layout: ViewLayoutPB; icon?: PageIcon; + cover?: PageCover; } export interface PageIcon { @@ -21,6 +24,17 @@ export interface PageIcon { value: string; } +export enum CoverType { + Color = 'CoverType.color', + Image = 'CoverType.file', + Asset = 'CoverType.asset', +} +export type PageCover = Nullable<{ + image_type?: ImageType; + cover_selection_type?: CoverType; + cover_selection?: string; +}>; + export function parserViewPBToPage(view: ViewPB): Page { const icon = view.icon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts new file mode 100644 index 0000000000..4861e4de2d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts @@ -0,0 +1,31 @@ +export enum ColorEnum { + Purple = 'appflowy_them_color_tint1', + Pink = 'appflowy_them_color_tint2', + LightPink = 'appflowy_them_color_tint3', + Orange = 'appflowy_them_color_tint4', + Yellow = 'appflowy_them_color_tint5', + Lime = 'appflowy_them_color_tint6', + Green = 'appflowy_them_color_tint7', + Aqua = 'appflowy_them_color_tint8', + Blue = 'appflowy_them_color_tint9', +} + +export const colorMap = { + [ColorEnum.Purple]: 'var(--tint-purple)', + [ColorEnum.Pink]: 'var(--tint-pink)', + [ColorEnum.LightPink]: 'var(--tint-red)', + [ColorEnum.Orange]: 'var(--tint-orange)', + [ColorEnum.Yellow]: 'var(--tint-yellow)', + [ColorEnum.Lime]: 'var(--tint-lime)', + [ColorEnum.Green]: 'var(--tint-green)', + [ColorEnum.Aqua]: 'var(--tint-aqua)', + [ColorEnum.Blue]: 'var(--tint-blue)', +}; + +export function renderColor(color: string) { + if (colorMap[color as ColorEnum]) { + return colorMap[color as ColorEnum]; + } + + return color.replace('0x', '#'); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index fa2520bb7a..91105bdf55 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -39,12 +39,21 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => { styleOverrides: { contained: { color: 'var(--content-on-fill)', + boxShadow: 'var(--shadow)', }, containedPrimary: { '&:hover': { backgroundColor: 'var(--fill-default)', }, }, + containedInherit: { + color: 'var(--text-title)', + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)', + '&:hover': { + backgroundColor: 'var(--bg-body)', + boxShadow: 'var(--shadow)', + }, + }, }, }, MuiButtonBase: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index cbd71ec7d4..03ba493c10 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -8,13 +8,7 @@ function DocumentPage() { const documentId = params.id; if (!documentId) return null; - return ( -
-
- -
-
- ); + return ; } export default DocumentPage; diff --git a/frontend/appflowy_tauri/src/styles/variables/light.variables.css b/frontend/appflowy_tauri/src/styles/variables/light.variables.css index b0ce72e1a5..cdaaf791a5 100644 --- a/frontend/appflowy_tauri/src/styles/variables/light.variables.css +++ b/frontend/appflowy_tauri/src/styles/variables/light.variables.css @@ -92,7 +92,7 @@ --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; --fill-list-active: #edeef2; - --content-blue-400: #00bcf0; + --content-blue-400: rgb(0, 188, 240); --content-blue-300: #52d1f4; --content-blue-600: #009fd1; --content-blue-100: #e0f8ff; @@ -111,7 +111,7 @@ --function-info: #00bcf0; --tint-purple: #e8e0ff; --tint-pink: #ffe7ee; - --tint-red: #ffe7ee; + --tint-red: #ffdddd; --tint-lime: #f5ffdc; --tint-green: #ddffd6; --tint-aqua: #defff1; diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json index 48ff92e680..4e31b0523d 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/base.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -134,7 +134,7 @@ "type": "color" }, "red": { - "value": "#ffe7ee", + "value": "#ffdddd", "type": "color" } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fdf804c38e..8b8a9bd3a2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1239,6 +1239,8 @@ "backgroundColorPurple": "Purple background", "backgroundColorPink": "Pink background", "backgroundColorRed": "Red background", + "backgroundColorLime": "Lime background", + "backgroundColorAqua": "Aqua background", "done": "Done", "cancel": "Cancel", "tint1": "Tint 1",