Dashboard palette: switch dashboard, publish, settings; ⌘S publishes
This commit is contained in:
@@ -39,6 +39,7 @@ import { appPath } from "@/lib/portal"
|
||||
import { useShortcuts } from "@/lib/shortcuts"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { handleError } from "@/utils"
|
||||
import { DashboardPalette } from "./DashboardPalette"
|
||||
import {
|
||||
areaOf,
|
||||
CanvasSurface,
|
||||
@@ -66,7 +67,6 @@ import { PaletteProvider } from "./settings"
|
||||
import { useUi } from "./ui"
|
||||
import { showTitle } from "./ui/core/config"
|
||||
import { LookProvider, useCanvasRoot } from "./ui/core/look"
|
||||
import { WidgetPalette } from "./WidgetPalette"
|
||||
import {
|
||||
WIDGET_LABELS,
|
||||
WIDGET_SIZES,
|
||||
@@ -335,11 +335,25 @@ export function DashboardEditor({
|
||||
setSelected(id)
|
||||
}
|
||||
|
||||
// ⌘P, as on the flow canvas. Reachable while typing, so a settings field
|
||||
// having focus does not hide it.
|
||||
const openSettings = () => {
|
||||
setSelected(null)
|
||||
setSettingsOpen(true)
|
||||
}
|
||||
|
||||
// As on the flow canvas, and reachable while typing for the same reasons.
|
||||
// Outside edit mode both keys stay the browser's.
|
||||
useShortcuts(
|
||||
{ "mod+p": () => (edit ? setPaletteOpen((open) => !open) : false) },
|
||||
["mod+p"],
|
||||
{
|
||||
"mod+p": () => (edit ? setPaletteOpen((open) => !open) : false),
|
||||
// An edit still waiting for its autosave counts: publishing flushes it.
|
||||
"mod+s": () => {
|
||||
if (!edit) return false
|
||||
if (!publish.isPending && (hasDraft || timer.current)) {
|
||||
void publishDashboard()
|
||||
}
|
||||
},
|
||||
},
|
||||
["mod+p", "mod+s"],
|
||||
)
|
||||
|
||||
const patch = (id: string, changes: Partial<WidgetDef>) =>
|
||||
@@ -639,14 +653,9 @@ export function DashboardEditor({
|
||||
"text-muted-foreground",
|
||||
settingsOpen && "text-primary",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (settingsOpen) {
|
||||
setSettingsOpen(false)
|
||||
return
|
||||
}
|
||||
setSelected(null)
|
||||
setSettingsOpen(true)
|
||||
}}
|
||||
onClick={() =>
|
||||
settingsOpen ? setSettingsOpen(false) : openSettings()
|
||||
}
|
||||
aria-pressed={settingsOpen}
|
||||
aria-label="Dashboard settings"
|
||||
data-testid="edit-dashboard"
|
||||
@@ -718,10 +727,12 @@ export function DashboardEditor({
|
||||
|
||||
{edit ? (
|
||||
<>
|
||||
<WidgetPalette
|
||||
<DashboardPalette
|
||||
open={paletteOpen}
|
||||
onOpenChange={setPaletteOpen}
|
||||
onAdd={addWidget}
|
||||
onSettings={openSettings}
|
||||
onPublish={hasDraft ? () => void publishDashboard() : undefined}
|
||||
/>
|
||||
|
||||
<WidgetPanel
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import { dashboardsQueryOptions } from "./queries"
|
||||
import {
|
||||
INPUT_WIDGETS,
|
||||
WIDGET_DESCRIPTIONS,
|
||||
WIDGET_LABELS,
|
||||
type WidgetKind,
|
||||
} from "./widgets"
|
||||
|
||||
const KINDS = Object.keys(WIDGET_LABELS) as WidgetKind[]
|
||||
|
||||
const GROUPS = [
|
||||
["Show a value", KINDS.filter((kind) => !INPUT_WIDGETS.has(kind))],
|
||||
["Control something", KINDS.filter((kind) => INPUT_WIDGETS.has(kind))],
|
||||
] as const
|
||||
|
||||
/**
|
||||
* ⌘P on a dashboard being edited: add a widget, jump to another dashboard, or
|
||||
* publish — the flow canvas's palette, for this editor.
|
||||
*/
|
||||
export function DashboardPalette({
|
||||
open,
|
||||
onOpenChange,
|
||||
onAdd,
|
||||
onSettings,
|
||||
onPublish,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onAdd: (kind: WidgetKind) => void
|
||||
onSettings: () => void
|
||||
/** Absent while there is nothing to publish. */
|
||||
onPublish?: () => void
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const { data } = useQuery({ ...dashboardsQueryOptions(), enabled: open })
|
||||
|
||||
// Unmounted rather than faded, for the reason the flow palette gives.
|
||||
if (!open) return null
|
||||
|
||||
const dashboards = data?.data ?? []
|
||||
|
||||
const close = (action: () => void) => {
|
||||
onOpenChange(false)
|
||||
action()
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title="Commands"
|
||||
description="Add a widget, switch dashboard, or publish"
|
||||
>
|
||||
<CommandInput placeholder="Add a widget, switch dashboard, publish…" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nothing matches that.</CommandEmpty>
|
||||
|
||||
{GROUPS.map(([heading, kinds]) => (
|
||||
<CommandGroup key={heading} heading={heading}>
|
||||
{kinds.map((kind) => (
|
||||
<CommandItem
|
||||
key={kind}
|
||||
value={`${WIDGET_LABELS[kind]} ${WIDGET_DESCRIPTIONS[kind]}`}
|
||||
data-testid={`add-widget-${kind}`}
|
||||
onSelect={() => close(() => onAdd(kind))}
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span>{WIDGET_LABELS[kind]}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{WIDGET_DESCRIPTIONS[kind]}
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
|
||||
{dashboards.length > 0 ? (
|
||||
<CommandGroup heading="Dashboards">
|
||||
{dashboards.map((dashboard) => (
|
||||
<CommandItem
|
||||
key={dashboard.name}
|
||||
value={`dashboard ${dashboard.name} ${dashboard.title}`}
|
||||
onSelect={() =>
|
||||
close(() =>
|
||||
// Editing a neighbour is the same job, as on the rail.
|
||||
navigate({
|
||||
to: "/dashboards/$name",
|
||||
params: { name: dashboard.name },
|
||||
search: { edit: true },
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
{dashboard.title || dashboard.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : null}
|
||||
|
||||
<CommandGroup heading="Actions">
|
||||
{onPublish ? (
|
||||
<CommandItem
|
||||
value="publish dashboard"
|
||||
onSelect={() => close(onPublish)}
|
||||
>
|
||||
Publish this dashboard
|
||||
</CommandItem>
|
||||
) : null}
|
||||
<CommandItem
|
||||
value="dashboard settings"
|
||||
onSelect={() => close(onSettings)}
|
||||
>
|
||||
Dashboard settings
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
INPUT_WIDGETS,
|
||||
WIDGET_DESCRIPTIONS,
|
||||
WIDGET_LABELS,
|
||||
type WidgetKind,
|
||||
} from "./widgets"
|
||||
|
||||
const KINDS = Object.keys(WIDGET_LABELS) as WidgetKind[]
|
||||
|
||||
const GROUPS = [
|
||||
["Show a value", KINDS.filter((kind) => !INPUT_WIDGETS.has(kind))],
|
||||
["Control something", KINDS.filter((kind) => INPUT_WIDGETS.has(kind))],
|
||||
] as const
|
||||
|
||||
/** ⌘P on a dashboard being edited: the flow canvas's add-a-node palette, for widgets. */
|
||||
export function WidgetPalette({
|
||||
open,
|
||||
onOpenChange,
|
||||
onAdd,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onAdd: (kind: WidgetKind) => void
|
||||
}) {
|
||||
// Unmounted rather than faded, for the reason the flow palette gives.
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title="Add a widget"
|
||||
description="Search the widget types"
|
||||
>
|
||||
<CommandInput placeholder="Add a widget…" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nothing matches that.</CommandEmpty>
|
||||
{GROUPS.map(([heading, kinds]) => (
|
||||
<CommandGroup key={heading} heading={heading}>
|
||||
{kinds.map((kind) => (
|
||||
<CommandItem
|
||||
key={kind}
|
||||
value={`${WIDGET_LABELS[kind]} ${WIDGET_DESCRIPTIONS[kind]}`}
|
||||
data-testid={`add-widget-${kind}`}
|
||||
onSelect={() => {
|
||||
onOpenChange(false)
|
||||
onAdd(kind)
|
||||
}}
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span>{WIDGET_LABELS[kind]}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{WIDGET_DESCRIPTIONS[kind]}
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user