mirror of https://github.com/fantasticit/think.git
feat: support mention
This commit is contained in:
parent
6496422754
commit
4b151c3d8b
|
@ -35,6 +35,7 @@
|
|||
"@tiptap/extension-italic": "^2.0.0-beta.25",
|
||||
"@tiptap/extension-link": "^2.0.0-beta.36",
|
||||
"@tiptap/extension-list-item": "^2.0.0-beta.20",
|
||||
"@tiptap/extension-mention": "^2.0.0-beta.95",
|
||||
"@tiptap/extension-ordered-list": "^2.0.0-beta.27",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.23",
|
||||
"@tiptap/extension-placeholder": "^2.0.0-beta.47",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import Router from 'next/router';
|
||||
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
||||
import cls from 'classnames';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { BackTop } from '@douyinfe/semi-ui';
|
||||
|
@ -17,10 +18,13 @@ import {
|
|||
getIndexdbProvider,
|
||||
destoryIndexdbProvider,
|
||||
} from 'tiptap';
|
||||
import { findMentions } from 'tiptap/utils/find-mention';
|
||||
import { useCollaborationDocument } from 'data/document';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Banner } from 'components/banner';
|
||||
import { debounce } from 'helpers/debounce';
|
||||
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
|
||||
import { DocumentUserSetting } from './users';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
interface IProps {
|
||||
|
@ -31,8 +35,10 @@ interface IProps {
|
|||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({ user, documentId, authority, className, style }) => {
|
||||
if (!user) return null;
|
||||
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
|
||||
if (!currentUser) return null;
|
||||
const $hasShowUserSettingModal = useRef(false);
|
||||
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
|
||||
const [status, setStatus] = useState<ProviderStatus>('connecting');
|
||||
const { online } = useNetwork();
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
|
@ -40,9 +46,9 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: documentId,
|
||||
token: user.token,
|
||||
token: currentUser.token,
|
||||
cacheType: 'EDITOR',
|
||||
user,
|
||||
user: currentUser,
|
||||
docType: 'document',
|
||||
events: {
|
||||
onAwarenessUpdate({ states }) {
|
||||
|
@ -57,15 +63,14 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [documentId, user.token]);
|
||||
|
||||
}, [documentId, currentUser.token]);
|
||||
const editor = useEditor({
|
||||
editable: authority && authority.editable,
|
||||
extensions: [
|
||||
...BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
getCollaborationCursorExtension(provider, user),
|
||||
getCollaborationCursorExtension(provider, currentUser),
|
||||
],
|
||||
onTransaction: debounce(({ transaction }) => {
|
||||
try {
|
||||
|
@ -74,6 +79,8 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
} catch (e) {}
|
||||
}, 50),
|
||||
});
|
||||
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
||||
const [mentionUsers, setMentionUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const indexdbProvider = getIndexdbProvider(documentId, provider.document);
|
||||
|
@ -96,11 +103,61 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
if (!editor) return;
|
||||
const handler = (data) => editor.commands.setContent(data);
|
||||
event.on(USE_DOCUMENT_VERSION, handler);
|
||||
|
||||
return () => {
|
||||
event.off(USE_DOCUMENT_VERSION, handler);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const handler = () => {
|
||||
// 已经拦截过一次,不再拦截
|
||||
if ($hasShowUserSettingModal.current) return;
|
||||
|
||||
const mentionUsers = findMentions(editor);
|
||||
if (!mentionUsers || !mentionUsers.length) return;
|
||||
|
||||
const currentUserAuth = users.find((user) => {
|
||||
return user.user.name === currentUser.name;
|
||||
});
|
||||
const isCurrentUserCreateUser = currentUserAuth.auth.createUserId === currentUser.id;
|
||||
|
||||
if (!isCurrentUserCreateUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Array.from(new Set(mentionUsers))
|
||||
.filter((userName) => {
|
||||
const exist = users.find((user) => {
|
||||
return user.user.name === userName;
|
||||
});
|
||||
if (!exist || !exist.auth.readable) return true;
|
||||
return false;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!data.length) return;
|
||||
|
||||
setMentionUsers(data);
|
||||
toggleMentionUsersSettingVisible(true);
|
||||
$hasShowUserSettingModal.current = true;
|
||||
// ignore-me
|
||||
const newErr = new Error('请完成权限操作后关闭页面');
|
||||
throw newErr;
|
||||
};
|
||||
|
||||
Router.events.on('routeChangeStart', handler);
|
||||
window.addEventListener('unload', handler);
|
||||
|
||||
return () => {
|
||||
$hasShowUserSettingModal.current = false;
|
||||
Router.events.off('routeChangeStart', handler);
|
||||
window.removeEventListener('unload', handler);
|
||||
};
|
||||
}, [editor, users, currentUser]);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
|
@ -125,6 +182,14 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
|||
</div>
|
||||
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
|
||||
</main>
|
||||
<DocumentUserSetting
|
||||
visible={mentionUsersSettingVisible}
|
||||
toggleVisible={toggleMentionUsersSettingVisible}
|
||||
mentionUsers={mentionUsers}
|
||||
users={users}
|
||||
addUser={addUser}
|
||||
updateUser={updateUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Modal, Typography, Table, Checkbox } from '@douyinfe/semi-ui';
|
||||
import { IAuthority, IUser } from '@think/domains';
|
||||
import { DocAuth } from 'data/document';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
toggleVisible: (arg: boolean) => void;
|
||||
mentionUsers: string[];
|
||||
users: Array<{ user: IUser; auth: IAuthority }>;
|
||||
addUser: (auth: DocAuth) => Promise<unknown>;
|
||||
updateUser: (auth: DocAuth) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked, data) => {
|
||||
const handle = (evt) => {
|
||||
const ret = {
|
||||
...data,
|
||||
};
|
||||
ret[authKey] = evt.target.checked;
|
||||
onChange(ret);
|
||||
};
|
||||
return <Checkbox style={{ display: 'inline-block' }} checked={checked} onChange={handle} />;
|
||||
};
|
||||
|
||||
export const DocumentUserSetting: React.FC<IProps> = ({
|
||||
visible,
|
||||
toggleVisible,
|
||||
mentionUsers,
|
||||
users,
|
||||
addUser,
|
||||
updateUser,
|
||||
}) => {
|
||||
const renderUsers = useMemo(() => {
|
||||
return mentionUsers
|
||||
.map((mentionUser) => {
|
||||
const exist = users.find((user) => {
|
||||
return user.user.name === mentionUser;
|
||||
});
|
||||
|
||||
if (!exist) return { userName: mentionUser, readable: false, editable: false, shouldAddToDocument: true };
|
||||
|
||||
return {
|
||||
userName: mentionUser,
|
||||
readable: exist.auth.readable,
|
||||
editable: exist.auth.editable,
|
||||
shouldAddToDocument: false,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [users, mentionUsers]);
|
||||
|
||||
const handler = async (data) => {
|
||||
if (data.shouldAddToDocument) {
|
||||
await addUser(data.userName);
|
||||
}
|
||||
|
||||
await updateUser(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={'权限操作'}
|
||||
visible={visible}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
maskClosable={false}
|
||||
style={{ maxWidth: '96vw' }}
|
||||
footer={null}
|
||||
>
|
||||
<Text>您在该文档中 @ 了以下用户,请为他们操作权限,否则他们无法阅读该文档。</Text>
|
||||
<Table style={{ margin: '24px 0' }} dataSource={renderUsers} size="small" pagination>
|
||||
<Column title="用户名" dataIndex="userName" key="name" />
|
||||
<Column
|
||||
title="是否可读"
|
||||
dataIndex="readable"
|
||||
key="readable"
|
||||
render={renderChecked(handler, 'readable')}
|
||||
align="center"
|
||||
/>
|
||||
<Column
|
||||
title="是否可编辑"
|
||||
dataIndex="editable"
|
||||
key="editable"
|
||||
render={renderChecked(handler, 'editable')}
|
||||
align="center"
|
||||
/>
|
||||
</Table>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,18 +1,10 @@
|
|||
import type { IUser, IDocument, IWiki } from '@think/domains';
|
||||
import type { IUser, IDocument, IWiki, IAuthority } from '@think/domains';
|
||||
import useSWR from 'swr';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useAsyncLoading } from 'hooks/use-async-loading';
|
||||
import { HttpClient } from 'services/http-client';
|
||||
import { getPublicDocumentDetail } from 'services/document';
|
||||
|
||||
interface IAuthority {
|
||||
id: string;
|
||||
documentId: string;
|
||||
userId: string;
|
||||
readable: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
type ICreateDocument = Partial<Pick<IDocument, 'wikiId' | 'parentDocumentId'>>;
|
||||
type IDocumentWithAuth = { document: IDocument; authority: IAuthority };
|
||||
type IUpdateDocument = Partial<Pick<IDocument, 'title' | 'content'>>;
|
||||
|
@ -171,13 +163,18 @@ export const usePublicDocument = (documentId: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
export type DocAuth = {
|
||||
userName: string;
|
||||
readable?: boolean;
|
||||
editable?: boolean;
|
||||
};
|
||||
/**
|
||||
* 协作文档
|
||||
* @param documentId
|
||||
* @returns
|
||||
*/
|
||||
export const useCollaborationDocument = (documentId) => {
|
||||
const { data, error, mutate } = useSWR<Array<IAuthority & IUser>>(
|
||||
const { data, error, mutate } = useSWR<Array<{ user: IUser; auth: IAuthority }>>(
|
||||
`/document/user/${documentId}`,
|
||||
(url) => HttpClient.get(url),
|
||||
{ shouldRetryOnError: false }
|
||||
|
@ -195,7 +192,7 @@ export const useCollaborationDocument = (documentId) => {
|
|||
return ret;
|
||||
};
|
||||
|
||||
const updateUser = async (docAuth) => {
|
||||
const updateUser = async (docAuth: DocAuth) => {
|
||||
const ret = await HttpClient.post(`/document/user/${documentId}/update`, {
|
||||
documentId,
|
||||
...docAuth,
|
||||
|
@ -204,7 +201,7 @@ export const useCollaborationDocument = (documentId) => {
|
|||
return ret;
|
||||
};
|
||||
|
||||
const deleteUser = async (docAuth) => {
|
||||
const deleteUser = async (docAuth: DocAuth) => {
|
||||
const ret = await HttpClient.post(`/document/user/${documentId}/delete`, {
|
||||
documentId,
|
||||
...docAuth,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getStorage, setStorage } from 'helpers/storage';
|
|||
|
||||
export const useUser = () => {
|
||||
const router = useRouter();
|
||||
const { data, error, mutate } = useSWR('user', getStorage);
|
||||
const { data, error, mutate } = useSWR<ILoginUser>('user', getStorage);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
window.localStorage.removeItem('user');
|
||||
|
@ -29,7 +29,7 @@ export const useUser = () => {
|
|||
|
||||
const updateUser = async (patch: Pick<IUser, 'email' | 'avatar'>) => {
|
||||
const res = await HttpClient.patch('/user/update', patch);
|
||||
const ret = { ...data, ...res } as unknown as IUser;
|
||||
const ret = { ...data, ...res } as unknown as ILoginUser;
|
||||
setStorage('user', JSON.stringify(ret));
|
||||
mutate(ret);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import type { IUser } from '@think/domains';
|
||||
import type { IPagination, IUser } from '@think/domains';
|
||||
import { HttpClient } from './http-client';
|
||||
|
||||
export const register = (data: Partial<IUser>): Promise<IUser> => {
|
||||
return HttpClient.post('/user/register', data);
|
||||
};
|
||||
|
||||
export const getUsers = (): Promise<IUser[]> => {
|
||||
return HttpClient.get('/user');
|
||||
};
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import BulitInMention from '@tiptap/extension-mention';
|
||||
import { ReactRenderer } from '@tiptap/react';
|
||||
import tippy from 'tippy.js';
|
||||
import { getUsers } from 'services/user';
|
||||
import { MentionList } from '../wrappers/mention-list';
|
||||
|
||||
const suggestion = {
|
||||
items: async ({ query }) => {
|
||||
const res = await getUsers();
|
||||
const data = res.map((item) => item.name);
|
||||
return data.filter((item) => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5);
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component;
|
||||
let popup;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component.updateProps(props);
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup[0].destroy();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
command: ({ editor, range, props }) => {
|
||||
// increase range.to by one when the next node is of type "text"
|
||||
// and starts with a space character
|
||||
const nodeAfter = editor.view.state.selection.$to.nodeAfter;
|
||||
const overrideSpace = nodeAfter?.text?.startsWith(' ');
|
||||
|
||||
if (overrideSpace) {
|
||||
range.to += 1;
|
||||
}
|
||||
|
||||
console.log('mention', props);
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, [
|
||||
{
|
||||
type: BulitInMention.name,
|
||||
attrs: props,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' ',
|
||||
},
|
||||
])
|
||||
.run();
|
||||
},
|
||||
};
|
||||
|
||||
export const Mention = BulitInMention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion,
|
||||
});
|
|
@ -28,6 +28,7 @@ import { Katex } from './extensions/katex';
|
|||
import { Link } from './extensions/link';
|
||||
import { ListItem } from './extensions/listItem';
|
||||
import { Loading } from './extensions/loading';
|
||||
import { Mention } from './extensions/mention';
|
||||
import { Mind } from './extensions/mind';
|
||||
import { OrderedList } from './extensions/ordered-list';
|
||||
import { Paragraph } from './extensions/paragraph';
|
||||
|
@ -84,6 +85,7 @@ export const BaseKit = [
|
|||
Link,
|
||||
ListItem,
|
||||
Loading,
|
||||
Mention,
|
||||
Mind,
|
||||
OrderedList,
|
||||
Paragraph,
|
||||
|
@ -132,6 +134,7 @@ export const CommentKit = [
|
|||
Katex,
|
||||
Link,
|
||||
ListItem,
|
||||
Mention,
|
||||
OrderedList,
|
||||
Paragraph,
|
||||
Placeholder,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@import './heading.scss';
|
||||
@import './katex.scss';
|
||||
@import './list.scss';
|
||||
@import './mention.scss';
|
||||
@import './menu.scss';
|
||||
@import './mind.scss';
|
||||
@import './placeholder.scss';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.mention {
|
||||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
background-color: var(--semi-color-primary);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 999em;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Editor } from '@tiptap/core';
|
||||
import { Mention } from '../extensions/mention';
|
||||
|
||||
export const findMentions = (editor: Editor) => {
|
||||
const content = editor.getJSON();
|
||||
const queue = [content];
|
||||
const res = [];
|
||||
|
||||
while (queue.length) {
|
||||
const node = queue.shift();
|
||||
|
||||
if (node.type === Mention.name) {
|
||||
res.push(node.attrs.id);
|
||||
}
|
||||
|
||||
if (node.content && node.content.length) {
|
||||
queue.push(...node.content);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
.items {
|
||||
width: 200px;
|
||||
max-height: 380px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: rgb(9 30 66 / 31%) 0 0 1px, rgb(9 30 66 / 25%) 0 4px 8px -2px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 12px 12px 11px;
|
||||
color: rgb(9 30 66);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
background-color: rgb(255 255 255);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
outline: 0;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
fill: rgb(255 255 255);
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f5f7;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: rgb(0 82 204);
|
||||
text-decoration: none;
|
||||
background-color: rgb(222 235 255);
|
||||
fill: rgb(222 235 255);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { Modal, Typography } from '@douyinfe/semi-ui';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import cls from 'classnames';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { useUser } from 'data/user';
|
||||
import { useCollaborationDocument } from 'data/document';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
interface IProps {
|
||||
editor: Editor;
|
||||
items: Array<string>;
|
||||
command: any;
|
||||
}
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export const MentionList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||
const $container = useRef<HTMLDivElement>();
|
||||
const router = useRouter();
|
||||
const { user: currentUser } = useUser();
|
||||
const { users, addUser, updateUser } = useCollaborationDocument(router.query.documentId);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
const userName = props.items[index];
|
||||
if (!userName) return;
|
||||
props.command({ id: userName });
|
||||
|
||||
// const currentUserAuth = users.find((user) => {
|
||||
// return user.user.name === currentUser.name;
|
||||
// });
|
||||
// const isCurrentUserCreateUser = currentUserAuth.auth.createUserId === currentUser.id;
|
||||
|
||||
// const target = users.find((user) => {
|
||||
// return user.user.name === userName;
|
||||
// });
|
||||
|
||||
// if (isCurrentUserCreateUser) {
|
||||
// if (!target) {
|
||||
// Modal.confirm({
|
||||
// title: <Title heading={5}>权限操作</Title>,
|
||||
// content: <Text>当前用户尚未加入该文档,是否添加他?</Text>,
|
||||
// onCancel: () => {},
|
||||
// onOk: async () => {
|
||||
// addUser(userName).then((res) => {
|
||||
// console.log('用户已经添加', res);
|
||||
// props.command({ id: userName });
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// if (!target.auth.readable) {
|
||||
// Modal.confirm({
|
||||
// title: <Title heading={5}>权限操作</Title>,
|
||||
// content: <Text>当前用户无法阅读该文档,是否添加阅读权限?</Text>,
|
||||
// onCancel: () => {},
|
||||
// onOk: async () => {
|
||||
// updateUser({
|
||||
// userName,
|
||||
// readable: true,
|
||||
// editable: target.auth.editable,
|
||||
// }).then((res) => {
|
||||
// props.command({ id: userName });
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// props.command({ id: userName });
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// }
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Number.isNaN(selectedIndex + 1)) return;
|
||||
const el = $container.current.querySelector(`button:nth-of-type(${selectedIndex + 1})`);
|
||||
el && scrollIntoView(el, { behavior: 'smooth', scrollMode: 'if-needed' });
|
||||
}, [selectedIndex]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={styles.items}>
|
||||
<div ref={$container}>
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.item}>没有找到结果</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -12,7 +12,6 @@ interface IProps {
|
|||
|
||||
export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||
const $container = useRef<HTMLDivElement>();
|
||||
const $image = useRef<HTMLInputElement>();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
|
@ -35,10 +34,6 @@ export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
|||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
const handleSelectImage = function () {
|
||||
console.log('image', this.files);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -27,4 +27,5 @@ export interface IAuthority {
|
|||
userId: IUser['id'];
|
||||
readable: boolean;
|
||||
editable: boolean;
|
||||
createUserId: IUser['id'];
|
||||
}
|
||||
|
|
|
@ -39,4 +39,5 @@ export interface IAuthority {
|
|||
userId: IUser['id'];
|
||||
readable: boolean;
|
||||
editable: boolean;
|
||||
createUserId: IUser['id'];
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Request,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
Get,
|
||||
Patch,
|
||||
ClassSerializerInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
@ -42,4 +43,12 @@ export class UserController {
|
|||
async updateUser(@Request() req, @Body() dto: UpdateUserDto) {
|
||||
return await this.userService.updateUser(req.user, dto);
|
||||
}
|
||||
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@Get('/')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(JwtGuard)
|
||||
async getUsers() {
|
||||
return this.userService.getUsers();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,4 +149,15 @@ export class UserService {
|
|||
const user = this.jwtService.decode(token) as UserEntity;
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param pagination
|
||||
* @returns
|
||||
*/
|
||||
async getUsers() {
|
||||
const query = this.userRepo.createQueryBuilder('user');
|
||||
const [data] = await query.getManyAndCount();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ importers:
|
|||
'@tiptap/extension-italic': ^2.0.0-beta.25
|
||||
'@tiptap/extension-link': ^2.0.0-beta.36
|
||||
'@tiptap/extension-list-item': ^2.0.0-beta.20
|
||||
'@tiptap/extension-mention': ^2.0.0-beta.95
|
||||
'@tiptap/extension-ordered-list': ^2.0.0-beta.27
|
||||
'@tiptap/extension-paragraph': ^2.0.0-beta.23
|
||||
'@tiptap/extension-placeholder': ^2.0.0-beta.47
|
||||
|
@ -143,6 +144,7 @@ importers:
|
|||
'@tiptap/extension-italic': 2.0.0-beta.25_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-link': 2.0.0-beta.36_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-list-item': 2.0.0-beta.20_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-mention': 2.0.0-beta.95_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-ordered-list': 2.0.0-beta.27_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-paragraph': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171
|
||||
'@tiptap/extension-placeholder': 2.0.0-beta.47_@tiptap+core@2.0.0-beta.171
|
||||
|
@ -1740,6 +1742,17 @@ packages:
|
|||
'@tiptap/core': 2.0.0-beta.171
|
||||
dev: false
|
||||
|
||||
/@tiptap/extension-mention/2.0.0-beta.95_@tiptap+core@2.0.0-beta.171:
|
||||
resolution: {integrity: sha512-AiikYJa33APtMI7c6a4EpPhqAYxHHnub5b9hd62zM1peBp2D2aO1dEIL7cB7O2P8EBZ2pu2QN2mFcCbVlSi0Xw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.0.0-beta.1
|
||||
dependencies:
|
||||
'@tiptap/core': 2.0.0-beta.171
|
||||
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
||||
prosemirror-model: 1.16.1
|
||||
prosemirror-state: 1.3.4
|
||||
dev: false
|
||||
|
||||
/@tiptap/extension-ordered-list/2.0.0-beta.27_@tiptap+core@2.0.0-beta.171:
|
||||
resolution: {integrity: sha512-apFDeignxdZb3cA3p1HJu0zw1JgJdBYUBz1r7f99qdNybYuk3I/1MPUvlOuOgvIrBB/wydoyVDP+v9F7QN3tfQ==}
|
||||
peerDependencies:
|
||||
|
|
Loading…
Reference in New Issue