mirror of
https://github.com/docmost/docmost.git
synced 2025-11-12 19:22:39 +10:00
page icon emoji picker
This commit is contained in:
@ -9,6 +9,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.1.2",
|
||||||
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@mantine/core": "^7.2.2",
|
"@mantine/core": "^7.2.2",
|
||||||
"@mantine/form": "^7.2.2",
|
"@mantine/form": "^7.2.2",
|
||||||
"@mantine/hooks": "^7.2.2",
|
"@mantine/hooks": "^7.2.2",
|
||||||
@ -20,6 +22,7 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"emoji-mart": "^5.5.2",
|
||||||
"jotai": "^2.5.1",
|
"jotai": "^2.5.1",
|
||||||
"jotai-optics": "^0.3.1",
|
"jotai-optics": "^0.3.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|||||||
39
apps/client/src/components/emoji-picker.tsx
Normal file
39
apps/client/src/components/emoji-picker.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import data from '@emoji-mart/data';
|
||||||
|
import Picker from '@emoji-mart/react';
|
||||||
|
import { ActionIcon, Popover } from '@mantine/core';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
|
||||||
|
export interface EmojiPickerInterface {
|
||||||
|
onEmojiSelect: (emoji: any) => void;
|
||||||
|
icon: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiPicker({ onEmojiSelect, icon }: EmojiPickerInterface) {
|
||||||
|
const [opened, handlers] = useDisclosure(false);
|
||||||
|
|
||||||
|
const handleEmojiSelect = (emoji) => {
|
||||||
|
onEmojiSelect(emoji);
|
||||||
|
handlers.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
opened={opened}
|
||||||
|
onClose={handlers.close}
|
||||||
|
width={200}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<Popover.Target>
|
||||||
|
<ActionIcon color="gray" variant="transparent" onClick={handlers.toggle}>
|
||||||
|
{icon}
|
||||||
|
</ActionIcon>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown bg="000" style={{ border: "none" }}>
|
||||||
|
<Picker data={data} onEmojiSelect={handleEmojiSelect} />
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiPicker;
|
||||||
@ -1,25 +1,38 @@
|
|||||||
import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query';
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
createPage,
|
createPage,
|
||||||
deletePage,
|
deletePage,
|
||||||
getPageById,
|
getPageById,
|
||||||
|
getPages,
|
||||||
getRecentChanges,
|
getRecentChanges,
|
||||||
updatePage,
|
updatePage,
|
||||||
} from '@/features/page/services/page-service';
|
} from "@/features/page/services/page-service";
|
||||||
import { IPage } from '@/features/page/types/page.types';
|
import { IPage } from "@/features/page/types/page.types";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
const RECENT_CHANGES_KEY = ['recentChanges'];
|
const RECENT_CHANGES_KEY = ["recentChanges"];
|
||||||
|
|
||||||
export function usePageQuery(pageId: string): UseQueryResult<IPage, Error> {
|
export function usePageQuery(pageId: string): UseQueryResult<IPage, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['pages', pageId],
|
queryKey: ["pages", pageId],
|
||||||
queryFn: () => getPageById(pageId),
|
queryFn: () => getPageById(pageId),
|
||||||
enabled: !!pageId,
|
enabled: !!pageId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGetPagesQuery(): UseQueryResult<IPage[], Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["pages"],
|
||||||
|
queryFn: () => getPages(),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
|
export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: RECENT_CHANGES_KEY,
|
queryKey: RECENT_CHANGES_KEY,
|
||||||
@ -31,17 +44,14 @@ export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
|
|||||||
export function useCreatePageMutation() {
|
export function useCreatePageMutation() {
|
||||||
return useMutation<IPage, Error, Partial<IPage>>({
|
return useMutation<IPage, Error, Partial<IPage>>({
|
||||||
mutationFn: (data) => createPage(data),
|
mutationFn: (data) => createPage(data),
|
||||||
|
onSuccess: (data) => {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdatePageMutation() {
|
export function useUpdatePageMutation() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation<IPage, Error, Partial<IPage>>({
|
return useMutation<IPage, Error, Partial<IPage>>({
|
||||||
mutationFn: (data) => updatePage(data),
|
mutationFn: (data) => updatePage(data),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {},
|
||||||
queryClient.setQueryData(['pages', data.id], data);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +59,7 @@ export function useDeletePageMutation() {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (pageId: string) => deletePage(pageId),
|
mutationFn: (pageId: string) => deletePage(pageId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notifications.show({ message: 'Page deleted successfully' });
|
notifications.show({ message: "Page deleted successfully" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,24 +23,28 @@ import { FillFlexParent } from './components/fill-flex-parent';
|
|||||||
import { TreeNode } from './types';
|
import { TreeNode } from './types';
|
||||||
import { treeApiAtom } from './atoms/tree-api-atom';
|
import { treeApiAtom } from './atoms/tree-api-atom';
|
||||||
import { usePersistence } from '@/features/page/tree/hooks/use-persistence';
|
import { usePersistence } from '@/features/page/tree/hooks/use-persistence';
|
||||||
import { getPages } from '@/features/page/services/page-service';
|
|
||||||
import useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
|
import useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { convertToTree } from '@/features/page/tree/utils';
|
import { convertToTree, updateTreeNodeIcon } from '@/features/page/tree/utils';
|
||||||
|
import { useGetPagesQuery, useUpdatePageMutation } from '@/features/page/queries/page-query';
|
||||||
|
import EmojiPicker from '@/components/emoji-picker';
|
||||||
|
import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom';
|
||||||
|
|
||||||
export default function PageTree() {
|
export default function PageTree() {
|
||||||
const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>();
|
const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>();
|
||||||
const [tree, setTree] = useAtom<TreeApi<TreeNode>>(treeApiAtom);
|
const [tree, setTree] = useAtom<TreeApi<TreeNode>>(treeApiAtom);
|
||||||
const { data: pageOrderData } = useWorkspacePageOrder();
|
const { data: pageOrderData } = useWorkspacePageOrder();
|
||||||
|
const { data: pagesData, isLoading } = useGetPagesQuery();
|
||||||
const rootElement = useRef<HTMLDivElement>();
|
const rootElement = useRef<HTMLDivElement>();
|
||||||
const { pageId } = useParams();
|
const { pageId } = useParams();
|
||||||
|
|
||||||
const fetchAndSetTreeData = async () => {
|
const fetchAndSetTreeData = async () => {
|
||||||
if (pageOrderData?.childrenIds) {
|
if (pageOrderData?.childrenIds) {
|
||||||
try {
|
try {
|
||||||
const pages = await getPages();
|
if (!isLoading) {
|
||||||
const treeData = convertToTree(pages, pageOrderData.childrenIds);
|
const treeData = convertToTree(pagesData, pageOrderData.childrenIds);
|
||||||
setData(treeData);
|
setData(treeData);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching tree data: ', err);
|
console.error('Error fetching tree data: ', err);
|
||||||
}
|
}
|
||||||
@ -49,7 +53,7 @@ export default function PageTree() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAndSetTreeData();
|
fetchAndSetTreeData();
|
||||||
}, [pageOrderData?.childrenIds]);
|
}, [pageOrderData?.childrenIds, isLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -87,11 +91,28 @@ export default function PageTree() {
|
|||||||
|
|
||||||
function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
|
function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const updatePageMutation = useUpdatePageMutation();
|
||||||
|
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
navigate(`/p/${node.id}`);
|
navigate(`/p/${node.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateNodeIcon = (nodeId, newIcon) => {
|
||||||
|
const updatedTreeData = updateTreeNodeIcon(treeData, nodeId, newIcon);
|
||||||
|
setTreeData(updatedTreeData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmojiIconClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmojiSelect = (emoji) => {
|
||||||
|
handleUpdateNodeIcon(node.id, emoji.native);
|
||||||
|
updatePageMutation.mutateAsync({ id: node.id, icon: emoji.native });
|
||||||
|
};
|
||||||
|
|
||||||
if (node.willReceiveDrop && node.isClosed) {
|
if (node.willReceiveDrop && node.isClosed) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (node.state.willReceiveDrop) node.open();
|
if (node.state.willReceiveDrop) node.open();
|
||||||
@ -108,7 +129,13 @@ function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
|
|||||||
>
|
>
|
||||||
<PageArrow node={node} />
|
<PageArrow node={node} />
|
||||||
|
|
||||||
<IconFileDescription size="18px" style={{ marginRight: '4px' }} />
|
<div onClick={handleEmojiIconClick} style={{ marginRight: '4px' }}>
|
||||||
|
<EmojiPicker onEmojiSelect={handleEmojiSelect} icon={
|
||||||
|
node.data.icon ? node.data.icon :
|
||||||
|
<IconFileDescription size="18px" />
|
||||||
|
|
||||||
|
}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className={classes.text}>
|
<span className={classes.text}>
|
||||||
{node.isEditing ? (
|
{node.isEditing ? (
|
||||||
|
|||||||
@ -60,3 +60,15 @@ export const updateTreeNodeName = (nodes: TreeNode[], nodeId: string, newName: s
|
|||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateTreeNodeIcon = (nodes: TreeNode[], nodeId: string, newIcon: string): TreeNode[] => {
|
||||||
|
return nodes.map(node => {
|
||||||
|
if (node.id === nodeId) {
|
||||||
|
return { ...node, icon: newIcon };
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
return { ...node, children: updateTreeNodeIcon(node.children, nodeId, newIcon) };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -9,6 +9,10 @@ export class CreatePageDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
parentPageId?: string;
|
parentPageId?: string;
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -111,6 +111,12 @@ importers:
|
|||||||
|
|
||||||
apps/client:
|
apps/client:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@emoji-mart/data':
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2
|
||||||
|
'@emoji-mart/react':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0)
|
||||||
'@mantine/core':
|
'@mantine/core':
|
||||||
specifier: ^7.2.2
|
specifier: ^7.2.2
|
||||||
version: 7.4.0(@mantine/hooks@7.4.0)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
|
version: 7.4.0(@mantine/hooks@7.4.0)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -144,6 +150,9 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^2.30.0
|
specifier: ^2.30.0
|
||||||
version: 2.30.0
|
version: 2.30.0
|
||||||
|
emoji-mart:
|
||||||
|
specifier: ^5.5.2
|
||||||
|
version: 5.5.2
|
||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.5.1
|
specifier: ^2.5.1
|
||||||
version: 2.6.1(@types/react@18.2.47)(react@18.2.0)
|
version: 2.6.1(@types/react@18.2.47)(react@18.2.0)
|
||||||
@ -2377,6 +2386,20 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
|
/@emoji-mart/data@1.1.2:
|
||||||
|
resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==}
|
||||||
|
peerDependencies:
|
||||||
|
emoji-mart: ^5.2
|
||||||
|
react: ^16.8 || ^17 || ^18
|
||||||
|
dependencies:
|
||||||
|
emoji-mart: 5.5.2
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@esbuild/aix-ppc64@0.19.11:
|
/@esbuild/aix-ppc64@0.19.11:
|
||||||
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
|
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -6567,6 +6590,10 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/emoji-mart@5.5.2:
|
||||||
|
resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/emoji-regex@8.0.0:
|
/emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user