diff --git a/frontend/appflowy_web_app/src/application/publish/context.tsx b/frontend/appflowy_web_app/src/application/publish/context.tsx index 9e7773624b871..efdae2bf2fbca 100644 --- a/frontend/appflowy_web_app/src/application/publish/context.tsx +++ b/frontend/appflowy_web_app/src/application/publish/context.tsx @@ -1,7 +1,9 @@ import { GetViewRowsMap, LoadView, LoadViewMeta } from '@/application/collab.type'; import { db } from '@/application/db'; import { ViewMeta } from '@/application/db/tables/view_metas'; -import { AFConfigContext } from '@/components/app/app.hooks'; +import { View } from '@/application/types'; +import { useService } from '@/components/app/app.hooks'; +import { notify } from '@/components/_shared/notify'; import { useLiveQuery } from 'dexie-react-hooks'; import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -14,8 +16,8 @@ export interface PublishContextType { toView: (viewId: string) => Promise; loadViewMeta: LoadViewMeta; getViewRowsMap?: GetViewRowsMap; - loadView: LoadView; + outline?: View; } export const PublishContext = createContext(null); @@ -36,6 +38,9 @@ export const PublishProvider = ({ return db.view_metas.get(name); }, [namespace, publishName]); + + const [outline, setOutline] = useState(); + const [subscribers, setSubscribers] = useState void>>(new Map()); useEffect(() => { @@ -43,6 +48,7 @@ export const PublishProvider = ({ setSubscribers(new Map()); }; }, []); + useEffect(() => { db.view_metas.hook('creating', (primaryKey, obj) => { const subscriber = subscribers.get(primaryKey); @@ -72,7 +78,8 @@ export const PublishProvider = ({ const prevViewMeta = useRef(viewMeta); - const service = useContext(AFConfigContext)?.service; + const service = useService(); + const navigate = useNavigate(); const toView = useCallback( async (viewId: string) => { @@ -83,9 +90,13 @@ export const PublishProvider = ({ throw new Error('Not found'); } - const { namespace, publishName } = res; + const { namespace: viewNamespace, publishName } = res; - navigate(`/${namespace}/${publishName}`); + prevViewMeta.current = undefined; + navigate(`/${viewNamespace}/${publishName}`, { + replace: true, + }); + return; } catch (e) { return Promise.reject(e); } @@ -93,6 +104,22 @@ export const PublishProvider = ({ [navigate, service], ); + const loadOutline = useCallback(async () => { + if (!service || !namespace) return; + console.log('loadOutline', namespace); + try { + const res = await service?.getPublishOutline(namespace); + + if (!res) { + throw new Error('Publish outline not found'); + } + + setOutline(res); + } catch (e) { + notify.error('Publish outline not found'); + } + }, [namespace, service]); + const loadViewMeta = useCallback( async (viewId: string, callback?: (meta: ViewMeta) => void) => { try { @@ -188,6 +215,10 @@ export const PublishProvider = ({ prevViewMeta.current = viewMeta; }, [viewMeta]); + useEffect(() => { + void loadOutline(); + }, [loadOutline]); + return ( {children} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts index b95a38257850d..cfeda5e915b72 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts @@ -11,7 +11,7 @@ import { TemplateCreator, TemplateCreatorFormValues, TemplateSummary, UploadTemplatePayload, } from '@/application/template.type'; -import { FolderView, User, Workspace } from '@/application/types'; +import { FolderView, User, View, Workspace } from '@/application/types'; import axios, { AxiosInstance } from 'axios'; import dayjs from 'dayjs'; @@ -233,6 +233,23 @@ export async function getPublishInfoWithViewId (viewId: string) { return Promise.reject(data); } +export async function getPublishOutline (publishNamespace: string) { + const url = `/api/workspace/published-outline/${publishNamespace}`; + const response = await axiosInstance?.get<{ + code: number; + data?: View; + message: string; + }>(url); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data; + } + + return Promise.reject(data); +} + export async function getPublishViewComments (viewId: string): Promise { const url = `/api/workspace/published-info/${viewId}/comment`; const response = await axiosInstance?.get<{ diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index b8df216c22512..fe75849a7f679 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -164,6 +164,10 @@ export class AFClientService implements AFService { return data; } + async getPublishOutline (namespace: string) { + return APIService.getPublishOutline(namespace); + } + async loginAuth (url: string) { try { console.log('loginAuth', url); diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index a346800058f8b..bde0cc4dbe47a 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -9,7 +9,7 @@ import { UploadTemplatePayload, } from '@/application/template.type'; import * as Y from 'yjs'; -import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types'; +import { DuplicatePublishView, FolderView, User, View, Workspace } from '@/application/types'; export type AFService = PublishService; @@ -37,6 +37,8 @@ export interface PublishService { destroy: () => void; }>; + getPublishOutline (namespace: string): Promise; + getPublishViewGlobalComments: (viewId: string) => Promise; createCommentOnPublishView: (viewId: string, content: string, replyCommentId?: string) => Promise; deleteCommentOnPublishView: (viewId: string, commentId: string) => Promise; diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index e6691c473ed32..cfdcda4acbb9c 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -24,6 +24,10 @@ export class AFClientService implements AFService { return Promise.reject('Method not implemented'); } + async getPublishOutline (_namespace: string) { + return Promise.reject('Method not implemented'); + } + async getPublishViewMeta (_namespace: string, _publishName: string) { return Promise.reject('Method not implemented'); } diff --git a/frontend/appflowy_web_app/src/application/types.ts b/frontend/appflowy_web_app/src/application/types.ts index 1549df084bace..d9d89d080ca7a 100644 --- a/frontend/appflowy_web_app/src/application/types.ts +++ b/frontend/appflowy_web_app/src/application/types.ts @@ -1,4 +1,4 @@ -import { CollabType } from '@/application/collab.type'; +import { CollabType, ViewLayout } from '@/application/collab.type'; export interface Workspace { icon: string; @@ -38,3 +38,25 @@ export interface DuplicatePublishView { collabType: CollabType; viewId: string; } + +export interface ViewIcon { + ty: number; + value: string; +} + +export interface ViewExtra { + is_space: boolean; + space_created_at?: number; + space_icon?: string; + space_icon_color?: string; + space_permission?: number; +} + +export interface View { + view_id: string; + name: string; + icon: ViewIcon | null; + layout: ViewLayout; + extra: ViewExtra | null; + children: View[]; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/appflowy.svg b/frontend/appflowy_web_app/src/assets/appflowy.svg index c282c112e1345..7ef34c9713b5c 100644 --- a/frontend/appflowy_web_app/src/assets/appflowy.svg +++ b/frontend/appflowy_web_app/src/assets/appflowy.svg @@ -1,25 +1,34 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/chevron_down.svg b/frontend/appflowy_web_app/src/assets/chevron_down.svg index fbf3c9aabd7d6..880c2dde18544 100644 --- a/frontend/appflowy_web_app/src/assets/chevron_down.svg +++ b/frontend/appflowy_web_app/src/assets/chevron_down.svg @@ -1,5 +1,5 @@ - + diff --git a/frontend/appflowy_web_app/src/components/_shared/appflowy-power/AppFlowyPower.tsx b/frontend/appflowy_web_app/src/components/_shared/appflowy-power/AppFlowyPower.tsx new file mode 100644 index 0000000000000..2b3698c327b50 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/appflowy-power/AppFlowyPower.tsx @@ -0,0 +1,41 @@ +import { Divider } from '@mui/material'; +import React from 'react'; +import { ReactComponent as AppFlowyLogo } from '@/assets/appflowy.svg'; + +function AppFlowyPower ({ + divider, + width, +}: { + divider?: boolean; + width?: number; +}) { + return ( +
+ {divider && } + +
{ + window.open('https://appflowy.io', '_blank'); + }} + style={{ + width, + }} + className={ + 'flex w-full cursor-pointer gap-2 items-center justify-center py-4 text-sm text-text-title opacity-50' + } + > + Powered by + +
+
+ ); +} + +export default AppFlowyPower; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx index bb90b80c4186d..e67b8a388a607 100644 --- a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx @@ -7,9 +7,13 @@ interface Props { open: boolean; onClose: () => void; placement?: PopperPlacementType; + PaperProps?: { + className?: string; + }; + } -export const RichTooltip = ({ placement = 'top', open, onClose, content, children }: Props) => { +export const RichTooltip = ({ placement = 'top', open, onClose, content, children, PaperProps }: Props) => { const [childNode, setChildNode] = React.useState(null); const [, setTransitioning] = React.useState(false); @@ -48,7 +52,8 @@ export const RichTooltip = ({ placement = 'top', open, onClose, content, childre > - + {content} diff --git a/frontend/appflowy_web_app/src/components/_shared/skeleton/BreadcrumbSkeleton.tsx b/frontend/appflowy_web_app/src/components/_shared/skeleton/BreadcrumbSkeleton.tsx new file mode 100644 index 0000000000000..d43b8d5a99220 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/skeleton/BreadcrumbSkeleton.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Skeleton, Box } from '@mui/material'; + +export const BreadcrumbsSkeleton = () => { + return ( + + + + + + + + ); +}; + +export default BreadcrumbsSkeleton; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/document/DocumentSkeleton.tsx b/frontend/appflowy_web_app/src/components/_shared/skeleton/DocumentSkeleton.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/document/DocumentSkeleton.tsx rename to frontend/appflowy_web_app/src/components/_shared/skeleton/DocumentSkeleton.tsx diff --git a/frontend/appflowy_web_app/src/components/_shared/skeleton/OutlineSkeleton.tsx b/frontend/appflowy_web_app/src/components/_shared/skeleton/OutlineSkeleton.tsx new file mode 100644 index 0000000000000..dcf0f19e8bd59 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/skeleton/OutlineSkeleton.tsx @@ -0,0 +1,47 @@ +import { Box, Skeleton } from '@mui/material'; +import './skeleton.scss'; + +export const DirectoryStructure = () => { + return ( + +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/_shared/skeleton/skeleton.scss b/frontend/appflowy_web_app/src/components/_shared/skeleton/skeleton.scss new file mode 100644 index 0000000000000..37e86ff1db9b2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/skeleton/skeleton.scss @@ -0,0 +1,14 @@ +.directory-item { + display: flex; + align-items: center; + margin-bottom: 8px; + +} + +.directory-item .MuiSkeleton-root { + margin-right: 8px; +} + +.nested { + margin-left: 24px; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/as-template/AsTemplate.tsx b/frontend/appflowy_web_app/src/components/as-template/AsTemplate.tsx index 7136f41f99566..fcb7ed7a0e22e 100644 --- a/frontend/appflowy_web_app/src/components/as-template/AsTemplate.tsx +++ b/frontend/appflowy_web_app/src/components/as-template/AsTemplate.tsx @@ -136,7 +136,7 @@ function AsTemplate ({ submitRef.current?.click(); }} variant={'contained'} color={'primary'} > - {t('template.asTemplate')} + {t('button.save')} diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 63609a8a1608a..a1f6de747f651 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -1,5 +1,5 @@ import { GetViewRowsMap, LoadView, LoadViewMeta, YDoc } from '@/application/collab.type'; -import DocumentSkeleton from '@/components/document/DocumentSkeleton'; +import DocumentSkeleton from '@/components/_shared/skeleton/DocumentSkeleton'; import { Editor } from '@/components/editor'; import React, { Suspense } from 'react'; import ViewMetaPreview, { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; @@ -24,7 +24,7 @@ export const Document = ({ isTemplateThumb, }: DocumentProps) => { return ( -
+
}>
diff --git a/frontend/appflowy_web_app/src/components/global-comment/add-comment/AddCommentWrapper.tsx b/frontend/appflowy_web_app/src/components/global-comment/add-comment/AddCommentWrapper.tsx index c5ad785db6e12..1cf71bf60100b 100644 --- a/frontend/appflowy_web_app/src/components/global-comment/add-comment/AddCommentWrapper.tsx +++ b/frontend/appflowy_web_app/src/components/global-comment/add-comment/AddCommentWrapper.tsx @@ -9,6 +9,7 @@ export function AddCommentWrapper () { const { replyCommentId } = useGlobalCommentContext(); const addCommentRef = useRef(null); const [showFixedAddComment, setShowFixedAddComment] = useState(false); + const [offsetLeft, setOffsetLeft] = useState(0); const [focus, setFocus] = useState(false); const [content, setContent] = useState(''); @@ -22,12 +23,14 @@ export function AddCommentWrapper () { const element = addCommentRef.current; if (!element) return; + const scrollContainer = getScrollParent(element); if (!scrollContainer) return; const handleScroll = () => { - const isIntersecting = element.getBoundingClientRect().top < HEADER_HEIGHT; + const rect = element.getBoundingClientRect(); + const isIntersecting = rect.top < HEADER_HEIGHT; if (isIntersecting) { setShowFixedAddComment(true); @@ -42,6 +45,20 @@ export function AddCommentWrapper () { }; }, []); + useEffect(() => { + const element = addCommentRef.current; + + if (!element) return; + const scrollContainer = getScrollParent(element); + + if (!scrollContainer) return; + if (showFixedAddComment) { + setOffsetLeft(scrollContainer.getBoundingClientRect().left || 0); + } else { + setOffsetLeft(0); + } + }, [showFixedAddComment]); + return ( <>
@@ -55,7 +72,11 @@ export function AddCommentWrapper () {
{showFixedAddComment && ( -
+
diff --git a/frontend/appflowy_web_app/src/components/publish/PublishView.tsx b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx index 9c1246da998a6..554e5dc9663d0 100644 --- a/frontend/appflowy_web_app/src/components/publish/PublishView.tsx +++ b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx @@ -6,7 +6,8 @@ import { AFConfigContext } from '@/components/app/app.hooks'; import { GlobalCommentProvider } from '@/components/global-comment'; import CollabView from '@/components/publish/CollabView'; import { OutlineDrawer } from '@/components/publish/outline'; -import React, { Suspense, useCallback, useContext, useEffect, useState } from 'react'; +import { getPlatform } from '@/utils/platform'; +import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { PublishViewHeader } from '@/components/publish/header'; import NotFound from '@/components/error/NotFound'; import { useSearchParams } from 'react-router-dom'; @@ -42,18 +43,26 @@ export function PublishView ({ namespace, publishName }: PublishViewProps) { void openPublishView(); }, [openPublishView]); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(() => { + return localStorage.getItem('publish_outline_open') === 'true'; + }); const [search] = useSearchParams(); const isTemplate = search.get('template') === 'true'; const isTemplateThumb = isTemplate && search.get('thumbnail') === 'true'; + useEffect(() => { + localStorage.setItem('publish_outline_open', open ? 'true' : 'false'); + }, [open]); + useEffect(() => { if (!isTemplateThumb) return; document.documentElement.setAttribute('thumbnail', 'true'); }, [isTemplateThumb]); + const drawerOpened = useMemo(() => open && !getPlatform().isMobile, [open]); + if (notFound && !doc) { return ; } @@ -73,8 +82,8 @@ export function PublishView ({ namespace, publishName }: PublishViewProps) { overflowXHidden overflowYHidden={isTemplateThumb} style={{ - transform: open ? `translateX(${drawerWidth}px)` : 'none', - width: open ? `calc(100% - ${drawerWidth}px)` : '100%', + transform: drawerOpened ? `translateX(${drawerWidth}px)` : 'none', + width: drawerOpened ? `calc(100% - ${drawerWidth}px)` : '100%', transition: 'width 0.2s ease-in-out, transform 0.2s ease-in-out', }} className={'appflowy-layout appflowy-scroll-container h-full'} @@ -83,7 +92,11 @@ export function PublishView ({ namespace, publishName }: PublishViewProps) { onOpenDrawer={() => { setOpen(true); }} - openDrawer={open} + drawerWidth={drawerWidth} + onCloseDrawer={() => { + setOpen(false); + }} + openDrawer={drawerOpened} />} @@ -93,7 +106,8 @@ export function PublishView ({ namespace, publishName }: PublishViewProps) { )} - {open && setOpen(false)} />} + {drawerOpened && + 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 2e3c5d2e48b33..14d6e01cdb020 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx @@ -42,7 +42,7 @@ function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: Crumb; disabl return (
{ if (disableClick) return; try { @@ -54,10 +54,10 @@ function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: Crumb; disabl > {extraObj && extraObj.is_space ? ( 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 8d7eec6eb14af..66125dbd6b5a3 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx @@ -12,9 +12,6 @@ import { ReactComponent as SunIcon } from '@/assets/sun.svg'; import { ReactComponent as LoginIcon } from '@/assets/login.svg'; import { ReactComponent as ReportIcon } from '@/assets/report.svg'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; -import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; - import { useNavigate } from 'react-router-dom'; function MoreActions () { @@ -110,18 +107,6 @@ function MoreActions () { ))} -
{ - window.open('https://appflowy.io', '_blank'); - }} - className={ - 'flex w-full cursor-pointer items-center justify-center py-2 text-sm text-text-title opacity-50' - } - > - Powered by - - -
)} 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 16e0e71f7235e..82e28412d9f72 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx @@ -2,6 +2,7 @@ import { usePublishContext } from '@/application/publish'; import { useCurrentUser } from '@/components/app/app.hooks'; import { openOrDownload } from '@/components/publish/header/utils'; import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; +import { getPlatform } from '@/utils/platform'; import { Divider, IconButton, Tooltip } from '@mui/material'; import { debounce } from 'lodash-es'; import React, { useCallback, useEffect, useMemo } from 'react'; @@ -15,7 +16,14 @@ import { Duplicate } from './duplicate'; export const HEADER_HEIGHT = 48; -export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: () => void; openDrawer: boolean }) { +export function PublishViewHeader ({ + drawerWidth, onOpenDrawer, openDrawer, onCloseDrawer, +}: { + onOpenDrawer: () => void; + drawerWidth: number; + openDrawer: boolean; + onCloseDrawer: () => void +}) { const { t } = useTranslation(); const viewMeta = usePublishContext()?.viewMeta; const crumbs = useMemo(() => { @@ -28,7 +36,7 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: const extra = ancestor?.extra ? JSON.parse(ancestor.extra) : {}; icon = extra.icon?.value || ancestor.icon?.value; - } catch(e) { + } catch (e) { // ignore } @@ -42,6 +50,9 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: }); }, [viewMeta]); const [openPopover, setOpenPopover] = React.useState(false); + const isMobile = useMemo(() => { + return getPlatform().isMobile; + }, []); const debounceClosePopover = useMemo(() => { return debounce(() => { @@ -50,15 +61,20 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: }, []); const onKeyDown = useCallback((e: KeyboardEvent) => { - switch(true) { + switch (true) { case createHotkey(HOT_KEY_NAME.TOGGLE_SIDEBAR)(e): e.preventDefault(); - // setOpen((prev) => !prev); + if (openDrawer) { + onCloseDrawer(); + } else { + onOpenDrawer(); + } + break; default: break; } - }, []); + }, [onCloseDrawer, onOpenDrawer, openDrawer]); useEffect(() => { window.addEventListener('keydown', onKeyDown); @@ -69,13 +85,18 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: const handleOpenPopover = useCallback(() => { debounceClosePopover.cancel(); - if(openDrawer) { + if (openDrawer) { return; } setOpenPopover(true); }, [openDrawer, debounceClosePopover]); + const debounceOpenPopover = useMemo(() => { + debounceClosePopover.cancel(); + return debounce(handleOpenPopover, 100); + }, [handleOpenPopover, debounceClosePopover]); + const currentUser = useCurrentUser(); const isAppFlowyUser = currentUser?.email?.endsWith('@appflowy.io'); @@ -89,22 +110,31 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: }} className={'appflowy-top-bar sticky top-0 z-10 flex px-5'} > -
- {!openDrawer && openPopover && ( +
+ {!openDrawer && ( { - setOpenPopover(false); - onOpenDrawer(); + {...isMobile ? { + onTouchEnd: () => { + setOpenPopover(prev => !prev); + }, + } : { + onMouseEnter: debounceOpenPopover, + onMouseLeave: debounceClosePopover, + onClick: () => { + 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 index 69ac61158a624..1cc005e6d5364 100644 --- a/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx +++ b/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx @@ -1,62 +1,29 @@ -import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; +import { usePublishContext } from '@/application/publish'; +import emptyImageSrc from '@/assets/images/empty.png'; +import { DirectoryStructure } from '@/components/_shared/skeleton/OutlineSkeleton'; 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'; -import { useTranslation } from 'react-i18next'; +import React from 'react'; -function Outline({ viewMeta, width }: { viewMeta?: PublishViewInfo; width: number }) { - const hasChildren = Boolean(viewMeta?.child_views?.length); - const { t } = useTranslation(); - const [children, setChildren] = React.useState([]); +function Outline ({ width }: { width: number }) { + const outline = usePublishContext()?.outline; - 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 ; - } + const isEmpty = outline && outline.children.length === 0; return ( -
-
+ {isEmpty && {'No} + {!outline ?
- -
- - {hasChildren ? ( -
- {children - .filter((view) => view.layout === ViewLayout.Document) - .map((view: PublishViewInfo) => ( - - ))} -
- ) : ( -
{t('noPagesInside')}
- )} + >
: + outline.children.map((view) => + , + ) + }
); } diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx index c25a447f14cb3..62c715df79ba7 100644 --- a/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx @@ -1,15 +1,13 @@ -import { usePublishContext } from '@/application/publish'; -import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppFlowyLogo } from '@/assets/appflowy.svg'; import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; +import AppFlowyPower from '@/components/_shared/appflowy-power/AppFlowyPower'; 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'; -export function OutlineDrawer({ open, width, onClose }: { open: boolean; width: number; onClose: () => void }) { +export 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'); }} > - - +
-
- +
+
+
); diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx index 63e92afea52c4..36798653b741f 100644 --- a/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx @@ -1,13 +1,24 @@ -import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; -import { PublishContext } from '@/application/publish'; +import { PublishContext, usePublishContext } from '@/application/publish'; +import { View } from '@/application/types'; import { notify } from '@/components/_shared/notify'; import { ViewIcon } from '@/components/_shared/view-icon'; -import React, { useCallback, useContext } from 'react'; +import SpaceIcon from '@/components/publish/header/SpaceIcon'; +import { renderColor } from '@/utils/color'; +import { isFlagEmoji } from '@/utils/emoji'; +import React, { useCallback, useContext, useEffect } from 'react'; import { ReactComponent as ChevronDownIcon } from '@/assets/chevron_down.svg'; import { useTranslation } from 'react-i18next'; -function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: number; level?: number }) { - const [isExpanded, setIsExpanded] = React.useState(false); +function OutlineItem ({ view, level = 0, width }: { view: View; width: number; level?: number }) { + const [isExpanded, setIsExpanded] = React.useState(() => { + return localStorage.getItem('publish_outline_expanded_' + view.view_id) === 'true'; + }); + + useEffect(() => { + localStorage.setItem('publish_outline_expanded_' + view.view_id, isExpanded ? 'true' : 'false'); + }, [isExpanded, view.view_id]); + + const selected = usePublishContext()?.viewMeta?.view_id === view.view_id; const getIcon = useCallback(() => { if (isExpanded) { return ( @@ -18,6 +29,7 @@ function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: onClick={() => { setIsExpanded(false); }} + className={'opacity-50 hover:opacity-100'} > @@ -29,6 +41,7 @@ function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: style={{ paddingLeft: 1.125 * level + 'rem', }} + className={'opacity-50 hover:opacity-100'} onClick={() => { setIsExpanded(true); }} @@ -40,34 +53,53 @@ function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: const { t } = useTranslation(); const navigateToView = useContext(PublishContext)?.toView; - const renderItem = (item: PublishViewInfo) => { - const { icon, layout, name, view_id } = item; + const renderItem = (item: View) => { + const { icon, layout, name, view_id, extra } = item; + + const isSpace = extra?.is_space; return ( -
+
- {item.child_views?.length ? getIcon() : null} + {item.children?.length ? getIcon() : null}
{ try { await navigateToView?.(view_id); } catch (e) { - notify.error(t('publish.hasNotBeenPublished')); + notify.default(t('publish.hasNotBeenPublished')); } }} style={{ - paddingLeft: item.child_views?.length ? 0 : 1.125 * (level + 1) + 'rem', + paddingLeft: item.children?.length ? 0 : 1.125 * (level + 1) + 'rem', }} className={'flex flex-1 cursor-pointer items-center gap-1.5 overflow-hidden'} > -
{icon?.value || }
+ {isSpace && extra ? + + : +
+ {icon?.value || } +
+ } +
{name}
@@ -75,10 +107,10 @@ function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: ); }; - const children = view.child_views || []; + const children = view.children || []; return ( -
+
{renderItem(view)}
{children - .filter((view) => view.layout === ViewLayout.Document) .map((item, index) => ( ))} diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx index b876d857e2626..afad16631fe54 100644 --- a/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx @@ -1,18 +1,18 @@ import { usePublishContext } from '@/application/publish'; +import AppFlowyPower from '@/components/_shared/appflowy-power/AppFlowyPower'; import Outline from '@/components/publish/outline/Outline'; -import { Divider, PopperPlacementType } from '@mui/material'; +import { PopperPlacementType } from '@mui/material'; import React, { ReactElement, useMemo } from 'react'; import { RichTooltip } from '@/components/_shared/popover'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; -import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; -export function OutlinePopover({ +export function OutlinePopover ({ children, open, onClose, placement, onMouseEnter, onMouseLeave, + drawerWidth, }: { open: boolean; onClose: () => void; @@ -20,6 +20,7 @@ export function OutlinePopover({ placement?: PopperPlacementType; onMouseEnter?: () => void; onMouseLeave?: () => void; + drawerWidth: number; }) { const viewMeta = usePublishContext()?.viewMeta; @@ -28,36 +29,21 @@ export function OutlinePopover({
- -
- {Boolean(viewMeta?.child_views?.length) && } + -
{ - window.open('https://appflowy.io', '_blank'); - }} - className={'flex w-full cursor-pointer items-center justify-center text-sm text-text-title opacity-50'} - > - - -
-
+
); - }, [onMouseEnter, onMouseLeave, viewMeta]); + }, [onMouseEnter, onMouseLeave, viewMeta, drawerWidth]); return ( - + {children} ); diff --git a/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx b/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx index beece2661c976..de5999a4b8ec1 100644 --- a/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx +++ b/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx @@ -1,9 +1,8 @@ import { Button, Typography } from '@mui/material'; import React, { useCallback, useEffect } from 'react'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; -function AfterPaymentPage() { +function AfterPaymentPage () { const openAppFlowy = useCallback(() => { window.open(`appflowy-flutter://payment-success/${window.location.search || ''}`, '_self'); }, []); @@ -14,9 +13,8 @@ function AfterPaymentPage() { return (
- + <> - @@ -31,8 +29,8 @@ function AfterPaymentPage() {