import noteAutocompleteService from "../services/note_autocomplete.js"; import SpacedUpdate from "../services/spaced_update.js"; import server from "../services/server.js"; import TabAwareWidget from "./tab_aware_widget.js"; import treeCache from "../services/tree_cache.js"; import ws from "../services/ws.js"; import toastService from "../services/toast.js"; import utils from "../services/utils.js"; import DeleteNoteSearchAction from "./search_actions/delete_note.js"; import DeleteLabelSearchAction from "./search_actions/delete_label.js"; import DeleteRelationSearchAction from "./search_actions/delete_relation.js"; import RenameLabelSearchAction from "./search_actions/rename_label.js"; import SetLabelValueSearchAction from "./search_actions/set_label_value.js"; import SetRelationTargetSearchAction from "./search_actions/set_relation_target.js"; const TPL = `
`; const ACTION_CLASSES = {}; for (const clazz of [ DeleteNoteSearchAction, DeleteLabelSearchAction, DeleteRelationSearchAction, RenameLabelSearchAction, SetLabelValueSearchAction, SetRelationTargetSearchAction ]) { ACTION_CLASSES[clazz.actionName] = clazz; } export default class SearchDefinitionWidget extends TabAwareWidget { static getType() { return "search"; } renderTitle(note) { return { show: note.type === 'search', activate: true, $title: 'Search' }; } doRender() { this.$widget = $(TPL); this.$component = this.$widget.find('.search-definition-widget'); this.contentSized(); this.overflowing(); this.$searchString = this.$widget.find(".search-string"); this.$searchString.on('input', () => this.searchStringSU.scheduleUpdate()); utils.bindElShortcut(this.$searchString, 'return', async () => { await this.searchStringSU.updateNowIfNecessary(); this.refreshResults(); }); this.searchStringSU = new SpacedUpdate(async () => { const searchString = this.$searchString.val(); await this.setAttribute('label', 'searchString', searchString); if (this.note.title.startsWith('Search: ')) { await server.put(`notes/${this.noteId}/change-title`, { title: 'Search: ' + (searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`) }); } }, 1000); this.$ancestor = this.$widget.find('.ancestor'); noteAutocompleteService.initNoteAutocomplete(this.$ancestor); this.$ancestor.on('autocomplete:closed', async () => { const ancestorOfNoteId = this.$ancestor.getSelectedNoteId(); await this.setAttribute('relation', 'ancestor', ancestorOfNoteId); }); this.$widget.on('click', '[data-search-option-add]', async event => { const searchOption = $(event.target).attr('data-search-option-add'); if (searchOption === 'fastSearch') { await this.setAttribute('label', 'fastSearch'); } else if (searchOption === 'orderBy') { await this.setAttribute('label', 'orderBy', 'relevancy'); await this.setAttribute('label', 'orderDirection', 'asc'); } else if (searchOption === 'includeArchivedNotes') { await this.setAttribute('label', 'includeArchivedNotes'); } else if (searchOption === 'ancestor') { await this.setAttribute('relation', 'ancestor', 'root'); } this.refresh(); }); this.$widget.on('click', '[data-search-option-del]', async event => { async function deleteAttr(note, attrName) { for (const attr of note.getOwnedAttributes()) { if (attr.name === attrName) { await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`); } } } const searchOption = $(event.target).attr('data-search-option-del'); await deleteAttr(this.note, searchOption); if (searchOption === 'orderBy') { await deleteAttr(this.note, 'orderDirection'); } await ws.waitForMaxKnownEntityChangeId(); this.refresh(); }); this.$orderBy = this.$widget.find('select[name=orderBy]'); this.$orderBy.on('change', async () => { const orderBy = this.$orderBy.val(); await this.setAttribute('label', 'orderBy', orderBy); }); this.$orderDirection = this.$widget.find('select[name=orderDirection]'); this.$orderDirection.on('change', async () => { const orderDirection = this.$orderDirection.val(); await this.setAttribute('label', 'orderDirection', orderDirection); }); this.$actionOptions = this.$widget.find('.action-options'); this.$widget.on('click', '[data-action-add]', async event => { const actionName = $(event.target).attr('data-action-add'); await server.post(`notes/${this.noteId}/attributes`, { type: 'label', name: 'action', value: JSON.stringify({ name: actionName }) }); this.$widget.find('.action-add-toggle').dropdown('toggle'); await ws.waitForMaxKnownEntityChangeId(); this.refresh(); }); this.$widget.on('click', '[data-action-conf-del]', async event => { const attributeId = $(event.target).closest('[data-attribute-id]').attr('data-attribute-id'); await server.remove(`notes/${this.noteId}/attributes/${attributeId}`); await ws.waitForMaxKnownEntityChangeId(); this.refresh(); }); this.$searchButton = this.$widget.find('.search-button'); this.$searchButton.on('click', () => this.refreshResults()); this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button'); this.$searchAndExecuteButton.on('click', () => this.searchAndExecute()); } async setAttribute(type, name, value = '') { await server.put(`notes/${this.noteId}/set-attribute`, { type, name, value }); await ws.waitForMaxKnownEntityChangeId(); } async refreshResults() { await treeCache.reloadNotes([this.noteId]); this.triggerEvent('searchRefreshed', {tabId: this.tabContext.tabId}); } async refreshWithNote(note) { this.$component.show(); this.$searchString.val(this.note.getLabelValue('searchString')); for (const attrName of ['includeArchivedNotes', 'ancestor', 'fastSearch', 'orderBy']) { const has = note.hasLabel(attrName) || note.hasRelation(attrName); this.$widget.find(`[data-search-option-add='${attrName}'`).toggle(!has); this.$widget.find(`[data-search-option-conf='${attrName}'`).toggle(has); } const ancestorNoteId = this.note.getRelationValue('ancestor'); await this.$ancestor.setNote(ancestorNoteId); if (note.hasLabel('orderBy')) { this.$orderBy.val(note.getLabelValue('orderBy')); this.$orderDirection.val(note.getLabelValue('orderDirection') || 'asc'); } this.$actionOptions.empty(); const actionLabels = this.note.getLabels('action'); for (const actionAttr of actionLabels) { let actionDef; try { actionDef = JSON.parse(actionAttr.value); } catch (e) { logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); continue; } const ActionClass = ACTION_CLASSES[actionDef.name]; if (!ActionClass) { logError(`No action class for '${actionDef.name}' found.`); continue; } const action = new ActionClass(actionAttr, actionDef); this.$actionOptions.append(action.render()); } this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden'); //this.refreshResults(); // important specifically when this search note was not yet refreshed } focusOnSearchDefinitionEvent() { this.$searchString.focus(); } getContent() { return ''; } async searchAndExecute() { await server.post(`search-and-execute-note/${this.noteId}`); this.refreshResults(); toastService.showMessage('Actions have been executed.', 3000); } }