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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions resources/js/Components/CodecheckDataAndSoftwareAvailability.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { useLocalize } = pkp.modules.useLocalize;

export default {
props: {
name: { type: String, required: true },
value: { type: String, required: true }
},
setup() {
Expand All @@ -39,19 +40,25 @@ export default {
methods: {
onInput(e) {
const val = e.target.value;
// update local bound value
this.dataSoftwareAvail = val;
this.$emit("input", val);
this.$el.dispatchEvent(new CustomEvent('input', { detail: val }));

// resize the textarea if the textarea value is empty and the placeholder appears again
this.$emit("update", val);

const vueRoot = document.querySelector(`textarea[name="dataAvailabilityStatement"]`)?.previousElementSibling;
if (vueRoot) {
vueRoot.dispatchEvent(new CustomEvent('update', { detail: val, bubbles: true }));
}

this.resizeTextarea();
},

clearText() {
this.dataSoftwareAvail = '';
this.$emit("input", "");
this.$el.dispatchEvent(new CustomEvent('input', { detail: "" }));
this.$emit("update", "");

const vueRoot = document.querySelector(`textarea[name="dataAvailabilityStatement"]`)?.previousElementSibling;
if (vueRoot) {
vueRoot.dispatchEvent(new CustomEvent('update', { detail: "", bubbles: true }));
}
},

adjustHeight() {
Expand Down
280 changes: 275 additions & 5 deletions resources/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,281 @@ pkp.registry.storeExtend("fileManager_SUBMISSION_FILES", (piniaContext) => {
});
});

// Submission wizard field management
class CodecheckWizardManager {
constructor() {
this.textareas = {};
this.saveInProgress = false;
}

async loadSavedData() {
const submissionId = this.getSubmissionId();
if (!submissionId) return;

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) {
this.setTextareaValue('codeRepository', publication.codeRepository);
this.setTextareaValue('dataRepository', publication.dataRepository);
this.setTextareaValue('manifestFiles', publication.manifestFiles);
this.setTextareaValue('dataAvailabilityStatement', publication.dataAvailabilityStatement);
}
} catch (error) {
console.error('CODECHECK: Failed to load saved data', error);
}
}

setTextareaValue(name, value) {
const textarea = document.querySelector(`textarea[name="${name}"]`);
if (textarea && value) {
textarea.value = value;
this.textareas[name] = textarea;
}
}

async saveData() {
if (this.saveInProgress) return;

const submissionId = this.getSubmissionId();
if (!submissionId) return;

const data = {};
['codeRepository', 'dataRepository', 'manifestFiles', 'dataAvailabilityStatement'].forEach(field => {
const textarea = document.querySelector(`textarea[name="${field}"]`);
if (textarea && textarea.value) {
data[field] = textarea.value;
}
});

if (Object.keys(data).length === 0) return;

this.saveInProgress = true;

try {
const submissionResponse = await fetch(`${pkp.context.apiBaseUrl}/submissions/${submissionId}`);
const submission = await submissionResponse.json();
const publicationId = submission.currentPublicationId;

await fetch(
`${pkp.context.apiBaseUrl}/submissions/${submissionId}/publications/${publicationId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Csrf-Token': pkp.currentUser.csrfToken
},
body: JSON.stringify(data)
}
);
} catch (error) {
console.error('CODECHECK: Save failed', error);
} finally {
this.saveInProgress = false;
}
}

getSubmissionId() {
const match = window.location.search.match(/id=(\d+)/);
return match ? match[1] : null;
}

setupButtonListener() {
document.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (button && (button.textContent.includes('Continue') || button.textContent.includes('Save for Later'))) {
Comment thread
subhanLabs marked this conversation as resolved.
Outdated
this.saveData();
}
}, true);
}

async init() {
await this.loadSavedData();
this.setupButtonListener();
}
}

// Review section refresher
class CodecheckReviewRefresher {
constructor() {
this.refreshedPanels = new Set();
this.observeStepChanges();
}

observeStepChanges() {
setInterval(() => {
if (this.isOnReviewStep()) {
this.checkForReviewPanel();
}
}, 300);

const observer = new MutationObserver(() => {
if (this.isOnReviewStep()) {
this.checkForReviewPanel();
}
});

observer.observe(document.body, {
childList: true,
subtree: true
});
}

isOnReviewStep() {
const wizardSteps = document.querySelectorAll('.submissionWizard__step');

for (const step of wizardSteps) {
if (step.classList.contains('isActive') || step.classList.contains('isCurrent')) {
const stepText = step.textContent.toLowerCase();
if (stepText.includes('review') || stepText.includes('submit')) {
Comment thread
subhanLabs marked this conversation as resolved.
Outdated
return true;
}
}
}

const reviewPanels = document.querySelectorAll('.submissionWizard__reviewPanel');
const visiblePanels = Array.from(reviewPanels).filter(panel => {
const rect = panel.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});

return visiblePanels.length > 2;
}

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);

return;
}
}
}
}

async refreshReviewData(panel) {
const submissionId = this.getSubmissionId();
if (!submissionId) return;

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;

const body = panel.querySelector('.submissionWizard__reviewPanel__body');
if (!body) return;

body.innerHTML = '';

let hasData = false;

if (publication.codeRepository) {
hasData = true;
body.innerHTML += `
<div class="submissionWizard__reviewPanel__item">
<h4>${this.escapeHtml('Code Repository URLs')}</h4>
Comment thread
subhanLabs marked this conversation as resolved.
Outdated
<div class="review-value">
<p>${this.escapeHtml(publication.codeRepository).replace(/\n/g, '<br>')}</p>
</div>
</div>
`;
}

if (publication.dataRepository) {
hasData = true;
body.innerHTML += `
<div class="submissionWizard__reviewPanel__item">
<h4>${this.escapeHtml('Data Repository URLs')}</h4>
Comment thread
subhanLabs marked this conversation as resolved.
Outdated
<div class="review-value">
<p>${this.escapeHtml(publication.dataRepository).replace(/\n/g, '<br>')}</p>
</div>
</div>
`;
}

if (publication.manifestFiles) {
hasData = true;
body.innerHTML += `
<div class="submissionWizard__reviewPanel__item">
<h4>${this.escapeHtml('Expected Output Files')}</h4>
<div class="review-value">
<pre>${this.escapeHtml(publication.manifestFiles)}</pre>
</div>
</div>
`;
}

if (publication.dataAvailabilityStatement) {
hasData = true;
body.innerHTML += `
<div class="submissionWizard__reviewPanel__item">
<h4>${this.escapeHtml('Data and Software Availability')}</h4>
<div class="review-value">
<div>${publication.dataAvailabilityStatement}</div>
</div>
</div>
`;
}

if (!hasData) {
body.innerHTML = `
<div class="submissionWizard__reviewPanel__item">
<p class="description" style="color: #d00a0a;">
<em>No CODECHECK data found.</em>
</p>
</div>
`;
}
} catch (error) {
console.error('CODECHECK: Failed to refresh review data', error);
}
}

escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

getSubmissionId() {
const match = window.location.search.match(/id=(\d+)/);
return match ? match[1] : null;
}
}

// Initialize — mount Vue components only after WizardManager has loaded saved data
// into textareas, so components receive the correct initial values
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
new CodecheckReviewRefresher();
}, 200);

setTimeout(async () => {
const manager = new CodecheckWizardManager();
await manager.init();
mountCodecheckVueComponents();
}, 1000);
}, 100);
});

function mountCodecheckVueComponents() {
Expand All @@ -181,7 +452,6 @@ function mountCodecheckVueComponents() {
});
}

// Mount code repository component
const codeRepoContainer = document.querySelector('textarea[name="codeRepository"]')?.parentElement;
if (codeRepoContainer) {
const textarea = codeRepoContainer.querySelector('textarea');
Expand Down Expand Up @@ -230,13 +500,13 @@ function mountCodecheckVueComponents() {
textarea.style.display = 'none';

createApp(CodecheckDataAndSoftwareAvailability, {
name: 'dataSoftwareAvailability',
name: 'dataAvailabilityStatement',
label: t('plugins.generic.codecheck.dataSoftwareAvailability'),
description: t('plugins.generic.codecheck.dataSoftwareAvailability.description'),
value: textarea.value,
}).mount(vueDiv);

vueDiv.addEventListener('input', (e) => {
vueDiv.addEventListener('update', (e) => {
textarea.value = e.detail;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
});
Expand Down Expand Up @@ -265,4 +535,4 @@ const CodecheckFileStatus = {

pkp.registry.registerComponent("CodecheckFileStatus", CodecheckFileStatus);

console.log("CODECHECK plugin initialized successfully");
console.log("CODECHECK plugin initialized successfully");