added pages copy and deletion functionality
This commit is contained in:
parent
788342808f
commit
cd588bac8b
@ -199,6 +199,23 @@ router.post(
|
||||
}),
|
||||
);
|
||||
|
||||
// POST - Duplicate a dev page within a project
|
||||
router.post(
|
||||
'/:id/duplicate',
|
||||
wrapAsync(async (req, res) => {
|
||||
if (!isUuidV4(req.params.id)) {
|
||||
return res.status(400).send('Invalid tour_pages id');
|
||||
}
|
||||
|
||||
const payload = await Tour_pagesService.duplicatePage(
|
||||
req.params.id,
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
);
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
// PUT - Update
|
||||
router.put(
|
||||
'/:id',
|
||||
|
||||
@ -14,6 +14,7 @@ const ValidationError = require('./notifications/errors/validation');
|
||||
const videoProcessing = require('./videoProcessing');
|
||||
const { logger } = require('../utils/logger');
|
||||
const db = require('../db/models');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const projectRegenInProgress = new Set();
|
||||
const singleReverseGenerationInProgress = new Set();
|
||||
@ -23,6 +24,81 @@ const DEFAULT_AUTO_REVERSE_FALLBACK_FPS = 30;
|
||||
const YUV420_BYTES_PER_PIXEL = 1.5;
|
||||
const MAX_AUTO_REVERSE_ESTIMATED_DECODED_BYTES = 2 * 1024 * 1024 * 1024;
|
||||
|
||||
const createLocalElementId = () => crypto.randomUUID();
|
||||
|
||||
const sanitizeSlug = (value) => {
|
||||
const slug = String(value || '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
return slug || 'page';
|
||||
};
|
||||
|
||||
const buildCopyName = (name) => {
|
||||
const baseName = String(name || 'Page').trim() || 'Page';
|
||||
const copyName = `${baseName} Copy`;
|
||||
return copyName.length > 255 ? copyName.slice(0, 255) : copyName;
|
||||
};
|
||||
|
||||
const parseUiSchema = (uiSchema) => {
|
||||
if (!uiSchema) return null;
|
||||
if (typeof uiSchema !== 'string') return JSON.parse(JSON.stringify(uiSchema));
|
||||
|
||||
try {
|
||||
return JSON.parse(uiSchema);
|
||||
} catch {
|
||||
return uiSchema;
|
||||
}
|
||||
};
|
||||
|
||||
const regenerateNestedItemIds = (items) => {
|
||||
if (!Array.isArray(items)) return items;
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
id: createLocalElementId(),
|
||||
}));
|
||||
};
|
||||
|
||||
const regenerateElementInstanceIds = (uiSchema) => {
|
||||
if (!uiSchema || typeof uiSchema !== 'object') return uiSchema;
|
||||
|
||||
const clonedSchema = JSON.parse(JSON.stringify(uiSchema));
|
||||
if (!Array.isArray(clonedSchema.elements)) return clonedSchema;
|
||||
|
||||
clonedSchema.elements = clonedSchema.elements.map((element) => {
|
||||
const clonedElement = {
|
||||
...element,
|
||||
id: createLocalElementId(),
|
||||
};
|
||||
|
||||
clonedElement.galleryCards = regenerateNestedItemIds(
|
||||
clonedElement.galleryCards,
|
||||
);
|
||||
clonedElement.galleryInfoSpans = regenerateNestedItemIds(
|
||||
clonedElement.galleryInfoSpans,
|
||||
);
|
||||
clonedElement.carouselSlides = regenerateNestedItemIds(
|
||||
clonedElement.carouselSlides,
|
||||
);
|
||||
|
||||
if (Array.isArray(clonedElement.infoPanelSections)) {
|
||||
clonedElement.infoPanelSections = clonedElement.infoPanelSections.map(
|
||||
(section) => ({
|
||||
...section,
|
||||
id: `section-${createLocalElementId()}`,
|
||||
spans: regenerateNestedItemIds(section.spans),
|
||||
images: regenerateNestedItemIds(section.images),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return clonedElement;
|
||||
});
|
||||
|
||||
return clonedSchema;
|
||||
};
|
||||
|
||||
// Create base service from factory
|
||||
const BaseService = createEntityService(Tour_pagesDBApi, {
|
||||
entityName: 'tour_pages',
|
||||
@ -238,6 +314,126 @@ class TourPagesService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
static async buildUniqueDuplicateSlug({
|
||||
projectId,
|
||||
environment,
|
||||
preferredSlug,
|
||||
sourceSlug,
|
||||
transaction,
|
||||
}) {
|
||||
const baseSlug = sanitizeSlug(
|
||||
preferredSlug || `${sourceSlug || 'page'} copy`,
|
||||
);
|
||||
let candidate = baseSlug;
|
||||
let counter = 2;
|
||||
|
||||
while (counter < 1000) {
|
||||
const existingPage = await db.tour_pages.findOne({
|
||||
where: { projectId, environment, slug: candidate },
|
||||
attributes: ['id'],
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!existingPage) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate = `${baseSlug}-${counter}`;
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
return `${baseSlug}-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
static async duplicatePage(sourcePageId, data = {}, currentUser) {
|
||||
if (!sourcePageId) {
|
||||
throw new ValidationError('Source page is required');
|
||||
}
|
||||
|
||||
return db.sequelize.transaction(async (transaction) => {
|
||||
const sourcePage = await db.tour_pages.findOne({
|
||||
where: { id: sourcePageId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!sourcePage) {
|
||||
throw new ValidationError('Source page not found');
|
||||
}
|
||||
|
||||
const source = sourcePage.get({ plain: true });
|
||||
const projectId = data.projectId || data.project || source.projectId;
|
||||
const environment = data.environment || source.environment || 'dev';
|
||||
|
||||
if (source.projectId !== projectId) {
|
||||
throw new ValidationError(
|
||||
'Source page does not belong to the selected project',
|
||||
);
|
||||
}
|
||||
|
||||
if (source.environment !== 'dev' || environment !== 'dev') {
|
||||
throw new ValidationError(
|
||||
'Pages can only be duplicated in the dev environment. Use Save to Stage and Publish to update presentations.',
|
||||
);
|
||||
}
|
||||
|
||||
const maxSortOrder = await db.tour_pages.max('sort_order', {
|
||||
where: { projectId, environment },
|
||||
transaction,
|
||||
});
|
||||
const slug = await TourPagesService.buildUniqueDuplicateSlug({
|
||||
projectId,
|
||||
environment,
|
||||
preferredSlug: data.slug,
|
||||
sourceSlug: source.slug,
|
||||
transaction,
|
||||
});
|
||||
|
||||
const uiSchema = regenerateElementInstanceIds(
|
||||
parseUiSchema(source.ui_schema_json),
|
||||
);
|
||||
const requestedName = String(data.name || '').trim();
|
||||
const duplicatePayload = {
|
||||
project: projectId,
|
||||
environment,
|
||||
source_key: '',
|
||||
name: requestedName
|
||||
? requestedName.slice(0, 255)
|
||||
: buildCopyName(source.name),
|
||||
slug,
|
||||
sort_order: Number(maxSortOrder || 0) + 1,
|
||||
background_image_url: source.background_image_url || '',
|
||||
background_video_url: source.background_video_url || '',
|
||||
background_embed_url: source.background_embed_url || '',
|
||||
background_audio_url: source.background_audio_url || '',
|
||||
background_audio_autoplay: source.background_audio_autoplay,
|
||||
background_audio_loop: source.background_audio_loop,
|
||||
background_audio_start_time: source.background_audio_start_time,
|
||||
background_audio_end_time: source.background_audio_end_time,
|
||||
background_loop: source.background_loop,
|
||||
background_video_autoplay: source.background_video_autoplay,
|
||||
background_video_loop: source.background_video_loop,
|
||||
background_video_muted: source.background_video_muted,
|
||||
background_video_start_time: source.background_video_start_time,
|
||||
background_video_end_time: source.background_video_end_time,
|
||||
design_width: source.design_width,
|
||||
design_height: source.design_height,
|
||||
requires_auth: source.requires_auth,
|
||||
ui_schema_json: uiSchema,
|
||||
};
|
||||
|
||||
const processedPayload =
|
||||
await TourPagesService.processReversedVideosAndUpdateSchema(
|
||||
duplicatePayload,
|
||||
currentUser,
|
||||
);
|
||||
|
||||
return Tour_pagesDBApi.create(processedPayload, {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tour page - generate reversed videos if needed
|
||||
*/
|
||||
|
||||
@ -9,6 +9,7 @@ import React, { useState, useRef, useEffect, forwardRef } from 'react';
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiChevronDown,
|
||||
mdiDelete,
|
||||
mdiImageMultiple,
|
||||
mdiViewCarousel,
|
||||
mdiSwapHorizontal,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiChevronUp,
|
||||
mdiContentDuplicate,
|
||||
mdiMusicNote,
|
||||
mdiVideo,
|
||||
mdiInformationOutline,
|
||||
@ -42,6 +44,11 @@ const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>(
|
||||
onPageChange,
|
||||
onMovePage,
|
||||
isReorderingPages = false,
|
||||
onDuplicatePage,
|
||||
isDuplicatingPage = false,
|
||||
onDeletePage,
|
||||
canDeletePage = true,
|
||||
isDeletingPage = false,
|
||||
interactionMode,
|
||||
onModeChange,
|
||||
onSelectMenuItem,
|
||||
@ -93,6 +100,17 @@ const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>(
|
||||
!isReorderingPages &&
|
||||
activePageIndex >= 0 &&
|
||||
activePageIndex < sortedPages.length - 1;
|
||||
const canDuplicatePage =
|
||||
Boolean(onDuplicatePage) &&
|
||||
!isDuplicatingPage &&
|
||||
!isReorderingPages &&
|
||||
activePageIndex >= 0;
|
||||
const canDeleteCurrentPage =
|
||||
Boolean(onDeletePage) &&
|
||||
canDeletePage &&
|
||||
!isDeletingPage &&
|
||||
!isReorderingPages &&
|
||||
activePageIndex >= 0;
|
||||
|
||||
// Keyboard handling (Escape closes dropdown)
|
||||
useEffect(() => {
|
||||
@ -195,6 +213,26 @@ const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>(
|
||||
>
|
||||
<BaseIcon path={mdiChevronDown} size={22} />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onDuplicatePage}
|
||||
disabled={!canDuplicatePage}
|
||||
className='flex h-9 w-9 items-center justify-center rounded border border-white/20 bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white disabled:cursor-not-allowed disabled:opacity-35'
|
||||
title='Duplicate page'
|
||||
aria-label='Duplicate page'
|
||||
>
|
||||
<BaseIcon path={mdiContentDuplicate} size={20} />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onDeletePage}
|
||||
disabled={!canDeleteCurrentPage}
|
||||
className='flex h-9 w-9 items-center justify-center rounded border border-red-300/30 bg-red-500/10 text-red-200 transition-colors hover:bg-red-500/20 hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-35'
|
||||
title='Delete page'
|
||||
aria-label='Delete page'
|
||||
>
|
||||
<BaseIcon path={mdiDelete} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mode Toggle - reuse with compact=true */}
|
||||
|
||||
@ -253,6 +253,11 @@ export interface ConstructorToolbarProps {
|
||||
onPageChange: (pageId: string) => void;
|
||||
onMovePage?: (direction: 'up' | 'down') => void;
|
||||
isReorderingPages?: boolean;
|
||||
onDuplicatePage?: () => void;
|
||||
isDuplicatingPage?: boolean;
|
||||
onDeletePage?: () => void;
|
||||
canDeletePage?: boolean;
|
||||
isDeletingPage?: boolean;
|
||||
|
||||
// Mode toggle (reuse InteractionModeToggle with compact=true)
|
||||
interactionMode: ConstructorInteractionMode;
|
||||
|
||||
@ -81,12 +81,20 @@ interface UseConstructorPageActionsResult {
|
||||
isSavingToStage: boolean;
|
||||
/** Whether page creation is in progress */
|
||||
isCreatingPage: boolean;
|
||||
/** Whether page duplication is in progress */
|
||||
isDuplicatingPage: boolean;
|
||||
/** Save current constructor state */
|
||||
saveConstructor: () => Promise<void>;
|
||||
saveConstructor: () => Promise<boolean>;
|
||||
/** Save dev content to stage environment */
|
||||
saveToStage: () => Promise<void>;
|
||||
/** Create a new page with the given name and slug */
|
||||
createPage: (pageName: string, slug: string) => Promise<void>;
|
||||
/** Duplicate an existing page with the given name and slug */
|
||||
duplicatePage: (
|
||||
sourcePageId: string,
|
||||
pageName: string,
|
||||
slug: string,
|
||||
) => Promise<TourPage | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,6 +155,7 @@ export function useConstructorPageActions({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSavingToStage, setIsSavingToStage] = useState(false);
|
||||
const [isCreatingPage, setIsCreatingPage] = useState(false);
|
||||
const [isDuplicatingPage, setIsDuplicatingPage] = useState(false);
|
||||
|
||||
// Polling hook for reverse video generation status
|
||||
const { startPolling } = useReverseVideoPolling({
|
||||
@ -180,7 +189,7 @@ export function useConstructorPageActions({
|
||||
const saveConstructor = useCallback(async () => {
|
||||
if (!activePageId) {
|
||||
onError?.('Select a page before saving.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -241,6 +250,7 @@ export function useConstructorPageActions({
|
||||
});
|
||||
startPolling(pendingReverseKeys, activePageId);
|
||||
}
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { message?: string } };
|
||||
@ -254,6 +264,7 @@ export function useConstructorPageActions({
|
||||
error instanceof Error ? error : { error },
|
||||
);
|
||||
onError?.(message);
|
||||
return false;
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@ -283,7 +294,8 @@ export function useConstructorPageActions({
|
||||
return;
|
||||
}
|
||||
|
||||
await saveConstructor();
|
||||
const didSave = await saveConstructor();
|
||||
if (!didSave) return;
|
||||
|
||||
try {
|
||||
setIsSavingToStage(true);
|
||||
@ -394,13 +406,83 @@ export function useConstructorPageActions({
|
||||
],
|
||||
);
|
||||
|
||||
const duplicatePage = useCallback(
|
||||
async (sourcePageId: string, pageName: string, slug: string) => {
|
||||
if (!projectId) {
|
||||
onError?.('Project is required.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!sourcePageId) {
|
||||
onError?.('Select a page before duplicating.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pageName.trim()) {
|
||||
onError?.('Page name is required.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!slug.trim()) {
|
||||
onError?.('Page slug is required.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDuplicatingPage(true);
|
||||
const response = await axios.post(
|
||||
`/tour_pages/${sourcePageId}/duplicate`,
|
||||
{
|
||||
data: {
|
||||
projectId,
|
||||
environment: 'dev',
|
||||
name: pageName.trim(),
|
||||
slug: slug.trim(),
|
||||
},
|
||||
},
|
||||
);
|
||||
const createdPage = response?.data as TourPage | null;
|
||||
|
||||
await onReload(createdPage?.id);
|
||||
|
||||
if (createdPage?.id) {
|
||||
onSetActivePageId(createdPage.id);
|
||||
}
|
||||
|
||||
return createdPage;
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { message?: string } | string };
|
||||
};
|
||||
const responseData = axiosError?.response?.data;
|
||||
const message =
|
||||
(typeof responseData === 'string'
|
||||
? responseData
|
||||
: responseData?.message) ||
|
||||
(error instanceof Error ? error.message : null) ||
|
||||
'Failed to duplicate page.';
|
||||
logger.error(
|
||||
'Failed to duplicate page:',
|
||||
error instanceof Error ? error : { error },
|
||||
);
|
||||
onError?.(message);
|
||||
return null;
|
||||
} finally {
|
||||
setIsDuplicatingPage(false);
|
||||
}
|
||||
},
|
||||
[projectId, onError, onReload, onSetActivePageId],
|
||||
);
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
isSavingToStage,
|
||||
isCreatingPage,
|
||||
isDuplicatingPage,
|
||||
saveConstructor,
|
||||
saveToStage,
|
||||
createPage,
|
||||
duplicatePage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBoxModal from '../components/CardBoxModal';
|
||||
import CanvasBackground from '../components/Constructor/CanvasBackground';
|
||||
import CanvasLoadingSpinner from '../components/CanvasLoadingSpinner';
|
||||
import TransitionBlackOverlay from '../components/TransitionBlackOverlay';
|
||||
@ -113,6 +114,8 @@ import { useCanvasScale } from '../hooks/useCanvasScale';
|
||||
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
|
||||
import { useNetworkAware } from '../hooks/useNetworkAware';
|
||||
import { queryClient, queryKeys } from '../lib/queryClient';
|
||||
import { buildUniqueSlug } from '../lib/slugHelpers';
|
||||
import { hasPermission } from '../helpers/userPermissions';
|
||||
|
||||
// TourPage type is imported from '../types/entities'
|
||||
// NavigationElementType is imported from '../context/ConstructorContext'
|
||||
@ -142,6 +145,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
const globalTransitionDefaults = useAppSelector(
|
||||
(state) => state.global_transition_defaults.data,
|
||||
);
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const elementEditorRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
@ -319,6 +323,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
] = useState<ElementTransitionSettings | null>(null);
|
||||
// Create page modal state
|
||||
const [isCreatePageModalActive, setIsCreatePageModalActive] = useState(false);
|
||||
const [isDeletePageModalActive, setIsDeletePageModalActive] = useState(false);
|
||||
const [isDeletingPage, setIsDeletingPage] = useState(false);
|
||||
|
||||
const isConstructorEditMode = constructorInteractionMode === 'edit';
|
||||
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
||||
@ -464,6 +470,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
|
||||
// Suggested page number for new page name
|
||||
const suggestedPageNumber = useMemo(() => pages.length + 1, [pages.length]);
|
||||
const canDeletePage = useMemo(
|
||||
() => hasPermission(currentUser, 'DELETE_TOUR_PAGES'),
|
||||
[currentUser],
|
||||
);
|
||||
|
||||
// Last project-level save: most recent updatedAt across all pages
|
||||
const lastProjectSaveAt = useMemo(() => {
|
||||
@ -1000,9 +1010,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
isSaving,
|
||||
isSavingToStage,
|
||||
isCreatingPage,
|
||||
isDuplicatingPage,
|
||||
saveConstructor,
|
||||
saveToStage,
|
||||
createPage,
|
||||
duplicatePage,
|
||||
} = useConstructorPageActions({
|
||||
projectId,
|
||||
project,
|
||||
@ -1048,6 +1060,104 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
setIsCreatePageModalActive(false);
|
||||
}, []);
|
||||
|
||||
const handleDuplicatePage = useCallback(async () => {
|
||||
if (!activePageId || !activePage) {
|
||||
setErrorMessage('Select a page before duplicating.');
|
||||
return;
|
||||
}
|
||||
|
||||
const didSave = await saveConstructor();
|
||||
if (!didSave) return;
|
||||
|
||||
const pageName = `${activePage.name?.trim() || 'Page'} Copy`;
|
||||
const slug = buildUniqueSlug(pageName, existingSlugs);
|
||||
const createdPage = await duplicatePage(activePageId, pageName, slug);
|
||||
|
||||
if (createdPage?.id) {
|
||||
setSuccessMessage('Page duplicated.');
|
||||
}
|
||||
}, [
|
||||
activePage,
|
||||
activePageId,
|
||||
duplicatePage,
|
||||
existingSlugs,
|
||||
saveConstructor,
|
||||
]);
|
||||
|
||||
const handleShowDeletePageModal = useCallback(() => {
|
||||
if (!activePageId || !activePage) {
|
||||
setErrorMessage('Select a page before deleting.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeletePageModalActive(true);
|
||||
}, [activePage, activePageId]);
|
||||
|
||||
const handleCloseDeletePageModal = useCallback(() => {
|
||||
if (!isDeletingPage) {
|
||||
setIsDeletePageModalActive(false);
|
||||
}
|
||||
}, [isDeletingPage]);
|
||||
|
||||
const handleDeletePage = useCallback(async () => {
|
||||
if (!activePageId || !activePage) {
|
||||
setErrorMessage('Select a page before deleting.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedPages = sortTourPagesForDisplay(pages);
|
||||
const currentIndex = sortedPages.findIndex(
|
||||
(page) => page.id === activePageId,
|
||||
);
|
||||
const remainingPages = sortedPages.filter((page) => page.id !== activePageId);
|
||||
const fallbackPage =
|
||||
currentIndex >= 0
|
||||
? remainingPages[Math.min(currentIndex, remainingPages.length - 1)]
|
||||
: remainingPages[0];
|
||||
|
||||
try {
|
||||
setIsDeletingPage(true);
|
||||
setErrorMessage('');
|
||||
await axios.delete(`/tour_pages/${activePageId}`);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.tourPages.all,
|
||||
});
|
||||
|
||||
clearSelection();
|
||||
setSelectedMenuItem('none');
|
||||
await refetchData();
|
||||
setActivePageId(fallbackPage?.id || '');
|
||||
|
||||
setIsDeletePageModalActive(false);
|
||||
setSuccessMessage('Page deleted.');
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { message?: string } | string };
|
||||
};
|
||||
const responseData = axiosError?.response?.data;
|
||||
const message =
|
||||
(typeof responseData === 'string'
|
||||
? responseData
|
||||
: responseData?.message) ||
|
||||
(error instanceof Error ? error.message : null) ||
|
||||
'Failed to delete page.';
|
||||
setErrorMessage(message);
|
||||
logger.error(
|
||||
'Failed to delete page:',
|
||||
error instanceof Error ? error : { error },
|
||||
);
|
||||
} finally {
|
||||
setIsDeletingPage(false);
|
||||
}
|
||||
}, [
|
||||
activePage,
|
||||
activePageId,
|
||||
clearSelection,
|
||||
pages,
|
||||
refetchData,
|
||||
setActivePageId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady || typeof window === 'undefined') return;
|
||||
|
||||
@ -1918,7 +2028,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
normalizeNavigationType,
|
||||
|
||||
// Actions
|
||||
save: saveConstructor,
|
||||
save: async () => {
|
||||
await saveConstructor();
|
||||
},
|
||||
isSaving,
|
||||
}),
|
||||
[
|
||||
@ -2017,6 +2129,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
}}
|
||||
onMovePage={handleMovePage}
|
||||
isReorderingPages={isReorderingPages}
|
||||
onDuplicatePage={handleDuplicatePage}
|
||||
isDuplicatingPage={isSaving || isDuplicatingPage}
|
||||
onDeletePage={handleShowDeletePageModal}
|
||||
canDeletePage={canDeletePage}
|
||||
isDeletingPage={isDeletingPage || isSaving || isDuplicatingPage}
|
||||
interactionMode={constructorInteractionMode}
|
||||
onModeChange={setConstructorInteractionMode}
|
||||
onSelectMenuItem={selectMenuItemForEdit}
|
||||
@ -2402,6 +2519,24 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
onCancel={handleCloseCreatePageModal}
|
||||
/>
|
||||
|
||||
<CardBoxModal
|
||||
title='Delete page'
|
||||
buttonColor='danger'
|
||||
buttonLabel={isDeletingPage ? 'Deleting...' : 'Delete'}
|
||||
isConfirmDisabled={isDeletingPage}
|
||||
isActive={isDeletePageModalActive}
|
||||
onConfirm={handleDeletePage}
|
||||
onCancel={isDeletingPage ? undefined : handleCloseDeletePageModal}
|
||||
>
|
||||
<p className='text-sm text-gray-700 dark:text-gray-200'>
|
||||
Delete {activePage?.name || 'this page'} from this presentation?
|
||||
</p>
|
||||
<p className='text-xs text-gray-500'>
|
||||
This removes the dev page immediately. Stage and production are
|
||||
updated only after Save to Stage and Publish.
|
||||
</p>
|
||||
</CardBoxModal>
|
||||
|
||||
<style jsx>{`
|
||||
.menu-action-btn {
|
||||
width: 100%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user