318 lines
14 KiB
TypeScript
318 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { updateAsset, getTaxonomyMeta } from '../../../services/assets-api';
|
|
import type { Asset, TaxonomyMeta } from '../../../types/assets';
|
|
import Modal from '../../../components/ui/Modal';
|
|
import Button from '../../../components/ui/Button';
|
|
import { useToast } from '../../../hooks/use-toast';
|
|
|
|
interface EditAssetModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
asset: Asset | null;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export const EditAssetModal: React.FC<EditAssetModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
asset,
|
|
onSuccess
|
|
}) => {
|
|
const { success, error } = useToast();
|
|
const [taxonomyMeta, setTaxonomyMeta] = useState<TaxonomyMeta | null>(null);
|
|
|
|
const [editVerticalIds, setEditVerticalIds] = useState<string[]>([]);
|
|
const [editTechStackIds, setEditTechStackIds] = useState<string[]>([]);
|
|
const [editEngagementTypeIds, setEditEngagementTypeIds] = useState<string[]>([]);
|
|
const [editComplianceIds, setEditComplianceIds] = useState<string[]>([]);
|
|
|
|
const [editTitle, setEditTitle] = useState('');
|
|
const [editDescription, setEditDescription] = useState('');
|
|
const [editCategory, setEditCategory] = useState('Marketing');
|
|
const [editSubcategory, setEditSubcategory] = useState('');
|
|
const [editTags, setEditTags] = useState('');
|
|
const [editGithubUrl, setEditGithubUrl] = useState('');
|
|
const [editIsDownloadable, setEditIsDownloadable] = useState(true);
|
|
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
|
|
|
useEffect(() => {
|
|
getTaxonomyMeta().then(setTaxonomyMeta).catch(console.error);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (asset) {
|
|
setEditTitle(asset.title);
|
|
setEditDescription(asset.description || '');
|
|
setEditCategory(asset.categoryId || 'Marketing');
|
|
setEditSubcategory(asset.subcategory || '');
|
|
setEditTags(asset.tags.join(', '));
|
|
setEditGithubUrl(asset.githubUrl || '');
|
|
setEditIsDownloadable(asset.isDownloadable);
|
|
|
|
setEditVerticalIds(asset.verticals ? asset.verticals.map(v => v.id) : []);
|
|
setEditTechStackIds(asset.techStacks ? asset.techStacks.map(t => t.id) : []);
|
|
setEditEngagementTypeIds(asset.engagementTypes ? asset.engagementTypes.map(e => e.id) : []);
|
|
setEditComplianceIds(asset.complianceStandards ? asset.complianceStandards.map(c => c.id) : []);
|
|
}
|
|
}, [asset]);
|
|
|
|
const toggleSelection = (list: string[], item: string) => {
|
|
return list.includes(item) ? list.filter(i => i !== item) : [...list, item];
|
|
};
|
|
|
|
const handleEditSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!asset) return;
|
|
|
|
setIsSavingEdit(true);
|
|
try {
|
|
await updateAsset(asset.id, {
|
|
title: editTitle,
|
|
description: editDescription,
|
|
categoryId: editCategory,
|
|
subcategory: editSubcategory,
|
|
verticalIds: editVerticalIds,
|
|
techStackIds: editTechStackIds,
|
|
engagementTypeIds: editEngagementTypeIds,
|
|
complianceIds: editComplianceIds,
|
|
tags: editTags.split(',').map(t => t.trim()).filter(Boolean),
|
|
githubUrl: editGithubUrl,
|
|
isDownloadable: editIsDownloadable,
|
|
});
|
|
success('Changes saved successfully', 'Asset details have been updated.');
|
|
onSuccess();
|
|
onClose();
|
|
} catch (err: any) {
|
|
console.error('Failed to save asset details', err);
|
|
error('Failed to save changes', err.response?.data?.error || 'Something went wrong.');
|
|
} finally {
|
|
setIsSavingEdit(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen && !!asset}
|
|
onClose={onClose}
|
|
title="Edit Asset Details"
|
|
size="lg"
|
|
footer={
|
|
<>
|
|
<Button
|
|
type="button"
|
|
onClick={onClose}
|
|
variant="ghost"
|
|
size="sm"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
form="edit-asset-form"
|
|
disabled={isSavingEdit}
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
{isSavingEdit ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{asset && (
|
|
<form id="edit-asset-form" onSubmit={handleEditSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Asset Title</label>
|
|
<input
|
|
type="text"
|
|
value={editTitle}
|
|
onChange={(e) => setEditTitle(e.target.value)}
|
|
required
|
|
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
|
|
/>
|
|
</div>
|
|
|
|
{/* 4-Group Taxonomy Demarcation Selection */}
|
|
<div className="space-y-4 pt-3 pb-3 border-t border-b border-slate-200 dark:border-slate-800">
|
|
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
|
Taxonomy Classification (Strict Admin-Managed)
|
|
</h4>
|
|
|
|
{/* Group 1: Industry Verticals */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
|
|
1. Industry Verticals / Domains
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5 max-h-32 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
|
|
{(taxonomyMeta?.verticals || []).map(v => {
|
|
const isSelected = editVerticalIds.includes(v.id);
|
|
return (
|
|
<button
|
|
key={v.id}
|
|
type="button"
|
|
onClick={() => setEditVerticalIds(toggleSelection(editVerticalIds, v.id))}
|
|
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
|
|
isSelected
|
|
? 'bg-blue-600 text-white font-bold border-blue-600 shadow-sm ring-2 ring-blue-500/30'
|
|
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
{v.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Group 2: Tech Stack */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
|
|
2. Technology Stack & Capabilities
|
|
</label>
|
|
<div className="flex flex-col gap-2 max-h-40 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
|
|
{['Languages & Frameworks', 'AI & ML', 'Data & Backend', 'Cloud & Infra'].map(catName => {
|
|
const items = (taxonomyMeta?.techStacks || []).filter(t => t.category === catName || (!t.category && catName === 'Languages & Frameworks'));
|
|
if (items.length === 0) return null;
|
|
return (
|
|
<div key={catName} className="space-y-1">
|
|
<div className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
|
{catName}
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{items.map(t => {
|
|
const isSelected = editTechStackIds.includes(t.id);
|
|
return (
|
|
<button
|
|
key={t.id}
|
|
type="button"
|
|
onClick={() => setEditTechStackIds(toggleSelection(editTechStackIds, t.id))}
|
|
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
|
|
isSelected
|
|
? 'bg-purple-600 text-white font-bold border-purple-600 shadow-sm ring-2 ring-purple-500/30'
|
|
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
{t.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Group 3 & Group 4 Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{/* Group 3: Engagement Type */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
|
|
3. Engagement Type
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
|
|
{(taxonomyMeta?.engagementTypes || []).map(e => {
|
|
const isSelected = editEngagementTypeIds.includes(e.id);
|
|
return (
|
|
<button
|
|
key={e.id}
|
|
type="button"
|
|
onClick={() => setEditEngagementTypeIds(toggleSelection(editEngagementTypeIds, e.id))}
|
|
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
|
|
isSelected
|
|
? 'bg-sky-600 text-white font-bold border-sky-600 shadow-sm ring-2 ring-sky-500/30'
|
|
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
{e.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Group 4: Compliance Standards */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
|
|
4. Compliance & Governance
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
|
|
{(taxonomyMeta?.complianceStandards || []).map(c => {
|
|
const isSelected = editComplianceIds.includes(c.id);
|
|
return (
|
|
<button
|
|
key={c.id}
|
|
type="button"
|
|
onClick={() => setEditComplianceIds(toggleSelection(editComplianceIds, c.id))}
|
|
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
|
|
isSelected
|
|
? 'bg-emerald-600 text-white font-bold border-emerald-600 shadow-sm ring-2 ring-emerald-500/30'
|
|
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
{c.name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Description</label>
|
|
<textarea
|
|
value={editDescription}
|
|
onChange={(e) => setEditDescription(e.target.value)}
|
|
placeholder="Enter short description..."
|
|
rows={3}
|
|
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400 resize-none"
|
|
/>
|
|
</div>
|
|
|
|
{asset.type !== 'url' && (
|
|
<div className="flex items-center gap-3 p-3 bg-ink-50 border border-ink-200 rounded-lg">
|
|
<input
|
|
type="checkbox"
|
|
id="editIsDownloadable"
|
|
checked={editIsDownloadable}
|
|
onChange={(e) => setEditIsDownloadable(e.target.checked)}
|
|
className="w-4 h-4 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer"
|
|
/>
|
|
<div>
|
|
<label htmlFor="editIsDownloadable" className="text-xs font-bold text-ink-900 cursor-pointer block">
|
|
Allow Direct Download (Strict View Only if unchecked)
|
|
</label>
|
|
<span className="text-[10px] text-ink-500">
|
|
Toggle client authorization requirement for asset downloads.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Tags (comma-separated)</label>
|
|
<input
|
|
type="text"
|
|
value={editTags}
|
|
onChange={(e) => setEditTags(e.target.value)}
|
|
placeholder="branding, guideline, pitch"
|
|
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">GitHub/Documentation URL (Optional)</label>
|
|
<input
|
|
type="url"
|
|
value={editGithubUrl}
|
|
onChange={(e) => setEditGithubUrl(e.target.value)}
|
|
placeholder="https://github.com/..."
|
|
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
|
|
/>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|