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']()} -
- - } onSelect={onDisablePublic}> -
-
- {t['com.affine.share-menu.option.link.no-access']()} -
- {!isSharedPage && ( - - )} -
-
- } - onSelect={onClickAnyoneReadOnlyShare} - data-testid="share-link-menu-enable-share" - > -
-
- {t['com.affine.share-menu.option.link.readonly']()} -
- {isSharedPage && ( - - )} -
-
- - } - > - - {isSharedPage - ? t['com.affine.share-menu.option.link.readonly']() - : t['com.affine.share-menu.option.link.no-access']()} - -
-
-
-
- {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']()} +
+ + +
+
+ {t['com.affine.share-menu.option.permission.can-manage']()} +
+
+
+ +
+
+ {t['com.affine.share-menu.option.permission.can-edit']()} +
+
+
+ +
+
+ {t['com.affine.share-menu.option.permission.can-read']()} +
+
+
+ + } + > + + {t['com.affine.share-menu.option.permission.can-edit']()} + +
+
+ ); +}; 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']()} +
+ + + } + onSelect={onDisablePublic} + selected={!isSharedPage} + > +
+
{t['com.affine.share-menu.option.link.no-access']()}
+
+
+ } + onSelect={onClickAnyoneReadOnlyShare} + data-testid="share-link-menu-enable-share" + selected={!!isSharedPage} + > +
+
{t['com.affine.share-menu.option.link.readonly']()}
+
+
+ + } + > + + {isSharedPage + ? t['com.affine.share-menu.option.link.readonly']() + : t['com.affine.share-menu.option.link.no-access']()} + +
+
+ ); +}; 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 ( +
+
+ Send Invite +
+
+
+
+ {selectedMembers.map((member, idx) => { + if (!member) { + return null; + } + const onRemoved = () => handleRemoved(member.id); + return ( + + ); + })} + +
+ {!selectedMembers.length ? null : ( +
+ + + {t[ + 'com.affine.share-menu.option.permission.can-manage' + ]()} + + + {t['com.affine.share-menu.option.permission.can-edit']()} + + + {t['com.affine.share-menu.option.permission.can-read']()} + + + } + > + + {t['com.affine.share-menu.option.permission.can-edit']()} + + +
+ )} +
+ +
+ +
+
+
+ + {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 ( +
+
+ + {docOwner.name} +
+
{t['Owner']()}
+
+ ); +}; 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 ? ( + + {item.label} + + ) : null + )} + {isOwner ? ( + <> + + {}}> + {t['com.affine.share-menu.member-management.set-as-owner']()} + + + ) : 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",