supbase
This commit is contained in:
parent
49e75ffd30
commit
f60e171051
96
packages/ui/shared/src/commons.ts
Normal file
96
packages/ui/shared/src/commons.ts
Normal file
@ -0,0 +1,96 @@
|
||||
|
||||
|
||||
export interface UserIdentity {
|
||||
id: string
|
||||
user_id: string
|
||||
identity_data?: {
|
||||
[key: string]: any
|
||||
}
|
||||
identity_id: string
|
||||
provider: string
|
||||
created_at?: string
|
||||
last_sign_in_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
const FactorTypes = ['totp', 'phone', 'webauthn'] as const
|
||||
|
||||
/**
|
||||
* Type of factor. `totp` and `phone` supported with this version
|
||||
*/
|
||||
export type FactorType = (typeof FactorTypes)[number]
|
||||
|
||||
const FactorVerificationStatuses = ['verified', 'unverified'] as const
|
||||
|
||||
/**
|
||||
* The verification status of the factor, default is `unverified` after `.enroll()`, then `verified` after the user verifies it with `.verify()`
|
||||
*/
|
||||
type FactorVerificationStatus = (typeof FactorVerificationStatuses)[number]
|
||||
|
||||
export type Factor<
|
||||
Type extends FactorType = FactorType,
|
||||
Status extends FactorVerificationStatus = (typeof FactorVerificationStatuses)[number],
|
||||
> = {
|
||||
/** ID of the factor. */
|
||||
id: string
|
||||
|
||||
/** Friendly name of the factor, useful to disambiguate between multiple factors. */
|
||||
friendly_name?: string
|
||||
|
||||
/**
|
||||
* Type of factor. `totp` and `phone` supported with this version
|
||||
*/
|
||||
factor_type: Type
|
||||
|
||||
/**
|
||||
* The verification status of the factor, default is `unverified` after `.enroll()`, then `verified` after the user verifies it with `.verify()`
|
||||
*/
|
||||
status: Status
|
||||
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface UserAppMetadata {
|
||||
/**
|
||||
* The first provider that the user used to sign up with.
|
||||
*/
|
||||
provider?: string
|
||||
/**
|
||||
* A list of all providers that the user has linked to their account.
|
||||
*/
|
||||
providers?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface UserMetadata {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
app_metadata: UserAppMetadata
|
||||
user_metadata: UserMetadata
|
||||
aud: string
|
||||
confirmation_sent_at?: string
|
||||
recovery_sent_at?: string
|
||||
email_change_sent_at?: string
|
||||
new_email?: string
|
||||
new_phone?: string
|
||||
invited_at?: string
|
||||
action_link?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
created_at: string
|
||||
confirmed_at?: string
|
||||
email_confirmed_at?: string
|
||||
phone_confirmed_at?: string
|
||||
last_sign_in_at?: string
|
||||
role?: string
|
||||
updated_at?: string
|
||||
identities?: UserIdentity[]
|
||||
is_anonymous?: boolean
|
||||
is_sso_user?: boolean
|
||||
factors?: (Factor<FactorType, 'verified'> | Factor<FactorType, 'unverified'>)[]
|
||||
deleted_at?: string
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './commons.js';
|
||||
export * from './ui/schemas.js';
|
||||
export * from './ui/page-iterator.js';
|
||||
export * from './competitors/schemas.js';
|
||||
|
||||
@ -46,8 +46,6 @@ const enablePlaygrounds = import.meta.env.VITE_ENABLE_PLAYGROUNDS === 'true';
|
||||
|
||||
let PlaygroundEditor: any;
|
||||
let PlaygroundEditorLLM: any;
|
||||
let VideoPlayerPlayground: any;
|
||||
let VideoFeedPlayground: any;
|
||||
let VideoPlayerPlaygroundIntern: any;
|
||||
let PlaygroundImages: any;
|
||||
let PlaygroundImageEditor: any;
|
||||
@ -77,7 +75,6 @@ if (enablePlaygrounds) {
|
||||
PlaygroundImages = React.lazy(() => import("./pages/PlaygroundImages"));
|
||||
PlaygroundImageEditor = React.lazy(() => import("./pages/PlaygroundImageEditor"));
|
||||
VideoGenPlayground = React.lazy(() => import("./pages/VideoGenPlayground"));
|
||||
PlaygroundCanvas = React.lazy(() => import("./modules/layout/PlaygroundCanvas"));
|
||||
VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor })));
|
||||
I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
|
||||
PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat"));
|
||||
@ -184,10 +181,7 @@ const AppWrapper = () => {
|
||||
<>
|
||||
<Route path="/playground/editor" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundEditor /></React.Suspense>} />
|
||||
<Route path="/playground/editor-llm" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundEditorLLM /></React.Suspense>} />
|
||||
<Route path="/playground/video-player" element={<React.Suspense fallback={<div>Loading...</div>}><VideoPlayerPlayground /></React.Suspense>} />
|
||||
<Route path="/playground-video-player-intern" element={<React.Suspense fallback={<div>Loading...</div>}><VideoPlayerPlaygroundIntern /></React.Suspense>} />
|
||||
<Route path="/video-feed" element={<React.Suspense fallback={<div>Loading...</div>}><VideoFeedPlayground /></React.Suspense>} />
|
||||
<Route path="/video-feed/:id" element={<React.Suspense fallback={<div>Loading...</div>}><VideoFeedPlayground /></React.Suspense>} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -271,7 +271,7 @@ const TopNavigation = () => {
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{roles.includes("admin") && (
|
||||
{roles && roles.includes("admin") && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/admin/users" className="flex items-center">
|
||||
|
||||
@ -1,471 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLayout } from '@/modules/layout/LayoutContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useWidgetLoader } from './useWidgetLoader.tsx';
|
||||
import { useLayouts } from '@/modules/layout/useLayouts';
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import { apiClient } from "@/lib/db";
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type LayoutVisibility = Database['public']['Enums']['layout_visibility'];
|
||||
|
||||
export function usePlaygroundLogic() {
|
||||
// UI State
|
||||
const [viewMode, setViewMode] = useState<'design' | 'preview'>('design');
|
||||
const [previewHtml, setPreviewHtml] = useState<string>('');
|
||||
const [htmlSize, setHtmlSize] = useState<number>(0);
|
||||
const [isAppReady, setIsAppReady] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(true);
|
||||
|
||||
// Page State
|
||||
const pageId = 'playground-canvas-demo';
|
||||
const pageName = 'Playground Canvas';
|
||||
const [layoutJson, setLayoutJson] = useState<string | null>(null);
|
||||
|
||||
// Layout Context
|
||||
const {
|
||||
loadedPages,
|
||||
importPageLayout,
|
||||
loadPageLayout
|
||||
} = useLayout();
|
||||
|
||||
// Template State (now from Supabase)
|
||||
const [templates, setTemplates] = useState<Layout[]>([]);
|
||||
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
|
||||
const [newTemplateName, setNewTemplateName] = useState('');
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(false);
|
||||
|
||||
// Paste JSON State
|
||||
const [isPasteDialogOpen, setIsPasteDialogOpen] = useState(false);
|
||||
const [pasteJsonContent, setPasteJsonContent] = useState('');
|
||||
|
||||
const { loadWidgetBundle } = useWidgetLoader();
|
||||
const { getLayouts, getLayout, createLayout, updateLayout, deleteLayout } = useLayouts();
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to save", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshTemplates();
|
||||
}, []);
|
||||
|
||||
const refreshTemplates = async () => {
|
||||
setIsLoadingTemplates(true);
|
||||
try {
|
||||
const { data, error } = await getLayouts({ type: 'canvas' });
|
||||
if (error) {
|
||||
console.error('Failed to load layouts:', error);
|
||||
toast.error('Failed to load layouts');
|
||||
} else {
|
||||
setTemplates(data || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh templates:', e);
|
||||
} finally {
|
||||
setIsLoadingTemplates(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialization Effect
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
let layout = loadedPages.get(pageId);
|
||||
if (!layout) {
|
||||
console.log('[Playground] Loading layout...');
|
||||
try {
|
||||
await loadPageLayout(pageId, pageName);
|
||||
} catch (e) {
|
||||
console.error("Failed to load layout", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [pageId, pageName, loadPageLayout, loadedPages]);
|
||||
|
||||
// Restoration Effect
|
||||
useEffect(() => {
|
||||
const restore = async () => {
|
||||
const layout = loadedPages.get(pageId);
|
||||
if (layout) {
|
||||
if (!isAppReady) {
|
||||
let detectedRootTemplate: string | undefined;
|
||||
|
||||
if (layout.loadedBundles && layout.loadedBundles.length > 0) {
|
||||
console.log('[Playground] Restoring bundles:', layout.loadedBundles);
|
||||
for (const bundleUrl of layout.loadedBundles) {
|
||||
try {
|
||||
const result = await loadWidgetBundle(bundleUrl);
|
||||
// Capture root template from the first bundle that has one
|
||||
if (result.rootTemplateUrl && !detectedRootTemplate) {
|
||||
detectedRootTemplate = result.rootTemplateUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to restore bundle", bundleUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Self-repair: If layout is missing rootTemplate but we found one, update it.
|
||||
if (detectedRootTemplate && !layout.rootTemplate) {
|
||||
console.log('[Playground] Backfilling rootTemplate:', detectedRootTemplate);
|
||||
layout.rootTemplate = detectedRootTemplate;
|
||||
layout.rootTemplate = detectedRootTemplate;
|
||||
await handleSave();
|
||||
}
|
||||
|
||||
setIsAppReady(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
// If layout exists and we haven't marked ready, try restore
|
||||
// We depend on loadedPages map or specific entry
|
||||
if (loadedPages.get(pageId) && !isAppReady) {
|
||||
restore();
|
||||
}
|
||||
}, [loadedPages, pageId, isAppReady, loadWidgetBundle]);
|
||||
|
||||
// Preview Generation Effect
|
||||
// Preview Generation Effect
|
||||
useEffect(() => {
|
||||
const updatePreview = async () => {
|
||||
const layout = loadedPages.get(pageId);
|
||||
if (layout && layout.rootTemplate) {
|
||||
try {
|
||||
const { generateEmailHtml } = await import('@/lib/emailExporter');
|
||||
let html = await generateEmailHtml(layout, layout.rootTemplate);
|
||||
|
||||
// Inject Preview-Only CSS to fix "font-size: 0" visibility issues
|
||||
// This ensures the preview looks correct without modifying the actual email export structure
|
||||
const previewFixStyles = `
|
||||
<style>
|
||||
.text-block .long-text { font-size: 16px; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
if (html.includes('</head>')) {
|
||||
html = html.replace('</head>', `${previewFixStyles}</head>`);
|
||||
} else {
|
||||
html = html.replace('<body>', `<head>${previewFixStyles}</head><body>`);
|
||||
}
|
||||
|
||||
setPreviewHtml(html);
|
||||
|
||||
// Approximate size of the email body (excluding images, as per Gmail limit on HTML only)
|
||||
const size = new Blob([html]).size;
|
||||
setHtmlSize(size);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Preview generation failed", e);
|
||||
// toast.error("Failed to generate preview"); // Suppress toast on auto-update to avoid spam
|
||||
}
|
||||
} else if (!layout?.rootTemplate) {
|
||||
setPreviewHtml("<html><body><p>No root template found. Please load the email context first.</p></body></html>");
|
||||
}
|
||||
};
|
||||
updatePreview();
|
||||
}, [loadedPages, pageId]); // Removed viewMode dependency
|
||||
|
||||
const handleDumpJson = async () => {
|
||||
if (!currentLayout) {
|
||||
toast.error("No layout loaded");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Use current state directly
|
||||
const json = JSON.stringify(currentLayout, null, 2);
|
||||
setLayoutJson(json);
|
||||
await navigator.clipboard.writeText(json);
|
||||
toast.success("JSON dumped to console, clipboard, and view");
|
||||
console.log(json);
|
||||
} catch (e) {
|
||||
console.error("Failed to dump JSON", e);
|
||||
toast.error("Failed to dump JSON");
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadTemplate = async (template: Layout) => {
|
||||
try {
|
||||
// Fetch fresh layout data to ensure we have the latest version
|
||||
const { data, error } = await getLayout(template.id);
|
||||
|
||||
if (error || !data) {
|
||||
console.error("Failed to fetch fresh layout", error);
|
||||
toast.error("Failed to load latest version of layout");
|
||||
return;
|
||||
}
|
||||
|
||||
// layout_json is already a parsed object, convert to string for importPageLayout
|
||||
const layoutJsonString = JSON.stringify(data.layout_json);
|
||||
await importPageLayout(pageId, layoutJsonString);
|
||||
toast.success(`Loaded layout: ${data.name || template.name}`);
|
||||
setLayoutJson(null);
|
||||
} catch (e) {
|
||||
console.error("Failed to load layout", e);
|
||||
toast.error("Failed to load layout");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveTemplate = async () => {
|
||||
if (!newTemplateName.trim()) {
|
||||
toast.error("Please enter a layout name");
|
||||
return;
|
||||
}
|
||||
if (!currentLayout) {
|
||||
toast.error("No layout loaded to save");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const layoutObject = currentLayout;
|
||||
|
||||
const { data, error } = await createLayout({
|
||||
name: newTemplateName.trim(),
|
||||
layout_json: layoutObject as any,
|
||||
type: 'canvas',
|
||||
visibility: 'private' as LayoutVisibility,
|
||||
meta: {}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to save layout:', error);
|
||||
toast.error('Failed to save layout');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Layout saved to database");
|
||||
setIsSaveDialogOpen(false);
|
||||
setNewTemplateName('');
|
||||
await refreshTemplates();
|
||||
} catch (e) {
|
||||
console.error("Failed to save layout", e);
|
||||
toast.error("Failed to save layout");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasteJson = async () => {
|
||||
if (!pasteJsonContent.trim()) {
|
||||
toast.error("Please enter JSON content");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSON.parse(pasteJsonContent);
|
||||
await importPageLayout(pageId, pasteJsonContent);
|
||||
toast.success("Layout imported from JSON");
|
||||
setIsPasteDialogOpen(false);
|
||||
setPasteJsonContent('');
|
||||
setLayoutJson(null);
|
||||
} catch (e) {
|
||||
console.error("Failed to import JSON", e);
|
||||
toast.error("Invalid JSON format");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = async (layoutId: string) => {
|
||||
try {
|
||||
const { error } = await deleteLayout(layoutId);
|
||||
if (error) {
|
||||
console.error('Failed to delete layout:', error);
|
||||
toast.error('Failed to delete layout');
|
||||
return;
|
||||
}
|
||||
toast.success('Layout deleted');
|
||||
await refreshTemplates();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete layout:', e);
|
||||
toast.error('Failed to delete layout');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVisibility = async (layoutId: string, currentVisibility: LayoutVisibility) => {
|
||||
try {
|
||||
// Cycle through visibility options: private -> listed -> public -> private
|
||||
const visibilityOrder: LayoutVisibility[] = ['private', 'listed', 'public'];
|
||||
const currentIndex = visibilityOrder.indexOf(currentVisibility);
|
||||
const newVisibility = visibilityOrder[(currentIndex + 1) % visibilityOrder.length];
|
||||
|
||||
const { error } = await updateLayout(layoutId, {
|
||||
visibility: newVisibility
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to update visibility:', error);
|
||||
toast.error('Failed to update visibility');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Layout visibility: ${newVisibility}`);
|
||||
await refreshTemplates();
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle visibility:', e);
|
||||
toast.error('Failed to toggle visibility');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameLayout = async (layoutId: string, newName: string) => {
|
||||
if (!newName.trim()) {
|
||||
toast.error('Layout name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await updateLayout(layoutId, {
|
||||
name: newName.trim()
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to rename layout:', error);
|
||||
toast.error('Failed to rename layout');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Layout renamed');
|
||||
await refreshTemplates();
|
||||
} catch (e) {
|
||||
console.error('Failed to rename layout:', e);
|
||||
toast.error('Failed to rename layout');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadContext = async () => {
|
||||
const bundleUrl = '/widgets/email/library.json';
|
||||
try {
|
||||
const result = await loadWidgetBundle(bundleUrl);
|
||||
const { count, rootTemplateUrl } = result;
|
||||
|
||||
toast.success(`Loaded ${count} email widgets`);
|
||||
|
||||
const currentLayout = loadedPages.get(pageId);
|
||||
if (currentLayout) {
|
||||
const bundles = new Set(currentLayout.loadedBundles || []);
|
||||
let changed = false;
|
||||
|
||||
if (!bundles.has(bundleUrl)) {
|
||||
bundles.add(bundleUrl);
|
||||
currentLayout.loadedBundles = Array.from(bundles);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (rootTemplateUrl && currentLayout.rootTemplate !== rootTemplateUrl) {
|
||||
currentLayout.rootTemplate = rootTemplateUrl;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (changed) {
|
||||
await handleSave();
|
||||
toast.success("Context saved to layout");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load context", e);
|
||||
toast.error("Failed to load email context");
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportHtml = async () => {
|
||||
const layout = loadedPages.get(pageId);
|
||||
if (!layout) return;
|
||||
|
||||
if (!layout.rootTemplate) {
|
||||
toast.error("No root template found. Please load a context first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { generateEmailHtml } = await import('@/lib/emailExporter');
|
||||
const html = await generateEmailHtml(layout, layout.rootTemplate);
|
||||
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${layout.name.toLowerCase().replace(/\s+/g, '-')}.html`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success("Exported HTML");
|
||||
} catch (e) {
|
||||
console.error("Failed to export HTML", e);
|
||||
toast.error("Export failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendTestEmail = async () => {
|
||||
const layout = loadedPages.get(pageId);
|
||||
if (!layout) {
|
||||
toast.error("Layout not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layout.rootTemplate) {
|
||||
toast.error("No root template found. Please load a context first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toast.info("Generating email...");
|
||||
const { generateEmailHtml } = await import('@/lib/emailExporter');
|
||||
let html = await generateEmailHtml(layout, layout.rootTemplate);
|
||||
|
||||
const dummyId = import.meta.env.DEFAULT_POST_TEST_ID || '00000000-0000-0000-0000-000000000000';
|
||||
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
|
||||
|
||||
toast.info("Sending test email...");
|
||||
|
||||
await apiClient(`/api/send/email/${dummyId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
html,
|
||||
subject: `[Test] ${layout.name} - ${new Date().toLocaleTimeString()}`
|
||||
})
|
||||
});
|
||||
toast.success("Test email sent!");
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to send test email", e);
|
||||
toast.error("Failed to send test email");
|
||||
}
|
||||
};
|
||||
|
||||
const currentLayout = loadedPages.get(pageId);
|
||||
|
||||
return {
|
||||
// State
|
||||
currentLayout,
|
||||
viewMode, setViewMode,
|
||||
previewHtml,
|
||||
htmlSize,
|
||||
isAppReady,
|
||||
isEditMode, setIsEditMode,
|
||||
pageId,
|
||||
pageName,
|
||||
layoutJson,
|
||||
templates,
|
||||
isLoadingTemplates,
|
||||
isSaveDialogOpen, setIsSaveDialogOpen,
|
||||
newTemplateName, setNewTemplateName,
|
||||
isPasteDialogOpen, setIsPasteDialogOpen,
|
||||
pasteJsonContent, setPasteJsonContent,
|
||||
|
||||
// Handlers
|
||||
handleDumpJson,
|
||||
handleLoadTemplate,
|
||||
handleSaveTemplate,
|
||||
handleDeleteTemplate,
|
||||
handleToggleVisibility,
|
||||
handleRenameLayout,
|
||||
handlePasteJson,
|
||||
handleLoadContext,
|
||||
handleExportHtml,
|
||||
handleSendTestEmail,
|
||||
importPageLayout // Expose importPageLayout for direct external updates
|
||||
};
|
||||
}
|
||||
@ -95,13 +95,11 @@ const getAuthToken = async (): Promise<string | null> => {
|
||||
|
||||
// Create OpenAI client
|
||||
export const createOpenAIClient = async (apiKey?: string): Promise<OpenAI | null> => {
|
||||
// We use the Supabase session token as the "apiKey" for the proxy
|
||||
// If a legacy OpenAI key (sk-...) is passed, we ignore it and use the session token
|
||||
let token = apiKey;
|
||||
|
||||
if (!token || token.startsWith('sk-')) {
|
||||
if (token?.startsWith('sk-')) {
|
||||
consoleLogger.warn('Legacy OpenAI key detected and ignored. Using Supabase session token for proxy.');
|
||||
consoleLogger.warn('Legacy OpenAI key detected and ignored. Using Zitadel session token for proxy.');
|
||||
}
|
||||
token = (await getAuthToken()) || undefined;
|
||||
}
|
||||
|
||||
@ -234,7 +234,6 @@ export function useChatEngine(namespace = 'chat') {
|
||||
if (!user) return null;
|
||||
if (prov === 'openai') {
|
||||
// Return null since the createOpenAIClient will automatically grab
|
||||
// the Supabase session token instead of requiring raw OpenAI keys.
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
|
||||
@ -4,7 +4,7 @@ import GenericCanvasView from './GenericCanvasView';
|
||||
// Re-export the full props type so existing consumers keep working
|
||||
export type { GenericCanvasEditProps as GenericCanvasProps } from './GenericCanvasEdit';
|
||||
|
||||
// Lazy-load the heavy edit component (WidgetPalette, upload utils, supabase, etc.)
|
||||
// Lazy-load the heavy edit component (WidgetPalette, upload utils, etc.)
|
||||
const GenericCanvasEdit = lazy(() => import('./GenericCanvasEdit'));
|
||||
|
||||
import type { GenericCanvasEditProps } from './GenericCanvasEdit';
|
||||
|
||||
@ -1,260 +0,0 @@
|
||||
import { toast } from 'sonner';
|
||||
import React, { useEffect } from 'react';
|
||||
import { GenericCanvas } from '@/modules/layout/GenericCanvas';
|
||||
import { usePlaygroundLogic } from '@/hooks/usePlaygroundLogic.tsx';
|
||||
import { useWebSocket } from '@/contexts/WS_Socket';
|
||||
import { PlaygroundHeader } from '@/components/playground/PlaygroundHeader';
|
||||
import { TemplateDialogs } from '@/components/playground/TemplateDialogs';
|
||||
import { useSelection } from '@/hooks/useSelection';
|
||||
import { SelectionHandler } from '@/modules/layout/SelectionHandler';
|
||||
import { WidgetPropertyPanel } from '@/components/widgets/WidgetPropertyPanel';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { LayoutProvider } from '@/modules/layout/LayoutContext';
|
||||
|
||||
const PlaygroundCanvasContent = () => {
|
||||
const {
|
||||
// State
|
||||
viewMode, setViewMode,
|
||||
previewHtml,
|
||||
isAppReady,
|
||||
isEditMode, setIsEditMode,
|
||||
pageId,
|
||||
pageName,
|
||||
layoutJson,
|
||||
templates,
|
||||
isSaveDialogOpen, setIsSaveDialogOpen,
|
||||
newTemplateName, setNewTemplateName,
|
||||
isPasteDialogOpen, setIsPasteDialogOpen,
|
||||
pasteJsonContent, setPasteJsonContent,
|
||||
|
||||
// Handlers
|
||||
handleDumpJson,
|
||||
handleLoadTemplate,
|
||||
handleSaveTemplate,
|
||||
handleDeleteTemplate,
|
||||
handleToggleVisibility,
|
||||
handleRenameLayout,
|
||||
handlePasteJson,
|
||||
handleLoadContext,
|
||||
handleExportHtml,
|
||||
handleSendTestEmail,
|
||||
htmlSize,
|
||||
currentLayout,
|
||||
importPageLayout
|
||||
} = usePlaygroundLogic();
|
||||
|
||||
const { connectToServer, isConnected, wsStatus, disconnectFromServer } = useWebSocket();
|
||||
const { selectedWidgetId, selectWidget, clearSelection, moveSelection } = useSelection({ pageId });
|
||||
|
||||
// Log "playground ready" when widgets are loaded
|
||||
useEffect(() => {
|
||||
if (isAppReady && isConnected) {
|
||||
// We need to access the socket directly or expose a send method.
|
||||
// The context currently only exposes connection methods.
|
||||
// We should update the context or modbusService to allow sending generic messages.
|
||||
// But modbusService.sendCommand exists.
|
||||
import('@/services/modbusService').then(module => {
|
||||
module.default.sendCommand('log', {
|
||||
name: 'playground-canvas',
|
||||
level: 'info',
|
||||
message: 'Playground ready, loaded widgets',
|
||||
|
||||
}).catch(err => console.error('Failed to send log:', err));
|
||||
});
|
||||
}
|
||||
}, [isAppReady, isConnected]);
|
||||
|
||||
// Auto-log Layout JSON on change
|
||||
useEffect(() => {
|
||||
if (isConnected && currentLayout) {
|
||||
import('@/services/modbusService').then(module => {
|
||||
const service = module.default;
|
||||
service.log({
|
||||
name: 'canvas-page-latest',
|
||||
message: currentLayout,
|
||||
options: { mode: 'overwrite', format: 'json' }
|
||||
}).catch(err => console.error('Failed to log layout json:', err));
|
||||
});
|
||||
}
|
||||
}, [currentLayout, isConnected]);
|
||||
|
||||
// Auto-log Preview HTML on change
|
||||
useEffect(() => {
|
||||
if (isConnected && previewHtml) {
|
||||
import('@/services/modbusService').then(module => {
|
||||
const service = module.default;
|
||||
service.log({
|
||||
name: 'canvas-html-latest',
|
||||
message: previewHtml,
|
||||
options: { mode: 'overwrite', format: 'html' }
|
||||
}).catch(err => console.error('Failed to log preview html:', err));
|
||||
});
|
||||
}
|
||||
}, [previewHtml, isConnected]);
|
||||
|
||||
// Handle external layout updates (from file watcher)
|
||||
// Handle external layout updates (from file watcher)
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let isCancelled = false;
|
||||
|
||||
import('@/services/modbusService').then(module => {
|
||||
if (isCancelled) return;
|
||||
const service = module.default;
|
||||
unsubscribe = service.addMessageHandler('layout-update', (data) => {
|
||||
console.log(`[Playground] Received layout update`, typeof data);
|
||||
if (typeof data === 'string') {
|
||||
// Handle Base64 content (HTML/MD)
|
||||
try {
|
||||
const decoded = atob(data);
|
||||
if (decoded.trim().startsWith('<')) {
|
||||
toast.info("Received HTML update (View in logs)");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to decode base64 layout update', e);
|
||||
}
|
||||
} else {
|
||||
// Handle JSON Object (Layout)
|
||||
importPageLayout(pageId, JSON.stringify(data)).then(() => {
|
||||
console.log('[Playground] External layout applied successfully');
|
||||
toast.info(`Layout updated from watcher`);
|
||||
}).catch(err => {
|
||||
console.error('Failed to import external layout:', err);
|
||||
toast.error(`Failed to update layout from watcher`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
if (unsubscribe) unsubscribe();
|
||||
};
|
||||
}
|
||||
}, [isConnected, pageId, importPageLayout]);
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-3.5rem)] bg-background flex flex-col overflow-hidden">
|
||||
<PlaygroundHeader
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
handleExportHtml={handleExportHtml}
|
||||
htmlSize={htmlSize}
|
||||
onSendTestEmail={handleSendTestEmail}
|
||||
templates={templates}
|
||||
handleLoadTemplate={handleLoadTemplate}
|
||||
handleDeleteTemplate={handleDeleteTemplate}
|
||||
handleToggleVisibility={handleToggleVisibility}
|
||||
handleRenameLayout={handleRenameLayout}
|
||||
onSaveTemplateClick={() => setIsSaveDialogOpen(true)}
|
||||
onPasteJsonClick={() => setIsPasteDialogOpen(true)}
|
||||
handleDumpJson={handleDumpJson}
|
||||
handleLoadContext={handleLoadContext}
|
||||
isEditMode={isEditMode}
|
||||
setIsEditMode={setIsEditMode}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-0 bg-slate-50 dark:bg-slate-900/50">
|
||||
{!isAppReady ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
<p className="text-sm text-muted-foreground">Restoring Playground...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={viewMode === 'design' ? 'h-full' : 'hidden'}>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
autoSaveId="playground-layout"
|
||||
className="h-full"
|
||||
>
|
||||
<ResizablePanel defaultSize={75} minSize={50}>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-8">
|
||||
<SelectionHandler
|
||||
onMoveSelection={moveSelection}
|
||||
onClearSelection={clearSelection}
|
||||
enabled={viewMode === 'design'}
|
||||
/>
|
||||
<GenericCanvas
|
||||
pageId={pageId}
|
||||
pageName={pageName}
|
||||
isEditMode={isEditMode}
|
||||
showControls={true}
|
||||
selectedWidgetId={selectedWidgetId}
|
||||
onSelectWidget={selectWidget}
|
||||
/>
|
||||
{layoutJson && (
|
||||
<div className="mt-8 p-4 border rounded-lg bg-muted/50">
|
||||
<h3 className="text-sm font-semibold mb-2">Layout JSON</h3>
|
||||
<pre className="text-xs font-mono overflow-auto max-h-[400px] whitespace-pre-wrap break-all bg-background p-4 rounded border">
|
||||
{layoutJson}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</ResizablePanel>
|
||||
|
||||
{/* Property Panel - Desktop Only (Hidden on mobile via CSS) */}
|
||||
{selectedWidgetId && isEditMode && (
|
||||
<>
|
||||
<ResizableHandle withHandle className="hidden md:flex" />
|
||||
<ResizablePanel
|
||||
defaultSize={25}
|
||||
minSize={15}
|
||||
maxSize={40}
|
||||
className="hidden md:block"
|
||||
// Ensure panel state is remembered or defaults gracefully
|
||||
id="property-panel"
|
||||
order={2}
|
||||
>
|
||||
<WidgetPropertyPanel
|
||||
pageId={pageId}
|
||||
selectedWidgetId={selectedWidgetId}
|
||||
onWidgetRenamed={selectWidget}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'preview' ? 'block h-full w-full' : 'hidden'}>
|
||||
<iframe
|
||||
title="Email Preview"
|
||||
srcDoc={previewHtml}
|
||||
className="w-full h-full border-0 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TemplateDialogs
|
||||
isSaveDialogOpen={isSaveDialogOpen}
|
||||
setIsSaveDialogOpen={setIsSaveDialogOpen}
|
||||
newTemplateName={newTemplateName}
|
||||
setNewTemplateName={setNewTemplateName}
|
||||
handleSaveTemplate={handleSaveTemplate}
|
||||
isPasteDialogOpen={isPasteDialogOpen}
|
||||
setIsPasteDialogOpen={setIsPasteDialogOpen}
|
||||
pasteJsonContent={pasteJsonContent}
|
||||
setPasteJsonContent={setPasteJsonContent}
|
||||
handlePasteJson={handlePasteJson}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlaygroundCanvas = () => (
|
||||
<LayoutProvider>
|
||||
<PlaygroundCanvasContent />
|
||||
</LayoutProvider>
|
||||
);
|
||||
|
||||
export default PlaygroundCanvas;
|
||||
@ -7,9 +7,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Search, Plus, X, Copy, Trash2, Pencil, Check, BookmarkCheck, Loader2 } from 'lucide-react';
|
||||
import { T, translate } from '@/i18n';
|
||||
import { useWidgetSnippets, WidgetSnippetData } from './useWidgetSnippets';
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
interface WidgetPaletteProps {
|
||||
isVisible: boolean;
|
||||
|
||||
@ -1,138 +1,39 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { fetchWithDeduplication, getAuthToken } from "@/lib/db";
|
||||
import { apiClient, fetchWithDeduplication } from "@/lib/db";
|
||||
|
||||
export const createLayout = async (layoutData: any, client?: SupabaseClient) => {
|
||||
const token = await getAuthToken();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/layouts`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(layoutData)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to create layout: ${res.statusText}`);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
export const createLayout = async (layoutData: any) => {
|
||||
return apiClient(`/api/layouts`, { method: 'POST', body: JSON.stringify(layoutData) });
|
||||
};
|
||||
|
||||
export const getLayout = async (layoutId: string, client?: SupabaseClient) => {
|
||||
const key = `layout-${layoutId}`;
|
||||
return fetchWithDeduplication(key, async () => {
|
||||
const token = await getAuthToken();
|
||||
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/layouts/${layoutId}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch layout: ${res.statusText}`);
|
||||
}
|
||||
|
||||
// Wrap in object to match Supabase response format { data, error }
|
||||
const data = await res.json();
|
||||
return { data, error: null };
|
||||
});
|
||||
export const getLayout = async (layoutId: string) => {
|
||||
return fetchWithDeduplication(`layout-${layoutId}`, () =>
|
||||
apiClient(`/api/layouts/${layoutId}`)
|
||||
);
|
||||
};
|
||||
|
||||
export const getLayouts = async (filters?: { type?: string, visibility?: string, limit?: number, offset?: number }, client?: SupabaseClient) => {
|
||||
export const getLayouts = async (filters?: { type?: string, visibility?: string, limit?: number, offset?: number }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.type) params.append('type', filters.type);
|
||||
if (filters?.visibility) params.append('visibility', filters.visibility);
|
||||
if (filters?.limit) params.append('limit', filters.limit.toString());
|
||||
if (filters?.offset) params.append('offset', filters.offset.toString());
|
||||
|
||||
const key = `layouts-${params.toString()}`;
|
||||
return fetchWithDeduplication(key, async () => {
|
||||
const token = await getAuthToken();
|
||||
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/layouts?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch layouts: ${res.statusText}`);
|
||||
}
|
||||
|
||||
// Wrap in object to match Supabase response format { data, error }
|
||||
const data = await res.json();
|
||||
return { data, error: null };
|
||||
});
|
||||
const qs = params.toString();
|
||||
return fetchWithDeduplication(`layouts-${qs}`, () =>
|
||||
apiClient(`/api/layouts${qs ? `?${qs}` : ''}`)
|
||||
);
|
||||
};
|
||||
|
||||
export const updateLayoutMeta = async (layoutId: string, metaUpdates: any) => {
|
||||
// Fetch current layout to merge meta
|
||||
const token = await getAuthToken();
|
||||
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
// Get current layout
|
||||
const getRes = await fetch(`/api/layouts/${layoutId}`, { headers });
|
||||
if (!getRes.ok) throw new Error(`Failed to fetch layout: ${getRes.statusText}`);
|
||||
const layout = await getRes.json();
|
||||
|
||||
const currentMeta = (layout?.meta as any) || {};
|
||||
const newMeta = { ...currentMeta, ...metaUpdates };
|
||||
|
||||
// Update layout with merged meta
|
||||
const updateRes = await fetch(`/api/layouts/${layoutId}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ meta: newMeta })
|
||||
});
|
||||
|
||||
if (!updateRes.ok) throw new Error(`Failed to update layout meta: ${updateRes.statusText}`);
|
||||
return await updateRes.json();
|
||||
};
|
||||
export const updateLayout = async (layoutId: string, layoutData: any, client?: SupabaseClient) => {
|
||||
const token = await getAuthToken();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/layouts/${layoutId}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(layoutData)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to update layout: ${res.statusText}`);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
const layout: any = await apiClient(`/api/layouts/${layoutId}`);
|
||||
const newMeta = { ...(layout?.meta ?? {}), ...metaUpdates };
|
||||
return apiClient(`/api/layouts/${layoutId}`, { method: 'PATCH', body: JSON.stringify({ meta: newMeta }) });
|
||||
};
|
||||
|
||||
export const updateLayout = async (layoutId: string, layoutData: any) => {
|
||||
return apiClient(`/api/layouts/${layoutId}`, { method: 'PATCH', body: JSON.stringify(layoutData) });
|
||||
};
|
||||
|
||||
export const deleteLayout = async (layoutId: string, client?: SupabaseClient) => {
|
||||
const token = await getAuthToken();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/layouts/${layoutId}`, {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to delete layout: ${res.statusText}`);
|
||||
}
|
||||
|
||||
export const deleteLayout = async (layoutId: string) => {
|
||||
await apiClient(`/api/layouts/${layoutId}`, { method: 'DELETE' });
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import { getLayouts, getLayout, createLayout, updateLayout, deleteLayout } from './client-layouts';
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type LayoutInsert = Database['public']['Tables']['layouts']['Insert'];
|
||||
type LayoutUpdate = Database['public']['Tables']['layouts']['Update'];
|
||||
type LayoutVisibility = Database['public']['Enums']['layout_visibility'];
|
||||
import { getLayouts, getLayout, createLayout, updateLayout, deleteLayout } from './client-layouts';
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
type LayoutInsert = any; // TODO: replace with actual type
|
||||
type LayoutUpdate = any; // TODO: replace with actual type
|
||||
type LayoutVisibility = any; // TODO: replace with actual type
|
||||
|
||||
export interface UseLayoutsReturn {
|
||||
getLayouts: (filters?: {
|
||||
|
||||
@ -2,9 +2,8 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { translate } from "@/i18n";
|
||||
import { useLayouts } from "./useLayouts";
|
||||
import { Database } from "@/integrations/supabase/types";
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
const SNIPPET_TYPE = 'widget-snippet';
|
||||
|
||||
|
||||
@ -3,8 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { T, translate } from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
|
||||
import { Page } from "./types";
|
||||
import { Eye, EyeOff, Edit3, Trash2, Share2, Link as LinkIcon, FileText, Download, FolderTree, FileJson, LayoutTemplate, ShoppingCart, ExternalLink, History } from "lucide-react";
|
||||
import { useCartStore } from "@polymech/ecommerce";
|
||||
import {
|
||||
@ -22,9 +21,8 @@ import { updatePage, updatePageMeta, deletePage } from "./client-pages";
|
||||
const CategoryManager = React.lazy(() => import("@/components/widgets/CategoryManager").then(module => ({ default: module.CategoryManager })));
|
||||
const VersionManager = React.lazy(() => import("./VersionManager").then(module => ({ default: module.VersionManager })));
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
import { Page } from "./types";
|
||||
|
||||
interface PageActionsProps {
|
||||
page: Page;
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { Page, UserProfile } from "../types";
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import UserPageDetailsView from './UserPageDetailsView';
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
export interface UserPageDetailsProps {
|
||||
page: Page;
|
||||
|
||||
@ -16,10 +16,9 @@ import { useLayout } from "@/modules/layout/LayoutContext";
|
||||
import { UpdatePageMetaCommand } from "@/modules/layout/commands";
|
||||
import { Page, UserProfile } from "../types";
|
||||
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import { updatePageMeta } from '../client-pages';
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
export interface UserPageDetailsEditProps {
|
||||
page: Page;
|
||||
|
||||
@ -5,9 +5,8 @@ import { T } from "@/i18n";
|
||||
import { FileText, Calendar, FolderTree, EyeOff } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Page, UserProfile } from "../types";
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
const PageActions = React.lazy(() => import("../PageActions").then(module => ({ default: module.PageActions })));
|
||||
|
||||
|
||||
@ -4,9 +4,8 @@ import { translate } from "@/i18n";
|
||||
import { useLayout } from "@/modules/layout/LayoutContext";
|
||||
import { useLayouts } from "@/modules/layout/useLayouts";
|
||||
import { createLayout, updateLayout, deleteLayout } from "@/modules/layout/client-layouts";
|
||||
import { Database } from "@/integrations/supabase/types";
|
||||
|
||||
type Layout = Database['public']['Tables']['layouts']['Row'];
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
interface UseTemplateManagerParams {
|
||||
pageId: string | undefined;
|
||||
|
||||
@ -63,7 +63,6 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { T, translate } from "@/i18n";
|
||||
import { Database as DatabaseType } from '@/integrations/supabase/types';
|
||||
import { CategoryManager } from "@/components/widgets/CategoryManager";
|
||||
import { VariablesEditor } from "@/components/variables/VariablesEditor";
|
||||
import { mergePageVariables } from "@/lib/page-variables";
|
||||
@ -77,12 +76,12 @@ import { ActionProvider } from "@/actions/ActionProvider";
|
||||
import { useActions } from "@/actions/useActions";
|
||||
import { UNDO_ACTION_ID, REDO_ACTION_ID, FINISH_ACTION_ID, CANCEL_ACTION_ID } from "@/actions/default-actions";
|
||||
import { updatePage } from '../../client-pages';
|
||||
import { Page } from "../../types";
|
||||
|
||||
const { UpdatePageParentCommand, UpdatePageMetaCommand } = PageCommands;
|
||||
type Layout = DatabaseType['public']['Tables']['layouts']['Row'];
|
||||
|
||||
type Layout = any; // TODO: replace with actual type
|
||||
|
||||
import { Page } from "../../types";
|
||||
|
||||
// Snippets sub-component (uses hook, must be its own component)
|
||||
const SnippetsRibbonGroup: React.FC<{
|
||||
|
||||
@ -3,28 +3,7 @@ import { MediaItem } from "@/types";
|
||||
import { fetchWithDeduplication, apiClient, getAuthHeaders } from "@/lib/db";
|
||||
import { uploadImage } from "@/lib/uploadUtils";
|
||||
import { FetchMediaOptions } from "@/utils/mediaUtils";
|
||||
/*
|
||||
{
|
||||
"id": "widget-1772107599497-uw366gapl",
|
||||
"order": 0,
|
||||
"props": {
|
||||
"postId": "863996a5-8400-450d-8189-046c64927e0e",
|
||||
"imageFit": "contain",
|
||||
"pictureId": "8662babe-6407-47eb-b068-3b335e144049",
|
||||
"showTitle": true,
|
||||
"variables": {},
|
||||
"showAuthor": false,
|
||||
"showFooter": true,
|
||||
"showHeader": false,
|
||||
"showActions": false,
|
||||
"contentDisplay": "below",
|
||||
"showDescription": false
|
||||
},
|
||||
"rowId": "row-1772107451319-24v5mwood",
|
||||
"column": 1,
|
||||
"widgetId": "photo-card"
|
||||
}
|
||||
*/
|
||||
|
||||
export const fetchVersions = async (mediaItem: PostMediaItem, userId?: string) => {
|
||||
const key = `versions-${mediaItem.id}-${userId || 'anon'}`;
|
||||
return fetchWithDeduplication(key, async () => {
|
||||
@ -220,7 +199,6 @@ export const fetchMediaItemsByIds = async (
|
||||
// Call server API endpoint using apiClient
|
||||
const data = await apiClient<any[]>(`/api/media-items?${params.toString()}`);
|
||||
|
||||
// The server returns raw Supabase data, so we need to adapt it
|
||||
const { adaptSupabasePicturesToMediaItems } = await import('@/modules/posts/views/adapters');
|
||||
return adaptSupabasePicturesToMediaItems(data);
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import {
|
||||
MediaItem,
|
||||
ImageMediaItem,
|
||||
@ -10,7 +9,7 @@ import {
|
||||
} from '@/types';
|
||||
import { detectMediaType } from '@/lib/mediaRegistry';
|
||||
|
||||
type SupabasePicture = Database['public']['Tables']['pictures']['Row'];
|
||||
type SupabasePicture = any; // TODO: replace with actual type
|
||||
|
||||
/**
|
||||
* Adapter to convert Supabase picture row to MediaItem discriminated union
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import ImageLightbox from "@/components/ImageLightbox";
|
||||
import { PostMediaItem } from "../types";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { User } from "@polymech/shared";
|
||||
|
||||
// Lazy load the LLM-powered editor — only fetched when user is logged in
|
||||
const SmartLightboxEditor = lazy(() => import('./SmartLightboxEditor'));
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ImageLightbox from "@/components/ImageLightbox";
|
||||
import { usePostLLM } from "../llm";
|
||||
import { PostMediaItem } from "../types";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { User } from "@polymech/shared";
|
||||
|
||||
interface SmartLightboxEditorProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { ImageFile, MediaItem } from "@/types";
|
||||
import { User } from "@polymech/shared";
|
||||
|
||||
// PostMediaItem extends MediaItem with post-specific fields
|
||||
export type PostMediaItem = MediaItem & {
|
||||
|
||||
@ -6,7 +6,7 @@ import { MediaItem } from "@/types";
|
||||
import { translate } from "@/i18n";
|
||||
import { updateMediaPositions } from "./utils";
|
||||
import { PostItem } from './types';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { User } from "@polymech/shared";
|
||||
import { deletePost, updatePostMeta } from '@/modules/posts/client-posts';
|
||||
import { deletePictures, updatePicture } from '@/modules/posts/client-pictures';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import type { User } from "@polymech/shared";
|
||||
import type { INode } from '@/modules/storage/types';
|
||||
import type { VfsPanelActionSpec } from '@/modules/storage/useRegisterVfsPanelActions';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user