mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
47
.github/workflows/build-master.yml
vendored
47
.github/workflows/build-master.yml
vendored
@@ -123,6 +123,47 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
build-frontend-dev:
|
||||||
|
name: Build @affine/web dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: development
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
- name: Cache Next.js
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ github.workspace }}/apps/web/.next/cache
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||||
|
API_SERVER_PROFILE: local
|
||||||
|
ENABLE_DEBUG_PAGE: true
|
||||||
|
|
||||||
|
- name: Export
|
||||||
|
run: yarn export
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: next-js-dev
|
||||||
|
path: ./apps/web/out
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
storybook-test:
|
storybook-test:
|
||||||
name: Storybook Test
|
name: Storybook Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -160,7 +201,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4]
|
shard: [1, 2, 3, 4]
|
||||||
environment: development
|
environment: development
|
||||||
needs: [build-frontend, build-storybook]
|
needs: [build-frontend-dev, build-storybook]
|
||||||
services:
|
services:
|
||||||
octobase:
|
octobase:
|
||||||
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
|
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
|
||||||
@@ -183,7 +224,7 @@ jobs:
|
|||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: next-js
|
name: next-js-dev
|
||||||
path: ./apps/web/.next
|
path: ./apps/web/.next
|
||||||
|
|
||||||
- name: Download storybook artifact
|
- name: Download storybook artifact
|
||||||
@@ -213,7 +254,7 @@ jobs:
|
|||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e
|
name: test-results-e2e-${{ matrix.shard }}
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -73,6 +73,8 @@ jobs:
|
|||||||
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||||
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||||
|
API_SERVER_PROFILE: local
|
||||||
|
ENABLE_DEBUG_PAGE: true
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -168,7 +170,7 @@ jobs:
|
|||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e
|
name: test-results-e2e-${{ matrix.shard }}
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,10 @@
|
|||||||
"name": "@affine/web",
|
"name": "@affine/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node src/server.mjs",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"export": "next export",
|
"export": "next export",
|
||||||
"start": "NODE_ENV=production node src/server.mjs",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17,18 +17,18 @@
|
|||||||
"@affine/jotai": "workspace:*",
|
"@affine/jotai": "workspace:*",
|
||||||
"@affine/templates": "workspace:*",
|
"@affine/templates": "workspace:*",
|
||||||
"@affine/workspace": "workspace:*",
|
"@affine/workspace": "workspace:*",
|
||||||
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/editor": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/icons": "^2.1.9",
|
"@blocksuite/icons": "^2.1.10",
|
||||||
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^7.0.2",
|
||||||
"@emotion/cache": "^11.10.7",
|
"@emotion/cache": "^11.10.7",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@mui/material": "^5.11.16",
|
"@mui/material": "^5.12.0",
|
||||||
"@sentry/nextjs": "^7.47.0",
|
"@sentry/nextjs": "^7.47.0",
|
||||||
"@toeverything/hooks": "workspace:*",
|
"@toeverything/hooks": "workspace:*",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/webpack-env": "^1.18.0",
|
"@types/webpack-env": "^1.18.0",
|
||||||
"@vanilla-extract/css": "^1.11.0",
|
"@vanilla-extract/css": "^1.11.0",
|
||||||
"@vanilla-extract/next-plugin": "^2.1.1",
|
"@vanilla-extract/next-plugin": "^2.1.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-config-next": "^13.3.0",
|
"eslint-config-next": "^13.3.0",
|
||||||
@@ -68,6 +68,6 @@
|
|||||||
"redux": "^4.2.1",
|
"redux": "^4.2.1",
|
||||||
"swc-plugin-coverage-instrument": "=0.0.14",
|
"swc-plugin-coverage-instrument": "=0.0.14",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"webpack": "^5.78.0"
|
"webpack": "^5.79.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const config = {
|
|||||||
enableDebugPage: Boolean(
|
enableDebugPage: Boolean(
|
||||||
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
|
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
|
||||||
),
|
),
|
||||||
enableSubpage: Boolean(process.env.ENABLE_SUBPAGE),
|
enableSubpage: Boolean(process.env.ENABLE_SUBPAGE ?? '1'),
|
||||||
enableChangeLog: Boolean(process.env.ENABLE_CHANGELOG),
|
enableChangeLog: Boolean(process.env.ENABLE_CHANGELOG ?? '1'),
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export class AffineErrorBoundary extends Component<
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Sorry.. there was an error</h1>
|
<h1>Sorry.. there was an error</h1>
|
||||||
|
{error.message ?? error.toString()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './CopyLink';
|
export * from './CopyLink';
|
||||||
|
export * from './DisablePublicSharing';
|
||||||
export * from './Export';
|
export * from './Export';
|
||||||
export * from './MoveTo';
|
export * from './MoveTo';
|
||||||
export * from './MoveToTrash';
|
export * from './MoveToTrash';
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ export const TransformWorkspaceToAffineModal: React.FC<
|
|||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
|
|
||||||
return (
|
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}>
|
<ModalWrapper width={560} height={292}>
|
||||||
<Header>
|
<Header>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import {
|
|||||||
ResetIcon,
|
ResetIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
|
import { assertExists } from '@blocksuite/store';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||||
import { toast } from '../../../../utils';
|
import { toast } from '../../../../utils';
|
||||||
import { MoveTo, MoveToTrash } from '../../../affine/operation-menu-items';
|
import {
|
||||||
|
DisablePublicSharing,
|
||||||
|
MoveTo,
|
||||||
|
MoveToTrash,
|
||||||
|
} from '../../../affine/operation-menu-items';
|
||||||
|
|
||||||
export type OperationCellProps = {
|
export type OperationCellProps = {
|
||||||
pageMeta: PageMeta;
|
pageMeta: PageMeta;
|
||||||
@@ -40,12 +45,24 @@ export const OperationCell: React.FC<OperationCellProps> = ({
|
|||||||
onToggleFavoritePage,
|
onToggleFavoritePage,
|
||||||
onToggleTrashPage,
|
onToggleTrashPage,
|
||||||
}) => {
|
}) => {
|
||||||
const { id, favorite } = pageMeta;
|
const { id, favorite, isPublic } = pageMeta;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||||
|
|
||||||
|
const page = blockSuiteWorkspace.getPage(id);
|
||||||
|
assertExists(page);
|
||||||
|
|
||||||
const OperationMenu = (
|
const OperationMenu = (
|
||||||
<>
|
<>
|
||||||
|
{isPublic && (
|
||||||
|
<DisablePublicSharing
|
||||||
|
testId="disable-public-sharing"
|
||||||
|
onItemClick={() => {
|
||||||
|
setOpenDisableShared(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onToggleFavoritePage(id);
|
onToggleFavoritePage(id);
|
||||||
@@ -111,6 +128,13 @@ export const OperationCell: React.FC<OperationCellProps> = ({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<DisablePublicSharing.DisablePublicSharingModal
|
||||||
|
page={page}
|
||||||
|
open={openDisableShared}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenDisableShared(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const FavoriteTag: React.FC<FavoriteTagProps> = ({
|
|||||||
type PageListProps = {
|
type PageListProps = {
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
listType?: 'all' | 'trash' | 'favorite';
|
listType?: 'all' | 'trash' | 'favorite' | 'shared';
|
||||||
onClickPage: (pageId: string, newTab?: boolean) => void;
|
onClickPage: (pageId: string, newTab?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ const filter = {
|
|||||||
return !parentMeta?.trash && pageMeta.trash;
|
return !parentMeta?.trash && pageMeta.trash;
|
||||||
},
|
},
|
||||||
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
|
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
|
||||||
|
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageList: React.FC<PageListProps> = ({
|
export const PageList: React.FC<PageListProps> = ({
|
||||||
@@ -108,6 +109,7 @@ export const PageList: React.FC<PageListProps> = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||||
const isTrash = listType === 'trash';
|
const isTrash = listType === 'trash';
|
||||||
|
const isShared = listType === 'shared';
|
||||||
const record = useAtomValue(workspacePreferredModeAtom);
|
const record = useAtomValue(workspacePreferredModeAtom);
|
||||||
const list = useMemo(
|
const list = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -130,7 +132,11 @@ export const PageList: React.FC<PageListProps> = ({
|
|||||||
<TableCell proportion={0.5}>{t('Title')}</TableCell>
|
<TableCell proportion={0.5}>{t('Title')}</TableCell>
|
||||||
<TableCell proportion={0.2}>{t('Created')}</TableCell>
|
<TableCell proportion={0.2}>{t('Created')}</TableCell>
|
||||||
<TableCell proportion={0.2}>
|
<TableCell proportion={0.2}>
|
||||||
{isTrash ? t('Moved to Trash') : t('Updated')}
|
{isTrash
|
||||||
|
? t('Moved to Trash')
|
||||||
|
: isShared
|
||||||
|
? 'Shared'
|
||||||
|
: t('Updated')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell proportion={0.1}></TableCell>
|
<TableCell proportion={0.1}></TableCell>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { CloseIcon } from '@blocksuite/icons';
|
import { CloseIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
import type React from 'react';
|
|
||||||
import { forwardRef, useEffect, useMemo, useState } from 'react';
|
import { forwardRef, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,9 +12,12 @@ import {
|
|||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||||
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
||||||
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
|
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 SyncUser from './header-right-items/SyncUser';
|
||||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||||
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
|
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
|
||||||
|
import UserAvatar from './header-right-items/UserAvatar';
|
||||||
import {
|
import {
|
||||||
StyledBrowserWarning,
|
StyledBrowserWarning,
|
||||||
StyledCloseButton,
|
StyledCloseButton,
|
||||||
@@ -56,10 +59,12 @@ export const enum HeaderRightItemName {
|
|||||||
ThemeModeSwitch = 'themeModeSwitch',
|
ThemeModeSwitch = 'themeModeSwitch',
|
||||||
SyncUser = 'syncUser',
|
SyncUser = 'syncUser',
|
||||||
ShareMenu = 'shareMenu',
|
ShareMenu = 'shareMenu',
|
||||||
|
EditPage = 'editPage',
|
||||||
|
UserAvatar = 'userAvatar',
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderItem = {
|
type HeaderItem = {
|
||||||
Component: React.FC<BaseHeaderProps>;
|
Component: FC<BaseHeaderProps>;
|
||||||
// todo: public workspace should be one of the flavour
|
// todo: public workspace should be one of the flavour
|
||||||
availableWhen: (
|
availableWhen: (
|
||||||
workspace: AffineOfficialWorkspace,
|
workspace: AffineOfficialWorkspace,
|
||||||
@@ -70,7 +75,6 @@ type HeaderItem = {
|
|||||||
}
|
}
|
||||||
) => boolean;
|
) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||||
[HeaderRightItemName.TrashButtonGroup]: {
|
[HeaderRightItemName.TrashButtonGroup]: {
|
||||||
Component: TrashButtonGroup,
|
Component: TrashButtonGroup,
|
||||||
@@ -90,18 +94,30 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
|||||||
return currentPage?.meta.trash !== true;
|
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]: {
|
[HeaderRightItemName.EditorOptionMenu]: {
|
||||||
Component: EditorOptionMenu,
|
Component: EditorOptionMenu,
|
||||||
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
|
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
|
||||||
return !!currentPage && !isPublic && !isPreview;
|
return !!currentPage && !isPublic && !isPreview;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[HeaderRightItemName.ShareMenu]: {
|
|
||||||
Component: () => null,
|
|
||||||
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeaderProps = BaseHeaderProps;
|
export type HeaderProps = BaseHeaderProps;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
|||||||
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import { stringToColour } from '../../../utils';
|
import { stringToColour } from '../../../utils';
|
||||||
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles';
|
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles';
|
||||||
@@ -74,54 +75,58 @@ interface WorkspaceAvatarProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceAvatar: React.FC<WorkspaceAvatarProps> = props => {
|
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||||
const size = props.size || 20;
|
function WorkspaceAvatar(props, ref) {
|
||||||
const sizeStr = size + 'px';
|
const size = props.size || 20;
|
||||||
|
const sizeStr = size + 'px';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.avatar ? (
|
{props.avatar ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...props.style,
|
...props.style,
|
||||||
width: sizeStr,
|
width: sizeStr,
|
||||||
height: sizeStr,
|
height: sizeStr,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
}}
|
}}
|
||||||
>
|
ref={ref}
|
||||||
<picture>
|
>
|
||||||
<img
|
<picture>
|
||||||
style={{ width: sizeStr, height: sizeStr }}
|
<img
|
||||||
src={props.avatar}
|
style={{ width: sizeStr, height: sizeStr }}
|
||||||
alt=""
|
src={props.avatar}
|
||||||
referrerPolicy="no-referrer"
|
alt=""
|
||||||
/>
|
referrerPolicy="no-referrer"
|
||||||
</picture>
|
/>
|
||||||
</div>
|
</picture>
|
||||||
) : (
|
</div>
|
||||||
<div
|
) : (
|
||||||
style={{
|
<div
|
||||||
...props.style,
|
style={{
|
||||||
width: sizeStr,
|
...props.style,
|
||||||
height: sizeStr,
|
width: sizeStr,
|
||||||
border: '1px solid #fff',
|
height: sizeStr,
|
||||||
color: '#fff',
|
border: '1px solid #fff',
|
||||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
color: '#fff',
|
||||||
background: stringToColour(props.name || 'AFFiNE'),
|
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||||
borderRadius: '50%',
|
background: stringToColour(props.name || 'AFFiNE'),
|
||||||
textAlign: 'center',
|
borderRadius: '50%',
|
||||||
lineHeight: size + 'px',
|
textAlign: 'center',
|
||||||
display: 'inline-block',
|
lineHeight: size + 'px',
|
||||||
verticalAlign: 'middle',
|
display: 'inline-block',
|
||||||
}}
|
verticalAlign: 'middle',
|
||||||
>
|
}}
|
||||||
{(props.name || 'AFFiNE').substring(0, 1)}
|
ref={ref}
|
||||||
</div>
|
>
|
||||||
)}
|
{(props.name || 'AFFiNE').substring(0, 1)}
|
||||||
</>
|
</div>
|
||||||
);
|
)}
|
||||||
};
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
|
ShareIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { Page, PageMeta } from '@blocksuite/store';
|
import type { Page, PageMeta } from '@blocksuite/store';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import type { UIEvent } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { usePageMeta } from '../../../hooks/use-page-meta';
|
import { usePageMeta } from '../../../hooks/use-page-meta';
|
||||||
@@ -57,6 +60,7 @@ export type WorkSpaceSliderBarProps = {
|
|||||||
favorite: (workspaceId: string) => string;
|
favorite: (workspaceId: string) => string;
|
||||||
trash: (workspaceId: string) => string;
|
trash: (workspaceId: string) => string;
|
||||||
setting: (workspaceId: string) => string;
|
setting: (workspaceId: string) => string;
|
||||||
|
shared: (workspaceId: string) => string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +178,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
|||||||
|
|
||||||
<StyledScrollWrapper
|
<StyledScrollWrapper
|
||||||
showTopBorder={!isScrollAtTop}
|
showTopBorder={!isScrollAtTop}
|
||||||
onScroll={e => {
|
onScroll={(e: UIEvent<HTMLDivElement>) => {
|
||||||
(e.target as HTMLDivElement).scrollTop === 0
|
(e.target as HTMLDivElement).scrollTop === 0
|
||||||
? setIsScrollAtTop(true)
|
? setIsScrollAtTop(true)
|
||||||
: setIsScrollAtTop(false);
|
: setIsScrollAtTop(false);
|
||||||
@@ -196,6 +200,37 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
|||||||
)}
|
)}
|
||||||
</StyledScrollWrapper>
|
</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
|
<StyledListItem
|
||||||
active={
|
active={
|
||||||
currentPath ===
|
currentPath ===
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ beforeAll(() => {
|
|||||||
'/workspace/[workspaceId]/favorite',
|
'/workspace/[workspaceId]/favorite',
|
||||||
'/workspace/[workspaceId]/trash',
|
'/workspace/[workspaceId]/trash',
|
||||||
'/workspace/[workspaceId]/setting',
|
'/workspace/[workspaceId]/setting',
|
||||||
|
'/workspace/[workspaceId]/shared',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
58
apps/web/src/hooks/root/use-on-transform-workspace.ts
Normal file
58
apps/web/src/hooks/root/use-on-transform-workspace.ts
Normal 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]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,7 +55,8 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
|||||||
path[3] === 'all' ||
|
path[3] === 'all' ||
|
||||||
path[3] === 'setting' ||
|
path[3] === 'setting' ||
|
||||||
path[3] === 'trash' ||
|
path[3] === 'trash' ||
|
||||||
path[3] === 'favorite'
|
path[3] === 'favorite' ||
|
||||||
|
path[3] === 'shared'
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ListSkeleton } from '@affine/component';
|
import { ListSkeleton } from '@affine/component';
|
||||||
import { useAtomValue } from 'jotai';
|
import type { AffinePublicWorkspace } from '@affine/workspace/type';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -7,10 +7,6 @@ import type React from 'react';
|
|||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
import { openQuickSearchModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom } from '../atoms';
|
||||||
import {
|
|
||||||
publicWorkspaceAtom,
|
|
||||||
publicWorkspaceIdAtom,
|
|
||||||
} from '../atoms/public-workspace';
|
|
||||||
import { StyledTableContainer } from '../components/blocksuite/block-suite-page-list/page-list/styles';
|
import { StyledTableContainer } from '../components/blocksuite/block-suite-page-list/page-list/styles';
|
||||||
import { useRouterTitle } from '../hooks/use-router-title';
|
import { useRouterTitle } from '../hooks/use-router-title';
|
||||||
import { MainContainer, StyledPage } from './styles';
|
import { MainContainer, StyledPage } from './styles';
|
||||||
@@ -21,8 +17,13 @@ const QuickSearchModal = lazy(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PublicQuickSearch: React.FC = () => {
|
type PublicQuickSearchProps = {
|
||||||
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
|
workspace: AffinePublicWorkspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PublicQuickSearch: React.FC<PublicQuickSearchProps> = ({
|
||||||
|
workspace,
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
||||||
openQuickSearchModalAtom
|
openQuickSearchModalAtom
|
||||||
@@ -30,7 +31,7 @@ export const PublicQuickSearch: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<QuickSearchModal
|
<QuickSearchModal
|
||||||
blockSuiteWorkspace={publicWorkspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||||
open={openQuickSearchModal}
|
open={openQuickSearchModal}
|
||||||
setOpen={setOpenQuickSearchModalAtom}
|
setOpen={setOpenQuickSearchModalAtom}
|
||||||
router={router}
|
router={router}
|
||||||
@@ -42,7 +43,6 @@ export const PublicQuickSearch: React.FC = () => {
|
|||||||
const PublicWorkspaceLayoutInner: React.FC<React.PropsWithChildren> = props => {
|
const PublicWorkspaceLayoutInner: React.FC<React.PropsWithChildren> = props => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const title = useRouterTitle(router);
|
const title = useRouterTitle(router);
|
||||||
const workspaceId = useAtomValue(publicWorkspaceIdAtom);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -52,10 +52,6 @@ const PublicWorkspaceLayoutInner: React.FC<React.PropsWithChildren> = props => {
|
|||||||
<MainContainer className="main-container">
|
<MainContainer className="main-container">
|
||||||
{props.children}
|
{props.children}
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<Suspense fallback="">
|
|
||||||
{/* `publicBlockSuiteAtom` is available only when `publicWorkspaceIdAtom` loaded */}
|
|
||||||
{workspaceId && <PublicQuickSearch />}
|
|
||||||
</Suspense>
|
|
||||||
</StyledPage>
|
</StyledPage>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { StyledTableContainer } from '../../components/blocksuite/block-suite-page-list/page-list/styles';
|
||||||
import { WorkspaceAvatar } from '../../components/pure/footer';
|
import { WorkspaceAvatar } from '../../components/pure/footer';
|
||||||
import { PageLoading } from '../../components/pure/loading';
|
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 type { NextPageWithLayout } from '../../shared';
|
||||||
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
|
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ const ListPageInner: React.FC<{
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PublicQuickSearch workspace={publicWorkspace} />
|
||||||
<NavContainer sx={{ px: '20px' }}>
|
<NavContainer sx={{ px: '20px' }}>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<StyledBreadcrumbs
|
<StyledBreadcrumbs
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import { WorkspaceAvatar } from '../../../components/pure/footer';
|
|||||||
import { PageLoading } from '../../../components/pure/loading';
|
import { PageLoading } from '../../../components/pure/loading';
|
||||||
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
|
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
|
||||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
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 type { NextPageWithLayout } from '../../../shared';
|
||||||
import { initPage } from '../../../utils';
|
import { initPage } from '../../../utils';
|
||||||
|
|
||||||
@@ -62,10 +65,6 @@ const PublicWorkspaceDetailPageInner: React.FC<{
|
|||||||
}
|
}
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { openPage } = useRouterHelper(router);
|
const { openPage } = useRouterHelper(router);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
|
|
||||||
}, [blockSuiteWorkspace]);
|
|
||||||
useReferenceLink({
|
useReferenceLink({
|
||||||
pageLinkClicked: useCallback(
|
pageLinkClicked: useCallback(
|
||||||
({ pageId }: { pageId: string }) => {
|
({ pageId }: { pageId: string }) => {
|
||||||
@@ -81,6 +80,7 @@ const PublicWorkspaceDetailPageInner: React.FC<{
|
|||||||
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
|
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PublicQuickSearch workspace={publicWorkspace} />
|
||||||
<PageDetailEditor
|
<PageDetailEditor
|
||||||
isPublic={true}
|
isPublic={true}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
import { atomWithSyncStorage } from '@affine/jotai';
|
import { atomWithSyncStorage } from '@affine/jotai';
|
||||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
import type { SettingPanel } from '@affine/workspace/type';
|
||||||
import {
|
|
||||||
getLoginStorage,
|
|
||||||
parseIdToken,
|
|
||||||
setLoginStorage,
|
|
||||||
SignMethod,
|
|
||||||
} from '@affine/workspace/affine/login';
|
|
||||||
import type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type';
|
|
||||||
import {
|
import {
|
||||||
settingPanel,
|
settingPanel,
|
||||||
settingPanelValues,
|
settingPanelValues,
|
||||||
@@ -15,7 +8,7 @@ import {
|
|||||||
} from '@affine/workspace/type';
|
} from '@affine/workspace/type';
|
||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
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 { PageLoading } from '../../../components/pure/loading';
|
||||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||||
import { useTransformWorkspace } from '../../../hooks/use-transform-workspace';
|
|
||||||
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
|
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
|
||||||
import { WorkspaceLayout } from '../../../layouts';
|
import { WorkspaceLayout } from '../../../layouts';
|
||||||
import { WorkspacePlugins } from '../../../plugins';
|
import { WorkspacePlugins } from '../../../plugins';
|
||||||
import { affineAuth } from '../../../plugins/affine';
|
|
||||||
import type { NextPageWithLayout } from '../../../shared';
|
import type { NextPageWithLayout } from '../../../shared';
|
||||||
|
|
||||||
const settingPanelAtom = atomWithSyncStorage<SettingPanel>(
|
const settingPanelAtom = atomWithSyncStorage<SettingPanel>(
|
||||||
@@ -107,33 +99,7 @@ const SettingPage: NextPageWithLayout = () => {
|
|||||||
const workspaceId = currentWorkspace.id;
|
const workspaceId = currentWorkspace.id;
|
||||||
return helper.deleteWorkspace(workspaceId);
|
return helper.deleteWorkspace(workspaceId);
|
||||||
}, [currentWorkspace, helper]);
|
}, [currentWorkspace, helper]);
|
||||||
const transformWorkspace = useTransformWorkspace();
|
const onTransformWorkspace = useOnTransformWorkspace();
|
||||||
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]
|
|
||||||
);
|
|
||||||
if (!router.isReady) {
|
if (!router.isReady) {
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
} else if (currentWorkspace === null) {
|
} else if (currentWorkspace === null) {
|
||||||
|
|||||||
66
apps/web/src/pages/workspace/[workspaceId]/shared.tsx
Normal file
66
apps/web/src/pages/workspace/[workspaceId]/shared.tsx
Normal 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>;
|
||||||
|
};
|
||||||
@@ -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}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -25,6 +25,7 @@ export const enum WorkspaceSubPath {
|
|||||||
FAVORITE = 'favorite',
|
FAVORITE = 'favorite',
|
||||||
SETTING = 'setting',
|
SETTING = 'setting',
|
||||||
TRASH = 'trash',
|
TRASH = 'trash',
|
||||||
|
SHARED = 'shared',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceSubPathName = {
|
export const WorkspaceSubPathName = {
|
||||||
@@ -32,6 +33,7 @@ export const WorkspaceSubPathName = {
|
|||||||
[WorkspaceSubPath.FAVORITE]: 'Favorites',
|
[WorkspaceSubPath.FAVORITE]: 'Favorites',
|
||||||
[WorkspaceSubPath.SETTING]: 'Settings',
|
[WorkspaceSubPath.SETTING]: 'Settings',
|
||||||
[WorkspaceSubPath.TRASH]: 'Trash',
|
[WorkspaceSubPath.TRASH]: 'Trash',
|
||||||
|
[WorkspaceSubPath.SHARED]: 'Shared',
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[Path in WorkspaceSubPath]: string;
|
[Path in WorkspaceSubPath]: string;
|
||||||
};
|
};
|
||||||
@@ -41,6 +43,7 @@ export const pathGenerator = {
|
|||||||
favorite: workspaceId => `/workspace/${workspaceId}/favorite`,
|
favorite: workspaceId => `/workspace/${workspaceId}/favorite`,
|
||||||
trash: workspaceId => `/workspace/${workspaceId}/trash`,
|
trash: workspaceId => `/workspace/${workspaceId}/trash`,
|
||||||
setting: workspaceId => `/workspace/${workspaceId}/setting`,
|
setting: workspaceId => `/workspace/${workspaceId}/setting`,
|
||||||
|
shared: workspaceId => `/workspace/${workspaceId}/shared`,
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
|
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
|
||||||
};
|
};
|
||||||
@@ -50,6 +53,7 @@ export const publicPathGenerator = {
|
|||||||
favorite: workspaceId => `/public-workspace/${workspaceId}/favorite`,
|
favorite: workspaceId => `/public-workspace/${workspaceId}/favorite`,
|
||||||
trash: workspaceId => `/public-workspace/${workspaceId}/trash`,
|
trash: workspaceId => `/public-workspace/${workspaceId}/trash`,
|
||||||
setting: workspaceId => `/public-workspace/${workspaceId}/setting`,
|
setting: workspaceId => `/public-workspace/${workspaceId}/setting`,
|
||||||
|
shared: workspaceId => `/public-workspace/${workspaceId}/shared`,
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
|
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
|
||||||
};
|
};
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -38,8 +38,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@affine/cli": "workspace:*",
|
"@affine/cli": "workspace:*",
|
||||||
"@commitlint/cli": "^17.5.1",
|
"@commitlint/cli": "^17.6.0",
|
||||||
"@commitlint/config-conventional": "^17.4.4",
|
"@commitlint/config-conventional": "^17.6.0",
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"@istanbuljs/schema": "^0.1.3",
|
"@istanbuljs/schema": "^0.1.3",
|
||||||
"@perfsee/sdk": "^1.5.2",
|
"@perfsee/sdk": "^1.5.2",
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
"@typescript-eslint/parser": "^5.58.0",
|
"@typescript-eslint/parser": "^5.58.0",
|
||||||
"@vanilla-extract/vite-plugin": "^3.8.0",
|
"@vanilla-extract/vite-plugin": "^3.8.0",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"@vitest/coverage-istanbul": "^0.30.0",
|
"@vitest/coverage-istanbul": "^0.30.1",
|
||||||
"@vitest/ui": "^0.30.0",
|
"@vitest/ui": "^0.30.1",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"fake-indexeddb": "4.0.1",
|
"fake-indexeddb": "4.0.1",
|
||||||
"got": "^12.6.0",
|
"got": "^12.6.0",
|
||||||
"happy-dom": "^9.1.9",
|
"happy-dom": "^9.5.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.1",
|
"lint-staged": "^13.2.1",
|
||||||
"msw": "^1.2.1",
|
"msw": "^1.2.1",
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vite": "^4.2.1",
|
"vite": "^4.2.1",
|
||||||
"vite-plugin-istanbul": "^4.0.1",
|
"vite-plugin-istanbul": "^4.0.1",
|
||||||
"vite-tsconfig-paths": "^4.0.9",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"vitest": "^0.30.0",
|
"vitest": "^0.30.1",
|
||||||
"vitest-fetch-mock": "^0.2.2"
|
"vitest-fetch-mock": "^0.2.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@blocksuite/blocks": "0.0.0-20230409084303-221991d4-nightly",
|
"@blocksuite/blocks": "0.0.0-20230409084303-221991d4-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230409084303-221991d4-nightly",
|
"@blocksuite/editor": "0.0.0-20230409084303-221991d4-nightly",
|
||||||
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly",
|
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly",
|
||||||
"@blocksuite/icons": "2.1.7",
|
"@blocksuite/icons": "2.1.10",
|
||||||
"@blocksuite/store": "0.0.0-20230409084303-221991d4-nightly"
|
"@blocksuite/store": "0.0.0-20230409084303-221991d4-nightly"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@mui/base": "5.0.0-alpha.124",
|
"@mui/base": "5.0.0-alpha.125",
|
||||||
"@mui/icons-material": "^5.11.16",
|
"@mui/icons-material": "^5.11.16",
|
||||||
"@mui/material": "^5.11.16",
|
"@mui/material": "^5.12.0",
|
||||||
"@radix-ui/react-avatar": "^1.0.2",
|
"@radix-ui/react-avatar": "^1.0.2",
|
||||||
"@toeverything/hooks": "workspace:*",
|
"@toeverything/hooks": "workspace:*",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
@@ -48,22 +48,22 @@
|
|||||||
"react-is": "^18.2.0"
|
"react-is": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/editor": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/editor": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/icons": "^2.1.9",
|
"@blocksuite/icons": "^2.1.10",
|
||||||
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@storybook/addon-actions": "^7.0.2",
|
"@storybook/addon-actions": "^7.0.4",
|
||||||
"@storybook/addon-coverage": "^0.0.8",
|
"@storybook/addon-coverage": "^0.0.8",
|
||||||
"@storybook/addon-essentials": "^7.0.2",
|
"@storybook/addon-essentials": "^7.0.4",
|
||||||
"@storybook/addon-interactions": "^7.0.2",
|
"@storybook/addon-interactions": "^7.0.4",
|
||||||
"@storybook/addon-links": "^7.0.2",
|
"@storybook/addon-links": "^7.0.4",
|
||||||
"@storybook/addon-storysource": "^7.0.2",
|
"@storybook/addon-storysource": "^7.0.4",
|
||||||
"@storybook/blocks": "^7.0.2",
|
"@storybook/blocks": "^7.0.4",
|
||||||
"@storybook/builder-vite": "^7.0.2",
|
"@storybook/builder-vite": "^7.0.4",
|
||||||
"@storybook/jest": "^0.1.0",
|
"@storybook/jest": "^0.1.0",
|
||||||
"@storybook/react": "^7.0.2",
|
"@storybook/react": "^7.0.4",
|
||||||
"@storybook/react-vite": "^7.0.2",
|
"@storybook/react-vite": "^7.0.4",
|
||||||
"@storybook/test-runner": "^0.10.0",
|
"@storybook/test-runner": "^0.10.0",
|
||||||
"@storybook/testing-library": "^0.1.0",
|
"@storybook/testing-library": "^0.1.0",
|
||||||
"@types/react": "=18.0.31",
|
"@types/react": "=18.0.31",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"concurrently": "^8.0.1",
|
"concurrently": "^8.0.1",
|
||||||
"jest-mock": "^29.5.0",
|
"jest-mock": "^29.5.0",
|
||||||
"serve": "^14.2.0",
|
"serve": "^14.2.0",
|
||||||
"storybook": "^7.0.2",
|
"storybook": "^7.0.4",
|
||||||
"storybook-dark-mode": "^3.0.0",
|
"storybook-dark-mode": "^3.0.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vite": "^4.2.1",
|
"vite": "^4.2.1",
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
|
import { useTranslation } from '@affine/i18n';
|
||||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||||
|
import { ExportToHtmlIcon, ExportToMarkdownIcon } from '@blocksuite/icons';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import { Button } from '../..';
|
import { Button } from '../..';
|
||||||
import type { ShareMenuProps } from './index';
|
import {
|
||||||
import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css';
|
actionsStyle,
|
||||||
|
descriptionStyle,
|
||||||
|
exportButtonStyle,
|
||||||
|
menuItemStyle,
|
||||||
|
svgStyle,
|
||||||
|
} from './index.css';
|
||||||
|
import type { ShareMenuProps } from './ShareMenu';
|
||||||
|
|
||||||
export const Export: FC<ShareMenuProps> = props => {
|
export const Export: FC<ShareMenuProps> = props => {
|
||||||
const contentParserRef = useRef<ContentParser>();
|
const contentParserRef = useRef<ContentParser>();
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className={menuItemStyle}>
|
<div className={menuItemStyle}>
|
||||||
<div className={descriptionStyle}>
|
<div className={descriptionStyle}>
|
||||||
Download a static copy of your page to share with others.
|
{t('Export Shared Pages Description')}
|
||||||
</div>
|
</div>
|
||||||
<div className={actionsStyle}>
|
<div className={actionsStyle}>
|
||||||
<Button
|
<Button
|
||||||
|
className={exportButtonStyle}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!contentParserRef.current) {
|
if (!contentParserRef.current) {
|
||||||
contentParserRef.current = new ContentParser(props.currentPage);
|
contentParserRef.current = new ContentParser(props.currentPage);
|
||||||
@@ -22,9 +32,11 @@ export const Export: FC<ShareMenuProps> = props => {
|
|||||||
return contentParserRef.current.onExportHtml();
|
return contentParserRef.current.onExportHtml();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export to HTML
|
<ExportToHtmlIcon className={svgStyle} />
|
||||||
|
{t('Export to HTML')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
className={exportButtonStyle}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!contentParserRef.current) {
|
if (!contentParserRef.current) {
|
||||||
contentParserRef.current = new ContentParser(props.currentPage);
|
contentParserRef.current = new ContentParser(props.currentPage);
|
||||||
@@ -32,7 +44,8 @@ export const Export: FC<ShareMenuProps> = props => {
|
|||||||
return contentParserRef.current.onExportMarkdown();
|
return contentParserRef.current.onExportMarkdown();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export to Markdown
|
<ExportToMarkdownIcon className={svgStyle} />
|
||||||
|
{t('Export to Markdown')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
145
packages/component/src/components/share-menu/ShareMenu.tsx
Normal file
145
packages/component/src/components/share-menu/ShareMenu.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||||
|
import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons';
|
||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { Menu } from '../..';
|
||||||
|
import { Export } from './Export';
|
||||||
|
import { containerStyle, indicatorContainerStyle, tabStyle } from './index.css';
|
||||||
|
import { SharePage } from './SharePage';
|
||||||
|
import { ShareWorkspace } from './ShareWorkspace';
|
||||||
|
import { StyledIndicator, StyledShareButton, TabItem } from './styles';
|
||||||
|
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
|
||||||
|
const MenuItems: Record<SharePanel, FC<ShareMenuProps>> = {
|
||||||
|
SharePage: SharePage,
|
||||||
|
Export: Export,
|
||||||
|
ShareWorkspace: ShareWorkspace,
|
||||||
|
};
|
||||||
|
const tabIcons = {
|
||||||
|
SharePage: <ShareIcon />,
|
||||||
|
Export: <ExportIcon />,
|
||||||
|
ShareWorkspace: <PublishIcon />,
|
||||||
|
};
|
||||||
|
export type ShareMenuProps<
|
||||||
|
Workspace extends AffineWorkspace | LocalWorkspace =
|
||||||
|
| AffineWorkspace
|
||||||
|
| LocalWorkspace
|
||||||
|
> = {
|
||||||
|
workspace: Workspace;
|
||||||
|
currentPage: Page;
|
||||||
|
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
|
||||||
|
onOpenWorkspaceSettings: (workspace: Workspace) => void;
|
||||||
|
togglePagePublic: (page: Page, isPublic: boolean) => Promise<void>;
|
||||||
|
toggleWorkspacePublish: (
|
||||||
|
workspace: Workspace,
|
||||||
|
publish: boolean
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function assertInstanceOf<T, U extends T>(
|
||||||
|
obj: T,
|
||||||
|
type: new (...args: any[]) => U
|
||||||
|
): asserts obj is U {
|
||||||
|
if (!(obj instanceof type)) {
|
||||||
|
throw new Error('Object is not instance of type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareMenu: FC<ShareMenuProps> = props => {
|
||||||
|
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
|
||||||
|
const [isPublic] = useBlockSuiteWorkspacePageIsPublic(props.currentPage);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const indicatorRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const startTransaction = useCallback(() => {
|
||||||
|
if (indicatorRef.current && containerRef.current) {
|
||||||
|
const indicator = indicatorRef.current;
|
||||||
|
const activeTabElement = containerRef.current.querySelector(
|
||||||
|
`[data-tab-key="${activeItem}"]`
|
||||||
|
);
|
||||||
|
assertInstanceOf(activeTabElement, HTMLElement);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
indicator.style.left = `${activeTabElement.offsetLeft}px`;
|
||||||
|
indicator.style.width = `${activeTabElement.offsetWidth}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [activeItem]);
|
||||||
|
const handleMenuChange = useCallback(
|
||||||
|
(selectedItem: SharePanel) => {
|
||||||
|
setActiveItem(selectedItem);
|
||||||
|
startTransaction();
|
||||||
|
},
|
||||||
|
[setActiveItem, startTransaction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const ActiveComponent = MenuItems[activeItem];
|
||||||
|
interface ShareMenuProps {
|
||||||
|
activeItem: SharePanel;
|
||||||
|
onChangeTab: (selectedItem: SharePanel) => void;
|
||||||
|
}
|
||||||
|
const ShareMenu: FC<ShareMenuProps> = ({ activeItem, onChangeTab }) => {
|
||||||
|
const handleButtonClick = (itemName: SharePanel) => {
|
||||||
|
onChangeTab(itemName);
|
||||||
|
setActiveItem(itemName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={tabStyle} ref={containerRef}>
|
||||||
|
{Object.keys(MenuItems).map(item => (
|
||||||
|
<TabItem
|
||||||
|
isActive={activeItem === item}
|
||||||
|
key={item}
|
||||||
|
data-tab-key={item}
|
||||||
|
onClick={() => handleButtonClick(item as SharePanel)}
|
||||||
|
>
|
||||||
|
{tabIcons[item as SharePanel]}
|
||||||
|
{isPublic ? (item === 'SharePage' ? 'SharedPage' : item) : item}
|
||||||
|
</TabItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const Share = (
|
||||||
|
<>
|
||||||
|
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
|
||||||
|
<div className={indicatorContainerStyle}>
|
||||||
|
<StyledIndicator
|
||||||
|
ref={(ref: HTMLDivElement | null) => {
|
||||||
|
indicatorRef.current = ref;
|
||||||
|
startTransaction();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={containerStyle}>
|
||||||
|
<ActiveComponent {...props} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
content={Share}
|
||||||
|
visible={open}
|
||||||
|
placement="bottom-end"
|
||||||
|
trigger={['click']}
|
||||||
|
width={439}
|
||||||
|
disablePortal={true}
|
||||||
|
onClickAway={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledShareButton
|
||||||
|
data-testid="share-menu-button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
isShared={isPublic}
|
||||||
|
>
|
||||||
|
<div>{isPublic ? 'Shared' : 'Share'}</div>
|
||||||
|
</StyledShareButton>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,11 +3,22 @@ import type { LocalWorkspace } from '@affine/workspace/type';
|
|||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { Button } from '../..';
|
import { PublicLinkDisableModal } from './disable-public-link';
|
||||||
import type { ShareMenuProps } from './index';
|
import {
|
||||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
descriptionStyle,
|
||||||
|
inputButtonRowStyle,
|
||||||
|
menuItemStyle,
|
||||||
|
} from './index.css';
|
||||||
|
import type { ShareMenuProps } from './ShareMenu';
|
||||||
|
import {
|
||||||
|
StyledButton,
|
||||||
|
StyledDisableButton,
|
||||||
|
StyledInput,
|
||||||
|
StyledLinkSpan,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
export const LocalSharePage: FC<ShareMenuProps> = props => {
|
export const LocalSharePage: FC<ShareMenuProps> = props => {
|
||||||
return (
|
return (
|
||||||
@@ -15,17 +26,14 @@ export const LocalSharePage: FC<ShareMenuProps> = props => {
|
|||||||
<div className={descriptionStyle}>
|
<div className={descriptionStyle}>
|
||||||
Sharing page publicly requires AFFiNE Cloud service.
|
Sharing page publicly requires AFFiNE Cloud service.
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<StyledButton
|
||||||
data-testid="share-menu-enable-affine-cloud-button"
|
data-testid="share-menu-enable-affine-cloud-button"
|
||||||
className={buttonStyle}
|
|
||||||
type="light"
|
|
||||||
shape="round"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enable AFFiNE Cloud
|
Enable AFFiNE Cloud
|
||||||
</Button>
|
</StyledButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -34,6 +42,7 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
|
|||||||
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
|
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
|
||||||
props.currentPage
|
props.currentPage
|
||||||
);
|
);
|
||||||
|
const [showDisable, setShowDisable] = useState(false);
|
||||||
const sharingUrl = useMemo(() => {
|
const sharingUrl = useMemo(() => {
|
||||||
const env = getEnvironment();
|
const env = getEnvironment();
|
||||||
if (env.isBrowser) {
|
if (env.isBrowser) {
|
||||||
@@ -48,14 +57,59 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
|
|||||||
const onClickCopyLink = useCallback(() => {
|
const onClickCopyLink = useCallback(() => {
|
||||||
navigator.clipboard.writeText(sharingUrl);
|
navigator.clipboard.writeText(sharingUrl);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={menuItemStyle}>
|
<div className={menuItemStyle}>
|
||||||
<div className={descriptionStyle}>
|
<div className={descriptionStyle}>
|
||||||
Create a link you can easily share with anyone.
|
Create a link you can easily share with anyone.
|
||||||
</div>
|
</div>
|
||||||
<span>{isPublic ? sharingUrl : 'not public'}</span>
|
<div className={inputButtonRowStyle}>
|
||||||
{!isPublic && <Button onClick={onClickCreateLink}>Create</Button>}
|
<StyledInput
|
||||||
{isPublic && <Button onClick={onClickCopyLink}>Copy Link</Button>}
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={isPublic ? sharingUrl : 'https://app.affine.pro/xxxx'}
|
||||||
|
/>
|
||||||
|
{!isPublic && (
|
||||||
|
<StyledButton
|
||||||
|
data-testid="affine-share-create-link"
|
||||||
|
onClick={onClickCreateLink}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
{isPublic && (
|
||||||
|
<StyledButton
|
||||||
|
data-testid="affine-share-copy-link"
|
||||||
|
onClick={onClickCopyLink}
|
||||||
|
>
|
||||||
|
Copy Link
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={descriptionStyle}>
|
||||||
|
The entire Workspace is published on the web and can be edited via
|
||||||
|
<StyledLinkSpan
|
||||||
|
onClick={() => {
|
||||||
|
props.onOpenWorkspaceSettings(props.workspace);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Workspace Settings.
|
||||||
|
</StyledLinkSpan>
|
||||||
|
</div>
|
||||||
|
{isPublic && (
|
||||||
|
<>
|
||||||
|
<StyledDisableButton onClick={() => setShowDisable(true)}>
|
||||||
|
Disable Public Link
|
||||||
|
</StyledDisableButton>
|
||||||
|
<PublicLinkDisableModal
|
||||||
|
page={props.currentPage}
|
||||||
|
open={showDisable}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDisable(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,27 +2,24 @@ import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
|||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { Button } from '../..';
|
import { descriptionStyle, menuItemStyle } from './index.css';
|
||||||
import type { ShareMenuProps } from '.';
|
import type { ShareMenuProps } from './ShareMenu';
|
||||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
import { StyledButton } from './styles';
|
||||||
|
|
||||||
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
|
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
|
||||||
return (
|
return (
|
||||||
<div className={menuItemStyle}>
|
<div className={menuItemStyle}>
|
||||||
<div className={descriptionStyle}>
|
<div className={descriptionStyle}>
|
||||||
Sharing page publicly requires AFFiNE Cloud service.
|
Invite others to join the Workspace or publish it to web.
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<StyledButton
|
||||||
data-testid="share-menu-enable-affine-cloud-button"
|
data-testid="share-menu-enable-affine-cloud-button"
|
||||||
className={buttonStyle}
|
|
||||||
type="light"
|
|
||||||
shape="circle"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
props.onOpenWorkspaceSettings(props.workspace);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enable AFFiNE Cloud
|
Open Workspace Settings
|
||||||
</Button>
|
</StyledButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -36,16 +33,14 @@ const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
|
|||||||
? `Current workspace has been published to the web as a public workspace.`
|
? `Current workspace has been published to the web as a public workspace.`
|
||||||
: `Invite others to join the Workspace or publish it to web`}
|
: `Invite others to join the Workspace or publish it to web`}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<StyledButton
|
||||||
data-testid="share-menu-publish-to-web-button"
|
data-testid="share-menu-publish-to-web-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onOpenWorkspaceSettings(props.workspace);
|
props.onOpenWorkspaceSettings(props.workspace);
|
||||||
}}
|
}}
|
||||||
type="light"
|
|
||||||
shape="circle"
|
|
||||||
>
|
>
|
||||||
Open Workspace Settings
|
Open Workspace Settings
|
||||||
</Button>
|
</StyledButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Modal, ModalCloseButton, toast } from '../../..';
|
||||||
|
import {
|
||||||
|
StyledButton,
|
||||||
|
StyledButtonContent,
|
||||||
|
StyledDangerButton,
|
||||||
|
StyledModalHeader,
|
||||||
|
StyledModalWrapper,
|
||||||
|
StyledTextContent,
|
||||||
|
} from './style';
|
||||||
|
|
||||||
|
export type PublicLinkDisableProps = {
|
||||||
|
page: Page;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PublicLinkDisableModal = ({
|
||||||
|
page,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: PublicLinkDisableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(page);
|
||||||
|
const handleDisable = useCallback(() => {
|
||||||
|
setIsPublic(false);
|
||||||
|
toast('Successfully disabled', {
|
||||||
|
portal: document.body,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<StyledModalWrapper>
|
||||||
|
<ModalCloseButton onClick={onClose} top={12} right={12} />
|
||||||
|
<StyledModalHeader>{t('Disable Public Link ?')}</StyledModalHeader>
|
||||||
|
|
||||||
|
<StyledTextContent>
|
||||||
|
{t('Disable Public Link Description')}
|
||||||
|
</StyledTextContent>
|
||||||
|
|
||||||
|
<StyledButtonContent>
|
||||||
|
<StyledButton onClick={onClose}>{t('Cancel')}</StyledButton>
|
||||||
|
<StyledDangerButton
|
||||||
|
data-testid="disable-public-link-confirm-button"
|
||||||
|
onClick={handleDisable}
|
||||||
|
style={{ marginLeft: '24px' }}
|
||||||
|
>
|
||||||
|
{t('Disable')}
|
||||||
|
</StyledDangerButton>
|
||||||
|
</StyledButtonContent>
|
||||||
|
</StyledModalWrapper>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { styled, TextButton } from '@affine/component';
|
||||||
|
|
||||||
|
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0px',
|
||||||
|
width: '560px',
|
||||||
|
background: theme.colors.popoverBackground,
|
||||||
|
borderRadius: '12px',
|
||||||
|
// height: '312px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
margin: '44px 0px 12px 0px',
|
||||||
|
width: '560px',
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: theme.font.h6,
|
||||||
|
textAlign: 'center',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledTextContent = styled('div')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
margin: 'auto',
|
||||||
|
width: '560px',
|
||||||
|
padding: '0px 84px',
|
||||||
|
fontWeight: '400',
|
||||||
|
fontSize: theme.font.base,
|
||||||
|
textAlign: 'center',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledButtonContent = styled('div')(() => {
|
||||||
|
return {
|
||||||
|
margin: '32px 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const StyledButton = styled(TextButton)(({ theme }) => {
|
||||||
|
return {
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
height: '32px',
|
||||||
|
background: '#F3F0FF',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '4px 20px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const StyledDangerButton = styled(TextButton)(({ theme }) => {
|
||||||
|
return {
|
||||||
|
color: '#FF631F',
|
||||||
|
height: '32px',
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(255, 99, 31, 0.1), rgba(255, 99, 31, 0.1)), #FFFFFF;',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '4px 20px',
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -2,22 +2,27 @@ import { style } from '@vanilla-extract/css';
|
|||||||
|
|
||||||
export const tabStyle = style({
|
export const tabStyle = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-around',
|
flex: '1',
|
||||||
|
width: '100%',
|
||||||
|
padding: '0 10px',
|
||||||
|
margin: '0',
|
||||||
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
marginTop: '4px',
|
|
||||||
marginLeft: '10px',
|
|
||||||
marginRight: '10px',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const menuItemStyle = style({
|
export const menuItemStyle = style({
|
||||||
marginLeft: '20px',
|
padding: '4px 18px',
|
||||||
marginRight: '20px',
|
paddingBottom: '16px',
|
||||||
marginTop: '30px',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const descriptionStyle = style({
|
export const descriptionStyle = style({
|
||||||
fontSize: '1rem',
|
wordWrap: 'break-word',
|
||||||
|
// wordBreak: 'break-all',
|
||||||
|
fontSize: '16px',
|
||||||
|
marginTop: '16px',
|
||||||
|
marginBottom: '16px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const buttonStyle = style({
|
export const buttonStyle = style({
|
||||||
@@ -30,5 +35,32 @@ export const actionsStyle = style({
|
|||||||
gap: '9px',
|
gap: '9px',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'start',
|
alignItems: 'flex-start',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const containerStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
export const indicatorContainerStyle = style({
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
export const inputButtonRowStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '16px',
|
||||||
|
});
|
||||||
|
export const exportButtonStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0',
|
||||||
|
border: 'none',
|
||||||
|
});
|
||||||
|
export const svgStyle = style({
|
||||||
|
fontSize: '20px',
|
||||||
|
marginRight: '12px',
|
||||||
|
verticalAlign: 'top',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,100 +1,2 @@
|
|||||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
export * from './disable-public-link';
|
||||||
import { ExportIcon } from '@blocksuite/icons';
|
export * from './ShareMenu';
|
||||||
import type { Page } from '@blocksuite/store';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import { Menu } from '../..';
|
|
||||||
import { Export } from './Export';
|
|
||||||
import { tabStyle } from './index.css';
|
|
||||||
import { SharePage } from './SharePage';
|
|
||||||
import { ShareWorkspace } from './ShareWorkspace';
|
|
||||||
import { StyledIndicator, StyledShareButton, TabItem } from './styles';
|
|
||||||
|
|
||||||
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
|
|
||||||
const MenuItems: Record<SharePanel, FC<ShareMenuProps>> = {
|
|
||||||
SharePage: SharePage,
|
|
||||||
Export: Export,
|
|
||||||
ShareWorkspace: ShareWorkspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShareMenuProps<
|
|
||||||
Workspace extends AffineWorkspace | LocalWorkspace =
|
|
||||||
| AffineWorkspace
|
|
||||||
| LocalWorkspace
|
|
||||||
> = {
|
|
||||||
workspace: Workspace;
|
|
||||||
currentPage: Page;
|
|
||||||
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
|
|
||||||
onOpenWorkspaceSettings: (workspace: Workspace) => void;
|
|
||||||
togglePagePublic: (page: Page, publish: boolean) => Promise<void>;
|
|
||||||
toggleWorkspacePublish: (
|
|
||||||
workspace: Workspace,
|
|
||||||
publish: boolean
|
|
||||||
) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ShareMenu: FC<ShareMenuProps> = props => {
|
|
||||||
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const handleMenuChange = useCallback((selectedItem: SharePanel) => {
|
|
||||||
setActiveItem(selectedItem);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const ActiveComponent = MenuItems[activeItem];
|
|
||||||
interface ShareMenuProps {
|
|
||||||
activeItem: SharePanel;
|
|
||||||
onChangeTab: (selectedItem: SharePanel) => void;
|
|
||||||
}
|
|
||||||
const ShareMenu: FC<ShareMenuProps> = ({ activeItem, onChangeTab }) => {
|
|
||||||
const handleButtonClick = (itemName: SharePanel) => {
|
|
||||||
onChangeTab(itemName);
|
|
||||||
setActiveItem(itemName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={tabStyle}>
|
|
||||||
{Object.keys(MenuItems).map(item => (
|
|
||||||
<TabItem
|
|
||||||
isActive={activeItem === item}
|
|
||||||
key={item}
|
|
||||||
onClick={() => handleButtonClick(item as SharePanel)}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</TabItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const activeIndex = Object.keys(MenuItems).indexOf(activeItem);
|
|
||||||
const Share = (
|
|
||||||
<>
|
|
||||||
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
|
|
||||||
<StyledIndicator activeIndex={activeIndex} />
|
|
||||||
<ActiveComponent {...props} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
content={Share}
|
|
||||||
visible={open}
|
|
||||||
width={439}
|
|
||||||
placement="bottom-end"
|
|
||||||
trigger={['click']}
|
|
||||||
disablePortal={true}
|
|
||||||
onClickAway={() => {
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledShareButton
|
|
||||||
data-testid="share-menu-button"
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(!open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExportIcon />
|
|
||||||
<div>Share</div>
|
|
||||||
</StyledShareButton>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { displayFlex, styled, TextButton } from '../..';
|
import { Button, displayFlex, styled, TextButton } from '../..';
|
||||||
|
|
||||||
export const StyledShareButton = styled(TextButton)(({ theme }) => {
|
export const StyledShareButton = styled(TextButton, {
|
||||||
|
shouldForwardProp: (prop: string) => prop !== 'isShared',
|
||||||
|
})<{ isShared?: boolean }>(({ theme, isShared }) => {
|
||||||
return {
|
return {
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
marginLeft: '4px',
|
marginLeft: '4px',
|
||||||
marginRight: '16px',
|
marginRight: '16px',
|
||||||
border: `1px solid ${theme.colors.primaryColor}`,
|
border: `1px solid ${
|
||||||
color: theme.colors.primaryColor,
|
isShared ? theme.colors.primaryColor : theme.colors.iconColor
|
||||||
|
}`,
|
||||||
|
color: isShared ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
|
':hover': {
|
||||||
|
border: `1px solid ${theme.colors.primaryColor}`,
|
||||||
|
},
|
||||||
span: {
|
span: {
|
||||||
...displayFlex('center', 'center'),
|
...displayFlex('center', 'center'),
|
||||||
},
|
},
|
||||||
@@ -26,21 +33,41 @@ export const TabItem = styled('li')<{ isActive?: boolean }>(
|
|||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
...displayFlex('center', 'center'),
|
...displayFlex('center', 'center'),
|
||||||
width: 'calc(100% / 3)',
|
flex: '1',
|
||||||
height: '34px',
|
height: '30px',
|
||||||
color: theme.colors.textColor,
|
color: theme.colors.textColor,
|
||||||
opacity: isActive ? 1 : 0.2,
|
opacity: isActive ? 1 : 0.2,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: theme.font.h6,
|
fontSize: theme.font.base,
|
||||||
lineHeight: theme.font.lineHeight,
|
lineHeight: theme.font.lineHeight,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
|
padding: '0 10px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
position: 'relative',
|
||||||
|
':hover': {
|
||||||
|
background: theme.colors.hoverBackground,
|
||||||
|
opacity: 1,
|
||||||
|
color: isActive
|
||||||
|
? theme.colors.textColor
|
||||||
|
: theme.colors.secondaryTextColor,
|
||||||
|
svg: {
|
||||||
|
fill: isActive
|
||||||
|
? theme.colors.textColor
|
||||||
|
: theme.colors.secondaryTextColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontSize: '20px',
|
||||||
|
marginRight: '12px',
|
||||||
|
},
|
||||||
':after': {
|
':after': {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '-2px',
|
bottom: '-6px',
|
||||||
left: '-2px',
|
left: '0',
|
||||||
width: 'calc(100% + 4px)',
|
width: '100%',
|
||||||
height: '2px',
|
height: '2px',
|
||||||
background: theme.colors.textColor,
|
background: theme.colors.textColor,
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
@@ -49,16 +76,54 @@ export const TabItem = styled('li')<{ isActive?: boolean }>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
export const StyledIndicator = styled('div')<{ activeIndex: number }>(
|
export const StyledIndicator = styled('div')(({ theme }) => {
|
||||||
({ theme, activeIndex }) => {
|
return {
|
||||||
return {
|
height: '2px',
|
||||||
height: '2px',
|
background: theme.colors.textColor,
|
||||||
margin: '0 10px',
|
position: 'absolute',
|
||||||
background: theme.colors.textColor,
|
left: '0',
|
||||||
position: 'absolute',
|
transition: 'left .3s, width .3s',
|
||||||
left: `calc(${activeIndex * 100}% / 3)`,
|
};
|
||||||
width: `calc(100% / 3)`,
|
});
|
||||||
transition: 'left .3s, width .3s',
|
export const StyledInput = styled('input')(({ theme }) => {
|
||||||
};
|
return {
|
||||||
}
|
padding: '4px 8px',
|
||||||
);
|
height: '28px',
|
||||||
|
color: theme.colors.placeHolderColor,
|
||||||
|
border: `1px solid ${theme.colors.placeHolderColor}`,
|
||||||
|
cursor: 'default',
|
||||||
|
overflow: 'hidden',
|
||||||
|
userSelect: 'text',
|
||||||
|
borderRadius: '4px',
|
||||||
|
flexGrow: 1,
|
||||||
|
marginRight: '10px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const StyledButton = styled(TextButton)(({ theme }) => {
|
||||||
|
return {
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
height: '32px',
|
||||||
|
background: '#F3F0FF',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '4px 20px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const StyledDisableButton = styled(Button)(() => {
|
||||||
|
return {
|
||||||
|
color: '#FF631F',
|
||||||
|
height: '32px',
|
||||||
|
border: 'none',
|
||||||
|
marginTop: '16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '0',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const StyledLinkSpan = styled('span')(({ theme }) => {
|
||||||
|
return {
|
||||||
|
marginLeft: '4px',
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
fontWeight: '500',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Page } from '@blocksuite/store';
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { StoryFn } from '@storybook/react';
|
import type { StoryFn } from '@storybook/react';
|
||||||
|
|
||||||
import { ShareMenu } from '../components/share-menu';
|
import { ShareMenu } from '../components/share-menu/ShareMenu';
|
||||||
import toast from '../ui/toast/toast';
|
import toast from '../ui/toast/toast';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"module": "./src/index.ts",
|
"module": "./src/index.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"next": "=13.2.3",
|
"next": "=13.2.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -204,5 +204,11 @@
|
|||||||
"Discover what's new!": "Discover what's new!",
|
"Discover what's new!": "Discover what's new!",
|
||||||
"Navigation Path": "Navigation Path",
|
"Navigation Path": "Navigation Path",
|
||||||
"View Navigation Path": "View Navigation Path",
|
"View Navigation Path": "View Navigation Path",
|
||||||
"Back to Quick Search": "Back to Quick Search"
|
"Back to Quick Search": "Back to Quick Search",
|
||||||
|
"Shared Pages": "Shared Pages",
|
||||||
|
"Disable Public Sharing": "Disable Public Sharing",
|
||||||
|
"Disable": "Disable",
|
||||||
|
"Disable Public Link ?": "Disable Public Link ?",
|
||||||
|
"Disable Public Link Description": "Disabling this public link will prevent anyone with the link from accessing this page.",
|
||||||
|
"Export Shared Pages Description": "Download a static copy of your page to share with others."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,7 +381,9 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
}
|
}
|
||||||
).then(r =>
|
).then(r =>
|
||||||
r.ok ? r.arrayBuffer() : Promise.reject(new Error(`${r.status}`))
|
r.ok
|
||||||
|
? r.arrayBuffer()
|
||||||
|
: Promise.reject(new RequestError(MessageCode.noPermission))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
downloadWorkspace: async (
|
downloadWorkspace: async (
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
"idb": "^7.1.1"
|
"idb": "^7.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
|
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
|
||||||
"vite": "^4.2.1",
|
"vite": "^4.2.1",
|
||||||
"vite-plugin-dts": "^2.2.0",
|
"vite-plugin-dts": "^2.2.0",
|
||||||
"y-indexeddb": "^9.0.10"
|
"y-indexeddb": "^9.0.10"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const config: PlaywrightTestConfig = {
|
|||||||
browserName:
|
browserName:
|
||||||
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
|
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
|
||||||
'chromium',
|
'chromium',
|
||||||
|
permissions: ['clipboard-read', 'clipboard-write'],
|
||||||
viewport: { width: 1440, height: 800 },
|
viewport: { width: 1440, height: 800 },
|
||||||
actionTimeout: 5 * 1000,
|
actionTimeout: 5 * 1000,
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
@@ -66,8 +67,9 @@ const config: PlaywrightTestConfig = {
|
|||||||
ENABLE_DEBUG_PAGE: '1',
|
ENABLE_DEBUG_PAGE: '1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Intentionally not building the web, reminds you to run it by yourself.
|
||||||
{
|
{
|
||||||
command: 'yarn build && yarn start -p 8080',
|
command: 'yarn start -p 8080',
|
||||||
port: 8080,
|
port: 8080,
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
|
|||||||
65
tests/parallels/affine/affine-public-single-page.spec.ts
Normal file
65
tests/parallels/affine/affine-public-single-page.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { waitMarkdownImported } from '../../libs/page-logic';
|
||||||
|
import { test } from '../../libs/playwright';
|
||||||
|
import { clickNewPageButton } from '../../libs/sidebar';
|
||||||
|
import { createFakeUser, loginUser, openHomePage } from '../../libs/utils';
|
||||||
|
import { createWorkspace } from '../../libs/workspace';
|
||||||
|
|
||||||
|
test.describe('affine single page', () => {
|
||||||
|
test('public single page', async ({ page, browser }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
const [a] = await createFakeUser();
|
||||||
|
await loginUser(page, a);
|
||||||
|
await waitMarkdownImported(page);
|
||||||
|
const name = `test-${Date.now()}`;
|
||||||
|
await createWorkspace({ name }, page);
|
||||||
|
await waitMarkdownImported(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
const page1Id = page.url().split('/').at(-1);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
const page2Id = page.url().split('/').at(-1);
|
||||||
|
expect(typeof page2Id).toBe('string');
|
||||||
|
expect(page1Id).not.toBe(page2Id);
|
||||||
|
const title = 'This is page 2';
|
||||||
|
await page.locator('[data-block-is-title="true"]').type(title, {
|
||||||
|
delay: 50,
|
||||||
|
});
|
||||||
|
await page.getByTestId('share-menu-button').click();
|
||||||
|
await page.getByTestId('share-menu-enable-affine-cloud-button').click();
|
||||||
|
const promise = page.evaluate(
|
||||||
|
async () =>
|
||||||
|
new Promise(resolve =>
|
||||||
|
window.addEventListener('affine-workspace:transform', resolve, {
|
||||||
|
once: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await page.getByTestId('confirm-enable-cloud-button').click();
|
||||||
|
await promise;
|
||||||
|
const newPage2Url = page.url().split('/');
|
||||||
|
newPage2Url[newPage2Url.length - 1] = page2Id as string;
|
||||||
|
await page.goto(newPage2Url.join('/'));
|
||||||
|
await page.waitForSelector('v-line');
|
||||||
|
const currentTitle = await page
|
||||||
|
.locator('[data-block-is-title="true"]')
|
||||||
|
.textContent();
|
||||||
|
expect(currentTitle).toBe(title);
|
||||||
|
await page.getByTestId('share-menu-button').click();
|
||||||
|
await page.getByTestId('affine-share-create-link').click();
|
||||||
|
await page.getByTestId('affine-share-copy-link').click();
|
||||||
|
const url = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const context2 = await browser.newContext();
|
||||||
|
const page2 = await context2.newPage();
|
||||||
|
await page2.goto(url);
|
||||||
|
await page2.waitForSelector('v-line');
|
||||||
|
const currentTitle2 = await page2
|
||||||
|
.locator('[data-block-is-title="true"]')
|
||||||
|
.textContent();
|
||||||
|
expect(currentTitle2).toBe(title);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,11 +3,11 @@ import { expect } from '@playwright/test';
|
|||||||
import { test } from '../libs/playwright';
|
import { test } from '../libs/playwright';
|
||||||
|
|
||||||
test.describe('Debug page broadcast', () => {
|
test.describe('Debug page broadcast', () => {
|
||||||
test('should broadcast a message to all debug pages', async ({ page }) => {
|
test('should have page0', async ({ page }) => {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'http://localhost:8080/_debug/init-page?type=importMarkdown'
|
'http://localhost:8080/_debug/init-page?type=importMarkdown'
|
||||||
);
|
);
|
||||||
await page.waitForSelector('rich-text');
|
await page.waitForSelector('v-line');
|
||||||
const pageId = await page.evaluate(async () => {
|
const pageId = await page.evaluate(async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return globalThis.page.id;
|
return globalThis.page.id;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ test.describe('AFFiNE change log', () => {
|
|||||||
const editorRightBottomChangeLog = page.locator(
|
const editorRightBottomChangeLog = page.locator(
|
||||||
'[data-testid=right-bottom-change-log-icon]'
|
'[data-testid=right-bottom-change-log-icon]'
|
||||||
);
|
);
|
||||||
|
await page.waitForTimeout(50);
|
||||||
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
|
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
|
||||||
await page.getByRole('link', { name: 'All pages' }).click();
|
await page.getByRole('link', { name: 'All pages' }).click();
|
||||||
const normalRightBottomChangeLog = page.locator(
|
const normalRightBottomChangeLog = page.locator(
|
||||||
|
|||||||
Reference in New Issue
Block a user