diff --git a/babel.config.js b/babel.config.js
deleted file mode 100644
index dcd852039..000000000
--- a/babel.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- presets: ['@vue/cli-plugin-babel/preset'],
-};
diff --git a/index.html b/index.html
deleted file mode 100644
index 88d0bda8e..000000000
--- a/index.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
+
diff --git a/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue b/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue
deleted file mode 100644
index ab62a66a9..000000000
--- a/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue
+++ /dev/null
@@ -1,531 +0,0 @@
-
-
-
-
-
-
-
-
- {{ title }}
-
-
-
- {{ reviewerName }}
-
-
-
-
-
-
-
triggerEmit(config.textAction)"
- >
- {{ textActionLabel }}
-
-
-
-
triggerEmit(config.primaryAction)"
- >
- {{ primaryActionLabel }}
-
-
triggerEmit(config.negativeAction)"
- >
- {{ negativeActionLabel }}
-
-
-
-
-
-
diff --git a/src/components/StageBubble/StageBubble.vue b/src/components/StageBubble/StageBubble.vue
index 7b54b1ef0..0b965c221 100644
--- a/src/components/StageBubble/StageBubble.vue
+++ b/src/components/StageBubble/StageBubble.vue
@@ -1,74 +1,41 @@
-
+
-
+
-
+
+});
-
+const stageColorClass = computed(
+ () => ExtendedStagesColorClass[props.extendedStage],
+);
+
diff --git a/src/components/UserAvatar/UserAvatar.mdx b/src/components/UserAvatar/UserAvatar.mdx
new file mode 100644
index 000000000..806818130
--- /dev/null
+++ b/src/components/UserAvatar/UserAvatar.mdx
@@ -0,0 +1,11 @@
+import {Primary, Controls, Stories, Meta, Description} from '@storybook/blocks';
+
+import * as UserAvatarStories from './UserAvatar.stories.js';
+
+
+
+# UserAvatar
+
+
+
+
diff --git a/src/components/UserAvatar/UserAvatar.stories.js b/src/components/UserAvatar/UserAvatar.stories.js
new file mode 100644
index 000000000..27052a967
--- /dev/null
+++ b/src/components/UserAvatar/UserAvatar.stories.js
@@ -0,0 +1,27 @@
+import UserAvatar from './UserAvatar.vue';
+
+export default {
+ title: 'Components/UserAvatar',
+ component: UserAvatar,
+ render: (args) => ({
+ components: {UserAvatar},
+ setup() {
+ return {args};
+ },
+ template: '
',
+ }),
+};
+
+export const Default = {
+ args: {
+ userFullName: 'Nama Sampan-Nirmal Lengkap',
+ userId: 136,
+ },
+};
+
+export const Arabic = {
+ args: {
+ userFullName: 'خالد محمود الفارسي',
+ userId: 136,
+ },
+};
diff --git a/src/components/UserAvatar/UserAvatar.vue b/src/components/UserAvatar/UserAvatar.vue
new file mode 100644
index 000000000..a93509478
--- /dev/null
+++ b/src/components/UserAvatar/UserAvatar.vue
@@ -0,0 +1,55 @@
+
+
+
+ {{ initials }}
+
+
+
+
+
diff --git a/src/composables/useUser.js b/src/composables/useCurrentUser.js
similarity index 90%
rename from src/composables/useUser.js
rename to src/composables/useCurrentUser.js
index 1b0d81a73..ed392c1d1 100644
--- a/src/composables/useUser.js
+++ b/src/composables/useCurrentUser.js
@@ -1,6 +1,6 @@
import {computed} from 'vue';
-export function useUser() {
+export function useCurrentUser() {
const isSiteAdmin = computed(
() =>
!!pkp.currentUser.roles.find(
diff --git a/src/composables/useDataChanged.js b/src/composables/useDataChanged.js
new file mode 100644
index 000000000..4f59b5ba5
--- /dev/null
+++ b/src/composables/useDataChanged.js
@@ -0,0 +1,11 @@
+export function useDataChanged() {
+ const callbacks = [];
+ function registerDataChangeCallback(callback) {
+ callbacks.push(callback);
+ }
+ function triggerDataChange() {
+ callbacks.forEach((callback) => callback());
+ }
+
+ return {registerDataChangeCallback, triggerDataChange};
+}
diff --git a/src/composables/useDate.js b/src/composables/useDate.js
new file mode 100644
index 000000000..a9b6975ff
--- /dev/null
+++ b/src/composables/useDate.js
@@ -0,0 +1,19 @@
+import moment from 'moment';
+
+export function useDate() {
+ function calculateDaysBetweenDates(startDate, endDate) {
+ const oneDay = 1000 * 60 * 60 * 24; // milliseconds in one day
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ const difference = end - start; // difference in milliseconds
+
+ return Math.round(difference / oneDay);
+ }
+
+ function formatShortDate(dateString) {
+ return moment(dateString).format('DD-MM-YYYY');
+ }
+
+ return {calculateDaysBetweenDates, formatShortDate};
+}
diff --git a/src/composables/useFiltersForm.js b/src/composables/useFiltersForm.js
index 82ba31dd8..8eb101db8 100644
--- a/src/composables/useFiltersForm.js
+++ b/src/composables/useFiltersForm.js
@@ -26,7 +26,7 @@ function createSelected(values, labels) {
export function useFiltersForm(_filtersForm) {
const filtersForm = ref(_filtersForm);
- const {clearForm, setValue} = useForm(_filtersForm);
+ const {clearForm, removeFieldValue, setValue} = useForm(_filtersForm);
const filtersFormList = computed(() => {
const list = [];
@@ -42,6 +42,7 @@ export function useFiltersForm(_filtersForm) {
);
list.push({
+ name: field.name,
fieldLabel: field.label,
label: select.label,
value: select.value,
@@ -52,6 +53,7 @@ export function useFiltersForm(_filtersForm) {
);
list.push({
+ name: field.name,
fieldLabel: field.label,
label: option.label,
value: option.value,
@@ -59,6 +61,7 @@ export function useFiltersForm(_filtersForm) {
} else if (field.component === 'field-slider') {
if (fieldValue !== field.min) {
list.push({
+ name: field.name,
fieldLabel: field.label,
value: fieldValue,
label: fieldValue,
@@ -66,6 +69,7 @@ export function useFiltersForm(_filtersForm) {
}
} else {
list.push({
+ name: field.name,
fieldLabel: field.label,
value: fieldValue,
label: 'TODO',
@@ -124,7 +128,11 @@ export function useFiltersForm(_filtersForm) {
}
function clearFiltersForm() {
- clearForm(filtersForm.value);
+ clearForm();
+ }
+
+ function clearFiltersFormField(fieldName, fieldValue) {
+ removeFieldValue(fieldName, fieldValue);
}
return {
@@ -135,5 +143,6 @@ export function useFiltersForm(_filtersForm) {
initFiltersFormFromQueryParams,
updateFiltersForm,
clearFiltersForm,
+ clearFiltersFormField,
};
}
diff --git a/src/composables/useForm.js b/src/composables/useForm.js
index 8aba64950..f705279eb 100644
--- a/src/composables/useForm.js
+++ b/src/composables/useForm.js
@@ -99,20 +99,36 @@ export function useForm(_form) {
}
}
+ function clearFormField(fieldName) {
+ const field = getField(form.value, fieldName);
+
+ if (field.isMultilingual) {
+ const newValueMultilingual = {};
+ form.value.supportedFormLocales.forEach((localeObject) => {
+ const localeKey = localeObject.key;
+ const newValue = getClearValue(field, localeKey);
+ newValueMultilingual[localeKey] = newValue;
+ });
+ setValue(field.name, newValueMultilingual);
+ } else {
+ const newValue = getClearValue(field);
+ setValue(field.name, newValue);
+ }
+ }
+
+ function removeFieldValue(fieldName, fieldValue) {
+ const value = getValue(fieldName);
+ if (Array.isArray(value)) {
+ const newValue = value.filter((v) => v !== fieldValue);
+ setValue(fieldName, newValue);
+ } else {
+ clearFormField(fieldName);
+ }
+ }
+
function clearForm() {
form.value.fields.forEach((field) => {
- if (field.isMultilingual) {
- const newValueMultilingual = {};
- form.value.supportedFormLocales.forEach((localeObject) => {
- const localeKey = localeObject.key;
- const newValue = getClearValue(field, localeKey);
- newValueMultilingual[localeKey] = newValue;
- });
- setValue(field.name, newValueMultilingual);
- } else {
- const newValue = getClearValue(field);
- setValue(field.name, newValue);
- }
+ clearFormField(field.name);
});
}
@@ -120,6 +136,7 @@ export function useForm(_form) {
set,
setValue,
getValue,
+ removeFieldValue,
clearForm,
form,
connectWithPayload,
diff --git a/src/composables/useId.js b/src/composables/useId.js
new file mode 100644
index 000000000..454256530
--- /dev/null
+++ b/src/composables/useId.js
@@ -0,0 +1,8 @@
+let id = 0;
+function generateId(componentName) {
+ return `pkp-id-${++id}`;
+}
+
+export function useId() {
+ return {generateId};
+}
diff --git a/src/composables/useLegacyGridUrl.js b/src/composables/useLegacyGridUrl.js
new file mode 100644
index 000000000..ee51f8c4f
--- /dev/null
+++ b/src/composables/useLegacyGridUrl.js
@@ -0,0 +1,38 @@
+import {ref, computed} from 'vue';
+
+function camelCaseToDashes(str) {
+ return str.replace(/([a-z]+)([A-Z])/g, '$1-$2').toLowerCase();
+}
+export function useLegacyGridUrl({
+ component: _component,
+ op: _op,
+ params: _params,
+} = {}) {
+ if (typeof pkp === 'undefined' || !pkp?.context?.legacyGridBaseUrl) {
+ throw new Error('pkp.context.legacyGridBaseUrl is not configured');
+ }
+
+ const component = ref(_component);
+ const op = ref(_op);
+ const params = ref(_params);
+
+ const queryParamsString = computed(() => {
+ if (params.value && Object.keys(params.value).length) {
+ return `?${new URLSearchParams(params.value).toString()}`;
+ }
+ return '';
+ });
+
+ const url = computed(() => {
+ let componentPath = component.value.slice(0, -7);
+ componentPath = camelCaseToDashes(componentPath.split('.').join('/'));
+ const opPath = camelCaseToDashes(op.value);
+
+ let baseUrl = pkp.context.legacyGridBaseUrl
+ .replace('component', componentPath)
+ .replace('action', opPath);
+ return `${baseUrl}${queryParamsString.value}`;
+ });
+
+ return {url};
+}
diff --git a/src/composables/useLegacyGridUrl.test.js b/src/composables/useLegacyGridUrl.test.js
new file mode 100644
index 000000000..5b347c05e
--- /dev/null
+++ b/src/composables/useLegacyGridUrl.test.js
@@ -0,0 +1,47 @@
+import {expect, test, describe} from 'vitest';
+import {useLegacyGridUrl} from './useLegacyGridUrl';
+
+global.pkp = global.pkp || {};
+global.pkp.context = {
+ legacyGridBaseUrl:
+ 'http://mock/index.php/publicknowledge/$$$call$$$/component/action',
+};
+
+describe('useLegacyGridUrl', () => {
+ test('grid.users.stageParticipant.StageParticipantGridHandler', () => {
+ // http://localhost:7003/index.php/publicknowledge/$$$call$$$/grid/users/stage-participant/stage-participant-grid/add-participant?submissionId=13&stageId=3
+ const {url} = useLegacyGridUrl({
+ component: 'grid.users.stageParticipant.StageParticipantGridHandler',
+ op: 'addParticipant',
+ params: {submissionId: 13, stageId: 3},
+ });
+
+ expect(url.value).toBe(
+ 'http://mock/index.php/publicknowledge/$$$call$$$/grid/users/stage-participant/stage-participant-grid/add-participant?submissionId=13&stageId=3',
+ );
+ });
+
+ test('grid.users.reviewer.ReviewerGridHandler', () => {
+ const {url} = useLegacyGridUrl({
+ component: 'grid.users.reviewer.ReviewerGridHandler',
+ op: 'readReview',
+ params: {submissionId: 13, reviewAssignmentId: 19, stageId: 3},
+ });
+
+ expect(url.value).toBe(
+ 'http://mock/index.php/publicknowledge/$$$call$$$/grid/users/reviewer/reviewer-grid/read-review?submissionId=13&reviewAssignmentId=19&stageId=3',
+ );
+ });
+
+ test('modals.publish.AssignToIssueHandler', () => {
+ const {url} = useLegacyGridUrl({
+ component: 'modals.publish.AssignToIssueHandler',
+ op: 'assign',
+ params: {submissionId: 13, publicationId: 14},
+ });
+
+ expect(url.value).toBe(
+ 'http://mock/index.php/publicknowledge/$$$call$$$/modals/publish/assign-to-issue/assign?submissionId=13&publicationId=14',
+ );
+ });
+});
diff --git a/src/composables/useModal.js b/src/composables/useModal.js
index 2fb67e2b5..548a1b54b 100644
--- a/src/composables/useModal.js
+++ b/src/composables/useModal.js
@@ -7,8 +7,8 @@ export function useModal() {
modalStore.openDialog(props);
}
- function openSideModal(component, props) {
- modalStore.openSideModal(component, props);
+ function openSideModal(component, props, opts) {
+ modalStore.openSideModal(component, props, opts);
}
return {openDialog, openSideModal};
diff --git a/src/composables/useParticipant.js b/src/composables/useParticipant.js
new file mode 100644
index 000000000..bd1783310
--- /dev/null
+++ b/src/composables/useParticipant.js
@@ -0,0 +1,37 @@
+export function useParticipant() {
+ function getEditorRoleIds() {
+ return [
+ pkp.const.ROLE_ID_MANAGER,
+ pkp.const.ROLE_ID_SUB_EDITOR,
+ pkp.const.ROLE_ID_ASSISTANT,
+ ];
+ }
+ function hasParticipantAtLeastOneRole(participant, roleIds = []) {
+ return participant.groups.some((group) => roleIds.includes(group.roleId));
+ }
+
+ function getFirstGroupWithFollowingRoles(participant, roleIds = []) {
+ return participant.groups.find((group) => roleIds.includes(group.roleId));
+ }
+
+ function getUserAvatarInitialsFromName(fullName) {
+ const fullNameParts = fullName.split(' ');
+
+ return fullNameParts
+ .map((part) => {
+ const partTrimmed = part.trim();
+ if (partTrimmed.length) {
+ return partTrimmed[0].toUpperCase();
+ }
+ return '';
+ })
+ .join('')
+ .substring(0, 3);
+ }
+ return {
+ getUserAvatarInitialsFromName,
+ getEditorRoleIds,
+ hasParticipantAtLeastOneRole,
+ getFirstGroupWithFollowingRoles,
+ };
+}
diff --git a/src/composables/useParticipant.test.js b/src/composables/useParticipant.test.js
new file mode 100644
index 000000000..745547f6b
--- /dev/null
+++ b/src/composables/useParticipant.test.js
@@ -0,0 +1,29 @@
+import {expect, test, describe} from 'vitest';
+import {useParticipant} from './useParticipant';
+
+describe('useParticipant', () => {
+ describe('getUserAvatarInitials', () => {
+ const {getUserAvatarInitialsFromName} = useParticipant();
+ test('Two names', () => {
+ expect(getUserAvatarInitialsFromName('Charlotte Reynolds')).toBe('CR');
+ });
+
+ test('Three names with dash', () => {
+ expect(getUserAvatarInitialsFromName('Nama Sampan-Nirmal Lengkap')).toBe(
+ 'NSL',
+ );
+ });
+
+ test('Three names', () => {
+ expect(getUserAvatarInitialsFromName('Theresa Jessie Franklin')).toBe(
+ 'TJF',
+ );
+ });
+
+ test('Four names (max 3 initials)', () => {
+ expect(
+ getUserAvatarInitialsFromName('Theresa Jessie Franklin Jasmin'),
+ ).toBe('TJF');
+ });
+ });
+});
diff --git a/src/composables/useReviewAssignment.js b/src/composables/useReviewAssignment.js
new file mode 100644
index 000000000..3674aee44
--- /dev/null
+++ b/src/composables/useReviewAssignment.js
@@ -0,0 +1,61 @@
+const InProgressReviewAssignmentStatuses = [
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_ACCEPTED,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE,
+];
+const CompletedReviewAssignmentStatuses = [
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_RECEIVED,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_COMPLETE,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_THANKED,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_CANCELLED,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND,
+];
+
+const IgnoredReviewAssignmentStatuses = [
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_DECLINED,
+ pkp.const.REVIEW_ASSIGNMENT_STATUS_CANCELLED,
+];
+
+export function useReviewAssignment() {
+ function getActiveReviewAssignments(reviewAssignments) {
+ return reviewAssignments.filter(
+ (reviewAssignment) =>
+ !IgnoredReviewAssignmentStatuses.includes(reviewAssignment.statusId),
+ );
+ }
+
+ function getCompletedReviewAssignments(reviewAssignments = []) {
+ return getActiveReviewAssignments(reviewAssignments).filter(
+ (reviewAssignment) =>
+ CompletedReviewAssignmentStatuses.includes(reviewAssignment.statusId),
+ );
+ }
+
+ function getOpenReviewAssignments(reviewAssignments = []) {
+ return reviewAssignments.filter(
+ (reviewAssignment) =>
+ reviewAssignment.reviewMethod ===
+ pkp.const.SUBMISSION_REVIEW_METHOD_OPEN,
+ );
+ }
+
+ function getReviewMethodIcons(reviewAssignment) {
+ switch (reviewAssignment.reviewMethod) {
+ case pkp.const.SUBMISSION_REVIEW_METHOD_ANONYMOUS:
+ return ['OpenReview', 'AnonymousReview'];
+ case pkp.const.SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS:
+ return ['AnonymousReview', 'AnonymousReview'];
+ case pkp.const.SUBMISSION_REVIEW_METHOD_OPEN:
+ return ['OpenReview', 'OpenReview'];
+ }
+
+ return ['OpenReview', 'OpenReview'];
+ }
+
+ return {
+ getActiveReviewAssignments,
+ getCompletedReviewAssignments,
+ getOpenReviewAssignments,
+ getReviewMethodIcons,
+ InProgressReviewAssignmentStatuses,
+ };
+}
diff --git a/src/composables/useSubmission.js b/src/composables/useSubmission.js
new file mode 100644
index 000000000..9e962cda2
--- /dev/null
+++ b/src/composables/useSubmission.js
@@ -0,0 +1,112 @@
+import {useLocalize} from './useLocalize';
+
+const {t, tk} = useLocalize();
+
+export const ExtendedStages = {
+ INCOMPLETE: 'incomplete',
+ SUBMISSION: 'submission',
+ INTERNAL_REVIEW: 'internalReview',
+ EXTERNAL_REVIEW: 'externalReview',
+ EDITING: 'editing',
+ PRODUCTION_QUEUED: 'productionQueued',
+ PRODUCTION_SCHEDULED: 'productionScheduled',
+ PRODUCTION_PUBLISHED: 'productionPublished',
+ DECLINED: 'declined',
+};
+
+export const ExtendedStagesLabels = {
+ incomplete: tk('submissions.incomplete'),
+ submission: tk('dashboard.stage.deskReview'),
+ internalReview: tk('todo'),
+ externalReview: tk('dashboard.stage.reviewWithRound'),
+ editing: tk('dashboard.stage.copyediting'),
+ productionQueued: tk('dashboard.stage.production'),
+ productionScheduled: tk('dashboard.stage.scheduledForPublication'),
+ productionPublished: tk('dashboard.stage.published'),
+ declined: tk('submissions.declined'),
+};
+
+export function useSubmission() {
+ function getActiveStage(submission) {
+ return submission.stages.find((stage) => stage.isActiveStage);
+ }
+
+ function getCurrentReviewRound(submission) {
+ return submission?.reviewRounds?.length
+ ? submission?.reviewRounds[submission.reviewRounds.length - 1]
+ : null;
+ }
+
+ function getCurrentReviewAssignments(submission) {
+ const currentReviewRound = getCurrentReviewRound(submission);
+
+ return submission.reviewAssignments.filter(
+ (reviewAssignment) => reviewAssignment.round === currentReviewRound.round,
+ );
+ }
+
+ function getCurrentPublication(submission) {
+ return submission.publications.find(
+ (publication) => publication.id === submission.currentPublicationId,
+ );
+ }
+
+ function getExtendedStage(submission) {
+ const activeStage = getActiveStage(submission);
+
+ switch (activeStage.id) {
+ case pkp.const.WORKFLOW_STAGE_ID_SUBMISSION:
+ return submission.submissionProgress
+ ? ExtendedStages.INCOMPLETE
+ : ExtendedStages.SUBMISSION;
+ case pkp.const.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW:
+ return ExtendedStages.EXTERNAL_REVIEW;
+ case pkp.const.WORKFLOW_STAGE_ID_EDITING:
+ return ExtendedStages.EDITING;
+ case pkp.const.WORKFLOW_STAGE_ID_PRODUCTION:
+ switch (submission.status) {
+ case pkp.const.STATUS_QUEUED:
+ return ExtendedStages.PRODUCTION_QUEUED;
+ case pkp.const.STATUS_SCHEDULED:
+ return ExtendedStages.PRODUCTION_SCHEDULED;
+ case pkp.const.STATUS_PUBLISHED:
+ return ExtendedStages.PRODUCTION_PUBLISHED;
+ case pkp.const.STATUS_DECLINED:
+ return ExtendedStages.PRODUCTION_DECLINED;
+ }
+ }
+ }
+
+ function getExtendedStageLabel(submission) {
+ const extendedStage = getExtendedStage(submission);
+ const round =
+ extendedStage === ExtendedStages.EXTERNAL_REVIEW
+ ? submission.reviewRounds[submission.reviewRounds.length - 1].round
+ : undefined;
+ return t(ExtendedStagesLabels[extendedStage], {
+ round,
+ });
+ }
+
+ function getFileStageFromWorkflowStage(submission) {
+ const FileStageMapping = {
+ [pkp.const.WORKFLOW_STAGE_ID_SUBMISSION]:
+ pkp.const.SUBMISSION_FILE_SUBMISSION,
+ [pkp.const.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW]:
+ pkp.const.SUBMISSION_FILE_REVIEW_REVISION,
+ [pkp.const.WORKFLOW_STAGE_ID_EDITING]: pkp.const.SUBMISSION_FILE_FINAL,
+ };
+
+ return FileStageMapping[submission.stageId];
+ }
+
+ return {
+ getActiveStage,
+ getExtendedStage,
+ getExtendedStageLabel,
+ getCurrentReviewRound,
+ getCurrentReviewAssignments,
+ getCurrentPublication,
+ getFileStageFromWorkflowStage,
+ };
+}
diff --git a/src/composables/useUrl.js b/src/composables/useUrl.js
index dd59cb260..d02feb95d 100644
--- a/src/composables/useUrl.js
+++ b/src/composables/useUrl.js
@@ -5,7 +5,7 @@ import {ref, computed} from 'vue';
* is covered in useFetch
*/
-export function useUrl(_path) {
+export function useUrl(_path, _queryParams = {}) {
if (typeof pkp === 'undefined' || !pkp?.context?.apiBaseUrl) {
throw new Error('pkp.context.apiBaseUrl is not configured');
}
@@ -16,9 +16,27 @@ export function useUrl(_path) {
// normalise to be ref even if its not passed as ref
const path = ref(_path);
+ const queryParams = ref(_queryParams);
- const apiUrl = computed(() => `${pkp.context.apiBaseUrl}${path.value}`);
- const pageUrl = computed(() => `${pkp.context.pageBaseUrl}${path.value}`);
+ const queryParamsString = computed(() => {
+ if (queryParams.value && Object.keys(queryParams.value).length) {
+ return `?${new URLSearchParams(queryParams.value).toString()}`;
+ }
+ return '';
+ });
- return {apiUrl, pageUrl};
+ const apiUrl = computed(
+ () => `${pkp.context.apiBaseUrl}${path.value}${queryParamsString.value}`,
+ );
+ const pageUrl = computed(() =>
+ path.value.startsWith('http')
+ ? `${path.value}${queryParamsString.value}`
+ : `${pkp.context.pageBaseUrl}${path.value}${queryParamsString.value}`,
+ );
+
+ function redirectToPage() {
+ window.location.href = pageUrl.value;
+ }
+
+ return {apiUrl, pageUrl, redirectToPage};
}
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index 1c0ed8706..000000000
--- a/src/main.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import {createApp, h} from 'vue';
-import {createPinia} from 'pinia';
-
-import emitter from 'tiny-emitter/instance';
-
-//import './styles/style.css';
-import App from './App.vue';
-
-import router from './router';
-
-import GlobalMixins from '@/mixins/global.js';
-import VueAnnouncer from '@vue-a11y/announcer';
-import FloatingVue from 'floating-vue';
-
-import VueScrollTo from 'vue-scrollto';
-
-import Badge from '@/components/Badge/Badge.vue';
-import Dropdown from '@/components/Dropdown/Dropdown.vue';
-import Icon from '@/components/Icon/Icon.vue';
-import Notification from '@/components/Notification/Notification.vue';
-import Panel from '@/components/Panel/Panel.vue';
-import PanelSection from '@/components/Panel/PanelSection.vue';
-import PkpButton from '@/components/Button/Button.vue';
-import PkpHeader from '@/components/Header/Header.vue';
-import Spinner from '@/components/Spinner/Spinner.vue';
-import Step from '@/components/Steps/Step.vue';
-import Steps from '@/components/Steps/Steps.vue';
-import Tab from '@/components/Tabs/Tab.vue';
-import Tabs from '@/components/Tabs/Tabs.vue';
-
-export default window.pkp.eventBus = {
- $on: (...args) => emitter.on(...args),
- $once: (...args) => emitter.once(...args),
- $off: (...args) => emitter.off(...args),
- $emit: (...args) => emitter.emit(...args),
-};
-
-const vueApp = createApp({
- data() {
- /**
- * Fake data that is passed to the root component
- *
- * This data is usually added to every PageComponent by the
- * PKPTemplateManager class in OJS, OMP or OPS.
- */
-
- return {
- /**
- * File genres
- */
- fileGenres: [
- {
- id: 1,
- name: 'Book Manuscript',
- isPrimary: true,
- },
- {
- id: 2,
- name: 'Chapter Manuscript',
- isPrimary: true,
- },
- {
- id: 3,
- name: 'Preface',
- },
- {
- id: 4,
- name: 'Index',
- },
- {
- id: 5,
- name: 'Glossary',
- },
- {
- id: 7,
- name: 'Prospectus',
- },
- {
- id: 11,
- name: 'Table',
- },
- {
- id: 8,
- name: 'Figure',
- },
- {
- id: 9,
- name: 'Audio',
- },
- {
- id: 10,
- name: 'Other',
- },
- ],
- /**
- * TinyMCE configuration
- */
- tinyMCE: {
- skinUrl: '/styles/tinymce',
- },
- };
- },
- render: () => h(App),
-});
-
-const pinia = createPinia();
-vueApp.use(pinia);
-
-vueApp.config.productionTip = false;
-vueApp.config.compilerOptions.whitespace = 'preserve';
-
-vueApp.mixin(GlobalMixins);
-
-vueApp.component('Badge', Badge);
-vueApp.component('Dropdown', Dropdown);
-vueApp.component('Icon', Icon);
-vueApp.component('Notification', Notification);
-vueApp.component('Panel', Panel);
-vueApp.component('PanelSection', PanelSection);
-vueApp.component('PkpButton', PkpButton);
-vueApp.component('PkpHeader', PkpHeader);
-vueApp.component('Spinner', Spinner);
-vueApp.component('Step', Step);
-vueApp.component('Steps', Steps);
-vueApp.component('Tab', Tab);
-vueApp.component('Tabs', Tabs);
-
-vueApp.use(router);
-
-vueApp.use(VueScrollTo);
-vueApp.use(VueAnnouncer);
-vueApp.use(FloatingVue, {
- themes: {
- 'pkp-tooltip': {
- $extend: 'tooltip',
- triggers: ['click'],
- delay: {
- show: 0,
- hide: 0,
- },
- },
- },
-});
-
-vueApp.mount('#app');
diff --git a/src/managers/ContributorManager/ContributorManager.vue b/src/managers/ContributorManager/ContributorManager.vue
new file mode 100644
index 000000000..17ea0201d
--- /dev/null
+++ b/src/managers/ContributorManager/ContributorManager.vue
@@ -0,0 +1,37 @@
+
+
+
+
+ {{ title }}
+
+
+
+ {{ item.fullName }}
+
+ {{ localize(item.userGroupName) }}
+
+
+
+
+
diff --git a/src/managers/ContributorManager/contributorManagerStore.js b/src/managers/ContributorManager/contributorManagerStore.js
new file mode 100644
index 000000000..d3f7ad95b
--- /dev/null
+++ b/src/managers/ContributorManager/contributorManagerStore.js
@@ -0,0 +1,24 @@
+import {defineComponentStore} from '@/utils/defineComponentStore';
+
+import {computed} from 'vue';
+import {useFetch} from '@/composables/useFetch';
+import {useUrl} from '@/composables/useUrl';
+
+export const useContributorManagerStore = defineComponentStore(
+ 'contributorManager',
+ (props) => {
+ const {apiUrl: contributorApiUrl} = useUrl(
+ `submissions/${props.submissionId}/publications/${props.publicationId}/contributors`,
+ );
+
+ const {data, fetch: fetchContributors} = useFetch(contributorApiUrl, {
+ query: {},
+ });
+
+ const contributors = computed(() => data.value?.items || []);
+
+ fetchContributors();
+
+ return {title: props.title, contributors, fetchContributors};
+ },
+);
diff --git a/src/managers/FileManager/FileManager.vue b/src/managers/FileManager/FileManager.vue
new file mode 100644
index 000000000..41a78b1eb
--- /dev/null
+++ b/src/managers/FileManager/FileManager.vue
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/src/managers/FileManager/fileManagerStore.js b/src/managers/FileManager/fileManagerStore.js
new file mode 100644
index 000000000..18afca9fa
--- /dev/null
+++ b/src/managers/FileManager/fileManagerStore.js
@@ -0,0 +1,29 @@
+import {defineComponentStore} from '@/utils/defineComponentStore';
+
+import {ref, computed} from 'vue';
+import {useFetch} from '@/composables/useFetch';
+import {useUrl} from '@/composables/useUrl';
+
+export const useFileManagerStore = defineComponentStore(
+ 'fileManager',
+ (props) => {
+ const submissionId = ref(props.submissionId);
+
+ const {apiUrl: filesApiUrl} = useUrl(
+ `submissions/${submissionId.value}/files`,
+ );
+
+ const {data, fetch: fetchFiles} = useFetch(filesApiUrl, {
+ query: {
+ fileStages: props.fileStages,
+ reviewRoundId: props.reviewRoundId,
+ },
+ });
+
+ const files = computed(() => data.value?.items);
+
+ fetchFiles();
+
+ return {title: props.title, files, fetchFiles};
+ },
+);
diff --git a/src/managers/ReviewerManager/ReviewerManager.vue b/src/managers/ReviewerManager/ReviewerManager.vue
new file mode 100644
index 000000000..8fa5b6213
--- /dev/null
+++ b/src/managers/ReviewerManager/ReviewerManager.vue
@@ -0,0 +1,75 @@
+
+
+
+
+ {{ t('dashboard.summary.reviewers') }}
+
+
+
+
+ {{ t('dashboard.summary.reviewer') }}
+
+ {{ t('dashboard.summary.reviewerStatus') }}
+
+ {{ t('common.type"') }}
+
+
+
+
+
+ {{ reviewAssignment.reviewerFullName }}
+
+
+
+
+ {{ reviewAssignment.status }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/managers/ReviewerManager/reviewerManagerStore.js b/src/managers/ReviewerManager/reviewerManagerStore.js
new file mode 100644
index 000000000..b5f98261e
--- /dev/null
+++ b/src/managers/ReviewerManager/reviewerManagerStore.js
@@ -0,0 +1,24 @@
+import {computed} from 'vue';
+import {defineComponentStore} from '@/utils/defineComponentStore';
+import {useReviewAssignment} from '@/composables/useReviewAssignment';
+
+export const useReviewerManagerStore = defineComponentStore(
+ 'reviewerManagerStore',
+ (props) => {
+ const {getReviewMethodIcons, getOpenReviewAssignments} =
+ useReviewAssignment();
+
+ const reviewAssignments = computed(() => {
+ if (props.redactedForAuthors) {
+ return getOpenReviewAssignments(props.reviewAssignments);
+ }
+
+ return props.reviewAssignments;
+ });
+
+ return {
+ getReviewMethodIcons,
+ reviewAssignments,
+ };
+ },
+);
diff --git a/src/pages/dashboard/DashboardPage.mdx b/src/pages/dashboard/DashboardPage.mdx
new file mode 100644
index 000000000..cf87f50d7
--- /dev/null
+++ b/src/pages/dashboard/DashboardPage.mdx
@@ -0,0 +1,10 @@
+import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';
+
+import * as DashboardPage from './DashboardPage.stories.js';
+
+
+
+# DashboardPage page
+
+
+./DashboardPage.stories.js
diff --git a/src/pages/submissions/SubmissionsPage.stories.js b/src/pages/dashboard/DashboardPage.stories.js
similarity index 83%
rename from src/pages/submissions/SubmissionsPage.stories.js
rename to src/pages/dashboard/DashboardPage.stories.js
index 4c1ff7bd8..02edd2c22 100644
--- a/src/pages/submissions/SubmissionsPage.stories.js
+++ b/src/pages/dashboard/DashboardPage.stories.js
@@ -1,17 +1,17 @@
-import SubmissionsPage from './SubmissionsPage.vue';
+import DashboardPage from './DashboardPage.vue';
import {http, HttpResponse} from 'msw';
import SubmissionsMock25 from './mocks/submissions25.js';
-import PageInitConfigMock from './mocks/pageInitConfig';
+import PageInitConfigMock from './mocks/pageInitConfig.js';
-export default {title: 'Pages/Submissions', component: SubmissionsPage};
+export default {title: 'Pages/Dashboard', component: DashboardPage};
export const Init = {
render: (args) => ({
- components: {SubmissionsPage},
+ components: {DashboardPage},
setup() {
return {args};
},
- template: '
',
+ template: '
',
}),
parameters: {
// mock date to consistently show sensible editorial activity popups
diff --git a/src/pages/submissions/SubmissionsPage.vue b/src/pages/dashboard/DashboardPage.vue
similarity index 76%
rename from src/pages/submissions/SubmissionsPage.vue
rename to src/pages/dashboard/DashboardPage.vue
index 3efa0d6a5..626d3181b 100644
--- a/src/pages/submissions/SubmissionsPage.vue
+++ b/src/pages/dashboard/DashboardPage.vue
@@ -1,5 +1,5 @@
-