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 { Button } from '@affine/component/ui/button';
|
||||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||||
@@ -26,7 +29,7 @@ import {
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { Suspense, useCallback, useEffect } from 'react';
|
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
import { CloudSvg } from '../cloud-svg';
|
import { CloudSvg } from '../cloud-svg';
|
||||||
@@ -65,6 +68,8 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
workspaceMetadata: { id: workspaceId },
|
workspaceMetadata: { id: workspaceId },
|
||||||
} = props;
|
} = props;
|
||||||
const editor = useService(EditorService).editor;
|
const editor = useService(EditorService).editor;
|
||||||
|
const currentMode = useLiveData(editor.mode$);
|
||||||
|
const editorContainer = useLiveData(editor.editorContainer$);
|
||||||
const shareInfoService = useService(ShareInfoService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -153,6 +158,10 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
|
|
||||||
const isMac = environment.isMacOs;
|
const isMac = environment.isMacOs;
|
||||||
|
|
||||||
|
const { blockIds, elementIds } = useMemo(
|
||||||
|
() => getSelectedNodes(editorContainer?.host || null, currentMode),
|
||||||
|
[editorContainer, currentMode]
|
||||||
|
);
|
||||||
const { onClickCopyLink } = useSharingUrl({
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId: editor.doc.id,
|
pageId: editor.doc.id,
|
||||||
@@ -165,9 +174,8 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
onClickCopyLink('edgeless' as DocMode);
|
onClickCopyLink('edgeless' as DocMode);
|
||||||
}, [onClickCopyLink]);
|
}, [onClickCopyLink]);
|
||||||
const onCopyBlockLink = useCallback(() => {
|
const onCopyBlockLink = useCallback(() => {
|
||||||
// TODO(@JimmFly): handle frame
|
onClickCopyLink(currentMode, blockIds, elementIds);
|
||||||
onClickCopyLink();
|
}, [currentMode, onClickCopyLink, blockIds, elementIds]);
|
||||||
}, [onClickCopyLink]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// TODO(@eyhn): loading and error UI
|
// TODO(@eyhn): loading and error UI
|
||||||
@@ -286,7 +294,11 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
>
|
>
|
||||||
{t['com.affine.share-menu.copy.edgeless']()}
|
{t['com.affine.share-menu.copy.edgeless']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem prefixIcon={<BlockIcon />} onSelect={onCopyBlockLink}>
|
<MenuItem
|
||||||
|
prefixIcon={<BlockIcon />}
|
||||||
|
onSelect={onCopyBlockLink}
|
||||||
|
disabled={blockIds.length + elementIds.length === 0}
|
||||||
|
>
|
||||||
{t['com.affine.share-menu.copy.block']()}
|
{t['com.affine.share-menu.copy.block']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { notify } from '@affine/component';
|
|||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { BaseSelection } from '@blocksuite/block-std';
|
import { type EditorHost } from '@blocksuite/block-std';
|
||||||
import type { DocMode } from '@blocksuite/blocks';
|
import { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
|
||||||
|
import type { DocMode, EdgelessRootService } from '@blocksuite/blocks';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useActiveBlocksuiteEditor } from '../use-block-suite-editor';
|
|
||||||
|
|
||||||
export type UseSharingUrl = {
|
export type UseSharingUrl = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
pageId: 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 = ({
|
export const generateUrl = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -75,29 +76,72 @@ const getShareLinkType = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectionIds = (selections?: BaseSelection[]) => {
|
export const getSelectedNodes = (
|
||||||
if (!selections || selections.length === 0) {
|
host: EditorHost | null,
|
||||||
return { blockIds: [], elementIds: [] };
|
mode: DocMode = 'page'
|
||||||
}
|
) => {
|
||||||
|
const std = host?.std;
|
||||||
const blockIds: string[] = [];
|
const blockIds: string[] = [];
|
||||||
const elementIds: string[] = [];
|
const elementIds: string[] = [];
|
||||||
// TODO(@JimmFly): handle multiple selections and elementIds
|
const result = { blockIds, elementIds };
|
||||||
if (selections[0].type === 'block') {
|
|
||||||
blockIds.push(selections[0].blockId);
|
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) => {
|
export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [editor] = useActiveBlocksuiteEditor();
|
|
||||||
|
|
||||||
const onClickCopyLink = useCallback(
|
const onClickCopyLink = useCallback(
|
||||||
(shareMode?: DocMode) => {
|
(shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => {
|
||||||
const selectManager = editor?.host?.selection;
|
|
||||||
const selections = selectManager?.value;
|
|
||||||
const { blockIds, elementIds } = getSelectionIds(selections);
|
|
||||||
|
|
||||||
const sharingUrl = generateUrl({
|
const sharingUrl = generateUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId,
|
pageId,
|
||||||
@@ -130,8 +174,9 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editor, pageId, t, workspaceId]
|
[pageId, t, workspaceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onClickCopyLink,
|
onClickCopyLink,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user