From b964801765bdf2d91d182fa705a01e5711dce161 Mon Sep 17 00:00:00 2001
From: Gabriel Kaszewski
Date: Fri, 19 Jun 2026 13:08:00 +0200
Subject: [PATCH] remove all modals, inline editing, live layout preview, clock
preview
all Dialog/AlertDialog removed from widgets, data-sources, presets,
layout-builder pages. replaced with inline card expansion for
edit/create and inline confirm bars for delete.
data source form: live clock format preview with 1s tick, timezone
validation against Intl.supportedValuesOf.
layout preview: fetches live widget data via useWidgetPreview, renders
formatted content based on display_hint kind instead of widget names.
---
spa/src/components/layout-preview.tsx | 117 ++++++--
spa/src/pages/data-sources.tsx | 415 ++++++++++++--------------
spa/src/pages/layout-builder.tsx | 151 ++++++----
spa/src/pages/presets.tsx | 173 ++++++-----
spa/src/pages/widgets.tsx | 332 ++++++++-------------
5 files changed, 577 insertions(+), 611 deletions(-)
diff --git a/spa/src/components/layout-preview.tsx b/spa/src/components/layout-preview.tsx
index c594b6c..9b49127 100644
--- a/spa/src/components/layout-preview.tsx
+++ b/spa/src/components/layout-preview.tsx
@@ -1,6 +1,7 @@
import { useMemo, useRef } from "react"
import type { LayoutNode, ThemeConfig, Widget } from "@/api/types"
import { computeLayout } from "@/lib/layout-engine"
+import { useWidgetPreview } from "@/api/widgets"
interface LayoutPreviewProps {
layout: LayoutNode
@@ -19,6 +20,91 @@ function collectWidgetIds(node: LayoutNode): number[] {
return (node.children ?? []).flatMap((c) => collectWidgetIds(c.node))
}
+function formatPreviewData(
+ kind: string,
+ data: Record | undefined,
+): string {
+ if (!data) return ""
+ const entries = Object.entries(data)
+ if (entries.length === 0) return ""
+
+ switch (kind) {
+ case "key_value":
+ return entries.map(([k, v]) => `${k}: ${v}`).join("\n")
+ case "text_block":
+ return entries.map(([, v]) => String(v ?? "")).join("\n")
+ case "icon_value":
+ return entries.map(([, v]) => String(v ?? "")).join(" ")
+ default:
+ return entries.map(([, v]) => String(v ?? "")).join("\n")
+ }
+}
+
+function WidgetCell({ wid, widget, scale, theme }: {
+ wid: number
+ widget: Widget | undefined
+ scale: number
+ theme: ThemeConfig
+}) {
+ const { data } = useWidgetPreview(wid, true)
+ const hAlign = widget?.display_hint?.h_align ?? "left"
+ const vAlign = widget?.display_hint?.v_align ?? "top"
+ const flexAlign = hAlign === "center" ? "center" : hAlign === "right" ? "flex-end" : "flex-start"
+ const flexJustify = vAlign === "middle" ? "center" : vAlign === "bottom" ? "flex-end" : "flex-start"
+ const textAlign = hAlign === "center" ? "center" as const : hAlign === "right" ? "right" as const : "left" as const
+
+ const kind = widget?.display_hint?.kind ?? "text_block"
+ const previewText = formatPreviewData(kind, data as Record | undefined)
+ const hasData = previewText.length > 0
+
+ return (
+
+ {hasData ? (
+
+ {previewText}
+
+ ) : (
+ <>
+
+ {widget?.name ?? `#${wid}`}
+
+ {widget && (
+
+ {kind}
+
+ )}
+ >
+ )}
+
+ )
+}
+
export function LayoutPreview({
layout,
screenWidth,
@@ -54,11 +140,6 @@ export function LayoutPreview({
const box = bounds.get(wid)
if (!box) return null
const w = widgets.find((w) => w.id === wid)
- const hAlign = w?.display_hint?.h_align ?? "left"
- const vAlign = w?.display_hint?.v_align ?? "top"
- const flexAlign = hAlign === "center" ? "center" : hAlign === "right" ? "flex-end" : "flex-start"
- const flexJustify = vAlign === "middle" ? "center" : vAlign === "bottom" ? "flex-end" : "flex-start"
- const textAlign = hAlign === "center" ? "center" as const : hAlign === "right" ? "right" as const : "left" as const
return (
-
- {w?.name ?? `#${wid}`}
-
- {w && (
-
- {w.display_hint.kind}
-
- )}
+
)
})}
diff --git a/spa/src/pages/data-sources.tsx b/spa/src/pages/data-sources.tsx
index 8df8b30..6c4705a 100644
--- a/spa/src/pages/data-sources.tsx
+++ b/spa/src/pages/data-sources.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react"
+import { useState, useEffect } from "react"
import {
useDataSources,
useCreateDataSource,
@@ -14,13 +14,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
@@ -30,28 +23,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
-import { Plus, Pencil, Trash2, X, Eye, EyeOff } from "lucide-react"
+import { Plus, Pencil, Trash2, X, Eye, EyeOff, ChevronUp } from "lucide-react"
import { toast } from "sonner"
const SOURCE_TYPES: SourceType[] = [
- "weather",
- "media",
- "rss",
- "http_json",
- "webhook",
- "clock",
- "static_text",
+ "weather", "media", "rss", "http_json", "webhook", "clock", "static_text",
]
const EXTERNAL_TYPES: SourceType[] = ["weather", "media", "rss", "http_json", "webhook"]
@@ -70,83 +47,158 @@ const EMPTY: DataSource = {
config: { type: "external", url: null, api_key: null, headers: [] },
}
+const VALID_TIMEZONES = new Set(Intl.supportedValuesOf("timeZone"))
+
+const STRFTIME_MAP: Record string> = {
+ "%H": (d) => String(d.getHours()).padStart(2, "0"),
+ "%M": (d) => String(d.getMinutes()).padStart(2, "0"),
+ "%S": (d) => String(d.getSeconds()).padStart(2, "0"),
+ "%I": (d) => String(d.getHours() % 12 || 12).padStart(2, "0"),
+ "%p": (d) => (d.getHours() >= 12 ? "PM" : "AM"),
+ "%Y": (d) => String(d.getFullYear()),
+ "%m": (d) => String(d.getMonth() + 1).padStart(2, "0"),
+ "%d": (d) => String(d.getDate()).padStart(2, "0"),
+}
+
+function formatClockPreview(fmt: string, tz: string): string {
+ try {
+ const parts = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz,
+ year: "numeric", month: "2-digit", day: "2-digit",
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
+ hour12: false,
+ }).formatToParts(new Date())
+
+ const get = (type: string) => parts.find((p) => p.type === type)?.value ?? ""
+ const h24 = Number(get("hour"))
+ const fakeDate = new Date(2000, 0, 1, h24, Number(get("minute")), Number(get("second")))
+
+ let result = fmt
+ for (const [token, fn] of Object.entries(STRFTIME_MAP)) {
+ if (token === "%Y") result = result.replaceAll(token, get("year"))
+ else if (token === "%m") result = result.replaceAll(token, get("month"))
+ else if (token === "%d") result = result.replaceAll(token, get("day"))
+ else result = result.replaceAll(token, fn(fakeDate))
+ }
+ return result
+ } catch {
+ return "invalid timezone"
+ }
+}
+
+function isValidSave(ds: DataSource): boolean {
+ if (!ds.name) return false
+ if (EXTERNAL_TYPES.includes(ds.source_type) && ds.source_type !== "webhook" && ds.poll_interval_secs <= 0) return false
+ if (EXTERNAL_TYPES.includes(ds.source_type) && ds.source_type !== "webhook" && ds.config.type === "external" && !ds.config.url) return false
+ return true
+}
+
export function DataSourcesPage() {
const { data: sources = [], isLoading } = useDataSources()
const create = useCreateDataSource()
const update = useUpdateDataSource()
const del = useDeleteDataSource()
- const [editing, setEditing] = useState(null)
- const [deleting, setDeleting] = useState(null)
+ const [editingId, setEditingId] = useState(null)
+ const [editingData, setEditingData] = useState(null)
+ const [newSource, setNewSource] = useState(null)
+ const [confirmingDelete, setConfirmingDelete] = useState(null)
function openNew() {
- const nextId =
- sources.length > 0 ? Math.max(...sources.map((s) => s.id)) + 1 : 1
- setEditing({ ...EMPTY, id: nextId })
+ const nextId = sources.length > 0 ? Math.max(...sources.map((s) => s.id)) + 1 : 1
+ setNewSource({ ...EMPTY, id: nextId })
+ setEditingId(null)
}
- function openEdit(ds: DataSource) {
- setEditing({ ...ds })
+ function startEdit(ds: DataSource) {
+ setEditingId(ds.id)
+ setEditingData({ ...ds })
+ setNewSource(null)
}
- async function save() {
- if (!editing) return
- const isNew = !sources.some((s) => s.id === editing.id)
+ function cancelEdit() {
+ setEditingId(null)
+ setEditingData(null)
+ setNewSource(null)
+ }
+
+ async function saveExisting() {
+ if (!editingData) return
try {
- if (isNew) {
- await create.mutateAsync(editing)
- toast.success("Data source created")
- } else {
- await update.mutateAsync(editing)
- toast.success("Data source updated")
- }
- setEditing(null)
+ await update.mutateAsync(editingData)
+ toast.success("Data source updated")
+ setEditingId(null)
+ setEditingData(null)
} catch (e) {
toast.error(String(e))
}
}
- async function confirmDelete() {
- if (deleting == null) return
+ async function saveNew() {
+ if (!newSource) return
try {
- await del.mutateAsync(deleting)
+ await create.mutateAsync(newSource)
+ toast.success("Data source created")
+ setNewSource(null)
+ } catch (e) {
+ toast.error(String(e))
+ }
+ }
+
+ async function confirmDelete(id: number) {
+ try {
+ await del.mutateAsync(id)
toast.success("Data source deleted")
} catch (e) {
toast.error(String(e))
}
- setDeleting(null)
+ setConfirmingDelete(null)
}
- if (isLoading) return Loading…
+ if (isLoading) return Loading...
return (
-
- Data Sources
-
-
- Configure external data feeds
-
+
Data Sources
+
Configure data feeds
-
- {sources.length === 0 ? (
-
-
-
- No data sources configured yet.
-
-
-
- ) : (
-
- {sources.map((ds) => (
+
+ {newSource && (
+
+
+ New Data Source
+
+
+
+
+ Cancel
+ Save
+
+
+
+ )}
+
+ {sources.length === 0 && !newSource && (
+
+
+ No data sources configured yet.
+
+
+ )}
+
+ {sources.map((ds) => {
+ const isEditing = editingId === ds.id
+ const isDeleting = confirmingDelete === ds.id
+
+ return (
@@ -155,91 +207,43 @@ export function DataSourcesPage() {
{ds.source_type}
{ds.poll_interval_secs > 0 && every {ds.poll_interval_secs}s}
{ds.config.type === "external" && ds.config.url && (
-
- {ds.config.url}
-
+ {ds.config.url}
)}
-
openEdit(ds)}
- >
-
+ isEditing ? cancelEdit() : startEdit(ds)}>
+ {isEditing ? : }
- setDeleting(ds.id)}
- >
+ setConfirmingDelete(isDeleting ? null : ds.id)}>
+
+ {isDeleting && (
+
+
+ Delete? Widgets referencing it will lose their feed.
+ confirmDelete(ds.id)}>Delete
+ setConfirmingDelete(null)}>Cancel
+
+
+ )}
+
+ {isEditing && editingData && (
+
+
+
+ Cancel
+ Save
+
+
+ )}
- ))}
-
- )}
-
- {/* Edit / Create Dialog */}
-
-
- {/* Delete Confirmation */}
-
!o && setDeleting(null)}
- >
-
-
- Delete data source?
-
- This will permanently remove this data source. Widgets referencing
- it will lose their feed.
-
-
-
- Cancel
-
- Delete
-
-
-
-
+ )
+ })}
+
)
}
@@ -251,11 +255,7 @@ function isSensitiveKey(key: string) {
}
function HeaderRow({
- headerKey,
- headerValue,
- onChangeKey,
- onChangeValue,
- onRemove,
+ headerKey, headerValue, onChangeKey, onChangeValue, onRemove,
}: {
headerKey: string
headerValue: string
@@ -268,12 +268,7 @@ function HeaderRow({
return (
-
onChangeKey(e.target.value)}
- placeholder="key"
- className="flex-1"
- />
+
onChangeKey(e.target.value)} placeholder="key" className="flex-1" />
{sensitive && (
- setVisible((v) => !v)}
- >
- {visible ? (
-
- ) : (
-
- )}
+ setVisible((v) => !v)}>
+ {visible ? : }
)}
@@ -304,13 +290,28 @@ function HeaderRow({
)
}
-function DataSourceForm({
- value,
- onChange,
-}: {
- value: DataSource
- onChange: (ds: DataSource) => void
-}) {
+function ClockPreview({ format, timezone }: { format: string; timezone: string }) {
+ const [preview, setPreview] = useState("")
+
+ useEffect(() => {
+ setPreview(formatClockPreview(format, timezone))
+ const id = setInterval(() => setPreview(formatClockPreview(format, timezone)), 1000)
+ return () => clearInterval(id)
+ }, [format, timezone])
+
+ const validTz = VALID_TIMEZONES.has(timezone)
+
+ return (
+
+
{preview}
+ {timezone && !validTz && (
+
Unknown timezone
+ )}
+
+ )
+}
+
+function DataSourceForm({ value, onChange }: { value: DataSource; onChange: (ds: DataSource) => void }) {
const set =
(k: K, v: DataSource[K]) =>
onChange({ ...value, [k]: v })
@@ -326,29 +327,18 @@ function DataSourceForm({
const isStaticText = value.config.type === "static_text"
return (
-
+
- set("name", e.target.value)}
- placeholder="e.g. weather"
- />
+ set("name", e.target.value)} placeholder="e.g. weather" />
-
)}
-
-
- {isRoot ? "Clear Layout" : "Remove"}
-
+ {pendingDelete !== null &&
+ JSON.stringify(pendingDelete) === JSON.stringify(path) ? (
+
+
+ {isRoot
+ ? "Clear entire layout? You can rebuild afterward."
+ : "Remove this node and all its children?"}
+
+
+
+ {isRoot ? "Clear" : "Remove"}
+
+
+ Cancel
+
+
+
+ ) : (
+
+
+ {isRoot ? "Clear Layout" : "Remove"}
+
+ )}
)
}
@@ -637,14 +639,20 @@ function ContainerProps({
function LeafProps({
path,
widgetId,
- onRemove,
+ pendingDelete,
+ onRequestDelete,
+ onConfirmDelete,
+ onCancelDelete,
onUpdateSizing,
widgets,
sizing,
}: {
path: Path
widgetId: number
- onRemove: () => void
+ pendingDelete: Path | null
+ onRequestDelete: () => void
+ onConfirmDelete: () => void
+ onCancelDelete: () => void
onUpdateSizing: (sizing: LayoutChild["sizing"]) => void
widgets: { id: number; name: string }[]
sizing?: LayoutChild["sizing"]
@@ -657,10 +665,31 @@ function LeafProps({
{w?.name ?? `#${widgetId}`}
{sizing && }
-
-
- Remove
-
+ {pendingDelete !== null &&
+ JSON.stringify(pendingDelete) === JSON.stringify(path) ? (
+
+
+ Remove this widget?
+
+
+
+ Remove
+
+
+ Cancel
+
+
+
+ ) : (
+
+
+ Remove
+
+ )}
)
}
diff --git a/spa/src/pages/presets.tsx b/spa/src/pages/presets.tsx
index bb70883..b3d6d33 100644
--- a/spa/src/pages/presets.tsx
+++ b/spa/src/pages/presets.tsx
@@ -15,26 +15,8 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from "@/components/ui/dialog"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Save, Upload, Trash2 } from "lucide-react"
+import { Save, Upload, Trash2, ChevronUp } from "lucide-react"
import { toast } from "sonner"
export function PresetsPage() {
@@ -45,22 +27,22 @@ export function PresetsPage() {
const loadPreset = useLoadPreset()
const [saving, setSaving] = useState(false)
- const [saveName, setSaveName] = useState("")
- const [deleting, setDeleting] = useState(null)
+ const [presetName, setPresetName] = useState("")
+ const [confirmingDelete, setConfirmingDelete] = useState(null)
async function saveAsPreset() {
- if (!layout || !saveName) return
+ if (!layout || !presetName) return
const nextId =
presets.length > 0 ? Math.max(...presets.map((p) => p.id)) + 1 : 1
try {
await createPreset.mutateAsync({
id: nextId,
- name: saveName,
+ name: presetName,
layout,
})
toast.success("Preset saved")
setSaving(false)
- setSaveName("")
+ setPresetName("")
} catch (e) {
toast.error(String(e))
}
@@ -75,15 +57,14 @@ export function PresetsPage() {
}
}
- async function confirmDelete() {
- if (deleting == null) return
+ async function confirmDelete(id: number) {
try {
- await deletePreset.mutateAsync(deleting)
+ await deletePreset.mutateAsync(id)
toast.success("Preset deleted")
} catch (e) {
toast.error(String(e))
}
- setDeleting(null)
+ setConfirmingDelete(null)
}
function nodeCount(node: Preset["layout"]["root"]): number {
@@ -104,12 +85,50 @@ export function PresetsPage() {
Save and restore layout configurations
- setSaving(true)} disabled={!layout}>
-
- Save Current Layout
+ setSaving((v) => !v)}
+ disabled={!layout}
+ variant={saving ? "secondary" : "default"}
+ >
+ {saving ? (
+
+ ) : (
+
+ )}
+ {saving ? "Close" : "Save Current Layout"}
+ {saving && (
+
+
+ Save Current Layout as Preset
+
+
+ setPresetName(e.target.value)}
+ placeholder="e.g. dashboard"
+ onKeyDown={(e) => e.key === "Enter" && saveAsPreset()}
+ autoFocus
+ />
+
+ Save
+
+ {
+ setSaving(false)
+ setPresetName("")
+ }}
+ >
+ Cancel
+
+
+
+ )}
+
{presets.length === 0 ? (
@@ -128,69 +147,47 @@ export function PresetsPage() {
- load(p.id)}>
-
- Load
-
- setDeleting(p.id)}
- >
-
-
+ {confirmingDelete === p.id ? (
+ <>
+
+ Delete?
+
+ confirmDelete(p.id)}
+ >
+ Delete
+
+ setConfirmingDelete(null)}
+ >
+ Cancel
+
+ >
+ ) : (
+ <>
+ load(p.id)}>
+
+ Load
+
+ setConfirmingDelete(p.id)}
+ >
+
+
+ >
+ )}
))}
)}
-
-
-
- !o && setDeleting(null)}
- >
-
-
- Delete preset?
-
- This will permanently remove this preset.
-
-
-
- Cancel
-
- Delete
-
-
-
-
)
}
diff --git a/spa/src/pages/widgets.tsx b/spa/src/pages/widgets.tsx
index e9690b6..7ca7d14 100644
--- a/spa/src/pages/widgets.tsx
+++ b/spa/src/pages/widgets.tsx
@@ -16,13 +16,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
@@ -32,18 +25,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
-import { Plus, Pencil, Trash2, X, Eye } from "lucide-react"
+import { Plus, Pencil, Trash2, X, Eye, ChevronUp } from "lucide-react"
import { toast } from "sonner"
const DISPLAY_HINT_KINDS: DisplayHintKind[] = ["icon_value", "text_block", "key_value"]
@@ -64,52 +47,72 @@ export function WidgetsPage() {
const update = useUpdateWidget()
const del = useDeleteWidget()
- const [editing, setEditing] = useState(null)
- const [deleting, setDeleting] = useState(null)
- const [previewing, setPreviewing] = useState(null)
+ const [editingId, setEditingId] = useState(null)
+ const [editingData, setEditingData] = useState(null)
+ const [newWidget, setNewWidget] = useState(null)
+ const [confirmingDelete, setConfirmingDelete] = useState(null)
+ const [previewingId, setPreviewingId] = useState(null)
function openNew() {
const nextId =
widgets.length > 0 ? Math.max(...widgets.map((w) => w.id)) + 1 : 1
- setEditing({
- ...EMPTY,
- id: nextId,
- data_source_id: sources[0]?.id ?? 0,
- })
+ setNewWidget({ ...EMPTY, id: nextId, data_source_id: sources[0]?.id ?? 0 })
+ setEditingId(null)
}
- async function save() {
- if (!editing) return
- const isNew = !widgets.some((w) => w.id === editing.id)
+ function startEdit(w: Widget) {
+ setEditingId(w.id)
+ setEditingData({ ...w })
+ setNewWidget(null)
+ }
+
+ function cancelEdit() {
+ setEditingId(null)
+ setEditingData(null)
+ setNewWidget(null)
+ }
+
+ async function saveExisting() {
+ if (!editingData) return
try {
- if (isNew) {
- await create.mutateAsync(editing)
- toast.success("Widget created")
- } else {
- await update.mutateAsync(editing)
- toast.success("Widget updated")
- }
- setEditing(null)
+ await update.mutateAsync(editingData)
+ toast.success("Widget updated")
+ setEditingId(null)
+ setEditingData(null)
} catch (e) {
toast.error(String(e))
}
}
- async function confirmDelete() {
- if (deleting == null) return
+ async function saveNew() {
+ if (!newWidget) return
try {
- await del.mutateAsync(deleting)
+ await create.mutateAsync(newWidget)
+ toast.success("Widget created")
+ setNewWidget(null)
+ } catch (e) {
+ toast.error(String(e))
+ }
+ }
+
+ async function confirmDelete(id: number) {
+ try {
+ await del.mutateAsync(id)
toast.success("Widget deleted")
} catch (e) {
toast.error(String(e))
}
- setDeleting(null)
+ setConfirmingDelete(null)
+ }
+
+ function togglePreview(id: number) {
+ setPreviewingId(previewingId === id ? null : id)
}
const sourceName = (id: number) =>
sources.find((s) => s.id === id)?.name ?? `#${id}`
- if (isLoading) return Loading…
+ if (isLoading) return Loading...
return (
@@ -120,21 +123,42 @@ export function WidgetsPage() {
Display primitives bound to data sources
-
+
Add Widget
- {widgets.length === 0 ? (
-
-
- No widgets configured yet.
-
-
- ) : (
-
- {widgets.map((w) => (
+
+ {newWidget && (
+
+
+ New Widget
+
+
+
+
+ Cancel
+ Save
+
+
+
+ )}
+
+ {widgets.length === 0 && !newWidget && (
+
+
+ No widgets configured yet.
+
+
+ )}
+
+ {widgets.map((w) => {
+ const isEditing = editingId === w.id
+ const isDeleting = confirmingDelete === w.id
+ const isPreviewing = previewingId === w.id
+
+ return (
@@ -142,143 +166,65 @@ export function WidgetsPage() {
{w.display_hint.kind}
source: {sourceName(w.data_source_id)}
- {w.mappings.length} mapping(s)
+ {w.mappings.length > 0 && {w.mappings.length} mapping(s)}
-
setPreviewing(w.id)}
- title="Preview current data"
- >
-
+ togglePreview(w.id)} title="Preview data">
+ {isPreviewing ? : }
- setEditing({ ...w })}
- >
-
+ isEditing ? cancelEdit() : startEdit(w)}>
+ {isEditing ? : }
- setDeleting(w.id)}
- >
+ setConfirmingDelete(isDeleting ? null : w.id)}>
+
+ {isDeleting && (
+
+
+ Delete this widget? Layout references will become dangling.
+ confirmDelete(w.id)}>Delete
+ setConfirmingDelete(null)}>Cancel
+
+
+ )}
+
+ {isPreviewing && !isEditing && (
+
+
+
+ )}
+
+ {isEditing && editingData && (
+
+
+
+ Cancel
+ Save
+
+
+ )}
- ))}
-
- )}
-
-
-
-
!o && setDeleting(null)}
- >
-
-
- Delete widget?
-
- This will permanently remove this widget. Layout references will
- become dangling.
-
-
-
- Cancel
-
- Delete
-
-
-
-
-
- {previewing != null && (
-
w.id === previewing)?.name ?? ""}
- onClose={() => setPreviewing(null)}
- />
- )}
+ )
+ })}
+
)
}
-function WidgetPreviewDialog({
- widgetId,
- widgetName,
- onClose,
-}: {
- widgetId: number
- widgetName: string
- onClose: () => void
-}) {
+function WidgetPreviewInline({ widgetId }: { widgetId: number }) {
const { data, isLoading, isError } = useWidgetPreview(widgetId, true)
+ if (isLoading) return Loading...
+ if (isError) return No data yet
+
return (
-
+
+ {JSON.stringify(data, null, 2)}
+
)
}
@@ -312,7 +258,7 @@ function WidgetForm({
}
return (
-
+
set("display_hint", { ...value.display_hint, kind: v as DisplayHintKind })}
>
-
-
-
+
{DISPLAY_HINT_KINDS.map((h) => (
-
- {h}
-
+ {h}
))}
@@ -346,9 +288,7 @@ function WidgetForm({
value={value.display_hint.h_align}
onValueChange={(v) => set("display_hint", { ...value.display_hint, h_align: v as HAlign })}
>
-
-
-
+
Left
Center
@@ -362,9 +302,7 @@ function WidgetForm({
value={value.display_hint.v_align}
onValueChange={(v) => set("display_hint", { ...value.display_hint, v_align: v as VAlign })}
>
-
-
-
+
Top
Middle
@@ -379,14 +317,10 @@ function WidgetForm({
value={String(value.data_source_id)}
onValueChange={(v) => set("data_source_id", Number(v))}
>
-
-
-
+
{sources.map((s) => (
-
- {s.name}
-
+ {s.name}
))}
@@ -412,26 +346,18 @@ function WidgetForm({
- updateMapping(i, { ...m, source_path: e.target.value })
- }
+ onChange={(e) => updateMapping(i, { ...m, source_path: e.target.value })}
placeholder="$.path.to.value"
className="flex-1"
/>
- →
+ →
- updateMapping(i, { ...m, target_key: e.target.value })
- }
+ onChange={(e) => updateMapping(i, { ...m, target_key: e.target.value })}
placeholder="target_key"
className="flex-1"
/>
- removeMapping(i)}
- >
+ removeMapping(i)}>