diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff19b62559832e0afe72cb8f0f289d90c0ebcc5..0d3259356b4c86b90d142af0dc6f88e21e6006b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +6.2.1 (29/03/2024) +------------------ + +- Nouveau composant UTableAjax permettant de créer un tableau avec chargement des données en Ajax et affichage avec Vue, avec son côté serveur. +- Suppression de la fonction de formatage de date + 6.2.0 (26/03/2024) ------------------ diff --git a/components/UTableAjax.vue b/components/UTableAjax.vue new file mode 100644 index 0000000000000000000000000000000000000000..75ba19f581e61adceaa3a146b932894cea0faec8 --- /dev/null +++ b/components/UTableAjax.vue @@ -0,0 +1,199 @@ +<template> + <div class="dt-bootstrap5"> + <b-row> + <b-col> + Afficher <label> + <select v-model="dSize" class="form-select form-select-sm"> + <option v-for="ps in pageSizes" :key="ps" :value="ps">{{ ps }}</option> + </select> + </label> éléments + </b-col> + <b-col> + <div class="float-end"> + Rechercher : <label><input v-model="dSearch" + class="form-control form-inline form-control-sm"/></label> + </div> + </b-col> + </b-row> + <table class="table table-bordered dataTable mb-2" ref="tableRef"> + <slot></slot> + </table> + <b-row> + <b-col>Affichage de l'élément {{ elStart }} à {{ elEnd }} sur {{ dCount }} éléments</b-col> + <b-col> + <div class="dataTables_paginate paging_simple_numbers"> + <b-pagination align="end" :page="page" v-model="page" :total-rows="dCount" :per-page="cSize" + last-number="true" + firstNumber="true" + prev-text="Précédent" + next-text="Suivant" + /> + </div> + </b-col> + </b-row> + </div> +</template> +<script> + +export default { + name: "UTableAjax", + props: { + id: {required: false, type: String}, + size: {required: false, default: 10}, + count: {required: false}, + search: {required: false}, + dataUrl: {required: true, type: String}, + }, + data() + { + return { + page: 1, + pageSizes: [10, 25, 50, 100, 'Tous'], + defaultSize: 10, + dSize: this.size, + dCount: this.count, + dSearch: this.search, + searchTimer: null, + columns: {}, + loading: false, + orderCol: undefined, + orderDir: 'asc', + }; + }, + computed: { + cSize() + { + if (isNaN(this.dSize)) { + return 9999999999999; + } + return this.dSize; + }, + storageIdentifier() + { + return 'UTableAjax-' + this.id + '-' + window.location.href; + }, + elStart() + { + if (isNaN(this.dSize)) { + return 1; + } + return ((this.page - 1) * this.dSize) + 1; + }, + elEnd() + { + if (isNaN(this.dCount)) { + return (this.page - 1) * this.dSize + this.dSize; + } + if (isNaN(this.dSize)) { + return this.dCount; + } + return Math.min(this.dCount, (this.page - 1) * this.dSize + this.dSize); + }, + }, + watch: { + dSize(newSize) + { + localStorage.setItem(this.storageIdentifier, newSize); + this.getData(); + }, + dSearch(newSearch) + { + const that = this; + clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(() => { + if (this.page > 1) { + this.page = 1; + }else { + that.getData(); + } + }, 500); + }, + page(newPage) + { + this.getData(); + }, + }, + methods: { + getData() + { + unicaenVue.axios.post(this.dataUrl, { + page: this.page, + size: this.dSize, + elStart: this.elStart, + elEnd: this.elEnd, + search: this.dSearch, + orderCol: this.orderCol, + orderDir: this.orderDir, + }).then(response => { + let data = response.data; + this.dCount = data.count; + this.$emit('data', data.data); + }); + }, + orderBy(column) + { + const element = this.columns[column]; + for(let col in this.columns){ + if (col != column){ + if (this.columns[col].classList.contains('sorting_asc')){ + this.columns[col].classList.remove('sorting_asc'); + } + if (this.columns[col].classList.contains('sorting_desc')){ + this.columns[col].classList.remove('sorting_desc'); + } + console.log(col) + } + } + let newOrder = 'asc'; + + if (element.classList.contains('sorting_asc')){ + newOrder = 'desc'; + element.classList.remove('sorting_asc'); + element.classList.add('sorting_desc'); + }else if (element.classList.contains('sorting_desc')){ + element.classList.remove('sorting_desc'); + element.classList.add('sorting_asc'); + }else{ + element.classList.add('sorting_asc'); + } + + this.orderCol = column; + this.orderDir = newOrder; + + this.getData(); + }, + }, + mounted() + { + this.dSize = parseInt(localStorage.getItem(this.storageIdentifier)) || this.defaultSize; + this.page = 1; + this.dSize = this.size; + this.dCount = this.count; + const headEl = this.$refs.tableRef; + const that = this; + if (headEl) { + // Récupération des éléments <th> qui ont l'attribut 'column' + const thElements = headEl.querySelectorAll('th[column]'); + thElements.forEach(th => { + Array.from(th.attributes).forEach(attr => { + if (attr.name == 'column') { + this.columns[attr.value] = th; + th.dataset.column = attr.value; + th.onclick = function () { + that.orderBy(this.dataset.column); + } + th.removeAttribute(attr.name); + th.classList.add('sorting'); + } + }); + }); + } + + this.getData(); + }, +} +</script> + +<style scoped> + +</style> \ No newline at end of file diff --git a/doc/composants.md b/doc/composants.md new file mode 100644 index 0000000000000000000000000000000000000000..a3c7083a19bd673b48f9e847993491b36a663b0d --- /dev/null +++ b/doc/composants.md @@ -0,0 +1,4 @@ +# Composants proposés en standard + +- [UCalendar]() Calendrier pour la saisie d'informations par mois (à documenter) +- [UTableAjax](composants/UTableAjax.md) Permet d'afficher des tableaux de données chargés en Ajax et formatés avec Vue.js diff --git a/doc/composants/UTableAjax.md b/doc/composants/UTableAjax.md new file mode 100644 index 0000000000000000000000000000000000000000..6babbc2b9615076899b1d77cc32445634635deb8 --- /dev/null +++ b/doc/composants/UTableAjax.md @@ -0,0 +1,103 @@ +# UTableAjax + +Permet de dessiner un pseudo DataTable avec un chargement des données en Ajax et un affichage en VueJS. + +Exemple d'utilisation côté client, dans un composant Vue : + +```vue +<template> + <u-table-ajax :data-url="this.dataUrl" @data="maj"> + <thead> + <tr> + <th column="ID">Id</th><!-- l'attribut column doit être renseigné pour pouvoir la rendre triable --> + <th column="LIBELLE">Libellé</th> + <th column="FORMULE">Formule</th> + <th column="ANNEE">Année</th> + <th> </th> + </tr> + </thead> + <tbody> + <!-- On liste toute les lignes et on les affiche ici --> + <tr v-for="(line,i) in lines" :key="i"> + <td>{{ line['ID'] }}</td> + <td>{{ line['LIBELLE'] }}</td> + <td>{{ line['FORMULE'] }}</td> + <td>{{ line['ANNEE'] }}</td> + <td><a :href="editUrl(line['ID'])">Modifier</a></td> + </tr> + </tbody> + </u-table-ajax> +</template> + +<script> + +export default { + name: 'Test', + data() + { + return { + dataUrl: unicaenVue.url('.../data'), + + lines: [], + }; + }, + methods: { + maj(lines) + { + this.lines = lines; + }, + editUrl(id) + { + return unicaenVue.url('mon-url/:id', {id: id}); + }, + }, +} + +</script> +<style scoped> + +</style> +``` + +Exemple de génération de données côté serveur, dans un contrôleur : +```php +<?php + +namespace MonModule\Controller; + +use Laminas\Mvc\Controller\AbstractActionController; +use UnicaenVue\Util; + +class TestController extends AbstractActionController +{ + + public function dataAction() + { + // Requête SQL + // Elle doit avoir un paramètre :search pour traiter les recherches + // Elle ne doit pas avoir d'OrderBY : il sera ajouté ensuite selon le besoin + $sql = " + SELECT + fti.id id, + fti.libelle libelle, + f.libelle formule, + a.libelle annee + FROM + formule_test_intervenant fti + JOIN formule f ON f.id = fti.formule_id + JOIN annee a ON a.id = fti.annee_id + WHERE + lower(fti.libelle || ' ' || f.libelle || ' ' || a.libelle) like :search + "; + + // renvoie un axiosModel avec les données issues du requêtage + $em = /* Récupération de l'entityManager de Doctrine */; + + return Util::tableAjaxData($em, $this->axios()->fromPOst(), $sql); + } +} +``` + +Résultat : + + \ No newline at end of file diff --git a/doc/doc.md b/doc/doc.md index 6bfdcc3d85c1b2304a41afa8ab8b0ca0e1a2419a..6572a9e71d9a8001681bcf76eae20f155cd8641f 100644 --- a/doc/doc.md +++ b/doc/doc.md @@ -8,6 +8,8 @@ [Partie serveur PHP](serveur.md) +[Composants proposés en standard](composants.md) + Pour faire vos premiers pas avec Vue : - [Doc d'introduction à VueJS de Stéphane](https://git.unicaen.fr/bouvry/presentation-dev/-/blob/master/src/vuejs.md) diff --git a/src/Controller/Plugin/AxiosPlugin.php b/src/Controller/Plugin/AxiosPlugin.php index 695ecc907e95e51c19a7a84e69deee48f7f50400..2179d4bd02033465e095393dd2bc2b184e7d9bd5 100755 --- a/src/Controller/Plugin/AxiosPlugin.php +++ b/src/Controller/Plugin/AxiosPlugin.php @@ -2,13 +2,7 @@ namespace UnicaenVue\Controller\Plugin; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Query; use Laminas\Mvc\Controller\Plugin\AbstractPlugin; -use Laminas\Mvc\Plugin\FlashMessenger\FlashMessenger; -use Laminas\View\Model\JsonModel; -use UnicaenVue\Axios\AxiosExtractor; -use UnicaenVue\Axios\AxiosExtractorInterface; /** * Description of Axios diff --git a/src/Util.php b/src/Util.php new file mode 100644 index 0000000000000000000000000000000000000000..8b64f494824a2c93dbe761f94434719c1cc89789 --- /dev/null +++ b/src/Util.php @@ -0,0 +1,43 @@ +<?php + +namespace UnicaenVue; + +use Doctrine\ORM\EntityManager; +use UnicaenVue\View\Model\AxiosModel; + +class Util +{ + static public function tableAjaxData(EntityManager $em, array $post, string $sql): AxiosModel + { + $search = $post['search'] ?? ''; + $elStart = ($post['elStart'] ?? 1) - 1; + $size = $post['size'] ?? 10; + $orderCol = $post['orderCol'] ?? null; + $orderDir = ($post['orderDir'] ?? 'asc') == 'asc' ? 'asc' : 'desc'; + + if ($orderCol && (str_contains($orderCol, '"') || str_contains($orderCol, "'"))){ + $orderCol = null; // protection contre les injections + } + + $limitSql = "OFFSET :elStart ROWS FETCH FIRST :size ROWS ONLY"; + if ($orderCol) { + $orderSql = "ORDER BY $orderCol $orderDir"; + } else { + $orderSql = ''; + } + $params = [ + 'elStart' => $elStart, + 'size' => $size, + 'search' => strtolower('%' . $search . '%'), + ]; + $count = (int)$em->getConnection()->fetchOne('SELECT count(*) cc FROM (' . $sql . ') t', $params); + $data = $em->getConnection()->fetchAllAssociative($sql . "\n" . $orderSql . "\n" . $limitSql, $params); + + $result = [ + 'count' => $count, + 'data' => $data, + ]; + + return new AxiosModel($result); + } +} \ No newline at end of file diff --git a/src/View/Model/AxiosModel.php b/src/View/Model/AxiosModel.php index a259898df830dd6c1d3f5dd8f46c89522484498b..8b1756ffde587413383e198cfe87f16fe2ff055a 100644 --- a/src/View/Model/AxiosModel.php +++ b/src/View/Model/AxiosModel.php @@ -2,7 +2,6 @@ namespace UnicaenVue\View\Model; -use Exception; use Laminas\View\Model\ModelInterface; use Traversable;