From 3c96bfa73e406e85b6b63fb9438be1891dc2aed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Sun, 28 Aug 2022 19:09:48 +0200 Subject: [PATCH] Modals and forms improvements --- client/src/Root.tsx | 5 +- client/src/assets/components/_modal.scss | 49 +++++++ client/src/assets/style.scss | 51 +------ client/src/components/ConfirmModal.tsx | 35 +++++ client/src/components/Modal.tsx | 10 +- ...alContext.tsx => ConfirmModalsContext.tsx} | 47 +++--- .../components/BoxSettings/BoxSettings.tsx | 134 +++++++++--------- .../dashboard/components/SensorSettings.tsx | 111 --------------- .../sensors/components/SensorFormModal.tsx | 54 +++---- client/src/utils/hooks/useForm.ts | 44 ++++++ .../1661668953214_migrate_sensors.sql | 6 +- 11 files changed, 261 insertions(+), 285 deletions(-) create mode 100644 client/src/assets/components/_modal.scss create mode 100644 client/src/components/ConfirmModal.tsx rename client/src/contexts/{ConfirmModalContext.tsx => ConfirmModalsContext.tsx} (51%) delete mode 100644 client/src/pages/dashboard/components/SensorSettings.tsx create mode 100644 client/src/utils/hooks/useForm.ts diff --git a/client/src/Root.tsx b/client/src/Root.tsx index 02f20d6..2756746 100644 --- a/client/src/Root.tsx +++ b/client/src/Root.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from 'react-query' import { AppContextProvider } from './contexts/AppContext' +import { ConfirmModalsContextProvider } from './contexts/ConfirmModalsContext' import { Router } from './pages/Router' const queryClient = new QueryClient({ @@ -14,7 +15,9 @@ export const Root = () => { return ( - + + + ) diff --git a/client/src/assets/components/_modal.scss b/client/src/assets/components/_modal.scss new file mode 100644 index 0000000..2824126 --- /dev/null +++ b/client/src/assets/components/_modal.scss @@ -0,0 +1,49 @@ +.modal { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: auto; + display: none; + justify-content: center; + align-items: flex-start; + background-color: var(--modal-overlay-bg-color); + z-index: 5; + + &.show { + display: flex; + } + + &.confirm { + background-color: var(--confirm-overlay-bg-color); + } + + .inner { + background-color: var(--box-bg-color); + color: var(--box-fg-color); + margin-top: 5vh; + box-shadow: var(--box-shadow); + border-radius: 0.5rem; + width: 20rem; + max-height: 90%; + overflow: auto; + max-width: 100%; + + .body { + padding: 0.75rem 1rem; + } + } + + .actions { + display: flex; + align-items: center; + justify-content: flex-end; + + .remove { + margin-right: auto; + background-color: var(--button-remove-bg-color); + color: var(--button-remove-fg-color); + } + } +} diff --git a/client/src/assets/style.scss b/client/src/assets/style.scss index 2e33aa6..e9955d3 100644 --- a/client/src/assets/style.scss +++ b/client/src/assets/style.scss @@ -20,7 +20,8 @@ --box-action-fg-color: #666; --box-preview-bg-color: #3988ff; --box-shadow: 0px 10px 15px -3px rgba(0, 0, 0, 0.1); - --modal-overlay-bg-color: rgba(0, 0, 0, 0.2); + --modal-overlay-bg-color: rgba(0, 0, 0, 0.5); + --confirm-overlay-bg-color: rgba(0, 0, 0, 0.8); --graph-axis-fg-color: #777; --graph-grid-color: rgb(238, 238, 238); --link-fg-color: #3988ff; @@ -76,7 +77,7 @@ select { font-family: var(--main-font); } -@import 'components/button'; +@import "components/button"; input, select { @@ -216,51 +217,7 @@ section.content { border-radius: 0.5rem; } -.settings-modal { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - overflow: auto; - display: none; - justify-content: center; - align-items: flex-start; - background-color: var(--modal-overlay-bg-color); - z-index: 5; -} - -.settings-modal.show { - display: flex; -} - -.settings-modal .inner { - background-color: var(--box-bg-color); - color: var(--box-fg-color); - margin-top: 5vh; - box-shadow: var(--box-shadow); - border-radius: 0.5rem; - width: 20rem; - max-height: 90%; - overflow: auto; - max-width: 100%; -} - -.settings-modal .inner .body { - padding: 0.75rem 1rem; -} - -.settings-modal .actions { - display: flex; - align-items: center; - justify-content: flex-end; -} - -.settings-modal .actions .remove { - margin-right: auto; - background-color: var(--button-remove-bg-color); - color: var(--button-remove-fg-color); -} +@import "components/modal"; form { display: flex; diff --git a/client/src/components/ConfirmModal.tsx b/client/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..eee24f9 --- /dev/null +++ b/client/src/components/ConfirmModal.tsx @@ -0,0 +1,35 @@ +import { ComponentChildren } from 'preact' +import { Modal } from './Modal' + +type Props = { + open: boolean + onConfirm: () => void + onCancel: () => void + confirmText?: string + cancelText?: string + children: ComponentChildren +} + +export const ConfirmModal = ({ + children, + confirmText = 'Yes', + cancelText = 'No', + onCancel, + onConfirm, + open, +}: Props) => { + return ( + + {children} + +
+ + +
+
+ ) +} diff --git a/client/src/components/Modal.tsx b/client/src/components/Modal.tsx index b093420..7de9805 100644 --- a/client/src/components/Modal.tsx +++ b/client/src/components/Modal.tsx @@ -5,20 +5,26 @@ type Props = { open: boolean onClose: () => void children: ComponentChild + width?: string + className?: string } -export const Modal = ({ open, onClose, children }: Props) => { +export const Modal = ({ open, onClose, children, width, className }: Props) => { const preventPropagation = (e: Event) => { e.stopPropagation() } return ( -
+
{children}
diff --git a/client/src/contexts/ConfirmModalContext.tsx b/client/src/contexts/ConfirmModalsContext.tsx similarity index 51% rename from client/src/contexts/ConfirmModalContext.tsx rename to client/src/contexts/ConfirmModalsContext.tsx index b18dc1c..c3dbc70 100644 --- a/client/src/contexts/ConfirmModalContext.tsx +++ b/client/src/contexts/ConfirmModalsContext.tsx @@ -1,4 +1,4 @@ -import { Modal } from '@/components/Modal' +import { ConfirmModal } from '@/components/ConfirmModal' import { ComponentChild, createContext } from 'preact' import { useCallback, useContext, useMemo, useState } from 'preact/hooks' @@ -6,31 +6,33 @@ type Props = { children: ComponentChild } -type ShowModalProps = { +type CreateModalProps = { content: ComponentChild onConfirm: () => void - onCancel: () => void + onCancel?: () => void } -type ShowModelResult = { +type CreateModalResult = { show: () => void } -type ConfirmModalContext = { - createModal: (props: ShowModalProps) => ShowModelResult +type ConfirmModalsContextType = { + createModal: (props: CreateModalProps) => CreateModalResult } type ModalState = { id: string - props: ShowModalProps + props: CreateModalProps } -const ConfirmModalContext = createContext(null) +const ConfirmModalsContext = createContext( + null +) -export const ConfirmModalContextProvider = ({ children }: Props) => { +export const ConfirmModalsContextProvider = ({ children }: Props) => { const [modals, setModals] = useState([] as ModalState[]) - const createModal = useCallback((props: ShowModalProps) => { + const createModal = useCallback((props: CreateModalProps) => { return { show: () => setModals((p) => [ @@ -43,7 +45,7 @@ export const ConfirmModalContextProvider = ({ children }: Props) => { const handleClose = (modal: ModalState) => { setModals((p) => p.filter((m) => m.id !== modal.id)) - modal.props.onCancel() + modal.props.onCancel?.() } const handleConfirm = (modal: ModalState) => { @@ -55,21 +57,24 @@ export const ConfirmModalContextProvider = ({ children }: Props) => { const value = useMemo(() => ({ createModal }), [createModal]) return ( - + {children} {modals.map((m) => ( - handleClose(m)}> + handleClose(m)} + onConfirm={() => handleConfirm(m)} + > {m.props.content} - - - + ))} - + ) } -export const useConfirmModalContext = () => { - const ctx = useContext(ConfirmModalContext) +export const useConfirmModalsContext = () => { + const ctx = useContext(ConfirmModalsContext) if (!ctx) { throw new Error('useConfirmModalContext used outside context') @@ -78,8 +83,8 @@ export const useConfirmModalContext = () => { return ctx } -export const useConfirmModal = (modal: ShowModalProps) => { - const ctx = useConfirmModalContext() +export const useConfirmModal = (modal: CreateModalProps) => { + const ctx = useConfirmModalsContext() return ctx.createModal(modal) } diff --git a/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx b/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx index b572014..e1ff990 100644 --- a/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx +++ b/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx @@ -1,4 +1,6 @@ import { getSensors } from '@/api/sensors' +import { Modal } from '@/components/Modal' +import { useConfirmModal } from '@/contexts/ConfirmModalsContext' import { DashboardDialData, DashboardGraphData } from '@/utils/parseDashboard' import { useState } from 'preact/hooks' import { useQuery } from 'react-query' @@ -24,6 +26,13 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => { const [data, setData] = useState(() => value.data) + const deleteConfirm = useConfirmModal({ + content: 'Are you sure you want to delete the box?', + onConfirm: () => { + onRemove() + }, + }) + const handleSave = async (e: Event) => { e.preventDefault() e.stopPropagation() @@ -47,86 +56,73 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => { }) } - const preventPropagation = (e: Event) => { - e.stopPropagation() - } - return ( -
-
-
-
+ + +
+ + +
+ + {formState.sensor && ( + <>
- + + +
+
+
- {formState.sensor && ( - <> -
- - -
-
- - -
- - {formState.type === 'graph' && ( - - )} - - {formState.type === 'dial' && ( - - )} - + {formState.type === 'graph' && ( + )} -
- + {formState.type === 'dial' && ( + + )} + + )} - - -
- +
+ + + +
-
-
+ + ) } diff --git a/client/src/pages/dashboard/components/SensorSettings.tsx b/client/src/pages/dashboard/components/SensorSettings.tsx deleted file mode 100644 index ffcddb9..0000000 --- a/client/src/pages/dashboard/components/SensorSettings.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { setSensorConfig } from '@/api/sensor' -import { SensorInfo } from '@/api/sensors' -import { useState } from 'preact/hooks' - -type Props = { - sensor: SensorInfo - onClose: () => void - onUpdate: () => void -} - -export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => { - const [value, setValue] = useState(() => ({ ...sensor.config })) - const [saving, setSaving] = useState(false) - - const handleSave = async (e: Event) => { - e.preventDefault() - e.stopPropagation() - - if (saving) { - return - } - - setSaving(true) - - try { - await Promise.all( - Object.entries(value).map(([name, value]) => - setSensorConfig({ sensor: sensor.id, name, value }) - ) - ) - } catch (err) { - // TODO: Better error handling - alert(err) - } - - setSaving(false) - - onClose() - onUpdate() - } - - const handleChange = (e: Event) => { - const target = e.target as HTMLSelectElement | HTMLInputElement - - setValue({ - ...value, - [target.name]: target.value, - }) - } - - const preventPropagation = (e: Event) => { - e.stopPropagation() - } - - return ( -
-
-
-
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
- -
- - -
-
-
-
-
- ) -} diff --git a/client/src/pages/sensors/components/SensorFormModal.tsx b/client/src/pages/sensors/components/SensorFormModal.tsx index 0974d02..5bbe34c 100644 --- a/client/src/pages/sensors/components/SensorFormModal.tsx +++ b/client/src/pages/sensors/components/SensorFormModal.tsx @@ -1,6 +1,6 @@ import { createSensor, SensorInfo, updateSensor } from '@/api/sensors' import { Modal } from '@/components/Modal' -import { useState } from 'preact/hooks' +import { useForm } from '@/utils/hooks/useForm' import { useMutation, useQueryClient } from 'react-query' type Props = { @@ -14,43 +14,35 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => { const createMutation = useMutation(createSensor) const updateMutation = useMutation(updateSensor) - const [formState, setFormState] = useState(() => ({ - name: sensor?.name ?? '', - })) + const { value, handleSubmit, handleChange } = useForm({ + defaultValue: () => ({ + name: sensor?.name ?? '', + }), + onSubmit: async (v) => { + if (isLoading) { + return + } - const handleSave = async (e: Event) => { - e.preventDefault() - e.stopPropagation() + if (sensor) { + await updateMutation.mutateAsync({ + id: sensor.id, + name: v.name, + }) + } else { + await createMutation.mutateAsync(v.name) + } - if (isLoading) { - return - } + queryClient.invalidateQueries(['/sensors']) - if (sensor) { - await updateMutation.mutateAsync({ id: sensor.id, name: formState.name }) - } else { - await createMutation.mutateAsync(formState.name) - } - - queryClient.invalidateQueries(['/sensors']) - - onClose() - } - - const handleChange = (e: Event) => { - const target = e.target as HTMLSelectElement | HTMLInputElement - - setFormState({ - ...formState, - [target.name]: target.value, - }) - } + onClose() + }, + }) const isLoading = createMutation.isLoading || updateMutation.isLoading return ( -
+
{ required onChange={handleChange} autoFocus - value={formState.name} + value={value.name} />
diff --git a/client/src/utils/hooks/useForm.ts b/client/src/utils/hooks/useForm.ts new file mode 100644 index 0000000..4e1c544 --- /dev/null +++ b/client/src/utils/hooks/useForm.ts @@ -0,0 +1,44 @@ +import { useCallback, useRef, useState } from 'preact/hooks' + +type Props = { + onSubmit: (value: TValue) => void + defaultValue: TValue | (() => TValue) +} + +export const useForm = ({ + defaultValue, + onSubmit, +}: Props) => { + const [formState, setFormState] = useState(defaultValue) + + const submitRef = useRef(onSubmit) + const stateRef = useRef(formState) + + submitRef.current = onSubmit + stateRef.current = formState + + const handleChange = useCallback((e: Event) => { + const target = e.target as HTMLSelectElement | HTMLInputElement + + setFormState( + (previous) => + ({ + ...previous, + [target.name]: target.value, + } as TValue) + ) + }, []) + + const handleSubmit = useCallback((e: Event) => { + e.preventDefault() + e.stopPropagation() + + submitRef.current(stateRef.current) + }, []) + + return { + value: formState, + handleChange, + handleSubmit, + } +} diff --git a/server/database/migrations/1661668953214_migrate_sensors.sql b/server/database/migrations/1661668953214_migrate_sensors.sql index 7c52ff6..8e02328 100644 --- a/server/database/migrations/1661668953214_migrate_sensors.sql +++ b/server/database/migrations/1661668953214_migrate_sensors.sql @@ -1,13 +1,13 @@ /* Add rows for sensors */ INSERT INTO sensors (ident, name, auth_key) SELECT - sensor, + vals.sensor, IFNULL( - (SELECT c.value FROM sensor_config c WHERE c.sensor = sensor), + (SELECT c.value FROM sensor_config c WHERE c.sensor = vals.sensor AND c.key = 'name'), sensor ) as "name", hex(randomblob(32)) as "auth_key" - FROM sensor_values + FROM sensor_values vals GROUP BY sensor; /* We need to add FK key and the only way is to create new table */