updated page dropdown in the constructor
This commit is contained in:
parent
06a29dbf6a
commit
027af5082b
123
frontend/src/components/Constructor/CreatePageModal.tsx
Normal file
123
frontend/src/components/Constructor/CreatePageModal.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* CreatePageModal Component
|
||||
*
|
||||
* Modal dialog for creating new pages with custom name.
|
||||
* Slug is auto-generated from the page name behind the scenes.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import CardBoxModal from '../CardBoxModal';
|
||||
import { sanitizeSlug, buildUniqueSlug } from '../../lib/slugHelpers';
|
||||
|
||||
interface CreatePageModalProps {
|
||||
/** Whether the modal is visible */
|
||||
isActive: boolean;
|
||||
/** Whether page creation is in progress */
|
||||
isCreating: boolean;
|
||||
/** Set of existing slugs for uniqueness validation */
|
||||
existingSlugs: Set<string>;
|
||||
/** Suggested page number for default name */
|
||||
suggestedPageNumber: number;
|
||||
/** Called when user confirms with valid name and slug */
|
||||
onConfirm: (pageName: string, slug: string) => void;
|
||||
/** Called when user cancels */
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for creating new pages - only asks for name, slug is auto-generated
|
||||
*/
|
||||
const CreatePageModal: React.FC<CreatePageModalProps> = ({
|
||||
isActive,
|
||||
isCreating,
|
||||
existingSlugs,
|
||||
suggestedPageNumber,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [pageName, setPageName] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
const defaultName = `Page ${suggestedPageNumber}`;
|
||||
setPageName(defaultName);
|
||||
setNameError('');
|
||||
}
|
||||
}, [isActive, suggestedPageNumber]);
|
||||
|
||||
// Validate name
|
||||
const nameValidationError = useMemo(() => {
|
||||
const trimmed = pageName.trim();
|
||||
if (!trimmed) return 'Page name is required';
|
||||
if (trimmed.length > 255) return 'Page name must be 255 characters or less';
|
||||
return '';
|
||||
}, [pageName]);
|
||||
|
||||
// Auto-generate unique slug from name
|
||||
const generatedSlug = useMemo(() => {
|
||||
const baseSlug = sanitizeSlug(pageName) || 'page';
|
||||
return buildUniqueSlug(baseSlug, existingSlugs);
|
||||
}, [pageName, existingSlugs]);
|
||||
|
||||
const handleNameChange = useCallback((value: string) => {
|
||||
setPageName(value);
|
||||
setNameError('');
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (nameValidationError) {
|
||||
setNameError(nameValidationError);
|
||||
return;
|
||||
}
|
||||
|
||||
onConfirm(pageName.trim(), generatedSlug);
|
||||
}, [pageName, generatedSlug, nameValidationError, onConfirm]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (!isCreating) {
|
||||
onCancel();
|
||||
}
|
||||
}, [isCreating, onCancel]);
|
||||
|
||||
const isConfirmDisabled = isCreating || Boolean(nameValidationError);
|
||||
|
||||
return (
|
||||
<CardBoxModal
|
||||
title="Create page"
|
||||
buttonColor="info"
|
||||
buttonLabel={isCreating ? 'Creating...' : 'Create'}
|
||||
isConfirmDisabled={isConfirmDisabled}
|
||||
isActive={isActive}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={isCreating ? undefined : handleCancel}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="create-page-name"
|
||||
className="block text-sm font-semibold mb-1"
|
||||
>
|
||||
Page name
|
||||
</label>
|
||||
<input
|
||||
id="create-page-name"
|
||||
type="text"
|
||||
value={pageName}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
placeholder="Enter page name"
|
||||
className="w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800"
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
/>
|
||||
{(nameError || nameValidationError) && (
|
||||
<p className="text-xs text-red-600 mt-1">
|
||||
{nameError || nameValidationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardBoxModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePageModal;
|
||||
@ -6,6 +6,7 @@ import {
|
||||
mdiViewDashboard,
|
||||
mdiChevronDown,
|
||||
mdiChevronUp,
|
||||
mdiPencil,
|
||||
} from '@mdi/js';
|
||||
import Icon from '@mdi/react';
|
||||
import axios from 'axios';
|
||||
@ -22,7 +23,7 @@ import { getPageTitle } from '../config';
|
||||
import { hasPermission } from '../helpers/userPermissions';
|
||||
import { useAppSelector, useAppDispatch } from '../stores/hooks';
|
||||
import { logger } from '../lib/logger';
|
||||
import { sanitizeSlug, buildUniqueSlug, slugPattern } from '../lib/slugHelpers';
|
||||
import { sanitizeSlug, buildUniqueSlug } from '../lib/slugHelpers';
|
||||
import {
|
||||
toRoutePath,
|
||||
compareRoutes,
|
||||
@ -114,10 +115,15 @@ const TourFlowManager = () => {
|
||||
const [isCreatingPage, setIsCreatingPage] = useState(false);
|
||||
const [isCreatingTransition, setIsCreatingTransition] = useState(false);
|
||||
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
|
||||
const [newPageSlug, setNewPageSlug] = useState('');
|
||||
const [newPageSlugError, setNewPageSlugError] = useState('');
|
||||
const [newPageName, setNewPageName] = useState('');
|
||||
const [deletingId, setDeletingId] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
// Edit page modal state
|
||||
const [isEditPageModalActive, setIsEditPageModalActive] = useState(false);
|
||||
const [editingPageId, setEditingPageId] = useState('');
|
||||
const [editPageName, setEditPageName] = useState('');
|
||||
const [editPageNameError, setEditPageNameError] = useState('');
|
||||
const [isSavingPageName, setIsSavingPageName] = useState(false);
|
||||
|
||||
// Use selector for current project's dev transition settings
|
||||
const projectTransitionSettingsEntity = useAppSelector((state) =>
|
||||
@ -337,15 +343,25 @@ const TourFlowManager = () => {
|
||||
[pages, targetEnvironment],
|
||||
);
|
||||
|
||||
const slugValidationError = useMemo(() => {
|
||||
const slug = newPageSlug.trim();
|
||||
if (!slug) return 'Slug is required.';
|
||||
if (!slugPattern.test(slug))
|
||||
return 'Use lowercase letters, numbers, and hyphens only.';
|
||||
if (pageSlugsInEnvironment.has(slug))
|
||||
return 'This slug already exists in the selected environment.';
|
||||
const nameValidationError = useMemo(() => {
|
||||
const name = newPageName.trim();
|
||||
if (!name) return 'Page name is required.';
|
||||
if (name.length > 255) return 'Page name must be 255 characters or less.';
|
||||
return '';
|
||||
}, [newPageSlug, pageSlugsInEnvironment]);
|
||||
}, [newPageName]);
|
||||
|
||||
// Auto-generate unique slug from name
|
||||
const generatedSlug = useMemo(() => {
|
||||
const baseSlug = sanitizeSlug(newPageName) || 'page';
|
||||
return buildUniqueSlug(baseSlug, pageSlugsInEnvironment);
|
||||
}, [newPageName, pageSlugsInEnvironment]);
|
||||
|
||||
const editNameValidationError = useMemo(() => {
|
||||
const name = editPageName.trim();
|
||||
if (!name) return 'Page name is required.';
|
||||
if (name.length > 255) return 'Page name must be 255 characters or less.';
|
||||
return '';
|
||||
}, [editPageName]);
|
||||
|
||||
const nextPageNumber = useMemo(() => pages.length + 1, [pages.length]);
|
||||
|
||||
@ -407,24 +423,113 @@ const TourFlowManager = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedSlug = buildUniqueSlug(
|
||||
`page-${nextPageNumber}`,
|
||||
pageSlugsInEnvironment,
|
||||
);
|
||||
setNewPageSlug(suggestedSlug);
|
||||
setNewPageSlugError('');
|
||||
const suggestedName = `Page ${nextPageNumber}`;
|
||||
setNewPageName(suggestedName);
|
||||
setIsCreatePageModalActive(true);
|
||||
};
|
||||
|
||||
const handleSlugChange = (value: string) => {
|
||||
setNewPageSlug(value);
|
||||
setNewPageSlugError('');
|
||||
const handleNameChange = (value: string) => {
|
||||
setNewPageName(value);
|
||||
};
|
||||
|
||||
const closeCreatePageModal = () => {
|
||||
setIsCreatePageModalActive(false);
|
||||
setNewPageSlug('');
|
||||
setNewPageSlugError('');
|
||||
setNewPageName('');
|
||||
};
|
||||
|
||||
// Edit page modal handlers
|
||||
const openEditPageModal = (event: React.MouseEvent, page: TourPage) => {
|
||||
event.stopPropagation();
|
||||
setEditingPageId(page.id);
|
||||
setEditPageName(page.name || '');
|
||||
setEditPageNameError('');
|
||||
setIsEditPageModalActive(true);
|
||||
};
|
||||
|
||||
const handleEditNameChange = (value: string) => {
|
||||
setEditPageName(value);
|
||||
setEditPageNameError('');
|
||||
};
|
||||
|
||||
const closeEditPageModal = () => {
|
||||
setIsEditPageModalActive(false);
|
||||
setEditingPageId('');
|
||||
setEditPageName('');
|
||||
setEditPageNameError('');
|
||||
};
|
||||
|
||||
const handleSavePageName = async () => {
|
||||
if (editNameValidationError) {
|
||||
setEditPageNameError(editNameValidationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSavingPageName(true);
|
||||
setEditPageNameError('');
|
||||
|
||||
// Fetch full page data from API to preserve all fields
|
||||
// The backend's getFieldMapping converts missing fields to null
|
||||
const pageResponse = await axios.get(`/tour_pages/${editingPageId}`);
|
||||
const fullPageData = pageResponse.data;
|
||||
|
||||
if (!fullPageData) {
|
||||
setEditPageNameError('Page not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send all existing fields with only name changed
|
||||
await axios.put(`/tour_pages/${editingPageId}`, {
|
||||
id: editingPageId,
|
||||
data: {
|
||||
environment: fullPageData.environment,
|
||||
source_key: fullPageData.source_key,
|
||||
name: editPageName.trim(),
|
||||
slug: fullPageData.slug,
|
||||
sort_order: fullPageData.sort_order,
|
||||
background_image_url: fullPageData.background_image_url,
|
||||
background_video_url: fullPageData.background_video_url,
|
||||
background_audio_url: fullPageData.background_audio_url,
|
||||
background_loop: fullPageData.background_loop,
|
||||
background_video_autoplay: fullPageData.background_video_autoplay,
|
||||
background_video_loop: fullPageData.background_video_loop,
|
||||
background_video_muted: fullPageData.background_video_muted,
|
||||
background_video_start_time: fullPageData.background_video_start_time,
|
||||
background_video_end_time: fullPageData.background_video_end_time,
|
||||
design_width: fullPageData.design_width,
|
||||
design_height: fullPageData.design_height,
|
||||
requires_auth: fullPageData.requires_auth,
|
||||
ui_schema_json: fullPageData.ui_schema_json,
|
||||
},
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setPages((prev) =>
|
||||
prev.map((page) =>
|
||||
page.id === editingPageId
|
||||
? { ...page, name: editPageName.trim() }
|
||||
: page,
|
||||
),
|
||||
);
|
||||
|
||||
closeEditPageModal();
|
||||
toast.success('Page name updated');
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { message?: string } };
|
||||
};
|
||||
const message =
|
||||
axiosError?.response?.data?.message ||
|
||||
(error instanceof Error ? error.message : null) ||
|
||||
'Failed to update page name.';
|
||||
setEditPageNameError(message);
|
||||
logger.error(
|
||||
'Failed to update page name:',
|
||||
error instanceof Error ? error : { error },
|
||||
);
|
||||
} finally {
|
||||
setIsSavingPageName(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePage = async () => {
|
||||
@ -433,23 +538,22 @@ const TourFlowManager = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const slug = newPageSlug.trim();
|
||||
const validationError = slugValidationError || newPageSlugError;
|
||||
if (validationError) {
|
||||
setNewPageSlugError(validationError);
|
||||
const name = newPageName.trim();
|
||||
|
||||
// Validate name
|
||||
if (nameValidationError) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingPage(true);
|
||||
setErrorMessage('');
|
||||
setNewPageSlugError('');
|
||||
const payload = {
|
||||
project: activeProjectId,
|
||||
environment: targetEnvironment,
|
||||
source_key: '',
|
||||
name: `Page ${nextPageNumber}`,
|
||||
slug,
|
||||
name,
|
||||
slug: generatedSlug,
|
||||
sort_order:
|
||||
Math.max(...pages.map((item) => Number(item.sort_order || 0)), 0) + 1,
|
||||
background_image_url: '',
|
||||
@ -461,8 +565,7 @@ const TourFlowManager = () => {
|
||||
};
|
||||
|
||||
await axios.post('/tour_pages', { data: payload });
|
||||
setIsCreatePageModalActive(false);
|
||||
setNewPageSlug('');
|
||||
closeCreatePageModal();
|
||||
await loadData();
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
@ -473,7 +576,6 @@ const TourFlowManager = () => {
|
||||
(error instanceof Error ? error.message : null) ||
|
||||
'Failed to create page.';
|
||||
setErrorMessage(message);
|
||||
setNewPageSlugError(message);
|
||||
logger.error(
|
||||
'Failed to create page:',
|
||||
error instanceof Error ? error : { error },
|
||||
@ -776,33 +878,66 @@ const TourFlowManager = () => {
|
||||
title='Create page'
|
||||
buttonColor='info'
|
||||
buttonLabel={isCreatingPage ? 'Creating...' : 'Create'}
|
||||
isConfirmDisabled={Boolean(slugValidationError) || isCreatingPage}
|
||||
isConfirmDisabled={Boolean(nameValidationError) || isCreatingPage}
|
||||
isActive={isCreatePageModalActive}
|
||||
onConfirm={handleCreatePage}
|
||||
onCancel={isCreatingPage ? undefined : closeCreatePageModal}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='new-page-slug'
|
||||
htmlFor='new-page-name'
|
||||
className='block text-sm font-semibold mb-1'
|
||||
>
|
||||
Page slug
|
||||
Page name
|
||||
</label>
|
||||
<input
|
||||
id='new-page-slug'
|
||||
id='new-page-name'
|
||||
type='text'
|
||||
value={newPageSlug}
|
||||
onChange={(event) => handleSlugChange(event.target.value)}
|
||||
placeholder='my-page-slug'
|
||||
value={newPageName}
|
||||
onChange={(event) => handleNameChange(event.target.value)}
|
||||
placeholder='Enter page name'
|
||||
className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800'
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
/>
|
||||
<p className='text-xs text-gray-500 mt-2'>
|
||||
Use lowercase letters, numbers, and hyphens.
|
||||
</p>
|
||||
{(newPageSlugError || slugValidationError) && (
|
||||
<p className='text-xs text-red-600 mt-2'>
|
||||
{newPageSlugError || slugValidationError}
|
||||
{nameValidationError && (
|
||||
<p className='text-xs text-red-600 mt-1'>
|
||||
{nameValidationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardBoxModal>
|
||||
|
||||
{/* Edit Page Name Modal */}
|
||||
<CardBoxModal
|
||||
title='Edit page name'
|
||||
buttonColor='info'
|
||||
buttonLabel={isSavingPageName ? 'Saving...' : 'Save'}
|
||||
isConfirmDisabled={Boolean(editNameValidationError) || isSavingPageName}
|
||||
isActive={isEditPageModalActive}
|
||||
onConfirm={handleSavePageName}
|
||||
onCancel={isSavingPageName ? undefined : closeEditPageModal}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='edit-page-name'
|
||||
className='block text-sm font-semibold mb-1'
|
||||
>
|
||||
Page name
|
||||
</label>
|
||||
<input
|
||||
id='edit-page-name'
|
||||
type='text'
|
||||
value={editPageName}
|
||||
onChange={(event) => handleEditNameChange(event.target.value)}
|
||||
placeholder='Enter page name'
|
||||
className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800'
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
/>
|
||||
{(editPageNameError || editNameValidationError) && (
|
||||
<p className='text-xs text-red-600 mt-1'>
|
||||
{editPageNameError || editNameValidationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -829,6 +964,9 @@ const TourFlowManager = () => {
|
||||
const canDelete =
|
||||
entry.type === 'page' ? canDeletePage : canDeleteTransition;
|
||||
const isDeleting = deletingId === entry.id;
|
||||
const pageData = entry.type === 'page'
|
||||
? pages.find((p) => p.id === entry.id)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<li key={`${entry.type}-${entry.id}`}>
|
||||
@ -854,7 +992,7 @@ const TourFlowManager = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='pr-8'>
|
||||
<div className='pr-20'>
|
||||
<p className='text-xs uppercase text-gray-500 mb-1'>
|
||||
{entry.description}
|
||||
</p>
|
||||
@ -864,17 +1002,29 @@ const TourFlowManager = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
className='!absolute top-3 right-3'
|
||||
icon={mdiClose}
|
||||
color='danger'
|
||||
outline
|
||||
small
|
||||
onClick={(event) =>
|
||||
handleDelete(event, entry.id, entry.type)
|
||||
}
|
||||
disabled={!canDelete || isDeleting}
|
||||
/>
|
||||
<div className='absolute top-3 right-3 flex gap-1'>
|
||||
{entry.type === 'page' && pageData && (
|
||||
<BaseButton
|
||||
icon={mdiPencil}
|
||||
color='info'
|
||||
outline
|
||||
small
|
||||
onClick={(event) =>
|
||||
openEditPageModal(event, pageData)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<BaseButton
|
||||
icon={mdiClose}
|
||||
color='danger'
|
||||
outline
|
||||
small
|
||||
onClick={(event) =>
|
||||
handleDelete(event, entry.id, entry.type)
|
||||
}
|
||||
disabled={!canDelete || isDeleting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@ -105,9 +105,16 @@ export function useElementWrapperStyle({
|
||||
// Build inline style from element properties
|
||||
const inlineStyle = buildElementStyle(element);
|
||||
|
||||
// If element has configured padding, use it exclusively (skip default paddingStyle)
|
||||
// This avoids React warning about conflicting shorthand/non-shorthand properties
|
||||
const finalStyle =
|
||||
'padding' in inlineStyle
|
||||
? inlineStyle
|
||||
: { ...paddingStyle, ...inlineStyle };
|
||||
|
||||
return {
|
||||
className: classNames,
|
||||
style: { ...paddingStyle, ...inlineStyle },
|
||||
style: finalStyle,
|
||||
};
|
||||
}, [element, isSelected, isEditMode]);
|
||||
}
|
||||
|
||||
@ -80,8 +80,8 @@ interface UseConstructorPageActionsResult {
|
||||
saveConstructor: () => Promise<void>;
|
||||
/** Save dev content to stage environment */
|
||||
saveToStage: () => Promise<void>;
|
||||
/** Create a new page */
|
||||
createPage: () => Promise<void>;
|
||||
/** Create a new page with the given name and slug */
|
||||
createPage: (pageName: string, slug: string) => Promise<void>;
|
||||
/** Create a transition (legacy - transitions are now stored on elements) */
|
||||
createTransition: (params: {
|
||||
name?: string;
|
||||
@ -256,24 +256,33 @@ export function useConstructorPageActions({
|
||||
}
|
||||
}, [projectId, saveConstructor, onError, onSuccess]);
|
||||
|
||||
const createPage = useCallback(async () => {
|
||||
const createPage = useCallback(async (pageName: string, slug: string) => {
|
||||
if (!projectId) {
|
||||
onError?.('Project is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pageName.trim()) {
|
||||
onError?.('Page name is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slug.trim()) {
|
||||
onError?.('Page slug is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSortOrder = Math.max(
|
||||
0,
|
||||
...pages.map((item) => Number(item.sort_order || 0)),
|
||||
);
|
||||
const nextPageNumber = pages.length + 1;
|
||||
|
||||
const payload = {
|
||||
project: projectId,
|
||||
environment: activePage?.environment || 'dev',
|
||||
source_key: '',
|
||||
name: `Page ${nextPageNumber}`,
|
||||
slug: `page-${nextPageNumber}-${Date.now().toString().slice(-4)}`,
|
||||
name: pageName.trim(),
|
||||
slug: slug.trim(),
|
||||
sort_order: maxSortOrder + 1,
|
||||
background_image_url: '',
|
||||
background_video_url: '',
|
||||
|
||||
@ -47,17 +47,14 @@ export function sanitizeSlug(value: string): string {
|
||||
*/
|
||||
export function buildUniqueSlug(
|
||||
baseValue: string,
|
||||
usedSlugs: Set<string>,
|
||||
_usedSlugs: Set<string>,
|
||||
): string {
|
||||
const baseSlug = sanitizeSlug(baseValue) || 'page';
|
||||
if (!usedSlugs.has(baseSlug)) return baseSlug;
|
||||
|
||||
let suffix = 2;
|
||||
while (usedSlugs.has(`${baseSlug}-${suffix}`)) {
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
return `${baseSlug}-${suffix}`;
|
||||
// Always append timestamp to guarantee uniqueness
|
||||
// (_usedSlugs kept for API compatibility but not used - cached data may be stale)
|
||||
const timestamp = Date.now().toString(36);
|
||||
return `${baseSlug}-${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -19,6 +19,7 @@ import TransitionPreviewOverlay from '../components/Constructor/TransitionPrevie
|
||||
import CanvasElementComponent from '../components/Constructor/CanvasElement';
|
||||
import GalleryCarouselOverlay from '../components/UiElements/GalleryCarouselOverlay';
|
||||
import ElementEditorPanel from '../components/Constructor/ElementEditorPanel';
|
||||
import CreatePageModal from '../components/Constructor/CreatePageModal';
|
||||
import { BackdropPortalProvider } from '../components/BackdropPortal';
|
||||
import { getPageTitle } from '../config';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
@ -286,6 +287,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
currentElementTransitionSettings,
|
||||
setCurrentElementTransitionSettings,
|
||||
] = useState<ElementTransitionSettings | null>(null);
|
||||
// Create page modal state
|
||||
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
|
||||
|
||||
const isConstructorEditMode = constructorInteractionMode === 'edit';
|
||||
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
||||
@ -368,6 +371,20 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
[activePageId, pages],
|
||||
);
|
||||
|
||||
// Existing page slugs for uniqueness validation (current environment only)
|
||||
const existingSlugs = useMemo(() => {
|
||||
const targetEnv = activePage?.environment || 'dev';
|
||||
return new Set(
|
||||
pages
|
||||
.filter((p) => (p.environment || 'dev') === targetEnv)
|
||||
.map((p) => p.slug || '')
|
||||
.filter(Boolean),
|
||||
);
|
||||
}, [pages, activePage?.environment]);
|
||||
|
||||
// Suggested page number for new page name
|
||||
const suggestedPageNumber = useMemo(() => pages.length + 1, [pages.length]);
|
||||
|
||||
// Last project-level save: most recent updatedAt across all pages
|
||||
const lastProjectSaveAt = useMemo(() => {
|
||||
if (!pages.length) return null;
|
||||
@ -765,6 +782,25 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
await refreshPublishStatus();
|
||||
}, [saveToStage, refreshPublishStatus]);
|
||||
|
||||
// Handlers for create page modal
|
||||
const handleShowCreatePageModal = useCallback(async () => {
|
||||
// Refresh pages data to ensure existingSlugs is up to date
|
||||
await refetchData();
|
||||
setIsCreatePageModalActive(true);
|
||||
}, [refetchData]);
|
||||
|
||||
const handleCreatePageWithName = useCallback(
|
||||
async (pageName: string, slug: string) => {
|
||||
await createPage(pageName, slug);
|
||||
setIsCreatePageModalActive(false);
|
||||
},
|
||||
[createPage],
|
||||
);
|
||||
|
||||
const handleCloseCreatePageModal = useCallback(() => {
|
||||
setIsCreatePageModalActive(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady || typeof window === 'undefined') return;
|
||||
|
||||
@ -1077,31 +1113,22 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
updateBackgroundFromPage,
|
||||
]);
|
||||
|
||||
// Separate effect for initial background loading (matches RuntimePresentation pattern)
|
||||
// This effect ONLY handles initial page load when backgrounds are empty.
|
||||
// switchToPage handles all subsequent navigation by calling navNavigateToPage directly.
|
||||
// Keeping this separate prevents race conditions where state updates trigger
|
||||
// this effect before activePageId has been updated via applyPageSelection.
|
||||
// Separate effect for background loading when activePage changes.
|
||||
// This handles both initial page load AND page switches (e.g., after creating a new page).
|
||||
// The lastInitializedPageIdRef prevents redundant calls for the same page.
|
||||
useEffect(() => {
|
||||
if (!activePage || lastInitializedPageIdRef.current === activePage.id)
|
||||
return;
|
||||
|
||||
// Only initialize when backgrounds are EMPTY (initial load)
|
||||
if (!navCurrentBgImageUrl && !navCurrentBgVideoUrl) {
|
||||
lastInitializedPageIdRef.current = activePage.id;
|
||||
navNavigateToPage({
|
||||
id: activePage.id,
|
||||
background_image_url: activePage.background_image_url,
|
||||
background_video_url: activePage.background_video_url,
|
||||
background_audio_url: activePage.background_audio_url,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
activePage,
|
||||
navCurrentBgImageUrl,
|
||||
navCurrentBgVideoUrl,
|
||||
navNavigateToPage,
|
||||
]);
|
||||
// Update navigation state with new page's backgrounds
|
||||
lastInitializedPageIdRef.current = activePage.id;
|
||||
navNavigateToPage({
|
||||
id: activePage.id,
|
||||
background_image_url: activePage.background_image_url,
|
||||
background_video_url: activePage.background_video_url,
|
||||
background_audio_url: activePage.background_audio_url,
|
||||
});
|
||||
}, [activePage, navNavigateToPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (allowedNavigationTypes.length !== 1) return;
|
||||
@ -1735,7 +1762,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
onSelectMenuItem={selectMenuItemForEdit}
|
||||
allowedNavigationTypes={allowedNavigationTypes}
|
||||
onAddElement={addElement}
|
||||
onCreatePage={createPage}
|
||||
onCreatePage={handleShowCreatePageModal}
|
||||
isCreatingPage={isCreatingPage}
|
||||
onSave={saveConstructor}
|
||||
onSaveToStage={handleSaveToStage}
|
||||
@ -1844,7 +1871,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
isCreatingPage ? 'Creating...' : 'Create First Page'
|
||||
}
|
||||
icon={mdiPlus}
|
||||
onClick={createPage}
|
||||
onClick={handleShowCreatePageModal}
|
||||
disabled={isCreatingPage}
|
||||
/>
|
||||
</div>
|
||||
@ -1962,6 +1989,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create Page Modal */}
|
||||
<CreatePageModal
|
||||
isActive={isCreatePageModalActive}
|
||||
isCreating={isCreatingPage}
|
||||
existingSlugs={existingSlugs}
|
||||
suggestedPageNumber={suggestedPageNumber}
|
||||
onConfirm={handleCreatePageWithName}
|
||||
onCancel={handleCloseCreatePageModal}
|
||||
/>
|
||||
|
||||
<style jsx>{`
|
||||
.menu-action-btn {
|
||||
width: 100%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user