diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/ui-component.md b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/ui-component.md
index de67b977..2cd81289 100644
--- a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/ui-component.md
+++ b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/ui-component.md
@@ -11,7 +11,7 @@ The need for custom widgets typically arises when existing Simplicité component
> ***Note:*** Such requirements are often niche. Most technical operations can already be accomplished using Simplicité's core functionalities. Consequently, the primary purpose of creating custom widgets is to address unique visualization needs, enabling you to embed tailored styles and feature combinations within a custom interface.
-### Our Example: Welcome Card
+### Example: Welcome Card
![](welcome-card-result.png)
@@ -33,7 +33,7 @@ Another important step is to grant your widget the rights of the module and view
![](welcome-card-permissions.png)
-### Adding Content & Styles
+### Creating Resources
Then use the *Create Resources* action button, and click *Yes*. By doing so, you are adding 3 files to your object's *Resources* (visible in the bottom tab section "Resources"):
@@ -41,138 +41,92 @@ Then use the *Create Resources* action button, and click *Yes*. By doing so, you
- **STYLES**; the *CSS* file that serves as stylesheet for your object. There are no default style defined, only an empty bracket `#ext-obj { /* Custom styles */ }`.
-- **CLASS**; the *Javascript* script that will be useful for the next lesson.
+- **CLASS**; the *Javascript* script that will be useful for the next sections.
![](welcome-card-resources.png)
> ***Note:*** The ressources are organized as any web element, in order to be easily integrated and created by designers and frontend developers.
-For the welcome card, both content and style ressources are quite easy to create. Below are the *HTML* and *CSS* codes.
+For the next steps, there are several ways to conceive your object:
+
+1) **Dynamic Instantiation** *(recommended)*
+
+As at some point you will have to fetch data and dynamically create your html elements (for example if you are retrieving an object and then using its data within a div). Thus it is more relevant to instantiate all of your HTML directly from your *CLASS* resource file using javascript.
+This way is the one recommended by Simplicité when creating your External Objects, as they won't be very large in the DOM and mostly embedded in your existing Simplicité Interfaces, it is preferred to have the lowest loading time possible --through lighter HTML--, and overall the workflow of the *CLASS* and *HTML* resource file together is simpler using this way.
+
+This way your *HTML* resource file will remain very light and only contain sort of 'anchors' that will help you tell where and how to instantiate your different elements. And your *CLASS* file on the contrary, will be slightly bigger and complex. Below are shown the examples of the basic setup for our *DemoWelcomeCard* using this instantiation type:
+
+```html
+
+
+
+
+
+
+```
+
+```javascript
+Simplicite.UI.ExternalObjects.DemoWelcomeCard = class extends Simplicite.UI.ExternalObject {
+ async render(params, data = {}) {
+ $('#demowelcomecard').append('Hello world!');
+ }
+}
+```
+
+* A class with the name of your External Object is declared within the `Simplicite.UI.ExternalObjects` module, extending the `Simplicite.UI.ExternalObject` to properly access any method from it.
+* Only the `render(params, data)` function is declared yet, as it is the one called inside the server-side java code resource `com.simplicite.webapp.web.ResponsiveExternalObject`.
+* We dynamically instantiate an "Hello world!" text simply to test that the connection & basic instantiation is enabled using `$('#demowelcomecard').append(...)`.
+
+The *HTML* will remain the same during this lesson (for this instantiation), while the *CLASS* will be modified and furtherly explained later on.
+
+> ***WARNING:*** The javascript code snippet is the one you'll have from V6.1.19, if you use an outdated version the resource file for javascript might be *SCRIPT* and then you can refer to the code presented at the very end of this document, that uses a more general and fully working setup; `var DemoWelcomeCard = DemoWelcomeCard || (function(){ ... })(jQuery);`.
+
+V5 Default Javascript
+
+```javascript
+var CustomWelcomeCard = CustomWelcomeCard || (function($) {
+ function render(url) {
+ $('#customwelcomecard').append('Hello world!');
+ }
+
+ return { render: render };
+})(jQuery);
+```
+
+
+2) **Static Instantation**
+
+There are some use cases where it is easier to work with a static setup (i.e longer HTML), for example if you only fetch few data and have everything doable from HTML or from any "static function". This is thus not recommended compared to the previous version, as it will be slightly less effective while co-existing with/within Simplicité's solution.
+With this method, the setup remains the same for the *CLASS* file, but you'll maybe already setup a more complete structure for your *HTML*:
```html
Welcome User
- Welcome to Simplicité's solution! We're excited to have you onboard. Explore, interact, and enjoy a seamless experience tailored for you.
+ Welcome to Simplicité's solution! We're excited to have you onboard. Explore, interact, and enjoy your experience with us !
-
-
-
+
+
+
```
-
-CSS Stylesheet
-
-```css
-#customwelcomecard {
- display: flex;
- flex-direction: column;
+Yet we won't actually instantiate the different features and functions, but still we can include them to know how they'll be included. Moreover, the presented scripts are not the final ones and will be modified with explanations later on this lesson.
- width: 100%;
- padding: 1rem 2rem;
-
- justify-content: start;
- align-items: center;
-}
-.welcome-title {
- width: 100%;
- color: #303030;
- text-align: center;
- font-size: 3.5rem;
- border-right: solid 0.25rem #5451FF;
-}
-.welcome-text {
- padding: 0rem 1.5rem;
- color: #474747;
- text-align: left;
- font-size: 2rem;
- margin-bottom: 1rem;
- border-right: solid 0.25rem #58EC9B;
-}
-.welcome-buttons {
- display: flex;
- flex-direction: row;
- width: 100%
- justify-content: center;
- align-items: center;
- gap: 3rem;
- margin-top: 2rem;
-}
-.welcome-btn {
- position: relative;
- padding: 1.5rem 3rem;
- border: none;
- color: #303030;
- background-color: transparent;
- overflow: hidden;
- cursor: pointer;
- text-align: center;
- font-size: 2rem;
-}
-.welcome-btn::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- width: 0.25rem;
- height: 100%;
- background-color: transparent;
- border-left: solid 0.25rem transparent;
- border-bottom: solid 0.125rem transparent;
- transition: all 0.5s ease;
-}
-.welcome-btn:hover::before {
- border-left-width: 1.5rem;
-}
-.welcome-btn:active::before {
- border-left-width: 3rem;
-}
-.tuto {
- background-color: #FFF6E0;
- transition: all 0.25s ease;
- &:active {
- background-color: darken(#FFF6E0,4%);
- }
- &::before {
- border-color: #FFD166;
- }
-}
-.info {
- background-color: #FBEBFB;
- transition: all 0.25s ease;
- &:active {
- background-color: darken(#FBEBFB,4%);
- }
- &::before {
- border-color: #EC9DED;
- }
-}
-.prd-nav {
- background-color: #FED7D9;
- transition: all 0.25s ease;
- &:active {
- background-color: darken(#FED7D9,4%);
- }
- &::before {
- border-color: #FB3640;
- }
-}
-```
-
+> ***Note:*** Both techniques are working fine in Simplicité and it indeed depends on how you prefer to do it. The main presented examples are gonna be for the first one, but at the end we provide a similar code that works with the second technique.
## Integration (Welcome Card)
As for the *creation process*, the process of integrating the created widget is very straightforward, everything will be done in the *User Interface > Views > Show All*, select a view that is of type *Home Page*, and click the *Edit View* action button:
-1 - Add a new **Sub-View**.
-2 - Make it of type **External Page**.
-3 - Select *External Object* as Source.
-4 - Fill the *External Object* field with the name of your widget (for the welcome-card use **CustomWelcomeCard**).
-5 - Save it, and you shall see a preview of your object integrated within the view.
+1) Add a new **Sub-View**.
+2) Make it of type **External Page**.
+3) Select *External Object* as Source.
+4) Fill the *External Object* field with the name of your widget (for the welcome-card use **CustomWelcomeCard**).
+5) Save it, and you shall see a preview of your object integrated within the view.
> ***Warning:*** Make sure to grant the rights for your external object !
> - If encountering the *External object ____ not granted*, first try to clear your cache.
@@ -182,45 +136,116 @@ The creation of an object's behavior and interaction within one Simplicité appl
## Implementation (Welcome Card)
-The first step is to make sure that our object can be aware of the Simplicité's system that he is a part f. To make such things we are gonna use the *javascript* resources of our External Object: **extobj_script** or **CLASS** or **SCRIPT**.
+The first step is to make sure that our object can be aware of the Simplicité's system that he is a part of. To make such things we are gonna use the *javascript* resources of our External Object: **CLASS**.
+
+> ***Note:*** The **CLASS** resource file name is automatically assigned at creation from v6.1.19, above it's possible that your javascript resource file is named **SCRIPT**. Thus the inner workflow remains similar.
### File structure
-The created *javascript* file initially contains only the base structure to later implement whatever we want in our object:
+The default *CLASS* vows to be organized is a certain way while you develop it, this it is important to understand its structure and how to navigate through it:
```javascript
-class CustomWelcomeCard extends Simplicite.UI.ExternalObject {
+Simplicite.UI.ExternalObjects.DemoWelcomeCard = class extends Simplicite.UI.ExternalObject {
async render(params, data = {}) {
- $('#demowelcomecard').append('Hello world!');
+ $('#demowelcomecard')
+ .append($('
').addId("custom-result-text").text("none"))
+ .append($('').addClass("custom-btn-big").text("Click Me").on("click", this.foo()))
+ .append($('').addClass("custom-btn-small").text("click me").on("click", () => { $('#custom-result-text').text("Clicked on the small button") }))
+ }
+
+ foo()
+ {
+ $('#custom-result-text').text("Clicked on the BIG button");
}
}
```
-* The class extends the `Simplicite.UI.ExternalObject` class, so the object is set to access all (possibly needed features)[]
-* Only the `render(params, data)` function is declared yet, as it is the one called inside the server-side java code resource `com.simplicite.webapp.web.ResponsiveExternalObject`.
+1) **Dynamically instantiate HTML:**
+As with this way you dynamically create all of your HTML content, the syntax we recommend to use is the illustrated one above; `$(target).append(content)` with content usually being `$(selector)` to add an empty div. Such expression means and does several things:
+- `$()` is a shorthand for `jQuery()`, and allows to both select a target element in the DOM from its 'id', `$('#container')` will select the ``.
+- `.addClass("class0 class1")` to specify the differente classes (may be single) of your element, here ``.
+- `.text("txt")` to input a given string (possibly not static) to your element, from the example we would have `
txt
`.
+- `.on("event", function)` allows to define an *eventListener* to your element, giving first a string naming the event, and then specifying the function to call when this event occurs (events can come from several interacting sources such as mouse, forms, keyboard, documents, and even touch).
+
+With such approach, passing out variables and methods is pretty straighforward and intuitive, as everything happens within your *CLASS* javascript resource file. Indeed you can simply link some elements of your *HTML* content file to specific functions and features implemented in your *CLASS* javascript file. Thus you have nothing to actually implement in the *HTML*, just some anchors to then dynamically instantiate your elements from the *CLASS* file.
+
+2) ***Alternative "Hybrid" Script Structure***
-But for simple embedded *External Objects* we can use a slightly simpler setup, by simply declaring our object as a peculiar namespace in which we'll declare everything we need to ensure it is only related to our object:
+If you still like to have elements with associated features statically instantiated within your *HTML* file, then there is one slightly different way to think your structure; you need a reference or an entry point to your javascript methods, this is done through an actual *instance* of your class that can be declared as follows:
```javascript
-var CustomWelcomeCard = (function(){
- return {};
-})();
+Simplicite.UI.ExternalObjects.DemoWelcomeCard = class extends Simplicite.UI.ExternalObject {
+ async render(params, data = {}) {
+ // do whatever you need/want here
+ }
+
+ foo()
+ {
+ $('#custom-result-text').text("Clicked on the static button :)");
+ }
+}
+
+const demowelcomecard = new Simplicite.UI.ExternalObjects.CustomExternalObjectTest();
+window.demowelcomecard = demowelcomecard;
```
-### Accessing the current session
+Then using function from statically instantiated elements can be done as:
-The communication with Simplicité's environment is allowed by using the `$ui` call within our object's class extending `Simplicite.UI.ExternalObject`. And we get the current session by using the `getApp()` function, returning the current `Simplicite.Ajax` instance.
+```html
+
+
Hello World !
+
none
+
+
+```
-From the previous example we just need a small improvement, adding the `let app= $ui.getApp()` line to instantiate a reference to the current Simplicité session:
+3) ***Previous Script Structure***
+For the older *SCRIPT* the structure is thus more straigthforward, as you declare your whole object's script as an *Immediately Invoked Function Expression*, everything is contained in it, and for a proper organisation you can refer to the following script:
```javascript
-var CustomWelcomeCard = (function(){
+var CustomWelcomeCard = CustomWelcomeCard || (function($){
let app = $ui.getApp();
-
- return {};
-})();
+ let login = $ui.getGrant().login;
+ let product = app.getBusinessObject("DemoProduct");
+
+ function foo()
+ {
+ console.log("Hello There !");
+ }
+
+ function asyncFoo()
+ {
+ product.search( function(){
+ // access and manipulate the "DemoProduct" BusinessObject's instances here
+ }, null { inlineDocs: true });
+ }
+
+ return {
+ foo: foo,
+ asyncFoo: asyncFoo,
+ };
+})(jQuery);
```
+With such structure, you don't especially need the `render(params, data)` method to be called, and you can declare your variables as you would in a regular function. The only specificity is to actually return all of your functions and variables in order to access them in your *HTML*.
+
+Then accessing the returned methods in the *HTML* would be done similarly to how it is in the below code snippet:
+
+```html
+
+
+
+
+
+```
+
+> ***Note:*** With this method you can't pass variables or constants statically, so you still need to add the fetched datas dynamically.
+
### Manipulating Business Objects
As they are a core element of any Simplicité application, it is important to know how to get and use them properly within any **External Object**. In order to do so you first need to have your app declared above, `let app = $ui.getApp();`, and then the method is `getBusinessObject(string name)`, thanks to which you can fetch all the Business Objects having the wanted `name`.
@@ -272,44 +297,60 @@ And the final step to access your different objects from here is to know how the
![](demo-prd-fields-list.png)
-Now here is how we implemented a simple product fetching & display function within the **CustomWelcomeCard** External Oject using the previously explained methods:
+Now here is how we implemented a simple product fetching & display function within the **CustomWelcomeCard** External Oject using the previously explained methods, so inside the `BusinessObject.search(function(){ ... })` call we can use the following code snippet:
```javascript
-function displayProductsWithin()
+for (let i=0; i').addClass("demowelcomecard-product-card").on("click", () => {
+ // triggers an error but still saves & runs ...
+ $ui.displayForm(null, "DemoProduct", prd.row_id, {
+ nav: "add",
+ target: "work"
+ });
+ });
+ let cardLeft = $('
').addClass("dwc-product-card-left");
+
+ let cardLeftHeader = $('
').addClass("dwc-product-card-left-header");
+ let cardLeftHeaderTitle = $('').addClass("dwc-product-card-left-header-title").text(prd.demoPrdName);
+ let cardLeftHeaderSubtitle = $('').addClass("dwc-product-card-left-header-subtitle").text(prd.demoPrdSupId__demoSupName+" - "+prd.demoPrdType);
+
+ cardLeftHeader
+ .append(cardLeftHeaderTitle)
+ .append(cardLeftHeaderSubtitle);
+
+ let cardLeftFooter = $('
').addClass("dwc-product-card-left-footer");
+ let cardLeftFooterStock = $('').addClass("dwc-product-card-left-footer-stock").text(prd.demoPrdStock+" left in stock.");
+ let cardLeftFooterPrice = $('').addClass("dwc-product-card-left-footer-price").text(prd.demoPrdUnitPrice+"€");
+
+ cardLeftFooter
+ .append(cardLeftFooterStock)
+ .append(cardLeftFooterPrice);
+
+ cardLeft
+ .append(cardLeftHeader)
+ .append(cardLeftFooter);
+
+ let cardRight = $('
').addClass("demowelcomecard-product-card-right");
- productdBusinessObject.search( function() { //Here it is important to put the function here and not outside, so the search() operation is actually done before accessing the object, otherwise you might access a non-updated (empty) version of your object.
- document.getElementById("welcome-list").hidden = false;
-
- console.dir(productdBusinessObject);
-
- for (let i=0; i
-
- . . . `
-);
-
-// ... end of function & rest of code ...
-
-function goToProductForm(prdRowId) {
- $ui.displayForm(null, "DemoProduct", prdRowId, {
- nav: "add",
- target: "work"
- });
-}
+$ui.displayForm(null, "DemoProduct", prd.row_id, {
+ nav: "add",
+ target: "work"
+});
```
-The `display` methods available for **Business Objects** are all working as follows; specify the name of the Business Object, the `row_id` of the specific instance of this object, and then some options as `nav` --that defines the behavior regarding the navigation element (breadcrumb)-- or `target` --that specifies the UI area in which the form will be displayed (`"work"` is the only appropriate for forms, lists etc)--.
+The `display*` methods available for **Business Objects** are all working as follows; specify the name of the Business Object, the `row_id` of the specific instance of this object, and then some options as `nav` --that defines the behavior regarding the navigation element (breadcrumb)-- or `target` --that specifies the UI area in which the form will be displayed (`"work"` is the only appropriate for forms, lists etc)--.
### Getting User Infos
@@ -408,17 +478,17 @@ Keeping the idea of redirecting our user, we will implement a slightly different
let grant = $ui.getGrant(); // can be removed if using $grant
let currentUserLogin = grant.login; // equivalent: $grant.login
-let userBusinessObject = app.getBusinessObject("User");
+let user = app.getBusinessObject("User");
```
Then thanks to the following script, we can easily use the previously fetched informations to properly display the currently logged user's form:
```javascript
-userBusinessObject.search( function(){
- const user = userBusinessObject.list.find(u => u.usr_login === currentUserLogin);
+user.search( function(){
+ const usr = user.list.find(u => u.usr_login === currentUserLogin);
- if (user && user.row_id) {
- $ui.displayForm(null, "User", user.row_id, {
+ if (usr && usr.row_id) {
+ $ui.displayForm(null, "User", usr.row_id, {
nav: "add",
target: "work"
});
@@ -428,10 +498,325 @@ userBusinessObject.search( function(){
}, null, {});
```
-## Final Welcome-Card
+## Final Welcome-Card (V6.1.19)
After all that we should be done with the implementation of our customized Welcome-Card widget ! We so have 3 resource files that should look like this:
+***HTML*** resource file:
+```html
+
+
+
+
+
+
+```
+
+***CLASS*** resource file (script):
+```javascript
+Simplicite.UI.ExternalObjects.DemoWelcomeCard = class extends Simplicite.UI.ExternalObject {
+ async render(params, data = {})
+ {
+ let app = $ui.getApp();
+ let product = app.getBusinessObject("DemoProduct");
+ let user = app.getBusinessObject("User");
+ let login = $ui.getGrant().login;
+
+
+ $("#demowelcomecard-header")
+ .append($('
').text("Welcome to Simplicité's Demo !"))
+ .append($('
').text("We're excited to have you onboard. Explore, interact, and enjoy your experience with us !"));
+
+ $("#demowelcomecard-actions")
+ .append($('').text("Check the Tutorial").attr("id", "tutorial").addClass("demowelcomecard-btn").on("click", () => { window.open("https://docs.simplicite.io/", "_blank"); } ))
+ .append($('').text("Display Products").attr("id","products").addClass("demowelcomecard-btn").on("click", () => {
+ console.log("HELLO MISTER USER");
+ let b = document.getElementById("demowelcomecard-productlist").hidden;
+ document.getElementById("demowelcomecard-productlist").hidden = !b;
+ } ))
+ .append($('').text("See my Infos").attr("id","user-infos").addClass("demowelcomecard-btn").on("click", () => {
+ user.search( function(){
+ const usr = user.list.find(u => u.usr_login === login);
+
+ if (usr && usr.row_id) {
+ $ui.displayForm(null, "User", usr.row_id, {
+ nav: "add",
+ target: "work"
+ });
+ } else {
+ console.error("User not found.");
+ }
+ }, null, {});
+ } ));
+
+ product.search(function() {
+ for (let i=0; i').addClass("demowelcomecard-product-card").on("click", () => {
+ // triggers an error but still saves & runs ...
+ $ui.displayForm(null, "DemoProduct", prd.row_id, {
+ nav: "add",
+ target: "work"
+ });
+ });
+
+ let cardLeft = $('
').addClass("dwc-product-card-left");
+
+ let cardLeftHeader = $('
').addClass("dwc-product-card-left-header");
+ let cardLeftHeaderTitle = $('').addClass("dwc-product-card-left-header-title").text(prd.demoPrdName);
+ let cardLeftHeaderSubtitle = $('').addClass("dwc-product-card-left-header-subtitle").text(prd.demoPrdSupId__demoSupName+" - "+prd.demoPrdType);
+
+ cardLeftHeader
+ .append(cardLeftHeaderTitle)
+ .append(cardLeftHeaderSubtitle);
+
+ let cardLeftFooter = $('
').addClass("dwc-product-card-left-footer");
+ let cardLeftFooterStock = $('').addClass("dwc-product-card-left-footer-stock").text(prd.demoPrdStock+" left in stock.");
+ let cardLeftFooterPrice = $('').addClass("dwc-product-card-left-footer-price").text(prd.demoPrdUnitPrice+"€");
+
+ cardLeftFooter
+ .append(cardLeftFooterStock)
+ .append(cardLeftFooterPrice);
+
+ cardLeft
+ .append(cardLeftHeader)
+ .append(cardLeftFooter);
+
+ let cardRight = $('
@@ -450,7 +835,7 @@ After all that we should be done with the implementation of our customized Welco
***CLASS*** resource file (script):
```javascript
-var CustomWelcomeCard = (function(){
+var CustomWelcomeCard = CustomWelcomeCard || (function(){
let app = $ui.getApp();
let grant = $ui.getGrant();
let productdBusinessObject = app.getBusinessObject("DemoProduct");
@@ -700,4 +1085,4 @@ var CustomWelcomeCard = (function(){
text-align: left;
font-size: 1rem;
}
-```
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-object-form.png b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-object-form.png
index 8c3ab5aa..8bdc8be2 100644
Binary files a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-object-form.png and b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-object-form.png differ
diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-permissions.png b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-permissions.png
index 74a68574..17a60b6f 100644
Binary files a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-permissions.png and b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-permissions.png differ
diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-resources.png b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-resources.png
index 76e929a2..415a5fcf 100644
Binary files a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-resources.png and b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-resources.png differ
diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-result.png b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-result.png
index 1e128d93..16ad7bae 100644
Binary files a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-result.png and b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-card-result.png differ
diff --git a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-product-cards.png b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-product-cards.png
index ef1c22b6..25803e5d 100644
Binary files a/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-product-cards.png and b/content/CTG_50_docs/CTG_25_front/LSN_40_ui-component/welcome-product-cards.png differ