mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-12 08:54:00 +02:00
c63c6370b5
The goal is to make the command system more modular, so each command can be defined as a single object that includes a declaration (name, label, etc.) and a runtime (to execute the command, test if it should be enabled, etc.) Utility methods are provided to convert a command to a menu item or a toolbar button, thus reducing duplicated and boiler plate code across the codebase (often the menu item logic was duplicated in the toolbar button logic and vice versa). The goal is to make it easier to add new commands (and associated menu item and toolbar buttons) and to call them from anywhere. This is also useful for plugins, which can also easily define new commands. Could also allow creating a command palette.
1030 lines
30 KiB
JavaScript
1030 lines
30 KiB
JavaScript
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');
|
|
const CommandService = require('lib/services/CommandService').default;
|
|
|
|
const defaultState = {
|
|
notes: [],
|
|
notesSource: '',
|
|
notesParentType: null,
|
|
folders: [],
|
|
tags: [],
|
|
masterKeys: [],
|
|
notLoadedMasterKeys: [],
|
|
searches: [],
|
|
selectedNoteIds: [],
|
|
selectedNoteHash: '',
|
|
selectedFolderId: null,
|
|
selectedTagId: null,
|
|
selectedSearchId: null,
|
|
selectedItemType: 'note',
|
|
lastSelectedNotesIds: {
|
|
Folder: {},
|
|
Tag: {},
|
|
Search: {},
|
|
},
|
|
showSideMenu: false,
|
|
screens: {},
|
|
historyCanGoBack: false,
|
|
syncStarted: false,
|
|
syncReport: {},
|
|
searchQuery: '',
|
|
settings: {},
|
|
sharedData: null,
|
|
appState: 'starting',
|
|
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: [],
|
|
editorNoteStatuses: {},
|
|
isInsertingNotes: false,
|
|
};
|
|
|
|
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) => {
|
|
key = `${key}_${JSON.stringify(output)}`;
|
|
if (derivedStateCache_[key]) return derivedStateCache_[key];
|
|
|
|
derivedStateCache_[key] = output;
|
|
return derivedStateCache_[key];
|
|
};
|
|
|
|
stateUtils.hasOneSelectedNote = function(state) {
|
|
return state.selectedNoteIds.length === 1;
|
|
};
|
|
|
|
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',
|
|
},
|
|
]);
|
|
}
|
|
};
|
|
|
|
stateUtils.foldersOrder = function(stateSettings) {
|
|
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 };
|
|
};
|
|
|
|
stateUtils.lastSelectedNoteIds = function(state) {
|
|
const parent = stateUtils.parentItem(state);
|
|
if (!parent) return [];
|
|
const output = state.lastSelectedNotesIds[parent.type][parent.id];
|
|
return output ? output : [];
|
|
};
|
|
|
|
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++) {
|
|
if (array[i].encryption_applied) return true;
|
|
}
|
|
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);
|
|
}
|
|
|
|
// 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],
|
|
};
|
|
|
|
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);
|
|
}
|
|
}
|
|
if (item.id == action.id) {
|
|
continue;
|
|
}
|
|
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;
|
|
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;
|
|
|
|
let found = false;
|
|
for (let i = 0; i < newItems.length; i++) {
|
|
const n = newItems[i];
|
|
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);
|
|
|
|
newState[itemsKey] = newItems;
|
|
|
|
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];
|
|
|
|
let newState = Object.assign({}, state);
|
|
|
|
if (action.type === 'NOTE_SELECT') {
|
|
if (JSON.stringify(newState.selectedNoteIds) === JSON.stringify(noteIds)) return state;
|
|
newState.selectedNoteIds = noteIds;
|
|
newState.selectedNoteHash = action.hash ? action.hash : '';
|
|
} else if (action.type === 'NOTE_SELECT_ADD') {
|
|
if (!noteIds.length) return state;
|
|
newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
|
|
} else if (action.type === 'NOTE_SELECT_REMOVE') {
|
|
if (!noteIds.length) return state; // Nothing to unselect
|
|
if (state.selectedNoteIds.length <= 1) return state; // Cannot unselect the last note
|
|
|
|
const newSelectedNoteIds = [];
|
|
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') {
|
|
if (!noteIds.length) return state;
|
|
|
|
if (newState.selectedNoteIds.indexOf(noteIds[0]) >= 0) {
|
|
newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_REMOVE', id: noteIds[0] });
|
|
} else {
|
|
newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] });
|
|
}
|
|
} else {
|
|
throw new Error('Unreachable');
|
|
}
|
|
|
|
newState = recordLastSelectedNoteIds(newState, newState.selectedNoteIds);
|
|
|
|
return newState;
|
|
}
|
|
|
|
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) {
|
|
return Object.assign({}, note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
|
|
}
|
|
return note;
|
|
});
|
|
|
|
forwardHistoryNotes = forwardHistoryNotes.map(note => {
|
|
if (note.id === modNote.id) {
|
|
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;
|
|
}
|
|
|
|
const reducer = (state = defaultState, action) => {
|
|
// if (!['SIDE_MENU_OPEN_PERCENT'].includes(action.type)) console.info('Action', action.type);
|
|
|
|
let newState = state;
|
|
|
|
// NOTE_DELETE requires post processing
|
|
if (action.type !== 'NOTE_DELETE') {
|
|
newState = handleHistory(newState, action);
|
|
}
|
|
|
|
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':
|
|
{
|
|
newState = Object.assign({}, state);
|
|
|
|
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();
|
|
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;
|
|
|
|
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));
|
|
if (allSelected) {
|
|
newState.selectedNoteIds = [];
|
|
} else {
|
|
newState.selectedNoteIds = newState.notes.map(n => n.id);
|
|
}
|
|
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);
|
|
const noteSelectAction = Object.assign({}, action, { type: 'NOTE_SELECT' });
|
|
newState = changeSelectedNotes(newState, noteSelectAction);
|
|
}
|
|
break;
|
|
|
|
case 'SETTING_UPDATE_ALL':
|
|
newState = Object.assign({}, state);
|
|
newState.settings = action.settings;
|
|
break;
|
|
|
|
case 'SETTING_UPDATE_ONE':
|
|
{
|
|
newState = Object.assign({}, state);
|
|
const newSettings = Object.assign({}, state.settings);
|
|
newSettings[action.key] = action.value;
|
|
newState.settings = newSettings;
|
|
}
|
|
break;
|
|
|
|
case 'NOTE_PROVISIONAL_FLAG_CLEAR':
|
|
{
|
|
const newIds = ArrayUtils.removeElement(state.provisionalNoteIds, action.id);
|
|
if (newIds !== state.provisionalNoteIds) {
|
|
newState = Object.assign({}, state, { provisionalNoteIds: newIds });
|
|
}
|
|
}
|
|
break;
|
|
|
|
|
|
// 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;
|
|
|
|
// 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':
|
|
{
|
|
const modNote = action.note;
|
|
const isViewingAllNotes = (state.notesParentType === 'SmartFilter' && state.selectedSmartFilterId === ALL_NOTES_FILTER_ID);
|
|
const isViewingConflictFolder = state.notesParentType === 'Folder' && state.selectedFolderId === Folder.conflictFolderId();
|
|
|
|
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;
|
|
};
|
|
|
|
let movedNotePreviousIndex = 0;
|
|
let noteFolderHasChanged = false;
|
|
let newNotes = state.notes.slice();
|
|
let found = false;
|
|
for (let i = 0; i < newNotes.length; i++) {
|
|
const n = newNotes[i];
|
|
if (n.id == modNote.id) {
|
|
// Note is still in the same folder
|
|
if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
|
|
// 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) {
|
|
if (!modNote.hasOwnProperty(n)) continue;
|
|
newNotes[i][n] = modNote[n];
|
|
}
|
|
} else {
|
|
// Note has moved to a different folder
|
|
newNotes.splice(i, 1);
|
|
noteFolderHasChanged = true;
|
|
movedNotePreviousIndex = i;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// newNotes = Note.sortNotes(newNotes, state.notesOrder, newState.settings.uncompletedTodosOnTop);
|
|
newNotes = Note.sortNotes(newNotes, stateUtils.notesOrder(state.settings), newState.settings.uncompletedTodosOnTop);
|
|
newState = Object.assign({}, state);
|
|
newState.notes = newNotes;
|
|
|
|
if (noteFolderHasChanged) {
|
|
let newIndex = movedNotePreviousIndex;
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
case 'FOLDER_UPDATE_ALL':
|
|
newState = Object.assign({}, state);
|
|
newState.folders = action.items;
|
|
break;
|
|
|
|
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;
|
|
|
|
case 'TAG_SELECT':
|
|
newState.selectedTagId = action.id;
|
|
if (!action.id) {
|
|
newState.notesParentType = defaultNotesParentType(state, 'Tag');
|
|
} else {
|
|
newState.notesParentType = 'Tag';
|
|
}
|
|
newState.selectedNoteIds = [];
|
|
break;
|
|
|
|
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;
|
|
newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', tagRemoved.id);
|
|
}
|
|
break;
|
|
|
|
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;
|
|
|
|
case 'FOLDER_DELETE':
|
|
newState = handleItemDelete(newState, action);
|
|
break;
|
|
|
|
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;
|
|
|
|
case 'SYNC_COMPLETED':
|
|
newState = Object.assign({}, state);
|
|
newState.syncStarted = false;
|
|
break;
|
|
|
|
case 'SYNC_REPORT_UPDATE':
|
|
newState = Object.assign({}, state);
|
|
newState.syncReport = action.report;
|
|
break;
|
|
|
|
case 'SEARCH_QUERY':
|
|
newState = Object.assign({}, state);
|
|
newState.searchQuery = action.query.trim();
|
|
break;
|
|
|
|
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;
|
|
|
|
case 'SYNC_HAS_DISABLED_SYNC_ITEMS':
|
|
newState = Object.assign({}, state);
|
|
newState.hasDisabledSyncItems = true;
|
|
break;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
} catch (error) {
|
|
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
|
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);
|
|
}
|
|
|
|
CommandService.instance().scheduleMapStateToProps(newState);
|
|
|
|
return newState;
|
|
};
|
|
|
|
module.exports = { reducer, defaultState, stateUtils, MAX_HISTORY };
|