diff --git a/CodecheckPlugin.php b/CodecheckPlugin.php index 2127bdb5..e6d8d7d7 100644 --- a/CodecheckPlugin.php +++ b/CodecheckPlugin.php @@ -10,6 +10,8 @@ use APP\plugins\generic\codecheck\classes\migration\CodecheckSchemaMigration; use APP\plugins\generic\codecheck\classes\Submission\Schema; use APP\plugins\generic\codecheck\classes\Submission\SubmissionWizardHandler; +use APP\plugins\generic\codecheck\classes\Orcid\OrcidAuthHandler; +use APP\plugins\generic\codecheck\classes\Orcid\OrcidDepositService; use APP\plugins\generic\codecheck\classes\Log\CodecheckLogger; use PKP\plugins\GenericPlugin; use PKP\plugins\Hook; @@ -33,8 +35,6 @@ class CodecheckPlugin extends GenericPlugin public function register($category, $path, $mainContextId = null): bool { - CodecheckLogger::debug('register() called, path=' . $path); - $success = parent::register($category, $path); if ($success && $this->getEnabled()) { @@ -75,6 +75,9 @@ public function register($category, $path, $mainContextId = null): bool Hook::add('Template::SubmissionWizard::Section::Review', function($hookName, $params) use ($codecheckWizard) { return $codecheckWizard->addToSubmissionWizardReviewTemplate($hookName, $params); }); + + // ORCID: automatically deposit when an article is published + Hook::add('Publication::publish', $this->onPublicationPublish(...)); // Test if we can hook into the publication to block it if codecheck failed Hook::add('Publication::validatePublish', $this->validateCodecheckStatus(...)); @@ -131,66 +134,75 @@ public function addCodecheckStatusLocalizations($hookName, $args) { } /** - * Setup the `CodecheckApiHandler` - * - * @param string $hookname The name of the hook - * @param array $args The arguments passed by the hook - * - * @return void + * Triggered when an editor publishes an article. + */ + public function onPublicationPublish(string $hookName, array $args): bool + { + $publication = $args[0]; + $submission = Repo::submission()->get($publication->getData('submissionId')); + + if (!$submission) return false; + if (!$submission->getData('codecheckOptIn')) return false; + + $context = Application::get()->getRequest()->getContext(); + if (!$this->getSetting($context->getId(), Constants::ORCID_ENABLED)) return false; + + try { + $depositService = new OrcidDepositService($this); + $results = $depositService->depositForSubmission($submission->getId()); + foreach ($results as $result) { + if ($result['status'] === 'success') { + CodecheckLogger::info('ORCID deposited for ' . $result['orcidId'] . ' put-code=' . $result['putCode']); + } else { + CodecheckLogger::error('ORCID deposit failed for ' . $result['orcidId'] . ': ' . ($result['error'] ?? 'unknown')); + } + } + } catch (\Throwable $e) { + CodecheckLogger::error('ORCID deposit exception on publish: ' . $e->getMessage()); + } + + return false; + } + + /** + * Setup the CodecheckApiHandler. + * The constructor handles the request and exits — no need to set a router handler. */ public function setupAPIHandler(string $hookName, array $args): void { $request = $args[0]; - $router = $request->getRouter(); + $router = $request->getRouter(); - if (!($router instanceof \PKP\core\APIRouter)) { - return; - } + if (!($router instanceof \PKP\core\APIRouter)) return; if (str_contains($request->getRequestPath(), 'api/v1/codecheck')) { CodecheckLogger::debug('Instantiating the CODECHECK APIHandler'); $adminRoles = new CodecheckRoleArray([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN]); - $editRoles = new CodecheckRoleArray([$adminRoles, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_MANAGER]); + $editRoles = new CodecheckRoleArray([$adminRoles, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_MANAGER, Role::ROLE_ID_REVIEWER]); $readRoles = new CodecheckRoleArray([$editRoles, Role::ROLE_ID_READER, Role::ROLE_ID_AUTHOR]); $roles = new CodecheckRoleManager( readMetadata: $readRoles, - editMetadata: $editRoles, - admin: $adminRoles, + editMetadata: $editRoles, + admin: $adminRoles, ); - $apiHandler = new CodecheckApiHandler($this, $request, $roles); - CodecheckLogger::debug('API request: ' . $request->getRequestPath()); - } - - if (!isset($apiHandler)) { - return; + new CodecheckApiHandler($this, $request, $roles); } - - $router->setHandler($apiHandler); - exit; } /** - * Declare the handler function to process the actual page PATH - * - * @param string $hookName The name of the invoked hook - * @param array $args Hook parameters - * - * @return bool Hook handling status + * Declare the handler function to process the actual page PATH. */ public function setCodecheckPageHandler($hookName, $args) { $request = Application::get()->getRequest(); - $templateMgr = TemplateManager::getManager($request); - $page = &$args[0]; - $op = &$args[1]; + $page = &$args[0]; + $op = &$args[1]; $handler = &$args[3]; - - // Construct a path to look for $path = $page; if ($op !== 'index') { $path .= "/{$op}"; @@ -199,13 +211,19 @@ public function setCodecheckPageHandler($hookName, $args) $path .= '/' . implode('/', $ops); } - // Check if this is a request for a static page or preview. + // ORCID OAuth routes + if ($page === 'codecheck' && $op === 'orcid') { + $subOp = $request->getRequestedArgs()[0] ?? ''; + if (in_array($subOp, ['startAuth', 'callback'], true)) { + $handler = new OrcidAuthHandler($this); + $args[1] = $subOp; + return true; + } + } + if ($page = 'codecheck' && $op == 'info') { - // Trick the handler into dealing with it normally $page = 'pages'; $op = 'view'; - - // It is -- attach the static pages handler. $handler = new CodecheckPageHandler($this); return true; } @@ -270,6 +288,26 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool 'priority' => TemplateManager::STYLE_SEQUENCE_LAST, ] ); + + $orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth'; + + $orcidConfig = json_encode([ + 'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED), + 'authUrl' => $orcidAuthUrl, + 'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE) + ?? Constants::ORCID_API_TYPE_SANDBOX, + 'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(), + ]); + + $templateMgr->addJavaScript( + 'codecheck-orcid-config', + 'window.codecheckOrcidConfig = ' . $orcidConfig . ';', + [ + 'inline' => true, + 'contexts' => ['backend'], + 'priority' => TemplateManager::STYLE_SEQUENCE_LAST + ] + ); } // ---------------------------------------------------------------- @@ -277,7 +315,6 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool // ---------------------------------------------------------------- if ($request->getRequestedOp() == 'workflow') { $submission = $request->getRouter()->getHandler()->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION); - if ($submission) { $templateMgr->setState([ 'codecheckSubmission' => [ @@ -288,23 +325,57 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool 'dataRepository' => $submission->getData('dataRepository'), 'manifestFiles' => $submission->getData('manifestFiles'), 'dataAvailabilityStatement' => $submission->getData('dataAvailabilityStatement'), - ] + ], ]); } } - + + // ---------------------------------------------------------------- + // Reviewer page — inject submission data + ORCID config for Vue + // ---------------------------------------------------------------- + if ($request->getRequestedPage() == 'reviewer' && $request->getRequestedOp() == 'submission') { + $requestArgs = $request->getRequestedArgs(); + $submissionId = (int) ($requestArgs[0] ?? 0); + + if ($submissionId) { + $context = $request->getContext(); + $contextId = $context->getId(); + $submission = Repo::submission()->get($submissionId); + + if ($submission && $submission->getData('codecheckOptIn')) { + $orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth'; + + $reviewerData = json_encode([ + 'submissionId' => $submission->getId(), + 'codecheckOptIn' => true, + 'orcid' => [ + 'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED), + 'authUrl' => $orcidAuthUrl, + 'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE) ?? Constants::ORCID_API_TYPE_SANDBOX, + 'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(), + ], + ]); + + $templateMgr->addJavaScript( + 'codecheck-reviewer-data', + 'window.codecheckReviewerData = ' . $reviewerData . ';', + [ + 'inline' => true, + 'contexts' => ['backend'], + 'priority' => TemplateManager::STYLE_SEQUENCE_LAST, + ] + ); + } + } + } + return false; } public function getUrlPageRoute(string $page): string { $request = Application::get()->getRequest(); - return $request->getDispatcher()->url( - $request, - ROUTE_PAGE, - null, - $page - ); + return $request->getDispatcher()->url($request, ROUTE_PAGE, null, $page); } public function addOptInToSchema(string $hookName, array $args): bool @@ -339,7 +410,7 @@ public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormCom 'codecheckLink' => "" . __('plugins.generic.codecheck.displayName') . "" ]); - if($codecheckMode == 'opt-out') { + if ($codecheckMode == 'opt-out') { $checkboxValue = true; } elseif ($codecheckMode == 'mandatory') { $checkboxValue = true; @@ -360,7 +431,7 @@ public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormCom 'disabled' => $codecheckMandatory, ] ], - 'value' => $checkboxValue, + 'value' => $checkboxValue, 'groupId' => 'default' ])); } @@ -383,10 +454,7 @@ public function saveOptIn(string $hookName, array $params): bool public function saveWizardFieldsFromRequest(string $hookName, array $params): bool { $submission = $params[1]; - - if (!$submission) { - return false; - } + if (!$submission) return false; $request = Application::get()->getRequest(); @@ -415,9 +483,6 @@ public function saveWizardFieldsFromRequest(string $hookName, array $params): bo /** * Provide a name for this plugin - * - * The name will appear in the Plugin Gallery where editors can - * install, enable and disable plugins. */ public function getDisplayName(): string { @@ -426,9 +491,6 @@ public function getDisplayName(): string /** * Provide a description for this plugin - * - * The description will appear in the Plugin Gallery where editors can - * install, enable and disable plugins. */ public function getDescription(): string { @@ -436,10 +498,7 @@ public function getDescription(): string } /** - * Add a settings action to the plugin's entry in the CODECHECK plugins list. - * - * @param Request $request - * @param array $actionArgs + * Add a settings action to the plugin's entry in the plugins list. */ public function getActions($request, $actionArgs): array { @@ -447,13 +506,6 @@ public function getActions($request, $actionArgs): array return $actions->execute($request, $actionArgs, parent::getActions($request, $actionArgs)); } - /** - * Load a form when the `settings` button is clicked and - * save the form when the user saves it. - * - * @param array $args - * @param Request $request - */ public function manage($args, $request): JSONMessage { $manage = new Manage($this); diff --git a/README.md b/README.md index ebe99179..b34f5556 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The ojs-codecheck plugin development was started as part of the [CHECK-PUB](http --> - **Certificate display**: Automatically display CODECHECK certificates for verified submissions - **Customizable settings**: Configure CODECHECK workflow and display preferences +- **ORCID deposition**: Automatically deposit CODECHECK peer-review activity to codecheckers' ORCID profiles using the ORCID Member API ## Installation @@ -131,6 +132,27 @@ function getStatus() { } ``` +## ORCID Peer-Review Deposition + +The plugin deposits CODECHECK activity as a [peer-review item](https://support.orcid.org/hc/en-us/articles/360006971333-Peer-Reviews) to the codechecker's ORCID record. The following fields are set (see `classes/Orcid/PeerReviewPayloadBuilder.php`): + +| Field | Value | +|---|---| +| Role | `reviewer` | +| Reviewer identifier | Codechecker's ORCID iD | +| Convening organization | Publisher name (Journal Settings → Masthead → Publisher) | +| Country | Journal Settings → Masthead → Country | +| City | Not set — OJS has no publisher city field | +| Group | `issn:{ISSN}` or `orcid-generated:codecheck-ojs` if no ISSN configured | +| Type | `review` | +| Date | Check completion date (`codecheck_metadata.check_time`) | +| Review container name | Journal name | +| Review identifier | Certificate DOI or source work ID (`codecheck_metadata.certificate`) | +| Review URL | `https://doi.org/{certificate}` or CODECHECK register URL | +| Subject name | Submission title | +| Subject type | `journal-article` | +| Subject identifier | Article DOI | + ## Development ### Requirements @@ -242,7 +264,7 @@ codecheck/ ├── composer.lock # composer lock-file ├── css/* # CODECHECK CSS stylesheets ├── cypress/tests/functional/ # End-to-end testing -│   └── CodecheckPlugin.cy.js # Cypress test suite +│ └── CodecheckPlugin.cy.js # Cypress test suite ├── locale/* # Internationalization (language localization strings) ├── package-lock.json ├── package.json @@ -414,4 +436,4 @@ The [CHECK-PUB](https://codecheck.org.uk/pub/) project (2025-2026) is empored by ## Related Projects - [CODECHECK](https://codecheck.org.uk/) -- [OJS](https://pkp.sfu.ca/software/ojs/) by [PKP](https://pkp.sfu.ca/) +- [OJS](https://pkp.sfu.ca/software/ojs/) by [PKP](https://pkp.sfu.ca/) \ No newline at end of file diff --git a/api/v1/CodecheckApiHandler.php b/api/v1/CodecheckApiHandler.php index 232e4af6..bffa4012 100644 --- a/api/v1/CodecheckApiHandler.php +++ b/api/v1/CodecheckApiHandler.php @@ -18,6 +18,9 @@ use APP\plugins\generic\codecheck\classes\CodecheckRoles\CodecheckRoleManager; use APP\plugins\generic\codecheck\classes\Exceptions\RoleExceptions\RoleNotFoundException; use APP\plugins\generic\codecheck\classes\CodecheckRegister\CodecheckIssueLabels; +use APP\plugins\generic\codecheck\classes\Orcid\OrcidApiClient; +use APP\plugins\generic\codecheck\classes\Orcid\OrcidTokenDAO; +use APP\plugins\generic\codecheck\classes\Orcid\OrcidDepositService; use Exception; use Illuminate\Support\Facades\Schema; use APP\plugins\generic\codecheck\classes\Workflow\CodecheckStatusHandler; @@ -61,19 +64,19 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck 'roles' => $roles->editMetadata(), ], [ - 'route' => 'metadata', + 'route' => 'metadata', 'handler' => [$this, 'getMetadata'], - 'roles' => $roles->readMetadata(), + 'roles' => $roles->readMetadata(), ], [ - 'route' => 'download', + 'route' => 'download', 'handler' => [$this, 'downloadFile'], - 'roles' => $roles->readMetadata(), + 'roles' => $roles->readMetadata(), ], [ - 'route' => 'yaml', + 'route' => 'yaml', 'handler' => [$this, 'generateYaml'], - 'roles' => $roles->readMetadata(), + 'roles' => $roles->readMetadata(), ], [ 'route' => 'register', @@ -90,12 +93,22 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck 'handler' => [$this, 'getStatusHistory'], 'roles' => $roles->readMetadata(), ], + [ + 'route' => 'orcid-status', + 'handler' => [$this, 'getOrcidStatus'], + 'roles' => $roles->readMetadata(), + ], + [ + 'route' => 'orcid-test', + 'handler' => [$this, 'testOrcidSetup'], + 'roles' => $roles->admin(), + ], ], 'POST' => [ [ - 'route' => 'identifier', + 'route' => 'identifier', 'handler' => [$this, 'reserveIdentifier'], - 'roles' => $roles->editMetadata(), + 'roles' => $roles->editMetadata(), ], [ 'route' => 'issue', @@ -103,24 +116,24 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck 'roles' => $roles->editMetadata(), ], [ - 'route' => 'metadata', + 'route' => 'metadata', 'handler' => [$this, 'saveMetadata'], - 'roles' => $roles->editMetadata(), + 'roles' => $roles->editMetadata(), ], [ - 'route' => 'upload', + 'route' => 'upload', 'handler' => [$this, 'uploadFile'], - 'roles' => $roles->editMetadata(), + 'roles' => $roles->editMetadata(), ], [ - 'route' => 'repository', + 'route' => 'repository', 'handler' => [$this, 'loadMetadataFromRepository'], - 'roles' => $roles->editMetadata(), + 'roles' => $roles->editMetadata(), ], [ - 'route' => 'yaml/validate', + 'route' => 'yaml/validate', 'handler' => [$this, 'validateYamlStructure'], - 'roles' => $roles->readMetadata(), + 'roles' => $roles->readMetadata(), ], [ 'route' => 'status/update', @@ -132,6 +145,11 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck 'handler' => [$this, 'validateUserAccessRightsToStatus'], 'roles' => $roles->readMetadata(), ], + [ + 'route' => 'orcid-deposit', + 'handler' => [$this, 'depositToOrcid'], + 'roles' => $roles->editMetadata(), + ], ], ]; @@ -148,7 +166,6 @@ public function __construct(CodecheckPlugin $plugin, Request $request, Codecheck private function getEndpoint(): ApiEndpoint { - // get the request Method like POST or GET $requestMethod = $this->request->getRequestMethod(); CodecheckLogger::debug("API Method: " . $requestMethod); @@ -179,21 +196,21 @@ public function authorize() $contextId = $this->request->getContext()->getId(); $apiEndpoint = $this->getEndpoint(); $codecheckRole = $apiEndpoint->getRoles(); - + try { $pkpRoles = $codecheckRole->getRoles(); if(!($user && $user->hasRole($pkpRoles, $contextId))) { JsonResponse::staticResponse([ - 'success' => false, - 'error' => "User has no assigned Role or doesn't have the right roles assigned to access this resource" + 'success' => false, + 'error' => "User has no assigned Role or doesn't have the right roles assigned to access this resource", ], 400); return; } } catch (RoleNotFoundException $roleNotFoundException) { JsonResponse::staticResponse([ - 'success' => false, - 'error' => $roleNotFoundException->getMessage() + 'success' => false, + 'error' => $roleNotFoundException->getMessage(), ], $roleNotFoundException->getCode()); return; } @@ -737,11 +754,12 @@ public function loadMetadataFromRepository(): void public function getMetadata(): void { $submissionId = $this->codecheckMetadataHandler->getSubmissionId(); + $result = $this->codecheckMetadataHandler->getMetadata($this->request, $submissionId); - if(isset($result['error'])) { - $result = array_merge($result, ['success' => false, 'submissionID' => $submissionId]); - JsonResponse::staticResponse($result, 404); + if (isset($result['error'])) { + JsonResponse::staticResponse(array_merge($result, ['success' => false, 'submissionID' => $submissionId]), 404); + return; } JsonResponse::staticResponse(array_merge($result, ['success' => true]), 200); @@ -755,11 +773,12 @@ public function getMetadata(): void public function saveMetadata(): void { $submissionId = $this->codecheckMetadataHandler->getSubmissionId(); + $result = $this->codecheckMetadataHandler->saveMetadata($this->request, $submissionId); - if(isset($result['error'])) { - $result = array_merge($result, ['success' => false, 'submissionID' => $submissionId]); - JsonResponse::staticResponse($result, 404); + if (isset($result['error'])) { + JsonResponse::staticResponse(array_merge($result, ['success' => false, 'submissionID' => $submissionId]), 404); + return; } JsonResponse::staticResponse(array_merge($result, ['success' => true]), 200); @@ -908,11 +927,12 @@ public function downloadFile(): void public function generateYaml(): void { $submissionId = $this->codecheckMetadataHandler->getSubmissionId(); + $result = $this->codecheckMetadataHandler->generateYaml($this->request, $submissionId); - if(isset($result['error'])) { - $result = array_merge($result, ['success' => false, 'submissionID' => $submissionId]); - JsonResponse::staticResponse($result, 404); + if (isset($result['error'])) { + JsonResponse::staticResponse(array_merge($result, ['success' => false, 'submissionID' => $submissionId]), 404); + return; } JsonResponse::staticResponse(array_merge($result, ['success' => true]), 200); @@ -948,6 +968,177 @@ public function validateYamlStructure(): void ], 200); } + /** + * GET api/v1/codecheck/orcid-status?submissionId=XX + */ + public function getOrcidStatus(): void + { + $submissionId = (int) $this->request->getUserVar('submissionId'); + + if (!$submissionId) { + $this->response->response(['success' => false, 'error' => 'Missing submissionId'], 400); + return; + } + + $submission = Repo::submission()->get($submissionId); + if (!$submission) { + $this->response->response(['success' => false, 'error' => 'Submission not found'], 404); + return; + } + + $metadata = DB::table('codecheck_metadata')->where('submission_id', $submissionId)->first(); + + $codecheckerNames = []; + if ($metadata && $metadata->codecheckers) { + $decoded = json_decode($metadata->codecheckers, true); + if (is_array($decoded)) { + $codecheckerNames = $decoded; + } + } + + $tokenDAO = new OrcidTokenDAO(); + $tokenRows = $tokenDAO->getAllBySubmission($submissionId); + + $tokensByOrcid = []; + foreach ($tokenRows as $row) { + if ($row->orcid_id) { + $tokensByOrcid[$row->orcid_id] = $row; + } + } + + $codecheckers = []; + + if (!empty($codecheckerNames)) { + foreach ($codecheckerNames as $cc) { + $name = is_array($cc) ? ($cc['name'] ?? '') : (string) $cc; + $orcidId = is_array($cc) ? ($cc['orcid'] ?? $cc['ORCID'] ?? null) : null; + $tokenRow = $orcidId ? ($tokensByOrcid[$orcidId] ?? null) : null; + + $codecheckers[] = [ + 'name' => $name, + 'orcidId' => $tokenRow->orcid_id ?? null, + 'depositStatus' => $tokenRow->deposit_status ?? null, + 'putCode' => $tokenRow->put_code ?? null, + 'depositedAt' => $tokenRow->deposited_at ?? null, + 'errorMessage' => $tokenRow->error_message ?? null, + ]; + } + } else { + foreach ($tokenRows as $row) { + $codecheckers[] = [ + 'name' => $row->orcid_id ?? 'Unknown', + 'orcidId' => $row->orcid_id, + 'depositStatus' => $row->deposit_status, + 'putCode' => $row->put_code, + 'depositedAt' => $row->deposited_at, + 'errorMessage' => $row->error_message, + ]; + } + } + + $journalConfigError = null; + try { + $depositService = new OrcidDepositService($this->plugin); + $depositService->getValidatedJournalInfo($this->request->getContext()->getId()); + } catch (\InvalidArgumentException $e) { + $journalConfigError = $e->getMessage(); + } + + $this->response->response([ + 'success' => true, + 'submissionId' => $submissionId, + 'codecheckers' => $codecheckers, + 'journalConfigError' => $journalConfigError, + ], 200); + } + + /** + * POST api/v1/codecheck/orcid-deposit + */ + public function depositToOrcid(): void + { + $postParams = json_decode(file_get_contents('php://input'), true); + $submissionId = (int) ($postParams['submissionId'] ?? 0); + + if (!$submissionId) { + $this->response->response(['success' => false, 'error' => 'Missing submissionId'], 400); + return; + } + + $context = $this->request->getContext(); + + if (!$this->plugin->getSetting($context->getId(), Constants::ORCID_ENABLED)) { + $this->response->response(['success' => false, 'error' => 'ORCID deposition is not enabled for this journal.'], 400); + return; + } + + try { + $depositService = new OrcidDepositService($this->plugin); + $results = $depositService->depositForSubmission($submissionId); + + $this->response->response(['success' => true, 'results' => $results], 200); + } catch (\Throwable $e) { + CodecheckLogger::error('ORCID depositToOrcid API error: ' . $e->getMessage()); + $this->response->response(['success' => false, 'error' => $e->getMessage()], 500); + } + } + + /** + * GET api/v1/codecheck/orcid-test + * + * Tests the ORCID setup without writing any data: + * 1. Validates required journal metadata + * 2. Makes an authenticated token request to verify credentials + */ + public function testOrcidSetup(): void + { + $context = $this->request->getContext(); + $contextId = $context->getId(); + + try { + $depositService = new OrcidDepositService($this->plugin); + $depositService->getValidatedJournalInfo($contextId); + } catch (\InvalidArgumentException $e) { + $this->response->response([ + 'success' => false, + 'step' => 'metadata', + 'error' => $e->getMessage(), + ], 400); + return; + } + + $clientId = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_ID); + $clientSecret = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_SECRET); + $apiType = $this->plugin->getSetting($contextId, Constants::ORCID_API_TYPE) + ?? Constants::ORCID_API_TYPE_SANDBOX; + + if (!$clientId || !$clientSecret) { + $this->response->response([ + 'success' => false, + 'step' => 'credentials', + 'error' => __('plugins.generic.codecheck.orcid.test.error.noCredentials'), + ], 400); + return; + } + + try { + $client = new OrcidApiClient($clientId, $clientSecret, $apiType); + $client->getClientCredentialsToken(); + } catch (\Throwable $e) { + $this->response->response([ + 'success' => false, + 'step' => 'credentials', + 'error' => __('plugins.generic.codecheck.orcid.test.error.credentialsFailed') . ' ' . $e->getMessage(), + ], 400); + return; + } + + $this->response->response([ + 'success' => true, + 'message' => __('plugins.generic.codecheck.orcid.test.success'), + ], 200); + } + public function getCurrentStatus(): void { $submissionId = (int) $this->codecheckMetadataHandler->getSubmissionId(); diff --git a/api/v1/JsonResponse.php b/api/v1/JsonResponse.php index 81d9616b..4136a491 100644 --- a/api/v1/JsonResponse.php +++ b/api/v1/JsonResponse.php @@ -8,22 +8,17 @@ class JsonResponse private int $httpResponseCode; /** - * The JSON Response with an array as payload and a HTTP Response Code - * - * @param $json_array The array that will be json encoded into the response - * @param $responseState The HTTP Response Code that will be set accordingly - * @return void + * @param array $json_array The array that will be json encoded into the response + * @param int $httpResponseCode The HTTP Response Code */ - public function __construct(array $json_array, int $httpResponseCode) + public function __construct(array $json_array = [], int $httpResponseCode = 200) { $this->payload = json_encode($json_array); $this->httpResponseCode = $httpResponseCode; } /** - * This function returns the Payload of the JSON Response - * - * @return string The Payload of the JSON Response + * Returns the Payload of the JSON Response */ public function getPayload(): string { @@ -31,9 +26,7 @@ public function getPayload(): string } /** - * This function returns the HTTP Response Code of the JSON Response - * - * @return int The HTTP Response Code of the JSON Response + * Returns the HTTP Response Code of the JSON Response */ public function getHttpResponseCode(): int { @@ -41,15 +34,10 @@ public function getHttpResponseCode(): int } /** - * This function creates a new JSON Response, echoes it to the HTML page it was calles upon and sets the according HTTP Response Code - * - * @param $json_array The array that will be json encoded into the response - * @param $responseState The HTTP Response Code that will be set accordingly - * @return void + * Echoes the response and exits. */ public function constructResponse(): void { - // header for AJAX calls define('INDEX_FILE_STARTED', true); header('Content-Type: application/json'); http_response_code($this->httpResponseCode); @@ -58,15 +46,23 @@ public function constructResponse(): void } /** - * This function creates a new JSON Response, echoes it to the HTML page it was calles upon and sets the according HTTP Response Code - * - * @param $json_array The array that will be json encoded into the response - * @param $responseState The HTTP Response Code that will be set accordingly - * @return void + * Convenience instance method used by CodecheckApiHandler. + * Immediately sends the response and exits. + */ + public function response(array $json_array, int $httpResponseCode): void + { + header('Content-Type: application/json'); + http_response_code($httpResponseCode); + echo json_encode($json_array); + exit; + } + + /** + * Static helper to create and send a response immediately. */ public static function staticResponse(array $json_array, int $httpResponseCode): void { $jsonResponse = new JsonResponse($json_array, $httpResponseCode); $jsonResponse->constructResponse(); } -} +} \ No newline at end of file diff --git a/classes/Constants.php b/classes/Constants.php index 183b906b..82b6c28d 100644 --- a/classes/Constants.php +++ b/classes/Constants.php @@ -2,7 +2,7 @@ /** * @file classes/Constants.php * - * Copyright (c) 2025 CODECHECK Initiative + * Copyright (c) 2026 CODECHECK Initiative * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class Constants @@ -47,9 +47,9 @@ class Constants Constants::CODECHECK_STATUS_PUBLISHED_PARTIAL_REPRODUCTION, Constants::CODECHECK_STATUS_PUBLISHED_FULL_REPRODUCTION, ]; - + /** - * Plugin settings keys - NEW ADDITIONS + * Plugin settings keys */ public const CODECHECK_ENABLED = 'codecheckEnabled'; public const CODECHECK_AUTHOR_ANONYMITY = 'authorAnonymity'; @@ -61,12 +61,39 @@ class Constants public const CODECHECK_GITHUB_CUSTOM_LABELS = 'githubCustomLabels'; public const CODECHECK_MODE = 'codecheckMode'; public const CODECHECK_SHOW_DASHBOARD_COLUMN = 'showDashboardColumn'; - # Update Github Register Issue + + // Update Github Register Issue public const CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_FIELDS = 'codecheckGithubUpdateFields'; public const CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_TITLE = 'updateTitle'; public const CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_BODY = 'updateBody'; - # Codecheck Status + + // Codecheck Status public const CODECHECK_STATUS = 'codecheckStatus'; public const CODECHECK_STATUSES_SELECTED = 'codecheckStatusesSelected'; public const CODECHECK_STATUS_KEYS_SELECTED = 'codecheckStatusKeysSelected'; + + // ORCID integration settings + public const ORCID_ENABLED = 'orcidEnabled'; + public const ORCID_API_TYPE = 'orcidApiType'; + public const ORCID_CLIENT_ID = 'orcidClientId'; + public const ORCID_CLIENT_SECRET = 'orcidClientSecret'; + public const ORCID_CITY = 'orcidCity'; + + // ORCID API type values + public const ORCID_API_TYPE_SANDBOX = 'memberSandbox'; + public const ORCID_API_TYPE_PRODUCTION = 'member'; + + // ORCID API base URLs + public const ORCID_URL_SANDBOX = 'https://sandbox.orcid.org'; + public const ORCID_URL_PRODUCTION = 'https://orcid.org'; + public const ORCID_API_URL_SANDBOX = 'https://api.sandbox.orcid.org/v3.0'; + public const ORCID_API_URL_PRODUCTION = 'https://api.orcid.org/v3.0'; + + // OAuth scope needed to deposit peer-review items + public const ORCID_ACTIVITIES_SCOPE = '/activities/update'; + + // Deposit status values stored in codecheck_orcid_tokens + public const ORCID_DEPOSIT_STATUS_PENDING = 'pending'; + public const ORCID_DEPOSIT_STATUS_SUCCESS = 'success'; + public const ORCID_DEPOSIT_STATUS_FAILED = 'failed'; } \ No newline at end of file diff --git a/classes/Orcid/OrcidApiClient.php b/classes/Orcid/OrcidApiClient.php new file mode 100644 index 00000000..238a0cff --- /dev/null +++ b/classes/Orcid/OrcidApiClient.php @@ -0,0 +1,313 @@ +clientId = $clientId; + $this->clientSecret = $clientSecret; + + if ($apiType === Constants::ORCID_API_TYPE_PRODUCTION) { + $this->orcidBaseUrl = Constants::ORCID_URL_PRODUCTION; + $this->orcidApiUrl = Constants::ORCID_API_URL_PRODUCTION; + } else { + $this->orcidBaseUrl = Constants::ORCID_URL_SANDBOX; + $this->orcidApiUrl = Constants::ORCID_API_URL_SANDBOX; + } + } + + /** + * Build the ORCID OAuth authorisation URL the codechecker must visit. + */ + public function buildAuthorizationUrl(string $redirectUri, string $state): string + { + $params = http_build_query([ + 'client_id' => $this->clientId, + 'response_type' => 'code', + 'scope' => Constants::ORCID_ACTIVITIES_SCOPE, + 'redirect_uri' => $redirectUri, + 'state' => $state, + ]); + + return $this->orcidBaseUrl . '/oauth/authorize?' . $params; + } + + /** + * Exchange the short-lived authorisation code for an access token. + * + * @throws \RuntimeException on failure + */ + public function exchangeCodeForToken(string $code, string $redirectUri): array + { + $tokenUrl = $this->orcidBaseUrl . '/oauth/token'; + + $postFields = http_build_query([ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $redirectUri, + ]); + + $response = $this->httpPost($tokenUrl, $postFields, [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + ]); + + $data = json_decode($response['body'], true); + + if (empty($data['access_token'])) { + throw new \RuntimeException( + 'ORCID token exchange failed: ' . ($data['error_description'] ?? $response['body']) + ); + } + + return $data; + } + + /** + * Get a client credentials token (2-legged OAuth) for group-id management. + * + * @throws \RuntimeException on failure + */ + public function getClientCredentialsToken(): string + { + $tokenUrl = $this->orcidBaseUrl . '/oauth/token'; + + $postFields = http_build_query([ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'client_credentials', + 'scope' => '/group-id-record/update', + ]); + + $response = $this->httpPost($tokenUrl, $postFields, [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + ]); + + $data = json_decode($response['body'], true); + + if (empty($data['access_token'])) { + throw new \RuntimeException( + 'ORCID client credentials token failed: ' . ($data['error_description'] ?? $response['body']) + ); + } + + return $data['access_token']; + } + + /** + * Create a group-id record in ORCID using client credentials. + * Returns the put-code of the created group, or null if it already exists. + * + * @throws \RuntimeException on failure + */ + public function createGroupId(string $groupId, string $groupName, string $groupType = 'journal'): ?string + { + $clientToken = $this->getClientCredentialsToken(); + + $payload = json_encode([ + 'name' => $groupName, + 'group-id' => $groupId, + 'description' => 'CODECHECK peer-review group for ' . $groupName, + 'type' => $groupType, + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $url = $this->orcidApiUrl . '/group-id-record'; + + CodecheckLogger::debug('Creating ORCID group-id: ' . $groupId); + + $response = $this->httpPost($url, $payload, [ + 'Accept: application/vnd.orcid+json', + 'Content-Type: application/vnd.orcid+json', + 'Authorization: Bearer ' . $clientToken, + ]); + + if ($response['status'] === 201) { + $putCode = $this->extractPutCodeFromLocation($response['headers']['location'] ?? ''); + CodecheckLogger::info('ORCID group-id created with put-code: ' . $putCode); + return $putCode; + } + + if ($response['status'] === 409) { + CodecheckLogger::debug('ORCID group-id already exists: ' . $groupId); + return null; + } + + throw new \RuntimeException( + 'ORCID group-id creation failed (HTTP ' . $response['status'] . '): ' . $response['body'] + ); + } + + /** + * POST a peer-review item to the codechecker's ORCID record. + * Returns the put-code string on success. + * + * @throws \RuntimeException on failure + */ + public function postPeerReview(string $orcidId, string $accessToken, array $payload): string + { + $url = $this->orcidApiUrl . '/' . $orcidId . '/peer-review'; + $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + CodecheckLogger::debug('POST peer-review to ORCID: ' . $orcidId); + + $response = $this->httpPost($url, $json, [ + 'Accept: application/vnd.orcid+json', + 'Content-Type: application/vnd.orcid+json', + 'Authorization: Bearer ' . $accessToken, + ]); + + if ($response['status'] === 409) { + $body = json_decode($response['body'], true); + $msg = $body['developer-message'] ?? ''; + if (preg_match('/put-code\s+(\d+)/', $msg, $matches)) { + CodecheckLogger::info('ORCID peer-review already exists, reusing put-code: ' . $matches[1]); + return $matches[1]; + } + } + + if ($response['status'] !== 201) { + throw new \RuntimeException( + 'ORCID peer-review POST failed (HTTP ' . $response['status'] . '): ' . $response['body'] + ); + } + + $putCode = $this->extractPutCodeFromLocation($response['headers']['location'] ?? ''); + + if (!$putCode) { + throw new \RuntimeException('ORCID peer-review POST succeeded but put-code could not be parsed.'); + } + + CodecheckLogger::info('ORCID peer-review deposited, put-code: ' . $putCode); + + return $putCode; + } + + /** + * PUT an updated peer-review item (re-deposit using existing put-code). + * + * @throws \RuntimeException on failure + */ + public function putPeerReview(string $orcidId, string $accessToken, string $putCode, array $payload): void + { + $payload['put-code'] = (int) $putCode; + + $url = $this->orcidApiUrl . '/' . $orcidId . '/peer-review/' . $putCode; + $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + CodecheckLogger::debug('PUT peer-review to ORCID: ' . $orcidId . ' put-code: ' . $putCode); + + $response = $this->httpPut($url, $json, [ + 'Accept: application/vnd.orcid+json', + 'Content-Type: application/vnd.orcid+json', + 'Authorization: Bearer ' . $accessToken, + ]); + + if ($response['status'] !== 200) { + throw new \RuntimeException( + 'ORCID peer-review PUT failed (HTTP ' . $response['status'] . '): ' . $response['body'] + ); + } + + CodecheckLogger::info('ORCID peer-review updated, put-code: ' . $putCode); + } + + // ------------------------------------------------------------------ + // Internal HTTP helpers + // ------------------------------------------------------------------ + + private function httpPost(string $url, string $body, array $headers): array + { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ]); + return $this->executeAndParse($ch); + } + + private function httpPut(string $url, string $body, array $headers): array + { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ]); + return $this->executeAndParse($ch); + } + + private function executeAndParse($ch): array + { + $raw = curl_exec($ch); + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($raw === false) { + throw new \RuntimeException('cURL error: ' . $error); + } + + $headerSize = strrpos($raw, "\r\n\r\n"); + $rawHeaders = substr($raw, 0, $headerSize); + $body = substr($raw, $headerSize + 4); + + $headers = []; + foreach (explode("\r\n", $rawHeaders) as $line) { + if (str_contains($line, ':')) { + [$key, $value] = explode(':', $line, 2); + $headers[strtolower(trim($key))] = trim($value); + } + } + + return ['status' => $status, 'body' => $body, 'headers' => $headers]; + } + + private function extractPutCodeFromLocation(string $location): ?string + { + if (empty($location)) { + return null; + } + $parts = explode('/', rtrim($location, '/')); + $last = end($parts); + return is_numeric($last) ? $last : null; + } +} \ No newline at end of file diff --git a/classes/Orcid/OrcidAuthHandler.php b/classes/Orcid/OrcidAuthHandler.php new file mode 100644 index 00000000..cbf37115 --- /dev/null +++ b/classes/Orcid/OrcidAuthHandler.php @@ -0,0 +1,230 @@ +plugin = $plugin; + parent::__construct(); + } + + /** + * GET /codecheck/orcid/startAuth?submissionId=XX + * Redirects the codechecker to ORCID's consent screen. + */ + public function startAuth($args, $request): void + { + $context = $request->getContext(); + $contextId = $context->getId(); + + if (!$this->plugin->getSetting($contextId, Constants::ORCID_ENABLED)) { + $this->sendPopupError('ORCID integration is not enabled for this journal.'); + return; + } + + $clientId = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_ID); + $clientSecret = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_SECRET); + + if (!$clientId || !$clientSecret) { + $this->sendPopupError( + 'ORCID credentials are not configured. Please ask the journal manager to set ' . + 'the Client ID and Client Secret in the CODECHECK plugin settings.' + ); + return; + } + + $submissionId = (int) $request->getUserVar('submissionId'); + if (!$submissionId) { + $this->sendPopupError('Missing submissionId parameter.'); + return; + } + + $nonce = bin2hex(random_bytes(16)); + $request->getSession()->put('orcid_nonce_' . $submissionId, $nonce); + + $state = base64_encode(json_encode([ + 'submissionId' => $submissionId, + 'nonce' => $nonce, + 'contextPath' => $context->getPath(), + ])); + + $client = $this->buildApiClient($contextId); + $redirectUri = $this->buildRedirectUri($request); + $authUrl = $client->buildAuthorizationUrl($redirectUri, $state); + + $request->redirectUrl($authUrl); + } + + /** + * GET /codecheck/orcid/callback?code=XX&state=YY + * ORCID redirects here after the codechecker grants access. + */ + public function callback($args, $request): void + { + $error = $request->getUserVar('error'); + if ($error) { + $desc = $request->getUserVar('error_description') ?? 'Access denied.'; + $this->sendPopupError('ORCID authorisation denied: ' . $desc); + return; + } + + $code = $request->getUserVar('code'); + $state = $request->getUserVar('state'); + + if (!$code || !$state) { + $this->sendPopupError('Invalid ORCID callback: missing code or state.'); + return; + } + + $stateData = json_decode(base64_decode($state), true); + $submissionId = (int) ($stateData['submissionId'] ?? 0); + $nonce = $stateData['nonce'] ?? ''; + $contextPath = $stateData['contextPath'] ?? 'index'; + + if (!$submissionId) { + $this->sendPopupError('Invalid state parameter.'); + return; + } + + $sessionNonce = $request->getSession()->get('orcid_nonce_' . $submissionId); + if (!$sessionNonce || !hash_equals($sessionNonce, $nonce)) { + $this->sendPopupError('Security check failed. Please try again.'); + return; + } + $request->getSession()->forget('orcid_nonce_' . $submissionId); + + $submission = Repo::submission()->get($submissionId); + if (!$submission) { + $this->sendPopupError('Submission not found.'); + return; + } + $contextId = $submission->getData('contextId'); + + try { + $client = $this->buildApiClient($contextId); + $redirectUri = $this->buildRedirectUri($request); + $tokenData = $client->exchangeCodeForToken($code, $redirectUri); + + $orcidId = $tokenData['orcid']; + $accessToken = $tokenData['access_token']; + $refreshToken = $tokenData['refresh_token'] ?? null; + $expiresAt = null; + + // Verify the authenticated ORCID iD belongs to one of the + // codecheckers assigned to this submission. If stored ORCIDs + // exist and none match, reject the authorisation. + $metadata = DB::table('codecheck_metadata') + ->where('submission_id', $submissionId) + ->first(); + + if ($metadata && $metadata->codecheckers) { + $codecheckers = json_decode($metadata->codecheckers, true); + if (is_array($codecheckers)) { + $storedOrcids = array_filter(array_map( + fn($cc) => $cc['orcid'] ?? $cc['ORCID'] ?? null, + $codecheckers + )); + + if (!empty($storedOrcids) && !in_array($orcidId, $storedOrcids)) { + CodecheckLogger::error( + 'ORCID iD mismatch for submission ' . $submissionId . + ': authenticated as ' . $orcidId . ' but not in codechecker list' + ); + $this->sendPopupError( + 'The ORCID iD you authenticated with (' . $orcidId . ') ' . + 'does not match any codechecker ORCID on record for this submission. ' . + 'Please sign in with the correct ORCID account.' + ); + return; + } + } + } + + $tokenDAO = new OrcidTokenDAO(); + $tokenDAO->upsertToken($submissionId, $orcidId, $accessToken, $refreshToken, $expiresAt); + + CodecheckLogger::info('ORCID token stored for ' . $orcidId . ' / submission ' . $submissionId); + + $workflowUrl = $request->getBaseUrl() + . '/index.php/' . $contextPath + . '/dashboard/editorial?workflowSubmissionId=' . $submissionId; + + echo ''; + echo ''; + echo '

Authorisation successful. You may close this window.

'; + echo ''; + + } catch (\Throwable $e) { + CodecheckLogger::error('ORCID token exchange failed: ' . $e->getMessage()); + $this->sendPopupError('ORCID token exchange failed: ' . $e->getMessage()); + } + } + + private function buildApiClient(int $contextId): OrcidApiClient + { + return new OrcidApiClient( + $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_ID), + $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_SECRET), + $this->plugin->getSetting($contextId, Constants::ORCID_API_TYPE) ?? Constants::ORCID_API_TYPE_SANDBOX + ); + } + + private function buildRedirectUri($request): string + { + return $request->getBaseUrl() + . '/index.php/index/codecheck/orcid/callback'; + } + + private function sendPopupError(string $message): void + { + CodecheckLogger::error('ORCID auth error: ' . $message); + echo ''; + echo ''; + echo '

Error: ' . htmlspecialchars($message) . '

'; + echo '

Close this window

'; + echo ''; + exit; + } +} \ No newline at end of file diff --git a/classes/Orcid/OrcidDepositService.php b/classes/Orcid/OrcidDepositService.php new file mode 100644 index 00000000..8f9d2886 --- /dev/null +++ b/classes/Orcid/OrcidDepositService.php @@ -0,0 +1,183 @@ +plugin = $plugin; + $this->tokenDAO = new OrcidTokenDAO(); + $this->payloadBuilder = new PeerReviewPayloadBuilder(); + } + + /** + * Deposit for every authorised codechecker of a submission. + * + * @throws \InvalidArgumentException if required journal metadata is missing + */ + public function depositForSubmission(int $submissionId): array + { + $context = Application::get()->getRequest()->getContext(); + $contextId = $context->getId(); + + $clientId = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_ID); + $clientSecret = $this->plugin->getSetting($contextId, Constants::ORCID_CLIENT_SECRET); + $apiType = $this->plugin->getSetting($contextId, Constants::ORCID_API_TYPE) + ?? Constants::ORCID_API_TYPE_SANDBOX; + + if (empty($clientId) || empty($clientSecret)) { + CodecheckLogger::error('ORCID deposit skipped: no credentials configured.'); + return [['status' => 'failed', 'error' => __('plugins.generic.codecheck.orcid.test.error.noCredentials')]]; + } + + $submission = Repo::submission()->get($submissionId); + if (!$submission) { + CodecheckLogger::error('ORCID deposit skipped: submission ' . $submissionId . ' not found.'); + return [['status' => 'failed', 'error' => __('plugins.generic.codecheck.orcid.auth.error.submissionNotFound')]]; + } + + $journal = $this->loadJournalInfo($contextId); + + // Validate required journal metadata before attempting any deposit. + // Throws InvalidArgumentException with a clear message if missing. + $this->validateJournalInfo($journal); + + $client = new OrcidApiClient($clientId, $clientSecret, $apiType); + $meta = $this->loadCodecheckMeta($submissionId); + + // Ensure the group-id exists in ORCID before depositing peer-reviews. + $issn = !empty($journal['issn']) ? trim($journal['issn']) : ''; + if (!empty($issn)) { + $groupId = 'issn:' . $issn; + $groupName = !empty($journal['name']) ? $journal['name'] : $journal['publisherName']; + } else { + $groupId = 'orcid-generated:codecheck-ojs'; + $groupName = !empty($journal['name']) ? $journal['name'] : $journal['publisherName']; + } + + try { + $client->createGroupId($groupId, $groupName, 'journal'); + } catch (\Throwable $e) { + // Log but do not block — the group-id may already exist + CodecheckLogger::debug('ORCID group-id registration note: ' . $e->getMessage()); + } + + $tokenRows = $this->tokenDAO->getAuthorizedBySubmission($submissionId); + $results = []; + + foreach ($tokenRows as $row) { + $results[] = $this->depositOneCodechecker($client, $submission, $row, $meta, $journal); + } + + return $results; + } + + /** + * Validate that all required journal metadata for ORCID deposition is present. + * OJS does not have a publisher city field, so only name and country are required. + * + * @throws \InvalidArgumentException with a descriptive message listing missing fields + */ + public function validateJournalInfo(array $journal): void + { + $publisherName = !empty($journal['publisherName']) + ? trim($journal['publisherName']) + : (!empty($journal['name']) ? trim($journal['name']) : ''); + + $country = !empty($journal['publisherCountry']) ? trim($journal['publisherCountry']) : ''; + + $missing = []; + if (empty($publisherName)) $missing[] = 'Publisher Name (Journal Settings → Masthead → Publisher)'; + if (empty($country)) $missing[] = 'Country (Journal Settings → Masthead → Country)'; + + if (!empty($missing)) { + throw new \InvalidArgumentException( + 'ORCID deposition requires the following journal metadata to be configured: ' . + implode(', ', $missing) . '.' + ); + } + } + + /** + * Load journal info and validate it — convenience method for API handler. + */ + public function getValidatedJournalInfo(int $contextId): array + { + $journal = $this->loadJournalInfo($contextId); + $this->validateJournalInfo($journal); + return $journal; + } + + private function depositOneCodechecker( + OrcidApiClient $client, + $submission, + object $row, + array $meta, + array $journal + ): array { + $orcidId = $row->orcid_id; + $accessToken = $row->access_token; + $putCode = $row->put_code; + + try { + $payload = $this->payloadBuilder->build($submission, $orcidId, $meta, $journal); + + if ($putCode) { + $client->putPeerReview($orcidId, $accessToken, $putCode, $payload); + $this->tokenDAO->markSuccess($row->id, $putCode); + return ['orcidId' => $orcidId, 'status' => 'success', 'putCode' => $putCode, 'action' => 'updated']; + } else { + $newPutCode = $client->postPeerReview($orcidId, $accessToken, $payload); + $this->tokenDAO->markSuccess($row->id, $newPutCode); + return ['orcidId' => $orcidId, 'status' => 'success', 'putCode' => $newPutCode, 'action' => 'created']; + } + } catch (\Throwable $e) { + $error = $e->getMessage(); + CodecheckLogger::error('ORCID deposit failed for ' . $orcidId . ': ' . $error); + $this->tokenDAO->markFailed($row->id, $error); + return ['orcidId' => $orcidId, 'status' => 'failed', 'error' => $error]; + } + } + + private function loadCodecheckMeta(int $submissionId): array + { + $row = DB::table('codecheck_metadata') + ->where('submission_id', $submissionId) + ->first(); + return $row ? (array) $row : []; + } + + private function loadJournalInfo(int $contextId): array + { + $context = Application::get()->getRequest()->getContext(); + return [ + 'name' => $context->getLocalizedName() ?? '', + 'issn' => $context->getData('onlineIssn') ?? $context->getData('printIssn') ?? '', + 'publisherName' => $context->getData('publisherInstitution') ?? '', + 'publisherCity' => $this->plugin->getSetting($contextId, Constants::ORCID_CITY) ?? '', + 'publisherCountry' => $context->getData('country') ?? '', + 'ringgoldId' => $context->getData('ringgoldId') ?? null, + ]; + } +} \ No newline at end of file diff --git a/classes/Orcid/OrcidTokenDAO.php b/classes/Orcid/OrcidTokenDAO.php new file mode 100644 index 00000000..6c742433 --- /dev/null +++ b/classes/Orcid/OrcidTokenDAO.php @@ -0,0 +1,120 @@ +where('submission_id', $submissionId) + ->where('orcid_id', $orcidId) + ->first(); + } + + /** + * All token rows for a submission (authorised or not). + */ + public function getAllBySubmission(int $submissionId): \Illuminate\Support\Collection + { + return DB::table('codecheck_orcid_tokens') + ->where('submission_id', $submissionId) + ->get(); + } + + /** + * Only rows that have a stored access token (OAuth completed). + */ + public function getAuthorizedBySubmission(int $submissionId): \Illuminate\Support\Collection + { + return DB::table('codecheck_orcid_tokens') + ->where('submission_id', $submissionId) + ->whereNotNull('access_token') + ->whereNotNull('orcid_id') + ->get(); + } + + /** + * Insert or update a token row after a codechecker authorises. + */ + public function upsertToken( + int $submissionId, + string $orcidId, + string $accessToken, + ?string $refreshToken = null, + ?string $tokenExpiresAt = null + ): void { + $existing = $this->getBySubmissionAndOrcid($submissionId, $orcidId); + + if ($existing) { + DB::table('codecheck_orcid_tokens') + ->where('submission_id', $submissionId) + ->where('orcid_id', $orcidId) + ->update([ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'token_expires_at' => $tokenExpiresAt, + 'deposit_status' => Constants::ORCID_DEPOSIT_STATUS_PENDING, + 'put_code' => null, + 'error_message' => null, + 'updated_at' => now(), + ]); + } else { + DB::table('codecheck_orcid_tokens')->insert([ + 'submission_id' => $submissionId, + 'orcid_id' => $orcidId, + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'token_expires_at' => $tokenExpiresAt, + 'deposit_status' => Constants::ORCID_DEPOSIT_STATUS_PENDING, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + /** + * Mark deposit successful and store the ORCID put-code. + */ + public function markSuccess(int $id, string $putCode): void + { + DB::table('codecheck_orcid_tokens') + ->where('id', $id) + ->update([ + 'deposit_status' => Constants::ORCID_DEPOSIT_STATUS_SUCCESS, + 'put_code' => $putCode, + 'error_message' => null, + 'deposited_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * Mark deposit failed and store the error for the editor to see. + */ + public function markFailed(int $id, string $errorMessage): void + { + DB::table('codecheck_orcid_tokens') + ->where('id', $id) + ->update([ + 'deposit_status' => Constants::ORCID_DEPOSIT_STATUS_FAILED, + 'error_message' => $errorMessage, + 'updated_at' => now(), + ]); + } +} \ No newline at end of file diff --git a/classes/Orcid/PeerReviewPayloadBuilder.php b/classes/Orcid/PeerReviewPayloadBuilder.php new file mode 100644 index 00000000..4712728b --- /dev/null +++ b/classes/Orcid/PeerReviewPayloadBuilder.php @@ -0,0 +1,166 @@ +getCurrentPublication(); + + $checkDate = !empty($meta['check_time']) + ? new \DateTime($meta['check_time']) + : new \DateTime(); + + $submissionTitle = $publication + ? strip_tags($publication->getLocalizedFullTitle() ?? '') + : __('plugins.generic.codecheck.orcid.payload.untitledSubmission'); + + if (empty($submissionTitle)) { + $submissionTitle = __('plugins.generic.codecheck.orcid.payload.untitledSubmission'); + } + + $certificateDoi = !empty($meta['certificate']) ? $meta['certificate'] : null; + $articleDoi = $publication ? ($publication->getData('pub-id::doi') ?? null) : null; + + $issn = !empty($journal['issn']) ? trim($journal['issn']) : ''; + if (!empty($issn)) { + $groupId = 'issn:' . $issn; + } else { + $groupId = 'orcid-generated:codecheck-ojs'; + } + + $payload = [ + 'reviewer-role' => 'reviewer', + 'review-type' => 'review', + 'review-completion-date' => $this->buildDate($checkDate), + 'review-group-id' => $groupId, + 'review-identifiers' => $this->buildReviewIdentifiers($certificateDoi), + 'convening-organization' => $this->buildConveningOrganization($journal), + 'subject-type' => 'journal-article', + 'subject-name' => ['title' => ['value' => $submissionTitle]], + ]; + + if ($certificateDoi) { + $payload['review-url'] = str_starts_with(ltrim($certificateDoi, '/'), '10.') + ? 'https://doi.org/' . ltrim($certificateDoi, '/') + : 'https://codecheck.org.uk/register/certs/' . ltrim($certificateDoi, '/'); + } + + $journalName = !empty($journal['name']) ? trim($journal['name']) : ''; + if (!empty($journalName)) { + $payload['subject-container-name'] = ['value' => $journalName]; + } + + if ($articleDoi) { + $payload['subject-external-identifier'] = $this->buildExternalId('doi', $articleDoi); + } + + return $payload; + } + + private function buildDate(\DateTime $date): array + { + return [ + 'year' => ['value' => $date->format('Y')], + 'month' => ['value' => $date->format('m')], + 'day' => ['value' => $date->format('d')], + ]; + } + + private function buildReviewIdentifiers(?string $certificateDoi): array + { + if (!$certificateDoi) { + return [ + 'external-id' => [[ + 'external-id-type' => 'uri', + 'external-id-value' => 'codecheck:unknown', + 'external-id-relationship' => 'self', + ]] + ]; + } + + $isDoi = str_starts_with(ltrim($certificateDoi, '/'), '10.'); + + return [ + 'external-id' => [[ + 'external-id-type' => $isDoi ? 'doi' : 'source-work-id', + 'external-id-value' => ltrim($certificateDoi, '/'), + 'external-id-url' => ['value' => $isDoi + ? 'https://doi.org/' . ltrim($certificateDoi, '/') + : 'https://codecheck.org.uk/register/venues/journals/' . ltrim($certificateDoi, '/') + ], + 'external-id-relationship' => 'self', + ]] + ]; + } + + private function buildExternalId(string $type, string $value): array + { + return [ + 'external-id-type' => $type, + 'external-id-value' => $value, + 'external-id-url' => ['value' => 'https://doi.org/' . ltrim($value, '/')], + 'external-id-relationship' => 'self', + ]; + } + + /** + * Build convening-organization. + * + * ORCID requires name and country. City is optional — OJS does not + * expose a publisher city field so we include it only when available. + * + * @throws \InvalidArgumentException if name or country are missing + */ + private function buildConveningOrganization(array $journal): array + { + $publisherName = !empty($journal['publisherName']) + ? trim($journal['publisherName']) + : (!empty($journal['name']) ? trim($journal['name']) : ''); + + $country = !empty($journal['publisherCountry']) ? trim($journal['publisherCountry']) : ''; + + $missing = []; + if (empty($publisherName)) $missing[] = __('plugins.generic.codecheck.orcid.payload.missingPublisherName'); + if (empty($country)) $missing[] = __('plugins.generic.codecheck.orcid.payload.missingCountry'); + + if (!empty($missing)) { + throw new \InvalidArgumentException( + __('plugins.generic.codecheck.orcid.payload.missingMetadata', ['fields' => implode(', ', $missing)]) + ); + } + + $org = [ + 'name' => $publisherName, + 'address' => [ + 'country' => $country, + ], + ]; + + $city = !empty($journal['publisherCity']) ? trim($journal['publisherCity']) : ''; + if (!empty($city)) { + $org['address']['city'] = $city; + } + + if (!empty($journal['ringgoldId'])) { + $org['disambiguated-organization'] = [ + 'disambiguated-organization-identifier' => $journal['ringgoldId'], + 'disambiguation-source' => 'RINGGOLD', + ]; + } + + return $org; + } +} \ No newline at end of file diff --git a/classes/Settings/SettingsForm.php b/classes/Settings/SettingsForm.php index 5a7e86bf..820f24dc 100644 --- a/classes/Settings/SettingsForm.php +++ b/classes/Settings/SettingsForm.php @@ -142,7 +142,7 @@ public function initData(): void Constants::CODECHECK_SHOW_DASHBOARD_COLUMN, $showDashboardColumn === null ? true : (bool) $showDashboardColumn ); - + $updateFields = $this->plugin->getSetting( $context->getId(), Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_FIELDS @@ -152,6 +152,47 @@ public function initData(): void $this->setData(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_TITLE, in_array(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_TITLE, $updateFields)); $this->setData(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_BODY, in_array(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_BODY, $updateFields)); + // ORCID integration settings + $this->setData( + Constants::ORCID_ENABLED, + $this->plugin->getSetting( + $context->getId(), + Constants::ORCID_ENABLED + ) + ); + + $this->setData( + Constants::ORCID_API_TYPE, + $this->plugin->getSetting( + $context->getId(), + Constants::ORCID_API_TYPE + ) ?? Constants::ORCID_API_TYPE_SANDBOX + ); + + $this->setData( + Constants::ORCID_CLIENT_ID, + $this->plugin->getSetting( + $context->getId(), + Constants::ORCID_CLIENT_ID + ) + ); + + $this->setData( + Constants::ORCID_CLIENT_SECRET, + $this->plugin->getSetting( + $context->getId(), + Constants::ORCID_CLIENT_SECRET + ) + ); + + $this->setData( + Constants::ORCID_CITY, + $this->plugin->getSetting( + $context->getId(), + Constants::ORCID_CITY + ) + ); + parent::initData(); } @@ -177,6 +218,11 @@ public function readInputData(): void Constants::CODECHECK_STATUS, Constants::CODECHECK_STATUSES_SELECTED, Constants::CODECHECK_STATUS_KEYS_SELECTED, + Constants::ORCID_ENABLED, + Constants::ORCID_API_TYPE, + Constants::ORCID_CLIENT_ID, + Constants::ORCID_CLIENT_SECRET, + Constants::ORCID_CITY, ]); parent::readInputData(); @@ -202,7 +248,7 @@ public function fetch($request, $template = null, $display = false): ?string 'opt-out' => __('plugins.generic.codecheck.settings.mode.opt.out'), 'mandatory' => __('plugins.generic.codecheck.settings.mode.mandatory'), ]); - + $templateMgr->assign( 'showDashboardColumn', $this->getData(Constants::CODECHECK_SHOW_DASHBOARD_COLUMN) @@ -214,6 +260,11 @@ public function fetch($request, $template = null, $display = false): ?string ); $templateMgr->assign('codecheckStatuses', Constants::CODECHECK_STATUSES); + $templateMgr->assign('orcidApiTypes', [ + Constants::ORCID_API_TYPE_SANDBOX => __('plugins.generic.codecheck.orcid.apiType.sandbox'), + Constants::ORCID_API_TYPE_PRODUCTION => __('plugins.generic.codecheck.orcid.apiType.production'), + ]); + return parent::fetch($request, $template, $display); } @@ -295,7 +346,7 @@ public function execute(...$functionArgs): mixed Constants::CODECHECK_SHOW_DASHBOARD_COLUMN, (bool) $this->getData(Constants::CODECHECK_SHOW_DASHBOARD_COLUMN) ); - + $updateFields = array_values(array_filter([ $this->getData(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_TITLE) ? Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_TITLE : null, $this->getData(Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_BODY) ? Constants::CODECHECK_GITHUB_REGISTER_ISSUE_UPDATE_BODY : null, @@ -307,6 +358,41 @@ public function execute(...$functionArgs): mixed $updateFields ); + // Save ORCID integration settings + $this->plugin->updateSetting( + $context->getId(), + Constants::ORCID_ENABLED, + $this->getData(Constants::ORCID_ENABLED) + ); + + $this->plugin->updateSetting( + $context->getId(), + Constants::ORCID_API_TYPE, + $this->getData(Constants::ORCID_API_TYPE) + ); + + $this->plugin->updateSetting( + $context->getId(), + Constants::ORCID_CLIENT_ID, + $this->getData(Constants::ORCID_CLIENT_ID) + ); + + // Only update secret if a new value was provided + $newSecret = $this->getData(Constants::ORCID_CLIENT_SECRET); + if (!empty($newSecret)) { + $this->plugin->updateSetting( + $context->getId(), + Constants::ORCID_CLIENT_SECRET, + $newSecret + ); + } + + $this->plugin->updateSetting( + $context->getId(), + Constants::ORCID_CITY, + $this->getData(Constants::ORCID_CITY) + ); + $notificationMgr = new NotificationManager(); $notificationMgr->createTrivialNotification( Application::get()->getRequest()->getUser()->getId(), diff --git a/classes/migration/CodecheckSchemaMigration.php b/classes/migration/CodecheckSchemaMigration.php index 78f5705a..5368adc9 100644 --- a/classes/migration/CodecheckSchemaMigration.php +++ b/classes/migration/CodecheckSchemaMigration.php @@ -30,6 +30,24 @@ public function up(): void $table->index('submission_id'); }); } + + if (!Schema::hasTable('codecheck_orcid_tokens')) { + Schema::create('codecheck_orcid_tokens', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->bigInteger('submission_id')->unsigned(); + $table->string('orcid_id', 20)->nullable(); + $table->string('access_token', 255)->nullable(); + $table->string('refresh_token', 255)->nullable(); + $table->timestamp('token_expires_at')->nullable(); + $table->string('put_code', 50)->nullable(); + $table->enum('deposit_status', ['pending', 'success', 'failed'])->default('pending'); + $table->text('error_message')->nullable(); + $table->timestamp('deposited_at')->nullable(); + $table->timestamps(); + $table->index('submission_id'); + $table->index(['submission_id', 'orcid_id']); + }); + } $this->createCodecheckGenres(); } @@ -93,6 +111,7 @@ private function createCodecheckGenres(): void public function down(): void { + Schema::dropIfExists('codecheck_orcid_tokens'); Schema::dropIfExists('codecheck_status'); Schema::dropIfExists('codecheck_metadata'); } diff --git a/locale/en/locale.po b/locale/en/locale.po index bb08ba1a..3b4bf202 100644 --- a/locale/en/locale.po +++ b/locale/en/locale.po @@ -50,8 +50,11 @@ msgstr "Reset" msgid "plugins.generic.codecheck.settings.enableCodecheck" msgstr "Enable CODECHECK for this journal" +msgid "plugins.generic.codecheck.settings.enableCodecheck.description" +msgstr "Allow editors to assign codecheckers to submissions" + msgid "plugins.generic.codecheck.settings.mode" -msgstr "Select the CODECHECK mode for submissions" +msgstr "Select the CODECHECK mode used during the submission of a paper" msgid "plugins.generic.codecheck.settings.mode.opt.in" msgstr "opt-in by the author" @@ -62,9 +65,6 @@ msgstr "opt-out by the author" msgid "plugins.generic.codecheck.settings.mode.mandatory" msgstr "CODECHECK is mandatory" -msgid "plugins.generic.codecheck.settings.enableCodecheck.description" -msgstr "Allow editors to assign codecheckers to submissions" - msgid "plugins.generic.codecheck.settings.authorAnonymity" msgstr "Author anonymity" @@ -125,6 +125,12 @@ msgstr "Select Status(es)" msgid "plugins.generic.codecheck.settings.status.noBlock" msgstr "Ignore the CODECHECK status and never block Publication" +msgid "plugins.generic.codecheck.settings.showDashboardColumn" +msgstr "Show CODECHECK column in submissions dashboard" + +msgid "plugins.generic.codecheck.settings.showDashboardColumn.description" +msgstr "Display a CODECHECK status column in the editorial submissions dashboard. Default: enabled." + msgid "plugins.generic.codecheck.title" msgstr "CODECHECK" @@ -156,7 +162,7 @@ msgid "plugins.generic.codecheck.dataSoftwareAvailability" msgstr "Data and Software Availability" msgid "plugins.generic.codecheck.dataSoftwareAvailability.description" -msgstr "Describe where your data, code, and documentation is available and under what conditions, e.g., copyright, licenses, sensitivity limitations, or access procedures for protected data. The information should be concise and ideally comprises (a) for code, a minimal description of the workflow environment and main tools including versions, and (b) for data, persistent links to repositories for code and data using Digital Object Identifiers (DOI), but may also be plain URLs with last access dates or even a description of data access steps. Remove links for anonymity during peer review (“xxx”), or share anonymized links if your repository supports them. Used data, software and (third-party) tools should be cited." +msgstr "Describe where your data, code, and documentation is available and under what conditions, e.g., copyright, licenses, sensitivity limitations, or access procedures for protected data. The information should be concise and ideally comprises (a) for code, a minimal description of the workflow environment and main tools including versions, and (b) for data, persistent links to repositories for code and data using Digital Object Identifiers (DOI), but may also be plain URLs with last access dates or even a description of data access steps. Remove links for anonymity during peer review ("xxx"), or share anonymized links if your repository supports them. Used data, software and (third-party) tools should be cited." msgid "plugins.generic.codecheck.manifestFiles.label" msgstr "Expected Output Files" @@ -170,6 +176,9 @@ msgstr "Data and Software Availability" msgid "plugins.generic.codecheck.dataAvailability.description" msgstr "Copy from your manuscript's data availability section" +msgid "plugins.generic.codecheck.dataAvailability.placeholder" +msgstr "Describe how your data and code are available..." + msgid "plugins.generic.codecheck.manifestFiles.filenamePlaceholder" msgstr "Filename (e.g., figures/figure1.png)" @@ -179,12 +188,27 @@ msgstr "Comment (e.g., Main result visualization)" msgid "plugins.generic.codecheck.manifestFiles.addFile" msgstr "+ Add File" +msgid "plugins.generic.codecheck.manifestFiles.addButton" +msgstr "+ Add File" + +msgid "plugins.generic.codecheck.manifestFiles.removeConfirm" +msgstr "Remove this file from the manifest?" + msgid "plugins.generic.codecheck.repository.placeholder" msgstr "Repository URL (e.g., https://github.com/username/repo)" msgid "plugins.generic.codecheck.repository.add" msgstr "+ Add Repository" +msgid "plugins.generic.codecheck.repository.addButton" +msgstr "+ Add URL" + +msgid "plugins.generic.codecheck.repository.validation.protocol" +msgstr "URL must start with http:// or https://" + +msgid "plugins.generic.codecheck.repository.validation.invalid" +msgstr "Please enter a valid URL" + msgid "plugins.generic.codecheck.review.status" msgstr "Status" @@ -194,6 +218,12 @@ msgstr "This submission will be codechecked" msgid "plugins.generic.codecheck.workflow.title" msgstr "WORKFLOW: CODECHECK" +msgid "plugins.generic.codecheck.workflow.label" +msgstr "CODECHECK" + +msgid "plugins.generic.codecheck.workflow.description" +msgstr "Complete the following fields to document the CODECHECK process." + msgid "plugins.generic.codecheck.configVersion" msgstr "CODECHECK config version" @@ -215,6 +245,12 @@ msgstr "Output File" msgid "plugins.generic.codecheck.manifest.description" msgstr "Description" +msgid "plugins.generic.codecheck.manifest.emptyState" +msgstr "No manifest files added yet. Click \"+ Add File\" to add files." + +msgid "plugins.generic.codecheck.manifest.removeConfirm" +msgstr "Remove this file from the manifest?" + msgid "plugins.generic.codecheck.paperMetadata.title" msgstr "Paper Metadata" @@ -233,6 +269,18 @@ msgstr "Enter the DOI or URL of the preprint" msgid "plugins.generic.codecheck.paperMetadata.source" msgstr "Source of the checked material" +msgid "plugins.generic.codecheck.paperMetadata.readOnlyNotice" +msgstr "This information comes from the original submission and cannot be edited here." + +msgid "plugins.generic.codecheck.paperMetadata.readonlyDescription" +msgstr "These fields are automatically populated from the submission and cannot be edited here." + +msgid "plugins.generic.codecheck.paperMetadata.fieldReadonly" +msgstr "This field is read-only" + +msgid "plugins.generic.codecheck.paperMetadata.noAuthors" +msgstr "No authors found" + msgid "plugins.generic.codecheck.codecheckers.title" msgstr "CODECHECKers" @@ -242,6 +290,18 @@ msgstr "+ CODECHECKer" msgid "plugins.generic.codecheck.codecheckers.addCodechecker" msgstr "Add CODECHECKer" +msgid "plugins.generic.codecheck.codecheckers.emptyState" +msgstr "No codecheckers added yet" + +msgid "plugins.generic.codecheck.codecheckers.enterName" +msgstr "Enter codechecker name:" + +msgid "plugins.generic.codecheck.codecheckers.enterOrcid" +msgstr "Enter ORCID (optional):" + +msgid "plugins.generic.codecheck.codecheckers.removeConfirm" +msgstr "Remove this codechecker?" + msgid "plugins.generic.codecheck.certificate.title" msgstr "CODECHECK Certificate" @@ -260,12 +320,60 @@ msgstr "Upload Certificate" msgid "plugins.generic.codecheck.certificate.summary" msgstr "Summary of the CODECHECK" +msgid "plugins.generic.codecheck.certificate.summaryDescription" +msgstr "Describe the CODECHECK process and any notable findings." + +msgid "plugins.generic.codecheck.certificate.summaryPlaceholder" +msgstr "Successfully reproduced all main figures..." + +msgid "plugins.generic.codecheck.certificate.report" +msgstr "Report URL" + +msgid "plugins.generic.codecheck.certificate.reportDescription" +msgstr "Link to the published CODECHECK report (e.g., Zenodo)." + +msgid "plugins.generic.codecheck.certificate.generateId" +msgstr "Generate ID" + +msgid "plugins.generic.codecheck.certificate.prefix" +msgstr "CODECHECK" + +msgid "plugins.generic.codecheck.certificate.viewLink" +msgstr "View Certificate" + msgid "plugins.generic.codecheck.repositories.title" msgstr "Repositories" msgid "plugins.generic.codecheck.repositories.add" msgstr "+ Repository" +msgid "plugins.generic.codecheck.repositories.description" +msgstr "URL of the forked/archived repository used for the CODECHECK." + +msgid "plugins.generic.codecheck.repositories.emptyState" +msgstr "No repositories added yet" + +msgid "plugins.generic.codecheck.repositories.removeConfirm" +msgstr "Remove this repository?" + +msgid "plugins.generic.codecheck.repositories.infoTitle" +msgstr "Repositories Info" + +msgid "plugins.generic.codecheck.repositories.infoTextOne" +msgstr "If you desire, you can directly load the metadata from the codecheck.yml of the provided CODECHECK Repository, if it contains a valid codecheck.yml." + +msgid "plugins.generic.codecheck.repositories.infoTextTwo" +msgstr "The supported CODECHECK Repository Sites are GitHub, OSF, Zenodo and GitLab." + +msgid "plugins.generic.codecheck.repositories.infoTextNote" +msgstr "Note that loading the data from a remote repository will overwrite, but not save, the values in the current form." + +msgid "plugins.generic.codecheck.repositories.infoTextMoreInformation" +msgstr "More Information: " + +msgid "plugins.generic.codecheck.repositories.infoTextLinkToAllowedRepositories" +msgstr "https://github.com/codecheckers/register/blob/master/README.md#editing-the-register" + msgid "plugins.generic.codecheck.completionTime.title" msgstr "Time the CODECHECK was completed" @@ -281,27 +389,36 @@ msgstr "CODECHECK Certificate ID, Venue Type and Venue Name" msgid "plugins.generic.codecheck.identifier.label" msgstr "ID of the CODECHECK certificate - e.g.: 2025-001" -msgid "plugins.generic.codecheck.identifier.venue.fetch.error.curl" -msgstr "Venue Data Error" - -msgid "plugins.generic.codecheck.identifier.venue.fetch.error.codecheckAPI" -msgstr "Failed to fetch venue data" - msgid "plugins.generic.codecheck.identifier.venue.type" msgstr "Venue Type" msgid "plugins.generic.codecheck.identifier.venue.name" msgstr "Venue Name" +msgid "plugins.generic.codecheck.identifier.venue.fetch.error.curl" +msgstr "Venue Data Error" + +msgid "plugins.generic.codecheck.identifier.venue.fetch.error.codecheckAPI" +msgstr "Failed to fetch venue data" + msgid "plugins.generic.codecheck.identifier.customLabels" msgstr "Custom Labels" +msgid "plugins.generic.codecheck.identifier.labels" +msgstr "GitHub Labels" + msgid "plugins.generic.codecheck.identifier.viewGithubIssue" msgstr "View GitHub Issue" msgid "plugins.generic.codecheck.identifier.save" msgstr "Save Identifier" +msgid "plugins.generic.codecheck.identifier.saved" +msgstr "Identifier \"{$identifier}\" saved!" + +msgid "plugins.generic.codecheck.identifier.enterFirst" +msgstr "Please enter an identifier first." + msgid "plugins.generic.codecheck.identifier.update.error.message" msgstr "Error while updating the GitHub Issue" @@ -326,6 +443,12 @@ msgstr "The GitHub Issue was linked to OJS with the Certificate Identifier" msgid "plugins.generic.codecheck.identifier.reserve.linkExistingIdentifier.fail.message" msgstr "Error while linking an existing GitHub Issue" +msgid "plugins.generic.codecheck.identifier.reserve" +msgstr "Reserve Identifier" + +msgid "plugins.generic.codecheck.identifier.reserved" +msgstr "New identifier reserved: {$identifier}" + msgid "plugins.generic.codecheck.identifier.reserve.success.message" msgstr "You have reserved the Certificate Identifier" @@ -347,24 +470,66 @@ msgstr "Do you want to reserve a new identifier for this CODECHECK?" msgid "plugins.generic.codecheck.identifier.removeConfirm" msgstr "Are you sure you want to remove this identifier?" -msgid "plugins.generic.codecheck.identifier.labels" -msgstr "GitHub Labels" +msgid "plugins.generic.codecheck.source.label" +msgstr "Source" + +msgid "plugins.generic.codecheck.source.description" +msgstr "Description or URL of the source material (use when material comes from multiple sources)" + +msgid "plugins.generic.codecheck.source.placeholder" +msgstr "e.g., Code from GitHub, data from Zenodo and OSF" + +msgid "plugins.generic.codecheck.additionalContent.label" +msgstr "Additional YAML Content (Optional)" + +msgid "plugins.generic.codecheck.additionalContent.description" +msgstr "Add custom YAML fields for specific use cases. This content will be appended at the end of the generated YAML file. For advanced users only." + +msgid "plugins.generic.codecheck.additionalContent.placeholder" +msgstr "Example:\npublishing_inc_identifier: 12345\npublishing_inc_handler: Ed Editor\n\nTheBestRepository:\n recordId: 1a2b3c\n checksum: abc123..." + +msgid "plugins.generic.codecheck.additionalContent.frontendLabel" +msgstr "Additional information" msgid "plugins.generic.codecheck.previewYaml" msgstr "Preview CODECHECK metadata file" +msgid "plugins.generic.codecheck.yamlPreview" +msgstr "YAML Preview" + +msgid "plugins.generic.codecheck.yamlPreviewFailed" +msgstr "Failed to generate preview" + +msgid "plugins.generic.codecheck.yamlGenerationFailed" +msgstr "Failed to generate YAML Content" + msgid "plugins.generic.codecheck.yaml.valid" msgstr "codecheck.yml is valid." msgid "plugins.generic.codecheck.yaml.invalid" msgstr "CODECHECK metadata creates a structurally invalid codecheck.yml file. Please check the detailed error message below." +msgid "plugins.generic.codecheck.yaml.download" +msgstr "Download" + +msgid "plugins.generic.codecheck.yaml.close" +msgstr "Close" + +msgid "plugins.generic.codecheck.yaml.previewTitle" +msgstr "CODECHECK Metadata Preview (codecheck.yml)" + msgid "plugins.generic.codecheck.savedSuccessfully" msgstr "CODECHECK metadata saved successfully" msgid "plugins.generic.codecheck.saveFailed" msgstr "Failed to save CODECHECK metadata. Please try again." +msgid "plugins.generic.codecheck.loadError" +msgstr "Failed to load CODECHECK data" + +msgid "plugins.generic.codecheck.noDataFound" +msgstr "No CODECHECK data found." + msgid "plugins.generic.codecheck.reviewTitle" msgstr "CODECHECK Information" @@ -407,6 +572,9 @@ msgstr "The mandatory CODECHECK for this submission has no status (so it was not msgid "plugins.generic.codecheck.status.pending" msgstr "Pending" +msgid "plugins.generic.codecheck.status.inProgress" +msgstr "In Progress" + msgid "plugins.generic.codecheck.status.needsCodechecker" msgstr "needs codechecker" @@ -434,107 +602,53 @@ msgstr "published certificate: partial reproduction" msgid "plugins.generic.codecheck.status.publishedCertificate.fullReproduction" msgstr "published certificate: full reproduction" -msgid "plugins.generic.codecheck.dataAvailability.placeholder" -msgstr "Describe how your data and code are available..." - -msgid "plugins.generic.codecheck.review.title" -msgstr "CODECHECK Information" - -msgid "plugins.generic.codecheck.notYetAssigned" -msgstr "Not yet assigned" - -msgid "plugins.generic.codecheck.viewFullMetadata" -msgstr "View Full CODECHECK Metadata" - -msgid "plugins.generic.codecheck.notOptedIn" -msgstr "This submission has not opted in for CODECHECK" - -msgid "plugins.generic.codecheck.workflow.label" -msgstr "CODECHECK" - -msgid "plugins.generic.codecheck.codecheckStatus" -msgstr "CODECHECK Status" - -msgid "plugins.generic.codecheck.markAsOutput" -msgstr "Mark as CODECHECK Output" - -msgid "plugins.generic.codecheck.markAsOutputTitle" -msgstr "Mark as CODECHECK Output" - -msgid "plugins.generic.codecheck.markAsOutputConfirm" -msgstr "Do you want to mark \"{$fileName}\" as a CODECHECK output file?" - msgid "plugins.generic.codecheck.status.marked" msgstr "Marked" msgid "plugins.generic.codecheck.status.notMarked" msgstr "Not Marked" -msgid "plugins.generic.codecheck.paperMetadata.readOnlyNotice" -msgstr "This information comes from the original submission and cannot be edited here." - -msgid "plugins.generic.codecheck.paperMetadata.noAuthors" -msgstr "No authors found" - -msgid "plugins.generic.codecheck.workflow.description" -msgstr "Complete the following fields to document the CODECHECK process." - -msgid "plugins.generic.codecheck.manifest.emptyState" -msgstr "No manifest files added yet. Click \"+ Add File\" to add files." - -msgid "plugins.generic.codecheck.manifest.removeConfirm" -msgstr "Remove this file from the manifest?" - -msgid "plugins.generic.codecheck.repositories.description" -msgstr "URL of the forked/archived repository used for the CODECHECK." +msgid "plugins.generic.codecheck.status.verificationInProgress" +msgstr "Code verification is currently in progress." -msgid "plugins.generic.codecheck.codecheckers.emptyState" -msgstr "No codecheckers added yet" +msgid "plugins.generic.codecheck.review.title" +msgstr "CODECHECK Information" -msgid "plugins.generic.codecheck.codecheckers.enterName" -msgstr "Enter codechecker name:" +msgid "plugins.generic.codecheck.review.configVersion" +msgstr "Config Version" -msgid "plugins.generic.codecheck.codecheckers.enterOrcid" -msgstr "Enter ORCID (optional):" +msgid "plugins.generic.codecheck.review.publicationType" +msgstr "Publication Type" -msgid "plugins.generic.codecheck.codecheckers.removeConfirm" -msgstr "Remove this codechecker?" +msgid "plugins.generic.codecheck.review.publicationType.doi" +msgstr "DOI (e.g., Zenodo)" -msgid "plugins.generic.codecheck.certificate.generateId" -msgstr "Generate ID" +msgid "plugins.generic.codecheck.review.publicationType.separate" +msgstr "Separate Journal Section" -msgid "plugins.generic.codecheck.certificate.summaryDescription" -msgstr "Describe the CODECHECK process and any notable findings." +msgid "plugins.generic.codecheck.review.manifestFiles" +msgstr "Manifest Files" -msgid "plugins.generic.codecheck.certificate.summaryPlaceholder" -msgstr "Successfully reproduced all main figures..." +msgid "plugins.generic.codecheck.review.codecheckers" +msgstr "Codecheckers" -msgid "plugins.generic.codecheck.certificate.report" +msgid "plugins.generic.codecheck.review.report" msgstr "Report URL" -msgid "plugins.generic.codecheck.certificate.reportDescription" -msgstr "Link to the published CODECHECK report (e.g., Zenodo)." - -msgid "plugins.generic.codecheck.identifier.saved" -msgstr "Identifier \"{$identifier}\" saved!" - -msgid "plugins.generic.codecheck.identifier.enterFirst" -msgstr "Please enter an identifier first." - -msgid "plugins.generic.codecheck.identifier.reserved" -msgstr "New identifier reserved: {$identifier}" +msgid "plugins.generic.codecheck.review.reportUrl" +msgstr "Report URL" -msgid "plugins.generic.codecheck.loadError" -msgstr "Failed to load CODECHECK data" +msgid "plugins.generic.codecheck.modal.cancel" +msgstr "Cancel" -msgid "plugins.generic.codecheck.yamlPreview" -msgstr "YAML Preview" +msgid "plugins.generic.codecheck.modal.add" +msgstr "Add" -msgid "plugins.generic.codecheck.yamlPreviewFailed" -msgstr "Failed to generate preview" +msgid "plugins.generic.codecheck.modal.close" +msgstr "Close" -msgid "plugins.generic.codecheck.yamlGenerationFailed" -msgstr "Failed to generate YAML Content" +msgid "plugins.generic.codecheck.modal.change" +msgstr "Change" msgid "plugins.generic.codecheck.validation.manifestRequired" msgstr "Please add at least one manifest file" @@ -551,200 +665,242 @@ msgstr "Please link the OJS form to the corresponding GitHub Issue with the same msgid "plugins.generic.codecheck.validation.summaryRequired" msgstr "Please provide a summary" -msgid "plugins.generic.codecheck.repositories.emptyState" -msgstr "No repositories added yet" +msgid "plugins.generic.codecheck.preview.saveFirst" +msgstr "Please save your changes before previewing" -msgid "plugins.generic.codecheck.repositories.infoTitle" -msgstr "Repositories Info" +msgid "plugins.generic.codecheck.preview.missingRequired" +msgstr "Please complete all required fields (manifest, codecheckers, certificate)" -msgid "plugins.generic.codecheck.repositories.infoTextOne" -msgstr "If you desire, you can directly load the metadata from the codecheck.yml of the provided CODECHECK Repository, if it contains a valid codecheck.yml." +msgid "plugins.generic.codecheck.preview.ready" +msgstr "Click to preview YAML metadata" -msgid "plugins.generic.codecheck.repositories.infoTextTwo" -msgstr "The supported CODECHECK Repository Sites are GitHub, OSF, Zenodo and GitLab." +msgid "plugins.generic.codecheck.notYetAssigned" +msgstr "Not yet assigned" -msgid "plugins.generic.codecheck.repositories.infoTextNote" -msgstr "Note that loading the data from a remote repository will overwrite, but not save, the values in the current form." +msgid "plugins.generic.codecheck.viewFullMetadata" +msgstr "View Full CODECHECK Metadata" -msgid "plugins.generic.codecheck.repositories.infoTextMoreInformation" -msgstr "More Information: " +msgid "plugins.generic.codecheck.notOptedIn" +msgstr "This submission has not opted in for CODECHECK" -msgid "plugins.generic.codecheck.repositories.infoTextLinkToAllowedRepositories" -msgstr "https://github.com/codecheckers/register/blob/master/README.md#editing-the-register" +msgid "plugins.generic.codecheck.viewCertificate" +msgstr "View CODECHECK Certificate" -msgid "plugins.generic.codecheck.repositories.removeConfirm" -msgstr "Remove this repository?" +msgid "plugins.generic.codecheck.badge.altText" +msgstr "CODE WORKS" -msgid "plugins.generic.codecheck.manifestFiles.removeConfirm" -msgstr "Remove this file from the manifest?" +msgid "plugins.generic.codecheck.codecheckStatus" +msgstr "CODECHECK Status" -msgid "plugins.generic.codecheck.yaml.download" -msgstr "Download" +msgid "plugins.generic.codecheck.markAsOutput" +msgstr "Mark as CODECHECK Output" -msgid "plugins.generic.codecheck.yaml.close" -msgstr "Close" +msgid "plugins.generic.codecheck.markAsOutputTitle" +msgstr "Mark as CODECHECK Output" -msgid "plugins.generic.codecheck.yaml.previewTitle" -msgstr "CODECHECK Metadata Preview (codecheck.yml)" +msgid "plugins.generic.codecheck.markAsOutputConfirm" +msgstr "Do you want to mark \"{$fileName}\" as a CODECHECK output file?" -msgid "plugins.generic.codecheck.modal.close" -msgstr "Close" +msgid "plugins.generic.codecheck.moreDetails" +msgstr "More details" -msgid "plugins.generic.codecheck.modal.cancel" -msgstr "Cancel" +msgid "plugins.generic.codecheck.hideDetails" +msgstr "Hide details" -msgid "plugins.generic.codecheck.modal.add" +msgid "plugins.generic.codecheck.checkDate" +msgstr "Check date" + +msgid "plugins.generic.codecheck.dashboard.columnHeader" +msgstr "CODECHECK" + +msgid "plugins.generic.codecheck.dashboard.add" msgstr "Add" -msgid "plugins.generic.codecheck.modal.change" -msgstr "Change" +msgid "plugins.generic.codecheck.warning.notOptedIn" +msgstr "This article was not opted into CODECHECK during submission." -msgid "plugins.generic.codecheck.review.configVersion" -msgstr "Config Version" +msgid "plugins.generic.codecheck.warning.optedOut" +msgstr "The author opted out of CODECHECK for this article." -msgid "plugins.generic.codecheck.review.publicationType" -msgstr "Publication Type" +msgid "plugins.generic.codecheck.github.issue.display" +msgstr "Github Register Issue" -msgid "plugins.generic.codecheck.review.publicationType.doi" -msgstr "DOI (e.g., Zenodo)" +msgid "plugins.generic.codecheck.github.issue.display.errorModal.title" +msgstr "No URL is currently configured for the GitHub Register Issue." -msgid "plugins.generic.codecheck.review.publicationType.separate" -msgstr "Separate Journal Section" +msgid "plugins.generic.codecheck.github.issue.display.errorModal.message" +msgstr "Please first connect to the Issue with the reservation of a certificate identifier." -msgid "plugins.generic.codecheck.review.manifestFiles" -msgstr "Manifest Files" +msgid "plugins.generic.codecheck.github.issue.display.button.viewIssue" +msgstr "View Issue" -msgid "plugins.generic.codecheck.review.codecheckers" -msgstr "Codecheckers" +msgid "plugins.generic.codecheck.github.issue.display.connected" +msgstr "Connected" -msgid "plugins.generic.codecheck.review.report" -msgstr "Report URL" +msgid "plugins.generic.codecheck.github.issue.display.disconnected" +msgstr "Disconnected" -msgid "plugins.generic.codecheck.paperMetadata.readonlyDescription" -msgstr "These fields are automatically populated from the submission and cannot be edited here." +msgid "plugins.generic.codecheck.github.issue.display.certificateIdentifier" +msgstr "Certificate Identifier: {$identifier}" -msgid "plugins.generic.codecheck.paperMetadata.fieldReadonly" -msgstr "This field is read-only" +msgid "plugins.generic.codecheck.github.issue.display.certificateIdentifier.notSet" +msgstr "not set" -msgid "plugins.generic.codecheck.source.label" -msgstr "Source" +msgid "plugins.generic.codecheck.github.issue.display.registerRepository" +msgstr "Register Repository: {$repository}" -msgid "plugins.generic.codecheck.source.description" -msgstr "Description or URL of the source material (use when material comes from multiple sources)" +msgid "plugins.generic.codecheck.orcid.settingsTitle" +msgstr "ORCID Deposition" -msgid "plugins.generic.codecheck.source.placeholder" -msgstr "e.g., Code from GitHub, data from Zenodo and OSF" +msgid "plugins.generic.codecheck.orcid.settingsDescription" +msgstr "Deposit CODECHECK activity to codecheckers' ORCID profiles using the ORCID Member API. The journal must register a Member API Client at orcid.org. Start with the sandbox API during development." -msgid "plugins.generic.codecheck.additionalContent.label" -msgstr "Additional YAML Content (Optional)" +msgid "plugins.generic.codecheck.orcid.enable" +msgstr "Enable ORCID deposition for CODECHECKs" -msgid "plugins.generic.codecheck.additionalContent.description" -msgstr "Add custom YAML fields for specific use cases. This content will be appended at the end of the generated YAML file. For advanced users only." +msgid "plugins.generic.codecheck.orcid.apiType" +msgstr "ORCID API" -msgid "plugins.generic.codecheck.additionalContent.placeholder" -msgstr "Example:\npublishing_inc_identifier: 12345\npublishing_inc_handler: Ed Editor\n\nTheBestRepository:\n recordId: 1a2b3c\n checksum: abc123..." +msgid "plugins.generic.codecheck.orcid.apiType.sandbox" +msgstr "Member Sandbox (development/testing)" -msgid "plugins.generic.codecheck.viewCertificate" -msgstr "View CODECHECK Certificate" +msgid "plugins.generic.codecheck.orcid.apiType.production" +msgstr "Member API (production)" -msgid "plugins.generic.codecheck.badge.altText" -msgstr "CODE WORKS" +msgid "plugins.generic.codecheck.orcid.clientId" +msgstr "Client ID" -msgid "plugins.generic.codecheck.certificate.prefix" -msgstr "CODECHECK" +msgid "plugins.generic.codecheck.orcid.clientIdDescription" +msgstr "Your ORCID Member API Client ID, e.g. APP-XXXXXXXXXXXX" -msgid "plugins.generic.codecheck.certificate.viewLink" -msgstr "View Certificate" +msgid "plugins.generic.codecheck.orcid.clientSecret" +msgstr "Client Secret" -msgid "plugins.generic.codecheck.noDataFound" -msgstr "No CODECHECK data found." +msgid "plugins.generic.codecheck.orcid.clientSecretDescription" +msgstr "Your ORCID Member API Client Secret. Leave blank to keep the current value." -msgid "plugins.generic.codecheck.repository.placeholder" -msgstr "https://github.com/organization/repo" +msgid "plugins.generic.codecheck.orcid.sectionTitle" +msgstr "ORCID Deposition" -msgid "plugins.generic.codecheck.repository.addButton" -msgstr "+ Add URL" +msgid "plugins.generic.codecheck.orcid.sectionDescription" +msgstr "Codecheckers must authorise this journal to deposit their CODECHECK activity to their ORCID profile. Once authorised, deposition happens automatically on publication or can be triggered manually below." -msgid "plugins.generic.codecheck.repository.validation.protocol" -msgstr "URL must start with http:// or https://" +msgid "plugins.generic.codecheck.orcid.noCodecheckers" +msgstr "No codecheckers have been assigned yet." -msgid "plugins.generic.codecheck.repository.validation.invalid" -msgstr "Please enter a valid URL" +msgid "plugins.generic.codecheck.orcid.authorise" +msgstr "Authorise ORCID" -msgid "plugins.generic.codecheck.manifestFiles.filenamePlaceholder" -msgstr "Filename (e.g., figures/figure1.png)" +msgid "plugins.generic.codecheck.orcid.reAuthorise" +msgstr "Re-authorise ORCID" -msgid "plugins.generic.codecheck.manifestFiles.commentPlaceholder" -msgstr "Comment (e.g., Main result visualization)" +msgid "plugins.generic.codecheck.orcid.deposit" +msgstr "Deposit to ORCID" -msgid "plugins.generic.codecheck.manifestFiles.addButton" -msgstr "+ Add File" +msgid "plugins.generic.codecheck.orcid.reDeposit" +msgstr "Re-deposit to ORCID" -msgid "plugins.generic.codecheck.preview.saveFirst" -msgstr "Please save your changes before previewing" +msgid "plugins.generic.codecheck.orcid.depositAll" +msgstr "Deposit to all authorised ORCID profiles" -msgid "plugins.generic.codecheck.preview.missingRequired" -msgstr "Please complete all required fields (manifest, codecheckers, certificate)" +msgid "plugins.generic.codecheck.orcid.viewOnOrcid" +msgstr "View on ORCID" -msgid "plugins.generic.codecheck.preview.ready" -msgstr "Click to preview YAML metadata" +msgid "plugins.generic.codecheck.orcid.status.notAuthorised" +msgstr "Not authorised" -msgid "plugins.generic.codecheck.moreDetails" -msgstr "More details" +msgid "plugins.generic.codecheck.orcid.status.pending" +msgstr "Pending deposit" -msgid "plugins.generic.codecheck.hideDetails" -msgstr "Hide details" +msgid "plugins.generic.codecheck.orcid.status.deposited" +msgstr "Deposited" -msgid "plugins.generic.codecheck.checkDate" -msgstr "Check date" +msgid "plugins.generic.codecheck.orcid.status.failed" +msgstr "Deposit failed" -msgid "plugins.generic.codecheck.status.verificationInProgress" -msgstr "Code verification is currently in progress." +msgid "plugins.generic.codecheck.orcid.authoriseByCodechecker" +msgstr "Must be authorised by the codechecker." -msgid "plugins.generic.codecheck.additionalContent.frontendLabel" -msgstr "Additional information" +msgid "plugins.generic.codecheck.orcid.test.title" +msgstr "Test ORCID Setup" -msgid "plugins.generic.codecheck.dashboard.columnHeader" -msgstr "CODECHECK" +msgid "plugins.generic.codecheck.orcid.test.description" +msgstr "Verify that journal metadata and ORCID credentials are correctly configured. This makes an authenticated request to ORCID without writing any data." -msgid "plugins.generic.codecheck.dashboard.add" -msgstr "Add" +msgid "plugins.generic.codecheck.orcid.test.button" +msgstr "Test ORCID Setup" -msgid "plugins.generic.codecheck.warning.notOptedIn" -msgstr "This article was not opted into CODECHECK during submission." +msgid "plugins.generic.codecheck.orcid.test.success" +msgstr "ORCID setup is correctly configured. Journal metadata and credentials are valid." -msgid "plugins.generic.codecheck.warning.optedOut" -msgstr "The author opted out of CODECHECK for this article." +msgid "plugins.generic.codecheck.orcid.test.error.noCredentials" +msgstr "Client ID and Client Secret are not configured in plugin settings." -msgid "plugins.generic.codecheck.settings.showDashboardColumn" -msgstr "Show CODECHECK column in submissions dashboard" +msgid "plugins.generic.codecheck.orcid.test.error.credentialsFailed" +msgstr "ORCID credentials test failed:" -msgid "plugins.generic.codecheck.settings.showDashboardColumn.description" -msgstr "Display a CODECHECK status column in the editorial submissions dashboard. Default: enabled." +msgid "plugins.generic.codecheck.orcid.auth.error.notEnabled" +msgstr "ORCID integration is not enabled for this journal." -msgid "plugins.generic.codecheck.github.issue.display" -msgstr "Github Register Issue" +msgid "plugins.generic.codecheck.orcid.auth.error.noCredentials" +msgstr "ORCID credentials are not configured. Please ask the journal manager to set the Client ID and Client Secret in the CODECHECK plugin settings." -msgid "plugins.generic.codecheck.github.issue.display.errorModal.title" -msgstr "No URL is currently configured for the GitHub Register Issue." +msgid "plugins.generic.codecheck.orcid.auth.error.missingSubmissionId" +msgstr "Missing submission ID parameter." -msgid "plugins.generic.codecheck.github.issue.display.errorModal.message" -msgstr "Please first connect to the Issue with the reservation of a certificate identifier." +msgid "plugins.generic.codecheck.orcid.auth.error.accessDenied" +msgstr "Access denied." -msgid "plugins.generic.codecheck.github.issue.display.button.viewIssue" -msgstr "View Issue" +msgid "plugins.generic.codecheck.orcid.auth.error.denied" +msgstr "ORCID authorisation denied: {$error}" -msgid "plugins.generic.codecheck.github.issue.display.connected" -msgstr "Connected" +msgid "plugins.generic.codecheck.orcid.auth.error.invalidCallback" +msgstr "Invalid ORCID callback: missing code or state." -msgid "plugins.generic.codecheck.github.issue.display.disconnected" -msgstr "Disconnected" +msgid "plugins.generic.codecheck.orcid.auth.error.invalidState" +msgstr "Invalid state parameter." -msgid "plugins.generic.codecheck.github.issue.display.certificateIdentifier" -msgstr "Certificate Identifier: {$identifier}" +msgid "plugins.generic.codecheck.orcid.auth.error.securityCheck" +msgstr "Security check failed. Please try again." -msgid "plugins.generic.codecheck.github.issue.display.certificateIdentifier.notSet" -msgstr "not set" +msgid "plugins.generic.codecheck.orcid.auth.error.submissionNotFound" +msgstr "Submission not found." -msgid "plugins.generic.codecheck.github.issue.display.registerRepository" -msgstr "Register Repository: {$repository}" +msgid "plugins.generic.codecheck.orcid.auth.error.orcidMismatch" +msgstr "The ORCID iD you authenticated with ({$orcidId}) does not match any codechecker ORCID on record for this submission. Please sign in with the correct ORCID account." + +msgid "plugins.generic.codecheck.orcid.auth.error.tokenExchange" +msgstr "ORCID token exchange failed: {$error}" + +msgid "plugins.generic.codecheck.orcid.auth.error.prefix" +msgstr "Error" + +msgid "plugins.generic.codecheck.orcid.auth.success" +msgstr "Authorisation successful. You may close this window." + +msgid "plugins.generic.codecheck.orcid.auth.closeWindow" +msgstr "Close this window" + +msgid "plugins.generic.codecheck.orcid.test.button.testing" +msgstr "Testing..." + +msgid "plugins.generic.codecheck.orcid.test.error.requestFailed" +msgstr "Request failed. Please check your network connection." + +msgid "plugins.generic.codecheck.orcid.city" +msgstr "Publisher City" + +msgid "plugins.generic.codecheck.orcid.cityDescription" +msgstr "City of the publishing organisation, required for ORCID peer-review deposition via the Member API." + +msgid "plugins.generic.codecheck.orcid.payload.untitledSubmission" +msgstr "Untitled submission" + +msgid "plugins.generic.codecheck.orcid.payload.missingMetadata" +msgstr "ORCID deposition requires the following journal metadata to be configured: {$fields}" + +msgid "plugins.generic.codecheck.orcid.payload.missingPublisherName" +msgstr "Publisher Name (Journal Settings → Masthead → Publisher)" + +msgid "plugins.generic.codecheck.orcid.payload.missingCountry" +msgstr "Country (Journal Settings → Masthead → Country)" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 22f959ab..4f45b088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -27,9 +27,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -37,13 +37,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -53,14 +53,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -97,9 +97,9 @@ } }, "node_modules/@cypress/vue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@cypress/vue/-/vue-6.0.2.tgz", - "integrity": "sha512-To+Ik4CnhTPMmDh7VGpl5k0Z1LuE0xrI0j6LF7QyjROY2bkQwUE50WTmVPCz/Dvez9WrVpkJjRmflR5KlVmGiQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@cypress/vue/-/vue-6.0.3.tgz", + "integrity": "sha512-nJyjzvEaqD7i8NZ0mXudlwkth7+26OCM27x3SC9swFsN4NELe8+6yNch3z/Ja5Bq+tSwpP8LsF0O7zDHN2Q7zQ==", "dev": true, "license": "MIT", "engines": { @@ -536,9 +536,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -550,9 +550,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -564,9 +564,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -578,9 +578,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -592,9 +592,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -606,9 +606,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -620,9 +620,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], @@ -634,9 +634,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], @@ -648,9 +648,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], @@ -662,9 +662,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], @@ -676,9 +676,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], @@ -690,9 +690,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], @@ -704,9 +704,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], @@ -718,9 +718,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], @@ -732,9 +732,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], @@ -746,9 +746,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], @@ -760,9 +760,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], @@ -774,9 +774,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], @@ -788,9 +788,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], @@ -802,9 +802,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", "cpu": [ "x64" ], @@ -816,9 +816,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -830,9 +830,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -844,9 +844,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -858,9 +858,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -872,9 +872,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -893,14 +893,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -943,57 +943,57 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", + "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", + "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-core": "3.5.35", + "@vue/shared": "3.5.35" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", - "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", + "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.32", - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.35", + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", - "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", + "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.35", + "@vue/shared": "3.5.35" } }, "node_modules/@vue/devtools-api": { @@ -1033,57 +1033,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", - "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", + "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.32" + "@vue/shared": "3.5.35" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", - "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", + "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/reactivity": "3.5.35", + "@vue/shared": "3.5.35" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", - "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", + "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/runtime-core": "3.5.32", - "@vue/shared": "3.5.32", + "@vue/reactivity": "3.5.35", + "@vue/runtime-core": "3.5.35", + "@vue/shared": "3.5.35", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", - "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", + "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-ssr": "3.5.35", + "@vue/shared": "3.5.35" }, "peerDependencies": { - "vue": "3.5.32" + "vue": "3.5.35" } }, "node_modules/@vue/shared": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", + "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", "dev": true, "license": "MIT" }, @@ -1698,9 +1698,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "dev": true, "license": "MIT" }, @@ -1823,9 +1823,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -2251,9 +2251,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2455,9 +2455,9 @@ "license": "ISC" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2689,9 +2689,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2853,9 +2853,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2873,7 +2873,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2970,9 +2970,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -2986,31 +2986,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, @@ -3053,9 +3053,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -3109,14 +3109,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3337,9 +3337,9 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -3407,9 +3407,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT", "optional": true @@ -3438,6 +3438,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "dev": true, "license": "MIT", "bin": { @@ -3520,17 +3521,17 @@ } }, "node_modules/vue": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", - "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", + "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-sfc": "3.5.32", - "@vue/runtime-dom": "3.5.32", - "@vue/server-renderer": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.35", + "@vue/compiler-sfc": "3.5.35", + "@vue/runtime-dom": "3.5.35", + "@vue/server-renderer": "3.5.35", + "@vue/shared": "3.5.35" }, "peerDependencies": { "typescript": "*" diff --git a/registry/uiLocaleKeysBackend.json b/registry/uiLocaleKeysBackend.json index d481ba68..f294d7ac 100644 --- a/registry/uiLocaleKeysBackend.json +++ b/registry/uiLocaleKeysBackend.json @@ -91,6 +91,20 @@ "plugins.generic.codecheck.no", "plugins.generic.codecheck.noDataFound", "plugins.generic.codecheck.notOptedIn", + "plugins.generic.codecheck.orcid.authorise", + "plugins.generic.codecheck.orcid.authoriseByCodechecker", + "plugins.generic.codecheck.orcid.deposit", + "plugins.generic.codecheck.orcid.depositAll", + "plugins.generic.codecheck.orcid.noCodecheckers", + "plugins.generic.codecheck.orcid.reAuthorise", + "plugins.generic.codecheck.orcid.reDeposit", + "plugins.generic.codecheck.orcid.sectionDescription", + "plugins.generic.codecheck.orcid.sectionTitle", + "plugins.generic.codecheck.orcid.status.deposited", + "plugins.generic.codecheck.orcid.status.failed", + "plugins.generic.codecheck.orcid.status.notAuthorised", + "plugins.generic.codecheck.orcid.status.pending", + "plugins.generic.codecheck.orcid.viewOnOrcid", "plugins.generic.codecheck.paperMetadata.authors", "plugins.generic.codecheck.paperMetadata.fieldReadonly", "plugins.generic.codecheck.paperMetadata.noAuthors", diff --git a/resources/js/Components/CodecheckOrcidSection.vue b/resources/js/Components/CodecheckOrcidSection.vue new file mode 100644 index 00000000..21bc5460 --- /dev/null +++ b/resources/js/Components/CodecheckOrcidSection.vue @@ -0,0 +1,412 @@ + + + + + \ No newline at end of file diff --git a/resources/js/main.js b/resources/js/main.js index 14ebeb3c..49ea5d8c 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -3,6 +3,7 @@ import CodecheckManifestFiles from "./Components/CodecheckManifestFiles.vue"; import CodecheckRepositoryList from "./Components/CodecheckRepositoryList.vue"; import CodecheckReviewDisplay from "./Components/CodecheckReviewDisplay.vue"; import CodecheckDataAndSoftwareAvailability from "./Components/CodecheckDataAndSoftwareAvailability.vue"; +import CodecheckOrcidSection from "./Components/CodecheckOrcidSection.vue"; import CodecheckMetadataForm from './Components/CodecheckMetadataForm.vue'; import CodecheckStatusForm from './Components/CodecheckStatusForm.vue'; import CodecheckGithubIssueDisplay from "./Components/CodecheckGithubIssueDisplay.vue"; @@ -12,6 +13,7 @@ pkp.registry.registerComponent("CodecheckMetadataForm", CodecheckMetadataForm); pkp.registry.registerComponent("CodecheckManifestFiles", CodecheckManifestFiles); pkp.registry.registerComponent("CodecheckRepositoryList", CodecheckRepositoryList); pkp.registry.registerComponent("CodecheckDataAndSoftwareAvailability", CodecheckDataAndSoftwareAvailability); +pkp.registry.registerComponent("CodecheckOrcidSection", CodecheckOrcidSection); pkp.registry.registerComponent("CodecheckStatusForm", CodecheckStatusForm); pkp.registry.registerComponent("CodecheckGithubIssueDisplay", CodecheckGithubIssueDisplay); @@ -60,23 +62,39 @@ pkp.registry.storeExtend("workflow", (piniaContext) => { workflowStore.extender.extendFn("getPrimaryItems", (primaryItems, args) => { const submission = args?.submission; - + if ( args?.selectedMenuState?.primaryMenuItem === "workflow" && args?.selectedMenuState?.stageId === 999 ) { - return [ + const orcidConfig = window.codecheckOrcidConfig ?? {}; + + const items = [ { title: "WORKFLOW: CODECHECK", component: "CodecheckMetadataForm", props: { submission: submission, canEdit: true, - // Pass the journal mode so the form can show the opt-in warning box (Issue #30) codecheckMode: window.codecheckDashboardConfig?.codecheckMode ?? 'opt-in', }, } ]; + + if (orcidConfig.enabled) { + items.push({ + component: "CodecheckOrcidSection", + props: { + submission: submission, + orcidEnabled: orcidConfig.enabled, + orcidAuthUrl: orcidConfig.authUrl, + orcidApiType: orcidConfig.apiType, + canAuthorise: false, + }, + }); + } + + return items; } if ( @@ -97,7 +115,6 @@ pkp.registry.storeExtend("workflow", (piniaContext) => { }); workflowStore.extender.extendFn("getSecondaryItems", (sidebarItems, args) => { - const store = pkp.registry.stores?.workflow; const submission = args?.submission; if ( @@ -190,7 +207,9 @@ pkp.registry.storeExtend("fileManager_SUBMISSION_FILES", (piniaContext) => { }); }); -// Submission wizard field management +// ----------------------------------------------------------------------- +// Submission wizard: save/load field data via API +// ----------------------------------------------------------------------- class CodecheckWizardManager { constructor() { this.textareas = {}; @@ -275,8 +294,6 @@ class CodecheckWizardManager { document.addEventListener('click', (e) => { const button = e.target.closest('button'); if (!button) return; - - // Save on any button click except cancel if (button.id !== 'cancelSubmission') { this.saveData(); } @@ -289,7 +306,9 @@ class CodecheckWizardManager { } } -// Review section refresher +// ----------------------------------------------------------------------- +// Submission wizard: refresh review panel data +// ----------------------------------------------------------------------- class CodecheckReviewRefresher { constructor() { this.refreshedPanels = new Set(); @@ -309,50 +328,31 @@ class CodecheckReviewRefresher { } }); - observer.observe(document.body, { - childList: true, - subtree: true - }); + observer.observe(document.body, { childList: true, subtree: true }); } isOnReviewStep() { - // Find the review step container const allSteps = document.querySelectorAll('.pkpStep'); - for (const step of allSteps) { - // Check if this step contains review panels AND is not hidden const hasReviewPanels = step.querySelectorAll('.submissionWizard__reviewPanel').length >= 3; const isVisible = !step.hasAttribute('hidden'); - - if (hasReviewPanels && isVisible) { - return true; - } + if (hasReviewPanels && isVisible) return true; } - return false; } checkForReviewPanel() { const allH3s = document.querySelectorAll('.submissionWizard__reviewPanel h3'); - for (const h3 of allH3s) { if (h3.textContent.includes('CODECHECK')) { const panel = h3.closest('.submissionWizard__reviewPanel'); - if (!panel) continue; - const rect = panel.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) continue; - const panelContent = panel.innerHTML.substring(0, 100); - if (!this.refreshedPanels.has(panelContent)) { this.refreshedPanels.add(panelContent); - - setTimeout(() => { - this.refreshReviewData(panel); - }, 200); - + setTimeout(() => { this.refreshReviewData(panel); }, 200); return; } } @@ -366,7 +366,6 @@ class CodecheckReviewRefresher { try { const response = await fetch(`${pkp.context.apiBaseUrl}/submissions/${submissionId}`); const submission = await response.json(); - const publication = submission.publications?.find(p => p.id === submission.currentPublicationId); if (!publication) return; @@ -374,65 +373,26 @@ class CodecheckReviewRefresher { if (!body) return; body.innerHTML = ''; - let hasData = false; - + if (publication.codeRepository) { hasData = true; - body.innerHTML += ` -
-

${this.escapeHtml(t('plugins.generic.codecheck.codeRepository'))}

-
-

${this.escapeHtml(publication.codeRepository).replace(/\n/g, '
')}

-
-
- `; + body.innerHTML += `

${this.escapeHtml(t('plugins.generic.codecheck.codeRepository'))}

${this.escapeHtml(publication.codeRepository).replace(/\n/g, '
')}

`; } - if (publication.dataRepository) { hasData = true; - body.innerHTML += ` -
-

${this.escapeHtml(t('plugins.generic.codecheck.dataRepository'))}

-
-

${this.escapeHtml(publication.dataRepository).replace(/\n/g, '
')}

-
-
- `; + body.innerHTML += `

${this.escapeHtml(t('plugins.generic.codecheck.dataRepository'))}

${this.escapeHtml(publication.dataRepository).replace(/\n/g, '
')}

`; } - if (publication.manifestFiles) { hasData = true; - body.innerHTML += ` -
-

${this.escapeHtml(t('plugins.generic.codecheck.manifestFiles.label'))}

-
-
${this.escapeHtml(publication.manifestFiles)}
-
-
- `; + body.innerHTML += `

${this.escapeHtml(t('plugins.generic.codecheck.manifestFiles.label'))}

${this.escapeHtml(publication.manifestFiles)}
`; } - if (publication.dataAvailabilityStatement) { hasData = true; - body.innerHTML += ` -
-

${this.escapeHtml(t('plugins.generic.codecheck.dataAvailability'))}

-
-
${publication.dataAvailabilityStatement}
-
-
- `; + body.innerHTML += `

${this.escapeHtml(t('plugins.generic.codecheck.dataAvailability'))}

${publication.dataAvailabilityStatement}
`; } - if (!hasData) { - body.innerHTML = ` -
-

- ${this.escapeHtml(t('plugins.generic.codecheck.noDataFound'))} -

-
- `; + body.innerHTML = `

${this.escapeHtml(t('plugins.generic.codecheck.noDataFound'))}

`; } } catch (error) { console.error('CODECHECK: Failed to refresh review data', error); @@ -452,20 +412,108 @@ class CodecheckReviewRefresher { } } -// Initialize — mount Vue components only after WizardManager has loaded saved data -// into textareas, so components receive the correct initial values +// ----------------------------------------------------------------------- +// Initialization +// ----------------------------------------------------------------------- window.addEventListener('DOMContentLoaded', () => { - setTimeout(() => { - new CodecheckReviewRefresher(); - }, 200); - setTimeout(async () => { const manager = new CodecheckWizardManager(); await manager.init(); mountCodecheckVueComponents(); }, 100); + + setTimeout(() => { + new CodecheckReviewRefresher(); + + if (window.codecheckReviewerData) { + const tab3Link = document.querySelector('#reviewTabs ul li:nth-child(3) a'); + if (tab3Link) { + const badge = document.createElement('span'); + badge.style.cssText = 'margin-left: 0.4rem; font-size: 0.7rem; background: #008033; color: white; padding: 0.1rem 0.3rem; border-radius: 3px; vertical-align: middle;'; + badge.textContent = 'CODECHECK'; + tab3Link.appendChild(badge); + } + } + }, 1000); + + const observer = new MutationObserver(() => { + const step3 = document.querySelector('#reviewStep3'); + if (step3 && step3.children.length > 0 && !document.querySelector('#codecheck-reviewer-form')) { + window.mountCodecheckReviewerForm(); + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); }); +// ----------------------------------------------------------------------- +// Reviewer page: mount CODECHECK form inside tab 3 (Download & Review). +// ----------------------------------------------------------------------- +function mountCodecheckReviewerForm() { + if (!window.codecheckReviewerData) return; + + const step3 = document.querySelector('#reviewStep3'); + if (!step3) return; + if (document.querySelector('#codecheck-reviewer-form')) return; + if (step3.children.length === 0) return; + + const reviewerData = window.codecheckReviewerData; + const submission = { + id: reviewerData.submissionId, + codecheckOptIn: reviewerData.codecheckOptIn, + }; + + const details = document.createElement('details'); + details.id = 'codecheck-reviewer-form'; + details.style.cssText = 'margin-top: 2rem; border: 1px solid #ddd; border-radius: 4px; background: #fff;'; + + const summary = document.createElement('summary'); + summary.style.cssText = 'padding: 1rem; font-weight: 600; font-size: 1rem; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.5rem; background: #f8f8f8; border-radius: 4px;'; + summary.innerHTML = ' CODECHECK Documentation'; + + const content = document.createElement('div'); + content.style.cssText = 'padding: 1rem;'; + + details.appendChild(summary); + details.appendChild(content); + + const form = document.querySelector('#reviewStep3Form'); + if (form) { + form.after(details); + } else { + step3.appendChild(details); + } + + const metadataDiv = document.createElement('div'); + content.appendChild(metadataDiv); + const metadataApp = createApp(CodecheckMetadataForm, { + submission: submission, + canEdit: true, + }); + metadataApp.component('pkp-button', pkp.registry.getComponent('PkpButton')); + metadataApp.mount(metadataDiv); + + const orcid = reviewerData.orcid ?? {}; + if (orcid.enabled) { + window.codecheckOrcidConfig = orcid; + const orcidDiv = document.createElement('div'); + content.appendChild(orcidDiv); + const orcidApp = createApp(CodecheckOrcidSection, { + submission: submission, + orcidEnabled: orcid.enabled, + orcidAuthUrl: orcid.authUrl, + orcidApiType: orcid.apiType, + canAuthorise: true, + }); + orcidApp.component('pkp-button', pkp.registry.getComponent('PkpButton')); + orcidApp.mount(orcidDiv); + } +} +window.mountCodecheckReviewerForm = mountCodecheckReviewerForm; + +// ----------------------------------------------------------------------- +// Submission wizard: mount Vue components into textareas +// ----------------------------------------------------------------------- function mountCodecheckVueComponents() { const manifestContainer = document.querySelector('textarea[name="manifestFiles"]')?.parentElement; if (manifestContainer) { @@ -571,18 +619,8 @@ const CodecheckFileStatus = { pkp.registry.registerComponent("CodecheckFileStatus", CodecheckFileStatus); -console.log("CODECHECK plugin initialized successfully"); - // ----------------------------------------------------------------------- // Issue #30: Dashboard CODECHECK status column -// -// Injects a CODECHECK column into the editorial submissions dashboard. -// Each cell fetches from api/v1/codecheck/metadata and shows: -// - certificate id (green) if a CODECHECK is complete -// - "Add" link if no CODECHECK certificate exists -// -// Controlled by the showDashboardColumn plugin setting (default: true). -// window.codecheckDashboardConfig is injected by CodecheckPlugin.php. // ----------------------------------------------------------------------- const DashboardCellCodecheck = { name: 'DashboardCellCodecheck', @@ -637,8 +675,6 @@ const DashboardCellCodecheck = { pkp.registry.registerComponent("DashboardCellCodecheck", DashboardCellCodecheck); pkp.registry.storeExtend("dashboard", (piniaContext) => { - // Read setting inside the callback so it's evaluated when the store loads, - // not at script load time when window.codecheckDashboardConfig may not exist yet. if (!(window.codecheckDashboardConfig ?? { showDashboardColumn: true }).showDashboardColumn) { return; } diff --git a/templates/settings.tpl b/templates/settings.tpl index d4d83fe9..44b12051 100644 --- a/templates/settings.tpl +++ b/templates/settings.tpl @@ -69,6 +69,45 @@ } ); }); + + $('#testOrcidSetup').on('click', function () { + const $btn = $(this); + const $result = $('#orcidTestResult'); + + const labelTesting = $btn.data('label-testing'); + const labelDefault = $btn.data('label-default'); + const labelFallback = $btn.data('label-fallback'); + + $btn.prop('disabled', true).text(labelTesting); + $result.hide().removeClass('orcid-test--success orcid-test--error'); + + fetch(pkp.context.apiBaseUrl.replace('/api/v1', '') + '/api/v1/codecheck/orcid-test', { + headers: { 'X-Csrf-Token': pkp.currentUser.csrfToken } + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + $result + .addClass('orcid-test--success') + .text('✓ ' + data.message) + .show(); + } else { + $result + .addClass('orcid-test--error') + .text('✗ ' + data.error) + .show(); + } + }) + .catch((err) => { + $result + .addClass('orcid-test--error') + .text('✗ ' + (err.message || labelFallback)) + .show(); + }) + .finally(() => { + $btn.prop('disabled', false).text(labelDefault); + }); + }); }); $('.settings-droptown.dropdown').on('mouseenter', function() { @@ -87,6 +126,26 @@ {/literal} + +
{translate key="plugins.generic.codecheck.settings.description"}

{* Option to enable/ disable CODECHECK *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -115,7 +172,7 @@ label="plugins.generic.codecheck.settings.enableCodecheck.description" } {/fbvFormSection} - + {* Show CODECHECK column in submissions dashboard *} {fbvFormSection list=true}
@@ -128,11 +185,9 @@ label="plugins.generic.codecheck.settings.showDashboardColumn.description" } {/fbvFormSection} - + {* Setting for different CODECHECK modes *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -147,9 +202,7 @@ {/fbvFormSection} {* Clear / Reset CODECHECK Metadata DB *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -164,9 +217,7 @@ {/fbvFormSection} {* Author anonymity option *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -179,9 +230,7 @@ {/fbvFormSection} {* GitHub Personal Access Token option *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -197,9 +246,7 @@ {/fbvFormSection} {* Repository connection settings option *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -259,9 +306,7 @@ {/fbvFormSection} {* Select which parts of the codecheck GitHub Issue are updated *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -281,9 +326,7 @@ {/fbvFormSection} {* Block Publication, when CODECHECK has specific status *} - {fbvFormSection - list=true - } + {fbvFormSection list=true}
@@ -311,10 +354,101 @@ {/fbvFormSection} - {* TODO: Add more settings in future development *} - {* - ORCID integration settings *} - {* - Email template settings *} - + {* ------------------------------------------------------------------ *} + {* ORCID Deposition Settings *} + {* ------------------------------------------------------------------ *} + {fbvFormSection id="settingsHeader" list=true} +

{translate key="plugins.generic.codecheck.orcid.settingsTitle"}

+

{translate key="plugins.generic.codecheck.orcid.settingsDescription"}

+ {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ {fbvElement + type="checkbox" + id="orcidEnabled" + checked=$orcidEnabled + label="plugins.generic.codecheck.orcid.enable" + } + {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ {fbvElement + type="select" + id="orcidApiType" + class="codecheck-form-select" + from=$orcidApiTypes + selected=$orcidApiType + translate=false + } + {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ + + {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ + + {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ + + {/fbvFormSection} + + {fbvFormSection list=true} +
+ +
+ + +
+ {/fbvFormSection} + {/fbvFormArea} {fbvFormButtons submitText="common.save"} - \ No newline at end of file +