diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 308ae6ca..b98989c9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -28,6 +28,12 @@ Please describe the changes - [ ] Preset/Faders/Folders sort order is maintained - [ ] Presets can be triggered by HTTP requests +### Preset Triggers + +- [ ] Presets can be triggered by HTTP requests +- [ ] Time clock triggers recall presets successfully +- [ ] Time clock triggers do not recall when device locked + ### Preset Types #### sACN diff --git a/.vscode/launch.json b/.vscode/launch.json index b0d7899c..d2c0c956 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,10 @@ "DEV_MODE": "true" }, "console": "integratedTerminal", + "skipFiles": [ + "/**/*.js", + "${workspaceFolder}/node_modules/**/*.js", + ] } ] } diff --git a/docs/docs/user-guide/admin/scheduled-presets.md b/docs/docs/user-guide/admin/scheduled-presets.md new file mode 100644 index 00000000..38b2cb20 --- /dev/null +++ b/docs/docs/user-guide/admin/scheduled-presets.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 30 +title: Scheduled Presets +--- + +The Scheduled Presets page allows you to define times of the day when presets are triggered automatically. + +To start, use the plus button to add a new schedule. Next, select the days of the week you'd like the schedule to run on, and the time to run during those days. Finally, select the preset you'd like to run at that time, and click save. + +To trigger a preset at multiple times during the day, simply add multiple schedules for that preset. + +![scheduled presets page](@site/static/img/tutorial/admin/admin-scheduled-presets.png) + +The cog icon on the right side of each preset allows you to edit the advanced settings for that preset. + +![scheduled presets cog page](@site/static/img/tutorial/admin/admin-scheduled-presets-cog.png) + +The advanced settings allow you to set the following: + + - Notes: A description of why the schedule exists as a reminder in the administrator menu - this can help you differentiate between large numbers of schedules. + - Enabled: whether the schedule is enabled or not. If disabled, the schedule will not run. + - Run when locked: if the control panel is locked, the schedule will still run if this is enabled. + - Timeout: how many minutes to keep trying to trigger the preset for. If the preset is not triggered within this time, the schedule will be skipped for that day. This is particularly relevant if ParadisePi is powered off at the time the schedule is due to run, and later powered on - if the timeout plus the scheduled time is earlier than the time ParadisePi boots, then it will be skipped. \ No newline at end of file diff --git a/docs/static/img/tutorial/admin/admin-scheduled-presets-cog.png b/docs/static/img/tutorial/admin/admin-scheduled-presets-cog.png new file mode 100644 index 00000000..1ba49cb1 Binary files /dev/null and b/docs/static/img/tutorial/admin/admin-scheduled-presets-cog.png differ diff --git a/docs/static/img/tutorial/admin/admin-scheduled-presets.png b/docs/static/img/tutorial/admin/admin-scheduled-presets.png new file mode 100644 index 00000000..aca06404 Binary files /dev/null and b/docs/static/img/tutorial/admin/admin-scheduled-presets.png differ diff --git a/package-lock.json b/package-lock.json index ee57c220..d0678f12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paradisepi", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paradisepi", - "version": "2.3.0", + "version": "2.4.0", "license": "GPL-3.0-only", "dependencies": { "@emotion/react": "^11.11.0", diff --git a/package.json b/package.json index 28b0586d..c4446c0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paradisepi", - "version": "2.3.0", + "version": "2.4.0", "description": "Raspberry Pi based sACN lighting/OSC sound facility control panel", "main": ".webpack/main", "scripts": { diff --git a/src/api/config/configRouter.ts b/src/api/config/configRouter.ts index d9d102c5..2f789e6b 100644 --- a/src/api/config/configRouter.ts +++ b/src/api/config/configRouter.ts @@ -15,7 +15,7 @@ export const configRouter = ( method: 'GET' | 'POST' | 'PUT' | 'DELETE', payload: apiObject ): Promise => { - logger.debug('Preset router has a request', { path, method, payload }) + logger.debug('Config router has a request', { path, method, payload }) return new Promise((resolve, reject) => { if (method === 'POST') { let restartE131 = false diff --git a/src/api/database.ts b/src/api/database.ts index acab28ad..044f7a7d 100644 --- a/src/api/database.ts +++ b/src/api/database.ts @@ -1,11 +1,12 @@ -import { DatabasePreset, PresetRepository } from './../database/repository/preset' -import { DatabaseFolder, FolderRepository } from './../database/repository/folder' +import ip from 'ip' +import { version } from './../../package.json' import { ConfigRepository } from './../database/repository/config' +import { DatabaseFader, FaderRepository } from './../database/repository/fader' +import { DatabaseFolder, FolderRepository } from './../database/repository/folder' +import { DatabasePreset, PresetRepository } from './../database/repository/preset' +import { DatabaseTimeClockTrigger, TimeClockTriggersRepository } from './../database/repository/timeClockTrigger' import { getOperatingSystemName } from './about/operatingSystem/info' -import { version } from './../../package.json' import { broadcast } from './broadcast' -import ip from 'ip' -import { DatabaseFader, FaderRepository } from './../database/repository/fader' export interface Database { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -42,10 +43,10 @@ export interface Database { e131Frequency: number e131FadeTime: number e131Sampler_time: number - e131Sampler_effectMode: number } } presets: Array + timeClockTriggers: Array folders: { [key: number]: DatabaseFolder } @@ -91,10 +92,10 @@ export const createDatabaseObject = async (message: string): Promise = e131Frequency: parseInt(await ConfigRepository.getItem('e131Frequency')), e131FadeTime: parseInt(await ConfigRepository.getItem('e131FadeTime')), e131Sampler_time: parseInt(await ConfigRepository.getItem('e131Sampler_time')), - e131Sampler_effectMode: parseInt(await ConfigRepository.getItem('e131Sampler_effectMode')), }, }, presets: await PresetRepository.getAll(), + timeClockTriggers: await TimeClockTriggersRepository.getAll(), folders: await FolderRepository.getAll(), faders: await FaderRepository.getAll(), } diff --git a/src/api/preset/presetRouter.ts b/src/api/preset/presetRouter.ts index 05d6ff88..a33a33a8 100644 --- a/src/api/preset/presetRouter.ts +++ b/src/api/preset/presetRouter.ts @@ -1,9 +1,9 @@ -import { Preset } from './../../database/model/Preset' -import { DatabasePreset, PresetRepository } from './../../database/repository/preset' -import { createDatabaseObject, Database, sendDatabaseObject } from './../database' import axios from 'axios' import { parseJSON } from '../parseUserJson' +import { Preset } from './../../database/model/Preset' import { ConfigRepository } from './../../database/repository/config' +import { DatabasePreset, PresetRepository } from './../../database/repository/preset' +import { Database, createDatabaseObject, sendDatabaseObject } from './../database' /** * This is a REST router for the preset API. * @param path - The path requested by the original route requestor diff --git a/src/api/router.ts b/src/api/router.ts index 8ed81b75..421ec43d 100644 --- a/src/api/router.ts +++ b/src/api/router.ts @@ -1,12 +1,13 @@ +import { reboot } from './../electron/windowUtilities' import { aboutRouter } from './about/aboutRouter' -import { createDatabaseObject } from './database' -import { presetRouter } from './preset/presetRouter' -import { folderRouter } from './folder/folderRouter' import { configRouter } from './config/configRouter' -import { outputModulesRouter } from './outputModules/outputModulesRouter' -import { reboot } from './../electron/windowUtilities' +import { createDatabaseObject } from './database' import { faderRouter } from './fader/faderRouter' +import { folderRouter } from './folder/folderRouter' import { createImagesObject } from './images' +import { outputModulesRouter } from './outputModules/outputModulesRouter' +import { presetRouter } from './preset/presetRouter' +import { timeClockTriggersRouter } from './timeClockTriggers/timeClockTriggers' /** * This is a REST router that triages all requests and sends them to relevant routers @@ -52,6 +53,10 @@ export const routeRequest = ( // {@link configRouter} - this router handles all about requests for the /config path resolve(configRouter(pathArr.slice(1), method, payload)) break + case 'timeClockTriggers': + // {@link timeClockTriggers} - the timeClockTriggers router handles all about requests for the /timeClockTriggers path + resolve(timeClockTriggersRouter(pathArr.slice(1), method, payload)) + break case 'outputModules': // {@link outputModulesRouter} - this router handles all about requests for the /outputModules path resolve(outputModulesRouter(pathArr.slice(1), method, payload)) diff --git a/src/api/timeClockTriggers/timeClockTriggers.ts b/src/api/timeClockTriggers/timeClockTriggers.ts new file mode 100644 index 00000000..41593702 --- /dev/null +++ b/src/api/timeClockTriggers/timeClockTriggers.ts @@ -0,0 +1,29 @@ +import { Database, createDatabaseObject, sendDatabaseObject } from '../database' +import { DatabaseTimeClockTrigger, TimeClockTriggersRepository } from './../../database/repository/timeClockTrigger' +/** + * This is a REST router for the preset API. + * @param path - The path requested by the original route requestor + * @param method - The method requested by the original route requestor + * @param payload - Any payload sent + * @returns the retrieved response from the given route + * @throws an error if the requested route is not found + */ +export const timeClockTriggersRouter = ( + path: Array, + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + payload: apiObject +): Promise => { + logger.debug('Time clock trigger router has a request', { path, method, payload }) + return new Promise((resolve, reject) => { + if (method === 'PUT') { + return TimeClockTriggersRepository.setAllFromApp(payload as Array) + .then(() => { + return createDatabaseObject('updating all time clock triggers in bulk') + }) + .then((response: Database) => { + sendDatabaseObject(response) + resolve({}) + }) + } else reject(new Error('Path not found')) + }) +} diff --git a/src/app/Components/Admin/Controls/Presets/EditModal/TimeClockTriggers.tsx b/src/app/Components/Admin/Controls/Presets/EditModal/TimeClockTriggers.tsx new file mode 100644 index 00000000..130148fd --- /dev/null +++ b/src/app/Components/Admin/Controls/Presets/EditModal/TimeClockTriggers.tsx @@ -0,0 +1,104 @@ +import React, { useEffect } from 'react' +import { Group, ActionIcon, Button, Text, Checkbox, NumberInput } from '@mantine/core' +import { useForm } from '@mantine/form' +import { FaTrash } from '@react-icons/all-files/fa/FaTrash' +import { randomId } from '@mantine/hooks' +import { InputProps } from '../../../../InputProps' + +interface Trigger { + time: string + enabled: boolean + timeout: number + countdownWarning: number + key: string +} +interface FormValues { + triggers: Array +} + +export const TimeClockTriggersEditor = (props: InputProps) => { + const form = useForm({ + initialValues: { + triggers: [], + }, + }) + useEffect(() => { + if (props.value !== null) { + const valueObject = JSON.parse(props.value) || {} + form.setValues({ + triggers: valueObject.map((item: Trigger) => ({ + time: item.time, + key: item.key, + })), + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.value]) + return ( + <> + + Time Clock Triggers + + {form.isDirty() ? ( + + ) : null} + + Time Clock Triggers will recall this preset at the following times (if enabled) + {form.values.triggers.map((item, index) => ( + + + value.match(/\d(?=(?:\D*\d){0,3}$)/g) ? value.match(/\d(?=(?:\D*\d){0,3}$)/g).join('') : '' + } + formatter={value => + !Number.isNaN(parseFloat(value)) + ? value.replace(/\b\d{1,4}\b/g, match => { + const paddedNumber = match.padStart(4, '0') + const hours = paddedNumber.slice(0, 2) + const minutes = paddedNumber.slice(2) + return `${hours}:${minutes}` + }) + : '' + } + {...form.getInputProps(`triggers.${index}.time`)} + /> + + form.removeListItem('triggers', index)} + > + + + + ))} + + + + ) +} diff --git a/src/app/Components/ControlPanel/ButtonIcon.tsx b/src/app/Components/ControlPanel/ButtonIcon.tsx index 3b9c37cb..b0a30a81 100644 --- a/src/app/Components/ControlPanel/ButtonIcon.tsx +++ b/src/app/Components/ControlPanel/ButtonIcon.tsx @@ -1,5 +1,4 @@ import { Avatar, Group, Text } from '@mantine/core' -import { IconType } from '@react-icons/all-files/lib' import React, { forwardRef } from 'react' import { FaArrowDown } from '@react-icons/all-files/fa/FaArrowDown' @@ -21,68 +20,112 @@ import { FaVolumeMute } from '@react-icons/all-files/fa/FaVolumeMute' import { FaVolumeOff } from '@react-icons/all-files/fa/FaVolumeOff' import { FaVolumeUp } from '@react-icons/all-files/fa/FaVolumeUp' -export const AvailableIcons = { - FaLightbulb: 'Lightbulb', - FaRegLightbulb: 'Lightbulb', - FaVolumeUp: 'Volume Up', - FaVolumeDown: 'Volume Down', - FaVolumeMute: 'Volume Mute', - FaVolumeOff: 'Volume Off', - FaPhoneVolume: 'Phone', - FaMicrophone: 'Microphone', - FaMicrophoneAlt: 'Microphone', - FaVideo: 'Camera', - FaFolder: 'Folder', - FaArrowUp: 'Arrow Up', - FaArrowDown: 'Arrow Down', - FaArrowLeft: 'Arrow Left', - FaArrowRight: 'Arrow Right', - FaPowerOff: 'Power Off', - FaToggleOn: 'Toggle On', - FaToggleOff: 'Toggle Off', -} +const iconDatabase = [ + { + id: 'FaLightbulb', + name: 'Lightbulb', + icon: , + }, + { + id: 'FaRegLightbulb', + name: 'Lightbulb', + icon: , + }, + { + id: 'FaVolumeUp', + name: 'Volume Up', + icon: , + }, + { + id: 'FaVolumeDown', + name: 'Volume Down', + icon: , + }, + { + id: 'FaVolumeMute', + name: 'Volume Mute', + icon: , + }, + { + id: 'FaVolumeOff', + name: 'Volume Off', + icon: , + }, + { + id: 'FaPhoneVolume', + name: 'Phone', + icon: , + }, + { + id: 'FaMicrophone', + name: 'Microphone', + icon: , + }, + { + id: 'FaMicrophoneAlt', + name: 'Microphone', + icon: , + }, + { + id: 'FaVideo', + name: 'Camera', + icon: , + }, + { + id: 'FaFolder', + name: 'Folder', + icon: , + }, + { + id: 'FaArrowUp', + name: 'Arrow Up', + icon: , + }, + { + id: 'FaArrowDown', + name: 'Arrow Down', + icon: , + }, + { + id: 'FaArrowLeft', + name: 'Arrow Left', + icon: , + }, + { + id: 'FaArrowRight', + name: 'Arrow Right', + icon: , + }, + { + id: 'FaPowerOff', + name: 'Power Off', + icon: , + }, + { + id: 'FaToggleOn', + name: 'Toggle On', + icon: , + }, + { + id: 'FaToggleOff', + name: 'Toggle Off', + icon: , + }, +] -export const ButtonIcon = (icon: string): IconType => { - if (icon === 'FaLightbulb') return FaLightbulb - else if (icon === 'FaRegLightbulb') return FaRegLightbulb - else if (icon === 'FaVolumeDown') return FaVolumeDown - else if (icon === 'FaVolumeMute') return FaVolumeMute - else if (icon === 'FaVolumeOff') return FaVolumeOff - else if (icon === 'FaVolumeUp') return FaVolumeUp - else if (icon === 'FaPhoneVolume') return FaPhoneVolume - else if (icon === 'FaMicrophone') return FaMicrophone - else if (icon === 'FaMicrophoneAlt') return FaMicrophoneAlt - else if (icon === 'FaVideo') return FaVideo - else if (icon === 'FaFolder') return FaFolder - else if (icon === 'FaArrowUp') return FaArrowUp - else if (icon === 'FaArrowDown') return FaArrowDown - else if (icon === 'FaArrowLeft') return FaArrowLeft - else if (icon === 'FaArrowRight') return FaArrowRight - else if (icon === 'FaPowerOff') return FaPowerOff - else if (icon === 'FaToggleOn') return FaToggleOn - else if (icon === 'FaToggleOff') return FaToggleOff - else return null +export const availableIcons = () => { + const icons: { + [key: string]: string + } = {} + iconDatabase.forEach(icon => { + icons[icon.id] = icon.name + }) + return icons } -// TODO surely we don't need two functions? -export const ButtonIconReact = (props: { icon: string }) => { - if (props.icon === 'FaLightbulb') return - else if (props.icon === 'FaRegLightbulb') return - else if (props.icon === 'FaVolumeDown') return - else if (props.icon === 'FaVolumeMute') return - else if (props.icon === 'FaVolumeOff') return - else if (props.icon === 'FaVolumeUp') return - else if (props.icon === 'FaPhoneVolume') return - else if (props.icon === 'FaMicrophone') return - else if (props.icon === 'FaMicrophoneAlt') return - else if (props.icon === 'FaVideo') return - else if (props.icon === 'FaFolder') return - else if (props.icon === 'FaArrowUp') return - else if (props.icon === 'FaArrowDown') return - else if (props.icon === 'FaArrowLeft') return - else if (props.icon === 'FaArrowRight') return - else if (props.icon === 'FaPowerOff') return - else if (props.icon === 'FaToggleOn') return - else if (props.icon === 'FaToggleOff') return + +export const ButtonIcon = (props: { icon: string }) => { + const icon = iconDatabase.find(icon => icon.id === props.icon) + if (icon) return icon.icon else return <> } @@ -98,7 +141,7 @@ export const ButtonIconSelectItem = forwardRef( {icon ? ( - + ) : ( '' diff --git a/src/app/Components/Locked.tsx b/src/app/Components/Locked.tsx index 9f2371a1..97fc8a17 100644 --- a/src/app/Components/Locked.tsx +++ b/src/app/Components/Locked.tsx @@ -1,7 +1,7 @@ -import { Stack, Title, Collapse, Text } from '@mantine/core' -import React, { useCallback, useEffect, useState } from 'react' +import { Collapse, Stack, Text, Title } from '@mantine/core' import { useEventListener, useViewportSize } from '@mantine/hooks' import { FaLock } from '@react-icons/all-files/fa/FaLock' +import React, { useCallback, useEffect, useState } from 'react' import { useAppSelector } from './../apis/redux/mainStore' const LockedMessage = () => { @@ -12,7 +12,7 @@ const LockedMessage = () => { const port = useAppSelector(state => (state.database ? state.database.about.port : false)) if (count >= 5) { return ( - + Device is locked. To unlock it access the setup and administration area, by ensuring you are on the same network as this device and then navigating to http:// {ipAddress + ':' + port ?? ''} with a web browser @@ -23,6 +23,7 @@ const LockedMessage = () => { Device is locked, unlock it using the setup and administration area on another device diff --git a/src/app/Navigation/AdminNavigation.tsx b/src/app/Navigation/AdminNavigation.tsx index f70e4aa7..5e9acc7c 100644 --- a/src/app/Navigation/AdminNavigation.tsx +++ b/src/app/Navigation/AdminNavigation.tsx @@ -1,17 +1,18 @@ -import React from 'react' -import { Navbar, Group, Code, Text, ScrollArea } from '@mantine/core' +import { Code, Group, Navbar, ScrollArea, Text } from '@mantine/core' import { useViewportSize } from '@mantine/hooks' -import { Link } from 'react-router-dom' import { FaCogs } from '@react-icons/all-files/fa/FaCogs' +import { FaDatabase } from '@react-icons/all-files/fa/FaDatabase' import { FaLevelUpAlt } from '@react-icons/all-files/fa/FaLevelUpAlt' +import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock' import { FaRegFolder } from '@react-icons/all-files/fa/FaRegFolder' -import { FaDatabase } from '@react-icons/all-files/fa/FaDatabase' import { FaRegPlayCircle } from '@react-icons/all-files/fa/FaRegPlayCircle' import { FaWindowClose } from '@react-icons/all-files/fa/FaWindowClose' -import { useStyles } from './Styles' -import { NavbarItem } from './NavbarItem' -import { useAppSelector } from './../apis/redux/mainStore' +import React from 'react' +import { Link } from 'react-router-dom' import { runningInElectron } from '../apis/utilities/version' +import { useAppSelector } from './../apis/redux/mainStore' +import { NavbarItem } from './NavbarItem' +import { useStyles } from './Styles' export const AdminNavigation = () => { const { classes, cx } = useStyles() @@ -32,13 +33,22 @@ export const AdminNavigation = () => { Exit ) : null} - - - - + } + /> + } /> + } /> + } /> + } + /> - + } /> ) diff --git a/src/app/Navigation/ControlPanelNavigation.tsx b/src/app/Navigation/ControlPanelNavigation.tsx index b0fab8bd..35dd9bae 100644 --- a/src/app/Navigation/ControlPanelNavigation.tsx +++ b/src/app/Navigation/ControlPanelNavigation.tsx @@ -1,15 +1,16 @@ -import React from 'react' import { Navbar, ScrollArea } from '@mantine/core' import { useViewportSize } from '@mantine/hooks' import { FaQuestion } from '@react-icons/all-files/fa/FaQuestion' -import { useStyles } from './Styles' -import { NavbarItem } from './NavbarItem' -import { useAppSelector } from './../apis/redux/mainStore' -import { DatabaseFolder } from './../../database/repository/folder' +import React from 'react' import { ButtonIcon } from '../Components/ControlPanel/ButtonIcon' +import { DatabaseFolder } from './../../database/repository/folder' +import { useAppSelector } from './../apis/redux/mainStore' +import { NavbarItem } from './NavbarItem' +import { useStyles } from './Styles' const TopLevelFolders = () => { const folders = useAppSelector(state => (state.database ? state.database.folders : false)) + const { classes } = useStyles() const topLevelFolders: Array = [] if (folders !== false) { Object.entries(folders) @@ -31,7 +32,11 @@ const TopLevelFolders = () => { key={item.id} link={'folder/' + item.id.toString()} label={item.name} - Icon={ButtonIcon(item.icon)} + Icon={ + + + + } /> ))} @@ -46,7 +51,7 @@ export const ControlPanelNavigation = () => { - + } /> ) diff --git a/src/app/Navigation/NavbarItem.tsx b/src/app/Navigation/NavbarItem.tsx index 1d11c4fc..ce7bf17b 100644 --- a/src/app/Navigation/NavbarItem.tsx +++ b/src/app/Navigation/NavbarItem.tsx @@ -1,9 +1,8 @@ import React from 'react' import { Link, useLocation } from 'react-router-dom' -import { IconType } from '@react-icons/all-files/lib' import { useStyles } from './Styles' -export const NavbarItem = ({ link, label, Icon }: { link: string; label: string; Icon: IconType }) => { +export const NavbarItem = ({ link, label, Icon }: { link: string; label: string; Icon: React.ReactNode }) => { const location = useLocation() const pathLink = location.pathname.replace('/admin/', '').replace('/controlPanel/', '') const { classes, cx } = useStyles() @@ -15,7 +14,7 @@ export const NavbarItem = ({ link, label, Icon }: { link: string; label: string; to={link} key={label} > - + {Icon} {label} ) diff --git a/src/app/Navigation/Styles.tsx b/src/app/Navigation/Styles.tsx index a3aeb1ce..ff979a82 100644 --- a/src/app/Navigation/Styles.tsx +++ b/src/app/Navigation/Styles.tsx @@ -1,7 +1,7 @@ import { createStyles } from '@mantine/core' -export const useStyles = createStyles((theme, _params, getRef) => { - const icon = getRef('icon') +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const useStyles = createStyles((theme, _params) => { return { navbar: { backgroundColor: theme.colors.dark[6], @@ -48,7 +48,6 @@ export const useStyles = createStyles((theme, _params, getRef) => { }, linkIcon: { - ref: icon, color: theme.white, opacity: 0.75, marginRight: theme.spacing.sm, @@ -58,9 +57,6 @@ export const useStyles = createStyles((theme, _params, getRef) => { linkActive: { '&, &:hover': { backgroundColor: theme.colors.dark[7], - [`& .${icon}`]: { - opacity: 0.9, - }, }, }, } diff --git a/src/app/Pages/Admin/Folders.tsx b/src/app/Pages/Admin/Folders.tsx index 923dd675..ef64b832 100644 --- a/src/app/Pages/Admin/Folders.tsx +++ b/src/app/Pages/Admin/Folders.tsx @@ -1,34 +1,34 @@ -import React, { useEffect, useState } from 'react' import { - Group, - TextInput, + ActionIcon, Box, Button, Center, - ActionIcon, - Select, + Group, LoadingOverlay, SelectItem as MantineSelectItem, Modal, - Text, + Select, Table, + Text, + TextInput, Title, } from '@mantine/core' import { useForm } from '@mantine/form' -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' +import { showNotification } from '@mantine/notifications' +import { RichTextEditor } from '@mantine/rte' +import { FaCheck } from '@react-icons/all-files/fa/FaCheck' import { FaFolder } from '@react-icons/all-files/fa/FaFolder' import { FaGripVertical } from '@react-icons/all-files/fa/FaGripVertical' +import { FaPencilAlt } from '@react-icons/all-files/fa/FaPencilAlt' +import { FaPlus } from '@react-icons/all-files/fa/FaPlus' +import { FaSave } from '@react-icons/all-files/fa/FaSave' import { FaTrash } from '@react-icons/all-files/fa/FaTrash' -import { useAppSelector } from './../../apis/redux/mainStore' +import React, { useEffect, useState } from 'react' +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' +import { ButtonIconSelectItem, availableIcons } from '../../Components/ControlPanel/ButtonIcon' import { DatabaseFolder } from './../../../database/repository/folder' +import { useAppSelector } from './../../apis/redux/mainStore' import { ApiCall } from './../../apis/wrapper' -import { AvailableIcons, ButtonIconSelectItem } from '../../Components/ControlPanel/ButtonIcon' -import { FaPencilAlt } from '@react-icons/all-files/fa/FaPencilAlt' -import { RichTextEditor } from '@mantine/rte' -import { FaSave } from '@react-icons/all-files/fa/FaSave' -import { FaPlus } from '@react-icons/all-files/fa/FaPlus' -import { showNotification } from '@mantine/notifications' -import { FaCheck } from '@react-icons/all-files/fa/FaCheck' interface FormValues { folders: Array @@ -137,7 +137,7 @@ export const FoldersConfigurationPage = () => { searchable={true} nothingFound="No icons found" itemComponent={ButtonIconSelectItem} - data={Object.entries(AvailableIcons).map(([value, name]) => ({ + data={Object.entries(availableIcons()).map(([value, name]) => ({ value: value, icon: value, label: name, diff --git a/src/app/Pages/Admin/ModuleConfiguration/E131.tsx b/src/app/Pages/Admin/ModuleConfiguration/E131.tsx index 6affe7a5..fdf16819 100644 --- a/src/app/Pages/Admin/ModuleConfiguration/E131.tsx +++ b/src/app/Pages/Admin/ModuleConfiguration/E131.tsx @@ -1,16 +1,16 @@ -import { Box, Button, Divider, Loader, LoadingOverlay, TextInput, Checkbox, NumberInput, Text } from '@mantine/core' +import { Box, Button, Checkbox, Divider, Loader, LoadingOverlay, NumberInput, Text, TextInput } from '@mantine/core' import { useForm } from '@mantine/form' -import React, { useEffect, useState } from 'react' -import { ApiCall } from '../../../apis/wrapper' -import { useAppSelector } from '../../../apis/redux/mainStore' -import { FaIdBadge } from '@react-icons/all-files/fa/FaIdBadge' -import { FaPlay } from '@react-icons/all-files/fa/FaPlay' +import { useModals } from '@mantine/modals' import { FaClipboardList } from '@react-icons/all-files/fa/FaClipboardList' import { FaCrown } from '@react-icons/all-files/fa/FaCrown' -import { FaWaveSquare } from '@react-icons/all-files/fa/FaWaveSquare' +import { FaIdBadge } from '@react-icons/all-files/fa/FaIdBadge' +import { FaPlay } from '@react-icons/all-files/fa/FaPlay' import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock' -import { useModals } from '@mantine/modals' import { FaSave } from '@react-icons/all-files/fa/FaSave' +import { FaWaveSquare } from '@react-icons/all-files/fa/FaWaveSquare' +import React, { useEffect, useState } from 'react' +import { useAppSelector } from '../../../apis/redux/mainStore' +import { ApiCall } from '../../../apis/wrapper' export const E131ModuleConfigurationPage = () => { const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false) @@ -26,7 +26,6 @@ export const E131ModuleConfigurationPage = () => { e131Frequency: 0, e131FadeTime: 0, e131Sampler_time: 0, - e131Sampler_effectMode: 0, }, validate: { e131SourceName: value => (value.length > 5 ? null : 'Must be longer than 5 characters'), @@ -43,7 +42,6 @@ export const E131ModuleConfigurationPage = () => { e131Frequency: e131Config.e131Frequency, e131FadeTime: e131Config.e131FadeTime, e131Sampler_time: e131Config.e131Sampler_time, - e131Sampler_effectMode: e131Config.e131Sampler_effectMode, }) } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/app/Pages/Admin/Presets.tsx b/src/app/Pages/Admin/Presets.tsx index 30a5fd2b..d1a1fb0d 100644 --- a/src/app/Pages/Admin/Presets.tsx +++ b/src/app/Pages/Admin/Presets.tsx @@ -1,49 +1,49 @@ -import React, { useEffect, useState } from 'react' import { - Group, - TextInput, + ActionIcon, + Alert, + Badge, Box, Button, Center, - ActionIcon, - Select, - LoadingOverlay, Checkbox, + Chip, ColorInput, + Group, + LoadingOverlay, Modal, - SelectItem, NumberInput, - Chip, + Select, + SelectItem, + Table, Text, + TextInput, Title, - Table, - Alert, - Badge, } from '@mantine/core' import { useForm } from '@mantine/form' -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' +import { useModals } from '@mantine/modals' +import { showNotification } from '@mantine/notifications' +import { FaCheck } from '@react-icons/all-files/fa/FaCheck' import { FaFolder } from '@react-icons/all-files/fa/FaFolder' import { FaGripVertical } from '@react-icons/all-files/fa/FaGripVertical' -import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock' -import { FaTrash } from '@react-icons/all-files/fa/FaTrash' -import { FaRegClone } from '@react-icons/all-files/fa/FaRegClone' import { FaPencilAlt } from '@react-icons/all-files/fa/FaPencilAlt' -import { FaSpaceShuttle } from '@react-icons/all-files/fa/FaSpaceShuttle' -import { FaSave } from '@react-icons/all-files/fa/FaSave' import { FaPlus } from '@react-icons/all-files/fa/FaPlus' import { FaRecycle } from '@react-icons/all-files/fa/FaRecycle' -import { useAppSelector } from './../../apis/redux/mainStore' +import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock' +import { FaRegClone } from '@react-icons/all-files/fa/FaRegClone' +import { FaSave } from '@react-icons/all-files/fa/FaSave' +import { FaSpaceShuttle } from '@react-icons/all-files/fa/FaSpaceShuttle' +import { FaTrash } from '@react-icons/all-files/fa/FaTrash' +import React, { useEffect, useState } from 'react' +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' import { DatabasePreset, PresetTypes } from './../../../database/repository/preset' -import { ApiCall } from './../../apis/wrapper' -import { OSCPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/OSC' +import { E131PresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/E131' import { HTTPPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/HTTP' import { MacroPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/Macro' -import { E131PresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/E131' -import { useModals } from '@mantine/modals' -import { FaCheck } from '@react-icons/all-files/fa/FaCheck' -import { showNotification } from '@mantine/notifications' +import { OSCPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/OSC' import { isValidJson } from './../../Components/Admin/Controls/Presets/EditModal/isValidJson' -import { AvailableIcons, ButtonIconSelectItem } from './../../Components/ControlPanel/ButtonIcon' +import { ButtonIconSelectItem, availableIcons } from './../../Components/ControlPanel/ButtonIcon' +import { useAppSelector } from './../../apis/redux/mainStore' +import { ApiCall } from './../../apis/wrapper' interface FormValues { presets: Array @@ -170,7 +170,7 @@ export const PresetsConfigurationPage = () => { clearable={true} data={[ { value: null, icon: null, label: '' }, - ...Object.entries(AvailableIcons).map(([value, name]) => ({ + ...Object.entries(availableIcons()).map(([value, name]) => ({ value: value, icon: value, label: name, @@ -255,6 +255,7 @@ export const PresetsConfigurationPage = () => { universe: form.values.presets[index].universe, fadeTime: form.values.presets[index].fadeTime, data: form.values.presets[index].data, + timeClockTriggers: null, //Deliberate decision not to copy these httpTriggerEnabled: form.values.presets[index].httpTriggerEnabled, folderId: form.values.presets[index].folderId, color: form.values.presets[index].color, @@ -350,6 +351,7 @@ export const PresetsConfigurationPage = () => { universe: 1, fadeTime: 0, data: null, + timeClockTriggers: null, httpTriggerEnabled: false, folderId: '0', icon: null, diff --git a/src/app/Pages/Admin/TimeClockTriggers.tsx b/src/app/Pages/Admin/TimeClockTriggers.tsx new file mode 100644 index 00000000..93a9624a --- /dev/null +++ b/src/app/Pages/Admin/TimeClockTriggers.tsx @@ -0,0 +1,261 @@ +import { + ActionIcon, + Box, + Button, + Checkbox, + Group, + LoadingOverlay, + Modal, + NumberInput, + Select, + SelectItem, + Table, + Text, + TextInput, + Textarea, + Title, +} from '@mantine/core' +import { useForm } from '@mantine/form' +import { showNotification } from '@mantine/notifications' +import { FaCheck } from '@react-icons/all-files/fa/FaCheck' +import { FaCog } from '@react-icons/all-files/fa/FaCog' +import { FaPlus } from '@react-icons/all-files/fa/FaPlus' +import { FaRegClone } from '@react-icons/all-files/fa/FaRegClone' +import { FaSave } from '@react-icons/all-files/fa/FaSave' +import { FaTrash } from '@react-icons/all-files/fa/FaTrash' +import React, { useEffect, useState } from 'react' +import { DatabaseTimeClockTrigger } from '../../../database/repository/timeClockTrigger' +import { useAppSelector } from '../../apis/redux/mainStore' +import { ApiCall } from '../../apis/wrapper' + +interface FormValues { + triggers: Array +} + +export const TimeClockTriggersConfigurationPage = () => { + const [modalVisible, setModalVisible] = useState(false) + const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false) + const [formOriginalValues, setFormOriginalValues] = useState('') // Values used to detect unsaved changes + const presets = useAppSelector(state => (state.database ? state.database.presets : false)) + const folders = useAppSelector(state => (state.database ? state.database.folders : false)) + const timeClockTriggers = useAppSelector(state => (state.database ? state.database.timeClockTriggers : false)) + const presetsForSelect: Array = [] + // Prepare folders list for select dropdown + if (presets !== false && folders !== false) { + Object.entries(presets).forEach(([, value]) => { + if (value.folderId !== null) { + presetsForSelect.push({ + value: value.id.toString(), + label: value.name, + group: folders[value.folderId as unknown as number].name, + }) + } + }) + } + // Setup the form + const form = useForm({ + initialValues: { + triggers: Array(), + }, + validate: { + triggers: { + timeout: (value: number) => (value < 0 ? 'Timeout should be a number' : null), + time: (value: string) => + value.match(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/) ? null : 'Time should be in hh:mm format', + presetId: value => + typeof value === 'undefined' || value == null || parseInt(value) == 0 + ? 'Preset must be selected' + : null, + }, + }, + }) + useEffect(() => { + if (timeClockTriggers !== false) { + const formValues = { triggers: timeClockTriggers.map(item => ({ ...item })) } + form.setValues(formValues) // Make a copy of the presets using map because the object is not extensible + setFormOriginalValues(JSON.stringify(formValues)) + setLoadingOverlayVisible(false) + } else if (!loadingOverlayVisible) setLoadingOverlayVisible(true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [presets]) + const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes + // Handle the submit button + const handleSubmit = (values: typeof form.values) => { + setLoadingOverlayVisible(true) + ApiCall.put('/timeClockTriggers', values.triggers).then(() => { + showNotification({ + message: 'Your changes have been saved', + autoClose: 2000, + disallowClose: true, + color: 'green', + icon: , + }) + }) + } + const fields = form.values.triggers.map((_, index) => ( + + + {form.values.triggers[index].notes ? {form.values.triggers[index].notes} : null} + + {['Mon', 'Tues', 'Weds', 'Thurs', 'Fri', 'Sat', 'Sun'].map((day, i) => ( + + ))} + + + + + + +