Skip to content

Commit

Permalink
Implement "show all notes" feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
miciasto committed Feb 21, 2020
1 parent c79ee74 commit f501395
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 4 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ module.exports = {
'expect': 'readonly',
'describe': 'readonly',
'it': 'readonly',
'beforeAll': 'readonly',
'afterAll': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'jasmine': 'readonly',

// React Native variables
Expand Down Expand Up @@ -88,4 +91,4 @@ module.exports = {
"react",
"@typescript-eslint",
],
};
};
123 changes: 123 additions & 0 deletions CliClient/tests/integration_SmartFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const { setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { BaseApplication } = require('lib/BaseApplication.js');
const { time } = require('lib/time-utils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids.js');

//
// The integration tests are to test the integration of the core system, comprising the
// base application with middleware, reducer and models in response to dispatched events.
//
// The general strategy for each integration test is:
// - create a starting application state,
// - inject the event to be tested
// - check the resulting application state
//
// In particular, this file contains integration tests for smart filter features.
//

async function createNTestFolders(n) {
let folders = [];
for (let i = 0; i < n; i++) {
let folder = await Folder.save({ title: 'folder' });
folders.push(folder);
}
return folders;
}

async function createNTestNotes(n, folder) {
let notes = [];
for (let i = 0; i < n; i++) {
let note = await Note.save({ title: 'note', parent_id: folder.id, is_conflict: 0 });
notes.push(note);
}
return notes;
}

async function createNTestTags(n) {
let tags = [];
for (let i = 0; i < n; i++) {
let tag = await Tag.save({ title: 'tag' });
tags.push(tag);
}
return tags;
}

// use this until Javascript arr.flat() function works in Travis
function flatten(arr) {
return (arr.reduce((acc, val) => acc.concat(val), []));
}

let baseApplication = null;

describe('integration_SmartFilters', function() {

beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);

baseApplication = new BaseApplication();
baseApplication.initRedux();
Setting.dispatchUpdateAll();
done();
});

afterEach(async (done) => {
baseApplication.deinitRedux();
baseApplication.destroy();
baseApplication = null;
done();
});

it('should show notes in a folder', asyncTest(async () => {
let folders = await createNTestFolders(2);
let notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(await createNTestNotes(3, folders[i]));
}

baseApplication.dispatch({
type: 'FOLDER_SELECT',
id: folders[1].id,
});
await time.msleep(100);

let state = baseApplication.store().getState();

expect(state.notesParentType).toEqual('Folder');
expect(state.selectedFolderId).toEqual(folders[1].id);

let expectedNoteIds = notes[1].map(n => n.id).sort();
let noteIds = state.notes.map(n => n.id).sort();
expect(noteIds).toEqual(expectedNoteIds);
}));

it('should show all notes', asyncTest(async () => {
let folders = await createNTestFolders(2);
let notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(await createNTestNotes(3, folders[i]));
}

baseApplication.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
await time.msleep(100);

let state = baseApplication.store().getState();

expect(state.notesParentType).toEqual('SmartFilter');
expect(state.selectedSmartFilterId).toEqual(ALL_NOTES_FILTER_ID);

// let expectedNoteIds = notes.map(n => n.map(o => o.id)).flat().sort();
let expectedNoteIds = flatten(notes.map(n => n.map(o => o.id))).sort();
let noteIds = state.notes.map(n => n.id).sort();
expect(noteIds).toEqual(expectedNoteIds);
}));
});
2 changes: 1 addition & 1 deletion ElectronClient/gui/NoteText.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1621,7 +1621,7 @@ class NoteTextComponent extends React.Component {

createToolbarItems(note, editorIsVisible) {
const toolbarItems = [];
if (note && this.state.folder && ['Search', 'Tag'].includes(this.props.notesParentType)) {
if (note && this.state.folder && ['Search', 'Tag', 'SmartFilter'].includes(this.props.notesParentType)) {
toolbarItems.push({
title: _('In: %s', substrWithEllipsis(this.state.folder.title, 0, 16)),
iconName: 'fa-book',
Expand Down
20 changes: 20 additions & 0 deletions ElectronClient/gui/SideBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require('../InteropServiceHelper.js');
const { substrWithEllipsis } = require('lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');

class SideBarComponent extends React.Component {
constructor() {
Expand Down Expand Up @@ -89,6 +90,7 @@ class SideBarComponent extends React.Component {
this.tagItemsOrder_ = [];

this.onKeyDown = this.onKeyDown.bind(this);
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);

this.rootRef = React.createRef();

Expand Down Expand Up @@ -570,6 +572,9 @@ class SideBarComponent extends React.Component {
let isExpanded = this.state[toggleKey];
toggleIcon = <i className={`fa ${isExpanded ? 'fa-chevron-down' : 'fa-chevron-left'}`} style={{ fontSize: style.fontSize * 0.75, marginRight: 12, marginLeft: 5, marginTop: style.fontSize * 0.125 }}></i>;
}
if (extraProps.selected) {
style.backgroundColor =this.style().listItemSelected.backgroundColor;
}

const ref = this.anchorItemRef('headers', key);

Expand Down Expand Up @@ -696,6 +701,13 @@ class SideBarComponent extends React.Component {
}
}

onAllNotesClick_() {
this.props.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
}

synchronizeButton(type) {
const style = Object.assign({}, this.style().button, { marginBottom: 5 });
const iconName = 'fa-refresh';
Expand Down Expand Up @@ -732,6 +744,13 @@ class SideBarComponent extends React.Component {
});

let items = [];
items.push(
this.makeHeader('allNotesHeader', _('All notes'), 'fa-clone', {
onClick: this.onAllNotesClick_,
selected: this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID,
})
);

items.push(
this.makeHeader('folderHeader', _('Notebooks'), 'fa-book', {
onDrop: this.onFolderDrop_,
Expand Down Expand Up @@ -821,6 +840,7 @@ const mapStateToProps = state => {
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
theme: state.settings.theme,
Expand Down
28 changes: 28 additions & 0 deletions ReactNativeClient/lib/BaseApplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class BaseApplication {
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = this.decryptionWorker_resourceMetadataButNotBlobDecrypted.bind(this);
}

destroy() {
FoldersScreenUtils.cancelTimers();
this.logger_ = null;
this.dbLogger_ = null;
this.eventEmitter_ = null;
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = null;
}

logger() {
return this.logger_;
}
Expand Down Expand Up @@ -217,6 +225,9 @@ class BaseApplication {
} else if (parentType === 'Search') {
parentId = state.selectedSearchId;
parentType = BaseModel.TYPE_SEARCH;
} else if (parentType === 'SmartFilter') {
parentId = state.selectedSmartFilterId;
parentType = BaseModel.TYPE_SMART_FILTER;
}

this.logger().debug('Refreshing notes:', parentType, parentId);
Expand All @@ -243,6 +254,8 @@ class BaseApplication {
} else if (parentType === BaseModel.TYPE_SEARCH) {
const search = BaseModel.byId(state.searches, parentId);
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
} else if (parentType === BaseModel.TYPE_SMART_FILTER) {
notes = await Note.previews(parentId, options);
}
}

Expand Down Expand Up @@ -435,6 +448,11 @@ class BaseApplication {
refreshNotes = true;
}

if (action.type == 'SMART_FILTER_SELECT') {
refreshNotes = true;
refreshNotesUseSelectedNoteId = true;
}

if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
refreshNotes = true;
}
Expand Down Expand Up @@ -515,6 +533,16 @@ class BaseApplication {
ResourceFetcher.instance().dispatch = this.store().dispatch;
}

deinitRedux() {
this.store_ = null;
BaseModel.dispatch = function() {};
FoldersScreenUtils.dispatch = function() {};
reg.dispatch = function() {};
BaseSyncTarget.dispatch = function() {};
DecryptionWorker.instance().dispatch = function() {};
ResourceFetcher.instance().dispatch = function() {};
}

async readFlagsFromFile(flagPath) {
if (!fs.existsSync(flagPath)) return {};
let flagContent = fs.readFileSync(flagPath, 'utf8');
Expand Down
2 changes: 1 addition & 1 deletion ReactNativeClient/lib/BaseModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ class BaseModel {
}
}

BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14]];
BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14], ['TYPE_SMART_FILTER', 15]];

for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i];
Expand Down
4 changes: 4 additions & 0 deletions ReactNativeClient/lib/folders-screen-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class FoldersScreenUtils {
this.refreshFolders();
}, 1000);
}

static cancelTimers() {
if (this.scheduleRefreshFoldersIID_) clearTimeout(this.scheduleRefreshFoldersIID_);
}
}

module.exports = { FoldersScreenUtils };
3 changes: 2 additions & 1 deletion ReactNativeClient/lib/models/Note.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const ArrayUtils = require('lib/ArrayUtils.js');
const lodash = require('lodash');
const urlUtils = require('lib/urlUtils.js');
const { MarkupToHtml } = require('lib/joplin-renderer');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');

class Note extends BaseItem {
static tableName() {
Expand Down Expand Up @@ -275,7 +276,7 @@ class Note extends BaseItem {
options.conditions.push('is_conflict = 1');
} else {
options.conditions.push('is_conflict = 0');
if (parentId) {
if (parentId && parentId !== ALL_NOTES_FILTER_ID) {
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
}
Expand Down
13 changes: 13 additions & 0 deletions ReactNativeClient/lib/models/SmartFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const BaseModel = require('lib/BaseModel.js');

class SmartFilter extends BaseModel {
static tableName() {
throw new Error('Not using database');
}

static modelType() {
return BaseModel.TYPE_SMART_FILTER;
}
}

module.exports = SmartFilter;
6 changes: 6 additions & 0 deletions ReactNativeClient/lib/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@ const reducer = (state = defaultState, action) => {
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(state, action, { clearSelectedNoteIds: true });
break;
Expand Down
6 changes: 6 additions & 0 deletions ReactNativeClient/lib/reserved-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

module.exports = Object.freeze({

ALL_NOTES_FILTER_ID: 'c3176726992c11e9ac940492261af972',

});

0 comments on commit f501395

Please sign in to comment.