Exemples

Exemples

Description

Cette partie contient plusieurs exemples complets et concrets de développement d’un plugin.

Le développement de trois fonctions :

  • la première permettant de transformer le contenu d’un champ date en une date en toutes lettres
  • la seconde permettant, de calculer et de mettre à jour, une superficie et un prix en fonction de la longueur, de la largeur et du prix au m² d’un terrain
  • La troisième permettant de présenter le fonctionnement de la paramservlet et de l’implémentation de l’interface de paramétrage spécifique

Et le développement de 4 composants :

  • le premier affichant la météo d’une ville renseignée
  • le second affichant un compteur qui s’incrémente au clic sur un bouton et se décrémente au clic sur un autre bouton
  • le troisièmre utilisant la servletFile qui transforme deux images : une en noir et blanc et l’autre qui effectue une rotation
  • le quatrième utilisant la YbList pour fabriquer une liste utilisant le style de YellowBox

Fonction 1

Développement

La fonction prend un seul paramètre en entrée qui correspond au champ date à transformer et retourne un String qui est la valeur en toutes lettres.
La définition des paramètres dans la classe Fonction se fait comme suit :

public Params getParams() {
		Params parameters = new Params();
		
		Param valueDate = parameters.addNew(Type.DATE, InOut.IN, "Valeur du champ date", PARAMVALDATESOURCE);
		valueDate.setDescription("Sélection du champ date à convertir");
		valueDate.setMandatory(true);
		
		Param lettersDate = parameters.addNew(Type.STRING, InOut.OUT, "Champ date en lettres", PARAMVALDATEDEST);
		lettersDate.setDescription("Sélection du champ où sauver la valeur de la date en lettres");
		lettersDate.setMandatory(true);
				
		return parameters;
	}

Ici, nous avons défini deux constantes static final qui correspondent à l’attribut varName de l’objet Param.

public static final String PARAMVALDATESOURCE = "paramValDateSource";
public static final String PARAMVALDATEDEST = "paramValDateDest";

Cela permet d’utiliser des constantes pour récupérer les valeurs des paramètres.

Ensuite le traitement de la fonction est assez simple :

public ParamValues call() {
		
		// valeur de la date du paramètre
		Date dateSource = getParamValues().getDateValue(PARAMVALDATESOURCE);
		
		// Formatage de la date
		SimpleDateFormat formatter = new SimpleDateFormat("dd MMMM yyyy hh:mm:ss");
		
		// Création du paramètre de sortie
		ParamValues out = new ParamValues();
		out.addStringValue(PARAMVALDATEDEST, formatter.format(dateSource));
		
		return out;
	}

On récupère la valeur du paramètre du champ Date, puis on la formate à l’aide d’un SimpleDateFormat avec le pattern voulu.
Enfin, on créé un objet ParamValues qui correspond au paramètre de sortie de la fonction. On lui ajoute le paramètre de sortie PARAMVALDATEDEST ainsi que sa valeur formatée.

Paramétrage

Après avoir chargé le package dans YellowBox, il faut se rendre sur la liste des Plugins et cliquer sur “Ajouter” dans le bandeau haut.
Saisir un nom, sélectionner le plugin chargé, choisir la fonction dateToLetters dans “Instance de” et sélectionner une Table, ici on choisira la table Sociétés.
Le 1er paramètre correspond au paramètre d’entrée, il faut renseigner le champ date à transformer, ici Date.
Le second paramètre est le paramètre de sortie, il doit correspondre à un champ alphanumérique où le résultat sera injecté, ici Date en lettres.

Paramétrage date to letters

Valider la création du plugin.
Nous souhaitons fournir en utilisation un accès à cette fonction depuis une fiche Sociétés. Pour cela, il est nécessaire de créer un processus qui exécutera ce plugin.
Pour ce faire, il faut se rendre sur la liste des Processus en paramétrage en déroulant le sous menu “Automatismes”, puis cliquer sur “Ajouter”.
Saisir un nom de processus, choisir “Fiche” dans le type et sélectionner la table, ici Sociétés.

Paramétrage processus

Cliquer sur “Suivant” puis cliquer sur le logo “plus” à droite de “Fonctions associées au processus”, sélectionner Restriction “Fiche”, puis “Plugin” dans le type et enfin choisir le plugin paramétré.

Paramétrage fonction processus

Valider la fonction processus et le processus et passer en mode utilisation.

Utilisation

Ouvrir une fiche Sociétés, ouvrir le mode éditeur en bas de l’écran et sélectionner un objet dans l’écran pour ajouter le processus. Cliquer sur le bouton “Ajouter”, choisir “Processus” dans Type, sélectionner le processus précédemment paramétré, ici dateToLetters et valider.

Mode éditeur

Un bouton processus apparaît alors et il suffit de cliquer dessus pour lancer son exécution :

Execution processus

Au clic sur le bouton processus, la fiche est sauvegardée puis le plugin est exécuté.
On peut observer le résultat dans le champ Date en lettres.

Fonction 2

Développement

La deuxième fonction prend plusieurs paramètres en entrée qui correspondent aux différents champs de YellowBox nécessaires aux calculs :

public Params getParams() {
		Params parameters = new Params();

		Param idElement = parameters.addNew(Type.STRING, InOut.IN, "idElement", PARAMVALIDELEMENT);
		idElement.setDescription("Id element sur lequel la fonction va s'appliquer");
		idElement.setMandatory(true);
		
		Param longueur = parameters.addNew(Type.IDFIELD, InOut.IN, "Champ longueur", PARAMIDFIELDLONGUEUR);
		longueur.setDescription("Référence du champ longueur d'un terrain");
		longueur.setMandatory(true);
		
		Param largeur = parameters.addNew(Type.IDFIELD, InOut.IN, "Champ largeur", PARAMIDFIELDLARGEUR);
		largeur.setDescription("Référence du champ largeur d'un terrain");
		largeur.setMandatory(true);
		
		Param prixMetreCarre = parameters.addNew(Type.IDFIELD, InOut.IN, "Champ prix m2", PARAMIDFIELDPRIXMETRECARRE);
		prixMetreCarre.setDescription("Référence du champ du prix au m2 d'un terrain");
		prixMetreCarre.setMandatory(true);
		
		Param prix = parameters.addNew(Type.IDFIELD, InOut.IN, "Champ prix du terrain", PARAMIDFIELDPRIX);
		prix.setDescription("Référence du champ du prix d'un terrain");
		prix.setMandatory(true);
		
		Param superficie = parameters.addNew(Type.IDFIELD, InOut.IN, "Champ superficie", PARAMIDFIELDSUPERFICIE);
		superficie.setDescription("Référence du champ superficie d'un terrain");
		superficie.setMandatory(true);

		return parameters;
	}

Nous passons des paramètres de type IDFIELD pour voir comment récupérer les valeurs de ces champs à partir de leur ID.

La fonction call() est expliquée en détail ci-dessous.
La première partie de la méthode permet de récupérer l’ensemble des paramètres définis :

// Récupération des paramètres
String idElement = getParamValues().getStringValue(PARAMVALIDELEMENT);
Record terrain = getYellowboxEntryPoint().getServices().getRecordService().getRecordFromId(idElement);
String idFieldLongueur = getParamValues().getStringValue(PARAMIDFIELDLONGUEUR);
String idFieldLargeur = getParamValues().getStringValue(PARAMIDFIELDLARGEUR);
String idFieldPrixMettreCarre = getParamValues().getStringValue(PARAMIDFIELDPRIXMETRECARRE);
String idFieldPrix = getParamValues().getStringValue(PARAMIDFIELDPRIX);
String idFieldSuperficie = getParamValues().getStringValue(PARAMIDFIELDSUPERFICIE);

On récupère un objet Record à partir de l’id de l’elément sur lequel on exécute le processus (id de la fiche Terrains ici) en utilisant le service Record getRecordFromId.

On va ensuite créé les objet Field que nous allons mettre à jour dans la fiche Terrains : le champ Superficie et le champ Prix :

// Création des Champs qui vont être mis à jour
Field fieldSuperficie = new Field();
fieldSuperficie.setId(idFieldSuperficie);
	
Field fieldPrix = new Field();
fieldPrix.setId(idFieldPrix);

On boucle ensuite sur l’ensemble des champs du Record pour récupérer les valeurs des champs longueur, largeur et prix en fonction de leur ID :

int longueur = 0;
int largeur = 0;
int prixMettreCarre = 0;
		
// On boucle sur l'ensemble des champs du record
// pour récupérer les valeurs en fonction des ID de champ
for(Value value : terrain.getValues()) {
	String fieldId = value.getField().getId();
	if(fieldId.equals(idFieldLongueur)) {
		longueur = Integer.valueOf(value.getValue());
	} else if(fieldId.equals(idFieldLargeur)) {
		largeur = Integer.valueOf(value.getValue());
	} else if(fieldId.equals(idFieldPrixMettreCarre)) {
		prixMettreCarre = Integer.valueOf(value.getValue());
	}
}

Une fois les valeurs des champs trouvées, on réalise les calculs de la superficie et du prix du terrain :

// calcul de la superficie et du prix du terrain
int superficie = longueur * largeur;
int prix = prixMettreCarre * superficie;

On créé ensuite les Value pour la superficie et le prix, on y associe les bons champs et les valeurs calculées et on les ajoute dans une liste de Value :

// Création des Value Superficie et Prix
Value valSuperficie = new Value();
valSuperficie.setField(fieldSuperficie);
valSuperficie.setValue(String.valueOf(superficie));
values.add(valSuperficie);

Value valPrix = new Value();
valPrix.setField(fieldPrix);
valPrix.setValue(String.valueOf(prix));
values.add(valPrix);

Il ne reste plus qu’à mettre la liste de Value dans le Record terrain, à crééer un objet ImportRecord à partir du record et à appeler le service createOrUpdateV2 pour mettre à jour le record :

// on set la liste de values au Record et on execute le service de mise à jour
terrain.setValues(values);

// Création de l'objet ImportRecord
ImportRecord irTerrain = new ImportRecord(terrain);
getYellowboxEntryPoint().getServices().getRecordService().createOrUpdateV2(irTerrain);

On retourne un objet ParamValues vide dans la méthode call() car il n’y a pas de paramètre de sortie.

// pas de paramètres de sortie, on retourne un objet vide
return new ParamValues();

Paramétrage

Nous avons préalablement créé une table Terrains avec 5 champs :

  • un champ entier longueur
  • un champ entier largeur
  • un champ entier superficie
  • un champ entier prix au m²
  • un champ entier prix total du terrain

Le paramétrage est similaire à la première fonction, il faut sélectionner les bons champs correspondants à nos différents paramètres.

Utilisation

Une fois le plugin paramétré et le processus contenant le plugin créé, il est possible d’exécuter ce processus en liste. Pour ce faire, il faut se rendre dans le menu de paramétrage puis dérouler le sous menu “Utilisateurs et accès” et cliquer sur “Accès”. Dans l’arbre de structure, déplier l’objet “Traitements” tout en bas puis “Processus” et cliquer sur le processus nouvellement créé. Changer la visibilité en liste pour “Toujours” puis valider.

Processus liste

Retourner sur la liste de la table Terrains, sélectionner des fiches pour exécuter le processus, cliquer sur le bouton avec le logo “éclair” puis cliquer sur le nom du processus et valider l’exécution. Cela va exécuter le processus sur l’ensemble des fiches selectionnés.

Processus liste exécution

Le résultat de l’exécution de la fonction sur une fiche est le suivant :

Fiche terrains

Fonction 3

Développement

La fonction propose 4 paramètres d’entrée : la saisie d’un login, d’un mot de passe, un paramètre déclencheur non visible et un paramètre Autre informationvisible selon la valeur du paramètre declencheur. La définition des paramètres dans la classe Fonction se fait comme suit :

public Params getParams() {
		Params parameters = new Params();
		Param param = new Param();
		param.setType(Type.STRING);
		param.setInOut(InOut.IN);
		param.setName("Login");
		param.setVarName("login"); 
		param.setDescription(""); 
		param.setMandatory(true);
		parameters.add(param);
		param = new Param();
		param.setType(Type.STRING);
		param.setInOut(InOut.IN);
		param.setName("Password");
		param.setVarName("password"); 
		param.setDescription(""); 
		param.setMandatory(true);
		parameters.add(param);
		param = new Param();
		param.setType(Type.STRING);
		param.setInOut(InOut.IN);
		param.setName("autre info");
		param.setVarName("other"); 
		param.setDescription(""); 
		param.setMandatory(true);
		parameters.add(param);
		param = new Param();
		param.setType(Type.STRING);
		param.setInOut(InOut.IN);
		param.setName("declencheur");
		param.setVarName("declencheur"); 
		param.setDescription(""); 
		param.setMandatory(true);
		parameters.add(param);
		return parameters;
	}

Nous ne nous intéressons pas ici à l’utilisation du plugin car cet exemple sert à comprendre comment implémenter sa propre interface de paramétrage, et les interactions possibles. Nous implémentons donc la méthode getParamInstanceUI() comme suit :

public UIRoot getParamInstanceUI() {
		UIRoot root = new UIRoot();
		UIPanel panel = new UIPanel("Connexion");
		panel.add(new UIParamDefault("login"));
		panel.add(new UIParamDefault("password"));
		UIRow line = new UIRow();
		UIHtml uiHtml = new UIHtml();
		uiHtml.setHtml("<a id=\"executeParamServlet\" href=\"javascript:callParamServlet('testparamservlet')\">Tester la connexion</a>");
		line.add(uiHtml);
		panel.add(line);
		UIParamDefault u = new UIParamDefault("other");
		String declencheurValue =null;
		try {
			declencheurValue = this.getParamValues().getStringValue("declencheur");
		}catch(Exception e) {
			declencheurValue = null;
		}
		if(declencheurValue != null && this.getParamValues().getStringValue("declencheur").equals("2") ) {
			uiHtml = new UIHtml();
			uiHtml.setHtml("Fonctionnement ok");
			panel.add(uiHtml);
			panel.add(u);
		}
		root.add(panel);
		
		return root;
	}

On crée un uiElement de type UIRoot root, racine de l’interface. Nous insérons dedans un élément UIPanel pour afficher un cadre. Nous utilisons la notion de UILine pour définir une ligne dans laquelle une première colonne contiendra une zone HTML UIHtml. Enfin, nous définissons, sous condition d’une valeur spécifique dans le paramètre declencheur, l’affichage d’une zone UIHtml et l’affichage du paramètre Autre information.

Pour finaliser notre affichage, nous implémentons une ParamServlet nommée TestParamServlet et recencée dans la méthode getParamServlets :

public List<Class<? extends ParamServlet>> getParamServlets() {
		 List<Class<? extends ParamServlet>> listParamServlet = new ArrayList<>();
		 listParamServlet.add(TestParamServlet.class);
		return listParamServlet;
}

Cette ParamServlet est implémentée comme suit :

public class TestParamServlet extends ParamServlet {

	@Override
	public ParamValues call(ParamValues paramValues) {
		System.out.println("login : "+paramValues.getStringValue("login"));
		for(ParamValue pvTemp :paramValues.getValues()) {
			if(pvTemp.getVarName().equals("declencheur")) {
				pvTemp.setString("2");
				break;
			}
		}
		
		return paramValues;
	}

	@Override
	public String getName() {
		return "testparamservlet";
	}

}

Elle présente une méthode getName() définissant le nom d’appel à réaliser à travers la méthode javascript callParamServlet (en exemple dans l’implémentation de la méthode getParamInstanceUI), une méthode call qui représente la traitement souhaité. Cette méthode call prend met à disposition un ParamValues reprenant tous les paramètres issus de l’interface avec leurs valeurs. Elle retourne aussi un ParamValues fournissant l’ensemble des paramvalues avec eventuiellement leurs valeurs modifiées (comme dans l’exemple ci-dessus où la valeur du paramètre declencheur a pris la valeur “2”). Si la valeur retournée est Null, les valeurs fournies en entrée seront conservées. Après chaque exécution d’un ParamServlet, la méthode getParamInstanceUI est réexécutée.

Paramétrage

Après avoir chargé le package dans YellowBox, il faut se rendre sur la liste des Plugins et cliquer sur “Ajouter” dans le bandeau haut.
Saisir un nom, sélectionner le plugin chargé, choisir la fonction Fonction dynamicParam dans “Instance de”.
Vous pouvez saisir des informations sur les deux paramètres visibles initialement (login et password). Paramétrage avant clic sur le lien Tester la connexion Un lien est disponible pour exécuter votre ParamServlet. En cliquant dessus, l’exécution du TestParamServlet a lieu et l’interface de paramétrage se recalcul présentant alors un paramètre complémentaire Autre information. Paramétrage après clic sur le lien Tester la connexion

Composant 1

Développement

Le composant prend un seul paramètre en entrée qui correspond à la ville où l’on souhaite obtenir la météo.
La définition des paramètres dans la classe Composant se fait comme suit :

public Params getParams() {
		Params parameters = new Params();
		
		// Paramètre ville de type String
		Param location = parameters.addNew(Type.STRING, InOut.IN, "Ville météo", PARAMLOCATION);
		location.setDescription("Ville pour obtenir la météo");
		location.setMandatory(true);
		
		return parameters;
	}

Il n’y a pas de paramètre de sortie dans un composant.

Pour ce composant, nous utilisons trois fichiers javascript :

  • la librairie JQuery via un CDN
  • la librairie openweather.js utilisant l’API OpenWeatherMap (Source)
  • un fichier weather.js rassemblant notre code javascript

La méthode getListJs() permettant de renseigner les fichiers javascript est implémentée comme suit :

public List<String> getListJs() {
		List<String> listJS = new ArrayList<>();
		listJS.add("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js");
		listJS.add("/js/openweather.min.js");
		listJS.add("/js/weather.js");
		return listJS;
	}

La méthode getListCss() permet de renseigner les fichiers css qui vont styliser le composant :

public List<String> getListCss() {
		List<String> listCss = new ArrayList<>();
		listCss.add("/css/weather.css");
		return listCss;
	}

L’affichage du composant est décrit dans le fichier weather.html :

<body lang="en">
	<div class="weather-wrapper hide">
		<img src="" class="weather-icon" alt="Weather Icon" />
		<p>
			<strong>Place</strong> <br />
			<span class="weather-place"></span>
		</p>
		<p>
			<strong>Temperature</strong> <br />
			<span class="weather-temperature"></span> 
			( <span class="weather-min-temperature"></span> - 
			<span class="weather-max-temperature"></span> )
		</p>
		<p>
			<strong>Description</strong> <br />
			<span class="weather-description capitalize"></span>
		</p>
		<p>
			<strong>Humidity</strong> <br />
			<span class="weather-humidity"></span>
		</p>
		<p>
			<strong>Wind speed</strong> <br />
			<span class="weather-wind-speed"></span>
		</p>
		<p>
			<strong>Sunrise</strong> <br />
			<span class="weather-sunrise"></span>
		</p>
		<p>
			<strong>Sunset</strong> <br />
			<span class="weather-sunset"></span>
		</p>
	</div>
</body>

<script>
	// Initialisation du widget uniquement lors du chargement de toute la page y compris les scripts
	$(window).on('load', function() {
		load_weather('${location}');
	});
</script>

Avec son style, décrit dans le fichier weather.css

body {
	font: 16px Arial, Helvetica, sans-serif;
	margin: 0;
}

.weather-wrapper {
	background: skyblue;
	margin: 5% 0 5% 5%;
	padding: 40px 5%;
	float: left;
	color: white;
	width: 70%;
	max-width: 400px;
}

strong {
	color: steelblue;
}

.capitalize {
	text-transform: capitalize;
}

.hide {
	display: none;
}

La variable ${location} correspond au paramètre d’entrée PARAMLOCATION et est passé au fichier html grâce à la méthode getHtml() :

public String getHtml() {
		// chargement du fichier HTML 
		String html = getRessourceAsString("/html/weather.html");
		
		// passage du paramètre location défini dans le fichier html
		html = html.replace("${location}", getParamValues().getStringValue(PARAMLOCATION));
		return html;
	}

La méthode load_weather est défini dans le fichier weather.js et appelle la méthode d’initialisation du Widget OpenWeather avec les bons paramètres :

load_weather = function(location) {
	$('.weather-temperature').openWeather({
		key: 'c9d49310f8023ee2617a7634de23c2aa',
		city: location,
		descriptionTarget: '.weather-description',
		windSpeedTarget: '.weather-wind-speed',
		minTemperatureTarget: '.weather-min-temperature',
		maxTemperatureTarget: '.weather-max-temperature',
		humidityTarget: '.weather-humidity',
		sunriseTarget: '.weather-sunrise',
		sunsetTarget: '.weather-sunset',
		placeTarget: '.weather-place',
		iconTarget: '.weather-icon',
		key: '895b596f10afb3a6edb1866f76edda15', // clé à renseigner https://openweathermap.org/
		success: function(data) {
			// show weather
			$('.weather-wrapper').show();
		},
		error: function(data) {
			console.log(data.error);
			$('.weather-wrapper').remove();
		}
	});
}

Paramétrage

Le paramétrage du composant est assez simple : on indique un nom, on sélectionne le plugin et on choisit l’instance Composant. Pour pouvoir insérer le composant sur un Accueil de YellowBox, on ne sélectionne pas de table.
On renseigne alors l’unique paramètre qui est la ville de la météo.

Paramétrage composant weather

Utilisation

Pour insérer le composant, nous allons nous rendre sur l’Accueil de YellowBox et créé un onglet “weather” dans le groupe d’onglets.
Ouvrir ensuite le mode éditeur et sélectionner ce nouvel onglet, puis cliquer sur “Ajouter”. Sélectionner “Composant” dans Type, puis le composant paramétré et valider.

Ajout du composant

Le composant s’afficher alors dans notre onglet. Il est possible de l’agrandir en largeur en le sélectionnant via le mode éditeur puis en cliquant sur “+” sur la ligne “Largeur”.

Modification largeur composant

Le résultat est le suivant :

Composant weather

Composant 2

Ce second composant utilise une servlet pour comprendre son utilisation.

Développement

Ce composant n’a pas de paramètres.
Il comprend deux fichiers javascript (JQuery et un propre), un fichier de style css et un fichier HTML.
Il faut déclarer l’utilisation de Servlet comme suit :

public List<Class<? extends Servlet>> getServlets() {
		List<Class<? extends Servlet>> listServlet = new ArrayList<>();
		listServlet.add(CompteurServlet.class);
		return listServlet;
	}

Le fichier HTML est structuré comme suit :

<button id="buttonplus" onclick="majNumber('incremente');" class="btn btn-yellow">
	+
</button>
<span id="text-compteur">
	${compteur}
</span>
<button id="buttonmoins" onclick="majNumber('decremente');" class="btn btn-yellow">
	-
</button>

Ainsi que le fichier CSS associé :

.btn {
	display: inline-block;
    font-weight: 400;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    border: 1px solid transparent;
    padding: .375rem .75rem;
    font-size: 1rem;
    line-height: 1.5;
    border-radius: .25rem;
    transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    width: 100px;
    font-weight: 600;
}

.btn:hover {
	text-decoration: none;
}

.btn-yellow {
	color: #212529;
	background-color: #ffc107;
    border-color: #ffc107;
}

Il comprend deux boutons : un + et un - permettant d’incrémenter ou de décrementer la valeur ${compteur}.
Cette valeur est passé au fichier HTML comme pour l’exemple du 1er composant dans la méthode getHtml() :

public String getHtml() {
		// chargement du fichier HTML 
		String html = getRessourceAsString("/html/compteur.html");

		// passage du paramètre compteur défini dans le fichier html
		html = html.replace("${compteur}", String.valueOf(compteur));
		return html;
	}

Cette variable compteur correspond à un attribut de la classe CompteurComponent, initialisé à 0.

private int compteur = 0;

La fonction javascript majNumber prend en paramètre le type d’opération (“incrémente” ou “décrémente”) et appelle la servlet CompteurServlet avec le paramètre nommé “typeOperation” qui effectuera le traitement côté serveur.

majNumber = function(type){
		callServlet('CompteurServlet',{'typeOperation' : type},function(data) {
			$('#text-compteur').html(data);
		});
	};

La servlet reçoit donc ce paramètre “typeOperation” et effectue le traitement suivant :

public String call(ParamValues paramsServlet) {
	// récupération du type d'opération
	String typeOperation = paramsServlet.getStringValue("typeOperation");
	CompteurComponent compteur = (CompteurComponent) getComponent();
	if(typeOperation.equals("incremente")) {
		return String.valueOf(compteur.getNext());
	} else if (typeOperation.equals("decremente")) {
		return String.valueOf(compteur.getPrevious());
	}
	// Si pas le bon type, on renvoie le compteur tel quel
	return String.valueOf(compteur.getCompteur());
}

Le traitement est assez simple : si on reçoit la valeur “incrémente”, on appelle la fonction getNext du Component (à implémenter) qui incrémente et renvoit la valeur de l’attribut compteur du Component. Sinon, si on reçoit “décrémente”, on appelle la fonction getPrevious (à implémenter) qui décrémente et renvoit la valuer de l’attribut compteur. Si le type ne correspond à aucune de ces deux valeurs, on renvoie la valeur du compteur telle quelle avec la méthode getCompteur du Component (à implémenter).

Paramétrage

Le paramétrage de ce composant est très simple puisqu’il n’y a aucun paramètre.

Utilisation

Comme pour le précédent composant, on peut le positionner sur un Accueil de YellowBox et le résultat est le suivant :

Composant compteur 0

Après 4 clics sur le bouton + :

Composant compteur 4

Si on quitte l’affichage de l’Accueil, qu’on se rend ailleurs dans YellowBox et qu’on retourne sur l’affichage du composant, la valeur du compteur est conservée.

Composant 3

Ce troisième composant utilise une servletFile pour comprendre son utilisation.

Développement

Le code HTML suivant affiche deux sélecteurs de fichiers acceptant des images, lorsque l’utilisateur sélectionne une image, on l’affiche en dessous du sélecteur. Une fois les deux images selectionnés, l’utilisateur peut cliquer sur le bouton ‘Upload’ et les deux images sont envoyés à la Servlet.
Dans le code de la Servlet, nous modifions la première image en noir et blanc et la seconde nous effectuons une rotation à 90 degrés. Nous renvoyons le base64 de ces deux images au client pour les afficher.

<div id="plugin">
    <div id="bw">
        <input id="input-image-bw" type="file" name="image" accept="image/*" />
        <div id="display-images-bw" style="display: none;">
            <img width="200px;" height="200px;" id="img-original-bw" alt="original img bw" title="original image to bw"/>
            <img width="200px;" height="200px;" id="img-bw" alt="bw img" title="bw image"/>
        </div>
    </div>
    <div id="rotate">
        <input id="input-image-rotate" type="file" name="image" accept="image/*" />
        <div id="display-images-rotate" style="display: none;">
            <img width="200px;" height="200px;" id="img-original-rotate" alt="original img rotate" title="original image to rotate"/>
            <img width="200px;" height="200px;" id="img-rotate" alt="rotate img" title="rotate image"/>
        </div>
    </div>
    <button id="btn-upload" onclick="uploadImage();">Upload</button>
</div>
document.addEventListener("DOMContentLoaded", function() {
        //bw = black and white
        var reader;
        var extensionbw;
        var imagebw;

        var extensionrotate;
        var imagerotate;

        let inputBw = document.getElementById("input-image-bw");
        let intputRotate = document.getElementById("input-image-rotate");

        inputBw.onchange = function(event) {
            imagebw = inputBw.files[0];
            extensionbw = imagebw.type;
            reader = new FileReader();
            reader.readAsDataURL(imagebw);
            reader.onloadend = () => {
                var base64 = reader.result;
                document.getElementById("img-original-bw").src = base64;
                document.getElementById("display-images-bw").style.display = 'block';
            }
        }

        intputRotate.onchange = function(event) {
            imagerotate = intputRotate.files[0];
            extensionrotate = imagerotate.type;
            reader = new FileReader();
            reader.readAsDataURL(imagerotate);
            reader.onloadend = () => {
                var base64 = reader.result;
                document.getElementById("img-original-rotate").src = base64;
                document.getElementById("display-images-rotate").style.display = 'block';
            }
        }

        uploadImage = function() {
            var formData = new FormData();
            formData.append('image-bw',imagebw);
            formData.append('type-bw',extensionbw);
            formData.append('image-rotate', imagerotate);
            formData.append('type-rotate',extensionrotate);

            callServletFile('ServletFichier', formData, function(data) {
                var json = JSON.parse(data);
                var base64bw = "data:image/" + extensionbw+";base64," + json.imgbw;
                document.getElementById("img-bw").src = base64bw;

                var base64rotate = "data:image/" + extensionrotate+";base64," + json.imgrotate;
                document.getElementById("img-rotate").src = base64rotate;
            },
			function(error) {
				console.log("Error component",error);
			});
        }
});

Les deux images sont récupérés directement des input type file via un objet FileReader.
La servlet que nous appelons est ServletFichier.

public class ServletFichier extends Servlet {

	@Override
	public String call(ParamValues params) {
		byte[] toBW = params.getFileValue("image-bw"); // récupération de la 1ere image
		byte[] toRotate = params.getFileValue("image-rotate"); // récupération de la 2nde image
		String extensionBw = params.getStringValue("type-bw"); // récupération du paramètre 'type-bw'
		String extensionRotate = params.getStringValue("type-rotate"); // récupération du paramètre 'type-rotate'

		extensionBw = extensionBw.substring(extensionBw.indexOf('/') +1, extensionBw.length());
		extensionRotate = extensionRotate.substring(extensionRotate.indexOf('/') +1, extensionRotate.length());

		ByteArrayInputStream baisBw = new ByteArrayInputStream(toBW);
		ByteArrayInputStream baisRotate = new ByteArrayInputStream(toRotate);
		BufferedImage imageBw = null;
		BufferedImage imageRotate = null;

		try {
			imageBw = ImageIO.read(baisBw);
			imageRotate = ImageIO.read(baisRotate);
		} catch(Exception e) {
			// handle exception
		}

		BufferedImage bw = bwImage(imageBw);
		BufferedImage rotated = rotateImage(imageRotate);
		ByteArrayOutputStream outBw = new ByteArrayOutputStream();
		ByteArrayOutputStream outRotate = new ByteArrayOutputStream();

		try {
			ImageIO.write(bw, extensionBw, outBw);
			ImageIO.write(rotated, extensionRotate, outRotate);
		} catch (IOException e) {
			// handle exception
		}

		JSONObject json = new JSONObject(); // création de l'objet json de retour
		json.put("imgbw", Base64.getEncoder().encodeToString(outBw.toByteArray()));
		json.put("imgrotate", Base64.getEncoder().encodeToString(outRotate.toByteArray()));
		return json.toString();
	}
    
    private BufferedImage bwImage(BufferedImage image) {
    	BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
        Graphics2D graphics = result.createGraphics();
        graphics.drawImage(image, 0,0,Color.WHITE,null);
        graphics.dispose();
        return result;
    }
    
    private BufferedImage rotateImage(BufferedImage image) {
    	double rads = Math.toRadians(90);
    	double sin = Math.abs(Math.sin(rads));
    	double cos = Math.abs(Math.cos(rads));
    	int width = (int) Math.floor(image.getWidth() * cos + image.getHeight() * sin);
    	int height = (int) Math.floor(image.getHeight() * cos + image.getWidth() * sin);
    	BufferedImage rotateImage = new BufferedImage(width, height, image.getType());
    	AffineTransform affineTransform = new AffineTransform();
    	affineTransform.translate(width / 2, height / 2);
    	affineTransform.rotate(rads,0,0);
    	affineTransform.translate(-image.getWidth() / 2, -image.getHeight() / 2);
    	AffineTransformOp affineTransformOp = new AffineTransformOp(affineTransform, AffineTransformOp.TYPE_BILINEAR);
    	affineTransformOp.filter(image,rotateImage);
    	return rotateImage;
    }

    @Override
    public String getName() {
        return "ServletFichier";
    }
}

La servlet récupère les deux images ainsi que les deux paramètres extensions, transforme les deux images et les retourne dans un objet JSON.

Composant 4

Ce composant montre l’utilisation de l’API Java présente dans le plugin.jar pour construire une liste YellowBox à partir d’une recherche de société.

Exemple YbList

Développement

Pour ce qui est de la structure du projet, voici l’arborescence :

  • src
    • com.yellowbox.yblist
      • components
        • YbListComponent.java
      • servlets
        • YbListCellServlet.java
        • YbListServerServlet.java
        • YbListServerSideBuilderServlet.java
        • YbListServlet.java
      • utils
        • YbListUtils.java
      • YbListPlugin.java
  • assets
    • index.html
    • index.js
    • build.properties
  • libs
    • commons-lang3-3.7.jar
    • yellowbox_plugin-3.28.0.jar
  • build.xml

Si vous ne disposez pas des fichiers build.properties et build.xml, en voici le contenu :

  • build.properties :
buildversion=1.1.0
  • build.xml :
<?xml version="1.0" encoding="ISO-8859-1"?>

<project name="Plugin Yellowbox" default="CreateJar" basedir="." xmlns:ivy="antlib:org.apache.ivy.ant">


	<property name="src.dir"  value="src" />
	<property name="build.dir" value="build" />
	<property name="classes.dir" value="${build.dir}/classes" />
	<property name="lib.dir" value="libs" />
	<property name="lib.external" value="external" />
	<property name="assets.dir" value="assets" />
	<property file = "${assets.dir}/build.properties"/>
	<property environment="env"/>
	<property name="extension.name" value="yellowbox_test_component-${buildversion}-${env.BUILD_NUMBER}" />
	
	<target name="init">
		<mkdir dir="${build.dir}/classes" />
	</target>
			
	<target name="build" depends="">
        <echo>Compilation des fichiers source</echo>
		<antversion property="antversion"/>
		<echo>Version ant : ${antversion}</echo>
		<javac 
			srcdir="${src.dir}" 
			destdir="${classes.dir}" 
			classpathref="classpath"
			excludes= "**test**,test*.java"
		    debug="on"
			encoding="UTF-8"/>
		
	</target>

	<path id="classpath">
		<pathelement location="${classes.dir}" />
		<fileset dir="${lib.dir}">
			<include name="**/*.jar" />
		</fileset>
		<fileset dir="${lib.external}">
			<include name="**/*.jar" />
		</fileset>
	</path>
	
	<target name="clean" description="Removes all the generated files or directories">
		<delete dir="${build.dir}" />
	</target>
	
	<target name="CreateJar" depends="clean,init,build">
		<!-- Copie des dépendances jar(s) -->
		<copy todir="${classes.dir}/libs" overwrite="false">
		  <fileset dir="${lib.dir}" />
		</copy>
		<!-- Copie des assets -->
		<copy todir="${classes.dir}/assets" overwrite="false">
		  <fileset dir="${assets.dir}" />
		</copy> 
		<jar destfile="${build.dir}/${extension.name}.jar"
	       basedir="${classes.dir}"
	       includes="**"
	  />
	</target>
	
	<property name="ivy.install.version" value="2.4.0" />
		<property name="ivy.jar.dir" value="lib" />
		<property name="ivy.jar.file" value="lib/ivy-${ivy.install.version}.jar" />
	    <target name="install-ivy" description="--> install ivy">
	    	<!-- try to load ivy here from local ivy dir, in case the user has not already dropped
	    	      it into ant's lib dir (note that the latter copy will always take precedence).
	    	      We will not fail as long as local lib dir exists (it may be empty) and
	    	      ivy is in at least one of ant's lib dir or the local lib dir. -->
	    	<path id="ivy.lib.path">
	    	    <fileset dir="${ivy.jar.dir}" includes="*.jar"/>
	    	</path>
	    	<taskdef resource="org/apache/ivy/ant/antlib.xml"
	    	          uri="antlib:org.apache.ivy.ant" classpathref="ivy.lib.path"/>
	    </target>
	    	

	    <target name="resolve" depends="install-ivy" description="retrieve dependencies with ivy">
	    	<ivy:retrieve type="jar" pattern="${lib.dir}/[artifact]-[revision].[ext]"/>
	    	<ivy:retrieve type="source" pattern="/src-lib/[artifact]-[revision].[ext]"/>
	    	<ivy:report />
	        <copydir src="locallib" dest="${lib.dir}"></copydir>
	    </target>
	
</project>

Frontend

HTML

Prenons le fichier HTML suivant :

<!DOCTYPE html>
<html lang="fr">

<head>
	<meta charset="utf-8">
	<title>Yb Objects</title>
	<style>
		.button_wrapper {
			display: flex;
			justify-content: space-around;
			align-items: center;
			flex-wrap: wrap;
		}

		.button_wrapper button {
			min-width: 100px;
			max-width: 200px;
		}
	</style>
</head>

<body>
	<main>
		<div class="button_wrapper">
			<button onclick="fireSearch(0);">Dessiner depuis <strong>drawList</strong></button>
			<button onclick="fireSearch(1);">Dessiner depuis <strong>drawListFromServlet</strong></button>
			<button onclick="fireSearch(2);">Dessiner en <strong>gestion serveur</strong></button>
		</div>

		<div id="tab_wrapper">
			<table id="result"></table>
		</div>
	</main>
	
	<script>
	</script>
</body>

</html>

Ce fichier contient grossièrement :

  • 3 boutons pour lancer le dessin de la liste :
    • Un qui lancera la fonction drawList en passant 0 à la méthode fireSearch.
    • Un autre qui lancera drawListFromServlet en passant 1.
    • Le dernier qui dessinera une liste server-side en passant 2.
  • un tableau pour accueillir la liste.
CSS

On va y ajouter un peu de style dans une balise <style> dans la balise <head> :

.button_wrapper {
	display: flex;
	justify-content: space-around;
	align-items: center;
	flex-wrap: wrap;
}

.button_wrapper button {
	min-width: 100px;
	max-width: 200px;
}
JavaScript

Maintenant, on s’interesse au JavaScript dans le fichier index.js. On va d’abord définir une fonction à appeler lorsque les boutons sont cliqués :

function fireSearch(type) {
    switch(type) {
        case 1:
            buildWithServlet();
            break;

        case 2:
            buildServerSide();
            break;

        default:
            buildBasic();
    }
}

Avec, en dessous, les différentes fonctions de construction :

function buildWithServlet() {
    removeInputSearch();
    drawListFromServlet(
		"result",
		"YbListServlet",
		{ "responsive": true, "pageLength": 7 },
		() => { console.log('ok'); },
		onErrorServlet);
}

function buildServerSide() {
    buildInputSearch();
	callServlet(
		'YbListServerSideBuilderServlet',
		{ "responsive": true, "pageLength": 10 }, callBackServletBasic,
		onErrorServlet);
}

function buildBasic() {
    removeInputSearch();
    callServlet(
		"YbListServlet",
		{ "responsive": false, "pageLength": 5 }, callBackServletBasic,
		onErrorServlet);
}

Vous remarquerez que les 2 fonctions, buildBasic() et buildWithServlet(), sont assez similaires puisque l’on va toujours appeler YbListServlet. Cela est voulu pour faciliter le développement.

Dans le cas buildBasic(), on va appeler nous-même la servlet YbListServlet avec callServlet() et utiliser drawList() lorsque la servlet nous aura renvoyé son resultat. Pour rappel, voici la signature de la méthode callServlet() :

/**
 * @param servletName {string} - Servlet name
 * @param parameters {object} - JSON of parameters
 * @param onSuccess {function} - Function to call back on success. Servlet returns a string on success.
 * @param onError {function} - Function to call back on error. Servlet returns 3 parameters : jqXHR, textStatus, errorThrown.
 */
function callServlet(servletName, parameters, onSuccess, onError)

Pour cela, on définit la méthode callBackServlet() :

function callBackServletBasic(res) {
	const listData = JSON.parse(res);
	if(res?.error) {
		displayError(res.error);
	}
	
	try {
		drawList("result", listData);
	} catch (error) {
		displayError(error);
	}
}

Cette méthode va tout d’abord parser le resultat, qui est une String, renvoyé par la servlet en JSON. Ensuite, on va vérifier que ce JSON n’a pas de propriété error (on la définira nous même en Java plus tard) ; auquel cas, on affichera l’erreur grâce à la méthode displayError(). Finalement, on appelera drawList pour dessiner la fiche dans la <table id='result'> prévu à cet effet. On prendra soin d’entourer la méthode d’un try catch en cas d’erreur.

Voici les méthode qui gère les erreurs :

function onErrorServlet(jqXHR, textStatus, errorThrown) {
	displayError(`Status ${jqXHR && jqXHR.status} - ${textStatus} : ${errorThrown}`);
}

function displayError(err) {
	if(err) {
		if(err.message) {
			$('#result').html("Une erreur est survenue : " + err.message);
		} else {
			$('#result').html("Une erreur est survenue : " + err);
		}
	} else {
		$('#result').html("Une erreur est survenue");
	}
}

Dans le cas buildWithServlet(), on appelera la méthode drawListFromServlet(). Encore une fois, même s’il s’agit de la même servlet qui est appelée pour construire les données de la liste, on veut juste pouvoir montrer le fonctionnement ici. On fait 1 arrow function pour gérer le onSuccess avec un simple affichage en console. La méthode drawListFromServlet() appelera elle-même la méthode drawList() au succès. Pour rappel, voici le code :

drawListFromServlet(
	"result",
	"YbListServlet",
	{ "responsive": true, "pageLength": 7 },
	() => { console.log('ok'); },
	onErrorServlet
);

Dans le cas buildServerSide(), on fait relativement la même chose que dans buildBasic(). Vous noterez la présense de buildInputSearch() et removeInputSearch(). Je ne les mentionne que maintenant car elles serviront à ajouter ou retirer une input pour faire de la recherche dans notre YbList ser-side. Voici leur code :


function getInputSearch() {
    return document.getElementById('search');
}

function buildInputSearch() {
    if(getInputSearch() != null) {
        return;
    }

    const searchInput = document.createElement("input");
    searchInput.id = "search";
    searchInput.placeholder = "Raison sociale";
    searchInput.type = "text";
    searchInput.addEventListener('keydown', (e) => {
        if (e.key === "Enter") {
            callServlet('YbListServerSideBuilderServlet', { "responsive": true, "pageLength": 10 }, callBackServletBasic, onErrorServlet);
        }
    });

    document.getElementById('tab_wrapper').prepend(searchInput);
}

function removeInputSearch() {
    const input = getInputSearch();
    if(input == null) {
        return;
    }
    input.remove();
}
Bilan

Au final, vous devriez avoir le fichier HTML suivant :

<!DOCTYPE html>
<html lang="fr">

<head>
	<meta charset="utf-8">
	<title>Yb Objects</title>
	<style>
		.button_wrapper {
			display: flex;
			justify-content: space-around;
			align-items: center;
			flex-wrap: wrap;
		}

		.button_wrapper button {
			min-width: 100px;
			max-width: 200px;
		}
	</style>
</head>

<body>
	<main>
		<div class="button_wrapper">
			<button onclick="fireSearch(0);">Dessiner depuis <strong>drawList</strong></button>
			<button onclick="fireSearch(1);">Dessiner depuis <strong>drawListFromServlet</strong></button>
			<button onclick="fireSearch(2);">Dessiner en <strong>gestion serveur</strong></button>
		</div>

		<div id="tab_wrapper">
			<table id="result"></table>
		</div>
	</main>
	
	<script>
	</script>
</body>

</html>

Ainsi que le fichier Javascript suivant :

function fireSearch(type) {
    switch(type) {
        case 1:
            buildWithServlet();
            break;

        case 2:
            buildServerSide();
            break;

        default:
            buildBasic();
    }
}

function buildServerSide() {
    buildInputSearch();
	callServlet('YbListServerSideBuilderServlet', { "responsive": true, "pageLength": 10 }, callBackServletBasic, onErrorServlet);
}

function getInputSearch() {
    return document.getElementById('search');
}

function buildInputSearch() {
    if(getInputSearch() != null) {
        return;
    }

    const searchInput = document.createElement("input");
    searchInput.id = "search";
    searchInput.placeholder = "Raison sociale";
    searchInput.type = "text";
    searchInput.addEventListener('keydown', (e) => {
        if (e.key === "Enter") {
            callServlet('YbListServerSideBuilderServlet', { "responsive": true, "pageLength": 10 }, callBackServletBasic, onErrorServlet);
        }
    });

    document.getElementById('tab_wrapper').prepend(searchInput);
}

function removeInputSearch() {
    const input = getInputSearch();
    if(input == null) {
        return;
    }
    input.remove();
}

function buildWithServlet() {
    removeInputSearch();
    drawListFromServlet("result", "YbListServlet", { "responsive": true, "pageLength": 7 }, () => { console.log('ok'); }, onErrorServlet);
}

function buildBasic() {
    removeInputSearch();
    callServlet("YbListServlet", { "responsive": false, "pageLength": 5 }, callBackServletBasic, onErrorServlet);
}

function callBackServletBasic(res) {
    const listData = JSON.parse(res);
    if(res?.error) {
        displayError(res.error);
    }
    
    try {
        drawList("result", listData);
    } catch (error) {
        displayError(error);
    }
}

function onErrorServlet(jqXHR, textStatus, errorThrown) {
    displayError(`Status ${jqXHR && jqXHR.status} - ${textStatus} : ${errorThrown}`);
}

function displayError(err) {
    if(err) {
        if(err.message) {
            $('#result').html("Une erreur est survenue : " + err.message);
        } else {
            $('#result').html("Une erreur est survenue : " + err);
        }
    } else {
        $('#result').html("Une erreur est survenue");
    }
}

function searchValueCallback() {
    return $('#search').val();
}

Backend

Côté Java, on va définir 1 composant, 4 servlets et 1 classe utilitaire. Nous allons volontairement omettre la définition du Header qui ne nous intéresse pas ici (voir autres exemples).

  • Le composant, que l’on nommera YbListComponent.java, se chargera d’appeler le fichier HTML et de récupérer le paramètre de notre plugin (qui sera l’ID d’une recherche de sociétés).
  • La 1^ère^ servlet, que l’on nomera YbListServlet.java, se chargera de récupérer le résultat de la recherche et de construire la liste.
  • La 2^ème^ servlet, que l’on nomera YbListCellServlet.java, sera appelée dans le onClick d’une cellule de type LINK de notre liste afin de démontrer le fonctionnement du onClick avec une servlet.
  • La 3^ème^ servlet, que l’on nomera YbListServerSideBuilderServlet.java, sera appelée pour construire notre YbList server-side. Attention : elle hérite de com.yellowbox.plugin.v3.YbListBuilderServlet.
  • La 4^ème^ servlet, que l’on nomera YbListServerServlet.java, sera appelée pour gérer le contenu de notre YbList server-side. Attention : elle hérite de com.yellowbox.plugin.v3.YbListUpdateServlet.
  • La classe utilitaire, YbListUtils, permet de factoriser le code final. Elle gère la génération des colonnes et des données.

Créez ces fichiers nous allons les remplir dans la suite de cet exemple. Prenez soin de bien faire hériter les classes associées à chaque type de classe (i.e. com.yellowbox.plugin.v3.Component , com.yellowbox.plugin.v3.Servlet, …).

YbListComponent

On commence avec le composant. Nous ne nous attarderons pas dessus car celui-ci est assez simple :

package com.yellowbox.plugin.yblist.components;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.yellowbox.plugin.v3.Component;
import com.yellowbox.plugin.v3.Param;
import com.yellowbox.plugin.v3.ParamServlet;
import com.yellowbox.plugin.v3.Params;
import com.yellowbox.plugin.v3.Servlet;
import com.yellowbox.plugin.v3.Param.InOut;
import com.yellowbox.plugin.v3.Param.Type;
import com.yellowbox.plugin.v3.ui.UIRoot;
import com.yellowbox.plugin.yblist.servlets.YbListCellServlet;
import com.yellowbox.plugin.yblist.servlets.YbListServerServlet;
import com.yellowbox.plugin.yblist.servlets.YbListServerSideBuilderServlet;
import com.yellowbox.plugin.yblist.servlets.YbListServlet;

public class YbListComponent extends Component {

	public static final String IDRECHERCHE = "IDRECHERCHE";

	@Override
	public String getHtml() {
		return getRessourceAsString("/yblist/index.html");
	}

	@Override
	public List<Class<? extends Servlet>> getServlets() {
		List<Class<? extends Servlet>> listServlet = new ArrayList<>();
		listServlet.add(YbListServlet.class);
		listServlet.add(YbListCellServlet.class);
		listServlet.add(YbListServerSideBuilderServlet.class);
		listServlet.add(YbListServerServlet.class);
		return listServlet;
	}

	@Override
	public List<String> getAssets() {
		return Collections.emptyList();
	}

	@Override
	public List<String> getListCss() {
		return Collections.emptyList();
	}

	@Override
	public List<String> getListJs() {
		List<String> listJs = new ArrayList<>();
		listJs.add("/yblist/index.js");
		return listJs;
	}

	@Override
	public String getDescription() {
		return "Exemple d'utilisation de la YbList";
	}

	@Override
	public String getName() {
		return "YbListComponent";
	}

	@Override
	public UIRoot getParamInstanceUI() {
		return null;
	}

	@Override
	public List<Class<? extends ParamServlet>> getParamServlets() throws AbstractMethodError {
		return Collections.emptyList();
	}

	@Override
	public Params getParams() {
		Params parameters = new Params();

		Param paramTemp = parameters.addNew(Type.IDRECHERCHE, InOut.IN, "Choisir une recherche sur les societes", IDRECHERCHE);
		paramTemp.setMandatory(true);
		
		return parameters;
	}

	@Override
	public String getTitle() {
		return getName();
	}

}

Prenez soin de bien vérifier le chemin vers le fichier HTML si jamais votre arborescence est différente de celle de l’exemple.

YbListUtils

La classe utilitaire, le coeur du métier. Voici le code de base :

public class YbListUtils {

    public static final String ID_FIELD_RS = "107";
    public static final String ID_FIELD_ACT = "261";
    private YellowboxEntryPoint entryPoint;

    public YbListUtils(YellowboxEntryPoint entryPoint) {
        this.entryPoint = entryPoint;
    }

}

Nous y mettrons les IDs des champs de société qui nous interesse, ici, la raison sociale et l’activité. On mettra l’entry point en attribut pour avoir accès aux services de YellowBox.

Définissons d’abord une méthode qui va construire nos colonnes. Nous allons créer 3 colonnes :

  • La 1^ère^ de type BUTTON, qui s’appelera “Lien”. Son ID sera simplement “0”.
  • La 2^ème^ de type LINK, qui aura le nom du champ raison sociale. Son ID sera “raison_sociale”.
  • La 3^ème^ de type RAW, qui aura le nom du champ activité. Son ID sera “activite”.
public List<YbColumn> buildColumns() {
	List<YbColumn> columns = new ArrayList<>();
	columns.add(new YbColumn("0", Type.BUTTON, "Lien"));

	Field field;
	String id;
	Type type;
	for(String idChamp : Arrays.asList(ID_FIELD_RS, ID_FIELD_ACT)) {
		field = entryPoint.getServices().getFieldService().getFieldById(idChamp);

		id = ID_FIELD_RS.equals(idChamp) ? "raison_sociale" : "activite";
		type = field.getId().equals(ID_FIELD_RS) ? Type.LINK : Type.RAW;

		columns.add(new YbColumn(id, type, field.getName()));
	}

	return columns;
}

Ceci étant fait, on va créer une méthode qui va venir récupérer le résultat de la recherche avec les champs voulus :

public Page<Record> getRecordsFromSearch(String searchId) {
	List<Field> fields = new ArrayList<>();
	fields.add(new Field(ID_FIELD_RS, "", ""));
	fields.add(new Field(ID_FIELD_ACT, "", ""));

	QueryPage query = new QueryPage();
	query.setIdSearch(searchId);
	query.setFields(fields);

	return entryPoint.getServices().getRecordServiceV3().getRecordsBySearch(query);
}

On poursuit avec la construction des données. On définit encore une fois une méthode qui s’en chargera :

public List<Map<String, YbCell>> buildData(List<Record> records) {
	List<Map<String, YbCell>> data = new ArrayList<>();
	Map<String, YbCell> row;
	YbCell cell;

	for (Record societe : records) {
		row = new HashMap<>();

		cell = new YbButtonCell("() => { window.open('https://www.google.com', '_blank').focus(); }", "https://upload.wikimedia.org/wikipedia/commons/6/6d/Windows_Settings_app_icon.png");
		row.put("0", cell);

		for(Value val: societe.getValues()) {
			if(!val.getField().getId().equals(ID_FIELD_ACT) && !val.getField().getId().equals(ID_FIELD_RS)) {
				continue;
			}
			
			if(val.getField().getId().equals(ID_FIELD_ACT)) {
				cell = new YbRawCell(val.getValue());
				row.put("activite", cell);
			} else if(val.getField().getId().equals(ID_FIELD_RS)) {
				cell = new YbLinkCell(val.getValue(), "() => { callServlet('YbListCellServlet', { 'param1': 'val1', 'param2': 'val2' }, (res) => { alert(res); }, (err) => { console.error(err); }); }");
				row.put("raison_sociale", cell);
			}
		}
			
		data.add(row);
	}

	return data;
}

On fournit en paramètre de la méthode le résultat de la recherche. Le but va être de boucler sur cette liste de Record et extraire les données qui nous intéresse pour construire nos cellules.

Ainsi, dans notre boucle, on instancie une nouvelle ligne en premier lieu. Ensuite, on définit la première cellule, qui est supposé être un bouton. On lui passe 2 paramètres :

  • le onClick, sous forme d’arrow function au format String. On ouvrira simplement un nouvel onglet dans le navigateur de l’utilisateur qui redirige vers Google.
  • le imgUrl, qui est simplement l’URL d’une image de molette.

On ajoute notre cellule bouton à la ligne en précisant l’ID de la colonne à laquelle elle appartient (ici, “0”).

Pour la suite, on boucle sur les valeurs contenu dans le Record. On s’assure qu’il s’agit soit du champ raison sociale ou activité.

  • Si c’est le champ activité, on définit une cellule de type RAW et son label sera la valeur associée au champ activité.
  • Si c’est le champ raison sociale, on définit une cellule de type LINK. Son label sera la valeur associée au champ raison sociale et son onClickappelera notre fameuse servlet YbListCellServlet avec une série de paramètres très simples. On fait un simple alert() du résultat pour le onSuccess du callServlet() et un simple console.error() pour le onError.

Enfin, on pense à ajouter chaque cellule à la ligne avec le bon ID de colonne.

Finalement, on ajoute chaque ligne à data et on retourne le resultat.

Voici le fichier obtenu :

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.yellowbox.plugin.v3.YellowboxEntryPoint;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbButtonCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbLinkCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbRawCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcolumn.YbColumn;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcolumn.YbColumn.Type;
import com.yellowbox.ws.beans.Field;
import com.yellowbox.ws.beans.Page;
import com.yellowbox.ws.beans.QueryPage;
import com.yellowbox.ws.beans.Record;
import com.yellowbox.ws.beans.Value;

public class YbListUtils {

    public static final String ID_FIELD_RS = "107";
    public static final String ID_FIELD_ACT = "261";
    private YellowboxEntryPoint entryPoint;

    public YbListUtils(YellowboxEntryPoint entryPoint) {
        this.entryPoint = entryPoint;
    }

    public List<YbColumn> buildColumns() {
        List<YbColumn> columns = new ArrayList<>();
        columns.add(new YbColumn("0", Type.BUTTON, "Lien"));

        Field field;
        String id;
        Type type;
        for(String idChamp : Arrays.asList(ID_FIELD_RS, ID_FIELD_ACT)) {
        	field = entryPoint.getServices().getFieldService().getFieldById(idChamp);

            id = ID_FIELD_RS.equals(idChamp) ? "raison_sociale" : "activite";
            type = field.getId().equals(ID_FIELD_RS) ? Type.LINK : Type.RAW;

            columns.add(new YbColumn(id, type, field.getName()));
        }

        return columns;
    }

    public List<Map<String, YbCell>> buildData(List<Record> records) {
        List<Map<String, YbCell>> data = new ArrayList<>();
        Map<String, YbCell> row;
        YbCell cell;

        for (Record societe : records) {
        	row = new HashMap<>();

            cell = new YbButtonCell("() => { window.open('https://www.google.com', '_blank').focus(); }", "https://upload.wikimedia.org/wikipedia/commons/6/6d/Windows_Settings_app_icon.png");
            row.put("0", cell);

            for(Value val: societe.getValues()) {
                if(!val.getField().getId().equals(ID_FIELD_ACT) && !val.getField().getId().equals(ID_FIELD_RS)) {
                    continue;
                }
                
                if(val.getField().getId().equals(ID_FIELD_ACT)) {
                    cell = new YbRawCell(val.getValue());
                    row.put("activite", cell);
                } else if(val.getField().getId().equals(ID_FIELD_RS)) {
                    cell = new YbLinkCell(val.getValue(), "() => { callServlet('YbListCellServlet', { 'param1': 'val1', 'param2': 'val2' }, (res) => { alert(res); }, (err) => { console.error(err); }); }");
                    row.put("raison_sociale", cell);
                }
            }
            	
            data.add(row);
        }

        return data;
    }
    
    public Page<Record> getRecordsFromSearch(String searchId) {
        List<Field> fields = new ArrayList<>();
        fields.add(new Field(ID_FIELD_RS, "", ""));
        fields.add(new Field(ID_FIELD_ACT, "", ""));

        QueryPage query = new QueryPage();
        query.setIdSearch(searchId);
        query.setFields(fields);

        return entryPoint.getServices().getRecordServiceV3().getRecordsBySearch(query);
    }

}
YbListCellServlet

Voyons maintenant la servlet la plus simple, qui, pour rappel, servira à être appelée lors du clic sur une cellule de type LINK :

import java.util.stream.Collectors;

import com.yellowbox.plugin.v3.ParamValues;
import com.yellowbox.plugin.v3.Servlet;

public class YbListCellServlet extends Servlet {

    @Override
    public String call(ParamValues arg0) {
        return "Appel de la servlet '" + getName() + "' avec les arguments : " + arg0.getValues().stream().map(paramVal -> paramVal.getVarName() + " = " + paramVal.getString()).collect(Collectors.toList());
    }

    @Override
    public String getName() {
        return "YbListCellServlet";
    }
}

Ici encore, rien de très compliqué. On se charge juste de renvoyer du texte contenant le nom de la servlet avec ses arguments passés en paramètres.

YbListServlet

La servlet de construction de la liste :

import java.util.List;
import java.util.Map;

import com.yellowbox.plugin.v3.ParamValues;
import com.yellowbox.plugin.v3.Servlet;
import com.yellowbox.plugin.v3.ybobjects.yblist.YbList;
import com.yellowbox.plugin.v3.ybobjects.yblist.YbListException;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcolumn.YbColumn;
import com.yellowbox.plugin.yblist.components.YbListComponent;
import com.yellowbox.plugin.yblist.utils.YbListUtils;
import com.yellowbox.ws.beans.Page;
import com.yellowbox.ws.beans.Record;

public class YbListServlet extends Servlet {

    @Override
    public String call(ParamValues params) {
        YbListUtils utils = new YbListUtils(getYellowboxEntryPoint());

        String searchId = getComponent().getParamValues().getStringValue(YbListComponent.IDRECHERCHE);
        Page<Record> records = utils.getRecordsFromSearch(searchId);

        List<YbColumn> columns = utils.buildColumns();
        List<Map<String, YbCell>> data = utils.buildData(records.getContent());

        YbList ybList = new YbList(columns, data);
        ybList.setResponsive(Boolean.parseBoolean(params.getStringValue("responsive")));
        ybList.setPageLength(Integer.parseInt(params.getStringValue("pageLength")));

        try {
            return ybList.toJson();
        } catch (YbListException e) {
            return "{ 'error': '" + e.getMessage() + "' }";
        }
    }

    @Override
    public String getName() {
        return "YbListServlet";
    }

}

On commence par appeler la méthode utilitaire getRecordsFromSearch() pour récupérer les données de la recherche. Ensuite, on définit les colonnes avec la méthode utilitaire buildColumns() et on fait de même avec les données grâce à buildData(). On instancie une YbList et on appelle la méthode toJson().

Attention à bien entourer la méthode toJson() d’un try catch en cas d’erreur.

YbListServerSideBuilderServlet

Attardons nous maintenans sur la servlet de construction de la YbList server-side :

import java.util.List;

import com.yellowbox.plugin.v3.ParamValues;
import com.yellowbox.plugin.v3.ybobjects.yblist.YbList;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybcolumn.YbColumn;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybserverdata.servlets.YbListBuilderServlet;
import com.yellowbox.plugin.yblist.utils.YbListUtils;

public class YbListServerSideBuilderServlet extends YbListBuilderServlet {

    @Override
    public YbList build(ParamValues params) {
        YbListUtils utils = new YbListUtils(getYellowboxEntryPoint());
        
        List<YbColumn> columns = utils.buildColumns();

		YbList ybList = new YbList(columns, YbListServerServlet.class.getSimpleName());
        ybList.setResponsive(Boolean.parseBoolean(params.getStringValue("responsive")));
        ybList.setPageLength(Integer.parseInt(params.getStringValue("pageLength")));
        ybList.setServerSide(true);
        ybList.setSearchValueJsCallbackName("searchValueCallback");

        return ybList;
    }

    @Override
    public String getName() {
        return getClass().getSimpleName();
    }

}

Le code est assez similaire à celui de la Servlet précédente en ce qui concerne la construction de la liste, hormis que l’on ne va pas fournir de données à notre instance de YbList. En effet, on va lui fournir un nom de YbListUpdateServlet cette fois-ci (décrite à la section suivante). On précise que la liste sera server-side et on lui précise qu’il y aura une méthode Javascript qui lui fournira une valeur à chercher quand la liste se mettra à jour.

Vous remarquerez que cette Servlet retourne un YbList et pas un String.

YbListServerServlet

Partie importante, la gestion des données côté serveur de la liste :

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.yellowbox.plugin.v3.ybobjects.yblist.ybcell.YbCell;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybserverdata.YbListServerData;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybserverdata.servlets.YbListUpdateServlet;
import com.yellowbox.plugin.v3.ybobjects.yblist.ybserverdata.servlets.params.YbListUpdateServletParams;
import com.yellowbox.plugin.yblist.components.YbListComponent;
import com.yellowbox.plugin.yblist.utils.YbListUtils;
import com.yellowbox.ws.beans.Page;
import com.yellowbox.ws.beans.Record;
import com.yellowbox.ws.beans.Value;

public class YbListServerServlet extends YbListUpdateServlet {

    @Override
    public YbListServerData update(YbListUpdateServletParams params) {
        YbListUtils utils = new YbListUtils(getYellowboxEntryPoint());
        String searchId = getComponent().getParamValues().getStringValue(YbListComponent.IDRECHERCHE);
        Page<Record> recordPage = utils.getRecordsFromSearch(searchId);

		int start = params.getStart();
		int length = params.getLength();
        String search = params.getSearch().getValue();

        List<Record> records = recordPage.getContent().stream()
            .filter(r -> r.getValues().stream()
                .filter(v -> v.getField().getId().equals(YbListUtils.ID_FIELD_RS))
                .findAny()
                .orElse(new Value())
            .getValue().toLowerCase().contains(search.toLowerCase()))
            .collect(Collectors.toList());

        List<Record> subList = records.subList(start, (start + length > records.size() ? ((int) records.size()) : start + length));
        List<Map<String, YbCell>> data = utils.buildData(subList);
        
		int draw = params.getDraw();
        return new YbListServerData(data, draw, recordPage.getContent().size(), records.size());
    }

    @Override
    public String getName() {
        return getClass().getSimpleName();
    }
}

L’idée ici est de renvoyer les données demandées par la liste. Les paramètres de cette demande sont présentés dans l’objet YbListUpdateServletParams passé en paramètre de la méthode update(). Le but est que nous renvoyons un YbListServerData qui contient les élément de la page demandée :

  • les données, sous la forme classique d’une List<Map<String, YbCell>>
  • le draw, élément obligatoire pour vérifier l’authenticité des données (voir ici)
  • le nombre total de données : en tout, pas que dans la page demandée
  • le nombre de données filtrées : dans la même idée que la précédente, le nombre total d’élément après application du filtre

Dans un premier temps, on récupère les 3 informations suivantes :

  • start : à quelle ligne commence la demande
  • length : le nombre de ligne par page
  • search : la valeur à rechercher

Avec ces 3 informations, on va gérer l’application du filtre sur le total des données récupérées de la recherche grâce à l’élément search. Ainsi, on obtient une première liste filtrée. De cette liste, on va en tirer une sous-liste qui elle va représenter les données à afficher dans la page demandée grâce aux éléments start et length. On convertit cette sous-liste grâce à notre méthode utilitaire buildData(). Finalement, on contruit notre YbListServerData et on le retourne.

Notre backend est terminé ! Vous n’avez plus qu’à compiler le plugin en lançant le build.xml.

Paramétrage

Une fois le plugin installé sur votre YellowBox, la paramétrage est assez simple.

Les noms de plugin et de package sont différents mais il s’agit du même plugin que nous avons construit

Paramétrage YbList

Utilisation

Affichons le plugin dans l’accueil depuis le mode éditeur dans le 1^er^ onglet. Pensez à rdimensionner votre plugin pour un meilleur affichage.

Cliquez sur l’un des bouton pour afficher la liste. Voyez que les 2 affiche la même liste mais n’utilise pas la même méthode JavaScript.

Affichage YbList

Code source

Le code source est disponible ici sous la forme d’une archive, importable dans l’IDE Eclipse.