/o//commerce-media/accounts/-1/images/18578837?download=true

LAM
DXP App
Data Modeling, Process & Business Logic
18578788
An error occurred while processing the template.
The following has evaluated to null or missing:
==> filteredProductImages[0]  [in template "3192443#3192485#null" at line 27, column 31]

----
Tip: It's the final [] step that caused this error, not those before it.
----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----

----
FTL stack trace ("~" means nesting-related):
	- Failed at: ${filteredProductImages[0].title?html}  [in template "3192443#3192485#null" at line 27, column 29]
----
1<#assign 
2    channel = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?accountId=-1&filter=siteGroupId eq '${themeDisplay.getScopeGroupId()}'") 
3 
4    productImagesResponse = restClient.get( 
5        "/headless-commerce-delivery-catalog/v1.0/channels/" + channel.items[0].id + 
6        "/products/" + CPDefinition_cProductId.getData() + "/images?accountId=-1" 
7
8 
9    allProductImages = productImagesResponse.items![] 
10    filteredProductImages = [] 
11
12 
13<#list allProductImages as image> 
14    <#if image.galleryEnabled?? && image.galleryEnabled> 
15        <#assign filteredProductImages += [image] /> 
16    </#if> 
17</#list> 
18 
19<#assign totalCount = filteredProductImages?size > 
20 
21<div class = "carousel-container"> 
22	<div class = "main-image-wrapper"> 
23		<button class = "nav-button prev" aria-label = "Previous Image"> 
24			<span class = "lexicon-icon-overwide"> <@clay["icon"] symbol = "angle-left" /></span> 
25		</button> 
26 
27		<img alt = "${filteredProductImages[0].title?html}" id = "main-image" src = "${(filteredProductImages[0].src?replace("https://", "http://"))}" /> 
28 
29		<button class="nav-button next" aria-label="Next Image"> 
30			<span class="lexicon-icon-overwide"> <@clay["icon"] symbol="angle-right" /></span> 
31		</button> 
32	</div> 
33 
34	<div class="thumbnails-wrapper"> 
35		<div class="thumbnails align-items-center"></div> 
36 
37		<#assign count = (totalCount?default(0)?number) /> 
38 
39		<#if count gt 5> 
40			<button class="view-full-gallery"> 
41				<span class="title"> 
42					${languageUtil.get(locale, "full-gallery", "Full Gallery")} 
43				</span> 
44				<span class="subtitle"> 
45					${count} ${languageUtil.get(locale, "photos", "Photos")} 
46				</span> 
47			</button> 
48		</#if> 
49		</div> 
50	</div> 
51 
52<template id="modal-gallery"> 
53	<div class="modal-gallery-content"> 
54		<button class="modal-prev" data-role="modal-prev"> 
55			<@clay["icon"] symbol="angle-left" /> 
56		</button> 
57 
58		<img class="modal-image" data-role="modal-image" /> 
59 
60		<button class="modal-next" data-role="modal-next"> 
61			<@clay["icon"] symbol="angle-right" /> 
62		</button> 
63	</div> 
64</template> 
65 
66<script ${nonceAttribute}> 
67(function () { 
68	let currentIndex = 0; 
69	let images = []; 
70 
71	const carouselNextBtn = document.querySelector('.nav-button.next'); 
72	const carouselPrevBtn = document.querySelector('.nav-button.prev'); 
73	const carouselMainImage = document.getElementById('main-image'); 
74	const thumbnailsContainer = document.querySelector('.thumbnails'); 
75	const viewFullGalleryBtn = document.querySelector('.view-full-gallery'); 
76 
77	function loadImages() { 
78		images = [ 
79			<#list filteredProductImages as image> 
80
81				src: "${(image.src?replace('https://', 'http://'))?js_string}", 
82				alt: "${image.title?html?js_string}" 
83			}<#if image_has_next>,</#if> 
84			</#list> 
85
86
87 
88	function renderThumbnails() { 
89		const maxVisible = 5; 
90		let start = currentIndex - 2; 
91 
92		if (start < 0) start = 0; 
93		if (start > images.length - maxVisible) start = Math.max(images.length - maxVisible, 0); 
94 
95		const end = Math.min(images.length, start + maxVisible); 
96 
97		thumbnailsContainer.innerHTML = ''; 
98 
99		for (let i = start; i < end; i++) { 
100			const img = document.createElement('img'); 
101			img.className = 'thumbnail' + (i === currentIndex ? ' selected' : ''); 
102			img.src = images[i].src; 
103			img.alt = images[i].alt; 
104			img.dataset.index = i; 
105			img.addEventListener('click', () => updateMainImage(i)); 
106			thumbnailsContainer.appendChild(img); 
107
108
109 
110	function updateMainImage(index) { 
111		currentIndex = index; 
112		carouselMainImage.src = images[index].src; 
113		carouselMainImage.alt = images[index].alt; 
114 
115		carouselPrevBtn.disabled = index === 0; 
116		carouselNextBtn.disabled = index === images.length - 1; 
117 
118		renderThumbnails(); 
119
120 
121	function setupNavigationButtons() { 
122		carouselPrevBtn.addEventListener('click', () => { 
123			if (currentIndex > 0) updateMainImage(currentIndex - 1); 
124		}); 
125 
126		carouselNextBtn.addEventListener('click', () => { 
127			if (currentIndex < images.length - 1) updateMainImage(currentIndex + 1); 
128		}); 
129
130 
131	function setupModalTriggers() { 
132		carouselMainImage.addEventListener('click', () => openModalGallery(currentIndex)); 
133		if (viewFullGalleryBtn) { 
134				viewFullGalleryBtn.addEventListener('click', () => openModalGallery(currentIndex)); 
135
136
137 
138	function openModalGallery(startIndex) { 
139		let current = startIndex; 
140 
141		const template = document.getElementById('modal-gallery'); 
142		const clone = template.content.cloneNode(true); 
143		const container = document.createElement('div'); 
144		container.appendChild(clone); 
145 
146		Liferay.Util.openModal({ 
147			bodyHTML: container.innerHTML, 
148			center: true, 
149			headerHTML: '<h2 class="modal-gallery-header" id="modal-header-title"><@clay["icon"] symbol="picture"/> ${languageUtil.get(locale, "Image")} <span id="modal-index-display"></span></h2>', 
150			size: "full-screen", 
151			onOpen: () => { 
152				const modalContainer = document.querySelector('.modal-content'); 
153				if (modalContainer) { 
154					modalContainer.classList.add('custom-gallery-modal'); 
155
156 
157				const modalImage = document.querySelector('[data-role="modal-image"]'); 
158				const modalNext = document.querySelector('[data-role="modal-next"]'); 
159				const modalPrev = document.querySelector('[data-role="modal-prev"]'); 
160				const indexDisplay = document.getElementById('modal-index-display'); 
161 
162				function updateModalImage(index) { 
163					const img = images[index]; 
164					modalImage.src = img.src; 
165					modalImage.alt = img.alt; 
166 
167					modalNext.disabled = index === images.length - 1; 
168					modalPrev.disabled = index === 0; 
169 
170					if (indexDisplay) { 
171						indexDisplay.textContent = (index + 1) + ' ${languageUtil.get(locale, "of")} ' + images.length; 
172
173
174 
175				modalNext.addEventListener('click', () => { 
176					if (current < images.length - 1) { 
177						current++; 
178						updateModalImage(current); 
179
180				}); 
181 
182				modalPrev.addEventListener('click', () => { 
183					if (current > 0) { 
184						current--; 
185						updateModalImage(current); 
186
187				}); 
188 
189				updateModalImage(current); 
190
191		}); 
192
193 
194	function main() { 
195		loadImages(); 
196		setupNavigationButtons(); 
197		setupModalTriggers(); 
198		updateMainImage(0); 
199
200 
201	main(); 
202})(); 
203</script> 
204 
205<style ${nonceAttribute}> 
206.carousel-container img { 
207	cursor: pointer; 
208	object-fit: contain; 
209
210 
211.custom-gallery-modal button:disabled { 
212	cursor: default; 
213	opacity: 0.4; 
214	pointer-events: none; 
215
216 
217.custom-gallery-modal { 
218	background-color: #282934 !important; 
219	border-bottom: none; 
220	color: white !important; 
221
222 
223.custom-gallery-modal .liferay-modal-body { 
224	align-items: center; 
225	display: flex; 
226	justify-content: center; 
227	position: relative; 
228
229 
230.custom-gallery-modal .close { 
231	color: white !important; 
232	margin-right: 16px !important; 
233
234 
235.lexicon-icon-overwide .lexicon-icon { 
236	height: 2em; 
237	margin: 0px !important; 
238
239 
240.custom-gallery-modal{ 
241	height: 80% !important; 
242
243 
244.main-image-wrapper { 
245	align-items: center; 
246	display:flex; 
247	justify-content: center; 
248	position: relative; 
249	width: 902px; 
250	height: 454px; 
251
252 
253.main-image-wrapper img { 
254	border-radius: 8px; 
255	max-height: 100%; 
256
257 
258.main-image-wrapper:hover .nav-button { 
259	opacity: 1; 
260	pointer-events: auto; 
261
262 
263.main-image-wrapper:hover .nav-button:disabled{ 
264	cursor: default; 
265	opacity: 0.4; 
266
267 
268.modal-image { 
269	aspect-ratio: 16/9; 
270	object-fit: contain;; 
271	border-radius: 8px; 
272	max-width: 100vh !important; 
273
274 
275.modal-gallery-header .lexicon-icon { 
276	fill: #FFC124 !important; 
277	margin-right: 8px !important; 
278	width: 16px !important; 
279
280 
281.modal-gallery-header { 
282 
283	padding: 16px !important; 
284
285 
286.modal-prev, 
287.modal-next { 
288	align-items: center; 
289	background: rgba(105, 102, 102, 0.4) !important; 
290	border-radius: 50%; 
291	border: none; 
292	display: flex; 
293	font-size: 1.6rem; 
294	justify-content: center; 
295	padding: 14px !important; 
296	position: absolute; 
297	top: 45%; 
298
299 
300.modal-prev .lexicon-icon, 
301.modal-next .lexicon-icon { 
302	margin-top: 0px !important; 
303
304 
305.modal-next { 
306	right: 24px; 
307
308 
309.modal-prev { 
310	left: 24px; 
311
312 
313.nav-button { 
314	background: rgba(0,0,0,0.4); 
315	border-radius: 50%; 
316	border: none; 
317	color: white; 
318	cursor: pointer; 
319	font-size: 1rem; 
320	opacity: 0; 
321	padding: 0 8px; 
322	position: absolute; 
323	top: 50%; 
324	transform: translateY(-50%); 
325	transition: opacity 0.3s ease; 
326	user-select: none; 
327
328 
329.nav-button.prev { 
330	left: 10px; 
331
332 
333.nav-button.next { 
334	right: 10px; 
335
336 
337.thumbnail { 
338	border-radius: 12px; 
339	border: 2px solid transparent; 
340	cursor: pointer; 
341	height: 86px; 
342	object-fit: cover; 
343	opacity: 0.6; 
344	transition: 
345		border-color 250ms ease-out, 
346		opacity 250ms ease-out; 
347	width: 142px; 
348
349 
350.thumbnail.selected, 
351.thumbnail:hover { 
352	border-color: #8FB5FF; 
353	opacity: 1; 
354
355 
356.thumbnail.selected{ 
357	height: 102px; 
358
359 
360.thumbnails { 
361	display: flex; 
362	gap: 8px; 
363	overflow-x: auto; 
364
365 
366.thumbnails-wrapper { 
367	align-items: center; 
368	display: flex; 
369	justify-content: flex-start; 
370	margin-top: 24px; 
371	max-height: 86px; 
372	max-width: 902px; 
373
374 
375.view-full-gallery { 
376	background-color: white; 
377	border-radius: 12px; 
378	border: 1px solid #E2E2E4; 
379	color: #2563eb; 
380	cursor: pointer; 
381	display: flex; 
382	flex-direction: column; 
383	height: 86px; 
384	justify-content: center; 
385	margin-left: 8px; 
386	min-width: 152px; 
387	transition: background-color 0.3s ease, 
388		box-shadow 0.3s ease, 
389		border-color 250ms ease-out; 
390
391 
392.view-full-gallery .subtitle { 
393	color: #6b7280; 
394	font-size: 12px; 
395	font-weight: 400; 
396	line-height: 1; 
397
398 
399.view-full-gallery .title { 
400	font-size: 16px; 
401	font-weight: 600; 
402	line-height: 1; 
403	margin-bottom: 4px; 
404
405 
406.view-full-gallery:hover { 
407	background-color: #f3f4f6; 
408	box-shadow: 0 2px 4px rgb(0 0 0 / 0.1); 
409	border: 2px solid rgb(143, 181, 255); 
410
411</style> 
Description
Liferay Automatic Migrations (LAM) is a tool for developers and portal managers.

LAM is a component that lets you define your configuration and 'tech' content in a DSL and store this properly alongside the code for your project. Just deploy the scripts as a module and the component will make sure the portal is updated with the current version of the DSL scripts. It is quite similar to existing tooling in the SQL world, like Liquibase or Flyway.

In real life, projects that implement a Liferay portal will often have to deal not only with a few particular portlets, or hooks, but deliver the whole package of a working, interacting site. So this also includes configuring roles, user groups, custom fields, page sets, structures, templates etc. There are some tools/techniques available in Liferay but they do not cover the whole spectrum.
LAM is the answer to this burden.

It is a solution both for the flow of the various stages of production deployment (development -> test -> acceptance -> production) as well as for synchronizing the development between project members.

To use it, please follow the steps described in the documentation at https://github.com/finalist/liferay-lam/blob/master/README.md#usage
Liferay Automatic Migrations (LAM) is a tool for developers and portal managers.

LAM is a component that lets you define your configuration and 'tech' content in a DSL and store this properly alongside the code for your project. Just deploy the scripts as a module and the component will make sure the portal is updated with the current version of the DSL scripts. It is quite similar to existing tooling in the SQL world, like Liquibase or Flyway.

In real life, projects that implement a Liferay portal will often have to deal not only with a few particular portlets, or hooks, but deliver the whole package of a working, interacting site. So this also includes configuring roles, user groups, custom fields, page sets, structures, templates etc. There are some tools/techniques available in Liferay but they do not cover the whole spectrum.
LAM is the answer to this burden.

It is a solution both for the flow of the various stages of production deployment (development -> test -> acceptance -> production) as well as for synchronizing the development between project members.

To use it, please follow the steps described in the documentation at https://github.com/finalist/liferay-lam/blob/master/README.md#usage
DEVELOPER

Developer


Publisher Date

January 23, 2024


Deployment Method

Liferay Self-Hosted

Liferay PaaS


App Type

DXP

Version

0.1.1

Supported Versions

7.0

Standard Price

Free

Help and Support


Share Link

DEVELOPER
11/21/24 6:23 PM
Published date
11/21/24 6:23 PM
Published Date
11/21/24 6:23 PM
SUPPORTED OFFERINGS
Liferay PaaS
Supported Versions
7.0
Resource Requirements
Edition
CE
PRICE
Free
help & support
SHARE LINK
Copy & Share

HTML Example

A paragraph is a self-contained unit of a discourse in writing dealing with a particular point or idea. Paragraphs are usually an expected part of formal writing, used to organize longer prose.