diff --git a/package-lock.json b/package-lock.json index 94a84d3d179..d3057b01be0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,15 +17,16 @@ "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.2.0", "@hello-pangea/dnd": "^17.0.0", + "@hookform/resolvers": "^3.9.1", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/browser": "^8.42.0", @@ -56,6 +57,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", + "react-hook-form": "^7.54.1", "react-i18next": "^15.1.3", "react-infinite-scroll-component": "^6.1.0", "react-pdf": "^9.1.1", @@ -113,7 +115,7 @@ "vite-plugin-checker": "^0.8.0", "vite-plugin-pwa": "^0.20.5", "vite-plugin-static-copy": "^2.0.0", - "zod": "^3.23.8" + "zod": "^3.24.1" }, "engines": { "node": ">=22.11.0" @@ -2646,6 +2648,15 @@ "react-dom": "^18.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3687,6 +3698,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", @@ -3752,6 +3781,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -3891,11 +3938,35 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", - "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.0" + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3952,6 +4023,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz", @@ -3988,6 +4077,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -4106,6 +4213,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -4183,12 +4308,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4200,6 +4325,21 @@ } } }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", @@ -4268,6 +4408,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -15776,6 +15934,22 @@ "react": ">=16.4.1" } }, + "node_modules/react-hook-form": { + "version": "7.54.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.1.tgz", + "integrity": "sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-i18next": { "version": "15.1.3", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.3.tgz", @@ -20931,9 +21105,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index e5eaf3342c9..8edf7842d55 100644 --- a/package.json +++ b/package.json @@ -56,15 +56,16 @@ "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.2.0", "@hello-pangea/dnd": "^17.0.0", + "@hookform/resolvers": "^3.9.1", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/browser": "^8.42.0", @@ -95,6 +96,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.3.1", "react-google-recaptcha": "^3.1.0", + "react-hook-form": "^7.54.1", "react-i18next": "^15.1.3", "react-infinite-scroll-component": "^6.1.0", "react-pdf": "^9.1.1", @@ -152,7 +154,7 @@ "vite-plugin-checker": "^0.8.0", "vite-plugin-pwa": "^0.20.5", "vite-plugin-static-copy": "^2.0.0", - "zod": "^3.23.8" + "zod": "^3.24.1" }, "browserslist": { "production": [ diff --git a/src/components/Assets/AssetType/HL7Monitor.tsx b/src/components/Assets/AssetType/HL7Monitor.tsx index 269deacbf58..a03365a5859 100644 --- a/src/components/Assets/AssetType/HL7Monitor.tsx +++ b/src/components/Assets/AssetType/HL7Monitor.tsx @@ -100,20 +100,13 @@ const HL7Monitor = (props: HL7MonitorProps) => { label={

Middleware Hostname

- {resolvedMiddleware?.source != "asset" && ( -
- - - Middleware hostname sourced from asset{" "} - {resolvedMiddleware?.source} - -
- )}
} + message={ + resolvedMiddleware?.source != "asset" + ? `Middleware hostname sourced from asset ${resolvedMiddleware?.source}` + : undefined + } placeholder={resolvedMiddleware?.hostname} value={middlewareHostname} onChange={(e) => setMiddlewareHostname(e.value)} diff --git a/src/components/CameraFeed/ConfigureCamera.tsx b/src/components/CameraFeed/ConfigureCamera.tsx index 6a43740adb2..2c592ff1ba5 100644 --- a/src/components/CameraFeed/ConfigureCamera.tsx +++ b/src/components/CameraFeed/ConfigureCamera.tsx @@ -152,22 +152,16 @@ export default function ConfigureCamera(props: Props) { label={

{t("middleware_hostname")}

- {!!props.asset.resolved_middleware && - props.asset.resolved_middleware.source != "asset" && ( -
- - - {t("middleware_hostname_sourced_from", { - source: props.asset.resolved_middleware?.source, - })} - -
- )}
} + message={ + !!props.asset.resolved_middleware && + props.asset.resolved_middleware.source != "asset" + ? t("middleware_hostname_sourced_from", { + source: props.asset.resolved_middleware?.source, + }) + : undefined + } placeholder={ props.asset.resolved_middleware?.hostname ?? t("middleware_hostname_example") diff --git a/src/components/Facility/AddLocationForm.tsx b/src/components/Facility/AddLocationForm.tsx index 71ae8bf7dc1..52f4dbc182c 100644 --- a/src/components/Facility/AddLocationForm.tsx +++ b/src/components/Facility/AddLocationForm.tsx @@ -212,6 +212,7 @@ export const AddLocationForm = ({ facilityId, locationId }: Props) => { name="Location Middleware Address" type="text" label="Location Middleware Address" + message="Leave blank to apply facility middleware to assets" value={middlewareAddress} onChange={(e) => setMiddlewareAddress(e.value)} error={errors.middlewareAddress} diff --git a/src/components/Facility/FacilityConfigure.tsx b/src/components/Facility/FacilityConfigure.tsx index 4579c450584..d6bab357bfd 100644 --- a/src/components/Facility/FacilityConfigure.tsx +++ b/src/components/Facility/FacilityConfigure.tsx @@ -1,112 +1,74 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { navigate } from "raviger"; -import { useReducer, useState } from "react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; -import { Submit } from "@/components/Common/ButtonV2"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; -import TextFormField from "@/components/Form/FormFields/TextFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; import { PLUGIN_Component } from "@/PluginEngine"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; import request from "@/Utils/request/request"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; - -const initForm = { - name: "", - state: 0, - district: 0, - localbody: 0, - ward: 0, - middleware_address: "", -}; -const initialState = { - form: { ...initForm }, - errors: {}, -}; - -const FormReducer = (state = initialState, action: any) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - default: - return state; - } -}; +import { Submit } from "../Common/ButtonV2"; interface IProps { facilityId: string; } +const formSchema = z.object({ + middleware_address: z + .string() + .nonempty({ message: "Middleware Address is required" }) + .regex(/^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$/, { + message: "Invalid Middleware Address", + }), +}); + export const FacilityConfigure = (props: IProps) => { - const [state, dispatch] = useReducer(FormReducer, initialState); const { facilityId } = props; const [isLoading, setIsLoading] = useState(false); - const { loading } = useTanStackQueryInstead(routes.getPermittedFacility, { - pathParams: { id: facilityId }, - onResponse: (res) => { - if (res.data) { - const formData = { - name: res.data.name, - state: res.data.state, - district: res.data.district, - local_body: res.data.local_body, - ward: res.data.ward, - middleware_address: res.data.middleware_address, - }; - dispatch({ type: "set_form", form: formData }); - } - }, + const { isPending: loading, data } = useQuery({ + queryKey: [routes.getPermittedFacility.path, facilityId], + queryFn: query(routes.getPermittedFacility, { + pathParams: { id: facilityId }, + }), }); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - if (!state.form.middleware_address) { - dispatch({ - type: "set_error", - errors: { middleware_address: ["Middleware Address is required"] }, - }); - setIsLoading(false); - return; - } - if ( - state.form.middleware_address.match( - /^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$/, - ) === null - ) { - dispatch({ - type: "set_error", - errors: { - middleware_address: ["Invalid Middleware Address"], - }, - }); - setIsLoading(false); - return; - } + const onSubmit = async (values: z.infer) => { + if (!data) return; - const data = { - ...state.form, - middleware_address: state.form.middleware_address, + const formData = { + name: data.name, + state: data.state, + district: data.district, + local_body: data.local_body, + ward: data.ward, + middleware_address: values.middleware_address, }; + setIsLoading(true); + const { res, error } = await request(routes.partialUpdateFacility, { pathParams: { id: facilityId }, - body: data, + body: formData, }); setIsLoading(false); @@ -123,14 +85,18 @@ export const FacilityConfigure = (props: IProps) => { setIsLoading(false); }; - const handleChange = (e: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [e.name]: e.value }, - }); - }; + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { middleware_address: "" }, + }); + + useEffect(() => { + if (data && data.middleware_address) { + form.setValue("middleware_address", data.middleware_address); + } + }, [form, data]); - if (isLoading || loading) { + if (isLoading || !data || loading) { return ; } @@ -138,29 +104,42 @@ export const FacilityConfigure = (props: IProps) => {
-
-
-
- + + + ( + + + Facility Middleware Address + * + + + + + + This address will be applied to all assets when asset and + location middleware are Unspecified. + + + + )} + /> +
+
-
-
- -
- + +
void; } @@ -43,6 +48,15 @@ export default function LocationManagement({ facilityId }: Props) { id: "", }); + const { data } = useQuery({ + queryKey: [routes.getPermittedFacility.path, facilityId], + queryFn: query(routes.getPermittedFacility, { + pathParams: { id: facilityId }, + }), + }); + + const facilityMiddleware = data?.middleware_address; + const closeDeleteFailModal = () => { setShowDeleteFailModal({ ...showDeleteFailModal, open: false }); }; @@ -121,6 +135,7 @@ export default function LocationManagement({ facilityId }: Props) { {name}

-
-

- {location_type} -

-
+ + {location_type} +

{description || "-"}

-

+ Middleware Address: -

+ + {!middleware_address && facilityMiddleware && ( + + Fetched from facility + + )}

- {middleware_address || "-"} + {middleware_address || facilityMiddleware || "-"}

{ return ( - + ); }; @@ -48,6 +50,25 @@ export const FieldErrorText = (props: ErrorProps) => { ); }; +type MessageProps = { + message: string | undefined; + className?: string | undefined; +}; + +export const FieldMessageText = (props: MessageProps) => { + return ( + + {props.message} + + ); +}; + const FormField = ({ field, ...props @@ -73,6 +94,10 @@ const FormField = ({
{props.children}
+ ); }; diff --git a/src/components/Form/FormFields/Utils.ts b/src/components/Form/FormFields/Utils.ts index 1e88bcbd6a0..b017f7b92ee 100644 --- a/src/components/Form/FormFields/Utils.ts +++ b/src/components/Form/FormFields/Utils.ts @@ -20,9 +20,11 @@ export type FieldChangeEventHandler = (event: FieldChangeEvent) => void; export type FormFieldBaseProps = { label?: React.ReactNode; labelSuffix?: React.ReactNode; + message?: string; disabled?: boolean; className?: string; required?: boolean; + messageClassName?: string; labelClassName?: string; errorClassName?: string; name: string; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 00000000000..d642a211c7d --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,184 @@ +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import * as React from "react"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { cn } from "@/lib/utils"; + +import { Label } from "@/components/ui/label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +