import { mdiContentSave, mdiCog, mdiSync } from '@mdi/js'; import axios from 'axios'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { ReactElement, useCallback, useEffect, useMemo, useState, } from 'react'; import BaseButton from '../../components/BaseButton'; import CardBox from '../../components/CardBox'; import FormField from '../../components/FormField'; import LayoutAuthenticated from '../../layouts/Authenticated'; import SectionMain from '../../components/SectionMain'; import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; import { logger } from '../../lib/logger'; import type { CanvasElementType, ConstructorAsset, AssetOption, } from '../../types/constructor'; // Import shared element settings components import { ElementSettingsTabs, StyleSettingsSection, EffectsSettingsSection, CommonSettingsSection, NavigationSettingsSection, TooltipSettingsSection, DescriptionSettingsSection, MediaSettingsSection, GallerySettingsSection, CarouselSettingsSection, useElementSettingsForm, } from '../../components/ElementSettings'; type ProjectElementDefault = { id: string; element_type: string; name?: string; sort_order?: number; settings_json?: string | Record; source_element_id?: string; snapshot_version?: number; projectId: string; }; type DiffResult = { projectDefault: ProjectElementDefault | null; globalDefault: Record | null; isDifferent: boolean; hasGlobalDefault: boolean; }; const toHumanLabel = (value: string) => String(value || '') .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); const SETTINGS_TABS = [ { id: 'general', label: 'General Settings' }, { id: 'css', label: 'CSS Styles' }, { id: 'effects', label: 'Effects' }, ]; const ProjectElementDefaultDetailsPage = () => { const router = useRouter(); const id = useMemo(() => { const routeValue = router.query.id; if (Array.isArray(routeValue)) return routeValue[0] || ''; return String(routeValue || ''); }, [router.query.id]); const projectId = useMemo(() => { const routeValue = router.query.projectId; if (Array.isArray(routeValue)) return routeValue[0] || ''; return String(routeValue || ''); }, [router.query.projectId]); const [item, setItem] = useState(null); const [name, setName] = useState(''); const [sortOrder, setSortOrder] = useState(0); const [activeTab, setActiveTab] = useState('general'); const [assets, setAssets] = useState([]); const [diff, setDiff] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isResetting, setIsResetting] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [successMessage, setSuccessMessage] = useState(''); const currentElementType = (item?.element_type || '') as CanvasElementType; // Use shared form hook const form = useElementSettingsForm({ elementType: currentElementType }); // Helper functions for asset options const getAssetSourceValue = (asset: ConstructorAsset) => String(asset.storage_key || asset.cdn_url || '').trim(); const getAssetLabel = (asset: ConstructorAsset) => { const baseName = asset.name?.trim() || 'Untitled asset'; const source = getAssetSourceValue(asset); return `${baseName}${source ? ` ยท ${source}` : ''}`; }; // Build icon asset options from project assets const iconAssetOptions: AssetOption[] = useMemo( () => assets .filter( (asset) => asset.type === 'icon' && asset.asset_type === 'image' && getAssetSourceValue(asset), ) .map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset), })), [assets], ); // Extract stable callback reference to avoid infinite loop const applySettings = form.applySettings; const loadItem = useCallback(async () => { if (!id) return; try { setIsLoading(true); setErrorMessage(''); setSuccessMessage(''); const response = await axios.get(`/project-element-defaults/${id}`); const nextItem: ProjectElementDefault | null = response?.data || null; if (!nextItem) { setErrorMessage('Project element default not found.'); setItem(null); return; } setItem(nextItem); setName(String(nextItem.name || '')); setSortOrder(Number(nextItem.sort_order || 0)); applySettings(nextItem.settings_json); // Load project assets for icon selector if (nextItem.projectId) { try { const assetsResponse = await axios.get( `/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${nextItem.projectId}`, ); const assetRows: ConstructorAsset[] = Array.isArray( assetsResponse?.data?.rows, ) ? assetsResponse.data.rows : []; setAssets(assetRows); } catch (assetError) { logger.error( 'Failed to load project assets:', assetError instanceof Error ? assetError : { error: assetError }, ); setAssets([]); } } } catch (error: unknown) { const err = error as { response?: { data?: { message?: string } }; message?: string; }; const message = err?.response?.data?.message || err?.message || 'Failed to load project element default.'; logger.error( 'Failed to load project element default details:', error instanceof Error ? error : { error }, ); setErrorMessage(message); setItem(null); } finally { setIsLoading(false); } }, [applySettings, id]); const loadDiff = useCallback(async () => { if (!id) return; try { const response = await axios.get(`/project-element-defaults/${id}/diff`); setDiff(response?.data || null); } catch (error: unknown) { logger.error( 'Failed to load diff:', error instanceof Error ? error : { error }, ); setDiff(null); } }, [id]); useEffect(() => { if (!router.isReady) return; loadItem(); loadDiff(); }, [loadItem, loadDiff, router.isReady]); // Extract stable callback reference for buildSettingsJson const buildSettingsJson = form.buildSettingsJson; const handleSave = useCallback(async () => { if (!id || !item) return; const settings = buildSettingsJson(); try { setIsSaving(true); setErrorMessage(''); setSuccessMessage(''); await axios.put(`/project-element-defaults/${id}`, { id, data: { element_type: item.element_type, name: name.trim() || item.element_type, sort_order: sortOrder, settings_json: settings, }, }); setSuccessMessage('Project element default saved successfully.'); await loadItem(); await loadDiff(); } catch (error: unknown) { const err = error as { response?: { data?: { message?: string } }; message?: string; }; const message = err?.response?.data?.message || err?.message || 'Failed to save project element default.'; logger.error( 'Failed to save project element default:', error instanceof Error ? error : { error }, ); setErrorMessage(message); } finally { setIsSaving(false); } }, [buildSettingsJson, id, item, name, sortOrder, loadItem, loadDiff]); const handleResetToGlobal = useCallback(async () => { if (!id) return; const confirmed = window.confirm( 'Are you sure you want to reset this element default to the global default? ' + 'This will overwrite your project-specific settings.', ); if (!confirmed) return; try { setIsResetting(true); setErrorMessage(''); setSuccessMessage(''); await axios.post(`/project-element-defaults/${id}/reset`); setSuccessMessage('Successfully reset to global default.'); await loadItem(); await loadDiff(); } catch (error: unknown) { const err = error as { response?: { data?: { message?: string } }; message?: string; }; const message = err?.response?.data?.message || err?.message || 'Failed to reset to global default.'; logger.error( 'Failed to reset to global default:', error instanceof Error ? error : { error }, ); setErrorMessage(message); } finally { setIsResetting(false); } }, [id, loadItem, loadDiff]); // Extract stable callback reference for setField const setField = form.setField; // Handler for style section changes const handleStyleChange = useCallback( (prop: string, value: string) => { setField(prop as keyof typeof form.state, value); }, [setField], ); // Handler for effects section changes const handleEffectChange = useCallback( (prop: string, value: string) => { // Convert string 'true'/'false' to boolean for boolean fields if ( prop === 'hoverReveal' || prop === 'hoverRevealPersist' || prop === 'hoverPersistOnClick' ) { setField(prop as keyof typeof form.state, (value === 'true') as never); } else { setField(prop as keyof typeof form.state, value as never); } }, [setField], ); // Handler for common section changes const handleCommonChange = useCallback( (field: string, value: string) => { setField(field as keyof typeof form.state, value); }, [setField], ); // Handler for type-specific section changes const handleTypeChange = useCallback( (field: string, value: string | boolean) => { setField(field as keyof typeof form.state, value as never); }, [setField], ); if (isLoading) { return ( <> {getPageTitle('Project Element Default')}

Loading...

); } if (!item) { return ( <> {getPageTitle('Project Element Default')}

{errorMessage || 'Project element default not found.'}

Back to list
); } return ( <> {getPageTitle(`Edit: ${item.element_type}`)}
{errorMessage && (
{errorMessage}
)} {successMessage && (
{successMessage}
)} {diff?.isDifferent && (
This project element default has been customized and differs from the global default. {diff.hasGlobalDefault && ' Click "Reset to Global" to restore the global settings.'}
)}
setName(e.target.value)} placeholder={toHumanLabel(item.element_type)} /> setSortOrder(Number(e.target.value) || 0)} /> {/* Tabs */} {/* General Settings Tab */} {activeTab === 'general' && ( <> {/* Type-specific sections */} {form.isNavigationType && ( )} {form.isTooltipType && ( )} {form.isDescriptionType && ( )} {form.isGalleryType && ( )} {form.isCarouselType && ( )} {form.isMediaType && ( )} )} {/* CSS Styles Tab */} {activeTab === 'css' && ( )} {/* Effects Tab */} {activeTab === 'effects' && ( )}
Back to project element defaults list
); }; ProjectElementDefaultDetailsPage.getLayout = function getLayout( page: ReactElement, ) { return ( {page} ); }; export default ProjectElementDefaultDetailsPage;