bulk actions WIP

This commit is contained in:
zadam 2022-06-03 17:29:08 +02:00
parent e1cd09df36
commit 9ce3e7e7d2
18 changed files with 178 additions and 94 deletions

View file

@ -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>

View file

@ -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);
}

View 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
};

View file

@ -539,6 +539,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
subNode.load();
}
});
},
select: () => {
// TODO
}
});

View file

@ -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() {

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,
};

View file

@ -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>