Partie 2
Domaine Admin – Back-Office du Blog
L'objectif de ce chapitre est de créer le back-office de votre blog qui va permettre de gérer tous les articles qui apparaîtrons sur ce blog.
Nous commencerons par créer l'aspect visuel de ce back-office (header, footer, page de listing des articles et enfin page de création/modification d'un article.
A la fin de ce chapitre vous obtiendrez un back-office de blog comme celui-ci :
Version desktop
Version mobile
🔗 Rendez-vous sur la page d'admin de votre blog : https://admin.myblog.fr/bweb
Puis dans le menu de gauche cliquez sur « Mes domaines » puis sur « admin.myblog.fr »
Dans l'onglet Général nous allons sélectionner :
- Le type de template : cocher le second template : Header/Footer entre les barres de côtés
- La composition du template : cocher Header et Barre gauche
Propriétés du domaine
La prochaine étape va être de créer visuellement le rendu de ce header et du menu de gauche.
2.1 Création du Header
🖥️ Version Desktop
📱 Version Mobile
- Cliquez sur l'onglet « Entête » pour ouvrir l'éditeur visuel de votre Header
-
Dans le coin supérieur droit vous voyez apparaître le bouton
qui vous permet d'ouvrir le dev-panel, une fois ouvert activez les 4 premiers boutons de la partie droite pour laisser le dev-panel toujours ouvert :
-
Dans la liste des objets, sélectionner « Conteneur » et glisser le sur votre fenêtre de rendu puis paramétrez-le comme suivant :
Nous avons donc défini depuis le dev-panel :
- • La hauteur minimum de 80px pour ce header
- • Un espacement de 24px à droite et à gauche
- • Une couleur de fond avec un dégradé
- • Une ombre (directement en saisissant la classe CSS Tailwind mais vous auriez également pu le faire depuis l'onglet « ombre » du dev panel)
-
Ajoutons maintenant le bouton et le titre pour faire apparaître le bandeau de navigation sur la version mobile :
Nous avons donc :
- • Ajouté un bouton et un titre pour la version mobile du backoffice
- • Ajouté la classe hidden sur le bouton « button of mobile menu » pour les écrans LG
Le bouton du menu mobile utilise un système de breakpoints responsive : il est visible par défaut sur les petits écrans (mobiles et tablettes), mais devient automatiquement invisible sur les écrans larges (à partir de 1024px de largeur) grâce à la classe hidden appliquée sur le breakpoint LG.
Ce comportement adaptatif permet d'offrir une navigation appropriée selon la taille de l'écran de l'utilisateur.
Breakpoints par défaut de Tailwind CSS :
| Préfixe | Taille | Appareils concernés |
|---|---|---|
| Défaut | 0px et plus | Tous les écrans (mobile par défaut) |
| sm: | 640px et plus | Smartphones en paysage, petites tablettes |
| md: | 768px et plus | Tablettes en portrait |
| lg: | 1024px et plus | Tablettes en paysage, petits ordinateurs portables |
| xl: | 1280px et plus | Ordinateurs portables, écrans de bureau |
| 2xl: | 1536px et plus | Grands écrans de bureau |
Puis nous avons :
- • Ajouté la classe hidden sur le conteneur « mobile buttons » ce qui permet d'indiquer que ce conteneur sera masqué par défaut quelle que soit la taille d'écran.
- • Ajouté un événement onclick sur le bouton du menu mobile pour le faire apparaître/disparaître à chaque clic.
- • Cet événement a été placé dans le breakpoint Default mais comme le bouton est invisible à partir de LG, il n'est pas nécessaire de faire plus. L'action ne sera déclenchée qu'entre SM et MD.
2.2 Création de la barre latérale gauche
Cliquez sur l'onglet « Barre latérale de gauche » pour ouvrir l'éditeur visuel de votre menu de gauche :
En ajoutant la classe hidden sur le conteneur principal « left side bar » celui-ci se retrouve masqué par défaut et l'ajout de la classe flex sur le breakpoint LG permet de l'afficher pour les écrans dont la résolution est supérieure à 1024px.
💡 Vous aurez noté que les boutons du menu ayant déjà été réalisés pour le header du menu mobile, nous avons juste eu à copier/coller ces blocs.
2.3 Création de la page de listing des articles
Depuis la page d'administration, accédez au menu Pages, puis cliquez sur Home (qui est la page d'accueil et la seule page actuelle de votre application) et modifiez son contenu comme suivant :
Configuration de la page
Cliquez maintenant sur le lien « Voir le site » pour accéder à votre application, vous verrez le header et le menu de gauche que nous avons créé précédemment ainsi que la page par défaut générée lors de l'installation de BWEB.
⚠️ Nous allons commencer par tout supprimer
2.3.1 Header
Nous allons commencer par créer le bandeau supérieur de cette page de listing des articles pour arriver à un résultat comme ceci :
Bandeau supérieur de la page listing
2.3.2 Listing des posts
Maintenant nous allons ajouter le tableau qui liste tous les articles du blog pour arriver à ce résultat :
Tableau de listing des articles
Nous avons très simplement mappé notre table POST avec notre listbox.
📹 À 01:48 de la vidéo - La formule pour la valeur de la colonne Statut est :
This.isPublished?" Published":"Draft"
La formule pour la classe CSS du span est :
This.isPublished?"badge badge-soft badge-success":"badge badge-soft badge-info"
⚠️ Pour le moment les boutons d'ajout, d'édition et de suppression d'un post ne sont pas actifs, nous y reviendrons dans la suite de ce QuickStart.
💡 De façon tout à fait optionnelle, comme vous l'avez vu dans la vidéo, pour styliser en profondeur ces tableaux vous pouvez ajouter un bloc « code » et y inclure des styles CSS personnalisés.
De nouvelles options seront ajoutées dans des prochaines versions de BWEB, pour éviter d'ajouter du code, mais vous savez que pour toute présentation particulière que BWEB ne vous permet pas de faire, cette zone de code est disponible, moyennant un apprentissage basique du CSS (ou l'aide d'une IA bien entendu).
Exemple de CSS personnalisé :
<style>
.labelgroup {
border-top: 0 !important;
border-bottom: 0 !important;
}
.labelgroup label {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 1.5rem;
font-size: 0.875rem;
font-weight: 700;
color: #1f2937;
background-color: #ffffff;
border-bottom: 4px solid #3b82f6;
}
.labelgroup label span {
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
2.4 Création de la page d'édition des articles
2.4.1 Header
Nous n'allons pas réellement créer une nouvelle page, mais nous allons contextualiser le fonctionnement de la page de listing.
Par défaut cette page affichera bien le tableau des Posts créés précédemment et lors du clic sur le bouton Add Post ou sur le crayon d'édition d'un post, les actions suivantes vont être exécutées :
- • Masquage des éléments de la page de listing (titre + listbox)
- • Affichage du formulaire d'édition d'un Post
Dans cette première partie nous allons créer le header de la page d'édition comme suivant puis ajouter les fonctions de contextualisation :
Le conteneur edit-main-content est masqué par défaut puisque nous avons ajouté une condition pour le rendre visible :
(Entity#null) || (vo_selectedItemData#null)
Si Entity qui référence l'objet POSTEntity en cours de travail ou vo_selectedItemData qui représente la ligne du tableau actuellement sélectionnée sont tous les 2 null alors le container edit-main-content est masqué.
Nous avons également ajouté les 2 actions sur l'événement onclick du bouton « add Post » et du crayon d'édition :
-
•
renderFunction -> hideblock : toutes les fonctions de la class renderFunction sont exécutées côté client (le navigateur) en javascript et permettent d'afficher ou masquer un block, d'afficher une alerte, de recharger une listbox…Dans le cas présent nous avons sélectionné la fonction hideblock et ensuite indiqué le block à masquer à savoir listing-main-content
-
•
La deuxième étape consiste à afficher le block edit-main-content qui est masqué par défaut. Ce block nécessite un appel serveur pour récupérer les informations du POST, nous ne pouvons donc pas utiliser une renderFunction. Dans ce cas nous appelons la classe WebFormController et sa fonction loadBlockWithSelectedItem qui va permettre de recharger tout le block edit-main-content en récupérant auparavant le POSTEntity
2.4.2 Formulaire d'édition d'un POST
Intégration d'un Rich Editor "Quill"
BWEB n'intègre pas encore de composant Rich editor mais vous avez la possibilité d'ajouter grâce à l'objet code votre propre javascript, css et également inclure des bibliothèques javascript externes.
Dans ce QuickStart nous avons donc ajouté Quill qui est un Rich Editor très simple d'utilisation.
📚 Vous pouvez retrouver la documentation complète de Quill ici : https://quilljs.com/docs/quickstart
Le premier bloc de code intitulé « quill integration » permet d'inclure la bibliothèque Quill et de configurer légèrement le rendu :
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<style>
.ql-container {
height: 300px;
}
.ql-editor {
min-height: 200px;
width: 100%;
}
body:has(input[name="content"].error) #quill-content {
border: 1px solid red !important;
}
</style>
Le second bloc de code intitulé « quill loading » permet de créer le rich Editor sur notre container quill-content et de configurer les éléments de sa barre de menu.
<script>
var quill = new Quill('#quill-content', {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'font': [] }],
[{ 'align': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
['blockquote', 'code-block'],
[{ 'script': 'sub'}, { 'script': 'super' }],
['link'],
['clean']
]
}
});
contentInput = document.querySelector('input[name="content"]');
if (contentInput.value.trim().length > 0) {
quill.root.innerHTML = contentInput.value;
}
quill.on('text-change', () => {
const isEmpty = quill.getText().trim().length === 0;
contentInput.value = isEmpty ? '' : quill.root.innerHTML;
});
</script>
Il permet également de synchroniser le champ caché input avec Quill.
Affichage conditionnel de l'image du POST
Dans la vidéo vous avez pu constater que nous contextualisons l'affichage de l'image actuelle du Blog. En mode création lorsqu'il n'y a encore aucune image ce container est masqué et il apparaît quand une image a déjà été enregistrée.
Pour ce faire, nous avons complété l'onglet « Conditional Display » comme suivant :
((Entity#null) && (Entity.imageName#null)) && (Entity.imageName#"")
Si le POST actuel (stocké dans la variable Entity) est non null et qu'il possède une image alors le bloc est affiché et dans le cas contraire nous avons sélectionné l'effet « Not loaded » donc le container ne sera pas chargé du tout (nous aurions aussi pu simplement le masquer en choisissant « Hide block »)
Lien vers l'image du POST
BWEB ne possède pas encore d'objet par défaut pour afficher une image dynamique (venant de la base de données) mais là encore ce n'est pas un problème, en ajoutant un objet code (ici appelé « image link code ») nous pouvons ajouter notre lien dynamique vers l'image :
<img src="/images/post-<!--#4DTEXT Entity.uuidKey--> : Indéfinie/<!--#4DTEXT Entity.imageName--> : Indéfinie"
class="w-48 h-36 object-cover rounded-lg">
💡 Nous verrons dans le chapitre suivant que lors de l'enregistrement du POST, nous sauvegardons l'image dans un dossier images/post-[UUID du POST]/[Nom du fichier image]
2.4.3 Sauvegarde du POST
Nous allons coder cette fonction de sauvegarde dans notre classe POST puis mapper cette fonction sur le bouton Save.
Dans 4D, ouvrir la data classe POST puis ajouter la fonction save :
Function save($vo_POST : Object)->$vo_WebResponse : cs.bspkComponent.WebFormController
/* parameters
vt_DetailUuid:select:getBlocsNameCollection:["wrapper"]:tomSelect:label:block detail
vt_SendAllObjectsOfBlocName:hidden:vt_DetailUuid
*/
/*
The commented lines above are mandatory, their purpose is to indicate which elements of the page will be
transmitted when the form is submitted (when clicking the save button) This will add a selection list in the dev panel in
the configuration of the button's click event to allow us to select a container whose included form elements will all be transmitted
*/
Pour le moment cette fonction ne fait rien, nous allons la compléter par la suite.
💡 Pour que le dev panel voie cette nouvelle fonction vous devez redémarrer 4D ou alors exécuter la méthode BSPK_REFRESH_STORAGE.
Retournez maintenant sur votre page, nous allons associer le clic sur le bouton Save avec cette nouvelle fonction :
Comme vous avez pu le constater maintenant le bouton clic lance la fonction save mais avant BWEB vérifie les champs obligatoires automatiquement et dans notre cas les champs titre, résumé et contenu sont vides donc un warning apparaît.
Nous pouvons maintenant compléter la fonction save :
// Are we creating or editing?
If ($vo_POST.pk=Null)
$Post:=This.new()
Else
$Post:=This.get($vo_POST.pk)
End if
$Post.title:=$vo_POST.vt_FieldValue.title
$Post.summary:=$vo_POST.vt_FieldValue.summary
$Post.content:=$vo_POST.vt_FieldValue.content
$Post.publicationDate:=$vo_POST.vt_FieldValue.date
$Post.isPublished:=($vo_POST.vt_FieldValue.status="Published")
//cleaning of the title to become a slug
//not the most efficient and complete way but it's only a quickstart :-)
$Post.slug:=Lowercase(Uppercase($vo_POST.vt_FieldValue.title))
$Post.slug:=Replace string($Post.slug; " "; "-")
$Post.slug:=Replace string($Post.slug; "'"; "-")
$Post.slug:=Replace string($Post.slug; "!"; "-")
$Post.slug:=Replace string($Post.slug; "?"; "-")
$Post.save()
//saving the image
$vt_ImageFolder:=Get 4D folder(Database folder)+"BWEB"+Folder separator+"images"+Folder separator+"post-"+$Post.uuidKey
If (Test path name($vt_ImageFolder)#Is a folder)
CREATE FOLDER($vt_ImageFolder;*)
End if
If ($vo_POST.vt_FieldValue.image#"")
$vo_Blob:=BSPK_BASE64_TO_BLOB(JSON Parse($vo_POST.vt_FieldValue.image))
$vt_ImageName:="small"+String(Milliseconds)+".jpg"
BLOB TO DOCUMENT($vt_ImageFolder+Folder separator+$vt_ImageName; OB Values($vo_Blob)[0])
$Post.imageName:=$vt_ImageName
$Post.save()
End if
//response : in this case return to the root page
$vo_WebResponse:=cs.bspkComponent.WebFormController.new()
$vo_WebResponse.vc_Action.push({vt_Action: "redirect"; vt_Url: "/"})
✅ Et voilà l'enregistrement des POST est terminé !!
Voyons maintenant le résultat en action. Remplissez le formulaire avec les informations d'un article, téléchargez une image et enregistrez le post :
2.4.4 Suppression du POST
Sur le même principe nous allons développer la fonction de suppression puis la connecter sur les boutons de suppression :
Function delete($vo_POST : Object)->$vo_WebResponse : cs.bspkComponent.WebFormController
//are we deleting from the detail of the post or from the trash icon in the listbox?
//If the Entity process variable is set, we are in the detail page
If (Entity#Null)
$Post:=Entity
Else
//if we are deleting the post from the listbox directly, we can use the $vo_POST.triggerObject.vt_RowPk to get the id of the POST
$Post:=This.get($vo_POST.triggerObject.vt_RowPk)
End if
//delete the image folder and file
$vt_ImageFolder:=Get 4D folder(Database folder)+"BWEB"+Folder separator+"images"+Folder separator+"post-"+$Post.uuidKey
If (Test path name($vt_ImageFolder)=Is a folder)
DELETE FOLDER($vt_ImageFolder; Delete with contents)
End if
$Post.drop()
//response
$vo_WebResponse:=cs.bspkComponent.WebFormController.new()
If (Entity#Null)
//we are deleting from the detail page
//we add 2 actions :
// - changeUrl to remove the pk parameter
// - openAlert to open a toast alert to confirm the delete of the post
$vo_WebResponse.vc_Action.push({vt_Action: "changeUrl"; vc_ArgumentToChange: New collection({name: "pk"; value: ""})})
$vo_WebResponse.vc_Action.push({vt_Action: "openAlert"; vt_AlertTitle: ""; vt_AlertMessage: "Post deleted !!"; vt_AlertType: "success"; vl_AlertDuration: 3})
//and we reload the mainContent block (the entire page)
$vo_WebResponse.reloadBlock("mainContent")
Else
//we are deleting the post from the listbox
//we only add the action openAlert to confirm the delete of the post
//and the we only reload our listbox of posts
$vo_WebResponse.vc_Action.push({vt_Action: "openAlert"; vt_AlertTitle: ""; vt_AlertMessage: "Post deleted !!"; vt_AlertType: "success"; vl_AlertDuration: 3})
$vo_WebResponse.reloadBlock("listbox")
End if
💡 Pour que le dev panel voie cette nouvelle fonction vous devez redémarrer 4D ou alors exécuter la méthode BSPK_REFRESH_STORAGE.
Maintenant nous allons connecter cette fonction sur nos 2 boutons de suppression :
Vous aurez remarqué que nous avons ajouté une condition d'affichage sur le bouton « delete » de la page de détail du post pour qu'il ne s'affiche que si le post est déjà enregistré.
De plus, l'ajout d'un message d'une confirmation gère automatiquement la condition pour exécuter la suppression.
2.4.5 Suppression de l'image du POST
Sur le même principe nous allons développer la fonction de suppression de l'image puis la connecter sur le bouton de suppression de l'image :
Function removePostImage()->$vo_WebResponse : cs.bspkComponent.WebFormController
//remove the image from the entity
Entity.imageName:=""
Entity.save()
//delete the image folder and file
$vt_ImageFolder:=Get 4D folder(Database folder)+"BWEB"+Folder separator+"images"+Folder separator+"post-"+Entity.uuidKey
If (Test path name($vt_ImageFolder)=Is a folder)
DELETE FOLDER($vt_ImageFolder; Delete with contents)
End if
//response
$vo_WebResponse:=cs.bspkComponent.WebFormController.new()
$vo_WebResponse.reloadBlock("featured-image")
💡 Pour que le dev panel voit cette nouvelle fonction vous devez redémarrer 4D ou alors exécuter la méthode BSPK_REFRESH_STORAGE.
Maintenant nous allons connecter cette fonction sur notre bouton de suppression :
🎉 That's all folks!!
📚 La suite de notre QuickStart avec la partie frontend dans le chapitre 3 à venir…
Vous pouvez vérifier le rendu final de votre backend du blog en cliquant sur le bouton « logout ». Vous serez alors déconnecté de l'interface d'administrateur et le dev-panel disparaîtra. Vous naviguerez alors comme un visiteur standard dans votre application (Nous n'avons pas encore de gestion de droits à ce stade. Nous verrons cela dans la partie 3).
Besoin des ressources du projet ?

