Feature: Add Share-all-assets checkbox to admin dialogs, group tags, and visual Recommended flags on cards
This commit is contained in:
parent
9170b607fe
commit
c0c0e1a327
@ -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">
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user