Merge branch 'dev-1' of https://gitlab.com/hanifsalafi/new-netidhub-public
This commit is contained in:
commit
11d47e4c29
|
|
@ -427,14 +427,14 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
helpText="Group classification for organization"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
{/* <FormField
|
||||
label="Is Approval Active"
|
||||
name={`isApprovalActive-${index}`}
|
||||
type="checkbox"
|
||||
value={item.isApprovalActive}
|
||||
onChange={(value) => onUpdate({ ...item, isApprovalActive: value })}
|
||||
helpText="Users with this level can participate in approval process"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
label="Is Active"
|
||||
|
|
@ -1152,7 +1152,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
{/* <FormField
|
||||
label="Is Approval Active"
|
||||
name="isApprovalActive"
|
||||
type="checkbox"
|
||||
|
|
@ -1161,7 +1161,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
handleFieldChange("isApprovalActive", value)
|
||||
}
|
||||
helpText="Users with this level can participate in approval process"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
label="Is Active"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type CategoryDetail = {
|
|||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdByFullname: string;
|
||||
};
|
||||
|
||||
export default function CategoriesUpdateForm() {
|
||||
|
|
@ -257,8 +258,9 @@ export default function CategoriesUpdateForm() {
|
|||
<div>
|
||||
<Label>Created By</Label>
|
||||
<Input
|
||||
disabled
|
||||
type="text"
|
||||
value={formData.createdByName || formData.createdById}
|
||||
value={formData.createdByFullname}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ type Detail = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -161,6 +162,16 @@ export default function FormVideoDetail() {
|
|||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDetail() {
|
||||
if (!id) return;
|
||||
|
|
@ -175,9 +186,22 @@ export default function FormVideoDetail() {
|
|||
uploadedById: details?.createdById,
|
||||
files: details?.files || [],
|
||||
thumbnailUrl: details?.thumbnailUrl || details?.thumbnail || "",
|
||||
publishedFor: details?.publishedFor,
|
||||
};
|
||||
setDetail(mappedDetail);
|
||||
setFiles(details?.files || []);
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
|
||||
setApproval(approvals?.data?.data);
|
||||
} catch (err) {
|
||||
|
|
@ -359,7 +383,60 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-3 gap-2">
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* <div className="px-3 py-3 gap-2">
|
||||
<Label>Publish Target</Label>
|
||||
{[5, 6].map((target) => (
|
||||
<div key={target} className="flex items-center gap-2">
|
||||
|
|
@ -374,7 +451,7 @@ export default function FormVideoDetail() {
|
|||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="px-3 py-3 border mx-3">
|
||||
<p>Information:</p>
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ type Option = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
export default function FormVideo() {
|
||||
export default function FormVideo() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const editor = useRef(null);
|
||||
|
|
@ -930,12 +930,11 @@ export default function FormVideo() {
|
|||
}
|
||||
|
||||
if (id == undefined) {
|
||||
// New Articles API request data structure
|
||||
const articleData: CreateArticleData = {
|
||||
aiArticleId: 0, // default 0
|
||||
aiArticleId: 0,
|
||||
categoryIds: selectedCategory.toString(),
|
||||
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
createdById: Number(userId), // isi dengan userId valid
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
description: htmlToString(finalDescription),
|
||||
htmlDescription: finalDescription,
|
||||
isDraft: true,
|
||||
|
|
@ -948,9 +947,9 @@ export default function FormVideo() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 2,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// Use new Articles API
|
||||
const response = await createArticle(articleData);
|
||||
console.log("Article Data Submitted:", articleData);
|
||||
console.log("Article API Response:", response);
|
||||
|
|
@ -1651,7 +1650,8 @@ export default function FormVideo() {
|
|||
|
||||
<p className="text-sm font-semibold">Content Rewrite</p>
|
||||
<div className="my-2">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRewriteClick}
|
||||
className="bg-blue-500 text-white py-2 px-4 rounded"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,13 @@ export default function FormVideoUpdate() {
|
|||
setSelectedCategory(String(detailData.categories[0].id));
|
||||
|
||||
setTags(detailData.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
setPublishedFor(detailData.publishedFor?.split(",") || []);
|
||||
// setPublishedFor(detailData.publishedFor?.split(",") || []);
|
||||
const publishTargets = detailData?.publishedFor
|
||||
? detailData.publishedFor.split(",")
|
||||
: [];
|
||||
|
||||
setPublishedFor(publishTargets);
|
||||
setValue("publishedFor", publishTargets);
|
||||
} catch (err) {
|
||||
close();
|
||||
console.error("❌ Error loading detail:", err);
|
||||
|
|
@ -218,6 +224,7 @@ export default function FormVideoUpdate() {
|
|||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// const payload = {
|
||||
|
|
@ -282,6 +289,17 @@ export default function FormVideoUpdate() {
|
|||
|
||||
if (!detail) return <p className="p-5 text-center">Memuat data...</p>;
|
||||
|
||||
const getVideoSrc = (url?: string) => {
|
||||
if (!url) return "";
|
||||
|
||||
// kalau sudah absolute
|
||||
if (url.startsWith("http")) return url;
|
||||
|
||||
// kalau backend kirim relative path
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
return `${baseUrl}${url}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
|
||||
|
|
@ -363,21 +381,24 @@ export default function FormVideoUpdate() {
|
|||
{detailFiles.map((file) => (
|
||||
<div key={file.id} className="flex flex-row items-center gap-4">
|
||||
<video
|
||||
className="object-contain w-[300px] h-[200px]"
|
||||
src={file.fileUrl}
|
||||
className="object-contain w-[300px] h-[200px] rounded border"
|
||||
src={getVideoSrc(file.fileUrl)}
|
||||
controls
|
||||
preload="metadata"
|
||||
title={file.fileName}
|
||||
/>
|
||||
|
||||
<p>{file.fileName}</p>
|
||||
<a
|
||||
<button
|
||||
type="button"
|
||||
className="text-destructive"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
>
|
||||
<TimesIcon />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{files?.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
|
|
@ -466,6 +487,68 @@ export default function FormVideoUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const isAllChecked =
|
||||
field.value?.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: field.value?.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...(field.value || []), option.id]
|
||||
: field.value?.filter(
|
||||
(val) => val !== option.id,
|
||||
) || [];
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -512,13 +595,6 @@ export default function FormVideoUpdate() {
|
|||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="border"
|
||||
/>
|
||||
|
||||
{/* <Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/> */}
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -532,7 +608,7 @@ export default function FormVideoUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-5">
|
||||
|
|
|
|||
|
|
@ -91,25 +91,63 @@ type FileType = {
|
|||
};
|
||||
|
||||
type Detail = {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
htmlDescription: string;
|
||||
slug: string;
|
||||
category: {
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
typeId: number;
|
||||
tags: string;
|
||||
thumbnailUrl: string;
|
||||
pageUrl: string | null;
|
||||
createdById: number;
|
||||
createdByName: string;
|
||||
shareCount: number;
|
||||
viewCount: number;
|
||||
commentCount: number;
|
||||
aiArticleId: number | null;
|
||||
oldId: number;
|
||||
statusId: number;
|
||||
isBanner: boolean;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
slug: string | null;
|
||||
tags: string[];
|
||||
thumbnailPath: string | null;
|
||||
parentId: number;
|
||||
oldCategoryId: number | null;
|
||||
createdById: number;
|
||||
statusId: number;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isEnabled: boolean | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
// Legacy fields for backward compatibility
|
||||
category?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
categoryName: string;
|
||||
creatorName: string;
|
||||
thumbnailLink: string;
|
||||
tags: string;
|
||||
statusName: string;
|
||||
isPublish: boolean;
|
||||
needApprovalFromLevel: number;
|
||||
files: FileType[];
|
||||
uploadedById: number;
|
||||
creatorName?: string;
|
||||
thumbnailLink?: string;
|
||||
statusName?: string;
|
||||
needApprovalFromLevel?: number;
|
||||
uploadedById?: number;
|
||||
};
|
||||
|
||||
const ViewEditor = dynamic(
|
||||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
|
|
@ -219,6 +257,16 @@ export default function FormAudioDetail() {
|
|||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id: any) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
const handleCheckboxChange = (id: number) => {
|
||||
setSelectedPublishers((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
|
|
@ -266,6 +314,18 @@ export default function FormAudioDetail() {
|
|||
});
|
||||
setupPlacementCheck(details?.files?.length);
|
||||
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
if (details?.publishedForObject) {
|
||||
const publisherIds = details?.publishedForObject.map(
|
||||
(obj: any) => obj.id,
|
||||
|
|
@ -611,6 +671,58 @@ export default function FormAudioDetail() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
|
@ -632,7 +744,7 @@ export default function FormAudioDetail() {
|
|||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<SuggestionModal
|
||||
id={Number(id)}
|
||||
numberOfSuggestion={detail?.numberOfSuggestion}
|
||||
|
|
|
|||
|
|
@ -650,6 +650,7 @@ export default function FormAudio() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 4,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// Use new Articles API
|
||||
|
|
|
|||
|
|
@ -250,6 +250,14 @@ export default function FormAudioUpdate() {
|
|||
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
setPublishedFor(details.publishedFor?.split(",") || []);
|
||||
|
||||
if (details?.publishedFor) {
|
||||
const parsed = details.publishedFor
|
||||
.split(",")
|
||||
.map((id: string) => id.trim());
|
||||
|
||||
setValue("publishedFor", parsed); // 🔥 WAJIB
|
||||
}
|
||||
|
||||
if (details?.files) {
|
||||
setPrefFiles(details.files);
|
||||
// setFiles(details.files);
|
||||
|
|
@ -352,7 +360,7 @@ export default function FormAudioUpdate() {
|
|||
// isYoutube: false,
|
||||
// isInternationalMedia: false,
|
||||
// };
|
||||
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
|
|
@ -362,6 +370,7 @@ export default function FormAudioUpdate() {
|
|||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
|
|
@ -560,50 +569,87 @@ export default function FormAudioUpdate() {
|
|||
setFiles([...filtered]);
|
||||
};
|
||||
|
||||
const fileList = files.map((file: any) => (
|
||||
<div
|
||||
key={file.id} // Gunakan ID file sebagai key
|
||||
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
/>
|
||||
</svg>{" "}
|
||||
<div>
|
||||
<div className="text-sm text-card-foreground">
|
||||
{file.fileName || file.name}
|
||||
</div>
|
||||
<div className="text-xs font-light text-muted-foreground">
|
||||
{Math.round(file.size / 100) / 10 > 1000 ? (
|
||||
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
|
||||
) : (
|
||||
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
|
||||
)}
|
||||
{" kb"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const getAudioUrl = (file: any) => {
|
||||
if (file instanceof File) {
|
||||
return URL.createObjectURL(file);
|
||||
}
|
||||
return file.secondaryUrl || file.fileUrl || null;
|
||||
};
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
variant="outline"
|
||||
className="border-none rounded-full"
|
||||
onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
|
||||
const fileList = files.map((file: any) => {
|
||||
const audioSrc = getAudioUrl(file);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id || file.name}
|
||||
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
));
|
||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||
|
||||
{audioSrc && (
|
||||
<audio controls className="w-full">
|
||||
<source src={audioSrc} type={file.type || "audio/mpeg"} />
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="self-end"
|
||||
onClick={() => handleRemoveFile(file)}
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// const fileList = files.map((file: any) => (
|
||||
// <div
|
||||
// key={file.id} // Gunakan ID file sebagai key
|
||||
// className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
// >
|
||||
// <div className="flex gap-3 items-center">
|
||||
// <svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// width="48"
|
||||
// height="48"
|
||||
// viewBox="0 0 20 20"
|
||||
// >
|
||||
// <path
|
||||
// fill="currentColor"
|
||||
// d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
// />
|
||||
// </svg>{" "}
|
||||
// <div>
|
||||
// <div className="text-sm text-card-foreground">
|
||||
// {file.fileName || file.name}
|
||||
// </div>
|
||||
// <div className="text-xs font-light text-muted-foreground">
|
||||
// {Math.round(file.size / 100) / 10 > 1000 ? (
|
||||
// <>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
|
||||
// ) : (
|
||||
// <>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
|
||||
// )}
|
||||
// {" kb"}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <Button
|
||||
// type="button"
|
||||
// size="icon"
|
||||
// color="destructive"
|
||||
// variant="outline"
|
||||
// className="border-none rounded-full"
|
||||
// onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
|
||||
// >
|
||||
// <Icon icon="tabler:x" className="h-5 w-5" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// ));
|
||||
|
||||
const handleCheckboxChangeImage = (fileId: number, value: string) => {
|
||||
setSelectedOptions((prev: any) => {
|
||||
|
|
@ -792,9 +838,45 @@ export default function FormAudioUpdate() {
|
|||
</Fragment>
|
||||
) : null}
|
||||
{prevFiles?.length > 0 &&
|
||||
prevFiles.map((file: any) => {
|
||||
const audioSrc = file.secondaryUrl || file.fileUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
|
||||
>
|
||||
<p className="text-sm font-medium truncate">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
{audioSrc ? (
|
||||
<audio controls className="w-full">
|
||||
<source src={audioSrc} type="audio/mpeg" />
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
) : (
|
||||
<p className="text-xs text-red-500">
|
||||
Audio source tidak tersedia
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="self-end"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* {prevFiles?.length > 0 &&
|
||||
prevFiles.map((file: any) => (
|
||||
<div
|
||||
key={file.id} // Gunakan ID file sebagai key
|
||||
key={file.id}
|
||||
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
|
|
@ -842,7 +924,7 @@ export default function FormAudioUpdate() {
|
|||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
{/* {files.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label className="text-lg font-semibold">
|
||||
|
|
@ -1012,6 +1094,70 @@ export default function FormAudioUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const isAllChecked =
|
||||
field.value?.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: field.value?.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...(field.value || []), option.id]
|
||||
: field.value?.filter(
|
||||
(val) => val !== option.id,
|
||||
) || [];
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) =>
|
||||
handleChange(e.target.checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.name}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -1079,7 +1225,7 @@ export default function FormAudioUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">
|
||||
|
|
|
|||
|
|
@ -94,23 +94,62 @@ type FileType = {
|
|||
};
|
||||
|
||||
type Detail = {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
htmlDescription: string;
|
||||
slug: string;
|
||||
category: {
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
typeId: number;
|
||||
tags: string;
|
||||
thumbnailUrl: string;
|
||||
pageUrl: string | null;
|
||||
createdById: number;
|
||||
createdByName: string;
|
||||
shareCount: number;
|
||||
viewCount: number;
|
||||
commentCount: number;
|
||||
aiArticleId: number | null;
|
||||
oldId: number;
|
||||
statusId: number;
|
||||
isBanner: boolean;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
slug: string | null;
|
||||
tags: string[];
|
||||
thumbnailPath: string | null;
|
||||
parentId: number;
|
||||
oldCategoryId: number | null;
|
||||
createdById: number;
|
||||
statusId: number;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isEnabled: boolean | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
// Legacy fields for backward compatibility
|
||||
category?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
categoryName: string;
|
||||
creatorName: string;
|
||||
thumbnailLink: string;
|
||||
tags: string;
|
||||
statusName: string;
|
||||
isPublish: boolean;
|
||||
needApprovalFromLevel: number;
|
||||
files: FileType[];
|
||||
uploadedById: number;
|
||||
creatorName?: string;
|
||||
thumbnailLink?: string;
|
||||
statusName?: string;
|
||||
needApprovalFromLevel?: number;
|
||||
uploadedById?: number;
|
||||
};
|
||||
|
||||
const ViewEditor = dynamic(
|
||||
|
|
@ -208,6 +247,16 @@ export default function FormTeksDetail() {
|
|||
initState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id: any) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const categoryRes = await listArticleCategories(1, 100);
|
||||
|
|
@ -244,6 +293,18 @@ export default function FormTeksDetail() {
|
|||
setFiles(details?.files || []);
|
||||
setDetail(details);
|
||||
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
// ✅ Aman untuk fileType
|
||||
setMain({
|
||||
type: details?.fileType?.name || "Unknown",
|
||||
|
|
@ -563,6 +624,58 @@ export default function FormTeksDetail() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
|
@ -584,7 +697,7 @@ export default function FormTeksDetail() {
|
|||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<SuggestionModal
|
||||
id={Number(id)}
|
||||
numberOfSuggestion={detail?.numberOfSuggestion}
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ export default function FormTeks() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 3,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
const response = await createArticle(articleData);
|
||||
console.log("Article Data Submitted:", articleData);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import * as z from "zod";
|
|||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -88,6 +89,12 @@ type Category = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
interface DetailFile {
|
||||
id: number;
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
type Detail = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -110,6 +117,7 @@ type Detail = {
|
|||
|
||||
interface FileWithPreview extends File {
|
||||
preview: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Option = {
|
||||
|
|
@ -153,6 +161,7 @@ export default function FormTeksUpdate() {
|
|||
[fileId: number]: string[];
|
||||
}>({});
|
||||
const [selectedTarget, setSelectedTarget] = useState("");
|
||||
const [detailFiles, setDetailFiles] = useState<DetailFile[]>([]);
|
||||
const [unitSelection, setUnitSelection] = useState({
|
||||
allUnit: false,
|
||||
mabes: false,
|
||||
|
|
@ -161,18 +170,26 @@ export default function FormTeksUpdate() {
|
|||
});
|
||||
const [publishedFor, setPublishedFor] = useState<string[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [existingFiles, setExistingFiles] = useState<DetailFile[]>([]);
|
||||
|
||||
let fileTypeId = "3";
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFiles(acceptedFiles.map((file) => Object.assign(file)));
|
||||
setFiles(
|
||||
acceptedFiles.map((file) =>
|
||||
Object.assign(file, {
|
||||
id: uuidv4(),
|
||||
preview: URL.createObjectURL(file),
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
accept: {
|
||||
"application/pdf": [],
|
||||
"application/msword": [], // .doc
|
||||
"application/msword": [],
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
[], // .docx
|
||||
[],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -219,9 +236,15 @@ export default function FormTeksUpdate() {
|
|||
setValue("creatorName", details.createdByName ?? "");
|
||||
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
setPublishedFor(details.publishedFor?.split(",") || []);
|
||||
if (details?.publishedFor) {
|
||||
const publishArr = details.publishedFor.split(",");
|
||||
|
||||
setPublishedFor(publishArr); // state lokal
|
||||
setValue("publishedFor", publishArr); // ← WAJIB untuk react-hook-form
|
||||
}
|
||||
|
||||
if (details?.files) {
|
||||
setFiles(details.files);
|
||||
setExistingFiles(details.files);
|
||||
const initialOptions: { [key: number]: string[] } = {};
|
||||
details.files.forEach((file: any) => {
|
||||
if (file.placements) {
|
||||
|
|
@ -278,7 +301,6 @@ export default function FormTeksUpdate() {
|
|||
|
||||
return allSelected ? ["all", ...options] : options;
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (id: string) => {
|
||||
if (id === "all") {
|
||||
// Select all options except "all"
|
||||
|
|
@ -326,6 +348,7 @@ export default function FormTeksUpdate() {
|
|||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
|
|
@ -679,152 +702,142 @@ export default function FormTeksUpdate() {
|
|||
)}
|
||||
</div>
|
||||
<div className="py-3 space-y-2">
|
||||
<Label>Select File</Label>
|
||||
{/* <Input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
/> */}
|
||||
<Fragment>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUpload className="text-default-300 w-10 h-10" />
|
||||
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
|
||||
{/* Drop files here or click to upload. */}
|
||||
Drag File
|
||||
</h4>
|
||||
<div className=" text-xs text-muted-foreground">
|
||||
Upload File Text Max
|
||||
<div className="py-3 space-y-2">
|
||||
<Label>Select File</Label>
|
||||
|
||||
<Fragment>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className="w-full text-center border-dashed border border-black rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUpload className="w-10 h-10" />
|
||||
<h4 className="text-2xl font-medium mt-3">
|
||||
Drag File
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 👇 TARUH DI SINI */}
|
||||
{files.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-lg font-semibold">
|
||||
File Baru
|
||||
</Label>
|
||||
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className="flex justify-between items-center border p-3 rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setFiles((prev) =>
|
||||
prev.filter(
|
||||
(item) => item.name !== file.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
|
||||
{/* {files.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label className="text-md font-semibold">
|
||||
File Media
|
||||
</Label>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{files.map((file: any, index: number) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center border p-2 rounded-md"
|
||||
>
|
||||
{file.preview ? (
|
||||
<img
|
||||
src={file.preview}
|
||||
alt={file.name}
|
||||
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-16 h-16"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-grow">
|
||||
<p className="font-medium">
|
||||
{file.fileName || file.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setFiles((prev) =>
|
||||
prev.filter((f) => f.id !== file.id),
|
||||
)
|
||||
}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{files.length ? (
|
||||
<Fragment>
|
||||
<div>{fileList}</div>
|
||||
<div className=" flex justify-between gap-2">
|
||||
{/* <div className="flex flex-row items-center gap-3 py-3">
|
||||
<Label>Watermark</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch defaultChecked color="primary" id="c2" />
|
||||
</div>
|
||||
</div> */}
|
||||
<Button
|
||||
color="destructive"
|
||||
onClick={handleRemoveAllFiles}
|
||||
)} */}
|
||||
|
||||
{/* Existing Files */}
|
||||
{existingFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-lg font-semibold">
|
||||
File Sebelumnya
|
||||
</Label>
|
||||
|
||||
{existingFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex justify-between items-center border p-3 rounded-md"
|
||||
>
|
||||
Remove All
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : null}
|
||||
{files.length > 0 && (
|
||||
<></>
|
||||
// <div className="mt-4 space-y-2">
|
||||
// <Label className="text-lg font-semibold">
|
||||
// {" "}
|
||||
// File Media
|
||||
// </Label>
|
||||
// <div className="grid gap-4">
|
||||
// {files.map((file: any) => (
|
||||
// <div
|
||||
// key={file.id}
|
||||
// className="flex items-center border p-2 rounded-md"
|
||||
// >
|
||||
// <img
|
||||
// src={file.thumbnailFileUrl}
|
||||
// alt={file.fileName}
|
||||
// className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
// />
|
||||
// <div className="flex flex-wrap gap-3 items-center ">
|
||||
// <div className="flex-grow">
|
||||
// <p className="font-medium">{file.fileName}</p>
|
||||
// <a
|
||||
// href={file.url}
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// className="text-blue-500 text-sm"
|
||||
// >
|
||||
// View File
|
||||
// </a>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Label className="flex items-center space-x-2">
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={selectedOptions[
|
||||
// file.id
|
||||
// ]?.includes("all")}
|
||||
// onChange={() =>
|
||||
// handleCheckboxChangeImage(
|
||||
// file.id,
|
||||
// "all"
|
||||
// )
|
||||
// }
|
||||
// className="form-checkbox"
|
||||
// />
|
||||
// <span>All</span>
|
||||
// </Label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Label className="flex items-center space-x-2">
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={selectedOptions[
|
||||
// file.id
|
||||
// ]?.includes("nasional")}
|
||||
// onChange={() =>
|
||||
// handleCheckboxChangeImage(
|
||||
// file.id,
|
||||
// "nasional"
|
||||
// )
|
||||
// }
|
||||
// className="form-checkbox"
|
||||
// />
|
||||
// <span>Nasional</span>
|
||||
// </Label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Label className="flex items-center space-x-2">
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={selectedOptions[
|
||||
// file.id
|
||||
// ]?.includes("wilayah")}
|
||||
// onChange={() =>
|
||||
// handleCheckboxChangeImage(
|
||||
// file.id,
|
||||
// "wilayah"
|
||||
// )
|
||||
// }
|
||||
// className="form-checkbox"
|
||||
// />
|
||||
// <span>Wilayah</span>
|
||||
// </Label>
|
||||
// </div>
|
||||
// <div>
|
||||
// <Label className="flex items-center space-x-2">
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={selectedOptions[
|
||||
// file.id
|
||||
// ]?.includes("internasional")}
|
||||
// onChange={() =>
|
||||
// handleCheckboxChangeImage(
|
||||
// file.id,
|
||||
// "internasional"
|
||||
// )
|
||||
// }
|
||||
// className="form-checkbox"
|
||||
// />
|
||||
// <span>Internasional</span>
|
||||
// </Label>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-10 h-10"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium">{file.fileName}</p>
|
||||
<a
|
||||
href={file.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 text-sm"
|
||||
>
|
||||
Lihat File
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
|
|
@ -852,11 +865,18 @@ export default function FormTeksUpdate() {
|
|||
{/* <div className="mt-3 px-3">
|
||||
<Label>Pratinjau Gambar Utama</Label>
|
||||
<Card className="mt-2">
|
||||
<img
|
||||
src={detail.thumbnailLink}
|
||||
alt="Thumbnail Gambar Utama"
|
||||
className="w-full h-auto rounded"
|
||||
/>
|
||||
{files.preview ? (
|
||||
<img
|
||||
src={files.preview}
|
||||
alt={files.name}
|
||||
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-16 h-16"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div> */}
|
||||
<div className="px-3 py-3">
|
||||
|
|
@ -898,6 +918,76 @@ export default function FormTeksUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const currentValue = field.value || [];
|
||||
|
||||
const isAllChecked =
|
||||
currentValue.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: currentValue.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...currentValue, option.id]
|
||||
: currentValue.filter(
|
||||
(val) => val !== option.id,
|
||||
);
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) =>
|
||||
handleChange(e.target.checked)
|
||||
}
|
||||
/>
|
||||
|
||||
<Label htmlFor={option.id}>
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -967,7 +1057,7 @@ export default function FormTeksUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ type Detail = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
published_for?: string;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -322,6 +323,20 @@ export default function FormImageDetail() {
|
|||
try {
|
||||
const response = await getArticleDetail(Number(id));
|
||||
const details = response?.data?.data;
|
||||
|
||||
console.log("DETAIL RESPONSE:", details);
|
||||
// ===== PARSE published_for =====
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
const mappedDetail: Detail = {
|
||||
...details,
|
||||
category:
|
||||
|
|
@ -351,6 +366,14 @@ export default function FormImageDetail() {
|
|||
setFiles(mappedFiles);
|
||||
setDetail(mappedDetail);
|
||||
|
||||
if (details?.published_for) {
|
||||
const publisherIds = details.published_for
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
if (mappedFiles && mappedFiles.length > 0) {
|
||||
setMain({
|
||||
type: "image",
|
||||
|
|
@ -369,13 +392,13 @@ export default function FormImageDetail() {
|
|||
|
||||
setDetailThumb(fileUrls);
|
||||
|
||||
if (details?.publishedForObject?.length > 0) {
|
||||
const publisherIds = details.publishedForObject
|
||||
.map((obj: any) => Number(obj.id))
|
||||
.filter((id: number) => id === 5 || id === 6);
|
||||
// if (details?.publishedForObject?.length > 0) {
|
||||
// const publisherIds = details.publishedForObject
|
||||
// .map((obj: any) => Number(obj.id))
|
||||
// .filter((id: number) => id === 4 || id === 5);
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
// setSelectedPublishers(publisherIds);
|
||||
// }
|
||||
|
||||
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
|
||||
setApproval(approvals?.data?.data);
|
||||
|
|
@ -773,36 +796,54 @@ export default function FormImageDetail() {
|
|||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* <Checkbox
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
onChange={() => handleCheckboxChange(5)}
|
||||
className="border"
|
||||
/> */}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="5">UMUM</Label>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* <Checkbox
|
||||
id="6"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
onChange={() => handleCheckboxChange(6)}
|
||||
className="border"
|
||||
/> */}
|
||||
<Checkbox
|
||||
id="6"
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<SuggestionModal
|
||||
|
|
|
|||
|
|
@ -557,7 +557,7 @@ export default function FormImage() {
|
|||
}
|
||||
}, [articleBody, setValue]);
|
||||
|
||||
const userId = Cookies.get("userId"); // atau dari auth context / localStorage
|
||||
const userId = Cookies.get("userId");
|
||||
|
||||
const save = async (data: ImageSchema) => {
|
||||
loading();
|
||||
|
|
@ -599,10 +599,10 @@ export default function FormImage() {
|
|||
|
||||
// ✅ Sesuaikan dengan struktur Swagger
|
||||
const articleData: CreateArticleData = {
|
||||
aiArticleId: 0, // default 0
|
||||
aiArticleId: 0,
|
||||
categoryIds: selectedCategory.toString(),
|
||||
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
createdById: Number(userId), // isi dengan userId valid
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
description: htmlToString(finalDescription),
|
||||
htmlDescription: finalDescription,
|
||||
isDraft: true,
|
||||
|
|
@ -614,9 +614,31 @@ export default function FormImage() {
|
|||
.replace(/[^a-z0-9-]/g, ""),
|
||||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 1, // Image content type
|
||||
typeId: 1,
|
||||
|
||||
// 🔥 TAMBAHKAN INI
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// const articleData: CreateArticleData = {
|
||||
// aiArticleId: 0, // default 0
|
||||
// categoryIds: selectedCategory.toString(),
|
||||
// createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
// createdById: Number(userId), // isi dengan userId valid
|
||||
// description: htmlToString(finalDescription),
|
||||
// htmlDescription: finalDescription,
|
||||
// isDraft: true,
|
||||
// isPublish: false,
|
||||
// oldId: 0,
|
||||
// slug: finalTitle
|
||||
// .toLowerCase()
|
||||
// .replace(/\s+/g, "-")
|
||||
// .replace(/[^a-z0-9-]/g, ""),
|
||||
// tags: finalTags,
|
||||
// title: finalTitle,
|
||||
// typeId: 1, // Image content type
|
||||
// };
|
||||
|
||||
let id = Cookies.get("idCreate");
|
||||
|
||||
if (id == undefined) {
|
||||
|
|
@ -1526,7 +1548,7 @@ export default function FormImage() {
|
|||
option.id === "all"
|
||||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import { useTranslations } from "next-intl";
|
|||
|
||||
const CustomEditor = dynamic(
|
||||
() => import("@/components/editor/custom-editor"),
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const imageSchema = z.object({
|
||||
|
|
@ -99,7 +99,7 @@ export default function FormImageUpdate() {
|
|||
setFiles((prev) => [
|
||||
...prev,
|
||||
...acceptedFiles.map((f) =>
|
||||
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) })
|
||||
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }),
|
||||
),
|
||||
]),
|
||||
accept: { "image/*": [] },
|
||||
|
|
@ -171,12 +171,13 @@ export default function FormImageUpdate() {
|
|||
const allOptions = options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
);
|
||||
} else {
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -233,6 +234,23 @@ export default function FormImageUpdate() {
|
|||
// router.push("/admin/content/image");
|
||||
// });
|
||||
// };
|
||||
const formatDateTime = (date: Date) => {
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
|
||||
return (
|
||||
date.getFullYear() +
|
||||
"-" +
|
||||
pad(date.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(date.getDate()) +
|
||||
" " +
|
||||
pad(date.getHours()) +
|
||||
":" +
|
||||
pad(date.getMinutes()) +
|
||||
":" +
|
||||
pad(date.getSeconds())
|
||||
);
|
||||
};
|
||||
|
||||
// 🔹 ganti fungsi save di FormImageUpdate.tsx
|
||||
const save = async (data: ImageSchema) => {
|
||||
|
|
@ -248,7 +266,10 @@ export default function FormImageUpdate() {
|
|||
const payload = {
|
||||
aiArticleId: detail?.aiArticleId ?? null,
|
||||
categoryIds: selectedTarget ? String(selectedTarget) : "",
|
||||
createdAt: detail?.createdAt ?? new Date().toISOString(),
|
||||
// createdAt: detail?.createdAt ?? new Date().toISOString(),
|
||||
createdAt: detail?.createdAt
|
||||
? detail.createdAt.replace("T", " ").split("+")[0]
|
||||
: formatDateTime(new Date()),
|
||||
createdById: detail?.createdById ?? null,
|
||||
description: htmlToString(descFinal),
|
||||
htmlDescription: descFinal,
|
||||
|
|
@ -261,9 +282,9 @@ export default function FormImageUpdate() {
|
|||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, ""),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags,
|
||||
tags: tags.join(", "),
|
||||
title: data.title,
|
||||
typeId: 1, // 1 = image (sesuai struktur kamu)
|
||||
typeId: 1,
|
||||
};
|
||||
|
||||
console.log("📤 Payload Update Article:", payload);
|
||||
|
|
@ -379,14 +400,14 @@ export default function FormImageUpdate() {
|
|||
!categories?.find(
|
||||
(cat) =>
|
||||
String(cat.id) ===
|
||||
String(detail.categoryId || detail?.category?.id)
|
||||
String(detail.categoryId || detail?.category?.id),
|
||||
) && (
|
||||
<SelectItem
|
||||
key={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
detail.categoryId || detail?.category?.id,
|
||||
)}
|
||||
value={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
detail.categoryId || detail?.category?.id,
|
||||
)}
|
||||
>
|
||||
{detail.categoryName || detail?.category?.name}
|
||||
|
|
@ -447,7 +468,7 @@ export default function FormImageUpdate() {
|
|||
className="flex justify-between border p-3 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Image
|
||||
{/* <Image
|
||||
src={
|
||||
file.thumbnailFileUrl ||
|
||||
file.preview ||
|
||||
|
|
@ -457,11 +478,25 @@ export default function FormImageUpdate() {
|
|||
width={64}
|
||||
height={64}
|
||||
className="rounded border"
|
||||
/> */}
|
||||
<Image
|
||||
src={
|
||||
file.preview || // file baru (dropzone)
|
||||
file.thumbnailUrl || // dari backend jika ada
|
||||
file.fileUrl || // fallback utama dari backend
|
||||
"/placeholder.png"
|
||||
}
|
||||
alt={file.fileName || file.name || "file"}
|
||||
width={64}
|
||||
height={64}
|
||||
className="rounded border object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-medium">{file.fileName}</p>
|
||||
<a
|
||||
href={file.url}
|
||||
href={file.fileUrl || file.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 text-sm"
|
||||
|
|
@ -554,6 +589,36 @@ export default function FormImageUpdate() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{options.map((opt) => {
|
||||
const isAllSelected =
|
||||
publishedFor.length ===
|
||||
options.filter((o) => o.id !== "all").length;
|
||||
|
||||
const isChecked =
|
||||
opt.id === "all"
|
||||
? isAllSelected
|
||||
: publishedFor.includes(opt.id);
|
||||
|
||||
return (
|
||||
<div key={opt.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={opt.id}
|
||||
value={opt.id}
|
||||
checked={isChecked}
|
||||
onChange={() => handleCheckboxChange(opt.id)}
|
||||
className="w-4 h-4 accent-black"
|
||||
/>
|
||||
<Label htmlFor={opt.id}>{opt.name}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{options.map((opt) => (
|
||||
|
|
@ -572,12 +637,19 @@ export default function FormImageUpdate() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button type="submit">Simpan</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
className="hover:bg-gray-300"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
<Button
|
||||
className="hover:bg-gray-300"
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export interface CreateArticleData {
|
|||
tags: string;
|
||||
title: string;
|
||||
typeId: number;
|
||||
publishedFor: string;
|
||||
}
|
||||
|
||||
// Interface for Article Category
|
||||
|
|
|
|||
Loading…
Reference in New Issue