fix(product-options): update API routes and enhance product option models

This commit is contained in:
hosseintaromi 2025-07-28 09:11:08 +03:30
parent 58ecd2e94a
commit b3c3a8afd0
9 changed files with 5102 additions and 5401 deletions

View File

@ -1,4 +1,5 @@
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { MouseEvent } from 'react';
interface ButtonProps { interface ButtonProps {
children: any; children: any;
@ -6,7 +7,7 @@ interface ButtonProps {
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
onClick?: () => void; onClick?: (e?: MouseEvent<HTMLButtonElement>) => void;
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
className?: string; className?: string;
} }

View File

@ -120,7 +120,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
setFiles(prev => [...prev, newFile]); setFiles(prev => [...prev, newFile]);
try { try {
// شبیه‌سازی پروگرس
const progressInterval = setInterval(() => { const progressInterval = setInterval(() => {
setFiles(prev => prev.map(f => setFiles(prev => prev.map(f =>
f.id === fileId && f.progress < 90 f.id === fileId && f.progress < 90
@ -145,13 +144,13 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
: f : f
)); ));
} }
}, [onUpload, files.length, maxFiles, maxFileSize, acceptedTypes]); }, [onUpload, maxFiles, maxFileSize, acceptedTypes]);
const handleFileSelect = (selectedFiles: FileList) => { const handleFileSelect = useCallback((selectedFiles: FileList) => {
Array.from(selectedFiles).forEach(file => { Array.from(selectedFiles).forEach(file => {
handleFileUpload(file); handleFileUpload(file);
}); });
}; }, [handleFileUpload]);
const handleDrop = useCallback((e: React.DragEvent) => { const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@ -160,7 +159,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
const droppedFiles = e.dataTransfer.files; const droppedFiles = e.dataTransfer.files;
handleFileSelect(droppedFiles); handleFileSelect(droppedFiles);
}, [disabled, handleFileUpload]); }, [disabled, handleFileSelect]);
const handleDragOver = useCallback((e: React.DragEvent) => { const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@ -307,7 +306,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e?.stopPropagation();
handleRemove(file.id); handleRemove(file.id);
}} }}
className="p-1 h-8 w-8" className="p-1 h-8 w-8"

View File

@ -39,8 +39,8 @@ export const API_ROUTES = {
// Product Options APIs // Product Options APIs
GET_PRODUCT_OPTIONS: "api/v1/product-options", GET_PRODUCT_OPTIONS: "api/v1/product-options",
GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
CREATE_PRODUCT_OPTION: "api/v1/products/options", CREATE_PRODUCT_OPTION: "api/v1/product-options",
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/products/options/${id}`, UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
// Categories APIs // Categories APIs

View File

@ -1,24 +1,21 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { import { toast } from "react-hot-toast";
httpPostRequest, import { APIUrlGenerator } from "@/utils/baseHttpService";
httpDeleteRequest,
APIUrlGenerator,
} from "@/utils/baseHttpService";
import { API_ROUTES } from "@/constant/routes"; import { API_ROUTES } from "@/constant/routes";
import toast from "react-hot-toast"; import { httpPostRequest, httpDeleteRequest } from "@/utils/baseHttpService";
interface UploadFileResponse {
id: string;
name: string;
size: number;
type: string;
url: string;
serve_key: string;
created_at: string;
}
interface UploadResponse { interface UploadResponse {
file: UploadFileResponse; file: {
id: number;
url: string;
name: string;
original_name: string;
serve_key: string;
size: number;
mime_type: string;
created_at: string;
updated_at: string;
};
} }
export const useFileUpload = () => { export const useFileUpload = () => {
@ -26,12 +23,18 @@ export const useFileUpload = () => {
mutationFn: async (file: File): Promise<{ id: string; url: string }> => { mutationFn: async (file: File): Promise<{ id: string; url: string }> => {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("name", "uploaded-file");
console.log("Uploading file:", file.name); console.log("Uploading file:", file.name);
const response = await httpPostRequest<UploadResponse>( const response = await httpPostRequest<UploadResponse>(
APIUrlGenerator(API_ROUTES.UPLOAD_FILE), APIUrlGenerator(API_ROUTES.UPLOAD_FILE),
formData formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
); );
console.log("Upload response:", response); console.log("Upload response:", response);
@ -41,7 +44,7 @@ export const useFileUpload = () => {
} }
return { return {
id: response.data.file.id, id: response.data.file.id.toString(),
url: response.data.file.url, url: response.data.file.url,
}; };
}, },

View File

@ -1,14 +1,31 @@
export interface Maintenance {
content: string;
description: string;
image: string;
title: string;
}
export interface Option {
description: string;
meta_title: string;
title: string;
}
export interface ProductOption { export interface ProductOption {
id: number; id: number;
name: string; title: string;
values: string[]; description: string;
maintenance: Maintenance;
options: Option[];
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export interface ProductOptionFormData { export interface ProductOptionFormData {
name: string; title: string;
values: string[]; description: string;
maintenance: Maintenance;
options: Option[];
} }
export interface ProductOptionFilters { export interface ProductOptionFilters {
@ -18,14 +35,18 @@ export interface ProductOptionFilters {
} }
export interface CreateProductOptionRequest { export interface CreateProductOptionRequest {
name: string; title: string;
values: string[]; description: string;
maintenance: Maintenance;
options: Option[];
} }
export interface UpdateProductOptionRequest { export interface UpdateProductOptionRequest {
id: number; id: number;
name: string; title: string;
values: string[]; description: string;
maintenance: Maintenance;
options: Option[];
} }
export interface ProductOptionsResponse { export interface ProductOptionsResponse {

View File

@ -1,19 +1,33 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
import { useProductOption, useCreateProductOption, useUpdateProductOption } from '../core/_hooks'; import { useProductOption, useCreateProductOption, useUpdateProductOption } from '../core/_hooks';
import { ProductOptionFormData } from '../core/_models'; import { ProductOptionFormData, Maintenance, Option } from '../core/_models';
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { TagInput } from "@/components/ui/TagInput"; import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react";
import { ArrowRight, Settings } from "lucide-react";
const maintenanceSchema = yup.object({
title: yup.string().required('عنوان نگهداری الزامی است'),
description: yup.string().required('توضیحات نگهداری الزامی است'),
content: yup.string().required('محتوای نگهداری الزامی است'),
image: yup.string().required('تصویر نگهداری الزامی است'),
});
const optionSchema = yup.object({
title: yup.string().required('عنوان گزینه الزامی است'),
description: yup.string().required('توضیحات گزینه الزامی است'),
meta_title: yup.string().required('متا تایتل الزامی است'),
});
const productOptionSchema = yup.object({ const productOptionSchema = yup.object({
name: yup.string().required('نام گزینه الزامی است').min(2, 'نام گزینه باید حداقل 2 کاراکتر باشد'), title: yup.string().required('عنوان الزامی است').min(2, 'عنوان باید حداقل 2 کاراکتر باشد'),
values: yup.array().of(yup.string()).min(1, 'حداقل یک مقدار باید وارد شود').required('مقادیر الزامی است'), description: yup.string().required('توضیحات الزامی است'),
maintenance: maintenanceSchema.required('اطلاعات نگهداری الزامی است'),
options: yup.array().of(optionSchema).min(1, 'حداقل یک گزینه باید وارد شود').required('گزینه‌ها الزامی است'),
}); });
const ProductOptionFormPage = () => { const ProductOptionFormPage = () => {
@ -30,7 +44,7 @@ const ProductOptionFormPage = () => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isValid, isDirty }, formState: { errors, isValid },
setValue, setValue,
watch, watch,
control control
@ -38,17 +52,31 @@ const ProductOptionFormPage = () => {
resolver: yupResolver(productOptionSchema) as any, resolver: yupResolver(productOptionSchema) as any,
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
name: '', title: '',
values: [] description: '',
maintenance: {
title: '',
description: '',
content: '',
image: ''
},
options: []
} }
}); });
const { fields: optionFields, append: appendOption, remove: removeOption } = useFieldArray({
control,
name: "options"
});
const formValues = watch(); const formValues = watch();
useEffect(() => { useEffect(() => {
if (isEdit && productOption) { if (isEdit && productOption) {
setValue('name', productOption.name, { shouldValidate: true }); setValue('title', productOption.title, { shouldValidate: true });
setValue('values', productOption.values, { shouldValidate: true }); setValue('description', productOption.description, { shouldValidate: true });
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
setValue('options', productOption.options, { shouldValidate: true });
} }
}, [isEdit, productOption, setValue]); }, [isEdit, productOption, setValue]);
@ -56,18 +84,14 @@ const ProductOptionFormPage = () => {
if (isEdit && id) { if (isEdit && id) {
updateOption({ updateOption({
id: parseInt(id), id: parseInt(id),
name: data.name, ...data
values: data.values
}, { }, {
onSuccess: () => { onSuccess: () => {
navigate('/product-options'); navigate('/product-options');
} }
}); });
} else { } else {
createOption({ createOption(data, {
name: data.name,
values: data.values
}, {
onSuccess: () => { onSuccess: () => {
navigate('/product-options'); navigate('/product-options');
} }
@ -75,122 +99,172 @@ const ProductOptionFormPage = () => {
} }
}; };
const handleBack = () => { const addOption = () => {
navigate('/product-options'); appendOption({
title: '',
description: '',
meta_title: ''
});
}; };
const handleValuesChange = (values: string[]) => { if (isLoadingOption) {
setValue('values', values, { shouldValidate: true, shouldDirty: true });
};
if (isEdit && isLoadingOption) {
return ( return (
<div className="flex justify-center items-center h-64"> <div className="flex items-center justify-center min-h-screen">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
); );
} }
return ( return (
<div className="max-w-2xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto p-6">
{/* Header */} <div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center gap-4"> <div className="p-6 border-b border-gray-200 dark:border-gray-700">
<Button <div className="flex items-center gap-4">
variant="secondary" <Button
onClick={handleBack} variant="secondary"
className="flex items-center gap-2" onClick={() => navigate('/product-options')}
> className="flex items-center gap-2"
<ArrowRight className="h-4 w-4" /> >
بازگشت <ArrowRight className="h-4 w-4" />
</Button> برگشت
<div> </Button>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-6 w-6" /> <Settings className="h-6 w-6 text-primary-600" />
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول جدید'} <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
</h1> {isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول جدید'}
<p className="text-gray-600 dark:text-gray-400 mt-1"> </h1>
{isEdit ? 'ویرایش اطلاعات گزینه محصول' : 'اطلاعات گزینه محصول جدید را وارد کنید'} </div>
</p> </div>
</div> </div>
</div>
{/* Form */} <form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-6">
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <div>
<Input <Input
label="نام گزینه" label="عنوان"
{...register('name')} {...register('title')}
error={errors.name?.message} error={errors.title?.message}
placeholder="مثال: رنگ، سایز، جنس" placeholder="عنوان گزینه محصول را وارد کنید"
/> />
</div>
<div>
<Input
label="توضیحات"
{...register('description')}
error={errors.description?.message}
placeholder="توضیحات گزینه محصول را وارد کنید"
/>
</div>
</div>
<TagInput <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
label="مقادیر گزینه" <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">اطلاعات نگهداری</h3>
values={watch('values') || []} <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
onChange={handleValuesChange} <Input
placeholder="مقدار جدید اضافه کنید..." label="عنوان نگهداری"
error={errors.values?.message} {...register('maintenance.title')}
/> error={errors.maintenance?.title?.message}
placeholder="عنوان نگهداری را وارد کنید"
/>
<Input
label="توضیحات نگهداری"
{...register('maintenance.description')}
error={errors.maintenance?.description?.message}
placeholder="توضیحات نگهداری را وارد کنید"
/>
<Input
label="محتوای نگهداری"
{...register('maintenance.content')}
error={errors.maintenance?.content?.message}
placeholder="محتوای نگهداری را وارد کنید"
/>
<Input
label="تصویر نگهداری"
{...register('maintenance.image')}
error={errors.maintenance?.image?.message}
placeholder="آدرس تصویر نگهداری را وارد کنید"
/>
</div>
</div>
{/* Preview */} <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
{formValues.values && formValues.values.length > 0 && ( <div className="flex items-center justify-between mb-4">
<div className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-700"> <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">گزینهها</h3>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3"> <Button
پیشنمایش گزینه type="button"
</h3> variant="primary"
<div className="space-y-2"> onClick={addOption}
<div className="text-sm text-gray-600 dark:text-gray-400"> className="flex items-center gap-2"
<strong>نام:</strong> {formValues.name || 'نام گزینه'} >
<Plus className="h-4 w-4" />
افزودن گزینه
</Button>
</div>
{optionFields.map((field, index) => (
<div key={field.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between mb-4">
<h4 className="text-md font-medium text-gray-900 dark:text-gray-100">گزینه {index + 1}</h4>
<Button
type="button"
variant="danger"
onClick={() => removeOption(index)}
className="flex items-center gap-2"
>
<Trash2 className="h-4 w-4" />
حذف
</Button>
</div> </div>
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<strong>مقادیر:</strong> <Input
<div className="flex flex-wrap gap-1 mt-1"> label="عنوان"
{formValues.values.map((value, index) => ( {...register(`options.${index}.title`)}
<span error={errors.options?.[index]?.title?.message}
key={index} placeholder="عنوان گزینه را وارد کنید"
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200" />
> <Input
{value} label="توضیحات"
</span> {...register(`options.${index}.description`)}
))} error={errors.options?.[index]?.description?.message}
</div> placeholder="توضیحات گزینه را وارد کنید"
/>
<Input
label="متا تایتل"
{...register(`options.${index}.meta_title`)}
error={errors.options?.[index]?.meta_title?.message}
placeholder="متا تایتل را وارد کنید"
/>
</div> </div>
</div> </div>
</div> ))}
)}
<div className="flex justify-end space-x-4 space-x-reverse pt-6 border-t border-gray-200 dark:border-gray-600"> {optionFields.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
هیچ گزینهای تعریف نشده است. برای شروع گزینهای اضافه کنید.
</div>
)}
</div>
<div className="flex justify-end gap-4 pt-6">
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
onClick={handleBack} onClick={() => navigate('/product-options')}
disabled={isLoading} disabled={isLoading}
> >
انصراف لغو
</Button> </Button>
<Button <Button
type="submit" type="submit"
loading={isLoading} variant="primary"
disabled={!isValid || isLoading} disabled={!isValid || isLoading}
loading={isLoading}
> >
{isEdit ? 'به‌روزرسانی' : 'ایجاد'} {isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول'}
</Button> </Button>
</div> </div>
</form> </form>
</div> </div>
{/* Help Section */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
راهنما
</h3>
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li> گزینههای محصول برای تعریف ویژگیهایی مثل رنگ، سایز، جنس استفاده میشوند</li>
<li> هر گزینه میتواند چندین مقدار داشته باشد (مثل قرمز، آبی، سبز برای رنگ)</li>
<li> این گزینهها بعداً در ایجاد محصولات قابل استفاده خواهند بود</li>
<li> برای اضافه کردن مقدار جدید، آن را تایپ کنید و Enter بزنید</li>
</ul>
</div>
</div> </div>
); );
}; };

View File

@ -163,22 +163,22 @@ const ProductOptionsListPage = () => {
{(productOptions || []).map((option: ProductOption) => ( {(productOptions || []).map((option: ProductOption) => (
<tr key={option.id} className="hover:bg-gray-50 dark:hover:bg-gray-700"> <tr key={option.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
{option.name} {option.title}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">
<div className="flex flex-wrap gap-1 max-w-xs"> <div className="flex flex-wrap gap-1 max-w-xs">
{option.values.slice(0, 3).map((value, index) => ( {(option.options || []).slice(0, 3).map((optionItem, index) => (
<span <span
key={index} key={index}
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200" className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200"
> >
<Tag className="h-3 w-3 mr-1" /> <Tag className="h-3 w-3 mr-1" />
{value} {optionItem.title}
</span> </span>
))} ))}
{option.values.length > 3 && ( {(option.options || []).length > 3 && (
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200"> <span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
+{option.values.length - 3} بیشتر +{(option.options || []).length - 3} بیشتر
</span> </span>
)} )}
</div> </div>
@ -218,21 +218,21 @@ const ProductOptionsListPage = () => {
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{option.name} {option.title}
</h3> </h3>
<div className="flex flex-wrap gap-1 mt-2"> <div className="flex flex-wrap gap-1 mt-2">
{option.values.slice(0, 3).map((value, index) => ( {(option.options || []).slice(0, 3).map((optionItem, index) => (
<span <span
key={index} key={index}
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200" className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200"
> >
<Tag className="h-3 w-3 mr-1" /> <Tag className="h-3 w-3 mr-1" />
{value} {optionItem.title}
</span> </span>
))} ))}
{option.values.length > 3 && ( {(option.options || []).length > 3 && (
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200"> <span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
+{option.values.length - 3} بیشتر +{(option.options || []).length - 3} بیشتر
</span> </span>
)} )}
</div> </div>

View File

@ -195,8 +195,8 @@ const ProductFormPage = () => {
const productOptionOptions = (productOptions || []).map(option => ({ const productOptionOptions = (productOptions || []).map(option => ({
id: option.id, id: option.id,
title: option.name, title: option.title,
description: `مقادیر: ${option.values.join(', ')}` description: `تعداد گزینه‌ها: ${(option.options || []).length}`
})); }));
return ( return (
@ -329,7 +329,7 @@ const ProductFormPage = () => {
<option value="">بدون گزینه</option> <option value="">بدون گزینه</option>
{(productOptions || []).map((option) => ( {(productOptions || []).map((option) => (
<option key={option.id} value={option.id}> <option key={option.id} value={option.id}>
{option.name} ({option.values.join(', ')}) {option.title} ({(option.options || []).length} گزینه)
</option> </option>
))} ))}
</select> </select>

10099
swagger.json

File diff suppressed because it is too large Load Diff