mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
bulk actions WIP
This commit is contained in:
parent
e1cd09df36
commit
9ce3e7e7d2
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,7 +3,7 @@
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="openjdk-18" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
@ -1,7 +1,39 @@
|
||||
import utils from "../services/utils.js";
|
||||
import bulkActionService from "../services/bulk_action.js";
|
||||
import froca from "../services/froca.js";
|
||||
|
||||
const $dialog = $("#bulk-assign-attributes-dialog");
|
||||
const $availableActionList = $("#bulk-available-action-list");
|
||||
const $existingActionList = $("#bulk-existing-action-list");
|
||||
|
||||
$dialog.on('click', '[data-action-add]', async event => {
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await bulkActionService.addAction('bulkaction', actionName);
|
||||
|
||||
await refresh();
|
||||
});
|
||||
|
||||
for (const action of bulkActionService.ACTION_CLASSES) {
|
||||
$availableActionList.append(
|
||||
$('<button class="btn btn-sm">')
|
||||
.attr('data-action-add', action.actionName)
|
||||
.text(action.actionTitle)
|
||||
);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const bulkActionNote = await froca.getNote('bulkaction');
|
||||
|
||||
const actions = bulkActionService.parseActions(bulkActionNote);
|
||||
|
||||
$existingActionList
|
||||
.empty()
|
||||
.append(...actions.map(action => action.render()));
|
||||
}
|
||||
|
||||
export async function showDialog(nodes) {
|
||||
await refresh();
|
||||
|
||||
utils.openDialog($dialog);
|
||||
}
|
||||
|
68
src/public/app/services/bulk_action.js
Normal file
68
src/public/app/services/bulk_action.js
Normal file
@ -0,0 +1,68 @@
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import MoveNoteSearchAction from "../widgets/search_actions/move_note.js";
|
||||
import DeleteNoteSearchAction from "../widgets/search_actions/delete_note.js";
|
||||
import DeleteNoteRevisionsSearchAction from "../widgets/search_actions/delete_note_revisions.js";
|
||||
import DeleteLabelSearchAction from "../widgets/search_actions/delete_label.js";
|
||||
import DeleteRelationSearchAction from "../widgets/search_actions/delete_relation.js";
|
||||
import RenameLabelSearchAction from "../widgets/search_actions/rename_label.js";
|
||||
import RenameRelationSearchAction from "../widgets/search_actions/rename_relation.js";
|
||||
import SetLabelValueSearchAction from "../widgets/search_actions/set_label_value.js";
|
||||
import SetRelationTargetSearchAction from "../widgets/search_actions/set_relation_target.js";
|
||||
import ExecuteScriptSearchAction from "../widgets/search_actions/execute_script.js";
|
||||
|
||||
const ACTION_CLASSES = [
|
||||
MoveNoteSearchAction,
|
||||
DeleteNoteSearchAction,
|
||||
DeleteNoteRevisionsSearchAction,
|
||||
DeleteLabelSearchAction,
|
||||
DeleteRelationSearchAction,
|
||||
RenameLabelSearchAction,
|
||||
RenameRelationSearchAction,
|
||||
SetLabelValueSearchAction,
|
||||
SetRelationTargetSearchAction,
|
||||
ExecuteScriptSearchAction
|
||||
];
|
||||
|
||||
async function addAction(noteId, actionName) {
|
||||
await server.post(`notes/${noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
value: JSON.stringify({
|
||||
name: actionName
|
||||
})
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
}
|
||||
|
||||
function parseActions(note) {
|
||||
const actionLabels = note.getLabels('action');
|
||||
|
||||
return actionLabels.map(actionAttr => {
|
||||
let actionDef;
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
} catch (e) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ActionClass = ACTION_CLASSES.find(actionClass => actionClass.actionName === actionDef.name);
|
||||
|
||||
if (!ActionClass) {
|
||||
logError(`No action class for '${actionDef.name}' found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ActionClass(actionAttr, actionDef);
|
||||
})
|
||||
.filter(action => !!action);
|
||||
}
|
||||
|
||||
export default {
|
||||
addAction,
|
||||
parseActions,
|
||||
ACTION_CLASSES
|
||||
};
|
@ -539,6 +539,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
subNode.load();
|
||||
}
|
||||
});
|
||||
},
|
||||
select: () => {
|
||||
// TODO
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5,14 +5,6 @@ import ws from "../../services/ws.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import treeService from "../../services/tree.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";
|
||||
import RenameRelationSearchAction from "../search_actions/rename_relation.js";
|
||||
import ExecuteScriptSearchAction from "../search_actions/execute_script.js"
|
||||
import SearchString from "../search_options/search_string.js";
|
||||
import FastSearch from "../search_options/fast_search.js";
|
||||
import Ancestor from "../search_options/ancestor.js";
|
||||
@ -20,10 +12,9 @@ import IncludeArchivedNotes from "../search_options/include_archived_notes.js";
|
||||
import OrderBy from "../search_options/order_by.js";
|
||||
import SearchScript from "../search_options/search_script.js";
|
||||
import Limit from "../search_options/limit.js";
|
||||
import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js";
|
||||
import Debug from "../search_options/debug.js";
|
||||
import appContext from "../../services/app_context.js";
|
||||
import MoveNoteSearchAction from "../search_actions/move_note.js";
|
||||
import bulkActionService from "../../services/bulk_action.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="search-definition-widget">
|
||||
@ -127,28 +118,7 @@ const TPL = `
|
||||
<span class="bx bxs-zap"></span>
|
||||
action
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" data-action-add="moveNote">
|
||||
Move note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNote">
|
||||
Delete note</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions">
|
||||
Delete note revisions</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteLabel">
|
||||
Delete label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="deleteRelation">
|
||||
Delete relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameLabel">
|
||||
Rename label</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="renameRelation">
|
||||
Rename relation</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setLabelValue">
|
||||
Set label value</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="setRelationTarget">
|
||||
Set relation target</a>
|
||||
<a class="dropdown-item" href="#" data-action-add="executeScript">
|
||||
Execute script</a>
|
||||
</div>
|
||||
<div class="dropdown-menu action-list"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -193,23 +163,6 @@ const OPTION_CLASSES = [
|
||||
Debug
|
||||
];
|
||||
|
||||
const ACTION_CLASSES = {};
|
||||
|
||||
for (const clazz of [
|
||||
MoveNoteSearchAction,
|
||||
DeleteNoteSearchAction,
|
||||
DeleteNoteRevisionsSearchAction,
|
||||
DeleteLabelSearchAction,
|
||||
DeleteRelationSearchAction,
|
||||
RenameLabelSearchAction,
|
||||
RenameRelationSearchAction,
|
||||
SetLabelValueSearchAction,
|
||||
SetRelationTargetSearchAction,
|
||||
ExecuteScriptSearchAction
|
||||
]) {
|
||||
ACTION_CLASSES[clazz.actionName] = clazz;
|
||||
}
|
||||
|
||||
export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return this.note && this.note.type === 'search';
|
||||
@ -228,6 +181,15 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$component = this.$widget.find('.search-definition-widget');
|
||||
this.$actionList = this.$widget.find('.action-list');
|
||||
|
||||
for (const action of bulkActionService.ACTION_CLASSES) {
|
||||
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 => {
|
||||
const searchOptionName = $(event.target).attr('data-search-option-add');
|
||||
@ -244,19 +206,11 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
|
||||
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();
|
||||
const actionName = $(event.target).attr('data-action-add');
|
||||
|
||||
await bulkActionService.addAction(this.noteId, actionName);
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
@ -319,35 +273,13 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
this.$actionOptions.empty();
|
||||
const actions = bulkActionService.parseActions(this.note);
|
||||
|
||||
const actionLabels = this.note.getLabels('action');
|
||||
this.$actionOptions
|
||||
.empty()
|
||||
.append(...actions.map(action => action.render()));
|
||||
|
||||
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).setParent(this);
|
||||
this.child(action);
|
||||
|
||||
this.$actionOptions.append(action.render());
|
||||
}
|
||||
|
||||
this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden');
|
||||
this.$searchAndExecuteButton.css('visibility', actions.length > 0 ? 'visible' : 'hidden');
|
||||
}
|
||||
|
||||
getContent() {
|
||||
|
@ -3,10 +3,8 @@ import ws from "../../services/ws.js";
|
||||
import Component from "../component.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
export default class AbstractSearchAction extends Component {
|
||||
export default class AbstractSearchAction {
|
||||
constructor(attribute, actionDef) {
|
||||
super();
|
||||
|
||||
this.attribute = attribute;
|
||||
this.actionDef = actionDef;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ const TPL = `
|
||||
|
||||
export default class DeleteLabelSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "deleteLabel"; }
|
||||
static get actionTitle() { return "Delete label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -14,6 +14,7 @@ const TPL = `
|
||||
|
||||
export default class DeleteNoteSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "deleteNote"; }
|
||||
static get actionTitle() { return "Delete note"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
||||
|
@ -21,6 +21,7 @@ const TPL = `
|
||||
|
||||
export default class DeleteNoteRevisionsSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "deleteNoteRevisions"; }
|
||||
static get actionTitle() { return "Delete note revisions"; }
|
||||
|
||||
doRender() {
|
||||
return $(TPL);
|
||||
|
@ -22,6 +22,7 @@ const TPL = `
|
||||
|
||||
export default class DeleteRelationSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "deleteRelation"; }
|
||||
static get actionTitle() { return "Delete relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -35,6 +35,7 @@ const TPL = `
|
||||
|
||||
export default class ExecuteScriptSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "executeScript"; }
|
||||
static get actionTitle() { return "Execute script"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -35,6 +35,7 @@ const TPL = `
|
||||
|
||||
export default class MoveNoteSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "moveNote"; }
|
||||
static get actionTitle() { return "Move note"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -29,6 +29,7 @@ const TPL = `
|
||||
|
||||
export default class RenameLabelSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "renameLabel"; }
|
||||
static get actionTitle() { return "Rename label"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -29,6 +29,7 @@ const TPL = `
|
||||
|
||||
export default class RenameRelationSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "renameRelation"; }
|
||||
static get actionTitle() { return "Rename relation"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -39,6 +39,7 @@ const TPL = `
|
||||
|
||||
export default class SetLabelValueSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setLabelValue"; }
|
||||
static get actionTitle() { return "Set label value"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -41,6 +41,7 @@ const TPL = `
|
||||
|
||||
export default class SetRelationTargetSearchAction extends AbstractSearchAction {
|
||||
static get actionName() { return "setRelationTarget"; }
|
||||
static get actionTitle() { return "Set relation target"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -219,10 +219,28 @@ function getShareRoot() {
|
||||
return shareRoot;
|
||||
}
|
||||
|
||||
function getBulkActionNote() {
|
||||
let bulkActionNote = becca.getNote('bulkaction');
|
||||
|
||||
if (!bulkActionNote) {
|
||||
bulkActionNote = noteService.createNewNote({
|
||||
branchId: 'bulkaction',
|
||||
noteId: 'bulkaction',
|
||||
title: 'Bulk action',
|
||||
type: 'text',
|
||||
content: '',
|
||||
parentNoteId: getHiddenRoot().noteId
|
||||
}).note;
|
||||
}
|
||||
|
||||
return bulkActionNote;
|
||||
}
|
||||
|
||||
function createMissingSpecialNotes() {
|
||||
getSinglesNoteRoot();
|
||||
getSqlConsoleRoot();
|
||||
getGlobalNoteMap();
|
||||
getBulkActionNote();
|
||||
// share root is not automatically created since it's visible in the tree and many won't need it/use it
|
||||
|
||||
const hidden = getHiddenRoot();
|
||||
@ -239,5 +257,6 @@ module.exports = {
|
||||
createSearchNote,
|
||||
saveSearchNote,
|
||||
createMissingSpecialNotes,
|
||||
getShareRoot
|
||||
getShareRoot,
|
||||
getBulkActionNote,
|
||||
};
|
||||
|
@ -1,3 +1,12 @@
|
||||
<style>
|
||||
#bulk-available-action-list button {
|
||||
font-size: small;
|
||||
padding: 2px 7px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="bulk-assign-attributes-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
@ -10,10 +19,23 @@
|
||||
</div>
|
||||
<form id="clone-to-form">
|
||||
<div class="modal-body">
|
||||
Hi!
|
||||
Affected notes: <span id="affected-note-count">0</span>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="include-descendants">
|
||||
<label class="form-check-label" for="include-descendants">
|
||||
Include descendant notes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Available actions:
|
||||
|
||||
<div id="bulk-available-action-list"></div>
|
||||
|
||||
<div id="bulk-existing-action-list"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Assign attributes</button>
|
||||
<button type="submit" class="btn btn-primary">Execute bulk actions</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user