Pivot: save/restore edit mode and expand depth in named layouts
- Default selection mode is now SELECT_REGION - plugin.save()/restore() used to capture and apply edit mode - expand_depth tracked in ref and included in layout config - applyExpandDepth helper restores depth on layout recall and page load - Save button overwrites active layout in place (no re-typing name) - captureConfig() helper shared by save-over and save-as flows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b88795b015
commit
7c07434049
@ -74,12 +74,15 @@ function filterRowsByConfig(allRows, filters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LAYOUT_KEY = (source) => `psp_layout_${source}`
|
const LAYOUT_KEY = (source) => `psp_layout_${source}`
|
||||||
const DEFAULT_PLUGIN_CONFIG = { edit_mode: 'SELECT_ROW' }
|
const DEFAULT_PLUGIN_CONFIG = { edit_mode: 'SELECT_REGION' }
|
||||||
|
|
||||||
|
|
||||||
export default function Pivot({ source }) {
|
export default function Pivot({ source }) {
|
||||||
const viewerRef = useRef()
|
const viewerRef = useRef()
|
||||||
const workerRef = useRef()
|
const workerRef = useRef()
|
||||||
|
const tableRef = useRef()
|
||||||
const allRowsRef = useRef([])
|
const allRowsRef = useRef([])
|
||||||
|
const expandDepthRef = useRef(null)
|
||||||
const [status, setStatus] = useState('idle')
|
const [status, setStatus] = useState('idle')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [inspectedRows, setInspectedRows] = useState(null)
|
const [inspectedRows, setInspectedRows] = useState(null)
|
||||||
@ -135,8 +138,9 @@ export default function Pivot({ source }) {
|
|||||||
if (cancelled) { worker.terminate(); return }
|
if (cancelled) { worker.terminate(); return }
|
||||||
workerRef.current = worker
|
workerRef.current = worker
|
||||||
|
|
||||||
await worker.table(rows, { name: source })
|
const table = await worker.table(rows, { name: source })
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
tableRef.current = table
|
||||||
|
|
||||||
const viewer = viewerRef.current
|
const viewer = viewerRef.current
|
||||||
|
|
||||||
@ -147,18 +151,42 @@ export default function Pivot({ source }) {
|
|||||||
const eventFilters = (detail.config || {}).filter || []
|
const eventFilters = (detail.config || {}).filter || []
|
||||||
const config = await viewer.save()
|
const config = await viewer.save()
|
||||||
setClickDetail({ row, config, column_names, eventFilters })
|
setClickDetail({ row, config, column_names, eventFilters })
|
||||||
const matched = filterRowsByConfig(allRowsRef.current, eventFilters)
|
|
||||||
setInspectedRows(matched)
|
// Use a Perspective view with the event filters + expressions so computed
|
||||||
|
// columns (split_by) are evaluated and filtered correctly
|
||||||
|
try {
|
||||||
|
const view = await tableRef.current.view({
|
||||||
|
filter: eventFilters,
|
||||||
|
expressions: config.expressions || [],
|
||||||
|
})
|
||||||
|
const data = await view.to_json()
|
||||||
|
await view.delete()
|
||||||
|
// Strip expression columns — only show raw source columns
|
||||||
|
const exprNames = new Set(Object.keys(config.expressions || {}))
|
||||||
|
const cleaned = data.map(r =>
|
||||||
|
Object.fromEntries(Object.entries(r).filter(([k]) => !exprNames.has(k)))
|
||||||
|
)
|
||||||
|
setInspectedRows(cleaned)
|
||||||
|
} catch {
|
||||||
|
setInspectedRows(filterRowsByConfig(allRowsRef.current, eventFilters))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await viewer.load(worker)
|
await viewer.load(worker)
|
||||||
|
|
||||||
|
const plugin = await viewer.getPlugin()
|
||||||
const savedLayout = localStorage.getItem(LAYOUT_KEY(source))
|
const savedLayout = localStorage.getItem(LAYOUT_KEY(source))
|
||||||
if (savedLayout) {
|
if (savedLayout) {
|
||||||
await viewer.restore(JSON.parse(savedLayout))
|
const parsed = JSON.parse(savedLayout)
|
||||||
|
await viewer.restore(parsed)
|
||||||
|
await plugin.restore(parsed.plugin_config || DEFAULT_PLUGIN_CONFIG)
|
||||||
|
if (parsed.expand_depth != null) await applyExpandDepth(viewer, parsed.expand_depth)
|
||||||
} else {
|
} else {
|
||||||
await viewer.restore({ table: source, settings: true, plugin_config: DEFAULT_PLUGIN_CONFIG })
|
await viewer.restore({ table: source, settings: true, plugin_config: DEFAULT_PLUGIN_CONFIG })
|
||||||
|
await plugin.restore(DEFAULT_PLUGIN_CONFIG)
|
||||||
}
|
}
|
||||||
|
await viewer.flush()
|
||||||
|
|
||||||
setStatus('ready')
|
setStatus('ready')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!cancelled) { setStatus('error'); setError(err.message) }
|
if (!cancelled) { setStatus('error'); setError(err.message) }
|
||||||
@ -169,21 +197,58 @@ export default function Pivot({ source }) {
|
|||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [source])
|
}, [source])
|
||||||
|
|
||||||
|
async function applyExpandDepth(viewer, depth) {
|
||||||
|
if (depth == null) return
|
||||||
|
const view = await viewer.getView()
|
||||||
|
await view.set_depth(depth)
|
||||||
|
const plugin = await viewer.getPlugin()
|
||||||
|
await plugin.draw(view)
|
||||||
|
expandDepthRef.current = depth
|
||||||
|
}
|
||||||
|
|
||||||
async function applyLayout(layout) {
|
async function applyLayout(layout) {
|
||||||
const viewer = viewerRef.current
|
const viewer = viewerRef.current
|
||||||
if (!viewer) return
|
if (!viewer) return
|
||||||
await viewer.restore(layout.config)
|
await viewer.restore(layout.config)
|
||||||
|
if (layout.config.plugin_config) {
|
||||||
|
const plugin = await viewer.getPlugin()
|
||||||
|
await plugin.restore(layout.config.plugin_config)
|
||||||
|
}
|
||||||
|
await applyExpandDepth(viewer, layout.config.expand_depth ?? null)
|
||||||
setActiveLayoutId(layout.id)
|
setActiveLayoutId(layout.id)
|
||||||
// also persist to localStorage so it survives refresh
|
// also persist to localStorage so it survives refresh
|
||||||
localStorage.setItem(LAYOUT_KEY(source), JSON.stringify(layout.config))
|
localStorage.setItem(LAYOUT_KEY(source), JSON.stringify(layout.config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function captureConfig() {
|
||||||
|
const viewer = viewerRef.current
|
||||||
|
if (!viewer) return null
|
||||||
|
const plugin = await viewer.getPlugin()
|
||||||
|
const [viewerConfig, pluginConfig] = await Promise.all([viewer.save(), plugin.save()])
|
||||||
|
return { ...viewerConfig, plugin_config: pluginConfig, expand_depth: expandDepthRef.current }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveOver() {
|
||||||
|
const layout = layouts.find(l => l.id === activeLayoutId)
|
||||||
|
if (!layout) return
|
||||||
|
const config = await captureConfig()
|
||||||
|
if (!config) return
|
||||||
|
try {
|
||||||
|
const saved = await api.savePivotLayout(source, layout.layout_name, config)
|
||||||
|
localStorage.setItem(LAYOUT_KEY(source), JSON.stringify(config))
|
||||||
|
await loadLayouts()
|
||||||
|
setActiveLayoutId(saved.id)
|
||||||
|
flashMsg('Saved!')
|
||||||
|
} catch (err) {
|
||||||
|
flashMsg(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSaveAs() {
|
async function handleSaveAs() {
|
||||||
const name = saveAsName.trim()
|
const name = saveAsName.trim()
|
||||||
if (!name) return
|
if (!name) return
|
||||||
const viewer = viewerRef.current
|
const config = await captureConfig()
|
||||||
if (!viewer) return
|
if (!config) return
|
||||||
const config = await viewer.save()
|
|
||||||
try {
|
try {
|
||||||
const saved = await api.savePivotLayout(source, name, config)
|
const saved = await api.savePivotLayout(source, name, config)
|
||||||
localStorage.setItem(LAYOUT_KEY(source), JSON.stringify(config))
|
localStorage.setItem(LAYOUT_KEY(source), JSON.stringify(config))
|
||||||
@ -257,6 +322,13 @@ export default function Pivot({ source }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{activeLayoutId !== null && !showSaveAs && (
|
||||||
|
<button onClick={handleSaveOver}
|
||||||
|
className="text-xs text-blue-500 hover:text-blue-700 border border-blue-200 rounded px-2 py-0.5">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{showSaveAs ? (
|
{showSaveAs ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
@ -296,7 +368,8 @@ export default function Pivot({ source }) {
|
|||||||
await view.set_depth(d)
|
await view.set_depth(d)
|
||||||
const p = await v.getPlugin()
|
const p = await v.getPlugin()
|
||||||
await p.draw(view)
|
await p.draw(view)
|
||||||
}} className="text-xs border border-gray-200 rounded px-1.5 py-0.5 text-gray-500 hover:border-gray-400 hover:border-gray-400">
|
expandDepthRef.current = d
|
||||||
|
}} className="text-xs border border-gray-200 rounded px-1.5 py-0.5 text-gray-500 hover:border-gray-400">
|
||||||
{d}
|
{d}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user