feat: simplification du fonctionnement de l'affichage en vue bloc, ce n'est plus une url mais le contenu du fichier
This commit is contained in:
parent
c293014cea
commit
4c5a21caf0
4 changed files with 151 additions and 81 deletions
195
README.md
195
README.md
|
@ -3,38 +3,43 @@
|
|||
##fontello API
|
||||
|
||||
1. Ouvrir le navigateur
|
||||
|
||||
```bash
|
||||
fontello-cli open
|
||||
```
|
||||
|
||||
1. mettre à jour la police
|
||||
|
||||
```bash
|
||||
fontello-cli install --css css --font font
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Stockage
|
||||
>On utilise maintenant la lib localforage : https://localforage.github.io/localForage/
|
||||
Dans cette nouvelle version, on découpe le stockage en 2:
|
||||
* le header en localstorage avec la cle prefixe par `header_` afin d'éviter les pb avec les anciens stockages
|
||||
* le table est stocké via localforage : en indexedb et async
|
||||
|
||||
> On utilise maintenant la lib localforage : https://localforage.github.io/localForage/
|
||||
> Dans cette nouvelle version, on découpe le stockage en 2:
|
||||
|
||||
- le header en localstorage avec la cle prefixe par `header_` afin d'éviter les pb avec les anciens stockages
|
||||
- le table est stocké via localforage : en indexedb et async
|
||||
|
||||
## Configuration
|
||||
|
||||
Choisir entre la version minifiée de vuejs (sans le debug) ou la version de dev
|
||||
|
||||
### PHP dans php.ini
|
||||
* post_max_size which is directly related to the POST size
|
||||
* upload_max_filesize which may be unrelated, not sure
|
||||
* max_input_time, if the POSt takes too long
|
||||
* max_input_nesting_level if your data is an array with a lot of sublevels
|
||||
* max_execution_time, but quite sure it’s not that
|
||||
* memory_limit, as you may reach a size exceding the subprocess allowed memory
|
||||
* max_input_vars, if your data array has many elements => **le plus important**
|
||||
|
||||
- post_max_size which is directly related to the POST size
|
||||
- upload_max_filesize which may be unrelated, not sure
|
||||
- max_input_time, if the POSt takes too long
|
||||
- max_input_nesting_level if your data is an array with a lot of sublevels
|
||||
- max_execution_time, but quite sure it’s not that
|
||||
- memory_limit, as you may reach a size exceding the subprocess allowed memory
|
||||
- max_input_vars, if your data array has many elements => **le plus important**
|
||||
|
||||
## Utilisation
|
||||
|
||||
- charger l'inclure (5 args possibles et facultatifs)
|
||||
|
||||
```html
|
||||
<INCLURE{fond=inclure/gamutable,env}>
|
||||
|
||||
|
@ -52,7 +57,7 @@ Choisir entre la version minifiée de vuejs (sans le debug) ou la version de dev
|
|||
tparpage=[15,25,50,'Tous'],
|
||||
champcsv="search",
|
||||
delimitercsv=";",
|
||||
urlvuebloc=spip.php?page=mon_bloc_type_html,
|
||||
htmlvuebloc=exemple_bloc, // voir fichier d'exemple : exemple_bloc.html
|
||||
vueblocdefaut='bloc ou tableau', // par defaut tableau
|
||||
namecsv="souscripteurs.csv",
|
||||
url_sort_asc="#CHEMIN{...}",
|
||||
|
@ -65,26 +70,32 @@ Choisir entre la version minifiée de vuejs (sans le debug) ou la version de dev
|
|||
}>
|
||||
|
||||
```
|
||||
|
||||
- **url_sort_asc** et **url_sort_desc** => surcharge possible des icones de tri de colonnes
|
||||
|
||||
## les filtrages par url sont :
|
||||
|
||||
- &trier=champ1|asc => OK
|
||||
- &filtrer=champ1|valeur1 => OK
|
||||
- &afficher=50 => OK
|
||||
- &rechercher=toto => OK
|
||||
|
||||
## Personnalisation du contenu : surcharger `json_gamutable.json.html` en suivant son modele
|
||||
|
||||
- pour le header c'est de la forme : "champ":"label"
|
||||
- **IMPORTANT** pour le content du json, il que le cle de la KEY de la table soit "id" et non pas "id_souscription"
|
||||
- **IMPORTANT** pour le content du json, il que le cle de la KEY de la table soit "id" et non pas "id_souscription"
|
||||
- pour les champs date, pour avoir l'ordre de la col, il faut le format : dd/mm/yyyy ou dd/mm/yy
|
||||
- on peut ajouter une clé classes pour ajouter des classes spécifiques à certaines colonnes
|
||||
|
||||
```json
|
||||
"classes":{
|
||||
"nom": "toto",
|
||||
"email":"toto"
|
||||
}
|
||||
```
|
||||
|
||||
- On peut ajouter des filtres par colonne soit avec un select soit avec un input
|
||||
|
||||
```json
|
||||
"filtreCol" : {
|
||||
"statut" : "select",
|
||||
|
@ -92,49 +103,63 @@ Choisir entre la version minifiée de vuejs (sans le debug) ou la version de dev
|
|||
"prenom": "input"
|
||||
}
|
||||
```
|
||||
|
||||
- On peut ajouter des ordre de tri par colonne : `asc` ou `desc`
|
||||
|
||||
```json
|
||||
"ordreCol" : {
|
||||
"nom" : "desc"
|
||||
}
|
||||
```
|
||||
|
||||
- recharger qu'une partie du json via le timestamp (maj), il faut ajouter le critere : `{maj > #ENV{maj,0}}` et la clé dans le header :
|
||||
|
||||
```json
|
||||
"maj": [(#CONFIG{derniere_modif_osdve_intervention}|tsEnDate|json_encode)]
|
||||
```
|
||||
|
||||
- On peut ajouter des elements que l'on veut supprimer si on utilise le {maj}
|
||||
|
||||
```json
|
||||
"a_supprimer" : [ <BOUCLE_articlesAsup(ARTICLES){si #ENV{maj}}{staut = poubelle}{','}{maj > #ENV{maj}}> #ID_ARTICLE </BOUCLE_articlesAsup> ]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Utiliser les actions :
|
||||
```html
|
||||
[(#SET{statut,
|
||||
#SET{args,#ID_SOUSCRIPTION|concat{-}|concat{#STATUT}}
|
||||
<a class="url_action" data-id="#ID_SOUSCRIPTION" href="[(#URL_ACTION_AUTEUR{changer_statut_souscription,#GET{args}})]">
|
||||
[(#STATUT|!={publie}|oui)
|
||||
<i title="Souscription en attente" class="fa fa-check fa-2x orange" aria-hidden="true"></i>
|
||||
]
|
||||
[(#STATUT|=={publie}|oui)
|
||||
<i title="Souscription validée" class="fa fa-check fa-2x verte" aria-hidden="true"></i>
|
||||
]
|
||||
</a>
|
||||
})]
|
||||
"statut" : [(#GET{statut}|json_encode)],
|
||||
```
|
||||
* il faut ajouter data-confirm="Confirmez vous ..." si on veut ajouter un popin de confirmation
|
||||
* il faut ajouter data-id="" si on veut recharger que cette ligne
|
||||
* si data-id="" est négatif, cela supprime cette ligne
|
||||
* si une variable du nom de `nomBlocAjaxReload` est définie (ou un data-ajaxreload), alors, un ajaxReload de ce bloc sera joué dans la fonctione de callback de l'action
|
||||
* rechargement si 2 gamutables :
|
||||
* par défaut `gamutableUn` si le bouton est dans `gamutableUn`, `gamutableDeux` si il est dans `gamutableDeux`
|
||||
* possibilité d'ajouter à la balise `<a>` qui déclenche l'action un `data-treload` = `1` | `2` | `12` pour forcer le rechargement de l'un ou l'autre ou les deux gamutables
|
||||
|
||||
```html
|
||||
[(#SET{statut, #SET{args,#ID_SOUSCRIPTION|concat{-}|concat{#STATUT}}
|
||||
<a
|
||||
class="url_action"
|
||||
data-id="#ID_SOUSCRIPTION"
|
||||
href="[(#URL_ACTION_AUTEUR{changer_statut_souscription,#GET{args}})]"
|
||||
>
|
||||
[(#STATUT|!={publie}|oui)
|
||||
<i
|
||||
title="Souscription en attente"
|
||||
class="fa fa-check fa-2x orange"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
] [(#STATUT|=={publie}|oui)
|
||||
<i
|
||||
title="Souscription validée"
|
||||
class="fa fa-check fa-2x verte"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
]
|
||||
</a>
|
||||
})] "statut" : [(#GET{statut}|json_encode)],
|
||||
```
|
||||
|
||||
- il faut ajouter data-confirm="Confirmez vous ..." si on veut ajouter un popin de confirmation
|
||||
- il faut ajouter data-id="" si on veut recharger que cette ligne
|
||||
- si data-id="" est négatif, cela supprime cette ligne
|
||||
- si une variable du nom de `nomBlocAjaxReload` est définie (ou un data-ajaxreload), alors, un ajaxReload de ce bloc sera joué dans la fonctione de callback de l'action
|
||||
- rechargement si 2 gamutables :
|
||||
- par défaut `gamutableUn` si le bouton est dans `gamutableUn`, `gamutableDeux` si il est dans `gamutableDeux`
|
||||
- possibilité d'ajouter à la balise `<a>` qui déclenche l'action un `data-treload` = `1` | `2` | `12` pour forcer le rechargement de l'un ou l'autre ou les deux gamutables
|
||||
|
||||
## Utiliser les crayons :
|
||||
|
||||
```json
|
||||
[{
|
||||
"header":{
|
||||
|
@ -150,13 +175,17 @@ Choisir entre la version minifiée de vuejs (sans le debug) ou la version de dev
|
|||
"tarif_prive" : "activite"
|
||||
}
|
||||
```
|
||||
|
||||
Dans cet exemple :
|
||||
|
||||
- `activite` est le raccourci du nom de la table (spip_activites => pas de préfixe + singulier)
|
||||
- `tarif_prive` est **obligatoirement** l'intitulé du champs dans cette table (si besoin il faut modifier la clé du champ...)
|
||||
|
||||
Pour une colonne utiliser les crayons sur une table différente de celle de l'id "principal" (celui utilisé comme id des lignes du JSon) :
|
||||
|
||||
- dans le bloc header du JSon on passe le raccourci du nom de la table
|
||||
- dans le bloc html on passe l'id de l'objet dans cette table
|
||||
|
||||
```json
|
||||
{
|
||||
"header":{
|
||||
|
@ -183,13 +212,17 @@ Pour une colonne utiliser les crayons sur une table différente de celle de l'id
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dans cet exemple :
|
||||
|
||||
- les crayons sur `email` et `telephone` vont éditer les champs `email` et `telephone` de la table `spip_auteurs` pour l'`id_auteur` passé en `id` de la ligne
|
||||
- les crayons sur `adresse` vont éditer le champ `adresse` de la table `spip_gis` pour l'`id_gis` passé en valeur de `crayons/adresse` de la ligne
|
||||
|
||||
### Caches
|
||||
|
||||
l'invalidation du cache ne se fait que pour les objets publiés
|
||||
Les Crayons utilisent l'api modifier_objet de SPIP qui invalide le cache que si objet est publie, il faut donc forcer l'invalidation du cache en passant pour le pipeline post_edition
|
||||
|
||||
```php
|
||||
/**
|
||||
* invalider le cache pour l'objet souscription
|
||||
|
@ -205,11 +238,15 @@ function prefixPlugin_post_edition($flux){
|
|||
```
|
||||
|
||||
### Autorisations
|
||||
|
||||
Il faut surcharger cette autorisation : autoriser_crayonner_dist() `inc/crayons`
|
||||
|
||||
### @Deprecated
|
||||
|
||||
=> integre par default dans la 3.4.2 pour tous les objets
|
||||
|
||||
- il faut utiliser le pipeline `crayons_vue_affichage_final`
|
||||
|
||||
```php
|
||||
/**
|
||||
* injection du reload de vuejs pour les crayons de l'objet souscription
|
||||
|
@ -228,32 +265,37 @@ function prefixPlugin_crayons_vue_affichage_final($flux){
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Utiliser une mediabox ou modalbox :
|
||||
|
||||
- pour rechargement total/partiel de la page: on peut demander à vuejs de recharger tout le json ou simplement une ligne dans le cas d'une modification
|
||||
1. Dans le json : ajouter un lien avec la classe mediabox ou modalbox (cf ex de json)
|
||||
```html
|
||||
"modif" : [(#VAL{<a class="modalbox" href="[(#URL_PAGE{souscrire}|parametre_url{id_souscription,#ID_SOUSCRIPTION}|parametre_url{redirect,gamutable})]"><i class="fa fa-pencil"></i></a>}|json_encode)],
|
||||
```
|
||||
2. On charge en ajax le formulaire en l'englobant d'une div avec la classe ajax (du pur spip)
|
||||
3. Dans le traiter du formulaire => supprimer la redirection et on ajoute :
|
||||
```php
|
||||
if ($retour === 'gamutable') {
|
||||
$res['redirect'] = "";
|
||||
// pour recharger que la ligne $id_patate du tableau
|
||||
$res['message_ok'] = gamutable_fermer_modalbox($id_patate);
|
||||
// ou si on veut recharger tout le tableau
|
||||
$res['message_ok'] = gamutable_fermer_modalbox();
|
||||
}
|
||||
```
|
||||
1. Dans le json : ajouter un lien avec la classe mediabox ou modalbox (cf ex de json)
|
||||
```html
|
||||
"modif" : [(#VAL{<a
|
||||
class="modalbox"
|
||||
href="[(#URL_PAGE{souscrire}|parametre_url{id_souscription,#ID_SOUSCRIPTION}|parametre_url{redirect,gamutable})]"
|
||||
><i class="fa fa-pencil"></i></a
|
||||
>}|json_encode)],
|
||||
```
|
||||
2. On charge en ajax le formulaire en l'englobant d'une div avec la classe ajax (du pur spip)
|
||||
3. Dans le traiter du formulaire => supprimer la redirection et on ajoute :
|
||||
```php
|
||||
if ($retour === 'gamutable') {
|
||||
$res['redirect'] = "";
|
||||
// pour recharger que la ligne $id_patate du tableau
|
||||
$res['message_ok'] = gamutable_fermer_modalbox($id_patate);
|
||||
// ou si on veut recharger tout le tableau
|
||||
$res['message_ok'] = gamutable_fermer_modalbox();
|
||||
}
|
||||
```
|
||||
- Ajouter `data-confirm="Confirmez vous ..."` si on veut ajouter une popin de confirmation
|
||||
|
||||
|
||||
## Utiliser deux Gamutables dans la même page :
|
||||
|
||||
- créer les 2 apiuri dans l'#ENV : **apiuri** et **apiuri_deux**
|
||||
- tous les paramètres envoyés a `inclure/gamutable` peuvent être "dédoublés" avec le suffixe **_deux**
|
||||
- tous les paramètres envoyés a `inclure/gamutable` peuvent être "dédoublés" avec le suffixe **\_deux**
|
||||
- pour personnaliser le contenu entre les 2 gamutables on peut surcharger `inclure/separateur_gamutables`
|
||||
- Exemple :
|
||||
|
||||
```html
|
||||
<div class="tableau_resas">
|
||||
#SET{s,#VAL{spip.php?page=json_resas.json}}
|
||||
|
@ -282,54 +324,71 @@ function prefixPlugin_crayons_vue_affichage_final($flux){
|
|||
</div>
|
||||
```
|
||||
|
||||
## URLs avec filtrage :
|
||||
|
||||
## URLs avec filtrage :
|
||||
Concatener le filtrage dans l'url d'appel du json :
|
||||
|
||||
```html
|
||||
http://guides.spip/?page=grille_tarifs&activite=Alpinisme
|
||||
```
|
||||
ou
|
||||
```html
|
||||
<a class="btn" href="[(#URL_PAGE{commandes}|parametre_url{annee,2021})]">2021</a>
|
||||
http://guides.spip/?page=grille_tarifs&activite=Alpinisme
|
||||
```
|
||||
|
||||
ou
|
||||
|
||||
```html
|
||||
<a class="btn" href="[(#URL_PAGE{commandes}|parametre_url{annee,2021})]"
|
||||
>2021</a
|
||||
>
|
||||
```
|
||||
|
||||
## cellule checkbox
|
||||
|
||||
Il est possible d'ajouter pour une colonne des checbox qui declencheront soit une action, soit l'appel d'un formulaire.
|
||||
|
||||
1. il faut ajouter dans le header la cle checkbox (cf ex) pour definir l'url du payload de type action ou page= (formulaire)
|
||||
avec comme nom de premier parametre : data qui sera transformer en php via :
|
||||
avec comme nom de premier parametre : data qui sera transformer en php via :
|
||||
|
||||
```php
|
||||
$data = json_decode($data,true);
|
||||
```
|
||||
|
||||
2. Puis la valeur de la cellule doit etre du type dataid-#ID_PATATE (cf ex)
|
||||
|
||||
## Refs
|
||||
|
||||
## Refs
|
||||
https://unpkg.com/browse/vue-next-select@2.10.4/
|
||||
|
||||
|
||||
## Développement :
|
||||
|
||||
- désormais tout le JS autour du gamutable est dans `src/gamutable.js`
|
||||
- installer les dépendances avec `npm install` ou `pnpm install`
|
||||
- pour développer, le mieux est d'utiliser la commande `npm run dev` => qui va lancer un serveur de test, SPIP sait l'utiliser.
|
||||
- une fois le dev terminé, il faut builder le code `npm run build`
|
||||
|
||||
une autre alternative, est d'utiliser la commande `npm run watch` qui permet de builder après chaque changement de code source, mais le temps est BEAUCOUP plus lent qu'avec la commende `npm run dev`
|
||||
|
||||
> A utiliser pour une micro modification
|
||||
|
||||
### Modif pour contourner le problème de NODE_PATH provoqué par `npm run dev` avec Gitbash sous Windows :
|
||||
|
||||
- dans `package.json` remplacer la ligne
|
||||
|
||||
```json
|
||||
"dev": "APP_ENV=development vite",
|
||||
```
|
||||
|
||||
par
|
||||
|
||||
```json
|
||||
"dev": "SET APP_ENV=development & vite",
|
||||
```
|
||||
|
||||
### Problème de blocage des requêtes multi-origine provoquée par `npm run dev` :
|
||||
- `npm run dev` utilisant un serveur virtuel local sur le port 5134, on se retrouve avec une erreur CORS dans le navigateur si le dev est fait sur une URL en https.
|
||||
Par exemple :
|
||||
|
||||
- `npm run dev` utilisant un serveur virtuel local sur le port 5134, on se retrouve avec une erreur CORS dans le navigateur si le dev est fait sur une URL en https.
|
||||
Par exemple :
|
||||
|
||||
```
|
||||
Blocage d’une requête multiorigine (Cross-Origin Request) : la politique « Same Origin » ne permet pas de consulter la ressource distante située sur https://localhost:5134/gamutable.js. Raison : échec de la requête CORS.
|
||||
```
|
||||
> TODO : trouver comment supprimer cette limitation... (la config `Header set Access-Control-Allow-Origin "*"` dans le vhost apache ne suffit pas !)
|
||||
|
||||
> TODO : trouver comment supprimer cette limitation... (la config `Header set Access-Control-Allow-Origin "*"` dans le vhost apache ne suffit pas !)
|
||||
|
|
12
exemple_bloc.html
Normal file
12
exemple_bloc.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="bloc flex">
|
||||
<div class="flex-grow">
|
||||
<div> <strong>Nom :</strong> @@nom@@ </div>
|
||||
<div> <strong>Prénom :</strong> @@prenom@@ </div>
|
||||
<div> <strong>Age :</strong> @@age@@ </div>
|
||||
<div> <strong>Email :</strong> @@email@@ </div>
|
||||
<div> <strong>Téléphone :</strong> @@telephone@@ </div>
|
||||
</div>
|
||||
<div>
|
||||
@@logo@@
|
||||
</div>
|
||||
</div>
|
|
@ -22,6 +22,9 @@
|
|||
[(#SET{sort_desc,[(#CHEMIN{img/sprite_gamutable.svg})#sort_desc]})]
|
||||
[(#SET{pdfuri,#VAL{pdf_gamutable}|generer_url_action{"", 1}})]
|
||||
<span class="crayon gamutable-yyyy-nn"></span>
|
||||
[(#ENV{fichierVueBloc}|oui)
|
||||
[(#SET{htmlvuebloc,#INCLURE{fond=#ENV{fichierVueBloc}}|replace{'"',"'"}})]
|
||||
]
|
||||
|
||||
<div id="vueGamutable">
|
||||
<BOUCLE_un(CONDITION){si #ENV{apiuri}|oui}>
|
||||
|
@ -39,7 +42,7 @@
|
|||
namepdf="#ENV{namepdf}"
|
||||
argpdf="#ENV{argpdf}"
|
||||
fichierpdf="#ENV{fichierpdf}"
|
||||
urlvuebloc="[(#ENV{urlvuebloc})]"
|
||||
htmlvuebloc="[(#GET{htmlvuebloc})]"
|
||||
filtrecolmulti="#ENV{filtrecolmulti,oui}"
|
||||
nomblocajaxreload="#ENV{nomblocajaxreload}"
|
||||
stockage="#ENV{stockage,localstorage}"
|
||||
|
@ -57,6 +60,9 @@
|
|||
</BOUCLE_un>
|
||||
|
||||
<BOUCLE_deux(CONDITION){si #ENV{apiuri_deux}|oui}>
|
||||
[(#ENV{fichierVueBloc}|oui)
|
||||
[(#SET{htmlvuebloc_deux,#INCLURE{fond=#ENV{fichierVueBloc}}})]
|
||||
]
|
||||
<INCLURE{fond=inclure/separateur_gamutables,env}>
|
||||
<div class="container_deux">
|
||||
[<span class="h2-like titregamutable_deux">(#ENV{titregamutable_deux})</span>]
|
||||
|
@ -72,7 +78,7 @@
|
|||
namepdf="#ENV{namepdf_deux}"
|
||||
argpdf="#ENV{argpdf_deux}"
|
||||
fichierpdf="[(#ENV{fichierpdf_deux, #ENV{fichierpdf}})]"
|
||||
urlvuebloc="[(#ENV{urlvuebloc_deux})]"
|
||||
htmlvuebloc="[(#GET{htmlvuebloc_deux})]"
|
||||
filtrecolmulti="[(#ENV{filtrecolmulti_deux, #ENV{filtrecolmulti,oui}})]"
|
||||
nomblocajaxreload="#ENV{nomblocajaxreload}"
|
||||
stockage="#ENV{stockage,localstorage}"
|
||||
|
|
|
@ -219,9 +219,6 @@ const props = defineProps({
|
|||
namecsv: {
|
||||
type: String,
|
||||
},
|
||||
urlvuebloc: {
|
||||
type: String,
|
||||
},
|
||||
vueblocdefaut: {
|
||||
type: String,
|
||||
default: "tableau",
|
||||
|
@ -241,6 +238,9 @@ const props = defineProps({
|
|||
filtrer: {
|
||||
type: String,
|
||||
},
|
||||
htmlvuebloc: {
|
||||
type: String,
|
||||
},
|
||||
_id: {
|
||||
type: Number,
|
||||
},
|
||||
|
@ -282,7 +282,7 @@ let selectTr = ref([]);
|
|||
let champ_search = ref(props.champcsv ?? "html");
|
||||
let chargement = ref(true);
|
||||
let quelleVue = ref(props.vueblocdefaut);
|
||||
let vuebloc = ref(false);
|
||||
let vuebloc = ref(props.htmlvuebloc);
|
||||
// let loadingVueSelect = ref(true);
|
||||
let ajaxCrayons = ref(false);
|
||||
let maj = ref("");
|
||||
|
@ -296,13 +296,6 @@ let filtreColValeurs = [];
|
|||
onMounted(() => {
|
||||
localforage.setDriver(localforage[props.stockage.toUpperCase()]);
|
||||
chargerJson("maj");
|
||||
if (props.urlvuebloc) {
|
||||
fetch(props.urlvuebloc)
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
vuebloc.value = data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
Loading…
Add table
Reference in a new issue