even more ui shit
This commit is contained in:
parent
f98396f652
commit
2cf5af613d
@ -128,8 +128,6 @@ const GlobalDragDrop = () => {
|
||||
return () => window.removeEventListener('drop', handleDrop);
|
||||
}, [navigate]);
|
||||
|
||||
console.log('isDragging', isDragging)
|
||||
console.log('isLocalZoneActive', isLocalZoneActive)
|
||||
|
||||
if (!isDragging || isLocalZoneActive) return null;
|
||||
|
||||
|
||||
136
packages/ui/src/components/layouts/LayoutPropertyPanel.tsx
Normal file
136
packages/ui/src/components/layouts/LayoutPropertyPanel.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Database } from "@/integrations/supabase/types";
|
||||
import { T, translate } from "@/i18n";
|
||||
import { toast } from "sonner";
|
||||
import { Check, Trash2, Globe, Lock, Play } from "lucide-react";
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
|
||||
interface LayoutPropertyPanelProps {
|
||||
layout: Layout;
|
||||
onRename: (newName: string) => Promise<void>;
|
||||
onDelete: () => void;
|
||||
onApply: () => void;
|
||||
onToggleVisibility: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const LayoutPropertyPanel = ({
|
||||
layout,
|
||||
onRename,
|
||||
onDelete,
|
||||
onApply,
|
||||
onToggleVisibility,
|
||||
isLoading = false
|
||||
}: LayoutPropertyPanelProps) => {
|
||||
const [name, setName] = useState(layout.name);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setName(layout.name);
|
||||
}, [layout.id, layout.name]);
|
||||
|
||||
const handleRename = async () => {
|
||||
if (name.trim() === layout.name) return;
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
await onRename(name);
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<h3 className="text-sm font-semibold"><T>Layout Properties</T></h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
{/* Name Section */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="layout-name"><T>Name</T></Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="layout-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
|
||||
disabled={isLoading || isRenaming}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<span className="text-muted-foreground"><T>Type</T></span>
|
||||
<span className="font-medium">{layout.type}</span>
|
||||
|
||||
<span className="text-muted-foreground"><T>Created</T></span>
|
||||
<span className="font-medium">
|
||||
{new Date(layout.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visibility Section */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<Label><T>Visibility</T></Label>
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg bg-muted/20">
|
||||
<div className="flex items-center gap-2">
|
||||
{layout.visibility === 'public' ? (
|
||||
<Globe className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Lock className="h-4 w-4 text-amber-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium capitalize">
|
||||
<T>{layout.visibility}</T>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onToggleVisibility}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{layout.visibility === 'public' ? <T>Make Private</T> : <T>Make Public</T>}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{layout.visibility === 'public'
|
||||
? <T>Public layouts effectively become templates that are visible to everyone.</T>
|
||||
: <T>Private layouts are only visible to you.</T>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Footer */}
|
||||
<div className="p-4 border-t bg-muted/10 space-y-3">
|
||||
<Button
|
||||
className="w-full gap-2"
|
||||
onClick={onApply}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
<T>Apply Layout</T>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full gap-2"
|
||||
onClick={onDelete}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<T>Delete Layout</T>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -4,10 +4,8 @@ import { fetchWithDeduplication } from "@/lib/db";
|
||||
export const fetchUserPage = async (userId: string, slug: string) => {
|
||||
const key = `user-page-${userId}-${slug}`;
|
||||
return fetchWithDeduplication(key, async () => {
|
||||
console.log('Fetching user page for user:', userId, 'slug:', slug);
|
||||
const { data: sessionData } = await defaultSupabase.auth.getSession();
|
||||
const token = sessionData.session?.access_token;
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { Page } from "../types";
|
||||
const WidgetPropertyPanel = lazy(() => import("@/components/widgets/WidgetPropertyPanel").then(module => ({ default: module.WidgetPropertyPanel })));
|
||||
const ContainerPropertyPanel = lazy(() => import("@/components/containers/ContainerPropertyPanel").then(module => ({ default: module.ContainerPropertyPanel })));
|
||||
const UserPageTypeFields = lazy(() => import("./UserPageTypeFields").then(module => ({ default: module.UserPageTypeFields })));
|
||||
import { LayoutPropertyPanel } from "@/components/layouts/LayoutPropertyPanel";
|
||||
|
||||
interface EditorRightPanelProps {
|
||||
selectedWidgetId: string | null;
|
||||
@ -16,6 +17,11 @@ interface EditorRightPanelProps {
|
||||
onPageUpdate: (updatedPage: Page) => void;
|
||||
contextVariables?: Record<string, any>;
|
||||
setSelectedWidgetId: (id: string | null) => void;
|
||||
selectedLayout: any | null;
|
||||
onRenameLayout: (template: any, newName: string) => Promise<void>;
|
||||
onDeleteLayout: (template: any) => void;
|
||||
onApplyLayout: (template: any) => void;
|
||||
onToggleLayoutVisibility: (template: any) => void;
|
||||
}
|
||||
|
||||
export const EditorRightPanel = ({
|
||||
@ -28,15 +34,28 @@ export const EditorRightPanel = ({
|
||||
onPageUpdate,
|
||||
contextVariables,
|
||||
setSelectedWidgetId,
|
||||
selectedLayout,
|
||||
onRenameLayout,
|
||||
onDeleteLayout,
|
||||
onApplyLayout,
|
||||
onToggleLayoutVisibility
|
||||
}: EditorRightPanelProps) => {
|
||||
if (!selectedWidgetId && !selectedContainerId && !showTypeFields) return null;
|
||||
if (!selectedWidgetId && !selectedContainerId && !showTypeFields && !selectedLayout) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={25} minSize={20} maxSize={50} order={2} id="user-page-props">
|
||||
<div className="h-full flex flex-col shrink-0 transition-all duration-300 overflow-hidden bg-background">
|
||||
{selectedWidgetId ? (
|
||||
{selectedLayout ? (
|
||||
<LayoutPropertyPanel
|
||||
layout={selectedLayout}
|
||||
onRename={(name) => onRenameLayout(selectedLayout, name)}
|
||||
onDelete={() => onDeleteLayout(selectedLayout)}
|
||||
onApply={() => onApplyLayout(selectedLayout)}
|
||||
onToggleVisibility={() => onToggleLayoutVisibility(selectedLayout)}
|
||||
/>
|
||||
) : selectedWidgetId ? (
|
||||
<Suspense fallback={<div className="h-full flex items-center justify-center text-muted-foreground">Loading settings...</div>}>
|
||||
<WidgetPropertyPanel
|
||||
pageId={selectedPageId || `page-${page.id}`}
|
||||
|
||||
@ -110,6 +110,7 @@ const UserPageEditInner = ({
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||
const [settingsWidgetId, setSettingsWidgetId] = useState<string | null>(null);
|
||||
const [settingsLayoutId, setSettingsLayoutId] = useState<string | null>(null);
|
||||
const [selectedLayoutId, setSelectedLayoutId] = useState<string | null>(null);
|
||||
|
||||
// Confirmation dialog state
|
||||
const [confirmationState, setConfirmationState] = useState<{
|
||||
@ -148,6 +149,7 @@ const UserPageEditInner = ({
|
||||
getLoadedPageLayout,
|
||||
importPageLayout: useLayout().importPageLayout,
|
||||
setConfirmation: setConfirmationState,
|
||||
currentLayout: loadedPages.get(pageId) || null,
|
||||
});
|
||||
|
||||
const emailActions = useEmailActions({
|
||||
@ -224,6 +226,14 @@ const UserPageEditInner = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTemplate = (template: any) => {
|
||||
setSelectedWidgetId(null);
|
||||
setSelectedContainerId(null);
|
||||
setShowTypeFields(false);
|
||||
setSelectedLayoutId(template.id);
|
||||
setIsSidebarCollapsed(false);
|
||||
};
|
||||
|
||||
// --- Widget handlers ---
|
||||
const handleWidgetClick = async (widgetId: string, initialProps?: Record<string, any>) => {
|
||||
await handleAddWidget(widgetId, initialProps);
|
||||
@ -439,8 +449,9 @@ const UserPageEditInner = ({
|
||||
onDelete={handleDeletePage}
|
||||
onMetaUpdated={() => { }}
|
||||
templates={templateManager.templates}
|
||||
onLoadTemplate={templateManager.handleLoadTemplate}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
onDeleteTemplate={templateManager.handleDeleteTemplate}
|
||||
onToggleVisibility={templateManager.handleToggleVisibility}
|
||||
onToggleWidget={handleWidgetClick}
|
||||
onAddContainer={handleAddContainer}
|
||||
onUndo={undo}
|
||||
@ -450,6 +461,7 @@ const UserPageEditInner = ({
|
||||
onExportLayout={layoutIO.handleExportLayout}
|
||||
onImportLayout={layoutIO.handleImportLayout}
|
||||
activeTemplateId={templateManager.activeTemplateId || undefined}
|
||||
selectedLayoutId={selectedLayoutId}
|
||||
onSaveToTemplate={templateManager.handleSaveToTemplate}
|
||||
onSaveAsNewTemplate={templateManager.handleSaveAsNewTemplate}
|
||||
onNewLayout={templateManager.handleNewLayout}
|
||||
@ -613,6 +625,11 @@ const UserPageEditInner = ({
|
||||
onPageUpdate={onPageUpdate}
|
||||
contextVariables={contextVariables}
|
||||
setSelectedWidgetId={setSelectedWidgetId}
|
||||
selectedLayout={templateManager.templates?.find(t => t.id === selectedLayoutId) || null}
|
||||
onRenameLayout={templateManager.handleRenameTemplate}
|
||||
onDeleteLayout={templateManager.handleDeleteTemplate}
|
||||
onApplyLayout={templateManager.handleLoadTemplate}
|
||||
onToggleLayoutVisibility={templateManager.handleToggleVisibility}
|
||||
/>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
||||
@ -13,14 +13,15 @@ interface UseTemplateManagerParams {
|
||||
isOwner: boolean;
|
||||
getLoadedPageLayout: ReturnType<typeof useLayout>['getLoadedPageLayout'];
|
||||
importPageLayout: ReturnType<typeof useLayout>['importPageLayout'];
|
||||
setConfirmation: (state: {
|
||||
setConfirmation: (state: React.SetStateAction<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
onConfirm: () => void;
|
||||
confirmLabel?: string;
|
||||
variant?: "default" | "destructive";
|
||||
}) => void;
|
||||
}>) => void;
|
||||
currentLayout: any | null;
|
||||
}
|
||||
|
||||
export function useTemplateManager({
|
||||
@ -29,6 +30,7 @@ export function useTemplateManager({
|
||||
getLoadedPageLayout,
|
||||
importPageLayout,
|
||||
setConfirmation,
|
||||
currentLayout,
|
||||
}: UseTemplateManagerParams) {
|
||||
const [templates, setTemplates] = useState<Layout[]>([]);
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null);
|
||||
@ -41,6 +43,13 @@ export function useTemplateManager({
|
||||
}
|
||||
}, [isOwner]);
|
||||
|
||||
// Sync activeTemplateId with layout state (supports Undo/Redo)
|
||||
useEffect(() => {
|
||||
if (currentLayout?.rootTemplate) {
|
||||
setActiveTemplateId(currentLayout.rootTemplate);
|
||||
}
|
||||
}, [currentLayout?.rootTemplate]);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
const { data } = await getLayouts({ type: 'canvas' });
|
||||
if (data) {
|
||||
@ -51,8 +60,16 @@ export function useTemplateManager({
|
||||
const handleLoadTemplate = useCallback(async (template: Layout) => {
|
||||
if (!pageId) return;
|
||||
try {
|
||||
const layoutJsonString = JSON.stringify(template.layout_json);
|
||||
// Inject rootTemplate ID into the layout JSON so it persists in history
|
||||
const layoutJson = template.layout_json as any;
|
||||
const layoutWithTemplateId = {
|
||||
...(typeof layoutJson === 'object' ? layoutJson : {}),
|
||||
rootTemplate: template.id
|
||||
};
|
||||
const layoutJsonString = JSON.stringify(layoutWithTemplateId);
|
||||
await importPageLayout(pageId, layoutJsonString);
|
||||
|
||||
// Local state update is redundant due to useEffect but keeps UI snappy
|
||||
setActiveTemplateId(template.id);
|
||||
toast.success(`Loaded layout: ${template.name}`);
|
||||
} catch (e) {
|
||||
@ -127,6 +144,35 @@ export function useTemplateManager({
|
||||
});
|
||||
}, [activeTemplateId, setConfirmation]);
|
||||
|
||||
const handleToggleVisibility = useCallback(async (template: Layout) => {
|
||||
try {
|
||||
const newVisibility = template.visibility === 'public' ? 'private' : 'public';
|
||||
await updateLayout(template.id, {
|
||||
visibility: newVisibility,
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
toast.success(translate(newVisibility === 'public' ? "Template is now public" : "Template is now private"));
|
||||
loadTemplates();
|
||||
} catch (e) {
|
||||
console.error("Failed to update template visibility", e);
|
||||
toast.error(translate("Failed to update visibility"));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRenameTemplate = useCallback(async (template: Layout, newName: string) => {
|
||||
try {
|
||||
await updateLayout(template.id, {
|
||||
name: newName,
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
toast.success(translate("Template renamed"));
|
||||
loadTemplates();
|
||||
} catch (e) {
|
||||
console.error("Failed to rename template", e);
|
||||
toast.error(translate("Failed to rename template"));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNewLayout = useCallback(() => {
|
||||
if (!pageId) return;
|
||||
setConfirmation({
|
||||
@ -168,5 +214,7 @@ export function useTemplateManager({
|
||||
onSaveTemplate,
|
||||
handleDeleteTemplate,
|
||||
handleNewLayout,
|
||||
handleToggleVisibility,
|
||||
handleRenameTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
@ -34,7 +34,9 @@ import {
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
BookmarkCheck,
|
||||
Sparkles
|
||||
Sparkles,
|
||||
Globe,
|
||||
Lock
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -143,6 +145,9 @@ interface PageRibbonBarProps {
|
||||
onPaste?: () => void;
|
||||
hasClipboard?: boolean;
|
||||
onAIAssistant?: () => void;
|
||||
onToggleVisibility?: (template: Layout) => void;
|
||||
onSelectTemplate?: (template: Layout) => void;
|
||||
selectedLayoutId?: string | null;
|
||||
}
|
||||
|
||||
// Ribbon UI Components
|
||||
@ -347,7 +352,10 @@ export const PageRibbonBar = ({
|
||||
onCopy,
|
||||
onPaste,
|
||||
hasClipboard = false,
|
||||
onAIAssistant
|
||||
onAIAssistant,
|
||||
onToggleVisibility,
|
||||
onSelectTemplate,
|
||||
selectedLayoutId
|
||||
}: PageRibbonBarProps) => {
|
||||
const { executeCommand, loadPageLayout, clearHistory, getLoadedPageLayout } = useLayout();
|
||||
const navigate = useNavigate();
|
||||
@ -949,10 +957,9 @@ export const PageRibbonBar = ({
|
||||
id: t.id,
|
||||
icon: LayoutTemplate,
|
||||
label: t.name,
|
||||
onClick: () => onLoadTemplate?.(t),
|
||||
active: activeTemplateId === t.id,
|
||||
iconColor: activeTemplateId === t.id ? "text-green-600" : "text-gray-500",
|
||||
onDelete: onDeleteTemplate ? () => onDeleteTemplate(t) : undefined
|
||||
onClick: () => onSelectTemplate?.(t),
|
||||
active: selectedLayoutId === t.id,
|
||||
iconColor: activeTemplateId === t.id ? "text-green-600" : "text-gray-500"
|
||||
})) || []}
|
||||
/>
|
||||
{(!templates || templates.length === 0) && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user