/o//commerce-media/accounts/-1/images/18646053?download=true
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> 
		**Feature**
* Trigger Microsoft Auth0 login by sending your user to /o/o365/login
* Automatic log on to Microsoft after Liferay login
* Long term token persistence to reduce user re-login
* Easily extend to use the office 365 api in the way you need it
* Access scope management
* API usable from any custom plugin or template
**Feature**
* Trigger Microsoft Auth0 login by sending your user to /o/o365/login
* Automatic log on to Microsoft after Liferay login
* Long term token persistence to reduce user re-login
* Easily extend to use the office 365 api in the way you need it
* Access scope management
* API usable from any custom plugin or template
مطور برامج
Publisher Date
January 24, 2024
Deployment Method
Liferay Self-Hosted
Liferay PaaS
App Type
النسخة
الإصدارات المدعومة
Standard Price
Help and Support
مشاركة الرابط
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.