create-domain-rules-modal.modal.tsx•6 kB
/**
 * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
 * http://github.com/fonoster/fonoster
 *
 * This file is part of Fonoster
 *
 * Licensed under the MIT License (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    https://opensource.org/licenses/MIT
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Modal } from "~/core/components/design-system/ui/modal/modal";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  Form,
  FormControl,
  FormField,
  FormItem
} from "~/core/components/design-system/forms";
import { FormRoot } from "~/core/components/design-system/forms/form-root";
import { Input } from "~/core/components/design-system/ui/input/input";
import { Select } from "~/core/components/design-system/ui/select/select";
import { z } from "zod";
import { Button } from "~/core/components/design-system/ui/button/button";
import { useCallback } from "react";
import { useNumbers } from "~/numbers/services/numbers.service";
/**
 * Zod validation schema for the Create/Edit Domain Rule form.
 *
 * Ensures:
 * - A human-friendly name is required.
 * - The type is either "allow" or "deny".
 */
export const schema = z.object({
  /**
   * The egress rule itself.
   *
   * Required — defines the condition for outbound calls (e.g., "0.0.0.0/0").
   */
  rule: z
    .string()
    .regex(/^[^:]+$/, "Rule cannot contain ':' character")
    .nonempty("Rule is required"),
  /**
   * Reference to the number enforcing this egress rule.
   *
   * Required — must point to an existing number in the system.
   */
  numberRef: z.string().nonempty("Number Reference is required")
});
/**
 * Type representing the validated data structure for the form.
 *
 * This type helps with strong typing in the form state, handlers, and submissions.
 */
export type Schema = z.infer<typeof schema>;
/**
 * Props interface for the CreateRuleModal component.
 *
 * @property {boolean} isOpen - Controls the visibility of the modal.
 * @property {() => void} onClose - Function to close the modal.
 * @property {(data: Schema) => void} onFormSubmit - Function triggered when the form is successfully submitted.
 */
export interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  onFormSubmit: (data: Schema) => void;
}
/**
 * CreateRuleModal component.
 *
 * Renders a modal dialog containing a form to create a new Domain rule.
 * Uses React Hook Form for form state management and Zod for validation.
 *
 * When the form is submitted:
 * - Calls the onFormSubmit callback with the validated data.
 * - Closes the modal and resets the form state.
 *
 * @param {ModalProps} props - The component props controlling visibility and form behavior.
 * @returns {JSX.Element} The rendered modal containing the rule creation form.
 */
export const CreateRuleModal = ({
  isOpen,
  onClose,
  onFormSubmit
}: ModalProps) => {
  const { data: numbers, isLoading: isNumbersLoading } = useNumbers();
  /**
   * Initializes React Hook Form with Zod validation and default values.
   */
  const form = useForm<Schema>({
    resolver: zodResolver(schema),
    defaultValues: {
      rule: "",
      numberRef: ""
    },
    mode: "onChange"
  });
  /**
   * Handles the form submission.
   *
   * Calls the parent-provided onFormSubmit function with the validated data,
   * closes the modal, and resets the form after a short delay to avoid visual flicker.
   *
   * @param {Schema} data - The validated form data.
   */
  const onSubmit = useCallback(
    (data: Schema) => {
      onFormSubmit(data);
      onClose(); // Close the modal
      setTimeout(() => {
        form.reset(); // Reset the form state after closing the modal
      }, 100); // Slight delay to ensure the modal is closed before resetting
    },
    [onFormSubmit, onClose, form]
  );
  return (
    <Modal open={isOpen} onClose={onClose} title="Create New Rule">
      <Form {...form}>
        <FormRoot onSubmit={form.handleSubmit(onSubmit)}>
          <FormField
            control={form.control}
            name="rule"
            render={({ field }) => (
              <FormItem>
                <FormControl>
                  <Input
                    type="text"
                    label="Rule"
                    placeholder="(e.g. .*)"
                    {...field}
                  />
                </FormControl>
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="numberRef"
            render={({ field }) => (
              <FormItem>
                <FormControl>
                  <Select
                    label="Select Outbound Number"
                    options={numbers.map(({ ref, name }) => ({
                      value: ref,
                      label: name
                    }))}
                    disabled={isNumbersLoading || numbers.length === 0}
                    placeholder={
                      isNumbersLoading
                        ? "Loading numbers..."
                        : numbers.length === 0
                          ? "No numbers found. Please create a Number first."
                          : ""
                    }
                    {...field}
                  />
                </FormControl>
              </FormItem>
            )}
          />
          {/* Submit Button */}
          <Button
            type="submit"
            disabled={!form.formState.isValid}
            isFullWidth
            size="small"
          >
            Save Rule
          </Button>
        </FormRoot>
      </Form>
    </Modal>
  );
};