1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-30 08:26:59 +02:00

Chore: Converted desktop Sidebar to React Hooks

This commit is contained in:
Laurent Cozic 2022-09-05 16:21:26 +01:00
parent 597569745c
commit e37d980453
2 changed files with 238 additions and 258 deletions

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles'; import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
import { ButtonLevel } from '../Button/Button'; import { ButtonLevel } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
@ -23,6 +24,8 @@ import { store } from '@joplin/lib/reducer';
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService'; import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils'; import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import FolderIconBox from '../FolderIconBox'; import FolderIconBox from '../FolderIconBox';
import { Theme } from '@joplin/lib/themes/type';
import { RuntimeProps } from './commands/focusElementSideBar';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js'); const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
const { themeStyle } = require('@joplin/lib/theme'); const { themeStyle } = require('@joplin/lib/theme');
@ -50,11 +53,8 @@ interface Props {
tags: any[]; tags: any[];
syncStarted: boolean; syncStarted: boolean;
plugins: PluginStates; plugins: PluginStates;
}
interface State {
tagHeaderIsExpanded: boolean;
folderHeaderIsExpanded: boolean; folderHeaderIsExpanded: boolean;
tagHeaderIsExpanded: boolean;
} }
const commands = [ const commands = [
@ -119,56 +119,80 @@ function FolderItem(props: any) {
const menuUtils = new MenuUtils(CommandService.instance()); const menuUtils = new MenuUtils(CommandService.instance());
class SidebarComponent extends React.Component<Props, State> { const SidebarComponent = (props: Props) => {
private folderItemsOrder_: any[] = []; const folderItemsOrder_ = useRef<any[]>();
private tagItemsOrder_: any[] = []; folderItemsOrder_.current = [];
private rootRef: any = null; const tagItemsOrder_ = useRef<any[]>();
private anchorItemRefs: any = {}; tagItemsOrder_.current = [];
private pluginsRef: any;
constructor(props: any) { const rootRef = useRef(null);
super(props); const anchorItemRefs = useRef<Record<string, any>>(null);
anchorItemRefs.current = {};
CommandService.instance().componentRegisterCommands(this, commands); // 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;
this.state = { const getSelectedItem = useCallback(() => {
tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), if (props.notesParentType === 'Folder' && props.selectedFolderId) {
folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'), return { type: 'folder', id: props.selectedFolderId };
} else if (props.notesParentType === 'Tag' && props.selectedTagId) {
return { type: 'tag', id: props.selectedTagId };
}
return null;
}, [props.notesParentType, props.selectedFolderId, props.selectedTagId]);
const getFirstAnchorItemRef = useCallback((type: string) => {
const refs = anchorItemRefs.current[type];
if (!refs) return null;
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]);
useEffect(() => {
const runtimeProps: RuntimeProps = {
getSelectedItem,
anchorItemRefs,
getFirstAnchorItemRef,
}; };
// This whole component is a bit of a mess and rather than passing CommandService.instance().componentRegisterCommands(runtimeProps, commands);
// 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();
this.onFolderToggleClick_ = this.onFolderToggleClick_.bind(this); return () => {
this.onKeyDown = this.onKeyDown.bind(this); CommandService.instance().componentUnregisterCommands(commands);
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this); };
this.header_contextMenu = this.header_contextMenu.bind(this); }, [
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this); getSelectedItem,
this.folderItem_click = this.folderItem_click.bind(this); anchorItemRefs,
this.itemContextMenu = this.itemContextMenu.bind(this); getFirstAnchorItemRef,
} ]);
onFolderDragStart_(event: any) { const onFolderDragStart_ = useCallback((event: any) => {
const folderId = event.currentTarget.getAttribute('data-folder-id'); const folderId = event.currentTarget.getAttribute('data-folder-id');
if (!folderId) return; if (!folderId) return;
event.dataTransfer.setDragImage(new Image(), 1, 1); event.dataTransfer.setDragImage(new Image(), 1, 1);
event.dataTransfer.clearData(); event.dataTransfer.clearData();
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId])); event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
} }, []);
onFolderDragOver_(event: any) { const onFolderDragOver_ = useCallback((event: any) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault(); 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(); if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
} }, []);
async onFolderDrop_(event: any) { const onFolderDrop_ = useCallback(async (event: any) => {
const folderId = event.currentTarget.getAttribute('data-folder-id'); const folderId = event.currentTarget.getAttribute('data-folder-id');
const dt = event.dataTransfer; const dt = event.dataTransfer;
if (!dt) return; if (!dt) return;
@ -199,9 +223,9 @@ class SidebarComponent extends React.Component<Props, State> {
logger.error(error); logger.error(error);
alert(error.message); alert(error.message);
} }
} }, []);
async onTagDrop_(event: any) { const onTagDrop_ = useCallback(async (event: any) => {
const tagId = event.currentTarget.getAttribute('data-tag-id'); const tagId = event.currentTarget.getAttribute('data-tag-id');
const dt = event.dataTransfer; const dt = event.dataTransfer;
if (!dt) return; if (!dt) return;
@ -214,22 +238,18 @@ class SidebarComponent extends React.Component<Props, State> {
await Tag.addNote(tagId, noteIds[i]); await Tag.addNote(tagId, noteIds[i]);
} }
} }
} }, []);
async onFolderToggleClick_(event: any) { const onFolderToggleClick_ = useCallback((event: any) => {
const folderId = event.currentTarget.getAttribute('data-folder-id'); const folderId = event.currentTarget.getAttribute('data-folder-id');
this.props.dispatch({ props.dispatch({
type: 'FOLDER_TOGGLE', type: 'FOLDER_TOGGLE',
id: folderId, id: folderId,
}); });
} }, [props.dispatch]);
componentWillUnmount() { const header_contextMenu = useCallback(async () => {
CommandService.instance().componentUnregisterCommands(commands);
}
async header_contextMenu() {
const menu = new Menu(); const menu = new Menu();
menu.append( menu.append(
@ -237,9 +257,9 @@ class SidebarComponent extends React.Component<Props, State> {
); );
menu.popup(bridge().window()); menu.popup(bridge().window());
} }, []);
async itemContextMenu(event: any) { const itemContextMenu = useCallback(async (event: any) => {
const itemId = event.currentTarget.getAttribute('data-id'); const itemId = event.currentTarget.getAttribute('data-id');
if (itemId === Folder.conflictFolderId()) return; if (itemId === Folder.conflictFolderId()) return;
@ -265,7 +285,7 @@ class SidebarComponent extends React.Component<Props, State> {
let item = null; let item = null;
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
item = BaseModel.byId(this.props.folders, itemId); item = BaseModel.byId(props.folders, itemId);
} }
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
@ -289,7 +309,7 @@ class SidebarComponent extends React.Component<Props, State> {
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId); await Tag.untagAll(itemId);
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
this.props.dispatch({ props.dispatch({
type: 'SEARCH_DELETE', type: 'SEARCH_DELETE',
id: itemId, id: itemId,
}); });
@ -314,7 +334,7 @@ class SidebarComponent extends React.Component<Props, State> {
new MenuItem({ new MenuItem({
label: module.fullLabel(), label: module.fullLabel(),
click: async () => { click: async () => {
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId], plugins: this.pluginsRef.current }); await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: [itemId], plugins: pluginsRef.current });
}, },
}) })
); );
@ -374,7 +394,7 @@ class SidebarComponent extends React.Component<Props, State> {
); );
} }
const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem'); const pluginViews = pluginUtils.viewsByType(pluginsRef.current, 'menuItem');
for (const view of pluginViews) { for (const view of pluginViews) {
const location = view.location; const location = view.location;
@ -389,80 +409,79 @@ class SidebarComponent extends React.Component<Props, State> {
} }
menu.popup(bridge().window()); menu.popup(bridge().window());
} }, [props.folders, props.dispatch, pluginsRef]);
folderItem_click(folderId: string) { const folderItem_click = useCallback((folderId: string) => {
this.props.dispatch({ props.dispatch({
type: 'FOLDER_SELECT', type: 'FOLDER_SELECT',
id: folderId ? folderId : null, id: folderId ? folderId : null,
}); });
} }, [props.dispatch]);
tagItem_click(tag: any) { const tagItem_click = useCallback((tag: any) => {
this.props.dispatch({ props.dispatch({
type: 'TAG_SELECT', type: 'TAG_SELECT',
id: tag ? tag.id : null, id: tag ? tag.id : null,
}); });
} }, [props.dispatch]);
anchorItemRef(type: string, id: string) { const onHeaderClick_ = useCallback((key: string) => {
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {}; const isExpanded = key === 'tag' ? props.tagHeaderIsExpanded : props.folderHeaderIsExpanded;
if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id]; Setting.setValue(key === 'tag' ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded', !isExpanded);
this.anchorItemRefs[type][id] = React.createRef(); }, [props.folderHeaderIsExpanded, props.tagHeaderIsExpanded]);
return this.anchorItemRefs[type][id];
}
firstAnchorItemRef(type: string) { const onAllNotesClick_ = () => {
const refs = this.anchorItemRefs[type]; props.dispatch({
if (!refs) return null; type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
};
const n = `${type}s`; const anchorItemRef = (type: string, id: string) => {
const p = this.props as any; if (!anchorItemRefs.current[type]) anchorItemRefs.current[type] = {};
const item = p[n] && p[n].length ? p[n][0] : null; if (anchorItemRefs.current[type][id]) return anchorItemRefs.current[type][id];
if (!item) return null; anchorItemRefs.current[type][id] = React.createRef();
return anchorItemRefs.current[type][id];
};
return refs[item.id]; const renderNoteCount = (count: number) => {
}
renderNoteCount(count: number) {
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null; return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
} };
renderExpandIcon(isExpanded: boolean, isVisible: boolean = true) { const renderExpandIcon = (theme: any, isExpanded: boolean, isVisible: boolean) => {
const theme = themeStyle(this.props.themeId);
const style: any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' }; const style: any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
if (!isVisible) style.visibility = 'hidden'; if (!isVisible) style.visibility = 'hidden';
return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>; return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
} };
renderAllNotesItem(selected: boolean) { const renderAllNotesItem = (theme: Theme, selected: boolean) => {
return ( return (
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}> <StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}>
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink> <StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
<StyledAllNotesIcon className="icon-notes"/> <StyledAllNotesIcon className="icon-notes"/>
<StyledListItemAnchor <StyledListItemAnchor
className="list-item" className="list-item"
isSpecialItem={true} isSpecialItem={true}
href="#" href="#"
selected={selected} selected={selected}
onClick={this.onAllNotesClick_} onClick={onAllNotesClick_}
> >
{_('All notes')} {_('All notes')}
</StyledListItemAnchor> </StyledListItemAnchor>
</StyledListItem> </StyledListItem>
); );
} };
renderFolderItem(folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) { const renderFolderItem = (folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) =>{
const anchorRef = this.anchorItemRef('folder', folder.id); const anchorRef = anchorItemRef('folder', folder.id);
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0; const isExpanded = props.collapsedFolderIds.indexOf(folder.id) < 0;
let noteCount = (folder as any).note_count; let noteCount = (folder as any).note_count;
// Thunderbird count: Subtract children note_count from parent folder if it expanded. // Thunderbird count: Subtract children note_count from parent folder if it expanded.
if (isExpanded) { if (isExpanded) {
for (let i = 0; i < this.props.folders.length; i++) { for (let i = 0; i < props.folders.length; i++) {
if (this.props.folders[i].parent_id === folder.id) { if (props.folders[i].parent_id === folder.id) {
noteCount -= this.props.folders[i].note_count; noteCount -= props.folders[i].note_count;
} }
} }
} }
@ -472,40 +491,40 @@ class SidebarComponent extends React.Component<Props, State> {
folderId={folder.id} folderId={folder.id}
folderTitle={Folder.displayTitle(folder)} folderTitle={Folder.displayTitle(folder)}
folderIcon={Folder.unserializeIcon(folder.icon)} folderIcon={Folder.unserializeIcon(folder.icon)}
themeId={this.props.themeId} themeId={props.themeId}
depth={depth} depth={depth}
selected={selected} selected={selected}
isExpanded={isExpanded} isExpanded={isExpanded}
hasChildren={hasChildren} hasChildren={hasChildren}
anchorRef={anchorRef} anchorRef={anchorRef}
noteCount={noteCount} noteCount={noteCount}
onFolderDragStart_={this.onFolderDragStart_} onFolderDragStart_={onFolderDragStart_}
onFolderDragOver_={this.onFolderDragOver_} onFolderDragOver_={onFolderDragOver_}
onFolderDrop_={this.onFolderDrop_} onFolderDrop_={onFolderDrop_}
itemContextMenu={this.itemContextMenu} itemContextMenu={itemContextMenu}
folderItem_click={this.folderItem_click} folderItem_click={folderItem_click}
onFolderToggleClick_={this.onFolderToggleClick_} onFolderToggleClick_={onFolderToggleClick_}
shareId={folder.share_id} shareId={folder.share_id}
parentId={folder.parent_id} parentId={folder.parent_id}
/>; />;
} };
renderTag(tag: any, selected: boolean) { const renderTag = (tag: any, selected: boolean) => {
const anchorRef = this.anchorItemRef('tag', tag.id); const anchorRef = anchorItemRef('tag', tag.id);
let noteCount = null; let noteCount = null;
if (Setting.value('showNoteCounts')) { if (Setting.value('showNoteCounts')) {
if (Setting.value('showCompletedTodos')) noteCount = this.renderNoteCount(tag.note_count); if (Setting.value('showCompletedTodos')) noteCount = renderNoteCount(tag.note_count);
else noteCount = this.renderNoteCount(tag.note_count - tag.todo_completed_count); else noteCount = renderNoteCount(tag.note_count - tag.todo_completed_count);
} }
return ( return (
<StyledListItem selected={selected} <StyledListItem selected={selected}
className={`list-item-container ${selected ? 'selected' : ''}`} className={`list-item-container ${selected ? 'selected' : ''}`}
key={tag.id} key={tag.id}
onDrop={this.onTagDrop_} onDrop={onTagDrop_}
data-tag-id={tag.id} data-tag-id={tag.id}
> >
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink> <StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
<StyledListItemAnchor <StyledListItemAnchor
ref={anchorRef} ref={anchorRef}
className="list-item" className="list-item"
@ -513,9 +532,9 @@ class SidebarComponent extends React.Component<Props, State> {
selected={selected} selected={selected}
data-id={tag.id} data-id={tag.id}
data-type={BaseModel.TYPE_TAG} data-type={BaseModel.TYPE_TAG}
onContextMenu={this.itemContextMenu} onContextMenu={itemContextMenu}
onClick={() => { onClick={() => {
this.tagItem_click(tag); tagItem_click(tag);
}} }}
> >
<span className="tag-label">{Tag.displayTitle(tag)}</span> <span className="tag-label">{Tag.displayTitle(tag)}</span>
@ -523,16 +542,12 @@ class SidebarComponent extends React.Component<Props, State> {
</StyledListItemAnchor> </StyledListItemAnchor>
</StyledListItem> </StyledListItem>
); );
} };
makeDivider(key: string) { const renderHeader = (key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) => {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
renderHeader(key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) {
const headerClick = extraProps.onClick || null; const headerClick = extraProps.onClick || null;
delete extraProps.onClick; delete extraProps.onClick;
const ref = this.anchorItemRef('headers', key); const ref = anchorItemRef('headers', key);
return ( return (
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
@ -545,7 +560,7 @@ class SidebarComponent extends React.Component<Props, State> {
if (headerClick) { if (headerClick) {
headerClick(key, event); headerClick(key, event);
} }
this.onHeaderClick_(key); onHeaderClick_(key);
}} }}
> >
<StyledHeaderIcon className={iconName}/> <StyledHeaderIcon className={iconName}/>
@ -554,21 +569,11 @@ class SidebarComponent extends React.Component<Props, State> {
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SidebarSecondary}/> } { onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SidebarSecondary}/> }
</div> </div>
); );
} };
selectedItem() { const onKeyDown = useCallback((event: any) => {
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;
}
onKeyDown(event: any) {
const keyCode = event.keyCode; const keyCode = event.keyCode;
const selectedItem = this.selectedItem(); const selectedItem = getSelectedItem();
if (keyCode === 40 || keyCode === 38) { if (keyCode === 40 || keyCode === 38) {
// DOWN / UP // DOWN / UP
@ -576,14 +581,14 @@ class SidebarComponent extends React.Component<Props, State> {
const focusItems = []; const focusItems = [];
for (let i = 0; i < this.folderItemsOrder_.length; i++) { for (let i = 0; i < folderItemsOrder_.current.length; i++) {
const id = this.folderItemsOrder_[i]; const id = folderItemsOrder_.current[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' }); focusItems.push({ id: id, ref: anchorItemRefs.current['folder'][id], type: 'folder' });
} }
for (let i = 0; i < this.tagItemsOrder_.length; i++) { for (let i = 0; i < tagItemsOrder_.current.length; i++) {
const id = this.tagItemsOrder_[i]; const id = tagItemsOrder_.current[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' }); focusItems.push({ id: id, ref: anchorItemRefs.current['tag'][id], type: 'tag' });
} }
let currentIndex = 0; let currentIndex = 0;
@ -604,7 +609,7 @@ class SidebarComponent extends React.Component<Props, State> {
const actionName = `${focusItem.type.toUpperCase()}_SELECT`; const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
this.props.dispatch({ props.dispatch({
type: actionName, type: actionName,
id: focusItem.id, id: focusItem.id,
}); });
@ -627,7 +632,7 @@ class SidebarComponent extends React.Component<Props, State> {
// SPACE // SPACE
event.preventDefault(); event.preventDefault();
this.props.dispatch({ props.dispatch({
type: 'FOLDER_TOGGLE', type: 'FOLDER_TOGGLE',
id: selectedItem.id, id: selectedItem.id,
}); });
@ -637,24 +642,9 @@ class SidebarComponent extends React.Component<Props, State> {
// Ctrl+A key // Ctrl+A key
event.preventDefault(); event.preventDefault();
} }
} }, [getSelectedItem, props.dispatch]);
onHeaderClick_(key: string) { const renderSynchronizeButton = (type: string) => {
const toggleKey = `${key}IsExpanded`;
const isExpanded = (this.state as any)[toggleKey];
const newState: any = { [toggleKey]: !isExpanded };
this.setState(newState);
Setting.setValue(toggleKey, !isExpanded);
}
onAllNotesClick_() {
this.props.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
}
renderSynchronizeButton(type: string) {
const label = type === 'sync' ? _('Synchronise') : _('Cancel'); const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : ''; const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
@ -670,116 +660,98 @@ class SidebarComponent extends React.Component<Props, State> {
}} }}
/> />
); );
} };
onAddFolderButtonClick() { const onAddFolderButtonClick = useCallback(() => {
void CommandService.instance().execute('newFolder'); void CommandService.instance().execute('newFolder');
}, []);
const theme = themeStyle(props.themeId);
const items = [];
items.push(
renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', header_contextMenu, onAddFolderButtonClick, {
onDrop: onFolderDrop_,
['data-folder-id']: '',
toggleblock: 1,
})
);
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;
items.push(
<div
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
key="folder_items"
style={{ display: props.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
>
{folderItems}
</div>
);
} }
// componentDidUpdate(prevProps:any, prevState:any) { items.push(
// for (const n in prevProps) { renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
// if (prevProps[n] !== (this.props as any)[n]) { toggleblock: 1,
// console.info('CHANGED PROPS', n); })
// } );
// }
// for (const n in prevState) { if (props.tags.length) {
// if (prevState[n] !== (this.state as any)[n]) { const result = shared.renderTags(props, renderTag);
// console.info('CHANGED STATE', n); const tagItems = result.items;
// } tagItemsOrder_.current = result.order;
// }
// }
render() {
this.pluginsRef.current = this.props.plugins;
const theme = themeStyle(this.props.themeId);
const items = [];
items.push( items.push(
this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, { <div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
onDrop: this.onFolderDrop_, {tagItems}
['data-folder-id']: '', </div>
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 ${this.state.folderHeaderIsExpanded ? 'expanded' : ''}`}
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 (
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="sidebar">
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0, padding: theme.mainPadding }}>
{syncReportComp}
{syncButton}
</div>
</StyledRoot>
); );
} }
}
let decryptionReportText = '';
if (props.decryptionWorker && props.decryptionWorker.state !== 'idle' && props.decryptionWorker.itemCount) {
decryptionReportText = _('Decrypting items: %d/%d', props.decryptionWorker.itemIndex + 1, props.decryptionWorker.itemCount);
}
let resourceFetcherText = '';
if (props.resourceFetcher && props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d/%d', props.resourceFetcher.fetchingCount, props.resourceFetcher.toFetchCount);
}
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>
);
}
const syncButton = renderSynchronizeButton(props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<StyledSyncReport key="sync_report">
{syncReportText}
</StyledSyncReport>
);
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>
);
};
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
@ -799,6 +771,8 @@ const mapStateToProps = (state: AppState) => {
decryptionWorker: state.decryptionWorker, decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher, resourceFetcher: state.resourceFetcher,
plugins: state.pluginService.plugins, plugins: state.pluginService.plugins,
tagHeaderIsExpanded: state.settings.tagHeaderIsExpanded,
folderHeaderIsExpanded: state.settings.folderHeaderIsExpanded,
}; };
}; };

View File

@ -9,18 +9,24 @@ export const declaration: CommandDeclaration = {
parentLabel: () => _('Focus'), parentLabel: () => _('Focus'),
}; };
export const runtime = (comp: any): CommandRuntime => { export interface RuntimeProps {
getSelectedItem(): any;
getFirstAnchorItemRef(type: string): any;
anchorItemRefs: any;
}
export const runtime = (props: RuntimeProps): CommandRuntime => {
return { return {
execute: async (context: CommandContext) => { execute: async (context: CommandContext) => {
const sidebarVisible = layoutItemProp((context.state as AppState).mainLayout, 'sideBar', 'visible'); const sidebarVisible = layoutItemProp((context.state as AppState).mainLayout, 'sideBar', 'visible');
if (sidebarVisible) { if (sidebarVisible) {
const item = comp.selectedItem(); const item = props.getSelectedItem();
if (item) { if (item) {
const anchorRef = comp.anchorItemRefs[item.type][item.id]; const anchorRef = props.anchorItemRefs.current[item.type][item.id];
if (anchorRef) anchorRef.current.focus(); if (anchorRef) anchorRef.current.focus();
} else { } else {
const anchorRef = comp.firstAnchorItemRef('folder'); const anchorRef = props.getFirstAnchorItemRef('folder');
if (anchorRef) anchorRef.current.focus(); if (anchorRef) anchorRef.current.focus();
} }
} }