Upload new document api integrated and use can upload document at employee
This commit is contained in:
parent
b93eaf6b95
commit
daf7f11310
@ -1,33 +1,59 @@
|
||||
import { z } from "zod";
|
||||
import { normalizeAllowedContentTypes } from "../../utils/appUtils";
|
||||
|
||||
export const AttachmentSchema = z.object({
|
||||
fileName: z.string().min(1, {message:"File name is required"}),
|
||||
base64Data: z.string().min(1, {message:"File data is required"}),
|
||||
contentType: z.string().min(1, {message:"MIME type is required"}),
|
||||
fileSize: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative("fileSize must be ≥ 0")
|
||||
.max(25 * 1024 * 1024, "fileSize must be ≤ 25MB"),
|
||||
description: z.string().optional().default(""),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => {
|
||||
const allowedTypes = normalizeAllowedContentTypes(allowedContentType);
|
||||
|
||||
return z.object({
|
||||
fileName: z.string().min(1, { message: "File name is required" }),
|
||||
base64Data: z.string().min(1, { message: "File data is required" }),
|
||||
contentType: z
|
||||
.string()
|
||||
.min(1, { message: "MIME type is required" })
|
||||
.refine(
|
||||
(val) => (allowedTypes.length ? allowedTypes.includes(val) : true),
|
||||
{
|
||||
message: `File type must be one of: ${allowedTypes.join(", ")}`,
|
||||
}
|
||||
),
|
||||
fileSize: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative("fileSize must be ≥ 0")
|
||||
.max(
|
||||
(maxSizeAllowedInMB ?? 25) * 1024 * 1024,
|
||||
`fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB`
|
||||
),
|
||||
description: z.string().optional().default(""),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
};
|
||||
|
||||
export const TagSchema = z.object({
|
||||
name: z.string().min(1, {message:"Tag name is required"}),
|
||||
isActive: z.boolean(),
|
||||
name: z.string().min(1, "Tag name is required"),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const DocumentPayloadSchema = (isMandatory, regularExp) => {
|
||||
|
||||
export const DocumentPayloadSchema = (docConfig = {}) => {
|
||||
const {
|
||||
isMandatory,
|
||||
regexExpression,
|
||||
allowedContentType,
|
||||
maxSizeAllowedInMB,
|
||||
} = docConfig;
|
||||
|
||||
let documentIdSchema = z.string();
|
||||
|
||||
if (isMandatory) {
|
||||
documentIdSchema = documentIdSchema.min(1, {message:"DocumentId is required"});
|
||||
documentIdSchema = documentIdSchema.min(1, {
|
||||
message: "DocumentId is required",
|
||||
});
|
||||
}
|
||||
|
||||
if (regularExp) {
|
||||
if (regexExpression) {
|
||||
documentIdSchema = documentIdSchema.regex(
|
||||
new RegExp(regularExp),
|
||||
new RegExp(regexExpression),
|
||||
"Invalid DocumentId format"
|
||||
);
|
||||
}
|
||||
@ -35,22 +61,30 @@ export const DocumentPayloadSchema = (isMandatory, regularExp) => {
|
||||
return z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
documentId: documentIdSchema,
|
||||
description: z.string().min(1,{message:"Description is required"}),
|
||||
entityId: z.string().min(1,{message:"Please Select Document Entity"}),
|
||||
documentTypeId: z.string().min(1,{message:"Please Select Document Type"}),
|
||||
documentCategoryId:z.string().min(1,{message:"Please Select Document Category"}),
|
||||
attachment: AttachmentSchema,
|
||||
tags: z.array(TagSchema).default([]),
|
||||
description: z.string().min(1, { message: "Description is required" }),
|
||||
// entityId: z.string().min(1, { message: "Please Select Document Entity" }),
|
||||
documentTypeId: z.string().min(1, { message: "Please Select Document Type" }),
|
||||
documentCategoryId: z
|
||||
.string()
|
||||
.min(1, { message: "Please Select Document Category" }),
|
||||
attachment: AttachmentSchema(allowedContentType, maxSizeAllowedInMB).nullable().refine(
|
||||
(val) => val !== null,
|
||||
{ message: "Attachment is required" }
|
||||
),
|
||||
|
||||
tags: z.array(TagSchema).optional().default([]),
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const defaultDocumentValues = {
|
||||
name: "",
|
||||
documentId: "",
|
||||
description: "",
|
||||
entityId: "",
|
||||
// entityId: "",
|
||||
documentTypeId: "",
|
||||
documentCategoryId:"",
|
||||
documentCategoryId: "",
|
||||
attachment: {
|
||||
fileName: "",
|
||||
base64Data: "",
|
||||
@ -59,5 +93,5 @@ export const defaultDocumentValues = {
|
||||
description: "",
|
||||
isActive: true,
|
||||
},
|
||||
tags: [],
|
||||
tags: [],
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ const Documents = () => {
|
||||
</div>
|
||||
{isUpload && (
|
||||
<GlobalModel isOpen={isUpload} closeModal={()=>setUpload(false)}>
|
||||
<NewDocument/>
|
||||
<NewDocument closeModal={()=>setUpload(false)}/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { defaultDocumentValues, DocumentPayloadSchema } from "./DocumentSchema";
|
||||
import Label from "../common/Label";
|
||||
import { DOCUMENTS_ENTITIES } from "../../utils/constants";
|
||||
@ -8,8 +8,11 @@ import {
|
||||
useDocumentCategories,
|
||||
useDocumentTypes,
|
||||
} from "../../hooks/masterHook/useMaster";
|
||||
import TagInput from "../common/TagInput";
|
||||
import { useUploadDocument } from "../../hooks/useDocument";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
// util fn: convert file → base64
|
||||
const toBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@ -18,40 +21,70 @@ const toBase64 = (file) =>
|
||||
reader.onerror = (err) => reject(err);
|
||||
});
|
||||
|
||||
const NewDocument = () => {
|
||||
const [selectedType, setType] = useState("");
|
||||
|
||||
const DocumentUpload = DocumentPayloadSchema(
|
||||
selectedType?.isMandatory ?? null,
|
||||
selectedType?.regexExpression ?? null
|
||||
);
|
||||
const NewDocument = ({closeModal}) => {
|
||||
const { employeeId } = useParams();
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||||
const [schema, setSchema] = useState(() => DocumentPayloadSchema({}));
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaultDocumentValues,
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(DocumentUpload),
|
||||
defaultValues: defaultDocumentValues,
|
||||
} = methods;
|
||||
const { mutate: UploadDocument, isPending } = useUploadDocument(() => {
|
||||
showToast("Document Uploaded Successfully", "success");
|
||||
closeModal();
|
||||
});
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("Form submitted:", data);
|
||||
const DocumentPayload = { ...data, entityId: employeeId };
|
||||
UploadDocument(DocumentPayload);
|
||||
};
|
||||
|
||||
const documentTypeId = watch("documentTypeId");
|
||||
const categoryId = watch("documentCategoryId");
|
||||
const file = watch("attachment");
|
||||
|
||||
// API Data
|
||||
const documentTypeId = watch("documentTypeId");
|
||||
|
||||
// This hooks calling api base Entity(Employee) and Category
|
||||
const { DocumentCategories, isLoading } = useDocumentCategories(
|
||||
DOCUMENTS_ENTITIES.EmployeeEntity
|
||||
);
|
||||
|
||||
const { DocumentTypes, isLoading: isTypeLoading } =
|
||||
useDocumentTypes(categoryId);
|
||||
const categoryId = watch("documentCategoryId");
|
||||
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
|
||||
categoryId || null
|
||||
);
|
||||
|
||||
// Update schema whenever document type changes
|
||||
useEffect(() => {
|
||||
if (!documentTypeId) return;
|
||||
const type = DocumentTypes?.find((t) => t.id === documentTypeId);
|
||||
setSelectedType(type || null);
|
||||
|
||||
if (type) {
|
||||
const newSchema = DocumentPayloadSchema({
|
||||
isMandatory: type.isMandatory ?? false,
|
||||
regexExpression: type.regexExpression ?? null,
|
||||
allowedContentType: type.allowedContentType ?? [
|
||||
"application/pdf",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
],
|
||||
maxSizeAllowedInMB: type.maxSizeAllowedInMB ?? 25,
|
||||
});
|
||||
|
||||
setSchema(() => newSchema);
|
||||
|
||||
reset({ ...methods.getValues() }, { keepValues: true });
|
||||
}
|
||||
}, [documentTypeId, DocumentTypes, reset]);
|
||||
|
||||
// File Upload
|
||||
const onFileChange = async (e) => {
|
||||
@ -82,177 +115,207 @@ const NewDocument = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (documentTypeId) {
|
||||
setType(DocumentTypes?.find((type) => type.id === documentTypeId));
|
||||
}
|
||||
}, [documentTypeId, DocumentTypes]);
|
||||
|
||||
// build dynamic file accept string
|
||||
const fileAccept =
|
||||
selectedType?.allowedContentType
|
||||
?.split(",")
|
||||
.map((t) =>
|
||||
t === "application/pdf"
|
||||
? ".pdf"
|
||||
: t === "image/jpeg"
|
||||
? ".jpg,.jpeg"
|
||||
: t === "image/png"
|
||||
? ".png"
|
||||
: ""
|
||||
)
|
||||
.join(",") || "";
|
||||
return (
|
||||
<div className="p-2">
|
||||
<p className="fw-bold fs-6">Upload New Document</p>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Document Name */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="name" required>
|
||||
Document Name
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("name")}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div className="danger-text">{errors.name.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentCategoryId">Document Category</Label>
|
||||
<select
|
||||
{...register("documentCategoryId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
{isLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
<FormProvider key={documentTypeId} {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Document Name */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="name" required>
|
||||
Document Name
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("name")}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div className="danger-text">{errors.name.message}</div>
|
||||
)}
|
||||
{DocumentCategories?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentCategoryId && (
|
||||
<div className="danger-text">
|
||||
{errors.documentCategoryId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentTypeId">Document Type</Label>
|
||||
<select
|
||||
{...register("documentTypeId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
{isTypeLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
)}
|
||||
{DocumentTypes?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentTypeId && (
|
||||
<div className="danger-text">{errors.documentTypeId.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document ID */}
|
||||
<div className="mb-2">
|
||||
<Label
|
||||
htmlFor="documentId"
|
||||
required={selectedType?.isMandatory ?? false}
|
||||
>
|
||||
Document ID
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("documentId")}
|
||||
/>
|
||||
{errors.documentId && (
|
||||
<div className="danger-text">{errors.documentId.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upload */}
|
||||
<div className="row my-2">
|
||||
<div className="col-md-12">
|
||||
<label className="form-label">Upload Document</label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("attachment").click()}
|
||||
{/* Category */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentCategoryId">Document Category</Label>
|
||||
<select
|
||||
{...register("documentCategoryId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"></i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">(PDF, JPG, PNG, max 25MB)</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="attachment"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = ""; // reset input
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.attachment && (
|
||||
<small className="danger-text">
|
||||
{errors.attachment.fileName?.message ||
|
||||
errors.attachment.base64Data?.message ||
|
||||
errors.attachment.contentType?.message ||
|
||||
errors.attachment.fileSize?.message}
|
||||
</small>
|
||||
)}
|
||||
|
||||
{file && (
|
||||
<div className="d-flex justify-content-between text-start p-1 mt-2">
|
||||
<div>
|
||||
<span className="mb-0 text-secondary small d-block">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small d-block">
|
||||
{(file.fileSize / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
||||
onClick={removeFile}
|
||||
></i>
|
||||
{isLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
)}
|
||||
{!isLoading && <option value="">Select Category</option>}
|
||||
{DocumentCategories?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentCategoryId && (
|
||||
<div className="danger-text">
|
||||
{errors.documentCategoryId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<textarea
|
||||
rows="2"
|
||||
className="form-control"
|
||||
{...register("description")}
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<div className="danger-text">{errors.description.message}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Type */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="documentTypeId">Document Type</Label>
|
||||
<select
|
||||
{...register("documentTypeId")}
|
||||
className="form-select form-select-sm"
|
||||
>
|
||||
{isTypeLoading && (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
)}
|
||||
{DocumentTypes?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.documentTypeId && (
|
||||
<div className="danger-text">{errors.documentTypeId.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="d-flex justify-content-center gap-3">
|
||||
<button type="submit" className="btn btn-primary btn-sm">
|
||||
Submit
|
||||
</button>
|
||||
<button type="reset" className="btn btn-secondary btn-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/* Document ID */}
|
||||
<div className="mb-2">
|
||||
<Label
|
||||
htmlFor="documentId"
|
||||
required={selectedType?.isMandatory ?? false}
|
||||
>
|
||||
Document ID
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("documentId")}
|
||||
/>
|
||||
{errors.documentId && (
|
||||
<div className="danger-text">{errors.documentId.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upload */}
|
||||
<div className="row my-2">
|
||||
<div className="col-md-12">
|
||||
<label className="form-label">Upload Document</label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("attachment").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg"></i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">
|
||||
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
|
||||
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
|
||||
</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="attachment"
|
||||
accept={selectedType?.allowedContentType}
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = ""; // reset input
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.attachment && (
|
||||
<small className="danger-text">
|
||||
{errors.attachment.message
|
||||
? errors.attachment.message
|
||||
: errors.attachment.fileName?.message ||
|
||||
errors.attachment.base64Data?.message ||
|
||||
errors.attachment.contentType?.message ||
|
||||
errors.attachment.fileSize?.message}
|
||||
</small>
|
||||
)}
|
||||
|
||||
{file?.base64Data && (
|
||||
<div className="d-flex justify-content-between text-start p-1 mt-2">
|
||||
<div>
|
||||
<span className="mb-0 text-secondary small d-block">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small d-block">
|
||||
{(file.fileSize / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
||||
onClick={removeFile}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<TagInput name="tags" label="Tags" placeholder="Tags.." />
|
||||
{errors.tags && (
|
||||
<small className="danger-text">{errors.tags.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<textarea
|
||||
rows="2"
|
||||
className="form-control"
|
||||
{...register("description")}
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<div className="danger-text">{errors.description.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="d-flex justify-content-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : " Submit"}
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={isPending}
|
||||
onClick={()=>closeModal()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
const TagInput = ({
|
||||
label = "Tags",
|
||||
@ -8,104 +8,58 @@ const TagInput = ({
|
||||
color = "#e9ecef",
|
||||
options = [],
|
||||
}) => {
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tags, setTags] = useState([]); // now array of objects
|
||||
const [input, setInput] = useState("");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const { setValue, trigger, control } = useFormContext();
|
||||
const watchedTags = useWatch({ control, name });
|
||||
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
Array.isArray(watchedTags) &&
|
||||
JSON.stringify(tags) !== JSON.stringify(watchedTags)
|
||||
) {
|
||||
setTags(watchedTags);
|
||||
}
|
||||
}, [JSON.stringify(watchedTags)]);
|
||||
|
||||
// Keep form value synced
|
||||
useEffect(() => {
|
||||
if (input.trim() === "") {
|
||||
setSuggestions([]);
|
||||
} else {
|
||||
const filtered = options?.filter(
|
||||
(opt) =>
|
||||
opt?.name?.toLowerCase()?.includes(input.toLowerCase()) &&
|
||||
!tags?.some((tag) => tag.name === opt.name)
|
||||
);
|
||||
setSuggestions(filtered);
|
||||
}
|
||||
}, [input, options, tags]);
|
||||
setValue(name, tags, { shouldValidate: true });
|
||||
}, [tags, name, setValue]);
|
||||
|
||||
const addTag = async (tagObj) => {
|
||||
if (!tags.some((tag) => tag.name === tagObj.name)) {
|
||||
const cleanedTag = {
|
||||
id: tagObj.id ?? null,
|
||||
name: tagObj.name,
|
||||
};
|
||||
const newTags = [...tags, cleanedTag];
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
await trigger(name);
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === "Enter" && input.trim()) {
|
||||
e.preventDefault();
|
||||
if (!tags.some((t) => t.name === input.trim())) {
|
||||
setTags((prev) => [...prev, { name: input.trim(), isActive: true }]);
|
||||
}
|
||||
setInput("");
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (indexToRemove) => {
|
||||
const newTags = tags.filter((_, i) => i !== indexToRemove);
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
trigger(name);
|
||||
const handleRemove = (tagName) => {
|
||||
setTags((prev) => prev.filter((t) => t.name !== tagName));
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if ((e.key === "Enter" || e.key === " ")&& input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
const handleChange = (e) => {
|
||||
const val = e.target.value;
|
||||
setInput(val);
|
||||
if (val) {
|
||||
setSuggestions(
|
||||
options
|
||||
.filter(
|
||||
(opt) =>
|
||||
opt.toLowerCase().includes(val.toLowerCase()) &&
|
||||
!tags.some((t) => t.name === opt)
|
||||
)
|
||||
.map((s) => ({ name: s, isActive: true }))
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if (e.key === "Backspace" && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
const handleInputKey = (e) => {
|
||||
const key = e.key?.toLowerCase();
|
||||
|
||||
if ((key === "enter" || key === " " || e.code === "Space") && input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if ((key === "backspace" || e.code === "Backspace") && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
addTag(suggestion);
|
||||
if (!tags.some((t) => t.name === suggestion.name)) {
|
||||
setTags((prev) => [...prev, suggestion]);
|
||||
}
|
||||
setInput("");
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const backgroundColor = color || "#f8f9fa";
|
||||
const iconColor = `var(--bs-${color})`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={name} className="form-label">
|
||||
@ -122,17 +76,16 @@ useEffect(() => {
|
||||
key={index}
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
color: iconColor,
|
||||
backgroundColor,
|
||||
backgroundColor: color,
|
||||
padding: "2px 6px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "0.85rem",
|
||||
fontSize: "0.8rem",
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
<i
|
||||
className="bx bx-x bx-xs ms-1"
|
||||
onClick={() => removeTag(index)}
|
||||
onClick={() => handleRemove(tag.name)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</span>
|
||||
@ -141,9 +94,8 @@ useEffect(() => {
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKey}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
border: "none",
|
||||
@ -156,12 +108,14 @@ useEffect(() => {
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<ul
|
||||
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm "
|
||||
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm"
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
boxShadow:"0px 4px 10px rgba(0, 0, 0, 0.2)",borderRadius:"3px",border:"1px solid #ddd"
|
||||
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.2)",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
{suggestions.map((sugg, i) => (
|
||||
@ -169,8 +123,7 @@ useEffect(() => {
|
||||
key={i}
|
||||
className="dropdown-item p-1 hoverBox"
|
||||
onClick={() => handleSuggestionClick(sugg)}
|
||||
style={{cursor: "pointer", fontSize: "0.875rem"}}
|
||||
|
||||
style={{ cursor: "pointer", fontSize: "0.875rem" }}
|
||||
>
|
||||
{sugg.name}
|
||||
</li>
|
||||
@ -181,4 +134,5 @@ useEffect(() => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagInput;
|
||||
|
22
src/hooks/useDocument.js
Normal file
22
src/hooks/useDocument.js
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
//----------------------- MUTATION -------------------------
|
||||
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import showToast from "../services/toastService";
|
||||
import { DocumentRepository } from "../repositories/DocumentRepository";
|
||||
|
||||
export const useUploadDocument =(onSuccessCallBack)=>{
|
||||
return useMutation(({
|
||||
mutationFn:async(DocumentPayload)=>DocumentRepository.uploadDocument(DocumentPayload),
|
||||
onSuccess:(data,variables)=>{
|
||||
if(onSuccessCallBack) onSuccessCallBack()
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.message || "Something went wrong please try again !",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
}))
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
import { api } from "../utils/axiosClient";
|
||||
|
||||
export const DocumentRepository = {
|
||||
uploadDocument:(data)=> api.post(`/api/Document/upload`,data)
|
||||
uploadDocument:(data)=> api.post(`/api/Document/upload`,data),
|
||||
getDocumentList:(entityTypeId,entityId,pageSize, pageNumber, filter,searchString)=>{
|
||||
const payloadJsonString = JSON.stringify(filter);
|
||||
return api.get(`/api/Document/list/${entityTypeId}/entity/${entityId}/?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`)
|
||||
},
|
||||
getDocumentById:(id)=>api.get(`/api/Document/${id}`),
|
||||
|
||||
getFilterEntities:(entityId)=>api.get(`/api/Document/get/filter${entityTypeId}`),
|
||||
|
||||
}
|
||||
|
@ -61,3 +61,11 @@ export const getIconByFileType = (type = "") => {
|
||||
|
||||
return "bx bx-file";
|
||||
};
|
||||
|
||||
|
||||
export const normalizeAllowedContentTypes = (allowedContentType) => {
|
||||
if (!allowedContentType) return [];
|
||||
if (Array.isArray(allowedContentType)) return allowedContentType;
|
||||
if (typeof allowedContentType === "string") return allowedContentType.split(",");
|
||||
return [];
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user