Skip to content

Commit 313de5d

Browse files
authored
Improve Plugin Studio Layout Handling & Default Plugins (#81)
* Stage 1 * Split layoutengine * Updated new plugin repo links
1 parent 0e17ede commit 313de5d

26 files changed

+3902
-237
lines changed

backend/app/core/user_initializer/initializers/github_plugin_initializer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ class GitHubPluginInitializer(UserInitializerBase):
3030
# Default plugins to install for new users
3131
DEFAULT_PLUGINS = [
3232
{
33-
"repo_url": "https://github.com/DJJones66/BrainDriveSettings",
33+
"repo_url": "https://github.com/BrainDriveAI/BrainDrive-Settings-Plugin",
3434
"version": "latest",
3535
"name": "BrainDrive Settings"
3636
},
3737
{
38-
"repo_url": "https://github.com/DJJones66/BrainDriveChat",
38+
"repo_url": "https://github.com/BrainDriveAI/BrainDrive-Chat-Plugin",
3939
"version": "latest",
4040
"name": "BrainDrive Chat"
4141
}

frontend/src/features/plugin-studio/components/canvas/DropZone.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React, { useState } from 'react';
22
import { Box, Snackbar, Alert } from '@mui/material';
33
import { usePluginStudio } from '../../hooks';
4-
import { useLayout } from '../../hooks/layout/useLayout';
5-
import { usePlugins } from '../../hooks/plugin/usePlugins';
64
import { DragData } from '../../types';
75
import { VIEW_MODE_LAYOUTS } from '../../constants';
86

@@ -17,11 +15,8 @@ interface DropZoneProps {
1715
* @returns The drop zone component
1816
*/
1917
export const DropZone: React.FC<DropZoneProps> = ({ children, onNewItem }) => {
20-
const { viewMode, currentPage, savePage } = usePluginStudio();
21-
const { getModuleById } = usePlugins();
22-
const { addItem } = useLayout(currentPage, getModuleById);
18+
const { viewMode, currentPage, savePage, addItem } = usePluginStudio();
2319
const [isDragOver, setIsDragOver] = useState(false);
24-
const [newItemId, setNewItemId] = useState<string | null>(null);
2520
const [warningOpen, setWarningOpen] = useState(false);
2621

2722
/**
@@ -145,18 +140,13 @@ export const DropZone: React.FC<DropZoneProps> = ({ children, onNewItem }) => {
145140
}
146141
}, viewMode.type === 'custom' ? 'desktop' : viewMode.type);
147142

148-
// Set the new item ID for animation
149-
setNewItemId(uniqueId);
150-
151143
// Notify parent component about the new item
152144
if (onNewItem) {
153145
onNewItem(uniqueId);
154146
}
155147

156148
// Clear the new item ID after animation completes
157149
setTimeout(() => {
158-
setNewItemId(null);
159-
160150
// Notify parent component that animation is complete
161151
if (onNewItem) {
162152
onNewItem(null);
@@ -209,4 +199,4 @@ export const DropZone: React.FC<DropZoneProps> = ({ children, onNewItem }) => {
209199
</Snackbar>
210200
</>
211201
);
212-
};
202+
};

frontend/src/features/plugin-studio/components/canvas/GridContainer.tsx

Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
1212

1313
interface GridContainerProps {
1414
layouts: Layouts | null;
15-
onLayoutChange: (layout: Layout[], newLayouts: Layouts) => void;
15+
onLayoutChange: (layout: Layout[], newLayouts: Layouts, metadata?: { origin?: { source?: string } }) => void;
1616
onResizeStart?: () => void;
1717
onResizeStop?: () => void;
1818
viewMode: ViewModeState;
@@ -35,6 +35,59 @@ export const GridContainer: React.FC<GridContainerProps> = ({
3535
newItemId = null
3636
}) => {
3737
const { selectedItem, setSelectedItem, previewMode } = usePluginStudio();
38+
const interactionSourceRef = React.useRef<'user-drag' | 'user-resize' | 'drop-add' | null>(null);
39+
const interactionResetTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
40+
41+
const cancelInteractionReset = React.useCallback(() => {
42+
if (interactionResetTimeoutRef.current) {
43+
clearTimeout(interactionResetTimeoutRef.current);
44+
interactionResetTimeoutRef.current = null;
45+
}
46+
}, []);
47+
48+
const scheduleInteractionReset = React.useCallback((delay = 150) => {
49+
cancelInteractionReset();
50+
interactionResetTimeoutRef.current = setTimeout(() => {
51+
interactionSourceRef.current = null;
52+
interactionResetTimeoutRef.current = null;
53+
}, delay);
54+
}, [cancelInteractionReset]);
55+
56+
const handleDragStart = React.useCallback(() => {
57+
cancelInteractionReset();
58+
interactionSourceRef.current = 'user-drag';
59+
}, [cancelInteractionReset]);
60+
61+
const handleDragStop = React.useCallback(() => {
62+
scheduleInteractionReset();
63+
}, [scheduleInteractionReset]);
64+
65+
const handleResizeStartInternal = React.useCallback(() => {
66+
cancelInteractionReset();
67+
interactionSourceRef.current = 'user-resize';
68+
onResizeStart?.();
69+
}, [cancelInteractionReset, onResizeStart]);
70+
71+
const handleResizeStopInternal = React.useCallback(() => {
72+
onResizeStop?.();
73+
scheduleInteractionReset();
74+
}, [onResizeStop, scheduleInteractionReset]);
75+
76+
React.useEffect(() => {
77+
if (newItemId) {
78+
cancelInteractionReset();
79+
interactionSourceRef.current = 'drop-add';
80+
scheduleInteractionReset(200);
81+
}
82+
}, [newItemId, cancelInteractionReset, scheduleInteractionReset]);
83+
84+
React.useEffect(() => {
85+
return () => {
86+
if (interactionResetTimeoutRef.current) {
87+
clearTimeout(interactionResetTimeoutRef.current);
88+
}
89+
};
90+
}, []);
3891

3992
/**
4093
* Handle item selection
@@ -90,64 +143,58 @@ export const GridContainer: React.FC<GridContainerProps> = ({
90143

91144
// Memoize the layout change handler to prevent unnecessary re-renders
92145
const handleLayoutChange = React.useCallback((currentLayout: Layout[], allLayouts: ReactGridLayouts) => {
93-
// Debounce rapid layout changes from React Grid Layout
94-
requestAnimationFrame(() => {
95-
// Convert back to our Layouts type, preserving the original GridItem properties
96-
const convertLayoutArray = (layouts: Layout[] | undefined, currentLayouts: (GridItemType | LayoutItem)[] | undefined): (GridItemType | LayoutItem)[] => {
97-
if (!layouts) return [];
98-
99-
return layouts.map(layout => {
100-
// Find the original item to preserve its properties
101-
const originalItem = currentLayouts?.find(item => item.i === layout.i);
102-
103-
if (originalItem) {
104-
// Only update if position or size actually changed
105-
if (originalItem.x === layout.x &&
106-
originalItem.y === layout.y &&
107-
originalItem.w === layout.w &&
108-
originalItem.h === layout.h) {
109-
return originalItem; // No change, return original
110-
}
111-
112-
// Preserve all properties but update position and size
113-
return {
114-
...originalItem,
115-
x: layout.x,
116-
y: layout.y,
117-
w: layout.w,
118-
h: layout.h
119-
};
120-
} else {
121-
// If original item not found, create a basic LayoutItem
122-
return {
123-
moduleUniqueId: layout.i,
124-
i: layout.i,
125-
x: layout.x,
126-
y: layout.y,
127-
w: layout.w,
128-
h: layout.h
129-
} as LayoutItem;
146+
const convertLayoutArray = (candidateLayouts: Layout[] | undefined, currentLayouts: (GridItemType | LayoutItem)[] | undefined): (GridItemType | LayoutItem)[] => {
147+
if (!candidateLayouts) return [];
148+
149+
return candidateLayouts.map(layout => {
150+
const originalItem = currentLayouts?.find(item => item.i === layout.i);
151+
152+
if (originalItem) {
153+
if (originalItem.x === layout.x &&
154+
originalItem.y === layout.y &&
155+
originalItem.w === layout.w &&
156+
originalItem.h === layout.h) {
157+
return originalItem;
130158
}
131-
});
132-
};
133-
134-
const ourLayouts: Layouts = {
135-
desktop: convertLayoutArray(allLayouts.desktop, layouts?.desktop),
136-
tablet: convertLayoutArray(allLayouts.tablet, layouts?.tablet),
137-
mobile: convertLayoutArray(allLayouts.mobile, layouts?.mobile)
138-
};
139-
140-
onLayoutChange(currentLayout, ourLayouts);
141-
});
159+
160+
return {
161+
...originalItem,
162+
x: layout.x,
163+
y: layout.y,
164+
w: layout.w,
165+
h: layout.h
166+
};
167+
}
168+
169+
return {
170+
moduleUniqueId: layout.i,
171+
i: layout.i,
172+
x: layout.x,
173+
y: layout.y,
174+
w: layout.w,
175+
h: layout.h
176+
} as LayoutItem;
177+
});
178+
};
179+
180+
const ourLayouts: Layouts = {
181+
desktop: convertLayoutArray(allLayouts.desktop, layouts?.desktop),
182+
tablet: convertLayoutArray(allLayouts.tablet, layouts?.tablet),
183+
mobile: convertLayoutArray(allLayouts.mobile, layouts?.mobile)
184+
};
185+
186+
const originSource = interactionSourceRef.current;
187+
const metadata = originSource ? { origin: { source: originSource } } : undefined;
188+
189+
onLayoutChange(currentLayout, ourLayouts, metadata);
142190
}, [layouts, onLayoutChange]);
143191

144192
return (
145193
<Box
146194
sx={{
147-
border: '2px dashed rgba(0, 0, 0, 0.1)',
148-
borderRadius: 2,
149195
minHeight: 400,
150196
width: viewWidth,
197+
maxWidth: '100%',
151198
mx: 'auto'
152199
}}
153200
>
@@ -168,8 +215,10 @@ export const GridContainer: React.FC<GridContainerProps> = ({
168215
margin={currentViewModeConfig.margin}
169216
containerPadding={currentViewModeConfig.padding}
170217
onLayoutChange={handleLayoutChange}
171-
onResizeStart={onResizeStart}
172-
onResizeStop={onResizeStop}
218+
onResizeStart={handleResizeStartInternal}
219+
onResizeStop={handleResizeStopInternal}
220+
onDragStart={handleDragStart}
221+
onDragStop={handleDragStop}
173222
isDraggable={!previewMode}
174223
isResizable={!previewMode}
175224
compactType="vertical"
@@ -204,4 +253,4 @@ export const GridContainer: React.FC<GridContainerProps> = ({
204253
</ResponsiveGridLayout>
205254
</Box>
206255
);
207-
};
256+
};

frontend/src/features/plugin-studio/components/canvas/PluginCanvas.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,8 @@ export const PluginCanvas: React.FC = () => {
1717
const containerRef = useRef<HTMLDivElement>(null);
1818
const [saveSuccess, setSaveSuccess] = useState(false);
1919
const [saveError, setSaveError] = useState(false);
20-
const [key, setKey] = useState(0); // Add a key to force re-render
2120
const [newItemId, setNewItemId] = useState<string | null>(null);
2221

23-
// Log layouts whenever they change
24-
useEffect(() => {
25-
//console.log('PluginCanvas received layouts:', layouts);
26-
if (layouts) {
27-
// console.log('Desktop layouts:', layouts.desktop);
28-
// console.log('Tablet layouts:', layouts.tablet);
29-
// console.log('Mobile layouts:', layouts.mobile);
30-
31-
// Force a re-render when layouts change
32-
setKey(prevKey => prevKey + 1);
33-
}
34-
}, [layouts]);
35-
3622
// Handle save button click
3723
const handleSave = async () => {
3824
if (!currentPage) return;
@@ -65,11 +51,10 @@ export const PluginCanvas: React.FC = () => {
6551

6652
<Box
6753
ref={containerRef}
68-
sx={{ p: 3, flex: 1, overflow: 'auto' }}
54+
sx={{ p: 0, flex: 1, overflow: 'auto' }}
6955
>
7056
<DropZone onNewItem={setNewItemId}>
7157
<GridContainer
72-
key={key} // Add key to force re-render
7358
layouts={layouts}
7459
onLayoutChange={handleLayoutChange}
7560
onResizeStart={handleResizeStart}

frontend/src/features/plugin-studio/constants/layout.constants.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export const VIEW_MODE_LAYOUTS: ViewModeConfigs = {
2626
mobile: {
2727
cols: 4,
2828
rowHeight: 50,
29-
margin: [4, 4],
30-
padding: [8, 8],
29+
margin: [0, 0],
30+
padding: [0, 0],
3131
defaultItemSize: {
3232
w: 4,
3333
h: 4
@@ -36,8 +36,8 @@ export const VIEW_MODE_LAYOUTS: ViewModeConfigs = {
3636
tablet: {
3737
cols: 8,
3838
rowHeight: 60,
39-
margin: [8, 8],
40-
padding: [12, 12],
39+
margin: [0, 0],
40+
padding: [0, 0],
4141
defaultItemSize: {
4242
w: 4,
4343
h: 4
@@ -46,8 +46,8 @@ export const VIEW_MODE_LAYOUTS: ViewModeConfigs = {
4646
desktop: {
4747
cols: 12,
4848
rowHeight: 70,
49-
margin: [12, 12],
50-
padding: [16, 16],
49+
margin: [0, 0],
50+
padding: [0, 0],
5151
defaultItemSize: {
5252
w: 3,
5353
h: 4
@@ -56,8 +56,8 @@ export const VIEW_MODE_LAYOUTS: ViewModeConfigs = {
5656
custom: {
5757
cols: 12,
5858
rowHeight: 70,
59-
margin: [12, 12],
60-
padding: [16, 16],
59+
margin: [0, 0],
60+
padding: [0, 0],
6161
defaultItemSize: {
6262
w: 3,
6363
h: 4
@@ -73,4 +73,4 @@ export const MIN_CANVAS_WIDTH = 320;
7373
/**
7474
* Default width for the plugin toolbar (in pixels)
7575
*/
76-
export const PLUGIN_TOOLBAR_WIDTH = 280;
76+
export const PLUGIN_TOOLBAR_WIDTH = 280;

frontend/src/features/plugin-studio/context/PluginStudioContext.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext } from 'react';
2-
import { Page, Layouts, ViewModeState, DynamicPluginConfig } from '../types';
2+
import { Page, Layouts, ViewModeState, DynamicPluginConfig, GridItem, LayoutItem } from '../types';
33

44
/**
55
* Interface for the PluginStudio context
@@ -23,7 +23,8 @@ export interface PluginStudioContextType {
2323

2424
// Layout state
2525
layouts: Layouts | null;
26-
handleLayoutChange: (layout: any[], newLayouts: Layouts) => void;
26+
handleLayoutChange: (layout: any[], newLayouts: Layouts, metadata?: { origin?: { source?: string }; [key: string]: unknown }) => void;
27+
addItem: (item: GridItem | LayoutItem, activeDeviceType: keyof Layouts) => void;
2728
removeItem: (id: string) => void;
2829
handleResizeStart: () => void;
2930
handleResizeStop: () => void;
@@ -60,4 +61,4 @@ export interface PluginStudioContextType {
6061
/**
6162
* Create the PluginStudio context with null as the default value
6263
*/
63-
export const PluginStudioContext = createContext<PluginStudioContextType | null>(null);
64+
export const PluginStudioContext = createContext<PluginStudioContextType | null>(null);

0 commit comments

Comments
 (0)