/o//commerce-media/accounts/-1/images/18648439?download=true
Portal Cache Gogo Command
					
						Analytics and Optimization
					
			
	
	
		テンプレート処理中にエラーが発生しました。	
	
		
				
	
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> 
		説明
	
		This module adds some Gogo commands to interact with Liferay cache just
like the Control Panel section.
All the commands are within the "cache" scope as follows:
- cache:clearDb, clears the database cache
- cache:clearSingle, clears content cached by this VM
- cache:clearMulti, clears content cached across the cluster
- cache:clearServlet, clears the direct servlet cache
- cache:clearAll, invokes all of the commands above
All the commands are within the "cache" scope as follows:
- cache:clearDb, clears the database cache
- cache:clearSingle, clears content cached by this VM
- cache:clearMulti, clears content cached across the cluster
- cache:clearServlet, clears the direct servlet cache
- cache:clearAll, invokes all of the commands above
This module adds some Gogo commands to interact with Liferay cache just
like the Control Panel section.
All the commands are within the "cache" scope as follows:
- cache:clearDb, clears the database cache
- cache:clearSingle, clears content cached by this VM
- cache:clearMulti, clears content cached across the cluster
- cache:clearServlet, clears the direct servlet cache
- cache:clearAll, invokes all of the commands above
All the commands are within the "cache" scope as follows:
- cache:clearDb, clears the database cache
- cache:clearSingle, clears content cached by this VM
- cache:clearMulti, clears content cached across the cluster
- cache:clearServlet, clears the direct servlet cache
- cache:clearAll, invokes all of the commands above
DEVELOPER
開発者
Publisher Date
January 24, 2024
Deployment Method
Liferay Self-Hosted
Liferay PaaS
App Type
 	
 	
 	
 	 DXP 
 	
	
		バージョン
	1.0.0
	
		サポート対象のバージョン
			7.4
	
		Standard Price
			Free
	
	Help and Support
リンクを共有
DEVELOPER
24/11/21 18:41
Published date
24/11/21 18:41
Published Date
24/11/21 18:41
SUPPORTED OFFERINGS
Liferay PaaS
Supported Versions
					7.4
Resource Requirements
Edition
Community
PRICE
Free
help & support
SHARE LINK
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.