init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -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,
};
};