From e8b7eea4e324a9a6e9a412a9c8e95440c1eb5a8f Mon Sep 17 00:00:00 2001 From: David Jones Date: Tue, 19 Aug 2025 15:58:56 -0400 Subject: [PATCH] Cleaened up module controls --- frontend/.env.example | 2 + .../src/components/DynamicPageRenderer.tsx | 4 +- frontend/src/config/index.ts | 5 ++ .../src/contexts/ControlVisibilityContext.tsx | 66 ++++++++++++++ .../contexts/PluginStudioDevModeContext.tsx | 10 +-- .../components/GridItemControls.tsx | 8 ++ .../components/LayoutEngine.tsx | 58 +++++++++++-- .../components/ModeController.tsx | 6 +- .../components/UnifiedModuleControls.tsx | 8 ++ .../styles/index.css | 87 +++++++++++++++++++ frontend/src/hooks/useControlVisibility.ts | 51 +++++++++++ 11 files changed, 290 insertions(+), 15 deletions(-) create mode 100644 frontend/src/contexts/ControlVisibilityContext.tsx create mode 100644 frontend/src/hooks/useControlVisibility.ts diff --git a/frontend/.env.example b/frontend/.env.example index 281e8a1..0a85aa5 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -3,6 +3,8 @@ VITE_API_URL=http://localhost:8005 VITE_API_TIMEOUT=10000 # PluginStudio Dev Mode - Controls visibility of development UI elements VITE_PLUGIN_STUDIO_DEV_MODE=false +# Show editing controls - Controls visibility of module editing controls +VITE_SHOW_EDITING_CONTROLS=false # Development Only - Temporary Auto Login (Remove in production) diff --git a/frontend/src/components/DynamicPageRenderer.tsx b/frontend/src/components/DynamicPageRenderer.tsx index 17129ba..42a3ae8 100644 --- a/frontend/src/components/DynamicPageRenderer.tsx +++ b/frontend/src/components/DynamicPageRenderer.tsx @@ -300,9 +300,11 @@ export const DynamicPageRenderer: React.FC = ({ // Determine render mode based on current path const renderMode = useMemo((): RenderMode => { - if (location.pathname.startsWith('/plugin-studio') || location.pathname.startsWith('/pages/')) { + // Only Plugin Studio routes should use STUDIO mode + if (location.pathname.startsWith('/plugin-studio')) { return RenderMode.STUDIO; } + // All other routes, including /pages/, should use PUBLISHED mode return RenderMode.PUBLISHED; }, [location.pathname]); diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 34ba522..6fc8f22 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -20,6 +20,10 @@ const envSchema = z.object({ .string() .transform((val) => val === "true") .default("false"), + VITE_SHOW_EDITING_CONTROLS: z + .string() + .transform((val) => val === "true") + .default("false"), VITE_DEV_AUTO_LOGIN: z .string() .transform((val) => val === "true") @@ -74,6 +78,7 @@ export const config = { }, devMode: { pluginStudio: env.VITE_PLUGIN_STUDIO_DEV_MODE || false, + showEditingControls: env.VITE_SHOW_EDITING_CONTROLS || false, }, } as const; diff --git a/frontend/src/contexts/ControlVisibilityContext.tsx b/frontend/src/contexts/ControlVisibilityContext.tsx new file mode 100644 index 0000000..f3048ef --- /dev/null +++ b/frontend/src/contexts/ControlVisibilityContext.tsx @@ -0,0 +1,66 @@ +import React, { createContext, useContext, ReactNode, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { RenderMode } from '../features/unified-dynamic-page-renderer/types'; +import { usePluginStudioDevMode } from '../hooks/usePluginStudioDevMode'; + +interface ControlVisibilityContextType { + showControls: boolean; + isPluginStudio: boolean; + renderMode: RenderMode; + canEdit: boolean; + controlsEnabled: boolean; +} + +const ControlVisibilityContext = createContext(undefined); + +interface ControlVisibilityProviderProps { + children: ReactNode; + renderMode?: RenderMode; +} + +export const ControlVisibilityProvider: React.FC = ({ + children, + renderMode: propRenderMode +}) => { + const location = useLocation(); + const { isPluginStudioDevMode } = usePluginStudioDevMode(); + + const contextValue = useMemo(() => { + // Determine if we're in Plugin Studio based on route + const isPluginStudio = location.pathname.startsWith('/plugin-studio'); + + // Determine render mode - prioritize prop, then derive from context + const renderMode = propRenderMode || (isPluginStudio ? RenderMode.STUDIO : RenderMode.PUBLISHED); + + // Controls should only be shown in Plugin Studio with dev mode enabled + const controlsEnabled = isPluginStudioDevMode && isPluginStudio; + + // Additional check for studio mode + const canEdit = renderMode === RenderMode.STUDIO && controlsEnabled; + + // Final decision on showing controls + const showControls = canEdit && controlsEnabled; + + return { + showControls, + isPluginStudio, + renderMode, + canEdit, + controlsEnabled, + }; + }, [location.pathname, isPluginStudioDevMode, propRenderMode]); + + return ( + + {children} + + ); +}; + +export const useControlVisibilityContext = () => { + const context = useContext(ControlVisibilityContext); + if (context === undefined) { + throw new Error('useControlVisibilityContext must be used within a ControlVisibilityProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/contexts/PluginStudioDevModeContext.tsx b/frontend/src/contexts/PluginStudioDevModeContext.tsx index 5368db7..cfd2bfa 100644 --- a/frontend/src/contexts/PluginStudioDevModeContext.tsx +++ b/frontend/src/contexts/PluginStudioDevModeContext.tsx @@ -23,12 +23,12 @@ export const PluginStudioDevModeProvider: React.FC = ({ onRemove, onSelect }) => { + const showControls = useShowControls(); + + // Early return if controls should not be shown + if (!showControls) { + return null; + } + // Use onRemove if provided, otherwise use onDelete const handleDelete = onRemove || onDelete; diff --git a/frontend/src/features/unified-dynamic-page-renderer/components/LayoutEngine.tsx b/frontend/src/features/unified-dynamic-page-renderer/components/LayoutEngine.tsx index 0182308..6e3be26 100644 --- a/frontend/src/features/unified-dynamic-page-renderer/components/LayoutEngine.tsx +++ b/frontend/src/features/unified-dynamic-page-renderer/components/LayoutEngine.tsx @@ -7,6 +7,7 @@ import { useBreakpoint } from '../hooks/useBreakpoint'; import { GridItemControls } from '../../plugin-studio/components/canvas/GridItemControls'; import { useUnifiedLayoutState } from '../hooks/useUnifiedLayoutState'; import { LayoutChangeOrigin } from '../utils/layoutChangeManager'; +import { useControlVisibility } from '../../../hooks/useControlVisibility'; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -107,6 +108,48 @@ export const LayoutEngine: React.FC = React.memo(({ const { currentBreakpoint } = useBreakpoint(); + // Control visibility based on context + const { showControls } = useControlVisibility(mode); + + // Failsafe: Programmatically remove resize handles when controls should be hidden + useEffect(() => { + if (!showControls) { + const removeResizeHandles = () => { + const resizeHandles = document.querySelectorAll('.react-resizable-handle'); + resizeHandles.forEach(handle => { + (handle as HTMLElement).style.display = 'none'; + (handle as HTMLElement).style.visibility = 'hidden'; + (handle as HTMLElement).style.pointerEvents = 'none'; + }); + }; + + // Remove immediately + removeResizeHandles(); + + // Also remove after a short delay to catch any dynamically added handles + const timeoutId = setTimeout(removeResizeHandles, 100); + + // Set up a mutation observer to catch any new resize handles + const observer = new MutationObserver(() => { + if (!showControls) { + removeResizeHandles(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'] + }); + + return () => { + clearTimeout(timeoutId); + observer.disconnect(); + }; + } + }, [showControls]); + // Track operation IDs for proper state management const currentOperationId = useRef(null); const pageIdRef = useRef(pageId); @@ -512,7 +555,7 @@ export const LayoutEngine: React.FC = React.memo(({ // Instead of returning null, try to render with the layout item data directly // This allows the LegacyModuleAdapter to handle the module loading const isSelected = selectedItem === item.i; - const isStudioMode = mode === RenderMode.STUDIO; + const isStudioMode = showControls; // Use control visibility instead of just mode check // Try to extract pluginId from moduleId if item.pluginId is 'unknown' let fallbackPluginId = item.pluginId; @@ -559,7 +602,7 @@ export const LayoutEngine: React.FC = React.memo(({ style={{ position: 'relative' }} > {/* Use the legacy GridItemControls component for consistent behavior */} - {isStudioMode && ( + {showControls && ( onItemConfig?.(item.i)} @@ -585,7 +628,7 @@ export const LayoutEngine: React.FC = React.memo(({ } const isSelected = selectedItem === item.i; - const isStudioMode = mode === RenderMode.STUDIO; + const isStudioMode = showControls; // Use control visibility instead of just mode check return (
= React.memo(({ style={{ position: 'relative' }} > {/* Use the legacy GridItemControls component for consistent behavior */} - {isStudioMode && ( + {showControls && ( onItemConfig?.(item.i)} @@ -679,8 +722,9 @@ export const LayoutEngine: React.FC = React.memo(({ onDragStop: handleDragStop, onResizeStart: handleResizeStart, onResizeStop: handleResizeStop, - isDraggable: mode === RenderMode.STUDIO, - isResizable: mode === RenderMode.STUDIO, + isDraggable: showControls, // Use control visibility instead of mode + isResizable: showControls, // Use control visibility instead of mode + resizeHandles: showControls ? ['se' as const] : [], // Only show resize handles when controls are visible draggableHandle: '.react-grid-dragHandleExample', compactType: 'vertical' as const, useCSSTransforms: true, @@ -690,7 +734,7 @@ export const LayoutEngine: React.FC = React.memo(({ transformScale: 1, ...defaultGridConfig, }; - }, [currentLayouts, mode, handleLayoutChange, handleDragStart, handleDragStop, handleResizeStart, handleResizeStop]); + }, [currentLayouts, mode, showControls, handleLayoutChange, handleDragStart, handleDragStop, handleResizeStart, handleResizeStop]); // Memoize the rendered grid items with minimal stable dependencies const gridItems = useMemo(() => { diff --git a/frontend/src/features/unified-dynamic-page-renderer/components/ModeController.tsx b/frontend/src/features/unified-dynamic-page-renderer/components/ModeController.tsx index 65e1714..f1b4e48 100644 --- a/frontend/src/features/unified-dynamic-page-renderer/components/ModeController.tsx +++ b/frontend/src/features/unified-dynamic-page-renderer/components/ModeController.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { RenderMode, PageData } from '../types'; import { StudioFeatures, PublishedFeatures, PreviewFeatures } from '../types/layout'; import { usePluginStudioDevMode } from '../../../hooks/usePluginStudioDevMode'; +import { useControlVisibility } from '../../../hooks/useControlVisibility'; export interface ModeControllerProps { mode: RenderMode; @@ -67,11 +68,12 @@ export const ModeController: React.FC = ({ onModeTransition, }) => { const { isPluginStudioDevMode } = usePluginStudioDevMode(); + const { showControls } = useControlVisibility(mode); const [isTransitioning, setIsTransitioning] = useState(false); const [activeFeatures, setActiveFeatures] = useState>({}); - // Don't render the ModeController if Plugin Studio dev mode is disabled - if (!isPluginStudioDevMode) { + // Don't render the ModeController if controls should not be shown + if (!showControls) { return null; } diff --git a/frontend/src/features/unified-dynamic-page-renderer/components/UnifiedModuleControls.tsx b/frontend/src/features/unified-dynamic-page-renderer/components/UnifiedModuleControls.tsx index 9935105..1fe029e 100644 --- a/frontend/src/features/unified-dynamic-page-renderer/components/UnifiedModuleControls.tsx +++ b/frontend/src/features/unified-dynamic-page-renderer/components/UnifiedModuleControls.tsx @@ -3,6 +3,7 @@ import { Box, IconButton, Tooltip } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from '@mui/icons-material/Settings'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { useShowControls } from '../../../hooks/useControlVisibility'; interface UnifiedModuleControlsProps { moduleId: string; @@ -28,6 +29,13 @@ export const UnifiedModuleControls: React.FC = ({ onDelete, onSelect }) => { + const showControls = useShowControls(); + + // Early return if controls should not be shown + if (!showControls) { + return null; + } + return ( <> {/* Move handle - top right, always visible on hover */} diff --git a/frontend/src/features/unified-dynamic-page-renderer/styles/index.css b/frontend/src/features/unified-dynamic-page-renderer/styles/index.css index 5eea155..cd55ccd 100644 --- a/frontend/src/features/unified-dynamic-page-renderer/styles/index.css +++ b/frontend/src/features/unified-dynamic-page-renderer/styles/index.css @@ -1567,4 +1567,91 @@ .configuration-dialog__title { font-size: 1.125rem; } +} + +/* Hide React Grid Layout resize handles on published pages */ +.layout-engine--published .react-resizable-handle { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-se { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-sw { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-ne { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-nw { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-n { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-s { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-e { + display: none !important; +} + +.layout-engine--published .react-resizable-handle-w { + display: none !important; +} + +/* Also hide any resize cursors */ +.layout-engine--published .react-grid-item { + cursor: default !important; +} + +.layout-engine--published .react-grid-item:hover { + cursor: default !important; +} + +/* Disable pointer events on resize handles */ +.layout-engine--published .react-resizable { + pointer-events: none; +} + +.layout-engine--published .react-resizable > * { + pointer-events: auto; +} + +/* Additional high-specificity rules to ensure resize handles are completely hidden */ +.layout-engine--published .react-grid-item .react-resizable-handle, +.layout-engine--published .react-grid-item > .react-resizable-handle, +.layout-engine--published .react-grid-item .react-resizable-handle-se, +.layout-engine--published .react-grid-item .react-resizable-handle-sw, +.layout-engine--published .react-grid-item .react-resizable-handle-ne, +.layout-engine--published .react-grid-item .react-resizable-handle-nw, +.layout-engine--published .react-grid-item .react-resizable-handle-n, +.layout-engine--published .react-grid-item .react-resizable-handle-s, +.layout-engine--published .react-grid-item .react-resizable-handle-e, +.layout-engine--published .react-grid-item .react-resizable-handle-w { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + width: 0 !important; + height: 0 !important; + overflow: hidden !important; + position: absolute !important; + left: -9999px !important; + top: -9999px !important; +} + +/* Force hide any resize handles with even higher specificity */ +.unified-page-renderer--published .layout-engine--published .react-resizable-handle { + display: none !important; +} + +.unified-page-renderer--published .layout-engine--published .react-grid-item .react-resizable-handle { + display: none !important; } \ No newline at end of file diff --git a/frontend/src/hooks/useControlVisibility.ts b/frontend/src/hooks/useControlVisibility.ts new file mode 100644 index 0000000..7021d00 --- /dev/null +++ b/frontend/src/hooks/useControlVisibility.ts @@ -0,0 +1,51 @@ +import { useLocation } from 'react-router-dom'; +import { usePluginStudioDevMode } from './usePluginStudioDevMode'; +import { RenderMode } from '../features/unified-dynamic-page-renderer/types'; + +export interface ControlVisibilityState { + showControls: boolean; + isPluginStudio: boolean; + renderMode: RenderMode; + canEdit: boolean; + controlsEnabled: boolean; +} + +/** + * Hook to determine if control icons should be visible + * Controls are only shown in Plugin Studio with dev mode enabled + */ +export const useControlVisibility = (overrideRenderMode?: RenderMode): ControlVisibilityState => { + const location = useLocation(); + const { isPluginStudioDevMode } = usePluginStudioDevMode(); + + // Determine if we're in Plugin Studio based on route + const isPluginStudio = location.pathname.startsWith('/plugin-studio'); + + // Determine render mode - use override if provided, otherwise derive from context + const renderMode = overrideRenderMode || (isPluginStudio ? RenderMode.STUDIO : RenderMode.PUBLISHED); + + // Controls should only be enabled in Plugin Studio with dev mode + const controlsEnabled = isPluginStudioDevMode && isPluginStudio; + + // Can edit only in studio mode with controls enabled + const canEdit = renderMode === RenderMode.STUDIO && controlsEnabled; + + // Final decision on showing controls + const showControls = canEdit && controlsEnabled; + + return { + showControls, + isPluginStudio, + renderMode, + canEdit, + controlsEnabled, + }; +}; + +/** + * Simple hook that just returns whether controls should be shown + */ +export const useShowControls = (overrideRenderMode?: RenderMode): boolean => { + const { showControls } = useControlVisibility(overrideRenderMode); + return showControls; +}; \ No newline at end of file