Feature: Add Share-all-assets checkbox to admin dialogs, group tags, and visual Recommended flags on cards

This commit is contained in:
kenilkb 2026-07-16 13:19:50 +05:30
parent 9170b607fe
commit c0c0e1a327
3 changed files with 120 additions and 23 deletions

View File

@ -13,7 +13,8 @@ import {
Download,
Clock,
AlertCircle,
Globe
Globe,
Sparkles
} from 'lucide-react';
import type { Asset } from '../../../types/assets';
import type { User } from '../../../types/auth';
@ -47,6 +48,7 @@ interface AssetCardProps {
onToggleSelect?: (assetId: string) => void;
isExpanded?: boolean;
onToggleExpand?: () => void;
isRecommended?: boolean;
}
export const AssetCard: React.FC<AssetCardProps> = ({
@ -64,7 +66,8 @@ export const AssetCard: React.FC<AssetCardProps> = ({
isSelected = false,
onToggleSelect,
isExpanded = false,
onToggleExpand
onToggleExpand,
isRecommended = false
}) => {
const isMenuOpen = activeMenuId === asset.id;
const canDirectDownload = user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED';
@ -169,6 +172,12 @@ export const AssetCard: React.FC<AssetCardProps> = ({
className="w-3.5 h-3.5 rounded border-ink-300 bg-ink-0 text-ink-900 focus:ring-ink-900/10 cursor-pointer shadow-sm"
/>
)}
{isRecommended && (
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-500 text-ink-950 text-[9px] font-extrabold uppercase tracking-wider shadow-sm border border-amber-400">
<Sparkles className="w-2.5 h-2.5 fill-ink-950" />
<span>Recommended</span>
</div>
)}
</div>
<div className="absolute top-2 right-2 z-10 flex items-center gap-1">

View File

@ -224,13 +224,19 @@ export const AssetsPage = () => {
}
};
const assignedGroups = user?.partnerGroup && user.role === "PARTNER_USER"
? user.partnerGroup.split(',').map(s => s.trim())
: [];
const CATEGORIES = [
"ALL",
...assignedGroups,
...Array.from(
new Set(
assets
.map((asset) => asset.categoryId || "General")
.filter((catId): catId is string => !!catId)
.filter(catId => !assignedGroups.some(g => g.toLowerCase() === catId.toLowerCase()))
)
).sort()
];
@ -248,10 +254,13 @@ export const AssetsPage = () => {
asset.type.toLowerCase().includes(query) ||
asset.tags.some((tag) => tag.toLowerCase().includes(query));
const groupForFilter = groups.find(g => g.name.toLowerCase() === selectedCategory.toLowerCase());
const matchesCategory =
selectedCategory === "ALL" ||
asset.categoryId === selectedCategory ||
(!asset.categoryId && selectedCategory === "General");
(groupForFilter
? groupForFilter.assets.some(a => a.id === asset.id)
: (asset.categoryId === selectedCategory || (!asset.categoryId && selectedCategory === "General"))
);
return matchesSearch && matchesCategory;
});
@ -296,19 +305,26 @@ export const AssetsPage = () => {
</div>
<div className="flex gap-1.5 overflow-x-auto pb-1 sm:pb-0 scrollbar-none">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-2.5 py-1.5 rounded-lg border text-[10px] uppercase tracking-wider font-bold transition-all whitespace-nowrap shadow-sm cursor-pointer ${
selectedCategory === cat
? "bg-ink-900 text-ink-0 border-ink-900"
: "bg-ink-50 text-ink-700 border-ink-200 hover:bg-ink-100"
}`}
>
{cat}
</button>
))}
{CATEGORIES.map((cat) => {
const isGroup = assignedGroups.some(g => g.toLowerCase() === cat.toLowerCase());
return (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-2.5 py-1.5 rounded-lg border text-[10px] uppercase tracking-wider font-bold transition-all whitespace-nowrap shadow-sm cursor-pointer ${
selectedCategory === cat
? isGroup
? "bg-amber-500 text-ink-950 border-amber-500 font-extrabold"
: "bg-ink-900 text-ink-0 border-ink-900"
: isGroup
? "bg-amber-500/10 text-amber-400 border-amber-500/20 hover:bg-amber-500/20"
: "bg-ink-50 text-ink-700 border-ink-200 hover:bg-ink-100"
}`}
>
{isGroup ? `${cat}` : cat}
</button>
);
})}
</div>
</div>
@ -403,9 +419,16 @@ export const AssetsPage = () => {
<div className="absolute top-0 right-0 w-64 h-64 bg-ink-800/10 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 relative z-10 mb-6">
<div>
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-xs font-bold text-amber-400 uppercase tracking-wider mb-3">
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
<span>Curated for {user?.partnerGroup}</span>
<div className="flex flex-wrap items-center gap-2 mb-3">
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-xs font-bold text-amber-400 uppercase tracking-wider">
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
<span>Curated for You</span>
</div>
{(user?.partnerGroup ? user.partnerGroup.split(',').map(s => s.trim()) : []).map(g => (
<span key={g} className="px-2.5 py-0.5 rounded-full text-xs font-extrabold uppercase tracking-wider bg-ink-800 text-amber-400 border border-ink-700 shadow-sm">
{g}
</span>
))}
</div>
<h2 className="text-2xl font-bold text-ink-0 font-sans tracking-tight">Recommended for You</h2>
<p className="text-slate-400 text-sm mt-1 max-w-xl">
@ -439,6 +462,7 @@ export const AssetsPage = () => {
onToggleSelect={handleToggleSelectAsset}
isExpanded={expandedAssetId === asset.id}
onToggleExpand={() => setExpandedAssetId(expandedAssetId === asset.id ? null : asset.id)}
isRecommended={true}
/>
</motion.div>
))}
@ -485,6 +509,7 @@ export const AssetsPage = () => {
onToggleSelect={handleToggleSelectAsset}
isExpanded={expandedAssetId === asset.id}
onToggleExpand={() => setExpandedAssetId(expandedAssetId === asset.id ? null : asset.id)}
isRecommended={recommendedAssets.some(r => r.id === asset.id)}
/>
</motion.div>
))}

View File

@ -136,6 +136,16 @@ export const DirectoryPage: React.FC = () => {
const [mfaRequired, setMfaRequired] = useState(true);
const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]);
const [inviteAssetSearch, setInviteAssetSearch] = useState("");
const [shareAllAssets, setShareAllAssets] = useState(true);
// Helper for sync checkbox
const handleSetSelectedAssetIds = (value: React.SetStateAction<string[]>) => {
setSelectedAssetIds(prev => {
const next = typeof value === 'function' ? (value as Function)(prev) : value;
setShareAllAssets(next.length === allAssets.length && allAssets.length > 0);
return next;
});
};
// Editing partner states
const [editingPartner, setEditingPartner] = useState<any>(null);
@ -146,6 +156,16 @@ export const DirectoryPage: React.FC = () => {
const [editAssetIds, setEditAssetIds] = useState<string[]>([]);
const [editAssetSearch, setEditAssetSearch] = useState("");
const [isEditOpen, setIsEditOpen] = useState(false);
const [editShareAll, setEditShareAll] = useState(false);
// Helper for edit sync checkbox
const handleSetEditAssetIds = (value: React.SetStateAction<string[]>) => {
setEditAssetIds(prev => {
const next = typeof value === 'function' ? (value as Function)(prev) : value;
setEditShareAll(next.length === allAssets.length && allAssets.length > 0);
return next;
});
};
const [assetGroups, setAssetGroups] = useState<AssetGroup[]>([]);
const [selectedPartnerForAssets, setSelectedPartnerForAssets] = useState<any | null>(null);
const [isPartnerAssetsOpen, setIsPartnerAssetsOpen] = useState(false);
@ -601,6 +621,7 @@ export const DirectoryPage: React.FC = () => {
setAssignedMsaId("");
setMfaRequired(true);
setSelectedAssetIds(allAssets.map(a => a.id));
setShareAllAssets(true);
setInviteAssetSearch("");
setIsInviteOpen(true);
}}
@ -894,7 +915,9 @@ export const DirectoryPage: React.FC = () => {
setEditNdaId(partner.assignedNdaId === null ? "NONE" : partner.assignedNdaId || "");
setEditMsaId(partner.assignedMsaId === null ? "NONE" : partner.assignedMsaId || "");
setEditMfaEnabled(partner.mfaEnabled);
setEditAssetIds(partner.sharedAssets?.map((sa: any) => sa.assetId) || []);
const sharedIds = partner.sharedAssets?.map((sa: any) => sa.assetId) || [];
setEditAssetIds(sharedIds);
setEditShareAll(sharedIds.length === allAssets.length && allAssets.length > 0);
setIsEditOpen(true);
}}
className="p-1.5 text-ink-600 hover:text-ink-950 hover:bg-ink-100 rounded-lg transition-colors cursor-pointer"
@ -1265,7 +1288,27 @@ export const DirectoryPage: React.FC = () => {
</span>
)}
</div>
{renderAssetSelector(selectedAssetIds, setSelectedAssetIds, inviteAssetSearch, setInviteAssetSearch, handleToggleInviteGroup)}
<div className="mb-2.5 flex items-center gap-2 p-2.5 bg-ink-50 border border-ink-200 rounded-lg">
<input
type="checkbox"
id="share-all-invite"
checked={shareAllAssets}
onChange={(e) => {
const checked = e.target.checked;
setShareAllAssets(checked);
if (checked) {
setSelectedAssetIds(allAssets.map(a => a.id));
} else {
setSelectedAssetIds([]);
}
}}
className="rounded border-ink-300 text-ink-900 focus:ring-ink-950 w-4 h-4 cursor-pointer"
/>
<label htmlFor="share-all-invite" className="text-xs font-bold text-ink-900 cursor-pointer select-none">
Share all catalog assets by default
</label>
</div>
{renderAssetSelector(selectedAssetIds, handleSetSelectedAssetIds, inviteAssetSearch, setInviteAssetSearch, handleToggleInviteGroup)}
</div>
{inviteResult?.error && (
@ -1429,7 +1472,27 @@ export const DirectoryPage: React.FC = () => {
</span>
)}
</div>
{renderAssetSelector(editAssetIds, setEditAssetIds, editAssetSearch, setEditAssetSearch, handleToggleEditGroup)}
<div className="mb-2.5 flex items-center gap-2 p-2.5 bg-ink-50 border border-ink-200 rounded-lg">
<input
type="checkbox"
id="share-all-edit"
checked={editShareAll}
onChange={(e) => {
const checked = e.target.checked;
setEditShareAll(checked);
if (checked) {
setEditAssetIds(allAssets.map(a => a.id));
} else {
setEditAssetIds([]);
}
}}
className="rounded border-ink-300 text-ink-900 focus:ring-ink-950 w-4 h-4 cursor-pointer"
/>
<label htmlFor="share-all-edit" className="text-xs font-bold text-ink-900 cursor-pointer select-none">
Share all catalog assets by default
</label>
</div>
{renderAssetSelector(editAssetIds, handleSetEditAssetIds, editAssetSearch, setEditAssetSearch, handleToggleEditGroup)}
</div>
<div className="flex items-center gap-3 pt-2">