diff --git a/app/components/device/new/advanced-info.tsx b/app/components/device/new/advanced-info.tsx index 6a45bc6b..2bbbe3f9 100644 --- a/app/components/device/new/advanced-info.tsx +++ b/app/components/device/new/advanced-info.tsx @@ -1,124 +1,149 @@ -import Form from "@rjsf/core"; -import validator from "@rjsf/validator-ajv8"; -import { useEffect, useState } from "react"; -import { useFormContext } from "react-hook-form"; -import { ArrayFieldTemplate } from "~/components/rjsf/arrayFieldTemplate"; -import { CheckboxWidget } from "~/components/rjsf/checkboxWidget"; -import { FieldTemplate } from "~/components/rjsf/fieldTemplate"; -import { BaseInputTemplate } from "~/components/rjsf/inputTemplate"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; -import { Label } from "~/components/ui/label"; -import { Switch } from "~/components/ui/switch"; +import Form from '@rjsf/core' +import validator from '@rjsf/validator-ajv8' +import { T } from 'node_modules/vitest/dist/chunks/traces.d.402V_yFI' +import { useState } from 'react' +import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { ArrayFieldTemplate } from '~/components/rjsf/arrayFieldTemplate' +import { CheckboxWidget } from '~/components/rjsf/checkboxWidget' +import { FieldTemplate } from '~/components/rjsf/fieldTemplate' +import { BaseInputTemplate } from '~/components/rjsf/inputTemplate' +import { Callout } from '~/components/ui/alert' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '~/components/ui/card' +import { Label } from '~/components/ui/label' +import { Switch } from '~/components/ui/switch' interface Integration { - id: string; - name: string; - slug: string; - icon?: string | null; - description?: string | null; - order: number; + id: string + name: string + slug: string + icon?: string | null + description?: string | null + order: number } interface AdvancedStepProps { - integrations: Integration[]; + integrations: Integration[] } export function AdvancedStep({ integrations }: AdvancedStepProps) { - const { watch, setValue, resetField } = useFormContext(); - const [schemas, setSchemas] = useState>({}); - const [loading, setLoading] = useState>({}); + const { watch, setValue, resetField } = useFormContext() + const [schemas, setSchemas] = useState< + Record + >({}) + const [loading, setLoading] = useState>({}) + const { t } = useTranslation('newdevice') + const loadSchema = async (slug: string) => { + if (schemas[slug]) return - const loadSchema = async (slug: string) => { - if (schemas[slug]) return; + setLoading((prev) => ({ ...prev, [slug]: true })) - setLoading((prev) => ({ ...prev, [slug]: true })); + try { + const res = await fetch(`/api/integrations/schema/${slug}`) + if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`) - try { - const res = await fetch(`/api/integrations/schema/${slug}`); - if (!res.ok) throw new Error(`Failed to fetch ${slug} schema`); + const data = await res.json() + setSchemas((prev) => ({ ...prev, [slug]: data })) + } catch (err) { + console.error(`Failed to load ${slug} schema`, err) + } finally { + setLoading((prev) => ({ ...prev, [slug]: false })) + } + } - const data = await res.json(); - setSchemas((prev) => ({ ...prev, [slug]: data })); - } catch (err) { - console.error(`Failed to load ${slug} schema`, err); - } finally { - setLoading((prev) => ({ ...prev, [slug]: false })); - } - } + const handleToggle = (slug: string, checked: boolean) => { + setValue(`${slug}Enabled`, checked) - const handleToggle = (slug: string, checked: boolean) => { - setValue(`${slug}Enabled`, checked); + if (checked) { + void loadSchema(slug) + } else { + resetField(`${slug}Config`) + } + } - if (checked) { - void loadSchema(slug); - } else { - resetField(`${slug}Config`); - } - }; + return ( + <> + {integrations.map((intg) => { + const enabled = watch(`${intg.slug}Enabled`) ?? false + const config = watch(`${intg.slug}Config`) ?? {} + const isLoading = loading[intg.slug] ?? false + const schema = schemas[intg.slug] - return ( - <> - {integrations.map((intg) => { - const enabled = watch(`${intg.slug}Enabled`) ?? false; - const config = watch(`${intg.slug}Config`) ?? {}; - const isLoading = loading[intg.slug] ?? false; - const schema = schemas[intg.slug]; + return ( + + + + {intg.name} {t('configuration')} + + {intg.description && ( + {intg.description} + )} + - return ( - - - {intg.name} Configuration - {intg.description && ( - {intg.description} - )} - + +
+ + + handleToggle(intg.slug, checked) + } + /> +
- -
- - handleToggle(intg.slug, checked)} - /> -
+ {enabled && ( + <> + {isLoading && ( +

+ {t('loading')} {intg.name} {t('configuration')}... +

+ )} - {enabled && ( - <> - {isLoading && ( -

- Loading {intg.name} configuration… -

- )} - - {schema && ( -
{ - setValue(`${intg.slug}Config`, e.formData, { - shouldDirty: true, - shouldValidate: true, - }); - }} - onSubmit={() => {}} - > - <> -
- )} - - )} -
-
- ); - })} - - ); -} \ No newline at end of file + {schema && ( +
{ + setValue(`${intg.slug}Config`, e.formData, { + shouldDirty: true, + shouldValidate: true, + }) + }} + onSubmit={() => {}} + > + <> +
+ )} + + )} + +
+ ) + })} + {integrations.length === 0 && ( + {t('no_integrations_available')} + )} + + ) +} diff --git a/app/components/device/new/device-info.tsx b/app/components/device/new/device-info.tsx index 9a217d9c..4056939d 100644 --- a/app/components/device/new/device-info.tsx +++ b/app/components/device/new/device-info.tsx @@ -1,6 +1,7 @@ import { X } from 'lucide-react' import { useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Button } from '~/components/ui/button' import { Card, CardContent } from '~/components/ui/card' import { Label } from '~/components/ui/label' @@ -35,6 +36,7 @@ const connectionTypes = ['Wifi', 'Lora', 'Ethernet'] export function DeviceSelectionStep() { const { setValue, watch } = useFormContext() + const { t } = useTranslation('newdevice') // Watch the existing values from the form state const model = watch('model') @@ -154,7 +156,7 @@ export function DeviceSelectionStep() {

- Connection Type: + {t('connection_type')}

, label: 'Outdoor' }, - { value: 'indoor', icon: , label: 'Indoor' }, + { value: 'outdoor', icon: , label: t('outdoor') }, + { value: 'indoor', icon: , label: t('indoor') }, { value: 'mobile', icon: , - label: 'Mobile', + label: t('mobile'), }, { value: 'unknown', icon: , - label: 'Unknown', + label: t('unknown'), }, ] diff --git a/app/components/device/new/new-device-stepper.tsx b/app/components/device/new/new-device-stepper.tsx index 5ed8b0b9..391a8d62 100644 --- a/app/components/device/new/new-device-stepper.tsx +++ b/app/components/device/new/new-device-stepper.tsx @@ -4,7 +4,7 @@ import { Info, Slash } from 'lucide-react' import { useEffect, useState } from 'react' import { type FieldErrors, FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { Form, useLoaderData, useSubmit } from 'react-router' +import { Form, useLoaderData, useSubmit, useNavigation } from 'react-router' import { z } from 'zod' import { AdvancedStep } from './advanced-info' import { DeviceSelectionStep } from './device-info' @@ -159,6 +159,8 @@ export default function NewDeviceStepper() { const { toast } = useToast() const { t } = useTranslation('newdevice') const [isFirst, setIsFirst] = useState(false) + const navigation = useNavigation() + const isSubmitting = navigation.state === 'submitting' useEffect(() => { setIsFirst(stepper.isFirst) @@ -286,12 +288,12 @@ export default function NewDeviceStepper() { type="button" variant="secondary" onClick={stepper.prev} - disabled={isFirst} + disabled={isFirst || isSubmitting} > {t('back')} -
diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index c0e17167..478b987d 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -149,8 +149,7 @@ export function SensorSelectionStep() {

- {selectedSensors.length} {t('sensor')} - {selectedSensors.length !== 1 ? 's' : ''} {t('selected')} + {t('selectedSensors', { count: selectedSensors.length })}

{selectedSensors.length > 0 && (