fix: publish and search bugs (#11)

* fix: some bugs

* fix: uniq search result

* fix: publish bugs

* fix: add cover icon display on hover title

* fix: update editor version

* fix: update panel position

* fix: adjust search UI

* fix: modified unpublish toast

* fix: unrelease publish and search

* fix: add release yml

* fix: add sync release to change

* fix: update comment

* fix: update release ci
This commit is contained in:
Kilu.He
2025-01-10 13:08:14 +08:00
committed by GitHub
parent 92e97fce81
commit 4152b46bd2
32 changed files with 583 additions and 289 deletions

1
CHANGELOG.md Normal file
View File

@@ -0,0 +1 @@
# Release Notes

View File

@@ -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",

8
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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"
}

View File

@@ -1,4 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.6 19.4998H21V21.4998H15V15.4998H17V18.2298C18.83 16.7598 20 14.5198 20 11.9998C20 7.9298 16.94 4.5598 13 4.0698V2.0498C18.05 2.5498 22 6.8098 22 11.9998C22 14.9898 20.68 17.6698 18.6 19.4998ZM4 11.9998C4 9.4798 5.17 7.2298 7 5.7698V8.4998H9V2.4998H3V4.4998H5.4C3.32 6.3298 2 9.0098 2 11.9998C2 17.1898 5.95 21.4498 11 21.9498V19.9298C7.06 19.4398 4 16.0698 4 11.9998ZM16.24 8.1098L10.58 13.7698L7.75 10.9398L6.34 12.3498L10.58 16.5898L17.65 9.5198L16.24 8.1098Z"
fill="currentColor"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<g clip-path="url(#clip0_606_6514)">
<circle cx="6.99957" cy="6.99957" r="6.22222" fill="#00BCF0"/>
<path d="M4.27734 7.38878L6.37136 9.33323L10.1107 5.44434" stroke="white" stroke-width="1.2963"
stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_606_6514">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 528 B

View File

@@ -1,11 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="vuesax/linear/search-normal">
<g id="search-normal">
<path id="Vector"
d="M7.66665 13.9999C11.1644 13.9999 14 11.1644 14 7.66659C14 4.16878 11.1644 1.33325 7.66665 1.33325C4.16884 1.33325 1.33331 4.16878 1.33331 7.66659C1.33331 11.1644 4.16884 13.9999 7.66665 13.9999Z"
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M14.6666 14.6666L13.3333 13.3333" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round"/>
</g>
</g>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M8.67188 15.375C12.6069 15.375 15.7969 12.185 15.7969 8.25C15.7969 4.31497 12.6069 1.125 8.67188 1.125C4.73685 1.125 1.54688 4.31497 1.54688 8.25C1.54688 12.185 4.73685 15.375 8.67188 15.375Z"
stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.9106 16.5922L13.5703 14.252" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 555 B

View File

@@ -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);

View File

@@ -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<PopoverComponentProps> = {
@@ -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) {

View File

@@ -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={<Workspaces/>}
header={<Workspaces />}
onScroll={handleOnScroll}
>
<div
className={'flex w-full gap-1 flex-1 flex-col'}
>
<div
className={'px-[10px] bg-bg-base z-[1] flex-col gap-1.5 justify-around items-center sticky top-12'}
className={'px-[10px] bg-bg-base z-[1] flex-col gap-2 justify-around items-center sticky top-12'}
>
<Search />
{/*<Search />*/}
<div
style={{
borderColor: scrollTop > 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'}
>
<NewPage />
</div>

View File

@@ -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 <AppContext.Provider

View File

@@ -64,7 +64,7 @@ export function PublishManage ({
setLoading(true);
try {
const outline = await service.getPublishOutline(namespace);
setPublishViews(flattenViews(outline).filter(item => item.is_published));
// eslint-disable-next-line
} catch (e: any) {
@@ -241,7 +241,7 @@ export function PublishManage ({
onUpdateHomePage={handleUpdateHomePage}
/>
<Tooltip
title={isOwner ? (activeSubscription === SubscriptionPlan.Free ? t('settings.sites.error.onlyProCanSetHomepage') : undefined)
title={isOwner ? (activeSubscription === SubscriptionPlan.Free ? t('settings.sites.error.onlyProCanUpdateNamespace') : undefined)
: t('settings.sites.error.onlyWorkspaceOwnerCanUpdateNamespace')}
>
<IconButton

View File

@@ -84,7 +84,7 @@ function PublishedPageItem ({ onClose, view, onUnPublish, onPublish }: {
value: 'unpublish',
disabled: unPublishLoading,
label: t('shareAction.unPublish'),
tooltip: !(isOwner || isPublisher) ? t('settings.sites.error.publishPermissionDenied') : undefined,
tooltip: !(isOwner || isPublisher) ? t('settings.sites.error.unPublishPermissionDenied') : undefined,
IconComponent: unPublishLoading ? CircularProgress : TrashIcon,
onClick: async () => {
if (!(isOwner || isPublisher)) {
@@ -109,6 +109,7 @@ function PublishedPageItem ({ onClose, view, onUnPublish, onPublish }: {
IconComponent: SettingIcon,
onClick: () => {
if (!(isOwner || isPublisher)) return;
setAnchorEl(null);
setOpenSetting(true);
},
},

View File

@@ -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<View[]>([]);
const [views, setViews] = React.useState<View[] | undefined>(undefined);
const { t } = useTranslation();
const outline = useAppOutline();
const [loading, setLoading] = React.useState<boolean>(false);
const service = useService();
const currentWorkspaceId = useCurrentWorkspaceId()
const [loading, setLoading] = React.useState<boolean>(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 <ViewList views={views} title={t('commandPalette.bestMatches')} onClose={onClose} loading={loading} />
return <ViewList
views={views}
loading={loading}
title={t('searchResults')}
onClose={onClose}
/>;
}
export default BestMatch;

View File

@@ -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<boolean>(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 <Tooltip
enterDelay={700}
disableInteractive={true}
title={view.name}
>
<div
style={{
cursor: view.extra?.is_space ? 'default' : 'pointer',
}}
onClick={e => {
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`}
>
<span className={'truncate'}>{view.name || t('menuAppHeader.defaultNewPageName')}</span>
{isPrivate &&
<div className={'h-4 w-4 text-base min-w-4 text-text-title opacity-80'}>
<PrivateIcon />
</div>
}
</div>
</Tooltip>;
}, [onClose, t, toView]);
const breadcrumbs = useMemo(() => {
if (!ancestors) return null;
if (ancestors.length <= 3) {
return ancestors.map((ancestor, index) => {
return <div
key={ancestor.view_id}
className={'flex items-center gap-2'}
>
{renderBreadcrumb(ancestor)}
{index !== ancestors.length - 1 && <span>{'/'}</span>}
</div>;
});
}
const first = renderBreadcrumb(ancestors[0]);
const last = renderBreadcrumb(ancestors[ancestors.length - 1]);
return <>
{first}
<div className={'flex items-center gap-2'}>
<span>{'/'}</span>
<RichTooltip
open={open}
placement="bottom"
onClose={() => setOpen(false)}
content={
<Paper className={'p-1'}>
{ancestors.slice(1, -1).map((ancestor) => {
return <div
key={ancestor.view_id}
className={'flex items-center w-full gap-2 p-1.5'}
>
{renderBreadcrumb(ancestor)}
</div>;
})}
</Paper>
}
>
<IconButton
onClick={(e) => {
e.stopPropagation();
setOpen((prev) => !prev);
}}
size={'small'}
>
<MoreIcon />
</IconButton>
</RichTooltip>
<span>{'/'}</span>
{last}
</div>
</>;
}, [ancestors, open, renderBreadcrumb]);
return (
<div
data-item-id={view.view_id}
style={{
backgroundColor: selectedView === view.view_id ? 'var(--fill-list-active)' : undefined,
}}
onClick={onClick}
className={'flex flex-col w-full px-4 py-2 cursor-pointer hover:bg-fill-list-active gap-1'}
>
<div className={'flex items-center gap-3'}>
<div className={'w-7 h-7 border flex items-center justify-center rounded border-line-border'}>
<PageIcon
view={view}
className={'w-4 h-4 flex items-center justify-center'}
/>
</div>
<div className={'text-base font-medium flex-1 truncate'}>
{view.name.trim() || t('menuAppHeader.defaultNewPageName')}
</div>
</div>
<div className={'ml-10'}>
<div className={'text-sm text-text-caption overflow-hidden w-full gap-2 flex items-center'}>
{breadcrumbs}
</div>
</div>
</div>
);
}
export default ListItem;

View File

@@ -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<boolean>(false);
useEffect(() => {
void (async () => {
setLoading(true);
await loadRecentViews?.();
setLoading(false);
})();
}, [loadRecentViews]);
const { t } = useTranslation();
return (
<ViewList views={recentViews} title={t('commandPalette.recentHistory')} onClose={onClose} loading={loading} />
<ViewList
loading={loading}
views={recentViews}
title={t('recentPages')}
onClose={onClose}
/>
);
}

View File

@@ -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<boolean>(false);
const { t } = useTranslation();
const [searchValue, setSearchValue] = React.useState<string>('');
const [searchType, setSearchType] = React.useState<SEARCH_TYPE>(SEARCH_TYPE.TITLE_MATCH);
const [searchType, setSearchType] = React.useState<SEARCH_TYPE>(SEARCH_TYPE.AI_SUGGESTION);
const [searchTypeAnchorEl, setSearchTypeAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClose = () => {
setOpen(false);
setSearchValue('');
@@ -49,36 +49,63 @@ export function Search () {
};
}, [onKeyDown]);
const {
recentViews,
loadRecentViews,
} = useAppRecent();
const [loadingRecentViews, setLoadingRecentViews] = React.useState<boolean>(false);
useEffect(() => {
if (!open) return;
void (async () => {
setLoadingRecentViews(true);
await loadRecentViews?.();
setLoadingRecentViews(false);
})();
}, [loadRecentViews, open]);
return (
<>
<Button
onClick={(e) => {
e.currentTarget.blur();
setOpen(true);
}}
startIcon={<SearchIcon className={'w-5 opacity-60 h-5 mr-[1px]'} />}
size={'small'}
className={'text-sm font-normal py-1.5 justify-start w-full hover:bg-fill-list-hover'}
color={'inherit'}
<Tooltip
title={<div className={'flex flex-col gap-1'}>
<span>{t('search.sidebarSearchIcon')}</span>
<div className={'text-text-caption'}>{createHotKeyLabel(HOT_KEY_NAME.SEARCH)}</div>
</div>}
>
{t('button.search')}
</Button>
<Button
onClick={(e) => {
e.currentTarget.blur();
setOpen(true);
}}
startIcon={<SearchIcon className={'w-5 opacity-60 h-5 mr-[1px]'} />}
size={'small'}
className={'text-sm font-normal py-1.5 justify-start w-full hover:bg-fill-list-hover'}
color={'inherit'}
>
{t('button.search')}
</Button>
</Tooltip>
<Dialog
disableRestoreFocus={true}
open={open}
onClose={handleClose}
classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%]' }}
classes={{
container: 'items-start max-md:mt-auto max-md:items-center mt-[10%]',
paper: 'overflow-hidden min-w-[600px] w-[600px] max-w-[70vw]',
}}
>
<div className={'flex gap-2 border-b border-line-default w-full p-4'}>
<div className={'w-full flex gap-4 items-center min-w-[500px] max-w-[70vw]'}>
<div className={'w-full flex gap-4 items-center'}>
<SearchIcon className={'w-5 opacity-60 h-5 mr-[1px]'} />
<InputBase
value={searchValue}
onChange={(e) => 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')}
/>
<span
style={{
@@ -91,30 +118,35 @@ export function Search () {
setSearchValue('');
}}
><CloseIcon className={'w-3 h-3'} /></span>
<Tooltip title={searchType === SEARCH_TYPE.TITLE_MATCH ? undefined : 'we currently only support searching for pages and content in documents'}>
<div
onClick={e => {
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')
}
<DownIcon className={'w-3 h-3 ml-1 opacity-60'} />
</div>
<Tooltip title={'we currently only support searching for pages and content in documents'}>
<span className={'cursor-default flex items-center p-1 px-2 text-xs rounded bg-fill-list-hover'}>BETA</span>
</Tooltip>
</div>
</div>
{!searchValue ? <RecentViews onClose={handleClose} /> : searchType === SEARCH_TYPE.AI_SUGGESTION ? <BestMatch
<div className={'p-4 py-2 w-full flex items-center gap-2'}>
<div
onClick={(e) => {
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'}
>
<span className={' max-w-[100px] truncate'}>{searchType === SEARCH_TYPE.TITLE_MATCH ? t('titleOnly') : t('AIsearch')}</span>
<DownIcon className={'w-4 h-4'} />
</div>
</div>
<Divider className={'border-line-default'} />
{!searchValue ? <RecentViews
loading={loadingRecentViews}
recentViews={recentViews}
onClose={handleClose}
/> : searchType === SEARCH_TYPE.AI_SUGGESTION ? <BestMatch
searchValue={searchValue}
onClose={handleClose}
/> : <TitleMatch
searchValue={searchValue}
onClose={handleClose}
/>}
</Dialog>
<Popover
open={Boolean(searchTypeAnchorEl)}
@@ -126,17 +158,17 @@ export function Search () {
},
}}
>
{[SEARCH_TYPE.TITLE_MATCH, SEARCH_TYPE.AI_SUGGESTION].map(type => (
{[SEARCH_TYPE.AI_SUGGESTION, SEARCH_TYPE.TITLE_MATCH].map(type => (
<div
key={type}
className={'px-2 py-1.5 text-xs rounded-[8px] flex items-center gap-2 cursor-pointer hover:bg-fill-list-hover'}
className={'p-2 text-sm rounded-[8px] flex items-center gap-2 cursor-pointer hover:bg-fill-list-hover'}
onClick={() => {
setSearchType(type);
setSearchTypeAnchorEl(null);
}}
>
{type === SEARCH_TYPE.TITLE_MATCH ? t('titleMatch') : t('aiMatch')}
{type === searchType && <CheckIcon className={'w-4 text-function-info h-4 ml-2'} />}
{type === SEARCH_TYPE.TITLE_MATCH ? t('titleOnly') : t('AIsearch')}
{type === searchType && <CheckIcon className={'w-5 text-function-info h-5'} />}
</div>
))}
</Popover>

View File

@@ -21,10 +21,10 @@ function TitleMatch ({
return (
<ViewList
loading={false}
views={views}
title={t('commandPalette.bestMatches')}
onClose={onClose}
loading={false}
/>
);
}

View File

@@ -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 (
<div
ref={ref}
className={'flex flex-col'}
>
<div className={'px-4 py-2'}>
{title}
<div className={'px-4 pt-5 pb-2 flex items-center gap-4'}>
{!loading && views && views.length === 0 ? t('noSearchResults') : <>
{title}
{loading && <CircularProgress size={14} />}
</>}
</div>
<div className={'flex min-h-[280px] flex-col max-h-[360px] appflowy-scroller overflow-y-auto'}>
{views?.length ? views.map(view => (
<div
data-item-id={view.view_id}
style={{
backgroundColor: selectedView === view.view_id ? 'var(--fill-list-active)' : undefined
}}
{views?.map(view => (
<ListItem
key={view.view_id}
selectedView={selectedView}
view={view}
onClick={() => {
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'}
>
<PageIcon
view={view}
className={'w-5 h-5'}
/>
<div className={'text-sm font-normal flex-1 truncate'}>
{view.name.trim() || t('menuAppHeader.defaultNewPageName')}
</div>
</div>
)) : <div className={'text-center p-6 text-sm text-text-caption'}>
{t('findAndReplace.noResult')}
</div>}
{loading &&
<div className={'text-center text-sm text-text-caption bg-bg-body opacity-75 absolute w-full h-full inset-0 flex items-center justify-center'}>
<CircularProgress />
</div>
}
onClose={onClose}
/>
))}
</div>
<div className={'w-full p-4 flex text-text-caption text-xs gap-2 items-center'}>
<span className={'rounded bg-fill-list-hover p-1'}>TAB</span>

View File

@@ -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 ({
<>
<div className={'overflow-hidden items-center w-full flex'}>
<div className={'flex-1 overflow-hidden flex items-center gap-1'}>
<div className={'border w-[177px] truncate bg-fill-list-hover border-line-divider rounded-[6px] py-1 px-2'}>{window.location.origin}</div>
<Tooltip
placement={'top'}
title={window.location.origin}
>
<div className={'border flex-1 cursor-default truncate bg-fill-list-hover border-line-divider rounded-[6px] py-1 px-2'}>{window.location.origin}</div>
</Tooltip>
{'/'}
<div className={'border gap-1 w-[100px] border-line-divider rounded-[6px] py-1 px-2 flex items-center'}>
<div className={'border gap-1 w-[110px] border-line-divider rounded-[6px] py-1 px-2 flex items-center'}>
<Tooltip
placement={'top'}
title={publishInfo.namespace}
@@ -77,32 +84,39 @@ function PublishLinkPreview ({
</div>
{'/'}
<div
className={'border gap-1 flex items-center truncate w-[140px] border-line-divider rounded-[6px] py-1 px-2'}
className={'border gap-1 flex items-center truncate w-[150px] border-line-divider rounded-[6px] py-1 px-2'}
>
<InputBase
disabled={!isOwner && !isPublisher}
inputProps={{
className: 'pb-0',
}}
onFocus={() => {
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'}
/>
<Tooltip
placement={'top'}
title={publishName}
>
<InputBase
disabled={!isOwner && !isPublisher}
inputProps={{
className: 'pb-0',
}}
onFocus={() => {
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'}
/>
</Tooltip>
{(isOwner || isPublisher) && <Tooltip
placement={'top'}
title={focused ? t('button.save') : t('settings.sites.customUrl')}
@@ -135,6 +149,10 @@ function PublishLinkPreview ({
title={t('shareAction.copyLink')}
>
<IconButton
onClick={async () => {
await copyTextToClipboard(url);
notify.success(t('shareAction.copyLinkSuccess'));
}}
color={'inherit'}
size={'small'}
>

View File

@@ -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<boolean>(false);
const [publishLoading, setPublishLoading] = React.useState<boolean>(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')}</Button>
startIcon={unpublishLoading ? <CircularProgress size={16} /> : undefined}
>{
t('shareAction.unPublish')
}</Button>
<Button
className={'flex-1 max-w-[50%]'}
onClick={() => {
@@ -83,7 +101,7 @@ function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => voi
>{t('shareAction.visitSite')}</Button>
</div>
</div>;
}, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, url, view]);
}, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, unpublishLoading, url, view]);
const renderUnpublished = useCallback(() => {
return <Button
@@ -93,8 +111,14 @@ function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => voi
variant={'contained'}
className={'w-full'}
color={'primary'}
>{t('shareAction.publish')}</Button>;
}, [handlePublish, t]);
startIcon={publishLoading ? <CircularProgress
color={'inherit'}
size={16}
/> : undefined}
>{
t('shareAction.publish')
}</Button>;
}, [handlePublish, publishLoading, t]);
return (
<div className={'flex flex-col gap-2 w-full overflow-hidden'}>

View File

@@ -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 ? <PublishedWithChanges className={'w-4 h-4 text-function-success mb-0'} /> : undefined,
Panel: PublishPanel,
}, currentUser?.email?.endsWith('appflowy.io') && view?.is_published && {
value: TabKey.TEMPLATE,
label: t('template.asTemplate'),
icon: <Templates className={'w-4 h-4 mb-0'} />,
Panel: TemplatePanel,
}].filter(Boolean) as {
},
// {
// value: TabKey.PUBLISH,
// label: t('shareAction.publish'),
// icon: view?.is_published ? <PublishedWithChanges className={'w-4 h-4 text-function-success mb-0'} /> : undefined,
// Panel: PublishPanel,
// },
currentUser?.email?.endsWith('appflowy.io') && view?.is_published && {
value: TabKey.TEMPLATE,
label: t('template.asTemplate'),
icon: <Templates className={'w-4 h-4 mb-0'} />,
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 (
<>
<ViewTabs
@@ -69,7 +78,7 @@ function ShareTabs ({ opened, viewId, onClose }: { opened: boolean, viewId: stri
<div className={'p-2'}>
{options.map((option) => (
<TabPanel
className={'min-w-[460px] max-sm:min-w-[80vw]'}
className={'min-w-[500px] w-[500px] max-w-full max-sm:min-w-[80vw]'}
key={option.value}
index={option.value}
value={value}
@@ -77,6 +86,7 @@ function ShareTabs ({ opened, viewId, onClose }: { opened: boolean, viewId: stri
<option.Panel
viewId={viewId}
onClose={onClose}
opened={opened}
/>
</TabPanel>
))}

View File

@@ -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());

View File

@@ -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 <Popover
open={open}
onClose={handleClose}
@@ -118,12 +113,9 @@ function BlockPopover () {
slotProps={{
paper: {
ref: paperRef,
className: 'w-[560px] min-h-[200px]',
},
}}
onTransitionEnter={() => {
setOrigins(defaultOrigins);
}}
onTransitionEnd={debouncePosition}
{...origins}
disableRestoreFocus={true}
>

View File

@@ -8,7 +8,7 @@ export const Callout = memo(
<div
ref={ref}
{...attributes}
className={`${attributes.className ?? ''} flex pr-2 w-full flex-col rounded border border-line-divider bg-fill-list-active py-2`}
className={`${attributes.className ?? ''} flex pr-2 w-full flex-col rounded border border-line-divider bg-fill-list-active py-2.5`}
>
{children}
</div>

View File

@@ -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<HTMLButtonElement>(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',
}}
>
<span
className={`text-[18px] py-1 px-1 ${readOnly ? '' : 'hover:bg-fill-list-hover rounded-[6px]'}`}
className={`w-8 h-8 absolute -top-[4px] flex text-[18px] items-center justify-center ${readOnly ? '' : 'hover:bg-fill-list-hover rounded-[6px]'}`}
>{node.data.icon || `📌`}</span>
</span>

View File

@@ -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}

View File

@@ -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<PopoverOrigin | undefined>(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 (
<Popover
adjustOrigins={true}
adjustOrigins={false}
data-testid={'mention-panel'}
open={open}
onClose={closePanel}
@@ -339,10 +353,7 @@ export function MentionPanel() {
disableAutoFocus={true}
disableRestoreFocus={true}
disableEnforceFocus={true}
transformOrigin={{
vertical: -32,
horizontal: -8,
}}
transformOrigin={transformOrigin}
onMouseDown={e => e.preventDefault()}
>
<div
@@ -363,7 +374,10 @@ export function MentionPanel() {
key={view.view_id}
data-option-index={index}
startIcon={
<PageIcon view={view} className={'flex h-5 w-5 min-w-5 items-center justify-center'}/>
<PageIcon
view={view}
className={'flex h-5 w-5 min-w-5 items-center justify-center'}
/>
}
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() {
</div>
) :
<div
className={'text-text-caption text-sm flex justify-center items-center p-2'}>{t('findAndReplace.noResult')}</div>
className={'text-text-caption text-sm flex justify-center items-center p-2'}
>{t('findAndReplace.noResult')}</div>
}
{showMore &&
<div data-option-category={MentionTag.LoadMore} className={'w-full'}>
<div
data-option-category={MentionTag.LoadMore}
className={'w-full'}
>
<Button
color={'inherit'}
size={'small'}
data-option-index={0}
startIcon={<MoreIcon/>}
startIcon={<MoreIcon />}
className={`justify-start w-full scroll-m-2 min-h-[32px] hover:bg-fill-list-hover ${selectedOption?.index === 0 && selectedOption?.category === MentionTag.LoadMore ? 'bg-fill-list-hover' : ''}`}
onClick={handleClickMore}
>
@@ -391,7 +409,10 @@ export function MentionPanel() {
</div>
}
</div>
{showDate && <div className={'flex flex-col gap-2'} data-option-category={MentionTag.Date}>
{showDate && <div
className={'flex flex-col gap-2'}
data-option-category={MentionTag.Date}
>
<div className={'text-text-caption scroll-my-10 px-1'}>{t('inlineActions.date')}</div>
{
dateOptions.map((option, index) => (
@@ -416,10 +437,10 @@ export function MentionPanel() {
data-option-category={MentionTag.NewPage}
className={'flex w-full flex-col gap-2'}
>
<Divider/>
<Divider />
<Button
color={'inherit'}
startIcon={<AddIcon/>}
startIcon={<AddIcon />}
size={'small'}
data-option-index={0}
className={`justify-start scroll-m-2 min-h-[32px] hover:bg-fill-list-hover ${selectedOption?.index === 0 && selectedOption?.category === MentionTag.NewPage ? 'bg-fill-list-hover' : ''}`}
@@ -438,7 +459,7 @@ export function MentionPanel() {
<Button
color={'inherit'}
startIcon={<ArrowIcon className={' text-content-blue-900 w-[0.75em] h-[0.75em] mx-0.5'}/>}
startIcon={<ArrowIcon className={' text-content-blue-900 w-[0.75em] h-[0.75em] mx-0.5'} />}
size={'small'}
data-option-index={1}
className={`justify-start scroll-m-2 min-h-[32px] hover:bg-fill-list-hover ${selectedOption?.index === 1 && selectedOption?.category === MentionTag.NewPage ? 'bg-fill-list-hover' : ''}`}

View File

@@ -41,18 +41,19 @@ import { ReactComponent as ToggleHeading2Icon } from '@/assets/toggle_heading2.s
import { ReactComponent as ToggleHeading3Icon } from '@/assets/toggle_heading3.svg';
import { ReactComponent as MathIcon } from '@/assets/slash_menu_icon_math_equation.svg';
import { notify } from '@/components/_shared/notify';
import { Popover } from '@/components/_shared/popover';
import { calculateOptimalOrigins, Popover } from '@/components/_shared/popover';
import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext';
import { usePanelContext } from '@/components/editor/components/panels/Panels.hooks';
import { PanelType } from '@/components/editor/components/panels/PanelsContext';
import { getRangeRect } from '@/components/editor/components/toolbar/selection-toolbar/utils';
import { useEditorContext } from '@/components/editor/EditorContext';
import { Button } from '@mui/material';
import { PopoverOrigin } from '@mui/material/Popover/Popover';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactEditor, useSlateStatic } from 'slate-react';
export function SlashPanel({
export function SlashPanel ({
setEmojiPosition,
}: {
setEmojiPosition: (position: { top: number; left: number }) => void;
@@ -68,6 +69,7 @@ export function SlashPanel({
const optionsRef = useRef<HTMLDivElement>(null);
const editor = useSlateStatic() as YjsEditor;
const [selectedOption, setSelectedOption] = React.useState<string | null>(null);
const [transformOrigin, setTransformOrigin] = React.useState<PopoverOrigin | undefined>(undefined);
const selectedOptionRef = React.useRef<string | null>(null);
const {
openPopover,
@@ -142,7 +144,7 @@ export function SlashPanel({
{
label: t('document.slashMenu.name.text'),
key: 'text',
icon: <TextIcon/>,
icon: <TextIcon />,
onClick: () => {
turnInto(BlockType.Paragraph, {});
},
@@ -150,7 +152,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.heading1'),
key: 'heading1',
icon: <Heading1Icon/>,
icon: <Heading1Icon />,
keywords: ['heading1', 'h1', 'heading'],
onClick: () => {
turnInto(BlockType.HeadingBlock, {
@@ -160,7 +162,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.heading2'),
key: 'heading2',
icon: <Heading2Icon/>,
icon: <Heading2Icon />,
keywords: ['heading2', 'h2', 'subheading', 'heading'],
onClick: () => {
turnInto(BlockType.HeadingBlock, {
@@ -170,7 +172,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.heading3'),
key: 'heading3',
icon: <Heading3Icon/>,
icon: <Heading3Icon />,
keywords: ['heading3', 'h3', 'subheading', 'heading'],
onClick: () => {
turnInto(BlockType.HeadingBlock, {
@@ -180,7 +182,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.image'),
key: 'image',
icon: <ImageIcon/>,
icon: <ImageIcon />,
keywords: ['image', 'img'],
onClick: () => {
turnInto(BlockType.ImageBlock, {
@@ -191,7 +193,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.bulletedList'),
key: 'bulletedList',
icon: <BulletedListIcon/>,
icon: <BulletedListIcon />,
keywords: ['bulleted', 'list'],
onClick: () => {
turnInto(BlockType.BulletedListBlock, {});
@@ -199,7 +201,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.numberedList'),
key: 'numberedList',
icon: <NumberedListIcon/>,
icon: <NumberedListIcon />,
keywords: ['numbered', 'list'],
onClick: () => {
turnInto(BlockType.NumberedListBlock, {});
@@ -207,7 +209,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.todoList'),
key: 'todoList',
icon: <TodoListIcon/>,
icon: <TodoListIcon />,
keywords: ['todo', 'list'],
onClick: () => {
turnInto(BlockType.TodoListBlock, {});
@@ -215,7 +217,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.divider'),
key: 'divider',
icon: <DividerIcon/>,
icon: <DividerIcon />,
keywords: ['divider', 'line'],
onClick: () => {
turnInto(BlockType.DividerBlock, {});
@@ -223,7 +225,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.quote'),
key: 'quote',
icon: <QuoteIcon/>,
icon: <QuoteIcon />,
keywords: ['quote'],
onClick: () => {
turnInto(BlockType.QuoteBlock, {});
@@ -231,7 +233,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.linkedDoc'),
key: 'linkedDoc',
icon: <DocumentIcon/>,
icon: <DocumentIcon />,
keywords: ['linked', 'doc', 'page', 'document'],
onClick: () => {
const rect = getRangeRect();
@@ -242,7 +244,7 @@ export function SlashPanel({
}, {
label: t('document.menuName'),
key: 'document',
icon: <AddDocumentIcon/>,
icon: <AddDocumentIcon />,
keywords: ['document', 'doc', 'page', 'create', 'add'],
onClick: async () => {
if (!viewId || !addPage || !openPageModal) return;
@@ -350,7 +352,7 @@ export function SlashPanel({
{
label: t('document.slashMenu.name.callout'),
key: 'callout',
icon: <CalloutIcon/>,
icon: <CalloutIcon />,
keywords: ['callout'],
onClick: () => {
turnInto(BlockType.CalloutBlock, {
@@ -360,7 +362,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.outline'),
key: 'outline',
icon: <OutlineIcon/>,
icon: <OutlineIcon />,
keywords: ['outline', 'table', 'contents'],
onClick: () => {
turnInto(BlockType.OutlineBlock, {});
@@ -368,7 +370,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.mathEquation'),
key: 'math',
icon: <MathIcon/>,
icon: <MathIcon />,
keywords: ['math', 'equation', 'formula'],
onClick: () => {
turnInto(BlockType.EquationBlock, {});
@@ -376,7 +378,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.code'),
key: 'code',
icon: <CodeIcon/>,
icon: <CodeIcon />,
keywords: ['code', 'block'],
onClick: () => {
turnInto(BlockType.CodeBlock, {});
@@ -384,7 +386,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.toggleList'),
key: 'toggleList',
icon: <ToggleListIcon/>,
icon: <ToggleListIcon />,
keywords: ['toggle', 'list'],
onClick: () => {
turnInto(BlockType.ToggleListBlock, {
@@ -394,7 +396,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.toggleHeading1'),
key: 'toggleHeading1',
icon: <ToggleHeading1Icon/>,
icon: <ToggleHeading1Icon />,
keywords: ['toggle', 'heading1', 'h1', 'heading'],
onClick: () => {
turnInto(BlockType.ToggleListBlock, {
@@ -405,7 +407,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.toggleHeading2'),
key: 'toggleHeading2',
icon: <ToggleHeading2Icon/>,
icon: <ToggleHeading2Icon />,
keywords: ['toggle', 'heading2', 'h2', 'subheading', 'heading'],
onClick: () => {
turnInto(BlockType.ToggleListBlock, {
@@ -416,7 +418,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.toggleHeading3'),
key: 'toggleHeading3',
icon: <ToggleHeading3Icon/>,
icon: <ToggleHeading3Icon />,
keywords: ['toggle', 'heading3', 'h3', 'subheading', 'heading'],
onClick: () => {
turnInto(BlockType.ToggleListBlock, {
@@ -427,7 +429,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.emoji'),
key: 'emoji',
icon: <EmojiIcon/>,
icon: <EmojiIcon />,
keywords: ['emoji'],
onClick: () => {
setTimeout(() => {
@@ -444,7 +446,7 @@ export function SlashPanel({
}, {
label: t('document.slashMenu.name.file'),
key: 'file',
icon: <FileIcon/>,
icon: <FileIcon />,
keywords: ['file', 'upload'],
onClick: () => {
turnInto(BlockType.FileBlock, {});
@@ -542,9 +544,21 @@ export function SlashPanel({
setSelectedOption(null);
}, [options.length]);
useEffect(() => {
if (open && panelPosition) {
const origins = calculateOptimalOrigins(panelPosition, 320, 400, undefined, 16);
const isAlignBottom = origins.transformOrigin.vertical === 'bottom';
setTransformOrigin(isAlignBottom ? origins.transformOrigin : {
vertical: -30,
horizontal: origins.transformOrigin.horizontal,
});
}
}, [open, panelPosition]);
return (
<Popover
adjustOrigins={true}
adjustOrigins={false}
data-testid={'slash-panel'}
open={open}
onClose={closePanel}
@@ -553,10 +567,7 @@ export function SlashPanel({
disableAutoFocus={true}
disableRestoreFocus={true}
disableEnforceFocus={true}
transformOrigin={{
vertical: -32,
horizontal: 'left',
}}
transformOrigin={transformOrigin}
onMouseDown={e => e.preventDefault()}
>
<div
@@ -580,7 +591,8 @@ export function SlashPanel({
</Button>
)) :
<div
className={'text-text-caption text-sm flex justify-center items-center py-4'}>{t('findAndReplace.noResult')}</div>}
className={'text-text-caption text-sm flex justify-center items-center py-4'}
>{t('findAndReplace.noResult')}</div>}
</div>

View File

@@ -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)',
},

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<HTMLDivElement>(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 (
<div className={'flex w-full flex-col items-center'}>
{cover && <ViewCover
@@ -131,8 +156,7 @@ export function ViewMetaPreview({
readOnly={readOnly}
/>}
<div
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
ref={ref}
className={'flex mt-2 flex-col relative w-full overflow-hidden'}
>
<div className={'relative flex justify-center max-sm:h-[38px] h-[52px] w-full'}>