1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-13 00:10:37 +02:00

Desktop: Fixes #3904, Fixes #3973: Fixed inconsistent note list state when using search

This commit is contained in:
Laurent Cozic
2020-11-09 12:07:37 +00:00
parent ca8b05631a
commit d0ec598ee4
6 changed files with 84 additions and 73 deletions

View File

@ -1,47 +1,18 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
const BaseModel = require('@joplin/lib/BaseModel').default;
const uuid = require('@joplin/lib/uuid').default;
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'search', name: 'search',
iconName: 'icon-search', iconName: 'icon-search',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (_context:CommandContext, query:string) => { execute: async () => {
if (!comp.searchId_) comp.searchId_ = uuid.create(); // Doesn't do anything for now but could be used to
// automatically focus the search field (using the
comp.props.dispatch({ // `focusSearch` command) and then set an initial search.
type: 'SEARCH_UPDATE', // However not straightforward to implement since the SearchBar
search: { // is not a controlled component.
id: comp.searchId_,
title: query,
query_pattern: query,
query_folder_id: null,
type_: BaseModel.TYPE_SEARCH,
},
});
if (query) {
comp.props.dispatch({
type: 'SEARCH_SELECT',
id: comp.searchId_,
});
} else {
// Note: Normally there's no need to do anything when the search query
// is cleared as the reducer should handle all state changes.
// https://github.com/laurent22/joplin/issues/3748
// const note = await Note.load(comp.props.selectedNoteId);
// if (note) {
// comp.props.dispatch({
// type: 'FOLDER_AND_NOTE_SELECT',
// folderId: note.parent_id,
// noteId: note.id,
// });
// }
}
}, },
}; };
}; };

View File

@ -7,6 +7,8 @@ const Resource = require('@joplin/lib/models/Resource');
// handler on the webContent. This function will return null if the point is // handler on the webContent. This function will return null if the point is
// not within the TinyMCE editor. // not within the TinyMCE editor.
function contextMenuElement(editor:any, x:number, y:number) { function contextMenuElement(editor:any, x:number, y:number) {
if (!editor || !editor.getDoc()) return null;
const iframes = document.getElementsByClassName('tox-edit-area__iframe'); const iframes = document.getElementsByClassName('tox-edit-area__iframe');
if (!iframes.length) return null; if (!iframes.length) return null;
@ -15,7 +17,6 @@ function contextMenuElement(editor:any, x:number, y:number) {
if (iframeRect.x < x && iframeRect.y < y && iframeRect.right > x && iframeRect.bottom > y) { if (iframeRect.x < x && iframeRect.y < y && iframeRect.right > x && iframeRect.bottom > y) {
const relativeX = x - iframeRect.x; const relativeX = x - iframeRect.x;
const relativeY = y - iframeRect.y; const relativeY = y - iframeRect.y;
return editor.getDoc().elementFromPoint(relativeX, relativeY); return editor.getDoc().elementFromPoint(relativeX, relativeY);
} }

View File

@ -1,23 +1,83 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
import useSearch from './hooks/useSearch';
import { Root, SearchInput, SearchButton, SearchButtonIcon } from './styles'; import { Root, SearchInput, SearchButton, SearchButtonIcon } from './styles';
import Setting from '@joplin/lib/models/Setting';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { stateUtils } from '@joplin/lib/reducer';
import BaseModel from '@joplin/lib/BaseModel';
import uuid from '@joplin/lib/uuid';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const Note = require('@joplin/lib/models/Note');
const debounce = require('debounce');
interface Props { interface Props {
inputRef?: any, inputRef?: any,
notesParentType: string, notesParentType: string,
dispatch?: Function, dispatch?: Function,
selectedNoteId: string,
} }
function SearchBar(props:Props) { function SearchBar(props:Props) {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const iconName = !query ? CommandService.instance().iconName('search') : 'fa fa-times'; const [searchStarted, setSearchStarted] = useState(false);
const iconName = !searchStarted ? CommandService.instance().iconName('search') : 'fa fa-times';
const searchId = useRef(uuid.create());
useEffect(() => {
function search(searchId:string, query:string, dispatch:Function) {
dispatch({
type: 'SEARCH_UPDATE',
search: {
id: searchId,
title: query,
query_pattern: query,
query_folder_id: null,
type_: BaseModel.TYPE_SEARCH,
},
});
dispatch({
type: 'SEARCH_SELECT',
id: searchId,
});
}
const debouncedSearch = debounce(search, 500);
if (searchStarted) debouncedSearch(searchId.current, query, props.dispatch);
return () => {
debouncedSearch.clear();
};
}, [query, searchStarted]);
const onExitSearch = useCallback(async (navigateAway = true) => {
setQuery('');
setSearchStarted(false);
if (navigateAway) {
const note = props.selectedNoteId ? await Note.load(props.selectedNoteId) : null;
if (note) {
props.dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: note.parent_id,
noteId: note.id,
});
} else {
const folderId = Setting.value('activeFolderId');
if (folderId) {
props.dispatch({
type: 'FOLDER_SELECT',
id: folderId,
});
}
}
}
}, [props.selectedNoteId]);
function onChange(event:any) { function onChange(event:any) {
setSearchStarted(true);
setQuery(event.currentTarget.value); setQuery(event.currentTarget.value);
} }
@ -40,24 +100,22 @@ function SearchBar(props:Props) {
}, 300); }, 300);
} }
function onKeyDown(event:any) { const onKeyDown = useCallback((event:any) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
setQuery('');
if (document.activeElement) (document.activeElement as any).blur(); if (document.activeElement) (document.activeElement as any).blur();
onExitSearch();
} }
} }, [onExitSearch]);
const onSearchButtonClick = useCallback(() => { const onSearchButtonClick = useCallback(() => {
setQuery(''); onExitSearch();
}, []); }, [onExitSearch]);
useSearch(query);
useEffect(() => { useEffect(() => {
if (props.notesParentType !== 'Search') { if (props.notesParentType !== 'Search') {
setQuery(''); onExitSearch(false);
} }
}, [props.notesParentType]); }, [props.notesParentType, onExitSearch]);
return ( return (
<Root> <Root>
@ -81,6 +139,7 @@ function SearchBar(props:Props) {
const mapStateToProps = (state:any) => { const mapStateToProps = (state:any) => {
return { return {
notesParentType: state.notesParentType, notesParentType: state.notesParentType,
selectedNoteId: stateUtils.selectedNoteId(state),
}; };
}; };

View File

@ -1,17 +0,0 @@
import { useEffect } from 'react';
import CommandService from '@joplin/lib/services/CommandService';
const debounce = require('debounce');
export default function useSearch(query:string) {
useEffect(() => {
const search = debounce((query:string) => {
CommandService.instance().execute('search', query);
}, 500);
search(query);
return () => {
search.clear();
};
}, [query]);
}

View File

@ -1019,12 +1019,7 @@ const reducer = produce((draft: Draft<State> = defaultState, action:any) => {
if (!found) searches.push(action.search); if (!found) searches.push(action.search);
if (!action.search.query_pattern) {
draft.notesParentType = defaultNotesParentType(draft, 'Search');
} else {
draft.notesParentType = 'Search'; draft.notesParentType = 'Search';
}
draft.searches = searches; draft.searches = searches;
} }
break; break;

View File

@ -672,6 +672,8 @@ class SearchEngine {
} }
async search(searchString, options = null) { async search(searchString, options = null) {
if (!searchString) return [];
options = Object.assign({}, { options = Object.assign({}, {
searchType: SearchEngine.SEARCH_TYPE_AUTO, searchType: SearchEngine.SEARCH_TYPE_AUTO,
fuzzy: Setting.value('db.fuzzySearchEnabled') === 1, fuzzy: Setting.value('db.fuzzySearchEnabled') === 1,