diff --git a/.eslintignore b/.eslintignore index 4b2edebc9..e653d4d79 100644 --- a/.eslintignore +++ b/.eslintignore @@ -180,6 +180,7 @@ ElectronClient/gui/SideBar/styles/index.js ElectronClient/gui/StatusScreen/StatusScreen.js ElectronClient/gui/style/StyledInput.js ElectronClient/gui/style/StyledTextInput.js +ElectronClient/gui/TagList.js ElectronClient/gui/ToggleEditorsButton/styles/index.js ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js ElectronClient/gui/ToolbarBase.js diff --git a/.gitignore b/.gitignore index 624e6d3f0..1e7bf7286 100644 --- a/.gitignore +++ b/.gitignore @@ -174,6 +174,7 @@ ElectronClient/gui/SideBar/styles/index.js ElectronClient/gui/StatusScreen/StatusScreen.js ElectronClient/gui/style/StyledInput.js ElectronClient/gui/style/StyledTextInput.js +ElectronClient/gui/TagList.js ElectronClient/gui/ToggleEditorsButton/styles/index.js ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js ElectronClient/gui/ToolbarBase.js diff --git a/.ignore b/.ignore index d50e97936..f79a42ff8 100644 --- a/.ignore +++ b/.ignore @@ -123,6 +123,7 @@ ElectronClient/gui/SideBar/styles/index.js ElectronClient/gui/StatusScreen/StatusScreen.js ElectronClient/gui/style/StyledInput.js ElectronClient/gui/style/StyledTextInput.js +ElectronClient/gui/TagList.js ElectronClient/gui/ToggleEditorsButton/styles/index.js ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js ElectronClient/gui/ToolbarBase.js diff --git a/ElectronClient/gui/NoteEditor/NoteEditor.tsx b/ElectronClient/gui/NoteEditor/NoteEditor.tsx index a1708c440..3b3da5842 100644 --- a/ElectronClient/gui/NoteEditor/NoteEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteEditor.tsx @@ -26,6 +26,7 @@ import { AppState } from '../../app'; import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils'; import { _ } from 'lib/locale'; import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext'; +import TagList from '../TagList'; const { themeStyle } = require('lib/theme'); const { substrWithEllipsis } = require('lib/string-utils'); @@ -39,7 +40,6 @@ const Note = require('lib/models/Note.js'); const bridge = require('electron').remote.require('./bridge').default; const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const NoteRevisionViewer = require('../NoteRevisionViewer.min'); -const TagList = require('../TagList.min.js'); const commands = [ require('./commands/showRevisions'), diff --git a/ElectronClient/gui/Root.tsx b/ElectronClient/gui/Root.tsx index e96149722..5ceb62c3a 100644 --- a/ElectronClient/gui/Root.tsx +++ b/ElectronClient/gui/Root.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import app from '../app'; import MainScreen from './MainScreen/MainScreen'; import ConfigScreen from './ConfigScreen/ConfigScreen'; @@ -10,6 +9,7 @@ import { themeStyle } from 'lib/theme'; import { Size } from './ResizableLayout/ResizableLayout'; import MenuBar from './MenuBar'; import { _ } from 'lib/locale'; +const React = require('react'); const { render } = require('react-dom'); const { connect, Provider } = require('react-redux'); diff --git a/ElectronClient/gui/SideBar/SideBar.tsx b/ElectronClient/gui/SideBar/SideBar.tsx index a42b58b64..e1c53cbcb 100644 --- a/ElectronClient/gui/SideBar/SideBar.tsx +++ b/ElectronClient/gui/SideBar/SideBar.tsx @@ -569,6 +569,20 @@ class SideBarComponent extends React.Component { CommandService.instance().execute('newFolder'); } + // 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); + // } + // } + // } + render() { const theme = themeStyle(this.props.themeId); diff --git a/ElectronClient/gui/TagList.jsx b/ElectronClient/gui/TagList.jsx deleted file mode 100644 index ff87b9a38..000000000 --- a/ElectronClient/gui/TagList.jsx +++ /dev/null @@ -1,52 +0,0 @@ -const React = require('react'); -const { connect } = require('react-redux'); -const { themeStyle } = require('lib/theme'); -const TagItem = require('./TagItem.min.js'); - -class TagListComponent extends React.Component { - render() { - const style = Object.assign({}, this.props.style); - const theme = themeStyle(this.props.themeId); - const tags = this.props.items; - - style.display = 'flex'; - style.flexDirection = 'row'; - // style.borderBottom = `1px solid ${theme.dividerColor}`; - style.boxSizing = 'border-box'; - style.fontSize = theme.fontSize; - style.whiteSpace = 'nowrap'; - // style.height = 40; - style.paddingTop = 8; - style.paddingBottom = 8; - - const tagItems = []; - if (tags && tags.length > 0) { - - tags.sort((a, b) => { - return a.title < b.title ? -1 : +1; - }); - - for (let i = 0; i < tags.length; i++) { - const props = { - title: tags[i].title, - key: tags[i].id, - }; - tagItems.push(); - } - } - - return ( -
- {tagItems} -
- ); - } -} - -const mapStateToProps = state => { - return { themeId: state.settings.theme }; -}; - -const TagList = connect(mapStateToProps)(TagListComponent); - -module.exports = TagList; diff --git a/ElectronClient/gui/TagList.tsx b/ElectronClient/gui/TagList.tsx new file mode 100644 index 000000000..6bb0c2293 --- /dev/null +++ b/ElectronClient/gui/TagList.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { AppState } from '../app'; + +const { connect } = require('react-redux'); +const { themeStyle } = require('lib/theme'); +const TagItem = require('./TagItem.min.js'); + +interface Props { + themeId: number, + style: any, + items: any[], +} + +function TagList(props:Props) { + const style = useMemo(() => { + const theme = themeStyle(props.themeId); + + const output = { ...props.style }; + output.display = 'flex'; + output.flexDirection = 'row'; + output.boxSizing = 'border-box'; + output.fontSize = theme.fontSize; + output.whiteSpace = 'nowrap'; + output.paddingTop = 8; + output.paddingBottom = 8; + return output; + }, [props.style, props.themeId]); + + const tags = useMemo(() => { + const output = props.items.slice(); + + output.sort((a:any, b:any) => { + return a.title < b.title ? -1 : +1; + }); + + return output; + }, [props.items]); + + const tagItems = useMemo(() => { + const output = []; + for (let i = 0; i < tags.length; i++) { + const props = { + title: tags[i].title, + key: tags[i].id, + }; + output.push(); + } + return output; + }, [tags]); + + return ( +
+ {tagItems} +
+ ); +} + +const mapStateToProps = (state:AppState) => { + return { themeId: state.settings.theme }; +}; + +export default connect(mapStateToProps)(TagList); diff --git a/ReactNativeClient/lib/BaseApplication.ts b/ReactNativeClient/lib/BaseApplication.ts index c383f30d2..aad06a824 100644 --- a/ReactNativeClient/lib/BaseApplication.ts +++ b/ReactNativeClient/lib/BaseApplication.ts @@ -546,7 +546,13 @@ export default class BaseApplication { await this.refreshNotes(newState, refreshNotesUseSelectedNoteId, refreshNotesHash); } - if (action.type === 'NOTE_UPDATE_ONE' || action.type === 'NOTE_DELETE') { + if (action.type === 'NOTE_UPDATE_ONE') { + if (!action.changedFields.length || action.changedFields.includes('parent_id') || action.changedFields.includes('encryption_applied')) { + refreshFolders = true; + } + } + + if (action.type === 'NOTE_DELETE') { refreshFolders = true; } diff --git a/ReactNativeClient/lib/components/SelectDateTimeDialog.tsx b/ReactNativeClient/lib/components/SelectDateTimeDialog.tsx index f528c39fc..7ec75c7ac 100644 --- a/ReactNativeClient/lib/components/SelectDateTimeDialog.tsx +++ b/ReactNativeClient/lib/components/SelectDateTimeDialog.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { View, Button, Text } from 'react-native'; import { themeStyle } from 'lib/theme'; import { _ } from 'lib/locale'; +const { View, Button, Text } = require('react-native'); const PopupDialog = require('react-native-popup-dialog').default; const { DialogTitle, DialogButton } = require('react-native-popup-dialog'); diff --git a/ReactNativeClient/lib/components/shared/reduxSharedMiddleware.js b/ReactNativeClient/lib/components/shared/reduxSharedMiddleware.js index a324c2f40..7e764650f 100644 --- a/ReactNativeClient/lib/components/shared/reduxSharedMiddleware.js +++ b/ReactNativeClient/lib/components/shared/reduxSharedMiddleware.js @@ -43,8 +43,11 @@ const reduxSharedMiddleware = async function(store, next, action) { DecryptionWorker.instance().scheduleStart(); } + // 2020-10-19: Removed "NOTE_UPDATE_ONE" because there's no property in a note that + // should trigger a refreshing of the tags. + // Trying to fix this: https://github.com/laurent22/joplin/issues/3893 if (action.type == 'NOTE_DELETE' || - action.type == 'NOTE_UPDATE_ONE' || + // action.type == 'NOTE_UPDATE_ONE' || action.type == 'NOTE_UPDATE_ALL' || action.type == 'NOTE_TAG_REMOVE' || action.type == 'TAG_UPDATE_ONE') { diff --git a/ReactNativeClient/lib/models/Note.js b/ReactNativeClient/lib/models/Note.js index 7902ac7ef..4409d1a21 100644 --- a/ReactNativeClient/lib/models/Note.js +++ b/ReactNativeClient/lib/models/Note.js @@ -587,10 +587,29 @@ class Note extends BaseItem { // decide what to keep and what to ignore, but in practice keeping the previous content is a bit // heavy - the note needs to be reloaded here, the JSON blob needs to be saved, etc. // So the check for old note here is basically an optimisation. + + // 2020-10-19: It's not ideal to reload the previous version of the note before saving it again + // but it should be relatively fast anyway. This is so that code that listens to the NOTE_UPDATE_ONE + // action can decide what to do based on the fields that have been modified. + // This is necessary for example so that the folder list is not refreshed every time a note is changed. + // Now it can look at the properties and refresh only if the "parent_id" property is changed. + // Trying to fix: https://github.com/laurent22/joplin/issues/3893 + const oldNote = await Note.load(o.id); + let beforeNoteJson = null; if (!isNew && this.revisionService().isOldNote(o.id)) { - beforeNoteJson = await Note.load(o.id); - if (beforeNoteJson) beforeNoteJson = JSON.stringify(beforeNoteJson); + if (oldNote) beforeNoteJson = JSON.stringify(oldNote); + } + + const changedFields = []; + + if (oldNote) { + for (const field in o) { + if (!o.hasOwnProperty(field)) continue; + if (o[field] !== oldNote[field]) { + changedFields.push(field); + } + } } const note = await super.save(o, options); @@ -603,6 +622,7 @@ class Note extends BaseItem { type: 'NOTE_UPDATE_ONE', note: note, provisional: isProvisional, + changedFields: changedFields, }); } diff --git a/ReactNativeClient/lib/models/Tag.js b/ReactNativeClient/lib/models/Tag.js index 11dd720c4..7eb8ef470 100644 --- a/ReactNativeClient/lib/models/Tag.js +++ b/ReactNativeClient/lib/models/Tag.js @@ -176,7 +176,12 @@ class Tag extends BaseItem { } static async save(o, options = null) { - if (options && options.userSideValidation) { + options = Object.assign({}, { + dispatchUpdateAction: true, + userSideValidation: false, + }, options); + + if (options.userSideValidation) { if ('title' in o) { o.title = o.title.trim().toLowerCase(); @@ -186,10 +191,13 @@ class Tag extends BaseItem { } return super.save(o, options).then(tag => { - this.dispatch({ - type: 'TAG_UPDATE_ONE', - item: tag, - }); + if (options.dispatchUpdateAction) { + this.dispatch({ + type: 'TAG_UPDATE_ONE', + item: tag, + }); + } + return tag; }); } diff --git a/ReactNativeClient/lib/services/debug/populateDatabase.ts b/ReactNativeClient/lib/services/debug/populateDatabase.ts index 5a1a43002..aa3cb8948 100644 --- a/ReactNativeClient/lib/services/debug/populateDatabase.ts +++ b/ReactNativeClient/lib/services/debug/populateDatabase.ts @@ -1,18 +1,42 @@ const Folder = require('lib/models/Folder'); const Note = require('lib/models/Note'); +const Tag = require('lib/models/Tag'); function randomIndex(array:any[]):number { return Math.round(Math.random() * (array.length - 1)); } +function randomIndexes(arrayLength:number, count:number):number[] { + const arr = []; + while (arr.length < count) { + const r = Math.floor(Math.random() * arrayLength); + if (arr.indexOf(r) === -1) arr.push(r); + } + return arr; +} + +function randomElements(array:any[], count:number):any[] { + const indexes = randomIndexes(array.length, count); + const output = []; + for (const index of indexes) { + output.push(array[index]); + } + return output; +} + +// Use the constants below to define how many folders, notes and tags +// should be created. export default async function populateDatabase(db:any) { await db.clearForTesting(); - const folderCount = 2000; - const noteCount = 20000; + const folderCount = 200; + const noteCount = 1000; + const tagCount = 5000; + const tagsPerNote = 10; const createdFolderIds:string[] = []; const createdNoteIds:string[] = []; + const createdTagIds:string[] = []; for (let i = 0; i < folderCount; i++) { const folder:any = { @@ -32,6 +56,24 @@ export default async function populateDatabase(db:any) { console.info(`Folders: ${i} / ${folderCount}`); } + let tagBatch = []; + for (let i = 0; i < tagCount; i++) { + tagBatch.push(Tag.save({ title: `tag${i}` }, { dispatchUpdateAction: false }).then((savedTag:any) => { + createdTagIds.push(savedTag.id); + console.info(`Tags: ${i} / ${tagCount}`); + })); + + if (tagBatch.length > 1000) { + await Promise.all(tagBatch); + tagBatch = []; + } + } + + if (tagBatch.length) { + await Promise.all(tagBatch); + tagBatch = []; + } + let noteBatch = []; for (let i = 0; i < noteCount; i++) { const note:any = { title: `note${i}`, body: `This is note num. ${i}` }; @@ -53,4 +95,20 @@ export default async function populateDatabase(db:any) { await Promise.all(noteBatch); noteBatch = []; } + + let noteTagBatch = []; + for (const noteId of createdNoteIds) { + const tagIds = randomElements(createdTagIds, tagsPerNote); + noteTagBatch.push(Tag.setNoteTagsByIds(noteId, tagIds)); + + if (noteTagBatch.length > 1000) { + await Promise.all(noteTagBatch); + noteTagBatch = []; + } + } + + if (noteTagBatch.length) { + await Promise.all(noteTagBatch); + noteTagBatch = []; + } }