diff --git a/htdocs/admin/tools/ui/class/documentation.class.php b/htdocs/admin/tools/ui/class/documentation.class.php index 6d451f43084..de8e84212c2 100644 --- a/htdocs/admin/tools/ui/class/documentation.class.php +++ b/htdocs/admin/tools/ui/class/documentation.class.php @@ -260,8 +260,11 @@ class Documentation 'Introduction' => '#titlesection-basicusage', 'ConsoleHelp' => '#titlesection-console-help', 'JSDolibarrhooks' => '#titlesection-hooks', + 'JSDolibarrAwaitHooks' => '#titlesection-await-hooks', + 'JSDolibarrhooksReadyVsInit' => '#titlesection-event-init-vs-ready', 'ExampleOfCreatingNewContextTool' => '#titlesection-create-tool-example', 'SetEventMessageTool' => '#titlesection-tool-seteventmessage', + 'SetAndUseContextVars' => '#titlesection-contextvars', ), ), ) diff --git a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js index fe4ed7c9292..a021e36ce7c 100644 --- a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js +++ b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js @@ -43,12 +43,58 @@ // Private storage for secure tools (non-replaceable) const _tools = {}; + // Private storage for secure context vars or constants (non-replaceable) + const _contextVars = {}; + // Native event dispatcher (standard DOM) const _events = new EventTarget(); + const _awaitHooks = {}; // Async hooks storage + // Debug flag (disabled by default) let _debug = false; + // ------------------------- + // Internal helper functions + // ------------------------- + function _ensureEvent(name) { if (!_awaitHooks[name]) _awaitHooks[name] = []; } + function _generateId() { return 'hook_' + Math.random().toString(36).slice(2); } + function _idExists(name, id) { return _awaitHooks[name].some(h => h.id === id); } + + /** + * Insert a new hook entry in the array respecting optional before/after lists + */ + function _insertWithOrder(arr, entry, beforeList, afterList) { + if ((!beforeList || beforeList.length === 0) && (!afterList || afterList.length === 0)) { + arr.push(entry); + return arr; + } + + let ordered = [...arr]; + let index = ordered.length; + + if (beforeList && beforeList.length > 0) { + for (const target of beforeList) { + const i = ordered.findIndex(h => h.id === target); + if (i !== -1 && i < index) index = i; + } + } + + if (afterList && afterList.length > 0) { + for (const target of afterList) { + const i = ordered.findIndex(h => h.id === target); + if (i !== -1 && i >= index) index = i + 1; + } + } + + if (index > ordered.length) index = ordered.length; + ordered.splice(index, 0, entry); + return ordered; + } + + // ------------------------- + // Dolibarr object + // ------------------------- const Dolibarr = { /** @@ -83,12 +129,12 @@ this.log(`Tool defined: ${name}, triggerHook: ${triggerHook}, overwrite: ${overwrite} `); if(triggerHook) { - Dolibarr.executeHook('defineTool', { toolName: name, overwrite: overwrite }); + this.executeHook('defineTool', { toolName: name, overwrite }); } }, /** - * Checks if a tool already exists. + * Check if tool exists * @param {string} name Tool name * @returns {boolean} true if exists */ @@ -97,12 +143,73 @@ }, /** - * Enables or disables debug mode. - * When enabled, Dolibarr.log() writes to the console. + * Get read-only snapshot of context variables + */ + get ContextVars() { + return Object.freeze({ ..._contextVars }); + }, + + /** + * Defines a new context variable. + * @param {string} key + * @param {string|number|boolean} value + * @param {boolean} overwrite Allow overwriting existing value + */ + setContextVar(key, value, overwrite = false) { + // Accept only string, number, or boolean + const type = typeof value; + if (type !== 'string' && type !== 'number' && type !== 'boolean') { + throw new TypeError(`Dolibarr: ContextVar '${key}' must be a string, number, or boolean`); + } + + if (!overwrite && _contextVars.hasOwnProperty(key)) { + throw new Error(`Dolibarr: ContextVar '${key}' already defined`); + } + + Object.defineProperty(_contextVars, key, { + value, + writable: false, + configurable: false, + enumerable: true + }); + + this.log(`ContextVar set: ${key} = ${value} (overwrite: ${overwrite})`); + this.executeHook('setContextVar', { key, value, overwrite }); + }, + + + /** + * Set multiple context variables + * @param {Object} vars Object of key/value pairs + * @param {boolean} overwrite Allow overwriting existing values + */ + setContextVars(vars, overwrite = false) { + if (typeof vars !== 'object' || vars === null) { + throw new Error('Dolibarr: setContextVars expects an object'); + } + + for (const [key, value] of Object.entries(vars)) { + this.setContextVar(key, value, overwrite); + } + }, + + /** + * Get a context variable safely + * @param {string} key + * @param {*} fallback Optional fallback if variable not set + * @returns {*} + */ + getContextVar(key, fallback = null) { + return _contextVars.hasOwnProperty(key) ? _contextVars[key] : fallback; + }, + + /** + * Enable or disable debug mode + * @param {boolean} state */ debugMode(state) { _debug = !!state; - // Sauvegarde dans localStorage + // save in localStorage if (typeof window !== "undefined" && window.localStorage) { localStorage.setItem('DolibarrDebugMode', _debug ? '1' : '0'); } @@ -110,7 +217,9 @@ }, /** - * Internal logger (only active when debug mode is enabled). + * Internal logger + * Only prints when debug mode is enabled + * @param {string} msg */ log(msg) { if (_debug) console.log(`Dolibarr: ${msg}`); @@ -145,16 +254,50 @@ }, /** - * Unregisters an event listener. - * @param {string} eventName Event name - * @param {function} callback Listener previously added + * Unregister an event listener + * @param {string} eventName + * @param {function} callback */ off(eventName, callback) { _events.removeEventListener(eventName, callback); + }, + + /** + * Register an asynchronous hook + * @param {string} eventName + * @param {function} fn Async function receiving previous result + * @param {Object} opts Optional {before, after, id} to control order + * @returns {string} The hook ID + */ + onAwait(eventName, fn, opts = {}) { + _ensureEvent(eventName); + let id = opts.id || _generateId(); + if (_idExists(eventName, id)) throw new Error(`onAwait: ID '${id}' already used for '${eventName}'`); + const before = Array.isArray(opts.before) ? opts.before : (opts.before ? [opts.before] : []); + const after = Array.isArray(opts.after) ? opts.after : (opts.after ? [opts.after] : []); + _awaitHooks[eventName] = _insertWithOrder(_awaitHooks[eventName], { id, fn }, before, after); + return id; + }, + + /** + * Execute async hooks sequentially + * @param {string} eventName + * @param {*} data Input data for first hook + * @returns {Promise<*>} Final result after all hooks + */ + async executeHookAwait(eventName, data) { + this.log(`Await Hook executed: ${eventName}`); + + _ensureEvent(eventName); + let result = data; + for (const h of _awaitHooks[eventName]) { + result = await h.fn(result); + } + return result; } }; - // Lock core object to prevent tampering + // Lock Dolibarr core object Object.freeze(Dolibarr); // Expose Dolibarr to window in a protected, non-writable way @@ -167,7 +310,7 @@ }); } - // Restaurer debug mode depuis localStorage + // Restore debug mode from localStorage if (typeof window !== "undefined" && window.localStorage) { const saved = localStorage.getItem('DolibarrDebugMode'); if (saved === '1') { @@ -176,6 +319,31 @@ } + // Force initialise hook init and Ready in good execution order + (function triggerDolibarrHooks() { + // Fire Init first + const fireInit = () => { + Dolibarr.executeHook('Init', { context: Dolibarr }); + Dolibarr.log('Context Init done'); + + // Only after Init is done, fire Ready + fireReady(); + }; + + const fireReady = () => { + Dolibarr.executeHook('Ready', { context: Dolibarr }); + Dolibarr.log('Context Ready done'); + }; + + if (document.readyState === 'complete' || document.readyState === 'interactive') { + // DOM already ready, trigger Init -> Ready in order + fireInit(); + } else { + // Wait for DOM ready, then trigger Init -> Ready + document.addEventListener('DOMContentLoaded', fireInit); + } + })(); + /** * Display help in console log */ @@ -197,23 +365,5 @@ Dolibarr.tools.showConsoleHelp(); - // Trigger Dolibarr:Ready as DOM ready - (function triggerContextInit() { - const initHook = () => { - Dolibarr.executeHook('Ready', { context: Dolibarr }); - Dolibarr.log('Context initialized'); - }; - - if (document.readyState === 'complete' || document.readyState === 'interactive') { - // DOM is already ready - initHook(); - } else { - // Wait for DOM to be ready - document.addEventListener('DOMContentLoaded', initHook); - } - })(); - - - return Dolibarr; }); diff --git a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-tool.seteventmessage.js b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-tool.seteventmessage.js index 2d6d58e2d43..6e8214af2f5 100644 --- a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-tool.seteventmessage.js +++ b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-tool.seteventmessage.js @@ -1,5 +1,4 @@ -document.addEventListener('Dolibarr:Ready', function(e) { - +document.addEventListener('Dolibarr:Init', function(e) { // this tool allow overwrite because of DISABLE_JQUERY_JNOTIFY conf /** * status : 'mesgs' by default, 'warnings', 'errors' @@ -14,9 +13,11 @@ document.addEventListener('Dolibarr:Ready', function(e) { return ''; }; + const type = normalizeStatus(status); + let jnotifyConf = { delay: 1500 // the default time to show each notification (in milliseconds) - , type : normalizeStatus(status) + , type : type , sticky: sticky // determines if the message should be considered "sticky" (user must manually close notification) , closeLabel: "×" // the HTML to use for the "Close" link , showClose: true // determines if the "Close" link should be shown if notification is also sticky @@ -25,7 +26,25 @@ document.addEventListener('Dolibarr:Ready', function(e) { } if(msg.length > 0){ - $.jnotify(msg, jnotifyConf); + if (typeof $.jnotify === "function") { + $.jnotify(msg, jnotifyConf); + } else { + const container = document.getElementById('alert-message-container'); + if (container) { + // Add message to #alert-message-container if exist + const div = document.createElement('div'); + div.className = type; // error, warning, success + div.textContent = msg; // safer than innerHTML + container.appendChild(div); + } else { + console.warn("jnotify is missing and setEventMessage tool wasn't replaced so use alert fallback instead"); + // fallback prefix + let prefix = ''; + if (type === 'error') prefix = 'Error: '; + else if (type === 'warning') prefix = 'Warning: '; + window.alert(prefix + msg); + } + } } else{ Dolibarr.log('setEventMessage : Message is empty'); diff --git a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php index 6bc367f311a..d38904df66e 100644 --- a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php +++ b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php @@ -140,7 +140,7 @@ $documentation->showSidebar(); ?>

Example of code usage

-
+
', @@ -192,31 +192,167 @@ $documentation->showSidebar(); ?>
-

Example of creating a new context tool

+

Async Hooks (Await Hooks) - sequential execution

+ +

+ Dolibarr supports asynchronous hooks using Dolibarr.onAwait() and Dolibarr.executeHookAwait(). + These hooks allow you to register functions that execute in sequence and can modify data before passing it to the next hook. + They are useful for complex workflows where multiple modules or scripts need to process or enrich the same data asynchronously. +

+ +

+ Each hook can optionally specify before or after to control the execution order relative to other hooks. + Every hook registration returns a unique id, which can be used to reference or unregister the hook later. +

+ +

+ Unlike standard synchronous hooks registered with Dolibarr.on(), await hooks return a Promise when executed. + This means you can await their results in your code, and any asynchronous operations inside a hook (e.g., API calls, timers) will be handled correctly before moving to the next hook. +

+ +
+ ">', + ' document.addEventListener(\'Dolibarr:Ready\', async function(e) {', + '', + ' // Register async hooks will be executed in first place', + ' Dolibarr.onAwait(\'calculateDiscount\', async function(order) {', + ' order.total *= 0.9; // Apply 10% discount', + ' return order;', + ' }, { id: \'discount10\' });', + '', + ' // Register async hooks will be executed in third place', + ' Dolibarr.onAwait(\'calculateDiscount\', async function(order) {', + ' if(order.total > 1000) order.total -= 50; // Extra discount over 1000', + ' return order;', + ' }, { id: \'discountOver1000\', after: \'discount10\' });', + '', + ' // Register async hooks will be executed in second place', + ' // this hook item as no id so plus10HookItemId will receive a unique random id ', + ' let plus10HookItemId = Dolibarr.onAwait(\'calculateDiscount\', async function(order) {', + ' order.newObjectAttribute = \'My value\';', + ' order.total += 10;', + ' return order;', + ' }, { before: \'discountOver1000\' });', + '', + ' document.getElementById(\'try-event-yourCustomAwaitHookName\').addEventListener(\'click\', async function(e) {', + ' // Execute all registered await hooks sequentially', + ' let order = {total: 1200};', + ' order = await Dolibarr.executeHookAwait(\'calculateDiscount\', order);', + ' console.log(order); // order.total : 1200 -> 1080 -> 1090 -> 1040', + ' });', + '', + ' });', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> + + Open your console F12 and click on + + +
+ +
+ + + +
+

Difference between Dolibarr:Init and Dolibarr:Ready

+ +

+ Dolibarr provides two main initialization events for its JavaScript context: Dolibarr:Init and Dolibarr:Ready. + Understanding their difference is important when developing modules or tools. +

+ + + +

+ In short, use Dolibarr:Init for setting up tools and context variables, and Dolibarr:Ready for code that needs the DOM and fully initialized context. +

+
+ + + + +
+

Example of creating a new context tool

Defining Tools

You can define reusable and protected tools in the Dolibarr context using Dolibarr.defineTool.

See also dolibarr-context.mock.js for defining all standard Dolibarr tools and creating mock implementations to improve code completion and editor support.

+

Note : a tool can be a class not only a function

', + 'document.addEventListener(\'Dolibarr:Init\', function(e) {', ' // Define a simple tool', ' let overwrite = false; // Once a tool is defined, it cannot be replaced.', ' Dolibarr.defineTool(\'alertUser\', (msg) => alert(\'[Dolibarr] \' + msg), overwrite);', + '});', '', + 'document.addEventListener(\'Dolibarr:Ready\', function(e) {', ' // Use the tool', ' Dolibarr.tools.alertUser(\'hello world\');', + '});', '', ); echo $documentation->showCode($lines, 'php'); ?> -

Protected Tools

@@ -257,7 +393,7 @@ $documentation->showSidebar(); ?>
-

Set event message tool

+

Set event message tool

Instead of calling JNotify directly in your code, use Dolibarr’s setEventMessage tool. @@ -276,7 +412,7 @@ $documentation->showSidebar(); ?>

">', + '', + ); + echo $documentation->showCode($lines, 'php'); + ?> +
+ + +

Add multiple context vars (overridable or not)

+
+ DOL_URL_ROOT,', + ' \'token\' => newToken(),', + ' \'cardObjectElement\' => $object->element,', + ' \'cardObjectId\' => $object->id,', + ' \'currentUserId\' => $user->id', + ' // ...', + ' ];', + '', + ' $contextVars = [', + ' \'lastCardDataRefresh\' => time(),', + ' // ...', + ' ]', + '?>', + '', + ); + echo $documentation->showCode($lines, 'php'); + ?> +
+ +

Get context var

+
+ " >', + ' document.addEventListener(\'Dolibarr:Ready\', function(e) {', + ' let url = Dolibarr.getContextVar(\'DOL_URL_ROOT\', \'The optional fallback value\'));', + ' console.log(url);', + ' });', + '', + ); + echo $documentation->showCode($lines, 'php'); + ?> +
+ +
+
diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index ea677844f4c..8da9a94beab 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -7881,7 +7881,7 @@ class Form // Icon calendar if ($disabled) { $retstringbutton = ''; - $retstring = $retstring . $retstringbutton; + $retstring .= $retstringbutton; } $retstring .= ''; diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index 8a6341ff4b1..5c3ebc2f913 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -7751,7 +7751,7 @@ function price2num($amount, $rounding = '', $option = 0) } elseif ($rounding == 'MT') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT'); // usually 2 or 3 } elseif ($rounding == 'MS') { - $nbofdectoround = isset($conf->global->MAIN_MAX_DECIMALS_STOCK) ? getDolGlobalInt('MAIN_MAX_DECIMALS_STOCK') : 5; + $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_STOCK', 5); } elseif ($rounding == 'CU') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_CURRENCY_UNIT', getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT')); // TODO Use param of currency } elseif ($rounding == 'CT') { @@ -11923,9 +11923,6 @@ function dol_eval_standard($s, $hideerrors = 1, $onlysimplestring = '1') if (!is_scalar($s)) { return "Bad call of dol_eval. First parameter must be a string, found ".var_export($s, true); } - if (!is_scalar($s)) { - return "Bad call of dol_eval. First parameter must be a string, found ".var_export($s, true); - } try { global $dolibarr_main_restrict_eval_methods; diff --git a/htdocs/install/mysql/data/llx_accounting_account_nl.sql b/htdocs/install/mysql/data/llx_accounting_account_nl.sql index edd68ce39ed..be9ceb9c5eb 100644 --- a/htdocs/install/mysql/data/llx_accounting_account_nl.sql +++ b/htdocs/install/mysql/data/llx_accounting_account_nl.sql @@ -31,7 +31,7 @@ -- ADD 7006000 to rowid # Do no remove this comment -- -- -INSERT INTO llx_accounting_account (entity, rowid, fk_pcg_version, pcg_type, account_number, account_parent, label, active) VALUES (__ENTITY__, 1000, 'NL_VERKORT', 'BALANS', '0050', '', 'Bedrijfspand en woning', 1); +INSERT INTO llx_accounting_account (entity, rowid, fk_pcg_version, pcg_type, account_number, account_parent, label, active) VALUES (__ENTITY__, 1000, 'NL-VERKORT', 'BALANS', '0050', '', 'Bedrijfspand en woning', 1); INSERT INTO llx_accounting_account (entity, rowid, fk_pcg_version, pcg_type, account_number, account_parent, label, active) VALUES (__ENTITY__, 1001, 'NL-VERKORT', 'BALANS', '0055', '', 'Afschrijving bedrijfspand en woning', 1); INSERT INTO llx_accounting_account (entity, rowid, fk_pcg_version, pcg_type, account_number, account_parent, label, active) VALUES (__ENTITY__, 1002, 'NL-VERKORT', 'BALANS', '0100', '', 'Inventaris', 1); INSERT INTO llx_accounting_account (entity, rowid, fk_pcg_version, pcg_type, account_number, account_parent, label, active) VALUES (__ENTITY__, 1003, 'NL-VERKORT', 'BALANS', '0105', '', 'Afschrijving inventaris', 1); diff --git a/htdocs/langs/en_US/uxdocumentation.lang b/htdocs/langs/en_US/uxdocumentation.lang index 47b5b4df282..49f345c5e82 100644 --- a/htdocs/langs/en_US/uxdocumentation.lang +++ b/htdocs/langs/en_US/uxdocumentation.lang @@ -180,9 +180,12 @@ DocTitleIconUsageMethod3=Use your image. You must set the 4th parameter to 1. DocTitleMoreContentDescription=You can further customize the headings by adding an ID or CSS class(es), or adding content. See the examples below. DocTitleWithFilters=Display title with filters, buttons or navigation DocTitleWithFiltersDescription=If you want to display a title with many navigation options like on list pages, you can use the print_barre_liste() function which is a bit more complex. See code below or core/lib/functions.lib.php file to see how it works. +JSDolibarrhooksReadyVsInit = Difference between Dolibarr:Init and Dolibarr:Ready +SetAndUseContextVars = Set and use context vars ConsoleHelp = Console help JSDolibarrhooks = JS Dolibarr hooks -ExampleOfCreatingNewContextTool = Example of creating a new context tool -SetEventMessageTool = Set event message tool +JSDolibarrAwaitHooks = JS Dolibarr Async Hooks (Await Hooks) - sequential execution +ExampleOfCreatingNewContextTool = Dolibarr Tool : Example of creating a new context tool +SetEventMessageTool = Dolibarr Tool : Set event message diff --git a/htdocs/user/api_token/card.php b/htdocs/user/api_token/card.php index 16fb4033b49..11ade41bcfe 100644 --- a/htdocs/user/api_token/card.php +++ b/htdocs/user/api_token/card.php @@ -3,7 +3,7 @@ * Copyright (C) 2010-2015 Regis Houssin * Copyright (C) 2013 Florian Henry * Copyright (C) 2018 Ferran Marcet - * Copyright (C) 2024 Frédéric France + * Copyright (C) 2024-2025 Frédéric France * Copyright (C) 2024-2025 MDW * * This program is free software; you can redistribute it and/or modify @@ -186,7 +186,7 @@ if (empty($reshook)) { $insertedtokenid = $db->last_insert_id(MAIN_DB_PREFIX."oauth_token"); $db->commit(); - header("Location: ".$_SERVER["PHP_SELF"].'?id='.((int) $useridtoadd).'&tokenid='.urlencode($insertedtokenid)); + header("Location: " . dolBuildUrl($_SERVER["PHP_SELF"], ['id' => $useridtoadd, 'tokenid' => $insertedtokenid])); exit; } } elseif ($action == 'confirm_delete' && $confirm == 'yes' && $canedittoken) { @@ -302,12 +302,10 @@ if ($action == 'create') { } else { print ''; $addadmin = ''; - if (property_exists($object, 'admin')) { - if (isModEnabled('multicompany') && !empty($object->admin) && empty($object->entity)) { - $addadmin .= img_picto($langs->trans("SuperAdministratorDesc"), "superadmin", 'class="paddingleft valignmiddle"'); - } elseif (!empty($object->admin)) { - $addadmin .= img_picto($langs->trans("AdministratorDesc"), "admin", 'class="paddingleft valignmiddle"'); - } + if (isModEnabled('multicompany') && !empty($object->admin) && empty($object->entity)) { + $addadmin .= img_picto($langs->trans("SuperAdministratorDesc"), "redstar", 'class="paddingleft valignmiddle"'); + } elseif (!empty($object->admin)) { + $addadmin .= img_picto($langs->trans("AdministratorDesc"), "star", 'class="paddingleft valignmiddle"'); } print showValueWithClipboardCPButton($object->login).$addadmin; print '';