diff --git a/apps/client/src/common/hooks-query/useAliases.ts b/apps/client/src/common/hooks-query/useAliases.ts index 08e5b5176..cb54e320f 100644 --- a/apps/client/src/common/hooks-query/useAliases.ts +++ b/apps/client/src/common/hooks-query/useAliases.ts @@ -5,7 +5,7 @@ import { ALIASES } from '../api/apiConstants'; import { getAliases } from '../api/ontimeApi'; export default function useAliases() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: ALIASES, queryFn: getAliases, placeholderData: [], @@ -15,5 +15,5 @@ export default function useAliases() { networkMode: 'always', }); - return { data, status, isError, refetch }; -} \ No newline at end of file + return { data, status, isFetching, isError, refetch }; +} diff --git a/apps/client/src/common/hooks-query/useEventData.ts b/apps/client/src/common/hooks-query/useEventData.ts index 85b24c28c..215605e31 100644 --- a/apps/client/src/common/hooks-query/useEventData.ts +++ b/apps/client/src/common/hooks-query/useEventData.ts @@ -6,7 +6,7 @@ import { fetchEventData } from '../api/eventDataApi'; import { eventDataPlaceholder } from '../models/EventData'; export default function useEventData() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: EVENT_DATA, queryFn: fetchEventData, placeholderData: eventDataPlaceholder, @@ -16,5 +16,5 @@ export default function useEventData() { networkMode: 'always', }); - return { data, status, isError, refetch }; + return { data, status, isFetching, isError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useOscSettings.ts b/apps/client/src/common/hooks-query/useOscSettings.ts index c7bc15fd9..610bc40a3 100644 --- a/apps/client/src/common/hooks-query/useOscSettings.ts +++ b/apps/client/src/common/hooks-query/useOscSettings.ts @@ -8,7 +8,7 @@ import { oscPlaceholderSettings } from '../models/OscSettings'; import { ontimeQueryClient } from '../queryClient'; export default function useOscSettings() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: OSC_SETTINGS, queryFn: getOSC, placeholderData: oscPlaceholderSettings, @@ -19,7 +19,7 @@ export default function useOscSettings() { }); // we need to jump through some hoops because of the type op port - return { data: data! as unknown as OSCSettings, status, isError, refetch }; + return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch }; } export function useOscSettingsMutation() { diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts index e2918d8d1..2bb284b57 100644 --- a/apps/client/src/common/hooks-query/useSettings.ts +++ b/apps/client/src/common/hooks-query/useSettings.ts @@ -6,7 +6,7 @@ import { getSettings } from '../api/ontimeApi'; import { ontimePlaceholderSettings } from '../models/OntimeSettings'; export default function useSettings() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: APP_SETTINGS, queryFn: getSettings, placeholderData: ontimePlaceholderSettings, @@ -16,5 +16,5 @@ export default function useSettings() { networkMode: 'always', }); - return { data, status, isError, refetch }; + return { data, status, isFetching, isError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useUserFields.ts b/apps/client/src/common/hooks-query/useUserFields.ts index 784e5f65c..56faaae9e 100644 --- a/apps/client/src/common/hooks-query/useUserFields.ts +++ b/apps/client/src/common/hooks-query/useUserFields.ts @@ -6,7 +6,7 @@ import { getUserFields } from '../api/ontimeApi'; import { userFieldsPlaceholder } from '../models/UserFields'; export default function useUserFields() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: USERFIELDS, queryFn: getUserFields, placeholderData: userFieldsPlaceholder, @@ -16,5 +16,5 @@ export default function useUserFields() { networkMode: 'always', }); - return { data, status, isError, refetch }; + return { data, status, isFetching, isError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useViewSettings.ts b/apps/client/src/common/hooks-query/useViewSettings.ts index 2f2bc2b9a..3c6abb01c 100644 --- a/apps/client/src/common/hooks-query/useViewSettings.ts +++ b/apps/client/src/common/hooks-query/useViewSettings.ts @@ -6,7 +6,7 @@ import { getView } from '../api/ontimeApi'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; export default function useViewSettings() { - const { data, status, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: VIEW_SETTINGS, queryFn: getView, placeholderData: viewsSettingsPlaceholder, @@ -16,5 +16,5 @@ export default function useViewSettings() { networkMode: 'always', }); - return { data, status, isError, refetch }; + return { data, status, isError, refetch, isFetching }; } diff --git a/apps/client/src/features/modals/ModalWrapper.tsx b/apps/client/src/features/modals/ModalWrapper.tsx index 7c364f50d..5b81ac1d0 100644 --- a/apps/client/src/features/modals/ModalWrapper.tsx +++ b/apps/client/src/features/modals/ModalWrapper.tsx @@ -10,7 +10,6 @@ interface ModalWrapperProps { export default function ModalWrapper(props: PropsWithChildren) { const { isOpen, onClose, title, size = 'xl', children } = props; - return ( ({ defaultValues: data.subscriptions, values: data.subscriptions, + resetOptions: { + keepDirtyValues: true, + }, }); const [showSection, setShowSection] = useState(TimerLifeCycle.onLoad); + useEffect(() => { + if (data) { + reset(data.subscriptions); + } + }, [data, reset]); + const resetForm = () => { reset(data.subscriptions); }; @@ -78,6 +88,10 @@ export default function OscIntegration() { } }; + if (isFetching) { + return ; + } + return (
({ defaultValues: data, values: data, + resetOptions: { + keepDirtyValues: true, + }, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); const onSubmit = async (values: PlaceholderSettings) => { const numericPortIn = Number(values.portIn); const numericPortOut = Number(values.portOut); @@ -51,6 +61,10 @@ export default function OscSettings() { reset(data); }; + if (isFetching) { + return ; + } + return (
diff --git a/apps/client/src/features/modals/modal-loader/ModalLoader.module.scss b/apps/client/src/features/modals/modal-loader/ModalLoader.module.scss new file mode 100644 index 000000000..300ffc3d9 --- /dev/null +++ b/apps/client/src/features/modals/modal-loader/ModalLoader.module.scss @@ -0,0 +1,46 @@ +@use '../../../theme/ontimeColours' as *; + +.screenLoader { + position: absolute; + z-index: 10; + width: 100%; + height: 100%; + display: grid; + place-content: center; + top: 0; + background-color: $white-60; +} + +$loader-size: 48px; + +.loader { + width: $loader-size; + height: $loader-size; + background: $blue-500; + display: inline-block; + border-radius: 50%; + box-sizing: border-box; + animation: animloader 1s ease-in infinite; +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} diff --git a/apps/client/src/features/modals/modal-loader/ModalLoader.tsx b/apps/client/src/features/modals/modal-loader/ModalLoader.tsx new file mode 100644 index 000000000..ed00a3b88 --- /dev/null +++ b/apps/client/src/features/modals/modal-loader/ModalLoader.tsx @@ -0,0 +1,9 @@ +import style from './ModalLoader.module.scss'; + +export default function ModalLoader() { + return ( +
+ +
+ ); +} diff --git a/apps/client/src/features/modals/quick-start/QuickStart.tsx b/apps/client/src/features/modals/quick-start/QuickStart.tsx index 396563875..e262e4b3a 100644 --- a/apps/client/src/features/modals/quick-start/QuickStart.tsx +++ b/apps/client/src/features/modals/quick-start/QuickStart.tsx @@ -38,11 +38,16 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) { register, reset, formState: { isSubmitting }, - } = useForm({ defaultValues: data }); + } = useForm({ + defaultValues: data, + resetOptions: { + keepDirtyValues: true, + }, + }); useEffect(() => { - reset(data); - }, [reset, data]); + if (data) reset(data); + }, [data, reset]); const onSubmit = async (data: Partial) => { try { diff --git a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx index 3e3565699..c542adff1 100644 --- a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx +++ b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, IconButton, Input, Switch } from '@chakra-ui/react'; import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline'; @@ -9,6 +10,7 @@ import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBt import useAliases from '../../../common/hooks-query/useAliases'; import { useEmitLog } from '../../../common/stores/logger'; import { handleLinks } from '../../../common/utils/linkUtils'; +import ModalLoader from '../modal-loader/ModalLoader'; import { inputProps } from '../modalHelper'; import ModalLink from '../ModalLink'; import OntimeModalFooter from '../OntimeModalFooter'; @@ -21,7 +23,7 @@ const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases'; type Aliases = { aliases: Alias[] }; export default function AliasesForm() { - const { data, status, refetch } = useAliases(); + const { data, status, isFetching, refetch } = useAliases(); const { emitError } = useEmitLog(); const { control, @@ -32,12 +34,21 @@ export default function AliasesForm() { } = useForm({ defaultValues: { aliases: data }, values: { aliases: data || [] }, + resetOptions: { + keepDirtyValues: true, + }, }); const { fields, append, remove } = useFieldArray({ name: 'aliases', control, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + const onSubmit = async (formData: Aliases) => { try { await postAliases(formData.aliases); @@ -67,6 +78,10 @@ export default function AliasesForm() { const disableInputs = status === 'loading'; const hasTooManyOptions = fields.length >= 20; + if (isFetching) { + return ; + } + return (
diff --git a/apps/client/src/features/modals/settings-modal/AppSettings.tsx b/apps/client/src/features/modals/settings-modal/AppSettings.tsx index 85d94a7d8..cd9456391 100644 --- a/apps/client/src/features/modals/settings-modal/AppSettings.tsx +++ b/apps/client/src/features/modals/settings-modal/AppSettings.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { Input, Select } from '@chakra-ui/react'; import type { Settings } from 'ontime-types'; @@ -6,6 +7,7 @@ import { postSettings } from '../../../common/api/ontimeApi'; import useSettings from '../../../common/hooks-query/useSettings'; import { useEmitLog } from '../../../common/stores/logger'; import { isOnlyNumbers } from '../../../common/utils/regex'; +import ModalLoader from '../modal-loader/ModalLoader'; import ModalSplitInput from '../ModalSplitInput'; import OntimeModalFooter from '../OntimeModalFooter'; @@ -14,7 +16,7 @@ import ModalPinInput from './ModalPinInput'; import style from './SettingsModal.module.scss'; export default function AppSettingsModal() { - const { data, status, refetch } = useSettings(); + const { data, status, isFetching, refetch } = useSettings(); const { emitError } = useEmitLog(); const { handleSubmit, @@ -24,8 +26,17 @@ export default function AppSettingsModal() { } = useForm({ defaultValues: data, values: data, + resetOptions: { + keepDirtyValues: true, + }, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + const onSubmit = async (formData: Settings) => { try { await postSettings(formData); @@ -42,6 +53,10 @@ export default function AppSettingsModal() { const disableInputs = status === 'loading'; + if (isFetching) { + return ; + } + return ( ({ defaultValues: data, values: data, + resetOptions: { + keepDirtyValues: true, + }, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + const onSubmit = async (formData: UserFields) => { try { await postUserFields(formData); @@ -43,6 +54,10 @@ export default function CuesheetSettings() { const disableInputs = status === 'loading'; + if (isFetching) { + return ; + } + return (
diff --git a/apps/client/src/features/modals/settings-modal/EventDataForm.tsx b/apps/client/src/features/modals/settings-modal/EventDataForm.tsx index 0889003ef..90f70e51d 100644 --- a/apps/client/src/features/modals/settings-modal/EventDataForm.tsx +++ b/apps/client/src/features/modals/settings-modal/EventDataForm.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { Input, Textarea } from '@chakra-ui/react'; import { EventData } from 'ontime-types'; @@ -5,6 +6,7 @@ import { EventData } from 'ontime-types'; import { postEventData } from '../../../common/api/eventDataApi'; import useEventData from '../../../common/hooks-query/useEventData'; import { useEmitLog } from '../../../common/stores/logger'; +import ModalLoader from '../modal-loader/ModalLoader'; import { inputProps } from '../modalHelper'; import ModalInput from '../ModalInput'; import OntimeModalFooter from '../OntimeModalFooter'; @@ -12,7 +14,7 @@ import OntimeModalFooter from '../OntimeModalFooter'; import style from './SettingsModal.module.scss'; export default function EventDataForm() { - const { data, status, refetch } = useEventData(); + const { data, status, isFetching, refetch } = useEventData(); const { emitError } = useEmitLog(); const { handleSubmit, @@ -22,8 +24,17 @@ export default function EventDataForm() { } = useForm({ defaultValues: data, values: data, + resetOptions: { + keepDirtyValues: true, + }, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + const onSubmit = async (formData: EventData) => { try { await postEventData(formData); @@ -40,6 +51,10 @@ export default function EventDataForm() { const disableInputs = status === 'loading'; + if (isFetching) { + return ; + } + return ( ({ defaultValues: data, values: data, + resetOptions: { + keepDirtyValues: true, + }, }); + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + const onSubmit = async (formData: ViewSettings) => { const parsedWarningThreshold = dirtyFields?.warningThreshold ? // @ts-expect-error -- trust me @@ -65,6 +76,10 @@ export default function ViewSettingsForm() { const disableInputs = status === 'loading'; + if (isFetching) { + return ; + } + return ( General view settings diff --git a/apps/client/src/theme/_ontimeColours.scss b/apps/client/src/theme/_ontimeColours.scss index c1f74ab5f..a9db62269 100644 --- a/apps/client/src/theme/_ontimeColours.scss +++ b/apps/client/src/theme/_ontimeColours.scss @@ -7,6 +7,7 @@ $white-9: rgba(255, 255, 255, 0.09); $white-10: rgba(255, 255, 255, 0.10); $white-13: rgba(255, 255, 255, 0.13); $white-20: rgba(255, 255, 255, 0.20); +$white-60: rgba(255, 255, 255, 0.60); $black-10: rgba(0, 0, 0, 0.10);