mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
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:
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
||||
# Release Notes
|
||||
@@ -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
8
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
150
src/components/app/search/ListItem.tsx
Normal file
150
src/components/app/search/ListItem.tsx
Normal 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;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,10 +21,10 @@ function TitleMatch ({
|
||||
|
||||
return (
|
||||
<ViewList
|
||||
loading={false}
|
||||
views={views}
|
||||
title={t('commandPalette.bestMatches')}
|
||||
onClose={onClose}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
>
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' : ''}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
Reference in New Issue
Block a user