Skip to content

Commit 3f97137

Browse files
pkp/pkp-lib#12262 [Frontend] Publication – Media Files (#794)
* pkp/pkp-lib#12262 Add locale keys for Media Files manager * pkp/pkp-lib#12262 Add MediaFileManager component * pkp/pkp-lib#12262 Add TableRowSpan and TableBodyGroup components to support multi-rowspan * pkp/pkp-lib#12262 Add fileUtils file * pkp/pkp-lib#12262 Add FileMediaUploader component * pkp/pkp-lib#12262 Adjust padding and border for FileMediaUploader component * pkp/pkp-lib#12262 Add modal to open for Add Media File action * pkp/pkp-lib#12262 Add FieldSelectBorderless field component * pkp/pkp-lib#12262 Add Batch Link Images modal component * pkp/pkp-lib#12262 Add hideOnDisplay prop to FieldBase component * pkp/pkp-lib#12262 Add Metadata modal form component * pkp/pkp-lib#12262 Add handler to delete media file * pkp/pkp-lib#12262 Add size prop to FieldSelectBorderless component * pkp/pkp-lib#12262 Filter high res options for bacth link form to exclude already selected files * pkp/pkp-lib#12262 Set null for unlinked files in payload * pkp/pkp-lib#12262 Move linking image methods to shared api for batch linking and manual linking of each files * pkp/pkp-lib#12262 Add component to handle manual linking of an image * pkp/pkp-lib#12262 Rename useMediaFileManagerLinkImageTypes file with useMediaFileImageLinking * pkp/pkp-lib#12262 Only allow manual linking of image action for image files * pkp/pkp-lib#12262 Add safety check for mediaFiles.value * pkp/pkp-lib#12262 Add resolution type field after uploading media file * pkp/pkp-lib#12262 Fix dropzone issue when dragging dropping files * pkp/pkp-lib#12262 Move dropzone handlers to composeable for FileUpload and FileMediaUpload * pkp/pkp-lib#12262 Remove duplicated locale keys for media files * pkp/pkp-lib#12262 Add MediaFileManager to workflow config for publication - OJS & OPS * pkp/pkp-lib#12262 Remove unused locale key common.selectOption * pkp/pkp-lib#12262 Add missing locale keys for editing metadata * pkp/pkp-lib#12262 Open legacy modal to view more information for each media files * pkp/pkp-lib#12262 Update media file manager to use variantGroupId and variantType properties for media files * pkp/pkp-lib#12262 Update upload endpoint and payload for media files * pkp/pkp-lib#12262 Update api and payload for linking images for media files * pkp/pkp-lib#12262 Update payload and api used for editing metadata of media files * pkp/pkp-lib#12262 Update endpoint used for deleting a media file * pkp/pkp-lib#12262 Update sample api responses for media files stories * pkp/pkp-lib#12262 Add No Items display for empty media files table * pkp/pkp-lib#12262 Fix display of media files when variantGroupId is undefined * pkp/pkp-lib#12262 Update locale messages for resolution type options * pkp/pkp-lib#12262 Use correct submission id when viewing more information of a media file * pkp/pkp-lib#12262 Fix table display when a file is deleted from a rowspan group * pkp/pkp-lib#12262 Remove registerRowSpanGroupIndex from Table and handle striped group rows in TableBodyGroup component * pkp/pkp-lib#12262 Sort the items in group for media files * pkp/pkp-lib#12262 Use genre api to get genre options for uploading media files * pkp/pkp-lib#12262 Add linking of other media types if the genre supports high resolution versions * pkp/pkp-lib#12262 Update Metadata form based on genre's file metadata fields * pkp/pkp-lib#12262 Update attributes used and docs for FileMediaUploader component * pkp/pkp-lib#12262 Render media file names as downloadable links * pkp/pkp-lib#12262 Add genre categories to global js vars * pkp/pkp-lib#12262 Update properties used based on media files updated api data * pkp/pkp-lib#12262 update labels and messages used for medial files * pkp/pkp-lib#12262 Use producton workflow stage when viewing media file information * pkp/pkp-lib#12262 Use file id for unlinked files/no group id * pkp/pkp-lib#12262 Set multilingual fields to supplementary metadata * pkp/pkp-lib#12262 Use pkp.const.MEDIA_VARIANT_TYPE_HIGH_RESOLUTION for checking uploaded high resolution files * pkp/pkp-lib#12262 Warn user before closing batch link image modal with unsaved changes * pkp/pkp-lib#12262 Warn for unsaved changes when closing add media files modal * pkp/pkp-lib#12262 Add a message to the upload media file modal when there are no genre options * pkp/pkp-lib#12262 Validate only supported media files when uploading * pkp/pkp-lib#12262 Use date field for date created metadata with type supplementary * pkp/pkp-lib#12262 Disable Link Media when there are no web version files to link in batch * pkp/pkp-lib#12262 Filter media files by submission and publication * pkp/pkp-lib#12262 Correct data shape for genre api data * pkp/pkp-lib#12262 Use textarea field for artwork metadata * pkp/pkp-lib#12262 Remove hardcoded string 'web' and use pkp.const.MEDIA_VARIANT_TYPE_WEB * pkp/pkp-lib#12262 Make supportedFileTypes prop required * pkp/pkp-lib#12262 Add story files for FieldSelectBorderless component and 'With row span' story for Table component * pkp/pkp-lib#12262 Add Media link to OMP config * pkp/pkp-lib#12262 Only reload list when deletion is successful * pkp/pkp-lib#12262 Update access config for media files * pkp/pkp-lib#12262 Code cleanup for media files * pkp/pkp-lib#12262 Add FileTypeIcon component and use it for any file type displays from other components * pkp/pkp-lib#12262 Get supportedFileTypes from fileTypeGroups * pkp/pkp-lib#12262 Remove duplicate locales for uploads * pkp/pkp-lib#12262 Add required supportedFileTypes prop to media file story * pkp/pkp-lib#12262 Increase pageSize to 9999 to fetch all items in one call for safer data set * pkp/pkp-lib#12262 Move parseDropzoneError from fileUtils to useDropzoneDragDrop * pkp/pkp-lib#12262 Add useFile composeable * pkp/pkp-lib#12262 Rename FieldSelectBorderless with InlineSelect * pkp/pkp-lib#12262 Switch groupSize from getter to toRef * pkp/pkp-lib#12262 Remove TableRowSpan and extend TableCell for rowspan attribute * pkp/pkp-lib#12262 Remove supportedFileTypes prop from FileMediaUploader component
1 parent 84a1b7b commit 3f97137

56 files changed

Lines changed: 3564 additions & 151 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

public/globals.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ window.pkp = {
161161
ORCID: 4,
162162
PROCESSED: 5,
163163
},
164+
165+
// Genre categories
166+
GENRE_CATEGORY_DOCUMENT: 1,
167+
GENRE_CATEGORY_ARTWORK: 2,
168+
GENRE_CATEGORY_SUPPLEMENTARY: 3,
169+
170+
// Media variant types
171+
MEDIA_VARIANT_TYPE_WEB: 'web',
172+
MEDIA_VARIANT_TYPE_HIGH_RESOLUTION: 'high_resolution',
164173
},
165174

166175
/**
@@ -237,6 +246,7 @@ window.pkp = {
237246
'common.cancel': 'Cancel',
238247
'common.changeLanguage': 'Change Language',
239248
'common.clearSearch': 'Clear search phrase',
249+
'common.clickToUploadFiles': 'Click to upload files',
240250
'common.close': 'Close',
241251
'common.closed': 'Closed',
242252
'common.commaListSeparator': ', ',
@@ -296,6 +306,7 @@ window.pkp = {
296306
'common.numberedMore': '{$number} more',
297307
'common.numero': 'No',
298308
'common.ok': 'OK',
309+
'common.or': 'or',
299310
'common.oneMonth': '1 month',
300311
'common.oneWeek': '1 week',
301312
'common.order': 'Order',
@@ -322,12 +333,14 @@ window.pkp = {
322333
'common.searchPhrase': 'Search Phrase',
323334
'common.searching': 'Searching',
324335
'common.selectAll': 'Select All',
336+
'common.selectedFile': 'Selected File',
325337
'common.selectNone': 'Select None',
326338
'common.selectWithName': 'Select {$name}',
327339
'common.semicolonListSeparator': '; ',
328340
'common.showingSteps': '{$current}/{$total} steps',
329341
'common.showingXofX':
330342
'Showing <strong>{$start} to {$finish}</strong> of {$total}',
343+
'common.size': 'Size',
331344
'common.startDate': 'Start Date',
332345
'common.status': 'Status',
333346
'common.switchTo': 'Switch to',
@@ -339,6 +352,7 @@ window.pkp = {
339352
'common.upload': 'Upload',
340353
'common.upload.addFile': 'Add File',
341354
'common.upload.addFile.description': 'Upload a file from your computer.',
355+
'common.upload.addFiles': 'Add Files',
342356
'common.uploadedBy': 'Uploaded by {$name}',
343357
'common.uploadedByAndWhen': 'Uploaded by {$name} on {$date}',
344358
'common.user': 'User',
@@ -670,12 +684,14 @@ window.pkp = {
670684
'grid.action.deleteContributor': 'Delete Contributor',
671685
'grid.action.deleteContributor.confirmationMessage':
672686
'Are you sure you want to remove {$name} as a contributor? This action can not be undone.',
687+
'grid.action.deleteFile': 'Delete File',
673688
'grid.action.deleteReviewerRecommendation': 'Delete Recommendation',
674689
'grid.action.deleteReviewerSuggestion': 'Delete Reviewer Suggestion',
675690
'grid.action.deleteReviewerSuggestion.confirmationMessage':
676691
'Are you sure you want to remove this suggestion? This action can not be undone.',
677692
'grid.action.edit': 'Edit',
678693
'grid.action.editFile': 'Edit a file',
694+
'grid.action.editMetadata': 'Edit Metadata',
679695
'grid.action.editReviewerRecommendation': 'Edit Recommendation',
680696
'grid.action.logInAs': 'Login As',
681697
'grid.action.mergeUser': 'Merge user',
@@ -685,6 +701,17 @@ window.pkp = {
685701
'grid.action.sendToTextEditor': 'Send to Text Editor',
686702
'grid.action.sort': 'Sort',
687703
'grid.action.updateFile': 'Update File Details',
704+
'grid.artworkFile.caption': 'Caption',
705+
'grid.artworkFile.copyrightOwner': 'Copyright Owner',
706+
'grid.artworkFile.credit': 'Credit',
707+
'grid.artworkFile.permissionTerms': 'Permission Terms',
708+
'grid.supplementaryFile.creator': 'Creator',
709+
'grid.supplementaryFile.publisher': 'Publisher',
710+
'grid.supplementaryFile.source': 'Source',
711+
'grid.supplementaryFile.subject': 'Subject',
712+
'grid.supplementaryFile.sponsor': 'Sponsor',
713+
'grid.supplementaryFile.date': 'Date Created',
714+
'grid.supplementaryFile.language': 'Language',
688715
'grid.category.add': 'Add Category',
689716
'grid.category.categories': 'Categories',
690717
'grid.category.categoryName': 'Category Name',
@@ -984,6 +1011,46 @@ window.pkp = {
9841011
'publication.jats.confirmDeleteFileTitle': 'Confirm deleting JATS XML',
9851012
'publication.jats.lastModified':
9861013
'Last Modification at {$modificationDate} by {$username}',
1014+
'publication.mediaFiles': 'Media Files',
1015+
'publication.mediaFiles.add': 'Add Media File',
1016+
'publication.mediaFiles.batchLinkMedia': 'Batch Link Media',
1017+
'publication.mediaFiles.batchLinkMedia.description':
1018+
'Link web version media files to their high-resolution counterparts. Select a high-resolution file for each web version below.',
1019+
'publication.mediaFiles.confirmDelete':
1020+
'Are you sure you want to delete "{$fileName}"? This action cannot be undone. If this file is linked to other media, those links will be removed.',
1021+
'publication.mediaFiles.delete': 'Delete media file?',
1022+
'publication.mediaFiles.description':
1023+
'Upload media files in bulk, including high-resolution versions. After uploading, link each file to its web or high resolution counterpart by cliking Manually Link Media from the More Actions dropdown, or use Batch Link Media to link multiple files at once.',
1024+
'publication.mediaFiles.linkHighResolutionVersion':
1025+
'Link High-Resolution Version',
1026+
'publication.mediaFiles.linkMedia': 'Link Media',
1027+
'publication.mediaFiles.manuallyLinkMedia': 'Manually Link Media',
1028+
'publication.mediaFiles.metadataName': 'Name of the file',
1029+
'publication.mediaFiles.metadataNameDescription':
1030+
'(e.g., Manuscript; Table 1)',
1031+
'publication.mediaFiles.upload.noMediaTypes':
1032+
'No media types are configured. Please contact the system administrator.',
1033+
'publication.mediaFiles.noHighResolutionFile': 'No high-resolution file',
1034+
'publication.mediaFiles.noWebVersionFile': 'No web version file',
1035+
'publication.mediaFiles.selectHighResolutionFor':
1036+
'Select high-resolution version for {$fileName}',
1037+
'publication.mediaFiles.selectMediaFileToLink':
1038+
'Select the media file to link as its counterpart',
1039+
'publication.mediaFiles.selectMediaFileToLink.description':
1040+
'Only one file can be linked. The file types must differ (web <> high-res).',
1041+
'publication.mediaFiles.selectedWebVersion': 'Selected Web Version',
1042+
'publication.mediaFiles.upload': 'Upload Media File',
1043+
'publication.mediaFiles.upload.description':
1044+
'Upload image or multimedia files in bulk. The system will auto-link them to your HTML galley using matching filenames. You can manually adjust or link files later if needed.',
1045+
'publication.mediaFiles.upload.variantTypeWeb': 'Web resolution',
1046+
'publication.mediaFiles.upload.variantTypeHighRes': 'High resolution',
1047+
'publication.mediaFiles.upload.selectMediaType':
1048+
'What kind of media is this?',
1049+
'publication.mediaFiles.upload.selectMediaTypeDescription':
1050+
'Select a media type from the dropdown.',
1051+
'publication.mediaFiles.upload.fileResType': 'File resolution type',
1052+
'publication.mediaFiles.upload.fileResTypeDescription':
1053+
'Select a file resolution from the dropdown.',
9871054
'publication.publicationLicense': 'Permissions & Disclosure',
9881055
'publication.publish': 'Publish',
9891056
'publication.revisionSignificance.description':

src/components/Composer/Composer.vue

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@
234234
:key="i"
235235
class="composer__attachment"
236236
>
237-
<Icon
238-
:icon="getDocumentTypeIcon(attachment)"
237+
<FileTypeIcon
238+
:document-type="attachment.documentType"
239239
:inline="true"
240240
class="composer__attachment__documentType"
241241
/>
@@ -280,6 +280,7 @@ import Panel from '@/components/Panel/Panel.vue';
280280
import PanelSection from '@/components/Panel/PanelSection.vue';
281281
import PkpButton from '@/components/Button/Button.vue';
282282
import Icon from '@/components/Icon/Icon.vue';
283+
import FileTypeIcon from '@/components/FileTypeIcon/FileTypeIcon.vue';
283284
import Badge from '@/components/Badge/Badge.vue';
284285
import Spinner from '@/components/Spinner/Spinner.vue';
285286
@@ -300,6 +301,7 @@ export default {
300301
Spinner,
301302
PkpButton,
302303
Icon,
304+
FileTypeIcon,
303305
Badge,
304306
},
305307
mixins: [ajaxErrorCallback, dialog, preparedContent],
@@ -782,21 +784,6 @@ export default {
782784
}
783785
},
784786
785-
/**
786-
* Get the icon to match this document type,
787-
* such as PDF, Word, spreadsheet, etc.
788-
*
789-
* @param {Object} attachment The file attachment
790-
* @return {String}
791-
*/
792-
getDocumentTypeIcon(attachment) {
793-
return !!pkp.documentTypeIcons &&
794-
!!attachment.documentType &&
795-
!!pkp.documentTypeIcons[attachment.documentType]
796-
? pkp.documentTypeIcons[attachment.documentType]
797-
: 'DefaultDocument';
798-
},
799-
800787
/**
801788
* Emit an event to change a prop
802789
*

src/components/File/File.vue

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<template>
22
<div class="flex min-w-0 items-center">
3-
<Icon :icon="documentTypeIcon" class="h-6 w-6 flex-none text-heading" />
3+
<FileTypeIcon
4+
:document-type="documentType"
5+
class="h-6 w-6 flex-none text-heading"
6+
/>
47
<span v-if="fileId" class="file__id">
58
{{ fileId }}
69
</span>
@@ -16,10 +19,10 @@
1619
</template>
1720

1821
<script>
19-
import Icon from '@/components/Icon/Icon.vue';
22+
import FileTypeIcon from '@/components/FileTypeIcon/FileTypeIcon.vue';
2023
2124
export default {
22-
components: {Icon},
25+
components: {FileTypeIcon},
2326
props: {
2427
/** Optional but recommended. Pass one of the `DOCUMENT_TYPE_` constants to show an icon that will match this document type. */
2528
documentType: {
@@ -49,18 +52,5 @@ export default {
4952
},
5053
},
5154
},
52-
computed: {
53-
/**
54-
* The icon to match this document type
55-
*
56-
* @return {String}
57-
*/
58-
documentTypeIcon() {
59-
return !!pkp.documentTypeIcons &&
60-
!!pkp.documentTypeIcons[this.documentType]
61-
? pkp.documentTypeIcons[this.documentType]
62-
: 'DocumentDefault';
63-
},
64-
},
6555
};
6656
</script>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {Meta, ArgTypes, Canvas} from '@storybook/addon-docs/blocks';
2+
import * as FileMediaUploaderStories from './FileMediaUploader.stories';
3+
4+
<Meta of={FileMediaUploaderStories} />
5+
6+
# FileMediaUploader
7+
8+
A file upload component for media files with drag-and-drop support, progress tracking, genre selection, and resolution type selection.
9+
10+
## Usage
11+
12+
The `FileMediaUploader` component allows users to upload files via drag-and-drop or by clicking to browse. After each file is uploaded, users must select a genre and resolution type before submitting.
13+
14+
Genre options should be filtered to only include dependent genres (e.g., Image, Multimedia). Each genre option includes a `supportsFileVariants` flag that controls whether the resolution type dropdown is enabled.
15+
16+
### Basic usage
17+
18+
```vue
19+
<template>
20+
<FileMediaUploader
21+
id="mediaUploader"
22+
api-url="/api/v1/temporaryFiles"
23+
:genre-options="genreOptions"
24+
@uploaded="handleUploaded"
25+
/>
26+
</template>
27+
28+
<script setup>
29+
const genreOptions = [
30+
{value: 9, label: 'Multimedia', supportsFileVariants: true},
31+
{value: 10, label: 'Image', supportsFileVariants: true},
32+
{value: 11, label: 'HTML Stylesheet', supportsFileVariants: false},
33+
];
34+
35+
function handleUploaded(files) {
36+
// files: [{fileId, genreId, variantType, name, mimetype, ...}]
37+
console.log('Uploaded files:', files);
38+
}
39+
</script>
40+
```
41+
42+
### Tracking unsaved changes
43+
44+
Listen to `file-count-change` to know when the user has staged at least one file. Useful for warning before closing a modal with pending uploads.
45+
46+
```vue
47+
<FileMediaUploader
48+
id="mediaUploader"
49+
api-url="/api/v1/temporaryFiles"
50+
:genre-options="genreOptions"
51+
@uploaded="handleUploaded"
52+
@file-count-change="handleFileCountChange"
53+
/>
54+
```
55+
56+
## Features
57+
58+
- **Drag and Drop**: Users can drag files directly onto the dropzone area
59+
- **Click to Upload**: Users can click to open the file browser
60+
- **Progress Tracking**: Shows upload progress with a progress bar and percentage
61+
- **Genre Selection**: After upload, users select a genre from a dropdown (required)
62+
- **Resolution Type Selection**: Users select a resolution type (Web or High-resolution). The dropdown is disabled for genres that do not support high-resolution versions
63+
- **File Removal**: Users can remove files before uploading
64+
- **Error Handling**: Displays error messages when uploads fail
65+
66+
## Props
67+
68+
<ArgTypes />
69+
70+
## Events
71+
72+
### `uploaded`
73+
74+
Emitted when the user clicks the "Upload Files" button after all files have been uploaded and assigned a genre and resolution type.
75+
76+
**Payload**: An array of file objects with the following structure:
77+
78+
```js
79+
[
80+
{
81+
fileId: 123,
82+
genreId: 10,
83+
variantType: 'web',
84+
name: 'photo.jpg',
85+
mimetype: 'image/jpeg',
86+
// ...other server response fields
87+
},
88+
];
89+
```
90+
91+
### `fileCountChange`
92+
93+
Emitted on file count changes (add/remove/failed upload). Useful for parents that need to know if anything is staged - e.g., to trigger an "unsaved changes" warning before closing a modal.
94+
95+
**Payload**: `Number` — current file count.
96+
97+
<Canvas of={FileMediaUploaderStories.Default} />
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import FileMediaUploader from './FileMediaUploader.vue';
2+
import {http, HttpResponse, delay} from 'msw';
3+
import dropzoneOptions from '@/mocks/dropzoneOptions';
4+
import articleComponentGenres from '@/mocks/articleComponentGenres';
5+
import {useLocalize} from '@/composables/useLocalize';
6+
7+
export default {
8+
title: 'Components/FileMediaUploader',
9+
component: FileMediaUploader,
10+
};
11+
12+
const {localize} = useLocalize();
13+
14+
const genreOptions = articleComponentGenres
15+
.filter((genre) => genre.dependent)
16+
.map((genre) => ({
17+
value: genre.id,
18+
label: localize(genre.name),
19+
supportsFileVariants: genre.supportsFileVariants,
20+
}));
21+
22+
export const Default = {
23+
render: (args) => ({
24+
components: {FileMediaUploader},
25+
setup() {
26+
function onUploaded(files) {
27+
console.log('Uploaded files:', JSON.stringify(files, null, 2));
28+
alert(
29+
`Uploaded ${files.length} file(s):\n${files.map((f) => `- ${f.name} (genreId: ${f.genreId})`).join('\n')}`,
30+
);
31+
}
32+
33+
return {args, onUploaded};
34+
},
35+
template: `
36+
<FileMediaUploader
37+
v-bind="args"
38+
@uploaded="onUploaded"
39+
/>
40+
`,
41+
}),
42+
args: {
43+
id: 'fileMediaUploader',
44+
apiUrl: 'https://mock/index.php/publicknowledge/api/v1/temporaryFiles',
45+
genreOptions,
46+
options: dropzoneOptions,
47+
},
48+
parameters: {
49+
msw: {
50+
handlers: [
51+
http.post(
52+
'https://mock/index.php/publicknowledge/api/v1/temporaryFiles',
53+
async ({request}) => {
54+
// Simulate upload delay
55+
await delay(1500);
56+
57+
const formData = await request.formData();
58+
const file = formData.get('file');
59+
const fileName =
60+
file?.name || formData.get('name') || 'uploaded-file';
61+
62+
return HttpResponse.json({
63+
id: Math.floor(Math.random() * 1000) + 1,
64+
name: fileName,
65+
mimetype: file?.type || 'application/octet-stream',
66+
documentType: 'default',
67+
});
68+
},
69+
),
70+
],
71+
},
72+
},
73+
};

0 commit comments

Comments
 (0)