feat: enable share menu (#1883)

Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
Himself65
2023-04-13 16:22:49 -05:00
committed by GitHub
parent 32b206a137
commit 01a686dc28
48 changed files with 2666 additions and 2113 deletions

View File

@@ -122,6 +122,7 @@ export class AffineErrorBoundary extends Component<
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
</>
);
}

View File

@@ -0,0 +1,58 @@
import { MenuItem, styled } from '@affine/component';
import type { PublicLinkDisableProps } from '@affine/component/share-menu';
import { PublicLinkDisableModal } from '@affine/component/share-menu';
import { useTranslation } from '@affine/i18n';
import { ShareIcon } from '@blocksuite/icons';
import type { CommonMenuItemProps } from './types';
const StyledMenuItem = styled(MenuItem)(({ theme }) => {
return {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
':hover': {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
},
};
});
export const DisablePublicSharing = ({
onSelect,
onItemClick,
testId,
}: CommonMenuItemProps) => {
const { t } = useTranslation();
return (
<>
<StyledMenuItem
data-testid={testId}
onClick={() => {
onItemClick?.();
onSelect?.();
}}
style={{ color: 'red' }}
icon={<ShareIcon />}
>
{t('Disable Public Sharing')}
</StyledMenuItem>
</>
);
};
const DisablePublicSharingModal = ({
page,
open,
onClose,
}: PublicLinkDisableProps) => {
return <PublicLinkDisableModal page={page} open={open} onClose={onClose} />;
};
DisablePublicSharing.DisablePublicSharingModal = DisablePublicSharingModal;

View File

@@ -1,4 +1,5 @@
export * from './CopyLink';
export * from './DisablePublicSharing';
export * from './Export';
export * from './MoveTo';
export * from './MoveToTrash';

View File

@@ -19,7 +19,11 @@ export const TransformWorkspaceToAffineModal: React.FC<
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<Modal
open={open}
onClose={onClose}
data-testid="enable-affine-cloud-modal"
>
<ModalWrapper width={560} height={292}>
<Header>
<IconButton

View File

@@ -16,12 +16,17 @@ import {
ResetIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import type React from 'react';
import { useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { MoveTo, MoveToTrash } from '../../../affine/operation-menu-items';
import {
DisablePublicSharing,
MoveTo,
MoveToTrash,
} from '../../../affine/operation-menu-items';
export type OperationCellProps = {
pageMeta: PageMeta;
@@ -40,12 +45,24 @@ export const OperationCell: React.FC<OperationCellProps> = ({
onToggleFavoritePage,
onToggleTrashPage,
}) => {
const { id, favorite } = pageMeta;
const { id, favorite, isPublic } = pageMeta;
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openDisableShared, setOpenDisableShared] = useState(false);
const page = blockSuiteWorkspace.getPage(id);
assertExists(page);
const OperationMenu = (
<>
{isPublic && (
<DisablePublicSharing
testId="disable-public-sharing"
onItemClick={() => {
setOpenDisableShared(true);
}}
/>
)}
<MenuItem
onClick={() => {
onToggleFavoritePage(id);
@@ -111,6 +128,13 @@ export const OperationCell: React.FC<OperationCellProps> = ({
setOpen(false);
}}
/>
<DisablePublicSharing.DisablePublicSharingModal
page={page}
open={openDisableShared}
onClose={() => {
setOpenDisableShared(false);
}}
/>
</>
);
};

View File

@@ -81,7 +81,7 @@ const FavoriteTag: React.FC<FavoriteTagProps> = ({
type PageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
isPublic?: boolean;
listType?: 'all' | 'trash' | 'favorite';
listType?: 'all' | 'trash' | 'favorite' | 'shared';
onClickPage: (pageId: string, newTab?: boolean) => void;
};
@@ -92,6 +92,7 @@ const filter = {
return !parentMeta?.trash && pageMeta.trash;
},
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
export const PageList: React.FC<PageListProps> = ({
@@ -108,6 +109,7 @@ export const PageList: React.FC<PageListProps> = ({
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const isTrash = listType === 'trash';
const isShared = listType === 'shared';
const record = useAtomValue(workspacePreferredModeAtom);
const list = useMemo(
() =>
@@ -130,7 +132,11 @@ export const PageList: React.FC<PageListProps> = ({
<TableCell proportion={0.5}>{t('Title')}</TableCell>
<TableCell proportion={0.2}>{t('Created')}</TableCell>
<TableCell proportion={0.2}>
{isTrash ? t('Moved to Trash') : t('Updated')}
{isTrash
? t('Moved to Trash')
: isShared
? 'Shared'
: t('Updated')}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</>

View File

@@ -0,0 +1,39 @@
import { displayFlex, styled, TextButton } from '@affine/component';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { useRouterHelper } from '../../../../hooks/use-router-helper';
export const EditPage = () => {
const router = useRouter();
const pageId = router.query.pageId as string;
const workspaceId = router.query.workspaceId as string;
const { jumpToPage } = useRouterHelper(router);
const onClickPage = useCallback(() => {
if (workspaceId && pageId) {
jumpToPage(workspaceId, pageId);
}
}, [jumpToPage, pageId, workspaceId]);
return (
<div>
<StyledEditPageButton onClick={() => onClickPage()}>
Edit Page
</StyledEditPageButton>
</div>
);
};
export default EditPage;
const StyledEditPageButton = styled(
TextButton,
{}
)(({ theme }) => {
return {
border: `1px solid ${theme.colors.primaryColor}`,
color: theme.colors.primaryColor,
width: '100%',
borderRadius: '8px',
whiteSpace: 'nowrap',
padding: '0 16px',
...displayFlex('center', 'center'),
};
});

View File

@@ -0,0 +1,116 @@
import { ShareMenu } from '@affine/component/share-menu';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { Page } from '@blocksuite/store';
import { assertEquals } from '@blocksuite/store';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-workspace-publish';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import { useRouterHelper } from '../../../../hooks/use-router-helper';
import { WorkspaceSubPath } from '../../../../shared';
import { Unreachable } from '../../../affine/affine-error-eoundary';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
import type { BaseHeaderProps } from '../header';
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level
const togglePublish = useToggleWorkspacePublish(
props.workspace as AffineWorkspace
);
const helper = useRouterHelper(useRouter());
return (
<ShareMenu
workspace={props.workspace as AffineWorkspace}
currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(async () => {
throw new Unreachable(
'Affine workspace should not enable affine cloud again'
);
}, [])}
onOpenWorkspaceSettings={useCallback(
async workspace => {
return helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper]
)}
togglePagePublic={useCallback(async (page, isPublic) => {
page.workspace.setPageMeta(page.id, { isPublic });
}, [])}
toggleWorkspacePublish={useCallback(
async (workspace, publish) => {
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE);
assertEquals(workspace.id, props.workspace.id);
await togglePublish(publish);
},
[props.workspace.id, togglePublish]
)}
/>
);
};
const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level
const onTransformWorkspace = useOnTransformWorkspace();
const helper = useRouterHelper(useRouter());
const [open, setOpen] = useState(false);
return (
<>
<ShareMenu
workspace={props.workspace as LocalWorkspace}
currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(
async workspace => {
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
assertEquals(workspace.id, props.workspace.id);
setOpen(true);
},
[props.workspace.id]
)}
onOpenWorkspaceSettings={useCallback(
async workspace => {
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper]
)}
togglePagePublic={useCallback(async (page, isPublic) => {
// local workspace should not have public page
throw new Error('unreachable');
}, [])}
toggleWorkspacePublish={useCallback(
async (workspace, publish) => {
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
assertEquals(workspace.id, props.workspace.id);
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper, props.workspace.id]
)}
/>
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={() => {
onTransformWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
props.workspace as LocalWorkspace
);
setOpen(false);
}}
/>
</>
);
};
export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
return <AffineHeaderShareMenu {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <LocalHeaderShareMenu {...props} />;
}
throw new Error('unreachable');
};

View File

@@ -0,0 +1,101 @@
import { Menu, MenuItem } from '@affine/component';
import { AffineIcon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import { useCurrentUser } from '../../../../hooks/current/use-current-user';
const EditMenu = (
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
Sign Out
</MenuItem>
);
export const UserAvatar = () => {
const user = useCurrentUser();
return (
<Menu
width={276}
content={EditMenu}
placement="bottom-end"
disablePortal={true}
trigger="click"
>
{user ? (
<WorkspaceAvatar
size={24}
name={user.name}
avatar={user.avatar_url}
></WorkspaceAvatar>
) : (
<WorkspaceAvatar size={24}></WorkspaceAvatar>
)}
</Menu>
);
};
interface WorkspaceAvatarProps {
size: number;
name?: string;
avatar?: string;
style?: CSSProperties;
}
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
function WorkspaceAvatar(props, ref) {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
{props.name ? (
props.name.substring(0, 1)
) : (
<AffineIcon fontSize={24} color={'#5438FF'} />
)}
</div>
)}
</>
);
}
);
export default UserAvatar;

View File

@@ -1,8 +1,8 @@
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import type React from 'react';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useEffect, useMemo, useState } from 'react';
import {
@@ -12,9 +12,12 @@ import {
import type { AffineOfficialWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import EditPage from './header-right-items/EditPage';
import { HeaderShareMenu } from './header-right-items/ShareMenu';
import SyncUser from './header-right-items/SyncUser';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import UserAvatar from './header-right-items/UserAvatar';
import {
StyledBrowserWarning,
StyledCloseButton,
@@ -56,10 +59,12 @@ export const enum HeaderRightItemName {
ThemeModeSwitch = 'themeModeSwitch',
SyncUser = 'syncUser',
ShareMenu = 'shareMenu',
EditPage = 'editPage',
UserAvatar = 'userAvatar',
}
type HeaderItem = {
Component: React.FC<BaseHeaderProps>;
Component: FC<BaseHeaderProps>;
// todo: public workspace should be one of the flavour
availableWhen: (
workspace: AffineOfficialWorkspace,
@@ -70,7 +75,6 @@ type HeaderItem = {
}
) => boolean;
};
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.TrashButtonGroup]: {
Component: TrashButtonGroup,
@@ -90,18 +94,30 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
return currentPage?.meta.trash !== true;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: HeaderShareMenu,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return workspace.flavour !== WorkspaceFlavour.PUBLIC && !!currentPage;
},
},
[HeaderRightItemName.EditPage]: {
Component: EditPage,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return isPublic;
},
},
[HeaderRightItemName.UserAvatar]: {
Component: UserAvatar,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return isPublic;
},
},
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return !!currentPage && !isPublic && !isPreview;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: () => null,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return false;
},
},
};
export type HeaderProps = BaseHeaderProps;

View File

@@ -6,6 +6,7 @@ import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import type React from 'react';
import { forwardRef } from 'react';
import { stringToColour } from '../../../utils';
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles';
@@ -74,54 +75,58 @@ interface WorkspaceAvatarProps {
style?: CSSProperties;
}
export const WorkspaceAvatar: React.FC<WorkspaceAvatarProps> = props => {
const size = props.size || 20;
const sizeStr = size + 'px';
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
function WorkspaceAvatar(props, ref) {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(props.name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
)}
</>
);
};
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(props.name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
)}
</>
);
}
);

View File

@@ -1,14 +1,17 @@
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
FolderIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
} from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store';
import type React from 'react';
import type { UIEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { usePageMeta } from '../../../hooks/use-page-meta';
@@ -57,6 +60,7 @@ export type WorkSpaceSliderBarProps = {
favorite: (workspaceId: string) => string;
trash: (workspaceId: string) => string;
setting: (workspaceId: string) => string;
shared: (workspaceId: string) => string;
};
};
@@ -174,7 +178,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={e => {
onScroll={(e: UIEvent<HTMLDivElement>) => {
(e.target as HTMLDivElement).scrollTop === 0
? setIsScrollAtTop(true)
: setIsScrollAtTop(false);
@@ -196,6 +200,37 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
)}
</StyledScrollWrapper>
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="Published-to-web">Published to web</span>
</StyledLink>
</StyledListItem>
) : (
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.shared(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.shared(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t('Shared Pages')}</span>
</StyledLink>
</StyledListItem>
)}
<StyledListItem
active={
currentPath ===

View File

@@ -18,6 +18,7 @@ beforeAll(() => {
'/workspace/[workspaceId]/favorite',
'/workspace/[workspaceId]/trash',
'/workspace/[workspaceId]/setting',
'/workspace/[workspaceId]/shared',
])
);
});

View File

@@ -0,0 +1,58 @@
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
getLoginStorage,
parseIdToken,
setLoginStorage,
SignMethod,
storageChangeSlot,
} from '@affine/workspace/affine/login';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { affineAuth } from '../../plugins/affine';
import { useTransformWorkspace } from '../use-transform-workspace';
export function useOnTransformWorkspace() {
const transformWorkspace = useTransformWorkspace();
const setUser = useSetAtom(currentAffineUserAtom);
const router = useRouter();
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
): Promise<void> => {
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
if (needRefresh) {
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
setUser(parseIdToken(response.token));
storageChangeSlot.emit();
}
}
const workspaceId = await transformWorkspace(from, to, workspace);
await router.replace({
pathname: `/workspace/[workspaceId]/setting`,
query: {
...router.query,
workspaceId,
},
});
window.dispatchEvent(
new CustomEvent('affine-workspace:transform', {
detail: {
from,
to,
oldId: workspace.id,
newId: workspaceId,
},
})
);
},
[router, setUser, transformWorkspace]
);
}

View File

@@ -55,7 +55,8 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
path[3] === 'all' ||
path[3] === 'setting' ||
path[3] === 'trash' ||
path[3] === 'favorite'
path[3] === 'favorite' ||
path[3] === 'shared'
) {
return;
}

View File

@@ -1,5 +1,5 @@
import { ListSkeleton } from '@affine/component';
import { useAtomValue } from 'jotai';
import type { AffinePublicWorkspace } from '@affine/workspace/type';
import { useAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
@@ -7,10 +7,6 @@ import type React from 'react';
import { lazy, Suspense } from 'react';
import { openQuickSearchModalAtom } from '../atoms';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
} from '../atoms/public-workspace';
import { StyledTableContainer } from '../components/blocksuite/block-suite-page-list/page-list/styles';
import { useRouterTitle } from '../hooks/use-router-title';
import { MainContainer, StyledPage } from './styles';
@@ -21,8 +17,13 @@ const QuickSearchModal = lazy(() =>
}))
);
export const PublicQuickSearch: React.FC = () => {
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
type PublicQuickSearchProps = {
workspace: AffinePublicWorkspace;
};
export const PublicQuickSearch: React.FC<PublicQuickSearchProps> = ({
workspace,
}) => {
const router = useRouter();
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
openQuickSearchModalAtom
@@ -30,7 +31,7 @@ export const PublicQuickSearch: React.FC = () => {
return (
<Suspense>
<QuickSearchModal
blockSuiteWorkspace={publicWorkspace.blockSuiteWorkspace}
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
open={openQuickSearchModal}
setOpen={setOpenQuickSearchModalAtom}
router={router}
@@ -42,7 +43,6 @@ export const PublicQuickSearch: React.FC = () => {
const PublicWorkspaceLayoutInner: React.FC<React.PropsWithChildren> = props => {
const router = useRouter();
const title = useRouterTitle(router);
const workspaceId = useAtomValue(publicWorkspaceIdAtom);
return (
<>
<Head>
@@ -52,10 +52,6 @@ const PublicWorkspaceLayoutInner: React.FC<React.PropsWithChildren> = props => {
<MainContainer className="main-container">
{props.children}
</MainContainer>
<Suspense fallback="">
{/* `publicBlockSuiteAtom` is available only when `publicWorkspaceIdAtom` loaded */}
{workspaceId && <PublicQuickSearch />}
</Suspense>
</StyledPage>
</>
);

View File

@@ -16,7 +16,10 @@ import { QueryParamError } from '../../components/affine/affine-error-eoundary';
import { StyledTableContainer } from '../../components/blocksuite/block-suite-page-list/page-list/styles';
import { WorkspaceAvatar } from '../../components/pure/footer';
import { PageLoading } from '../../components/pure/loading';
import { PublicWorkspaceLayout } from '../../layouts/public-workspace-layout';
import {
PublicQuickSearch,
PublicWorkspaceLayout,
} from '../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../shared';
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
@@ -58,6 +61,7 @@ const ListPageInner: React.FC<{
}
return (
<>
<PublicQuickSearch workspace={publicWorkspace} />
<NavContainer sx={{ px: '20px' }}>
<Breadcrumbs>
<StyledBreadcrumbs

View File

@@ -21,7 +21,10 @@ import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { PublicWorkspaceLayout } from '../../../layouts/public-workspace-layout';
import {
PublicQuickSearch,
PublicWorkspaceLayout,
} from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
import { initPage } from '../../../utils';
@@ -62,10 +65,6 @@ const PublicWorkspaceDetailPageInner: React.FC<{
}
const router = useRouter();
const { openPage } = useRouterHelper(router);
useEffect(() => {
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
}, [blockSuiteWorkspace]);
useReferenceLink({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
@@ -81,6 +80,7 @@ const PublicWorkspaceDetailPageInner: React.FC<{
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
return (
<>
<PublicQuickSearch workspace={publicWorkspace} />
<PageDetailEditor
isPublic={true}
pageId={pageId}

View File

@@ -1,13 +1,6 @@
import { useTranslation } from '@affine/i18n';
import { atomWithSyncStorage } from '@affine/jotai';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
getLoginStorage,
parseIdToken,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type';
import type { SettingPanel } from '@affine/workspace/type';
import {
settingPanel,
settingPanelValues,
@@ -15,7 +8,7 @@ import {
} from '@affine/workspace/type';
import { SettingsIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useAtom, useSetAtom } from 'jotai';
import { useAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
@@ -24,12 +17,11 @@ import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { useTransformWorkspace } from '../../../hooks/use-transform-workspace';
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins';
import { affineAuth } from '../../../plugins/affine';
import type { NextPageWithLayout } from '../../../shared';
const settingPanelAtom = atomWithSyncStorage<SettingPanel>(
@@ -107,33 +99,7 @@ const SettingPage: NextPageWithLayout = () => {
const workspaceId = currentWorkspace.id;
return helper.deleteWorkspace(workspaceId);
}, [currentWorkspace, helper]);
const transformWorkspace = useTransformWorkspace();
const setUser = useSetAtom(currentAffineUserAtom);
const onTransformWorkspace = useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
): Promise<void> => {
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
if (needRefresh) {
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
setUser(parseIdToken(response.token));
}
}
const workspaceId = await transformWorkspace(from, to, workspace);
await router.replace({
pathname: `/workspace/[workspaceId]/setting`,
query: {
...router.query,
workspaceId,
},
});
},
[router, setUser, transformWorkspace]
);
const onTransformWorkspace = useOnTransformWorkspace();
if (!router.isReady) {
return <PageLoading />;
} else if (currentWorkspace === null) {

View File

@@ -0,0 +1,66 @@
import { useTranslation } from '@affine/i18n';
import { ShareIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import PageList from '../../../components/blocksuite/block-suite-page-list/page-list';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import type { NextPageWithLayout } from '../../../shared';
const SharedPages: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);
if (newTab) {
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
} else {
jumpToPage(currentWorkspace.id, pageId);
}
},
[currentWorkspace, jumpToPage]
);
if (currentWorkspace === null) {
return <PageLoading />;
}
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
return (
<>
<Head>
<title>{t('Shared Pages')} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
currentPage={null}
isPreview={false}
isPublic={false}
icon={<ShareIcon />}
>
{t('Shared Pages')}
</WorkspaceTitle>
<PageList
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onClickPage}
listType="shared"
/>
</>
);
};
export default SharedPages;
SharedPages.getLayout = page => {
return <WorkspaceLayout>{page}</WorkspaceLayout>;
};

View File

@@ -1,33 +0,0 @@
// server.js
import { createServer } from 'http';
import next from 'next';
import { parse } from 'url';
const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = 8080;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer(async (req, res) => {
try {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
})
.once('error', err => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});

View File

@@ -25,6 +25,7 @@ export const enum WorkspaceSubPath {
FAVORITE = 'favorite',
SETTING = 'setting',
TRASH = 'trash',
SHARED = 'shared',
}
export const WorkspaceSubPathName = {
@@ -32,6 +33,7 @@ export const WorkspaceSubPathName = {
[WorkspaceSubPath.FAVORITE]: 'Favorites',
[WorkspaceSubPath.SETTING]: 'Settings',
[WorkspaceSubPath.TRASH]: 'Trash',
[WorkspaceSubPath.SHARED]: 'Shared',
} satisfies {
[Path in WorkspaceSubPath]: string;
};
@@ -41,6 +43,7 @@ export const pathGenerator = {
favorite: workspaceId => `/workspace/${workspaceId}/favorite`,
trash: workspaceId => `/workspace/${workspaceId}/trash`,
setting: workspaceId => `/workspace/${workspaceId}/setting`,
shared: workspaceId => `/workspace/${workspaceId}/shared`,
} satisfies {
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
};
@@ -50,6 +53,7 @@ export const publicPathGenerator = {
favorite: workspaceId => `/public-workspace/${workspaceId}/favorite`,
trash: workspaceId => `/public-workspace/${workspaceId}/trash`,
setting: workspaceId => `/public-workspace/${workspaceId}/setting`,
shared: workspaceId => `/public-workspace/${workspaceId}/shared`,
} satisfies {
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
};