diff --git a/web2/api/api-zod.ts b/web2/api/api-zod.ts index 215fd53f..f6de7ae2 100644 --- a/web2/api/api-zod.ts +++ b/web2/api/api-zod.ts @@ -217,7 +217,7 @@ const Settings = z .describe( "Topic pattern individual intercepted commands will be sent to. More detail on the format in README." ), - mqtt_update_state_pattern: z + mqtt_state_topic_pattern: z .string() .describe( "Topic pattern device state will be sent to. More detail on the format in README." diff --git a/web2/package-lock.json b/web2/package-lock.json index 9077daa1..96ace8de 100644 --- a/web2/package-lock.json +++ b/web2/package-lock.json @@ -33,6 +33,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "esbuild-plugin-postcss": "^0.2.1", + "lodash": "^4.17.21", "lucide-react": "^0.447.0", "react-hook-form": "^7.53.0", "react-router": "^6.26.2", @@ -4661,6 +4662,12 @@ "node": ">= 12.13.0" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", diff --git a/web2/package.json b/web2/package.json index 42b8d815..1b7637a8 100644 --- a/web2/package.json +++ b/web2/package.json @@ -54,6 +54,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "esbuild-plugin-postcss": "^0.2.1", + "lodash": "^4.17.21", "lucide-react": "^0.447.0", "react-hook-form": "^7.53.0", "react-router": "^6.26.2", diff --git a/web2/src/pages/settings/form-components.tsx b/web2/src/pages/settings/form-components.tsx index 880285cf..0a4cb02d 100644 --- a/web2/src/pages/settings/form-components.tsx +++ b/web2/src/pages/settings/form-components.tsx @@ -258,7 +258,7 @@ export const FormFields: React.FC<{ }; export const FieldSection: React.FC<{ - title: string; + title?: string; description?: string; fields: (keyof typeof schemas.Settings.shape)[]; fieldNames?: Partial>; @@ -266,9 +266,9 @@ export const FieldSection: React.FC<{ children?: React.ReactNode; }> = ({ title, description, fields, fieldNames, fieldTypes, children }) => (
-

{title}

+ {title &&

{title}

} {description &&

{description}

} -
+ {title &&
} ; +type SettingsKey = keyof typeof schemas.Settings.shape; + +const TOPIC_PRESETS: Record> = { + Default: { + mqtt_topic_pattern: "milight/commands/:device_id/:device_type/:group_id", + mqtt_update_topic_pattern: + "milight/updates/:device_id/:device_type/:group_id", + mqtt_state_topic_pattern: "milight/state/:device_id/:device_type/:group_id", + mqtt_client_status_topic: "milight/client_status", + simple_mqtt_client_status: true, + }, + Custom: {}, +}; + +const TopicFieldsSelector: React.FC<{}> = ({}) => { + const form = useFormContext(); + const [selectedPreset, setSelectedPreset] = useState("Custom"); + + useEffect(() => { + const currentFields = form.getValues(); + for (const [preset, fields] of Object.entries(TOPIC_PRESETS)) { + if (isPresetEqual(currentFields, fields)) { + setSelectedPreset(preset); + break; + } + } + }, []); + + const isPresetEqual = (a: FieldValues, b: Partial) => + Object.keys(b).every((key) => a[key] === b[key]); + + const handlePresetChange = (value: string) => { + setSelectedPreset(value); + if (value !== "Custom") { + const topicFields = TOPIC_PRESETS[value]; + for (const [key, value] of Object.entries(topicFields)) { + form.setValue(key as SettingsKey, value as string, { + shouldDirty: true, + shouldValidate: true, + shouldTouch: true, + }); + } + form.handleSubmit((data) => { + console.log(data); + })(); + } + }; + + return ( +
+ ( + + Preset + + ({ + label: preset, + value: preset, + }))} + value={{ label: selectedPreset, value: selectedPreset }} + onChange={(option) => + handlePresetChange(option?.value as string) + } + /> + + + Customize the MQTT topic patterns. Use the "Default" preset for + standard configurations. + + + )} + /> + + {selectedPreset !== "Custom" && ( +
+

Preview:

+
    + {Object.entries(TOPIC_PRESETS[selectedPreset]).map( + ([key, value]) => ( +
  • +
    + + {key + .replace(/_/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase())} + : + +
    +
    + + {(value as any).toString()} + +
    +
    + {schemas.Settings.shape[key as SettingsKey].description} +
    +
  • + ) + )} +
+
+ )} + + {selectedPreset === "Custom" && ( + + )} +
+ ); +}; export const MQTTSettings: React.FC> = () => ( + + + - ); diff --git a/web2/src/pages/settings/settings-index.tsx b/web2/src/pages/settings/settings-index.tsx index 7861fe18..f97c5ebc 100644 --- a/web2/src/pages/settings/settings-index.tsx +++ b/web2/src/pages/settings/settings-index.tsx @@ -18,6 +18,7 @@ import { SystemSettings } from "./section-system"; import { RadioSettings } from "./section-radio"; import { StateSettings } from "./section-state"; import { UDPSettings } from "./section-udp"; +import { debounce } from "lodash"; type Settings = z.infer; @@ -40,26 +41,27 @@ export default function SettingsPage() { mode: "onBlur", }); - const onSubmit = () => { - const update: Partial = {}; + const debouncedOnSubmit = useCallback( + debounce(() => { + const update: Partial = {}; - for (const field in form.formState.dirtyFields) { - update[field as keyof Settings] = form.getValues(field); - } + for (const field in form.formState.dirtyFields) { + update[field as keyof Settings] = form.getValues(field); + } - if (Object.keys(update).length > 0) { - api.putSettings(update).then(() => { - form.reset(form.getValues()); - }); - } - }; + if (Object.keys(update).length > 0) { + api.putSettings(update).then(() => { + form.reset(form.getValues()); + }); + } + }, 300), // Adjust the debounce delay as needed + [form] + ); const handleFieldChange = useCallback((name: keyof Settings, value: any) => { console.log(`Field ${name} changed to:`, value); - // Here you would typically send the individual field update to your API - // api.updateSetting(name, value); - onSubmit(); - }, []); + debouncedOnSubmit(); + }, [debouncedOnSubmit]); useEffect(() => { const subscription = form.watch((value, { name, type }) => { @@ -70,18 +72,21 @@ export default function SettingsPage() { const fieldKey: SettingsKey = name as SettingsKey; const fieldType = extractSchemaType(schemas.Settings.shape[fieldKey]); - console.log("watch update", type, fieldKey, fieldType, value[fieldKey]); + handleFieldChange(fieldKey, value[fieldKey]); - if ( - name && - ((type === "change" && - (fieldType instanceof z.ZodEnum || - fieldType instanceof z.ZodBoolean || - fieldType instanceof z.ZodArray)) || - name == "gateway_configs") // this is a hack but I CBA to figure out why type is undefined - ) { - handleFieldChange(fieldKey, value[fieldKey]); - } + // console.log("watch update", type, fieldKey, fieldType, value[fieldKey]); + + // if ( + // name && + // ((type === "change" && + // (fieldType instanceof z.ZodEnum || + // fieldType instanceof z.ZodBoolean || + // fieldType instanceof z.ZodArray)) || + // name == "gateway_configs" || + // name == "group_state_fields") // this is a hack but I CBA to figure out why type is undefined for controlled fields + // ) { + // handleFieldChange(fieldKey, value[fieldKey]); + // } }); return () => subscription.unsubscribe(); }, [form, handleFieldChange]); @@ -107,10 +112,10 @@ export default function SettingsPage() { ) : (
{ e.preventDefault(); - form.handleSubmit(onSubmit)(); + form.handleSubmit(debouncedOnSubmit)(); }} >