From 96749c47b478643bd2aeccb6750e097f6159c61a Mon Sep 17 00:00:00 2001 From: Michael Douchin Date: Mon, 4 Mar 2024 10:49:25 +0100 Subject: [PATCH 01/16] Presentation - create new module with installation & needed files --- lizmap/app/system/mainconfig.ini.php | 18 ++-- lizmap/modules/lizmap/install/install.php | 7 +- .../lizmap/install/sql/presentation.pgsql.sql | 60 ++++++++++++ .../lizmap/install/upgrade_presentation.php | 21 +++++ .../classes/presentation.listener.php | 44 +++++++++ .../classes/presentationConfig.class.php | 88 ++++++++++++++++++ .../classes/presentationDockable.listener.php | 34 +++++++ .../controllers/service.classic.php | 92 +++++++++++++++++++ .../presentation/daos/presentation.dao.xml | 37 ++++++++ .../daos/presentation_page.dao.xml | 42 +++++++++ lizmap/modules/presentation/events.xml | 8 ++ .../modules/presentation/install/install.php | 26 ++++++ .../modules/presentation/install/upgrade.php | 17 ++++ .../en_US/presentation.UTF-8.properties | 2 + lizmap/modules/presentation/module.xml | 17 ++++ .../templates/presentation_dock.tpl | 4 + lizmap/modules/presentation/urls.xml | 5 + 17 files changed, 514 insertions(+), 8 deletions(-) create mode 100644 lizmap/modules/lizmap/install/sql/presentation.pgsql.sql create mode 100644 lizmap/modules/lizmap/install/upgrade_presentation.php create mode 100644 lizmap/modules/presentation/classes/presentation.listener.php create mode 100644 lizmap/modules/presentation/classes/presentationConfig.class.php create mode 100644 lizmap/modules/presentation/classes/presentationDockable.listener.php create mode 100644 lizmap/modules/presentation/controllers/service.classic.php create mode 100644 lizmap/modules/presentation/daos/presentation.dao.xml create mode 100644 lizmap/modules/presentation/daos/presentation_page.dao.xml create mode 100644 lizmap/modules/presentation/events.xml create mode 100644 lizmap/modules/presentation/install/install.php create mode 100644 lizmap/modules/presentation/install/upgrade.php create mode 100644 lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties create mode 100644 lizmap/modules/presentation/module.xml create mode 100644 lizmap/modules/presentation/templates/presentation_dock.tpl create mode 100644 lizmap/modules/presentation/urls.xml diff --git a/lizmap/app/system/mainconfig.ini.php b/lizmap/app/system/mainconfig.ini.php index fd48caba65..585e103a71 100644 --- a/lizmap/app/system/mainconfig.ini.php +++ b/lizmap/app/system/mainconfig.ini.php @@ -1,4 +1,4 @@ -; +; ;for security reasons , don't remove or modify the first line ;this file doesn't list all possible properties. See lib/jelix/core/defaultconfig.ini.php for that @@ -42,17 +42,22 @@ lizmapDesktopPluginDate="2024-05-22" ; Versions written in QGIS/CFG files, for the GIS administrator -; Lizmap CFG files with a lower target version are not displayed in the landing page, but displayed in the administration panel to warn the GIS administrator -; Lizmap CFG files with this target version are still displayed in the landing page, but have a warning in the administration panel +; Lizmap CFG files with a lower target version are not displayed in the landing page, +; but displayed in the administration panel to warn the GIS administrator +; Lizmap CFG files with this target version are still displayed in the landing page, +; but have a warning in the administration panel ; 3 versions behind the current version of LWC lizmapWebClientTargetVersion=30400 [lizmap] ; CSP header for the map interface -; Exemple value: "default-src 'self' http: https:;connect-src 'self' http: https:;script-src http: https: 'unsafe-inline' 'unsafe-eval'; style-src http: https: 'unsafe-inline';object-src 'none';font-src https:;base-uri 'self';form-action 'self' http: https:;img-src http: https: data: blob:;frame-ancestors http: https:" +; Exemple value: "default-src 'self' http: https:;connect-src 'self' http: https:;script-src http: https:'unsafe-inline' +; 'unsafe-eval'; style-src http: https: 'unsafe-inline';object-src 'none';font-src https:;base-uri 'self';form-action +; 'self': http: https:;img-src http: https: data: blob:;frame-ancestors http: https:" ; Why these values: ; - some tiles servers or custom scripts may be on http instead of https servers -; - script-src: lizmap or external modules may still have inline script code, and integrated old libraries like Plotly and openlayers2 are using eval() sometimes :-/ +; - script-src: lizmap or external modules may still have inline script code, +; and integrated old libraries like Plotly and openlayers2 are using eval() sometimes :-/ ; - style-src: lizmap or external modules may still have some inline CSS code ; - some JS code and modules may use the "data:" uri ; - frame-ancestors: lizmap has a specific url to be used into frames @@ -102,6 +107,7 @@ ldapdao.path="app:vendor/jelix/ldapdao-module/ldapdao" saml.installparam[localconfig]=on saml.installparam[authep]=admin +presentation.enabled=on [coordplugins] ;name = file_ini_name or 1 @@ -219,7 +225,7 @@ ; if mailer = smtp , fill the following parameters -; SMTP hosts. All hosts must be separated by a semicolon : "smtp1.example.com:25;smtp2.example.com" +; SMTP hosts. All hosts must be separated by a semicolon : "smtp1.example.com:25;smtp2.example.com" smtpHost=localhost ; default SMTP server port smtpPort=25 diff --git a/lizmap/modules/lizmap/install/install.php b/lizmap/modules/lizmap/install/install.php index c6762c5084..94d5b98fc0 100644 --- a/lizmap/modules/lizmap/install/install.php +++ b/lizmap/modules/lizmap/install/install.php @@ -27,7 +27,7 @@ public function install() if (file_exists($localConfigDist)) { copy($localConfigDist, $localConfig); } else { - file_put_contents($localConfig, ';<'.'?php die(\'\');?'.'>'); + file_put_contents($localConfig, ';'); } } $ini = new \Jelix\IniFile\IniModifier($localConfig); @@ -35,7 +35,6 @@ public function install() $ini->save(); if ($this->firstDbExec()) { - // Add log table $this->useDbProfile('lizlog'); $this->execSQLScript('sql/lizlog'); @@ -43,6 +42,10 @@ public function install() // Add geobookmark table $this->useDbProfile('jauth'); $this->execSQLScript('sql/lizgeobookmark'); + + // Add presentation tables + $this->useDbProfile('jauth'); + $this->execSQLScript('sql/presentation'); } } } diff --git a/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql b/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql new file mode 100644 index 0000000000..70fdccac52 --- /dev/null +++ b/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql @@ -0,0 +1,60 @@ +CREATE TABLE IF NOT EXISTS presentation ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + repository text NOT NULL, + project text NOT NULL, + title text NOT NULL, + description text, + footer text, + author text NOT NULL, + published boolean NOT NULL DEFAULT False, + granted_groups text +); + +COMMENT ON TABLE presentation IS 'Stores the presentations created for Lizmap maps.'; +COMMENT ON COLUMN presentation.id IS 'Automatic unique ID'; +COMMENT ON COLUMN presentation.repository IS 'Lizmap repository key'; +COMMENT ON COLUMN presentation.project IS 'Lizmap project key'; +COMMENT ON COLUMN presentation.title IS 'Presentation title'; +COMMENT ON COLUMN presentation.description IS 'Description of the presentation'; +COMMENT ON COLUMN presentation.footer IS 'Optional footer visible in all pages'; +COMMENT ON COLUMN presentation.author IS 'Author (Lizmap login) of the presentation'; +COMMENT ON COLUMN presentation.published IS 'True if the presentation is published, i.e. visible for the users'; +COMMENT ON COLUMN presentation.granted_groups IS 'List of user groups that can see this presentation: a list of groups identifier separated by coma. Ex: admins, others'; + + +CREATE TABLE IF NOT EXISTS presentation_page ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + presentation_id integer NOT NULL, + title text NOT NULL, + description text, + page_order smallint NOT NULL, + model text NOT NULL, + background_image text, + background_color text, + map_extent text, + tree_state json, + illustration_type text, + illustration_media text, + illustration_url text, + illustration_feature json, + CONSTRAINT fk_presentation + FOREIGN KEY(presentation_id) + REFERENCES presentation (id) + ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE presentation_page IS 'Caracteristics of a presentation page'; +COMMENT ON COLUMN presentation_page.id IS 'Automatic unique ID'; +COMMENT ON COLUMN presentation_page.presentation_id IS 'Identifier of the parent presentation (foreign key)'; +COMMENT ON COLUMN presentation_page.title IS 'Title of the page'; +COMMENT ON COLUMN presentation_page.description IS 'Description of the page'; +COMMENT ON COLUMN presentation_page.page_order IS 'Order of the page in the presentation. It means the page number'; +COMMENT ON COLUMN presentation_page.model IS 'Model of the presentation, among a hard-coded list'; +COMMENT ON COLUMN presentation_page.background_image IS 'Image media URL to be used as a page background'; +COMMENT ON COLUMN presentation_page.background_color IS 'Background color of the page'; +COMMENT ON COLUMN presentation_page.map_extent IS 'Map extent to zoom to for this page. Always stored in EPSG:4326'; +COMMENT ON COLUMN presentation_page.tree_state IS 'An JSON array containing objects with key: name of the preset, and a JSON with the Lizmap layer tree state. Maximum 5 presets are stored'; +COMMENT ON COLUMN presentation_page.illustration_type IS 'Type of the page illustration : media, iframe, popup, etc.'; +COMMENT ON COLUMN presentation_page.illustration_media IS 'Relative path of the illustration media, such as media/some_directory/a_file.jpg'; +COMMENT ON COLUMN presentation_page.illustration_url IS 'URL of the iframe'; +COMMENT ON COLUMN presentation_page.illustration_feature IS 'If the illustration if a specific vector feature popup, this JSON field stores the feature layer and ID and the options (zoom to, filter, etc.)'; diff --git a/lizmap/modules/lizmap/install/upgrade_presentation.php b/lizmap/modules/lizmap/install/upgrade_presentation.php new file mode 100644 index 0000000000..8070e97065 --- /dev/null +++ b/lizmap/modules/lizmap/install/upgrade_presentation.php @@ -0,0 +1,21 @@ +firstDbExec()) { + // modify jlx_user columns + $this->useDbProfile('jauth'); + $this->execSQLScript('sql/presentation'); + } + } +} diff --git a/lizmap/modules/presentation/classes/presentation.listener.php b/lizmap/modules/presentation/classes/presentation.listener.php new file mode 100644 index 0000000000..311c8a4c5e --- /dev/null +++ b/lizmap/modules/presentation/classes/presentation.listener.php @@ -0,0 +1,44 @@ +urlengine['basePath']; + + // Add JS and CSS for module + $jsCode = array(); + $css = array(); + + // Check config + jClasses::inc('presentation~presentationConfig'); + $presentationConfigInstance = new presentationConfig($event->repository, $event->project); + if ($presentationConfigInstance->getStatus()) { + $presentationConfig = $presentationConfigInstance->getConfig(); + $presentationConfigData = array( + 'url' => jUrl::get( + 'presentation~service:index', + array( + 'repository' => $event->repository, + 'project' => $event->project, + ) + ), + ); + + $jsCode = array( + 'var presentationConfig = '.json_encode($presentationConfig), + 'var presentationConfigData = '.json_encode($presentationConfigData), + ); + $css = array( + $basePath.'assets/css/presentation.css', + ); + } + + $event->add( + array( + 'jscode' => $jsCode, + 'css' => $css, + ) + ); + } +} diff --git a/lizmap/modules/presentation/classes/presentationConfig.class.php b/lizmap/modules/presentation/classes/presentationConfig.class.php new file mode 100644 index 0000000000..a8a56a0675 --- /dev/null +++ b/lizmap/modules/presentation/classes/presentationConfig.class.php @@ -0,0 +1,88 @@ +errors = array( + 'title' => 'Invalid Query Parameter', + 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + ); + + return false; + } + } catch (\Lizmap\Project\UnknownLizmapProjectException $e) { + $this->errors = array( + 'title' => 'Invalid Query Parameter', + 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + ); + + return false; + } + + // Check acl + if (!$lproj->checkAcl()) { + $this->errors = array( + 'title' => 'Access Denied', + 'detail' => jLocale::get('view~default.repository.access.denied'), + ); + + return false; + } + + // presentation config may be an empty array + $this->repository = $repository; + $this->project = $project; + $this->lproj = $lproj; + $this->status = true; + $this->config = null; + } + + /** + * Get the presentations stored in the database + * for the current Lizmap project. + * + * @return null|array $presentations List of presentations + */ + private function getPresentations() + { + return null; + } + + /** + * Get presentation configuration. + */ + public function getConfig() + { + return $this->config; + } + + public function getStatus() + { + return $this->status; + } + + public function getErrors() + { + return $this->errors; + } +} diff --git a/lizmap/modules/presentation/classes/presentationDockable.listener.php b/lizmap/modules/presentation/classes/presentationDockable.listener.php new file mode 100644 index 0000000000..f97cd82cbc --- /dev/null +++ b/lizmap/modules/presentation/classes/presentationDockable.listener.php @@ -0,0 +1,34 @@ +getParam('project'); + $repository = $event->getParam('repository'); + + // Check config + jClasses::inc('presentation~presentationConfig'); + $dv = new presentationConfig($event->repository, $event->project); + + if ($dv->getStatus()) { + // Use template presentationConfig + $assign = array(); + $content = array('presentation~presentation_'.$dock, $assign); + $dock = new lizmapMapDockItem( + 'presentation', + jLocale::get('presentation~presentation.dock.title'), + $content, + 15, + null, // done getMapAdditions event + null + ); + $event->add($dock); + } + } + + public function onmapDockable($event) + { + $this->checkConfig($event, 'dock'); + } + } diff --git a/lizmap/modules/presentation/controllers/service.classic.php b/lizmap/modules/presentation/controllers/service.classic.php new file mode 100644 index 0000000000..0751f37c83 --- /dev/null +++ b/lizmap/modules/presentation/controllers/service.classic.php @@ -0,0 +1,92 @@ +param('repository'); + $project = $this->param('project'); + + // Check presentation config + jClasses::inc('presentation~presentationConfig'); + $dv = new presentationConfig($repository, $project); + if (!$dv->getStatus()) { + return $this->error($dv->getErrors()); + } + $config = $dv->getConfig(); + if (empty($config)) { + return $this->error($dv->getErrors()); + } + $this->repository = $repository; + $this->project = $project; + $this->config = $config; + + // Redirect to method corresponding on REQUEST param + $request = $this->param('request', 'getFeatureCount'); + + switch ($request) { + case 'getFeatureCount': + return $this->getFeatureCount(); + + break; + } + + return $this->error( + array( + 'title' => 'Not supported request', + 'detail' => 'The request "'.$request.'" is not supported!', + ), + ); + } + + /** + * Provide errors. + * + * @param mixed $errors + * + * @return jResponseJson the errors response + */ + public function error($errors) + { + /** @var jResponseJson $rep */ + $rep = $this->getResponse('json'); + $rep->data = array('errors' => $errors); + + return $rep; + } +} diff --git a/lizmap/modules/presentation/daos/presentation.dao.xml b/lizmap/modules/presentation/daos/presentation.dao.xml new file mode 100644 index 0000000000..69c385dd17 --- /dev/null +++ b/lizmap/modules/presentation/daos/presentation.dao.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/lizmap/modules/presentation/daos/presentation_page.dao.xml b/lizmap/modules/presentation/daos/presentation_page.dao.xml new file mode 100644 index 0000000000..b28f9940c4 --- /dev/null +++ b/lizmap/modules/presentation/daos/presentation_page.dao.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lizmap/modules/presentation/events.xml b/lizmap/modules/presentation/events.xml new file mode 100644 index 0000000000..4c9226d500 --- /dev/null +++ b/lizmap/modules/presentation/events.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lizmap/modules/presentation/install/install.php b/lizmap/modules/presentation/install/install.php new file mode 100644 index 0000000000..2ce5329ad5 --- /dev/null +++ b/lizmap/modules/presentation/install/install.php @@ -0,0 +1,26 @@ +copyDirectoryContent('www', jApp::wwwPath()); + + // if ($this->firstDbExec()) + // $this->execSQLScript('sql/install'); + + /*if ($this->firstExec('acl2')) { + jAcl2DbManager::addSubject('my.subject', 'presentation~acl.my.subject', 'subject.group.id'); + jAcl2DbManager::addRight('admins', 'my.subject'); // for admin group + } + */ + } +} diff --git a/lizmap/modules/presentation/install/upgrade.php b/lizmap/modules/presentation/install/upgrade.php new file mode 100644 index 0000000000..dc21049d0c --- /dev/null +++ b/lizmap/modules/presentation/install/upgrade.php @@ -0,0 +1,17 @@ +copyDirectoryContent('www', jApp::wwwPath()); + } +} diff --git a/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties b/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties new file mode 100644 index 0000000000..d777484b09 --- /dev/null +++ b/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties @@ -0,0 +1,2 @@ +dock.title=Presentation +dock.subtitle=Presentation diff --git a/lizmap/modules/presentation/module.xml b/lizmap/modules/presentation/module.xml new file mode 100644 index 0000000000..08ad0b1ea3 --- /dev/null +++ b/lizmap/modules/presentation/module.xml @@ -0,0 +1,17 @@ + + + + 3.8.0-pre + + + Mozilla Public License + 2017-2024 3liz + + http://www.3liz.com + + + + + + + diff --git a/lizmap/modules/presentation/templates/presentation_dock.tpl b/lizmap/modules/presentation/templates/presentation_dock.tpl new file mode 100644 index 0000000000..13f5164046 --- /dev/null +++ b/lizmap/modules/presentation/templates/presentation_dock.tpl @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/lizmap/modules/presentation/urls.xml b/lizmap/modules/presentation/urls.xml new file mode 100644 index 0000000000..235f685c60 --- /dev/null +++ b/lizmap/modules/presentation/urls.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From d55dc4995ea9c219b6c49f19ec2d604f17078ad2 Mon Sep 17 00:00:00 2001 From: Michael Douchin Date: Mon, 4 Mar 2024 18:27:29 +0100 Subject: [PATCH 02/16] Presentation - Show a list of presentations in the dock panel --- assets/src/components/PresentationCards.js | 124 +++++++++ assets/src/index.js | 6 + assets/src/modules/Lizmap.js | 3 + assets/src/modules/Presentation.js | 254 ++++++++++++++++++ .../lizmap/install/sql/presentation.pgsql.sql | 12 +- .../classes/presentationConfig.class.php | 9 +- .../presentation/daos/presentation.dao.xml | 5 +- .../en_US/presentation.UTF-8.properties | 11 +- .../templates/presentation_dock.tpl | 23 ++ lizmap/www/assets/css/presentation.css | 34 +++ 10 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 assets/src/components/PresentationCards.js create mode 100644 assets/src/modules/Presentation.js create mode 100644 lizmap/www/assets/css/presentation.css diff --git a/assets/src/components/PresentationCards.js b/assets/src/components/PresentationCards.js new file mode 100644 index 0000000000..3b33f1a329 --- /dev/null +++ b/assets/src/components/PresentationCards.js @@ -0,0 +1,124 @@ +/** + * @module components/PresentationCards.js + * @name PresentationCards + * @copyright 2023 3Liz + * @author DOUCHIN Michaël + * @license MPL-2.0 + */ + +import {mainLizmap} from '../modules/Globals.js'; + +/** + * @class + * @name PresentationCards + * @augments HTMLElement + */ +export default class PresentationCards extends HTMLElement { + + constructor() { + super(); + + // Id of the component + this.id = this.getAttribute('id'); + + // Get presentations related to the element scope + this.presentations = mainLizmap.presentation.getPresentations(); + } + + connectedCallback() { + // Get the content from the template + let template = document.getElementById('lizmap-presentation-card-template'); + + // Add the Items from the presentations object + for (let a in this.presentations) { + // Get the presentation + let presentation = this.presentations[a]; + + // Create the div and fill it with the template content + let div = document.createElement("div"); + div.classList.add('lizmap-presentation-card'); + div.innerHTML = template.innerHTML; + + // Edit the content + div.querySelector('h3.lizmap-presentation-title').innerText = presentation.title; + div.querySelector('p.lizmap-presentation-description').innerText = presentation.description; + div.querySelector('button.liz-presentation-edit').value = presentation.id; + div.querySelector('button.liz-presentation-delete').value = presentation.id; + div.querySelector('button.liz-presentation-launch').value = presentation.id; + + // Add the card to the parent + this.appendChild(div); + } + + // Add click event on the create button + const createButton = this.querySelector("button.liz-presentation-create"); + createButton.addEventListener("click", this.onButtonCreateClick); + + // Add click event on the presentation cards buttons + const buttons = this.querySelectorAll("div.lizmap-presentation-card-toolbar button"); + Array.from(buttons).forEach(button => { + if (button.classList.contains('liz-presentation-edit')) { + button.addEventListener('click', this.onButtonEditClick); + } else if (button.classList.contains('liz-presentation-delete')) { + button.addEventListener('click', this.onButtonDeleteClick); + } else if (button.classList.contains('liz-presentation-launch')) { + button.addEventListener('click', this.onButtonLaunchClick); + } + }); + } + + getPresentationById(presentationId) { + for (let a in this.presentations) { + let presentation = this.presentations[a]; + if (presentation.id == presentationId) { + return presentation; + } + } + + return null; + } + + onButtonCreateClick(event) { + // Get the host component + const host = event.target.closest("lizmap-presentation-cards"); + + console.log('Create a new presentation'); + } + + onButtonEditClick(event) { + const button = event.currentTarget; + const presentationId = button.value; + console.log(`Edit the presentation ${presentationId}`); + } + + onButtonDeleteClick(event) { + const button = event.currentTarget; + const presentationId = button.value; + console.log(`Delete the presentation ${presentationId}`); + } + + onButtonLaunchClick(event) { + const button = event.currentTarget; + const presentationId = button.value; + console.log(`Launch the presentation ${presentationId}`); + mainLizmap.presentation.runLizmapPresentation(presentationId); + } + + disconnectedCallback() { + // Remove click events on the presentation buttons + const createButton = this.querySelector("button.liz-presentation-create"); + createButton.removeEventListener("click", this.onButtonCreateClick); + const buttons = this.querySelectorAll("div.lizmap-presentation-card-toolbar button"); + Array.from(buttons).forEach(button => { + if (button.classList.contains('liz-presentation-edit')) { + button.removeEventListener('click', this.onButtonEditClick); + } else if (button.classList.contains('liz-presentation-delete')) { + button.removeEventListener('click', this.onButtonDeleteClick); + } else if (button.classList.contains('liz-presentation-launch')) { + button.removeEventListener('click', this.onButtonLaunchClick); + } + }); + } + + +} diff --git a/assets/src/index.js b/assets/src/index.js index 21a9c46574..d953b4c277 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -26,6 +26,9 @@ import NavBar from './components/NavBar.js'; import Tooltip from './components/Tooltip.js'; import Message from './components/Message.js'; + +import PresentationCards from './components/PresentationCards.js'; + import { mainLizmap, mainEventDispatcher } from './modules/Globals.js'; import executeJSFromServer from './modules/ExecuteJSFromServer.js'; @@ -58,6 +61,9 @@ lizMap.events.on({ window.customElements.define('lizmap-tooltip', Tooltip); window.customElements.define('lizmap-message', Message); + + window.customElements.define('lizmap-presentation-cards', PresentationCards); + lizMap.mainLizmap = mainLizmap; lizMap.mainEventDispatcher = mainEventDispatcher; diff --git a/assets/src/modules/Lizmap.js b/assets/src/modules/Lizmap.js index 929dc2f9a8..1611c7435e 100644 --- a/assets/src/modules/Lizmap.js +++ b/assets/src/modules/Lizmap.js @@ -25,6 +25,7 @@ import Legend from './Legend.js'; import Permalink from './Permalink.js'; import Search from './Search.js'; import Tooltip from './Tooltip.js'; +import Presentation from './Presentation.js'; import WMSCapabilities from 'ol/format/WMSCapabilities.js'; import { Coordinate as olCoordinate } from 'ol/coordinate.js' @@ -63,6 +64,7 @@ export default class Lizmap { proj4.defs(ref, def); } } + // Register project projection if unknown const configProj = this._initialConfig.options.projection; if (configProj.ref !== "" && !proj4.defs(configProj.ref)) { @@ -164,6 +166,7 @@ export default class Lizmap { this.legend = new Legend(); this.search = new Search(); this.tooltip = new Tooltip(); + this.presentation = new Presentation(); // Removed unusable button if (!this.config['printTemplates'] || this.config.printTemplates.length == 0 ) { diff --git a/assets/src/modules/Presentation.js b/assets/src/modules/Presentation.js new file mode 100644 index 0000000000..f7d13d5752 --- /dev/null +++ b/assets/src/modules/Presentation.js @@ -0,0 +1,254 @@ +/** + * @module modules/Presentation.js + * @name Presentation + * @copyright 2023 3Liz + * @author DOUCHIN Michaël + * @license MPL-2.0 + */ + +import { mainLizmap } from '../modules/Globals.js'; +import { Vector as VectorSource } from 'ol/source.js'; +import { Vector as VectorLayer } from 'ol/layer.js'; +import GeoJSON from 'ol/format/GeoJSON.js'; + +/** + * @class + * @name Presentation + */ +export default class Presentation { + + /** + * @boolean If the project has presentations + */ + hasPresentations = false; + + /** + * @object List of presentations + */ + presentations = []; + + /** + * @string Unique ID of an presentation object + * We allow only one active presentation at a time + */ + ACTIVE_LIZMAP_PRESENTATION = null; + + /** + * OpenLayers vector layer to draw the presentation results + */ + presentationLayer = null; + + /** + * Build the lizmap presentation instance + */ + constructor() { + + this.hasPresentations = true; + if (typeof presentationConfig === 'undefined') { + this.hasPresentations = false; + } + + if (this.hasPresentations) { + + // Get the list of presentations + this.presentations = presentationConfig; + + // Hide the presentation dock if no presentation exists + // And the user has no right to manage them + const hideDock = false; + if (hideDock) { + let presentationMenu = document.querySelector('#mapmenu li.presentation'); + if (presentationMenu) { + presentationMenu.style.display = "none"; + } + } + + // Add an OpenLayers layer to show & use the geometries returned by an presentation + this.createPresentationMapLayer(); + + // React on the main Lizmap events + mainLizmap.lizmap3.events.on({ + }); + } + + } + + /** + * Create the OpenLayers layer to display the presentation geometries. + * + */ + createPresentationMapLayer() { + // Create the OL layer + const strokeColor = 'blue'; + const strokeWidth = 3; + const fillColor = 'rgba(173,216,230,0.8)'; // lightblue + this.presentationLayer = new VectorLayer({ + source: new VectorSource({ + wrapX: false + }), + style: { + 'circle-radius': 6, + 'circle-stroke-color': strokeColor, + 'circle-stroke-width': strokeWidth, + 'circle-fill-color': fillColor, + 'stroke-color': strokeColor, + 'stroke-width': strokeWidth, + 'fill-color': fillColor, + } + }); + + // Add the layer inside Lizmap objects + mainLizmap.map.addLayer(this.presentationLayer); + } + + /** + * Get an presentation item by its uid. + * + * @param {integer} presentationId - id of the presentation + * + * @return {object} The corresponding presentation data + */ + getPresentationById(presentationId) { + + if (!this.hasPresentations) { + return null; + } + + // Loop through the presentations + for (let i in presentationConfig) { + // Current presentations + let presentation = presentationConfig[i]; + + // Return the presentation if its uid matches + if (presentation.id == presentationId) { + return presentation; + } + } + + return null; + } + + /** + * Get the list of presentations + * + * @return {array} presentations - Array of the presentations + */ + getPresentations() { + + return this.presentations; + } + + /** + * Run a Lizmap presentation. + * + * @param {integer} presentationId - The presentation id + */ + async runLizmapPresentation(presentationId) { + if (!this.hasPresentations) { + return false; + } + + // Get the presentation + let presentation = this.getPresentationById(presentationId); + if (!presentation) { + console.warn('No corresponding presentation found in the configuration !'); + return false; + } + + // Reset the other presentations + // We allow only one active presentation at a time + // We do not remove the active status of the button (btn-primary) + this.resetLizmapPresentation(true, true, true, false); + + try { + // Show a message + const message = `Run presentation n° ${presentationId}`; + mainLizmap.lizmap3.addMessage( + message, 'info', true + ).attr('id', 'lizmap-presentation-message'); + + /** + * Lizmap event to allow other scripts to process the data if needed + * @event presentationLaunched + * @property {string} presentationId Id of the presentation + */ + lizMap.events.triggerEvent("presentationLaunched", + { + 'presentation': presentationId + } + ); + + // Set the presentation as active + this.ACTIVE_LIZMAP_PRESENTATION = presentationId; + + } catch (error) { + // Display the error + console.warn(error); + + // Reset the presentation + this.resetLizmapPresentation(true, true, true, true); + + } + } + + /** + * Reset presentation + * + * @param {boolean} destroyFeatures - If we must remove the geometries in the map. + * @param {boolean} removeMessage - If we must remove the message displayed at the top. + * @param {boolean} resetGlobalVariable - If we must empty the global variable ACTIVE_LIZMAP_PRESENTATION + * @param {boolean} resetActiveInterfaceElements - If we must remove the "active" interface for the buttons + */ + resetLizmapPresentation(destroyFeatures = true, removeMessage = true, resetGlobalVariable = true, resetActiveInterfaceElements = true) { + + // Remove the objects in the map + if (destroyFeatures) { + this.presentationLayer.getSource().clear(); + } + + // Clear the previous Lizmap message + if (removeMessage) { + let previousMessage = document.getElementById('lizmap-presentation-message'); + if (previousMessage) previousMessage.remove(); + } + + // Remove all btn-primary classes in the target objects + if (resetActiveInterfaceElements) { + let selector = '.popup-presentation.btn-primary'; + Array.from(document.querySelectorAll(selector)).map(element => { + element.classList.remove('btn-primary'); + }); + } + + // Reset the global variable + if (resetGlobalVariable) { + this.ACTIVE_LIZMAP_PRESENTATION = null; + } + } + + /** + * Add the geometry features + * to the OpenLayers layer in the map + * + * @param {object} data - The data to add in GeoJSON format + * @param {object|undefined} style - Optional OpenLayers style object + * + * @return {object} features The OpenLayers features converted from the data + */ + addFeatures(data, style) { + // Change the layer style + if (style) { + this.presentationLayer.setStyle(style); + } + + // Convert the GeoJSON data into OpenLayers features + const features = (new GeoJSON()).readFeatures(data, { + featureProjection: mainLizmap.projection + }); + + // Add them to the presentation layer + this.presentationLayer.getSource().addFeatures(features); + + return features; + } +}; diff --git a/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql b/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql index 70fdccac52..5f622cfd83 100644 --- a/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql +++ b/lizmap/modules/lizmap/install/sql/presentation.pgsql.sql @@ -5,9 +5,12 @@ CREATE TABLE IF NOT EXISTS presentation ( title text NOT NULL, description text, footer text, - author text NOT NULL, published boolean NOT NULL DEFAULT False, - granted_groups text + granted_groups text, + created_by text NOT NULL, + created_at timestamp DEFAULT now()::timestamp(0), + updated_by text, + updated_at timestamp DEFAULT now()::timestamp(0) ); COMMENT ON TABLE presentation IS 'Stores the presentations created for Lizmap maps.'; @@ -17,9 +20,12 @@ COMMENT ON COLUMN presentation.project IS 'Lizmap project key'; COMMENT ON COLUMN presentation.title IS 'Presentation title'; COMMENT ON COLUMN presentation.description IS 'Description of the presentation'; COMMENT ON COLUMN presentation.footer IS 'Optional footer visible in all pages'; -COMMENT ON COLUMN presentation.author IS 'Author (Lizmap login) of the presentation'; COMMENT ON COLUMN presentation.published IS 'True if the presentation is published, i.e. visible for the users'; COMMENT ON COLUMN presentation.granted_groups IS 'List of user groups that can see this presentation: a list of groups identifier separated by coma. Ex: admins, others'; +COMMENT ON COLUMN presentation.created_by IS 'Author (Lizmap login) of the presentation'; +COMMENT ON COLUMN presentation.created_at IS 'Creation timestamp'; +COMMENT ON COLUMN presentation.updated_by IS 'Last update author (Lizmap login)'; +COMMENT ON COLUMN presentation.updated_at IS 'Last update timestamp'; CREATE TABLE IF NOT EXISTS presentation_page ( diff --git a/lizmap/modules/presentation/classes/presentationConfig.class.php b/lizmap/modules/presentation/classes/presentationConfig.class.php index a8a56a0675..da62685aee 100644 --- a/lizmap/modules/presentation/classes/presentationConfig.class.php +++ b/lizmap/modules/presentation/classes/presentationConfig.class.php @@ -54,18 +54,21 @@ public function __construct($repository, $project) $this->project = $project; $this->lproj = $lproj; $this->status = true; - $this->config = null; + $this->config = $this->getPresentations(); } /** * Get the presentations stored in the database * for the current Lizmap project. * - * @return null|array $presentations List of presentations + * @return null|json $presentations List of presentations */ private function getPresentations() { - return null; + $dao = \jDao::get('presentation~presentation'); + $getPresentations = $dao->findAll(); + + return $getPresentations->fetchAllAssociative(); } /** diff --git a/lizmap/modules/presentation/daos/presentation.dao.xml b/lizmap/modules/presentation/daos/presentation.dao.xml index 69c385dd17..6988ba43fb 100644 --- a/lizmap/modules/presentation/daos/presentation.dao.xml +++ b/lizmap/modules/presentation/daos/presentation.dao.xml @@ -10,9 +10,12 @@ - + + + + diff --git a/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties b/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties index d777484b09..9316eb816f 100644 --- a/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties +++ b/lizmap/modules/presentation/locales/en_US/presentation.UTF-8.properties @@ -1,2 +1,11 @@ -dock.title=Presentation +dock.title=Presentations dock.subtitle=Presentation +dock.introduction.label=This panel shows a list of presentations. +dock.card.button.create.label=Create a presentation +dock.card.button.create.title=Create a new presentation +dock.card.button.edit.label=Edit +dock.card.button.edit.title=Edit this presentation +dock.card.button.delete.label=Delete +dock.card.button.delete.title=Delete this presentation +dock.card.button.launch.label=Launch +dock.card.button.launch.title=Launch this presentation diff --git a/lizmap/modules/presentation/templates/presentation_dock.tpl b/lizmap/modules/presentation/templates/presentation_dock.tpl index 13f5164046..946bf296e3 100644 --- a/lizmap/modules/presentation/templates/presentation_dock.tpl +++ b/lizmap/modules/presentation/templates/presentation_dock.tpl @@ -1,4 +1,27 @@
+
+

{@presentation~presentation.dock.introduction.label@}

+
+
+ +
+ +
+
+
+ + + + diff --git a/lizmap/www/assets/css/presentation.css b/lizmap/www/assets/css/presentation.css new file mode 100644 index 0000000000..eaf11ef23f --- /dev/null +++ b/lizmap/www/assets/css/presentation.css @@ -0,0 +1,34 @@ +/* SVG credit : https://remixicon.com/ */ +#mapmenu li.presentation a .icon, +#mini-dock #presentation div.presentation h3 span.icon { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='rgba(255,255,255,1)'%3E%3Cpath d='M13 18V20H17V22H7V20H11V18H3C2.44772 18 2 17.5523 2 17V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V17C22 17.5523 21.5523 18 21 18H13ZM4 5V16H20V5H4ZM10 7.5L15 10.5L10 13.5V7.5Z'%3E%3C/path%3E%3C/svg%3E"); + background-size: contain; +} + +#mini-dock #presentation div.presentation h3 span.icon { + background-position: unset; +} +#mapmenu li.presentation a:hover .icon, +#mapmenu li.presentation.active a .icon { + filter: invert(1); +} + +/* Presentation card */ +div#presentation-introduction { + margin: 5px; + padding: 5px; +} + +button.liz-presentation-create { + margin: 5px; + width: calc(100% - 10px); +} +div.lizmap-presentation-card { + border: 1px solid var(--color-contrasted-elements); + border-radius: 5px; + padding: 5px; + margin: 10px; +} +div.lizmap-presentation-card-toolbar { + text-align: right; +} From 9b40f7667676cc03241e58dce8c06373bd940ff6 Mon Sep 17 00:00:00 2001 From: Michael Douchin Date: Sat, 23 Mar 2024 18:13:49 +0100 Subject: [PATCH 03/16] Presentation - Add presentation editing capabilities && improve list view --- assets/src/components/PresentationCards.js | 118 ++++- assets/src/modules/Presentation.js | 284 ++++++++++- .../classes/presentation.listener.php | 10 +- .../classes/presentationConfig.class.php | 55 +- .../controllers/presentation.classic.php | 473 ++++++++++++++++++ .../presentation/daos/presentation.dao.xml | 6 +- .../daos/presentation_page.dao.xml | 2 +- .../presentation/forms/presentation.form.xml | 49 ++ .../en_US/presentation.UTF-8.properties | 32 ++ .../presentation/templates/html_error.tpl | 9 + .../templates/presentation_dock.tpl | 71 ++- .../templates/presentation_form.tpl | 5 + lizmap/www/assets/css/presentation.css | 46 ++ 13 files changed, 1084 insertions(+), 76 deletions(-) create mode 100644 lizmap/modules/presentation/controllers/presentation.classic.php create mode 100644 lizmap/modules/presentation/forms/presentation.form.xml create mode 100644 lizmap/modules/presentation/templates/html_error.tpl create mode 100644 lizmap/modules/presentation/templates/presentation_form.tpl diff --git a/assets/src/components/PresentationCards.js b/assets/src/components/PresentationCards.js index 3b33f1a329..d654b31fc4 100644 --- a/assets/src/components/PresentationCards.js +++ b/assets/src/components/PresentationCards.js @@ -6,7 +6,7 @@ * @license MPL-2.0 */ -import {mainLizmap} from '../modules/Globals.js'; +import { mainLizmap } from '../modules/Globals.js'; /** * @class @@ -21,27 +21,65 @@ export default class PresentationCards extends HTMLElement { // Id of the component this.id = this.getAttribute('id'); - // Get presentations related to the element scope - this.presentations = mainLizmap.presentation.getPresentations(); + // Attribute to force the refresh of data + // Store the last refresh timestamp + this.updated = this.getAttribute('updated'); } - connectedCallback() { - // Get the content from the template - let template = document.getElementById('lizmap-presentation-card-template'); + async load() { - // Add the Items from the presentations object - for (let a in this.presentations) { + // Get presentations related to the element scope from query + mainLizmap.presentation.getPresentations() + .then(data => { + // Set property + this.presentations = data; + + // Render + this.render(); + }) + .catch(err => console.log(err)) + + } + + render() { + // Remove previous content + this.innerHTML = ''; + + // Get the base content of a card from the template + const createTemplate = document.getElementById('lizmap-presentation-create-button-template'); + this.innerHTML = createTemplate.innerHTML; + + // Get the base content of a card from the template + const cardTemplate = document.getElementById('lizmap-presentation-card-template'); + for (const a in this.presentations) { // Get the presentation - let presentation = this.presentations[a]; + const presentation = this.presentations[a]; // Create the div and fill it with the template content let div = document.createElement("div"); div.classList.add('lizmap-presentation-card'); - div.innerHTML = template.innerHTML; + div.dataset.id = presentation.id; + div.dataset.display = 'normal'; + div.innerHTML = cardTemplate.innerHTML; // Edit the content - div.querySelector('h3.lizmap-presentation-title').innerText = presentation.title; - div.querySelector('p.lizmap-presentation-description').innerText = presentation.description; + div.querySelector('h3.lizmap-presentation-title').innerHTML = ` + ${presentation.title}${presentation.id} + `; + div.querySelector('p.lizmap-presentation-description').innerHTML = presentation.description; + + // Detailed information + const table = div.querySelector('table.presentation-detail-table'); + const fields = [ + 'footer', 'published', 'granted_groups', + 'created_by', 'created_at', 'updated_by', 'updated_at' + ]; + fields.forEach(field => { + table.querySelector(`td#presentation-detail-${field}`).innerHTML = presentation[field]; + }) + + // Buttons + div.querySelector('button.liz-presentation-detail').value = presentation.id; div.querySelector('button.liz-presentation-edit').value = presentation.id; div.querySelector('button.liz-presentation-delete').value = presentation.id; div.querySelector('button.liz-presentation-launch').value = presentation.id; @@ -59,6 +97,8 @@ export default class PresentationCards extends HTMLElement { Array.from(buttons).forEach(button => { if (button.classList.contains('liz-presentation-edit')) { button.addEventListener('click', this.onButtonEditClick); + } else if (button.classList.contains('liz-presentation-detail')) { + button.addEventListener('click', this.onButtonDetailClick); } else if (button.classList.contains('liz-presentation-delete')) { button.addEventListener('click', this.onButtonDeleteClick); } else if (button.classList.contains('liz-presentation-launch')) { @@ -67,6 +107,20 @@ export default class PresentationCards extends HTMLElement { }); } + connectedCallback() { + this.load(); + } + + static get observedAttributes() { return ['updated']; } + + attributeChangedCallback(name, oldValue, newValue) { + // Listen to the change of the updated attribute + // This will trigger the load (refresh the content) + if (name === 'updated') { + this.load(); + } + } + getPresentationById(presentationId) { for (let a in this.presentations) { let presentation = this.presentations[a]; @@ -81,26 +135,54 @@ export default class PresentationCards extends HTMLElement { onButtonCreateClick(event) { // Get the host component const host = event.target.closest("lizmap-presentation-cards"); - console.log('Create a new presentation'); + mainLizmap.presentation.launchPresentationCreationForm(); } onButtonEditClick(event) { const button = event.currentTarget; const presentationId = button.value; - console.log(`Edit the presentation ${presentationId}`); + mainLizmap.presentation.launchPresentationCreationForm(presentationId); + } + + onButtonDetailClick(event) { + const host = event.target.closest("lizmap-presentation-cards"); + const button = event.currentTarget; + const presentationId = button.value; + + // Chosen card + const chosenCard = host.querySelector(`[data-id='${presentationId}']`); + const isActive = (chosenCard.dataset.display == 'detail'); + + // Set other cards status + host.presentations.forEach(item => { + const card = host.querySelector(`[data-id='${item.id}']`); + const display = (isActive) ? 'normal' : 'none'; + card.dataset.display = display; + }) + + // Set the clicked card display property + chosenCard.dataset.display = (isActive) ? 'normal' : 'detail'; + + // Set its detail button label & title + button.innerText = (isActive) ? button.dataset.label : button.dataset.reverseLabel; + button.setAttribute('title', (isActive) ? button.dataset.title : button.dataset.reverseTitle); + + // Set the full panel class + const parentDiv = document.getElementById('presentation-container'); + parentDiv.dataset.display = (isActive) ? 'normal' : 'detail'; } onButtonDeleteClick(event) { + const host = event.target.closest("lizmap-presentation-cards"); const button = event.currentTarget; const presentationId = button.value; - console.log(`Delete the presentation ${presentationId}`); + mainLizmap.presentation.deletePresentation(presentationId); } onButtonLaunchClick(event) { const button = event.currentTarget; const presentationId = button.value; - console.log(`Launch the presentation ${presentationId}`); mainLizmap.presentation.runLizmapPresentation(presentationId); } @@ -112,6 +194,8 @@ export default class PresentationCards extends HTMLElement { Array.from(buttons).forEach(button => { if (button.classList.contains('liz-presentation-edit')) { button.removeEventListener('click', this.onButtonEditClick); + } else if (button.classList.contains('liz-presentation-detail')) { + button.removeEventListener('click', this.onButtonDetailClick); } else if (button.classList.contains('liz-presentation-delete')) { button.removeEventListener('click', this.onButtonDeleteClick); } else if (button.classList.contains('liz-presentation-launch')) { @@ -119,6 +203,4 @@ export default class PresentationCards extends HTMLElement { } }); } - - } diff --git a/assets/src/modules/Presentation.js b/assets/src/modules/Presentation.js index f7d13d5752..e45abcce9d 100644 --- a/assets/src/modules/Presentation.js +++ b/assets/src/modules/Presentation.js @@ -10,6 +10,7 @@ import { mainLizmap } from '../modules/Globals.js'; import { Vector as VectorSource } from 'ol/source.js'; import { Vector as VectorLayer } from 'ol/layer.js'; import GeoJSON from 'ol/format/GeoJSON.js'; +import Utils from './Utils.js'; /** * @class @@ -135,7 +136,24 @@ export default class Presentation { */ getPresentations() { - return this.presentations; + const url = presentationConfig.url; + let formData = new FormData(); + formData.append('request', 'list'); + + // Return promise + return fetch(url, { + method: 'POST', + body: formData + }).then(function (response) { + if (response.ok) { + return response.json(); + } + return Promise.reject(response); + }).then(function (json) { + return json; + }).catch(function (error) { + console.warn(error); + }); } /** @@ -163,9 +181,7 @@ export default class Presentation { try { // Show a message const message = `Run presentation n° ${presentationId}`; - mainLizmap.lizmap3.addMessage( - message, 'info', true - ).attr('id', 'lizmap-presentation-message'); + this.addMessage(message, 'info', 5000); /** * Lizmap event to allow other scripts to process the data if needed @@ -214,7 +230,7 @@ export default class Presentation { // Remove all btn-primary classes in the target objects if (resetActiveInterfaceElements) { - let selector = '.popup-presentation.btn-primary'; + const selector = '.popup-presentation.btn-primary'; Array.from(document.querySelectorAll(selector)).map(element => { element.classList.remove('btn-primary'); }); @@ -251,4 +267,262 @@ export default class Presentation { return features; } + + /** + * Display a lizMap message + * + * @param {string} message Message to display + * @param {string} type Type : error or info + * @param {number} duration Number of millisecond the message must be displayed + */ + addMessage(message, type, duration) { + + let previousMessage = document.getElementById('lizmap-presentation-message'); + if (previousMessage) previousMessage.remove(); + mainLizmap.lizmap3.addMessage( + message, type, true, duration + ).attr('id', 'lizmap-presentation-message'); + } + + /** + * Hide all presentation containers + * except the given one. + * + * Optionally replace the given container inner HTML + * + * @param {string} activeContainer Active container class + * @param {string} html If given, replace the active container inner HTML + * @param {boolean} emptyInactive If true, empty the inactive container inner HTML + */ + toggleContainersDisplay(activeContainer, html = null, emptyInactive = false) { + const selector = '#presentation-container div.presentation-container-item'; + Array.from(document.querySelectorAll(selector)).map(element => { + if (element.classList.contains(activeContainer)) { + if (html !== null) { + element.innerHTML = html; + } + element.style.display = 'block'; + } else { + element.style.display = 'none'; + if (emptyInactive) { + element.innerHTML = ''; + } + } + }); + } + + /** + * Display the form to create a new presentation + * + * @param {null|number} id Id of the presentation. If null, it is a creation form. + */ + async launchPresentationCreationForm(id = null) { + // Get the form + try { + const url = presentationConfig.url; + const request = (id === null) ? 'create' : 'modify'; + let formData = new FormData(); + formData.append('request', request); + formData.append('id', id); + const response = await fetch(url, { + method: "POST", + body: formData + }); + + // Check content type + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("text/plain")) { + throw new TypeError("Wrong content-type. HTML Expected !"); + } + + // Get the response + const htmlContent = await response.text(); + + // Display it + this.toggleContainersDisplay('form-container', htmlContent, false); + + // Add events + const formContainer = document.getElementById('presentation-form-container'); + const form = formContainer.querySelector('form'); + this.addFormEvents(form); + + } catch(error) { + console.log(error); + let previousMessage = document.getElementById('lizmap-presentation-message'); + if (previousMessage) previousMessage.remove(); + const message = ` + ${error} + `; + this.addMessage(message, 'error', 5000); + } + } + + /** + * Trigger actions when submitting the form + * + * @param {HTMLFormElement} form + */ + addFormEvents(form) { + + // Detect click on the submit buttons + // to change the hidden input submit_button value + Array.from(form.querySelectorAll('input[type=submit]')).map(element => { + element.addEventListener('click', function(event) { + const button = event.currentTarget; + form.querySelector("input[name=submit_button]").value = button.name; + }); + }); + + // Listen to the form submit + form.addEventListener('submit', function (event) { + + // Prevent form from submitting to the server + event.preventDefault(); + + // Form data + const formData = new FormData(event.target); + const formDataObject = Object.fromEntries(formData) + const formAction = formDataObject['submit_button']; + // console.log(`Submit bouton = ${formAction}`); + + // Return to the list of presentations if user canceled + if (formAction == 'cancel') { + // Go back to the list of presentations + mainLizmap.presentation.toggleContainersDisplay('list-container', null, true); + + return true; + } + + // Send the form data + mainLizmap.presentation.saveForm(form); + }); + } + + /** + * Save the form data + * + * @param {HTMLFormElement} form The form to save + */ + saveForm(form) { + const url = form.getAttribute('action'); + fetch(url, { + method: 'POST', + body: new FormData(form) + }).then(function (response) { + if (response.ok) { + return response.text(); + } + return Promise.reject(response); + }).then(function (html) { + // Display it + const formContainer = document.getElementById('presentation-form-container'); + formContainer.innerHTML = html; + + // Check if the response contains a form or not + const form = formContainer.querySelector('form'); + if (form) { + // Add form events + mainLizmap.presentation.addFormEvents(form); + } else { + // Display a message + const message = html; + mainLizmap.presentation.addMessage(message, 'info', 5000); + + // Refresh the content of the list of presentations + const cardsElement = document.querySelector('#presentation-list-container lizmap-presentation-cards'); + cardsElement.setAttribute('updated', 'done'); + + // Go back to the list of presentations + mainLizmap.presentation.toggleContainersDisplay('list-container', null, true); + } + }).catch(function (error) { + console.warn(error); + }); + } + + /** + * Delete the given presentation + * + * @param {number} id ID of the presentation to delete + */ + deletePresentation(id) { + // Confirmation message + const areYourSure = window.confirm('Are you sure you want to delete this presentation ?'); + if (!areYourSure) { + console.log('Delete aborted'); + + return false; + } + + const url = presentationConfig.url; + const formData = new FormData(); + formData.append('request', 'delete'); + formData.append('id', id); + fetch(url, { + method: 'POST', + body: formData + }).then(function (response) { + if (response.ok) { + return response.text(); + } + return Promise.reject(response); + }).then(function (html) { + // Display a message + const message = html; + mainLizmap.presentation.addMessage(message, 'info', 5000); + + // Refresh the content of the list of presentations + const cardsElement = document.querySelector('#presentation-list-container lizmap-presentation-cards'); + cardsElement.setAttribute('updated', 'done'); + + // Go back to the list of presentations + this.toggleContainersDisplay('list-container', null, true); + }).catch(function (error) { + console.warn(error); + }); + } + + /** + * Display the HTML to configure the presentation pages + * + * @param {number} id Id of the presentation. + */ + async showPresentationDetail(id) { + // Get the form + try { + const url = presentationConfig.url; + const request = 'detail'; + let formData = new FormData(); + formData.append('request', request); + formData.append('id', id); + const response = await fetch(url, { + method: "POST", + body: formData + }); + + // Check content type + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("text/plain")) { + throw new TypeError("Wrong content-type. HTML Expected !"); + } + + // Get the response + const htmlContent = await response.text(); + + // Display it + this.toggleContainersDisplay('detail-container', htmlContent, false); + + // Add events + + } catch(error) { + console.log(error); + let previousMessage = document.getElementById('lizmap-presentation-message'); + if (previousMessage) previousMessage.remove(); + const message = ` + ${error} + `; + this.addMessage(message, 'error', 5000); + } + } + }; diff --git a/lizmap/modules/presentation/classes/presentation.listener.php b/lizmap/modules/presentation/classes/presentation.listener.php index 311c8a4c5e..f237ee028b 100644 --- a/lizmap/modules/presentation/classes/presentation.listener.php +++ b/lizmap/modules/presentation/classes/presentation.listener.php @@ -12,12 +12,11 @@ public function ongetMapAdditions($event) // Check config jClasses::inc('presentation~presentationConfig'); - $presentationConfigInstance = new presentationConfig($event->repository, $event->project); - if ($presentationConfigInstance->getStatus()) { - $presentationConfig = $presentationConfigInstance->getConfig(); - $presentationConfigData = array( + $getConfig = new presentationConfig($event->repository, $event->project); + if ($getConfig->getStatus()) { + $presentationConfig = array( 'url' => jUrl::get( - 'presentation~service:index', + 'presentation~presentation:index', array( 'repository' => $event->repository, 'project' => $event->project, @@ -27,7 +26,6 @@ public function ongetMapAdditions($event) $jsCode = array( 'var presentationConfig = '.json_encode($presentationConfig), - 'var presentationConfigData = '.json_encode($presentationConfigData), ); $css = array( $basePath.'assets/css/presentation.css', diff --git a/lizmap/modules/presentation/classes/presentationConfig.class.php b/lizmap/modules/presentation/classes/presentationConfig.class.php index da62685aee..bb7960f1ce 100644 --- a/lizmap/modules/presentation/classes/presentationConfig.class.php +++ b/lizmap/modules/presentation/classes/presentationConfig.class.php @@ -15,75 +15,64 @@ class presentationConfig private $errors = array(); private $repository; private $project; - private $lproj; - private $config; public function __construct($repository, $project) { try { - $lproj = lizmap::getProject($repository.'~'.$project); - if (!$lproj) { + $lizmapProject = lizmap::getProject($repository.'~'.$project); + if (!$lizmapProject) { $this->errors = array( - 'title' => 'Invalid Query Parameter', - 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + array( + 'title' => 'Invalid Query Parameter', + 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + ), ); return false; } } catch (\Lizmap\Project\UnknownLizmapProjectException $e) { $this->errors = array( - 'title' => 'Invalid Query Parameter', - 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + array( + 'title' => 'Invalid Query Parameter', + 'detail' => 'The lizmap project '.strtoupper($project).' does not exist !', + ), ); return false; } // Check acl - if (!$lproj->checkAcl()) { + if (!$lizmapProject->checkAcl()) { $this->errors = array( - 'title' => 'Access Denied', - 'detail' => jLocale::get('view~default.repository.access.denied'), + array( + 'title' => 'Access Denied', + 'detail' => jLocale::get('view~default.repository.access.denied'), + ), ); return false; } - // presentation config may be an empty array $this->repository = $repository; $this->project = $project; - $this->lproj = $lproj; $this->status = true; - $this->config = $this->getPresentations(); } /** - * Get the presentations stored in the database - * for the current Lizmap project. + * Get the status. * - * @return null|json $presentations List of presentations - */ - private function getPresentations() - { - $dao = \jDao::get('presentation~presentation'); - $getPresentations = $dao->findAll(); - - return $getPresentations->fetchAllAssociative(); - } - - /** - * Get presentation configuration. + * @return bool Status of the configuration for the given project */ - public function getConfig() - { - return $this->config; - } - public function getStatus() { return $this->status; } + /** + * Get the errors. + * + * @return array + */ public function getErrors() { return $this->errors; diff --git a/lizmap/modules/presentation/controllers/presentation.classic.php b/lizmap/modules/presentation/controllers/presentation.classic.php new file mode 100644 index 0000000000..be3f9d24e0 --- /dev/null +++ b/lizmap/modules/presentation/controllers/presentation.classic.php @@ -0,0 +1,473 @@ +param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Redirect to method corresponding on REQUEST param + $request = $this->param('request', 'create'); + + switch ($request) { + case 'list': + return $this->list(); + + break; + + case 'create': + return $this->create(); + + break; + + case 'modify': + return $this->modify(); + + break; + + case 'delete': + return $this->delete(); + + break; + + case 'detail': + return $this->detail(); + + break; + } + + return $this->error( + array( + array( + 'title' => 'Not supported request', + 'detail' => 'The request "'.$request.'" is not supported!', + ), + ), + ); + } + + /** + * List the available presentations. + * + * @return \jResponseJson The JSON containing an array of presentation objects + */ + public function list() + { + // todo return only presentations available for the given user + // check rights && check groups + /** var \jDaoFactoryBase $dao */ + $dao = \jDao::get('presentation~presentation'); + $getPresentations = $dao->findAll(); + $presentations = $getPresentations->fetchAllAssociative(); + + // Return html fragment response + /** @var \jResponseJson $rep */ + $rep = $this->getResponse('json'); + $rep->data = $presentations; + + return $rep; + } + + /** + * Setup the request. + * + * @param $repository Name of the repository + * @param $project Name of the project + * + * @urlparam $REQUEST Request type + * + * @return null|\jResponseHtmlFragment the request response + */ + private function setup($repository, $project) + { + // Check presentation config + jClasses::inc('presentation~presentationConfig'); + $presentationConfig = new presentationConfig($repository, $project); + if (!$presentationConfig->getStatus()) { + return $this->error($presentationConfig->getErrors()); + } + + $this->repository = $repository; + $this->project = $project; + + return null; + } + + /** + * Provide errors. + * + * @param mixed $errors + * + * @return \jResponseHtmlFragment the errors response + */ + public function error($errors) + { + // Use template to create html form content + $tpl = new jTpl(); + $tpl->assign('errors', $errors); + $content = $tpl->fetch('presentation~html_error'); + + // Return html fragment response + /** @var \jResponseHtmlFragment $rep */ + $rep = $this->getResponse('htmlfragment'); + $rep->addContent($content); + + return $rep; + } + + /** + * Check if the given id corresponds to an existing presentation. + * + * @param $id Id to check + * + * @return null|jResponseHtmlFragment The error response if a problem has been detected, + * null if the presentation exists + */ + private function checkGivenId($id) + { + // Get the presentation with the given id + if ($id === null) { + return $this->error( + array( + array( + 'title' => 'Parameter id is not valid', + 'detail' => 'The required parameter id must be a positive integer !', + ), + ) + ); + } + + // Get the corresponding presentation + $dao = \jDao::get('presentation~presentation'); + $presentation = $dao->get($id); + if ($presentation === null) { + return $this->error( + array( + array( + 'title' => 'No presentation for the given id', + 'detail' => 'There is no presentation with id = "'.$id.'" !', + ), + ) + ); + } + + $this->id = $id; + + return null; + } + + /** + * Create a new presentation. + * + * @return \jResponseHtmlFragment|\jResponseRedirect The HTML form for presentation creation + */ + public function create() + { + // Setup + $repository = $this->param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Get the form + $form = \jForms::create('presentation~presentation', -999); + $form->setData('submit_button', 'submit'); + $form->setData('repository', $this->repository); + $form->setData('project', $this->project); + + // Get login + $login = null; + $isConnected = \jAuth::isConnected(); + if ($isConnected) { + $user = \jAuth::getUserSession(); + $login = $user->login; + } + $form->setData('created_by', $login); + $form->setData('updated_by', $login); + + // Redirect to the edit method + /** @var \jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + $rep->params = array( + 'project' => $this->project, + 'repository' => $this->repository, + 'status' => 'create', + ); + $rep->action = 'presentation~presentation:edit'; + + return $rep; + } + + /** + * Modify an existing presentation. + * + * @return \jResponseHtmlFragment|\jResponseRedirect The HTML form for presentation creation + */ + public function modify() + { + // Setup + $repository = $this->param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Check the given ID + $id = $this->intParam('id', -999, true); + $checkId = $this->checkGivenId($id); + if ($checkId !== null) { + return $checkId; + } + + // Get the form + $form = \jForms::create('presentation~presentation', $this->id); + $form->initFromDao('presentation~presentation', $this->id); + $form->setData('submit_button', 'submit'); + $form->setData('repository', $this->repository); + $form->setData('project', $this->project); + + // Get login + $login = null; + $isConnected = \jAuth::isConnected(); + if ($isConnected) { + $user = \jAuth::getUserSession(); + $login = $user->login; + } + $form->setData('updated_by', $login); + + // Redirect to the edit method + /** @var \jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + $rep->params = array( + 'project' => $this->project, + 'repository' => $this->repository, + 'id' => $this->id, + 'status' => 'modify', + ); + $rep->action = 'presentation~presentation:edit'; + + return $rep; + } + + /** + * Display the editing form for a new or existing presentation. + * + * @return \jResponseHtmlFragment|\jResponseRedirect The HTML form for presentation creation + */ + public function edit() + { + // Setup + $repository = $this->param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Check the given ID + $action = $this->param('status', 'create'); + $id = $this->intParam('id', -999, true); + $this->id = $id; + if ($action == 'modify') { + $checkId = $this->checkGivenId($id); + if ($checkId !== null) { + return $checkId; + } + } + + // Get the form + $form = \jForms::get('presentation~presentation', $id); + if (!$form) { + $form = jForms::create('presentation~presentation', $id); + } + + // Use template to create html form content + $tpl = new jTpl(); + $tpl->assign('form', $form); + $content = $tpl->fetch('presentation~presentation_form'); + + // Return html fragment response + /** @var \jResponseHtmlFragment $rep */ + $rep = $this->getResponse('htmlfragment'); + $rep->addContent($content); + + return $rep; + } + + /** + * Save the given presentation form data. + * + * @return \jResponseHtmlFragment|\jResponseRedirect + */ + public function save() + { + $id = $this->intParam('id', -999, true); + + // Get the form + $form = \jForms::fill('presentation~presentation', $id); + + // Setup + $repository = $form->getData('repository'); + $project = $form->getData('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Checks + if (!$form->check()) { + // Invalid form: redirect to the display action + /** @var \jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + $rep->action = 'presentation~presentation:edit'; + $rep->params = array( + 'project' => $this->project, + 'repository' => $this->repository, + 'id' => $this->id, + 'status' => 'error', + ); + + return $rep; + } + + // Save the data + $primaryKey = $form->saveToDao('presentation~presentation', $id); + + // Destroy the form + $title = $form->getData('title'); + jForms::destroy('presentation~presentation', $id); + + // Display confirmation + /** @var \jResponseHtmlFragment $rep */ + $rep = $this->getResponse('htmlfragment'); + $rep->addContent("The presentation '{$title}' has been successfully saved"); + + return $rep; + } + + /** + * Delete an existing presentation. + * + * @return \jResponseHtmlFragment The HTML form for presentation creation + */ + public function delete() + { + // Setup + $repository = $this->param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Check the given ID + $id = $this->intParam('id', -999, true); + $checkId = $this->checkGivenId($id); + if ($checkId !== null) { + return $checkId; + } + + // Delete the given presentation + /** var \jDaoFactoryBase $dao */ + $dao = \jDao::get('presentation~presentation'); + $presentation = $dao->get($this->id); + + try { + $delete = $dao->delete($this->id); + + /** var \jResponseHtmlFragment $rep */ + $rep = $this->getResponse('htmlfragment'); + $content = "The presentation '{$presentation->title}' has been successfully deleted"; + $rep->addContent($content); + + return $rep; + } catch (Exception $e) { + return $this->error( + array( + array( + 'title' => 'The presentation cannot be deleted', + 'detail' => 'An error occurred while deleting the presentation n°"'.$this->id.'" !', + ), + ), + ); + } + } + + /** + * Returns the HTML to setup a given presentation. + * + * This will show a list of pages and buttons to add or remove pages. + * + * @return \jResponseHtmlFragment the HTML content + */ + public function detail() + { + // Setup + $repository = $this->param('repository'); + $project = $this->param('project'); + $setup = $this->setup($repository, $project); + if ($setup !== null) { + return $setup; + } + + // Check the given ID + $id = $this->intParam('id', -999, true); + $checkId = $this->checkGivenId($id); + if ($checkId !== null) { + return $checkId; + } + + // Use template to create html form content + $content = "Detail presentation for {$this->id}"; + + // Return html fragment response + /** @var \jResponseHtmlFragment $rep */ + $rep = $this->getResponse('htmlfragment'); + $rep->addContent($content); + + return $rep; + } +} diff --git a/lizmap/modules/presentation/daos/presentation.dao.xml b/lizmap/modules/presentation/daos/presentation.dao.xml index 6988ba43fb..6e041f88e1 100644 --- a/lizmap/modules/presentation/daos/presentation.dao.xml +++ b/lizmap/modules/presentation/daos/presentation.dao.xml @@ -4,7 +4,7 @@ - + @@ -13,9 +13,9 @@ - + - + diff --git a/lizmap/modules/presentation/daos/presentation_page.dao.xml b/lizmap/modules/presentation/daos/presentation_page.dao.xml index b28f9940c4..3a410c70ee 100644 --- a/lizmap/modules/presentation/daos/presentation_page.dao.xml +++ b/lizmap/modules/presentation/daos/presentation_page.dao.xml @@ -4,7 +4,7 @@ - + diff --git a/lizmap/modules/presentation/forms/presentation.form.xml b/lizmap/modules/presentation/forms/presentation.form.xml new file mode 100644 index 0000000000..45d3d73926 --- /dev/null +++ b/lizmap/modules/presentation/forms/presentation.form.xml @@ -0,0 +1,49 @@ + +
+ + + + + + + + + + + +