diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..38bf0d77 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Release Notes diff --git a/package.json b/package.json index 9e476d38..eb00e937 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "coverage": "pnpm run test:unit && pnpm run test:components" }, "dependencies": { - "@appflowyinc/editor": "^0.0.39", + "@appflowyinc/editor": "^0.0.40", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b338cf18..a60bbd92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@appflowyinc/editor': - specifier: ^0.0.39 - version: 0.0.39(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5) + specifier: ^0.0.40 + version: 0.0.40(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5) '@atlaskit/primitives': specifier: ^5.5.3 version: 5.7.0(@types/react@18.2.66)(react@18.2.0) @@ -533,8 +533,8 @@ packages: resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} dev: false - /@appflowyinc/editor@0.0.39(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5): - resolution: {integrity: sha512-Cbitz/yNcioB+QS3K06Xzt/7lN+AnlAbzZLQ4BPw6dPLvTFbE8f+b6a7QkxN/EU2WKgc1qMfHI+m4UAU7v5tvg==} + /@appflowyinc/editor@0.0.40(@types/react-dom@18.2.22)(@types/react@18.2.66)(i18next-resources-to-backend@1.2.1)(i18next@22.5.1)(react-dom@18.2.0)(react-i18next@14.1.2)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.101.6)(slate@0.101.5): + resolution: {integrity: sha512-JLKQfkNtzTlfTA8Unv/tBtVdnCp3lKSFjIRaEFyf6XpA/6JcK6mV0shzWjMRMX1AbmC7iCiTDlRbTkS3p+qpQA==} peerDependencies: i18next: ^22.4.10 i18next-resources-to-backend: ^1.2.1 diff --git a/src/@types/translations/en.json b/src/@types/translations/en.json index 0574eee5..d2cda5e5 100644 --- a/src/@types/translations/en.json +++ b/src/@types/translations/en.json @@ -509,6 +509,7 @@ "onlyWorkspaceOwnerCanUpdateNamespace": "Only workspace owner can update the namespace", "onlyWorkspaceOwnerCanRemoveHomepage": "Only workspace owner can remove the homepage", "onlyWorkspaceOwnerCanChangeHomepage": "Only workspace owner can change the homepage", + "onlyProCanUpdateNamespace": "Only Pro Plan workspace owner can update the namespace", "onlyProCanSetHomepage": "Only Pro Plan workspace owner can set the homepage", "setHomepageFailed": "Failed to set homepage", "namespaceTooLong": "The namespace is too long, please try another one", @@ -522,6 +523,7 @@ "publishNameAlreadyInUse": "The path name is already in use, please try another one", "namespaceContainsInvalidCharacters": "The namespace contains invalid character(s), please try another one", "publishPermissionDenied": "Only the workspace owner or page publisher can manage the publish settings", + "unPublishPermissionDenied": "Only the workspace owner or page publisher can unpublish the page", "publishNameCannotBeEmpty": "The path name cannot be empty, please try another one" }, "success": { @@ -3043,9 +3045,14 @@ "memberCount_one": "{{count}} member", "memberCount_many": "{{count}} members", "memberCount_other": "{{count}} members", - "aiMatch": "AI match", - "titleMatch": "Title match", + "AIsearch": "AI search", + "titleOnly": "Title only", "namespace": "Namespace", "manageNamespaceDescription": "Manage your namespace and homepage", - "homepage": "Homepage" + "homepage": "Homepage", + "recentPages": "Recent pages", + "searchResults": "Search results", + "noSearchResults": "No search results", + "AISearchPlaceholder": "Search or ask a question...", + "searchLabel": "Search" } diff --git a/src/assets/published_with_changes.svg b/src/assets/published_with_changes.svg index 97da3976..c6dcaef3 100644 --- a/src/assets/published_with_changes.svg +++ b/src/assets/published_with_changes.svg @@ -1,4 +1,12 @@ - - - + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/search.svg b/src/assets/search.svg index fdad7591..e3aec129 100644 --- a/src/assets/search.svg +++ b/src/assets/search.svg @@ -1,11 +1,6 @@ - - - - - - - + + + diff --git a/src/components/_shared/outline/utils.ts b/src/components/_shared/outline/utils.ts index 5a67ba5b..2a82be80 100644 --- a/src/components/_shared/outline/utils.ts +++ b/src/components/_shared/outline/utils.ts @@ -7,7 +7,9 @@ export function filterViews (views: View[], keyword: string): View[] { for (const view of views) { if (view.name.toLowerCase().includes(keyword.toLowerCase())) { result.push(view); - } else if (view.children) { + } + + if (view.children) { const filteredChildren = filterAndFlatten(view.children); result = result.concat(filteredChildren); diff --git a/src/components/_shared/popover/Popover.tsx b/src/components/_shared/popover/Popover.tsx index 12452fa2..bc14c2d4 100644 --- a/src/components/_shared/popover/Popover.tsx +++ b/src/components/_shared/popover/Popover.tsx @@ -1,6 +1,6 @@ import { PopoverOrigin } from '@mui/material/Popover/Popover'; import isEqual from 'lodash-es/isEqual'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Popover as PopoverComponent, PopoverProps as PopoverComponentProps } from '@mui/material'; const defaultProps: Partial = { @@ -33,7 +33,7 @@ const DEFAULT_ORIGINS: Origins = { }, }; -function calculateOptimalOrigins( +export function calculateOptimalOrigins ( position: Position, popoverWidth: number, popoverHeight: number, @@ -103,7 +103,7 @@ function calculateOptimalOrigins( }; } -export function Popover({ +export function Popover ({ children, transformOrigin = DEFAULT_ORIGINS.transformOrigin, anchorOrigin = DEFAULT_ORIGINS.anchorOrigin, @@ -119,7 +119,7 @@ export function Popover({ anchorOrigin, }); - const handleEntered = (element: HTMLElement) => { + const handleEntered = useCallback((element: HTMLElement) => { const { width, height } = element.getBoundingClientRect(); let position: Position; @@ -145,7 +145,7 @@ export function Popover({ ); setOrigins(newOrigins); - }; + }, [anchorEl, anchorOrigin, anchorPosition, transformOrigin]); useEffect(() => { if (!adjustOrigins) { diff --git a/src/components/app/SideBar.tsx b/src/components/app/SideBar.tsx index ef7d84be..d587b78d 100644 --- a/src/components/app/SideBar.tsx +++ b/src/components/app/SideBar.tsx @@ -4,7 +4,7 @@ import React, { lazy } from 'react'; import { Workspaces } from '@/components/app/workspaces'; import Outline from 'src/components/app/outline/Outline'; import { UIVariant } from '@/application/types'; -import { Search } from 'src/components/app/search'; +// import { Search } from 'src/components/app/search'; const SideBarBottom = lazy(() => import('@/components/app/SideBarBottom')); @@ -15,7 +15,7 @@ interface SideBarProps { onResizeDrawerWidth: (width: number) => void; } -function SideBar({ +function SideBar ({ drawerWidth, drawerOpened, toggleOpenDrawer, @@ -35,21 +35,21 @@ function SideBar({ open={drawerOpened} variant={UIVariant.App} onClose={() => toggleOpenDrawer(false)} - header={} + header={} onScroll={handleOnScroll} >
- + {/**/}
10 ? 'var(--line-divider)' : undefined, }} - className={'flex border-b pb-2 w-full border-transparent'} + className={'flex border-b pb-3 w-full border-transparent'} >
diff --git a/src/components/app/app.hooks.tsx b/src/components/app/app.hooks.tsx index 8c28d49d..36fc9317 100644 --- a/src/components/app/app.hooks.tsx +++ b/src/components/app/app.hooks.tsx @@ -15,7 +15,6 @@ import { YjsEditorKey, YSharedRoot, } from '@/application/types'; -import { notify } from '@/components/_shared/notify'; import { findAncestors, findView, findViewByLayout } from '@/components/_shared/outline/utils'; import RequestAccess from '@/components/app/landing-pages/RequestAccess'; import { AFConfigContext, useService } from '@/components/main/app.hooks'; @@ -404,12 +403,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const views = uniqBy(res, 'view_id'); - setRecentViews(views); + setRecentViews(views.filter(item => { + return !item.extra?.is_space && findView(outline || [], item.view_id); + })); return views; } catch (e) { console.error('Recent views not found'); } - }, [currentWorkspaceId, service]); + }, [currentWorkspaceId, service, outline]); const loadTrash = useCallback(async (currentWorkspaceId: string) => { @@ -648,18 +649,20 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { if (!service || !currentWorkspaceId) return; const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout); const viewId = view.view_id; + const children = view.children || []; + const visibleViewIds = [view.view_id, ...(children.map((v) => v.view_id))]; await service.publishView(currentWorkspaceId, viewId, { publish_name: publishName, - visible_database_view_ids: isDatabase ? view.children?.map((v) => v.view_id) : undefined, + visible_database_view_ids: isDatabase ? visibleViewIds : undefined, }); - void loadOutline(currentWorkspaceId, false); + await loadOutline(currentWorkspaceId, false); }, [currentWorkspaceId, loadOutline, service]); const unpublish = useCallback(async (viewId: string) => { if (!service || !currentWorkspaceId) return; await service.unpublishView(currentWorkspaceId, viewId); - void loadOutline(currentWorkspaceId, false); + await loadOutline(currentWorkspaceId, false); }, [currentWorkspaceId, loadOutline, service]); return item.is_published)); // eslint-disable-next-line } catch (e: any) { @@ -241,7 +241,7 @@ export function PublishManage ({ onUpdateHomePage={handleUpdateHomePage} /> { if (!(isOwner || isPublisher)) { @@ -109,6 +109,7 @@ function PublishedPageItem ({ onClose, view, onUnPublish, onPublish }: { IconComponent: SettingIcon, onClick: () => { if (!(isOwner || isPublisher)) return; + setAnchorEl(null); setOpenSetting(true); }, }, diff --git a/src/components/app/search/BestMatch.tsx b/src/components/app/search/BestMatch.tsx index ac2722f2..6bf914c7 100644 --- a/src/components/app/search/BestMatch.tsx +++ b/src/components/app/search/BestMatch.tsx @@ -4,23 +4,24 @@ import { findView } from '@/components/_shared/outline/utils'; import { useAppOutline, useCurrentWorkspaceId } from '@/components/app/app.hooks'; import ViewList from '@/components/app/search/ViewList'; import { useService } from '@/components/main/app.hooks'; -import { debounce } from 'lodash-es'; +import { debounce, uniq } from 'lodash-es'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; function BestMatch ({ onClose, - searchValue + searchValue, }: { onClose: () => void; searchValue: string; }) { - const [views, setViews] = React.useState([]); + const [views, setViews] = React.useState(undefined); const { t } = useTranslation(); const outline = useAppOutline(); - const [loading, setLoading] = React.useState(false); const service = useService(); - const currentWorkspaceId = useCurrentWorkspaceId() + const [loading, setLoading] = React.useState(false); + + const currentWorkspaceId = useCurrentWorkspaceId(); const handleSearch = useCallback(async (searchTerm: string) => { if (!outline) return; if (!currentWorkspaceId || !service) return; @@ -29,34 +30,41 @@ function BestMatch ({ return; } - setLoading(true) + setLoading(true); try { const res = await service.searchWorkspace(currentWorkspaceId, searchTerm); - const views = res.map(id => { + const views = uniq(res).map(id => { return findView(outline, id); }); - setViews(views.filter(Boolean) as View[]); + setViews(views.filter(item => { + if (!item) return false; + return !item.extra?.is_space; + }) as View[]); // eslint-disable-next-line } catch (e: any) { notify.error(e.message); } setLoading(false); - + }, [currentWorkspaceId, outline, service]); const debounceSearch = useMemo(() => { return debounce(handleSearch, 300); }, [handleSearch]); - + useEffect(() => { void debounceSearch(searchValue); }, [searchValue, debounceSearch]); - - return + return ; } export default BestMatch; \ No newline at end of file diff --git a/src/components/app/search/ListItem.tsx b/src/components/app/search/ListItem.tsx new file mode 100644 index 00000000..75203074 --- /dev/null +++ b/src/components/app/search/ListItem.tsx @@ -0,0 +1,150 @@ +import { View } from '@/application/types'; +import { findAncestors } from '@/components/_shared/outline/utils'; +import { RichTooltip } from '@/components/_shared/popover'; +import PageIcon from '@/components/_shared/view-icon/PageIcon'; +import { useAppHandlers, useAppOutline } from '@/components/app/app.hooks'; +import { IconButton, Paper, Tooltip } from '@mui/material'; +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as MoreIcon } from '@/assets/more.svg'; +import { ReactComponent as PrivateIcon } from '@/assets/lock.svg'; + +function ListItem ({ + selectedView, + view, + onClick, + onClose, +}: { + selectedView: string; + view: View; + onClick: () => void; + onClose: () => void; +}) { + const { t } = useTranslation(); + const outline = useAppOutline(); + const [open, setOpen] = React.useState(false); + const toView = useAppHandlers().toView; + + const ancestors = useMemo(() => { + if (!outline) return []; + return findAncestors(outline, view.view_id)?.slice(0, -1) || []; + }, [outline, view.view_id]); + + const renderBreadcrumb = useCallback((view: View) => { + const isPrivate = view.is_private && view.extra?.is_space; + + return +
{ + e.stopPropagation(); + if (view.extra?.is_space) return; + void toView(view.view_id); + onClose(); + }} + className={`text-text-caption max-w-[250px] overflow-hidden ${view.extra?.is_space ? '' : 'hover:underline'} flex items-center gap-2`} + > + {view.name || t('menuAppHeader.defaultNewPageName')} + {isPrivate && +
+ +
+ } +
+
; + }, [onClose, t, toView]); + + const breadcrumbs = useMemo(() => { + if (!ancestors) return null; + if (ancestors.length <= 3) { + return ancestors.map((ancestor, index) => { + return
+ {renderBreadcrumb(ancestor)} + {index !== ancestors.length - 1 && {'/'}} +
; + }); + } + + const first = renderBreadcrumb(ancestors[0]); + const last = renderBreadcrumb(ancestors[ancestors.length - 1]); + + return <> + {first} +
+ {'/'} + setOpen(false)} + content={ + + {ancestors.slice(1, -1).map((ancestor) => { + return
+ {renderBreadcrumb(ancestor)} +
; + })} +
+ } + > + { + e.stopPropagation(); + setOpen((prev) => !prev); + }} + size={'small'} + > + + +
+ {'/'} + {last} +
+ ; + + }, [ancestors, open, renderBreadcrumb]); + + return ( +
+
+
+ + +
+
+ {view.name.trim() || t('menuAppHeader.defaultNewPageName')} +
+
+ +
+
+ {breadcrumbs} +
+
+ +
+ ); +} + +export default ListItem; \ No newline at end of file diff --git a/src/components/app/search/RecentViews.tsx b/src/components/app/search/RecentViews.tsx index 74bbb4db..6c171573 100644 --- a/src/components/app/search/RecentViews.tsx +++ b/src/components/app/search/RecentViews.tsx @@ -1,31 +1,27 @@ -import { useAppRecent } from '@/components/app/app.hooks'; +import { View } from '@/application/types'; import ViewList from '@/components/app/search/ViewList'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; function RecentViews ({ - onClose + onClose, + loading, + recentViews, }: { onClose: () => void; + loading: boolean; + recentViews?: View[]; }) { - const { - recentViews, - loadRecentViews - } = useAppRecent(); - const { t } = useTranslation(); - const [loading, setLoading] = React.useState(false); - useEffect(() => { - void (async () => { - setLoading(true); - await loadRecentViews?.(); - setLoading(false); - })(); - }, [loadRecentViews]); - + const { t } = useTranslation(); return ( - + ); } diff --git a/src/components/app/search/Search.tsx b/src/components/app/search/Search.tsx index c42462cf..e87ab650 100644 --- a/src/components/app/search/Search.tsx +++ b/src/components/app/search/Search.tsx @@ -1,9 +1,10 @@ import { Popover } from '@/components/_shared/popover'; +import { useAppRecent } from '@/components/app/app.hooks'; import BestMatch from '@/components/app/search/BestMatch'; import RecentViews from '@/components/app/search/RecentViews'; import TitleMatch from '@/components/app/search/TitleMatch'; -import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; -import { Button, Dialog, InputBase, Tooltip } from '@mui/material'; +import { createHotkey, createHotKeyLabel, HOT_KEY_NAME } from '@/utils/hotkeys'; +import { Button, Dialog, Divider, InputBase, Tooltip } from '@mui/material'; import React, { useCallback, useEffect } from 'react'; import { ReactComponent as SearchIcon } from '@/assets/search.svg'; import { ReactComponent as CheckIcon } from '@/assets/check.svg'; @@ -21,9 +22,8 @@ export function Search () { const [open, setOpen] = React.useState(false); const { t } = useTranslation(); const [searchValue, setSearchValue] = React.useState(''); - const [searchType, setSearchType] = React.useState(SEARCH_TYPE.TITLE_MATCH); + const [searchType, setSearchType] = React.useState(SEARCH_TYPE.AI_SUGGESTION); const [searchTypeAnchorEl, setSearchTypeAnchorEl] = React.useState(null); - const handleClose = () => { setOpen(false); setSearchValue(''); @@ -49,36 +49,63 @@ export function Search () { }; }, [onKeyDown]); + const { + recentViews, + loadRecentViews, + } = useAppRecent(); + const [loadingRecentViews, setLoadingRecentViews] = React.useState(false); + + useEffect(() => { + if (!open) return; + void (async () => { + setLoadingRecentViews(true); + await loadRecentViews?.(); + setLoadingRecentViews(false); + })(); + }, [loadRecentViews, open]); + return ( <> -
} > - {t('button.search')} - + + +
-
+
+ setSearchValue(e.target.value)} autoFocus={true} className={'flex-1'} fullWidth={true} - placeholder={t('commandPalette.placeholder')} + placeholder={searchType === SEARCH_TYPE.AI_SUGGESTION ? t('AISearchPlaceholder') : t('searchLabel')} /> - -
{ - setSearchTypeAnchorEl(e.currentTarget); - }} - className={'cursor-pointer flex items-center p-1 px-2 text-xs rounded bg-fill-list-hover'} - > - { - searchType === SEARCH_TYPE.TITLE_MATCH ? - t('titleMatch') : - t('aiMatch') - } - -
+ + BETA
- {!searchValue ? : searchType === SEARCH_TYPE.AI_SUGGESTION ? +
{ + setSearchTypeAnchorEl(e.currentTarget); + }} + className={'rounded-[8px] p-2 gap-2 border text-sm overflow-hidden cursor-pointer hover:border-text-title border-line-divider flex items-center'} + > + {searchType === SEARCH_TYPE.TITLE_MATCH ? t('titleOnly') : t('AIsearch')} + +
+
+ + {!searchValue ? : searchType === SEARCH_TYPE.AI_SUGGESTION ? : } +
- {[SEARCH_TYPE.TITLE_MATCH, SEARCH_TYPE.AI_SUGGESTION].map(type => ( + {[SEARCH_TYPE.AI_SUGGESTION, SEARCH_TYPE.TITLE_MATCH].map(type => (
{ setSearchType(type); setSearchTypeAnchorEl(null); }} > - {type === SEARCH_TYPE.TITLE_MATCH ? t('titleMatch') : t('aiMatch')} - {type === searchType && } + {type === SEARCH_TYPE.TITLE_MATCH ? t('titleOnly') : t('AIsearch')} + {type === searchType && }
))}
diff --git a/src/components/app/search/TitleMatch.tsx b/src/components/app/search/TitleMatch.tsx index c733580c..209744a0 100644 --- a/src/components/app/search/TitleMatch.tsx +++ b/src/components/app/search/TitleMatch.tsx @@ -21,10 +21,10 @@ function TitleMatch ({ return ( ); } diff --git a/src/components/app/search/ViewList.tsx b/src/components/app/search/ViewList.tsx index 31d6bce1..aab969a5 100644 --- a/src/components/app/search/ViewList.tsx +++ b/src/components/app/search/ViewList.tsx @@ -1,16 +1,16 @@ import { View } from '@/application/types'; -import PageIcon from '@/components/_shared/view-icon/PageIcon'; import { useAppHandlers } from '@/components/app/app.hooks'; +import ListItem from '@/components/app/search/ListItem'; import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; -import CircularProgress from '@mui/material/CircularProgress'; import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import CircularProgress from '@mui/material/CircularProgress'; function ViewList ({ title, views, onClose, - loading + loading, }: { title: string; views?: View[]; @@ -52,57 +52,45 @@ function ViewList ({ el.scrollIntoView({ behavior: 'smooth', block: 'nearest', - inline: 'nearest' + inline: 'nearest', }); } } - } + }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); - } + }; }, [navigateToView, onClose, views, selectedView]); + return (
-
- {title} +
+ {!loading && views && views.length === 0 ? t('noSearchResults') : <> + {title} + {loading && } + }
- {views?.length ? views.map(view => ( -
( + { + setSelectedView(view.view_id); void navigateToView(view.view_id); onClose(); }} - key={view.view_id} - className={'flex items-center border-t border-line-default w-full p-4 cursor-pointer hover:bg-fill-list-active gap-2'} - > - -
- {view.name.trim() || t('menuAppHeader.defaultNewPageName')} -
-
- )) :
- {t('findAndReplace.noResult')} -
} - {loading && -
- -
- } + onClose={onClose} + /> + ))}
TAB diff --git a/src/components/app/share/PublishLinkPreview.tsx b/src/components/app/share/PublishLinkPreview.tsx index 288edf6b..8118b766 100644 --- a/src/components/app/share/PublishLinkPreview.tsx +++ b/src/components/app/share/PublishLinkPreview.tsx @@ -1,6 +1,8 @@ import { NormalModal } from '@/components/_shared/modal'; +import { notify } from '@/components/_shared/notify'; import { PublishManage } from '@/components/app/publish-manage'; import { PublishNameSetting } from '@/components/app/publish-manage/PublishNameSetting'; +import { copyTextToClipboard } from '@/utils/copy'; import { CircularProgress, IconButton, InputBase, Tooltip } from '@mui/material'; import { ReactComponent as LinkIcon } from '@/assets/link.svg'; import { ReactComponent as DownIcon } from '@/assets/chevron_down.svg'; @@ -51,9 +53,14 @@ function PublishLinkPreview ({ <>
-
{window.location.origin}
+ +
{window.location.origin}
+
{'/'} -
+
{'/'} + +
- { - setFocused(true); - }} - onBlur={() => { - setFocused(false); - }} - onKeyDown={async (e) => { - if (e.key === 'Enter') { - void handlePublish(); - } - }} - size={'small'} - value={publishName} - onChange={e => { - setPublishName(e.target.value); - }} - className={'flex-1 truncate'} - /> + + { + setFocused(true); + }} + onBlur={() => { + setFocused(false); + }} + onKeyDown={async (e) => { + if (e.key === 'Enter') { + void handlePublish(); + } + }} + size={'small'} + value={publishName} + onChange={e => { + setPublishName(e.target.value); + }} + className={'flex-1 truncate'} + /> + {(isOwner || isPublisher) && { + await copyTextToClipboard(url); + notify.success(t('shareAction.copyLinkSuccess')); + }} color={'inherit'} size={'small'} > diff --git a/src/components/app/share/PublishPanel.tsx b/src/components/app/share/PublishPanel.tsx index 9af091bc..cb68e054 100644 --- a/src/components/app/share/PublishPanel.tsx +++ b/src/components/app/share/PublishPanel.tsx @@ -3,11 +3,11 @@ import { useAppHandlers } from '@/components/app/app.hooks'; import { useLoadPublishInfo } from '@/components/app/share/publish.hooks'; import PublishLinkPreview from '@/components/app/share/PublishLinkPreview'; import { Button, CircularProgress, Typography } from '@mui/material'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as PublishIcon } from '@/assets/publish.svg'; -function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => void }) { +function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: () => void; opened: boolean }) { const { t } = useTranslation(); const { publish, @@ -22,27 +22,40 @@ function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => voi isOwner, isPublisher, } = useLoadPublishInfo(viewId); + const [unpublishLoading, setUnpublishLoading] = React.useState(false); + const [publishLoading, setPublishLoading] = React.useState(false); + + useEffect(() => { + if (opened) { + void loadPublishInfo(); + } + }, [loadPublishInfo, opened]); const handlePublish = useCallback(async (publishName?: string) => { if (!publish || !view) return; + setPublishLoading(true); try { - await publish(view, publishName); - void loadPublishInfo(); + await publish(view, publishName || publishInfo?.publishName); + await loadPublishInfo(); notify.success(t('publish.publishSuccessfully')); // eslint-disable-next-line } catch (e: any) { notify.error(e.message); + } finally { + setPublishLoading(false); } - }, [loadPublishInfo, publish, t, view]); + }, [loadPublishInfo, publish, t, view, publishInfo]); const handleUnpublish = useCallback(async () => { if (!view || !unpublish) return; if (!isOwner && !isPublisher) { - notify.error(t('settings.sites.error.publishPermissionDenied')); + notify.error(t('settings.sites.error.unPublishPermissionDenied')); return; } + setUnpublishLoading(true); + try { await unpublish(viewId); await loadPublishInfo(); @@ -50,6 +63,8 @@ function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => voi // eslint-disable-next-line } catch (e: any) { notify.error(e.message); + } finally { + setUnpublishLoading(false); } }, [isOwner, isPublisher, loadPublishInfo, t, unpublish, view, viewId]); @@ -73,7 +88,10 @@ function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => voi }} color={'inherit'} variant={'outlined'} - >{t('shareAction.unPublish')} + startIcon={unpublishLoading ? : undefined} + >{ + t('shareAction.unPublish') + }
; - }, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, url, view]); + }, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, unpublishLoading, url, view]); const renderUnpublished = useCallback(() => { return ; - }, [handlePublish, t]); + startIcon={publishLoading ? : undefined} + >{ + t('shareAction.publish') + }; + }, [handlePublish, publishLoading, t]); return (
diff --git a/src/components/app/share/ShareTabs.tsx b/src/components/app/share/ShareTabs.tsx index ec11b9da..9e0a9951 100644 --- a/src/components/app/share/ShareTabs.tsx +++ b/src/components/app/share/ShareTabs.tsx @@ -1,13 +1,14 @@ import { useAppView } from '@/components/app/app.hooks'; -import PublishPanel from '@/components/app/share/PublishPanel'; +// import PublishPanel from '@/components/app/share/PublishPanel'; import TemplatePanel from '@/components/app/share/TemplatePanel'; import SharePanel from '@/components/app/share/SharePanel'; import { useCurrentUser } from '@/components/main/app.hooks'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ViewTabs, ViewTab, TabPanel } from 'src/components/_shared/tabs/ViewTabs'; import { ReactComponent as Templates } from '@/assets/template.svg'; -import { ReactComponent as PublishedWithChanges } from '@/assets/published_with_changes.svg'; + +// import { ReactComponent as PublishedWithChanges } from '@/assets/published_with_changes.svg'; enum TabKey { SHARE = 'share', @@ -26,21 +27,23 @@ function ShareTabs ({ opened, viewId, onClose }: { opened: boolean, viewId: stri value: TabKey.SHARE, label: t('shareAction.shareTab'), Panel: SharePanel, - }, { - value: TabKey.PUBLISH, - label: t('shareAction.publish'), - icon: view?.is_published ? : undefined, - Panel: PublishPanel, - }, currentUser?.email?.endsWith('appflowy.io') && view?.is_published && { - value: TabKey.TEMPLATE, - label: t('template.asTemplate'), - icon: , - Panel: TemplatePanel, - }].filter(Boolean) as { + }, + // { + // value: TabKey.PUBLISH, + // label: t('shareAction.publish'), + // icon: view?.is_published ? : undefined, + // Panel: PublishPanel, + // }, + currentUser?.email?.endsWith('appflowy.io') && view?.is_published && { + value: TabKey.TEMPLATE, + label: t('template.asTemplate'), + icon: , + Panel: TemplatePanel, + }].filter(Boolean) as { value: TabKey; label: string; icon?: React.JSX.Element; - Panel: React.FC<{ viewId: string; onClose: () => void }> + Panel: React.FC<{ viewId: string; onClose: () => void; opened: boolean }> }[]; }, [currentUser?.email, t, view?.is_published]); @@ -49,6 +52,12 @@ function ShareTabs ({ opened, viewId, onClose }: { opened: boolean, viewId: stri setValue(newValue); }, []); + useEffect(() => { + if (opened) { + setValue(TabKey.SHARE); + } + }, [opened]); + return ( <> {options.map((option) => ( ))} diff --git a/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx b/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx index cd3f5de7..5fc1bed8 100644 --- a/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx +++ b/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -17,7 +17,7 @@ export interface CalculationCellProps { cell?: ICalculationCell; } -export function CalculationCell({ cell }: CalculationCellProps) { +export function CalculationCell ({ cell }: CalculationCellProps) { const { t } = useTranslation(); const fieldId = cell?.fieldId || ''; @@ -27,7 +27,7 @@ export function CalculationCell({ cell }: CalculationCellProps) { field && Number(field?.get(YjsDatabaseKey.type)) === FieldType.Number ? parseNumberTypeOptions(field).format : undefined, - [field] + [field], ); const prefix = useMemo(() => { @@ -56,7 +56,7 @@ export function CalculationCell({ cell }: CalculationCellProps) { const num = useMemo(() => { const value = cell?.value; - if (value === undefined || isNaN(value)) return ''; + if (value === undefined || isNaN(parseInt(value))) return ''; if (format && currencyFormaterMap[format]) { return currencyFormaterMap[format](new Decimal(value).toNumber()); diff --git a/src/components/editor/components/block-popover/index.tsx b/src/components/editor/components/block-popover/index.tsx index 58e5c25d..6d4881ea 100644 --- a/src/components/editor/components/block-popover/index.tsx +++ b/src/components/editor/components/block-popover/index.tsx @@ -1,12 +1,11 @@ import { YjsEditor } from '@/application/slate-yjs'; import { findSlateEntryByBlockId } from '@/application/slate-yjs/utils/editor'; import { BlockType } from '@/application/types'; -import { Origins, Popover } from '@/components/_shared/popover'; +import { calculateOptimalOrigins, Origins, Popover } from '@/components/_shared/popover'; import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext'; import FileBlockPopoverContent from '@/components/editor/components/block-popover/FileBlockPopoverContent'; import ImageBlockPopoverContent from '@/components/editor/components/block-popover/ImageBlockPopoverContent'; import { useEditorContext } from '@/components/editor/EditorContext'; -import { debounce } from 'lodash-es'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { ReactEditor, useSlateStatic } from 'slate-react'; import MathEquationPopoverContent from './MathEquationPopoverContent'; @@ -79,37 +78,33 @@ function BlockPopover () { } }, [blockId, setSelectedBlockIds]); - const debouncePosition = useMemo(() => { - return debounce(() => { - if (!anchorEl || !paperRef.current) return; - - const rect = anchorEl.getBoundingClientRect(); - const paperRect = paperRef.current.getBoundingClientRect(); - const isImage = type === BlockType.ImageBlock; - const paperHeight = isImage ? paperRect.height + 100 : paperRect.height; - - if (rect.bottom + paperHeight > window.innerHeight) { - setOrigins({ - anchorOrigin: { - vertical: -8, - horizontal: 'center', - }, - transformOrigin: { - vertical: 'bottom', - horizontal: 'center', - }, - }); - return; - } - - }, 50); - }, [anchorEl, type]); - useEffect(() => { if (!open) return; editor.deselect(); }, [open, editor]); + useEffect(() => { + const panelPosition = anchorEl?.getBoundingClientRect(); + + if (open && panelPosition) { + const origins = calculateOptimalOrigins({ + top: panelPosition.bottom, + left: panelPosition.left, + }, 560, type === BlockType.ImageBlock ? 400 : 200, defaultOrigins, 16); + + setOrigins({ + transformOrigin: { + vertical: origins.transformOrigin.vertical, + horizontal: 'center', + }, + anchorOrigin: { + vertical: origins.anchorOrigin.vertical, + horizontal: 'center', + }, + }); + } + }, [open, anchorEl, type]); + return { - setOrigins(defaultOrigins); - }} - onTransitionEnd={debouncePosition} {...origins} disableRestoreFocus={true} > diff --git a/src/components/editor/components/blocks/callout/Callout.tsx b/src/components/editor/components/blocks/callout/Callout.tsx index 30d678c5..fd9791cf 100644 --- a/src/components/editor/components/blocks/callout/Callout.tsx +++ b/src/components/editor/components/blocks/callout/Callout.tsx @@ -8,7 +8,7 @@ export const Callout = memo(
{children}
diff --git a/src/components/editor/components/blocks/callout/CalloutIcon.tsx b/src/components/editor/components/blocks/callout/CalloutIcon.tsx index a41a125a..4e56bb62 100644 --- a/src/components/editor/components/blocks/callout/CalloutIcon.tsx +++ b/src/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useRef } from 'react'; import { useReadOnly, useSlateStatic } from 'slate-react'; import { Element } from 'slate'; -function CalloutIcon ({ block: node, className }: { block: CalloutNode; className: string }) { +function CalloutIcon ({ block: node }: { block: CalloutNode; className: string }) { const ref = useRef(null); const editor = useSlateStatic(); const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element); @@ -34,13 +34,13 @@ function CalloutIcon ({ block: node, className }: { block: CalloutNode; classNam }} contentEditable={false} ref={ref} - className={`icon ${className} ${readOnly ? '' : 'cursor-pointer'} flex h-[24px] max-h-full items-center`} + className={`${readOnly ? '' : 'cursor-pointer'} relative flex items-start justify-center`} style={{ width: '58px', }} > {node.data.icon || `📌`} diff --git a/src/components/editor/components/blocks/code/MermaidChat.tsx b/src/components/editor/components/blocks/code/MermaidChat.tsx index 093976d4..621aa16b 100644 --- a/src/components/editor/components/blocks/code/MermaidChat.tsx +++ b/src/components/editor/components/blocks/code/MermaidChat.tsx @@ -94,7 +94,7 @@ function MermaidChat ({ node }: { }, [diagram, id, isDark]); const deboucenUpdateMermaid = useMemo(() => { - return debounce(updateMermaid, 300); + return debounce(updateMermaid, 1000); }, [updateMermaid]); useEffect(() => { @@ -121,6 +121,7 @@ function MermaidChat ({ node }: { display: 'flex', flexDirection: 'row', placeContent: 'center', + minHeight: '250px', }} contentEditable={false} ref={ref} diff --git a/src/components/editor/components/panels/mention-panel/MentionPanel.tsx b/src/components/editor/components/panels/mention-panel/MentionPanel.tsx index 3ab8e83d..a718ba29 100644 --- a/src/components/editor/components/panels/mention-panel/MentionPanel.tsx +++ b/src/components/editor/components/panels/mention-panel/MentionPanel.tsx @@ -7,6 +7,7 @@ import { usePanelContext } from '@/components/editor/components/panels/Panels.ho import { PanelType } from '@/components/editor/components/panels/PanelsContext'; import { useEditorContext } from '@/components/editor/EditorContext'; import { Button, Divider } from '@mui/material'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; import { sortBy, uniqBy } from 'lodash-es'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,7 +16,7 @@ import { ReactEditor, useSlateStatic } from 'slate-react'; import { ReactComponent as AddIcon } from '@/assets/add.svg'; import { ReactComponent as ArrowIcon } from '@/assets/north_east.svg'; import { ReactComponent as MoreIcon } from '@/assets/more.svg'; -import { Popover } from '@/components/_shared/popover'; +import { calculateOptimalOrigins, Popover } from '@/components/_shared/popover'; import dayjs from 'dayjs'; import PageIcon from '@/components/_shared/view-icon/PageIcon'; @@ -33,7 +34,7 @@ interface Option { index: number; } -function createMentionOptions({ +function createMentionOptions ({ showMore, viewsLength, dateLength, @@ -67,7 +68,7 @@ function createMentionOptions({ return options; } -export function MentionPanel() { +export function MentionPanel () { const { isPanelOpen, panelPosition, @@ -327,10 +328,23 @@ export function MentionPanel() { slateDom.removeEventListener('keydown', handleKeyDown); }; }, [editor, handleClickMore, handleAddPage, handleSelectedPage, open, selectedOptionRef, splicedViews, dateOptions, showMore]); + const [transformOrigin, setTransformOrigin] = React.useState(undefined); + + useEffect(() => { + if (open && panelPosition) { + const origins = calculateOptimalOrigins(panelPosition, 320, 560, undefined, 16); + const isAlignBottom = origins.transformOrigin.vertical === 'bottom'; + + setTransformOrigin(isAlignBottom ? origins.transformOrigin : { + vertical: -30, + horizontal: origins.transformOrigin.horizontal, + }); + } + }, [open, panelPosition]); return ( e.preventDefault()} >
+ } className={`justify-start truncate scroll-m-2 min-h-[32px] hover:bg-fill-list-hover ${selectedOption?.index === index && selectedOption?.category === MentionTag.Page ? 'bg-fill-list-hover' : ''}`} onClick={() => handleSelectedPage(view.view_id)} @@ -374,15 +388,19 @@ export function MentionPanel() {
) :
{t('findAndReplace.noResult')}
+ className={'text-text-caption text-sm flex justify-center items-center p-2'} + >{t('findAndReplace.noResult')}
} {showMore && -
+
}
- {showDate &&
+ {showDate &&
{t('inlineActions.date')}
{ dateOptions.map((option, index) => ( @@ -416,10 +437,10 @@ export function MentionPanel() { data-option-category={MentionTag.NewPage} className={'flex w-full flex-col gap-2'} > - +
diff --git a/src/components/main/AppTheme.tsx b/src/components/main/AppTheme.tsx index 691a4921..c24a5300 100644 --- a/src/components/main/AppTheme.tsx +++ b/src/components/main/AppTheme.tsx @@ -10,7 +10,7 @@ import 'src/styles/tailwind.css'; import 'src/styles/template.css'; import { I18nextProvider } from 'react-i18next'; -function AppTheme({ children }: { children: React.ReactNode; }) { +function AppTheme ({ children }: { children: React.ReactNode; }) { const { isDark, setIsDark } = useAppThemeMode(); const theme = useMemo( @@ -104,6 +104,7 @@ function AppTheme({ children }: { children: React.ReactNode; }) { }, '&.MuiButton-containedSecondary': { backgroundColor: 'var(--billing-primary)', + color: 'white', '&:hover': { backgroundColor: 'var(--billing-primary-hover)', }, diff --git a/src/components/publish/header/duplicate/DuplicateModal.tsx b/src/components/publish/header/duplicate/DuplicateModal.tsx index 4c4193e8..4ae4705b 100644 --- a/src/components/publish/header/duplicate/DuplicateModal.tsx +++ b/src/components/publish/header/duplicate/DuplicateModal.tsx @@ -51,10 +51,10 @@ function DuplicateModal ({ open, onClose }: { open: boolean; onClose: () => void }, [loadWorkspaces, open]); useEffect(() => { - if (selectedWorkspaceId) { + if (selectedWorkspaceId && open) { void loadSpaces(selectedWorkspaceId); } - }, [loadSpaces, selectedWorkspaceId]); + }, [loadSpaces, selectedWorkspaceId, open]); const handleDuplicate = useCallback(async () => { if (!viewId) return; diff --git a/src/components/publish/header/duplicate/useDuplicate.ts b/src/components/publish/header/duplicate/useDuplicate.ts index e41225d0..db59967f 100644 --- a/src/components/publish/header/duplicate/useDuplicate.ts +++ b/src/components/publish/header/duplicate/useDuplicate.ts @@ -111,7 +111,7 @@ export function useLoadWorkspaces () { setSpaceList([]); } } catch (e) { - notify.error('Failed to load spaces'); + console.error('Failed to load spaces'); } finally { setSelectedSpaceId(''); setSpaceLoading(false); diff --git a/src/components/view-meta/ViewMetaPreview.tsx b/src/components/view-meta/ViewMetaPreview.tsx index 14bc9357..96173563 100644 --- a/src/components/view-meta/ViewMetaPreview.tsx +++ b/src/components/view-meta/ViewMetaPreview.tsx @@ -8,7 +8,7 @@ import PageIcon from '@/components/_shared/view-icon/PageIcon'; const AddIconCover = lazy(() => import('@/components/view-meta/AddIconCover')); -export function ViewMetaPreview({ +export function ViewMetaPreview ({ icon: iconProp, cover: coverProp, name, @@ -121,6 +121,31 @@ export function ViewMetaPreview({ } }, [extra, icon, name, updatePage, viewId]); + const ref = React.useRef(null); + + useEffect(() => { + const el = ref.current; + const handleMouseEnter = () => { + setIsHover(true); + }; + + const handleMouseLeave = () => { + setIsHover(false); + }; + + if (el) { + el.addEventListener('mouseenter', handleMouseEnter); + el.addEventListener('mouseleave', handleMouseLeave); + } + + return () => { + if (el) { + el.removeEventListener('mouseenter', handleMouseEnter); + el.removeEventListener('mouseleave', handleMouseLeave); + } + }; + }, []); + return (
{cover && }
setIsHover(true)} - onMouseLeave={() => setIsHover(false)} + ref={ref} className={'flex mt-2 flex-col relative w-full overflow-hidden'} >