From 753f3763199a56e03befcf377c4745851a9f1060 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 23 Nov 2019 17:05:17 +0900 Subject: [PATCH 01/20] Implement untrash and purge --- .../NotePage/NoteDetail/NoteDetail.tsx | 58 ++++++++++++++++++- src/components/NotePage/NotePage.tsx | 55 +++++++++++------- src/lib/db/NoteDb.ts | 1 + src/lib/db/store.ts | 17 +++--- 4 files changed, 97 insertions(+), 34 deletions(-) diff --git a/src/components/NotePage/NoteDetail/NoteDetail.tsx b/src/components/NotePage/NoteDetail/NoteDetail.tsx index 576528db81..97536bcdf8 100644 --- a/src/components/NotePage/NoteDetail/NoteDetail.tsx +++ b/src/components/NotePage/NoteDetail/NoteDetail.tsx @@ -9,7 +9,9 @@ import { mdiTrashCan, mdiEyeOutline, mdiArrowSplitVertical, - mdiFormatText + mdiFormatText, + mdiDeleteEmpty, + mdiRestore } from '@mdi/js' import ToolbarIconButton from '../../atoms/ToolbarIconButton' import Toolbar from '../../atoms/Toolbar' @@ -95,7 +97,11 @@ type NoteDetailProps = { props: Partial ) => Promise trashNote: (storageId: string, noteId: string) => Promise - removeNote: (storageId: string, noteId: string) => Promise + untrashNote: ( + storageId: string, + noteId: string + ) => Promise + purgeNote: (storageId: string, noteId: string) => void splitMode: boolean previewMode: boolean toggleSplitMode: () => void @@ -248,6 +254,36 @@ export default class NoteDetail extends React.Component< await this.props.trashNote(storageId, noteId) } + untrashNote = async () => { + const { storageId, note } = this.props + const noteId = note._id + + if (this.queued) { + const { title, content, tags } = this.state + await this.saveNote(storageId, noteId, { + title, + content, + tags + }) + } + await this.props.untrashNote(storageId, noteId) + } + + purgeNote = async () => { + const { storageId, note } = this.props + const noteId = note._id + + if (this.queued) { + const { title, content, tags } = this.state + await this.saveNote(storageId, noteId, { + title, + content, + tags + }) + } + await this.props.purgeNote(storageId, noteId) + } + queued = false timer?: number @@ -358,7 +394,23 @@ export default class NoteDetail extends React.Component< onClick={togglePreviewMode} path={mdiEyeOutline} /> - + {note.trashed ? ( + <> + + + + ) : ( + + )} )} diff --git a/src/components/NotePage/NotePage.tsx b/src/components/NotePage/NotePage.tsx index 6a200a9fff..5452135b1d 100644 --- a/src/components/NotePage/NotePage.tsx +++ b/src/components/NotePage/NotePage.tsx @@ -14,6 +14,7 @@ import { useDb } from '../../lib/db' import TwoPaneLayout from '../atoms/TwoPaneLayout' import { NoteDoc } from '../../lib/db/types' import { useGeneralStatus } from '../../lib/generalStatus' +import { useDialog, DialogIconTypes } from '../../lib/dialog' function sortByUpdatedAt(a: NoteDoc, b: NoteDoc) { return b.updatedAt.localeCompare(a.updatedAt) @@ -68,17 +69,18 @@ export default () => { const router = useRouter() - const currentNote = useMemo(() => { - if (currentStorage == null) return null - if (noteId == null) { - if (notes.length > 0) { - return currentStorage.noteMap[notes[0]._id] - } else { - return null + const currentNoteIndex = useMemo(() => { + for (let i = 0; i < notes.length; i++) { + if (notes[i]._id === noteId) { + return i } } - return currentStorage.noteMap[noteId] - }, [noteId, currentStorage, notes]) + return 0 + }, [notes, noteId]) + + const currentNote = useMemo(() => { + return notes[currentNoteIndex] + }, [notes, currentNoteIndex]) const createNote = useCallback(async () => { if (storageId == null || routeParams.name === 'storages.trashCan') { @@ -95,15 +97,6 @@ export default () => { }) }, [db, routeParams, storageId]) - const currentNoteIndex = useMemo(() => { - for (let i = 0; i < notes.length; i++) { - if (notes[i]._id === noteId) { - return i - } - } - return 0 - }, [notes, noteId]) - const naviagateUp = useCallback(() => { if (currentNoteIndex > 0) { router.push( @@ -120,8 +113,6 @@ export default () => { } }, [notes, currentNoteIndex, router, currentPathnameWithoutNoteId]) - const removeNote = async () => {} - const { generalStatus, setGeneralStatus } = useGeneralStatus() const updateNoteListWidth = useCallback( (leftWidth: number) => { @@ -144,6 +135,27 @@ export default () => { })) }, [setGeneralStatus]) + const { messageBox } = useDialog() + const purgeNoteFromDb = db.purgeNote + const purgeNote = useCallback( + (storageId: string, noteId: string) => { + messageBox({ + title: 'Delete a Note', + message: 'The note will be deleted permanently', + iconType: DialogIconTypes.Warning, + buttons: ['Delete Note', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + purgeNoteFromDb(storageId, noteId) + } + } + }) + }, + [messageBox, purgeNoteFromDb] + ) + return storageId != null ? ( { note={currentNote} updateNote={db.updateNote} trashNote={db.trashNote} - removeNote={removeNote} + untrashNote={db.untrashNote} + purgeNote={purgeNote} splitMode={generalStatus.noteSplitMode} previewMode={generalStatus.notePreviewMode} toggleSplitMode={toggleSplitMode} diff --git a/src/lib/db/NoteDb.ts b/src/lib/db/NoteDb.ts index a215a2e9ca..05161d6636 100644 --- a/src/lib/db/NoteDb.ts +++ b/src/lib/db/NoteDb.ts @@ -344,6 +344,7 @@ export default class NoteDb { } const { rev } = await this.pouchDb.put(noteDocProps) + console.log(noteDocProps) return { ...noteDocProps, _rev: rev diff --git a/src/lib/db/store.ts b/src/lib/db/store.ts index 86578e7a75..bb70a60db0 100644 --- a/src/lib/db/store.ts +++ b/src/lib/db/store.ts @@ -462,13 +462,7 @@ export function createDbStoreCreator( } let folder: PopulatedFolderDoc | undefined - if (storage.folderMap[noteDoc.folderPathname] == null) { - folder = { - ...(await storage.db.getFolder(noteDoc.folderPathname)!), - pathname: noteDoc.folderPathname, - noteIdSet: new Set() - } as PopulatedFolderDoc - } else { + if (storage.folderMap[noteDoc.folderPathname] != null) { const newFolderNoteIdSet = new Set( storage.folderMap[noteDoc.folderPathname]!.noteIdSet ) @@ -481,6 +475,9 @@ export function createDbStoreCreator( const modifiedTags: ObjectMap = noteDoc.tags.reduce( (acc, tag) => { + if (storage.tagMap[tag] == null) { + return acc + } const newNoteIdSet = new Set(storage.tagMap[tag]!.noteIdSet) newNoteIdSet.delete(noteDoc._id) acc[tag] = { @@ -515,9 +512,6 @@ export function createDbStoreCreator( return } const noteDoc = await storage.db.untrashNote(noteId) - if (noteDoc == null) { - return - } const folder: PopulatedFolderDoc = storage.folderMap[noteDoc.folderPathname] == null @@ -780,6 +774,9 @@ async function prepareStorage( } for (const noteDoc of Object.values(noteMap) as NoteDoc[]) { + if (noteDoc.trashed) { + continue + } storage.folderMap[noteDoc.folderPathname]!.noteIdSet.add(noteDoc._id) noteDoc.tags.forEach(tagName => { storage.tagMap[tagName]!.noteIdSet.add(noteDoc._id) From ddaaab3e839c2eee4e167d608f169aa0622e0639 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 02:22:15 +0900 Subject: [PATCH 02/20] Discard types of styled-components --- package-lock.json | 39 +++++-------------- package.json | 1 - src/components/GlobalStyle.tsx | 10 ++--- .../NotePage/NoteDetail/NoteDetail.tsx | 2 +- src/components/NotePage/NoteList/NoteItem.tsx | 28 ++++++++++--- src/components/NotePage/NoteList/NoteList.tsx | 2 +- src/components/PreferencesModal/TabButton.tsx | 8 ++-- src/components/atoms/ToolbarButtonGroup.tsx | 4 +- src/components/atoms/ToolbarIconInput.tsx | 2 +- src/components/atoms/TwoPaneLayout.tsx | 4 +- typings/styled-components.d.ts | 6 +++ 11 files changed, 53 insertions(+), 53 deletions(-) create mode 100644 typings/styled-components.d.ts diff --git a/package-lock.json b/package-lock.json index 54df709565..800c58f50a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,9 +111,9 @@ } }, "@aws-crypto/crc32": { - "version": "0.1.0-preview.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-0.1.0-preview.2.tgz", - "integrity": "sha512-XtGVJZhBUAWYG1IdpbXviDNjBJqogPVGi7k4xLZoj4FZQ3GKNhSSiaJrBKRGMh0ixd/o/yxKR1Cf0I0y1gPN2A==", + "version": "0.1.0-preview.3", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-0.1.0-preview.3.tgz", + "integrity": "sha512-zFTrBn6zsfzSLRYld7AcyvQBd1+Fl+8io7AHFgCMlcYxVaeMXsVrP+QKzJZADsi/XYB0XavI9Dh9GucSjsiW4Q==", "requires": { "tslib": "^1.9.3" } @@ -2078,16 +2078,6 @@ "@types/react": "*" } }, - "@types/react-native": { - "version": "0.60.15", - "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.60.15.tgz", - "integrity": "sha512-HcPSAlpFRiFqJ647YfUgzpr0aeMrH2SXkGPsLjkiG53oDj1upRvArVt/yBWRY+aykOs7C44EtZ5puzesO/U7HA==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/react": "*" - } - }, "@types/react-test-renderer": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.0.tgz", @@ -2139,17 +2129,6 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, - "@types/styled-components": { - "version": "4.1.19", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.19.tgz", - "integrity": "sha512-nDkoTQ8ItcJiyEvTa24TwsIpIfNKCG+Lq0LvAwApOcjQ8OaeOOCg66YSPHBePHUh6RPt1LA8aEzRlgWhQPFqPg==", - "dev": true, - "requires": { - "@types/react": "*", - "@types/react-native": "*", - "csstype": "^2.2.0" - } - }, "@types/tapable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", @@ -6884,9 +6863,9 @@ "dev": true }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -15272,9 +15251,9 @@ "dev": true }, "zen-observable": { - "version": "0.8.14", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.14.tgz", - "integrity": "sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g==" + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" }, "zwitch": { "version": "1.0.4", diff --git a/package.json b/package.json index 6cf7d269c4..0a0836b249 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@types/react-dom": "^16.9.0", "@types/react-hot-loader": "^4.1.0", "@types/shortid": "0.0.29", - "@types/styled-components": "^4.1.19", "@types/webpack": "^4.39.1", "@types/webpack-dev-server": "^3.1.7", "@types/webpack-env": "^1.14.0", diff --git a/src/components/GlobalStyle.tsx b/src/components/GlobalStyle.tsx index e3715c2d45..0ad3f56fb9 100644 --- a/src/components/GlobalStyle.tsx +++ b/src/components/GlobalStyle.tsx @@ -6,8 +6,8 @@ export default createGlobalStyle` margin: 0; ${backgroundColor} ${textColor} - font-family: ${({ theme }) => theme.fontFamily}; - font-size: ${({ theme }) => theme.fontSize}px; + font-family: ${({ theme }: any) => theme.fontFamily}; + font-size: ${({ theme }: any) => theme.fontSize}px; } * { @@ -18,7 +18,7 @@ export default createGlobalStyle` } input { - font-size: ${({ theme }) => theme.fontSize}px; + font-size: ${({ theme }: any) => theme.fontSize}px; } button, @@ -38,12 +38,12 @@ export default createGlobalStyle` /* background of the scrollbar except button or resizer */ ::-webkit-scrollbar-track { - background-color: ${({ theme }) => theme.scrollBarTrackColor}; + background-color: ${({ theme }: any) => theme.scrollBarTrackColor}; } /* scrollbar itself */ ::-webkit-scrollbar-thumb { - background-color: ${({ theme }) => theme.scrollBarThumbColor}; + background-color: ${({ theme }: any) => theme.scrollBarThumbColor}; } /* set button(top and bottom of the scrollbar) */ diff --git a/src/components/NotePage/NoteDetail/NoteDetail.tsx b/src/components/NotePage/NoteDetail/NoteDetail.tsx index 97536bcdf8..5595dc60b9 100644 --- a/src/components/NotePage/NoteDetail/NoteDetail.tsx +++ b/src/components/NotePage/NoteDetail/NoteDetail.tsx @@ -285,7 +285,7 @@ export default class NoteDetail extends React.Component< } queued = false - timer?: number + timer?: any queueToSave = () => { this.queued = true diff --git a/src/components/NotePage/NoteList/NoteItem.tsx b/src/components/NotePage/NoteList/NoteItem.tsx index 36ecd956bb..c0ac273fdf 100644 --- a/src/components/NotePage/NoteList/NoteItem.tsx +++ b/src/components/NotePage/NoteList/NoteItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useCallback } from 'react' import { Link } from '../../../lib/router' import styled from '../../../lib/styled/styled' import { NoteDoc } from '../../../lib/db/types' @@ -22,9 +22,8 @@ const StyledNoteListItem = styled.div` ${secondaryBackgroundColor} } &.active { - border-left: 2px solid ${({ theme }) => theme.primaryColor}; + border-left: 2px solid ${({ theme }: any) => theme.primaryColor}; } - user-select: none; ${borderBottom} transition: 200ms background-color; @@ -55,7 +54,7 @@ type NoteItemProps = { focusList: () => void } -export default ({ note, active, basePathname, focusList }: NoteItemProps) => { +export default ({ storageId, note, active, basePathname }: NoteItemProps) => { const href = `${basePathname}/${note._id}` const contentPreview = useMemo(() => { @@ -67,9 +66,26 @@ export default ({ note, active, basePathname, focusList }: NoteItemProps) => { ) }, [note.content]) + const handleDragStart = useCallback( + (event: React.DragEvent) => { + event.dataTransfer.setData( + 'application/x-note-json', + JSON.stringify({ + note, + storageId + }) + ) + }, + [note, storageId] + ) + return ( - - + +
{note.title}
{contentPreview}
diff --git a/src/components/NotePage/NoteList/NoteList.tsx b/src/components/NotePage/NoteList/NoteList.tsx index 449a03aaf0..3900401973 100644 --- a/src/components/NotePage/NoteList/NoteList.tsx +++ b/src/components/NotePage/NoteList/NoteList.tsx @@ -24,7 +24,7 @@ const StyledContainer = styled.div` overflow-y: auto; li.empty { - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }: any) => theme.uiTextColor}; } } diff --git a/src/components/PreferencesModal/TabButton.tsx b/src/components/PreferencesModal/TabButton.tsx index 8b3165fd5f..b194db38b5 100644 --- a/src/components/PreferencesModal/TabButton.tsx +++ b/src/components/PreferencesModal/TabButton.tsx @@ -23,19 +23,19 @@ const StyledButton = styled.button` .label { margin-left: 18px; flex: 1; - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }: any) => theme.uiTextColor}; text-align: left; font-size: 14px; } &.active { - color: ${({ theme }) => theme.textColor}; + color: ${({ theme }: any) => theme.textColor}; .border { - background-color: ${({ theme }) => theme.primaryColor}; + background-color: ${({ theme }: any) => theme.primaryColor}; } .label { - color: ${({ theme }) => theme.textColor}; + color: ${({ theme }: any) => theme.textColor}; } } ` diff --git a/src/components/atoms/ToolbarButtonGroup.tsx b/src/components/atoms/ToolbarButtonGroup.tsx index 0b02747e4a..72224d8d4e 100644 --- a/src/components/atoms/ToolbarButtonGroup.tsx +++ b/src/components/atoms/ToolbarButtonGroup.tsx @@ -2,11 +2,11 @@ import styled from '../../lib/styled' export default styled.div` border-radius: 2px; - border: solid 1px ${({ theme }) => theme.colors.border}; + border: solid 1px ${({ theme }: any) => theme.colors.border}; & > button { margin: 0; border: 0; - border-right: 1px solid ${({ theme }) => theme.colors.border}; + border-right: 1px solid ${({ theme }: any) => theme.colors.border}; border-radius: 0; &:first-child { border-top-left-radius: 2px; diff --git a/src/components/atoms/ToolbarIconInput.tsx b/src/components/atoms/ToolbarIconInput.tsx index 67a5c84fa9..6d9ead5615 100644 --- a/src/components/atoms/ToolbarIconInput.tsx +++ b/src/components/atoms/ToolbarIconInput.tsx @@ -16,7 +16,7 @@ const StyledContainer = styled.div` position: relative; width: 100%; background-color: transparent; - border: solid 1px ${({ theme }) => theme.colors.border}; + border: solid 1px ${({ theme }: any) => theme.colors.border}; height: 22px; padding-left: 18px; border-radius: 2px; diff --git a/src/components/atoms/TwoPaneLayout.tsx b/src/components/atoms/TwoPaneLayout.tsx index 1b4250cf1f..ade3147fee 100644 --- a/src/components/atoms/TwoPaneLayout.tsx +++ b/src/components/atoms/TwoPaneLayout.tsx @@ -37,7 +37,7 @@ const Pane = styled.div` const DividerBorder = styled.div` width: 1px; height: 100%; - background-color: ${({ theme }) => theme.borderColor}; + background-color: ${({ theme }: any) => theme.borderColor}; ` const DividerGraple = styled.div` @@ -52,7 +52,7 @@ const DividerGraple = styled.div` user-select: none; cursor: col-resize; &.active { - border-color: ${({ theme }) => theme.primaryColor}; + border-color: ${({ theme }: any) => theme.primaryColor}; } ` diff --git a/typings/styled-components.d.ts b/typings/styled-components.d.ts new file mode 100644 index 0000000000..a8ab092d38 --- /dev/null +++ b/typings/styled-components.d.ts @@ -0,0 +1,6 @@ +declare module 'styled-components' { + export type StyledComponent = any + export function createGlobalStyle(...args: any[]): any + export type ThemedBaseStyledInterface = any + export const ThemeProvider: any +} From 782620921317e3195fe09e2f2dce8bf0a58ab0c9 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 02:23:55 +0900 Subject: [PATCH 03/20] Fix contextMenu style --- src/components/ContextMenu/styled.ts | 3 +++ src/lib/styled/styleFunctions.ts | 3 +++ webpack.config.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ContextMenu/styled.ts b/src/components/ContextMenu/styled.ts index 79f4ff4798..2a71315522 100644 --- a/src/components/ContextMenu/styled.ts +++ b/src/components/ContextMenu/styled.ts @@ -44,4 +44,7 @@ export const StyledContextMenuItem = styled.button` &.active { ${activeBackgroundColor} } + &:disabled { + background-color: transparent; + } ` diff --git a/src/lib/styled/styleFunctions.ts b/src/lib/styled/styleFunctions.ts index 0d217bee05..c30e04039e 100644 --- a/src/lib/styled/styleFunctions.ts +++ b/src/lib/styled/styleFunctions.ts @@ -35,6 +35,9 @@ transition: 200ms color; &:active, &.active { color: ${theme.activeUiTextColor}; +} +&:disabled { + color: ${theme.uiTextColor}; }` export const borderColor = ({ theme }: StyledProps) => diff --git a/webpack.config.ts b/webpack.config.ts index 5685ad44b1..335075d7c6 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -94,7 +94,7 @@ module.exports = { hot: true, // enable HMR on the server - before: function (app, server) { + before: function(app, server) { app.use( '/codemirror/mode', express.static(path.join(__dirname, 'node_modules/codemirror/mode')) From cd6b044bc7225c720e6d077381288c208fa38f29 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 02:24:32 +0900 Subject: [PATCH 04/20] Add sideNavOpenedItemList prop to GeneralStatus --- src/lib/generalStatus/store.ts | 3 ++- src/lib/generalStatus/types.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/generalStatus/store.ts b/src/lib/generalStatus/store.ts index 5e9b5fbbc7..4590be33c8 100644 --- a/src/lib/generalStatus/store.ts +++ b/src/lib/generalStatus/store.ts @@ -27,7 +27,8 @@ const baseGeneralStatus: GeneralStatus = { sideBarWidth: 160, noteListWidth: 250, noteSplitMode: true, - notePreviewMode: false + notePreviewMode: false, + sideNavOpenedItemList: [] } function useGeneralStatusStore() { diff --git a/src/lib/generalStatus/types.ts b/src/lib/generalStatus/types.ts index 3927b02b70..d87fde0a63 100644 --- a/src/lib/generalStatus/types.ts +++ b/src/lib/generalStatus/types.ts @@ -3,4 +3,5 @@ export interface GeneralStatus { noteListWidth: number noteSplitMode: boolean notePreviewMode: boolean + sideNavOpenedItemList: string[] } From a41208017cebce2f9900f15ab1270a2538350ebd Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 03:25:50 +0900 Subject: [PATCH 05/20] Use set for openedItem --- src/lib/generalStatus/store.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/lib/generalStatus/store.ts b/src/lib/generalStatus/store.ts index 4590be33c8..2c2bb56779 100644 --- a/src/lib/generalStatus/store.ts +++ b/src/lib/generalStatus/store.ts @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from 'react' +import { useMemo, useCallback, useEffect } from 'react' import { localLiteStorage } from 'ltstrg' import { useSetState } from 'react-use' import { generalStatusKey } from '../localStorageKeys' @@ -43,13 +43,35 @@ function useGeneralStatusStore() { } }, [generalStatus]) + const { sideNavOpenedItemList } = mergedGeneralStatus + const sideNavOpenedItemSet = useMemo(() => { + return new Set(sideNavOpenedItemList) + }, [sideNavOpenedItemList]) + + const toggleSideNavOpenedItem = useCallback( + (itemId: string) => { + const newSet = new Set(sideNavOpenedItemSet) + if (newSet.has(itemId)) { + newSet.delete(itemId) + } else { + newSet.add(itemId) + } + setGeneralStatus({ + sideNavOpenedItemList: [...newSet] + }) + }, + [setGeneralStatus, sideNavOpenedItemSet] + ) + useEffect(() => { saveGeneralStatus(generalStatus) }, [generalStatus]) return { generalStatus: mergedGeneralStatus, - setGeneralStatus + setGeneralStatus, + sideNavOpenedItemSet, + toggleSideNavOpenedItem } } From 92d106eb09b1967b6a8b75b54e6d3ebc426eccf4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 12:33:15 +0900 Subject: [PATCH 06/20] Improve sc types --- src/components/GlobalStyle.tsx | 13 ++++++----- src/components/NotePage/NoteList/NoteItem.tsx | 2 +- src/components/NotePage/NoteList/NoteList.tsx | 2 +- src/components/PreferencesModal/TabButton.tsx | 8 +++---- src/components/atoms/ToolbarButtonGroup.tsx | 4 ++-- src/components/atoms/ToolbarIconInput.tsx | 2 +- src/components/atoms/TwoPaneLayout.tsx | 4 ++-- src/lib/styled/styled.ts | 4 ++-- typings/styled-components.d.ts | 22 +++++++++++++++++-- 9 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/components/GlobalStyle.tsx b/src/components/GlobalStyle.tsx index 0ad3f56fb9..9114d9bdd3 100644 --- a/src/components/GlobalStyle.tsx +++ b/src/components/GlobalStyle.tsx @@ -1,13 +1,14 @@ import { createGlobalStyle } from 'styled-components' import { backgroundColor, textColor } from '../lib/styled/styleFunctions' +import { BaseTheme } from '../lib/styled/themes/types' -export default createGlobalStyle` +export default createGlobalStyle` body { margin: 0; ${backgroundColor} ${textColor} - font-family: ${({ theme }: any) => theme.fontFamily}; - font-size: ${({ theme }: any) => theme.fontSize}px; + font-family: ${({ theme }) => theme.fontFamily}; + font-size: ${({ theme }) => theme.fontSize}px; } * { @@ -18,7 +19,7 @@ export default createGlobalStyle` } input { - font-size: ${({ theme }: any) => theme.fontSize}px; + font-size: ${({ theme }) => theme.fontSize}px; } button, @@ -38,12 +39,12 @@ export default createGlobalStyle` /* background of the scrollbar except button or resizer */ ::-webkit-scrollbar-track { - background-color: ${({ theme }: any) => theme.scrollBarTrackColor}; + background-color: ${({ theme }) => theme.scrollBarTrackColor}; } /* scrollbar itself */ ::-webkit-scrollbar-thumb { - background-color: ${({ theme }: any) => theme.scrollBarThumbColor}; + background-color: ${({ theme }) => theme.scrollBarThumbColor}; } /* set button(top and bottom of the scrollbar) */ diff --git a/src/components/NotePage/NoteList/NoteItem.tsx b/src/components/NotePage/NoteList/NoteItem.tsx index c0ac273fdf..9885651ad7 100644 --- a/src/components/NotePage/NoteList/NoteItem.tsx +++ b/src/components/NotePage/NoteList/NoteItem.tsx @@ -22,7 +22,7 @@ const StyledNoteListItem = styled.div` ${secondaryBackgroundColor} } &.active { - border-left: 2px solid ${({ theme }: any) => theme.primaryColor}; + border-left: 2px solid ${({ theme }) => theme.primaryColor}; } ${borderBottom} diff --git a/src/components/NotePage/NoteList/NoteList.tsx b/src/components/NotePage/NoteList/NoteList.tsx index 3900401973..449a03aaf0 100644 --- a/src/components/NotePage/NoteList/NoteList.tsx +++ b/src/components/NotePage/NoteList/NoteList.tsx @@ -24,7 +24,7 @@ const StyledContainer = styled.div` overflow-y: auto; li.empty { - color: ${({ theme }: any) => theme.uiTextColor}; + color: ${({ theme }) => theme.uiTextColor}; } } diff --git a/src/components/PreferencesModal/TabButton.tsx b/src/components/PreferencesModal/TabButton.tsx index b194db38b5..8b3165fd5f 100644 --- a/src/components/PreferencesModal/TabButton.tsx +++ b/src/components/PreferencesModal/TabButton.tsx @@ -23,19 +23,19 @@ const StyledButton = styled.button` .label { margin-left: 18px; flex: 1; - color: ${({ theme }: any) => theme.uiTextColor}; + color: ${({ theme }) => theme.uiTextColor}; text-align: left; font-size: 14px; } &.active { - color: ${({ theme }: any) => theme.textColor}; + color: ${({ theme }) => theme.textColor}; .border { - background-color: ${({ theme }: any) => theme.primaryColor}; + background-color: ${({ theme }) => theme.primaryColor}; } .label { - color: ${({ theme }: any) => theme.textColor}; + color: ${({ theme }) => theme.textColor}; } } ` diff --git a/src/components/atoms/ToolbarButtonGroup.tsx b/src/components/atoms/ToolbarButtonGroup.tsx index 72224d8d4e..0b02747e4a 100644 --- a/src/components/atoms/ToolbarButtonGroup.tsx +++ b/src/components/atoms/ToolbarButtonGroup.tsx @@ -2,11 +2,11 @@ import styled from '../../lib/styled' export default styled.div` border-radius: 2px; - border: solid 1px ${({ theme }: any) => theme.colors.border}; + border: solid 1px ${({ theme }) => theme.colors.border}; & > button { margin: 0; border: 0; - border-right: 1px solid ${({ theme }: any) => theme.colors.border}; + border-right: 1px solid ${({ theme }) => theme.colors.border}; border-radius: 0; &:first-child { border-top-left-radius: 2px; diff --git a/src/components/atoms/ToolbarIconInput.tsx b/src/components/atoms/ToolbarIconInput.tsx index 6d9ead5615..67a5c84fa9 100644 --- a/src/components/atoms/ToolbarIconInput.tsx +++ b/src/components/atoms/ToolbarIconInput.tsx @@ -16,7 +16,7 @@ const StyledContainer = styled.div` position: relative; width: 100%; background-color: transparent; - border: solid 1px ${({ theme }: any) => theme.colors.border}; + border: solid 1px ${({ theme }) => theme.colors.border}; height: 22px; padding-left: 18px; border-radius: 2px; diff --git a/src/components/atoms/TwoPaneLayout.tsx b/src/components/atoms/TwoPaneLayout.tsx index ade3147fee..1b4250cf1f 100644 --- a/src/components/atoms/TwoPaneLayout.tsx +++ b/src/components/atoms/TwoPaneLayout.tsx @@ -37,7 +37,7 @@ const Pane = styled.div` const DividerBorder = styled.div` width: 1px; height: 100%; - background-color: ${({ theme }: any) => theme.borderColor}; + background-color: ${({ theme }) => theme.borderColor}; ` const DividerGraple = styled.div` @@ -52,7 +52,7 @@ const DividerGraple = styled.div` user-select: none; cursor: col-resize; &.active { - border-color: ${({ theme }: any) => theme.primaryColor}; + border-color: ${({ theme }) => theme.primaryColor}; } ` diff --git a/src/lib/styled/styled.ts b/src/lib/styled/styled.ts index 46f8da4a2b..573756305b 100644 --- a/src/lib/styled/styled.ts +++ b/src/lib/styled/styled.ts @@ -1,4 +1,4 @@ import styled, { ThemedBaseStyledInterface } from 'styled-components' -import { defaultTheme } from './themes/default' +import { BaseTheme } from './themes/types' -export default styled as ThemedBaseStyledInterface +export default styled as ThemedBaseStyledInterface diff --git a/typings/styled-components.d.ts b/typings/styled-components.d.ts index a8ab092d38..120813a766 100644 --- a/typings/styled-components.d.ts +++ b/typings/styled-components.d.ts @@ -1,6 +1,24 @@ declare module 'styled-components' { export type StyledComponent = any - export function createGlobalStyle(...args: any[]): any - export type ThemedBaseStyledInterface = any + export function createGlobalStyle( + strings: TemplateStringsArray, + ...keys: Array< + | number + | undefined + | string + | ((props: { theme: T }) => string | number | undefined) + > + ): any + export type ThemedBaseStyledInterface = { + [key: string]:

( + strings: TemplateStringsArray, + ...keys: Array< + | number + | undefined + | string + | ((props: { theme: T } & P) => string | number | undefined) + > + ) => any + } export const ThemeProvider: any } From 04536b76159769d3fc741b9e9f79c7e65f8a3b79 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 5 Dec 2019 12:35:31 +0900 Subject: [PATCH 07/20] Improve disabled text color --- src/lib/styled/styleFunctions.ts | 2 +- src/lib/styled/themes/default.ts | 5 +++-- src/lib/styled/themes/types.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/styled/styleFunctions.ts b/src/lib/styled/styleFunctions.ts index c30e04039e..38dac1cf3f 100644 --- a/src/lib/styled/styleFunctions.ts +++ b/src/lib/styled/styleFunctions.ts @@ -37,7 +37,7 @@ transition: 200ms color; color: ${theme.activeUiTextColor}; } &:disabled { - color: ${theme.uiTextColor}; + color: ${theme.disabledUiTextColor}; }` export const borderColor = ({ theme }: StyledProps) => diff --git a/src/lib/styled/themes/default.ts b/src/lib/styled/themes/default.ts index df8943ddd3..6759f74f73 100644 --- a/src/lib/styled/themes/default.ts +++ b/src/lib/styled/themes/default.ts @@ -36,8 +36,9 @@ export const defaultTheme: BaseTheme = { // General textColor: light100Color, - uiTextColor: light30Color, - activeUiTextColor: light70Color, + uiTextColor: light70Color, + activeUiTextColor: light100Color, + disabledUiTextColor: light30Color, primaryColor: primaryColor, borderColor: light12Color, diff --git a/src/lib/styled/themes/types.ts b/src/lib/styled/themes/types.ts index c28338ff2d..e430f262c1 100644 --- a/src/lib/styled/themes/types.ts +++ b/src/lib/styled/themes/types.ts @@ -7,6 +7,7 @@ export interface BaseTheme { textColor: string uiTextColor: string activeUiTextColor: string + disabledUiTextColor: string primaryColor: string borderColor: string From 403749b9d3caa7de5db442e726607bc882d0ffe9 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Fri, 6 Dec 2019 15:53:46 +0900 Subject: [PATCH 08/20] Renew SideNav component structure --- .../SideNavigator/ControlButton.tsx | 34 ++ .../SideNavigator/FolderListFragment.tsx | 212 +++++++ .../SideNavigator/SideNavigator.tsx | 152 ++--- .../SideNavigator/SideNavigatorItem.tsx | 199 ++++--- .../SideNavigator/StorageNavigatorItem.tsx | 560 +++++++++--------- .../SideNavigator/TagListFragment.tsx | 33 ++ src/lib/db/store.ts | 9 +- src/lib/generalStatus/store.ts | 35 +- src/lib/nav.ts | 3 + src/lib/router/Link.tsx | 16 +- 10 files changed, 826 insertions(+), 427 deletions(-) create mode 100644 src/components/SideNavigator/ControlButton.tsx create mode 100644 src/components/SideNavigator/FolderListFragment.tsx create mode 100644 src/components/SideNavigator/TagListFragment.tsx create mode 100644 src/lib/nav.ts diff --git a/src/components/SideNavigator/ControlButton.tsx b/src/components/SideNavigator/ControlButton.tsx new file mode 100644 index 0000000000..18b7995d16 --- /dev/null +++ b/src/components/SideNavigator/ControlButton.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import styled from '../../lib/styled' +import Icon from '../atoms/Icon' +import { uiTextColor } from '../../lib/styled/styleFunctions' + +const StyledButton = styled.button` + width: 30px; + height: 30px; + padding: 0; + border: none; + background-color: transparent; + border-radius: 2px; + top: 2px; + cursor: pointer; + ${uiTextColor} + &:focus { + box-shadow: none; + } +` + +interface ControlButtonProps { + iconPath: string + onClick?: (event: React.MouseEvent) => void +} + +const ControlButton = ({ iconPath, onClick }: ControlButtonProps) => { + return ( + + + + ) +} + +export default ControlButton diff --git a/src/components/SideNavigator/FolderListFragment.tsx b/src/components/SideNavigator/FolderListFragment.tsx new file mode 100644 index 0000000000..d98dc50036 --- /dev/null +++ b/src/components/SideNavigator/FolderListFragment.tsx @@ -0,0 +1,212 @@ +import React, { useMemo } from 'react' +import { useDb } from '../../lib/db' +import { + mdiFolderOutline, + mdiFolderOpenOutline, + mdiPlusCircleOutline +} from '@mdi/js' +import { useDialog, DialogIconTypes } from '../../lib/dialog' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import SideNavigatorItem from './SideNavigatorItem' +import { NoteStorage } from '../../lib/db/types' +import { usePathnameWithoutNoteId, useRouter } from '../../lib/router' +import { useGeneralStatus } from '../../lib/generalStatus' +import ControlButton from './ControlButton' +import { getFolderItemId } from '../../lib/nav' + +interface FolderListFragmentProps { + storage: NoteStorage + showPromptToCreateFolder: (folderPathname: string) => void +} + +const FolderListFragment = ({ + storage, + showPromptToCreateFolder +}: FolderListFragmentProps) => { + const { + removeFolder + // updateNote + } = useDb() + const { push } = useRouter() + const { messageBox } = useDialog() + const { popup } = useContextMenu() + const { folderMap, id: storageId } = storage + + const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() + + const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() + + const folderPathnameListExceptRoot = useMemo(() => { + const folderPathnameList = Object.keys(folderMap).sort((a, b) => + a.localeCompare(b) + ) + return folderPathnameList.slice(1) + }, [folderMap]) + + const createOnFolderItemClickHandler = (folderPathname: string) => { + return () => { + push( + `/app/storages/${storage.id}/notes${ + folderPathname === '/' ? '' : folderPathname + }` + ) + } + } + + const createOnContextMenuHandler = ( + storageId: string, + folderPathname: string + ) => { + return (event: React.MouseEvent) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Folder', + onClick: async () => { + showPromptToCreateFolder(folderPathname) + } + }, + { + type: MenuTypes.Normal, + label: 'Remove Folder', + enabled: folderPathname !== '/', + onClick: () => { + messageBox({ + title: `Remove "${folderPathname}" folder`, + message: 'All notes and subfolders will be deleted.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Folder', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeFolder(storageId, folderPathname) + } + } + }) + } + } + ]) + } + } + + const folderSetWithSubFolders = useMemo(() => { + return folderPathnameListExceptRoot.reduce((folderSet, folderPathname) => { + if (folderPathname !== '/') { + const nameElements = folderPathname.slice(1).split('/') + const parentFolderPathname = + '/' + nameElements.slice(0, nameElements.length - 1).join('/') + folderSet.add(parentFolderPathname) + } + return folderSet + }, new Set()) + }, [folderPathnameListExceptRoot]) + + const openedFolderPathnameList = useMemo(() => { + const tree = getFolderNameElementTree(folderPathnameListExceptRoot) + return getOpenedFolderPathnameList( + tree, + storageId, + sideNavOpenedItemSet, + '/' + ) + }, [folderPathnameListExceptRoot, storageId, sideNavOpenedItemSet]) + + const rootFolderIsActive = + currentPathnameWithoutNoteId === `/app/storages/${storageId}/notes` + + return ( + <> + + {openedFolderPathnameList.map((folderPathname: string) => { + const nameElements = folderPathname.split('/').slice(1) + const folderName = nameElements[nameElements.length - 1] + const itemId = getFolderItemId(storageId, folderPathname) + const depth = nameElements.length + const folded = folderSetWithSubFolders.has(folderPathname) + ? !sideNavOpenedItemSet.has(itemId) + : undefined + + const folderIsActive = + currentPathnameWithoutNoteId === + `/app/storages/${storageId}/notes${folderPathname}` + + return ( + toggleSideNavOpenedItem(itemId)} + controlComponents={[ + showPromptToCreateFolder(folderPathname)} + iconPath={mdiPlusCircleOutline} + /> + ]} + /> + ) + })} + + ) +} + +function getFolderNameElementTree(folderPathnameList: string[]) { + return folderPathnameList.reduce((tree, folderPathname) => { + const nameElements = folderPathname.slice(1).split('/') + + let targetTree = tree + for (const nameElement of nameElements) { + if (targetTree[nameElement] == null) { + targetTree[nameElement] = {} + } + targetTree = targetTree[nameElement] + } + + return tree + }, {}) +} + +function getOpenedFolderPathnameList( + tree: {}, + storageId: string, + openItemIdSet: Set, + parentPathname: string +) { + const names = Object.keys(tree) + const pathnameList: string[] = [] + for (const name of names) { + const pathname = + parentPathname === '/' ? `/${name}` : `${parentPathname}/${name}` + pathnameList.push(pathname) + if (openItemIdSet.has(getFolderItemId(storageId, pathname))) { + pathnameList.push( + ...getOpenedFolderPathnameList( + tree[name], + storageId, + openItemIdSet, + pathname + ) + ) + } + } + return pathnameList +} + +export default FolderListFragment diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx index 37ae43ee58..dc48c25f5b 100644 --- a/src/components/SideNavigator/SideNavigator.tsx +++ b/src/components/SideNavigator/SideNavigator.tsx @@ -1,15 +1,19 @@ import React, { useMemo, useCallback } from 'react' -import { useRouteParams, usePathnameWithoutNoteId } from '../../lib/router' +import { useRouter } from '../../lib/router' import { useDb } from '../../lib/db' import { entries } from '../../lib/db/utils' import styled from '../../lib/styled' -import { mdiTuneVertical, mdiPlusCircle, mdiDotsHorizontal } from '@mdi/js' -import StorageNavigatorItem from './StorageNavigatorItem' +import { mdiTuneVertical, mdiPlusCircleOutline } from '@mdi/js' import Icon from '../atoms/Icon' import { useDialog, DialogIconTypes } from '../../lib/dialog' import { useContextMenu, MenuTypes } from '../../lib/contextMenu' import { usePreferences } from '../../lib/preferences' import { backgroundColor, iconColor } from '../../lib/styled/styleFunctions' +import SideNavigatorItem from './SideNavigatorItem' +import { useGeneralStatus } from '../../lib/generalStatus' +import ControlButton from './ControlButton' +import FolderListFragment from './FolderListFragment' +import TagListFragment from './TagListFragment' const StyledSideNavContainer = styled.nav` display: flex; @@ -38,6 +42,8 @@ const StyledSideNavContainer = styled.nav` margin: 0; flex: 1; overflow: auto; + display: flex; + flex-direction: column; } .empty { padding: 4px; @@ -70,56 +76,30 @@ const StyledSideNavContainer = styled.nav` } ` +const Spacer = styled.div` + flex: 1; +` + export default () => { const { createStorage, - renameStorage, - removeStorage, createFolder, - removeFolder, + // renameStorage, + // removeStorage, + // removeFolder, + // updateNote, storageMap } = useDb() - const routeParams = useRouteParams() const { popup } = useContextMenu() const { prompt } = useDialog() + const { push } = useRouter() const storageEntries = useMemo(() => { return entries(storageMap) }, [storageMap]) - const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() - - const currentStorage = useMemo(() => { - switch (routeParams.name) { - case 'storages.notes': - return storageMap[routeParams.storageId] - } - return null - }, [routeParams, storageMap]) - - const addFolder = useCallback(() => { - if (currentStorage == null) { - return - } - - const defaultValue = - routeParams.name === 'storages.notes' ? routeParams.folderPathname : '/' - - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue, - submitButtonLabel: 'Create Folder', - onClose: (value: string | null) => { - if (value == null) return - createFolder(currentStorage.id, value) - } - }) - }, [prompt, createFolder, routeParams, currentStorage]) - const openSideNavContextMenu = useCallback( - (event: React.MouseEvent) => { + (event: React.MouseEvent) => { event.preventDefault() popup(event, [ { @@ -144,6 +124,11 @@ export default () => { ) const { toggleClosed } = usePreferences() + const { + toggleSideNavOpenedItem, + sideNavOpenedItemSet, + openSideNavFolderItemRecursively + } = useGeneralStatus() return ( @@ -153,41 +138,74 @@ export default () => {

-
    - {storageEntries.map(([id, storage]) => { +
    + {storageEntries.map(([, storage]) => { + const itemId = `storage:${storage.id}` + const storageIsFolded = !sideNavOpenedItemSet.has(itemId) + const showPromptToCreateFolder = (folderPathname: string) => { + prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue: folderPathname === '/' ? '/' : `${folderPathname}/`, + submitButtonLabel: 'Create Folder', + onClose: async (value: string | null) => { + if (value == null) { + return + } + if (value.endsWith('/')) { + value = value.slice(0, value.length - 1) + } + await createFolder(storage.id, value) + + push(`/app/storages/${storage.id}/notes${value}`) + + // Open folder item + openSideNavFolderItemRecursively(storage.id, value) + } + }) + } return ( - + + push(`/app/storages/${storage.id}`)} + onFoldButtonClick={() => { + toggleSideNavOpenedItem(itemId) + }} + label={storage.name} + depth={0} + controlComponents={[ + showPromptToCreateFolder('/')} + iconPath={mdiPlusCircleOutline} + /> + ]} + /> + {!storageIsFolded && ( + <> + + + + )} + ) })} {storageEntries.length === 0 && (
    No storages
    )} -
-
- - +
+ push('/app/storages')} + /> ) } diff --git a/src/components/SideNavigator/SideNavigatorItem.tsx b/src/components/SideNavigator/SideNavigatorItem.tsx index cd1e361933..3d1d351fa7 100644 --- a/src/components/SideNavigator/SideNavigatorItem.tsx +++ b/src/components/SideNavigator/SideNavigatorItem.tsx @@ -1,111 +1,140 @@ -import React, { useState, MouseEventHandler } from 'react' +import React from 'react' +import cc from 'classcat' import styled from '../../lib/styled' -import { Link } from '../../lib/router' import Icon from '../atoms/Icon' import { mdiChevronDown, mdiChevronRight } from '@mdi/js' -import cc from 'classcat' import { uiTextColor, activeBackgroundColor } from '../../lib/styled/styleFunctions' -const StyledContainer = styled.div` +const Container = styled.div` + position: relative; user-select: none; - .header { - position: relative; - height: 30px; - display: flex; - align-items: center; - } - .headerLink { - width: 100%; - height: 30px; - display: flex; - align-items: center; - text-decoration: none; - ${uiTextColor} + height: 30px; + display: flex; - transition: 200ms background-color; + transition: 200ms background-color; + &:hover, + &:focus, + &:active, + &.active { + ${activeBackgroundColor} + } + .control { + opacity: 0; + } + &:hover .control { + opacity: 1; + } +` - &:hover, - &:focus, - &:active, - &.active { - ${activeBackgroundColor} - } +const FoldButton = styled.button` + position: absolute; + width: 26px; + height: 26px; + padding: 0; + border: none; + background-color: transparent; + margin-right: 3px; + border-radius: 2px; + top: 2px; + ${uiTextColor} + &:focus { + box-shadow: none; } - .toggleButton { - position: absolute; - width: 26px; - height: 26px; - padding: 0; - border: none; - background-color: transparent; - margin-right: 3px; - border-radius: 2px; - top: 2px; - ${uiTextColor} - &:focus { - box-shadow: none; - } +` + +const ClickableContainer = styled.button` + background-color: transparent; + border: none; + height: 30px; + display: flex; + align-items: center; + width: 100%; + + color: ${({ theme }) => theme.uiTextColor}; + &:hover, + &:focus, + &:active, + &.active { + color: ${({ theme }) => theme.activeUiTextColor}; } - .storageIcon { + + .icon { margin-right: 4px; } ` -export interface NavigatorNode { +const Label = styled.div`` + +const ControlContainer = styled.div`` + +interface SideNaviagtorItemProps { + label: string iconPath?: string - name: string - href?: string - children?: NavigatorNode[] - onContextMenu?: MouseEventHandler + depth: number + controlComponents?: any[] + className?: string + folded?: boolean active?: boolean + onFoldButtonClick?: (event: React.MouseEvent) => void + onClick?: (event: React.MouseEvent) => void + onContextMenu?: (event: React.MouseEvent) => void + onDrop?: (event: React.DragEvent) => void + onDragOver?: (event: React.DragEvent) => void + onDragEnd?: (event: React.DragEvent) => void } -interface SideNavigatorItemProps { - node: NavigatorNode - openAlways?: boolean - depth?: number -} - -const SideNavigatorItem = ({ - node: item, - openAlways = false, - depth = 0 -}: SideNavigatorItemProps) => { - const { iconPath, name, href, children, onContextMenu, active } = item - const [open, setOpen] = useState(true) - const childrenExists = children != null && children.length > 0 +const SideNaviagtorItem = ({ + label, + iconPath, + depth, + controlComponents, + className, + folded, + active, + onFoldButtonClick, + onClick, + onContextMenu, + onDrop, + onDragOver, + onDragEnd +}: SideNaviagtorItemProps) => { return ( - -
- {childrenExists && !openAlways && ( - - )} - + {folded != null && ( + - {iconPath != null && } - {name} - -
- {open && - childrenExists && - children!.map(child => ( - - ))} -
+ + + )} + + {iconPath && } + + + {controlComponents && ( + + {controlComponents} + + )} + ) } -export default SideNavigatorItem +export default SideNaviagtorItem diff --git a/src/components/SideNavigator/StorageNavigatorItem.tsx b/src/components/SideNavigator/StorageNavigatorItem.tsx index 6ec425fec6..12d97f3f83 100644 --- a/src/components/SideNavigator/StorageNavigatorItem.tsx +++ b/src/components/SideNavigator/StorageNavigatorItem.tsx @@ -1,285 +1,307 @@ -import React, { useMemo, useCallback, MouseEventHandler } from 'react' -import SideNavigatorItem, { NavigatorNode } from './SideNavigatorItem' -import { NoteStorage } from '../../lib/db/types' -import { - mdiTagMultiple, - mdiDeleteOutline, - mdiFolderOutline, - mdiFolderOpenOutline, - mdiTag, - mdiTagOutline -} from '@mdi/js' -import { useContextMenu, MenuTypes } from '../../lib/contextMenu' -import { useDialog, DialogIconTypes } from '../../lib/dialog' +// import React, { useMemo, useCallback, MouseEventHandler } from 'react' +// import SideNavigatorItem, { NavigatorNode } from './SideNavigatorItem' +// import { NoteStorage, NoteDocEditibleProps, NoteDoc } from '../../lib/db/types' +// import { +// mdiTagMultiple, +// mdiDeleteOutline, +// mdiFolderOutline, +// mdiFolderOpenOutline, +// mdiTag, +// mdiTagOutline +// } from '@mdi/js' +// import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +// import { useDialog, DialogIconTypes } from '../../lib/dialog' -interface StorageNaviagtorItemProps { - storage: NoteStorage - currentPathname: string - renameStorage: (storageId: string, name: string) => Promise - removeStorage: (storageId: string) => Promise - createFolder: (storageId: string, folderPath: string) => Promise - removeFolder: (storageId: string, folderPath: string) => Promise -} +// interface StorageNaviagtorItemProps { +// storage: NoteStorage +// currentPathname: string +// renameStorage: (storageId: string, name: string) => Promise +// removeStorage: (storageId: string) => Promise +// createFolder: (storageId: string, folderPath: string) => Promise +// removeFolder: (storageId: string, folderPath: string) => Promise +// updateNote( +// storageId: string, +// noteId: string, +// noteProps: Partial +// ): Promise +// } -type FolderTree = { - [key: string]: FolderTree -} +// type FolderTree = { +// [key: string]: FolderTree +// } -const StorageNavigatorItem = ({ - storage, - currentPathname, - renameStorage, - removeStorage, - createFolder, - removeFolder -}: StorageNaviagtorItemProps) => { - const { prompt, messageBox } = useDialog() - const contextMenu = useContextMenu() - const { id: storageId, name: storageName } = storage - const openContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() +// const StorageNavigatorItem = ({ +// storage, +// currentPathname, +// renameStorage, +// removeStorage, +// createFolder, +// removeFolder +// }: StorageNaviagtorItemProps) => { +// const { prompt, messageBox } = useDialog() +// const { popup } = useContextMenu() +// const { id: storageId, name: storageName } = storage +// const openContextMenu = useCallback( +// (event: React.MouseEvent) => { +// event.preventDefault() - contextMenu.popup(event, [ - { - type: MenuTypes.Normal, - label: 'New Folder', - onClick: async () => { - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue: '/', - submitButtonLabel: 'Create Folder', - onClose: (value: string | null) => { - if (value == null) return - createFolder(storageId, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Rename Storage', - onClick: async () => { - prompt({ - title: `Rename "${storageName}" storage`, - message: 'Enter new name for the storage', - iconType: DialogIconTypes.Question, - defaultValue: storageName, - submitButtonLabel: 'Rename Folder', - onClose: (value: string | null) => { - if (value == null) return - renameStorage(storageId, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Remove Storage', - onClick: async () => { - messageBox({ - title: `Remove "${storageName}" storage`, - message: 'All notes and folders will be deleted.', - iconType: DialogIconTypes.Warning, - buttons: ['Remove Storage', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(storageId) - } - } - }) - } - } - ]) - }, - [ - contextMenu, - prompt, - messageBox, - createFolder, - storageId, - storageName, - renameStorage, - removeStorage - ] - ) +// popup(event, [ +// { +// type: MenuTypes.Normal, +// label: 'New Folder', +// onClick: async () => { +// prompt({ +// title: 'Create a Folder', +// message: 'Enter the path where do you want to create a folder', +// iconType: DialogIconTypes.Question, +// defaultValue: '/', +// submitButtonLabel: 'Create Folder', +// onClose: (value: string | null) => { +// if (value == null) return +// createFolder(storageId, value) +// } +// }) +// } +// }, +// { +// type: MenuTypes.Normal, +// label: 'Rename Storage', +// onClick: async () => { +// prompt({ +// title: `Rename "${storageName}" storage`, +// message: 'Enter new name for the storage', +// iconType: DialogIconTypes.Question, +// defaultValue: storageName, +// submitButtonLabel: 'Rename Folder', +// onClose: (value: string | null) => { +// if (value == null) return +// renameStorage(storageId, value) +// } +// }) +// } +// }, +// { +// type: MenuTypes.Normal, +// label: 'Remove Storage', +// onClick: async () => { +// messageBox({ +// title: `Remove "${storageName}" storage`, +// message: 'All notes and folders will be deleted.', +// iconType: DialogIconTypes.Warning, +// buttons: ['Remove Storage', 'Cancel'], +// defaultButtonIndex: 0, +// cancelButtonIndex: 1, +// onClose: (value: number | null) => { +// if (value === 0) { +// removeStorage(storageId) +// } +// } +// }) +// } +// } +// ]) +// }, +// [ +// popup, +// prompt, +// messageBox, +// createFolder, +// storageId, +// storageName, +// renameStorage, +// removeStorage +// ] +// ) - const createFolderContextMenuHandler = useCallback( - (pathname: string) => { - return (event: React.MouseEvent) => { - const folderIsRootFolder = pathname === '/' +// const createFolderContextMenuHandler = useCallback( +// (pathname: string) => { +// return (event: React.MouseEvent) => { +// const folderIsRootFolder = pathname === '/' - event.preventDefault() - contextMenu.popup(event, [ - { - type: MenuTypes.Normal, - label: 'New Folder', - onClick: async () => { - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue: folderIsRootFolder ? '/' : `${pathname}/`, - submitButtonLabel: 'Create Folder', - onClose: (value: string | null) => { - if (value == null) return - createFolder(storageId, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Remove Folder', - enabled: !folderIsRootFolder, - onClick: () => { - messageBox({ - title: `Remove "${pathname}" folder`, - message: 'All notes and subfolders will be deleted.', - iconType: DialogIconTypes.Warning, - buttons: ['Remove Folder', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeFolder(storageId, pathname) - } - } - }) - } - } - ]) - } - }, - [contextMenu, storageId, messageBox, prompt, createFolder, removeFolder] - ) +// event.preventDefault() +// popup(event, [ +// { +// type: MenuTypes.Normal, +// label: 'New Folder', +// onClick: async () => { +// prompt({ +// title: 'Create a Folder', +// message: 'Enter the path where do you want to create a folder', +// iconType: DialogIconTypes.Question, +// defaultValue: folderIsRootFolder ? '/' : `${pathname}/`, +// submitButtonLabel: 'Create Folder', +// onClose: (value: string | null) => { +// if (value == null) return +// createFolder(storageId, value) +// } +// }) +// } +// }, +// { +// type: MenuTypes.Normal, +// label: 'Remove Folder', +// enabled: !folderIsRootFolder, +// onClick: () => { +// messageBox({ +// title: `Remove "${pathname}" folder`, +// message: 'All notes and subfolders will be deleted.', +// iconType: DialogIconTypes.Warning, +// buttons: ['Remove Folder', 'Cancel'], +// defaultButtonIndex: 0, +// cancelButtonIndex: 1, +// onClose: (value: number | null) => { +// if (value === 0) { +// removeFolder(storageId, pathname) +// } +// } +// }) +// } +// } +// ]) +// } +// }, +// [popup, storageId, messageBox, prompt, createFolder, removeFolder] +// ) - const folderNodes = useMemo(() => { - const folderTree = getFolderTree(Object.keys(storage.folderMap)) +// const folderNodes = useMemo(() => { +// const folderTree = getFolderTree(Object.keys(storage.folderMap)) - return getNavigatorNodeFromPathnameTree( - folderTree, - storageId, - '/', - currentPathname, - createFolderContextMenuHandler - ) - }, [ - storageId, - currentPathname, - storage.folderMap, - createFolderContextMenuHandler - ]) +// return getNavigatorNodeFromPathnameTree( +// folderTree, +// storageId, +// '/', +// currentPathname, +// createFolderContextMenuHandler +// ) +// }, [ +// storageId, +// currentPathname, +// storage.folderMap, +// createFolderContextMenuHandler +// ]) - const tagNodes = useMemo(() => { - return Object.keys(storage.tagMap).map(tagName => { - const tagPathname = `/app/storages/${storage.id}/tags/${tagName}` - const tagIsActive = currentPathname === tagPathname - return { - name: tagName, - iconPath: tagIsActive ? mdiTag : mdiTagOutline, - href: `/app/storages/${storage.id}/tags/${tagName}`, - active: tagIsActive - } - }) - }, [storage, currentPathname]) +// const tagNodes = useMemo(() => { +// return Object.keys(storage.tagMap).map(tagName => { +// const tagPathname = `/app/storages/${storage.id}/tags/${tagName}` +// const tagIsActive = currentPathname === tagPathname +// return { +// name: tagName, +// iconPath: tagIsActive ? mdiTag : mdiTagOutline, +// href: `/app/storages/${storage.id}/tags/${tagName}`, +// active: tagIsActive +// } +// }) +// }, [storage, currentPathname]) - const node = useMemo(() => { - const storagePathname = `/app/storages/${storage.id}` - const notesPathname = `/app/storages/${storage.id}/notes` - const notesIsActive = currentPathname === notesPathname - return { - name: storage.name, - href: storagePathname, - active: currentPathname === storagePathname, - onContextMenu: openContextMenu, - children: [ - { - name: 'Notes', - iconPath: notesIsActive ? mdiFolderOpenOutline : mdiFolderOutline, - href: notesPathname, - active: notesIsActive, - onContextMenu: createFolderContextMenuHandler('/') - }, - ...folderNodes, - { - iconPath: mdiTagMultiple, - name: 'Tags', - href: `${storagePathname}/tags`, - children: tagNodes - }, - { - iconPath: mdiDeleteOutline, - href: `${storagePathname}/trashcan`, - name: 'Trash Can', - active: currentPathname === `/app/storages/${storage.id}/trashcan` - } - ] - } - }, [ - storage, - folderNodes, - tagNodes, - openContextMenu, - createFolderContextMenuHandler, - currentPathname - ]) +// const node = useMemo(() => { +// const storagePathname = `/app/storages/${storage.id}` +// const notesPathname = `/app/storages/${storage.id}/notes` +// const notesIsActive = currentPathname === notesPathname +// return { +// name: storage.name, +// href: storagePathname, +// active: currentPathname === storagePathname, +// onContextMenu: openContextMenu, +// children: [ +// { +// name: 'Notes', +// iconPath: notesIsActive ? mdiFolderOpenOutline : mdiFolderOutline, +// href: notesPathname, +// active: notesIsActive, +// onContextMenu: createFolderContextMenuHandler('/') +// }, +// ...folderNodes, +// { +// iconPath: mdiTagMultiple, +// name: 'Tags', +// href: `${storagePathname}/tags`, +// children: tagNodes +// }, +// { +// iconPath: mdiDeleteOutline, +// href: `${storagePathname}/trashcan`, +// name: 'Trash Can', +// active: currentPathname === `/app/storages/${storage.id}/trashcan` +// } +// ] +// } +// }, [ +// storage, +// folderNodes, +// tagNodes, +// openContextMenu, +// createFolderContextMenuHandler, +// currentPathname +// ]) - return -} +// return +// } -export default StorageNavigatorItem +// export default StorageNavigatorItem -function getFolderTree(pathnames: string[]) { - const tree = {} - for (const pathname of pathnames) { - if (pathname === '/') continue - const [, ...folderNames] = pathname.split('/') - let currentNode = tree - for (let index = 0; index < folderNames.length; index++) { - const currentPathname = folderNames[index] - if (currentNode[currentPathname] == null) { - currentNode[currentPathname] = {} - } - currentNode = currentNode[currentPathname] - } - } +// function getFolderTree(pathnames: string[]) { +// const tree = {} +// for (const pathname of pathnames) { +// if (pathname === '/') continue +// const [, ...folderNames] = pathname.split('/') +// let currentNode = tree +// for (let index = 0; index < folderNames.length; index++) { +// const currentPathname = folderNames[index] +// if (currentNode[currentPathname] == null) { +// currentNode[currentPathname] = {} +// } +// currentNode = currentNode[currentPathname] +// } +// } - return tree -} +// return tree +// } -function getNavigatorNodeFromPathnameTree( - tree: FolderTree, - storageId: string, - parentFolderPathname: string, - currentPathname: string, - contextMenuHandlerCreator: (pathname: string) => MouseEventHandler -): NavigatorNode[] { - return Object.entries(tree).map(([folderName, tree]) => { - const folderPathname = - parentFolderPathname === '/' - ? `/${folderName}` - : `${parentFolderPathname}/${folderName}` - const pathname = `/app/storages/${storageId}/notes${folderPathname}` - const folderIsActive = pathname === currentPathname +// function getNavigatorNodeFromPathnameTree( +// tree: FolderTree, +// storageId: string, +// parentFolderPathname: string, +// currentPathname: string, +// contextMenuHandlerCreator: (pathname: string) => MouseEventHandler +// ): NavigatorNode[] { +// return Object.entries(tree).map(([folderName, tree]) => { +// const folderPathname = +// parentFolderPathname === '/' +// ? `/${folderName}` +// : `${parentFolderPathname}/${folderName}` +// const pathname = `/app/storages/${storageId}/notes${folderPathname}` +// const folderIsActive = pathname === currentPathname - return { - name: folderName, - iconPath: folderIsActive ? mdiFolderOpenOutline : mdiFolderOutline, - href: pathname, - active: folderIsActive, - onContextMenu: contextMenuHandlerCreator(folderPathname), - children: getNavigatorNodeFromPathnameTree( - tree, - storageId, - folderPathname, - currentPathname, - contextMenuHandlerCreator - ) - } - }) -} +// return { +// name: folderName, +// iconPath: folderIsActive ? mdiFolderOpenOutline : mdiFolderOutline, +// href: pathname, +// active: folerIsActive, +// onContextMenu: contextMenuHandlerCreator(folderPathname), +// onDragOver: (event: React.DragEvent) => { +// event.preventDefault() +// }, +// onDrop: (event: React.DragEvent) => { +// const { storageId: targetNoteStorageId, note: targetNote } = JSON.parse( +// event.dataTransfer.getData('application/x-note-json') +// ) + +// if (storageId === targetNoteStorageId) { +// // Move note +// } else { +// // Ask copy or move +// // If move, create new one and remove original +// // If copy, just create new one +// } +// console.log(storageId, targetNote._id) +// }, +// children: getNavigatorNodeFromPathnameTree( +// tree, +// storageId, +// folderPathname, +// currentPathname, +// contextMenuHandlerCreator +// ) +// } +// }) +// } diff --git a/src/components/SideNavigator/TagListFragment.tsx b/src/components/SideNavigator/TagListFragment.tsx new file mode 100644 index 0000000000..6bab1f93be --- /dev/null +++ b/src/components/SideNavigator/TagListFragment.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { + mdiTagMultiple, + // mdiTag, + mdiTagOutline +} from '@mdi/js' +import SideNavigatorItem from './SideNavigatorItem' +import { NoteStorage } from '../../lib/db/types' + +interface TagListFragmentProps { + storage: NoteStorage +} + +const TagListFragment = ({ storage }: TagListFragmentProps) => { + const tagList = Object.keys(storage.tagMap).map(tagName => { + return ( + + ) + }) + return ( + <> + + {tagList} + + ) +} + +export default TagListFragment diff --git a/src/lib/db/store.ts b/src/lib/db/store.ts index bb70a60db0..562bbc8bee 100644 --- a/src/lib/db/store.ts +++ b/src/lib/db/store.ts @@ -21,7 +21,7 @@ import { generateId } from '../string' import PouchDB from './PouchDB' import { LiteStorage, localLiteStorage } from 'ltstrg' import { produce } from 'immer' -import { useRouter } from '../router' +import { useRouter, usePathnameWithoutNoteId } from '../router' import { values } from '../db/utils' import { storageDataListKey } from '../localStorageKeys' import { TAG_ID_PREFIX } from './consts' @@ -59,6 +59,7 @@ export function createDbStoreCreator( ) { return (): DbStore => { const router = useRouter() + const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() const [initialized, setInitialized] = useState(false) const [storageMap, setStorageMap] = useState>({}) @@ -202,7 +203,9 @@ export function createDbStoreCreator( } await storage.db.removeFolder(pathname) if ( - router.pathname.startsWith(`/app/storages/${id}/notes${pathname}`) + `${currentPathnameWithoutNoteId}/`.startsWith( + `/app/storages/${id}/notes${pathname}/` + ) ) { router.replace( `/app/storages/${id}/notes${getParentFolderPathname(pathname)}` @@ -270,7 +273,7 @@ export function createDbStoreCreator( }) ) }, - [storageMap, router] + [storageMap, router, currentPathnameWithoutNoteId] ) const createNote = useCallback( diff --git a/src/lib/generalStatus/store.ts b/src/lib/generalStatus/store.ts index 2c2bb56779..f9c3e6e1f5 100644 --- a/src/lib/generalStatus/store.ts +++ b/src/lib/generalStatus/store.ts @@ -4,6 +4,7 @@ import { useSetState } from 'react-use' import { generalStatusKey } from '../localStorageKeys' import { createStoreContext } from '../utils/context' import { GeneralStatus } from './types' +import { getFolderItemId } from '../nav' function loadGeneralStatus(): Partial { const stringifiedGeneralStatus = localLiteStorage.getItem(generalStatusKey) @@ -63,6 +64,36 @@ function useGeneralStatusStore() { [setGeneralStatus, sideNavOpenedItemSet] ) + const addSideNavOpenedItem = useCallback( + (...itemIdList: string[]) => { + const newSet = new Set(sideNavOpenedItemSet) + + for (const itemId of itemIdList) { + newSet.add(itemId) + } + + setGeneralStatus({ + sideNavOpenedItemList: [...newSet] + }) + }, + [setGeneralStatus, sideNavOpenedItemSet] + ) + + const openSideNavFolderItemRecursively = useCallback( + (storageId: string, folderPathname: string) => { + const folderPathElements = folderPathname.slice(1).split('/') + const itemIdListToOpen = [] + let currentPathname = '' + for (const element of folderPathElements) { + currentPathname = `${currentPathname}/${element}` + itemIdListToOpen.push(getFolderItemId(storageId, currentPathname)) + } + + addSideNavOpenedItem(...itemIdListToOpen) + }, + [addSideNavOpenedItem] + ) + useEffect(() => { saveGeneralStatus(generalStatus) }, [generalStatus]) @@ -71,7 +102,9 @@ function useGeneralStatusStore() { generalStatus: mergedGeneralStatus, setGeneralStatus, sideNavOpenedItemSet, - toggleSideNavOpenedItem + toggleSideNavOpenedItem, + addSideNavOpenedItem, + openSideNavFolderItemRecursively } } diff --git a/src/lib/nav.ts b/src/lib/nav.ts new file mode 100644 index 0000000000..6d0f1a1f78 --- /dev/null +++ b/src/lib/nav.ts @@ -0,0 +1,3 @@ +export function getFolderItemId(storageId: string, folderPathname: string) { + return `storage:${storageId}/folder:${folderPathname}` +} diff --git a/src/lib/router/Link.tsx b/src/lib/router/Link.tsx index 0d7c1b0e91..c70006085c 100644 --- a/src/lib/router/Link.tsx +++ b/src/lib/router/Link.tsx @@ -3,7 +3,8 @@ import React, { FC, CSSProperties, MouseEventHandler, - FocusEventHandler + FocusEventHandler, + DragEventHandler } from 'react' import { useRouter } from './store' @@ -14,6 +15,10 @@ export interface LinkProps { style?: CSSProperties onContextMenu?: MouseEventHandler onFocus?: FocusEventHandler + draggable?: boolean + onDragStart?: DragEventHandler + onDrop?: DragEventHandler + onDragOver?: DragEventHandler } export const Link: FC = ({ @@ -22,7 +27,10 @@ export const Link: FC = ({ className, style, onContextMenu, - onFocus + onFocus, + onDragStart, + onDragOver, + onDrop }) => { const router = useRouter() @@ -44,6 +52,10 @@ export const Link: FC = ({ className={className} style={style} onFocus={onFocus} + draggable={true} + onDragStart={onDragStart} + onDragOver={onDragOver} + onDrop={onDrop} > {children} From ef094559304886cdfb8595be17546020206d76aa Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 8 Dec 2019 09:18:12 +0900 Subject: [PATCH 09/20] Fix folderMap update after updating note's pathname --- src/lib/db/NoteDb.ts | 3 +++ src/lib/db/store.ts | 51 ++++++++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/lib/db/NoteDb.ts b/src/lib/db/NoteDb.ts index 05161d6636..a21cec48db 100644 --- a/src/lib/db/NoteDb.ts +++ b/src/lib/db/NoteDb.ts @@ -426,6 +426,9 @@ export default class NoteDb { } async getFoldersByPathnames(pathnames: string[]): Promise { + if (pathnames.length === 0) { + return [] + } const allDocsResponse = await this.pouchDb.allDocs({ keys: pathnames.map(pathname => getFolderId(pathname)), include_docs: true diff --git a/src/lib/db/store.ts b/src/lib/db/store.ts index 562bbc8bee..490d19925f 100644 --- a/src/lib/db/store.ts +++ b/src/lib/db/store.ts @@ -364,20 +364,48 @@ export function createDbStoreCreator( if (storage == null) { return } + let previousNoteDoc = await storage.db.getNote(noteId) const noteDoc = await storage.db.updateNote(noteId, noteProps) if (noteDoc == null) { return } + if (previousNoteDoc == null) { + previousNoteDoc = noteDoc + } + const folderPathnameIsChanged = + previousNoteDoc.folderPathname !== noteDoc.folderPathname + const folderListToRefresh: PopulatedFolderDoc[] = [] + + if (folderPathnameIsChanged) { + const previousFolder = + storage.folderMap[previousNoteDoc.folderPathname] + if (previousFolder != null) { + const newNoteIdSetForPreviousFolder = new Set( + previousFolder.noteIdSet + ) + newNoteIdSetForPreviousFolder.delete(noteId) + folderListToRefresh.push({ + ...previousFolder, + noteIdSet: newNoteIdSetForPreviousFolder + }) + } + } const parentFolderPathnamesToCheck = [ ...getAllParentFolderPathnames(noteDoc.folderPathname) ].filter(aPathname => storage.folderMap[aPathname] == null) - const parentFoldersToRefresh = - parentFolderPathnamesToCheck.length > 0 - ? await storage.db.getFoldersByPathnames( - parentFolderPathnamesToCheck - ) - : [] + folderListToRefresh.push( + ...(await storage.db.getFoldersByPathnames( + parentFolderPathnamesToCheck + )).map(folderDoc => { + return { + ...folderDoc, + pathname: getFolderPathname(folderDoc._id), + noteIdSet: new Set() + } + }) + ) + const folder: PopulatedFolderDoc = storage.folderMap[noteDoc.folderPathname] == null ? ({ @@ -392,6 +420,7 @@ export function createDbStoreCreator( noteDoc._id ]) } + folderListToRefresh.push(folder) const removedTags: ObjectMap = difference( storage.noteMap[noteDoc._id]!.tags, @@ -431,15 +460,9 @@ export function createDbStoreCreator( setStorageMap( produce((draft: ObjectMap) => { draft[storageId]!.noteMap[noteDoc._id] = noteDoc - parentFoldersToRefresh.forEach(folder => { - const aPathname = getFolderPathname(folder._id) - draft[storageId]!.folderMap[aPathname] = { - ...folder, - pathname: aPathname, - noteIdSet: new Set() - } + folderListToRefresh.forEach(folderDoc => { + draft[storageId]!.folderMap[folderDoc.pathname] = folderDoc }) - draft[storageId]!.folderMap[noteDoc.folderPathname] = folder draft[storageId]!.tagMap = { ...storage.tagMap, ...removedTags, From 9f2889e0634a6e3c351ce0b947c352fc69cf3c98 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 8 Dec 2019 09:18:51 +0900 Subject: [PATCH 10/20] Impelemnt moving a note via drag and drop(Same storage) --- .../SideNavigator/FolderListFragment.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/SideNavigator/FolderListFragment.tsx b/src/components/SideNavigator/FolderListFragment.tsx index d98dc50036..1c72958726 100644 --- a/src/components/SideNavigator/FolderListFragment.tsx +++ b/src/components/SideNavigator/FolderListFragment.tsx @@ -23,10 +23,7 @@ const FolderListFragment = ({ storage, showPromptToCreateFolder }: FolderListFragmentProps) => { - const { - removeFolder - // updateNote - } = useDb() + const { removeFolder, updateNote } = useDb() const { push } = useRouter() const { messageBox } = useDialog() const { popup } = useContextMenu() @@ -138,7 +135,6 @@ const FolderListFragment = ({ const folderIsActive = currentPathnameWithoutNoteId === `/app/storages/${storageId}/notes${folderPathname}` - return ( ]} + onDragOver={event => { + event.preventDefault() + }} + onDrop={async event => { + const { + storageId: targetNoteStorageId, + note: targetNote + } = JSON.parse( + event.dataTransfer.getData('application/x-note-json') + ) + + if (storageId === targetNoteStorageId) { + await updateNote(storageId, targetNote._id, { + folderPathname + }) + } else { + // Ask copy or move + // If move, create new one and remove original + // If copy, just create new one + } + }} /> ) })} From 3db9f1c23c11f7f712793a7788fe48b39ebf12ef Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 01:47:08 +0900 Subject: [PATCH 11/20] Fix dialog style --- src/components/Dialog/styled.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Dialog/styled.ts b/src/components/Dialog/styled.ts index 2a5bcfa516..ef7be63649 100644 --- a/src/components/Dialog/styled.ts +++ b/src/components/Dialog/styled.ts @@ -76,9 +76,9 @@ export const StyledDialogButtonGroup = styled.div` ` export const StyledDialogButton = styled.button` - padding: 5px 20px; + padding: 5px 10px; border-radius: 4px; - margin-left: 16px; + margin-left: 8px; user-select: none; ${secondaryButtonStyle} ` From dfe9a603b881ee4955f808dae600360e219ea12f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 01:47:37 +0900 Subject: [PATCH 12/20] Implement dnd note from different storage --- src/components/NotePage/NoteList/NoteItem.tsx | 9 +- .../SideNavigator/FolderListFragment.tsx | 60 +++++-- src/lib/db/NoteDb.ts | 2 +- src/lib/db/store.ts | 156 +++++++++++++++++- src/lib/dnd.ts | 38 +++++ 5 files changed, 239 insertions(+), 26 deletions(-) create mode 100644 src/lib/dnd.ts diff --git a/src/components/NotePage/NoteList/NoteItem.tsx b/src/components/NotePage/NoteList/NoteItem.tsx index 9885651ad7..d2d51d8bd4 100644 --- a/src/components/NotePage/NoteList/NoteItem.tsx +++ b/src/components/NotePage/NoteList/NoteItem.tsx @@ -10,6 +10,7 @@ import { secondaryBackgroundColor } from '../../../lib/styled/styleFunctions' import cc from 'classcat' +import { setTransferrableNoteData } from '../../../lib/dnd' const StyledNoteListItem = styled.div` margin: 0; @@ -68,13 +69,7 @@ export default ({ storageId, note, active, basePathname }: NoteItemProps) => { const handleDragStart = useCallback( (event: React.DragEvent) => { - event.dataTransfer.setData( - 'application/x-note-json', - JSON.stringify({ - note, - storageId - }) - ) + setTransferrableNoteData(event, storageId, note) }, [note, storageId] ) diff --git a/src/components/SideNavigator/FolderListFragment.tsx b/src/components/SideNavigator/FolderListFragment.tsx index 1c72958726..60e4500860 100644 --- a/src/components/SideNavigator/FolderListFragment.tsx +++ b/src/components/SideNavigator/FolderListFragment.tsx @@ -13,6 +13,7 @@ import { usePathnameWithoutNoteId, useRouter } from '../../lib/router' import { useGeneralStatus } from '../../lib/generalStatus' import ControlButton from './ControlButton' import { getFolderItemId } from '../../lib/nav' +import { getTransferrableNoteData } from '../../lib/dnd' interface FolderListFragmentProps { storage: NoteStorage @@ -23,7 +24,12 @@ const FolderListFragment = ({ storage, showPromptToCreateFolder }: FolderListFragmentProps) => { - const { removeFolder, updateNote } = useDb() + const { + removeFolder, + updateNote, + createNote, + moveNoteToOtherStorage + } = useDb() const { push } = useRouter() const { messageBox } = useDialog() const { popup } = useContextMenu() @@ -160,21 +166,51 @@ const FolderListFragment = ({ event.preventDefault() }} onDrop={async event => { + const transferrableNoteData = getTransferrableNoteData(event) + if (transferrableNoteData == null) { + return + } + const { - storageId: targetNoteStorageId, - note: targetNote - } = JSON.parse( - event.dataTransfer.getData('application/x-note-json') - ) - - if (storageId === targetNoteStorageId) { - await updateNote(storageId, targetNote._id, { + storageId: originalNoteStorageId, + note: originalNote + } = transferrableNoteData + + if (storageId === originalNoteStorageId) { + await updateNote(storageId, originalNote._id, { folderPathname }) } else { - // Ask copy or move - // If move, create new one and remove original - // If copy, just create new one + messageBox({ + title: 'Move Note to Other storage', + message: + 'You are trying to move a note to different storage.', + iconType: DialogIconTypes.Info, + buttons: ['Move Note', 'Copy Note', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 2, + onClose: async (value: number | null) => { + switch (value) { + case 0: + await moveNoteToOtherStorage( + originalNoteStorageId, + originalNote._id, + storageId, + folderPathname + ) + return + case 1: + await createNote(storageId, { + title: originalNote.title, + content: originalNote.content, + folderPathname, + tags: originalNote.tags, + data: originalNote.data + }) + return + } + } + }) } }} /> diff --git a/src/lib/db/NoteDb.ts b/src/lib/db/NoteDb.ts index a21cec48db..1ac697a6a1 100644 --- a/src/lib/db/NoteDb.ts +++ b/src/lib/db/NoteDb.ts @@ -201,7 +201,7 @@ export default class NoteDb { const now = getNow() const noteDocProps: ExceptRev = { _id: generateNoteId(), - title: 'Untitled', + title: '', content: '', tags: [], folderPathname: '/', diff --git a/src/lib/db/store.ts b/src/lib/db/store.ts index 490d19925f..5b76e2fd99 100644 --- a/src/lib/db/store.ts +++ b/src/lib/db/store.ts @@ -51,6 +51,12 @@ export interface DbStore { untrashNote(storageId: string, noteId: string): Promise purgeNote(storageId: string, noteId: string): Promise removeTag(storageId: string, tag: string): Promise + moveNoteToOtherStorage( + originalStorageId: string, + noteId: string, + targetStorageId: string, + targetFolderPathname: string + ): Promise } export function createDbStoreCreator( @@ -287,12 +293,9 @@ export function createDbStoreCreator( const parentFolderPathnamesToCheck = [ ...getAllParentFolderPathnames(noteDoc.folderPathname) ].filter(aPathname => storage.folderMap[aPathname] == null) - const parentFoldersToRefresh = - parentFolderPathnamesToCheck.length > 0 - ? await storage.db.getFoldersByPathnames( - parentFolderPathnamesToCheck - ) - : [] + const parentFoldersToRefresh = await storage.db.getFoldersByPathnames( + parentFolderPathnamesToCheck + ) const folder: PopulatedFolderDoc = storage.folderMap[noteDoc.folderPathname] == null @@ -476,6 +479,146 @@ export function createDbStoreCreator( [storageMap] ) + const moveNoteToOtherStorage = useCallback( + async ( + originalStorageId: string, + noteId: string, + targetStorageId: string, + targetFolderPathname: string + ) => { + const originalStorage = storageMap[originalStorageId] + const targetStorage = storageMap[targetStorageId] + if (originalStorage == null) { + throw new Error( + 'Original storage does not exist. Please refresh the app and try again.' + ) + } + if (targetStorage == null) { + throw new Error( + 'Target storage does not exist. Please refresh the app and try again.' + ) + } + const originalNote = await originalStorage.db.getNote(noteId) + if (originalNote == null) { + throw new Error( + 'Target note does not exist. Please refresh the app and try again.' + ) + } + + const newNote = await targetStorage.db.createNote({ + title: originalNote.title, + content: originalNote.content, + tags: originalNote.tags, + data: originalNote.data, + folderPathname: targetFolderPathname + }) + await originalStorage.db.purgeNote(originalNote._id) + + const modifiedTagsInOriginalStorage = originalNote.tags + .map(tagName => originalStorage.tagMap[tagName]) + .filter(tagDoc => tagDoc != null) + .map(tagDoc => { + const newNoteIdSet = new Set(tagDoc!.noteIdSet) + newNoteIdSet.delete(originalNote._id) + return { + ...tagDoc!, + noteIdSet: newNoteIdSet + } + }) + let modifiedFolderInOriginalStorage = + originalStorage.folderMap[originalNote.folderPathname] + if (modifiedFolderInOriginalStorage != null) { + const newNoteIdSet = new Set( + modifiedFolderInOriginalStorage.noteIdSet + ) + newNoteIdSet.delete(originalNote._id) + modifiedFolderInOriginalStorage = { + ...modifiedFolderInOriginalStorage, + noteIdSet: newNoteIdSet + } + } + + const modifiedFoldersInTargetStorage: PopulatedFolderDoc[] = [] + const targetFolder = + targetStorage.folderMap[targetFolderPathname] == null + ? { + ...(await targetStorage.db.getFolder(targetFolderPathname))!, + noteIdSet: new Set(), + pathname: targetFolderPathname + } + : targetStorage.folderMap[targetFolderPathname]! + const newNoteIdSetForTargetFolder = new Set([ + ...targetFolder.noteIdSet, + newNote._id + ]) + modifiedFoldersInTargetStorage.push({ + ...targetFolder, + noteIdSet: newNoteIdSetForTargetFolder + }) + + const parentFolderPathnamesToCheck = [ + ...getAllParentFolderPathnames(targetFolderPathname) + ].filter(aPathname => targetStorage.folderMap[aPathname] == null) + const parentFoldersToRefresh = await targetStorage.db.getFoldersByPathnames( + parentFolderPathnamesToCheck + ) + modifiedFoldersInTargetStorage.push( + ...parentFoldersToRefresh.map(folderDoc => { + return { + ...folderDoc, + pathname: getFolderPathname(folderDoc._id), + noteIdSet: new Set() + } + }) + ) + + const modifiedTagsInTargetStorage = await Promise.all( + newNote.tags.map(async tagName => { + const tagDoc = targetStorage.tagMap[tagName] + if (tagDoc == null) { + return { + ...(await targetStorage.db.getTag(tagName))!, + name: tagName, + noteIdSet: new Set([newNote._id]) + } + } + return { + ...tagDoc, + noteIdSet: new Set([...tagDoc.noteIdSet, newNote._id]) + } + }) + ) + + const modifiedNoteMapOfOriginalStorage = { + ...originalStorage.noteMap + } + delete modifiedNoteMapOfOriginalStorage[originalNote._id] + + setStorageMap( + produce((draft: ObjectMap) => { + draft[originalStorageId]!.noteMap = modifiedNoteMapOfOriginalStorage + if (modifiedFolderInOriginalStorage != null) { + draft[originalStorageId]!.folderMap[ + modifiedFolderInOriginalStorage.pathname + ] = modifiedFolderInOriginalStorage + } + modifiedTagsInOriginalStorage.forEach(tagDoc => { + draft[originalStorageId]!.tagMap[tagDoc.name] = tagDoc + }) + + draft[targetStorageId]!.noteMap[newNote._id] = newNote + modifiedFoldersInTargetStorage.forEach(folderDoc => { + draft[targetStorageId]!.folderMap[folderDoc.pathname] = folderDoc + }) + modifiedTagsInTargetStorage.forEach(tagDoc => { + draft[targetStorageId]!.tagMap[tagDoc.name] = tagDoc + }) + }) + ) + }, + [storageMap] + ) + const trashNote = useCallback( async (storageId: string, noteId: string) => { const storage = storageMap[storageId] @@ -700,6 +843,7 @@ export function createDbStoreCreator( trashNote, untrashNote, purgeNote, + moveNoteToOtherStorage, removeTag } } diff --git a/src/lib/dnd.ts b/src/lib/dnd.ts new file mode 100644 index 0000000000..f6cc85e448 --- /dev/null +++ b/src/lib/dnd.ts @@ -0,0 +1,38 @@ +import { NoteDoc } from './db/types' + +const noteFormat = 'application/x-boost-note-json' + +export interface TransferrableNoteData { + storageId: string + note: NoteDoc +} + +export function getTransferrableNoteData( + event: React.DragEvent | DragEvent +): TransferrableNoteData | null { + if (event.dataTransfer == null) return null + + const data = event.dataTransfer.getData(noteFormat) + if (data.length === 0) { + return null + } + + return JSON.parse(data) +} + +export function setTransferrableNoteData( + event: React.DragEvent | DragEvent, + storageId: string, + note: NoteDoc +) { + if (event.dataTransfer == null) { + return + } + event.dataTransfer.setData( + noteFormat, + JSON.stringify({ + storageId, + note + }) + ) +} From debd4189aecedde5d67fc3b7257c0ddf5c1d7fb3 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 01:55:19 +0900 Subject: [PATCH 13/20] Make tag list fragments foldable --- .../SideNavigator/TagListFragment.tsx | 45 +++++++++++++------ src/lib/nav.ts | 4 ++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/components/SideNavigator/TagListFragment.tsx b/src/components/SideNavigator/TagListFragment.tsx index 6bab1f93be..c26740e9cb 100644 --- a/src/components/SideNavigator/TagListFragment.tsx +++ b/src/components/SideNavigator/TagListFragment.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { mdiTagMultiple, // mdiTag, @@ -6,26 +6,45 @@ import { } from '@mdi/js' import SideNavigatorItem from './SideNavigatorItem' import { NoteStorage } from '../../lib/db/types' +import { useGeneralStatus } from '../../lib/generalStatus' +import { getTagListItemId } from '../../lib/nav' interface TagListFragmentProps { storage: NoteStorage } const TagListFragment = ({ storage }: TagListFragmentProps) => { - const tagList = Object.keys(storage.tagMap).map(tagName => { - return ( - - ) - }) + const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() + const { id: storageId, tagMap } = storage + + const tagListNavItemId = getTagListItemId(storage.id) + const tagListIsFolded = !sideNavOpenedItemSet.has(tagListNavItemId) + + const tagList = useMemo(() => { + return Object.keys(tagMap).map(tagName => { + return ( + + ) + }) + }, [storageId, tagMap]) + return ( <> - - {tagList} + { + toggleSideNavOpenedItem(tagListNavItemId) + }} + /> + {!tagListIsFolded && tagList} ) } diff --git a/src/lib/nav.ts b/src/lib/nav.ts index 6d0f1a1f78..e3cbecdef9 100644 --- a/src/lib/nav.ts +++ b/src/lib/nav.ts @@ -1,3 +1,7 @@ export function getFolderItemId(storageId: string, folderPathname: string) { return `storage:${storageId}/folder:${folderPathname}` } + +export function getTagListItemId(storageId: string) { + return `storage:${storageId}/tags` +} From d1fca256d08f41d00b9280cc7c10ee12a2e44dbf Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:02:21 +0900 Subject: [PATCH 14/20] Enable tag list link --- .../SideNavigator/TagListFragment.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/SideNavigator/TagListFragment.tsx b/src/components/SideNavigator/TagListFragment.tsx index c26740e9cb..259fb6dd93 100644 --- a/src/components/SideNavigator/TagListFragment.tsx +++ b/src/components/SideNavigator/TagListFragment.tsx @@ -1,13 +1,10 @@ import React, { useMemo } from 'react' -import { - mdiTagMultiple, - // mdiTag, - mdiTagOutline -} from '@mdi/js' +import { mdiTagMultiple, mdiTag, mdiTagOutline } from '@mdi/js' import SideNavigatorItem from './SideNavigatorItem' import { NoteStorage } from '../../lib/db/types' import { useGeneralStatus } from '../../lib/generalStatus' import { getTagListItemId } from '../../lib/nav' +import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' interface TagListFragmentProps { storage: NoteStorage @@ -16,22 +13,30 @@ interface TagListFragmentProps { const TagListFragment = ({ storage }: TagListFragmentProps) => { const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() const { id: storageId, tagMap } = storage + const { push } = useRouter() + const currentPathname = usePathnameWithoutNoteId() const tagListNavItemId = getTagListItemId(storage.id) const tagListIsFolded = !sideNavOpenedItemSet.has(tagListNavItemId) const tagList = useMemo(() => { return Object.keys(tagMap).map(tagName => { + const tagPathname = `/app/storages/${storageId}/tags/${tagName}` + const tagIsActive = currentPathname === tagPathname return ( { + push(tagPathname) + }} + active={tagIsActive} /> ) }) - }, [storageId, tagMap]) + }, [storageId, tagMap, push, currentPathname]) return ( <> From beef1aaeb8e3cc8c7e033303fa0f333ec89bcdb4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:16:25 +0900 Subject: [PATCH 15/20] Implement context menu for tag nav item --- .../SideNavigator/TagListFragment.tsx | 35 ++++++++++++++++++- src/lib/db/store.ts | 9 ++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/SideNavigator/TagListFragment.tsx b/src/components/SideNavigator/TagListFragment.tsx index 259fb6dd93..1a702014b6 100644 --- a/src/components/SideNavigator/TagListFragment.tsx +++ b/src/components/SideNavigator/TagListFragment.tsx @@ -5,6 +5,9 @@ import { NoteStorage } from '../../lib/db/types' import { useGeneralStatus } from '../../lib/generalStatus' import { getTagListItemId } from '../../lib/nav' import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import { useDialog, DialogIconTypes } from '../../lib/dialog' +import { useDb } from '../../lib/db' interface TagListFragmentProps { storage: NoteStorage @@ -14,6 +17,9 @@ const TagListFragment = ({ storage }: TagListFragmentProps) => { const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() const { id: storageId, tagMap } = storage const { push } = useRouter() + const { popup } = useContextMenu() + const { messageBox } = useDialog() + const { removeTag } = useDb() const currentPathname = usePathnameWithoutNoteId() const tagListNavItemId = getTagListItemId(storage.id) @@ -33,10 +39,34 @@ const TagListFragment = ({ storage }: TagListFragmentProps) => { push(tagPathname) }} active={tagIsActive} + onContextMenu={event => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'Remove Tag', + onClick: () => { + messageBox({ + title: `Remove "${tagName}" tag`, + message: 'The tag will be untagged from all notes.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Folder', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeTag(storageId, tagName) + } + } + }) + } + } + ]) + }} /> ) }) - }, [storageId, tagMap, push, currentPathname]) + }, [storageId, tagMap, push, currentPathname, popup, messageBox, removeTag]) return ( <> @@ -48,6 +78,9 @@ const TagListFragment = ({ storage }: TagListFragmentProps) => { onFoldButtonClick={() => { toggleSideNavOpenedItem(tagListNavItemId) }} + onContextMenu={event => { + event.preventDefault() + }} /> {!tagListIsFolded && tagList} diff --git a/src/lib/db/store.ts b/src/lib/db/store.ts index 5b76e2fd99..bbbd6290e4 100644 --- a/src/lib/db/store.ts +++ b/src/lib/db/store.ts @@ -796,6 +796,13 @@ export function createDbStoreCreator( await storage.db.removeTag(tag) + if ( + currentPathnameWithoutNoteId === + `/app/storages/${storageId}/tags/${tag}` + ) { + router.replace(`/app/storages/${storageId}/notes`) + } + const modifiedNotes: ObjectMap = Object.keys( storageMap[storageId]!.noteMap ).reduce((acc, noteId) => { @@ -825,7 +832,7 @@ export function createDbStoreCreator( return }, - [storageMap] + [storageMap, currentPathnameWithoutNoteId, router] ) return { From 7655b4cb6732b0cf4b099afb0584c9195c20273c Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:51:34 +0900 Subject: [PATCH 16/20] Add trashcan side nav item --- .../SideNavigator/SideNavigator.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx index dc48c25f5b..17a68a7e11 100644 --- a/src/components/SideNavigator/SideNavigator.tsx +++ b/src/components/SideNavigator/SideNavigator.tsx @@ -1,9 +1,14 @@ import React, { useMemo, useCallback } from 'react' -import { useRouter } from '../../lib/router' +import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' import { useDb } from '../../lib/db' import { entries } from '../../lib/db/utils' import styled from '../../lib/styled' -import { mdiTuneVertical, mdiPlusCircleOutline } from '@mdi/js' +import { + mdiTuneVertical, + mdiPlusCircleOutline, + mdiDeleteOutline, + mdiDelete +} from '@mdi/js' import Icon from '../atoms/Icon' import { useDialog, DialogIconTypes } from '../../lib/dialog' import { useContextMenu, MenuTypes } from '../../lib/contextMenu' @@ -130,6 +135,8 @@ export default () => { openSideNavFolderItemRecursively } = useGeneralStatus() + const currentPathname = usePathnameWithoutNoteId() + return (
@@ -165,6 +172,10 @@ export default () => { } }) } + + const trashcanPathname = `/app/storages/${storage.id}/trashcan` + const trashcanIsActive = currentPathname === trashcanPathname + return ( { showPromptToCreateFolder={showPromptToCreateFolder} /> + push(trashcanPathname)} + active={trashcanIsActive} + /> )} From b10ae9bd19ade037791c7f46ad3ed01611c02032 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:51:53 +0900 Subject: [PATCH 17/20] Discard legacy storageNavItem component --- .../SideNavigator/StorageNavigatorItem.tsx | 307 ------------------ 1 file changed, 307 deletions(-) delete mode 100644 src/components/SideNavigator/StorageNavigatorItem.tsx diff --git a/src/components/SideNavigator/StorageNavigatorItem.tsx b/src/components/SideNavigator/StorageNavigatorItem.tsx deleted file mode 100644 index 12d97f3f83..0000000000 --- a/src/components/SideNavigator/StorageNavigatorItem.tsx +++ /dev/null @@ -1,307 +0,0 @@ -// import React, { useMemo, useCallback, MouseEventHandler } from 'react' -// import SideNavigatorItem, { NavigatorNode } from './SideNavigatorItem' -// import { NoteStorage, NoteDocEditibleProps, NoteDoc } from '../../lib/db/types' -// import { -// mdiTagMultiple, -// mdiDeleteOutline, -// mdiFolderOutline, -// mdiFolderOpenOutline, -// mdiTag, -// mdiTagOutline -// } from '@mdi/js' -// import { useContextMenu, MenuTypes } from '../../lib/contextMenu' -// import { useDialog, DialogIconTypes } from '../../lib/dialog' - -// interface StorageNaviagtorItemProps { -// storage: NoteStorage -// currentPathname: string -// renameStorage: (storageId: string, name: string) => Promise -// removeStorage: (storageId: string) => Promise -// createFolder: (storageId: string, folderPath: string) => Promise -// removeFolder: (storageId: string, folderPath: string) => Promise -// updateNote( -// storageId: string, -// noteId: string, -// noteProps: Partial -// ): Promise -// } - -// type FolderTree = { -// [key: string]: FolderTree -// } - -// const StorageNavigatorItem = ({ -// storage, -// currentPathname, -// renameStorage, -// removeStorage, -// createFolder, -// removeFolder -// }: StorageNaviagtorItemProps) => { -// const { prompt, messageBox } = useDialog() -// const { popup } = useContextMenu() -// const { id: storageId, name: storageName } = storage -// const openContextMenu = useCallback( -// (event: React.MouseEvent) => { -// event.preventDefault() - -// popup(event, [ -// { -// type: MenuTypes.Normal, -// label: 'New Folder', -// onClick: async () => { -// prompt({ -// title: 'Create a Folder', -// message: 'Enter the path where do you want to create a folder', -// iconType: DialogIconTypes.Question, -// defaultValue: '/', -// submitButtonLabel: 'Create Folder', -// onClose: (value: string | null) => { -// if (value == null) return -// createFolder(storageId, value) -// } -// }) -// } -// }, -// { -// type: MenuTypes.Normal, -// label: 'Rename Storage', -// onClick: async () => { -// prompt({ -// title: `Rename "${storageName}" storage`, -// message: 'Enter new name for the storage', -// iconType: DialogIconTypes.Question, -// defaultValue: storageName, -// submitButtonLabel: 'Rename Folder', -// onClose: (value: string | null) => { -// if (value == null) return -// renameStorage(storageId, value) -// } -// }) -// } -// }, -// { -// type: MenuTypes.Normal, -// label: 'Remove Storage', -// onClick: async () => { -// messageBox({ -// title: `Remove "${storageName}" storage`, -// message: 'All notes and folders will be deleted.', -// iconType: DialogIconTypes.Warning, -// buttons: ['Remove Storage', 'Cancel'], -// defaultButtonIndex: 0, -// cancelButtonIndex: 1, -// onClose: (value: number | null) => { -// if (value === 0) { -// removeStorage(storageId) -// } -// } -// }) -// } -// } -// ]) -// }, -// [ -// popup, -// prompt, -// messageBox, -// createFolder, -// storageId, -// storageName, -// renameStorage, -// removeStorage -// ] -// ) - -// const createFolderContextMenuHandler = useCallback( -// (pathname: string) => { -// return (event: React.MouseEvent) => { -// const folderIsRootFolder = pathname === '/' - -// event.preventDefault() -// popup(event, [ -// { -// type: MenuTypes.Normal, -// label: 'New Folder', -// onClick: async () => { -// prompt({ -// title: 'Create a Folder', -// message: 'Enter the path where do you want to create a folder', -// iconType: DialogIconTypes.Question, -// defaultValue: folderIsRootFolder ? '/' : `${pathname}/`, -// submitButtonLabel: 'Create Folder', -// onClose: (value: string | null) => { -// if (value == null) return -// createFolder(storageId, value) -// } -// }) -// } -// }, -// { -// type: MenuTypes.Normal, -// label: 'Remove Folder', -// enabled: !folderIsRootFolder, -// onClick: () => { -// messageBox({ -// title: `Remove "${pathname}" folder`, -// message: 'All notes and subfolders will be deleted.', -// iconType: DialogIconTypes.Warning, -// buttons: ['Remove Folder', 'Cancel'], -// defaultButtonIndex: 0, -// cancelButtonIndex: 1, -// onClose: (value: number | null) => { -// if (value === 0) { -// removeFolder(storageId, pathname) -// } -// } -// }) -// } -// } -// ]) -// } -// }, -// [popup, storageId, messageBox, prompt, createFolder, removeFolder] -// ) - -// const folderNodes = useMemo(() => { -// const folderTree = getFolderTree(Object.keys(storage.folderMap)) - -// return getNavigatorNodeFromPathnameTree( -// folderTree, -// storageId, -// '/', -// currentPathname, -// createFolderContextMenuHandler -// ) -// }, [ -// storageId, -// currentPathname, -// storage.folderMap, -// createFolderContextMenuHandler -// ]) - -// const tagNodes = useMemo(() => { -// return Object.keys(storage.tagMap).map(tagName => { -// const tagPathname = `/app/storages/${storage.id}/tags/${tagName}` -// const tagIsActive = currentPathname === tagPathname -// return { -// name: tagName, -// iconPath: tagIsActive ? mdiTag : mdiTagOutline, -// href: `/app/storages/${storage.id}/tags/${tagName}`, -// active: tagIsActive -// } -// }) -// }, [storage, currentPathname]) - -// const node = useMemo(() => { -// const storagePathname = `/app/storages/${storage.id}` -// const notesPathname = `/app/storages/${storage.id}/notes` -// const notesIsActive = currentPathname === notesPathname -// return { -// name: storage.name, -// href: storagePathname, -// active: currentPathname === storagePathname, -// onContextMenu: openContextMenu, -// children: [ -// { -// name: 'Notes', -// iconPath: notesIsActive ? mdiFolderOpenOutline : mdiFolderOutline, -// href: notesPathname, -// active: notesIsActive, -// onContextMenu: createFolderContextMenuHandler('/') -// }, -// ...folderNodes, -// { -// iconPath: mdiTagMultiple, -// name: 'Tags', -// href: `${storagePathname}/tags`, -// children: tagNodes -// }, -// { -// iconPath: mdiDeleteOutline, -// href: `${storagePathname}/trashcan`, -// name: 'Trash Can', -// active: currentPathname === `/app/storages/${storage.id}/trashcan` -// } -// ] -// } -// }, [ -// storage, -// folderNodes, -// tagNodes, -// openContextMenu, -// createFolderContextMenuHandler, -// currentPathname -// ]) - -// return -// } - -// export default StorageNavigatorItem - -// function getFolderTree(pathnames: string[]) { -// const tree = {} -// for (const pathname of pathnames) { -// if (pathname === '/') continue -// const [, ...folderNames] = pathname.split('/') -// let currentNode = tree -// for (let index = 0; index < folderNames.length; index++) { -// const currentPathname = folderNames[index] -// if (currentNode[currentPathname] == null) { -// currentNode[currentPathname] = {} -// } -// currentNode = currentNode[currentPathname] -// } -// } - -// return tree -// } - -// function getNavigatorNodeFromPathnameTree( -// tree: FolderTree, -// storageId: string, -// parentFolderPathname: string, -// currentPathname: string, -// contextMenuHandlerCreator: (pathname: string) => MouseEventHandler -// ): NavigatorNode[] { -// return Object.entries(tree).map(([folderName, tree]) => { -// const folderPathname = -// parentFolderPathname === '/' -// ? `/${folderName}` -// : `${parentFolderPathname}/${folderName}` -// const pathname = `/app/storages/${storageId}/notes${folderPathname}` -// const folderIsActive = pathname === currentPathname - -// return { -// name: folderName, -// iconPath: folderIsActive ? mdiFolderOpenOutline : mdiFolderOutline, -// href: pathname, -// active: folerIsActive, -// onContextMenu: contextMenuHandlerCreator(folderPathname), -// onDragOver: (event: React.DragEvent) => { -// event.preventDefault() -// }, -// onDrop: (event: React.DragEvent) => { -// const { storageId: targetNoteStorageId, note: targetNote } = JSON.parse( -// event.dataTransfer.getData('application/x-note-json') -// ) - -// if (storageId === targetNoteStorageId) { -// // Move note -// } else { -// // Ask copy or move -// // If move, create new one and remove original -// // If copy, just create new one -// } -// console.log(storageId, targetNote._id) -// }, -// children: getNavigatorNodeFromPathnameTree( -// tree, -// storageId, -// folderPathname, -// currentPathname, -// contextMenuHandlerCreator -// ) -// } -// }) -// } From a6c2a044d56b44c5049a6b7382638aa8378c738a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:52:55 +0900 Subject: [PATCH 18/20] Add todo comment to trashcan side nav item --- src/components/SideNavigator/SideNavigator.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx index 17a68a7e11..7d797061bb 100644 --- a/src/components/SideNavigator/SideNavigator.tsx +++ b/src/components/SideNavigator/SideNavigator.tsx @@ -205,8 +205,12 @@ export default () => { depth={1} label='Trash Can' iconPath={trashcanIsActive ? mdiDelete : mdiDeleteOutline} - onClick={() => push(trashcanPathname)} active={trashcanIsActive} + onClick={() => push(trashcanPathname)} + onContextMenu={event => { + event.preventDefault() + // TODO: Implement context menu(restore all notes) + }} /> )} From 7f0a86a6428f5fa9ba2ad2ec33eb66fa22f95fc6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 02:55:57 +0900 Subject: [PATCH 19/20] Handle dnd for root note folder --- .../SideNavigator/FolderListFragment.tsx | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/src/components/SideNavigator/FolderListFragment.tsx b/src/components/SideNavigator/FolderListFragment.tsx index 60e4500860..9ad3070c85 100644 --- a/src/components/SideNavigator/FolderListFragment.tsx +++ b/src/components/SideNavigator/FolderListFragment.tsx @@ -119,6 +119,55 @@ const FolderListFragment = ({ const rootFolderIsActive = currentPathnameWithoutNoteId === `/app/storages/${storageId}/notes` + const createDropHandler = (folderPathname: string) => { + return async (event: React.DragEvent) => { + const transferrableNoteData = getTransferrableNoteData(event) + if (transferrableNoteData == null) { + return + } + + const { + storageId: originalNoteStorageId, + note: originalNote + } = transferrableNoteData + + if (storageId === originalNoteStorageId) { + await updateNote(storageId, originalNote._id, { + folderPathname + }) + } else { + messageBox({ + title: 'Move Note to Other storage', + message: 'You are trying to move a note to different storage.', + iconType: DialogIconTypes.Info, + buttons: ['Move Note', 'Copy Note', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 2, + onClose: async (value: number | null) => { + switch (value) { + case 0: + await moveNoteToOtherStorage( + originalNoteStorageId, + originalNote._id, + storageId, + folderPathname + ) + return + case 1: + await createNote(storageId, { + title: originalNote.title, + content: originalNote.content, + folderPathname, + tags: originalNote.tags, + data: originalNote.data + }) + return + } + } + }) + } + } + } return ( <> { + event.preventDefault() + }} + onDrop={createDropHandler('/')} /> {openedFolderPathnameList.map((folderPathname: string) => { const nameElements = folderPathname.split('/').slice(1) @@ -165,54 +218,7 @@ const FolderListFragment = ({ onDragOver={event => { event.preventDefault() }} - onDrop={async event => { - const transferrableNoteData = getTransferrableNoteData(event) - if (transferrableNoteData == null) { - return - } - - const { - storageId: originalNoteStorageId, - note: originalNote - } = transferrableNoteData - - if (storageId === originalNoteStorageId) { - await updateNote(storageId, originalNote._id, { - folderPathname - }) - } else { - messageBox({ - title: 'Move Note to Other storage', - message: - 'You are trying to move a note to different storage.', - iconType: DialogIconTypes.Info, - buttons: ['Move Note', 'Copy Note', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 2, - onClose: async (value: number | null) => { - switch (value) { - case 0: - await moveNoteToOtherStorage( - originalNoteStorageId, - originalNote._id, - storageId, - folderPathname - ) - return - case 1: - await createNote(storageId, { - title: originalNote.title, - content: originalNote.content, - folderPathname, - tags: originalNote.tags, - data: originalNote.data - }) - return - } - } - }) - } - }} + onDrop={createDropHandler(folderPathname)} /> ) })} From 58cfa510c8202b6ccf3f331dbece1c52f382f210 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 9 Dec 2019 03:03:57 +0900 Subject: [PATCH 20/20] Implement context menu for storage nav item --- .../SideNavigator/SideNavigator.tsx | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx index 7d797061bb..42d7366333 100644 --- a/src/components/SideNavigator/SideNavigator.tsx +++ b/src/components/SideNavigator/SideNavigator.tsx @@ -89,14 +89,12 @@ export default () => { const { createStorage, createFolder, - // renameStorage, - // removeStorage, - // removeFolder, - // updateNote, + renameStorage, + removeStorage, storageMap } = useDb() const { popup } = useContextMenu() - const { prompt } = useDialog() + const { prompt, messageBox } = useDialog() const { push } = useRouter() const storageEntries = useMemo(() => { @@ -179,13 +177,55 @@ export default () => { return ( push(`/app/storages/${storage.id}`)} onFoldButtonClick={() => { toggleSideNavOpenedItem(itemId) }} - label={storage.name} - depth={0} + onContextMenu={event => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'Rename Storage', + onClick: async () => { + prompt({ + title: `Rename "${storage.name}" storage`, + message: 'Enter new storage name', + iconType: DialogIconTypes.Question, + defaultValue: storage.name, + submitButtonLabel: 'Rename Storage', + onClose: async (value: string | null) => { + if (value == null) return + await renameStorage(storage.id, value) + } + }) + } + }, + { + type: MenuTypes.Normal, + label: 'Remove Storage', + onClick: async () => { + messageBox({ + title: `Remove "${storage.name}" storage`, + message: + 'The storage will be unlinked from this app.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Storage', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeStorage(storage.id) + } + } + }) + } + } + ]) + }} controlComponents={[