Fix Agency Admin campaign creation and proof upload permissions

Switch canWrite from blacklist (role !== 'oversight_admin') to explicit
whitelist (super_admin, agency_admin, basic_user) for clearer permission
logic. Propagate readOnly prop to CampaignDetail and ProofDetailView
subcomponents so upload/delete buttons are properly hidden for read-only
roles at all navigation levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael 2026-02-22 07:23:58 -06:00
parent 3207ec301c
commit a08d54ec6d
2 changed files with 61 additions and 49 deletions

View file

@ -1025,8 +1025,8 @@ const CampaignDeleteConfirmationModal: React.FC<{
);
};
const CampaignDetail: React.FC<{
campaignName: string;
const CampaignDetail: React.FC<{
campaignName: string;
onBack: () => void;
onSelectProof: (proof: any) => void;
campaignProofs: { [key: string]: any[] };
@ -1034,7 +1034,8 @@ const CampaignDetail: React.FC<{
dropdownOptions: DropdownOptions;
onRetryAnalysis: (campaignName: string, tempId: string) => void;
onDeleteProof: (campaignName: string, proofName: string) => void;
}> = ({ campaignName, onBack, onSelectProof, campaignProofs, onProofUpload, dropdownOptions, onRetryAnalysis, onDeleteProof }) => {
readOnly?: boolean;
}> = ({ campaignName, onBack, onSelectProof, campaignProofs, onProofUpload, dropdownOptions, onRetryAnalysis, onDeleteProof, readOnly = false }) => {
const [isUploadFormVisible, setIsUploadFormVisible] = useState(false);
const [proofToDelete, setProofToDelete] = useState<any | null>(null);
const [proofForUpload, setProofForUpload] = useState<any | null>(null);
@ -1206,13 +1207,15 @@ const CampaignDetail: React.FC<{
{isExporting ? <SpinnerIcon className="h-5 w-5 custom-spinner" /> : <ExportIcon className="h-5 w-5" />}
{isExporting ? 'Exporting...' : 'Export Campaign Report'}
</button>
<button
onClick={() => setIsUploadFormVisible(true)}
className="flex items-center gap-2 bg-active-blue text-white font-semibold py-2 px-4 rounded-full hover:bg-active-blue/90 transition-colors duration-300"
>
<PlusIcon className="h-5 w-5" />
Upload New Proof
</button>
{!readOnly && (
<button
onClick={() => setIsUploadFormVisible(true)}
className="flex items-center gap-2 bg-active-blue text-white font-semibold py-2 px-4 rounded-full hover:bg-active-blue/90 transition-colors duration-300"
>
<PlusIcon className="h-5 w-5" />
Upload New Proof
</button>
)}
</div>
<div className="bg-white rounded-[10px] shadow-md overflow-hidden border border-grey-300">
@ -1310,14 +1313,16 @@ const CampaignDetail: React.FC<{
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={(e) => handleNewVersionClick(e, proof)}
className="p-2 text-grey-700 rounded-full hover:bg-info-light hover:text-active-blue transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={`Upload new version for ${proof.proofName}`}
disabled={isUploading || isExporting}
>
<UploadIcon className="h-5 w-5" />
</button>
{!readOnly && (
<button
onClick={(e) => handleNewVersionClick(e, proof)}
className="p-2 text-grey-700 rounded-full hover:bg-info-light hover:text-active-blue transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={`Upload new version for ${proof.proofName}`}
disabled={isUploading || isExporting}
>
<UploadIcon className="h-5 w-5" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
@ -1329,17 +1334,19 @@ const CampaignDetail: React.FC<{
>
<PDFIcon className="h-5 w-5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
setProofToDelete(proof);
}}
disabled={isExporting}
className="p-2 text-grey-700 rounded-full hover:bg-error-light hover:text-error transition-colors disabled:opacity-50"
title={`Delete ${proof.proofName}`}
>
<TrashIcon className="h-5 w-5" />
</button>
{!readOnly && (
<button
onClick={(e) => {
e.stopPropagation();
setProofToDelete(proof);
}}
disabled={isExporting}
className="p-2 text-grey-700 rounded-full hover:bg-error-light hover:text-error transition-colors disabled:opacity-50"
title={`Delete ${proof.proofName}`}
>
<TrashIcon className="h-5 w-5" />
</button>
)}
</div>
</td>
</tr>
@ -1364,7 +1371,8 @@ const ProofDetailView: React.FC<{
onResolveSubmit: (resolveData: Omit<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => void;
flaggedItems: FlaggedItem[];
resolvedItems: ResolvedItem[];
}> = ({ campaignName, proof, onBack, onNewVersionUpload, isUploadingNewVersion, onFlagSubmit, onResolveSubmit, flaggedItems, resolvedItems }) => {
readOnly?: boolean;
}> = ({ campaignName, proof, onBack, onNewVersionUpload, isUploadingNewVersion, onFlagSubmit, onResolveSubmit, flaggedItems, resolvedItems, readOnly = false }) => {
const getInitialVersionIndex = () => {
if (proof.initialVersion && proof.versions) {
@ -1621,24 +1629,26 @@ const ProofDetailView: React.FC<{
</>
)}
</button>
<button
onClick={handleUploadClick}
disabled={isUploadingNewVersion}
className="flex items-center gap-2 text-sm bg-white text-active-blue font-semibold py-1.5 px-3 rounded-full border-2 border-active-blue hover:bg-active-blue hover:text-white transition-colors duration-200 disabled:bg-grey-300 disabled:text-grey-700 disabled:border-grey-300 disabled:cursor-wait"
title="Upload a new version of this proof"
>
{isUploadingNewVersion ? (
<>
<SpinnerIcon className="h-4 w-4 custom-spinner" />
Uploading...
</>
) : (
<>
<UploadIcon className="h-4 w-4" />
New Version
</>
)}
</button>
{!readOnly && (
<button
onClick={handleUploadClick}
disabled={isUploadingNewVersion}
className="flex items-center gap-2 text-sm bg-white text-active-blue font-semibold py-1.5 px-3 rounded-full border-2 border-active-blue hover:bg-active-blue hover:text-white transition-colors duration-200 disabled:bg-grey-300 disabled:text-grey-700 disabled:border-grey-300 disabled:cursor-wait"
title="Upload a new version of this proof"
>
{isUploadingNewVersion ? (
<>
<SpinnerIcon className="h-4 w-4 custom-spinner" />
Uploading...
</>
) : (
<>
<UploadIcon className="h-4 w-4" />
New Version
</>
)}
</button>
)}
</div>
<input
type="file"
@ -1778,6 +1788,7 @@ export const Campaigns: React.FC<CampaignsProps> = ({
onResolveSubmit={onResolveSubmit}
flaggedItems={flaggedItems}
resolvedItems={resolvedItems}
readOnly={readOnly}
/>;
}
@ -1791,6 +1802,7 @@ export const Campaigns: React.FC<CampaignsProps> = ({
dropdownOptions={dropdownOptions}
onRetryAnalysis={onRetryAnalysis}
onDeleteProof={onDeleteProof}
readOnly={readOnly}
/>;
}

View file

@ -70,7 +70,7 @@ export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children
isLoading,
isSuperAdmin: role === 'super_admin',
isOversightAdmin: role === 'oversight_admin',
canWrite: role !== 'oversight_admin' && role != null,
canWrite: role === 'super_admin' || role === 'agency_admin' || role === 'basic_user',
canSeeAnalytics: role === 'super_admin' || role === 'oversight_admin' || role === 'agency_admin',
canSeeAuditing: role === 'super_admin' || role === 'oversight_admin',
canSeeKnowledgeBase: role === 'super_admin',