From f192f07dc814bd168c74c16aac2d15a5562aaec5 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 08:35:02 +0900 Subject: [PATCH 01/20] fix: add missing text decoration for keyword --- app/globals.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index ca9e89e..e1db445 100644 --- a/app/globals.css +++ b/app/globals.css @@ -116,9 +116,9 @@ div.loading-bar { } mark { - text-decoration: dotted; + text-decoration: underline dotted; text-decoration-color: red; - text-decoration-thickness: 2px; + text-decoration-thickness: 3px; color: transparent; background: transparent; } From fa95768a81bbb223f3653fd1158a979df7039b0c Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:46:43 +0900 Subject: [PATCH 02/20] fix: use formData on file upload require file on document creation --- app/components/DocumentCreateDialogue/index.tsx | 13 ++++++++----- utils/api.ts | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/components/DocumentCreateDialogue/index.tsx b/app/components/DocumentCreateDialogue/index.tsx index 829a10f..7c85536 100644 --- a/app/components/DocumentCreateDialogue/index.tsx +++ b/app/components/DocumentCreateDialogue/index.tsx @@ -4,7 +4,6 @@ import Dialogue from '@/components/Dialogue'; import FileInput from '@/components/Dialogue/Input/FileInput'; import TextInput from '@/components/Dialogue/Input/TextInput'; import { useToast } from '@/states/toast'; -import { useViewer } from '@/states/viewer'; import { createDocument, uploadFile } from '@/utils/api'; import { throwError } from '@/utils/ui'; import { useRouter } from 'next/navigation'; @@ -21,7 +20,6 @@ export default function DocumentCreateDialogue({ }: DocumentCreateDialogueProps) { const router = useRouter(); const [load, setLoad] = useState(false); - const documentId = useViewer((state) => state.document?.documentId ?? 0); const pushToast = useToast((state) => state.pushToast); return ( @@ -43,13 +41,18 @@ export default function DocumentCreateDialogue({ return setLoad(false); } + // check file + if (fileInput.files === null || fileInput.files.length === 0) { + throwError('요약할 파일을 입력해 주세요'); + return setLoad(false); + } + (async () => { // create new Document const newDocument = await createDocument(titleInput.value); + // upload file (if exists) - if (fileInput.files !== null && fileInput.files.length > 0) { - await uploadFile(documentId, fileInput.files[0]); - } + await uploadFile(newDocument.documentId, fileInput.files![0]); pushToast({ id: new Date().getTime(), diff --git a/utils/api.ts b/utils/api.ts index 85bca13..5366558 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -59,7 +59,10 @@ export async function getKeywordInfo( export async function uploadFile(documentId: number, file: File) { const type = uploadFileType(file.type); - return await httpPost(`${API_BASE_URL}/documents/${documentId}/${type}`, file, true, false); + if (type === undefined) return; + const form = new FormData(); + form.append(type, file); + return await httpPost(`${API_BASE_URL}/documents/${documentId}/${type}`, form, false, false); } function uploadFileType(type: string) { From c1a8c19407cbdd84c04e9dcc9baad12bdfd5704b Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:47:25 +0900 Subject: [PATCH 03/20] feat: impl contextMenu for keyword manipulation --- .../contextMenuViewer.module.css | 23 +++++++ app/components/ContextMenuViewer/index.tsx | 39 +++++++++++ app/layout.tsx | 2 + components/DataViewer/OptionPanel/index.tsx | 66 ++++++++++++++++--- .../OptionPanel/optionPanel.module.css | 9 +++ states/contextMenu.ts | 21 ++++++ states/viewer.ts | 11 ++-- 7 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 app/components/ContextMenuViewer/contextMenuViewer.module.css create mode 100644 app/components/ContextMenuViewer/index.tsx create mode 100644 states/contextMenu.ts diff --git a/app/components/ContextMenuViewer/contextMenuViewer.module.css b/app/components/ContextMenuViewer/contextMenuViewer.module.css new file mode 100644 index 0000000..059e5f3 --- /dev/null +++ b/app/components/ContextMenuViewer/contextMenuViewer.module.css @@ -0,0 +1,23 @@ +.container { + position: absolute; + background-color: var(--background); + border-radius: 6px; + border: 1px solid var(--grey-normal); + margin: 0; + padding: 0; + animation: var(--intro-animation); + box-sizing: border-box; + overflow: hidden; +} + +.item { + min-width: 100px; + cursor: default; + list-style: none; + margin: 0; + padding: 10px; +} + +.item:hover { + background-color: var(--grey-normal); +} diff --git a/app/components/ContextMenuViewer/index.tsx b/app/components/ContextMenuViewer/index.tsx new file mode 100644 index 0000000..134d86a --- /dev/null +++ b/app/components/ContextMenuViewer/index.tsx @@ -0,0 +1,39 @@ +import { useContextMenu } from '@/states/contextMenu'; +import styles from './contextMenuViewer.module.css'; +import { CSSProperties, useEffect } from 'react'; + +export default function ContextMenuViewer() { + const { menu, setMenu } = useContextMenu((state) => ({ + menu: state.menu, + setMenu: state.setMenu, + })); + + useEffect(() => { + if (menu === null) return; + const pointerDown = () => setMenu(null); + document.addEventListener('pointerdown', pointerDown); + return () => document.removeEventListener('pointerdown', pointerDown); + }, [menu, setMenu]); + + if (menu === null) return <>; + + const style: CSSProperties = {}; + // check x overflow + if (menu.position.x < window.innerWidth / 2) style.left = menu.position.x; + else style.right = window.innerWidth - menu.position.x; + // check y overflow + if (menu.position.y < window.innerHeight / 2) style.top = menu.position.y; + else style.bottom = window.innerHeight - menu.position.y; + + return ( + + ); +} diff --git a/app/layout.tsx b/app/layout.tsx index f607247..0ddd847 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,7 @@ import { Inter } from 'next/font/google'; import ToastViewer from './components/ToastViewer'; import OverlayViewer from './components/OverlayViewer'; import Header from './components/Header'; +import ContextMenuViewer from './components/ContextMenuViewer'; const inter = Inter({ subsets: ['latin'] }); @@ -20,6 +21,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} + ); diff --git a/components/DataViewer/OptionPanel/index.tsx b/components/DataViewer/OptionPanel/index.tsx index caef3d8..1f9bc3c 100644 --- a/components/DataViewer/OptionPanel/index.tsx +++ b/components/DataViewer/OptionPanel/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; import styles from './optionPanel.module.css'; import 'material-symbols'; import { useViewer } from '@/states/viewer'; @@ -6,6 +6,7 @@ import SmallIconButton from '@/components/Button/SmallIconButton'; import ClickableBackgroundButton from '@/components/BackgroundButton/ClickableBackgroundButton'; import { updateKeywords } from '@/utils/api'; import Divider, { useDivider } from '@/components/Divider'; +import { ContextMenuItem, useContextMenu } from '@/states/contextMenu'; const DEFAULT_WIDTH = 350; @@ -52,8 +53,6 @@ export default function OptionPanel({ children }: OptionPanelProps) { clearSyncDocument(); // sync local changes await updateKeywords(documentId, addedKeys, removedKeys); - // wait for sync to finish - // TODO: implement this })(); }; @@ -88,9 +87,7 @@ export default function OptionPanel({ children }: OptionPanelProps) { icon={'sync'} onClick={syncKeywords} /> - ) : ( - <> - )} + ) : null} {addedKeys.map((v) => ( @@ -130,19 +127,72 @@ interface KeywordViewProps { } function KeywordView({ label }: KeywordViewProps) { - const { state, setKeyword } = useViewer((state) => ({ + const setMenu = useContextMenu((state) => state.setMenu); + const { state, setKeyword, deleteKeyword } = useViewer((state) => ({ state: state.keywords.get(label), setKeyword: state.setKeyword, + deleteKeyword: state.deleteKeyword, })); + const items = useMemo(() => { + const trimmedLabel = label.substring(0, 10); + const items: ContextMenuItem[] = []; + if (state === undefined) return items; + switch (state.type) { + case 'ADD': + items.push({ + label: `'${trimmedLabel}' 추가 취소`, + onClick: () => deleteKeyword(label), + }); + break; + case 'STABLE': + items.push({ + label: `'${trimmedLabel}' 삭제`, + onClick: () => setKeyword(label, { enabled: false, type: 'DELETE' }), + }); + break; + case 'DELETE': + items.push({ + label: `'${trimmedLabel}' 삭제 취소`, + onClick: () => setKeyword(label, { ...state, type: 'STABLE' }), + }); + break; + } + + return items; + }, [deleteKeyword, label, setKeyword, state]); + if (state === undefined) return <>; + const icon = typeToIcon(state.type); + return (
setKeyword(label, !state.enabled)} + onClick={() => setKeyword(label, { ...state, enabled: !state.enabled })} + onContextMenu={(e) => { + e.preventDefault(); + setMenu({ + items: items, + position: { x: e.clientX, y: e.clientY }, + }); + }} > + {icon !== undefined ? ( + {icon} + ) : null} {label}
); } + +function typeToIcon(type: KeywordType) { + switch (type) { + case 'ADD': + return 'add'; + case 'STABLE': + return undefined; + case 'DELETE': + return 'remove'; + } +} diff --git a/components/DataViewer/OptionPanel/optionPanel.module.css b/components/DataViewer/OptionPanel/optionPanel.module.css index 873c9f4..0c5f64f 100644 --- a/components/DataViewer/OptionPanel/optionPanel.module.css +++ b/components/DataViewer/OptionPanel/optionPanel.module.css @@ -61,6 +61,10 @@ } .keyword { + animation: var(--intro-animation); + display: flex; + gap: 5px; + align-items: center; padding: 12px 18px; font-size: 16px; color: var(--foreground); @@ -81,6 +85,11 @@ user-select: none; } +.keywordIcon { + font-size: 18px; + color: var(--grey-darker); +} + .tools { gap: 10px; height: 46px; diff --git a/states/contextMenu.ts b/states/contextMenu.ts new file mode 100644 index 0000000..9b6b9bb --- /dev/null +++ b/states/contextMenu.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +interface ContextMenuState { + menu: ContextMenu | null; + setMenu: (menus: ContextMenu | null) => void; +} + +export interface ContextMenu { + items: ContextMenuItem[]; + position: Coord; +} + +export interface ContextMenuItem { + label: string; + onClick: () => void; +} + +export const useContextMenu = create()((set) => ({ + menu: null, + setMenu: (menu) => set({ menu: menu }), +})); diff --git a/states/viewer.ts b/states/viewer.ts index 21b7c0b..46e84aa 100644 --- a/states/viewer.ts +++ b/states/viewer.ts @@ -25,7 +25,7 @@ interface ViewerActions { setFocusNodeCallback: (callback: ((node: NodeData) => void) | undefined) => void; addKeyword: (keyword: string, state?: boolean) => void; deleteKeyword: (keyword: string) => void; - setKeyword: (keyword: string, state: boolean) => void; + setKeyword: (keyword: string, state: KeywordState) => void; setAllKeyword: (state: boolean) => void; setDataStr: (dataStr: string[][]) => void; findDataStr: (from: number[], keyword: string) => KeywordSourceResult | undefined; @@ -80,11 +80,10 @@ export const useViewer = create()((set, get) => ({ }), deleteKeyword: (keyword) => { const keywords = get().keywords; + const newKeywords = new Map([...keywords]); + newKeywords.delete(keyword); set({ - keywords: new Map([...keywords]).set(keyword, { - enabled: false, - type: 'DELETE', - }), + keywords: newKeywords, }); }, setKeyword: (keyword, state) => { @@ -92,7 +91,7 @@ export const useViewer = create()((set, get) => ({ const prevState = keywords.get(keyword); if (prevState === undefined) return; set({ - keywords: new Map([...keywords]).set(keyword, { ...prevState, enabled: state }), + keywords: new Map([...keywords]).set(keyword, { ...state }), }); }, setAllKeyword: (state) => { From 1c38f6c223b0cb5b43dcc61f5bf2de9c3091e13d Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:15:34 +0900 Subject: [PATCH 04/20] feat: change progressBar animation --- app/globals.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index e1db445..2cb92e5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -76,10 +76,10 @@ div.loading-container { @keyframes barAnimation { 0% { - background-color: var(--primary-light); + background-color: var(--primary-dark); } 50% { - background-color: var(--primary-dark); + background-color: var(--highlight); } 100% { background-color: var(--primary-light); From 1021d8f08da3a14e275721f1fa6e233ce2b9eb3b Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:17:18 +0900 Subject: [PATCH 05/20] feat: dataSource cannot be none --- app/document/[documentId]/page.tsx | 32 ------------------------------ global.d.ts | 2 +- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/app/document/[documentId]/page.tsx b/app/document/[documentId]/page.tsx index 405b1b2..1347803 100644 --- a/app/document/[documentId]/page.tsx +++ b/app/document/[documentId]/page.tsx @@ -122,41 +122,9 @@ function DataViewer({ type }: DataViewerProps) { return ; case 'audio': return ; - case 'none': - return ; } } -function DataUploadDialogue() { - const pushToast = useToast((state) => state.pushToast); - const documentId = useViewer((state) => state.document?.documentId); - return ( -
- { - if (documentId === undefined) return; - const fileInput = (e.target as any).file as HTMLInputElement; - // upload file (if exists) - (async () => { - if (fileInput.files !== null && fileInput.files.length > 0) { - await uploadFile(documentId, fileInput.files[0]); - } - })(); - }} - > - pushToast({ id: new Date().getTime(), duraton: 3000, msg: msg })} - /> - -
- ); -} - function GraphViewer() { const hasGraph = useViewer((state) => state.mindMapData?.keywords.length !== 0); if (!hasGraph) return

no graph available

; diff --git a/global.d.ts b/global.d.ts index e146b50..a64806d 100644 --- a/global.d.ts +++ b/global.d.ts @@ -189,7 +189,7 @@ interface GraphData { graph: Map; } -type WBSourceDataType = 'pdf' | 'audio' | 'none'; +type WBSourceDataType = 'pdf' | 'audio'; interface WBSourceData { url: string; From 7f2d3993dc15491be57463d33c79cbabb2e474f2 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:18:54 +0900 Subject: [PATCH 06/20] feat: handle getMindmap exception from viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 202 일경우 마인드맵 생성중 자료 파일은 변경될 수 없음 --- app/document/[documentId]/page.tsx | 11 ++++------- states/viewer.ts | 14 +++++--------- utils/api.ts | 6 ++++-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/document/[documentId]/page.tsx b/app/document/[documentId]/page.tsx index 1347803..d25a230 100644 --- a/app/document/[documentId]/page.tsx +++ b/app/document/[documentId]/page.tsx @@ -11,10 +11,6 @@ const GraphCanvas = dynamic(() => import('@/components/GraphCanvas'), { ssr: fal import 'material-symbols'; import AudioViewer from '@/components/DataViewer/AudioViewer'; import ClickableBackgroundButton from '@/components/BackgroundButton/ClickableBackgroundButton'; -import Dialogue from '@/components/Dialogue'; -import FileInput from '@/components/Dialogue/Input/FileInput'; -import { useToast } from '@/states/toast'; -import { uploadFile } from '@/utils/api'; import { useViewerEvents } from '@/utils/ui'; import Divider, { useDivider } from '@/components/Divider'; @@ -46,16 +42,17 @@ export default function DoucumentsPage({ params }: DocumentPageProps) { const eventCallback = useCallback( (type: ViewerEventType) => { // reload on event + if (documentData === null) return loadDocument(documentId); // TODO: implement other types of events syncDocument(); }, - [syncDocument], + [documentData, documentId, loadDocument, syncDocument], ); // subscribe to events useViewerEvents(documentId, eventCallback); - if (documentData === null) return ; + if (documentData === null) return ; return (
@@ -131,7 +128,7 @@ function GraphViewer() { return ( <>
- }> + }>
diff --git a/states/viewer.ts b/states/viewer.ts index 46e84aa..751f4b5 100644 --- a/states/viewer.ts +++ b/states/viewer.ts @@ -134,6 +134,9 @@ export const useViewer = create()((set, get) => ({ // fetch mindMapData const newMindMapData = await getMindMapData(documentId); + // mindmap generation is in progress + if (newMindMapData === null) return; + // local keyword map const newKeywords = new Map(); for (const keyword of newMindMapData.keywords) { @@ -158,17 +161,10 @@ export const useViewer = create()((set, get) => ({ const document = get().document; const nextState: ViewerStateSlice = {}; if (document === null) return; - // fetch documentMetaData - const newDocumentData = await getDocument(document.documentId); + // fetch mindMapData: always update - nextState.mindMapData = await getMindMapData(document.documentId); + nextState.mindMapData = (await getMindMapData(document.documentId)) ?? undefined; - // fetch datasource: update only if datasource has changed - const newDataSource = await getDataSource(document.documentId, newDocumentData.dataType); - if (newDataSource.url !== get().dataSource?.url) { - nextState.dataSource = newDataSource; - nextState.document = newDocumentData; - } set({ nextState: nextState }); }, applySyncDocument: () => { diff --git a/utils/api.ts b/utils/api.ts index 5366558..d02bc02 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -30,8 +30,10 @@ export async function refreshToken(): Promise { ); } -export async function getMindMapData(documentId: number): Promise { - return (await httpGet(`${API_BASE_URL}/documents/${documentId}/mindmap`))?.json(); +export async function getMindMapData(documentId: number): Promise { + const res = await httpGet(`${API_BASE_URL}/documents/${documentId}/mindmap`); + if (res?.status === 200) return res.json(); + return null; } export async function getDataSource( From c62a4c16dd9e5c7ab78d137baa3bb8942e1b7a3c Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:28:14 +0900 Subject: [PATCH 07/20] feat: change labels and create validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문서 제목 필수 아님 문서 -> 학습 --- app/components/DocumentCreateDialogue/index.tsx | 17 ++++++++++------- app/components/Header/index.tsx | 2 +- components/Dialogue/Input/FileInput.tsx | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/components/DocumentCreateDialogue/index.tsx b/app/components/DocumentCreateDialogue/index.tsx index 7c85536..b76daa8 100644 --- a/app/components/DocumentCreateDialogue/index.tsx +++ b/app/components/DocumentCreateDialogue/index.tsx @@ -26,7 +26,7 @@ export default function DocumentCreateDialogue({
{ e.preventDefault(); @@ -35,18 +35,21 @@ export default function DocumentCreateDialogue({ const titleInput = (e.target as any).title as HTMLInputElement; const fileInput = (e.target as any).file as HTMLInputElement; - // check title - if (titleInput.value.length === 0) { - throwError('문서의 제목을 입력해 주세요'); - return setLoad(false); - } - // check file if (fileInput.files === null || fileInput.files.length === 0) { throwError('요약할 파일을 입력해 주세요'); return setLoad(false); } + // check title + if (titleInput.value.length === 0) { + // throwError('문서의 제목을 입력해 주세요'); + // return setLoad(false); + + // set to filename if left blank + titleInput.value = fileInput.files[0].name; + } + (async () => { // create new Document const newDocument = await createDocument(titleInput.value); diff --git a/app/components/Header/index.tsx b/app/components/Header/index.tsx index d783e08..d404157 100644 --- a/app/components/Header/index.tsx +++ b/app/components/Header/index.tsx @@ -21,7 +21,7 @@ export default function Header() { {userDataExists ? ( setOverlay( diff --git a/components/Dialogue/Input/FileInput.tsx b/components/Dialogue/Input/FileInput.tsx index 71bf92b..ac5bf45 100644 --- a/components/Dialogue/Input/FileInput.tsx +++ b/components/Dialogue/Input/FileInput.tsx @@ -28,7 +28,7 @@ export default function FileInput({ name, types, onError, typeNames }: FileInput return (
-

요약할 파일

+

학습 자료

e.preventDefault()} From 3d5efc8fb88c4bb62d950f26b9e5d9dfacd2f59b Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:34 +0900 Subject: [PATCH 08/20] feat: add warning color to global var --- app/globals.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/globals.css b/app/globals.css index 2cb92e5..dec1606 100644 --- a/app/globals.css +++ b/app/globals.css @@ -14,6 +14,7 @@ --grey-darker: #383b40; --foreground-disabled: #c7cfd0; --foreground: #ffffff; + --warning-color: #c2599a; --background: #f4f6f8; --background-accent: #e6f5f5; --intro-animation: show 0.5s; From 8b4b6a9770452d66b28eb2d5a69cda8a6ba4d43d Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:59 +0900 Subject: [PATCH 09/20] feat: update types to handle multiple documents in single viewer --- global.d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/global.d.ts b/global.d.ts index a64806d..c9368fc 100644 --- a/global.d.ts +++ b/global.d.ts @@ -66,9 +66,10 @@ type ExtendedSpriteText = import('three-spritetext').default & { }; interface NodeData { + documentId: number; children: NodeData[]; parent?: NodeData; - id: number; + id: string; label: string; labelMesh?: ExtendedSpriteText; scale: number; @@ -90,8 +91,9 @@ interface NodeDataLegacy { } interface LinkData { - source: number | NodeData; - target: number | NodeData; + documentId: number; + source: string | NodeData; + target: string | NodeData; } interface LinkDataLegacy { From 8973bdd96700d1e3fd9a7069e3eab3f8f9a91640 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:34:37 +0900 Subject: [PATCH 10/20] feat: now mindmap data is a map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit documentId: GraphData 뷰어 한개에서 여러개의 그래프를 볼 수 있게 하도록 --- states/viewer.ts | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/states/viewer.ts b/states/viewer.ts index 751f4b5..c0c671e 100644 --- a/states/viewer.ts +++ b/states/viewer.ts @@ -3,7 +3,7 @@ import { create } from 'zustand'; interface ViewerState { dataSource: WBSourceData | null; - mindMapData: GraphData | null; + mindMapData: Map | null; nextState: ViewerStateSlice | null; view: ViewerPage; document: WBDocument | null; @@ -37,11 +37,12 @@ interface ViewerActions { syncDocument: () => void; applySyncDocument: () => void; clearSyncDocument: () => void; + setMindMapData: (mindMapData: Map) => void; } interface ViewerStateSlice { dataSource?: WBSourceData; - mindMapData?: GraphData; + mindMapData?: Map; document?: WBDocument; } @@ -132,14 +133,18 @@ export const useViewer = create()((set, get) => ({ // fetch sourceData const newDataSource = await getDataSource(documentId, newDocument.dataType); // fetch mindMapData - const newMindMapData = await getMindMapData(documentId); + const newMindMap = await getMindMapData(documentId); // mindmap generation is in progress - if (newMindMapData === null) return; + if (newMindMap === null) return; + + // mindMapData map + const newMindMapData = new Map(); + newMindMapData.set(documentId, newMindMap); // local keyword map const newKeywords = new Map(); - for (const keyword of newMindMapData.keywords) { + for (const keyword of newMindMap.keywords) { newKeywords.set(keyword, { enabled: false, type: 'STABLE', @@ -163,7 +168,10 @@ export const useViewer = create()((set, get) => ({ if (document === null) return; // fetch mindMapData: always update - nextState.mindMapData = (await getMindMapData(document.documentId)) ?? undefined; + const newMindMap = await getMindMapData(document.documentId); + if (newMindMap !== null) { + nextState.mindMapData = new Map([[document.documentId, newMindMap]]); + } set({ nextState: nextState }); }, @@ -173,11 +181,13 @@ export const useViewer = create()((set, get) => ({ // local keyword map const newKeywords = new Map(); if (newState.mindMapData !== undefined) { - for (const keyword of newState.mindMapData.keywords) { - newKeywords.set(keyword, { - enabled: false, - type: 'STABLE', - }); + for (const [documentId, mindMap] of newState.mindMapData) { + for (const keyword of mindMap.keywords) { + newKeywords.set(keyword, { + enabled: false, + type: 'STABLE', + }); + } } } set({ @@ -191,4 +201,7 @@ export const useViewer = create()((set, get) => ({ clearSyncDocument: () => { set({ nextState: null }); }, + setMindMapData: (mindMapData: Map) => { + set({ mindMapData: mindMapData, selectedNode: undefined }); + }, })); From c815597eb9d042e5a183e2f54a8706c1493d2ba9 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:35:55 +0900 Subject: [PATCH 11/20] refactor: remove unused import --- app/components/Header/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Header/index.tsx b/app/components/Header/index.tsx index d404157..dbf2c46 100644 --- a/app/components/Header/index.tsx +++ b/app/components/Header/index.tsx @@ -6,6 +6,7 @@ import { useOverlay } from '@/states/overlay'; import DocumentCreateDialogue from '../DocumentCreateDialogue'; import Logo from '@/components/Logo'; import { useUser } from '@/states/user'; +import { useViewer } from '@/states/viewer'; export default function Header() { const { setOverlay } = useOverlay((state) => ({ setOverlay: state.setOverlay, @@ -33,7 +34,6 @@ export default function Header() { } /> ) : undefined} -
From 5661b766da5995f65d7becd9221518f1b245d5d4 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:38:29 +0900 Subject: [PATCH 12/20] feat: make graphRenderer to handle multiple graphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh 생성하는 부분을 각 factory(node, link)에서 분리 -> meshFactory 그래프 맵을 입력으로 받고, node, link 자체에 documentId 저장 (색 구분, NodeInfo에서 사용) node의 id를 string으로 변경 (documentId-id) --- components/GraphCanvas/GraphRenderer.tsx | 114 +++++++++++++++-------- components/GraphCanvas/index.tsx | 9 +- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/components/GraphCanvas/GraphRenderer.tsx b/components/GraphCanvas/GraphRenderer.tsx index e48288a..90134ea 100644 --- a/components/GraphCanvas/GraphRenderer.tsx +++ b/components/GraphCanvas/GraphRenderer.tsx @@ -2,7 +2,7 @@ import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from 'react'; import { invalidate, useFrame, useThree } from '@react-three/fiber'; import * as THREE from 'three'; -import { boundNumber, calculateLineGeometry, getPos } from '@/utils/whiteboardHelper'; +import { boundNumber, calculateLineGeometry, genColor, getPos } from '@/utils/whiteboardHelper'; import * as d3 from 'd3'; import SpriteText from 'three-spritetext'; import { useViewer } from '@/states/viewer'; @@ -26,7 +26,7 @@ const COLOR_HIGHLIGHT_STRING = '#027373'; const COLOR_HIGHLIGHT = new THREE.Color(COLOR_HIGHLIGHT_STRING); interface GraphRendererProps { - data: GraphData; + data: Map; // documentId : graphData } export default function GraphRenderer({ data }: GraphRendererProps) { @@ -41,13 +41,36 @@ export default function GraphRenderer({ data }: GraphRendererProps) { ); const lineMesh = useRef(); const circleMesh = useRef(); + const currentDocumentId = useViewer((state) => state.document?.documentId); + const staleColorMapRef = useRef>(new Map()); + + const meshFactory = useCallback( + (nodes: NodeData[], links: LinkData[]) => { + // create node instancedMesh + circleMesh.current = new THREE.InstancedMesh(geometry, nodeMaterial, nodes.length); + circleMesh.current.position.setZ(10); + circleMesh.current.frustumCulled = false; + // need to set color once before render! + circleMesh.current.setColorAt(0, COLOR_STALE); + groupRef.current!.add(circleMesh.current); + + // create line instancedMesh + lineMesh.current = new THREE.InstancedMesh(lineGeometry, lineMaterial, links.length); + lineMesh.current.frustumCulled = false; + groupRef.current!.add(lineMesh.current); + }, + [geometry, lineGeometry, lineMaterial, nodeMaterial], + ); const nodeDataFactory = useCallback( - (entries: IterableIterator<[number, string]>) => { + (documentId: number, entries: IterableIterator<[number, string]>) => { const nodes: NodeData[] = []; + // generate color + const color = documentId === currentDocumentId ? COLOR_STALE_STRING : genColor(); + staleColorMapRef.current.set(documentId, new THREE.Color(color)); for (const [id, keyword] of entries) { // label - const text = new SpriteText(keyword, 28, 'black') as ExtendedSpriteText; + const text = new SpriteText(keyword, 28, color) as ExtendedSpriteText; text.position.setZ(15); text.strokeColor = 'white'; text.strokeWidth = 2; @@ -55,7 +78,8 @@ export default function GraphRenderer({ data }: GraphRendererProps) { groupRef.current!.add(text); nodes.push({ - id: id, + documentId: documentId, + id: `${documentId}-${id}`, children: [], label: keyword, labelMesh: text, @@ -64,35 +88,27 @@ export default function GraphRenderer({ data }: GraphRendererProps) { } as NodeData); } - // create instancedMesh - circleMesh.current = new THREE.InstancedMesh(geometry, nodeMaterial, nodes.length); - circleMesh.current.position.setZ(10); - circleMesh.current.frustumCulled = false; - // need to set color once before render! - circleMesh.current.setColorAt(0, COLOR_STALE); - groupRef.current!.add(circleMesh.current); - return nodes; }, - [geometry, nodeMaterial], + [currentDocumentId], ); const linkDataFactory = useCallback( - (entries: IterableIterator<[string, number[]]>) => { + (documentId: number, entries: IterableIterator<[string, number[]]>) => { const links: LinkData[] = []; for (const [key, children] of entries) { const id = parseInt(key); for (const child of children) { - links.push({ source: id, target: child } as LinkData); + links.push({ + source: `${documentId}-${id}`, + target: `${documentId}-${child}`, + documentId: documentId, + } as LinkData); } } - // create instancedMesh - lineMesh.current = new THREE.InstancedMesh(lineGeometry, lineMaterial, links.length); - lineMesh.current.frustumCulled = false; - groupRef.current!.add(lineMesh.current); return links; }, - [lineGeometry, lineMaterial], + [], ); const { simulationRef, linksRef } = useSimulation( @@ -100,6 +116,7 @@ export default function GraphRenderer({ data }: GraphRendererProps) { groupRef, nodeDataFactory, linkDataFactory, + meshFactory, ); const { selectedNodeRef, hoverNodeRef, pointerDownRef } = usePointer(simulationRef, circleMesh); @@ -127,8 +144,10 @@ export default function GraphRenderer({ data }: GraphRendererProps) { const labelOpacity = needFocusOnNode ? Math.min(0.4, labelOpacityBasedOnZoom) : labelOpacityBasedOnZoom; - const defaultColor = needFocusOnNode ? COLOR_STALE_BACK : COLOR_STALE; + nodes.forEach((node, i) => { + const staleColor = staleColorMapRef.current.get(node.documentId) ?? COLOR_STALE; + const defaultColor = needFocusOnNode ? COLOR_STALE_BACK : staleColor; const m = new THREE.Matrix4(); if (node === selectedNodeRef.current?.node) { // selected node @@ -348,10 +367,14 @@ function usePointer( } function useSimulation( - data: GraphData, + data: Map, groupRef: RefObject, - nodeDataFactory: (entries: IterableIterator<[number, string]>) => NodeData[], - linkDataFactory: (entries: IterableIterator<[string, number[]]>) => LinkData[], + nodeDataFactory: (documentId: number, entries: IterableIterator<[number, string]>) => NodeData[], + linkDataFactory: ( + documentId: number, + entries: IterableIterator<[string, number[]]>, + ) => LinkData[], + meshFactory: (nodes: NodeData[], links: LinkData[]) => void, ) { const simulationRef = useRef>(d3.forceSimulation()); const linksRef = useRef([]); @@ -361,7 +384,9 @@ function useSimulation( if (simulationRef.current != null) simulationRef.current.stop(); // regenerated on graph data update - if (groupRef.current == null) return; + if (groupRef.current == null) { + return; + } // remove items from mesh group for (const child of groupRef.current.children) { @@ -371,19 +396,34 @@ function useSimulation( groupRef.current.clear(); - // generate nodes - const nodes: NodeData[] = nodeDataFactory(data.keywords.entries()); + const nodes: NodeData[] = []; + linksRef.current = []; + + // support multiple data + for (const [documentId, graphData] of data) { + // generate nodes + const newNodes = nodeDataFactory(documentId, graphData.keywords.entries()); + const nodesMap: Map = new Map(); + for (const node of newNodes) { + nodes.push(node); + nodesMap.set(node.id, node); + } - // generate links - const graph = new Map(Object.entries(data.graph)); - linksRef.current = linkDataFactory(graph.entries()); + // generate links + const graph = new Map(Object.entries(graphData.graph)); + const newLinks = linkDataFactory(documentId, graph.entries()); - // save links in nodes for future use - for (const link of linksRef.current) { - nodes[link.source as number].children?.push(nodes[link.target as number]); - nodes[link.target as number].parent = nodes[link.source as number]; + // save links in nodes for future use + for (const link of newLinks) { + linksRef.current.push(link); + nodesMap.get(link.source as string)!.children?.push(nodesMap.get(link.target as string)!); + nodesMap.get(link.target as string)!.parent = nodesMap.get(link.source as string); + } } + // generate meshes (from graphRenderer context) + meshFactory(nodes, linksRef.current); + simulationRef.current .nodes(nodes) .force('charge', d3.forceManyBody().strength(-500).distanceMax(500)) @@ -391,7 +431,7 @@ function useSimulation( 'link', d3 .forceLink(linksRef.current) - .id((d) => d.id) + .id((node, i, nodesData) => node.id) .strength(1), ) .force('collision', d3.forceCollide(GAP)) @@ -399,7 +439,7 @@ function useSimulation( simulationRef.current.alpha(1).stop(); invalidate(); - }, [data.graph, data.keywords, groupRef, linkDataFactory, nodeDataFactory]); + }, [data, groupRef, linkDataFactory, nodeDataFactory]); // simulation tick on frame useFrame((_state, delta) => { diff --git a/components/GraphCanvas/index.tsx b/components/GraphCanvas/index.tsx index f7f89e3..1a518aa 100644 --- a/components/GraphCanvas/index.tsx +++ b/components/GraphCanvas/index.tsx @@ -1,18 +1,15 @@ 'use client'; -import { CSSProperties, useEffect } from 'react'; +import { CSSProperties } from 'react'; import { Canvas } from '@react-three/fiber'; -import { useGraph } from '@/states/graph'; import dynamic from 'next/dynamic'; -import { useViewer } from '@/states/viewer'; const GraphRenderer = dynamic(() => import('./GraphRenderer'), { ssr: false }); interface GraphCanvasProps { style?: CSSProperties; + mindMapData: Map; } -export default function GraphCanvas({ style }: GraphCanvasProps) { - const mindMapData = useViewer((state) => state.mindMapData); - +export default function GraphCanvas({ style, mindMapData }: GraphCanvasProps) { if (mindMapData === null) return <>; return ( From 9566dc84a4631b32590d3da2f8da614ce2293211 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:38:47 +0900 Subject: [PATCH 13/20] feat: genColor limit range for visibility --- utils/whiteboardHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/whiteboardHelper.ts b/utils/whiteboardHelper.ts index 4f78cf5..7ea6749 100644 --- a/utils/whiteboardHelper.ts +++ b/utils/whiteboardHelper.ts @@ -16,9 +16,9 @@ export function genId(): string { * @return {string} Generated random color value in rgb string */ export function genColor(): string { - return `rgb(${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)},${Math.floor( - Math.random() * 255, - )})`; + return `rgb(${Math.floor(Math.random() * 50) + 130},${Math.floor(Math.random() * 50) + 130},${ + Math.floor(Math.random() * 50) + 130 + })`; } /** From 17287a174e6fe23422d912c8b9934310d4601d34 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:39:18 +0900 Subject: [PATCH 14/20] feat: update graphViewer input prop --- app/document/[documentId]/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/document/[documentId]/page.tsx b/app/document/[documentId]/page.tsx index d25a230..01dbbd1 100644 --- a/app/document/[documentId]/page.tsx +++ b/app/document/[documentId]/page.tsx @@ -123,13 +123,13 @@ function DataViewer({ type }: DataViewerProps) { } function GraphViewer() { - const hasGraph = useViewer((state) => state.mindMapData?.keywords.length !== 0); - if (!hasGraph) return

no graph available

; + const mindMapData = useViewer((state) => state.mindMapData); + if (mindMapData === null) return

no graph available

; return ( <>
}> - +
From efb6bc9b2db2d5a903d82a1fc9e24fd45ab5fe40 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:40:44 +0900 Subject: [PATCH 15/20] feat: implement mix mode panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit for client side graph-mix 문서 선택하여 뷰어에 추가 가능 --- components/GraphCanvas/MixModePanel/index.tsx | 130 ++++++++++++++++++ .../MixModePanel/mixModePanel.module.css | 61 ++++++++ 2 files changed, 191 insertions(+) create mode 100644 components/GraphCanvas/MixModePanel/index.tsx create mode 100644 components/GraphCanvas/MixModePanel/mixModePanel.module.css diff --git a/components/GraphCanvas/MixModePanel/index.tsx b/components/GraphCanvas/MixModePanel/index.tsx new file mode 100644 index 0000000..12d076b --- /dev/null +++ b/components/GraphCanvas/MixModePanel/index.tsx @@ -0,0 +1,130 @@ +'use client'; +import { useViewer } from '@/states/viewer'; +import styles from './mixModePanel.module.css'; +import ClickableBackgroundButton from '@/components/BackgroundButton/ClickableBackgroundButton'; +import { useDocument } from '@/states/document'; +import { useOverlay } from '@/states/overlay'; +import { useEffect, useState } from 'react'; +import { getMindMapData } from '@/utils/api'; + +export default function MixModePanel() { + const { mindMapData, setMindMapData, currentDocumentId } = useViewer((state) => ({ + currentDocumentId: state.document?.documentId ?? -1, + mindMapData: state.mindMapData, + setMindMapData: state.setMindMapData, + })); + + const { documentList, fetchDocumentList } = useDocument((state) => ({ + documentList: state.documentList, + fetchDocumentList: state.fetchDocumentList, + })); + + const [selection, setSelection] = useState>(new Map()); + + useEffect(() => { + fetchDocumentList(); + }, [fetchDocumentList]); + + const setOverlay = useOverlay((state) => state.setOverlay); + + if (mindMapData === null) return <>; + + const applyChanges = () => { + // loading dialogue + setOverlay(); + // current mindmap + const fixedMindMap = mindMapData.get(currentDocumentId); + if (fixedMindMap === undefined) return; + // create new mindmap state + const newMindMapData = new Map(); + newMindMapData.set(currentDocumentId, fixedMindMap); + (async () => { + // fetch new Mindmaps + for (const [id, v] of selection.entries()) { + if (v === false) continue; + const newData = await getMindMapData(id); + if (newData === null) continue; + newMindMapData.set(id, newData); + } + // apply new state + setMindMapData(newMindMapData); + // close dialogue + setOverlay(null); + })(); + }; + + return ( +
+
+

현재 {mindMapData.size}개의 문서 동시에 보는중

+ fetchDocumentList()} + /> + applyChanges()} + /> + setOverlay(null)} + /> +
+
+ {documentList.map((v) => { + const selected = + (mindMapData.get(v.documentId) !== undefined && + selection.get(v.documentId) === undefined) || + selection.get(v.documentId) === true; + const fixed = v.documentId === currentDocumentId; + return ( + { + if (!fixed) setSelection((state) => new Map([...state, [v.documentId, !selected]])); + }} + /> + ); + })} +
+
+ ); +} + +type MinCardState = 'selected' | 'fixed' | 'unselected'; + +interface MiniCardProps { + document: WBDocument; + state: MinCardState; + onClick: () => void; +} + +function MiniCard({ document, state, onClick }: MiniCardProps) { + return ( +
+

{document.documentName}

+
+ ); +} + +function LoadingDialogue() { + return ( +
+

요청한 문서들을 불러오는 중입니다. 잠시만 기다려 주세요.

+
+ ); +} diff --git a/components/GraphCanvas/MixModePanel/mixModePanel.module.css b/components/GraphCanvas/MixModePanel/mixModePanel.module.css new file mode 100644 index 0000000..78d2edc --- /dev/null +++ b/components/GraphCanvas/MixModePanel/mixModePanel.module.css @@ -0,0 +1,61 @@ +.container { + max-height: 90%; + overflow-y: auto; + max-width: 1000px; + padding: 20px; + display: flex; + border-radius: 12px; + background-color: var(--foreground); + border: 1px solid var(--grey-normal); + flex-direction: column; + animation: var(--intro-animation); +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + flex: 1; +} + +.bar { + background-color: var(--foreground); + position: sticky; + top: 0; + color: var(--grey-darker); + gap: 20px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0 0 10px 0; +} + +.card { + border: 2px solid var(--background); + box-sizing: border-box; + padding: 10px; + width: 500px; + color: var(--grey-darker); + border-radius: 10px; + background-color: var(--background); +} + +.selected { + box-sizing: border-box; + border: 2px solid var(--primary-dark); +} + +.fixed { + border: 2px solid var(--primary-dark); + background-color: var(--primary-dark); + color: var(--foreground); +} + +.cardText { + overflow: hidden; + text-overflow: ellipse; + margin: 0; + padding: 0; +} From 7253dd4730badd91fd7c83c8e6a596f2a904ab45 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:41:40 +0900 Subject: [PATCH 16/20] feat: NodeInfo now handles nodes from mix mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로컬 노드(현재 문서의 노드)만 자료 관련 기능 사용 가능 mix 로 불러온 외부 노드는 info 열람만 가능 --- components/NodeInfo/index.tsx | 45 ++++++++++++++++++++----- components/NodeInfo/nodeInfo.module.css | 22 ++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/components/NodeInfo/index.tsx b/components/NodeInfo/index.tsx index 644b6f7..7f7e03d 100644 --- a/components/NodeInfo/index.tsx +++ b/components/NodeInfo/index.tsx @@ -5,15 +5,19 @@ import { useViewer } from '@/states/viewer'; import RelationView from './RelationView'; import SourceDataView from './SourceDataView'; import { getKeywordInfo } from '@/utils/api'; +import Link from 'next/link'; interface NodeInfoProps { node?: NodeData; } + export default function NodeInfo({ node }: NodeInfoProps) { const [data, setData] = useState(); const [sourceView, setSourceView] = useState(true); const [relationView, setRelationView] = useState(true); - const documentId = useViewer((state) => state.document?.documentId); + const currentDocumentId = useViewer((state) => state.document?.documentId); + const documentId = node?.documentId; + const isLocalNode = documentId === currentDocumentId; useEffect(() => { if (node === undefined || documentId === undefined) return; @@ -35,6 +39,7 @@ export default function NodeInfo({ node }: NodeInfoProps) { return (

{node?.label}

+ {isLocalNode ? null : }
setRelationView((show) => !show)} /> - setSourceView((show) => !show)} - /> + {isLocalNode ? ( + setSourceView((show) => !show)} + /> + ) : null} +
); } +interface MixedNodeNoticeProps { + documentId: number; +} + +function MixedNodeNotice({ documentId }: MixedNodeNoticeProps) { + return ( +
+ warning +

+ 해당 노드는 MIX 로 불러온 외부 노드입니다. 수정하려면{' '} + + 여기 + + 를 클릭하여 원본 문서로 이동하세요 +

+
+ ); +} + function EmptyNodeInfo() { return (
diff --git a/components/NodeInfo/nodeInfo.module.css b/components/NodeInfo/nodeInfo.module.css index ed2a46e..c663042 100644 --- a/components/NodeInfo/nodeInfo.module.css +++ b/components/NodeInfo/nodeInfo.module.css @@ -26,3 +26,25 @@ flex-direction: row; gap: 10px; } + +.notice { + box-sizing: border-box; + gap: 10px; + padding: 10px 20px; + display: flex; + border-radius: 20px; + align-items: center; + border: 2px solid var(--warning-color); + color: var(--warning-color) !important; + animation: var(--intro-animation); +} + +.noticeText { + padding: 0; + margin: 0; +} + +.noticeLink { + color: var(--warning-color); + font-weight: bold; +} From 912072f0e45d313a728aee17ce7ca523819ded7c Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:42:09 +0900 Subject: [PATCH 17/20] feat: create button in toolbar for mix mode panel --- .../[documentId]/components/BottomToolBar/index.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/document/[documentId]/components/BottomToolBar/index.tsx b/app/document/[documentId]/components/BottomToolBar/index.tsx index a1bd5ef..cd2f8e3 100644 --- a/app/document/[documentId]/components/BottomToolBar/index.tsx +++ b/app/document/[documentId]/components/BottomToolBar/index.tsx @@ -4,11 +4,14 @@ import ClickableBackgroundButton from '@/components/BackgroundButton/ClickableBa import { useViewer } from '@/states/viewer'; import 'material-symbols'; import SmallIconButton from '@/components/Button/SmallIconButton'; +import { useOverlay } from '@/states/overlay'; +import MixModePanel from '@/components/GraphCanvas/MixModePanel'; export default function BottomToolBar() { const elementRef = useRef(null); const [pos, setPos] = useState({ x: 0, y: 0 }); const [drag, setDrag] = useState(undefined); + const setOverlay = useOverlay((state) => state.setOverlay); useEffect(() => { if (!drag) return; @@ -67,6 +70,13 @@ export default function BottomToolBar() { invert={true} onClick={() => setView('DATA')} /> + setOverlay()} + />
); From bd3e4a70863e6817210a8534aba75f6d499cfb02 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:42:27 +0900 Subject: [PATCH 18/20] fix: overlay should be always on top --- components/OverlayWrapper/OverlayWrapper.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/components/OverlayWrapper/OverlayWrapper.module.css b/components/OverlayWrapper/OverlayWrapper.module.css index ed4ee3b..8fb14e4 100644 --- a/components/OverlayWrapper/OverlayWrapper.module.css +++ b/components/OverlayWrapper/OverlayWrapper.module.css @@ -1,4 +1,5 @@ .overlay { + z-index: 500; animation: fadeIn 0.3s; overflow: auto; display: flex; From 2ef187d2b9b5507bc9750a892a625d3e6d531499 Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:12:10 +0900 Subject: [PATCH 19/20] fix: old mindmaps gets removed upon mix --- components/GraphCanvas/MixModePanel/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/GraphCanvas/MixModePanel/index.tsx b/components/GraphCanvas/MixModePanel/index.tsx index 12d076b..e1de8ad 100644 --- a/components/GraphCanvas/MixModePanel/index.tsx +++ b/components/GraphCanvas/MixModePanel/index.tsx @@ -32,16 +32,16 @@ export default function MixModePanel() { const applyChanges = () => { // loading dialogue setOverlay(); - // current mindmap - const fixedMindMap = mindMapData.get(currentDocumentId); - if (fixedMindMap === undefined) return; // create new mindmap state - const newMindMapData = new Map(); - newMindMapData.set(currentDocumentId, fixedMindMap); + const newMindMapData = new Map([...mindMapData]); (async () => { // fetch new Mindmaps for (const [id, v] of selection.entries()) { - if (v === false) continue; + if (v === false) { + // remove mindmap + newMindMapData.delete(id); + continue; + } const newData = await getMindMapData(id); if (newData === null) continue; newMindMapData.set(id, newData); From 6e498c614f405f8dfa99710f6f3b3de5d2d6937e Mon Sep 17 00:00:00 2001 From: junhea <97426534+junhea@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:18:53 +0900 Subject: [PATCH 20/20] fix: fix focusKeyword on dataViewer --- components/DataViewer/AudioViewer/index.tsx | 4 +++- components/DataViewer/PdfViewer/index.tsx | 4 +++- states/viewer.ts | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/components/DataViewer/AudioViewer/index.tsx b/components/DataViewer/AudioViewer/index.tsx index ebea449..1e3a349 100644 --- a/components/DataViewer/AudioViewer/index.tsx +++ b/components/DataViewer/AudioViewer/index.tsx @@ -67,7 +67,9 @@ function useFocusKeyword( useEffect(() => { setCallback((keyword, location) => { setAllKeyword(false); - setKeyWord(keyword, true); + setKeyWord(keyword, { + enabled: true, + }); setFocusIndex(location[0]); setSelectIndex(location[0]); if (audioRef.current === null) return; diff --git a/components/DataViewer/PdfViewer/index.tsx b/components/DataViewer/PdfViewer/index.tsx index 8d3dddb..f5505c7 100644 --- a/components/DataViewer/PdfViewer/index.tsx +++ b/components/DataViewer/PdfViewer/index.tsx @@ -100,7 +100,9 @@ function useFocusKeyword(setPage: Dispatch>) { setCallback((keyword, location) => { setPage(location[0]); setAllKeyword(false); - setKeyWord(keyword, true); + setKeyWord(keyword, { + enabled: true, + }); }); return () => setCallback(undefined); }, [setAllKeyword, setCallback, setKeyWord, setPage]); diff --git a/states/viewer.ts b/states/viewer.ts index c0c671e..c9dc406 100644 --- a/states/viewer.ts +++ b/states/viewer.ts @@ -25,7 +25,7 @@ interface ViewerActions { setFocusNodeCallback: (callback: ((node: NodeData) => void) | undefined) => void; addKeyword: (keyword: string, state?: boolean) => void; deleteKeyword: (keyword: string) => void; - setKeyword: (keyword: string, state: KeywordState) => void; + setKeyword: (keyword: string, state: KeywordStateSlice) => void; setAllKeyword: (state: boolean) => void; setDataStr: (dataStr: string[][]) => void; findDataStr: (from: number[], keyword: string) => KeywordSourceResult | undefined; @@ -46,6 +46,11 @@ interface ViewerStateSlice { document?: WBDocument; } +interface KeywordStateSlice { + enabled?: boolean; + type?: KeywordType; +} + const initialState = { dataSource: null, mindMapData: null, @@ -92,7 +97,7 @@ export const useViewer = create()((set, get) => ({ const prevState = keywords.get(keyword); if (prevState === undefined) return; set({ - keywords: new Map([...keywords]).set(keyword, { ...state }), + keywords: new Map([...keywords]).set(keyword, { ...prevState, ...state }), }); }, setAllKeyword: (state) => {