mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
introduced bulk action groups
This commit is contained in:
parent
f272238dde
commit
f9bee7cd4e
@ -14,12 +14,21 @@ $dialog.on('click', '[data-action-add]', async event => {
|
|||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const action of bulkActionService.ACTION_CLASSES) {
|
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
|
||||||
$availableActionList.append(
|
const $actionGroupList = $("<td>");
|
||||||
$('<button class="btn btn-sm">')
|
const $actionGroup = $("<tr>")
|
||||||
.attr('data-action-add', action.actionName)
|
.append($("<td>").text(actionGroup.title + ": "))
|
||||||
.text(action.actionTitle)
|
.append($actionGroupList);
|
||||||
);
|
|
||||||
|
for (const action of actionGroup.actions) {
|
||||||
|
$actionGroupList.append(
|
||||||
|
$('<button class="btn btn-sm">')
|
||||||
|
.attr('data-action-add', action.actionName)
|
||||||
|
.text(action.actionTitle)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableActionList.append($actionGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
@ -13,6 +13,25 @@ import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
|
|||||||
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
||||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
||||||
|
|
||||||
|
const ACTION_GROUPS = [
|
||||||
|
{
|
||||||
|
title: 'Labels',
|
||||||
|
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Relations',
|
||||||
|
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Notes',
|
||||||
|
actions: [DeleteNoteBulkAction, DeleteNoteRevisionsBulkAction, MoveNoteBulkAction],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Other',
|
||||||
|
actions: [ExecuteScriptBulkAction]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const ACTION_CLASSES = [
|
const ACTION_CLASSES = [
|
||||||
MoveNoteBulkAction,
|
MoveNoteBulkAction,
|
||||||
DeleteNoteBulkAction,
|
DeleteNoteBulkAction,
|
||||||
@ -68,5 +87,6 @@ function parseActions(note) {
|
|||||||
export default {
|
export default {
|
||||||
addAction,
|
addAction,
|
||||||
parseActions,
|
parseActions,
|
||||||
ACTION_CLASSES
|
ACTION_CLASSES,
|
||||||
|
ACTION_GROUPS
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ const TPL = `
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<div style="margin-right: 10px;" class="text-nowrap">Set label</div>
|
<div style="margin-right: 10px;" class="text-nowrap">Add label</div>
|
||||||
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control label-name"
|
class="form-control label-name"
|
||||||
|
@ -6,8 +6,8 @@ const TPL = `
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<div style="margin-right: 10px;" class="text-nowrap">Set relation</div>
|
<div style="margin-right: 10px;" class="text-nowrap">Add relation</div>
|
||||||
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control relation-name"
|
class="form-control relation-name"
|
||||||
placeholder="relation name"
|
placeholder="relation name"
|
||||||
|
@ -64,6 +64,10 @@ const TPL = `
|
|||||||
.add-search-option button {
|
.add-search-option button {
|
||||||
margin-top: 5px; /* to give some spacing when buttons overflow on the next line */
|
margin-top: 5px; /* to give some spacing when buttons overflow on the next line */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
background-color: var(--accented-background-color);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="search-settings">
|
<div class="search-settings">
|
||||||
@ -183,12 +187,16 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
|||||||
this.$component = this.$widget.find('.search-definition-widget');
|
this.$component = this.$widget.find('.search-definition-widget');
|
||||||
this.$actionList = this.$widget.find('.action-list');
|
this.$actionList = this.$widget.find('.action-list');
|
||||||
|
|
||||||
for (const action of bulkActionService.ACTION_CLASSES) {
|
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
|
||||||
this.$actionList.append(
|
this.$actionList.append($('<h6 class="dropdown-header">').append(actionGroup.title));
|
||||||
$('<a class="dropdown-item" href="#">')
|
|
||||||
.attr('data-action-add', action.actionName)
|
for (const action of actionGroup.actions) {
|
||||||
.text(action.actionTitle)
|
this.$actionList.append(
|
||||||
);
|
$('<a class="dropdown-item" href="#">')
|
||||||
|
.attr('data-action-add', action.actionName)
|
||||||
|
.text(action.actionTitle)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$widget.on('click', '[data-search-option-add]', async event => {
|
this.$widget.on('click', '[data-search-option-add]', async event => {
|
||||||
|
@ -5,9 +5,7 @@ const SearchContext = require('../../services/search/search_context');
|
|||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
const scriptService = require('../../services/script');
|
const scriptService = require('../../services/script');
|
||||||
const searchService = require('../../services/search/services/search');
|
const searchService = require('../../services/search/services/search');
|
||||||
const noteRevisionService = require("../../services/note_revisions");
|
const bulkActionService = require("../../services/bulk_actions");
|
||||||
const branchService = require("../../services/branches");
|
|
||||||
const cloningService = require("../../services/cloning");
|
|
||||||
const {formatAttrForSearch} = require("../../services/attribute_formatter");
|
const {formatAttrForSearch} = require("../../services/attribute_formatter");
|
||||||
|
|
||||||
async function searchFromNoteInt(note) {
|
async function searchFromNoteInt(note) {
|
||||||
@ -59,108 +57,6 @@ async function searchFromNote(req) {
|
|||||||
return await searchFromNoteInt(note);
|
return await searchFromNoteInt(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_HANDLERS = {
|
|
||||||
deleteNote: (action, note) => {
|
|
||||||
note.markAsDeleted();
|
|
||||||
},
|
|
||||||
deleteNoteRevisions: (action, note) => {
|
|
||||||
noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId));
|
|
||||||
},
|
|
||||||
deleteLabel: (action, note) => {
|
|
||||||
for (const label of note.getOwnedLabels(action.labelName)) {
|
|
||||||
label.markAsDeleted();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteRelation: (action, note) => {
|
|
||||||
for (const relation of note.getOwnedRelations(action.relationName)) {
|
|
||||||
relation.markAsDeleted();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renameLabel: (action, note) => {
|
|
||||||
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
|
||||||
// attribute name is immutable, renaming means delete old + create new
|
|
||||||
const newLabel = label.createClone('label', action.newLabelName, label.value);
|
|
||||||
|
|
||||||
newLabel.save();
|
|
||||||
label.markAsDeleted();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renameRelation: (action, note) => {
|
|
||||||
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
|
|
||||||
// attribute name is immutable, renaming means delete old + create new
|
|
||||||
const newRelation = relation.createClone('relation', action.newRelationName, relation.value);
|
|
||||||
|
|
||||||
newRelation.save();
|
|
||||||
relation.markAsDeleted();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateLabelValue: (action, note) => {
|
|
||||||
for (const label of note.getOwnedLabels(action.labelName)) {
|
|
||||||
label.value = action.labelValue;
|
|
||||||
label.save();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateRelationTarget: (action, note) => {
|
|
||||||
for (const relation of note.getOwnedLabels(action.relationName)) {
|
|
||||||
relation.value = action.targetNoteId;
|
|
||||||
relation.save();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
moveNote: (action, note) => {
|
|
||||||
const targetParentNote = becca.getNote(action.targetParentNoteId);
|
|
||||||
|
|
||||||
if (!targetParentNote) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res;
|
|
||||||
|
|
||||||
if (note.getParentBranches().length > 1) {
|
|
||||||
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.success) {
|
|
||||||
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
executeScript: (action, note) => {
|
|
||||||
if (!action.script || !action.script.trim()) {
|
|
||||||
log.info("Ignoring executeScript since the script is empty.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptFunc = new Function("note", action.script);
|
|
||||||
scriptFunc(note);
|
|
||||||
|
|
||||||
note.save();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getActions(note) {
|
|
||||||
return note.getLabels('action')
|
|
||||||
.map(actionLabel => {
|
|
||||||
let action;
|
|
||||||
|
|
||||||
try {
|
|
||||||
action = JSON.parse(actionLabel.value);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(action.name in ACTION_HANDLERS)) {
|
|
||||||
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
})
|
|
||||||
.filter(a => !!a);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchAndExecute(req) {
|
async function searchAndExecute(req) {
|
||||||
const note = becca.getNote(req.params.noteId);
|
const note = becca.getNote(req.params.noteId);
|
||||||
|
|
||||||
@ -179,26 +75,7 @@ async function searchAndExecute(req) {
|
|||||||
|
|
||||||
const searchResultNoteIds = await searchFromNoteInt(note);
|
const searchResultNoteIds = await searchFromNoteInt(note);
|
||||||
|
|
||||||
const actions = getActions(note);
|
bulkActionService.executeActions(note, searchResultNoteIds);
|
||||||
|
|
||||||
for (const resultNoteId of searchResultNoteIds) {
|
|
||||||
const resultNote = becca.getNote(resultNoteId);
|
|
||||||
|
|
||||||
if (!resultNote || resultNote.isDeleted) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const action of actions) {
|
|
||||||
try {
|
|
||||||
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
|
|
||||||
|
|
||||||
ACTION_HANDLERS[action.name](action, resultNote);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
log.error(`ExecuteScript search action failed with ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchFromRelation(note, relationName) {
|
function searchFromRelation(note, relationName) {
|
||||||
|
139
src/services/bulk_actions.js
Normal file
139
src/services/bulk_actions.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const log = require("./log.js");
|
||||||
|
const noteRevisionService = require("./note_revisions.js");
|
||||||
|
const becca = require("../becca/becca.js");
|
||||||
|
const cloningService = require("./cloning.js");
|
||||||
|
const branchService = require("./branches.js");
|
||||||
|
|
||||||
|
const ACTION_HANDLERS = {
|
||||||
|
addLabel: (action, note) => {
|
||||||
|
note.addLabel(action.labelName, action.labelValue);
|
||||||
|
},
|
||||||
|
addRelation: (action, note) => {
|
||||||
|
note.addRelation(action.relationName, action.targetNoteId);
|
||||||
|
},
|
||||||
|
deleteNote: (action, note) => {
|
||||||
|
note.markAsDeleted();
|
||||||
|
},
|
||||||
|
deleteNoteRevisions: (action, note) => {
|
||||||
|
noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId));
|
||||||
|
},
|
||||||
|
deleteLabel: (action, note) => {
|
||||||
|
for (const label of note.getOwnedLabels(action.labelName)) {
|
||||||
|
label.markAsDeleted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteRelation: (action, note) => {
|
||||||
|
for (const relation of note.getOwnedRelations(action.relationName)) {
|
||||||
|
relation.markAsDeleted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renameLabel: (action, note) => {
|
||||||
|
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
||||||
|
// attribute name is immutable, renaming means delete old + create new
|
||||||
|
const newLabel = label.createClone('label', action.newLabelName, label.value);
|
||||||
|
|
||||||
|
newLabel.save();
|
||||||
|
label.markAsDeleted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renameRelation: (action, note) => {
|
||||||
|
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
|
||||||
|
// attribute name is immutable, renaming means delete old + create new
|
||||||
|
const newRelation = relation.createClone('relation', action.newRelationName, relation.value);
|
||||||
|
|
||||||
|
newRelation.save();
|
||||||
|
relation.markAsDeleted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateLabelValue: (action, note) => {
|
||||||
|
for (const label of note.getOwnedLabels(action.labelName)) {
|
||||||
|
label.value = action.labelValue;
|
||||||
|
label.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateRelationTarget: (action, note) => {
|
||||||
|
for (const relation of note.getOwnedLabels(action.relationName)) {
|
||||||
|
relation.value = action.targetNoteId;
|
||||||
|
relation.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveNote: (action, note) => {
|
||||||
|
const targetParentNote = becca.getNote(action.targetParentNoteId);
|
||||||
|
|
||||||
|
if (!targetParentNote) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res;
|
||||||
|
|
||||||
|
if (note.getParentBranches().length > 1) {
|
||||||
|
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executeScript: (action, note) => {
|
||||||
|
if (!action.script || !action.script.trim()) {
|
||||||
|
log.info("Ignoring executeScript since the script is empty.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptFunc = new Function("note", action.script);
|
||||||
|
scriptFunc(note);
|
||||||
|
|
||||||
|
note.save();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getActions(note) {
|
||||||
|
return note.getLabels('action')
|
||||||
|
.map(actionLabel => {
|
||||||
|
let action;
|
||||||
|
|
||||||
|
try {
|
||||||
|
action = JSON.parse(actionLabel.value);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(action.name in ACTION_HANDLERS)) {
|
||||||
|
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
})
|
||||||
|
.filter(a => !!a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeActions(note, searchResultNoteIds) {
|
||||||
|
const actions = getActions(note);
|
||||||
|
|
||||||
|
for (const resultNoteId of searchResultNoteIds) {
|
||||||
|
const resultNote = becca.getNote(resultNoteId);
|
||||||
|
|
||||||
|
if (!resultNote || resultNote.isDeleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
try {
|
||||||
|
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
|
||||||
|
|
||||||
|
ACTION_HANDLERS[action.name](action, resultNote);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`ExecuteScript search action failed with ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
executeActions
|
||||||
|
};
|
@ -16,27 +16,25 @@
|
|||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="clone-to-form">
|
<div class="modal-body">
|
||||||
<div class="modal-body">
|
Affected notes: <span id="affected-note-count">0</span>
|
||||||
Affected notes: <span id="affected-note-count">0</span>
|
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="include-descendants">
|
<input class="form-check-input" type="checkbox" value="" id="include-descendants">
|
||||||
<label class="form-check-label" for="include-descendants">
|
<label class="form-check-label" for="include-descendants">
|
||||||
Include descendant notes
|
Include descendant notes
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
Available actions:
|
|
||||||
|
|
||||||
<div id="bulk-available-action-list"></div>
|
|
||||||
|
|
||||||
<div id="bulk-existing-action-list"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary">Execute bulk actions</button>
|
Available actions:
|
||||||
</div>
|
|
||||||
</form>
|
<table id="bulk-available-action-list"></table>
|
||||||
|
|
||||||
|
<div id="bulk-existing-action-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">Execute bulk actions</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user