import { useEffect, useRef, useState } from 'react' // The pivot layout, as one control rather than a row of equal chips. // // Two lists, because a layout is one of two different things: Published, which // everyone on this forecast sees and only its owner or an admin may change, and // Mine, which nobody else lists. Everything you cannot write is still there to // apply -- it is only the write controls that are withheld, so a click never // answers 403. export default function LayoutMenu({ layouts = [], activeLayoutId, dirty, onApply, onSave, onSaveAs, onSetVisibility, onSetDefault, onRename, onDelete, onReset }) { const [open, setOpen] = useState(false) const [saveName, setSaveName] = useState('') const [savePublic, setSavePublic] = useState(false) const [renaming, setRenaming] = useState(null) const [renameTo, setRenameTo] = useState('') const boxRef = useRef(null) // Close on an outside click or Escape. Capture phase: the pivot lives in a // shadow root and retargets its events, so a bubbled listener on document // sees the host rather than the real target and cannot tell inside from out. useEffect(() => { if (!open) return const onDown = (e) => { if (!boxRef.current?.contains(e.target)) setOpen(false) } const onKey = (e) => { if (e.key === 'Escape') setOpen(false) } document.addEventListener('mousedown', onDown, true) window.addEventListener('keydown', onKey) return () => { document.removeEventListener('mousedown', onDown, true) window.removeEventListener('keydown', onKey) } }, [open]) const active = layouts.find(l => l.id === activeLayoutId) const published = layouts.filter(l => l.visibility === 'published') const mine = layouts.filter(l => l.visibility === 'private') const submitSaveAs = () => { const name = saveName.trim() if (!name) return onSaveAs(name, savePublic ? 'published' : 'private') setSaveName('') setOpen(false) } const submitRename = (l) => { const name = renameTo.trim() if (name && name !== l.name) onRename(l, name) setRenaming(null) } const iconBtn = 'px-1 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100 leading-none' const row = (l) => (
{ if (renaming !== l.id) { onApply(l); setOpen(false) } }}> {renaming === l.id ? ( e.stopPropagation()} onChange={e => setRenameTo(e.target.value)} onBlur={() => submitRename(l)} onKeyDown={e => { if (e.key === 'Enter') submitRename(l) if (e.key === 'Escape') setRenaming(null) }} className="flex-1 min-w-0 border border-blue-300 rounded px-1 py-0 outline-none bg-white" /> ) : ( <> {l.name} {l.is_default && ( ★ )} {l.scope === 'source' && ( ALL )} )} {/* Withheld rather than offered-and-refused: the server allows a write only to the owner or an admin, and can_edit says which this is. */} {l.can_edit && renaming !== l.id && ( e.stopPropagation()}> {l.visibility === 'private' ? ( ) : ( <> {!l.is_default && ( )} )} )} {!l.can_edit && ( {l.owner} )}
) return (
Layout {/* Save is the common action and stays outside the menu, but only when there is something to save it to and it is yours to overwrite. */} {active?.can_edit && dirty && ( )} {open && (
{published.length > 0 && ( <>
Published
{published.map(row)}
)} {mine.length > 0 && ( <>
Mine
{mine.map(row)}
)} {!layouts.length && (
No saved layouts yet
)}
setSaveName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') submitSaveAs() }} placeholder="Save current view as…" className="flex-1 min-w-0 border border-gray-300 rounded px-1.5 py-0.5 outline-none focus:border-blue-400" />
)}
) }