mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix(core): link generation for selected blocks (#8087)
Closes [BS-1321](https://linear.app/affine-design/issue/BS-1321/copy-link-to-selected-block) https://github.com/user-attachments/assets/6102fc7f-9551-4dd4-83b1-758dd0a946f0
This commit is contained in:
@@ -2,7 +2,10 @@ import { notify, Skeleton } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
|
||||
import {
|
||||
getSelectedNodes,
|
||||
useSharingUrl,
|
||||
} from '@affine/core/hooks/affine/use-share-url';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||
@@ -26,7 +29,7 @@ import {
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { CloudSvg } from '../cloud-svg';
|
||||
@@ -65,6 +68,8 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
} = props;
|
||||
const editor = useService(EditorService).editor;
|
||||
const currentMode = useLiveData(editor.mode$);
|
||||
const editorContainer = useLiveData(editor.editorContainer$);
|
||||
const shareInfoService = useService(ShareInfoService);
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
useEffect(() => {
|
||||
@@ -153,6 +158,10 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
|
||||
const isMac = environment.isMacOs;
|
||||
|
||||
const { blockIds, elementIds } = useMemo(
|
||||
() => getSelectedNodes(editorContainer?.host || null, currentMode),
|
||||
[editorContainer, currentMode]
|
||||
);
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId: editor.doc.id,
|
||||
@@ -165,9 +174,8 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
onClickCopyLink('edgeless' as DocMode);
|
||||
}, [onClickCopyLink]);
|
||||
const onCopyBlockLink = useCallback(() => {
|
||||
// TODO(@JimmFly): handle frame
|
||||
onClickCopyLink();
|
||||
}, [onClickCopyLink]);
|
||||
onClickCopyLink(currentMode, blockIds, elementIds);
|
||||
}, [currentMode, onClickCopyLink, blockIds, elementIds]);
|
||||
|
||||
if (isLoading) {
|
||||
// TODO(@eyhn): loading and error UI
|
||||
@@ -286,7 +294,11 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
||||
>
|
||||
{t['com.affine.share-menu.copy.edgeless']()}
|
||||
</MenuItem>
|
||||
<MenuItem prefixIcon={<BlockIcon />} onSelect={onCopyBlockLink}>
|
||||
<MenuItem
|
||||
prefixIcon={<BlockIcon />}
|
||||
onSelect={onCopyBlockLink}
|
||||
disabled={blockIds.length + elementIds.length === 0}
|
||||
>
|
||||
{t['com.affine.share-menu.copy.block']()}
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@@ -2,12 +2,11 @@ import { notify } from '@affine/component';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { BaseSelection } from '@blocksuite/block-std';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import { type EditorHost } from '@blocksuite/block-std';
|
||||
import { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
|
||||
import type { DocMode, EdgelessRootService } from '@blocksuite/blocks';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useActiveBlocksuiteEditor } from '../use-block-suite-editor';
|
||||
|
||||
export type UseSharingUrl = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
@@ -18,7 +17,9 @@ export type UseSharingUrl = {
|
||||
};
|
||||
|
||||
/**
|
||||
* to generate a url like https://app.affine.pro/workspace/workspaceId/docId?mode=DocMode?element=seletedBlockid#seletedBlockid
|
||||
* To generate a url like
|
||||
*
|
||||
* https://app.affine.pro/workspace/workspaceId/docId?mode=DocMode&elementIds=seletedElementIds&blockIds=selectedBlockIds
|
||||
*/
|
||||
export const generateUrl = ({
|
||||
workspaceId,
|
||||
@@ -75,29 +76,72 @@ const getShareLinkType = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectionIds = (selections?: BaseSelection[]) => {
|
||||
if (!selections || selections.length === 0) {
|
||||
return { blockIds: [], elementIds: [] };
|
||||
}
|
||||
export const getSelectedNodes = (
|
||||
host: EditorHost | null,
|
||||
mode: DocMode = 'page'
|
||||
) => {
|
||||
const std = host?.std;
|
||||
const blockIds: string[] = [];
|
||||
const elementIds: string[] = [];
|
||||
// TODO(@JimmFly): handle multiple selections and elementIds
|
||||
if (selections[0].type === 'block') {
|
||||
blockIds.push(selections[0].blockId);
|
||||
const result = { blockIds, elementIds };
|
||||
|
||||
if (!std) {
|
||||
return result;
|
||||
}
|
||||
return { blockIds, elementIds };
|
||||
|
||||
if (mode === 'edgeless') {
|
||||
const service = std.getService<EdgelessRootService>('affine:page');
|
||||
if (!service) return result;
|
||||
|
||||
for (const element of service.selection.selectedElements) {
|
||||
if (element instanceof GfxBlockElementModel) {
|
||||
blockIds.push(element.id);
|
||||
} else {
|
||||
elementIds.push(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const [success, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.getTextSelection(),
|
||||
chain.getBlockSelections(),
|
||||
chain.getImageSelections(),
|
||||
])
|
||||
.getSelectedModels({
|
||||
mode: 'highest',
|
||||
})
|
||||
.run();
|
||||
|
||||
if (!success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// should return an empty array if `to` of the range is null
|
||||
if (
|
||||
ctx.currentTextSelection &&
|
||||
!ctx.currentTextSelection.to &&
|
||||
ctx.currentTextSelection.from.length === 0
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ctx.selectedModels?.length) {
|
||||
blockIds.push(...ctx.selectedModels.map(model => model.id));
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
|
||||
const t = useI18n();
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const onClickCopyLink = useCallback(
|
||||
(shareMode?: DocMode) => {
|
||||
const selectManager = editor?.host?.selection;
|
||||
const selections = selectManager?.value;
|
||||
const { blockIds, elementIds } = getSelectionIds(selections);
|
||||
|
||||
(shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => {
|
||||
const sharingUrl = generateUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
@@ -130,8 +174,9 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor, pageId, t, workspaceId]
|
||||
[pageId, t, workspaceId]
|
||||
);
|
||||
|
||||
return {
|
||||
onClickCopyLink,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user