2020-09-15 15:01:07 +02:00
import * as React from 'react' ;
2022-09-05 18:26:22 +02:00
import { useEffect , useRef , useCallback , useMemo } from 'react' ;
2023-05-29 12:20:11 +02:00
import styled , { css } from 'styled-components' ;
2023-03-10 14:01:35 +02:00
import shim from '@joplin/lib/shim' ;
2021-05-13 18:57:37 +02:00
import { StyledRoot , StyledAddButton , StyledShareIcon , StyledHeader , StyledHeaderIcon , StyledAllNotesIcon , StyledHeaderLabel , StyledListItem , StyledListItemAnchor , StyledExpandLink , StyledNoteCount , StyledSyncReportText , StyledSyncReport , StyledSynchronizeButton } from './styles' ;
2020-09-15 15:01:07 +02:00
import { ButtonLevel } from '../Button/Button' ;
2020-11-07 17:59:37 +02:00
import CommandService from '@joplin/lib/services/CommandService' ;
import InteropService from '@joplin/lib/services/interop/InteropService' ;
import Synchronizer from '@joplin/lib/Synchronizer' ;
import Setting from '@joplin/lib/models/Setting' ;
import MenuUtils from '@joplin/lib/services/commands/MenuUtils' ;
2020-10-09 19:35:46 +02:00
import InteropServiceHelper from '../../InteropServiceHelper' ;
2020-11-07 17:59:37 +02:00
import { _ } from '@joplin/lib/locale' ;
2020-12-11 15:28:59 +02:00
import { PluginStates , utils as pluginUtils } from '@joplin/lib/services/plugins/reducer' ;
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types' ;
2021-09-04 19:11:29 +02:00
import { AppState } from '../../app.reducer' ;
2020-12-11 15:28:59 +02:00
import { ModelType } from '@joplin/lib/BaseModel' ;
2021-01-22 19:41:11 +02:00
import BaseModel from '@joplin/lib/BaseModel' ;
import Folder from '@joplin/lib/models/Folder' ;
import Note from '@joplin/lib/models/Note' ;
import Tag from '@joplin/lib/models/Tag' ;
2021-05-13 18:57:37 +02:00
import Logger from '@joplin/lib/Logger' ;
2022-09-05 18:26:22 +02:00
import { FolderEntity , FolderIcon , FolderIconType } from '@joplin/lib/services/database/types' ;
2021-05-13 18:57:37 +02:00
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext' ;
import { store } from '@joplin/lib/reducer' ;
2021-11-11 17:33:37 +02:00
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService' ;
2021-10-16 11:07:41 +02:00
import { getFolderCallbackUrl , getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils' ;
2022-02-06 18:42:00 +02:00
import FolderIconBox from '../FolderIconBox' ;
2022-09-05 17:21:26 +02:00
import { Theme } from '@joplin/lib/themes/type' ;
import { RuntimeProps } from './commands/focusElementSideBar' ;
2021-05-13 18:57:37 +02:00
const { connect } = require ( 'react-redux' ) ;
const shared = require ( '@joplin/lib/components/shared/side-menu-shared.js' ) ;
2020-11-07 17:59:37 +02:00
const { themeStyle } = require ( '@joplin/lib/theme' ) ;
2021-10-01 20:35:27 +02:00
const bridge = require ( '@electron/remote' ) . require ( './bridge' ) . default ;
2020-09-15 15:01:07 +02:00
const Menu = bridge ( ) . Menu ;
const MenuItem = bridge ( ) . MenuItem ;
2020-11-07 17:59:37 +02:00
const { substrWithEllipsis } = require ( '@joplin/lib/string-utils' ) ;
const { ALL_NOTES_FILTER_ID } = require ( '@joplin/lib/reserved-ids' ) ;
2021-10-16 11:07:41 +02:00
const { clipboard } = require ( 'electron' ) ;
2020-09-15 15:01:07 +02:00
2021-05-13 18:57:37 +02:00
const logger = Logger . create ( 'Sidebar' ) ;
2023-05-29 12:20:11 +02:00
// Workaround sidebar rendering bug on Linux Intel GPU.
// https://github.com/laurent22/joplin/issues/7506
const StyledSpanFix = styled . span `
$ { shim . isLinux ( ) && css `
position : relative ;
` }
2023-03-31 23:13:59 +02:00
` ;
2023-03-10 14:01:35 +02:00
2023-05-29 12:20:11 +02:00
2020-09-15 15:01:07 +02:00
interface Props {
2020-11-12 21:29:22 +02:00
themeId : number ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
dispatch : Function ;
folders : any [ ] ;
collapsedFolderIds : string [ ] ;
notesParentType : string ;
selectedFolderId : string ;
selectedTagId : string ;
selectedSmartFilterId : string ;
decryptionWorker : any ;
resourceFetcher : any ;
syncReport : any ;
tags : any [ ] ;
syncStarted : boolean ;
2020-12-11 15:28:59 +02:00
plugins : PluginStates ;
2020-11-12 21:29:22 +02:00
folderHeaderIsExpanded : boolean ;
2022-09-05 17:21:26 +02:00
tagHeaderIsExpanded : boolean ;
2020-09-15 15:01:07 +02:00
}
const commands = [
require ( './commands/focusElementSideBar' ) ,
] ;
2020-11-12 21:13:28 +02:00
function ExpandIcon ( props : any ) {
2020-09-24 15:30:20 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-11-12 21:13:28 +02:00
const style : any = { width : 16 , maxWidth : 16 , opacity : 0.5 , fontSize : Math.round ( theme . toolbarIconSize * 0.8 ) , display : 'flex' , justifyContent : 'center' } ;
2020-09-24 15:30:20 +02:00
if ( ! props . isVisible ) style . visibility = 'hidden' ;
return < i className = { props . isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right' } style = { style } > < / i > ;
}
2020-11-12 21:13:28 +02:00
function ExpandLink ( props : any ) {
2020-09-24 15:30:20 +02:00
return props . hasChildren ? (
2020-09-29 15:26:05 +02:00
< StyledExpandLink href = "#" data - folder - id = { props . folderId } onClick = { props . onClick } >
2020-09-24 15:30:20 +02:00
< ExpandIcon themeId = { props . themeId } isVisible = { true } isExpanded = { props . isExpanded } / >
< / StyledExpandLink >
) : (
< StyledExpandLink > < ExpandIcon themeId = { props . themeId } isVisible = { false } isExpanded = { false } / > < / StyledExpandLink >
) ;
}
2022-02-06 18:42:00 +02:00
const renderFolderIcon = ( folderIcon : FolderIcon ) = > {
2022-09-05 18:26:22 +02:00
if ( ! folderIcon ) {
const defaultFolderIcon : FolderIcon = {
dataUrl : '' ,
emoji : '' ,
name : 'far fa-folder' ,
type : FolderIconType . FontAwesome ,
} ;
2022-10-11 12:20:47 +02:00
return < div style = { { marginRight : 7 , display : 'flex' } } > < FolderIconBox opacity = { 0.7 } folderIcon = { defaultFolderIcon } / > < / div > ;
2022-09-05 18:26:22 +02:00
}
2022-02-06 18:42:00 +02:00
2022-10-11 12:20:47 +02:00
return < div style = { { marginRight : 7 , display : 'flex' } } > < FolderIconBox folderIcon = { folderIcon } / > < / div > ;
2022-02-06 18:42:00 +02:00
} ;
2020-11-12 21:13:28 +02:00
function FolderItem ( props : any ) {
2022-09-05 18:26:22 +02:00
const { hasChildren , showFolderIcon , isExpanded , parentId , depth , selected , folderId , folderTitle , folderIcon , anchorRef , noteCount , onFolderDragStart_ , onFolderDragOver_ , onFolderDrop_ , itemContextMenu , folderItem_click , onFolderToggleClick_ , shareId } = props ;
2020-09-24 15:30:20 +02:00
2021-05-17 20:33:44 +02:00
const noteCountComp = noteCount ? < StyledNoteCount className = "note-count-label" > { noteCount } < / StyledNoteCount > : null ;
2020-09-29 12:49:51 +02:00
2021-05-13 18:57:37 +02:00
const shareIcon = shareId && ! parentId ? < StyledShareIcon className = "fas fa-share-alt" > < / StyledShareIcon > : null ;
2020-09-24 15:30:20 +02:00
return (
2021-05-17 20:33:44 +02:00
< StyledListItem depth = { depth } selected = { selected } className = { ` list-item-container list-item-depth- ${ depth } ${ selected ? 'selected' : '' } ` } onDragStart = { onFolderDragStart_ } onDragOver = { onFolderDragOver_ } onDrop = { onFolderDrop_ } draggable = { true } data - folder - id = { folderId } >
2020-09-24 15:30:20 +02:00
< ExpandLink themeId = { props . themeId } hasChildren = { hasChildren } folderId = { folderId } onClick = { onFolderToggleClick_ } isExpanded = { isExpanded } / >
< StyledListItemAnchor
ref = { anchorRef }
className = "list-item"
isConflictFolder = { folderId === Folder . conflictFolderId ( ) }
href = "#"
selected = { selected }
2021-05-13 18:57:37 +02:00
shareId = { shareId }
2020-09-24 15:30:20 +02:00
data - id = { folderId }
data - type = { BaseModel . TYPE_FOLDER }
onContextMenu = { itemContextMenu }
data - folder - id = { folderId }
onClick = { ( ) = > {
folderItem_click ( folderId ) ;
} }
onDoubleClick = { onFolderToggleClick_ }
>
2023-05-29 12:20:11 +02:00
{ showFolderIcon ? renderFolderIcon ( folderIcon ) : null } < StyledSpanFix className = "title" style = { { lineHeight : 0 } } > { folderTitle } < / StyledSpanFix >
2021-05-17 20:33:44 +02:00
{ shareIcon } { noteCountComp }
2020-09-24 15:30:20 +02:00
< / StyledListItemAnchor >
< / StyledListItem >
) ;
}
2020-10-09 19:35:46 +02:00
const menuUtils = new MenuUtils ( CommandService . instance ( ) ) ;
2022-09-05 17:21:26 +02:00
const SidebarComponent = ( props : Props ) = > {
const folderItemsOrder_ = useRef < any [ ] > ( ) ;
folderItemsOrder_ . current = [ ] ;
const tagItemsOrder_ = useRef < any [ ] > ( ) ;
tagItemsOrder_ . current = [ ] ;
const rootRef = useRef ( null ) ;
2023-01-10 20:32:06 +02:00
const anchorItemRefs = useRef < Record < string , any > > ( { } ) ;
2022-09-05 17:21:26 +02:00
// This whole component is a bit of a mess and rather than passing
// a plugins prop around, not knowing how it's going to affect
// re-rendering, we just keep a ref to it. Currently that's enough
// as plugins are only accessed from context menus. However if want
// to do more complex things with plugins in the sidebar, it will
// probably have to be refactored using React Hooks first.
const pluginsRef = useRef < PluginStates > ( null ) ;
pluginsRef . current = props . plugins ;
2022-09-05 18:26:22 +02:00
// If at least one of the folder has an icon, then we display icons for all
// folders (those without one will get the default icon). This is so that
// visual alignment is correct for all folders, otherwise the folder tree
// looks messy.
const showFolderIcons = useMemo ( ( ) = > {
2022-10-11 13:46:40 +02:00
return Folder . shouldShowFolderIcons ( props . folders ) ;
2022-09-05 18:26:22 +02:00
} , [ props . folders ] ) ;
2022-09-05 17:21:26 +02:00
const getSelectedItem = useCallback ( ( ) = > {
if ( props . notesParentType === 'Folder' && props . selectedFolderId ) {
return { type : 'folder' , id : props.selectedFolderId } ;
} else if ( props . notesParentType === 'Tag' && props . selectedTagId ) {
return { type : 'tag' , id : props.selectedTagId } ;
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
return null ;
} , [ props . notesParentType , props . selectedFolderId , props . selectedTagId ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const getFirstAnchorItemRef = useCallback ( ( type : string ) = > {
const refs = anchorItemRefs . current [ type ] ;
if ( ! refs ) return null ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const p = type === 'folder' ? props.folders : props.tags ;
const item = p && p . length ? p [ 0 ] : null ;
if ( ! item ) return null ;
return refs [ item . id ] ;
} , [ anchorItemRefs , props . folders , props . tags ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
useEffect ( ( ) = > {
const runtimeProps : RuntimeProps = {
getSelectedItem ,
anchorItemRefs ,
getFirstAnchorItemRef ,
2020-09-15 15:01:07 +02:00
} ;
2022-09-05 17:21:26 +02:00
CommandService . instance ( ) . componentRegisterCommands ( runtimeProps , commands ) ;
return ( ) = > {
CommandService . instance ( ) . componentUnregisterCommands ( commands ) ;
} ;
} , [
getSelectedItem ,
anchorItemRefs ,
getFirstAnchorItemRef ,
] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onFolderDragStart_ = useCallback ( ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const folderId = event . currentTarget . getAttribute ( 'data-folder-id' ) ;
if ( ! folderId ) return ;
event . dataTransfer . setDragImage ( new Image ( ) , 1 , 1 ) ;
event . dataTransfer . clearData ( ) ;
event . dataTransfer . setData ( 'text/x-jop-folder-ids' , JSON . stringify ( [ folderId ] ) ) ;
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onFolderDragOver_ = useCallback ( ( event : any ) = > {
2020-09-15 15:01:07 +02:00
if ( event . dataTransfer . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) event . preventDefault ( ) ;
if ( event . dataTransfer . types . indexOf ( 'text/x-jop-folder-ids' ) >= 0 ) event . preventDefault ( ) ;
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onFolderDrop_ = useCallback ( async ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const folderId = event . currentTarget . getAttribute ( 'data-folder-id' ) ;
const dt = event . dataTransfer ;
if ( ! dt ) return ;
// folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used
// to put the dropped folder at the root. But for notes, folderId needs to always be defined
// since there's no such thing as a root note.
2021-05-13 18:57:37 +02:00
try {
if ( dt . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) {
event . preventDefault ( ) ;
2020-09-15 15:01:07 +02:00
2021-05-13 18:57:37 +02:00
if ( ! folderId ) return ;
2020-09-15 15:01:07 +02:00
2021-05-13 18:57:37 +02:00
const noteIds = JSON . parse ( dt . getData ( 'text/x-jop-note-ids' ) ) ;
for ( let i = 0 ; i < noteIds . length ; i ++ ) {
await Note . moveToFolder ( noteIds [ i ] , folderId ) ;
}
} else if ( dt . types . indexOf ( 'text/x-jop-folder-ids' ) >= 0 ) {
event . preventDefault ( ) ;
2020-09-15 15:01:07 +02:00
2021-05-13 18:57:37 +02:00
const folderIds = JSON . parse ( dt . getData ( 'text/x-jop-folder-ids' ) ) ;
for ( let i = 0 ; i < folderIds . length ; i ++ ) {
await Folder . moveToFolder ( folderIds [ i ] , folderId ) ;
}
2020-09-15 15:01:07 +02:00
}
2021-05-13 18:57:37 +02:00
} catch ( error ) {
logger . error ( error ) ;
alert ( error . message ) ;
2020-09-15 15:01:07 +02:00
}
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onTagDrop_ = useCallback ( async ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const tagId = event . currentTarget . getAttribute ( 'data-tag-id' ) ;
const dt = event . dataTransfer ;
if ( ! dt ) return ;
if ( dt . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) {
event . preventDefault ( ) ;
const noteIds = JSON . parse ( dt . getData ( 'text/x-jop-note-ids' ) ) ;
for ( let i = 0 ; i < noteIds . length ; i ++ ) {
await Tag . addNote ( tagId , noteIds [ i ] ) ;
}
}
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onFolderToggleClick_ = useCallback ( ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const folderId = event . currentTarget . getAttribute ( 'data-folder-id' ) ;
2022-09-05 17:21:26 +02:00
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : 'FOLDER_TOGGLE' ,
id : folderId ,
} ) ;
2022-09-05 17:21:26 +02:00
} , [ props . dispatch ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const header_contextMenu = useCallback ( async ( ) = > {
2020-09-15 15:01:07 +02:00
const menu = new Menu ( ) ;
menu . append (
2020-10-09 19:35:46 +02:00
new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'newFolder' ) )
2020-09-15 15:01:07 +02:00
) ;
2023-02-22 20:15:21 +02:00
menu . popup ( { window : bridge ( ) . window ( ) } ) ;
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const itemContextMenu = useCallback ( async ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const itemId = event . currentTarget . getAttribute ( 'data-id' ) ;
if ( itemId === Folder . conflictFolderId ( ) ) return ;
const itemType = Number ( event . currentTarget . getAttribute ( 'data-type' ) ) ;
if ( ! itemId || ! itemType ) throw new Error ( 'No data on element' ) ;
2021-05-13 18:57:37 +02:00
const state : AppState = store ( ) . getState ( ) ;
2020-09-15 15:01:07 +02:00
let deleteMessage = '' ;
2021-05-13 18:57:37 +02:00
let deleteButtonLabel = _ ( 'Remove' ) ;
2020-09-15 15:01:07 +02:00
if ( itemType === BaseModel . TYPE_FOLDER ) {
const folder = await Folder . load ( itemId ) ;
deleteMessage = _ ( 'Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.' , substrWithEllipsis ( folder . title , 0 , 32 ) ) ;
2021-05-13 18:57:37 +02:00
deleteButtonLabel = _ ( 'Delete' ) ;
2020-09-15 15:01:07 +02:00
} else if ( itemType === BaseModel . TYPE_TAG ) {
const tag = await Tag . load ( itemId ) ;
deleteMessage = _ ( 'Remove tag "%s" from all notes?' , substrWithEllipsis ( tag . title , 0 , 32 ) ) ;
} else if ( itemType === BaseModel . TYPE_SEARCH ) {
deleteMessage = _ ( 'Remove this search from the sidebar?' ) ;
}
const menu = new Menu ( ) ;
let item = null ;
if ( itemType === BaseModel . TYPE_FOLDER ) {
2022-09-05 17:21:26 +02:00
item = BaseModel . byId ( props . folders , itemId ) ;
2020-09-15 15:01:07 +02:00
}
if ( itemType === BaseModel . TYPE_FOLDER && ! item . encryption_applied ) {
menu . append (
2020-10-18 22:52:10 +02:00
new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'newFolder' , itemId ) )
2020-09-15 15:01:07 +02:00
) ;
}
menu . append (
new MenuItem ( {
2021-05-13 18:57:37 +02:00
label : deleteButtonLabel ,
2020-09-15 15:01:07 +02:00
click : async ( ) = > {
const ok = bridge ( ) . showConfirmMessageBox ( deleteMessage , {
2021-05-13 18:57:37 +02:00
buttons : [ deleteButtonLabel , _ ( 'Cancel' ) ] ,
2020-09-15 15:01:07 +02:00
defaultId : 1 ,
} ) ;
if ( ! ok ) return ;
if ( itemType === BaseModel . TYPE_FOLDER ) {
await Folder . delete ( itemId ) ;
} else if ( itemType === BaseModel . TYPE_TAG ) {
await Tag . untagAll ( itemId ) ;
} else if ( itemType === BaseModel . TYPE_SEARCH ) {
2022-09-05 17:21:26 +02:00
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : 'SEARCH_DELETE' ,
id : itemId ,
} ) ;
}
} ,
} )
) ;
if ( itemType === BaseModel . TYPE_FOLDER && ! item . encryption_applied ) {
2021-12-31 08:26:06 +02:00
menu . append ( new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'openFolderDialog' , { folderId : itemId } ) ) ) ;
2020-09-15 15:01:07 +02:00
menu . append ( new MenuItem ( { type : 'separator' } ) ) ;
const exportMenu = new Menu ( ) ;
2020-10-09 19:35:46 +02:00
const ioService = InteropService . instance ( ) ;
2020-09-15 15:01:07 +02:00
const ioModules = ioService . module s ( ) ;
for ( let i = 0 ; i < ioModules . length ; i ++ ) {
const module = ioModules [ i ] ;
if ( module .type !== 'exporter' ) continue ;
exportMenu . append (
new MenuItem ( {
label : module.fullLabel ( ) ,
click : async ( ) = > {
2022-09-05 17:21:26 +02:00
await InteropServiceHelper . export ( props . dispatch , module , { sourceFolderIds : [ itemId ] , plugins : pluginsRef.current } ) ;
2020-09-15 15:01:07 +02:00
} ,
} )
) ;
}
2021-05-13 18:57:37 +02:00
// We don't display the "Share notebook" menu item for sub-notebooks
// that are within a shared notebook. If user wants to do this,
// they'd have to move the notebook out of the shared notebook
// first.
2021-10-13 19:02:54 +02:00
const whenClause = stateToWhenClauseContext ( state , { commandFolderId : itemId } ) ;
if ( CommandService . instance ( ) . isEnabled ( 'showShareFolderDialog' , whenClause ) ) {
2021-05-13 18:57:37 +02:00
menu . append ( new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'showShareFolderDialog' , itemId ) ) ) ;
}
2021-10-13 19:02:54 +02:00
if ( CommandService . instance ( ) . isEnabled ( 'leaveSharedFolder' , whenClause ) ) {
menu . append ( new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'leaveSharedFolder' , itemId ) ) ) ;
}
2020-09-15 15:01:07 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Export' ) ,
submenu : exportMenu ,
} )
) ;
2021-11-11 17:33:37 +02:00
if ( Setting . value ( 'notes.perFolderSortOrderEnabled' ) ) {
menu . append ( new MenuItem ( {
. . . menuUtils . commandToStatefulMenuItem ( 'togglePerFolderSortOrder' , itemId ) ,
type : 'checkbox' ,
checked : PerFolderSortOrderService.isSet ( itemId ) ,
} ) ) ;
}
2020-09-15 15:01:07 +02:00
}
2021-10-16 11:07:41 +02:00
if ( itemType === BaseModel . TYPE_FOLDER ) {
menu . append (
new MenuItem ( {
label : _ ( 'Copy external link' ) ,
click : ( ) = > {
clipboard . writeText ( getFolderCallbackUrl ( itemId ) ) ;
} ,
} )
) ;
}
2020-09-15 15:01:07 +02:00
if ( itemType === BaseModel . TYPE_TAG ) {
menu . append ( new MenuItem (
2020-10-18 22:52:10 +02:00
menuUtils . commandToStatefulMenuItem ( 'renameTag' , itemId )
2020-09-15 15:01:07 +02:00
) ) ;
2021-10-16 11:07:41 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Copy external link' ) ,
click : ( ) = > {
clipboard . writeText ( getTagCallbackUrl ( itemId ) ) ;
} ,
} )
) ;
2020-09-15 15:01:07 +02:00
}
2022-09-05 17:21:26 +02:00
const pluginViews = pluginUtils . viewsByType ( pluginsRef . current , 'menuItem' ) ;
2020-12-11 15:28:59 +02:00
for ( const view of pluginViews ) {
const location = view . location ;
if ( itemType === ModelType . Tag && location === MenuItemLocation . TagContextMenu ||
itemType === ModelType . Folder && location === MenuItemLocation . FolderContextMenu
) {
menu . append (
new MenuItem ( menuUtils . commandToStatefulMenuItem ( view . commandName , itemId ) )
) ;
}
}
2023-02-22 20:15:21 +02:00
menu . popup ( { window : bridge ( ) . window ( ) } ) ;
2022-09-05 17:21:26 +02:00
} , [ props . folders , props . dispatch , pluginsRef ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const folderItem_click = useCallback ( ( folderId : string ) = > {
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : 'FOLDER_SELECT' ,
2020-09-24 15:30:20 +02:00
id : folderId ? folderId : null ,
2020-09-15 15:01:07 +02:00
} ) ;
2022-09-05 17:21:26 +02:00
} , [ props . dispatch ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const tagItem_click = useCallback ( ( tag : any ) = > {
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : 'TAG_SELECT' ,
id : tag ? tag.id : null ,
} ) ;
2022-09-05 17:21:26 +02:00
} , [ props . dispatch ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onHeaderClick_ = useCallback ( ( key : string ) = > {
2022-10-26 16:54:18 +02:00
const isExpanded = key === 'tagHeader' ? props.tagHeaderIsExpanded : props.folderHeaderIsExpanded ;
Setting . setValue ( key === 'tagHeader' ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded' , ! isExpanded ) ;
2022-09-05 17:21:26 +02:00
} , [ props . folderHeaderIsExpanded , props . tagHeaderIsExpanded ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onAllNotesClick_ = ( ) = > {
props . dispatch ( {
type : 'SMART_FILTER_SELECT' ,
id : ALL_NOTES_FILTER_ID ,
} ) ;
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const anchorItemRef = ( type : string , id : string ) = > {
if ( ! anchorItemRefs . current [ type ] ) anchorItemRefs . current [ type ] = { } ;
if ( anchorItemRefs . current [ type ] [ id ] ) return anchorItemRefs . current [ type ] [ id ] ;
anchorItemRefs . current [ type ] [ id ] = React . createRef ( ) ;
return anchorItemRefs . current [ type ] [ id ] ;
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderNoteCount = ( count : number ) = > {
2021-05-17 20:33:44 +02:00
return count ? < StyledNoteCount className = "note-count-label" > { count } < / StyledNoteCount > : null ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderExpandIcon = ( theme : any , isExpanded : boolean , isVisible : boolean ) = > {
2020-11-12 21:13:28 +02:00
const style : any = { width : 16 , maxWidth : 16 , opacity : 0.5 , fontSize : Math.round ( theme . toolbarIconSize * 0.8 ) , display : 'flex' , justifyContent : 'center' } ;
2020-09-15 15:01:07 +02:00
if ( ! isVisible ) style . visibility = 'hidden' ;
return < i className = { isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right' } style = { style } > < / i > ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderAllNotesItem = ( theme : Theme , selected : boolean ) = > {
2020-09-15 15:01:07 +02:00
return (
2021-05-17 20:33:44 +02:00
< StyledListItem key = "allNotesHeader" selected = { selected } className = { 'list-item-container list-item-depth-0 all-notes' } isSpecialItem = { true } >
2022-09-05 17:21:26 +02:00
< StyledExpandLink > { renderExpandIcon ( theme , false , false ) } < / StyledExpandLink >
2020-09-29 12:49:51 +02:00
< StyledAllNotesIcon className = "icon-notes" / >
2020-09-15 15:01:07 +02:00
< StyledListItemAnchor
className = "list-item"
isSpecialItem = { true }
href = "#"
selected = { selected }
2022-09-05 17:21:26 +02:00
onClick = { onAllNotesClick_ }
2020-09-15 15:01:07 +02:00
>
2020-09-29 12:49:51 +02:00
{ _ ( 'All notes' ) }
2020-09-15 15:01:07 +02:00
< / StyledListItemAnchor >
< / StyledListItem >
) ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderFolderItem = ( folder : FolderEntity , selected : boolean , hasChildren : boolean , depth : number ) = > {
const anchorRef = anchorItemRef ( 'folder' , folder . id ) ;
const isExpanded = props . collapsedFolderIds . indexOf ( folder . id ) < 0 ;
2021-05-13 18:57:37 +02:00
let noteCount = ( folder as any ) . note_count ;
2020-10-12 11:13:41 +02:00
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
if ( isExpanded ) {
2022-09-05 17:21:26 +02:00
for ( let i = 0 ; i < props . folders . length ; i ++ ) {
if ( props . folders [ i ] . parent_id === folder . id ) {
noteCount -= props . folders [ i ] . note_count ;
2020-10-12 11:13:41 +02:00
}
}
}
2020-09-15 15:01:07 +02:00
2020-09-24 15:30:20 +02:00
return < FolderItem
key = { folder . id }
folderId = { folder . id }
folderTitle = { Folder . displayTitle ( folder ) }
2021-11-15 19:19:51 +02:00
folderIcon = { Folder . unserializeIcon ( folder . icon ) }
2022-09-05 17:21:26 +02:00
themeId = { props . themeId }
2020-09-24 15:30:20 +02:00
depth = { depth }
selected = { selected }
2020-10-12 11:13:41 +02:00
isExpanded = { isExpanded }
2020-09-24 15:30:20 +02:00
hasChildren = { hasChildren }
anchorRef = { anchorRef }
2020-10-12 11:13:41 +02:00
noteCount = { noteCount }
2022-09-05 17:21:26 +02:00
onFolderDragStart_ = { onFolderDragStart_ }
onFolderDragOver_ = { onFolderDragOver_ }
onFolderDrop_ = { onFolderDrop_ }
itemContextMenu = { itemContextMenu }
folderItem_click = { folderItem_click }
onFolderToggleClick_ = { onFolderToggleClick_ }
2021-05-13 18:57:37 +02:00
shareId = { folder . share_id }
parentId = { folder . parent_id }
2022-09-05 18:26:22 +02:00
showFolderIcon = { showFolderIcons }
2020-09-24 15:30:20 +02:00
/ > ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderTag = ( tag : any , selected : boolean ) = > {
const anchorRef = anchorItemRef ( 'tag' , tag . id ) ;
2021-05-27 13:44:58 +02:00
let noteCount = null ;
if ( Setting . value ( 'showNoteCounts' ) ) {
2022-09-05 17:21:26 +02:00
if ( Setting . value ( 'showCompletedTodos' ) ) noteCount = renderNoteCount ( tag . note_count ) ;
else noteCount = renderNoteCount ( tag . note_count - tag . todo_completed_count ) ;
2021-05-27 13:44:58 +02:00
}
2020-09-15 15:01:07 +02:00
return (
2021-05-17 20:33:44 +02:00
< StyledListItem selected = { selected }
className = { ` list-item-container ${ selected ? 'selected' : '' } ` }
key = { tag . id }
2022-09-05 17:21:26 +02:00
onDrop = { onTagDrop_ }
2021-05-17 20:33:44 +02:00
data - tag - id = { tag . id }
>
2022-09-05 17:21:26 +02:00
< StyledExpandLink > { renderExpandIcon ( theme , false , false ) } < / StyledExpandLink >
2020-09-15 15:01:07 +02:00
< StyledListItemAnchor
ref = { anchorRef }
className = "list-item"
href = "#"
selected = { selected }
data - id = { tag . id }
data - type = { BaseModel . TYPE_TAG }
2022-09-05 17:21:26 +02:00
onContextMenu = { itemContextMenu }
2020-09-15 15:01:07 +02:00
onClick = { ( ) = > {
2022-09-05 17:21:26 +02:00
tagItem_click ( tag ) ;
2020-09-15 15:01:07 +02:00
} }
>
2023-05-29 12:20:11 +02:00
< StyledSpanFix className = "tag-label" > { Tag . displayTitle ( tag ) } < / StyledSpanFix >
2021-05-17 20:33:44 +02:00
{ noteCount }
2020-09-15 15:01:07 +02:00
< / StyledListItemAnchor >
< / StyledListItem >
) ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2022-09-05 17:21:26 +02:00
const renderHeader = ( key : string , label : string , iconName : string , contextMenuHandler : Function = null , onPlusButtonClick : Function = null , extraProps : any = { } ) = > {
2020-09-15 15:01:07 +02:00
const headerClick = extraProps . onClick || null ;
delete extraProps . onClick ;
2022-09-05 17:21:26 +02:00
const ref = anchorItemRef ( 'headers' , key ) ;
2020-09-15 15:01:07 +02:00
return (
< div key = { key } style = { { display : 'flex' , flexDirection : 'row' , alignItems : 'center' } } >
< StyledHeader
ref = { ref }
{ . . . extraProps }
onContextMenu = { contextMenuHandler }
2020-11-12 21:13:28 +02:00
onClick = { ( event : any ) = > {
2020-09-15 15:01:07 +02:00
// if a custom click event is attached, trigger that.
if ( headerClick ) {
headerClick ( key , event ) ;
}
2022-09-05 17:21:26 +02:00
onHeaderClick_ ( key ) ;
2020-09-15 15:01:07 +02:00
} }
>
< StyledHeaderIcon className = { iconName } / >
< StyledHeaderLabel > { label } < / StyledHeaderLabel >
< / StyledHeader >
2021-01-12 14:28:55 +02:00
{ onPlusButtonClick && < StyledAddButton onClick = { onPlusButtonClick } iconName = "fas fa-plus" level = { ButtonLevel . SidebarSecondary } / > }
2020-09-15 15:01:07 +02:00
< / div >
) ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onKeyDown = useCallback ( ( event : any ) = > {
2020-09-15 15:01:07 +02:00
const keyCode = event . keyCode ;
2022-09-05 17:21:26 +02:00
const selectedItem = getSelectedItem ( ) ;
2020-09-15 15:01:07 +02:00
if ( keyCode === 40 || keyCode === 38 ) {
// DOWN / UP
event . preventDefault ( ) ;
const focusItems = [ ] ;
2022-09-05 17:21:26 +02:00
for ( let i = 0 ; i < folderItemsOrder_ . current . length ; i ++ ) {
const id = folderItemsOrder_ . current [ i ] ;
focusItems . push ( { id : id , ref : anchorItemRefs.current [ 'folder' ] [ id ] , type : 'folder' } ) ;
2020-09-15 15:01:07 +02:00
}
2022-09-05 17:21:26 +02:00
for ( let i = 0 ; i < tagItemsOrder_ . current . length ; i ++ ) {
const id = tagItemsOrder_ . current [ i ] ;
focusItems . push ( { id : id , ref : anchorItemRefs.current [ 'tag' ] [ id ] , type : 'tag' } ) ;
2020-09-15 15:01:07 +02:00
}
let currentIndex = 0 ;
for ( let i = 0 ; i < focusItems . length ; i ++ ) {
if ( ! selectedItem || focusItems [ i ] . id === selectedItem . id ) {
currentIndex = i ;
break ;
}
}
const inc = keyCode === 38 ? - 1 : + 1 ;
let newIndex = currentIndex + inc ;
if ( newIndex < 0 ) newIndex = 0 ;
if ( newIndex > focusItems . length - 1 ) newIndex = focusItems . length - 1 ;
const focusItem = focusItems [ newIndex ] ;
const actionName = ` ${ focusItem . type . toUpperCase ( ) } _SELECT ` ;
2022-09-05 17:21:26 +02:00
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : actionName ,
id : focusItem.id ,
} ) ;
focusItem . ref . current . focus ( ) ;
}
if ( keyCode === 9 ) {
// TAB
event . preventDefault ( ) ;
if ( event . shiftKey ) {
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'focusElement' , 'noteBody' ) ;
2020-09-15 15:01:07 +02:00
} else {
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'focusElement' , 'noteList' ) ;
2020-09-15 15:01:07 +02:00
}
}
if ( selectedItem && selectedItem . type === 'folder' && keyCode === 32 ) {
// SPACE
event . preventDefault ( ) ;
2022-09-05 17:21:26 +02:00
props . dispatch ( {
2020-09-15 15:01:07 +02:00
type : 'FOLDER_TOGGLE' ,
id : selectedItem.id ,
} ) ;
}
if ( keyCode === 65 && ( event . ctrlKey || event . metaKey ) ) {
// Ctrl+A key
event . preventDefault ( ) ;
}
2022-09-05 17:21:26 +02:00
} , [ getSelectedItem , props . dispatch ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const renderSynchronizeButton = ( type : string ) = > {
2020-09-15 15:01:07 +02:00
const label = type === 'sync' ? _ ( 'Synchronise' ) : _ ( 'Cancel' ) ;
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '' ;
return (
< StyledSynchronizeButton
2021-01-12 14:28:55 +02:00
level = { ButtonLevel . SidebarSecondary }
2020-09-15 15:01:07 +02:00
iconName = "icon-sync"
key = "sync_button"
iconAnimation = { iconAnimation }
title = { label }
onClick = { ( ) = > {
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'synchronize' , type !== 'sync' ) ;
2020-09-15 15:01:07 +02:00
} }
/ >
) ;
2022-09-05 17:21:26 +02:00
} ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const onAddFolderButtonClick = useCallback ( ( ) = > {
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'newFolder' ) ;
2022-09-05 17:21:26 +02:00
} , [ ] ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-12-11 15:28:59 +02:00
2022-09-05 17:21:26 +02:00
const items = [ ] ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
items . push (
renderHeader ( 'folderHeader' , _ ( 'Notebooks' ) , 'icon-notebooks' , header_contextMenu , onAddFolderButtonClick , {
onDrop : onFolderDrop_ ,
[ 'data-folder-id' ] : '' ,
toggleblock : 1 ,
} )
) ;
2020-09-15 15:01:07 +02:00
2023-01-10 20:32:06 +02:00
const foldersStyle = useMemo ( ( ) = > {
return { display : props.folderHeaderIsExpanded ? 'block' : 'none' , paddingBottom : 10 } ;
} , [ props . folderHeaderIsExpanded ] ) ;
2022-09-05 17:21:26 +02:00
if ( props . folders . length ) {
const allNotesSelected = props . notesParentType === 'SmartFilter' && props . selectedSmartFilterId === ALL_NOTES_FILTER_ID ;
const result = shared . renderFolders ( props , renderFolderItem ) ;
const folderItems = [ renderAllNotesItem ( theme , allNotesSelected ) ] . concat ( result . items ) ;
folderItemsOrder_ . current = result . order ;
2020-09-15 15:01:07 +02:00
items . push (
2023-05-29 12:20:11 +02:00
< div
2022-09-05 17:21:26 +02:00
className = { ` folders ${ props . folderHeaderIsExpanded ? 'expanded' : '' } ` }
key = "folder_items"
2023-01-10 20:32:06 +02:00
style = { foldersStyle }
2022-09-05 17:21:26 +02:00
>
{ folderItems }
2023-05-29 12:20:11 +02:00
< / div >
2020-09-15 15:01:07 +02:00
) ;
2022-09-05 17:21:26 +02:00
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
items . push (
renderHeader ( 'tagHeader' , _ ( 'Tags' ) , 'icon-tags' , null , null , {
toggleblock : 1 ,
} )
) ;
if ( props . tags . length ) {
const result = shared . renderTags ( props , renderTag ) ;
const tagItems = result . items ;
tagItemsOrder_ . current = result . order ;
2020-09-15 15:01:07 +02:00
items . push (
2023-05-29 12:20:11 +02:00
< div className = "tags" key = "tag_items" style = { { display : props.tagHeaderIsExpanded ? 'block' : 'none' } } >
2022-09-05 17:21:26 +02:00
{ tagItems }
2023-05-29 12:20:11 +02:00
< / div >
2020-09-15 15:01:07 +02:00
) ;
2022-09-05 17:21:26 +02:00
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
let decryptionReportText = '' ;
if ( props . decryptionWorker && props . decryptionWorker . state !== 'idle' && props . decryptionWorker . itemCount ) {
decryptionReportText = _ ( 'Decrypting items: %d/%d' , props . decryptionWorker . itemIndex + 1 , props . decryptionWorker . itemCount ) ;
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
let resourceFetcherText = '' ;
if ( props . resourceFetcher && props . resourceFetcher . toFetchCount ) {
resourceFetcherText = _ ( 'Fetching resources: %d/%d' , props . resourceFetcher . fetchingCount , props . resourceFetcher . toFetchCount ) ;
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const lines = Synchronizer . reportToLines ( props . syncReport ) ;
if ( resourceFetcherText ) lines . push ( resourceFetcherText ) ;
if ( decryptionReportText ) lines . push ( decryptionReportText ) ;
const syncReportText = [ ] ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
syncReportText . push (
< StyledSyncReportText key = { i } >
{ lines [ i ] }
< / StyledSyncReportText >
) ;
}
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const syncButton = renderSynchronizeButton ( props . syncStarted ? 'cancel' : 'sync' ) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
const syncReportComp = ! syncReportText . length ? null : (
< StyledSyncReport key = "sync_report" >
{ syncReportText }
< / StyledSyncReport >
) ;
2020-09-15 15:01:07 +02:00
2022-09-05 17:21:26 +02:00
return (
< StyledRoot ref = { rootRef } onKeyDown = { onKeyDown } className = "sidebar" >
< div style = { { flex : 1 , overflowX : 'hidden' , overflowY : 'auto' } } > { items } < / div >
< div style = { { flex : 0 , padding : theme.mainPadding } } >
{ syncReportComp }
{ syncButton }
< / div >
< / StyledRoot >
) ;
} ;
2020-09-15 15:01:07 +02:00
2020-12-11 15:28:59 +02:00
const mapStateToProps = ( state : AppState ) = > {
2020-09-15 15:01:07 +02:00
return {
folders : state.folders ,
tags : state.tags ,
searches : state.searches ,
syncStarted : state.syncStarted ,
syncReport : state.syncReport ,
selectedFolderId : state.selectedFolderId ,
selectedTagId : state.selectedTagId ,
selectedSearchId : state.selectedSearchId ,
selectedSmartFilterId : state.selectedSmartFilterId ,
notesParentType : state.notesParentType ,
locale : state.settings.locale ,
themeId : state.settings.theme ,
collapsedFolderIds : state.collapsedFolderIds ,
decryptionWorker : state.decryptionWorker ,
resourceFetcher : state.resourceFetcher ,
2020-12-11 15:28:59 +02:00
plugins : state.pluginService.plugins ,
2022-09-05 17:21:26 +02:00
tagHeaderIsExpanded : state.settings.tagHeaderIsExpanded ,
folderHeaderIsExpanded : state.settings.folderHeaderIsExpanded ,
2020-09-15 15:01:07 +02:00
} ;
} ;
2021-01-12 14:28:55 +02:00
export default connect ( mapStateToProps ) ( SidebarComponent ) ;