egroupware_official/api/js/egw_action/EgwAction.ts
milan 5e3c67a5cf converted egw_action from javascript to typescript
classes are now uppercase and in their own files. lowercase classes are deprecated.
Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
2023-07-10 16:54:22 +02:00

720 lines
26 KiB
TypeScript

/**
* EGroupware egw_action framework - egw action framework
*
* @link https://www.egroupware.org
* @author Andreas Stöckel <as@stylite.de>
* @copyright 2011 by Andreas Stöckel
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package egw_action
*/
import {egwActionStoreJSON, EgwFnct} from "./egw_action_common";
import {IegwAppLocal} from "../jsapi/egw_global";
import {egw_getObjectManager} from "./egw_action";
export class EgwAction {
public readonly id: string;
private caption: string;
group: number;
order: number;
public set_caption(_value) {
this.caption = _value;
}
private iconUrl: string;
public set_iconUrl(_value) {
this.iconUrl = _value;
}
allowOnMultiple: boolean | string | number;
/**
* The allowOnMultiple property may be true, false, "only" (> 1) or number of select, e.g. 2
*
* @param {(boolean|string|number)} _value
*/
public set_allowOnMultiple(_value: boolean | string | number) {
this.allowOnMultiple = _value
}
public readonly enabled: EgwFnct;
public set_enabled(_value) {
this.enabled.setValue(_value);
}
public hideOnDisabled = false;
public data: any = {}; // Data which can be freely assigned to the action
/**
* @deprecated just set the data parameter with '=' sign to use its setter
* @param _value
*/
public set_data(_value) {
this.data = _value
}
type = "default"; //All derived classes have to override this!
canHaveChildren: boolean | string[] = false; //Has to be overwritten by inheriting action classes
// this is not bool all the time. Can be ['popup'] e.g. List of egwActionClasses that are allowed to have children?
readonly parent: EgwAction;
children: EgwAction[] = []; //i guess
private readonly onExecute = new EgwFnct(this, null, []);
/**
* Set to either a confirmation prompt, or TRUE to indicate that this action
* cares about large selections and to ask the confirmation prompt(s)
*
* --set in egw_action_popup--
* @param {String|Boolean} _value
*/
public confirm_mass_selection: string | boolean = undefined
/**
* The set_onExecute function is the setter function for the onExecute event of
* the EgwAction object. There are three possible types the passed "_value" may
* take:
* 1. _value may be a string with the word "javaScript:" prefixed. The function
* which is specified behind the colon and which has to be in the global scope
* will be executed.
* 2. _value may be a boolean, which specifies whether the external onExecute handler
* (passed as "_handler" in the constructor) will be used.
* 3. _value may be a JS function which will then be called.
* In all possible situation, the called function will get the following parameters:
* 1. A reference to this action
* 2. The senders, an array of all objects (JS)/object ids (PHP) which evoked the event
*
* @param {(string|function|boolean)} _value
*/
public set_onExecute(_value) {
this.onExecute.setValue(_value)
}
public hideOnMobile = false;
public disableIfNoEPL = false;
/**
* Default icons for given id
*/
public defaultIcons = {
view: 'view',
edit: 'edit',
open: 'edit', // does edit if possible, otherwise view
add: 'new',
new: 'new',
delete: 'delete',
cat: 'attach', // add as category icon to api
document: 'etemplate/merge',
print: 'print',
copy: 'copy',
move: 'move',
cut: 'cut',
paste: 'editpaste',
save: 'save',
apply: 'apply',
cancel: 'cancel',
continue: 'continue',
next: 'continue',
finish: 'finish',
back: 'back',
previous: 'back',
close: 'close'
};
/**
* Constructor for EgwAction object
*
* @param {EgwAction} _parent
* @param {string} _id
* @param {string} _caption
* @param {string} _iconUrl
* @param {(string|function)} _onExecute
* @param {boolean} _allowOnMultiple
* @returns {EgwAction}
**/
constructor(_parent: EgwAction, _id: string, _caption: string = "", _iconUrl: string = "", _onExecute: string | Function = null, _allowOnMultiple: boolean = true) {
if (_parent && (typeof _id != "string" || !_id) && _parent.type !== "actionManager") {
throw "EgwAction _id must be a non-empty string!";
}
this.parent = _parent;
this.id = _id;
this.caption = _caption;
this.iconUrl = _iconUrl;
if (_onExecute !== null) {
this.set_onExecute(_onExecute)
}
this.allowOnMultiple = _allowOnMultiple;
this.enabled = new EgwFnct(this, true);
}
/**
* Clears the element and removes it from the parent container
*/
public remove() {
// Remove all references to the child elements
this.children = [];
// Remove this element from the parent list
if (this.parent) {
const idx = this.parent.children.indexOf(this);
if (idx >= 0) {
this.parent.children.splice(idx, 1);
}
}
}
/**
* Searches for a specific action with the given id
*
* @param {(string|number)} _id ID of the action to find
* @param {number} [_search_depth=Infinite] How deep into existing action children
* to search.
*
* @return {(EgwAction|null)}
*/
public getActionById(_id: string, _search_depth: number = Number.MAX_VALUE): EgwAction {
// If the current action object has the given id, return this object
if (this.id == _id) {
return this;
}
// If this element is capable of having children, search those for the given
// action id
if (this.canHaveChildren) {
for (let i = 0; i < this.children.length && _search_depth > 0; i++) {
const elem = this.children[i].getActionById(_id, _search_depth - 1);
if (elem) {
return elem;
}
}
}
return null;
};
/**
* Searches for actions having an attribute with a certain value
*
* Example: actionManager.getActionsByAttr("checkbox", true) returns all checkbox actions
*
* @param {string} _attr attribute name
* @param _val attribute value
* @return array
*/
public getActionsByAttr(_attr: string | number, _val: any = undefined) {
let _actions = [];
// If the current action object has the given attr AND value, or no value was provided, return it
if (typeof this[_attr] != "undefined" && (this[_attr] === _val || typeof _val === "undefined" && this[_attr] !== null)) {
_actions.push(this);
}
// If this element is capable of having children, search those too
if (this.canHaveChildren) {
for (let i = 0; i < this.children.length; i++) {
_actions = _actions.concat(this.children[i].getActionsByAttr(_attr, _val));
}
}
return _actions;
};
/**
* Adds a new action to the child elements.
*
* @param {string} _type
* @param {string} _id
* @param {string} _caption
* @param {string} _iconUrl
* @param {(string|function)} _onExecute
* @param {boolean} _allowOnMultiple
*/
public addAction(_type: string, _id: string, _caption: string = "", _iconUrl: string = "", _onExecute: string | Function = null, _allowOnMultiple: boolean = true): EgwAction {
//Get the constructor for the given action type
if (!(_type in window._egwActionClasses)) {
//TODO doesn't default instead of popup make more sense here??
_type = "popup"
}
// Only allow adding new actions, if this action class allows it.
if (this.canHaveChildren) {
const constructor: any = window._egwActionClasses[_type]?.actionConstructor;
if (typeof constructor == "function") {
const action: EgwAction = new constructor(this, _id, _caption, _iconUrl, _onExecute, _allowOnMultiple);
this.children.push(action);
return action;
} else {
throw "Given action type not registered.";
}
} else {
throw "This action does not allow child elements!";
}
};
/**
* Updates the children of this element
*
* @param {object} _actions { id: action, ...}
* @param {string} _app defaults to egw_getAppname()
*/
public updateActions(_actions: any[] | Object, _app) {
if (this.canHaveChildren) {
if (typeof _app == "undefined") _app = window.egw(window).app_name()
/*
this is an egw Object as defined in egw_core.js
probably not because it changes on runtime
*/
const localEgw: IegwAppLocal = window.egw(_app);
//replaced jQuery calls
if (Array.isArray(_actions)) {
//_actions is now an object for sure
//happens in test website
_actions = {..._actions};
}
for (const i in _actions) {
let elem = _actions[i];
if (typeof elem == "string") {
//changes type of elem to Object {caption:string}
_actions[i] = elem = {caption: elem};
}
if (typeof elem == "object") // isn't this always true because of step above? Yes if elem was a string before
{
// use attr name as id, if none given
if (typeof elem.id != "string") elem.id = i;
// if no iconUrl given, check icon and default icons
if (typeof elem.iconUrl == "undefined") {
if (typeof elem.icon == "undefined") elem.icon = this.defaultIcons[elem.id]; // only works if default Icon is available
if (typeof elem.icon != "undefined") {
elem.iconUrl = localEgw.image(elem.icon);
}
//if there is no icon and none can be found remove icon tag from the object
delete elem.icon;
}
// always add shortcut for delete
if (elem.id == "delete" && typeof elem.shortcut == "undefined") {
elem.shortcut = {
keyCode: 46, shift: false, ctrl: false, alt: false, caption: localEgw.lang('Del')
};
}
// translate caption
if (elem.caption && (typeof elem.no_lang == "undefined" || !elem.no_lang)) {
elem.caption = localEgw.lang(elem.caption);
if (typeof elem.hint == "string") elem.hint = localEgw.lang(elem.hint);
}
delete elem.no_lang;
// translate confirm messages and place '?' at the end iff not there yet
for (const attr in {confirm: '', confirm_multiple: ''}) {
if (typeof elem[attr] == "string") {
elem[attr] = localEgw.lang(elem[attr]) + ((elem[attr].substr(-1) != '?') ? '?' : '');
}
}
// set certain enabled functions iff elem.enabled is not set so false
if (typeof elem.enabled == 'undefined' || elem.enabled === true) {
if (typeof elem.enableClass != "undefined") {
elem.enabled = this.enableClass;
} else if (typeof elem.disableClass != "undefined") {
elem.enabled = this.not_disableClass;
} else if (typeof elem.enableId != "undefined") {
elem.enabled = this.enableId;
}
}
//Check whether the action already exists, and if no, add it to the
//actions list
let action = this.getActionById(elem.id);
if (!action) {
//elem will be popup on default
if (typeof elem.type == "undefined") {
elem.type = "popup";
}
let constructor = null;
// Check whether the given type is inside the "canHaveChildren"
// array // here can have children is used as array where possible types of children are stored
if (this.canHaveChildren !== true && this.canHaveChildren.indexOf(elem.type) == -1) {
throw "This child type '" + elem.type + "' is not allowed!";
}
if (typeof window._egwActionClasses[elem.type] != "undefined") {
constructor = window._egwActionClasses[elem.type].actionConstructor;
} else {
throw "Given action type \"" + elem.type + "\" not registered, because type does not exist";
}
if (typeof constructor == "function" && constructor) action = new constructor(this, elem.id); else throw "Given action type \"" + elem.type + "\" not registered.";
this.children.push(action);
}
action.updateAction(elem);
// Add sub-actions to the action
if (elem.children) {
action.updateActions(elem.children, _app);
}
}
}
} else {
throw "This action element cannot have children!";
}
};
/**
* Callback to check if none of _senders rows has disableClass set
*
* @param _action EgwAction object, we use _action.data.disableClass to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if none has disableClass, false otherwise
*/
private not_disableClass(_action: EgwAction, _senders: any, _target: any) {
if (_target.iface.getDOMNode()) {
return !(_target.iface.getDOMNode()).classList.contains(_action.data.disableClass);
} else if (_target.id) {
// Checking on a something that doesn't have a DOM node, like a nm row
// that's not currently rendered
const data = window.egw.dataGetUIDdata(_target.id);
if (data && data.data && data.data.class) {
return -1 === data.data.class.split(' ').indexOf(_action.data.disableClass);
}
}
};
/**
* Callback to check if all of _senders rows have enableClass set
*
* @param _action EgwAction object, we use _action.data.enableClass to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if none has disableClass, false otherwise
*/
//TODO senders is never used in function body??
private enableClass(_action: EgwAction, _senders: any[], _target: any) {
if (typeof _target == 'undefined') {
return false;
} else if (_target.iface.getDOMNode()) {
return (_target.iface.getDOMNode()).classList.contains(_action.data.enableClass);
} else if (_target.id) {
// Checking on a something that doesn't have a DOM node, like a nm row
// that's not currently rendered. Not as good as an actual DOM node check
// since things can get missed, but better than nothing.
const data = window.egw.dataGetUIDdata(_target.id);
if (data && data.data && data.data.class) {
return -1 !== data.data.class.split(' ').indexOf(_action.data.enableClass);
}
}
};
/**
* Enable an _action, if it matches a given regular expression in _action.data.enableId
*
* @param _action EgwAction object, we use _action.data.enableId to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if _target.id matches _action.data.enableId
*/
private enableId(_action: EgwAction, _senders: any[], _target: any) {
if (typeof _action.data.enableId == 'string') {
_action.data.enableId = new RegExp(_action.data.enableId);
}
return _target.id.match(_action.data.enableId);
};
/**
* Applies the same onExecute handler to all actions which don't have an execute
* handler set.
*
* @param {(string|function)} _value
*/
public setDefaultExecute(_value: string | Function): void {
// Check whether the onExecute handler of this action should be set
if (this.type != "actionManager" && !this.onExecute.hasHandler()) {
this.onExecute.isDefault = true;
this.onExecute.setValue(_value);
}
// Apply the value to all children
if (this.canHaveChildren) {
for (const elem of this.children) {
elem.setDefaultExecute(_value);
}
}
};
/**
* Executes this action by using the method specified in the onExecute setter.
*
* @param {array} _senders array with references to the objects which caused the action
* @param {object} _target is an optional parameter which may represent e.g. a drag drop target
*/
execute(_senders, _target = null): any {
if (!this._check_confirm_mass_selections(_senders, _target)) {
return this._check_confirm(_senders, _target);
}
};
/**
* If this action needs to confirm mass selections (attribute confirm_mass_selection = true),
* check for any checkboxes that have a confirmation prompt (confirm_mass_selection is a string)
* and are unchecked. We then show the prompt, and set the checkbox to their answer.
*
* * This is only considered if there are more than 20 entries selected.
*
* * Only the first confirmation prompt / checkbox action will be used, others
* will be ignored.
*
* @param {type} _senders
* @param {type} _target
* @returns {Boolean}
*/
private _check_confirm_mass_selections(_senders, _target) {
const obj_manager: any = egw_getObjectManager(this.getManager().parent.id, false);
if (!obj_manager) {
return false;
}
// Action needs to care about mass selection - check for parent that cares too
let confirm_mass_needed = false;
let action: EgwAction = this;
while (action && action !== obj_manager.manager && !confirm_mass_needed) {
confirm_mass_needed = !!action.confirm_mass_selection;
action = action.parent;
}
if (!confirm_mass_needed) return false;
// Check for confirm mass selection checkboxes
const confirm_mass_selections = obj_manager.manager.getActionsByAttr("confirm_mass_selection");
confirm_mass_needed = _senders.length > 20;
//no longer needed because of '=>' notation
//const self = this;
// Find & show prompt
for (let i = 0; confirm_mass_needed && i < confirm_mass_selections.length; i++) {
const check = confirm_mass_selections[i];
if (check.checkbox === false || check.checked === true) {
continue
}
// Show the mass selection prompt
const msg = window.egw.lang(check.confirm_mass_selection, obj_manager.getAllSelected() ? window.egw.lang('all') : _senders.length);
const callback = (_button) => {
// YES = unchecked, NO = checked
check.set_checked(_button === window.Et2Dialog.NO_BUTTON);
if (_button !== window.Et2Dialog.CANCEL_BUTTON) {
this._check_confirm(_senders, _target);
}
};
window.Et2Dialog.show_dialog(callback, msg, this.data.hint, {}, window.Et2Dialog.BUTTONS_YES_NO_CANCEL, window.Et2Dialog.QUESTION_MESSAGE);
return true;
}
return false;
};
/**
* Check to see if action needs to be confirmed by user before we do it
*/
private _check_confirm(_senders, _target) {
// check if actions needs to be confirmed first
if (this.data && (this.data.confirm || this.data.confirm_multiple) &&
this.onExecute.functionToPerform != window.nm_action && typeof window.Et2Dialog != 'undefined') // let old eTemplate run its own confirmation from nextmatch_action.js
{
let msg = this.data.confirm || '';
if (_senders.length > 1) {
if (this.data.confirm_multiple) {
msg = this.data.confirm_multiple;
}
// check if we have all rows selected
const obj_manager = egw_getObjectManager(this.getManager().parent.id, false);
if (obj_manager && obj_manager.getAllSelected()) {
msg += "\n\n" + window.egw.lang('Attention: action will be applied to all rows, not only visible ones!');
}
}
//no longer needed because of '=>' notation
//var self = this;
if (msg.trim().length > 0) {
if (this.data.policy_confirmation && window.egw.app('policy')) {
import(window.egw.link('/policy/js/app.min.js')).then(() => {
if (typeof window.app.policy === 'undefined' || typeof window.app.policy.confirm === 'undefined') {
window.app.policy = new window.app.classes.policy();
}
window.app.policy.confirm(this, _senders, _target);
}
);
return;
}
window.Et2Dialog.show_dialog((_button) => {
if (_button == window.Et2Dialog.YES_BUTTON) {
// @ts-ignore
return this.onExecute.exec(this, _senders, _target);
}
}, msg, this.data.hint, {}, window.Et2Dialog.BUTTONS_YES_NO, window.Et2Dialog.QUESTION_MESSAGE);
return;
}
}
// @ts-ignore
return this.onExecute.exec(this, _senders, _target);
};
private updateAction(_data: Object) {
egwActionStoreJSON(_data, this, "data")
}
/**
* Returns the parent action manager
*/
getManager(): EgwAction {
if (this.type == "actionManager") {
return this;
} else if (this.parent) {
return this.parent.getManager();
} else {
return null;
}
}
/**
* The appendToGraph function generates an action tree which automatically contains
* all parent elements. If the appendToGraph function is called for a
*
* @param {not an array} _tree contains the tree structure - pass an object containing {root:Tree}??TODO
* the empty array "root" to this function {"root": []}. The result will be stored in
* this array.
* @param {boolean} _addChildren is used internally to prevent parent elements from
* adding their children automatically to the tree.
*/
public appendToTree(_tree: { root: Tree }, _addChildren: boolean = true) {
if (typeof _addChildren == "undefined") {
_addChildren = true;
}
// Preset some variables
const root: Tree = _tree.root;
let parentNode: TreeElem = null;
let node: TreeElem = {
"action": this, "children": []
};
if (this.parent && this.type != "actionManager") {
// Check whether the parent container has already been added to the tree
parentNode = _egwActionTreeFind(root, this.parent);
if (!parentNode) {
parentNode = this.parent.appendToTree(_tree, false);
}
// Check whether this element has already been added to the parent container
let added = false;
for (const child of parentNode.children) {
if (child.action == this) {
node = child;
added = true;
break;
}
}
if (!added) {
parentNode.children.push(node);
}
} else {
let added = false;
for (const treeElem of root) {
if (treeElem.action == this) {
node = treeElem;
added = true;
break;
}
}
if (!added) {
// Add this element to the root if it has no parent
root.push(node);
}
}
if (_addChildren) {
for (const child of this.children) {
child.appendToTree(_tree, true);
}
}
return node;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_hideOnDisabled(_value) {
this.hideOnDisabled = _value;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_hideOnMobile(_value) {
this.hideOnMobile = _value;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_disableIfNoEPL(_value) {
this.disableIfNoEPL = _value;
};
set_hint(hint: string) {
}
}
type TreeElem = { action: EgwAction, children: Tree }
type Tree = TreeElem[]
/**
* finds an egwAction in the given tree
* @param {Tree}_tree where to search
* @param {EgwAction}_elem elem to search
* @returns {TreeElem} the treeElement for corresponding _elem if found, null else
*/
function _egwActionTreeFind(_tree: Tree, _elem: EgwAction): TreeElem {
for (const current of _tree) {
if (current.action == _elem) {
return current;
}
if (typeof current.children != "undefined") {
const elem = _egwActionTreeFind(current.children, _elem);
if (elem) {
return elem;
}
}
}
return null;
}