1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-27 10:32:58 +02:00
joplin/ReactNativeClient/lib/reducer.js

1023 lines
30 KiB
JavaScript
Raw Normal View History

const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const ArrayUtils = require('lib/ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
2017-10-07 18:30:27 +02:00
const defaultState = {
notes: [],
notesSource: '',
2017-10-07 18:30:27 +02:00
notesParentType: null,
folders: [],
tags: [],
masterKeys: [],
notLoadedMasterKeys: [],
searches: [],
2017-11-22 20:35:31 +02:00
selectedNoteIds: [],
selectedNoteHash: '',
2017-10-07 18:30:27 +02:00
selectedFolderId: null,
selectedTagId: null,
selectedSearchId: null,
selectedItemType: 'note',
lastSelectedNotesIds: {
Folder: {},
Tag: {},
Search: {},
},
2017-10-07 18:30:27 +02:00
showSideMenu: false,
screens: {},
historyCanGoBack: false,
syncStarted: false,
syncReport: {},
searchQuery: '',
2017-10-07 18:30:27 +02:00
settings: {},
sharedData: null,
appState: 'starting',
2017-12-05 20:56:39 +02:00
hasDisabledSyncItems: false,
hasDisabledEncryptionItems: false,
customCss: '',
templates: [],
collapsedFolderIds: [],
clipperServer: {
startState: 'idle',
port: null,
},
decryptionWorker: {
state: 'idle',
itemIndex: 0,
itemCount: 0,
decryptedItemCounts: {},
decryptedItemCount: 0,
skippedItemCount: 0,
},
selectedNoteTags: [],
resourceFetcher: {
toFetchCount: 0,
},
backwardHistoryNotes: [],
forwardHistoryNotes: [],
plugins: {},
provisionalNoteIds: [],
Desktop: Resolves #176: Added experimental WYSIWYG editor (#2556) * Trying to get TuiEditor to work * Tests with TinyMCE * Fixed build * Improved asset loading * Added support for Joplin source blocks * Added support for Joplin source blocks * Better integration * Make sure noteDidUpdate event is always dispatched at the right time * Minor tweaks * Fixed tests * Add support for checkboxes * Minor refactoring * Added support for file attachments * Add support for fenced code blocks * Fix new line issue on code block * Added support for Fountain scripts * Refactoring * Better handling of saving and loading notes * Fix saving and loading ntoes * Handle multi-note selection and fixed new note creation issue * Fixed newline issue in test * Fixed newline issue in test * Improve saving and loading * Improve saving and loading note * Removed undeeded prop * Fixed issue when new note being saved is incorrectly reloaded * Refactoring and improve saving of note when unmounting component * Fixed TypeScript error * Small changes * Improved further handling of saving and loading notes * Handle provisional notes and fixed various saving and loading bugs * Adding back support for HTML notes * Added support for HTML notes * Better handling of editable nodes * Preserve image HTML tag when the size is set * Handle switching between editor when the note has note finished saving * Handle templates * Handle templates * Handle loading note that is being saved * Handle note being reloaded via sync * Clean up * Clean up and improved logging * Fixed TS error * Fixed a few issues * Fixed test * Logging * Various improvements * Add blockquote support * Moved CWD operation to shim * Removed deleted files * Added support for Joplin commands
2020-03-10 01:24:57 +02:00
editorNoteStatuses: {},
isInsertingNotes: false,
2017-10-07 18:30:27 +02:00
};
const MAX_HISTORY = 200;
const stateUtils = {};
const derivedStateCache_ = {};
// Allows, for a given state, to return the same derived
// objects, to prevent unecessary updates on calling components.
const cacheEnabledOutput = (key, output) => {
2019-09-19 23:51:18 +02:00
key = `${key}_${JSON.stringify(output)}`;
if (derivedStateCache_[key]) return derivedStateCache_[key];
derivedStateCache_[key] = output;
return derivedStateCache_[key];
2019-07-29 15:43:53 +02:00
};
stateUtils.notesOrder = function(stateSettings) {
if (stateSettings['notes.sortOrder.field'] === 'order') {
return cacheEnabledOutput('notesOrder', [
{
by: 'order',
dir: 'DESC',
},
{
by: 'user_created_time',
dir: 'DESC',
},
]);
} else {
return cacheEnabledOutput('notesOrder', [
{
by: stateSettings['notes.sortOrder.field'],
dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
},
]);
}
2019-07-29 15:43:53 +02:00
};
stateUtils.foldersOrder = function(stateSettings) {
2019-07-29 15:43:53 +02:00
return cacheEnabledOutput('foldersOrder', [
{
by: stateSettings['folders.sortOrder.field'],
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
},
]);
};
stateUtils.hasNotesBeingSaved = function(state) {
for (const id in state.editorNoteStatuses) {
if (state.editorNoteStatuses[id] === 'saving') return true;
}
return false;
};
stateUtils.parentItem = function(state) {
const t = state.notesParentType;
let id = null;
if (t === 'Folder') id = state.selectedFolderId;
if (t === 'Tag') id = state.selectedTagId;
if (t === 'Search') id = state.selectedSearchId;
if (!t || !id) return null;
return { type: t, id: id };
2019-07-29 15:43:53 +02:00
};
stateUtils.lastSelectedNoteIds = function(state) {
const parent = stateUtils.parentItem(state);
if (!parent) return [];
const output = state.lastSelectedNotesIds[parent.type][parent.id];
return output ? output : [];
2019-07-29 15:43:53 +02:00
};
stateUtils.getCurrentNote = function(state) {
const selectedNoteIds = state.selectedNoteIds;
const notes = state.notes;
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
if (currNote != null) {
return {
id: currNote.id,
parent_id: currNote.parent_id,
notesParentType: state.notesParentType,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
searches: state.searches,
selectedSmartFilterId: state.selectedSmartFilterId,
};
}
}
return null;
};
function arrayHasEncryptedItems(array) {
for (let i = 0; i < array.length; i++) {
2019-07-29 15:43:53 +02:00
if (array[i].encryption_applied) return true;
}
2019-07-29 15:43:53 +02:00
return false;
}
function stateHasEncryptedItems(state) {
if (arrayHasEncryptedItems(state.notes)) return true;
if (arrayHasEncryptedItems(state.folders)) return true;
if (arrayHasEncryptedItems(state.tags)) return true;
return false;
}
function folderSetCollapsed(state, action) {
const collapsedFolderIds = state.collapsedFolderIds.slice();
const idx = collapsedFolderIds.indexOf(action.id);
if (action.collapsed) {
if (idx >= 0) return state;
collapsedFolderIds.push(action.id);
} else {
if (idx < 0) return state;
collapsedFolderIds.splice(idx, 1);
}
const newState = Object.assign({}, state);
newState.collapsedFolderIds = collapsedFolderIds;
return newState;
}
function removeAdjacentDuplicates(items) {
return items.filter((item, idx) => (idx >= 1) ? items[idx - 1].id !== item.id : true);
}
2017-11-08 23:39:07 +02:00
// When deleting a note, tag or folder
function handleItemDelete(state, action) {
const map = {
FOLDER_DELETE: ['folders', 'selectedFolderId', true],
NOTE_DELETE: ['notes', 'selectedNoteIds', false],
TAG_DELETE: ['tags', 'selectedTagId', true],
SEARCH_DELETE: ['searches', 'selectedSearchId', true],
2017-11-08 23:39:07 +02:00
};
const listKey = map[action.type][0];
const selectedItemKey = map[action.type][1];
const isSingular = map[action.type][2];
const selectedItemKeys = isSingular ? [state[selectedItemKey]] : state[selectedItemKey];
const isSelected = selectedItemKeys.includes(action.id);
const items = state[listKey];
const newItems = [];
let newSelectedIndexes = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (isSelected) {
// the selected item is deleted so select the following item
// if multiple items are selected then just use the first one
if (selectedItemKeys[0] == item.id) {
newSelectedIndexes.push(newItems.length);
}
} else {
// the selected item/s is not deleted so keep it selected
if (selectedItemKeys.includes(item.id)) {
newSelectedIndexes.push(newItems.length);
}
}
2017-11-08 23:39:07 +02:00
if (item.id == action.id) {
continue;
}
2017-11-08 23:39:07 +02:00
newItems.push(item);
}
if (newItems.length == 0) {
newSelectedIndexes = []; // no remaining items so no selection
} else if (newSelectedIndexes.length == 0) {
newSelectedIndexes.push(0); // no selection exists so select the top
} else {
// when the items at end of list are deleted then select the end
for (let i = 0; i < newSelectedIndexes.length; i++) {
if (newSelectedIndexes[i] >= newItems.length) {
newSelectedIndexes = [newItems.length - 1];
break;
}
}
}
const newState = Object.assign({}, state);
newState[listKey] = newItems;
const newIds = [];
for (let i = 0; i < newSelectedIndexes.length; i++) {
newIds.push(newItems[newSelectedIndexes[i]].id);
}
newState[selectedItemKey] = isSingular ? newIds[0] : newIds;
if ((newIds.length == 0) && newState.notesParentType !== 'Folder') {
newState.notesParentType = 'Folder';
}
return newState;
}
function updateOneItem(state, action, keyName = '') {
let itemsKey = null;
2020-03-14 01:57:34 +02:00
if (keyName) { itemsKey = keyName; } else {
if (action.type === 'TAG_UPDATE_ONE') itemsKey = 'tags';
if (action.type === 'FOLDER_UPDATE_ONE') itemsKey = 'folders';
if (action.type === 'MASTERKEY_UPDATE_ONE') itemsKey = 'masterKeys';
}
const newItems = state[itemsKey].splice(0);
const item = action.item;
2017-10-22 19:12:16 +02:00
let found = false;
2017-10-22 19:12:16 +02:00
for (let i = 0; i < newItems.length; i++) {
const n = newItems[i];
2017-10-22 19:12:16 +02:00
if (n.id == item.id) {
newItems[i] = Object.assign(newItems[i], item);
found = true;
break;
}
}
if (!found) newItems.push(item);
const newState = Object.assign({}, state);
2017-10-22 19:12:16 +02:00
newState[itemsKey] = newItems;
2017-10-22 19:12:16 +02:00
return newState;
}
function defaultNotesParentType(state, exclusion) {
let newNotesParentType = null;
if (exclusion !== 'Folder' && state.selectedFolderId) {
newNotesParentType = 'Folder';
} else if (exclusion !== 'Tag' && state.selectedTagId) {
newNotesParentType = 'Tag';
} else if (exclusion !== 'Search' && state.selectedSearchId) {
newNotesParentType = 'Search';
}
return newNotesParentType;
}
function changeSelectedFolder(state, action, options = null) {
if (!options) options = {};
const newState = Object.assign({}, state);
newState.selectedFolderId = 'folderId' in action ? action.folderId : action.id;
if (!newState.selectedFolderId) {
newState.notesParentType = defaultNotesParentType(state, 'Folder');
} else {
newState.notesParentType = 'Folder';
}
if (newState.selectedFolderId === state.selectedFolderId && newState.notesParentType === state.notesParentType) return state;
if (options.clearSelectedNoteIds) newState.selectedNoteIds = [];
return newState;
}
function recordLastSelectedNoteIds(state, noteIds) {
const newOnes = Object.assign({}, state.lastSelectedNotesIds);
const parent = stateUtils.parentItem(state);
if (!parent) return state;
newOnes[parent.type][parent.id] = noteIds.slice();
return Object.assign({}, state, {
lastSelectedNotesIds: newOnes,
});
}
function changeSelectedNotes(state, action, options = null) {
if (!options) options = {};
let noteIds = [];
if (action.id) noteIds = [action.id];
if (action.ids) noteIds = action.ids;
if (action.noteId) noteIds = [action.noteId];
2017-11-22 20:35:31 +02:00
let newState = Object.assign({}, state);
if (action.type === 'NOTE_SELECT') {
if (JSON.stringify(newState.selectedNoteIds) === JSON.stringify(noteIds)) return state;
2017-11-22 20:35:31 +02:00
newState.selectedNoteIds = noteIds;
newState.selectedNoteHash = action.hash ? action.hash : '';
} else if (action.type === 'NOTE_SELECT_ADD') {
2017-11-22 20:35:31 +02:00
if (!noteIds.length) return state;
newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
} else if (action.type === 'NOTE_SELECT_REMOVE') {
2017-11-22 20:35:31 +02:00
if (!noteIds.length) return state; // Nothing to unselect
if (state.selectedNoteIds.length <= 1) return state; // Cannot unselect the last note
const newSelectedNoteIds = [];
2017-11-22 20:35:31 +02:00
for (let i = 0; i < newState.selectedNoteIds.length; i++) {
const id = newState.selectedNoteIds[i];
if (noteIds.indexOf(id) >= 0) continue;
newSelectedNoteIds.push(id);
}
newState.selectedNoteIds = newSelectedNoteIds;
} else if (action.type === 'NOTE_SELECT_TOGGLE') {
2017-11-22 20:35:31 +02:00
if (!noteIds.length) return state;
if (newState.selectedNoteIds.indexOf(noteIds[0]) >= 0) {
newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_REMOVE', id: noteIds[0] });
2017-11-22 20:35:31 +02:00
} else {
newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] });
2017-11-22 20:35:31 +02:00
}
2019-07-29 15:43:53 +02:00
} else {
throw new Error('Unreachable');
2017-11-22 20:35:31 +02:00
}
newState = recordLastSelectedNoteIds(newState, newState.selectedNoteIds);
2019-07-29 15:43:53 +02:00
return newState;
2017-11-22 20:35:31 +02:00
}
function removeItemFromArray(array, property, value) {
for (let i = 0; i !== array.length; ++i) {
const currentItem = array[i];
if (currentItem[property] === value) {
array.splice(i, 1);
break;
}
}
return array;
}
const getContextFromHistory = (ctx) => {
const result = {};
result.notesParentType = ctx.notesParentType;
if (result.notesParentType === 'Folder') {
result.selectedFolderId = ctx.selectedFolderId;
} else if (result.notesParentType === 'Tag') {
result.selectedTagId = ctx.selectedTagId;
} else if (result.notesParentType === 'Search') {
result.selectedSearchId = ctx.selectedSearchId;
result.searches = ctx.searches;
} else if (result.notesParentType === 'SmartFilter') {
result.selectedSmartFilterId = ctx.selectedSmartFilterId;
}
return result;
};
function handleHistory(state, action) {
let newState = Object.assign({}, state);
let backwardHistoryNotes = newState.backwardHistoryNotes.slice();
let forwardHistoryNotes = newState.forwardHistoryNotes.slice();
const currentNote = stateUtils.getCurrentNote(state);
switch (action.type) {
case 'HISTORY_BACKWARD': {
const note = backwardHistoryNotes[backwardHistoryNotes.length - 1];
if (currentNote != null && (forwardHistoryNotes.length === 0 || currentNote.id != forwardHistoryNotes[forwardHistoryNotes.length - 1].id)) {
forwardHistoryNotes = forwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));
const ctx = backwardHistoryNotes[backwardHistoryNotes.length - 1];
newState = Object.assign(newState, getContextFromHistory(ctx));
backwardHistoryNotes.pop();
break;
}
case 'HISTORY_FORWARD': {
const note = forwardHistoryNotes[forwardHistoryNotes.length - 1];
if (currentNote != null && (backwardHistoryNotes.length === 0 || currentNote.id != backwardHistoryNotes[backwardHistoryNotes.length - 1].id)) {
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));
const ctx = forwardHistoryNotes[forwardHistoryNotes.length - 1];
newState = Object.assign(newState, getContextFromHistory(ctx));
forwardHistoryNotes.pop();
break;
}
case 'NOTE_SELECT':
if (currentNote != null && action.id != currentNote.id) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
// History should be free from duplicates.
if (backwardHistoryNotes != null && backwardHistoryNotes.length > 0 &&
action.id === backwardHistoryNotes[backwardHistoryNotes.length - 1].id) {
backwardHistoryNotes.pop();
}
break;
case 'TAG_SELECT':
case 'FOLDER_AND_NOTE_SELECT':
case 'FOLDER_SELECT':
if (currentNote != null) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
break;
case 'NOTE_UPDATE_ONE': {
const modNote = action.note;
backwardHistoryNotes = backwardHistoryNotes.map(note => {
if (note.id === modNote.id) {
2020-06-25 21:27:14 +02:00
return Object.assign({}, note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
}
return note;
});
forwardHistoryNotes = forwardHistoryNotes.map(note => {
if (note.id === modNote.id) {
2020-06-25 21:27:14 +02:00
return Object.assign({}, note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
}
return note;
});
break;
}
case 'SEARCH_UPDATE':
if (currentNote != null && (backwardHistoryNotes.length === 0 ||
backwardHistoryNotes[backwardHistoryNotes.length - 1].id != currentNote.id)) {
forwardHistoryNotes = [];
backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
}
break;
case 'FOLDER_DELETE':
backwardHistoryNotes = backwardHistoryNotes.filter(note => note.parent_id != action.id);
forwardHistoryNotes = forwardHistoryNotes.filter(note => note.parent_id != action.id);
backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);
break;
case 'NOTE_DELETE': {
backwardHistoryNotes = backwardHistoryNotes.filter(note => note.id != action.id);
forwardHistoryNotes = forwardHistoryNotes.filter(note => note.id != action.id);
backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);
// Fix the case where after deletion the currently selected note is also the latest in history
const selectedNoteIds = newState.selectedNoteIds;
if (selectedNoteIds.length && backwardHistoryNotes.length && backwardHistoryNotes[backwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
backwardHistoryNotes = backwardHistoryNotes.slice(0, backwardHistoryNotes.length - 1);
}
if (selectedNoteIds.length && forwardHistoryNotes.length && forwardHistoryNotes[forwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
forwardHistoryNotes = forwardHistoryNotes.slice(0, forwardHistoryNotes.length - 1);
}
break;
}
default:
// console.log('Unknown action in history reducer.' ,action.type);
return state;
}
newState.backwardHistoryNotes = backwardHistoryNotes;
newState.forwardHistoryNotes = forwardHistoryNotes;
return newState;
}
2017-10-07 18:30:27 +02:00
const reducer = (state = defaultState, action) => {
// if (!['SIDE_MENU_OPEN_PERCENT'].includes(action.type)) console.info('Action', action.type);
2017-10-07 18:30:27 +02:00
let newState = state;
// NOTE_DELETE requires post processing
if (action.type !== 'NOTE_DELETE') {
newState = handleHistory(newState, action);
}
2017-10-07 18:30:27 +02:00
try {
switch (action.type) {
case 'NOTE_SELECT':
case 'NOTE_SELECT_ADD':
case 'NOTE_SELECT_REMOVE':
case 'NOTE_SELECT_TOGGLE':
newState = changeSelectedNotes(newState, action);
break;
case 'NOTE_SELECT_EXTEND':
{
2017-10-07 23:01:03 +02:00
newState = Object.assign({}, state);
2017-11-22 20:35:31 +02:00
if (!newState.selectedNoteIds.length) {
newState.selectedNoteIds = [action.id];
} else {
const selectRangeId1 = state.selectedNoteIds[state.selectedNoteIds.length - 1];
const selectRangeId2 = action.id;
if (selectRangeId1 === selectRangeId2) return state;
const newSelectedNoteIds = state.selectedNoteIds.slice();
2017-11-22 20:35:31 +02:00
let selectionStarted = false;
for (let i = 0; i < state.notes.length; i++) {
const id = state.notes[i].id;
if (!selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
selectionStarted = true;
if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
continue;
} else if (selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
break;
}
if (selectionStarted && newSelectedNoteIds.indexOf(id) < 0) {
newSelectedNoteIds.push(id);
}
}
newState.selectedNoteIds = newSelectedNoteIds;
}
}
break;
2017-10-07 23:01:03 +02:00
case 'NOTE_SELECT_ALL':
newState = Object.assign({}, state);
newState.selectedNoteIds = newState.notes.map(n => n.id);
break;
case 'NOTE_SELECT_ALL_TOGGLE': {
newState = Object.assign({}, state);
const allSelected = state.notes.every(n => state.selectedNoteIds.includes(n.id));
2020-03-14 01:57:34 +02:00
if (allSelected) {
newState.selectedNoteIds = [];
2020-03-14 01:57:34 +02:00
} else {
newState.selectedNoteIds = newState.notes.map(n => n.id);
2020-03-14 01:57:34 +02:00
}
break;
}
case 'SMART_FILTER_SELECT':
newState = Object.assign({}, state);
newState.notesParentType = 'SmartFilter';
newState.selectedSmartFilterId = action.id;
break;
case 'FOLDER_SELECT':
newState = changeSelectedFolder(newState, action, { clearSelectedNoteIds: true });
break;
case 'FOLDER_AND_NOTE_SELECT':
{
newState = changeSelectedFolder(newState, action);
2019-07-29 15:43:53 +02:00
const noteSelectAction = Object.assign({}, action, { type: 'NOTE_SELECT' });
newState = changeSelectedNotes(newState, noteSelectAction);
}
break;
2019-07-29 15:43:53 +02:00
case 'SETTING_UPDATE_ALL':
newState = Object.assign({}, state);
newState.settings = action.settings;
break;
2017-10-07 18:30:27 +02:00
case 'SETTING_UPDATE_ONE':
{
2017-10-07 18:30:27 +02:00
newState = Object.assign({}, state);
const newSettings = Object.assign({}, state.settings);
2017-10-07 18:30:27 +02:00
newSettings[action.key] = action.value;
newState.settings = newSettings;
}
break;
2017-10-07 18:30:27 +02:00
Desktop: Resolves #176: Added experimental WYSIWYG editor (#2556) * Trying to get TuiEditor to work * Tests with TinyMCE * Fixed build * Improved asset loading * Added support for Joplin source blocks * Added support for Joplin source blocks * Better integration * Make sure noteDidUpdate event is always dispatched at the right time * Minor tweaks * Fixed tests * Add support for checkboxes * Minor refactoring * Added support for file attachments * Add support for fenced code blocks * Fix new line issue on code block * Added support for Fountain scripts * Refactoring * Better handling of saving and loading notes * Fix saving and loading ntoes * Handle multi-note selection and fixed new note creation issue * Fixed newline issue in test * Fixed newline issue in test * Improve saving and loading * Improve saving and loading note * Removed undeeded prop * Fixed issue when new note being saved is incorrectly reloaded * Refactoring and improve saving of note when unmounting component * Fixed TypeScript error * Small changes * Improved further handling of saving and loading notes * Handle provisional notes and fixed various saving and loading bugs * Adding back support for HTML notes * Added support for HTML notes * Better handling of editable nodes * Preserve image HTML tag when the size is set * Handle switching between editor when the note has note finished saving * Handle templates * Handle templates * Handle loading note that is being saved * Handle note being reloaded via sync * Clean up * Clean up and improved logging * Fixed TS error * Fixed a few issues * Fixed test * Logging * Various improvements * Add blockquote support * Moved CWD operation to shim * Removed deleted files * Added support for Joplin commands
2020-03-10 01:24:57 +02:00
case 'NOTE_PROVISIONAL_FLAG_CLEAR':
{
const newIds = ArrayUtils.removeElement(state.provisionalNoteIds, action.id);
if (newIds !== state.provisionalNoteIds) {
newState = Object.assign({}, state, { provisionalNoteIds: newIds });
}
}
break;
2017-10-07 18:30:27 +02:00
// Replace all the notes with the provided array
case 'NOTE_UPDATE_ALL':
newState = Object.assign({}, state);
newState.notes = action.notes;
newState.notesSource = action.notesSource;
break;
2017-10-07 18:30:27 +02:00
// Insert the note into the note list if it's new, or
// update it within the note array if it already exists.
case 'NOTE_UPDATE_ONE':
{
2017-10-07 18:30:27 +02:00
const modNote = action.note;
const isViewingAllNotes = (state.notesParentType === 'SmartFilter' && state.selectedSmartFilterId === ALL_NOTES_FILTER_ID);
const isViewingConflictFolder = state.notesParentType === 'Folder' && state.selectedFolderId === Folder.conflictFolderId();
2017-10-07 18:30:27 +02:00
const noteIsInFolder = function(note, folderId) {
if (note.is_conflict && isViewingConflictFolder) return true;
if (!('parent_id' in modNote) || note.parent_id == folderId) return true;
return false;
2019-07-29 15:43:53 +02:00
};
let movedNotePreviousIndex = 0;
let noteFolderHasChanged = false;
2017-10-07 18:30:27 +02:00
let newNotes = state.notes.slice();
let found = false;
2017-10-07 18:30:27 +02:00
for (let i = 0; i < newNotes.length; i++) {
const n = newNotes[i];
2017-10-07 18:30:27 +02:00
if (n.id == modNote.id) {
// Note is still in the same folder
if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
2017-10-07 18:30:27 +02:00
// Merge the properties that have changed (in modNote) into
// the object we already have.
newNotes[i] = Object.assign({}, newNotes[i]);
for (const n in modNote) {
2017-10-07 18:30:27 +02:00
if (!modNote.hasOwnProperty(n)) continue;
newNotes[i][n] = modNote[n];
}
2019-07-29 15:43:53 +02:00
} else {
// Note has moved to a different folder
2017-10-07 18:30:27 +02:00
newNotes.splice(i, 1);
noteFolderHasChanged = true;
movedNotePreviousIndex = i;
2017-10-07 18:30:27 +02:00
}
found = true;
break;
}
}
// Note was not found - if the current folder is the same as the note folder,
// add it to it.
if (!found) {
if (isViewingAllNotes || noteIsInFolder(modNote, state.selectedFolderId)) {
newNotes.push(modNote);
}
}
2017-10-07 18:30:27 +02:00
2019-10-09 21:35:13 +02:00
// newNotes = Note.sortNotes(newNotes, state.notesOrder, newState.settings.uncompletedTodosOnTop);
newNotes = Note.sortNotes(newNotes, stateUtils.notesOrder(state.settings), newState.settings.uncompletedTodosOnTop);
2017-10-07 18:30:27 +02:00
newState = Object.assign({}, state);
newState.notes = newNotes;
if (noteFolderHasChanged) {
let newIndex = movedNotePreviousIndex;
2019-07-29 15:43:53 +02:00
if (newIndex >= newNotes.length) newIndex = newNotes.length - 1;
if (!newNotes.length) newIndex = -1;
newState.selectedNoteIds = newIndex >= 0 ? [newNotes[newIndex].id] : [];
}
if (action.provisional) {
newState.provisionalNoteIds.push(modNote.id);
} else {
const idx = newState.provisionalNoteIds.indexOf(modNote.id);
if (idx >= 0) {
const t = newState.provisionalNoteIds.slice();
t.splice(idx, 1);
newState.provisionalNoteIds = t;
}
}
}
break;
2017-10-07 18:30:27 +02:00
case 'NOTE_DELETE':
{
newState = handleItemDelete(state, action);
const idx = newState.provisionalNoteIds.indexOf(action.id);
if (idx >= 0) {
const t = newState.provisionalNoteIds.slice();
t.splice(idx, 1);
newState.provisionalNoteIds = t;
}
}
break;
2017-11-08 23:39:07 +02:00
case 'NOTE_IS_INSERTING_NOTES':
if (state.isInsertingNotes !== action.value) {
newState = Object.assign({}, state);
newState.isInsertingNotes = action.value;
}
break;
case 'TAG_DELETE':
newState = handleItemDelete(state, action);
newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', action.id);
break;
2017-10-07 18:30:27 +02:00
case 'FOLDER_UPDATE_ALL':
newState = Object.assign({}, state);
newState.folders = action.items;
break;
2017-10-07 18:30:27 +02:00
case 'FOLDER_SET_COLLAPSED':
newState = folderSetCollapsed(state, action);
break;
case 'FOLDER_TOGGLE':
if (state.collapsedFolderIds.indexOf(action.id) >= 0) {
newState = folderSetCollapsed(state, Object.assign({ collapsed: false }, action));
} else {
newState = folderSetCollapsed(state, Object.assign({ collapsed: true }, action));
}
break;
case 'FOLDER_SET_COLLAPSED_ALL':
newState = Object.assign({}, state);
newState.collapsedFolderIds = action.ids.slice();
break;
case 'TAG_UPDATE_ALL':
newState = Object.assign({}, state);
newState.tags = action.items;
break;
2017-10-07 18:30:27 +02:00
case 'TAG_SELECT':
newState.selectedTagId = action.id;
if (!action.id) {
newState.notesParentType = defaultNotesParentType(state, 'Tag');
} else {
newState.notesParentType = 'Tag';
}
newState.selectedNoteIds = [];
break;
2017-10-07 18:30:27 +02:00
case 'TAG_UPDATE_ONE':
{
// We only want to update the selected note tags if the tag belongs to the currently open note
const selectedNoteHasTag = !!state.selectedNoteTags.find(tag => tag.id === action.item.id);
newState = updateOneItem(state, action);
if (selectedNoteHasTag) newState = updateOneItem(newState, action, 'selectedNoteTags');
}
break;
case 'NOTE_TAG_REMOVE':
{
newState = updateOneItem(state, action, 'tags');
const tagRemoved = action.item;
2019-07-29 15:43:53 +02:00
newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', tagRemoved.id);
}
break;
Desktop: Resolves #176: Added experimental WYSIWYG editor (#2556) * Trying to get TuiEditor to work * Tests with TinyMCE * Fixed build * Improved asset loading * Added support for Joplin source blocks * Added support for Joplin source blocks * Better integration * Make sure noteDidUpdate event is always dispatched at the right time * Minor tweaks * Fixed tests * Add support for checkboxes * Minor refactoring * Added support for file attachments * Add support for fenced code blocks * Fix new line issue on code block * Added support for Fountain scripts * Refactoring * Better handling of saving and loading notes * Fix saving and loading ntoes * Handle multi-note selection and fixed new note creation issue * Fixed newline issue in test * Fixed newline issue in test * Improve saving and loading * Improve saving and loading note * Removed undeeded prop * Fixed issue when new note being saved is incorrectly reloaded * Refactoring and improve saving of note when unmounting component * Fixed TypeScript error * Small changes * Improved further handling of saving and loading notes * Handle provisional notes and fixed various saving and loading bugs * Adding back support for HTML notes * Added support for HTML notes * Better handling of editable nodes * Preserve image HTML tag when the size is set * Handle switching between editor when the note has note finished saving * Handle templates * Handle templates * Handle loading note that is being saved * Handle note being reloaded via sync * Clean up * Clean up and improved logging * Fixed TS error * Fixed a few issues * Fixed test * Logging * Various improvements * Add blockquote support * Moved CWD operation to shim * Removed deleted files * Added support for Joplin commands
2020-03-10 01:24:57 +02:00
case 'EDITOR_NOTE_STATUS_SET':
{
const newStatuses = Object.assign({}, state.editorNoteStatuses);
newStatuses[action.id] = action.status;
newState = Object.assign({}, state, { editorNoteStatuses: newStatuses });
}
break;
case 'EDITOR_NOTE_STATUS_REMOVE':
{
const newStatuses = Object.assign({}, state.editorNoteStatuses);
delete newStatuses[action.id];
newState = Object.assign({}, state, { editorNoteStatuses: newStatuses });
}
break;
case 'FOLDER_UPDATE_ONE':
case 'MASTERKEY_UPDATE_ONE':
newState = updateOneItem(state, action);
break;
2017-10-07 18:30:27 +02:00
case 'FOLDER_DELETE':
newState = handleItemDelete(newState, action);
break;
2017-10-07 18:30:27 +02:00
case 'MASTERKEY_UPDATE_ALL':
newState = Object.assign({}, state);
newState.masterKeys = action.items;
break;
case 'MASTERKEY_SET_NOT_LOADED':
newState = Object.assign({}, state);
newState.notLoadedMasterKeys = action.ids;
break;
case 'MASTERKEY_ADD_NOT_LOADED':
{
if (state.notLoadedMasterKeys.indexOf(action.id) < 0) {
newState = Object.assign({}, state);
const keys = newState.notLoadedMasterKeys.slice();
keys.push(action.id);
newState.notLoadedMasterKeys = keys;
}
}
break;
case 'MASTERKEY_REMOVE_NOT_LOADED':
{
const ids = action.id ? [action.id] : action.ids;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const index = state.notLoadedMasterKeys.indexOf(id);
if (index >= 0) {
newState = Object.assign({}, state);
const keys = newState.notLoadedMasterKeys.slice();
keys.splice(index, 1);
newState.notLoadedMasterKeys = keys;
}
}
}
break;
case 'SYNC_STARTED':
newState = Object.assign({}, state);
newState.syncStarted = true;
break;
2017-10-07 18:30:27 +02:00
case 'SYNC_COMPLETED':
newState = Object.assign({}, state);
newState.syncStarted = false;
break;
2017-10-07 18:30:27 +02:00
case 'SYNC_REPORT_UPDATE':
newState = Object.assign({}, state);
newState.syncReport = action.report;
break;
2017-10-07 18:30:27 +02:00
case 'SEARCH_QUERY':
newState = Object.assign({}, state);
newState.searchQuery = action.query.trim();
break;
2017-10-07 18:30:27 +02:00
case 'SEARCH_ADD':
{
newState = Object.assign({}, state);
const searches = newState.searches.slice();
searches.push(action.search);
newState.searches = searches;
}
break;
case 'SEARCH_UPDATE':
{
const searches = newState.searches.slice();
let found = false;
for (let i = 0; i < searches.length; i++) {
if (searches[i].id === action.search.id) {
searches[i] = Object.assign({}, action.search);
found = true;
break;
}
}
if (!found) searches.push(action.search);
if (!action.search.query_pattern) {
newState.notesParentType = defaultNotesParentType(state, 'Search');
} else {
newState.notesParentType = 'Search';
}
newState.searches = searches;
}
break;
case 'SEARCH_DELETE':
newState = handleItemDelete(state, action);
break;
case 'SEARCH_SELECT':
newState = Object.assign({}, state);
newState.selectedSearchId = action.id;
if (!action.id) {
newState.notesParentType = defaultNotesParentType(state, 'Search');
} else {
newState.notesParentType = 'Search';
}
newState.selectedNoteIds = [];
break;
case 'APP_STATE_SET':
newState = Object.assign({}, state);
newState.appState = action.state;
break;
2017-10-07 18:30:27 +02:00
case 'SYNC_HAS_DISABLED_SYNC_ITEMS':
newState = Object.assign({}, state);
newState.hasDisabledSyncItems = true;
break;
2017-12-05 20:56:39 +02:00
case 'ENCRYPTION_HAS_DISABLED_ITEMS':
newState = Object.assign({}, state);
newState.hasDisabledEncryptionItems = action.value;
break;
case 'CLIPPER_SERVER_SET':
{
newState = Object.assign({}, state);
const clipperServer = Object.assign({}, newState.clipperServer);
if ('startState' in action) clipperServer.startState = action.startState;
if ('port' in action) clipperServer.port = action.port;
newState.clipperServer = clipperServer;
}
break;
case 'DECRYPTION_WORKER_SET':
{
newState = Object.assign({}, state);
const decryptionWorker = Object.assign({}, newState.decryptionWorker);
for (const n in action) {
if (!action.hasOwnProperty(n) || n === 'type') continue;
decryptionWorker[n] = action[n];
}
newState.decryptionWorker = decryptionWorker;
}
break;
case 'RESOURCE_FETCHER_SET':
{
newState = Object.assign({}, state);
const rf = Object.assign({}, action);
delete rf.type;
newState.resourceFetcher = rf;
}
break;
case 'LOAD_CUSTOM_CSS':
newState = Object.assign({}, state);
newState.customCss = action.css;
break;
case 'TEMPLATE_UPDATE_ALL':
newState = Object.assign({}, state);
newState.templates = action.templates;
break;
2019-07-29 15:43:53 +02:00
case 'SET_NOTE_TAGS':
newState = Object.assign({}, state);
newState.selectedNoteTags = action.items;
break;
case 'PLUGIN_DIALOG_SET':
{
if (!action.pluginName) throw new Error('action.pluginName not specified');
newState = Object.assign({}, state);
const newPlugins = Object.assign({}, newState.plugins);
const newPlugin = newState.plugins[action.pluginName] ? Object.assign({}, newState.plugins[action.pluginName]) : {};
if ('open' in action) newPlugin.dialogOpen = action.open;
newPlugins[action.pluginName] = newPlugin;
newState.plugins = newPlugins;
}
break;
2017-10-07 18:30:27 +02:00
}
} catch (error) {
2019-09-19 23:51:18 +02:00
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
2017-10-07 18:30:27 +02:00
throw error;
}
if (action.type.indexOf('NOTE_UPDATE') === 0 || action.type.indexOf('FOLDER_UPDATE') === 0 || action.type.indexOf('TAG_UPDATE') === 0) {
newState = Object.assign({}, newState);
newState.hasEncryptedItems = stateHasEncryptedItems(newState);
}
if (action.type === 'NOTE_DELETE') {
newState = handleHistory(newState, action);
}
2017-10-07 18:30:27 +02:00
return newState;
2019-07-29 15:43:53 +02:00
};
2017-10-07 18:30:27 +02:00
module.exports = { reducer, defaultState, stateUtils, MAX_HISTORY };