Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I10618 #451

Merged
merged 8 commits into from
Nov 28, 2024
13 changes: 13 additions & 0 deletions src/composables/useApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function useApp() {
function isOJS() {
return pkp.context.app === 'ojs2';
}
function isOMP() {
return pkp.context.app === 'omp';
}
function isOPS() {
return pkp.context.app === 'ops';
}

return {isOJS, isOMP, isOPS};
}
6 changes: 5 additions & 1 deletion src/composables/useDate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ export function useDate() {
return moment(dateString).format('DD-MM-YYYY');
}

return {calculateDaysBetweenDates, formatShortDate};
function formatDateAndTime(dateString) {
return moment(dateString).format('YYYY-MM-DD hh:mm A');
}

return {calculateDaysBetweenDates, formatShortDate, formatDateAndTime};
}
10 changes: 8 additions & 2 deletions src/composables/useFetch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ref, unref} from 'vue';
import {ofetch, createFetch} from 'ofetch';
import {useModalStore} from '@/stores/modalStore';
import {useDebounceFn} from '@vueuse/core';

let ofetchInstance = ofetch;

Expand All @@ -26,7 +27,8 @@ export function getCSRFToken() {
* @param {Object} [options.body] - The request payload, typically used with 'POST', 'PUT', or 'DELETE' requests.
* @param {Object} [options.headers] - Additional HTTP headers to be sent with the request.
* @param {string} [options.method] - The HTTP method to be used for the request (e.g., 'GET', 'POST', etc.).
*
* @param {number} options.debouncedMs - When the fetch should be debounce, this defines the delay

* @returns {Object} An object containing several reactive properties and a method for performing the fetch operation:
* @returns {Ref<Object|null>} return.data - A ref object containing the response data from the fetch operation.
* @returns {Ref<Object|null>} return.validationError - A ref object containing validation error data, relevant when `expectValidationError` is true.
Expand Down Expand Up @@ -62,7 +64,7 @@ export function useFetch(url, options = {}) {

let lastRequestController = null;

async function fetch() {
async function _fetch() {
if (lastRequestController) {
// abort in-flight request
lastRequestController.abort();
Expand Down Expand Up @@ -123,6 +125,10 @@ export function useFetch(url, options = {}) {
}
}

let fetch = _fetch;
if (options.debouncedMs) {
fetch = useDebounceFn(_fetch);
}
return {
data,
isSuccess,
Expand Down
21 changes: 15 additions & 6 deletions src/managers/FileManager/fileManagerStore.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {defineComponentStore} from '@/utils/defineComponentStore';

import {ref, computed} from 'vue';
import {ref, computed, watch} from 'vue';
import {useFetch} from '@/composables/useFetch';
import {useUrl} from '@/composables/useUrl';
import {useFileManagerActions} from './useFileManagerActions';
Expand All @@ -26,16 +26,25 @@ export const useFileManagerStore = defineComponentStore(
`submissions/${submissionId.value}/files`,
);

const queryParams = computed(() => ({
fileStages: managerConfig.value.fileStage,
reviewRoundIds: props.reviewRoundId ? props.reviewRoundId : undefined,
}));

const {data, fetch: fetchFiles} = useFetch(filesApiUrl, {
query: {
fileStages: managerConfig.value.fileStage,
reviewRoundIds: props.reviewRoundId ? props.reviewRoundId : undefined,
},
query: queryParams,
});

const files = computed(() => data.value?.items || []);

fetchFiles();
watch(
[filesApiUrl, queryParams],
() => {
files.value = null;
fetchFiles();
},
{immediate: true},
);

/** Reload files when data on screen changes */
const {triggerDataChange} = useDataChanged(() => fetchFiles());
Expand Down
5 changes: 3 additions & 2 deletions src/managers/GalleyManager/useGalleyManagerConfiguration.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useLocalize} from '@/composables/useLocalize';
import {useApp} from '@/composables/useApp';

export function useGalleyManagerConfiguration() {
const {t} = useLocalize();

const {isOPS} = useApp();
function getGalleyGridComponent() {
if (pkp.context.app === 'ops') {
if (isOPS()) {
return 'grid.preprintGalleys.PreprintGalleyGridHandler';
} else {
return 'grid.articleGalleys.ArticleGalleyGridHandler';
Expand Down
2 changes: 2 additions & 0 deletions src/pages/dashboard/dashboardPageStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ export const useDashboardPageStore = defineComponentStore(
currentPage,
pageSize: countPerPage,
query: submissionsQuery,
// to avoid multiple fetch calls while view changing watchers triggers query params recalculation
debouncedMs: 2,
});
watch(
[submissionsUrl, submissionsQuery, currentPage],
Expand Down
90 changes: 90 additions & 0 deletions src/pages/workflow/components/primary/WorkflowListingEmails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<div v-if="emails?.length" class="border border-light">
<h3 class="lg-bold m-3 text-heading">
{{ t('notification.notifications') }}
</h3>
<ul>
<li
v-for="email in emails"
:key="email.id"
class="flex items-center border-t border-light px-3 py-1"
>
<span class="flex-1 truncate">
<a
class="text cursor-pointer text-base-normal hover:underline"
@click.prevent="openEmail(email.id)"
>
{{ email.subject }}
</a>
</span>
<span class="ms-4 shrink-0 text-base-normal text-secondary">
{{ formatDateAndTime(email.dateSent) }}
</span>
</li>
</ul>
</div>
</template>

<script setup>
import {computed, watch} from 'vue';

import {useUrl} from '@/composables/useUrl';
import {useFetch} from '@/composables/useFetch';
import {useDate} from '@/composables/useDate';
import {useModal} from '@/composables/useModal';
import {useLocalize} from '@/composables/useLocalize';

const props = defineProps({
submission: {type: Object, required: true},
selectedStageId: {type: Number, required: true},
});

const {t} = useLocalize();
const {formatDateAndTime} = useDate();
const {apiUrl} = useUrl('emails/authorEmails');

const requestQuery = computed(() => {
if (props.selectedStageId === pkp.const.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW) {
return {
submissionId: props.submission.id,
eventType: pkp.const.EMAIL_LOG_EVENT_TYPE_EDITOR_NOTIFY_AUTHOR,
};
}
return null;
});

const {data: emails, fetch: fetchEmails} = useFetch(apiUrl, {
// currently only used in review stage, this can be extended if used across multiple stages
query: {
submissionId: props.submission.id,
eventType: pkp.const.EMAIL_LOG_EVENT_TYPE_EDITOR_NOTIFY_AUTHOR,
},
});

watch(
requestQuery,
(newRequestQuery) => {
if (newRequestQuery) {
fetchEmails();
}
},
{immediate: true},
);

function openEmail(emailId) {
const {openSideModal} = useModal();

//en/authorDashboard/readSubmissionEmail?submissionId=19&stageId=3&reviewRoundId=13&submissionEmailId=158
//en/authorDashboard/readSubmissionEmail?submissionId19&submissionEmailId=158
const {pageUrl} = useUrl(
`authorDashboard/readSubmissionEmail?submissionId=${props.submission.id}&submissionEmailId=${emailId}`,
);

openSideModal('LegacyAjax', {
legacyOptions: {
title: t('notification.notifications'),
url: pageUrl,
},
});
}
</script>
120 changes: 80 additions & 40 deletions src/pages/workflow/components/primary/WorkflowNotificationDisplay.vue
Original file line number Diff line number Diff line change
@@ -1,52 +1,77 @@
<template>
<div v-for="(notification, i) in notificationsToDisplay" :key="i">
{{ notification.text }}
<div v-if="notificationsToDisplay?.length" class="flex flex-row space-y-3">
<div
v-for="(notification, i) in notificationsToDisplay"
:key="i"
class="w-full border border-light p-3"
>
<h3 class="lg-bold text-heading">{{ notification.title }}</h3>
<p class="pt-2 text-base-normal">{{ notification.text }}</p>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
import {computed, watch} from 'vue';
import {useUrl} from '@/composables/useUrl';
import {useFetch} from '@/composables/useFetch';
import {useApp} from '@/composables/useApp';

const props = defineProps({submission: {type: Object, required: true}});

const {pageUrl} = useUrl(`notification/fetchNotification`);
const {isOJS, isOMP} = useApp();

/**
*
*
return [
Notification::NOTIFICATION_LEVEL_NORMAL => [
Notification::NOTIFICATION_TYPE_VISIT_CATALOG => [Application::ASSOC_TYPE_SUBMISSION, $submissionId],
Notification::NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER => [Application::ASSOC_TYPE_SUBMISSION, $submissionId],
Notification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS => [Application::ASSOC_TYPE_SUBMISSION, $submissionId],
],
Notification::NOTIFICATION_LEVEL_TRIVIAL => []
];

*
*
*/

const requestData = {
requestOptions: {
[pkp.const.NOTIFICATION_LEVEL_NORMAL]: {
[pkp.const.NOTIFICATION_TYPE_VISIT_CATALOG]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
[pkp.const.NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
[pkp.const.NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
},
[pkp.const.NOTIFICATION_LEVEL_TRIVIAL]: 0,
},
};
function getRequestOptionsPerStage(stageId) {
switch (stageId) {
case pkp.const.WORKFLOW_STAGE_ID_EDITING:
return {
[pkp.const.NOTIFICATION_LEVEL_NORMAL]: {
[pkp.const.NOTIFICATION_TYPE_ASSIGN_COPYEDITOR]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
[pkp.const.NOTIFICATION_TYPE_AWAITING_COPYEDITS]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
},
[pkp.const.NOTIFICATION_LEVEL_TRIVIAL]: 0,
};
case pkp.const.WORKFLOW_STAGE_ID_PRODUCTION:
if (isOJS()) {
return {
[pkp.const.NOTIFICATION_LEVEL_NORMAL]: {
[pkp.const.NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
[pkp.const.NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
},
[pkp.const.NOTIFICATION_LEVEL_TRIVIAL]: 0,
};
} else if (isOMP()) {
return {
[pkp.const.NOTIFICATION_LEVEL_NORMAL]: {
[pkp.const.NOTIFICATION_TYPE_VISIT_CATALOG]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
[pkp.const.NOTIFICATION_TYPE_FORMAT_NEEDS_APPROVED_SUBMISSION]: {
assocType: pkp.const.ASSOC_TYPE_SUBMISSION,
assocId: props.submission.id,
},
},
[pkp.const.NOTIFICATION_LEVEL_TRIVIAL]: 0,
};
}
return null;
default:
return null;
}
}

function objectToFormData(obj, formData = new FormData(), parentKey = '') {
for (const key in obj) {
Expand All @@ -64,12 +89,27 @@ function objectToFormData(obj, formData = new FormData(), parentKey = '') {
return formData;
}

const requestBody = computed(() => {
const requestOptions = getRequestOptionsPerStage(props.submission.stageId);
if (requestOptions) {
return objectToFormData({requestOptions});
}
return null;
});
const {data, fetch} = useFetch(pageUrl, {
method: 'POST',
body: objectToFormData(requestData),
body: requestBody,
});

fetch();
watch(
requestBody,
(requestBodyNew) => {
if (requestBodyNew) {
fetch();
}
},
{immediate: true},
);

const notificationsToDisplay = computed(() => {
const notifications = [];
Expand Down

This file was deleted.

Loading