Files
app/frontend/src/components/Dashboard/clipboard.ts
T
stroblme 85539741a1 Dashboard editor: copy and paste widgets across dashboards
Copy widget in the widget panel or mod+c keeps one widget in localStorage;
a paste button in the dock and mod+v insert it below the other widgets with
a fresh id. Placement is shared with adding a widget.
2026-10-16 09:20:23 +02:00

54 lines
1.8 KiB
TypeScript

import { useMemo, useSyncExternalStore } from "react"
import type { WidgetDef } from "@/client"
import { safeStorage } from "@/lib/safeStorage"
import { WIDGET_LABELS } from "./widgets"
/**
* One copied widget, waiting to be pasted.
*
* ponytail: localStorage, as with the flow editor's node clipboard, so a copy
* survives a reload and reaches every dashboard and tab of this instance. Its
* message references are flow-qualified, so it pastes anywhere here as-is.
*/
const CLIPBOARD_KEY = "fluksio.widgetClipboard"
// The `storage` event only reaches the other tabs, so this one is told here.
const listeners = new Set<() => void>()
/** What was stored, if it is still a widget this app draws. */
export function parseWidget(stored: string | null): WidgetDef | null {
if (!stored) return null
try {
const widget = JSON.parse(stored)
return Object.keys(WIDGET_LABELS).includes(widget?.type) ? widget : null
} catch {
return null
}
}
/** Replace the copy. False when the browser would not store it. */
export function copyWidget(widget: WidgetDef): boolean {
const stored = safeStorage.set(CLIPBOARD_KEY, JSON.stringify(widget))
for (const listener of listeners) listener()
return stored
}
function subscribe(listener: () => void) {
listeners.add(listener)
window.addEventListener("storage", listener)
return () => {
listeners.delete(listener)
window.removeEventListener("storage", listener)
}
}
/** The copied widget, kept current as any tab copies another. */
export function useCopiedWidget(): WidgetDef | null {
// The raw string is the snapshot: it compares by value, a parse would not.
const stored = useSyncExternalStore(subscribe, () =>
safeStorage.get(CLIPBOARD_KEY),
)
return useMemo(() => parseWidget(stored), [stored])
}