From 0c8461898654d412c32badf346cc533a4da266b2 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 27 May 2023 02:10:36 +0100 Subject: [PATCH] Checklist (#2168) * Checklist * Reload attempt * Fetch data at page and pass merged widget data down props * Fix * Required type fix * Fix * Fix type * Fix editor types * Preload and fix editor --- prisma/schema.prisma | 16 ++ src/app/dashboards/[dashboardId]/page.tsx | 22 ++- .../widgets/[widgetId]/edit/page.tsx | 28 ++-- src/components/dashboard/editors/Widget.tsx | 54 +++++-- .../dashboard/editors/widgets/Base.tsx | 27 ++-- .../dashboard/editors/widgets/Frame.tsx | 12 +- .../editors/widgets/HomeAssistant.tsx | 34 ++-- .../dashboard/editors/widgets/Image.tsx | 8 +- .../dashboard/editors/widgets/Markdown.tsx | 8 +- src/components/dashboard/views/Dashboard.tsx | 8 +- src/components/dashboard/views/Section.tsx | 7 +- src/components/dashboard/views/Widget.tsx | 72 ++++----- .../dashboard/views/widgets/Checklist.tsx | 145 ++++++++++++++++++ .../dashboard/views/widgets/Frame.tsx | 13 +- .../dashboard/views/widgets/HomeAssistant.tsx | 75 +++++---- .../dashboard/views/widgets/Image.tsx | 8 +- .../dashboard/views/widgets/Markdown.tsx | 9 +- src/types/dashboard.type.ts | 6 +- src/types/section.type.ts | 6 +- src/types/widget.type.ts | 17 +- src/utils/serverActions/widget.ts | 87 +++++++++++ 21 files changed, 493 insertions(+), 169 deletions(-) create mode 100644 src/components/dashboard/views/widgets/Checklist.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4700a4ff2..d5a0e42d6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -70,11 +70,27 @@ model Widget { homeAssistant WidgetHomeAssistant[] image WidgetImage[] markdown WidgetMarkdown[] + checklist WidgetChecklist[] @@index([id, sectionId], name: "id_sectionId_unique") @@index([sectionId, position], name: "sectionId_position_unique") } +model WidgetChecklist { + widgetId String @id + widget Widget @relation(fields: [widgetId], references: [id], onDelete: Cascade) + items WidgetChecklistItem[] +} + +model WidgetChecklistItem { + id String @id @default(cuid()) + position Int @default(10) + content String + checked Boolean @default(false) + checklist WidgetChecklist @relation(fields: [checklistWidgetId], references: [widgetId]) + checklistWidgetId String +} + model WidgetFrame { widgetId String @id widget Widget @relation(fields: [widgetId], references: [id], onDelete: Cascade) diff --git a/src/app/dashboards/[dashboardId]/page.tsx b/src/app/dashboards/[dashboardId]/page.tsx index c7c4ce5f8..ea14b1279 100644 --- a/src/app/dashboards/[dashboardId]/page.tsx +++ b/src/app/dashboards/[dashboardId]/page.tsx @@ -2,8 +2,11 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import type { DashboardModel } from "@/types/dashboard.type"; +import type { SectionModel } from "@/types/section.type"; import { Dashboard } from "@/components/dashboard/views/Dashboard"; import { prisma } from "@/utils/prisma"; +import { Section } from "@/components/dashboard/views/Section"; +import { widgetGetData } from "@/utils/serverActions/widget"; export const metadata: Metadata = { title: "Dashboard | Home Panel", @@ -23,7 +26,7 @@ export default async function Page({ * The dashboard object retrieved from the database. * Contains all the sections and widgets associated with the dashboard. */ - const dashboard: DashboardModel | null = await prisma.dashboard.findUnique({ + let dashboard: DashboardModel | null = (await prisma.dashboard.findUnique({ where: { id: params.dashboardId, }, @@ -40,9 +43,22 @@ export default async function Page({ orderBy: { position: "asc" }, }, }, - }); + })) as DashboardModel | null; if (!dashboard) return notFound(); - return ; + // Fetch data for all widgets + for (const section of dashboard.sections) { + for (const widget of section.widgets) { + widget.data = await widgetGetData(widget.id, widget.type); + } + } + + return ( + + {dashboard.sections.map((section: SectionModel) => ( +
+ ))} + + ); } diff --git a/src/app/dashboards/[dashboardId]/sections/[sectionId]/widgets/[widgetId]/edit/page.tsx b/src/app/dashboards/[dashboardId]/sections/[sectionId]/widgets/[widgetId]/edit/page.tsx index 975ef0f45..908bf03ad 100644 --- a/src/app/dashboards/[dashboardId]/sections/[sectionId]/widgets/[widgetId]/edit/page.tsx +++ b/src/app/dashboards/[dashboardId]/sections/[sectionId]/widgets/[widgetId]/edit/page.tsx @@ -1,10 +1,12 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import type { WidgetWithSectionModel } from "@/types/widget.type"; +import type { SectionModel } from "@/types/section.type"; +import type { WidgetModel } from "@/types/widget.type"; import { EditWidget } from "@/components/dashboard/editors/Widget"; import { HomeAssistantProvider } from "@/providers/HomeAssistantProvider"; import { prisma } from "@/utils/prisma"; +import { widgetGetData } from "@/utils/serverActions/widget"; export const metadata: Metadata = { title: "Edit Widget | Home Panel", @@ -20,20 +22,28 @@ export default async function Page({ }): Promise { console.log("Edit Widget:", params); - let data: WidgetWithSectionModel | null = await prisma.widget.findUnique({ - where: { - id: params.widgetId, - }, + const section: SectionModel | null = (await prisma.section.findUnique({ include: { - section: true, + widgets: { + where: { + id: params.widgetId, + }, + }, }, - }); + where: { + id: params.sectionId, + }, + })) as SectionModel | null; + + if (!section) return notFound(); - if (!data) return notFound(); + for (const widget of section.widgets) { + widget.data = await widgetGetData(widget.id, widget.type); + } return ( - + ); } diff --git a/src/components/dashboard/editors/Widget.tsx b/src/components/dashboard/editors/Widget.tsx index e147f617e..1e7fed33a 100644 --- a/src/components/dashboard/editors/Widget.tsx +++ b/src/components/dashboard/editors/Widget.tsx @@ -8,7 +8,8 @@ import { Unstable_Grid2 as Grid2, } from "@mui/material"; -import type { WidgetWithSectionModel } from "@/types/widget.type"; +import type { SectionModel } from "@/types/section.type"; +import type { WidgetModel } from "@/types/widget.type"; import { EditWidgetBase } from "@/components/dashboard/editors/widgets/Base"; import { EditWidgetFrame } from "@/components/dashboard/editors/widgets/Frame"; import { EditWidgetHomeAssistant } from "./widgets/HomeAssistant"; @@ -20,59 +21,61 @@ import { WidgetType } from "@/types/widget.type"; export function EditWidget({ dashboardId, - data, + section, }: { dashboardId: string; - data: WidgetWithSectionModel; + section: SectionModel; }): JSX.Element { + const widget: WidgetModel = section.widgets[0]; + const { id, position, sectionId, title, type, width } = widget; const [widgetData, setWidgetData] = useState(null); useEffect(() => { (async () => { - const newData = await widgetGetData(data.id, data.type); + const newData = await widgetGetData(id, type); setWidgetData(newData); })(); - }, [data.id, data.type]); + }, [id, type]); const widgetView: JSX.Element = useMemo(() => { if (!widgetData) return ; - switch (data.type) { + switch (type) { case WidgetType.Frame: return ( ); case WidgetType.HomeAssistant: return ( ); case WidgetType.Image: return ( ); case WidgetType.Markdown: return ( ); default: return
Unknown widget type
; } - }, [dashboardId, data.type, data.sectionId, widgetData]); + }, [dashboardId, type, sectionId, widgetData]); return ( Edit Widget - + {widgetView} -
+ {widgetData && ( +
+ )} ); diff --git a/src/components/dashboard/editors/widgets/Base.tsx b/src/components/dashboard/editors/widgets/Base.tsx index 377db0cd7..b5b4c4a75 100644 --- a/src/components/dashboard/editors/widgets/Base.tsx +++ b/src/components/dashboard/editors/widgets/Base.tsx @@ -7,43 +7,34 @@ import { WidgetType } from "@/types/widget.type"; export function EditWidgetBase({ dashboardId, - data, + widget, }: { dashboardId: string; - data: Widget; + widget: Widget; }): JSX.Element { + const { id, title, type, width } = widget; return ( <> - await widgetUpdate( - dashboardId, - data.id, - e.target.name, - e.target.value - ) + await widgetUpdate(dashboardId, id, e.target.name, e.target.value) } /> - await widgetUpdate( - dashboardId, - data.id, - e.target.name, - e.target.value - ) + await widgetUpdate(dashboardId, id, e.target.name, e.target.value) } /> { // Split camelCase to words and capitalize first letter @@ -62,7 +53,7 @@ export function EditWidgetBase({ )} onChange={async (_, value: string | null) => { if (!value) return; - await widgetUpdate(dashboardId, data.id, "type", value); + await widgetUpdate(dashboardId, id, "type", value); }} /> diff --git a/src/components/dashboard/editors/widgets/Frame.tsx b/src/components/dashboard/editors/widgets/Frame.tsx index 57bcccc68..0085fe00c 100644 --- a/src/components/dashboard/editors/widgets/Frame.tsx +++ b/src/components/dashboard/editors/widgets/Frame.tsx @@ -7,11 +7,11 @@ import { widgetFrameUpdate } from "@/utils/serverActions/widget"; export function EditWidgetFrame({ dashboardId, sectionId, - data, + widgetData, }: { dashboardId: string; sectionId: string; - data: WidgetFrame; + widgetData: WidgetFrame; }): JSX.Element { return ( <> @@ -19,12 +19,12 @@ export function EditWidgetFrame({ name="url" label="URL" margin="dense" - defaultValue={data.url || ""} + defaultValue={widgetData.url || ""} onChange={async (e) => await widgetFrameUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) @@ -34,12 +34,12 @@ export function EditWidgetFrame({ name="height" label="Height" margin="dense" - defaultValue={data.height || ""} + defaultValue={widgetData.height || ""} onChange={async (e) => await widgetFrameUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) diff --git a/src/components/dashboard/editors/widgets/HomeAssistant.tsx b/src/components/dashboard/editors/widgets/HomeAssistant.tsx index d56a13a45..8d21d05ef 100644 --- a/src/components/dashboard/editors/widgets/HomeAssistant.tsx +++ b/src/components/dashboard/editors/widgets/HomeAssistant.tsx @@ -16,11 +16,11 @@ import { useHomeAssistant } from "@/providers/HomeAssistantProvider"; export function EditWidgetHomeAssistant({ dashboardId, sectionId, - data, + widgetData, }: { dashboardId: string; sectionId: string; - data: WidgetHomeAssistant; + widgetData: WidgetHomeAssistant; }): JSX.Element { const homeAssistant = useHomeAssistant(); @@ -33,8 +33,8 @@ export function EditWidgetHomeAssistant({ const defaultEntity = useMemo(() => { if (!homeAssistant.entities) return; - return homeAssistant.entities[data.entityId]; - }, [data.entityId, homeAssistant.entities]); + return homeAssistant.entities[widgetData.entityId]; + }, [widgetData.entityId, homeAssistant.entities]); const entityAttributes = useMemo | undefined>(() => { if (!defaultEntity) return; @@ -71,7 +71,7 @@ export function EditWidgetHomeAssistant({ await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, "entityId", value.entity_id ); @@ -81,12 +81,12 @@ export function EditWidgetHomeAssistant({ control={ await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.checked ) @@ -99,12 +99,12 @@ export function EditWidgetHomeAssistant({ control={ await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.checked ) @@ -117,12 +117,12 @@ export function EditWidgetHomeAssistant({ control={ await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.checked ) @@ -135,12 +135,12 @@ export function EditWidgetHomeAssistant({ name="iconColor" label="Icon Color" margin="dense" - defaultValue={data.iconColor || ""} + defaultValue={widgetData.iconColor || ""} onChange={async (e) => await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) @@ -150,19 +150,19 @@ export function EditWidgetHomeAssistant({ name="iconSize" label="Icon Size" margin="dense" - defaultValue={data.iconSize || ""} + defaultValue={widgetData.iconSize || ""} onChange={async (e) => await widgetHomeAssistantUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) } /> ( @@ -19,12 +19,12 @@ export function EditWidgetImage({ name="url" label="Image URL" margin="dense" - defaultValue={data.url} + defaultValue={widgetData.url} onChange={async (e) => await widgetImageUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) diff --git a/src/components/dashboard/editors/widgets/Markdown.tsx b/src/components/dashboard/editors/widgets/Markdown.tsx index a28de03bf..63f077372 100644 --- a/src/components/dashboard/editors/widgets/Markdown.tsx +++ b/src/components/dashboard/editors/widgets/Markdown.tsx @@ -7,11 +7,11 @@ import { widgetMarkdownUpdate } from "@/utils/serverActions/widget"; export function EditWidgetMarkdown({ dashboardId, sectionId, - data, + widgetData, }: { dashboardId: string; sectionId: string; - data: WidgetMarkdown; + widgetData: WidgetMarkdown; }): JSX.Element { return ( <> @@ -19,12 +19,12 @@ export function EditWidgetMarkdown({ name="content" label="Content" margin="dense" - defaultValue={data.content} + defaultValue={widgetData.content} onChange={async (e) => await widgetMarkdownUpdate( dashboardId, sectionId, - data.widgetId, + widgetData.widgetId, e.target.name, e.target.value ) diff --git a/src/components/dashboard/views/Dashboard.tsx b/src/components/dashboard/views/Dashboard.tsx index 6c642ffa3..f8b9384c8 100644 --- a/src/components/dashboard/views/Dashboard.tsx +++ b/src/components/dashboard/views/Dashboard.tsx @@ -2,14 +2,14 @@ import { Unstable_Grid2 as Grid2, Stack } from "@mui/material"; import type { DashboardModel } from "@/types/dashboard.type"; -import type { SectionModel } from "@/types/section.type"; import { Heading } from "@/components/dashboard/views/Heading"; import { HomeAssistantProvider } from "@/providers/HomeAssistantProvider"; -import { Section } from "@/components/dashboard/views/Section"; export function Dashboard({ + children, dashboard, }: { + children: React.ReactNode; dashboard: DashboardModel; }): JSX.Element { return ( @@ -39,9 +39,7 @@ export function Dashboard({ maxHeight: "100%", }} > - {dashboard.sections.map((section: SectionModel) => ( -
- ))} + {children} diff --git a/src/components/dashboard/views/Section.tsx b/src/components/dashboard/views/Section.tsx index 36eabceb7..a6f9eaa98 100644 --- a/src/components/dashboard/views/Section.tsx +++ b/src/components/dashboard/views/Section.tsx @@ -1,5 +1,4 @@ "use client"; -import type { Widget as WidgetModel } from "@prisma/client"; import { AddRounded, ArrowBackRounded, @@ -12,10 +11,12 @@ import { Typography, Unstable_Grid2 as Grid2, IconButton } from "@mui/material"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import { SectionAction, SectionModel } from "@/types/section.type"; +import type { SectionModel } from "@/types/section.type"; +import type { WidgetModel } from "@/types/widget.type"; +import { SectionAction } from "@/types/section.type"; +import { sectionDelete, sectionUpdate } from "@/utils/serverActions/section"; import { Widget } from "@/components/dashboard/views/Widget"; import Link from "next/link"; -import { sectionDelete, sectionUpdate } from "@/utils/serverActions/section"; export function Section({ data }: { data: SectionModel }): JSX.Element { const [editing, setEditing] = useState(false); diff --git a/src/components/dashboard/views/Widget.tsx b/src/components/dashboard/views/Widget.tsx index 12e58db8f..1c6e193be 100644 --- a/src/components/dashboard/views/Widget.tsx +++ b/src/components/dashboard/views/Widget.tsx @@ -1,16 +1,13 @@ "use client"; -import type { Widget as WidgetModel } from "@prisma/client"; import { Skeleton } from "@mui/material"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import { WidgetBase } from "@/components/dashboard/views/widgets/Base"; -import { - widgetDelete, - widgetGetData, - widgetUpdate, -} from "@/utils/serverActions/widget"; +import type { WidgetModel } from "@/types/widget.type"; import { WidgetAction, WidgetType } from "@/types/widget.type"; +import { WidgetBase } from "@/components/dashboard/views/widgets/Base"; +import { WidgetChecklist } from "@/components/dashboard/views/widgets/Checklist"; +import { widgetDelete, widgetUpdate } from "@/utils/serverActions/widget"; import { WidgetFrame } from "@/components/dashboard/views/widgets/Frame"; import { WidgetHomeAssistant } from "@/components/dashboard/views/widgets/HomeAssistant"; import { WidgetImage } from "@/components/dashboard/views/widgets/Image"; @@ -25,82 +22,81 @@ export function Widget({ data: WidgetModel; editing: boolean; }): JSX.Element { + const { id, position, sectionId, type } = data; const [expanded, setExpanded] = useState(false); - const [widgetData, setWidgetData] = useState(null); const router = useRouter(); - useEffect(() => { - (async () => { - const newData = await widgetGetData(data.id, data.type); - setWidgetData(newData); - })(); - }, [data.id, data.type]); - const handleInteraction = useCallback( async (action: WidgetAction): Promise => { console.log("Handle interaction:", action); switch (action) { case WidgetAction.Delete: - await widgetDelete(dashboardId, data.id); + await widgetDelete(dashboardId, id); break; case WidgetAction.Edit: router.push( - `/dashboards/${dashboardId}/sections/${data.sectionId}/widgets/${data.id}/edit` + `/dashboards/${dashboardId}/sections/${sectionId}/widgets/${id}/edit` ); break; case WidgetAction.MoveDown: - await widgetUpdate( - dashboardId, - data.id, - "position", - data.position + 15 - ); + await widgetUpdate(dashboardId, id, "position", position + 15); break; case WidgetAction.MoveUp: - await widgetUpdate( - dashboardId, - data.id, - "position", - data.position - 15 - ); + await widgetUpdate(dashboardId, id, "position", position - 15); break; case WidgetAction.ToggleExpanded: setExpanded(!expanded); break; } }, - [dashboardId, data.id, data.position, data.sectionId, expanded, router] + [dashboardId, id, position, sectionId, expanded, router] ); const widgetView: JSX.Element = useMemo(() => { - if (!widgetData) return ; - switch (data.type) { + if (!data) return ; + switch (type) { + case WidgetType.Checklist: + return ( + + ); case WidgetType.Frame: - return ; + return ; case WidgetType.HomeAssistant: return ( ); case WidgetType.Image: return ( ); case WidgetType.Markdown: - return ; + return ; default: return
Unknown widget type
; } - }, [data.type, editing, expanded, handleInteraction, widgetData]); + }, [ + dashboardId, + data, + editing, + expanded, + handleInteraction, + sectionId, + type, + ]); return ( ; +}): JSX.Element { + const { id } = widget; + const { items } = widget.data; + + return ( + + {items.map((item: WidgetChecklistItemModel) => ( + + ))} + + + + + ); +} + +function WidgetChecklistItem({ + dashboardId, + item, + sectionId, +}: { + dashboardId: string; + item: WidgetChecklistItemModel; + sectionId: string; +}): JSX.Element { + const { id, checklistWidgetId, content } = item; + const [checked, setChecked] = useState(item.checked); + return ( + + + { + setChecked(!checked); + await widgetChecklistUpdate( + dashboardId, + sectionId, + checklistWidgetId, + id, + "checked", + !checked + ); + }} + edge="start" + > + {checked ? ( + + ) : ( + + )} + + + ), + endAdornment: ( + + { + await widgetChecklistUpdate( + dashboardId, + sectionId, + checklistWidgetId, + id, + "id", + "DELETE" + ); + }} + edge="end" + > + + + + ), + }} + onChange={async (e) => { + await widgetChecklistUpdate( + dashboardId, + sectionId, + checklistWidgetId, + id, + "content", + e.target.value + ); + }} + /> + + ); +} diff --git a/src/components/dashboard/views/widgets/Frame.tsx b/src/components/dashboard/views/widgets/Frame.tsx index 3f3cf5df1..dfda1039d 100644 --- a/src/components/dashboard/views/widgets/Frame.tsx +++ b/src/components/dashboard/views/widgets/Frame.tsx @@ -1,12 +1,19 @@ "use client"; import type { WidgetFrame as WidgetFrameModel } from "@prisma/client"; -export function WidgetFrame({ data }: { data: WidgetFrameModel }): JSX.Element { +import type { WidgetModel } from "@/types/widget.type"; + +export function WidgetFrame({ + widget, +}: { + widget: WidgetModel; +}): JSX.Element { + const { height, url } = widget.data; return (