Skip to content

Add initial drag n drop implementation (Wiki mode, local space) #889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/components/atoms/NavigatorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from '../../lib/styled'
import { textOverflow } from '../../lib/styled/styleFunctions'
import Icon from './Icon'
import { mdiChevronRight, mdiChevronDown } from '@mdi/js'
import cc from 'classcat'

const HeaderContainer = styled.header`
position: relative;
Expand Down Expand Up @@ -73,28 +74,50 @@ const ClickableContainer = styled.div`
&.subtle {
color: ${({ theme }) => theme.disabledUiTextColor};
}

&.dragged-over {
.dragged-over {
border-color: ${({ theme }) => theme.secondaryBorderColor};
}
background-color: ${({ theme }) =>
theme.secondaryButtonBackgroundColor} !important;
}
`

interface NavigatorHeaderProps {
label: string
active?: boolean
draggedOver?: boolean
control?: React.ReactNode
onContextMenu?: React.MouseEventHandler<HTMLDivElement>
folded?: boolean
onClick?: React.MouseEventHandler<HTMLDivElement>
onDrop?: (event: React.DragEvent) => void
onDragOver?: (event: React.DragEvent) => void
onDragLeave?: (event: React.DragEvent) => void
}

const NavigatorHeader = ({
folded,
label,
active = false,
draggedOver,
onContextMenu,
onClick,
onDrop,
onDragOver,
onDragLeave,
control,
}: NavigatorHeaderProps) => {
return (
<HeaderContainer onContextMenu={onContextMenu}>
<ClickableContainer onClick={onClick} className={active ? 'active' : ''}>
<ClickableContainer
onClick={onClick}
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
className={cc([active && 'active', draggedOver && 'dragged-over'])}
>
{folded != null && (
<Icon path={folded ? mdiChevronRight : mdiChevronDown} size={18} />
)}
Expand Down
23 changes: 22 additions & 1 deletion src/components/atoms/NavigatorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ const Container = styled.div`
opacity: 1;
}
}

&.dragged-over {
border-radius: 3px;
.dragged-over {
border-color: ${({ theme }) => theme.secondaryBorderColor};
}
background-color: ${({ theme }) =>
theme.secondaryButtonBackgroundColor} !important;
}
`

const FoldButton = styled.button`
Expand Down Expand Up @@ -90,7 +99,7 @@ const ClickableContainer = styled.button`
`

const Label = styled.div`
${textOverflow}
${textOverflow};
flex: 1;
font-size: 14px;

Expand Down Expand Up @@ -128,12 +137,16 @@ interface NavigatorItemProps {
active?: boolean
subtle?: boolean
alert?: boolean
draggable?: boolean
draggedOver?: boolean
onFoldButtonClick?: (event: React.MouseEvent) => void
onClick?: (event: React.MouseEvent) => void
onContextMenu?: (event: React.MouseEvent) => void
onDragStart?: (event: React.DragEvent) => void
onDrop?: (event: React.DragEvent) => void
onDragOver?: (event: React.DragEvent) => void
onDragEnd?: (event: React.DragEvent) => void
onDragLeave?: (event: React.DragEvent) => void
onDoubleClick?: (event: React.MouseEvent) => void
}

Expand All @@ -148,27 +161,35 @@ const NavigatorItem = ({
// TODO: Delete dot placeholder style
dotPlaceholder,
active,
draggable,
subtle,
alert,
onFoldButtonClick,
onClick,
onDoubleClick,
onContextMenu,
onDragStart,
onDrop,
onDragOver,
onDragEnd,
onDragLeave,
draggedOver,
}: NavigatorItemProps) => {
return (
<Container
className={cc([
className,
active && 'active',
visibleControl && 'visibleControl',
draggedOver && 'dragged-over',
])}
onContextMenu={onContextMenu}
onDragStart={onDragStart}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnd={onDragEnd}
onDragLeave={onDragLeave}
draggable={draggable}
>
{!dotPlaceholder && folded != null && (
<FoldButton
Expand Down
108 changes: 99 additions & 9 deletions src/components/molecules/FolderNavigatorItem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React, { useCallback, useMemo, MouseEvent } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { useDb } from '../../lib/db'
import { useDialog, DialogIconTypes } from '../../lib/dialog'
import NavigatorItem from '../atoms/NavigatorItem'
import { useGeneralStatus } from '../../lib/generalStatus'
import { getFolderItemId } from '../../lib/nav'
import { getTransferrableNoteData } from '../../lib/dnd'
import {
folderPathnameFormat,
getTransferableTextData,
getTransferrableNoteData,
noteIdFormat,
setTransferableTextData,
} from '../../lib/dnd'
import { useTranslation } from 'react-i18next'
import {
mdiFolderOpenOutline,
Expand All @@ -16,6 +22,7 @@ import {
import NavigatorButton from '../atoms/NavigatorButton'
import { useRouter } from '../../lib/router'
import { openContextMenu } from '../../lib/electronOnly'
import { useToast } from '../../lib/toast'

interface FolderNavigatorItemProps {
active: boolean
Expand All @@ -42,15 +49,22 @@ const FolderNavigatorItem = ({
showPromptToCreateFolder,
showPromptToRenameFolder,
}: FolderNavigatorItemProps) => {
const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus()
const [draggedOver, setDraggedOver] = useState<boolean>(false)
const {
toggleSideNavOpenedItem,
sideNavOpenedItemSet,
openSideNavFolderItemRecursively,
} = useGeneralStatus()
const { push } = useRouter()
const { messageBox } = useDialog()
const { t } = useTranslation()
const { pushMessage } = useToast()
const {
createNote,
updateNote,
removeFolder,
moveNoteToOtherStorage,
renameFolder,
} = useDb()

const itemId = useMemo(() => {
Expand Down Expand Up @@ -179,6 +193,7 @@ const FolderNavigatorItem = ({
const handleDrop = async (event: React.DragEvent) => {
const transferrableNoteData = getTransferrableNoteData(event)
if (transferrableNoteData == null) {
handleDropNavigatorItem(event)
return
}

Expand Down Expand Up @@ -224,6 +239,81 @@ const FolderNavigatorItem = ({
}
}

const handleDragOverNavigatorItem = useCallback((e) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
setDraggedOver(true)
}, [])

const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
console.log('Drag is over!')
setDraggedOver(false)
}

const handleDragStartNavigatorItem = (event: React.DragEvent) => {
setTransferableTextData(event, folderPathnameFormat, folderPathname)
}

const handleDropFolderToNavigatorItem = (
sourceItemPathname: string,
destinationFolderPathname: string
) => {
const sourceFolderName = sourceItemPathname.substring(
sourceItemPathname.lastIndexOf('/')
)
const newFolderPathname = `${destinationFolderPathname}${sourceFolderName}`
renameFolder(
storageId,
sourceItemPathname == '' ? '/' : sourceItemPathname,
newFolderPathname
)
.then(() => {
// refresh works for file system (FSNote) database
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno why, looked over code from Pouch and FSNote, seems similar, update is done on storage level,
But notes are not seen after dragging the folder into another place (it refreshes the folder location but sub-folders and notes disappear from view in Pouch DB. After Ctrl+R - reload, they are correctly placed in the new destination place.

push(`/app/storages/${storageId}/notes${newFolderPathname}`)
openSideNavFolderItemRecursively(storageId, newFolderPathname)
})
.catch((err) => {
pushMessage({
title: 'Folder Structure Update Failed',
description: `Updating folder location failed. Reason: ${
err != null && err.message != null ? err.message : 'Unknown'
}`,
})
})
}
const handleDropNoteToNavigatorItem = (
noteId: string,
destinationFolderPathname: string
) => {
// move folder pathname of the note to new location (target destination)
updateNote(storageId, noteId, {
folderPathname: destinationFolderPathname,
})
}

const handleDropNavigatorItem = (event: React.DragEvent) => {
Copy link
Contributor Author

@Komediruzecki Komediruzecki Mar 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code repeats in 3 places, but not sure how to refactor - should I send updateNote and renameFolder functions to a utility function, and then it can handle all details for all three usages (on note/folder/workspace drop)?

Another thing that comes to mind is to wrap the NavigatorItem component and use that everywhere. And in the wrapper provide all drag-drop functionality if parent enables it, in that way logic would be in Wrapper and those items folder/note/workspace can use the wrapper to add NavigatorItem with drag and drop?

event.preventDefault()
event.stopPropagation()
setDraggedOver(false)

const sourceFolderPathname = getTransferableTextData(
event,
folderPathnameFormat
)
if (sourceFolderPathname != null) {
handleDropFolderToNavigatorItem(sourceFolderPathname, folderPathname)
} else {
const noteId = getTransferableTextData(event, noteIdFormat)
if (noteId == null) {
return
}
handleDropNoteToNavigatorItem(noteId, folderPathname)
}
}

return (
<NavigatorItem
folded={folded}
Expand Down Expand Up @@ -253,14 +343,14 @@ const FolderNavigatorItem = ({
/>
</>
}
onDragOver={preventDefault}
onDrop={handleDrop}
draggable={true}
draggedOver={draggedOver}
onDragStart={handleDragStartNavigatorItem}
onDragOver={handleDragOverNavigatorItem}
onDrop={(e) => handleDrop(e)}
onDragLeave={(e) => handleDragLeave(e)}
/>
)
}

function preventDefault(event: MouseEvent) {
event.preventDefault()
}

export default FolderNavigatorItem
15 changes: 15 additions & 0 deletions src/components/molecules/FolderNoteNavigatorFragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
NoteDoc,
PopulatedFolderDoc,
ObjectMap,
NoteDocEditibleProps,
} from '../../lib/db/types'
import { useRouteParams, StorageNotesRouteParams } from '../../lib/routeParams'
import { useGeneralStatus } from '../../lib/generalStatus'
Expand All @@ -25,6 +26,16 @@ interface FolderNoteNavigatorFragment {
noteId: string
) => Promise<NoteDoc | undefined>
trashNote: (storageId: string, noteId: string) => Promise<NoteDoc | undefined>
updateNote(
storageId: string,
noteId: string,
noteProps: Partial<NoteDocEditibleProps>
): Promise<NoteDoc | undefined>
renameFolder: (
storageName: string,
pathname: string,
newName: string
) => Promise<void>
}

const FolderNoteNavigatorFragment = ({
Expand All @@ -35,6 +46,8 @@ const FolderNoteNavigatorFragment = ({
bookmarkNote,
unbookmarkNote,
trashNote,
updateNote,
renameFolder,
}: FolderNoteNavigatorFragment) => {
const { folderMap, id: storageId } = storage

Expand Down Expand Up @@ -145,6 +158,8 @@ const FolderNoteNavigatorFragment = ({
bookmarkNote={bookmarkNote}
unbookmarkNote={unbookmarkNote}
trashNote={trashNote}
renameFolder={renameFolder}
updateNote={updateNote}
/>
)
})}
Expand Down
Loading