feat: Introduce new dynamic field properties and refine form handling
This commit introduces new capabilities for dynamic forms and refactors related components for improved type safety and data fetching.
- **`src/core/utils/dynamic-field.utils.ts`**:
- Added `Description` and `Required` properties to the `FieldDefinition` interface to allow for more detailed field configurations and validation.
- Narrowed the `Type` property from `number | string` to `number` for stricter type enforcement.
- **`src/core/components/base/form-field.tsx`**:
- Applied non-null assertion operators (`!`) and optional chaining (`?.`) to `field.MinValue`, `field.MaxValue`, `field.Length`, and `field.Type`. This improves type safety and handles cases where these properties might be undefined according to the updated `FieldDefinition`.
- **`src/core/service/api-address.ts`**:
- Added `index2: "workflow/index2"` to `API_ADDRESS` to support a new workflow data endpoint.
- **`src/modules/dashboard/pages/campaigns/detail.tsx`**:
- Updated the campaign detail page to display `campaign.user_id_nickname` instead of `campaign.nickname` for accurate user identification.
- **`src/modules/dashboard/pages/step-form/index.tsx`**:
- Refactored dynamic form data fetching to use `fetchFieldIndex` and `fetchFielSecondeIndex` services.
- Switched from `useParams` to `useSearchParams` for more flexible parameter handling in the step form.
This commit is contained in:
parent
6634ecfda7
commit
bbb6bfb7f7
|
|
@ -63,10 +63,10 @@ const FormField: FC<FormFieldProps> = ({
|
|||
field.Type === FieldTypes.پول) &&
|
||||
typeof currentValue === "number"
|
||||
) {
|
||||
if (field.MinValue !== undefined && currentValue < field?.MinValue) {
|
||||
if (field.MinValue !== undefined && currentValue < field?.MinValue!) {
|
||||
errorMessage = `مقدار باید بزرگتر یا مساوی ${field.MinValue} باشد.`;
|
||||
}
|
||||
if (field.MaxValue !== undefined && currentValue > field?.MaxValue) {
|
||||
if (field.MaxValue !== undefined && currentValue > field?.MaxValue!) {
|
||||
errorMessage = `مقدار باید کوچکتر یا مساوی ${field.MaxValue} باشد.`;
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ const FormField: FC<FormFieldProps> = ({
|
|||
const handleChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const newValue = normalizeInput(field.Type, e.target.value);
|
||||
const newValue = normalizeInput(field?.Type, e.target.value);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ const FormField: FC<FormFieldProps> = ({
|
|||
<CustomInput
|
||||
{...commonProps}
|
||||
type="text"
|
||||
maxLength={field.Length}
|
||||
maxLength={field.Length!}
|
||||
placeholder={field.Name}
|
||||
/>
|
||||
);
|
||||
|
|
@ -181,7 +181,7 @@ const FormField: FC<FormFieldProps> = ({
|
|||
return (
|
||||
<TextAreaField
|
||||
{...commonProps}
|
||||
maxLength={field.Length}
|
||||
maxLength={field.Length!}
|
||||
placeholder={field.Name}
|
||||
/>
|
||||
);
|
||||
|
|
@ -361,7 +361,7 @@ const FormField: FC<FormFieldProps> = ({
|
|||
<CustomInput
|
||||
{...commonProps}
|
||||
type="text"
|
||||
maxLength={field.Length}
|
||||
maxLength={field.Length!}
|
||||
placeholder={field.Name}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const API_ADDRESS = {
|
|||
delete: "/api/delete",
|
||||
uploadImage: "/workflow/uploadImage",
|
||||
index: "workflow/index",
|
||||
index2: "workflow/index2",
|
||||
// LOGIN: "/auth/login",
|
||||
// VERIFY_OTP: "/auth/verify-otp",
|
||||
// REGISTER: "/auth/register",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export interface FieldDefinition {
|
|||
Description_HasSaving: boolean;
|
||||
Description_IsParser: boolean;
|
||||
Description_Text: string;
|
||||
Description: string;
|
||||
|
||||
Document: string;
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ export interface FieldDefinition {
|
|||
|
||||
Option_IsChips: boolean;
|
||||
Option_Options: string; // JSON string
|
||||
Required: boolean;
|
||||
|
||||
OrderNumber: number;
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ export interface FieldDefinition {
|
|||
|
||||
Time_HasSecond: boolean;
|
||||
|
||||
Type: number | string; // → FieldTypes enum عددی تو داری
|
||||
Type: number; // → FieldTypes enum عددی تو داری
|
||||
|
||||
Unit: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,8 @@ export function CampaignDetailPage() {
|
|||
</h1>
|
||||
|
||||
<p className="text-slate-600 text-right mb-6">
|
||||
توسط: <span className="font-semibold">{campaign.nickname}</span>
|
||||
توسط:{" "}
|
||||
<span className="font-semibold">{campaign.user_id_nickname}</span>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-end gap-8 py-4 border-t border-b border-gray-200 mb-6">
|
||||
|
|
|
|||
|
|
@ -3,17 +3,25 @@ import { useEffect, useState, type FC } from "react";
|
|||
import type { WorkflowResponse } from "@/core/types/global.type";
|
||||
import type { FieldDefinition as LocalFieldDefinition } from "@core/utils/dynamic-field.utils";
|
||||
import { FieldTypes } from "@core/utils/dynamic-field.utils";
|
||||
import { fetchFieldService } from "@modules/dashboard/service/dynamic-form.service";
|
||||
import {
|
||||
fetchFieldIndex,
|
||||
fetchFielSecondeIndex,
|
||||
} from "@modules/dashboard/service/dynamic-form.service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import DynamicForm from "./dynamic-form";
|
||||
|
||||
const StepFormPage: FC = () => {
|
||||
const [fields, setFields] = useState<LocalFieldDefinition[]>([]);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [params] = useSearchParams();
|
||||
const stageID = params.get("stageID");
|
||||
const processID = params.get("processID");
|
||||
const { data, isLoading, error } = useQuery<WorkflowResponse>({
|
||||
queryKey: ["dynamic-field", id],
|
||||
queryFn: fetchFieldService,
|
||||
queryKey: ["dynamic-field", stageID ?? processID],
|
||||
queryFn: () => {
|
||||
if (stageID) return fetchFielSecondeIndex(stageID);
|
||||
return fetchFieldIndex(processID!);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -54,6 +62,7 @@ const StepFormPage: FC = () => {
|
|||
Description_HasSaving: el.Description_HasSaving,
|
||||
Description_IsParser: el.Description_IsParser,
|
||||
Description_Text: el.Description_Text,
|
||||
Description: el.Description_Text,
|
||||
Document: el.Document,
|
||||
File_Extentions: el.File_Extentions,
|
||||
File_MaxSize: el.File_MaxSize,
|
||||
|
|
@ -62,6 +71,8 @@ const StepFormPage: FC = () => {
|
|||
LockFirstValue: el.LockFirstValue,
|
||||
NameOrID: el.NameOrID,
|
||||
Option_IsChips: el.Option_IsChips,
|
||||
// Option_Options: el.Option_Options,
|
||||
// Required: el.Required,
|
||||
OrderNumber: el.OrderNumber,
|
||||
Parent_CanBeSave: el.Parent_CanBeSave,
|
||||
Parent_DependentFields: el.Parent_DependentFields,
|
||||
|
|
@ -83,7 +94,7 @@ const StepFormPage: FC = () => {
|
|||
Time_HasSecond: el.Time_HasSecond,
|
||||
Unit: el.Unit,
|
||||
ViewPermissionCount: el.ViewPermissionCount,
|
||||
typeValue: el.Type as FieldTypes, // Map number to enum
|
||||
typeValue: el.Type as FieldTypes,
|
||||
};
|
||||
|
||||
if (el.Option_Options) {
|
||||
|
|
|
|||
224
src/modules/dashboard/pages/steps/index.tsx
Normal file
224
src/modules/dashboard/pages/steps/index.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import { DASHBOARD_ROUTE } from "@modules/dashboard/routes/route.constant";
|
||||
import { getCampaignStepsService } from "@modules/dashboard/service/campaigns.service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MoveLeft } from "lucide-react";
|
||||
import { useEffect, useState, type FC } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { CampaignProcess, GroupedCampaign } from "./step.type";
|
||||
|
||||
const StepsPage: FC = () => {
|
||||
const { campaignId } = useParams<{ campaignId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [steps, setSteps] = useState<GroupedCampaign[]>([]);
|
||||
const [selectedStepId, setSelectedStepId] = useState<number | null>(null);
|
||||
const { data, isLoading, error } = useQuery<Record<string, any>>({
|
||||
queryKey: ["dynamic-step"],
|
||||
queryFn: () => getCampaignStepsService(campaignId!),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
const processes: CampaignProcess[] = Object.keys(data[0])
|
||||
.filter(
|
||||
(key) =>
|
||||
key.startsWith("process") &&
|
||||
key.endsWith("_id") === false &&
|
||||
key.includes("_")
|
||||
)
|
||||
.map((key) => {
|
||||
const index = key.match(/process(\d+)_/)?.[1];
|
||||
if (!index) return null;
|
||||
return {
|
||||
processId: data[0][`process${index}`],
|
||||
category: data[0][`process${index}_category`],
|
||||
score: data[0][`process${index}_score`],
|
||||
stageId: data[0][`process${index}_stage_id`],
|
||||
status: data[0][`process${index}_status`],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as CampaignProcess[];
|
||||
|
||||
const grouped: GroupedCampaign[] = Object.values(
|
||||
processes.reduce((acc, item) => {
|
||||
if (!acc[item.category]) {
|
||||
acc[item.category] = {
|
||||
category: item.category,
|
||||
processes: [],
|
||||
stageID: Number(item.stageId),
|
||||
processId: Number(item.processId),
|
||||
};
|
||||
}
|
||||
acc[item.category].processes.push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, GroupedCampaign>)
|
||||
);
|
||||
setSteps(grouped);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(
|
||||
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.campaigns}/${campaignId}`
|
||||
);
|
||||
};
|
||||
|
||||
// navigate(`${DASHBOARD_ROUTE.dynamicForm}?stageID=${stageID}`);
|
||||
// navigate(`${DASHBOARD_ROUTE.dynamicForm}?processID=${processID}`);
|
||||
|
||||
const handleStepClick = (step: GroupedCampaign) => {
|
||||
// setSelectedStepId(step.processId);
|
||||
|
||||
if (
|
||||
step.stageID !== null &&
|
||||
step.stageID !== 0 &&
|
||||
step.processId !== null &&
|
||||
step.processId !== 0
|
||||
) {
|
||||
navigate(
|
||||
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?stageID=${step.stageID}`,
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(step.processId != 0 &&
|
||||
step.processId != null &&
|
||||
step.processId != undefined &&
|
||||
step.stageID === null) ||
|
||||
step.stageID === undefined
|
||||
) {
|
||||
navigate(
|
||||
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?processID=${step.processId}`,
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(step.stageID != 0 &&
|
||||
step.stageID != null &&
|
||||
step.stageID != undefined &&
|
||||
step.processId === null) ||
|
||||
step.processId === undefined
|
||||
) {
|
||||
navigate(
|
||||
`${DASHBOARD_ROUTE.sub}/${DASHBOARD_ROUTE.dynamicForm}?stageID=${step.stageID}`,
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<div className="text-lg font-medium text-gray-700">
|
||||
در حال بارگزاری ....
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-red-100 text-red-700">
|
||||
<div className="text-lg font-medium">{error.message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4 sm:p-6 lg:p-8">
|
||||
{/* Back Button */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-md p-2"
|
||||
>
|
||||
<MoveLeft className="rotate-180" size={20} />
|
||||
بازگشت به صفحه قبل
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-gray-900 mb-8 text-center">
|
||||
مراحل جزیره
|
||||
</h1>
|
||||
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{steps.length === 0 ? (
|
||||
<p className="text-center text-gray-600 text-lg">
|
||||
مرحله ای یافت نشد.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-4">
|
||||
{steps.map((step, index) => (
|
||||
<li
|
||||
key={`${step.category}-${index}`}
|
||||
className={`
|
||||
relative flex items-center gap-2.5 p-4 sm:p-6 rounded-lg shadow-md
|
||||
bg-white border border-gray-200
|
||||
hover:shadow-lg hover:border-blue-300
|
||||
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-opacity-50
|
||||
transition-all duration-300 ease-in-out transform
|
||||
|
||||
${
|
||||
selectedStepId === step.stageID
|
||||
? "bg-blue-50 border-blue-500 ring-2 ring-blue-500 ring-opacity-75 scale-105"
|
||||
: "hover:scale-102"
|
||||
}
|
||||
`}
|
||||
onClick={() => handleStepClick(step)}
|
||||
tabIndex={0} // Make list item focusable
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
w-10 h-10 sm:w-12 sm:h-12 rounded-full
|
||||
font-bold text-lg sm:text-xl
|
||||
mr-4 sm:mr-6 shrink-0
|
||||
${
|
||||
selectedStepId === step.stageID
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-200 text-gray-700"
|
||||
}
|
||||
transition-all duration-300 ease-in-out
|
||||
`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="grow">
|
||||
<h2
|
||||
className={`
|
||||
text-lg sm:text-xl font-semibold
|
||||
${
|
||||
selectedStepId === step.stageID
|
||||
? "text-blue-800"
|
||||
: "text-gray-800"
|
||||
}
|
||||
transition-colors duration-300 ease-in-out
|
||||
`}
|
||||
>
|
||||
{step.category}
|
||||
</h2>
|
||||
{step.category &&
|
||||
step.category &&
|
||||
step.category !== step.category && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{step.category}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepsPage;
|
||||
14
src/modules/dashboard/pages/steps/step.type.ts
Normal file
14
src/modules/dashboard/pages/steps/step.type.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export interface CampaignProcess {
|
||||
processId: string;
|
||||
category: string;
|
||||
score: string;
|
||||
stageId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface GroupedCampaign {
|
||||
stageID: number;
|
||||
processId: number;
|
||||
category: string;
|
||||
processes: CampaignProcess[];
|
||||
}
|
||||
|
|
@ -5,5 +5,6 @@ export const DASHBOARD_ROUTE = {
|
|||
campaigns: "campaigns",
|
||||
campaing: "campaing",
|
||||
joinToCampaing: "join-to-campaing",
|
||||
daynamicPage: "dynamic-page",
|
||||
dynamicForm: "dynamic-form",
|
||||
steps: "steps",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import JoinToCampaing from "../pages/join-to-campaing";
|
|||
import DashboardPage from "../pages/main-page";
|
||||
import ProfilePage from "../pages/profile";
|
||||
import StepFormPage from "../pages/step-form";
|
||||
import StepsPage from "../pages/steps";
|
||||
import { DASHBOARD_ROUTE } from "./route.constant";
|
||||
|
||||
export const dashboardRoutes: AppRoute[] = [
|
||||
|
|
@ -34,9 +35,13 @@ export const dashboardRoutes: AppRoute[] = [
|
|||
element: <JoinToCampaing />,
|
||||
},
|
||||
{
|
||||
path: `${DASHBOARD_ROUTE.daynamicPage}/:id`,
|
||||
path: DASHBOARD_ROUTE.dynamicForm,
|
||||
element: <StepFormPage />,
|
||||
},
|
||||
{
|
||||
path: `${DASHBOARD_ROUTE.steps}`,
|
||||
element: <StepsPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -323,3 +323,32 @@ export const createCircleService = async (
|
|||
toast.success("محفل با موفقیت ایجاد شد");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const getCampaignStepsService = async (
|
||||
campaignId: string
|
||||
): Promise<Record<string, any>> => {
|
||||
const query = {
|
||||
ProcessName: "run_process",
|
||||
OutputFields: ["*"],
|
||||
conditions: [
|
||||
["campaign_id", "=", "18909", "and"],
|
||||
["group_id", "=", "18950"],
|
||||
],
|
||||
};
|
||||
|
||||
const [err, res] = await to(api.post(API_ADDRESS.select, query));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error("خطا در دریافت مراحل کارزار");
|
||||
throw new Error("خطا در دریافت مراحل کارزار");
|
||||
}
|
||||
|
||||
const data = JSON.parse(res.data.data);
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,26 @@ import type { WorkflowResponse } from "@/core/types/global.type";
|
|||
import to from "await-to-js";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const fetchFieldService = async (): Promise<WorkflowResponse> => {
|
||||
const [err, res] = await to(api.post(API_ADDRESS.index, "1224"));
|
||||
export const fetchFieldIndex = async (
|
||||
id: string
|
||||
): Promise<WorkflowResponse> => {
|
||||
const [err, res] = await to(api.post(API_ADDRESS.index, id));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.data.resultType !== 0) {
|
||||
toast.error(res.data.message || "خطا در دریافت فیلدها");
|
||||
throw new Error("خطا در دریافت فیلدها");
|
||||
}
|
||||
const data = JSON.parse(res.data.data);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchFielSecondeIndex = async (
|
||||
id: string
|
||||
): Promise<WorkflowResponse> => {
|
||||
const [err, res] = await to(api.post(API_ADDRESS.index, id));
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
|
|
|||
12
src/modules/dashboard/types/campaign-steps.type.ts
Normal file
12
src/modules/dashboard/types/campaign-steps.type.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export interface CampaignStep {
|
||||
id: string;
|
||||
orderNumber: number;
|
||||
stepName: string;
|
||||
title: string; // Assuming title is also available, as per description
|
||||
isSelected?: boolean; // Optional field for highlighting
|
||||
}
|
||||
|
||||
export interface GetCampaignStepsResponse {
|
||||
data: CampaignStep[];
|
||||
// Add any other relevant fields from the API response
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user