PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / CategoryForm.tsx

CategoryForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.6, at resources/js/pages/CategoryForm.tsx

757 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Category Form Page
3 * Add/Edit Trip Category with parent/subcategory support
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2, Edit2, X, Eye } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { usePermissions } from "../hooks/usePermissions";
11 import { useToast } from "../components/ui/toast";
12 import { fetchSettings } from "../api/settings-api";
13 import { apiClient } from "../lib/api-client";
14 import { generateSlug } from "../lib/slug";
15 import { buildYatraSinglePreviewUrl } from "../lib/frontend-permalink-urls";
16 import { Button } from "../components/ui/button";
17 import { Input } from "../components/ui/input";
18 import { Select } from "../components/ui/select";
19 import { PageHeader } from "../components/common/PageHeader";
20 import {
21 Card,
22 CardContent,
23 CardHeader,
24 CardTitle,
25 } from "../components/ui/card";
26 import { ConditionalRender } from "../components/ui/conditional-render";
27 import { IconPicker, IconPickerValue } from "../components/ui/icon-picker";
28 import { RichTextEditor } from "../components/ui/rich-text-editor";
29
30 interface CategoryFormData {
31 name: string;
32 slug: string;
33 description: string;
34 icon: IconPickerValue | null;
35 parent_id: number | "";
36 status: string;
37 seo_title: string;
38 seo_description: string;
39 seo_keywords: string;
40 }
41
42 interface CategoryOption {
43 id: number;
44 name: string;
45 parent_id: number | null;
46 }
47
48 const CategoryForm: React.FC = () => {
49 const queryClient = useQueryClient();
50 const { can } = usePermissions();
51 const { showToast } = useToast();
52 const [formData, setFormData] = useState<CategoryFormData>({
53 name: "",
54 slug: "",
55 description: "",
56 icon: null,
57 parent_id: "",
58 status: "publish",
59 seo_title: "",
60 seo_description: "",
61 seo_keywords: "",
62 });
63 const [errors, setErrors] = useState<Record<string, string>>({});
64 const [isSubmitting, setIsSubmitting] = useState(false);
65 const [isSlugEditable, setIsSlugEditable] = useState(false);
66
67 const action = useMemo(() => {
68 const params = new URLSearchParams(window.location.search);
69 return params.get("action") || "create";
70 }, []);
71
72 const categoryId = useMemo(() => {
73 const params = new URLSearchParams(window.location.search);
74 return params.get("id") ? parseInt(params.get("id") || "0", 10) : null;
75 }, []);
76
77 const isEditMode = action === "edit" && categoryId !== null;
78
79 // Fetch category data if editing
80 const { data: categoryData, isLoading: isLoadingCategory } = useQuery({
81 queryKey: ["trip-category", categoryId],
82 queryFn: async () => {
83 if (!categoryId) return null;
84 try {
85 const response = await apiClient.get(`/trip-categories/${categoryId}`);
86 return response;
87 } catch (error: any) {
88 showToast(
89 error?.message || __("Failed to load category", "yatra"),
90 "error",
91 );
92 throw error;
93 }
94 },
95 enabled: isEditMode && can("yatra_view_trips"),
96 });
97
98 // Fetch parent category options (top-level categories)
99 const { data: parentCategoriesData } = useQuery({
100 queryKey: ["trip-categories", "parent-options"],
101 queryFn: async () => {
102 try {
103 const response = await apiClient.get("/trip-categories", {
104 params: {
105 per_page: 100,
106 parent_id: null,
107 hierarchical: false,
108 orderby: "name",
109 order: "ASC",
110 },
111 });
112 const payload = response?.data || response;
113 if (!payload) {
114 return [];
115 }
116 if (Array.isArray(payload)) {
117 return payload;
118 }
119 return payload.data || [];
120 } catch (error) {
121 return [];
122 }
123 },
124 enabled: can("yatra_view_trips"),
125 });
126
127 // Fetch settings for permalink handling
128 const { data: settings } = useQuery({
129 queryKey: ["settings"],
130 queryFn: async () => {
131 try {
132 const response = await fetchSettings();
133 return response?.data || response;
134 } catch (error) {
135 return {};
136 }
137 },
138 });
139
140 const parentOptions: CategoryOption[] = useMemo(() => {
141 if (!parentCategoriesData || !Array.isArray(parentCategoriesData)) {
142 return [];
143 }
144 return parentCategoriesData.filter((cat: any) => {
145 if (!cat || typeof cat !== "object") return false;
146 // Only allow top-level categories (no parent)
147 if (cat.parent_id) return false;
148 // Prevent selecting itself as parent when editing
149 if (isEditMode && categoryId && cat.id === categoryId) return false;
150 return true;
151 });
152 }, [parentCategoriesData, isEditMode, categoryId]);
153
154 // Load category data into form when editing
155 useEffect(() => {
156 if (categoryData && isEditMode) {
157 setFormData({
158 name: categoryData.name || "",
159 slug: categoryData.slug || "",
160 description: categoryData.description || "",
161 icon: (categoryData.icon as IconPickerValue) || null,
162 parent_id: categoryData.parent_id ?? "",
163 status: categoryData.status || "publish",
164 seo_title: categoryData.metadata?.seo_title || "",
165 seo_description: categoryData.metadata?.seo_description || "",
166 seo_keywords: categoryData.metadata?.seo_keywords || "",
167 });
168 }
169 }, [categoryData, isEditMode]);
170
171 const handleNameChange = (value: string) => {
172 // Only auto-generate slug on create; in edit mode, keep existing slug
173 if (!isEditMode && !isSlugEditable) {
174 const newSlug = generateSlug(value);
175 setFormData((prev) => ({
176 ...prev,
177 name: value,
178 slug: newSlug,
179 }));
180 } else {
181 setFormData((prev) => ({ ...prev, name: value }));
182 }
183 if (errors.name) {
184 setErrors((prev) => ({ ...prev, name: "" }));
185 }
186 };
187
188 const handleSlugChange = (value: string) => {
189 if (isSlugEditable) {
190 setFormData((prev) => ({ ...prev, slug: value }));
191 if (errors.slug) {
192 setErrors((prev) => ({ ...prev, slug: "" }));
193 }
194 }
195 };
196
197 const handleToggleSlugEdit = () => {
198 if (isSlugEditable) {
199 const newSlug = generateSlug(formData.name);
200 setFormData((prev) => ({ ...prev, slug: newSlug }));
201 }
202 setIsSlugEditable(!isSlugEditable);
203 };
204
205 const handleFieldChange = (field: keyof CategoryFormData, value: any) => {
206 setFormData((prev) => ({ ...prev, [field]: value }));
207 if (errors[field]) {
208 setErrors((prev) => ({ ...prev, [field]: "" }));
209 }
210 };
211
212 const validateForm = (): boolean => {
213 const newErrors: Record<string, string> = {};
214
215 if (!formData.name.trim()) {
216 newErrors.name = __("Name is required", "yatra");
217 }
218
219 if (!formData.slug.trim()) {
220 newErrors.slug = __("Slug is required", "yatra");
221 } else if (!/^[a-z0-9-]+$/.test(formData.slug)) {
222 newErrors.slug = __(
223 "Slug can only contain lowercase letters, numbers, and hyphens",
224 "yatra",
225 );
226 }
227
228 if (
229 formData.parent_id &&
230 categoryId &&
231 Number(formData.parent_id) === categoryId
232 ) {
233 newErrors.parent_id = __("Category cannot be its own parent", "yatra");
234 }
235
236 setErrors(newErrors);
237 return Object.keys(newErrors).length === 0;
238 };
239
240 const saveMutation = useMutation({
241 mutationFn: async (data: CategoryFormData) => {
242 const payload: any = {
243 name: data.name.trim(),
244 slug: data.slug.trim(),
245 description: data.description.trim(),
246 icon: data.icon,
247 parent_id: data.parent_id === "" ? null : Number(data.parent_id),
248 status: data.status,
249 seo_title: data.seo_title.trim(),
250 seo_description: data.seo_description.trim(),
251 seo_keywords: data.seo_keywords.trim(),
252 };
253
254 if (isEditMode && isSlugEditable) {
255 payload.preserve_slug = true;
256 }
257
258 if (isEditMode && categoryId) {
259 return await apiClient.put(`/trip-categories/${categoryId}`, payload);
260 }
261 return await apiClient.post("/trip-categories", payload);
262 },
263 onSuccess: (response) => {
264 queryClient.invalidateQueries({ queryKey: ["trip-categories"] });
265 queryClient.invalidateQueries({
266 queryKey: ["trip-category", categoryId],
267 });
268 showToast(
269 isEditMode
270 ? __("Category updated successfully", "yatra")
271 : __("Category created successfully", "yatra"),
272 "success",
273 );
274 setIsSubmitting(false);
275
276 if (!isEditMode) {
277 const newId = response?.id;
278 if (newId) {
279 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories&action=edit&id=${newId}`;
280 } else {
281 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`;
282 }
283 }
284 },
285 onError: (error: any) => {
286 const errorMessage =
287 error?.message ||
288 __("An error occurred while saving the category", "yatra");
289 showToast(errorMessage, "error");
290 setIsSubmitting(false);
291 },
292 });
293
294 const handleSubmit = (e: React.FormEvent) => {
295 e.preventDefault();
296
297 if (!validateForm()) {
298 showToast(__("Please fix the form errors", "yatra"), "warning");
299 return;
300 }
301
302 setIsSubmitting(true);
303 setErrors({});
304 saveMutation.mutate(formData);
305 };
306
307 const handleCancel = () => {
308 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`;
309 };
310
311 if (isEditMode && isLoadingCategory) {
312 return (
313 <div className="space-y-3">
314 {/* Header Skeleton */}
315 <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
316 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-2 animate-pulse"></div>
317 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2 animate-pulse"></div>
318 </div>
319
320 {/* Form Skeleton */}
321 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
322 {/* Main Fields */}
323 <div className="lg:col-span-2 space-y-3">
324 <Card>
325 <CardContent className="p-6 space-y-4">
326 {/* Name field */}
327 <div>
328 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2 animate-pulse"></div>
329 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
330 </div>
331 {/* Slug field */}
332 <div>
333 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2 animate-pulse"></div>
334 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
335 </div>
336 {/* Description field */}
337 <div>
338 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
339 <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
340 </div>
341 {/* Icon field */}
342 <div>
343 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2 animate-pulse"></div>
344 <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
345 </div>
346 </CardContent>
347 </Card>
348 </div>
349
350 {/* Sidebar */}
351 <div className="space-y-3">
352 <Card>
353 <CardContent className="p-6 space-y-4">
354 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
355 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
356 </CardContent>
357 </Card>
358 <Card>
359 <CardContent className="p-6 space-y-4">
360 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2 animate-pulse"></div>
361 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
362 </CardContent>
363 </Card>
364 </div>
365 </div>
366 </div>
367 );
368 }
369
370 return (
371 <div className="space-y-3">
372 <PageHeader
373 title={
374 isEditMode
375 ? __("Edit Category", "yatra")
376 : __("Add New Category", "yatra")
377 }
378 description={
379 isEditMode
380 ? __("Update category information", "yatra")
381 : __("Create a new trip category", "yatra")
382 }
383 actions={
384 <div className="flex items-center gap-2">
385 <Button
386 variant="outline"
387 onClick={handleCancel}
388 className="flex items-center gap-2"
389 >
390 <ArrowLeft className="w-4 h-4" />
391 {__("Back", "yatra")}
392 </Button>
393 {formData.slug && (
394 <Button
395 variant="outline"
396 onClick={() => {
397 const targetUrl = buildYatraSinglePreviewUrl({
398 entity: "category",
399 slug: formData.slug || "",
400 bases: settings as Record<string, unknown> | null,
401 });
402 window.open(targetUrl, "_blank", "noopener,noreferrer");
403 }}
404 className="flex items-center gap-2"
405 title={__("Preview category in new tab", "yatra")}
406 >
407 <Eye className="w-4 h-4" />
408 {__("Preview", "yatra")}
409 </Button>
410 )}
411 </div>
412 }
413 />
414
415 <ConditionalRender capability="yatra_edit_trips">
416 <form onSubmit={handleSubmit}>
417 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
418 <div className="lg:col-span-2 space-y-3">
419 <Card>
420 <CardHeader className="pb-2">
421 <CardTitle className="text-base">
422 {__("Basic Information", "yatra")}
423 </CardTitle>
424 </CardHeader>
425 <CardContent className="space-y-3">
426 <div>
427 <label
428 htmlFor="name"
429 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
430 >
431 {__("Name", "yatra")}{" "}
432 <span className="text-red-500">*</span>
433 </label>
434 <Input
435 id="name"
436 type="text"
437 value={formData.name}
438 onChange={(e) => handleNameChange(e.target.value)}
439 placeholder={__("Enter category name", "yatra")}
440 className={errors.name ? "border-red-500" : ""}
441 required
442 />
443 {errors.name && (
444 <p className="mt-1 text-sm text-red-500">{errors.name}</p>
445 )}
446 </div>
447
448 <div>
449 <label
450 htmlFor="slug"
451 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
452 >
453 {__("Slug", "yatra")}{" "}
454 <span className="text-red-500">*</span>
455 </label>
456 <div className="relative">
457 <Input
458 id="slug"
459 type="text"
460 value={formData.slug}
461 onChange={(e) => handleSlugChange(e.target.value)}
462 placeholder={__("category-slug", "yatra")}
463 className={`pr-10 ${errors.slug ? "border-red-500" : ""} ${!isSlugEditable ? "bg-gray-50 dark:bg-gray-800 cursor-not-allowed" : ""}`}
464 disabled={!isSlugEditable}
465 required
466 />
467 <button
468 type="button"
469 onClick={handleToggleSlugEdit}
470 className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors rounded"
471 aria-label={
472 isSlugEditable
473 ? __("Cancel editing slug", "yatra")
474 : __("Edit slug", "yatra")
475 }
476 >
477 {isSlugEditable ? (
478 <X className="w-4 h-4" />
479 ) : (
480 <Edit2 className="w-4 h-4" />
481 )}
482 </button>
483 </div>
484 {errors.slug && (
485 <p className="mt-1 text-sm text-red-500">{errors.slug}</p>
486 )}
487 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
488 {isSlugEditable
489 ? __(
490 "Manually editing slug. Click X to cancel and regenerate from name.",
491 "yatra",
492 )
493 : __(
494 "Auto-generated from name. Click edit icon to customize.",
495 "yatra",
496 )}
497 </p>
498 </div>
499
500 {/* Description */}
501 <div className="mt-4">
502 <RichTextEditor
503 label={__("Description", "yatra")}
504 value={formData.description || ""}
505 onChange={(value) =>
506 handleFieldChange("description", value)
507 }
508 placeholder={__("Enter category description...", "yatra")}
509 helperText={__(
510 "Describe what this category is for and what types of trips it contains.",
511 "yatra",
512 )}
513 minHeight={120}
514 maxHeight={360}
515 />
516 </div>
517 </CardContent>
518 </Card>
519 </div>
520
521 <div className="space-y-3">
522 <Card>
523 <CardHeader className="pb-2">
524 <CardTitle className="text-base">
525 {__("Hierarchy", "yatra")}
526 </CardTitle>
527 </CardHeader>
528 <CardContent className="space-y-3">
529 <div>
530 <label
531 htmlFor="parent_id"
532 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
533 >
534 {__("Parent Category", "yatra")}
535 </label>
536 <Select
537 id="parent_id"
538 value={
539 formData.parent_id === ""
540 ? ""
541 : String(formData.parent_id)
542 }
543 onChange={(e) =>
544 handleFieldChange(
545 "parent_id",
546 e.target.value === "" ? "" : Number(e.target.value),
547 )
548 }
549 className="w-full h-10"
550 >
551 <option value="">
552 {__("None (Top Level)", "yatra")}
553 </option>
554 {parentOptions.map((cat) => (
555 <option key={cat.id} value={cat.id}>
556 {cat.name}
557 </option>
558 ))}
559 </Select>
560 {errors.parent_id && (
561 <p className="mt-1 text-sm text-red-500">
562 {errors.parent_id}
563 </p>
564 )}
565 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
566 {__(
567 "Assign a parent category to create subcategories.",
568 "yatra",
569 )}
570 </p>
571 </div>
572 </CardContent>
573 </Card>
574
575 <Card>
576 <CardHeader className="pb-2">
577 <CardTitle className="text-base">
578 {__("Status", "yatra")}
579 </CardTitle>
580 </CardHeader>
581 <CardContent>
582 <Select
583 id="status"
584 value={formData.status}
585 onChange={(e) =>
586 handleFieldChange("status", e.target.value)
587 }
588 className="w-full h-10"
589 >
590 <option value="draft">{__("Draft", "yatra")}</option>
591 <option value="publish">{__("Publish", "yatra")}</option>
592 <option value="trash">{__("Trash", "yatra")}</option>
593 </Select>
594 </CardContent>
595 </Card>
596
597 {/* SEO Settings */}
598 <Card>
599 <CardHeader className="pb-2">
600 <CardTitle className="text-base">
601 {__("SEO Settings", "yatra")}
602 </CardTitle>
603 </CardHeader>
604 <CardContent className="space-y-3">
605 <div>
606 <label
607 htmlFor="seo_title"
608 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
609 >
610 {__("Meta Title", "yatra")}
611 </label>
612 <Input
613 id="seo_title"
614 type="text"
615 value={formData.seo_title}
616 onChange={(e) =>
617 handleFieldChange("seo_title", e.target.value)
618 }
619 placeholder={__(
620 "e.g., {name} Trip Categories | Your Travel Agency",
621 "yatra",
622 )}
623 className="w-full"
624 />
625 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
626 {__(
627 "Custom title for search engines. Use {name} as placeholder.",
628 "yatra",
629 )}
630 </p>
631 </div>
632
633 <div>
634 <label
635 htmlFor="seo_description"
636 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
637 >
638 {__("Meta Description", "yatra")}
639 </label>
640 <textarea
641 id="seo_description"
642 value={formData.seo_description}
643 onChange={(e) =>
644 handleFieldChange("seo_description", e.target.value)
645 }
646 placeholder={__(
647 "e.g., Browse {name} trip categories and find your perfect adventure. Book now!",
648 "yatra",
649 )}
650 rows={3}
651 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
652 />
653 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
654 {__(
655 "Description for search engines (150-160 characters). Use {name} as placeholder.",
656 "yatra",
657 )}
658 </p>
659 </div>
660
661 <div>
662 <label
663 htmlFor="seo_keywords"
664 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
665 >
666 {__("Meta Keywords", "yatra")}
667 </label>
668 <Input
669 id="seo_keywords"
670 type="text"
671 value={formData.seo_keywords}
672 onChange={(e) =>
673 handleFieldChange("seo_keywords", e.target.value)
674 }
675 placeholder={__(
676 "e.g., categories, trips, {name}, travel, adventure",
677 "yatra",
678 )}
679 className="w-full"
680 />
681 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
682 {__(
683 "Comma-separated keywords. Use {name} as placeholder.",
684 "yatra",
685 )}
686 </p>
687 </div>
688 </CardContent>
689 </Card>
690
691 <Card>
692 <CardHeader className="pb-2">
693 <CardTitle className="text-base">
694 {__("Category Icon or Image", "yatra")}
695 </CardTitle>
696 </CardHeader>
697 <CardContent>
698 <IconPicker
699 value={formData.icon}
700 onChange={(value) => handleFieldChange("icon", value)}
701 label={__("Select Icon or Upload Image", "yatra")}
702 helpText={__(
703 "Choose a library icon or upload a custom image for this category.",
704 "yatra",
705 )}
706 allowImageUpload={true}
707 allowIconSelection={true}
708 size="md"
709 />
710 </CardContent>
711 </Card>
712
713 <Card>
714 <CardContent className="p-3">
715 <div className="space-y-2">
716 <div className="flex gap-2">
717 <Button
718 type="submit"
719 disabled={isSubmitting}
720 className="flex-1 flex items-center justify-center gap-2"
721 >
722 {isSubmitting ? (
723 <>
724 <Loader2 className="w-4 h-4 animate-spin" />
725 {__("Saving...", "yatra")}
726 </>
727 ) : (
728 <>
729 <Save className="w-4 h-4" />
730 {isEditMode
731 ? __("Update Category", "yatra")
732 : __("Create Category", "yatra")}
733 </>
734 )}
735 </Button>
736 <Button
737 type="button"
738 variant="outline"
739 onClick={handleCancel}
740 disabled={isSubmitting}
741 >
742 {__("Cancel", "yatra")}
743 </Button>
744 </div>
745 </div>
746 </CardContent>
747 </Card>
748 </div>
749 </div>
750 </form>
751 </ConditionalRender>
752 </div>
753 );
754 };
755
756 export default CategoryForm;
757