diff --git a/packages/frontend/component/src/ui/menu/styles.css.ts b/packages/frontend/component/src/ui/menu/styles.css.ts
index 4ad3ad462c70e..668aa2805f6a7 100644
--- a/packages/frontend/component/src/ui/menu/styles.css.ts
+++ b/packages/frontend/component/src/ui/menu/styles.css.ts
@@ -85,7 +85,6 @@ export const menuItem = style({
'&.checked, &.selected': {
vars: {
[iconColor]: cssVar('primaryColor'),
- [labelColor]: cssVar('primaryColor'),
},
},
},
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts
deleted file mode 100644
index 4771c68717e09..0000000000000
--- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-import { cssVar } from '@toeverything/theme';
-import { cssVarV2 } from '@toeverything/theme/v2';
-import { globalStyle, style } from '@vanilla-extract/css';
-export const headerStyle = style({
- display: 'flex',
- alignItems: 'center',
- fontSize: cssVar('fontSm'),
- fontWeight: 600,
- lineHeight: '22px',
- padding: '0 4px',
- gap: '4px',
-});
-export const content = style({
- display: 'flex',
- flexDirection: 'column',
- gap: '8px',
-});
-export const menuStyle = style({
- width: '390px',
- height: 'auto',
- padding: '12px',
-});
-export const menuTriggerStyle = style({
- width: '150px',
- padding: '4px 10px',
- justifyContent: 'space-between',
-});
-export const publicItemRowStyle = style({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
-});
-export const DoneIconStyle = style({
- color: cssVarV2('button/primary'),
- fontSize: cssVar('fontH5'),
- marginLeft: '8px',
-});
-export const exportItemStyle = style({
- padding: '4px',
- transition: 'all 0.3s',
- gap: '0px',
-});
-globalStyle(`${exportItemStyle} > div:first-child`, {
- alignItems: 'center',
-});
-globalStyle(`${exportItemStyle} svg`, {
- width: '16px',
- height: '16px',
-});
-
-export const copyLinkContainerStyle = style({
- padding: '4px',
- display: 'flex',
- alignItems: 'center',
- width: '100%',
- position: 'relative',
- selectors: {
- '&.secondary': {
- padding: 0,
- marginTop: '12px',
- },
- },
-});
-export const copyLinkButtonStyle = style({
- flex: 1,
- padding: '4px 12px',
- paddingRight: '6px',
- borderRight: 'none',
- borderTopRightRadius: '0',
- borderBottomRightRadius: '0',
- color: 'transparent',
- position: 'initial',
- selectors: {
- '&.dark': {
- backgroundColor: cssVarV2('layer/pureBlack'),
- },
- '&.dark::hover': {
- backgroundColor: cssVarV2('layer/pureBlack'),
- },
- },
-});
-export const copyLinkLabelContainerStyle = style({
- width: '100%',
- borderRight: 'none',
- borderTopRightRadius: '0',
- borderBottomRightRadius: '0',
- position: 'relative',
-});
-export const copyLinkLabelStyle = style({
- position: 'absolute',
- textAlign: 'end',
- top: '50%',
- left: '50%',
- transform: 'translateX(-50%) translateY(-50%)',
- lineHeight: '20px',
- color: cssVarV2('text/pureWhite'),
- selectors: {
- '&.secondary': {
- color: cssVarV2('text/primary'),
- },
- },
-});
-export const copyLinkShortcutStyle = style({
- position: 'absolute',
- textAlign: 'end',
- top: '50%',
- right: '52px',
- transform: 'translateY(-50%)',
- opacity: 0.5,
- lineHeight: '20px',
- color: cssVarV2('text/pureWhite'),
- selectors: {
- '&.secondary': {
- color: cssVarV2('text/secondary'),
- },
- },
-});
-export const copyLinkTriggerStyle = style({
- padding: '4px 12px 4px 8px',
- borderLeft: 'none',
- borderTopLeftRadius: '0',
- borderBottomLeftRadius: '0',
- ':hover': {
- backgroundColor: cssVarV2('button/primary'),
- color: cssVarV2('button/pureWhiteText'),
- },
- '::after': {
- content: '""',
- position: 'absolute',
- left: '0',
- top: '0',
- height: '100%',
- width: '1px',
- backgroundColor: cssVarV2('button/innerBlackBorder'),
- },
- selectors: {
- '&.secondary': {
- backgroundColor: cssVarV2('button/secondary'),
- color: cssVarV2('text/secondary'),
- },
- '&.secondary:hover': {
- backgroundColor: cssVarV2('button/secondary'),
- color: cssVarV2('text/secondary'),
- },
- },
-});
-globalStyle(`${copyLinkTriggerStyle} svg`, {
- color: cssVarV2('button/pureWhiteText'),
- transform: 'translateX(2px)',
-});
-globalStyle(`${copyLinkTriggerStyle}.secondary svg`, {
- color: cssVarV2('text/secondary'),
- transform: 'translateX(2px)',
-});
-export const copyLinkMenuItemStyle = style({
- padding: '4px',
- transition: 'all 0.3s',
-});
-export const descriptionStyle = style({
- wordWrap: 'break-word',
- fontSize: cssVar('fontXs'),
- lineHeight: '20px',
- color: cssVarV2('text/secondary'),
- textAlign: 'left',
- padding: '0 6px',
-});
-export const containerStyle = style({
- display: 'flex',
- width: '100%',
- flexDirection: 'column',
- gap: '8px',
-});
-export const indicatorContainerStyle = style({
- position: 'relative',
-});
-export const titleContainerStyle = style({
- display: 'flex',
- alignItems: 'center',
- fontSize: cssVar('fontXs'),
- color: cssVarV2('text/secondary'),
- fontWeight: 400,
- padding: '8px 4px 0 4px',
-});
-export const columnContainerStyle = style({
- display: 'flex',
- flexDirection: 'column',
- justifyContent: 'center',
- width: '100%',
- gap: '8px',
-});
-export const rowContainerStyle = style({
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- padding: '4px',
-});
-export const exportContainerStyle = style({
- display: 'flex',
- flexDirection: 'column',
- gap: '8px',
-});
-export const labelStyle = style({
- fontSize: cssVar('fontSm'),
- fontWeight: 500,
-});
-export const disableSharePage = style({
- color: cssVarV2('button/error'),
-});
-export const localSharePage = style({
- padding: '12px 8px',
- display: 'flex',
- alignItems: 'center',
- borderRadius: '8px',
- backgroundColor: cssVarV2('layer/background/secondary'),
- minHeight: '84px',
- position: 'relative',
-});
-export const cloudSvgContainer = style({
- width: '146px',
- display: 'flex',
- justifyContent: 'flex-end',
- alignItems: 'center',
- position: 'absolute',
- bottom: '0',
- right: '0',
-});
-export const shareLinkStyle = style({
- padding: '4px',
- fontSize: cssVar('fontXs'),
- fontWeight: 500,
- lineHeight: '20px',
- transform: 'translateX(-4px)',
- gap: '4px',
-});
-globalStyle(`${shareLinkStyle} > span`, {
- color: cssVarV2('text/link'),
-});
-globalStyle(`${shareLinkStyle} > div > svg`, {
- color: cssVarV2('text/link'),
-});
-export const buttonContainer = style({
- display: 'flex',
- alignItems: 'center',
- gap: '4px',
- fontWeight: 500,
-});
-export const button = style({
- padding: '6px 8px',
- height: 32,
-});
-export const shortcutStyle = style({
- fontSize: cssVar('fontXs'),
- color: cssVarV2('text/secondary'),
- fontWeight: 400,
-});
-export const openWorkspaceSettingsStyle = style({
- color: cssVarV2('text/link'),
- fontSize: cssVar('fontXs'),
- fontWeight: 500,
- display: 'flex',
- gap: '8px',
- alignItems: 'center',
- justifyContent: 'flex-start',
- width: '100%',
- padding: '4px',
- cursor: 'pointer',
-});
-globalStyle(`${openWorkspaceSettingsStyle} svg`, {
- color: cssVarV2('text/link'),
-});
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx
deleted file mode 100644
index df1c75fcdb153..0000000000000
--- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import { notify, Skeleton } from '@affine/component';
-import { Button } from '@affine/component/ui/button';
-import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
-import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
-import { ServerService } from '@affine/core/modules/cloud';
-import { GlobalDialogService } from '@affine/core/modules/dialogs';
-import { WorkspacePermissionService } from '@affine/core/modules/permissions';
-import { ShareInfoService } from '@affine/core/modules/share-doc';
-import { PublicPageMode } from '@affine/graphql';
-import { useI18n } from '@affine/i18n';
-import { track } from '@affine/track';
-import {
- CollaborationIcon,
- DoneIcon,
- LockIcon,
- SingleSelectCheckSolidIcon,
- ViewIcon,
-} from '@blocksuite/icons/rc';
-import { useLiveData, useService } from '@toeverything/infra';
-import { cssVar } from '@toeverything/theme';
-import { Suspense, useCallback, useEffect } from 'react';
-import { ErrorBoundary } from 'react-error-boundary';
-
-import { CloudSvg } from '../cloud-svg';
-import { CopyLinkButton } from './copy-link-button';
-import * as styles from './index.css';
-import type { ShareMenuProps } from './share-menu';
-
-export const LocalSharePage = (props: ShareMenuProps) => {
- const t = useI18n();
- const {
- workspaceMetadata: { id: workspaceId },
- } = props;
- return (
- <>
-
-
-
- {t['com.affine.share-menu.EnableCloudDescription']()}
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export const AFFiNESharePage = (props: ShareMenuProps) => {
- const t = useI18n();
- const {
- workspaceMetadata: { id: workspaceId },
- } = props;
- const shareInfoService = useService(ShareInfoService);
- const serverService = useService(ServerService);
- useEffect(() => {
- shareInfoService.shareInfo.revalidate();
- }, [shareInfoService]);
- const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
- const sharedMode = useLiveData(shareInfoService.shareInfo.sharedMode$);
- const baseUrl = serverService.server.baseUrl;
- const isLoading =
- isSharedPage === null || sharedMode === null || baseUrl === null;
-
- const permissionService = useService(WorkspacePermissionService);
- const isOwner = useLiveData(permissionService.permission.isOwner$);
- const globalDialogService = useService(GlobalDialogService);
-
- const onOpenWorkspaceSettings = useCallback(() => {
- globalDialogService.open('setting', {
- activeTab: 'workspace:preference',
- workspaceMetadata: props.workspaceMetadata,
- });
- }, [globalDialogService, props.workspaceMetadata]);
-
- const onClickAnyoneReadOnlyShare = useAsyncCallback(async () => {
- if (isSharedPage) {
- return;
- }
- try {
- // TODO(@JimmFly): remove mode when we have a better way to handle it
- await shareInfoService.shareInfo.enableShare(PublicPageMode.Page);
- track.$.sharePanel.$.createShareLink();
- notify.success({
- title:
- t[
- 'com.affine.share-menu.create-public-link.notification.success.title'
- ](),
- message:
- t[
- 'com.affine.share-menu.create-public-link.notification.success.message'
- ](),
- style: 'normal',
- icon: ,
- });
- } catch (err) {
- notify.error({
- title:
- t[
- 'com.affine.share-menu.confirm-modify-mode.notification.fail.title'
- ](),
- message:
- t[
- 'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
- ](),
- });
- console.error(err);
- }
- }, [isSharedPage, shareInfoService.shareInfo, t]);
-
- const onDisablePublic = useAsyncCallback(async () => {
- try {
- await shareInfoService.shareInfo.disableShare();
- notify.error({
- title:
- t[
- 'com.affine.share-menu.disable-publish-link.notification.success.title'
- ](),
- message:
- t[
- 'com.affine.share-menu.disable-publish-link.notification.success.message'
- ](),
- });
- } catch (err) {
- notify.error({
- title:
- t[
- 'com.affine.share-menu.disable-publish-link.notification.fail.title'
- ](),
- message:
- t[
- 'com.affine.share-menu.disable-publish-link.notification.fail.message'
- ](),
- });
- console.log(err);
- }
- }, [shareInfoService, t]);
-
- if (isLoading) {
- // TODO(@eyhn): loading and error UI
- return (
- <>
-
-
- >
- );
- }
-
- return (
-
-
- {isSharedPage
- ? t['com.affine.share-menu.option.link.readonly.description']()
- : t['com.affine.share-menu.option.link.no-access.description']()}
-
-
-
-
- {t['com.affine.share-menu.option.link.label']()}
-
-
-
-
-
- {t['com.affine.share-menu.option.permission.label']()}
-
-
-
-
- {isOwner && (
-
-
- {t['com.affine.share-menu.navigate.workspace']()}
-
- )}
-
-
- );
-};
-
-export const SharePage = (props: ShareMenuProps) => {
- if (props.workspaceMetadata.flavour === 'local') {
- return ;
- } else {
- return (
- // TODO(@eyhn): refactor this part
-
-
-
-
-
- );
- }
-};
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
index 6e1d1bc54c73e..e4a1284242c20 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx
@@ -6,7 +6,6 @@ import {
MenuSub,
} from '@affine/component/ui/menu';
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
-import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
@@ -17,6 +16,7 @@ import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/worksp
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { EditorService } from '@affine/core/modules/editor';
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
+import { ShareMenuContent } from '@affine/core/modules/share-menu';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import { WorkspaceService } from '@affine/core/modules/workspace';
diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx
index 2aba7fdf03035..6576ae65e2e9f 100644
--- a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx
@@ -1,6 +1,5 @@
import { Avatar, ConfirmModal, Input, notify, Switch } from '@affine/component';
import type { ConfirmModalProps } from '@affine/component/ui/modal';
-import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService, ServersService } from '@affine/core/modules/cloud';
import {
@@ -9,6 +8,7 @@ import {
GlobalDialogService,
} from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
+import { CloudSvg } from '@affine/core/modules/share-menu';
import { WorkspacesService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx
index f599dbdfdb559..2c94bdc6bb078 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-header.tsx
@@ -5,7 +5,6 @@ import {
observeResize,
useDraggable,
} from '@affine/component';
-import { SharePageButton } from '@affine/core/components/affine/share-page-modal';
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
import { InfoButton } from '@affine/core/components/blocksuite/block-suite-header/info';
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
@@ -19,6 +18,7 @@ import { HeaderDivider } from '@affine/core/components/pure/header';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { EditorService } from '@affine/core/modules/editor';
import { JournalService } from '@affine/core/modules/journal';
+import { SharePageButton } from '@affine/core/modules/share-menu';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import type { Workspace } from '@affine/core/modules/workspace';
import type { AffineDNDData } from '@affine/core/types/dnd';
diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts
index 88b1569d5ecde..b3cb274acc555 100644
--- a/packages/frontend/core/src/modules/index.ts
+++ b/packages/frontend/core/src/modules/index.ts
@@ -35,6 +35,7 @@ import { configurePeekViewModule } from './peek-view';
import { configurePermissionsModule } from './permissions';
import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc';
+import { configureShareMenuModule } from './share-menu';
import { configureShareSettingModule } from './share-setting';
import {
configureCommonGlobalStorageImpls,
@@ -96,4 +97,5 @@ export function configureCommonModules(framework: Framework) {
configureAINetworkSearchModule(framework);
configureAIButtonModule(framework);
configureTemplateDocModule(framework);
+ configureShareMenuModule(framework);
}
diff --git a/packages/frontend/core/src/modules/share-menu/index.ts b/packages/frontend/core/src/modules/share-menu/index.ts
new file mode 100644
index 0000000000000..0cb36e194ec96
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/index.ts
@@ -0,0 +1,11 @@
+import { type Framework } from '@toeverything/infra';
+
+import { WorkspaceScope } from '../workspace';
+import { ShareMenuService } from './services/share-menu';
+
+export function configureShareMenuModule(framework: Framework) {
+ framework.scope(WorkspaceScope).service(ShareMenuService);
+}
+
+export { ShareMenuService, type ShareMenuTab } from './services/share-menu';
+export { CloudSvg, ShareMenuContent, SharePageButton } from './view';
diff --git a/packages/frontend/core/src/modules/share-menu/services/share-menu.tsx b/packages/frontend/core/src/modules/share-menu/services/share-menu.tsx
new file mode 100644
index 0000000000000..070b0c9a61ffd
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/services/share-menu.tsx
@@ -0,0 +1,46 @@
+import { LiveData, Service } from '@toeverything/infra';
+
+import type { Member } from '../../permissions';
+
+export enum ShareMenuTab {
+ Share = 'share',
+ Export = 'export',
+ Invite = 'invite',
+ Members = 'members',
+}
+
+export class ShareMenuService extends Service {
+ readonly currentTab$ = new LiveData(ShareMenuTab.Share);
+
+ constructor() {
+ super();
+ }
+
+ query$ = new LiveData('');
+ selectedMembers$ = new LiveData([]);
+
+ switchTab(tab: ShareMenuTab) {
+ this.currentTab$.next(tab);
+ }
+
+ setQuery(query: string) {
+ this.query$.next(query);
+ }
+
+ addToSelectedMembers(member: Member) {
+ // filter out duplicates
+ if (!this.selectedMembers$.value.some(m => m.id === member.id)) {
+ this.selectedMembers$.next([...this.selectedMembers$.value, member]);
+ }
+ }
+
+ removeFromSelectedMembers(memberId: string) {
+ this.selectedMembers$.next(
+ this.selectedMembers$.value.filter(member => member.id !== memberId)
+ );
+ }
+
+ clearSelectedMembers() {
+ this.selectedMembers$.next([]);
+ }
+}
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/cloud-svg.tsx b/packages/frontend/core/src/modules/share-menu/view/cloud-svg.tsx
similarity index 100%
rename from packages/frontend/core/src/components/affine/share-page-modal/cloud-svg.tsx
rename to packages/frontend/core/src/modules/share-menu/view/cloud-svg.tsx
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx b/packages/frontend/core/src/modules/share-menu/view/index.tsx
similarity index 91%
rename from packages/frontend/core/src/components/affine/share-page-modal/index.tsx
rename to packages/frontend/core/src/modules/share-menu/view/index.tsx
index 7b4cec49e770a..40af8cfcc95de 100644
--- a/packages/frontend/core/src/components/affine/share-page-modal/index.tsx
+++ b/packages/frontend/core/src/modules/share-menu/view/index.tsx
@@ -5,6 +5,8 @@ import type { Store } from '@blocksuite/affine/store';
import { useCallback } from 'react';
import { ShareMenu } from './share-menu';
+export { CloudSvg } from './cloud-svg';
+export { ShareMenuContent } from './share-menu';
type SharePageModalProps = {
workspace: Workspace;
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.css.ts
new file mode 100644
index 0000000000000..740d0c2b229d2
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.css.ts
@@ -0,0 +1,111 @@
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
+
+export const copyLinkContainerStyle = style({
+ padding: '4px',
+ display: 'flex',
+ alignItems: 'center',
+ width: '100%',
+ position: 'relative',
+ selectors: {
+ '&.secondary': {
+ padding: 0,
+ marginTop: '12px',
+ },
+ },
+});
+export const copyLinkButtonStyle = style({
+ flex: 1,
+ padding: '4px 12px',
+ paddingRight: '6px',
+ borderRight: 'none',
+ borderTopRightRadius: '0',
+ borderBottomRightRadius: '0',
+ color: 'transparent',
+ position: 'initial',
+ selectors: {
+ '&.dark': {
+ backgroundColor: cssVarV2('layer/pureBlack'),
+ },
+ '&.dark::hover': {
+ backgroundColor: cssVarV2('layer/pureBlack'),
+ },
+ },
+});
+export const copyLinkLabelContainerStyle = style({
+ width: '100%',
+ borderRight: 'none',
+ borderTopRightRadius: '0',
+ borderBottomRightRadius: '0',
+ position: 'relative',
+});
+export const copyLinkLabelStyle = style({
+ position: 'absolute',
+ textAlign: 'end',
+ top: '50%',
+ left: '50%',
+ transform: 'translateX(-50%) translateY(-50%)',
+ lineHeight: '20px',
+ color: cssVarV2('text/pureWhite'),
+ selectors: {
+ '&.secondary': {
+ color: cssVarV2('text/primary'),
+ },
+ },
+});
+export const copyLinkShortcutStyle = style({
+ position: 'absolute',
+ textAlign: 'end',
+ top: '50%',
+ right: '52px',
+ transform: 'translateY(-50%)',
+ opacity: 0.5,
+ lineHeight: '20px',
+ color: cssVarV2('text/pureWhite'),
+ selectors: {
+ '&.secondary': {
+ color: cssVarV2('text/secondary'),
+ },
+ },
+});
+export const copyLinkTriggerStyle = style({
+ padding: '4px 12px 4px 8px',
+ borderLeft: 'none',
+ borderTopLeftRadius: '0',
+ borderBottomLeftRadius: '0',
+ ':hover': {
+ backgroundColor: cssVarV2('button/primary'),
+ color: cssVarV2('button/pureWhiteText'),
+ },
+ '::after': {
+ content: '""',
+ position: 'absolute',
+ left: '0',
+ top: '0',
+ height: '100%',
+ width: '1px',
+ backgroundColor: cssVarV2('button/innerBlackBorder'),
+ },
+ selectors: {
+ '&.secondary': {
+ backgroundColor: cssVarV2('button/secondary'),
+ color: cssVarV2('text/secondary'),
+ },
+ '&.secondary:hover': {
+ backgroundColor: cssVarV2('button/secondary'),
+ color: cssVarV2('text/secondary'),
+ },
+ },
+});
+globalStyle(`${copyLinkTriggerStyle} svg`, {
+ color: cssVarV2('button/pureWhiteText'),
+ transform: 'translateX(2px)',
+});
+globalStyle(`${copyLinkTriggerStyle}.secondary svg`, {
+ color: cssVarV2('text/secondary'),
+ transform: 'translateX(2px)',
+});
+export const copyLinkMenuItemStyle = style({
+ padding: '4px',
+ transition: 'all 0.3s',
+});
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/copy-link-button.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx
similarity index 98%
rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/copy-link-button.tsx
rename to packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx
index 787dd1e9176ff..ecdec749f6f02 100644
--- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/copy-link-button.tsx
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/copy-link-button.tsx
@@ -11,7 +11,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
-import * as styles from './index.css';
+import * as styles from './copy-link-button.css';
export const CopyLinkButton = ({
workspaceId,
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/index.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/index.ts
new file mode 100644
index 0000000000000..76124f4ff0115
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/index.ts
@@ -0,0 +1,2 @@
+export * from './members-permission';
+export * from './public-page-button';
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx
new file mode 100644
index 0000000000000..bb7dffb97049f
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx
@@ -0,0 +1,91 @@
+import { Menu, MenuItem, MenuTrigger } from '@affine/component';
+import { useI18n } from '@affine/i18n';
+import { useCallback, useState } from 'react';
+
+import * as styles from './styles.css';
+
+enum MockDocPermission {
+ Edit = 'edit',
+ Read = 'read',
+ Manage = 'manage',
+}
+// TODO(@JimmFly): impl the real permission
+export const MembersPermission = () => {
+ const t = useI18n();
+ const [permission, setPermission] = useState(
+ MockDocPermission.Manage
+ );
+
+ const changePermission = useCallback((newPermission: MockDocPermission) => {
+ setPermission(newPermission);
+ }, []);
+
+ const selectManage = useCallback(() => {
+ changePermission(MockDocPermission.Manage);
+ }, [changePermission]);
+
+ const selectEdit = useCallback(() => {
+ changePermission(MockDocPermission.Edit);
+ }, [changePermission]);
+
+ const selectRead = useCallback(() => {
+ changePermission(MockDocPermission.Read);
+ }, [changePermission]);
+ return (
+
+
+ {t['com.affine.share-menu.option.permission.label']()}
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx
new file mode 100644
index 0000000000000..8a2df39cdcda4
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx
@@ -0,0 +1,139 @@
+import { Menu, MenuItem, MenuTrigger, notify } from '@affine/component';
+import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
+import { ShareInfoService } from '@affine/core/modules/share-doc';
+import { PublicPageMode } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import track from '@affine/track';
+import {
+ LockIcon,
+ SingleSelectCheckSolidIcon,
+ ViewIcon,
+} from '@blocksuite/icons/rc';
+import { useLiveData, useService } from '@toeverything/infra';
+import { cssVar } from '@toeverything/theme';
+import { useEffect } from 'react';
+
+import * as styles from './styles.css';
+
+export const PublicDoc = () => {
+ const t = useI18n();
+ const shareInfoService = useService(ShareInfoService);
+ const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
+
+ useEffect(() => {
+ shareInfoService.shareInfo.revalidate();
+ }, [shareInfoService]);
+
+ const onDisablePublic = useAsyncCallback(async () => {
+ try {
+ await shareInfoService.shareInfo.disableShare();
+ notify.error({
+ title:
+ t[
+ 'com.affine.share-menu.disable-publish-link.notification.success.title'
+ ](),
+ message:
+ t[
+ 'com.affine.share-menu.disable-publish-link.notification.success.message'
+ ](),
+ });
+ } catch (err) {
+ notify.error({
+ title:
+ t[
+ 'com.affine.share-menu.disable-publish-link.notification.fail.title'
+ ](),
+ message:
+ t[
+ 'com.affine.share-menu.disable-publish-link.notification.fail.message'
+ ](),
+ });
+ console.log(err);
+ }
+ }, [shareInfoService, t]);
+
+ const onClickAnyoneReadOnlyShare = useAsyncCallback(async () => {
+ if (isSharedPage) {
+ return;
+ }
+ try {
+ // TODO(@JimmFly): remove mode when we have a better way to handle it
+ await shareInfoService.shareInfo.enableShare(PublicPageMode.Page);
+ track.$.sharePanel.$.createShareLink();
+ notify.success({
+ title:
+ t[
+ 'com.affine.share-menu.create-public-link.notification.success.title'
+ ](),
+ message:
+ t[
+ 'com.affine.share-menu.create-public-link.notification.success.message'
+ ](),
+ style: 'normal',
+ icon: ,
+ });
+ } catch (err) {
+ notify.error({
+ title:
+ t[
+ 'com.affine.share-menu.confirm-modify-mode.notification.fail.title'
+ ](),
+ message:
+ t[
+ 'com.affine.share-menu.confirm-modify-mode.notification.fail.message'
+ ](),
+ });
+ console.error(err);
+ }
+ }, [isSharedPage, shareInfoService.shareInfo, t]);
+
+ return (
+
+
+ {t['com.affine.share-menu.option.link.label']()}
+
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/styles.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/styles.css.ts
new file mode 100644
index 0000000000000..956409c2aef85
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/styles.css.ts
@@ -0,0 +1,39 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const menuTriggerStyle = style({
+ padding: '4px 10px',
+ borderRadius: '4px',
+ justifyContent: 'space-between',
+ display: 'flex',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 400,
+});
+
+export const rowContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '4px',
+});
+export const exportContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+});
+export const labelStyle = style({
+ fontSize: cssVar('fontSm'),
+ fontWeight: 500,
+});
+export const publicItemRowStyle = style({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+});
+export const DoneIconStyle = style({
+ color: cssVarV2('button/primary'),
+ fontSize: cssVar('fontH5'),
+ marginLeft: '8px',
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/index.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/index.css.ts
new file mode 100644
index 0000000000000..e42cc2b3f797b
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/index.css.ts
@@ -0,0 +1,131 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
+export const headerStyle = style({
+ display: 'flex',
+ alignItems: 'center',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 600,
+ lineHeight: '22px',
+ padding: '0 4px',
+ gap: '4px',
+});
+export const content = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+});
+export const menuStyle = style({
+ width: '390px',
+ minHeight: '310px',
+ maxHeight: '562px',
+ padding: '12px',
+});
+export const menuTriggerStyle = style({
+ width: '150px',
+ padding: '4px 10px',
+ justifyContent: 'space-between',
+});
+export const exportItemStyle = style({
+ padding: '4px',
+ transition: 'all 0.3s',
+ gap: '0px',
+});
+globalStyle(`${exportItemStyle} > div:first-child`, {
+ alignItems: 'center',
+});
+globalStyle(`${exportItemStyle} svg`, {
+ width: '16px',
+ height: '16px',
+});
+
+export const descriptionStyle = style({
+ wordWrap: 'break-word',
+ fontSize: cssVar('fontXs'),
+ lineHeight: '20px',
+ color: cssVarV2('text/secondary'),
+ textAlign: 'left',
+ padding: '0 6px',
+});
+export const containerStyle = style({
+ display: 'flex',
+ width: '100%',
+ flexDirection: 'column',
+ gap: '8px',
+});
+export const indicatorContainerStyle = style({
+ position: 'relative',
+});
+export const columnContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ width: '100%',
+ gap: '8px',
+});
+export const exportContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+});
+export const labelStyle = style({
+ fontSize: cssVar('fontSm'),
+ fontWeight: 500,
+});
+export const disableSharePage = style({
+ color: cssVarV2('button/error'),
+});
+export const localSharePage = style({
+ padding: '12px 8px',
+ display: 'flex',
+ alignItems: 'center',
+ borderRadius: '8px',
+ backgroundColor: cssVarV2('layer/background/secondary'),
+ minHeight: '84px',
+ position: 'relative',
+});
+export const cloudSvgContainer = style({
+ width: '146px',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ position: 'absolute',
+ bottom: '0',
+ right: '0',
+});
+export const shareLinkStyle = style({
+ padding: '4px',
+ fontSize: cssVar('fontXs'),
+ fontWeight: 500,
+ lineHeight: '20px',
+ transform: 'translateX(-4px)',
+ gap: '4px',
+});
+globalStyle(`${shareLinkStyle} > span`, {
+ color: cssVarV2('text/link'),
+});
+globalStyle(`${shareLinkStyle} > div > svg`, {
+ color: cssVarV2('text/link'),
+});
+export const buttonContainer = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ fontWeight: 500,
+});
+export const button = style({
+ padding: '6px 8px',
+ height: 32,
+});
+export const shortcutStyle = style({
+ fontSize: cssVar('fontXs'),
+ color: cssVarV2('text/secondary'),
+ fontWeight: 400,
+});
+
+export const generalAccessStyle = style({
+ padding: '4px',
+ fontSize: cssVar('fontSm'),
+ color: cssVarV2('text/secondary'),
+ fontWeight: 500,
+});
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.jotai.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/index.jotai.ts
similarity index 100%
rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.jotai.ts
rename to packages/frontend/core/src/modules/share-menu/view/share-menu/index.jotai.ts
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/index.tsx
similarity index 100%
rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.tsx
rename to packages/frontend/core/src/modules/share-menu/view/share-menu/index.tsx
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/index.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/index.tsx
new file mode 100644
index 0000000000000..6e41ad493793e
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/index.tsx
@@ -0,0 +1,29 @@
+import { Input } from '@affine/component';
+import { useI18n } from '@affine/i18n';
+import { SearchIcon } from '@blocksuite/icons/rc';
+import { useService } from '@toeverything/infra';
+import { useCallback } from 'react';
+
+import { ShareMenuService, ShareMenuTab } from '../../../services/share-menu';
+import * as styles from './styles.css';
+
+export const InviteInput = () => {
+ const shareMenuService = useService(ShareMenuService);
+ const t = useI18n();
+ const handleFocus = useCallback(
+ () => shareMenuService.switchTab(ShareMenuTab.Invite),
+ [shareMenuService]
+ );
+
+ return (
+ }
+ className={styles.inputStyle}
+ onFocus={handleFocus}
+ inputStyle={{
+ paddingLeft: '0',
+ }}
+ placeholder={t['com.affine.share-menu.invite-editor.placeholder']()}
+ />
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.css.ts
new file mode 100644
index 0000000000000..14f98f2a6f738
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.css.ts
@@ -0,0 +1,118 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const containerStyle = style({
+ display: 'flex',
+ width: '100%',
+ flexDirection: 'column',
+ gap: '8px',
+ height: '100%',
+ flex: 1,
+});
+
+export const headerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`,
+ cursor: 'pointer',
+ gap: '4px',
+ padding: '4px 4px 6px',
+ color: cssVarV2('text/secondary'),
+});
+export const iconStyle = style({
+ fontSize: '20px',
+ color: cssVarV2('icon/primary'),
+});
+
+export const memberListStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+ overflowY: 'auto',
+ flex: 1,
+});
+
+export const footerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ borderTop: `1px solid ${cssVarV2('tab/divider/divider')}`,
+ paddingTop: '8px',
+});
+export const manageMemberStyle = style({
+ color: cssVarV2('text/link'),
+ cursor: 'pointer',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 500,
+ padding: '5px 4px',
+});
+
+export const searchInput = style({
+ flexGrow: 1,
+ padding: '10px 0',
+ margin: '-10px 0',
+ border: 'none',
+ outline: 'none',
+ fontSize: '14px',
+ fontFamily: 'inherit',
+ color: 'inherit',
+ backgroundColor: 'transparent',
+ '::placeholder': {
+ color: cssVarV2('text/placeholder'),
+ },
+});
+
+export const InputContainer = style({
+ display: 'flex',
+ gap: '4px',
+ borderRadius: '4px',
+ padding: '4px',
+ flexWrap: 'wrap',
+ width: '100%',
+ border: `1px solid ${cssVarV2('input/border/default')}`,
+
+ selectors: {
+ '&.focus': {
+ borderColor: cssVarV2('input/border/active'),
+ },
+ },
+});
+export const inlineMembersContainer = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ width: '100%',
+ flex: 1,
+ gap: '4px',
+});
+export const roleSelectorContainer = style({
+ flexShrink: 0,
+});
+
+export const menuTriggerStyle = style({
+ padding: '1px 2px',
+ gap: '4px',
+ height: '22px',
+ borderRadius: '2px',
+ justifyContent: 'space-between',
+ display: 'flex',
+ fontSize: cssVar('fontXs'),
+ fontWeight: 400,
+});
+
+export const buttonsContainer = style({
+ display: 'flex',
+ flexDirection: 'row',
+ gap: '12px',
+ flexShrink: 0,
+});
+
+export const button = style({
+ padding: '4px 12px',
+ borderRadius: '4px',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 500,
+ display: 'flex',
+ alignItems: 'center',
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx
new file mode 100644
index 0000000000000..4027deee85483
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx
@@ -0,0 +1,211 @@
+import {
+ Button,
+ Menu,
+ MenuItem,
+ MenuTrigger,
+ RowInput,
+} from '@affine/component';
+import type { Member } from '@affine/core/modules/permissions';
+import { Permission, WorkspaceMemberStatus } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
+import { useLiveData, useService } from '@toeverything/infra';
+import clsx from 'clsx';
+import { useCallback, useRef, useState } from 'react';
+
+import { ShareMenuService, ShareMenuTab } from '../../../services/share-menu';
+import * as styles from './invite-member-editor.css';
+import { MemberItem } from './member-item';
+import { SelectedMemberItem } from './selected-member-item';
+
+const mockMembers: Member[] = [
+ {
+ id: '2',
+ name: 'Member 1',
+ avatarUrl: '',
+ email: 'fakeemail@gamicl.com',
+ permission: Permission.Owner,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '3',
+ name: 'Member 2',
+ avatarUrl: '',
+ email: 'testloasnodknaksldnalkndlkasnd@gamil.com',
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '4',
+ name: 'loansodinsaodjsalkjdlkasnlkdnaslkdnl kasndlkaskldaslkdnalskndlkasn',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Read,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+];
+
+export const InviteMemberEditor = () => {
+ const t = useI18n();
+ const shareMenuService = useService(ShareMenuService);
+ const selectedMembers = useLiveData(shareMenuService.selectedMembers$);
+
+ const inputRef = useRef(null);
+ const [focused, setFocused] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const onInputChange = useCallback((value: string) => {
+ setInputValue(value);
+ }, []);
+ const focusInput = useCallback(() => {
+ inputRef.current?.focus();
+ }, []);
+ const onFocus = useCallback(() => {
+ setFocused(true);
+ }, []);
+ const onBlur = useCallback(() => {
+ setFocused(false);
+ }, []);
+ const handleRemoved = useCallback(
+ (memberId: string) => {
+ shareMenuService.removeFromSelectedMembers(memberId);
+ focusInput();
+ },
+ [shareMenuService, focusInput]
+ );
+
+ const switchToShareTab = useCallback(() => {
+ shareMenuService.setQuery('');
+ shareMenuService.switchTab(ShareMenuTab.Share);
+ }, [shareMenuService]);
+ const switchToMemberManagementTab = useCallback(() => {
+ shareMenuService.setQuery('');
+ shareMenuService.switchTab(ShareMenuTab.Members);
+ }, [shareMenuService]);
+
+ return (
+
+
+
+
+
+ {selectedMembers.map((member, idx) => {
+ if (!member) {
+ return null;
+ }
+ const onRemoved = () => handleRemoved(member.id);
+ return (
+
+ );
+ })}
+
+
+ {!selectedMembers.length ? null : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {t['com.affine.share-menu.invite-editor.manage-members']()}
+
+
+
+
+
+
+
+ );
+};
+
+// TODO(@JimmFly): handle overflow
+const Result = () => {
+ const shareMenuService = useService(ShareMenuService);
+
+ return (
+ <>
+ {mockMembers.map(member => {
+ const handleSelect = () => {
+ shareMenuService.addToSelectedMembers(member);
+ };
+ return (
+
+
+
+ );
+ })}
+ >
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.css.ts
new file mode 100644
index 0000000000000..0db774b9aba1c
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.css.ts
@@ -0,0 +1,68 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const memberItemStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '4px',
+ width: '100%',
+ gap: '12px',
+ cursor: 'pointer',
+ selectors: {
+ '&:hover': {
+ backgroundColor: cssVarV2('layer/background/hoverOverlay'),
+ borderRadius: '4px',
+ },
+ },
+});
+
+export const memberContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '12px',
+ flex: 1,
+ overflow: 'hidden',
+ width: '100%',
+});
+
+export const memberInfoStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ width: '100%',
+ overflow: 'hidden',
+});
+
+export const memberNameStyle = style({
+ color: cssVarV2('text/primary'),
+ fontSize: cssVar('fontSm'),
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const memberEmailStyle = style({
+ color: cssVarV2('text/secondary'),
+ fontSize: cssVar('fontXs'),
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const memberRoleStyle = style({
+ color: cssVarV2('text/primary'),
+ fontSize: cssVar('fontSm'),
+ flexShrink: 0,
+ selectors: {
+ '&.disable': {
+ color: cssVarV2('text/disable'),
+ },
+ },
+});
+
+export const tooltipContentStyle = style({
+ wordBreak: 'break-word',
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.tsx
new file mode 100644
index 0000000000000..f3339e0d173f2
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/member-item.tsx
@@ -0,0 +1,58 @@
+import { Avatar, Tooltip } from '@affine/component';
+import type { Member } from '@affine/core/modules/permissions';
+import { Permission } from '@affine/graphql';
+import clsx from 'clsx';
+import { useMemo } from 'react';
+
+import * as styles from './member-item.css';
+
+export const MemberItem = ({ member }: { member: Member }) => {
+ const role = useMemo(() => {
+ switch (member.permission) {
+ case Permission.Owner:
+ return 'Owner';
+ case Permission.Admin:
+ return 'Can manage';
+ case Permission.Write:
+ return 'Can edit';
+ case Permission.Read:
+ return 'Can read';
+ default:
+ return '';
+ }
+ }, [member.permission]);
+
+ return (
+
+
+
+
+
+ {member.name}
+
+
+ {member.email}
+
+
+
+
{role}
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.css.ts
new file mode 100644
index 0000000000000..380c9ea380dcc
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.css.ts
@@ -0,0 +1,54 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const member = style({
+ height: '22px',
+ display: 'flex',
+ minWidth: 0,
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ ':last-child': {
+ minWidth: 'max-content',
+ },
+});
+
+export const memberInnerWrapper = style({
+ fontSize: 'inherit',
+ borderRadius: '2px',
+ columnGap: '4px',
+ borderWidth: '1px',
+ borderStyle: 'solid',
+ background: cssVar('backgroundPrimaryColor'),
+ maxWidth: '128px',
+ height: '100%',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '1px 2px',
+ color: cssVarV2('text/primary'),
+ borderColor: cssVarV2('layer/insideBorder/blackBorder'),
+});
+
+export const label = style({
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ userSelect: 'none',
+});
+
+export const remove = style({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 14,
+ height: 14,
+ borderRadius: '2px',
+ flexShrink: 0,
+ cursor: 'pointer',
+ ':hover': {
+ background: 'var(--affine-hover-color)',
+ },
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.tsx
new file mode 100644
index 0000000000000..bc61f5342c8ab
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/selected-member-item.tsx
@@ -0,0 +1,45 @@
+import type { Member } from '@affine/core/modules/permissions';
+import { CloseIcon } from '@blocksuite/icons/rc';
+import { type MouseEventHandler, useCallback } from 'react';
+
+import * as styles from './selected-member-item.css';
+
+export interface TagItemProps {
+ member: Member;
+ idx?: number;
+ onRemoved?: () => void;
+ style?: React.CSSProperties;
+}
+
+export const SelectedMemberItem = ({
+ member,
+ idx,
+ onRemoved,
+ style,
+}: TagItemProps) => {
+ const handleRemove: MouseEventHandler = useCallback(
+ e => {
+ e.stopPropagation();
+ onRemoved?.();
+ },
+ [onRemoved]
+ );
+ return (
+
+
+
{member.name}
+ {onRemoved ? (
+
+
+
+ ) : null}
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/styles.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/styles.css.ts
new file mode 100644
index 0000000000000..882ad1ba618db
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/styles.css.ts
@@ -0,0 +1,7 @@
+import { style } from '@vanilla-extract/css';
+
+export const inputStyle = style({
+ marginTop: '6px',
+ padding: '4px',
+ gap: '4px',
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/index.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/index.tsx
new file mode 100644
index 0000000000000..1152a7adc591e
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/index.tsx
@@ -0,0 +1,108 @@
+import { Avatar, Tooltip } from '@affine/component';
+import type { Member } from '@affine/core/modules/permissions';
+import { useI18n } from '@affine/i18n';
+import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
+import { useService } from '@toeverything/infra';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import clsx from 'clsx';
+import { useCallback, useMemo } from 'react';
+
+import { ShareMenuService, ShareMenuTab } from '../../../services/share-menu';
+import * as styles from './styles.css';
+
+export { MemberManagement } from './member-management';
+
+export const MembersRow = ({
+ docOwner,
+ members,
+}: {
+ docOwner: Member;
+ members: Member[];
+}) => {
+ const t = useI18n();
+ const shareMenuService = useService(ShareMenuService);
+ const handleClick = useCallback(() => {
+ shareMenuService.switchTab(ShareMenuTab.Members);
+ }, [shareMenuService]);
+
+ const topThreeMembers = useMemo(
+ () =>
+ members.slice(0, Math.min(3, members.length)).map(member => ({
+ name: member.name || member.email || member.id,
+ avatarUrl: member.avatarUrl,
+ id: member.id,
+ })),
+ [members]
+ );
+
+ const description = useMemo(() => {
+ if (members.length <= 1) {
+ return '';
+ }
+ switch (members.length) {
+ case 2:
+ return t['com.affine.share-menu.member-management.member-count-2']({
+ member1: topThreeMembers[0].name,
+ member2: topThreeMembers[1].name,
+ });
+ case 3:
+ return t['com.affine.share-menu.member-management.member-count-3']({
+ member1: topThreeMembers[0].name,
+ member2: topThreeMembers[1].name,
+ member3: topThreeMembers[2].name,
+ });
+ default:
+ return t['com.affine.share-menu.member-management.member-count-more']({
+ member1: topThreeMembers[0].name,
+ member2: topThreeMembers[1].name,
+ memberCount: (members.length - 2).toString(),
+ });
+ }
+ }, [members.length, t, topThreeMembers]);
+
+ if (members.length > 1) {
+ return (
+
+
+
+
+ {topThreeMembers.map((member, index) => (
+
+ ))}
+
+
{description}
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.css.ts
new file mode 100644
index 0000000000000..acecb6a202741
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.css.ts
@@ -0,0 +1,71 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const memberItemStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '4px',
+ width: '100%',
+ gap: '12px',
+});
+
+export const memberContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '12px',
+ flex: 1,
+ overflow: 'hidden',
+ width: '100%',
+});
+
+export const memberInfoStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ width: '100%',
+ overflow: 'hidden',
+});
+
+export const memberNameStyle = style({
+ color: cssVarV2('text/primary'),
+ fontSize: cssVar('fontSm'),
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const memberEmailStyle = style({
+ color: cssVarV2('text/secondary'),
+ fontSize: cssVar('fontXs'),
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const memberRoleStyle = style({
+ color: cssVarV2('text/primary'),
+ fontSize: cssVar('fontSm'),
+ flexShrink: 0,
+ selectors: {
+ '&.disable': {
+ color: cssVarV2('text/disable'),
+ },
+ },
+});
+
+export const tooltipContentStyle = style({
+ wordBreak: 'break-word',
+});
+
+export const menuTriggerStyle = style({
+ padding: '4px',
+ paddingRight: '0',
+ borderRadius: '4px',
+ gap: '4px',
+ display: 'flex',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 400,
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx
new file mode 100644
index 0000000000000..0b6a3d7e9aac3
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx
@@ -0,0 +1,147 @@
+import {
+ Avatar,
+ Menu,
+ MenuItem,
+ MenuSeparator,
+ MenuTrigger,
+ Tooltip,
+} from '@affine/component';
+import type { Member } from '@affine/core/modules/permissions';
+import { Permission } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import clsx from 'clsx';
+import { useMemo } from 'react';
+
+import * as styles from './member-item.css';
+
+export const MemberItem = ({ member }: { member: Member }) => {
+ const isOwner = true;
+ const isManager = false;
+
+ const role = useMemo(() => {
+ switch (member.permission) {
+ case Permission.Owner:
+ return 'Owner';
+ case Permission.Admin:
+ return 'Can manage';
+ case Permission.Write:
+ return 'Can edit';
+ case Permission.Read:
+ return 'Can read';
+ default:
+ return '';
+ }
+ }, [member.permission]);
+
+ return (
+
+
+
+
+
+ {member.name}
+
+
+ {member.email}
+
+
+
+
+ {(!isOwner && !isManager) || member.permission === Permission.Owner ? (
+
{role}
+ ) : (
+
}
+ contentOptions={{
+ align: 'start',
+ }}
+ >
+
+ {role}
+
+
+ )}
+
+ );
+};
+
+const Options = ({ memberPermission }: { memberPermission: Permission }) => {
+ const t = useI18n();
+ const isOwner = true;
+ const isManager = false;
+ const isOwnerOrManager = isOwner || isManager;
+ const operationButtonInfo = useMemo(() => {
+ return [
+ {
+ label: t['com.affine.share-menu.option.permission.can-manage'](),
+ onClick: () => {},
+ permission: Permission.Admin,
+ show: isOwnerOrManager,
+ },
+ {
+ label: t['com.affine.share-menu.option.permission.can-edit'](),
+ onClick: () => {},
+ permission: Permission.Write,
+ show: isOwnerOrManager,
+ },
+ {
+ label: t['com.affine.share-menu.option.permission.can-read'](),
+ onClick: () => {},
+ permission: Permission.Read,
+ show: isOwnerOrManager,
+ },
+ {
+ label: t['com.affine.share-menu.member-management.remove'](),
+ onClick: () => {},
+ show: isOwnerOrManager,
+ },
+ ];
+ }, [isOwnerOrManager, t]);
+
+ return (
+ <>
+ {operationButtonInfo.map(item =>
+ item.show ? (
+
+ ) : null
+ )}
+ {isOwner ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.css.ts
new file mode 100644
index 0000000000000..6c2dd2b54738d
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.css.ts
@@ -0,0 +1,48 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const containerStyle = style({
+ display: 'flex',
+ width: '100%',
+ flexDirection: 'column',
+ gap: '8px',
+ height: '100%',
+ flex: 1,
+});
+
+export const headerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderBottom: `1px solid ${cssVarV2('tab/divider/divider')}`,
+ cursor: 'pointer',
+ gap: '4px',
+ padding: '4px 4px 6px',
+ color: cssVarV2('text/secondary'),
+});
+export const iconStyle = style({
+ fontSize: '20px',
+ color: cssVarV2('icon/primary'),
+});
+
+export const footerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ borderTop: `1px solid ${cssVarV2('tab/divider/divider')}`,
+ paddingTop: '8px',
+});
+export const addCollaboratorsStyle = style({
+ color: cssVarV2('text/link'),
+ cursor: 'pointer',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 500,
+ padding: '5px 4px',
+});
+
+export const memberListStyle = style({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ paddingTop: '6px',
+});
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.tsx
new file mode 100644
index 0000000000000..71c1d44db5378
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-management.tsx
@@ -0,0 +1,89 @@
+import type { Member } from '@affine/core/modules/permissions';
+import { Permission, WorkspaceMemberStatus } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
+import { useService } from '@toeverything/infra';
+import { useCallback } from 'react';
+
+import { ShareMenuService, ShareMenuTab } from '../../../services/share-menu';
+import { MemberItem } from './member-item';
+import * as styles from './member-management.css';
+
+const mockMembers: Member[] = [
+ {
+ id: '2',
+ name: 'Member 1',
+ avatarUrl: '',
+ email: 'fakeemail@gamicl.com',
+ permission: Permission.Owner,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '3',
+ name: 'Member 2',
+ avatarUrl: '',
+ email: 'testloasnodknaksldnalkndlkasnd@gamil.com',
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '4',
+ name: 'loansodinsaodjsalkjdlkasnlkdnaslkdnl kasndlkaskldaslkdnalskndlkasn',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Read,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+];
+
+// TODO(@JimmFly): Implement the member management page
+export const MemberManagement = () => {
+ const shareMenuService = useService(ShareMenuService);
+
+ const switchToShareTab = useCallback(() => {
+ shareMenuService.switchTab(ShareMenuTab.Share);
+ }, [shareMenuService]);
+ const switchToInviteTab = useCallback(() => {
+ shareMenuService.switchTab(ShareMenuTab.Invite);
+ }, [shareMenuService]);
+
+ const currentPermission = 'owner';
+ const t = useI18n();
+ return (
+
+
+
+ {t['com.affine.share-menu.member-management.header']({
+ memberCount: mockMembers.length.toString(),
+ })}
+
+
+ {currentPermission === 'owner' ? (
+
+
+ {t['com.affine.share-menu.member-management.add-collaborators']()}
+
+
+ ) : null}
+
+ );
+};
+
+const MemberList = () => {
+ return (
+
+ {mockMembers.map(member => {
+ return ;
+ })}
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/styles.css.ts b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/styles.css.ts
new file mode 100644
index 0000000000000..4bd87acb1b142
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/styles.css.ts
@@ -0,0 +1,77 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
+
+export const menuTriggerStyle = style({
+ width: '150px',
+ padding: '4px 10px',
+ justifyContent: 'space-between',
+});
+
+export const rowContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '4px',
+ selectors: {
+ '&.clickable:hover': {
+ backgroundColor: cssVarV2('layer/background/hoverOverlay'),
+ cursor: 'pointer',
+ borderRadius: '4px',
+ },
+ },
+});
+
+export const memberContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '8px',
+ fontSize: cssVar('fontSm'),
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ flex: 1,
+ overflow: 'hidden',
+});
+export const descriptionStyle = style({
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ width: '100%',
+});
+
+export const IconButtonStyle = style({
+ flexShrink: 0,
+ marginLeft: '8px',
+ fontSize: '20px',
+ display: 'flex',
+ alignItems: 'center',
+ color: cssVarV2('icon/primary'),
+});
+
+export const OwnerStyle = style({
+ color: cssVarV2('text/secondary'),
+ fontSize: cssVar('fontSm'),
+});
+
+export const avatarsContainerStyle = style({
+ display: 'flex',
+ flexDirection: 'row',
+});
+
+export const openWorkspaceSettingsStyle = style({
+ color: cssVarV2('text/link'),
+ fontSize: cssVar('fontXs'),
+ fontWeight: 500,
+ display: 'flex',
+ gap: '8px',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ width: '100%',
+ padding: '4px',
+ cursor: 'pointer',
+});
+globalStyle(`${openWorkspaceSettingsStyle} svg`, {
+ color: cssVarV2('text/link'),
+});
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/share-export.tsx
similarity index 100%
rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx
rename to packages/frontend/core/src/modules/share-menu/view/share-menu/share-export.tsx
diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/share-menu.tsx
similarity index 67%
rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx
rename to packages/frontend/core/src/modules/share-menu/view/share-menu/share-menu.tsx
index 3d4f616a2192f..b6f80141d3b00 100644
--- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/share-menu.tsx
@@ -7,9 +7,18 @@ import { useI18n } from '@affine/i18n';
import type { Store } from '@blocksuite/affine/store';
import { LockIcon, PublishIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
-import { forwardRef, type PropsWithChildren, type Ref, useEffect } from 'react';
+import {
+ forwardRef,
+ type PropsWithChildren,
+ type Ref,
+ useCallback,
+ useEffect,
+} from 'react';
+import { ShareMenuService, ShareMenuTab } from '../../services/share-menu';
import * as styles from './index.css';
+import { InviteMemberEditor } from './invite-member-editor/invite-member-editor';
+import { MemberManagement } from './member-management';
import { ShareExport } from './share-export';
import { SharePage } from './share-page';
@@ -22,21 +31,58 @@ export interface ShareMenuProps extends PropsWithChildren {
export const ShareMenuContent = (props: ShareMenuProps) => {
const t = useI18n();
+ const shareMenuService = useService(ShareMenuService);
+ const currentTab = useLiveData(shareMenuService.currentTab$);
+
+ const onValueChange = useCallback(
+ (value: string) => {
+ shareMenuService.switchTab(value as ShareMenuTab);
+ },
+ [shareMenuService]
+ );
+
+ if (currentTab === ShareMenuTab.Members) {
+ return ;
+ }
+ if (currentTab === ShareMenuTab.Invite) {
+ return ;
+ }
return (
-
+
-
+
{t['com.affine.share-menu.shareButton']()}
- {t['Export']()}
+
+ {t['Export']()}
+
+
+ invite
+
+
+ members
+
-
+
-
+
+
+ null
+
+
+ null
+
);
diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/share-page.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/share-page.tsx
new file mode 100644
index 0000000000000..0fd4c12b68ad4
--- /dev/null
+++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/share-page.tsx
@@ -0,0 +1,155 @@
+import { Skeleton } from '@affine/component';
+import { Button } from '@affine/component/ui/button';
+import { ServerService } from '@affine/core/modules/cloud';
+import { type Member } from '@affine/core/modules/permissions';
+import { ShareInfoService } from '@affine/core/modules/share-doc';
+import { Permission, WorkspaceMemberStatus } from '@affine/graphql';
+import { useI18n } from '@affine/i18n';
+import { useLiveData, useService } from '@toeverything/infra';
+import { Suspense, useEffect } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+import { CloudSvg } from '../cloud-svg';
+import { CopyLinkButton } from './copy-link-button';
+import { MembersPermission, PublicDoc } from './general-access';
+import * as styles from './index.css';
+import { InviteInput } from './invite-member-editor';
+import { MembersRow } from './member-management';
+import type { ShareMenuProps } from './share-menu';
+
+export const LocalSharePage = (props: ShareMenuProps) => {
+ const t = useI18n();
+ const {
+ workspaceMetadata: { id: workspaceId },
+ } = props;
+ return (
+ <>
+
+
+
+ {t['com.affine.share-menu.EnableCloudDescription']()}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const AFFiNESharePage = (props: ShareMenuProps) => {
+ const t = useI18n();
+ const {
+ workspaceMetadata: { id: workspaceId },
+ } = props;
+ const shareInfoService = useService(ShareInfoService);
+ const serverService = useService(ServerService);
+
+ useEffect(() => {
+ shareInfoService.shareInfo.revalidate();
+ }, [shareInfoService]);
+
+ const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
+ const sharedMode = useLiveData(shareInfoService.shareInfo.sharedMode$);
+ const baseUrl = serverService.server.baseUrl;
+ const isLoading =
+ isSharedPage === null || sharedMode === null || baseUrl === null;
+
+ if (isLoading) {
+ // TODO(@eyhn): loading and error UI
+ return (
+ <>
+
+
+ >
+ );
+ }
+ // TODO(@JimmFly): remove mock data
+ const mockOwner: Member = {
+ id: '1',
+ name: 'Owner',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ };
+
+ const mockMembers: Member[] = [
+ {
+ id: '2',
+ name: 'Member 1',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '3',
+ name: 'Member 2',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ {
+ id: '4',
+ name: 'Member 3',
+ avatarUrl: '',
+ email: null,
+ permission: Permission.Admin,
+ inviteId: '',
+ emailVerified: null,
+ status: WorkspaceMemberStatus.Accepted,
+ },
+ ];
+
+ return (
+
+
+
+
+
+ {t['com.affine.share-menu.generalAccess']()}
+
+
+
+
+
+
+ );
+};
+
+export const SharePage = (props: ShareMenuProps) => {
+ if (props.workspaceMetadata.flavour === 'local') {
+ return ;
+ } else {
+ return (
+ // TODO(@eyhn): refactor this part
+
+
+
+
+
+ );
+ }
+};
diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json
index 50d0b8626af00..4b701e65454fe 100644
--- a/packages/frontend/i18n/src/i18n-completenesses.json
+++ b/packages/frontend/i18n/src/i18n-completenesses.json
@@ -1,26 +1,26 @@
{
- "ar": 100,
+ "ar": 99,
"ca": 5,
"da": 5,
- "de": 100,
- "el-GR": 100,
+ "de": 99,
+ "el-GR": 99,
"en": 100,
- "es-AR": 100,
+ "es-AR": 99,
"es-CL": 100,
- "es": 100,
- "fa": 100,
- "fr": 100,
+ "es": 99,
+ "fa": 99,
+ "fr": 99,
"hi": 2,
- "it-IT": 100,
+ "it-IT": 99,
"it": 1,
- "ja": 100,
- "ko": 72,
- "pl": 100,
- "pt-BR": 100,
- "ru": 100,
- "sv-SE": 100,
- "uk": 100,
+ "ja": 99,
+ "ko": 71,
+ "pl": 99,
+ "pt-BR": 99,
+ "ru": 99,
+ "sv-SE": 99,
+ "uk": 99,
"ur": 2,
- "zh-Hans": 100,
- "zh-Hant": 100
+ "zh-Hans": 99,
+ "zh-Hant": 99
}
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index c17c5eed28a1b..f97ffc65f06a4 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -5491,6 +5491,10 @@ export function useAFFiNEI18N(): {
* `Share doc`
*/
["com.affine.share-menu.SharePage"](): string;
+ /**
+ * `General access`
+ */
+ ["com.affine.share-menu.generalAccess"](): string;
/**
* `Share via export`
*/
@@ -5596,9 +5600,17 @@ export function useAFFiNEI18N(): {
*/
["com.affine.share-menu.option.link.readonly.description"](): string;
/**
- * `Can Edit`
+ * `Can manage`
+ */
+ ["com.affine.share-menu.option.permission.can-manage"](): string;
+ /**
+ * `Can edit`
*/
["com.affine.share-menu.option.permission.can-edit"](): string;
+ /**
+ * `Can read`
+ */
+ ["com.affine.share-menu.option.permission.can-read"](): string;
/**
* `Members in workspace`
*/
@@ -5619,6 +5631,71 @@ export function useAFFiNEI18N(): {
* `Shared`
*/
["com.affine.share-menu.sharedButton"](): string;
+ /**
+ * `{{member1}} and {{member2}} are in this doc`
+ */
+ ["com.affine.share-menu.member-management.member-count-2"](options: Readonly<{
+ member1: string;
+ member2: string;
+ }>): string;
+ /**
+ * `{{member1}}, {{member2}} and {{member3}} are in this doc`
+ */
+ ["com.affine.share-menu.member-management.member-count-3"](options: Readonly<{
+ member1: string;
+ member2: string;
+ member3: string;
+ }>): string;
+ /**
+ * `{{member1}}, {{member2}} and {{memberCount}} others`
+ */
+ ["com.affine.share-menu.member-management.member-count-more"](options: Readonly<{
+ member1: string;
+ member2: string;
+ memberCount: string;
+ }>): string;
+ /**
+ * `Remove`
+ */
+ ["com.affine.share-menu.member-management.remove"](): string;
+ /**
+ * `Set as owner`
+ */
+ ["com.affine.share-menu.member-management.set-as-owner"](): string;
+ /**
+ * `{{memberCount}} collaborators in the doc`
+ */
+ ["com.affine.share-menu.member-management.header"](options: {
+ readonly memberCount: string;
+ }): string;
+ /**
+ * `Add collaborators`
+ */
+ ["com.affine.share-menu.member-management.add-collaborators"](): string;
+ /**
+ * `Send invite`
+ */
+ ["com.affine.share-menu.invite-editor.header"](): string;
+ /**
+ * `Manage members`
+ */
+ ["com.affine.share-menu.invite-editor.manage-members"](): string;
+ /**
+ * `Invite`
+ */
+ ["com.affine.share-menu.invite-editor.invite"](): string;
+ /**
+ * `No results found`
+ */
+ ["com.affine.share-menu.invite-editor.no-found"](): string;
+ /**
+ * `Invite other members`
+ */
+ ["com.affine.share-menu.invite-editor.placeholder"](): string;
+ /**
+ * `Notify via Email`
+ */
+ ["com.affine.share-menu.invite-editor.sent-email"](): string;
/**
* `Built with`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 5e6efd3e1952d..99f40db61b185 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1375,6 +1375,7 @@
"com.affine.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.",
"com.affine.share-menu.ShareMode": "Share mode",
"com.affine.share-menu.SharePage": "Share doc",
+ "com.affine.share-menu.generalAccess": "General access",
"com.affine.share-menu.ShareViaExport": "Share via export",
"com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your doc to share with others",
"com.affine.share-menu.ShareViaPrintDescription": "Print a paper copy",
@@ -1401,12 +1402,27 @@
"com.affine.share-menu.option.link.no-access.description": "Only workspace members can access this link",
"com.affine.share-menu.option.link.readonly": "Read Only",
"com.affine.share-menu.option.link.readonly.description": "Anyone can access this link",
- "com.affine.share-menu.option.permission.can-edit": "Can Edit",
+ "com.affine.share-menu.option.permission.can-manage": "Can manage",
+ "com.affine.share-menu.option.permission.can-edit": "Can edit",
+ "com.affine.share-menu.option.permission.can-read": "Can read",
"com.affine.share-menu.option.permission.label": "Members in workspace",
"com.affine.share-menu.publish-to-web": "Publish to web",
"com.affine.share-menu.share-privately": "Share privately",
"com.affine.share-menu.shareButton": "Share",
"com.affine.share-menu.sharedButton": "Shared",
+ "com.affine.share-menu.member-management.member-count-2": "{{member1}} and {{member2}} are in this doc",
+ "com.affine.share-menu.member-management.member-count-3": "{{member1}}, {{member2}} and {{member3}} are in this doc",
+ "com.affine.share-menu.member-management.member-count-more": "{{member1}}, {{member2}} and {{memberCount}} others",
+ "com.affine.share-menu.member-management.remove": "Remove",
+ "com.affine.share-menu.member-management.set-as-owner": "Set as owner",
+ "com.affine.share-menu.member-management.header": "{{memberCount}} collaborators in the doc",
+ "com.affine.share-menu.member-management.add-collaborators": "Add collaborators",
+ "com.affine.share-menu.invite-editor.header": "Send invite",
+ "com.affine.share-menu.invite-editor.manage-members": "Manage members",
+ "com.affine.share-menu.invite-editor.invite": "Invite",
+ "com.affine.share-menu.invite-editor.no-found": "No results found",
+ "com.affine.share-menu.invite-editor.placeholder": "Invite other members",
+ "com.affine.share-menu.invite-editor.sent-email": "Notify via Email",
"com.affine.share-page.footer.built-with": "Built with",
"com.affine.share-page.footer.create-with": "Create with",
"com.affine.share-page.footer.description": "Empower your sharing with AFFiNE Cloud: One-click doc sharing",