redesign of search input. Saved search is now saved under active note and doesn't need page reload

This commit is contained in:
zadam 2019-03-29 23:24:41 +01:00
parent 8fb6edad67
commit 89b8e2bb08
12 changed files with 104 additions and 69 deletions

View File

@ -123,7 +123,10 @@ if (utils.isElectron()) {
setTimeout(async () => { setTimeout(async () => {
const parentNode = treeService.getActiveNode(); const parentNode = treeService.getActiveNode();
const {note} = await treeService.createNote(parentNode, parentNode.data.noteId, 'into', "text", parentNode.data.isProtected); const {note} = await treeService.createNote(parentNode, parentNode.data.noteId, 'into', {
type: "text",
isProtected: parentNode.data.isProtected
});
await treeService.activateNote(note.noteId); await treeService.activateNote(note.noteId);

View File

@ -91,10 +91,10 @@ $("#note-menu-button").click(async e => {
const parentNoteId = node.data.parentNoteId; const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node); const isProtected = treeUtils.getParentProtectedStatus(node);
treeService.createNote(node, parentNoteId, 'after', null, isProtected); treeService.createNote(node, parentNoteId, 'after', { isProtected: isProtected });
} }
else if (cmd === "insertChildNote") { else if (cmd === "insertChildNote") {
treeService.createNote(node, node.data.noteId, 'into', null); treeService.createNote(node, node.data.noteId, 'into');
} }
else if (cmd === "delete") { else if (cmd === "delete") {
treeChangesService.deleteNodes([node]); treeChangesService.deleteNodes([node]);

View File

@ -1,6 +1,7 @@
import treeService from './tree.js'; import treeService from './tree.js';
import treeCache from "./tree_cache.js";
import server from './server.js'; import server from './server.js';
import treeUtils from "./tree_utils.js"; import infoService from "./info.js";
const $tree = $("#tree"); const $tree = $("#tree");
const $searchInput = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
@ -64,16 +65,35 @@ async function doSearch(searchText) {
$searchResultsInner.append($result); $searchResultsInner.append($result);
} }
// have at least some feedback which is good especially in situations
// when the result list does not change with a query
infoService.showMessage("Search finished successfully.");
} }
async function saveSearch() { async function saveSearch() {
const {noteId} = await server.post('search/' + encodeURIComponent($searchInput.val())); const searchString = $searchInput.val().trim();
if (searchString.length === 0) {
alert("Write some search criteria first so there is something to save.");
return;
}
let activeNode = treeService.getActiveNode();
const parentNote = await treeCache.getNote(activeNode.data.noteId);
if (parentNote.type === 'search') {
activeNode = activeNode.getParent();
}
await treeService.createNote(activeNode, activeNode.data.noteId, 'into', {
type: "search",
mime: "application/json",
title: searchString,
content: JSON.stringify({ searchString: searchString })
});
resetSearch(); resetSearch();
await treeService.reload();
await treeService.activateNote(noteId);
} }
function init() { function init() {

View File

@ -560,49 +560,44 @@ async function createNewTopLevelNote() {
const rootNode = getNodesByNoteId(hoistedNoteId)[0]; const rootNode = getNodesByNoteId(hoistedNoteId)[0];
await createNote(rootNode, hoistedNoteId, "into", null, false); await createNote(rootNode, hoistedNoteId, "into");
} }
/** async function createNote(node, parentNoteId, target, extraOptions) {
* @param type - type can be falsy - in that case it will be chosen automatically based on parent note
*/
async function createNote(node, parentNoteId, target, type, isProtected, saveSelection = false) {
utils.assertArguments(node, parentNoteId, target); utils.assertArguments(node, parentNoteId, target);
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
// but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
if (!isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) { if (!extraOptions.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()) {
isProtected = false; extraOptions.isProtected = false;
} }
if (noteDetailService.getActiveNoteType() !== 'text') { if (noteDetailService.getActiveNoteType() !== 'text') {
saveSelection = false; extraOptions.saveSelection = false;
} }
else { else {
// just disable this feature altogether - there's a problem that note containing image or table at the beginning // just disable this feature altogether - there's a problem that note containing image or table at the beginning
// of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save // of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save
// the selection - see https://github.com/ckeditor/ckeditor5/issues/1384 // the selection - see https://github.com/ckeditor/ckeditor5/issues/1384
saveSelection = false; extraOptions.saveSelection = false;
} }
let title, content; if (extraOptions.saveSelection) {
[extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
if (saveSelection) {
[title, content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
} }
const newNoteName = title || "new note"; const newNoteName = extraOptions.title || "new note";
const {note, branch} = await server.post('notes/' + parentNoteId + '/children', { const {note, branch} = await server.post('notes/' + parentNoteId + '/children', {
title: newNoteName, title: newNoteName,
content: content, content: extraOptions.content,
target: target, target: target,
target_branchId: node.data.branchId, target_branchId: node.data.branchId,
isProtected: isProtected, isProtected: extraOptions.isProtected,
type: type type: extraOptions.type
}); });
if (saveSelection) { if (extraOptions.saveSelection) {
// we remove the selection only after it was saved to server to make sure we don't lose anything // we remove the selection only after it was saved to server to make sure we don't lose anything
window.cutToNote.removeSelection(); window.cutToNote.removeSelection();
} }
@ -622,9 +617,11 @@ async function createNote(node, parentNoteId, target, type, isProtected, saveSel
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
refKey: branchEntity.noteId, refKey: branchEntity.noteId,
branchId: branchEntity.branchId, branchId: branchEntity.branchId,
isProtected: isProtected, isProtected: extraOptions.isProtected,
extraClasses: await treeBuilder.getExtraClasses(noteEntity), extraClasses: await treeBuilder.getExtraClasses(noteEntity),
icon: await treeBuilder.getIcon(noteEntity) icon: await treeBuilder.getIcon(noteEntity),
folder: extraOptions.type === 'search',
lazy: true
}; };
if (target === 'after') { if (target === 'after') {
@ -708,13 +705,19 @@ utils.bindShortcut('ctrl+o', async () => {
return; return;
} }
createNote(node, parentNoteId, 'after', null, isProtected, true); await createNote(node, parentNoteId, 'after', {
isProtected: isProtected,
saveSelection: true
});
}); });
function createNoteInto() { async function createNoteInto() {
const node = getActiveNode(); const node = getActiveNode();
createNote(node, node.data.noteId, 'into', null, node.data.isProtected, true); await createNote(node, node.data.noteId, 'into', {
isProtected: node.data.isProtected,
saveSelection: true
});
} }
async function checkFolderStatus(node) { async function checkFolderStatus(node) {
@ -768,10 +771,8 @@ $scrollToActiveNoteButton.click(scrollToActiveNote);
export default { export default {
reload, reload,
collapseTree, collapseTree,
scrollToActiveNote,
setBranchBackgroundBasedOnProtectedStatus, setBranchBackgroundBasedOnProtectedStatus,
setProtected, setProtected,
expandToNote,
activateNote, activateNote,
getFocusedNode, getFocusedNode,
getActiveNode, getActiveNode,
@ -779,7 +780,6 @@ export default {
setCurrentNotePathToHash, setCurrentNotePathToHash,
setNoteTitle, setNoteTitle,
setPrefix, setPrefix,
createNewTopLevelNote,
createNote, createNote,
createNoteInto, createNoteInto,
getSelectedNodes, getSelectedNodes,

View File

@ -165,12 +165,15 @@ function selectContextMenuItem(event, cmd) {
const isProtected = treeUtils.getParentProtectedStatus(node); const isProtected = treeUtils.getParentProtectedStatus(node);
const type = cmd.split("_")[1]; const type = cmd.split("_")[1];
treeService.createNote(node, parentNoteId, 'after', type, isProtected); treeService.createNote(node, parentNoteId, 'after', {
type: type,
isProtected: isProtected
});
} }
else if (cmd.startsWith("insertChildNote")) { else if (cmd.startsWith("insertChildNote")) {
const type = cmd.split("_")[1]; const type = cmd.split("_")[1];
treeService.createNote(node, node.data.noteId, 'into', type); treeService.createNote(node, node.data.noteId, 'into', { type: type });
} }
else if (cmd === "editBranchPrefix") { else if (cmd === "editBranchPrefix") {
branchPrefixDialog.showDialog(node); branchPrefixDialog.showDialog(node);

View File

@ -66,7 +66,7 @@ body {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
padding: 10px 0 10px 0; padding: 10px 0 10px 0;
margin: 0 20px 0 10px; margin: 0 10px 0 10px;
border: 1px solid var(--main-border-color); border: 1px solid var(--main-border-color);
border-radius: 7px; border-radius: 7px;
} }

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const noteService = require('../../services/notes');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const noteCacheService = require('../../services/note_cache'); const noteCacheService = require('../../services/note_cache');
const log = require('../../services/log'); const log = require('../../services/log');
@ -13,20 +12,6 @@ async function searchNotes(req) {
return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); return noteIds.map(noteCacheService.getNotePath).filter(res => !!res);
} }
async function saveSearchToNote(req) {
const content = {
searchString: req.params.searchString
};
const {note} = await noteService.createNote('root', req.params.searchString, content, {
json: true,
type: 'search',
mime: "application/json"
});
return { noteId: note.noteId };
}
async function searchFromNote(req) { async function searchFromNote(req) {
const note = await repository.getNote(req.params.noteId); const note = await repository.getNote(req.params.noteId);
@ -52,7 +37,7 @@ async function searchFromNote(req) {
noteIds = await searchFromRelation(note, relationName); noteIds = await searchFromRelation(note, relationName);
} }
else { else {
noteIds = searchService.searchForNoteIds(json.searchString); noteIds = await searchService.searchForNoteIds(json.searchString);
} }
// we won't return search note's own noteId // we won't return search note's own noteId
@ -100,6 +85,5 @@ async function searchFromRelation(note, relationName) {
module.exports = { module.exports = {
searchNotes, searchNotes,
saveSearchToNote,
searchFromNote searchFromNote
}; };

View File

@ -209,7 +209,6 @@ function register(app) {
route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler); route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler);
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote);
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);

View File

@ -1,6 +1,17 @@
const utils = require('./utils'); const utils = require('./utils');
const VIRTUAL_ATTRIBUTES = ["dateCreated", "dateCreated", "dateModified", "utcDateCreated", "utcDateModified", "isProtected", "title", "content", "type", "mime", "text"]; const VIRTUAL_ATTRIBUTES = [
"dateCreated",
"dateModified",
"utcDateCreated",
"utcDateModified",
"isProtected",
"title",
"content",
"type",
"mime",
"text"
];
module.exports = function(filters, selectedColumns = 'notes.*') { module.exports = function(filters, selectedColumns = 'notes.*') {
// alias => join // alias => join
@ -106,7 +117,7 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
else if (filter.operator === '=*' || filter.operator === '!=*') { else if (filter.operator === '=*' || filter.operator === '!=*') {
where += `${accessor}` where += `${accessor}`
+ (filter.operator.includes('!') ? ' NOT' : '') + (filter.operator.includes('!') ? ' NOT' : '')
+ ` LIKE '` + utils.prepareSqlForLike('', filter.value, '%'); + ` LIKE ` + utils.prepareSqlForLike('', filter.value, '%');
} }
else if (filter.operator === '*=*' || filter.operator === '!*=*') { else if (filter.operator === '*=*' || filter.operator === '!*=*') {
where += `${accessor}` where += `${accessor}`
@ -149,8 +160,5 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
GROUP BY notes.noteId GROUP BY notes.noteId
ORDER BY ${orderBy.join(", ")}`; ORDER BY ${orderBy.join(", ")}`;
console.log(query);
console.log(params);
return { query, params }; return { query, params };
}; };

View File

@ -77,6 +77,18 @@ async function createNewNote(parentNoteId, noteData) {
} }
} }
if (!noteData.mime) {
if (noteData.type === 'text') {
noteData.mime = 'text/html';
}
else if (noteData.type === 'code') {
noteData.mime = 'text/plain';
}
else if (noteData.type === 'relation-map' || noteData.type === 'search') {
noteData.mime = 'application/json';
}
}
noteData.type = noteData.type || parentNote.type; noteData.type = noteData.type || parentNote.type;
noteData.mime = noteData.mime || parentNote.mime; noteData.mime = noteData.mime || parentNote.mime;

View File

@ -1,6 +1,6 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\w_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\w_-]+|"[^"]+"))?/ig; const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\w_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\w_/-]+|"[^"]+"))?/ig;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i; const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) { function calculateSmartValue(v) {

View File

@ -105,17 +105,23 @@
</div> </div>
<div id="search-box"> <div id="search-box">
<div style="display: flex; align-items: center; flex-wrap: wrap;"> <div class="form-group">
<input name="search-text" id="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px; flex-basis: 5em; min-width: 0;" autocomplete="off"> <div class="input-group">
<button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button> <input name="search-text" id="search-text" class="form-control" placeholder="Search text, labels" autocomplete="off">
&nbsp; <div class="input-group-append">
<button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button>
</div>
</div>
</div>
<button id="save-search-button" class="btn btn-sm icon-button jam jam-save" title="Save search"></button>
&nbsp; <div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;">
<button id="save-search-button" class="btn btn-sm"
title="This will create new saved search note under active note.">
<span class="jam jam-save"></span> Save search</button>
<button id="close-search-button" class="btn btn-sm icon-button jam jam-close" title="Close search"></button> <button id="close-search-button" class="btn btn-sm"><span class="jam jam-close"></span> Close search</button>
</div> </div>
</div> </div>