Soliant Consulting

Membres
  • Compteur de contenus

    82
  • Inscription

  • Dernière visite

    jamais

À propos de Soliant Consulting

  • Rang
    50

Profil général

  • Genre
    Non précisé

Profil FileMaker

  • Certification
    FileMaker 15 Certified Developer
  1. A great skill a Salesforce administrator can have in his or her back pocket is basic knowledge of the Apex programming language. This awareness empowers administrators to better understand the behavior of an application. As a result, administrators can explain underlying logic to users as well as diagnose issues without having to go to a Salesforce developer for help. To help Salesforce administrators explore learning Apex, I recommend writing a Hello World program. In this post, I will walk you through it. In the process, you will learn a few simple but useful programming concepts: variables, primitives, conditionals and methods. Using the Developer Console Before we can start coding our Hello World program, we need to learn how to run it. There are several ways to execute Apex in the Salesforce platform. The simplest one is using the Developer Console to execute code in an anonymous block. Let’s start by opening the console. Make sure your profile has the "Author Apex" permission enabled. Click on the gear icon, then select Developer Console. If you are in classic mode, click on your name on the top right, then select Developer Console. Once the Developer Console loads, click debug on the menu and select Open Execute Anonymous Window. You can also use the shortcut CTRL+E. Figure 1 - Execute Anonymous Window Type the following line of code: System.debug('Hello world!'); Check the Open Log option. Click Execute. Once the Execution Log loads, check the Debug Only checkbox. You should see your Hello world message. Congratulations, you just wrote your first Apex program! Figure 2 - Execution Log Variables For our next step, let’s try something a little more interesting. We will use a variable and assign "Hello World!" to it. Then, we will output the value of that variable to the debug log. Open the Execute Anonymous Window from the debug menu or using the shortcut, CTRL+E. Modify your code to look like this: String message = 'Hello world!'; System.debug(message); Click Execute. Once the Execution Log loads, check the Debug Only checkbox. Notice the output is the same as the previous time we ran the code. Line 1 has two purposes — one is declaring the variable, which tells Apex we want to create a new variable of type String. The second purpose is to set its value to "Hello world!". An interesting property of variables is that they are mutable. That means we can modify their content after they are created. Let’s try it. Update the code again and add one more line. String message = 'Hello world!'; message = 'Hi there!'; System.debug(message); Run the code again following the same steps as before. This time you will see "Hi there!", but "Hello World!" will not be displayed. Let’s take a look at what just happened. In line 1, we created a variable called message and assigned a value of "Hello World!" to it. Then, in line two, we assigned a new value to it, "Hi there". The new value took the place of the old one, overwriting it. You can also perform operations on variables. In this case, since we are dealing with Strings, we can do things like concatenation (stitching two or more strings together) or changing the casing to all caps. String greeting = 'Hello'; String who = 'world'; who = who.toUpperCase(); String message = greeting + ' ' + who + '!'; System.debug(message); When you run this code, you will see it generates a similar output as the first example. That is because we are concatenating a variable called greeting which contains the word "Hello", a space, the who variable containing "WORLD", and the exclamation point symbol. Those four things together form the phrase "Hello WORLD!". Are you wondering why the word "world" is all in capitals? That is because of line 3 where we call the toUpperCase() function which gives us back the same string but with all its letters capitalized. Primitive types So far, we have only used variables of type String in our examples. String variables allow you to store a collection or characters, numbers or symbols but there are other types that we can use for different purposes. For example, Integer can be used for storing whole numbers, or Double for numbers with decimals. Apex provides 12 primitive types but you can also create custom types by writing your own classes. List of Apex Primitives Blob Binary Data Boolean The values true or false Date A calendar date Datetime A calendar date and time Decimal A number that includes a decimal point Double A double precision floating point number ID A 15 or 18-digit Salesforce ID Integer A whole number -- between -2,147,483,648 and 2,147,483,647 Long A large whole number -- between -263 and 263-1 Object A generic type that can store a value of any other type String Any set of characters -- surrounded by single quotes Time A time Conditional statements In order to have your code make decisions and behave differently based on the value of a variable (or user input), you need to use a conditional statement. The most common type of conditional is the if-else statement, which is very similar to the IF() function in Salesforce formulas. Just like its formula counterpart, it has three parts: the logical test, the true block, and (optionally) the false block. If the logical test evaluates to true, the true block is executed, otherwise the false/else block is. A code block is one or more lines of code wrapped with curly brackets. Boolean formal = true; String who = 'world'; String greeting; if (formal == true) { greeting = 'Hello'; } else { greeting = 'Hi'; } String message = greeting + ' ' + who; System.debug(message); When you run this code, you will see that it outputs "Hello world". In this case, the logical test was checking if the variable formal was equal to true. Since that is the case, it executed the first code block (line 5) which sets the variable greeting equals to "Hello". If it had not been true, it would have executed the false/else block (line 7) which would have set the greeting variable to "Hi". You can try running it again, but setting the variable formal equals to false on line 1. Writing Methods As a Salesforce administrator, you are probably already familiar with the concept of methods which are very similar to functions in validation rules and formula fields. We have also already used two methods in our examples. One was System.debug(), and the other one was String.toUpperCase(). You already know how to invoke them. Now let’s take a closer look. Methods represent a block of code that can be invoked (or called) from somewhere else. They are useful in many ways but have two main purposes: First, you can use methods to avoid repetition by writing your code once and calling it multiple times as needed. Second, methods make code cleaner and easier to read by allowing sections to be abstracted out. Let’s modify our Hello Word program to move the greeting selection logic inside a method, which we will call getGreeting(). Boolean formal = true; String who = 'world'; String greeting = getGreeting(formal); String message = greeting + ' ' + who; System.debug(message); private String getGreeting(Boolean formal) { if (formal == true) { return 'Hello'; } else { return 'Hi'; } } Now, if you want to quickly glance at the code, you should be able to tell at a high level what it does just by looking at the first 5 lines. If you are interested in the specifics of how the greeting is selected, you can look at lines 8 through 12 inside the method. This will make it easier for someone else to read your code or even for yourself, if in the future you want to refresh your memory on what the logic does. This latest variation of the code will output "Hello world" just like previous versions but the order in which the lines get executed is a little different. First lines 1 and 2 will run. Once we get to line 3, in order to get the value to store in greeting, lines 6, 7, and 8 will get executed and getGreeting() will return "Hello". Then execution continues on line 4, where "Hello world" will be stored in message. Lastly, line 5 will be executed which will output the value of message. At that point, the code is finished executing and terminates. Further Reading Congratulations, you have reached the end of this tutorial. You should now be able to read and understand the gist of the code in your org, even if you don’t know exactly what each line does. You should also be able to make simple changes to existing code with the supervision of a more experienced developer. If you enjoyed writing your first program and would like to learn more, you can take a look at these great resources: Trailhead – Gamified step-by-step guided tutorials on hundreds of topics Developer Console Basics – Trailhead module on using the Developer Console Apex Developer Guide - Official Apex documentation from Salesforce Primitive Data Types – Apex documentation about primitives The post Hello, World! The Basics of Apex Programming appeared first on Soliant Consulting. Afficher la totalité du billet
  2. Today I'm sharing a quick tutorial on how to add Lightning Components to Visualforce pages and then take an event from your Lightning Component and have your Visualforce page handle it. This assumes you already have basic knowledge of Visualforce pages and the ability to create a basic Lightning Component. Before you start, make sure you've defined stylings for your Lightning Design System. In order to construct this, we need a few elements: An app container (Visualforce page) A Lightning Component with a related controller and helper class An Apex Controller A Lightning Event This example uses Casper Harmer's code for a Lookup Component. Some functionality was removed, and the event was added: SVG Component <aura:component > <aura:attribute name="class" type="String" description="CSS classname for the SVG element" /> <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" /> <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" /> </aura:component> Lookup Component <aura:component controller="SampleController" access="global" > <ltng:require styles="{!$Resource.SLDS213 + '/assets/styles/salesforce-lightning-design-system.css'}" /> <!-- Component Init Handler --> <aura:handler name="init" value="{!this}" action="{!c.init}"/> <!-- Attributes --> <aura:attribute name="parentRecordId" type="Id" description="Record Id of the Host record (ie if this was a lookup on opp, the opp recid)" access="global"/> <aura:attribute name="lookupAPIName" type="String" description="Name of the lookup field ie Primary_Contact__c" access="global"/> <aura:attribute name="sObjectAPIName" type="String" required="true" description="The API name of the SObject to search" access="global"/> <aura:attribute name="label" type="String" required="true" description="The label to assign to the lookup, eg: Account" access="global"/> <aura:attribute name="pluralLabel" type="String" required="true" description="The plural label to assign to the lookup, eg: Accounts" access="global"/> <aura:attribute name="recordId" type="Id" description="The current record Id to display" access="global"/> <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS213/assets/icons/custom-sprite/svg/symbols.svg#custom11" description="The static resource path to the svg icon to use." access="global"/> <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" description="The SLDS class to use for the icon." access="global"/> <aura:attribute name="searchString" type="String" description="The search string to find." access="global"/> <aura:attribute name="required" type="Boolean" description="Set to true if this lookup is required" access="global"/> <aura:attribute name="filter" type="String" required="false" description="SOSL filter string ie AccountId = '0014B000003Sz5s'" access="global"/> <aura:attribute name="callback" type="String" description="Call this to communcate results to parent" access="global" /> <!-- PRIVATE ATTRS --> <aura:attribute name="matches" type="SampleController.Result[]" description="The resulting matches returned by the Apex controller." /> <aura:registerEvent name="updateLookup" type="c:LookupEvent" /> <div class="slds"> <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true"> <!-- This is the Input form markup --> <div class="slds-form-element"> <label class="slds-form-element__label" for="lookup">{!v.label}</label> <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right"> <c:PR_SVG class="slds-input__icon" xlinkHref="/resource/SLDS213/assets/icons/utility-sprite/svg/symbols.svg#search" /> <!-- This markup is for when an item is currently selected --> <div aura:id="lookup-pill" class="slds-pill-container slds-hide"> <span class="slds-pill slds-pill--bare"> <span class="slds-pill__label"> <c:PR_SVG class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString} </span> <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}"> <c:PR_SVG class="slds-button__icon" xlinkHref="/resource/SLDS213/assets/icons/utility-sprite/svg/symbols.svg#close" /> <span class="slds-assistive-text">Remove</span> </button> </span> </div> <!-- This markup is for when searching for a string --> <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" blur="{!c.handleBlur}"/> </div> </div> <!-- This is the lookup list markup. Initially it's hidden --> <div aura:id="lookuplist" class="" role="listbox"> <div class="slds-lookup__item"> <button class="slds-button"> <c:PR_SVG class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS213/assets/icons/utility-sprite/svg/symbols.svg#search" /> &quot;{!v.searchString}&quot; in {!v.pluralLabel} </button> </div> <ul aura:id="lookuplist-items" class="slds-lookup__list"> <aura:iteration items="{!v.matches}" var="match"> <li class="slds-lookup__item"> <a id="{!globalId + '_id_' + match.SObjectId}" role="option" onclick="{!c.select }"> <c:PR_SVG class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!match.SObjectLabel} </a> </li> </aura:iteration> </ul> </div> </div> </div> </aura:component> Lookup JS Controller /** * (c) Tony Scott. This code is provided as is and without warranty of any kind. * Adapted for use in a VF page, removed need for two components, removed events - Caspar Harmer * * This work by Tony Scott is licensed under a Creative Commons Attribution 3.0 Unported License. * http://creativecommons.org/licenses/by/3.0/deed.en_US */ ({ /** * Search an SObject for a match */ search : function(cmp, event, helper) { helper.doSearch(cmp); }, /** * Select an SObject from a list */ select: function(cmp, event, helper) { helper.handleSelection(cmp, event); }, /** * Clear the currently selected SObject */ clear: function(cmp, event, helper) { helper.clearSelection(cmp); }, /** * If the input is requred, check if there is a value on blur * and mark the input as error if no value */ handleBlur: function (cmp, event, helper) { helper.handleBlur(cmp); }, init : function(cmp, event, helper){ try{ //first load the current value of the lookup field helper.init(cmp); helper.loadFirstValue(cmp); }catch(ex){ console.log(ex); } } }) Lookup JS Helper /** * (c) Tony Scott. This code is provided as is and without warranty of any kind. * Adapted for use in a VF page, removed need for two components, removed events - Caspar Harmer * * This work by Tony Scott is licensed under a Creative Commons Attribution 3.0 Unported License. * http://creativecommons.org/licenses/by/3.0/deed.en_US */ ({ //lookup already initialized initStatus : {}, init : function (cmp){ var required = cmp.get('v.required'); if (required){ var cmpTarget = cmp.find('lookup-form-element'); $A.util.addClass(cmpTarget, 'slds-is-required'); } }, /** * Perform the SObject search via an Apex Controller */ doSearch : function(cmp) { // Get the search string, input element and the selection container var searchString = cmp.get('v.searchString'); var inputElement = cmp.find('lookup'); var lookupList = cmp.find('lookuplist'); // Clear any errors and destroy the old lookup items container inputElement.set('v.errors', null); // We need at least 2 characters for an effective search console.log('searchString = ' + searchString); if (typeof searchString === 'undefined' || searchString.length &lt; 2) { // Hide the lookuplist //$A.util.addClass(lookupList, 'slds-hide'); return; } // Show the lookuplist console.log('lookupList = ' + lookupList); $A.util.removeClass(lookupList, 'slds-hide'); // Get the API Name var sObjectAPIName = cmp.get('v.sObjectAPIName'); // Get the filter value, if any var filter = cmp.get('v.filter'); // Create an Apex action var action = cmp.get('c.lookup'); // Mark the action as abortable, this is to prevent multiple events from the keyup executing action.setAbortable(); // Set the parameters action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName, "filter" : filter}); // Define the callback action.setCallback(this, function(response) { var state = response.getState(); console.log("State: " + state); // Callback succeeded if (cmp.isValid() &amp;&amp; state === "SUCCESS") { // Get the search matches var matches = response.getReturnValue(); console.log("matches: " + matches); // If we have no matches, return nothing if (matches.length == 0) { //cmp.set('v.matches', null); return; } // Store the results cmp.set('v.matches', matches); } else if (state === "ERROR") // Handle any error by reporting it { var errors = response.getError(); if (errors) { if (errors[0] &amp;&amp; errors[0].message) { this.displayToast('Error', errors[0].message); } } else { this.displayToast('Error', 'Unknown error.'); } } }); // Enqueue the action $A.enqueueAction(action); }, /** * Handle the Selection of an Item */ handleSelection : function(cmp, event) { // Resolve the Object Id from the events Element Id (this will be the &lt;a&gt; tag) var objectId = this.resolveId(event.currentTarget.id); // Set the Id bound to the View cmp.set('v.recordId', objectId); // The Object label is the inner text) var objectLabel = event.currentTarget.innerText; // Update the Searchstring with the Label cmp.set("v.searchString", objectLabel); // Log the Object Id and Label to the console console.log('objectId=' + objectId); console.log('objectLabel=' + objectLabel); //This is important. Notice how i get the event. var updateEvent = $A.get("e.c:LookupEvent"); updateEvent.setParams({"lookupVal": objectId, "lookupLabel": objectLabel}); updateEvent.fire(); }, /** * Clear the Selection */ clearSelection : function(cmp) { // Clear the Searchstring cmp.set("v.searchString", ''); cmp.set('v.recordId', null); var func = cmp.get('v.callback'); console.log(func); if (func){ func({id:'',name:''}); } // Hide the Lookup pill var lookupPill = cmp.find("lookup-pill"); //$A.util.addClass(lookupPill, 'slds-hide'); // Show the Input Element var inputElement = cmp.find('lookup'); $A.util.removeClass(inputElement, 'slds-hide'); // Lookup Div has no selection var inputElement = cmp.find('lookup-div'); $A.util.removeClass(inputElement, 'slds-has-selection'); // If required, add error css var required = cmp.get('v.required'); if (required){ var cmpTarget = cmp.find('lookup-form-element'); $A.util.removeClass(cmpTarget, 'slds-has-error'); } }, handleBlur: function(cmp) { var required = cmp.get('v.required'); if (required){ var cmpTarget = cmp.find('lookup-form-element'); $A.util.addClass(cmpTarget, 'slds-has-error'); } }, /** * Resolve the Object Id from the Element Id by splitting the id at the _ */ resolveId : function(elmId) { var i = elmId.lastIndexOf('_'); return elmId.substr(i+1); }, /** * Display a message */ displayToast : function (title, message) { var toast = $A.get("e.force:showToast"); // For lightning1 show the toast if (toast) { //fire the toast event in Salesforce1 toast.setParams({ "title": title, "message": message }); toast.fire(); } else // otherwise throw an alert { alert(title + ': ' + message); } }, loadFirstValue : function(cmp){ var action = cmp.get('c.getCurrentValue'); var self = this; action.setParams({ 'type' : cmp.get('v.sObjectAPIName'), 'value' : cmp.get('v.recordId'), }); action.setCallback(this, function(a) { if(a.error &amp;&amp; a.error.length){ return $A.error('Unexpected error: '+a.error[0].message); } var result = a.getReturnValue(); cmp.set("v.searchString", result); if (null!=result){ // Show the Lookup pill var lookupPill = cmp.find("lookup-pill"); $A.util.removeClass(lookupPill, 'slds-hide'); // Lookup Div has selection var inputElement = cmp.find('lookup-div'); $A.util.addClass(inputElement, 'slds-has-selection'); } }); $A.enqueueAction(action); } }) Apex Controller public with sharing class SampleController { @AuraEnabled public static String getCurrentValue(String type, String value){ if(String.isBlank(type)) { System.debug('type is null'); return null; } ID lookupId = null; try { lookupId = (ID)value; }catch(Exception e){ System.debug('Exception = ' + e.getMessage()); return null; } if(String.isBlank(lookupId)) { System.debug('lookup is null'); return null; } SObjectType objType = Schema.getGlobalDescribe().get(type); if(objType == null){ System.debug('objType is null'); return null; } String nameField = getSobjectNameField(objType); String query = 'Select Id, ' + nameField + ' From ' + type + ' Where Id = \'' + lookupId + '\''; System.debug('### Query: '+query); List&lt;SObject&gt; oList = Database.query(query); if(oList.size()==0) { System.debug('objlist empty'); return null; } return (String) oList[0].get(nameField); } /* * Returns the "Name" field for a given SObject (e.g. Case has CaseNumber, Account has Name) */ private static String getSobjectNameField(SobjectType sobjType) { //describes lookup obj and gets its name field String nameField = 'Name'; Schema.DescribeSObjectResult dfrLkp = sobjType.getDescribe(); for(schema.SObjectField sotype : dfrLkp.fields.getMap().values()){ Schema.DescribeFieldResult fieldDescObj = sotype.getDescribe(); if(fieldDescObj.isNameField() ){ nameField = fieldDescObj.getName(); break; } } return nameField; } /** * Aura enabled method to search a specified SObject for a specific string */ @AuraEnabled public static Result[] lookup(String searchString, String sObjectAPIName) { // Sanitze the input String sanitizedSearchString = String.escapeSingleQuotes(searchString); String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName); List&lt;Result&gt; results = new List&lt;Result&gt;(); // Build our SOSL query String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; // Execute the Query List&lt;List&lt;SObject&gt;&gt; searchList = search.query(searchQuery); System.debug('searchList = ' + searchList); System.debug('searchQuery = ' + searchQuery); // Create a list of matches to return for (SObject so : searchList[0]) { results.add(new Result((String)so.get('Name'), so.Id)); } System.debug('results = ' + results); return results; } /** * Inner class to wrap up an SObject Label and its Id */ public class Result { @AuraEnabled public String SObjectLabel {get; set;} @AuraEnabled public Id SObjectId {get; set;} public Result(String sObjectLabel, Id sObjectId) { this.SObjectLabel = sObjectLabel; this.SObjectId = sObjectId; } } } Most of this code is from Caspar's post, but there are a few differences. The first major difference is in the helper's "handleSelection" method, where it instead activates the event for the selection and associates values to the event's properties: var updateEvent = $A.get("e.c:LookupEvent"); updateEvent.setParams({"lookupVal": objectId, "lookupLabel": objectLabel}); updateEvent.fire(); This is important because instead of passing the callback to the Visualforce page instead we will have it waiting for an event to fire. The Lookup component also contained a reference to an event: <aura:registerEvent name="updateLookup" type="c:LookupEvent" /> The event code is very simple. Note: the type needs to be APPLICATION instead of COMPONENT: <aura:event type="APPLICATION" description="Event template"> <aura:attribute name="lookupVal" type="String" description="Response from calls" access="global" /> <aura:attribute name="lookupLabel" type="String" description="Response from calls" access="global" /> </aura:event> At this point, you will have an active component that will run an event when a selection is made. The missing link so far is the Visualforce page: <apex:page applyBodyTag="false" standardController="Contact" docType="html-5.0" showHeader="true" sidebar="false" standardStylesheets="false"> <html xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"> <head> <apex:includeScript value="/lightning/lightning.out.js" /> <apex:stylesheet value="{!URLFOR($Resource.SLDS213, 'assets/styles/salesforce-lightning-design-system-vf.css')}"/> </head> <body > <div class="slds"> <div class="slds-form-element slds-m-top--xx-small"> <div class="slds-m-right--x-small" id="account_lookup"></div> </div> </div> </body> <script> var visualForceFunction = function(event) { var myEventData1 = event.getParam("lookupVal"); var label = event.getParam("lookupLabel"); console.log('response data = ' + myEventData1 + ' : ' + label); }; // and... $Lightning.use("c:expensesAppVF", function() { $Lightning.createComponent ( "c:Lookup", { recordId: "{!contact.AccountId}", label: "Account", pluralLabel: "Accounts", sObjectAPIName: "Account" }, "account_lookup", function() { $A.eventService.addHandler({ "event": "c:LookupEvent", "handler" : visualForceFunction}); } ); }); </script> </html> </apex:page> Instead of receiving a callback, this version adds a handler to wait for the event to fire when an Account is selected. This lookup could work for any object that you want as long as you change the API name of the objects. You can replace the console.log in the event handler and instead have it do some real functionality, such as actually saving the values returned from the Lightning Component on the Salesforce record. The post Handle Lightning Events from a Visualforce Page appeared first on Soliant Consulting. Afficher la totalité du billet
  3. Using the Card Window feature in FileMaker 16, Martha Zink demonstrates how to create a new window that is context-independent from the parent window/layout in the background. Download the demo file The post FileMaker 16: Context-Independent Card Windows appeared first on Soliant Consulting. Afficher la totalité du billet
  4. While you’re in the middle of doing your work, you might want to add something to do your to do list, but you don’t want to lose your spot in your FileMaker solution. With Card Windows, you can open a new window (within the current window) with a new layout that may or may not be related to the main window/layout. So even though you’re working on an invoice or looking at customer data, a Card Window can allow you to view your to do list, add a new task, close the to do list, and you’re right back where you were. Learn more about this feature by downloading the demo file. Complete the form to receive the demo file. Trouble with this form? Click here. The post FileMaker 16: Context-Independent Card Windows Demo appeared first on Soliant Consulting. Afficher la totalité du billet
  5. In this follow-up to her video introducing the new Card Window feature in FileMaker 16, Martha shows how to use Card Windows for navigation. Download the demo file The post FileMaker 16: Using Card Windows for Navigation appeared first on Soliant Consulting. Afficher la totalité du billet
  6. One of the most exciting features included with the FileMaker 16 release is Card Windows. We're used to seeing a lightbox on a web page; now you can do the same in FileMaker. In her video, Martha Zink shows how to use a Card Window for navigation. Download the demo to see how it's done. Complete the form to receive the demo file. Trouble with this form? Click here. The post FileMaker 16: Using Card Windows for Navigation Demo appeared first on Soliant Consulting. Afficher la totalité du billet
  7. Finished shoes from the engineering challenge (click to enlarge) In 2014, Soliant began a yearly tradition. On the fourth Monday of each April our Chicago office hosts, “Take Your Kids to Work Day.” Employees bring their children to the office to bond with them and to expand their kid's career horizons. In addition to this event being a fantastic learning experience for the kids, Soliant sees this as a great opportunity to have team engagement in the office. We have a committee that helps plan the event in advance and to ensure the children not only shadow their parents but also engage in skill-building exercises. Our goal is to expose the kids to new things so each year, so we are always coming up with different activities. Parents and family members also participate in the planning process; they help us brainstorm the activity options with information about their child interests. Kick-Off Meeting with our New Team Orientation meeting (click to enlarge) This year, we had a day full of activities planned. We kicked off the day with breakfast and orientation. The first activity planned for the day was to review the positions at Soliant. We explained the different departments and what each person is responsible for in the organization. To make it more fun and illustrate Soliant's roles, we gave each child a passport and a map. They had to follow the map to find stations around the office to learn more about our team members’ roles and responsibilities. Once they learned about each position, they received a passport stamp. Passports and map of stations in the office Engineering Challenge Next, we gave a presentation about the engineering design process. We explained the series of steps that one must repeat to develop or improve a product, process or system. We split the teams into age groups for a simple coding exercise. Our goal was to make this experience as interactive as possible so no matter what age, they would be able to engage and learn. Soon enough, it was time for the lunch break. Since the weather was nice and sunny, lunch took place at our rooftop. Pizza seemed like a win-win menu choice for adults and kids; besides, we wanted to keep the little ones fueled for the afternoon activities. Click to view slideshow. In the afternoon, we had more activities focused on engineering; the kids had to design and create a pair of shoes that could successfully undergo several challenges. The shoes would be tested for comfort and efficiency, and go through several challenges: jumping, walking and stair climbing. Each challenge had a point system, and the goal was to achieve the most points. Wrapping up Another Awesome Take Your Kids to Work Day Before the day ended, we asked the children to fill out a time sheet. It was a good exercise to recap the day. The day ended with an evaluation from the kids; we always want to encourage feedback. This year we had tons of fun with the kids and added another successful "Take Your Kids to Work Day." We hope the hands-on experiences gave new skills to the kids and helped them better understand Mom and Dad’s workday. We’re already looking forward to next year! The post Take Your Kids to Work Day appeared first on Soliant Consulting. Afficher la totalité du billet
  8. FileMaker 16 introduces newly available options to the "Insert from URL" script step that utilize the populate cURL library. If you are not familiar with cURL, it has long been a standard for command line interaction with all manner of internet protocols and applications. The introduction of these options heralds a host of new options for FileMaker developers looking to interact and integrate with all kinds of web services. In this post, we will focus on one particularly useful feature: the ability to upload a file stored in a container field directly from the field. That means you do not need to write the data out to the file system before referencing it with your script. It makes good sense. The location of the data, the contents of the container field, is known, so you should be able to reference it directly without having to save it somewhere locally first. Now we have a way to do just that, even if it is not apparent at first. Admittedly, the syntax was not intuitive for me when I first tried to get this working. The Need For This Functionality There are times when you need to upload a file to a web page, and in fact, most web scripting languages have built-in functions to handle file uploads. After all, the HTML form input type "file" has been around forever to allow exactly this sort of functionality. We know we can handle file uploaded on the web server side of things. We will assume that the file is being transferred as binary data and not Base64 encoded. Some web services may require it and there may be limits on the amount of Base64 text you can transfer. What we do with the file once it is uploaded is irrelevant to this blog post. Our focus is on how to make that request from a FileMaker 16 script. We will assume that the web scripting side of things works as expected. What we do with the file once it is uploaded is irrelevant to this blog post. Our focus is on how to make that request from a FileMaker 16 script. We will assume that the web scripting side of things works as expected. Enter cURL Luckily for us, the cURL functionality needed for this is included with FileMaker 16. External scripting or third party plugins are no longer required. Since "file" is a valid input type for HTML forms, the cURL library includes support for submitting this data as part of an HTTP request, most commonly transferred with the POST method. This means that the payload of the request is not sent as part of the URL string, like you would have with a GET request. Consequently, an arbitrarily large request can be made without running into URL string limits. cURL syntax with FileMaker Variables Let's review the relevant steps. First, we set a variable to the value of our container field. Set Var: ["$myFile" ; Table::UploadField_r ] How can we use this variable in our cURL expression? The "-F" flag, which is short for "form", allows cURL to emulate a filled in form in which the user has pressed the submit button. We can use this flag with the new "Insert from URL" cURL options like so: -F "file=@$myFile" Note that when we include the variable in the cURL options, it appears inside the quotation marks. This is the correct syntax; placing it outside the quotes will not work. This is analogous to how a file name is specified inside of quotation marks in an html form element: <input type="file" name="myFile" /> Similarly, on the server you can access the data the same way any uploaded files are received. For example, with PHP, you can use the $_FILE variable to handle and save the file on the server. Side Effects A side effect of this syntax is that, while it allows you to pass the binary data directly from a container, the file name that appears on the receiving end will be the name of the variable you use. In the above example, the file name would be “myFile” which may not be what you want. A possible solution will be to pass the file name as a separate parameter and then rename the file on the server. However if you are not in control of the web scripting, that won't be an option. To make it easier for web services to consume, we can set the variable name to the file name of the container data. The trick here is to do this dynamically and to avoid illegal characters and reserved words. Note that variable names are subject to the same rules as field names. A simplified version of this can be done with a few variables and using the Evaluate and Let functions to dynamically set a variable. Set Variable [$myFileName ; Substitute ( GetAsText ( Detail::Upload_File_r ) ; [" " ; "_"] ; ["(";""] ; [")";""] ; ["+";""] ; ["-";""] ; ["=";""] ; ["≠";""] ; ["&";""] ; ["<";""] ; [">";""] ; [",";""] ; [";";""] ; ["[";""] ; ["]";""] ; [":";""] ; ["\"";""] ; ["$";""] ; ["/";""] ; ["*";""] ; ["^";""] ; ["}";""] ; ["{";""] ) ] Set Variable [ $null ; Evaluate ( "Let ( $" & $myFileName & " = " & "Detail::Upload_File_r" & " ; \"\" )" ) ] Insert From URL [ Select ; With dialog: Off ; $your_result ; $your_url ; cURL options: Evaluate ( "\"-F file=@$" & $myFileName & "\"" ) ] You may also note that the target of the result of the insert can now be a variable. This is a new change in FileMaker 16. You can now call this function without requiring a field to be placed on a layout to hold the result, as was the case before. Testing As mentioned above, you can write your own web script and host it on your web server. You can also use a service like Request Bin to inspect HTTP requests that are made. If we do that, we can see that the file gets submitted as part of the HTTP request and that it is sent as binary data. Feel free to try it out yourself from the sample file. The sample file also includes a sample script to download a file directly to a container field. Although the functionality shown is not an entirely new feature to FileMaker, the part that is new in 16 is the ability to verify SSL before downloading data. The sample file has this new feature enabled. Of course, this just scratches the surface of what you can do with FileMaker 16 and the newly introduced cURL options. I look forward to seeing what can be done with it myself, and seeing what others can come up with. References cURL man page - curl.haxx.se Supported cURL Options - FileMaker 16 Help Insert From URL - FileMaker 16 Help The post FileMaker 16: cURL and Container Fields appeared first on Soliant Consulting. Afficher la totalité du billet
  9. FileMaker 16 introduces new options to the "Insert from URL" script step that utilize the populate cURL library. You can even write your own web script and host it on your web server or use a service like Request Bin to inspect HTTP requests. Try this functionality out yourself. Download our sample file by filling out the form on this page. This file includes a sample script to download a file directly to a container field and showcases the new ability to verify SSL before downloading data. Complete the form to receive the demo file: Trouble with this form? Click here. The post FileMaker 16: cURL and Container Fields Demo appeared first on Soliant Consulting. Afficher la totalité du billet
  10. In FileMaker Server 16, under Web Publishing you will find a brand-new entry called FileMaker Data API. Figure 1. FileMaker Data API under Web Publishing (Click to enlarge.) Its description explains that web services can use it to access FileMaker data. The truth is - it’s even better than that. It’s open to any kind of integration, not just web services. Any application or system that can make an HTTP call and works with received JSON data in response can use FileMaker Data API. It’s a very powerful and welcome addition to the FileMaker product line. It essentially turns your FileMaker app into a web service. How to Use FileMaker Data API You can reach the web service (your FileMaker app) through a URL endpoint that looks something like this: https://{{server}}/fmi/rest/api/record/the_UI/FRUIT_utility/1 (We’ll explain the structure of this URL later.) Then use one of the HTTP methods (verbs) to indicate what your desired action is (GET to retrieve data, POST to create records, PUT to update records, DELETE to delete). The next part of the request is one or more headers. The most common header here is the authentication token to prove that you are an authorized user. And finally, a JSON-formatted body contains the data you want to send to your FileMaker app. The web service then replies with a response in the JSON format. Online help for FileMaker Data API is available on the machine where FileMaker Server is installed, at the URL shown below. This detailed documentation provides good examples of what to send and what you receive in return. https://{{server}}/fmi/rest/apidoc/ Figure 2. FileMaker Data API - Get Record (Click to enlarge.) Example 1: Authentication Let’s get practical. For this post, I've created a collection of Postman test requests. Postman is a free testing tool our development team uses quite often. It allows you to construct API calls and see what you get back from the web service. The Postman test requests have been saved as a "Postman collection," which you can download below. You can import this Postman collection once you have the tool installed. Each test demonstrates the various actions that the FileMaker Data API allows. Figure 3. FileMaker Data API - Postman Test (Click to enlarge.) Figure 4. FileMaker Data API - Test Files (Click to enlarge.) Download the Demo Files In addition to the Postman collection, we've created two FileMaker files we used for these tests: the UI and the Data. Both have been enabled for the new FileMaker Data API. Download the files (includes the Postman collection, FileMaker UI, and the Data) Test Requests in FileMaker Data API Let’s look at three of the test requests and see how they work. First, you need to do is authenticate; i.e. log in. You then receive a token allowing you to interact with the data. The token is valid for 15 minutes after your last request. It needs to be added as a header to each HTTP request. The documentation shows that we need to use this endpoint (URL) syntax and that it requires the HTTP POST method. Figure 5. FileMaker Data API - HTTPS Post Method All requests to the FileMaker Data API will use '/fmi/rest/api/". Next the API calls on 'auth' for logins and logouts, ‘record’ for all data manipulations, ‘find’ for searches, and ‘global’ for setting global fields. Wherever the API documentation mentions ':solution', use the name of the FileMaker file you want to target. Use just the file name, no leading ':', no '.fmp12' extension. Where the API documentation uses ':layout', just use the target layout name, sans the ':'. Using web compatible file and layout names makes your life much easier. Don’t forget to avoid spaces and special characters. In our Postman request, we used this process, with the name of the FileMaker file ("the UI") that we want to work with. Figure 6. FileMaker Data API - Postman Request In Postman, the "{{server}}" is a placeholder set in the Postman ‘environment’ section. I can easily change servers without having to update each individual request. I use a similar placeholder for the token once we have it. Figure 7. FileMaker Data API - Manage Environment (Click to enlarge.) That takes care of the endpoint. The documentation also tells us what headers we must specify for the request: 'Content-Type' is required and the 'X-FM-Data-Login-Type' is only needed if we want to use OAuth to log in. Figure 8. FileMaker Data API - Content Type Specification (Click to enlarge.) We are going to use a normal FileMaker account to log in, so we’ll skip that optional header and just set the content type: Figure 9. FileMaker Data API - Content Type (Click to enlarge.) The last thing we need to do is to set the body. The documentation explains what needs to go in the body. It also provides an example of what the whole thing should look like: Figure 10. FileMaker Data API - Body Parameters (Click to enlarge.) Figure 11. FileMaker Data API - Postman Test - Get Layout (Click to enlarge.) Then use Postman to construct the piece of JSON to send over. In that JSON we add the username and the password to use for the login, but we also need to add the name of a layout. The layout is a bit of an odd duck in this request; it can obviously be the layout that you will want to use for the actual data interaction but at a minimum, the provided user credentials must have access to that specified layout or the authentication will fail. Here’s our request in Postman: Figure 12. FileMaker Data API - Postman Request (Click to enlarge.) We are now ready to send this request over to FileMaker. When we have everything right, we will get a JSON response that contains the token: Figure 13. FileMaker Data API - JSON Response (Click to enlarge.) If the login fails, the return response would be: Figure 14. FileMaker Data API - Login Fail Response (Click to enlarge.) After a successful login, you can copy the token to the {{token}} environment placeholder. This makes it available to all subsequent requests. Example 2: Retrieve Data Now that we have a valid token, let’s use it to retrieve some data. In fact, let’s ask for all 10,000 records in the FRUIT table. We will use the ‘FRUIT_utility’ layout for this as shown in the screenshot below. It has a nice mix of different field types on it: Figure 15. FileMaker Data API - FRUIT Utility Layout (Click to enlarge.) As per the documentation, we need to create a GET request, specifying the FileMaker file (:solution) and the target layout (:layout). We need to specify the token in the header. Figure 16. FileMaker Data API - Specify Token (Click to enlarge.) In Postman, it looks like what's shown below, where 'the_UI' is the file and 'FRUIT_utility' is the layout: Figure 17. FileMaker Data API - Specify Token - Postman Test (Click to enlarge.) When we send the request, we receive a response with the FileMaker data in JSON format: Figure 18. FileMaker Data API - Request Response (Click to enlarge.) You might expect this to contain the data for all 10,000 records, but that is not the case. The Data API limits the returned found set to 100 by default. Note that Postman also gives us feedback on the time it took for the Data API to respond and the size of the response: 78 milliseconds for 100 records. Figure 19. FileMaker Data API - Response Status (Click to enlarge.) If we really want all 10,000 records, we can modify the request to add the 'range' keyword and set it to the exact number of records. Or, if we are not sure of how many records there are, we can use a very large number. Figure 20. FileMaker Data API - Record Numbers (Click to enlarge.) As you can see, it now took just over a second and a half to get all 10,000 records. The total response size is just over 7MB. One thing we should note about JSON data we receive from FileMaker: all data is returned as JSON text (everything is quoted), even numbers and dates. The system receiving the JSON will need to factor this in. Figure 21. FileMaker Data API - JSON Data (Click to enlarge.) The test requests in the Postman collection will demonstrate some of the nuances in the JSON syntax for things like portal data, if you have an object name set for your portal, what repeating fields look like, and other variations. Example 3: Modify Data The last example shows the endpoint structure — headers and body to use when we want to edit a specific record. In this example, we want to change the value of the ‘fruit’ field on the record with record id 10007. As per the documentation, we’ll need to use the PUT HTTP method, set two headers, and use a JSON body that includes the field that we want to update and the new value. The URL has to include the FileMaker file name (:solution), layout (:layout), and the record’s id (:recordId). Figure 22. FileMaker Data API - PUT HTTP Method (Click to enlarge.) This raises a question: what do we use for the record id, and how do we know the value of the record id? 'recordId' is the internal id that FileMaker assigns to every record. It is not your own primary key or serial number. For the purpose of this demo, I have created a field in the FRUIT table that shows us the record id, so that we can actually see it on the layout: Figure 23. FileMaker Data API - Record ID (Click to enlarge.) Even if you do not have such a field, the record id is always part of the JSON that the Data API returns if you create a record or search for records. If you do a search through the Data API and then process that response, you will have the record id you need: Figure 24. FileMaker Data API - Record Search (Click to enlarge.) Back in Postman, our request looks like this with the URL (ending with the record id) and the headers: Figure 25. FileMaker Data API - Postman Request (Click to enlarge.) In the body, we have some JSON that contains the name of the field that we want to update: Figure 26. FileMaker Data API - Body JSON (Click to enlarge.) When the request is sent, the Data API response indicates all is well: Figure 27. FileMaker Data API - Response (Click to enlarge.) Or, if we messed up and specified a field name that does not exist, we will get this back: Figure 28. FileMaker Data API - Error Response (Click to enlarge.) These examples are basic. The JSON payload (body) gets more complex when you do searches and other requests, but I hope this introduction was helpful to get you going with the new FileMaker Data API. How does FileMaker Data API stack up against other APIs? We still have the traditional APIs at our disposal (ODBC/JDBC, XML and PHP). The most straightforward comparison is with the XML API. They both work off request URLs, they return data in a particular format, and they do not require any drivers or other setup, like ODBC does for example. The Postman collection includes one XML test that also asks for all 10,000 records from the same table, through the same layout. In our testing, we have found the Data API to be consistently faster than the XML API. Below is a screenshot showing that it took over 3 seconds to retrieve the data through the XML API. The Data API took just over 1.6 seconds. Figure 29. FileMaker Data API - Data Retrieval Time (Click to enlarge.) The Future with FileMaker Data API On the FileMaker Server admin console, it states that the Data API is a trial that expires at the end of September next year (2018). Given that FileMaker Inc. releases a new version every year around May, we expect the Data API to be officially released with FileMaker 17 well before this trial expires. We do expect that at some point the support for the XML and PHP APIs will be deprecated and that all development focus will go to this new FileMaker Data API. Keep an eye on the FileMaker roadmap for this. Even when they eventually get marked as deprecated, XML and PHP APIs will clearly be around for a long time. There’s no need to rework any existing integrations you have that use those APIs. However, consider the new Data API for any new and upcoming work We have one caveat: it is unclear at this point if the use of the new FileMaker Data API – once officially released – will require some sort of connection-pack licensing. We will keep our ear to the ground on this. There are definitely some downsides to the Data API. It does not have the ability to execute scripts, a capability both XML and PHP APIs boast. It also requires using the internal record id of a record to identify the record. That record id is not as set in stone as it needs to be to permanently point to the same record. For example, if you delete the record from the FileMaker file and re-import the same record from a backup, the record id will be different. Your primary key will still be the same, but the internal record id will change. Any REST url pointing to this record through its original record id will break. Our FileMaker Data API Recommendation: Get familiar with this new feature. It is an exciting and promising addition to FileMaker developers’ integration arsenal. However, be careful when committing to it for production projects. The API will likely undergo tweaks before its official release. The licensing pricing model is unknown at this point too. Have questions about FileMaker Data API capabilities? Let us know in a comment below. The post FileMaker Server 16: FileMaker Data API appeared first on Soliant Consulting. Afficher la totalité du billet
  11. One of our favorite features of FileMaker Server 16 is the brand-new FileMaker Data API. We've explained how it works in a blog post here. Now you can explore the functionality yourself by downloading our demo files. We've created two FileMaker files: the UI and the Data. Both have been enabled for the new FileMaker Data API. (This download includes the Postman collection, FileMaker UI, and the Data referenced in the preceding blog post.) Complete the form to receive the demo files: Trouble with this form? Click here. The post FileMaker Server 16: FileMaker Data API Demo appeared first on Soliant Consulting. Afficher la totalité du billet
  12. The FileMaker 16 platform includes FileMaker Server and introduces the ability to have multiple "worker" machines connected to a single FileMaker database server or "master" machine. This is mostly relevant to FileMaker WebDirect, which has greatly improved with the latest version. This involves an understanding of the different services running as part of FileMaker Server, only one of which is the database server itself. There are also web publishing and scripting engines, among other services. Most important here is the Web Publishing Engine (WPE). That’s where WebDirect sessions are run from. FileMaker 16 introduces some changes to how WPE is managed in the FileMaker Admin Console. Worker 1 FileMaker Server 16 represents the Web Publishing Engine a little differently. If you have enabled Web Publishing at all in the Deployment Wizard, you will now see the first "worker" machine attached. This represents a one-machine deployment. Previous versions allowed for a two-machine deployment you specified as part of the Deployment Wizard. With FileMaker 16, the server machine is labeled as Worker 1 in your admin console. A two-machine deployment will show two worker machines, the first one being from the main server machine. As the screenshot shows, there is no option to remove Worker 1, as it is part of the main server deployment. Figure 1. Worker Machine 1 Options (click image to enlarge) 500 Maximum Concurrent Connections The Web Publishing Engine in FileMaker Server 16 is now much more capable, judging from the new server specifications. Previous versions supported multiple WebDirect sessions on a single machine. Unfortunately, if you needed more than 7 or 8, a two-machine deployment was required. Now, a single machine deployment supports up to 100 concurrent WebDirect Sessions. Of course, that means your server will need to be provisioned well enough to handle the load. Make sure to give your server enough resources to handle anticipated usage. This is a big deal, because a WebDirect session is roughly equivalent to a FileMaker Pro or Go client. Once a user logs in via WebDirect, a session must maintain all global field contents, local and global variables, and other information about the user session until the user has either logged out or their session times out, effectively disconnecting them. The new supported number of maximum concurrent WebDirect connections is 500, with 100 users per worker machine in the deployment. That makes a total of five well-provisioned machines (the main server hosting worker 1, plus 4 additional worker machines) to handle 500 concurrent WebDirect users at the same time, accessing a hosted FileMaker solution. Adding a Worker The process for adding a worker machine is now different as well. As with earlier versions, you specify that the machine is a worker while installing FileMaker Server on that machine. In FileMaker 16, a deployment web page attaches the worker machine to the master machine. Each worker requires its own fully qualified domain name or IP address, including SSL certificate if appropriate. Once the worker machines connect, the master machine routes WebDirect sessions to them to balance the load. You will see the redirected URL in the location of your browser. FileMaker will route requests to the connected worker machines as much as possible, keeping as much of the main server available for handling all other services running on that machine. Once a user routes to a secondary worker machine, he stays connected to that worker. He will not re-route to another one for the remainder of his WebDirect session. This is true for any parameters included in the URL string. You can still reference the "homeurl" parameter to redirect to when the user logs out, or pass in additional parameters like you were able to in previous versions. Balancing the Load You should always have users log into the main machine, worker 1, and then let FileMaker Server route them to the worker with the smallest load. FileMaker Server performs its load balancing based on available resources of the worker machines and of course, perhaps the most important metric of all, user count per machine. The worker hosting the fewest users will typically get the next incoming user routed to it. Using WebDirect in Business Applications Considering its continued improvements, WebDirect serves as a strong contender for your next custom web-enabled business application. There may not be a better alternative for quickly building a feature-filled, robust web application providing such extensive customization options. If you’d like to learn more about how WebDirect can enhance your business, contact our team today. We’re happy to discuss potential solutions with you. References FileMaker Server 16 FileMaker WebDirect The post FileMaker Server 16: Multiple Worker Machines appeared first on Soliant Consulting. Afficher la totalité du billet
  13. The newly released FileMaker 16 product line introduces interesting new security features and adds three new External Authentication providers through OAuth: Amazon, Google and Microsoft Azure. Read all about them in two white papers co-authored with Steven H. Blackwell: Version 16 Brings Major New Security Features At this year's DevCon, I will present an in-depth look at those new OAuth providers and show you how to set them up and use them. See you there! In the meantime, contact us if you have any questions about these new features. The post FileMaker 16 – OAuth External Authentication and more security features appeared first on Soliant Consulting. Afficher la totalité du billet
  14. As with past releases of the product, FileMaker, Inc. invited Soliant's team to be a part of the initial testing of FileMaker 16. We've spent the past months working with this new version and we're happy to help familiarize you with its features. FileMaker 16 Oveview May 9, 2017 - FileMaker 16 is here, and its new features are game-changers for developers. For example, using its functionality will make custom applications more responsive and secure. You can now connect with other services, such as Weather.com, to integrate data from external data sources, but that's just the tip of the surface. In this video, we introduce the top three new functionalities: Card Windows, creating and parsing JSON, and the Layout Objects Inspector. An Introduction to Card Windows May 9, 2017 - The new Card Window feature in FileMaker 16 allows you to create overlays within the same window. The card can have its own context different from the background layout. This is going to be a game-changer in how FileMaker solutions are designed and architected. The post FileMaker 16 Playlist appeared first on Soliant Consulting. Afficher la totalité du billet
  15. FileMaker 16 is here, and its new features are game-changers for developers. For example, using its functionality will make custom applications more responsive and secure. You can now connect with other services, such as Weather.com, to integrate data from external data sources, but that's just the tip of the surface. In this video, we introduce the top three new functionalities: Card Windows, creating and parsing JSON, and the Layout Objects Inspector. Read the blog post Download the FileMaker 16 Executive Summary The post FileMaker 16 Overview appeared first on Soliant Consulting. Afficher la totalité du billet