feat: Implement layout persistence for card positions and rotation, loading from and saving to the database.
This commit is contained in:
@@ -55,4 +55,5 @@ export const handleDragEnd = (store: LayoutStore, selection: string[]) => {
|
|||||||
card.finishDrag();
|
card.finishDrag();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
store.saveLayout();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export interface DesktopWorkspaceProps {
|
|||||||
onDocumentActivate?: (doc: DeskDocument, event?: unknown) => void;
|
onDocumentActivate?: (doc: DeskDocument, event?: unknown) => void;
|
||||||
onSelectionChange?: (selectedIds: Identifier[]) => void;
|
onSelectionChange?: (selectedIds: Identifier[]) => void;
|
||||||
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
|
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
|
||||||
|
tenantId?: Identifier | null;
|
||||||
|
viewId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper to handle hooks per card
|
// Wrapper to handle hooks per card
|
||||||
@@ -84,6 +86,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
onDocumentActivate,
|
onDocumentActivate,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
onDocumentTagDrop,
|
onDocumentTagDrop,
|
||||||
|
tenantId,
|
||||||
|
viewId,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -99,6 +103,12 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
const layoutStore = useMemo(() => new LayoutStore(), []);
|
const layoutStore = useMemo(() => new LayoutStore(), []);
|
||||||
const layoutRef = useRef<Map<string, LayoutCard>>(new Map());
|
const layoutRef = useRef<Map<string, LayoutCard>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tenantId && viewId) {
|
||||||
|
layoutStore.loadLayout(String(tenantId), viewId);
|
||||||
|
}
|
||||||
|
}, [layoutStore, tenantId, viewId]);
|
||||||
|
|
||||||
// Selection Context
|
// Selection Context
|
||||||
const {
|
const {
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { constrainDimensions } from './utils/layoutUtils';
|
import { constrainDimensions } from './utils/layoutUtils';
|
||||||
|
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
||||||
|
|
||||||
export interface LayoutCardState {
|
export interface LayoutCardState {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -225,6 +226,10 @@ export class LayoutStore {
|
|||||||
items = new Map<string, LayoutCard>();
|
items = new Map<string, LayoutCard>();
|
||||||
zCounter = 100;
|
zCounter = 100;
|
||||||
|
|
||||||
|
private savedLayouts = new Map<string, { x: number, y: number, rotation: number, z: number }>();
|
||||||
|
private tenantId: string | null = null;
|
||||||
|
private viewId: string | null = null;
|
||||||
|
|
||||||
initialize(id: string, ref: HTMLElement | null, config: {
|
initialize(id: string, ref: HTMLElement | null, config: {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -238,10 +243,27 @@ export class LayoutStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!card) {
|
if (!card) {
|
||||||
// Apply defaults if not provided
|
// Check for saved layout
|
||||||
const x = Math.random() * 500;
|
const saved = this.savedLayouts.get(id);
|
||||||
const y = Math.random() * 500;
|
|
||||||
const rotation = Math.random() * 10 - 5;
|
let x, y, rotation, z;
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
x = saved.x;
|
||||||
|
y = saved.y;
|
||||||
|
rotation = saved.rotation;
|
||||||
|
z = saved.z;
|
||||||
|
// Ensure zCounter is higher than any loaded z
|
||||||
|
if (z >= this.zCounter) {
|
||||||
|
this.zCounter = z + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply defaults if not provided
|
||||||
|
x = Math.random() * 500;
|
||||||
|
y = Math.random() * 500;
|
||||||
|
rotation = Math.random() * 10 - 5;
|
||||||
|
z = this.zCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
card = new LayoutCard(id, this, {
|
card = new LayoutCard(id, this, {
|
||||||
x,
|
x,
|
||||||
@@ -249,7 +271,7 @@ export class LayoutStore {
|
|||||||
rotation,
|
rotation,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
z: this.zCounter++
|
z
|
||||||
}, ref);
|
}, ref);
|
||||||
this.items.set(id, card);
|
this.items.set(id, card);
|
||||||
} else {
|
} else {
|
||||||
@@ -319,4 +341,57 @@ export class LayoutStore {
|
|||||||
getSnapshot() {
|
getSnapshot() {
|
||||||
return Array.from(this.items.values()).map(card => card.toSnapshot());
|
return Array.from(this.items.values()).map(card => card.toSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadLayout(tenantId: string, viewId: string) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.viewId = viewId;
|
||||||
|
|
||||||
|
const records = await fetchLayoutRecords({ tenantId, viewId });
|
||||||
|
|
||||||
|
this.savedLayouts.clear();
|
||||||
|
let maxZ = this.zCounter;
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.documentId && record.centerX !== undefined && record.centerY !== undefined) {
|
||||||
|
this.savedLayouts.set(record.documentId, {
|
||||||
|
x: record.centerX,
|
||||||
|
y: record.centerY,
|
||||||
|
rotation: record.rotation || 0,
|
||||||
|
z: record.zIndex || 0
|
||||||
|
});
|
||||||
|
if (record.zIndex && record.zIndex >= maxZ) {
|
||||||
|
maxZ = record.zIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zCounter = maxZ;
|
||||||
|
|
||||||
|
// Apply to existing items if any (though usually this runs before items are created)
|
||||||
|
for (const [id, card] of this.items) {
|
||||||
|
const saved = this.savedLayouts.get(id);
|
||||||
|
if (saved) {
|
||||||
|
card.update(saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveLayout() {
|
||||||
|
if (!this.tenantId || !this.viewId) return;
|
||||||
|
|
||||||
|
const entries = Array.from(this.items.values()).map(card => ({
|
||||||
|
documentId: card.id,
|
||||||
|
centerX: card.x,
|
||||||
|
centerY: card.y,
|
||||||
|
rotation: card.rotation,
|
||||||
|
zIndex: card.z,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
}));
|
||||||
|
|
||||||
|
await upsertLayoutRecords({
|
||||||
|
tenantId: this.tenantId,
|
||||||
|
viewId: this.viewId,
|
||||||
|
entries
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -839,7 +839,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
|
|
||||||
switch (viewMode) {
|
switch (viewMode) {
|
||||||
case 'desk':
|
case 'desk':
|
||||||
return <DesktopWorkspace {...viewProps} />;
|
return <DesktopWorkspace {...viewProps} tenantId={currentTenantId} viewId={deskViewId} />;
|
||||||
case 'grid':
|
case 'grid':
|
||||||
return <DocumentsGrid {...viewProps} gridIconSize={gridIconSize} />;
|
return <DocumentsGrid {...viewProps} gridIconSize={gridIconSize} />;
|
||||||
case 'list':
|
case 'list':
|
||||||
|
|||||||
Reference in New Issue
Block a user