Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions rafts/frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,25 @@
"tooltip_rejected": "This RAFTS was rejected and needs revision",
"tooltip_review_ready": "This RAFTS is ready for review",
"tooltip_in_review": "This RAFTS has been submitted and is awaiting review",
"tooltip_draft": "This is a draft RAFTS that has not been submitted for review"
"tooltip_draft": "This is a draft RAFTS that has not been submitted for review",
"publishing": "Publishing...",
"publish_error": "Publish Error",
"locking data directory": "Publishing...",
"locked data directory": "Publishing...",
"registering to DataCite": "Publishing...",
"error locking data directory": "Error Locking Data",
"error registering to DataCite": "Error Registering",
"tooltip_publishing": "This RAFTS is being published — locking data and registering DOI",
"tooltip_publish_error": "An error occurred during the publishing process"
},
"review_page": {
"filter_by_status": "Filter by status:",
"status_all": "All",
"status_review_ready": "Ready for Review",
"status_under_review": "Under Review",
"status_approved": "Approved",
"status_rejected": "Rejected"
"status_rejected": "Rejected",
"status_publishing": "Publishing"
},
"raft_details": {
"overview": "Overview",
Expand Down
15 changes: 13 additions & 2 deletions rafts/frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,14 +347,25 @@
"tooltip_rejected": "Ce RAFTS a été rejeté et nécessite une révision",
"tooltip_review_ready": "Ce RAFTS est prêt pour révision",
"tooltip_in_review": "Ce RAFTS a été soumis et attend la révision",
"tooltip_draft": "Ceci est un brouillon RAFTS qui n'a pas été soumis pour révision"
"tooltip_draft": "Ceci est un brouillon RAFTS qui n'a pas été soumis pour révision",
"publishing": "Publication...",
"publish_error": "Erreur de publication",
"locking data directory": "Publication...",
"locked data directory": "Publication...",
"registering to DataCite": "Publication...",
"error locking data directory": "Erreur de verrouillage des données",
"error registering to DataCite": "Erreur d'enregistrement",
"tooltip_publishing": "Ce RAFTS est en cours de publication — verrouillage des données et enregistrement du DOI",
"tooltip_publish_error": "Une erreur s'est produite lors du processus de publication"
},
"review_page": {
"filter_by_status": "Filtrer par statut :",
"status_all": "Tous",
"status_review_ready": "Prêt pour révision",
"status_under_review": "En cours de révision",
"status_approved": "Approuvé",
"status_rejected": "Rejeté"
"status_rejected": "Rejeté",
"status_publishing": "Publication"
},
"raft_details": {
"overview": "Aperçu",
Expand Down
71 changes: 19 additions & 52 deletions rafts/frontend/src/actions/assignReviewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ import { SUBMIT_DOI_URL, MESSAGE, SUCCESS } from '@/actions/constants'
import { createDoiFormData } from '@/actions/utils/doiFormData'
import { IResponseData } from '@/actions/types'
import { BACKEND_STATUS } from '@/shared/backendStatus'
import { updateRaftMetadata } from '@/services/canfarStorage'
import { RaftStatusChange } from '@/types/doi'
import { getDOICurrentStatus } from '@/actions/updateDOIStatus'

/**
* Assigns a reviewer to a RAFT/DOI.
Expand Down Expand Up @@ -154,7 +153,6 @@ export const assignReviewer = async (
*/
export const claimForReview = async (
doiId: string,
dataDirectory?: string,
): Promise<IResponseData<string>> => {
try {
const session = await auth()
Expand All @@ -169,6 +167,12 @@ export const claimForReview = async (
}

const reviewerName = session.user.name

// Log current status before claiming
const beforeStatus = await getDOICurrentStatus(doiId, accessToken)
console.log(`[claimForReview] ${doiId}: "${beforeStatus}" → "${BACKEND_STATUS.IN_REVIEW}" (reviewer: ${reviewerName})`)

// Claim via DOI backend API (updates VOSpace node properties)
const url = `${SUBMIT_DOI_URL}/${doiId}`

// Backend supports setting both reviewer and status in one call
Expand All @@ -186,29 +190,9 @@ export const claimForReview = async (
})

if (response.status === 303 || response.ok) {
// Update RAFT.json metadata with status history
if (dataDirectory) {
try {
const statusChange: RaftStatusChange = {
fromStatus: BACKEND_STATUS.REVIEW_READY,
toStatus: BACKEND_STATUS.IN_REVIEW,
changedBy: reviewerName,
changedAt: new Date().toISOString(),
}

await updateRaftMetadata(
dataDirectory,
{
updatedAt: new Date().toISOString(),
updatedBy: reviewerName,
statusHistory: [statusChange],
},
accessToken,
)
} catch (metaError) {
console.warn('[claimForReview] Metadata update failed (non-critical):', metaError)
}
}
// Verify status after claim
const afterStatus = await getDOICurrentStatus(doiId, accessToken)
console.log(`[claimForReview] ${doiId}: confirmed status is now "${afterStatus}"`)

return {
[SUCCESS]: true,
Expand All @@ -217,7 +201,7 @@ export const claimForReview = async (
}

const errorText = await response.text().catch(() => '')
console.error('[claimForReview] Error response:', response.status, errorText)
console.error(`[claimForReview] ${doiId}: POST failed ${response.status}`, errorText)
return {
[SUCCESS]: false,
[MESSAGE]: `Failed to claim for review: ${response.status} ${errorText}`,
Expand All @@ -244,7 +228,6 @@ export const claimForReview = async (
*/
export const releaseReview = async (
doiId: string,
dataDirectory?: string,
): Promise<IResponseData<string>> => {
try {
const session = await auth()
Expand All @@ -254,6 +237,11 @@ export const releaseReview = async (
return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' }
}

// Log current status before releasing
const beforeStatus = await getDOICurrentStatus(doiId, accessToken)
console.log(`[releaseReview] ${doiId}: "${beforeStatus}" → "${BACKEND_STATUS.REVIEW_READY}"`)

// Release via DOI backend API (updates VOSpace node properties)
const url = `${SUBMIT_DOI_URL}/${doiId}`

// Send empty reviewer and status back to review ready
Expand All @@ -271,34 +259,13 @@ export const releaseReview = async (
})

if (response.status === 303 || response.ok) {
// Update RAFT.json metadata with status history
if (dataDirectory) {
try {
const statusChange: RaftStatusChange = {
fromStatus: BACKEND_STATUS.IN_REVIEW,
toStatus: BACKEND_STATUS.REVIEW_READY,
changedBy: session?.user?.name || '',
changedAt: new Date().toISOString(),
}

await updateRaftMetadata(
dataDirectory,
{
updatedAt: new Date().toISOString(),
updatedBy: session?.user?.name || '',
statusHistory: [statusChange],
},
accessToken,
)
} catch (metaError) {
console.warn('[releaseReview] Metadata update failed (non-critical):', metaError)
}
}

const afterStatus = await getDOICurrentStatus(doiId, accessToken)
console.log(`[releaseReview] ${doiId}: confirmed status is now "${afterStatus}"`)
return { [SUCCESS]: true, data: 'Review released successfully' }
}

const errorText = await response.text().catch(() => '')
console.error(`[releaseReview] ${doiId}: POST failed ${response.status}`, errorText)
return {
[SUCCESS]: false,
[MESSAGE]: `Failed to release review: ${response.status} ${errorText}`,
Expand Down
34 changes: 34 additions & 0 deletions rafts/frontend/src/actions/fetchDoiCitationXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use server'

import { STORAGE_VAULT_FILE_URL, DOI_XML_PREFIX } from '@/utilities/constants'

/**
* Fetches the DataCite citation XML for a RAFT from the VOSpace vault.
* Proxied through the server to avoid CORS restrictions.
*/
export const fetchDoiCitationXml = async (
dataDirectory: string,
): Promise<{ success: boolean; xml?: string; error?: string }> => {
try {
const raftRootDir = dataDirectory.replace(/\/data\/?$/, '')
const raftFolderName = raftRootDir.split('/').pop()
if (!raftFolderName) {
return { success: false, error: 'Could not determine RAFT folder name' }
}

const url = `${STORAGE_VAULT_FILE_URL}${raftRootDir.startsWith('/') ? '' : '/'}${raftRootDir}/${DOI_XML_PREFIX}${raftFolderName}.xml`
console.log(`[fetchDoiCitationXml] Fetching: ${url}`)

const response = await fetch(url, { redirect: 'follow' })

if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` }
}

const xml = await response.text()
return { success: true, xml }
} catch (error) {
console.error('[fetchDoiCitationXml] Error:', error)
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
}
}
36 changes: 30 additions & 6 deletions rafts/frontend/src/actions/getDOIRAFT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { SUBMIT_DOI_URL } from '@/actions/constants'
import { parseXmlToJson } from '@/utilities/xmlParser'
import { DOIData } from '@/types/doi'
import { dataCiteToRaft, parseDataCiteXml } from '@/utilities/dataCiteToRaft'
import { getDOICurrentStatus } from '@/actions/updateDOIStatus'

export const getDOIRaft = async (dataIdentifier: string): Promise<IResponseData<TRaftContext>> => {
try {
Expand Down Expand Up @@ -104,17 +105,40 @@ export const getDOIRaft = async (dataIdentifier: string): Promise<IResponseData<
const doiDataList: DOIData[] = await parseXmlToJson(xmlString)

// Find the DOI entry matching the dataIdentifier (identifier ends with the dataIdentifier)
const matchingDoi = doiDataList.find((doi) => doi.identifier.endsWith(`/${dataIdentifier}`))
let matchingDoi = doiDataList.find((doi) => doi.identifier.endsWith(`/${dataIdentifier}`))

// Fallback: DOI may not appear in the list during minting transitions.
// Use the dedicated /status endpoint to get the current status.
if (!matchingDoi) {
console.error('[getDOIRaft] No matching DOI found for:', dataIdentifier)
return { success: false, message: `No matching DOI found for: ${dataIdentifier}` }
console.warn(`[getDOIRaft] ${dataIdentifier}: not in list, trying /status endpoint`)
const status = await getDOICurrentStatus(dataIdentifier, accessToken)
if (status) {
matchingDoi = {
identifier: dataIdentifier,
identifierType: 'DOI',
title: '',
titleLang: null,
status,
dataDirectory: '',
journalRef: null,
reviewer: null,
}
console.log(`[getDOIRaft] ${dataIdentifier}: recovered status="${status}" from /status endpoint`)
} else {
console.error('[getDOIRaft] No matching DOI found for:', dataIdentifier)
return { success: false, message: `No matching DOI found for: ${dataIdentifier}` }
}
} else {
console.log(`[getDOIRaft] ${dataIdentifier}: backend status="${matchingDoi.status}", reviewer="${matchingDoi.reviewer || 'none'}"`)
}

// Try to download RAFT.json first
const response = await downloadRaftFile(matchingDoi.dataDirectory, accessToken)

if (response.success && response.data) {
// Try to download RAFT.json first (skip if dataDirectory is not available)
const response = matchingDoi.dataDirectory
? await downloadRaftFile(matchingDoi.dataDirectory, accessToken)
: null

if (response?.success && response.data) {
// Override status from DOI list (RAFT.json may have stale status)
const raftData = response.data
if (raftData.generalInfo && matchingDoi.status) {
Expand Down
71 changes: 54 additions & 17 deletions rafts/frontend/src/actions/getDOIsForReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,38 @@ import {
OPTION_UNDER_REVIEW,
OPTION_APPROVED,
OPTION_REJECTED,
OPTION_ALL,
OPTION_PUBLISHING,
} from '@/shared/constants'

import { BACKEND_STATUS } from '@/shared/backendStatus'

// Map frontend status constants to backend status values
const STATUS_MAPPING: Record<string, string> = {
[OPTION_REVIEW]: BACKEND_STATUS.REVIEW_READY, // review_ready -> review ready (waiting for reviewer)
[OPTION_UNDER_REVIEW]: BACKEND_STATUS.IN_REVIEW, // under_review -> in review (reviewer claimed)
[OPTION_APPROVED]: BACKEND_STATUS.APPROVED, // approved -> approved
[OPTION_REJECTED]: BACKEND_STATUS.REJECTED, // rejected -> rejected
// Backend statuses that count as "publishing" (intermediate minting states)
const PUBLISHING_STATUSES: string[] = [
BACKEND_STATUS.LOCKING_DATA,
BACKEND_STATUS.ERROR_LOCKING_DATA,
BACKEND_STATUS.LOCKED_DATA,
BACKEND_STATUS.REGISTERING,
BACKEND_STATUS.ERROR_REGISTERING,
]

// All statuses visible on the review page (everything except draft and minted)
const ALL_REVIEW_STATUSES = [
BACKEND_STATUS.REVIEW_READY,
BACKEND_STATUS.IN_REVIEW,
BACKEND_STATUS.APPROVED,
BACKEND_STATUS.REJECTED,
...PUBLISHING_STATUSES,
]

// Map frontend filter options to backend status values
const STATUS_MAPPING: Record<string, string | string[]> = {
[OPTION_REVIEW]: BACKEND_STATUS.REVIEW_READY,
[OPTION_UNDER_REVIEW]: BACKEND_STATUS.IN_REVIEW,
[OPTION_APPROVED]: BACKEND_STATUS.APPROVED,
[OPTION_REJECTED]: BACKEND_STATUS.REJECTED,
[OPTION_PUBLISHING]: PUBLISHING_STATUSES,
[OPTION_ALL]: ALL_REVIEW_STATUSES,
}

export interface ReviewRaftsResponse {
Expand Down Expand Up @@ -131,33 +153,48 @@ export const getDOIsForReview = async (
const xmlString = await response.text()
const doiDataList: DOIData[] = await parseXmlToJson(xmlString)

console.log(`[getDOIsForReview] Loaded ${doiDataList.length} DOIs from backend:`)
doiDataList.forEach((doi) => {
console.log(` - ${doi.identifier}: status="${doi.status}", reviewer="${doi.reviewer || 'none'}"`)
})

// Calculate counts for all statuses
const counts: Record<string, number> = {
[OPTION_ALL]: 0,
[OPTION_REVIEW]: 0,
[OPTION_UNDER_REVIEW]: 0,
[OPTION_APPROVED]: 0,
[OPTION_REJECTED]: 0,
[OPTION_PUBLISHING]: 0,
}

// Count DOIs by status
doiDataList.forEach((doi) => {
const backendStatus = doi.status?.toLowerCase()
if (backendStatus === BACKEND_STATUS.REVIEW_READY) {
counts[OPTION_REVIEW]++ // review ready (waiting for reviewer)
} else if (backendStatus === BACKEND_STATUS.IN_REVIEW) {
counts[OPTION_UNDER_REVIEW]++ // in review (reviewer claimed)
} else if (backendStatus === BACKEND_STATUS.APPROVED) {
const status = doi.status?.toLowerCase()
if (status === BACKEND_STATUS.REVIEW_READY) {
counts[OPTION_REVIEW]++
counts[OPTION_ALL]++
} else if (status === BACKEND_STATUS.IN_REVIEW) {
counts[OPTION_UNDER_REVIEW]++
counts[OPTION_ALL]++
} else if (status === BACKEND_STATUS.APPROVED) {
counts[OPTION_APPROVED]++
} else if (backendStatus === BACKEND_STATUS.REJECTED) {
counts[OPTION_ALL]++
} else if (status === BACKEND_STATUS.REJECTED) {
counts[OPTION_REJECTED]++
counts[OPTION_ALL]++
} else if (status && PUBLISHING_STATUSES.includes(status)) {
counts[OPTION_PUBLISHING]++
counts[OPTION_ALL]++
}
})

// Filter DOIs by requested status
const backendStatus = filterStatus ? STATUS_MAPPING[filterStatus] : null
const filteredDois = backendStatus
? doiDataList.filter((doi) => doi.status?.toLowerCase() === backendStatus)
: doiDataList.filter((doi) => doi.status?.toLowerCase() === BACKEND_STATUS.REVIEW_READY)
const mappedStatus = filterStatus ? STATUS_MAPPING[filterStatus] : STATUS_MAPPING[OPTION_ALL]
const targetStatuses = Array.isArray(mappedStatus) ? mappedStatus : [mappedStatus]
const filteredDois = doiDataList.filter(
(doi) => doi.status && targetStatuses.includes(doi.status.toLowerCase()),
)

// Fetch full RAFT data for each filtered DOI
const rafts: RaftData[] = []
Expand Down
Loading
Loading