Modals and forms improvements
This commit is contained in:
parent
ea682998a8
commit
3c96bfa73e
|
|
@ -1,5 +1,6 @@
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { AppContextProvider } from './contexts/AppContext'
|
import { AppContextProvider } from './contexts/AppContext'
|
||||||
|
import { ConfirmModalsContextProvider } from './contexts/ConfirmModalsContext'
|
||||||
import { Router } from './pages/Router'
|
import { Router } from './pages/Router'
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
|
|
@ -14,7 +15,9 @@ export const Root = () => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<Router />
|
<ConfirmModalsContextProvider>
|
||||||
|
<Router />
|
||||||
|
</ConfirmModalsContextProvider>
|
||||||
</AppContextProvider>
|
</AppContextProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
--box-action-fg-color: #666;
|
--box-action-fg-color: #666;
|
||||||
--box-preview-bg-color: #3988ff;
|
--box-preview-bg-color: #3988ff;
|
||||||
--box-shadow: 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
|
--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-axis-fg-color: #777;
|
||||||
--graph-grid-color: rgb(238, 238, 238);
|
--graph-grid-color: rgb(238, 238, 238);
|
||||||
--link-fg-color: #3988ff;
|
--link-fg-color: #3988ff;
|
||||||
|
|
@ -76,7 +77,7 @@ select {
|
||||||
font-family: var(--main-font);
|
font-family: var(--main-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import 'components/button';
|
@import "components/button";
|
||||||
|
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
|
|
@ -216,51 +217,7 @@ section.content {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-modal {
|
@import "components/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);
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<Modal open={open} onClose={onCancel} className="confirm">
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<button type="button" className="cancel" onClick={onCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm}>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -5,20 +5,26 @@ type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
children: ComponentChild
|
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) => {
|
const preventPropagation = (e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('settings-modal', open && 'show')} onMouseDown={onClose}>
|
<div
|
||||||
|
className={cn('modal', open && 'show', className)}
|
||||||
|
onMouseDown={onClose}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="inner"
|
className="inner"
|
||||||
onMouseDown={preventPropagation}
|
onMouseDown={preventPropagation}
|
||||||
onMouseUp={preventPropagation}
|
onMouseUp={preventPropagation}
|
||||||
onClick={preventPropagation}
|
onClick={preventPropagation}
|
||||||
|
style={{ width }}
|
||||||
>
|
>
|
||||||
<div className="body">{children}</div>
|
<div className="body">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Modal } from '@/components/Modal'
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
import { ComponentChild, createContext } from 'preact'
|
import { ComponentChild, createContext } from 'preact'
|
||||||
import { useCallback, useContext, useMemo, useState } from 'preact/hooks'
|
import { useCallback, useContext, useMemo, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
|
@ -6,31 +6,33 @@ type Props = {
|
||||||
children: ComponentChild
|
children: ComponentChild
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShowModalProps = {
|
type CreateModalProps = {
|
||||||
content: ComponentChild
|
content: ComponentChild
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
onCancel: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShowModelResult = {
|
type CreateModalResult = {
|
||||||
show: () => void
|
show: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfirmModalContext = {
|
type ConfirmModalsContextType = {
|
||||||
createModal: (props: ShowModalProps) => ShowModelResult
|
createModal: (props: CreateModalProps) => CreateModalResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalState = {
|
type ModalState = {
|
||||||
id: string
|
id: string
|
||||||
props: ShowModalProps
|
props: CreateModalProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmModalContext = createContext<ConfirmModalContext | null>(null)
|
const ConfirmModalsContext = createContext<ConfirmModalsContextType | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
export const ConfirmModalContextProvider = ({ children }: Props) => {
|
export const ConfirmModalsContextProvider = ({ children }: Props) => {
|
||||||
const [modals, setModals] = useState([] as ModalState[])
|
const [modals, setModals] = useState([] as ModalState[])
|
||||||
|
|
||||||
const createModal = useCallback((props: ShowModalProps) => {
|
const createModal = useCallback((props: CreateModalProps) => {
|
||||||
return {
|
return {
|
||||||
show: () =>
|
show: () =>
|
||||||
setModals((p) => [
|
setModals((p) => [
|
||||||
|
|
@ -43,7 +45,7 @@ export const ConfirmModalContextProvider = ({ children }: Props) => {
|
||||||
const handleClose = (modal: ModalState) => {
|
const handleClose = (modal: ModalState) => {
|
||||||
setModals((p) => p.filter((m) => m.id !== modal.id))
|
setModals((p) => p.filter((m) => m.id !== modal.id))
|
||||||
|
|
||||||
modal.props.onCancel()
|
modal.props.onCancel?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = (modal: ModalState) => {
|
const handleConfirm = (modal: ModalState) => {
|
||||||
|
|
@ -55,21 +57,24 @@ export const ConfirmModalContextProvider = ({ children }: Props) => {
|
||||||
const value = useMemo(() => ({ createModal }), [createModal])
|
const value = useMemo(() => ({ createModal }), [createModal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmModalContext.Provider value={value}>
|
<ConfirmModalsContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
{modals.map((m) => (
|
{modals.map((m) => (
|
||||||
<Modal open key={m.id} onClose={() => handleClose(m)}>
|
<ConfirmModal
|
||||||
|
key={m.id}
|
||||||
|
open
|
||||||
|
onCancel={() => handleClose(m)}
|
||||||
|
onConfirm={() => handleConfirm(m)}
|
||||||
|
>
|
||||||
{m.props.content}
|
{m.props.content}
|
||||||
|
</ConfirmModal>
|
||||||
<button onClick={() => handleConfirm(m)}>OK</button>
|
|
||||||
</Modal>
|
|
||||||
))}
|
))}
|
||||||
</ConfirmModalContext.Provider>
|
</ConfirmModalsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConfirmModalContext = () => {
|
export const useConfirmModalsContext = () => {
|
||||||
const ctx = useContext(ConfirmModalContext)
|
const ctx = useContext(ConfirmModalsContext)
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error('useConfirmModalContext used outside context')
|
throw new Error('useConfirmModalContext used outside context')
|
||||||
|
|
@ -78,8 +83,8 @@ export const useConfirmModalContext = () => {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConfirmModal = (modal: ShowModalProps) => {
|
export const useConfirmModal = (modal: CreateModalProps) => {
|
||||||
const ctx = useConfirmModalContext()
|
const ctx = useConfirmModalsContext()
|
||||||
|
|
||||||
return ctx.createModal(modal)
|
return ctx.createModal(modal)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { getSensors } from '@/api/sensors'
|
import { getSensors } from '@/api/sensors'
|
||||||
|
import { Modal } from '@/components/Modal'
|
||||||
|
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
||||||
import { DashboardDialData, DashboardGraphData } from '@/utils/parseDashboard'
|
import { DashboardDialData, DashboardGraphData } from '@/utils/parseDashboard'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
|
|
@ -24,6 +26,13 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
|
|
||||||
const [data, setData] = useState(() => value.data)
|
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) => {
|
const handleSave = async (e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
@ -47,86 +56,73 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const preventPropagation = (e: Event) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-modal show" onMouseDown={onClose}>
|
<Modal onClose={onClose} open>
|
||||||
<div
|
<form onSubmit={handleSave}>
|
||||||
className="inner"
|
<div className="input">
|
||||||
onMouseDown={preventPropagation}
|
<label>Sensor</label>
|
||||||
onMouseUp={preventPropagation}
|
<select
|
||||||
onClick={preventPropagation}
|
name="sensor"
|
||||||
>
|
value={formState.sensor || ''}
|
||||||
<div className="body">
|
onChange={handleChange}
|
||||||
<form onSubmit={handleSave}>
|
>
|
||||||
|
{sensors.data?.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formState.sensor && (
|
||||||
|
<>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Sensor</label>
|
<label>Title</label>
|
||||||
|
<input
|
||||||
|
name="title"
|
||||||
|
value={formState.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="input">
|
||||||
|
<label>Type</label>
|
||||||
<select
|
<select
|
||||||
name="sensor"
|
name="type"
|
||||||
value={formState.sensor || ''}
|
value={formState.type}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{sensors.data?.map((s) => (
|
<option value="graph">Graph</option>
|
||||||
<option key={s.id} value={s.id}>
|
<option value="dial">Dial</option>
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formState.sensor && (
|
{formState.type === 'graph' && (
|
||||||
<>
|
<GraphSettings
|
||||||
<div className="input">
|
value={data as DashboardGraphData}
|
||||||
<label>Title</label>
|
onChange={setData}
|
||||||
<input
|
/>
|
||||||
name="title"
|
|
||||||
value={formState.title}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<label>Type</label>
|
|
||||||
<select
|
|
||||||
name="type"
|
|
||||||
value={formState.type}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="graph">Graph</option>
|
|
||||||
<option value="dial">Dial</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formState.type === 'graph' && (
|
|
||||||
<GraphSettings
|
|
||||||
value={data as DashboardGraphData}
|
|
||||||
onChange={setData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{formState.type === 'dial' && (
|
|
||||||
<DialSettings
|
|
||||||
value={data as DashboardDialData}
|
|
||||||
onChange={setData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
{formState.type === 'dial' && (
|
||||||
<button className="remove" type="button" onClick={onRemove}>
|
<DialSettings
|
||||||
Remove
|
value={data as DashboardDialData}
|
||||||
</button>
|
onChange={setData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<button className="cancel" onClick={onClose} type="button">
|
<div className="actions">
|
||||||
Cancel
|
<button className="remove" type="button" onClick={deleteConfirm.show}>
|
||||||
</button>
|
Remove
|
||||||
<button>Save</button>
|
</button>
|
||||||
</div>
|
|
||||||
</form>
|
<button className="cancel" onClick={onClose} type="button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="settings-modal show" onMouseDown={onClose}>
|
|
||||||
<div
|
|
||||||
className="inner"
|
|
||||||
onMouseDown={preventPropagation}
|
|
||||||
onMouseUp={preventPropagation}
|
|
||||||
onClick={preventPropagation}
|
|
||||||
>
|
|
||||||
<div className="body">
|
|
||||||
<form onSubmit={handleSave}>
|
|
||||||
<div className="input">
|
|
||||||
<label>Sensor</label>
|
|
||||||
<input value={sensor.id} disabled />
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<label>Name</label>
|
|
||||||
<input name="name" value={value.name} onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<label>Type</label>
|
|
||||||
<select
|
|
||||||
name="graphType"
|
|
||||||
value={value.graphType || 'line'}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="line">Line</option>
|
|
||||||
<option value="points">Points</option>
|
|
||||||
<option value="lineAndPoints">Line + Points</option>
|
|
||||||
<option value="bar">Bar</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input">
|
|
||||||
<label>Unit</label>
|
|
||||||
<input name="unit" value={value.unit} onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<label>Min value</label>
|
|
||||||
<input name="min" value={value.min} onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<label>Max value</label>
|
|
||||||
<input name="max" value={value.max} onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="actions">
|
|
||||||
<button className="cancel" onClick={onClose} type="button">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button disabled={saving}>Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSensor, SensorInfo, updateSensor } from '@/api/sensors'
|
import { createSensor, SensorInfo, updateSensor } from '@/api/sensors'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { useState } from 'preact/hooks'
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -14,43 +14,35 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
|
||||||
const createMutation = useMutation(createSensor)
|
const createMutation = useMutation(createSensor)
|
||||||
const updateMutation = useMutation(updateSensor)
|
const updateMutation = useMutation(updateSensor)
|
||||||
|
|
||||||
const [formState, setFormState] = useState(() => ({
|
const { value, handleSubmit, handleChange } = useForm({
|
||||||
name: sensor?.name ?? '',
|
defaultValue: () => ({
|
||||||
}))
|
name: sensor?.name ?? '',
|
||||||
|
}),
|
||||||
|
onSubmit: async (v) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = async (e: Event) => {
|
if (sensor) {
|
||||||
e.preventDefault()
|
await updateMutation.mutateAsync({
|
||||||
e.stopPropagation()
|
id: sensor.id,
|
||||||
|
name: v.name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync(v.name)
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
queryClient.invalidateQueries(['/sensors'])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sensor) {
|
onClose()
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLoading = createMutation.isLoading || updateMutation.isLoading
|
const isLoading = createMutation.isLoading || updateMutation.isLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={onClose} open={open}>
|
<Modal onClose={onClose} open={open}>
|
||||||
<form onSubmit={handleSave}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -60,7 +52,7 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
|
||||||
required
|
required
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={formState.name}
|
value={value.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { useCallback, useRef, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
type Props<TValue> = {
|
||||||
|
onSubmit: (value: TValue) => void
|
||||||
|
defaultValue: TValue | (() => TValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useForm = <TValue = unknown>({
|
||||||
|
defaultValue,
|
||||||
|
onSubmit,
|
||||||
|
}: Props<TValue>) => {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/* Add rows for sensors */
|
/* Add rows for sensors */
|
||||||
INSERT INTO sensors (ident, name, auth_key)
|
INSERT INTO sensors (ident, name, auth_key)
|
||||||
SELECT
|
SELECT
|
||||||
sensor,
|
vals.sensor,
|
||||||
IFNULL(
|
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
|
sensor
|
||||||
) as "name",
|
) as "name",
|
||||||
hex(randomblob(32)) as "auth_key"
|
hex(randomblob(32)) as "auth_key"
|
||||||
FROM sensor_values
|
FROM sensor_values vals
|
||||||
GROUP BY sensor;
|
GROUP BY sensor;
|
||||||
|
|
||||||
/* We need to add FK key and the only way is to create new table */
|
/* We need to add FK key and the only way is to create new table */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue