2020-09-15 15:01:07 +02:00
import * as React from 'react' ;
2020-09-29 12:49:51 +02:00
import { StyledRoot , StyledAddButton , 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' ;
import { AppState } from '../../app' ;
import { ModelType } from '@joplin/lib/BaseModel' ;
2020-09-15 15:01:07 +02:00
const { connect } = require ( 'react-redux' ) ;
2020-11-07 17:59:37 +02:00
const shared = require ( '@joplin/lib/components/shared/side-menu-shared.js' ) ;
const BaseModel = require ( '@joplin/lib/BaseModel' ) . default ;
const Folder = require ( '@joplin/lib/models/Folder.js' ) ;
const Note = require ( '@joplin/lib/models/Note.js' ) ;
const Tag = require ( '@joplin/lib/models/Tag.js' ) ;
const { themeStyle } = require ( '@joplin/lib/theme' ) ;
2020-10-09 19:35:46 +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' ) ;
2020-09-15 15:01:07 +02:00
interface Props {
2020-11-12 21:29:22 +02:00
themeId : number ;
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-09-15 15:01:07 +02:00
}
interface State {
2020-11-12 21:29:22 +02:00
tagHeaderIsExpanded : boolean ;
folderHeaderIsExpanded : 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 >
) ;
}
2020-11-12 21:13:28 +02:00
function FolderItem ( props : any ) {
2020-09-24 15:30:20 +02:00
const { hasChildren , isExpanded , depth , selected , folderId , folderTitle , anchorRef , noteCount , onFolderDragStart_ , onFolderDragOver_ , onFolderDrop_ , itemContextMenu , folderItem_click , onFolderToggleClick_ } = props ;
2020-09-29 12:49:51 +02:00
const noteCountComp = noteCount ? < StyledNoteCount > { noteCount } < / StyledNoteCount > : null ;
2020-09-24 15:30:20 +02:00
return (
< StyledListItem depth = { depth } selected = { selected } className = { ` list-item-container list-item-depth- ${ depth } ` } onDragStart = { onFolderDragStart_ } onDragOver = { onFolderDragOver_ } onDrop = { onFolderDrop_ } draggable = { true } data - folder - id = { folderId } >
< 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 }
data - id = { folderId }
data - type = { BaseModel . TYPE_FOLDER }
onContextMenu = { itemContextMenu }
data - folder - id = { folderId }
onClick = { ( ) = > {
folderItem_click ( folderId ) ;
} }
onDoubleClick = { onFolderToggleClick_ }
>
2020-09-29 12:49:51 +02:00
{ folderTitle } { 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 ( ) ) ;
2021-01-12 14:28:55 +02:00
class SidebarComponent extends React . Component < Props , State > {
2020-09-15 15:01:07 +02:00
2020-11-12 21:13:28 +02:00
private folderItemsOrder_ : any [ ] = [ ] ;
private tagItemsOrder_ : any [ ] = [ ] ;
private rootRef : any = null ;
private anchorItemRefs : any = { } ;
2020-12-11 15:28:59 +02:00
private pluginsRef : any ;
2020-09-15 15:01:07 +02:00
2020-11-12 21:13:28 +02:00
constructor ( props : any ) {
2020-09-15 15:01:07 +02:00
super ( props ) ;
CommandService . instance ( ) . componentRegisterCommands ( this , commands ) ;
this . state = {
tagHeaderIsExpanded : Setting.value ( 'tagHeaderIsExpanded' ) ,
folderHeaderIsExpanded : Setting.value ( 'folderHeaderIsExpanded' ) ,
} ;
2020-12-11 15:28:59 +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.
this . pluginsRef = React . createRef ( ) ;
2020-09-15 15:01:07 +02:00
this . onFolderToggleClick_ = this . onFolderToggleClick_ . bind ( this ) ;
this . onKeyDown = this . onKeyDown . bind ( this ) ;
this . onAllNotesClick_ = this . onAllNotesClick_ . bind ( this ) ;
this . header_contextMenu = this . header_contextMenu . bind ( this ) ;
this . onAddFolderButtonClick = this . onAddFolderButtonClick . bind ( this ) ;
2020-09-24 15:30:20 +02:00
this . folderItem_click = this . folderItem_click . bind ( this ) ;
2020-09-30 09:16:20 +02:00
this . itemContextMenu = this . itemContextMenu . bind ( this ) ;
2020-09-15 15:01:07 +02:00
}
2020-11-12 21:13:28 +02:00
onFolderDragStart_ ( 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 ] ) ) ;
}
2020-11-12 21:13:28 +02:00
onFolderDragOver_ ( 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 ( ) ;
}
2020-11-12 21:13:28 +02:00
async onFolderDrop_ ( 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.
if ( dt . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) {
event . preventDefault ( ) ;
if ( ! folderId ) return ;
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 ( ) ;
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-11-12 21:13:28 +02:00
async onTagDrop_ ( 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 ] ) ;
}
}
}
2020-11-12 21:13:28 +02:00
async onFolderToggleClick_ ( event : any ) {
2020-09-15 15:01:07 +02:00
const folderId = event . currentTarget . getAttribute ( 'data-folder-id' ) ;
this . props . dispatch ( {
type : 'FOLDER_TOGGLE' ,
id : folderId ,
} ) ;
}
componentWillUnmount() {
CommandService . instance ( ) . componentUnregisterCommands ( commands ) ;
}
async header_contextMenu() {
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
) ;
menu . popup ( bridge ( ) . window ( ) ) ;
}
2020-11-12 21:13:28 +02:00
async itemContextMenu ( 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' ) ;
let deleteMessage = '' ;
let buttonLabel = _ ( 'Remove' ) ;
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 ) ) ;
buttonLabel = _ ( 'Delete' ) ;
} 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 ) {
item = BaseModel . byId ( this . props . folders , itemId ) ;
}
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 ( {
label : buttonLabel ,
click : async ( ) = > {
const ok = bridge ( ) . showConfirmMessageBox ( deleteMessage , {
buttons : [ buttonLabel , _ ( 'Cancel' ) ] ,
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 ) {
this . props . dispatch ( {
type : 'SEARCH_DELETE' ,
id : itemId ,
} ) ;
}
} ,
} )
) ;
if ( itemType === BaseModel . TYPE_FOLDER && ! item . encryption_applied ) {
2020-10-18 22:52:10 +02:00
menu . append ( new MenuItem ( menuUtils . commandToStatefulMenuItem ( 'renameFolder' , 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 ( ) = > {
2020-12-19 19:42:18 +02:00
await InteropServiceHelper . export ( this . props . dispatch . bind ( this ) , module , { sourceFolderIds : [ itemId ] , plugins : this.pluginsRef.current } ) ;
2020-09-15 15:01:07 +02:00
} ,
} )
) ;
}
menu . append (
new MenuItem ( {
label : _ ( 'Export' ) ,
submenu : exportMenu ,
} )
) ;
}
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
) ) ;
}
2020-12-11 15:28:59 +02:00
const pluginViews = pluginUtils . viewsByType ( this . pluginsRef . current , 'menuItem' ) ;
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 ) )
) ;
}
}
2020-09-15 15:01:07 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
}
2020-11-12 21:13:28 +02:00
folderItem_click ( folderId : string ) {
2020-09-15 15:01:07 +02:00
this . props . dispatch ( {
type : 'FOLDER_SELECT' ,
2020-09-24 15:30:20 +02:00
id : folderId ? folderId : null ,
2020-09-15 15:01:07 +02:00
} ) ;
}
2020-11-12 21:13:28 +02:00
tagItem_click ( tag : any ) {
2020-09-15 15:01:07 +02:00
this . props . dispatch ( {
type : 'TAG_SELECT' ,
id : tag ? tag.id : null ,
} ) ;
}
2020-11-12 21:13:28 +02:00
anchorItemRef ( type : string , id : string ) {
2020-09-15 15:01:07 +02:00
if ( ! this . anchorItemRefs [ type ] ) this . anchorItemRefs [ type ] = { } ;
if ( this . anchorItemRefs [ type ] [ id ] ) return this . anchorItemRefs [ type ] [ id ] ;
this . anchorItemRefs [ type ] [ id ] = React . createRef ( ) ;
return this . anchorItemRefs [ type ] [ id ] ;
}
2020-11-12 21:13:28 +02:00
firstAnchorItemRef ( type : string ) {
2020-09-15 15:01:07 +02:00
const refs = this . anchorItemRefs [ type ] ;
if ( ! refs ) return null ;
const n = ` ${ type } s ` ;
const p = this . props as any ;
const item = p [ n ] && p [ n ] . length ? p [ n ] [ 0 ] : null ;
if ( ! item ) return null ;
return refs [ item . id ] ;
}
2020-11-12 21:13:28 +02:00
renderNoteCount ( count : number ) {
2020-09-29 12:49:51 +02:00
return count ? < StyledNoteCount > { count } < / StyledNoteCount > : null ;
2020-09-15 15:01:07 +02:00
}
2020-11-12 21:13:28 +02:00
renderExpandIcon ( isExpanded : boolean , isVisible : boolean = true ) {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . 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-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 > ;
}
2020-11-12 21:13:28 +02:00
renderAllNotesItem ( selected : boolean ) {
2020-09-15 15:01:07 +02:00
return (
< StyledListItem key = "allNotesHeader" selected = { selected } className = { 'list-item-container list-item-depth-0' } isSpecialItem = { true } >
< StyledExpandLink > { this . renderExpandIcon ( 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 }
2020-09-29 12:49:51 +02:00
onClick = { this . 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 >
) ;
}
2020-11-12 21:13:28 +02:00
renderFolderItem ( folder : any , selected : boolean , hasChildren : boolean , depth : number ) {
2020-09-15 15:01:07 +02:00
const anchorRef = this . anchorItemRef ( 'folder' , folder . id ) ;
2020-10-12 11:13:41 +02:00
const isExpanded = this . props . collapsedFolderIds . indexOf ( folder . id ) < 0 ;
let noteCount = folder . note_count ;
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
if ( isExpanded ) {
for ( let i = 0 ; i < this . props . folders . length ; i ++ ) {
if ( this . props . folders [ i ] . parent_id === folder . id ) {
noteCount -= this . props . folders [ i ] . note_count ;
}
}
}
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 ) }
themeId = { this . props . themeId }
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 }
2020-09-24 15:30:20 +02:00
onFolderDragStart_ = { this . onFolderDragStart_ }
onFolderDragOver_ = { this . onFolderDragOver_ }
onFolderDrop_ = { this . onFolderDrop_ }
itemContextMenu = { this . itemContextMenu }
folderItem_click = { this . folderItem_click }
onFolderToggleClick_ = { this . onFolderToggleClick_ }
/ > ;
2020-09-15 15:01:07 +02:00
}
2020-11-12 21:13:28 +02:00
renderTag ( tag : any , selected : boolean ) {
2020-09-15 15:01:07 +02:00
const anchorRef = this . anchorItemRef ( 'tag' , tag . id ) ;
const noteCount = Setting . value ( 'showNoteCounts' ) ? this . renderNoteCount ( tag . note_count ) : '' ;
return (
< StyledListItem selected = { selected } className = { 'list-item-container' } key = { tag . id } onDrop = { this . onTagDrop_ } data - tag - id = { tag . id } >
< StyledExpandLink > { this . renderExpandIcon ( false , false ) } < / StyledExpandLink >
< StyledListItemAnchor
ref = { anchorRef }
className = "list-item"
href = "#"
selected = { selected }
data - id = { tag . id }
data - type = { BaseModel . TYPE_TAG }
2020-09-30 09:16:20 +02:00
onContextMenu = { this . itemContextMenu }
2020-09-15 15:01:07 +02:00
onClick = { ( ) = > {
this . tagItem_click ( tag ) ;
} }
>
{ Tag . displayTitle ( tag ) } { noteCount }
< / StyledListItemAnchor >
< / StyledListItem >
) ;
}
2020-11-12 21:13:28 +02:00
makeDivider ( key : string ) {
2020-09-15 15:01:07 +02:00
return < div style = { { height : 2 , backgroundColor : 'blue' } } key = { key } / > ;
}
2020-11-12 21:13:28 +02:00
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 ;
const ref = this . anchorItemRef ( 'headers' , key ) ;
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 ) ;
}
this . onHeaderClick_ ( key ) ;
} }
>
< 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 >
) ;
}
selectedItem() {
if ( this . props . notesParentType === 'Folder' && this . props . selectedFolderId ) {
return { type : 'folder' , id : this.props.selectedFolderId } ;
} else if ( this . props . notesParentType === 'Tag' && this . props . selectedTagId ) {
return { type : 'tag' , id : this.props.selectedTagId } ;
}
return null ;
}
2020-11-12 21:13:28 +02:00
onKeyDown ( event : any ) {
2020-09-15 15:01:07 +02:00
const keyCode = event . keyCode ;
const selectedItem = this . selectedItem ( ) ;
if ( keyCode === 40 || keyCode === 38 ) {
// DOWN / UP
event . preventDefault ( ) ;
const focusItems = [ ] ;
for ( let i = 0 ; i < this . folderItemsOrder_ . length ; i ++ ) {
const id = this . folderItemsOrder_ [ i ] ;
focusItems . push ( { id : id , ref : this.anchorItemRefs [ 'folder' ] [ id ] , type : 'folder' } ) ;
}
for ( let i = 0 ; i < this . tagItemsOrder_ . length ; i ++ ) {
const id = this . tagItemsOrder_ [ i ] ;
focusItems . push ( { id : id , ref : this.anchorItemRefs [ 'tag' ] [ id ] , type : 'tag' } ) ;
}
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 ` ;
this . props . dispatch ( {
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 ( ) ;
this . props . dispatch ( {
type : 'FOLDER_TOGGLE' ,
id : selectedItem.id ,
} ) ;
}
if ( keyCode === 65 && ( event . ctrlKey || event . metaKey ) ) {
// Ctrl+A key
event . preventDefault ( ) ;
}
}
2020-11-12 21:13:28 +02:00
onHeaderClick_ ( key : string ) {
2020-09-15 15:01:07 +02:00
const toggleKey = ` ${ key } IsExpanded ` ;
const isExpanded = ( this . state as any ) [ toggleKey ] ;
2020-11-12 21:13:28 +02:00
const newState : any = { [ toggleKey ] : ! isExpanded } ;
2020-09-15 15:01:07 +02:00
this . setState ( newState ) ;
Setting . setValue ( toggleKey , ! isExpanded ) ;
}
onAllNotesClick_() {
this . props . dispatch ( {
type : 'SMART_FILTER_SELECT' ,
id : ALL_NOTES_FILTER_ID ,
} ) ;
}
2020-11-12 21:13:28 +02:00
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
} }
/ >
) ;
}
onAddFolderButtonClick() {
2020-11-25 16:40:25 +02:00
void CommandService . instance ( ) . execute ( 'newFolder' ) ;
2020-09-15 15:01:07 +02:00
}
2020-10-20 00:24:40 +02:00
// componentDidUpdate(prevProps:any, prevState:any) {
// for (const n in prevProps) {
// if (prevProps[n] !== (this.props as any)[n]) {
// console.info('CHANGED PROPS', n);
// }
// }
// for (const n in prevState) {
// if (prevState[n] !== (this.state as any)[n]) {
// console.info('CHANGED STATE', n);
// }
// }
// }
2020-09-15 15:01:07 +02:00
render() {
2020-12-11 15:28:59 +02:00
this . pluginsRef . current = this . props . plugins ;
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
const items = [ ] ;
items . push (
this . renderHeader ( 'folderHeader' , _ ( 'Notebooks' ) , 'icon-notebooks' , this . header_contextMenu , this . onAddFolderButtonClick , {
onDrop : this.onFolderDrop_ ,
[ 'data-folder-id' ] : '' ,
toggleblock : 1 ,
} )
) ;
if ( this . props . folders . length ) {
const allNotesSelected = this . props . notesParentType === 'SmartFilter' && this . props . selectedSmartFilterId === ALL_NOTES_FILTER_ID ;
const result = shared . renderFolders ( this . props , this . renderFolderItem . bind ( this ) ) ;
const folderItems = [ this . renderAllNotesItem ( allNotesSelected ) ] . concat ( result . items ) ;
this . folderItemsOrder_ = result . order ;
items . push (
< div className = "folders" key = "folder_items" style = { { display : this.state.folderHeaderIsExpanded ? 'block' : 'none' , paddingBottom : 10 } } >
{ folderItems }
< / div >
) ;
}
items . push (
this . renderHeader ( 'tagHeader' , _ ( 'Tags' ) , 'icon-tags' , null , null , {
toggleblock : 1 ,
} )
) ;
if ( this . props . tags . length ) {
const result = shared . renderTags ( this . props , this . renderTag . bind ( this ) ) ;
const tagItems = result . items ;
this . tagItemsOrder_ = result . order ;
items . push (
< div className = "tags" key = "tag_items" style = { { display : this.state.tagHeaderIsExpanded ? 'block' : 'none' } } >
{ tagItems }
< / div >
) ;
}
let decryptionReportText = '' ;
if ( this . props . decryptionWorker && this . props . decryptionWorker . state !== 'idle' && this . props . decryptionWorker . itemCount ) {
decryptionReportText = _ ( 'Decrypting items: %d/%d' , this . props . decryptionWorker . itemIndex + 1 , this . props . decryptionWorker . itemCount ) ;
}
let resourceFetcherText = '' ;
if ( this . props . resourceFetcher && this . props . resourceFetcher . toFetchCount ) {
resourceFetcherText = _ ( 'Fetching resources: %d/%d' , this . props . resourceFetcher . fetchingCount , this . props . resourceFetcher . toFetchCount ) ;
}
const lines = Synchronizer . reportToLines ( this . 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 >
) ;
}
const syncButton = this . renderSynchronizeButton ( this . props . syncStarted ? 'cancel' : 'sync' ) ;
const syncReportComp = ! syncReportText . length ? null : (
< StyledSyncReport key = "sync_report" >
{ syncReportText }
< / StyledSyncReport >
) ;
return (
2021-01-12 14:28:55 +02:00
< StyledRoot ref = { this . rootRef } onKeyDown = { this . onKeyDown } className = "sidebar" >
2020-09-15 15:01:07 +02:00
< div style = { { flex : 1 , overflowX : 'hidden' , overflowY : 'auto' } } > { items } < / div >
< div style = { { flex : 0 , padding : theme.mainPadding } } >
{ syncReportComp }
{ syncButton }
< / div >
< / StyledRoot >
) ;
}
}
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 ,
2020-09-15 15:01:07 +02:00
} ;
} ;
2021-01-12 14:28:55 +02:00
export default connect ( mapStateToProps ) ( SidebarComponent ) ;