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

Dockbar Integration
Identity Management & Governance
18647768
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
The application adds a new custom permission called VIEW_DOCKBAR inside the "General Permissions" section of the role configuration (except for Guest role).
With this new custom permission, frontend developers can leverage the Liferay permission system to show/hide the portal dockbar inside a custom theme.

Here's how to initialize a Freemarker variable inside the init_custom.ftl file of a custom theme:
<#assign viewDockbar = permissionChecker.hasPermission(themeDisplay.scopeGroupId, "90", "90", "VIEW_DOCKBAR") />

And here's how to use the Freemarker variable:
<#if viewDockbar>
<@liferay.control_menu />
</#if>

On Liferay 7.4+ you need to add this property to your portal-ext.properties file in order to make it working:
resource.actions.strict.mode.enabled=false
The application adds a new custom permission called VIEW_DOCKBAR inside the "General Permissions" section of the role configuration (except for Guest role).
With this new custom permission, frontend developers can leverage the Liferay permission system to show/hide the portal dockbar inside a custom theme.

Here's how to initialize a Freemarker variable inside the init_custom.ftl file of a custom theme:
<#assign viewDockbar = permissionChecker.hasPermission(themeDisplay.scopeGroupId, "90", "90", "VIEW_DOCKBAR") />

And here's how to use the Freemarker variable:
<#if viewDockbar>
<@liferay.control_menu />
</#if>

On Liferay 7.4+ you need to add this property to your portal-ext.properties file in order to make it working:
resource.actions.strict.mode.enabled=false
DEVELOPER

Developer


Publisher Date

January 24, 2024


Deployment Method

Liferay Self-Hosted

Liferay PaaS


App Type

DXP

Version

1.0.0

Supported Versions

7.4

Standard Price

Free

Help and Support


Share Link

DEVELOPER
11/21/24 6:40 PM
Published date
11/21/24 6:40 PM
Published Date
11/21/24 6:40 PM
SUPPORTED OFFERINGS
Liferay PaaS
Supported Versions
7.4
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.