From 1fd1a73fda662fb9df4ca18526c039d8d8b298cd Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 12 Jan 2018 19:58:01 +0000 Subject: [PATCH] Electron: Improved the way new note are created, and automatically add a title. Made saving and loading notes more reliable. --- ElectronClient/app/gui/MainScreen.jsx | 30 +---- ElectronClient/app/gui/NoteList.jsx | 10 +- ElectronClient/app/gui/NoteText.jsx | 121 +++++++++++------- .../components/shared/note-screen-shared.js | 49 +++++-- ReactNativeClient/lib/models/Note.js | 2 - ReactNativeClient/lib/reducer.js | 12 ++ 6 files changed, 134 insertions(+), 90 deletions(-) diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx index 742d7f0b8..62b79b69e 100644 --- a/ElectronClient/app/gui/MainScreen.jsx +++ b/ElectronClient/app/gui/MainScreen.jsx @@ -44,16 +44,14 @@ class MainScreenComponent extends React.Component { const folderId = Setting.value('activeFolderId'); if (!folderId) return; - const note = await Note.save({ - title: title, + const newNote = { parent_id: folderId, is_todo: isTodo ? 1 : 0, - }); - Note.updateGeolocation(note.id); + }; this.props.dispatch({ - type: 'NOTE_SELECT', - id: note.id, + type: 'NOTE_SET_NEW_ONE', + item: newNote, }); } @@ -65,30 +63,14 @@ class MainScreenComponent extends React.Component { return; } - this.setState({ - promptOptions: { - label: _('Note title:'), - onClose: async (answer) => { - if (answer) await createNewNote(answer, false); - this.setState({ promptOptions: null }); - } - }, - }); + await createNewNote(null, false); } else if (command.name === 'newTodo') { if (!this.props.folders.length) { bridge().showErrorMessageBox(_('Please create a notebook first')); return; } - this.setState({ - promptOptions: { - label: _('To-do title:'), - onClose: async (answer) => { - if (answer) await createNewNote(answer, true); - this.setState({ promptOptions: null }); - } - }, - }); + await createNewNote(null, true); } else if (command.name === 'newNotebook') { this.setState({ promptOptions: { diff --git a/ElectronClient/app/gui/NoteList.jsx b/ElectronClient/app/gui/NoteList.jsx index 3d54c2ea7..fd40ddc8f 100644 --- a/ElectronClient/app/gui/NoteList.jsx +++ b/ElectronClient/app/gui/NoteList.jsx @@ -146,7 +146,10 @@ class NoteListComponent extends React.Component { const hPadding = 10; let style = Object.assign({ width: width }, this.style().listItem); - if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected); + + if (this.props.selectedNoteIds.indexOf(item.id) >= 0) { + style = Object.assign(style, this.style().listItemSelected); + } // Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows // but don't know how it will look in other OSes. @@ -182,8 +185,9 @@ class NoteListComponent extends React.Component { render() { const theme = themeStyle(this.props.theme); const style = this.props.style; + let notes = this.props.notes.slice(); - if (!this.props.notes.length) { + if (!notes.length) { const padding = 10; const emptyDivStyle = Object.assign({ padding: padding + 'px', @@ -202,7 +206,7 @@ class NoteListComponent extends React.Component { itemHeight={this.style().listItem.height} style={style} className={"note-list"} - items={this.props.notes} + items={notes} itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } } > ); diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index cc56b8b97..90416959b 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -36,7 +36,13 @@ class NoteTextComponent extends React.Component { isLoading: true, webviewReady: false, scrollHeight: null, - editorScrollTop: 0 + editorScrollTop: 0, + newNote: null, + + // If the current note was just created, and the title has never been + // changed by the user, this variable contains that note ID. Used + // to automatically set the title. + newAndNoTitleChangeNoteId: null, }; this.lastLoadedNoteId_ = null; @@ -75,7 +81,10 @@ class NoteTextComponent extends React.Component { async componentWillMount() { let note = null; - if (this.props.noteId) { + + if (this.props.newNote) { + note = Object.assign({}, this.props.newNote); + } else if (this.props.noteId) { note = await Note.load(this.props.noteId); } @@ -114,7 +123,14 @@ class NoteTextComponent extends React.Component { } async saveOneProperty(name, value) { - await shared.saveOneProperty(this, name, value); + if (this.state.note && !this.state.note.id) { + const note = Object.assign({}, this.state.note); + note[name] = value; + this.setState({ note: note }); + this.scheduleSave(); + } else { + await shared.saveOneProperty(this, name, value); + } } scheduleSave() { @@ -130,19 +146,30 @@ class NoteTextComponent extends React.Component { await this.saveIfNeeded(); - const stateNoteId = this.state.note ? this.state.note.id : null; - const noteId = props.noteId; - let loadingNewNote = stateNoteId !== noteId; - this.lastLoadedNoteId_ = noteId; - const note = noteId ? await Note.load(noteId) : null; - if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading - if (options.noReloadIfLocalChanges && this.isModified()) return; + const previousNote = this.state.note ? Object.assign({}, this.state.note) : null; - // If the note hasn't been changed, exit now - if (this.state.note && note) { - let diff = Note.diffObjects(this.state.note, note); - delete diff.type_; - if (!Object.getOwnPropertyNames(diff).length) return; + const stateNoteId = this.state.note ? this.state.note.id : null; + let noteId = null; + let note = null; + let loadingNewNote = true; + + if (props.newNote) { + note = Object.assign({}, props.newNote); + this.lastLoadedNoteId_ = null; + } else { + noteId = props.noteId; + loadingNewNote = stateNoteId !== noteId; + this.lastLoadedNoteId_ = noteId; + note = noteId ? await Note.load(noteId) : null; + if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading + if (options.noReloadIfLocalChanges && this.isModified()) return; + + // If the note hasn't been changed, exit now + if (this.state.note && note) { + let diff = Note.diffObjects(this.state.note, note); + delete diff.type_; + if (!Object.getOwnPropertyNames(diff).length) return; + } } this.mdToHtml_ = null; @@ -150,7 +177,7 @@ class NoteTextComponent extends React.Component { // If we are loading nothing (noteId == null), make sure to // set webviewReady to false too because the webview component // is going to be removed in render(). - const webviewReady = this.webview_ && this.state.webviewReady && noteId; + const webviewReady = this.webview_ && this.state.webviewReady && (noteId || props.newNote); // Scroll back to top when loading new note if (loadingNewNote) { @@ -162,26 +189,41 @@ class NoteTextComponent extends React.Component { // https://github.com/ajaxorg/ace/issues/2195 this.editorSetScrollTop(1); this.restoreScrollTop_ = 0; - } - this.setState({ - note: note, - lastSavedNote: Object.assign({}, note), - webviewReady: webviewReady, - }); - } - - async componentWillReceiveProps(nextProps) { - if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) { - await this.reloadNote(nextProps); - if (this.editor_){ + if (this.editor_) { const session = this.editor_.editor.getSession(); const undoManager = session.getUndoManager(); undoManager.reset(); session.setUndoManager(undoManager); + + this.editor_.editor.focus(); + this.editor_.editor.clearSelection(); + this.editor_.editor.moveCursorTo(0,0); } } + let newState = { + note: note, + lastSavedNote: Object.assign({}, note), + webviewReady: webviewReady, + }; + + if (!note) { + newState.newAndNoTitleChangeNoteId = null; + } else if (note.id !== this.state.newAndNoTitleChangeNoteId) { + newState.newAndNoTitleChangeNoteId = null; + } + + this.setState(newState); + } + + async componentWillReceiveProps(nextProps) { + if (nextProps.newNote) { + await this.reloadNote(nextProps); + } else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) { + await this.reloadNote(nextProps); + } + if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) { await this.reloadNote(nextProps, { noReloadIfLocalChanges: true }); } @@ -197,6 +239,7 @@ class NoteTextComponent extends React.Component { title_changeText(event) { shared.noteComponent_change(this, 'title', event.target.value); + this.setState({ newAndNoTitleChangeNoteId: null }); this.scheduleSave(); } @@ -404,20 +447,10 @@ class NoteTextComponent extends React.Component { menu.popup(bridge().window()); } - // shouldComponentUpdate(nextProps, nextState) { - // //console.info('NEXT PROPS', JSON.stringify(nextProps)); - // console.info('NEXT STATE ===================='); - // for (var n in nextProps) { - // if (!nextProps.hasOwnProperty(n)) continue; - // console.info(n + ' = ' + (nextProps[n] === this.props[n])); - // } - // return true; - // } - render() { const style = this.props.style; const note = this.state.note; - const body = note ? note.body : ''; + const body = note && note.body ? note.body : ''; const theme = themeStyle(this.props.theme); const visiblePanes = this.props.visiblePanes || ['editor', 'viewer']; @@ -521,13 +554,6 @@ class NoteTextComponent extends React.Component { const toolbarItems = []; - // toolbarItems.push({ - // title: _('Save'), - // iconName: 'fa-save', - // enabled: this.isModified(), - // onClick: () => { }, - // }); - toolbarItems.push({ title: _('Attach file'), iconName: 'fa-paperclip', @@ -551,7 +577,7 @@ class NoteTextComponent extends React.Component { const titleEditor = { this.title_changeText(event); }} /> @@ -619,6 +645,7 @@ const mapStateToProps = (state) => { theme: state.settings.theme, showAdvancedOptions: state.settings.showAdvancedOptions, syncStarted: state.syncStarted, + newNote: state.newNote, }; }; diff --git a/ReactNativeClient/lib/components/shared/note-screen-shared.js b/ReactNativeClient/lib/components/shared/note-screen-shared.js index ca13b1e5e..6b3080cb4 100644 --- a/ReactNativeClient/lib/components/shared/note-screen-shared.js +++ b/ReactNativeClient/lib/components/shared/note-screen-shared.js @@ -24,25 +24,29 @@ shared.saveNoteButton_press = async function(comp) { } let isNew = !note.id; - let titleWasAutoAssigned = false; - if (isNew && !note.title) { - note.title = Note.defaultTitle(note); - titleWasAutoAssigned = true; - } - - let options = {}; + let options = { userSideValidation: true }; if (!isNew) { options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note); } - const savedNote = ('fields' in options) && !options.fields.length ? Object.assign({}, note) : await Note.save(note, { userSideValidation: true }); + const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title); + if (hasAutoTitle) { + note.title = Note.defaultTitle(note); + if (options.fields && options.fields.indexOf('title') < 0) options.fields.push('title'); + } + + const savedNote = ('fields' in options) && !options.fields.length ? Object.assign({}, note) : await Note.save(note, options); const stateNote = comp.state.note; + + // Note was reloaded while being saved. + if (!isNew && (!stateNote || stateNote.id !== savedNote.id)) return; + // Re-assign any property that might have changed during saving (updated_time, etc.) note = Object.assign(note, savedNote); - if (stateNote) { + if (stateNote.id === note.id) { // But we preserve the current title and body because // the user might have changed them between the time // saveNoteButton_press was called and the note was @@ -50,17 +54,30 @@ shared.saveNoteButton_press = async function(comp) { // // If the title was auto-assigned above, we don't restore // it from the state because it will be empty there. - if (!titleWasAutoAssigned) note.title = stateNote.title; + if (!hasAutoTitle) note.title = stateNote.title; note.body = stateNote.body; } - comp.setState({ + let newState = { lastSavedNote: Object.assign({}, note), note: note, - }); + }; + + if (isNew) newState.newAndNoTitleChangeNoteId = note.id; + + comp.setState(newState); if (isNew) Note.updateGeolocation(note.id); comp.refreshNoteMetadata(); + + if (isNew) { + // Clear the newNote item now that the note has been saved, and + // make sure that the note we're editing is selected. + comp.props.dispatch({ + type: 'NOTE_SELECT', + id: savedNote.id, + }); + } } shared.saveOneProperty = async function(comp, name, value) { @@ -89,9 +106,13 @@ shared.saveOneProperty = async function(comp, name, value) { } shared.noteComponent_change = function(comp, propName, propValue) { + let newState = {} + let note = Object.assign({}, comp.state.note); note[propName] = propValue; - comp.setState({ note: note }); + newState.note = note; + + comp.setState(newState); } shared.refreshNoteMetadata = async function(comp, force = null) { @@ -103,7 +124,7 @@ shared.refreshNoteMetadata = async function(comp, force = null) { shared.isModified = function(comp) { if (!comp.state.note || !comp.state.lastSavedNote) return false; - let diff = BaseModel.diffObjects(comp.state.note, comp.state.lastSavedNote); + let diff = BaseModel.diffObjects(comp.state.lastSavedNote, comp.state.note); delete diff.type_; return !!Object.getOwnPropertyNames(diff).length; } diff --git a/ReactNativeClient/lib/models/Note.js b/ReactNativeClient/lib/models/Note.js index 71bc5a377..dedafab0c 100644 --- a/ReactNativeClient/lib/models/Note.js +++ b/ReactNativeClient/lib/models/Note.js @@ -69,8 +69,6 @@ class Note extends BaseItem { } static defaultTitle(note) { - if (note.title && note.title.length) return note.title; - if (note.body && note.body.length) { const lines = note.body.trim().split("\n"); return lines[0].trim().substr(0, 80).trim(); diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js index 90c95fbb9..4e121ec0f 100644 --- a/ReactNativeClient/lib/reducer.js +++ b/ReactNativeClient/lib/reducer.js @@ -29,6 +29,7 @@ const defaultState = { appState: 'starting', //windowContentSize: { width: 0, height: 0 }, hasDisabledSyncItems: false, + newNote: null, }; function arrayHasEncryptedItems(array) { @@ -144,12 +145,14 @@ function changeSelectedNotes(state, action) { if (action.type === 'NOTE_SELECT') { newState.selectedNoteIds = noteIds; + newState.newNote = null; return newState; } if (action.type === 'NOTE_SELECT_ADD') { if (!noteIds.length) return state; newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds)); + newState.newNote = null; return newState; } @@ -164,6 +167,7 @@ function changeSelectedNotes(state, action) { newSelectedNoteIds.push(id); } newState.selectedNoteIds = newSelectedNoteIds; + newState.newNote = null; return newState; } @@ -177,6 +181,8 @@ function changeSelectedNotes(state, action) { newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] }); } + newState.newNote = null; + return newState; } @@ -455,6 +461,12 @@ const reducer = (state = defaultState, action) => { newState.hasDisabledSyncItems = true; break; + case 'NOTE_SET_NEW_ONE': + + newState = Object.assign({}, state); + newState.newNote = action.item; + break; + } } catch (error) { error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);