mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 23:26:30 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { WithEditorSelectionType } from '../menu/inline-menu/types';
|
||||
import { AddCommentActions } from './AddCommentActions';
|
||||
import { AddCommentInput } from './AddCommentInput';
|
||||
import { useAddComment } from './use-add-comment';
|
||||
|
||||
export const AddComment = (props: WithEditorSelectionType) => {
|
||||
const {
|
||||
currentComment,
|
||||
setCurrentComment,
|
||||
createComment,
|
||||
handleSubmitCurrentComment,
|
||||
} = useAddComment(props);
|
||||
|
||||
return (
|
||||
<StyledContainerForAddComment>
|
||||
<AddCommentInput
|
||||
comment={currentComment}
|
||||
setComment={setCurrentComment}
|
||||
createComment={createComment}
|
||||
handleSubmitCurrentComment={handleSubmitCurrentComment}
|
||||
/>
|
||||
<AddCommentActions
|
||||
{...props}
|
||||
createComment={createComment}
|
||||
handleSubmitCurrentComment={handleSubmitCurrentComment}
|
||||
/>
|
||||
</StyledContainerForAddComment>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainerForAddComment = styled('div')(({ theme }) => {
|
||||
return {
|
||||
// display: 'flex',
|
||||
margin: theme.affine.spacing.main,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useCallback, type FC, type MouseEvent } from 'react';
|
||||
import {
|
||||
styled,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
type SvgIconProps,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
EmbedIcon,
|
||||
ImageIcon,
|
||||
ReactionIcon,
|
||||
CollaboratorIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { WithEditorSelectionType } from '../menu/inline-menu/types';
|
||||
import { getEditorMarkForCommentId } from '@toeverything/components/common';
|
||||
|
||||
const getCommentQuickActionsData = () => {
|
||||
return [
|
||||
{
|
||||
id: 'attachment',
|
||||
icon: EmbedIcon,
|
||||
tooltip: 'Add attachment file',
|
||||
},
|
||||
{
|
||||
id: 'mention',
|
||||
icon: CollaboratorIcon,
|
||||
tooltip: 'Mention someone',
|
||||
},
|
||||
{
|
||||
id: 'image',
|
||||
icon: ImageIcon,
|
||||
tooltip: 'Add image',
|
||||
},
|
||||
{
|
||||
id: 'emoji',
|
||||
icon: ReactionIcon,
|
||||
tooltip: 'Add emoji',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type AddCommentActionsProps = {
|
||||
createComment: () => Promise<{ commentsId: string } | undefined>;
|
||||
handleSubmitCurrentComment: () => Promise<void>;
|
||||
} & WithEditorSelectionType;
|
||||
|
||||
export const AddCommentActions = ({
|
||||
createComment,
|
||||
handleSubmitCurrentComment,
|
||||
editor,
|
||||
selectionInfo,
|
||||
setShow,
|
||||
}: AddCommentActionsProps) => {
|
||||
return (
|
||||
<StyledContainerForAddCommentActions>
|
||||
<StyledContainerForActionsButtons>
|
||||
{getCommentQuickActionsData().map(action => {
|
||||
const { id, icon, tooltip } = action;
|
||||
return (
|
||||
<IconButtonWithTooltip
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
key={id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<IconButton />
|
||||
</StyledContainerForActionsButtons>
|
||||
<StyledContainerForActionsButtons>
|
||||
<StyledCancelButton
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
<StyledSendButton onClick={handleSubmitCurrentComment}>
|
||||
Send
|
||||
</StyledSendButton>
|
||||
</StyledContainerForActionsButtons>
|
||||
</StyledContainerForAddCommentActions>
|
||||
);
|
||||
};
|
||||
|
||||
type IconButtonWithTooltipProps = {
|
||||
icon: FC<SvgIconProps>;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
const IconButtonWithTooltip = ({
|
||||
icon: Icon,
|
||||
tooltip,
|
||||
}: IconButtonWithTooltipProps) => {
|
||||
return (
|
||||
<Tooltip content={tooltip} placement="bottom" trigger="hover">
|
||||
<IconButton aria-label={tooltip}>
|
||||
<Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainerForAddCommentActions = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledContainerForActionsButtons = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledActionBaseButton = styled('button')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 50,
|
||||
height: 20,
|
||||
borderRadius: 5,
|
||||
fontSize: 12,
|
||||
};
|
||||
});
|
||||
|
||||
const StyledCancelButton = styled(StyledActionBaseButton)(({ theme }) => {
|
||||
return {
|
||||
border: `1px solid ${theme.affine.palette.tagHover}`,
|
||||
marginRight: theme.affine.spacing.smSpacing,
|
||||
};
|
||||
});
|
||||
|
||||
const StyledSendButton = styled(StyledActionBaseButton)(({ theme }) => {
|
||||
return {
|
||||
border: `none`,
|
||||
backgroundColor: theme.affine.palette.primary,
|
||||
color: theme.affine.palette.white,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useCallback, ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useAddComment } from './use-add-comment';
|
||||
import { WithEditorSelectionType } from '../menu/inline-menu/types';
|
||||
|
||||
type AddCommentInputProps = {
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
createComment: () => Promise<{ commentsId: string }>;
|
||||
handleSubmitCurrentComment: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const AddCommentInput = (props: AddCommentInputProps) => {
|
||||
const { comment, setComment, handleSubmitCurrentComment } = props;
|
||||
|
||||
const handleTextAreaChange = useCallback(
|
||||
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setComment(event.currentTarget.value);
|
||||
},
|
||||
[setComment]
|
||||
);
|
||||
|
||||
// 👀 keydown event won't work as expected
|
||||
const handleKeyUp = useCallback(
|
||||
async (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!e.metaKey && !e.shiftKey && e.code === 'Enter' && comment) {
|
||||
await handleSubmitCurrentComment();
|
||||
}
|
||||
},
|
||||
[comment, handleSubmitCurrentComment]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainerForAddCommentInput>
|
||||
{/* <input type="text" placeholder={'Add comment...'} /> */}
|
||||
<StyledTextArea
|
||||
placeholder={'Add comment...'}
|
||||
value={comment || ''}
|
||||
onChange={handleTextAreaChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
</StyledContainerForAddCommentInput>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainerForAddCommentInput = styled('div')(({ theme }) => {
|
||||
return {
|
||||
marginLeft: theme.affine.spacing.iconPadding,
|
||||
};
|
||||
});
|
||||
|
||||
const StyledTextArea = styled('textarea')(({ theme }) => {
|
||||
return {
|
||||
minWidth: 252,
|
||||
resize: 'none',
|
||||
// color: theme.affine.palette.primaryText,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
styled,
|
||||
MuiClickAwayListener as ClickAwayListener,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
Virgo,
|
||||
PluginHooks,
|
||||
SelectionInfo,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { AddComment } from './AddComment';
|
||||
|
||||
export type AddCommentPluginContainerProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
style?: { left: number; top: number };
|
||||
};
|
||||
|
||||
export const AddCommentPluginContainer = ({
|
||||
editor,
|
||||
hooks,
|
||||
style,
|
||||
}: AddCommentPluginContainerProps) => {
|
||||
const [showAddComment, setShowAddComment] = useState(false);
|
||||
const [containerStyle, setContainerStyle] = useState<{
|
||||
left: number;
|
||||
top: number;
|
||||
}>(null);
|
||||
const [selectionInfo, setSelectionInfo] = useState<SelectionInfo>();
|
||||
|
||||
useEffect(() => {
|
||||
const showAddCommentInput = () => {
|
||||
setShowAddComment(true);
|
||||
const rect = editor.selection?.currentSelectInfo?.browserSelection
|
||||
?.getRangeAt(0)
|
||||
?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
setSelectionInfo(editor.selection.currentSelectInfo);
|
||||
setContainerStyle({ left: rect.left, top: rect.top + 32 });
|
||||
}
|
||||
};
|
||||
editor.plugins.observe('show-add-comment', showAddCommentInput);
|
||||
|
||||
return () =>
|
||||
editor.plugins.unobserve('show-add-comment', showAddCommentInput);
|
||||
}, [editor.plugins, editor.selection.currentSelectInfo]);
|
||||
|
||||
return showAddComment && containerStyle ? (
|
||||
<ClickAwayListener onClickAway={() => setShowAddComment(false)}>
|
||||
<StyledContainerForAddCommentContainer style={containerStyle}>
|
||||
<AddComment
|
||||
editor={editor}
|
||||
selectionInfo={selectionInfo}
|
||||
setShow={setShowAddComment}
|
||||
/>
|
||||
</StyledContainerForAddCommentContainer>
|
||||
</ClickAwayListener>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const StyledContainerForAddCommentContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
boxShadow: theme.affine.shadows.shadowSxDownLg,
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { BasePlugin } from '../base-plugin';
|
||||
import { PluginRenderRoot } from '../utils';
|
||||
import { AddCommentPluginContainer } from './Container';
|
||||
|
||||
const PLUGIN_NAME = 'add-comment-plugin';
|
||||
|
||||
export class AddCommentPlugin extends BasePlugin {
|
||||
public static override get pluginName(): string {
|
||||
return PLUGIN_NAME;
|
||||
}
|
||||
|
||||
private root: PluginRenderRoot;
|
||||
|
||||
protected override on_render(): void {
|
||||
this.root = new PluginRenderRoot({
|
||||
name: AddCommentPlugin.pluginName,
|
||||
render: this.editor.reactRenderRoot?.render,
|
||||
});
|
||||
|
||||
this.root.mount();
|
||||
this.renderAddComment();
|
||||
}
|
||||
|
||||
private renderAddComment(): void {
|
||||
this.root?.render(
|
||||
<StrictMode>
|
||||
<AddCommentPluginContainer
|
||||
editor={this.editor}
|
||||
hooks={this.hooks}
|
||||
/>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
public override dispose() {
|
||||
this.root?.unmount();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AddCommentPlugin } from './Plugin';
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { WithEditorSelectionType } from '../menu/inline-menu/types';
|
||||
|
||||
export const useAddComment = ({
|
||||
editor,
|
||||
selectionInfo,
|
||||
setShow,
|
||||
}: WithEditorSelectionType) => {
|
||||
const { workspace_id: workspaceId, page_id: pageId } = useParams();
|
||||
const [currentComment, setCurrentComment] = useState('');
|
||||
|
||||
const createComment = useCallback(async (): Promise<{
|
||||
commentsId: string;
|
||||
}> => {
|
||||
const selectedBlockId = selectionInfo?.anchorNode?.id;
|
||||
if (!currentComment || !currentComment.trim()) {
|
||||
throw new Error(
|
||||
'Comment content must not be empty before creating in db. '
|
||||
);
|
||||
}
|
||||
if (!selectedBlockId) {
|
||||
throw new Error(
|
||||
'Commented block id must not be empty before creating in db. '
|
||||
);
|
||||
}
|
||||
|
||||
const created = await services.api.commentService.createComment({
|
||||
workspace: workspaceId,
|
||||
pageId: pageId,
|
||||
attachedToBlocksIds: [selectionInfo.anchorNode.id],
|
||||
quote: {
|
||||
value: [
|
||||
{
|
||||
text: editor.blockHelper.getBlockTextBetweenSelection(
|
||||
selectedBlockId
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
content: {
|
||||
value: [{ text: currentComment }],
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
}, [
|
||||
currentComment,
|
||||
editor.blockHelper,
|
||||
pageId,
|
||||
selectionInfo?.anchorNode?.id,
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
const handleSubmitCurrentComment = useCallback(async () => {
|
||||
const textBlockId = selectionInfo.anchorNode.id;
|
||||
if (!textBlockId) return;
|
||||
const created = await createComment();
|
||||
const commentId = created?.commentsId;
|
||||
|
||||
if (commentId) {
|
||||
editor.blockHelper.addComment(textBlockId, commentId);
|
||||
setShow(false);
|
||||
}
|
||||
}, [
|
||||
createComment,
|
||||
editor.blockHelper,
|
||||
selectionInfo.anchorNode.id,
|
||||
setShow,
|
||||
]);
|
||||
return {
|
||||
currentComment,
|
||||
setCurrentComment,
|
||||
createComment,
|
||||
handleSubmitCurrentComment,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user