diff --git a/README.md b/README.md index 2bbda467..45da28e4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Most of these modules have been optimized and made compatible for PrestaShop v9. | Module name | Description | Minimum version | |----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| | [api_module](https://github.com/PrestaShop/example-modules/tree/master/api_module) | This example module demonstrates how to modify PrestaShop's new API. | v9.0.0 | +| [dashexamplechartjs](https://github.com/PrestaShop/example-modules/tree/master/dashexamplechartjs) | PoC for spike #41970: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3. | v9.2.0 | | [demo_grid](https://github.com/PrestaShop/example-modules/tree/master/demo_grid) | This module demonstrates how to use Grid in PrestaShop | v9.0.0 | | [democonsolecommand](https://github.com/PrestaShop/example-modules/tree/master/democonsolecommand) | Example module showing how to implement a Symfony console command | v9.0.0 | | [democontrollertabs](https://github.com/PrestaShop/example-modules/tree/master/democontrollertabs) | Demo Creating modern Controllers and associate Tabs to them | v9.0.0 | diff --git a/dashexamplechartjs/.gitignore b/dashexamplechartjs/.gitignore new file mode 100644 index 00000000..a75dba19 --- /dev/null +++ b/dashexamplechartjs/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +composer.lock +.idea diff --git a/dashexamplechartjs/README.md b/dashexamplechartjs/README.md new file mode 100644 index 00000000..a3bf515a --- /dev/null +++ b/dashexamplechartjs/README.md @@ -0,0 +1,63 @@ +# Dashboard example - Chart.js (`dashexamplechartjs`) + +PoC for spike [PrestaShop/PrestaShop#41970](https://github.com/PrestaShop/PrestaShop/issues/41970) — validates +**Chart.js** as the replacement for D3 v3 + NVD3 on the migrated (Symfony) Back Office Dashboard, using the +same integration contract as the reference [`dashexample`](../dashexample) module from +[PrestaShop/PrestaShop#41968](https://github.com/PrestaShop/PrestaShop/issues/41968) / +[#254](https://github.com/PrestaShop/example-modules/pull/254). + +## What it demonstrates + +- Registers on the new dashboard hooks: `displayAdminDashboardZoneOne`, `displayAdminDashboardZoneTwo`, + `displayAdminDashboardToolbar`. No legacy Smarty/`HelperForm`/`Db::getInstance()`. +- Renders hook content through module Twig templates (`$this->get('twig')`, `@Modules/dashexamplechartjs/...`). +- Consumes the global `Chart` (Chart.js v4.5.1, MIT) provided by **core** as its own independent webpack + bundle (`admin-dev/themes/new-theme/js/vendor/chartjs.ts` → `chartjs.bundle.js`, not merged into the + main BO bundle) — this module no longer vendors Chart.js itself. It still loads its own init JS/CSS from + its hook output, exactly like `dashexample` loads its assets — no `actionAdminControllerSetMedia`, no + `get_class($controller)` detection. +- Covers the 4 chart types the current native widgets need: + - Zone One: doughnut chart → `dashactivity` equivalent (traffic sources). + - Zone Two: line/area chart with a previous-period overlay → `dashtrends` equivalent (sales trend), + plus a bar chart → `dashgoals` equivalent (goals vs. actual). +- Sample/static data only (no Doctrine queries) — this is a rendering/integration PoC, not a full + reimplementation of the native widgets' data layer. + +## Requirements + +- PrestaShop **9.2.0+**, with core PR [PrestaShop/PrestaShop#42077](https://github.com/PrestaShop/PrestaShop/pull/42077) + (Symfony `DashboardController`, routing, Twig layout, feature flag, 5 new hooks) merged. +- Chart.js loaded by core as its own bundle (`chart.js` npm dependency, dedicated `chartjs` webpack entry, + included from `Dashboard/index.html.twig`'s `javascripts` block) — no toggle yet, deferred to a later change. +- The `dashboard` feature flag enabled (*Advanced Parameters > New & Experimental Features*). + +## Install + +```bash +php bin/console prestashop:module install dashexamplechartjs +``` + +Then enable the `dashboard` feature flag and open the Dashboard. + +## Known issues on the dependency (PrestaShop/PrestaShop#42077) + +Building this PoC surfaced two bugs in the core PR this module depends on (reported there): + +- `index.html.twig` is missing `|raw` on `toolbarContent`/`topContent`, so any HTML a module returns from + `displayAdminDashboardToolbar` or `displayAdminDashboardTop` renders escaped instead of executing — this + breaks the toolbar-based asset-loading pattern this module uses (and that `dashexample` uses too). Until + fixed upstream, this module's assets won't actually load. +- The date-range calendar form loses its security token on submit (`method="get"` form whose `action` already + contains `?_token=...`, which gets dropped and rebuilt from the form fields alone), redirecting to + `security/compromised`. + +## Chart.js benchmark + +Chart.js was selected as the primary recommendation among Chart.js / ApexCharts / Billboard.js: MIT license, +~67.5k★ on GitHub, ~54M downloads/month, ~68 KB gzip, no license risk — over ApexCharts (excluded: proprietary +relicensing in June 2025, revenue-gated + no-redistribution clause) and Billboard.js (heavier bundle, smaller +community, keeps a D3 v7 dependency). + +**Known gap surfaced by this PoC**: Chart.js renders on ``, which is invisible to screen readers by +default. This PoC adds `role="img"` + `aria-label` on each canvas as a minimal baseline, but a real migration +should budget explicit accessibility work per chart. diff --git a/dashexamplechartjs/composer.json b/dashexamplechartjs/composer.json new file mode 100644 index 00000000..80cc95f0 --- /dev/null +++ b/dashexamplechartjs/composer.json @@ -0,0 +1,17 @@ +{ + "name": "prestashop/dashexamplechartjs", + "description": "Spike #41970 PoC: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3.", + "license": "AFL-3.0", + "authors": [ + { + "name": "PrestaShop Core Team" + } + ], + "require": { + "php": ">=8.1" + }, + "config": { + "prepend-autoloader": false + }, + "type": "prestashop-module" +} diff --git a/dashexamplechartjs/config.xml b/dashexamplechartjs/config.xml new file mode 100644 index 00000000..05a14ebe --- /dev/null +++ b/dashexamplechartjs/config.xml @@ -0,0 +1,11 @@ + + + dashexamplechartjs + + + + + + 0 + 0 + diff --git a/dashexamplechartjs/dashexamplechartjs.php b/dashexamplechartjs/dashexamplechartjs.php new file mode 100644 index 00000000..210aca34 --- /dev/null +++ b/dashexamplechartjs/dashexamplechartjs.php @@ -0,0 +1,108 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +use Twig\Environment; + +if (!defined('_PS_VERSION_')) { + exit; +} + +/** + * Spike #41970 PoC: demonstrates replacing D3 v3 + NVD3 with Chart.js on the migrated + * (Symfony) Back Office Dashboard, through the same dedicated hook family used by the + * `dashexample` module (see PrestaShop/PrestaShop#41968 and PrestaShop/example-modules#254). + * + * Chart.js itself is self-hosted (views/js/lib/chart.umd.min.js, v4.5.1, MIT) and loaded + * once from the toolbar hook, exactly like `dashexample` loads its own assets from there. + */ +class DashExampleChartjs extends Module +{ + public function __construct() + { + $this->name = 'dashexamplechartjs'; + $this->author = 'PrestaShop'; + $this->version = '1.0.0'; + $this->ps_versions_compliancy = ['min' => '9.2.0', 'max' => '9.99.99']; + $this->need_instance = 0; + + parent::__construct(); + + $this->displayName = $this->l('Dashboard example - Chart.js'); + $this->description = $this->l('Spike #41970 PoC: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3.'); + } + + public function install(): bool + { + return parent::install() + && $this->registerHook([ + 'displayAdminDashboardZoneOne', + 'displayAdminDashboardZoneTwo', + 'displayAdminDashboardToolbar', + ]); + } + + /** + * Zone One: doughnut chart (traffic sources) — the dashactivity-equivalent widget. + */ + public function hookDisplayAdminDashboardZoneOne(array $params): string + { + return $this->render('zone_one.html.twig', [ + 'dateFrom' => $params['date_from'] ?? null, + 'dateTo' => $params['date_to'] ?? null, + ]); + } + + /** + * Zone Two: line/area chart with a "previous period" overlay (dashtrends-equivalent) + * plus a goals-vs-actual bar chart (dashgoals-equivalent). + */ + public function hookDisplayAdminDashboardZoneTwo(array $params): string + { + return $this->render('zone_two.html.twig', [ + 'dateFrom' => $params['date_from'] ?? null, + 'dateTo' => $params['date_to'] ?? null, + 'moduleUri' => $this->getPathUri(), + ]); + } + + /** + * Loads Chart.js (self-hosted) and this module's own JS/CSS once, from the toolbar hook — + * no `actionAdminControllerSetMedia`, no `get_class($controller)` detection. + */ + public function hookDisplayAdminDashboardToolbar(array $params): string + { + return $this->render('toolbar.html.twig', [ + 'moduleUri' => $this->getPathUri(), + ]); + } + + /** + * Render one of this module's admin Twig templates. + */ + private function render(string $template, array $params = []): string + { + /** @var Environment $twig */ + $twig = $this->get('twig'); + + return $twig->render(sprintf('@Modules/%s/views/templates/admin/%s', $this->name, $template), $params); + } +} diff --git a/dashexamplechartjs/views/css/dashexamplechartjs.css b/dashexamplechartjs/views/css/dashexamplechartjs.css new file mode 100644 index 00000000..e52423ba --- /dev/null +++ b/dashexamplechartjs/views/css/dashexamplechartjs.css @@ -0,0 +1,29 @@ +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * This source file is subject to the Academic Free License 3.0 (AFL-3.0). + * It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + */ + +.dashexamplechartjs-toolbar { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + margin-bottom: 15px; + border-radius: 4px; + background-color: #e6f0ff; + color: #2b7dd8; + font-weight: 600; +} + +.dashexamplechartjs-card .panel-heading { + display: flex; + align-items: center; + gap: 6px; +} + +.dashexamplechartjs-card canvas { + max-width: 100%; +} diff --git a/dashexamplechartjs/views/js/dashexamplechartjs.js b/dashexamplechartjs/views/js/dashexamplechartjs.js new file mode 100644 index 00000000..c6ab70f9 --- /dev/null +++ b/dashexamplechartjs/views/js/dashexamplechartjs.js @@ -0,0 +1,115 @@ +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * This source file is subject to the Academic Free License 3.0 (AFL-3.0). + * It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + */ + +// Spike #41970 PoC — the module manages its own assets and chart initialization, +// loaded from its hook output (see toolbar.html.twig), not via actionAdminControllerSetMedia. +// Colors below approximate the BO's prestakit design tokens ($primary/$secondary/...); +// a real integration would read the actual computed CSS custom properties instead. +(function () { + var PALETTE = { + primary: '#2b7dd8', + secondary: '#6c757d', + success: '#2ecc71', + info: '#3ea8f5', + warning: '#f4b740', + danger: '#e6544a', + }; + + function readJson(id) { + var el = document.getElementById(id); + return el ? JSON.parse(el.textContent) : null; + } + + function initDoughnut() { + var canvas = document.getElementById('dashexamplechartjs-doughnut'); + var data = readJson('dashexamplechartjs-doughnut-data'); + if (!canvas || !data || typeof Chart === 'undefined') { + return; + } + + new Chart(canvas, { + type: 'doughnut', + data: { + labels: data.labels, + datasets: [{ + data: data.values, + backgroundColor: [PALETTE.primary, PALETTE.info, PALETTE.warning, PALETTE.secondary], + }], + }, + options: { + plugins: { legend: { position: 'bottom' } }, + }, + }); + } + + function initLine() { + var canvas = document.getElementById('dashexamplechartjs-line'); + var data = readJson('dashexamplechartjs-line-data'); + if (!canvas || !data || typeof Chart === 'undefined') { + return; + } + + new Chart(canvas, { + type: 'line', + data: { + labels: data.labels, + datasets: [ + { + label: 'Current period', + data: data.current, + borderColor: PALETTE.primary, + backgroundColor: 'rgba(43, 125, 216, 0.15)', + fill: true, + tension: 0.3, + }, + { + label: 'Previous period', + data: data.previous, + borderColor: PALETTE.secondary, + borderDash: [6, 4], + fill: false, + tension: 0.3, + }, + ], + }, + options: { + plugins: { legend: { position: 'bottom' } }, + scales: { y: { beginAtZero: true } }, + }, + }); + } + + function initBar() { + var canvas = document.getElementById('dashexamplechartjs-bar'); + var data = readJson('dashexamplechartjs-bar-data'); + if (!canvas || !data || typeof Chart === 'undefined') { + return; + } + + new Chart(canvas, { + type: 'bar', + data: { + labels: data.labels, + datasets: [ + { label: 'Goal (%)', data: data.goal, backgroundColor: PALETTE.secondary }, + { label: 'Actual (%)', data: data.actual, backgroundColor: PALETTE.success }, + ], + }, + options: { + plugins: { legend: { position: 'bottom' } }, + scales: { y: { beginAtZero: true } }, + }, + }); + } + + document.addEventListener('DOMContentLoaded', function () { + initDoughnut(); + initLine(); + initBar(); + }); +})(); diff --git a/dashexamplechartjs/views/templates/admin/toolbar.html.twig b/dashexamplechartjs/views/templates/admin/toolbar.html.twig new file mode 100644 index 00000000..f84e9774 --- /dev/null +++ b/dashexamplechartjs/views/templates/admin/toolbar.html.twig @@ -0,0 +1,20 @@ +{#** + # Copyright since 2007 PrestaShop SA and Contributors + # PrestaShop is an International Registered Trademark & Property of PrestaShop SA + # + # This source file is subject to the Academic Free License 3.0 (AFL-3.0). + # It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + #} + +{% trans_default_domain 'Module.Dashexamplechartjs.Admin' %} + +{# + # NOTE (spike #41970 finding): this hook's output is NOT rendered with |raw in core's + # index.html.twig (PR #42077), unlike the zone hooks — so the HTML below is displayed + # escaped as text instead of executing. Assets are therefore loaded from zone_two.html.twig + # instead (see that file). Kept here, unfixed, to document the gap for the core PR. + #} +
+ insert_chart + {{ 'Spike #41970 — Chart.js proof of concept active.'|trans }} +
diff --git a/dashexamplechartjs/views/templates/admin/zone_one.html.twig b/dashexamplechartjs/views/templates/admin/zone_one.html.twig new file mode 100644 index 00000000..9fcc1e06 --- /dev/null +++ b/dashexamplechartjs/views/templates/admin/zone_one.html.twig @@ -0,0 +1,28 @@ +{#** + # Copyright since 2007 PrestaShop SA and Contributors + # PrestaShop is an International Registered Trademark & Property of PrestaShop SA + # + # This source file is subject to the Academic Free License 3.0 (AFL-3.0). + # It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + #} + +{% trans_default_domain 'Module.Dashexamplechartjs.Admin' %} + +
+

+ pie_chart + {{ 'Traffic sources'|trans }} +

+
+

+ {{ 'dashactivity equivalent — doughnut chart, rendered with Chart.js. Range: %from% → %to%'|trans({'%from%': dateFrom, '%to%': dateTo}) }} +

+ + +
+
diff --git a/dashexamplechartjs/views/templates/admin/zone_two.html.twig b/dashexamplechartjs/views/templates/admin/zone_two.html.twig new file mode 100644 index 00000000..d5ef5fd5 --- /dev/null +++ b/dashexamplechartjs/views/templates/admin/zone_two.html.twig @@ -0,0 +1,58 @@ +{#** + # Copyright since 2007 PrestaShop SA and Contributors + # PrestaShop is an International Registered Trademark & Property of PrestaShop SA + # + # This source file is subject to the Academic Free License 3.0 (AFL-3.0). + # It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + #} + +{% trans_default_domain 'Module.Dashexamplechartjs.Admin' %} + +{# + # NOTE (spike #41970 finding): assets are loaded here, not from the toolbar hook as the + # `dashexample` reference module does, because index.html.twig (core PR #42077) applies + # `|raw` to zoneOneContent/zoneTwoContent/zoneThreeContent but NOT to toolbarContent/topContent + # — so HTML returned from those two hooks gets auto-escaped and never executes as markup. + # This breaks the toolbar-based asset-loading convention documented for every module, not just + # this one. Left as-is here to keep the PoC demonstrable; flagged to report upstream. + #} + + + +
+

+ show_chart + {{ 'Sales trend'|trans }} +

+
+

+ {{ 'dashtrends equivalent — line/area chart with a previous-period overlay, rendered with Chart.js. Range: %from% → %to%'|trans({'%from%': dateFrom, '%to%': dateTo}) }} +

+ + +
+
+ +
+

+ bar_chart + {{ 'Monthly goals'|trans }} +

+
+

{{ 'dashgoals equivalent — bar chart, rendered with Chart.js.'|trans }}

+ + +
+