From 127d0319ad1f07fe2acb0bd99b108bf1536302df Mon Sep 17 00:00:00 2001 From: Kilu Date: Thu, 27 Jun 2024 19:46:02 +0800 Subject: [PATCH] fix: some style --- frontend/appflowy_web_app/pnpm-lock.yaml | 8 +- frontend/appflowy_web_app/server.cjs | 8 +- .../appflowy_web_app/src/assets/appflowy.svg | 25 ++++ .../appflowy_web_app/src/assets/board.svg | 6 + .../appflowy_web_app/src/assets/calendar.svg | 7 + .../appflowy_web_app/src/assets/chat_ai.svg | 10 ++ .../src/assets/chevron_down.svg | 7 + .../appflowy_web_app/src/assets/document.svg | 9 ++ frontend/appflowy_web_app/src/assets/grid.svg | 7 + .../appflowy_web_app/src/assets/report.svg | 25 ++-- .../appflowy_web_app/src/assets/search.svg | 11 ++ .../src/assets/side_outlined.svg | 14 ++ frontend/appflowy_web_app/src/assets/sun.svg | 14 ++ .../src/components/document/Document.tsx | 2 +- .../editor/components/element/Element.tsx | 10 +- .../src/components/publish/PublishView.tsx | 36 ++++- .../publish/header/BreadcrumbItem.tsx | 10 +- .../components/publish/header/MoreActions.tsx | 3 +- .../publish/header/PublishViewHeader.tsx | 62 +++++++- .../components/publish/outline/Outline.tsx | 61 ++++++++ .../publish/outline/OutlineDrawer.tsx | 65 +++++++++ .../publish/outline/OutlineItem.tsx | 84 +++++++++++ .../publish/outline/OutlinePopover.tsx | 71 ++++++++++ .../publish/outline/SearchInput.tsx | 40 ++++++ .../src/components/publish/outline/index.ts | 1 + .../src/components/publish/outline/utils.ts | 21 +++ .../appflowy_web_app/src/utils/hotkeys.ts | 134 ++++++++++++++++++ frontend/resources/translations/en.json | 3 +- 28 files changed, 719 insertions(+), 35 deletions(-) create mode 100644 frontend/appflowy_web_app/src/assets/appflowy.svg create mode 100644 frontend/appflowy_web_app/src/assets/board.svg create mode 100644 frontend/appflowy_web_app/src/assets/calendar.svg create mode 100644 frontend/appflowy_web_app/src/assets/chat_ai.svg create mode 100644 frontend/appflowy_web_app/src/assets/chevron_down.svg create mode 100644 frontend/appflowy_web_app/src/assets/document.svg create mode 100644 frontend/appflowy_web_app/src/assets/grid.svg create mode 100644 frontend/appflowy_web_app/src/assets/search.svg create mode 100644 frontend/appflowy_web_app/src/assets/side_outlined.svg create mode 100644 frontend/appflowy_web_app/src/assets/sun.svg create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/index.ts create mode 100644 frontend/appflowy_web_app/src/components/publish/outline/utils.ts create mode 100644 frontend/appflowy_web_app/src/utils/hotkeys.ts diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 8cfcd10e06..c7c9daa795 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@appflowyinc/client-api-wasm': specifier: 0.1.1 @@ -11662,7 +11666,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/server.cjs b/frontend/appflowy_web_app/server.cjs index 4f7f929970..6b429f9bcc 100644 --- a/frontend/appflowy_web_app/server.cjs +++ b/frontend/appflowy_web_app/server.cjs @@ -5,7 +5,7 @@ const cheerio = require('cheerio'); const { fetch } = require('bun'); const distDir = path.join(__dirname, 'dist'); const indexPath = path.join(distDir, 'index.html'); - +const logo = path.join(distDir, 'appflowy.svg'); const setOrUpdateMetaTag = ($, selector, attribute, content) => { if ($(selector).length === 0) { $('head').append(``); @@ -88,13 +88,13 @@ const createServer = async (req) => { let htmlData = fs.readFileSync(indexPath, 'utf8'); const $ = cheerio.load(htmlData); - const description = 'Write, share, comment, react, and publish docs quickly and securely on AppFlowy.'; + const description = 'Write, share, and publish docs quickly on AppFlowy. \n Get started for free.'; let title = 'AppFlowy'; const url = 'https://appflowy.com'; - let image = 'https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png'; + let image = logo; // Inject meta data into the HTML to support SEO and social sharing if (metaData) { - title = metaData.view.name; + title = `${metaData.view.name} | AppFlowy`; try { const cover = metaData.view.extra ? JSON.parse(metaData.view.extra)?.cover : null; diff --git a/frontend/appflowy_web_app/src/assets/appflowy.svg b/frontend/appflowy_web_app/src/assets/appflowy.svg new file mode 100644 index 0000000000..c282c112e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/appflowy.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg new file mode 100644 index 0000000000..0ae7bbfd20 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/board.svg @@ -0,0 +1,6 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/calendar.svg b/frontend/appflowy_web_app/src/assets/calendar.svg new file mode 100644 index 0000000000..dc9ad4b579 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/calendar.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/chat_ai.svg b/frontend/appflowy_web_app/src/assets/chat_ai.svg new file mode 100644 index 0000000000..a01aa41887 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/chat_ai.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/chevron_down.svg b/frontend/appflowy_web_app/src/assets/chevron_down.svg new file mode 100644 index 0000000000..fbf3c9aabd --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/chevron_down.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg new file mode 100644 index 0000000000..d418c09dd2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/document.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg new file mode 100644 index 0000000000..19dfe4b6e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/grid.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/report.svg b/frontend/appflowy_web_app/src/assets/report.svg index 71dacf8aba..6a5bd10fa3 100644 --- a/frontend/appflowy_web_app/src/assets/report.svg +++ b/frontend/appflowy_web_app/src/assets/report.svg @@ -1,11 +1,18 @@ - - - - - + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg new file mode 100644 index 0000000000..fdad759166 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/search.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/side_outlined.svg b/frontend/appflowy_web_app/src/assets/side_outlined.svg new file mode 100644 index 0000000000..6f1be3f72d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/side_outlined.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/sun.svg b/frontend/appflowy_web_app/src/assets/sun.svg new file mode 100644 index 0000000000..fe4e2e8982 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/sun.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 79cf106649..f87963ee81 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -23,7 +23,7 @@ export const Document = ({ ...viewMeta }: DocumentProps) => { return ( -
+
}>
diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index 2772ecd01b..8c7b881313 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -2,7 +2,7 @@ import { BlockData, BlockType, InlineBlockType, YjsEditorKey } from '@/applicati import { BulletedList } from '@/components/editor/components/blocks/bulleted-list'; import { Callout } from '@/components/editor/components/blocks/callout'; import { CodeBlock } from '@/components/editor/components/blocks/code'; -import { DatabaseBlock } from '@/components/editor/components/blocks/database'; +// import { DatabaseBlock } from '@/components/editor/components/blocks/database'; import { DividerNode } from '@/components/editor/components/blocks/divider'; import { Heading } from '@/components/editor/components/blocks/heading'; import { ImageBlock } from '@/components/editor/components/blocks/image'; @@ -70,10 +70,10 @@ export const Element = memo( return TableBlock; case BlockType.TableCell: return TableCellBlock; - case BlockType.GridBlock: - case BlockType.BoardBlock: - case BlockType.CalendarBlock: - return DatabaseBlock; + // case BlockType.GridBlock: + // case BlockType.BoardBlock: + // case BlockType.CalendarBlock: + // return DatabaseBlock; default: return UnSupportedBlock; } diff --git a/frontend/appflowy_web_app/src/components/publish/PublishView.tsx b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx index 356bb5ac39..ce790e6b58 100644 --- a/frontend/appflowy_web_app/src/components/publish/PublishView.tsx +++ b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx @@ -3,6 +3,8 @@ import { PublishProvider } from '@/application/publish'; import { AFScroller } from '@/components/_shared/scroller'; import { AFConfigContext } from '@/components/app/AppConfig'; import CollabView from '@/components/publish/CollabView'; +import OutlineDrawer from '@/components/publish/outline/OutlineDrawer'; +import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { PublishViewHeader } from 'src/components/publish/header'; import NotFound from '@/components/error/NotFound'; @@ -12,6 +14,8 @@ export interface PublishViewProps { publishName: string; } +const drawerWidth = 268; + export function PublishView({ namespace, publishName }: PublishViewProps) { const [doc, setDoc] = useState(); const [notFound, setNotFound] = useState(false); @@ -35,6 +39,25 @@ export function PublishView({ namespace, publishName }: PublishViewProps) { void openPublishView(); }, [openPublishView]); + const [open, setOpen] = useState(false); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + switch (true) { + case createHotkey(HOT_KEY_NAME.TOGGLE_SIDEBAR)(e): + e.preventDefault(); + setOpen((prev) => !prev); + break; + default: + break; + } + }, []); + + useEffect(() => { + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); if (notFound && !doc) { return ; } @@ -42,11 +65,22 @@ export function PublishView({ namespace, publishName }: PublishViewProps) { return (
- + { + setOpen(true); + }} + openDrawer={open} + drawerWidth={drawerWidth} + /> + {open && setOpen(false)} />} + diff --git a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx index 4b7f2a9231..9c88c5b121 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx @@ -1,14 +1,14 @@ -import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; -import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; -import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; -import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '@/assets/board.svg'; +import { ReactComponent as CalendarSvg } from '@/assets/calendar.svg'; +import { ReactComponent as DocumentSvg } from '@/assets/document.svg'; +import { ReactComponent as GridSvg } from '@/assets/grid.svg'; import { ViewLayout } from '@/application/collab.type'; import { usePublishContext } from '@/application/publish'; import { notify } from '@/components/_shared/notify'; import React from 'react'; import { useTranslation } from 'react-i18next'; -const renderCrumbIcon = (icon: string) => { +export const renderCrumbIcon = (icon: string) => { if (Number(icon) === ViewLayout.Grid) { return ; } diff --git a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx index d0de0ad15b..8ec8e8a08d 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx @@ -5,6 +5,7 @@ import { IconButton } from '@mui/material'; import React, { useContext, useMemo } from 'react'; import { ReactComponent as MoreIcon } from '@/assets/more.svg'; import { ReactComponent as MoonIcon } from '@/assets/moon.svg'; +import { ReactComponent as SunIcon } from '@/assets/sun.svg'; import { ReactComponent as ReportIcon } from '@/assets/report.svg'; import { useTranslation } from 'react-i18next'; @@ -27,7 +28,7 @@ function MoreActions() { return [ isDark ? { - Icon: MoonIcon, + Icon: SunIcon, label: t('settings.appearance.themeMode.light'), onClick: () => { setDark?.(false); diff --git a/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx b/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx index 3e20aae8e4..dab2192b83 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx @@ -1,12 +1,23 @@ import { usePublishContext } from '@/application/publish'; import { openOrDownload } from '@/components/publish/header/utils'; -import { Divider } from '@mui/material'; -import React, { useMemo } from 'react'; +import { Divider, IconButton } from '@mui/material'; +import { debounce } from 'lodash-es'; +import React, { useCallback, useMemo } from 'react'; +import OutlinePopover from '@/components/publish/outline/OutlinePopover'; import Breadcrumb from './Breadcrumb'; import { ReactComponent as Logo } from '@/assets/logo.svg'; import MoreActions from './MoreActions'; +import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; -export function PublishViewHeader() { +export function PublishViewHeader({ + onOpenDrawer, + drawerWidth, + openDrawer, +}: { + onOpenDrawer: () => void; + openDrawer: boolean; + drawerWidth: number; +}) { const viewMeta = usePublishContext()?.viewMeta; const crumbs = useMemo(() => { const ancestors = viewMeta?.ancestor_views.slice(1) || []; @@ -29,10 +40,53 @@ export function PublishViewHeader() { }; }); }, [viewMeta]); + const [openPopover, setOpenPopover] = React.useState(false); + + const debounceClosePopover = useMemo(() => { + return debounce(() => { + setOpenPopover(false); + }, 200); + }, []); + + const handleOpenPopover = useCallback(() => { + debounceClosePopover.cancel(); + if (openDrawer) { + return; + } + + setOpenPopover(true); + }, [openDrawer, debounceClosePopover]); return ( -
+
+ {!openDrawer && ( + + { + setOpenPopover(false); + onOpenDrawer(); + }} + onMouseEnter={handleOpenPopover} + onMouseLeave={debounceClosePopover} + > + + + + )} +
diff --git a/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx b/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx new file mode 100644 index 0000000000..dff7ce60e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx @@ -0,0 +1,61 @@ +import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; +import OutlineItem from '@/components/publish/outline/OutlineItem'; +import SearchInput from '@/components/publish/outline/SearchInput'; +import { filterViews } from '@/components/publish/outline/utils'; +import { CircularProgress } from '@mui/material'; +import React, { useCallback, useEffect } from 'react'; + +function Outline({ viewMeta }: { viewMeta?: PublishViewInfo }) { + const hasChildren = Boolean(viewMeta?.child_views?.length); + + const [children, setChildren] = React.useState([]); + + useEffect(() => { + if (viewMeta) { + setChildren(viewMeta.child_views || []); + } + }, [viewMeta]); + + const handleSearch = useCallback( + (val: string) => { + if (!val) { + return setChildren(viewMeta?.child_views || []); + } + + setChildren(filterViews(viewMeta?.child_views || [], val)); + }, + [viewMeta] + ); + + if (!viewMeta) { + return ; + } + + return ( +
+
+ +
+ + {hasChildren && ( +
+ {children + .filter((view) => view.layout === ViewLayout.Document) + .map((view: PublishViewInfo) => ( + + ))} +
+ )} +
+ ); +} + +export default Outline; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx new file mode 100644 index 0000000000..3cbf82fa0a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx @@ -0,0 +1,65 @@ +import { usePublishContext } from '@/application/publish'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; +import Outline from '@/components/publish/outline/Outline'; +import { createHotKeyLabel, HOT_KEY_NAME } from '@/utils/hotkeys'; +import { Drawer, IconButton, Tooltip } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function OutlineDrawer({ open, width, onClose }: { open: boolean; width: number; onClose: () => void }) { + const { t } = useTranslation(); + const viewMeta = usePublishContext()?.viewMeta; + + return ( + +
+
+
{ + window.open('https://appflowy.io', '_blank'); + }} + > + + +
+ + {t('sideBar.closeSidebar')} + {createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)} +
+ } + > + + + + +
+
+ +
+
+ + ); +} + +export default OutlineDrawer; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx new file mode 100644 index 0000000000..93f945a1f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx @@ -0,0 +1,84 @@ +import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; +import { PublishContext } from '@/application/publish'; +import { notify } from '@/components/_shared/notify'; +import { renderCrumbIcon } from '@/components/publish/header/BreadcrumbItem'; +import React, { useCallback, useContext } from 'react'; +import { ReactComponent as ChevronDownIcon } from '@/assets/chevron_down.svg'; +import { useTranslation } from 'react-i18next'; + +function OutlineItem({ view }: { view: PublishViewInfo }) { + const [isExpanded, setIsExpanded] = React.useState(false); + const getIcon = useCallback(() => { + if (isExpanded) { + return ( + + ); + } + + return ( + + ); + }, [isExpanded]); + const { t } = useTranslation(); + + const navigateToView = useContext(PublishContext)?.toView; + const renderItem = (item: PublishViewInfo) => { + return ( +
+
+ {item.child_views?.length ? getIcon() : null} +
{ + try { + await navigateToView?.(item.view_id); + } catch (e) { + notify.error(t('publish.hasNotBeenPublished')); + } + }} + className={'flex flex-1 cursor-pointer items-center gap-1'} + > +
{renderCrumbIcon(item.icon?.value || String(item.layout))}
+
{item.name}
+
+
+
+ ); + }; + + return ( +
+ {renderItem(view)} +
+ {view.child_views + ?.filter((view) => view.layout === ViewLayout.Document) + ?.map((item, index) => ( + + ))} +
+
+ ); +} + +export default OutlineItem; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx new file mode 100644 index 0000000000..a64f2084ba --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx @@ -0,0 +1,71 @@ +import { usePublishContext } from '@/application/publish'; +import Outline from '@/components/publish/outline/Outline'; +import { Divider, PopperPlacementType } from '@mui/material'; +import React, { ReactElement, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import RichTooltip from 'src/components/_shared/popover/RichTooltip'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; + +export function OutlinePopover({ + children, + open, + onClose, + placement, + onMouseEnter, + onMouseLeave, +}: { + open: boolean; + onClose: () => void; + children: ReactElement; + placement?: PopperPlacementType; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +}) { + const viewMeta = usePublishContext()?.viewMeta; + const { t } = useTranslation(); + + const content = useMemo(() => { + return ( +
+ +
+ {Boolean(viewMeta?.child_views?.length) && } + +
+
{t('publish.createdWith')}
+
{ + window.open('https://appflowy.io', '_blank'); + }} + > + + +
+
+
+
+ ); + }, [onMouseEnter, onMouseLeave, t, viewMeta]); + + return ( + + {children} + + ); +} + +export default OutlinePopover; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx b/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx new file mode 100644 index 0000000000..5f83b758d6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx @@ -0,0 +1,40 @@ +import { InputAdornment, OutlinedInput } from '@mui/material'; +import { debounce } from 'lodash-es'; +import React from 'react'; +import { ReactComponent as SearchIcon } from '@/assets/search.svg'; +import { useTranslation } from 'react-i18next'; + +function SearchInput({ onSearch }: { onSearch: (value: string) => void }) { + const [value, setValue] = React.useState(''); + + const debounceSearch = React.useMemo(() => { + return debounce((value: string) => { + onSearch(value); + }, 200); + }, [onSearch]); + const handleChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + debounceSearch(event.target.value); + }; + + const { t } = useTranslation(); + + return ( + + + + } + onChange={handleChange} + placeholder={t('search.label')} + className={'h-[30px] w-full rounded-lg bg-bg-body'} + value={value} + size={'small'} + /> + ); +} + +export default SearchInput; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/index.ts b/frontend/appflowy_web_app/src/components/publish/outline/index.ts new file mode 100644 index 0000000000..626a625ee5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/index.ts @@ -0,0 +1 @@ +export * from './OutlinePopover'; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/utils.ts b/frontend/appflowy_web_app/src/components/publish/outline/utils.ts new file mode 100644 index 0000000000..284c24505f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/utils.ts @@ -0,0 +1,21 @@ +import { PublishViewInfo } from '@/application/collab.type'; + +export function filterViews(views: PublishViewInfo[], keyword: string): PublishViewInfo[] { + const filterAndFlatten = (views: PublishViewInfo[]): PublishViewInfo[] => { + let result: PublishViewInfo[] = []; + + for (const view of views) { + if (view.name.toLowerCase().includes(keyword.toLowerCase())) { + result.push(view); + } else if (view.child_views) { + const filteredChildren = filterAndFlatten(view.child_views); + + result = result.concat(filteredChildren); + } + } + + return result; + }; + + return filterAndFlatten(views); +} diff --git a/frontend/appflowy_web_app/src/utils/hotkeys.ts b/frontend/appflowy_web_app/src/utils/hotkeys.ts new file mode 100644 index 0000000000..20aa05db27 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/hotkeys.ts @@ -0,0 +1,134 @@ +import isHotkey from 'is-hotkey'; + +export const isMac = () => { + return navigator.userAgent.includes('Mac OS X'); +}; + +const MODIFIERS = { + control: 'Ctrl', + meta: '⌘', +}; + +export const getModifier = () => { + return isMac() ? MODIFIERS.meta : MODIFIERS.control; +}; + +export enum HOT_KEY_NAME { + LEFT = 'left', + RIGHT = 'right', + SELECT_ALL = 'select-all', + ESCAPE = 'escape', + ALIGN_LEFT = 'align-left', + ALIGN_CENTER = 'align-center', + ALIGN_RIGHT = 'align-right', + BOLD = 'bold', + ITALIC = 'italic', + UNDERLINE = 'underline', + STRIKETHROUGH = 'strikethrough', + CODE = 'code', + TOGGLE_TODO = 'toggle-todo', + TOGGLE_COLLAPSE = 'toggle-collapse', + INDENT_BLOCK = 'indent-block', + OUTDENT_BLOCK = 'outdent-block', + INSERT_SOFT_BREAK = 'insert-soft-break', + SPLIT_BLOCK = 'split-block', + BACKSPACE = 'backspace', + OPEN_LINK = 'open-link', + OPEN_LINKS = 'open-links', + EXTEND_LINE_BACKWARD = 'extend-line-backward', + EXTEND_LINE_FORWARD = 'extend-line-forward', + PASTE = 'paste', + PASTE_PLAIN_TEXT = 'paste-plain-text', + HIGH_LIGHT = 'high-light', + EXTEND_DOCUMENT_BACKWARD = 'extend-document-backward', + EXTEND_DOCUMENT_FORWARD = 'extend-document-forward', + SCROLL_TO_TOP = 'scroll-to-top', + SCROLL_TO_BOTTOM = 'scroll-to-bottom', + FORMAT_LINK = 'format-link', + FIND_REPLACE = 'find-replace', + /** + * Navigation + */ + TOGGLE_THEME = 'toggle-theme', + TOGGLE_SIDEBAR = 'toggle-sidebar', +} + +const defaultHotKeys = { + [HOT_KEY_NAME.ALIGN_LEFT]: ['control+shift+l'], + [HOT_KEY_NAME.ALIGN_CENTER]: ['control+shift+e'], + [HOT_KEY_NAME.ALIGN_RIGHT]: ['control+shift+r'], + [HOT_KEY_NAME.BOLD]: ['mod+b'], + [HOT_KEY_NAME.ITALIC]: ['mod+i'], + [HOT_KEY_NAME.UNDERLINE]: ['mod+u'], + [HOT_KEY_NAME.STRIKETHROUGH]: ['mod+shift+s', 'mod+shift+x'], + [HOT_KEY_NAME.CODE]: ['mod+e'], + [HOT_KEY_NAME.TOGGLE_TODO]: ['mod+enter'], + [HOT_KEY_NAME.TOGGLE_COLLAPSE]: ['mod+enter'], + [HOT_KEY_NAME.SELECT_ALL]: ['mod+a'], + [HOT_KEY_NAME.ESCAPE]: ['esc'], + [HOT_KEY_NAME.INDENT_BLOCK]: ['tab'], + [HOT_KEY_NAME.OUTDENT_BLOCK]: ['shift+tab'], + [HOT_KEY_NAME.SPLIT_BLOCK]: ['enter'], + [HOT_KEY_NAME.INSERT_SOFT_BREAK]: ['shift+enter'], + [HOT_KEY_NAME.BACKSPACE]: ['backspace', 'shift+backspace'], + [HOT_KEY_NAME.OPEN_LINK]: ['opt+enter'], + [HOT_KEY_NAME.OPEN_LINKS]: ['opt+shift+enter'], + [HOT_KEY_NAME.EXTEND_LINE_BACKWARD]: ['opt+shift+left'], + [HOT_KEY_NAME.EXTEND_LINE_FORWARD]: ['opt+shift+right'], + [HOT_KEY_NAME.PASTE]: ['mod+v'], + [HOT_KEY_NAME.PASTE_PLAIN_TEXT]: ['mod+shift+v'], + [HOT_KEY_NAME.HIGH_LIGHT]: ['mod+shift+h'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD]: ['mod+shift+up'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD]: ['mod+shift+down'], + [HOT_KEY_NAME.SCROLL_TO_TOP]: ['home'], + [HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['end'], + [HOT_KEY_NAME.TOGGLE_THEME]: ['mod+shift+l'], + [HOT_KEY_NAME.TOGGLE_SIDEBAR]: ['mod+.'], + [HOT_KEY_NAME.FORMAT_LINK]: ['mod+k'], + [HOT_KEY_NAME.LEFT]: ['left'], + [HOT_KEY_NAME.RIGHT]: ['right'], + [HOT_KEY_NAME.FIND_REPLACE]: ['mod+f'], +}; + +const replaceModifier = (hotkey: string) => { + return hotkey.replace('mod', getModifier()).replace('control', 'ctrl'); +}; + +/** + * Create a hotkey checker. + * @example trigger strike through when user press "Cmd + Shift + S" or "Cmd + Shift + X" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName]; + + return (event: KeyboardEvent) => { + return hotkeys.some((hotkey) => { + return isHotkey(hotkey, event); + }); + }; +}; + +/** + * Create a hotkey label. + * eg. "Ctrl + B / ⌘ + B" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName].map((key) => replaceModifier(key)); + + return hotkeys + .map((hotkey) => + hotkey + .split('+') + .map((key) => { + return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1); + }) + .join(' + ') + ) + .join(' / '); +}; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e6440bec7c..cc31b09841 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2037,6 +2037,7 @@ "publish": { "hasNotBeenPublished": "This page hasn't been published yet", "reportPage": "Report page", - "databaseHasNotBeenPublished": "This database hasn't been published yet" + "databaseHasNotBeenPublished": "This database hasn't been published yet", + "createdWith": "Created with" } }