diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 11ef8f679a..b552f30326 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9280,61 +9280,6 @@ parameters: count: 1 path: src/lib/UI/Config/Provider/Autosave.php - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:__construct\\(\\) has parameter \\$defaultMappings with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:__construct\\(\\) has parameter \\$fallbackContentType with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:__construct\\(\\) has parameter \\$locationMappings with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:buildFallbackContentTypeStructure\\(\\) has parameter \\$fallbackContentType with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:buildFallbackContentTypeStructure\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:buildMappingGroupStructure\\(\\) has parameter \\$mappingGroup with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:buildMappingGroupStructure\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Method Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:getConfig\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Property Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:\\$defaultMappings type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Property Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:\\$fallbackContentType type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - - - message: "#^Property Ibexa\\\\AdminUi\\\\UI\\\\Config\\\\Provider\\\\ContentTypeMappings\\:\\:\\$locationMappings type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/UI/Config/Provider/ContentTypeMappings.php - - message: "#^Offset 'dirname' does not exist on array\\{dirname\\?\\: string, basename\\: string, extension\\?\\: string, filename\\: string\\}\\.$#" count: 1 diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index 2cacdf6c22..0e715c8ce7 100644 --- a/src/bundle/Resources/encore/ibexa.js.config.js +++ b/src/bundle/Resources/encore/ibexa.js.config.js @@ -228,9 +228,7 @@ module.exports = (Encore) => { '../../ui-dev/src/modules/universal-discovery/components/tree-item-toggle-selection/tree.item.toggle.selection.js', ), ]) - .addEntry('ibexa-admin-ui-mfu-js', [ - path.resolve(__dirname, '../../ui-dev/src/modules/multi-file-upload/multi.file.upload.module.js'), - ]) + .addEntry('ibexa-admin-ui-mfu-js', [path.resolve(__dirname, '../../ui-dev/src/modules/multi-file-upload/config.loader.js')]) .addEntry('ibexa-admin-ui-subitems-js', [path.resolve(__dirname, '../../ui-dev/src/modules/sub-items/sub.items.module.js')]) .addEntry('ibexa-admin-ui-content-tree-js', [ path.resolve(__dirname, '../../ui-dev/src/modules/content-tree/content.tree.module.js'), diff --git a/src/bundle/Resources/public/img/icons/about-info.svg b/src/bundle/Resources/public/img/icons/about-info.svg index b84ce9bb9a..418403f2a3 100755 --- a/src/bundle/Resources/public/img/icons/about-info.svg +++ b/src/bundle/Resources/public/img/icons/about-info.svg @@ -1,4 +1,4 @@ about-info - + diff --git a/src/bundle/Resources/public/img/icons/trash.svg b/src/bundle/Resources/public/img/icons/trash.svg index d6f25ddfc5..7124b7f708 100755 --- a/src/bundle/Resources/public/img/icons/trash.svg +++ b/src/bundle/Resources/public/img/icons/trash.svg @@ -1,4 +1,4 @@ trash - + diff --git a/src/bundle/Resources/public/img/icons/upload.svg b/src/bundle/Resources/public/img/icons/upload.svg index ade71ee965..e83ea27913 100755 --- a/src/bundle/Resources/public/img/icons/upload.svg +++ b/src/bundle/Resources/public/img/icons/upload.svg @@ -1,4 +1,4 @@ upload - + diff --git a/src/bundle/Resources/public/js/scripts/admin.location.view.js b/src/bundle/Resources/public/js/scripts/admin.location.view.js index 3d54ea1f7e..b5ba200956 100644 --- a/src/bundle/Resources/public/js/scripts/admin.location.view.js +++ b/src/bundle/Resources/public/js/scripts/admin.location.view.js @@ -150,6 +150,7 @@ return contentTypeDataMap; }, {}); + const mfuAttrs = { adminUiConfig: { ...ibexa.adminUiConfig, @@ -160,6 +161,7 @@ contentTypeIdentifier: mfuContainer.dataset.parentContentTypeIdentifier, contentTypeId: parseInt(mfuContainer.dataset.parentContentTypeId, 10), locationPath: mfuContainer.dataset.parentLocationPath, + name: mfuContainer.dataset.parentName, language: mfuContainer.dataset.parentContentLanguage, }, currentLanguage: mfuContainer.dataset.currentLanguage, diff --git a/src/bundle/Resources/public/js/scripts/helpers/context.helper.js b/src/bundle/Resources/public/js/scripts/helpers/context.helper.js index 016b150042..1131fc685b 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/context.helper.js +++ b/src/bundle/Resources/public/js/scripts/helpers/context.helper.js @@ -1,5 +1,6 @@ let { bootstrap, flatpickr, moment, Popper, Routing, Translator } = window; let adminUiConfig = window.ibexa?.adminUiConfig; +let rootNode = document.body; const restInfo = { accessToken: null, instanceUrl: window.location.origin, @@ -7,10 +8,10 @@ const restInfo = { siteaccess: document.querySelector('meta[name="SiteAccess"]')?.content, }; -export const setRestInfo = ({ instanceUrl, token, csrfToken, siteaccess }) => { +export const setRestInfo = ({ instanceUrl, accessToken, token, siteaccess }) => { restInfo.instanceUrl = instanceUrl ?? restInfo.instanceUrl; + restInfo.accessToken = accessToken ?? restInfo.accessToken; restInfo.token = token ?? restInfo.token; - restInfo.csrfToken = csrfToken ?? restInfo.csrfToken; restInfo.siteaccess = siteaccess ?? restInfo.siteaccess; }; export const setAdminUiConfig = (loadedAdminUiConfig) => (adminUiConfig = loadedAdminUiConfig); @@ -44,6 +45,7 @@ export const setTranslator = (TranslatorInstance, forceSet = false) => { Translator = TranslatorInstance; } }; +export const setRootNode = (rootNodeParam) => (rootNode = rootNodeParam); export const getAdminUiConfig = () => adminUiConfig; export const getBootstrap = () => bootstrap; @@ -53,3 +55,4 @@ export const getPopper = () => Popper; export const getRouting = () => Routing; export const getTranslator = () => Translator; export const getRestInfo = () => restInfo; +export const getRootNode = () => rootNode; diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_tooltip.popup.scss b/src/bundle/Resources/public/scss/ui/modules/common/_tooltip.popup.scss index c1763cf6d9..25ff50a027 100644 --- a/src/bundle/Resources/public/scss/ui/modules/common/_tooltip.popup.scss +++ b/src/bundle/Resources/public/scss/ui/modules/common/_tooltip.popup.scss @@ -9,6 +9,12 @@ margin: 0; } + &__subtitle { + @include modal-subtitle(); + + color: $ibexa-color-dark; + } + &__close { @include close-button(); } diff --git a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_drop.area.scss b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_drop.area.scss index c4a006bef8..60e704d569 100644 --- a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_drop.area.scss +++ b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_drop.area.scss @@ -5,21 +5,81 @@ flex-direction: column; justify-content: center; align-items: center; + padding: calculateRem(47px); &__message { + color: $ibexa-color-dark; + margin-bottom: calculateRem(16px); + } + + &__message--main { + cursor: auto; + font-weight: 600; + } + + &__message--filesize { + margin: calculateRem(16px) 0 0 0; color: $ibexa-color-dark-400; - margin-bottom: calculateRem(12px); + font-size: $ibexa-text-font-size-medium; + } + + &__max-files-size { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + row-gap: calculateRem(8px); + + &--expanded { + .c-drop-area { + &__max-file-size-item { + display: flex; + } - &--main { - cursor: auto; - font-weight: bold; - margin-top: calculateRem(44px); + &__max-file-size-toggle-btn { + &::after { + transform: rotate(-180deg); + } + } + } } + } + + &__max-file-size-item { + display: none; + gap: calculateRem(4px); + justify-content: center; + align-items: center; + font-size: $ibexa-text-font-size-small; - &--filesize { - color: $ibexa-color-dark-300; + &:first-child { font-size: $ibexa-text-font-size-medium; - margin: calculateRem(12px) 0 calculateRem(44px); + display: flex; + } + } + + &__max-file-size-toggle-btn { + width: calculateRem(16px); + height: calculateRem(16px); + position: relative; + display: inline-block; + cursor: pointer; + border: none; + + &::after { + content: ''; + position: absolute; + width: calculateRem(6px); + height: calculateRem(3px); + top: calc(50% - #{calculateRem(3px)}); + right: 0; + border-left: calculateRem(6px) solid transparent; + border-right: calculateRem(6px) solid transparent; + border-top: calculateRem(6px) solid $ibexa-color-dark-400; + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; } } diff --git a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_progress.bar.scss b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_progress.bar.scss index 88c2ff41ff..0a7dd9ff5a 100644 --- a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_progress.bar.scss +++ b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_progress.bar.scss @@ -9,13 +9,13 @@ border-radius: calculateRem(4px); transition: width 0.2s linear; height: calculateRem(8px); - width: 10vw; + width: calculateRem(176px); position: relative; &::after { content: ''; width: calc(100% - var(--progress)); - height: calculateRem(10px); + height: calculateRem(11px); border-radius: calculateRem(4px); position: absolute; right: 0; @@ -29,7 +29,7 @@ } &__label { - font-size: $ibexa-text-font-size-small; + font-size: $ibexa-text-font-size-medium; color: $ibexa-color-dark-400; } } diff --git a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.item.scss b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.item.scss index bddf4aeed7..19696088af 100644 --- a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.item.scss +++ b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.item.scss @@ -1,20 +1,39 @@ .c-upload-list-item { display: flex; + flex-wrap: wrap; background: $ibexa-color-white; - padding: calculateRem(8px) 0; - min-height: calculateRem(60px); + padding: 0; + margin: calculateRem(12px) 0; + height: calculateRem(48px); &--errored { - background: $ibexa-color-danger-100; - color: $ibexa-color-danger; + background: $ibexa-color-danger-200; + color: $ibexa-color-danger-600; border-radius: $ibexa-border-radius; .ibexa-icon { - fill: $ibexa-color-danger; + fill: $ibexa-color-danger-600; } .c-upload-list-item__size { - color: $ibexa-color-danger; + color: $ibexa-color-danger-600; + } + } + + &--expanded-multiple-errors { + padding-bottom: 0; + height: auto; + + .c-upload-list-item { + &__multiple-errors-list { + display: block; + padding: calculateRem(4px) 0 calculateRem(8px) calculateRem(26px); + margin-top: calculateRem(4px); + } + + &__multiple-errors-toggle-btn { + transform: rotate(180deg); + } } } @@ -26,26 +45,25 @@ } &__meta { - padding: 0 calculateRem(16px); - line-height: 1.4; + padding: 0 calculateRem(8px); max-width: 25vw; + height: calculateRem(48px); display: flex; justify-content: center; align-items: center; } &__name { - font-size: calculateRem(16px); margin-right: calculateRem(8px); - max-width: 15vw; + max-width: calculateRem(172px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } &__size { - color: $ibexa-color-dark-300; - font-size: $ibexa-text-font-size-medium; + color: $ibexa-color-dark-400; + font-size: $ibexa-text-font-size-small; } &__info { @@ -56,8 +74,8 @@ } &__message { - font-style: italic; - font-size: $ibexa-text-font-size-small; + font-size: $ibexa-text-font-size-medium; + line-height: $ibexa-text-font-size-medium; .ibexa-icon { margin-right: calculateRem(4px); @@ -70,6 +88,14 @@ fill: $ibexa-color-success; } } + + &--error { + display: flex; + align-items: center; + font-size: $ibexa-text-font-size-small; + line-height: $ibexa-text-font-size-small; + padding-right: calculateRem(32px); + } } &__actions { @@ -93,4 +119,27 @@ margin-right: 0; } } + + &__multiple-errors-toggle-btn { + border: none; + outline: none; + margin: 0 0 0 calculateRem(8px); + padding: 0; + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + } + + &__multiple-errors-list { + display: none; + flex-basis: 100%; + background: $ibexa-color-danger-100; + padding: 0; + margin: 0; + list-style: none; + border-radius: 0 0 $ibexa-border-radius $ibexa-border-radius; + } + + &__multiple-errors-item { + margin: calculateRem(4px) 0; + font-size: $ibexa-text-font-size-medium; + } } diff --git a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.list.scss b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.list.scss index 5d3a1027fb..8d1813a068 100644 --- a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.list.scss +++ b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.list.scss @@ -1,14 +1,5 @@ .c-upload-list { - &__items { - margin-top: calculateRem(32px); - padding: calculateRem(16px) 0; - - &:not(:empty) { - border-top: calculateRem(1px) solid $ibexa-color-light-500; - } - - &:last-child { - padding-bottom: 0; - } - } + height: 100%; + overflow: auto; + margin-top: calculateRem(16px); } diff --git a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.popup.scss b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.popup.scss index b54978b669..e4cfc9cd25 100644 --- a/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.popup.scss +++ b/src/bundle/Resources/public/scss/ui/modules/muti-file-upload/_upload.popup.scss @@ -8,32 +8,54 @@ width: 100vw; color: $ibexa-color-dark; + &__label { + margin-bottom: calculateRem(8px); + color: $ibexa-color-dark-400; + font-size: $ibexa-text-font-size-small; + line-height: calculateRem(18px); + } + .c-tooltip-popup { width: 100%; - max-width: calculateRem(774px); + max-height: 100vh; + max-width: calculateRem(800px); position: absolute; z-index: 2; left: 50%; top: 50%; transform: translate(-50%, -50%); - padding: 0 calculateRem(24px); + overflow: hidden; &__header { @include modal-header(); - padding: $modal-header-padding-y $modal-header-padding-x; - border-bottom: $modal-header-border-width solid $modal-header-border-color; + height: calculateRem(92px); + padding: calculateRem(32px) calculateRem(32px) 0; + margin-bottom: calculateRem(36px); + } + + &__title { + line-height: calculateRem(42px); + } + + &__close { + top: 0; + } + + &__subtitle { + margin-top: calculateRem(8px); } &__content { @include modal-body(); - padding: $modal-inner-padding; + padding: 0 calculateRem(32px); + max-height: calc(100vh - #{calculateRem(208px)}); + overflow: auto; } - } - .c-upload-list { - overflow-y: auto; - max-height: 30vw; + &__footer { + padding: calculateRem(16px) calculateRem(32px) calculateRem(32px) calculateRem(32px); + } } } diff --git a/src/bundle/Resources/translations/ibexa_content.en.xliff b/src/bundle/Resources/translations/ibexa_content.en.xliff index 602535d1a6..d243909be4 100644 --- a/src/bundle/Resources/translations/ibexa_content.en.xliff +++ b/src/bundle/Resources/translations/ibexa_content.en.xliff @@ -106,6 +106,11 @@ Close key: tooltip.close_label + + Confirm + Confirm + key: tooltip.confirm_label + Are you sure you want to send this Content item to Trash? Are you sure you want to send this Content item to Trash? diff --git a/src/bundle/Resources/translations/ibexa_multi_file_upload.en.xliff b/src/bundle/Resources/translations/ibexa_multi_file_upload.en.xliff index 1f1a192945..5ef19468eb 100644 --- a/src/bundle/Resources/translations/ibexa_multi_file_upload.en.xliff +++ b/src/bundle/Resources/translations/ibexa_multi_file_upload.en.xliff @@ -21,10 +21,10 @@ Cannot get content type by identifier key: cannot_get_content_type_identifier.message - - Cannot upload file - Cannot upload file - key: cannot_upload.message + + An error occurred while deleting a file + An error occurred while deleting a file + key: delete.error.message Delete @@ -61,16 +61,36 @@ Edit key: edit.label + + An error occurred while publishing a file + An error occurred while publishing a file + key: general.error.message + - Max file size: - Max file size: + %contentTypeName% max file size: %maxFileSize% + %contentTypeName% max file size: %maxFileSize% key: max_file_size.message + + Max. file size + Max. file size + key: max_file_size.message.general + Upload Upload key: multi_file_upload_open_btn.label + + Under %name% + Under %name% + key: multi_file_upload_popup.subtitle + + + Failed to upload + Failed to upload + key: multierror.label + Uploading... Uploading... @@ -86,10 +106,25 @@ Upload file key: upload_btn.label - + + Confirm and close + Confirm and close + key: upload_popup.close_label + + + Cancel pending upload + Cancel pending upload + key: upload_popup.confirm_label + + + Upload + Upload + key: upload_popup.label + + Multi-file upload Multi-file upload - key: upload_popup.close + key: upload_popup.title diff --git a/src/bundle/Resources/views/themes/admin/ui/component/sub_items/multifile_upload.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/sub_items/multifile_upload.html.twig index 28bb623c67..8b23261988 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/sub_items/multifile_upload.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/sub_items/multifile_upload.html.twig @@ -3,5 +3,6 @@ data-parent-location-path="{{ location.pathString }}" data-parent-content-type-identifier="{{ contentType.identifier }}" data-parent-content-type-id="{{ contentType.id }}" + data-parent-name="{{ location.contentInfo.name }}" data-current-language="{{ app.request.get('languageCode') ?: content.prioritizedFieldLanguageCode }}" > diff --git a/src/bundle/ui-dev/src/modules/common/icon/icon.js b/src/bundle/ui-dev/src/modules/common/icon/icon.js index 24d093fae7..11385822aa 100644 --- a/src/bundle/ui-dev/src/modules/common/icon/icon.js +++ b/src/bundle/ui-dev/src/modules/common/icon/icon.js @@ -19,7 +19,7 @@ const Icon = (props) => { return ( <> {isIncludedIcon ? ( - + ) : ( )} @@ -32,6 +32,7 @@ Icon.propTypes = { name: PropTypes.string, customPath: PropTypes.string, useIncludedIcon: PropTypes.bool, + defaultIconName: PropTypes.string, }; Icon.defaultProps = { @@ -39,6 +40,7 @@ Icon.defaultProps = { name: null, extraClasses: null, useIncludedIcon: false, + defaultIconName: 'about-info', }; export default Icon; diff --git a/src/bundle/ui-dev/src/modules/common/icon/inculdedIcon.js b/src/bundle/ui-dev/src/modules/common/icon/inculdedIcon.js index 86b43abbb6..b8c058bc0c 100644 --- a/src/bundle/ui-dev/src/modules/common/icon/inculdedIcon.js +++ b/src/bundle/ui-dev/src/modules/common/icon/inculdedIcon.js @@ -25,12 +25,14 @@ import Place from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/place.s import Product from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/product.svg'; import Search from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/search.svg'; import Spinner from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/spinner.svg'; +import Trash from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/trash.svg'; import Video from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/video.svg'; import View from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/view.svg'; import ViewGrid from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/view-grid.svg'; import ViewList from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/view-list.svg'; import User from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/user.svg'; import UserGroup from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/user_group.svg'; +import Upload from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/upload.svg'; import UploadImage from '@ibexa-admin-ui/src/bundle/Resources/public/img/icons/upload-image.svg'; const iconsMap = { @@ -58,19 +60,20 @@ const iconsMap = { product: Product, search: Search, spinner: Spinner, + trash: Trash, video: Video, view: View, 'view-grid': ViewGrid, 'view-list': ViewList, - 'missing-icon': AboutInfo, user: User, user_group: UserGroup, + upload: Upload, 'upload-image': UploadImage, }; const InculdedIcon = (props) => { - const { name, cssClass } = props; - const IconComponent = iconsMap[name] ?? iconsMap['missing-icon']; + const { name, cssClass, defaultIconName } = props; + const IconComponent = iconsMap[name] ?? iconsMap[defaultIconName]; return ; }; @@ -78,11 +81,13 @@ const InculdedIcon = (props) => { InculdedIcon.propTypes = { cssClass: PropTypes.string, name: PropTypes.string, + defaultIconName: PropTypes.string, }; InculdedIcon.defaultProps = { cssClass: '', - name: 'missing-icon', + name: 'about-info', + defaultIconName: 'about-info', }; export default InculdedIcon; diff --git a/src/bundle/ui-dev/src/modules/common/tooltip-popup/tooltip.popup.component.js b/src/bundle/ui-dev/src/modules/common/tooltip-popup/tooltip.popup.component.js index 4d2513f198..2e59d49382 100644 --- a/src/bundle/ui-dev/src/modules/common/tooltip-popup/tooltip.popup.component.js +++ b/src/bundle/ui-dev/src/modules/common/tooltip-popup/tooltip.popup.component.js @@ -1,62 +1,45 @@ -import React, { useLayoutEffect, useRef, useState } from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; -import Icon from '../icon/icon'; - -const { Translator } = window; - -const INITIAL_HEIGHT = 'initial'; -const HEADER_HEIGHT = 35; +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; const TooltipPopupComponent = (props) => { const contentRef = useRef(); - const [maxHeight, setMaxHeight] = useState(INITIAL_HEIGHT); - - useLayoutEffect(() => { - const { top, height } = contentRef.current.getBoundingClientRect(); - const topRounded = Math.round(top); - - if (topRounded < HEADER_HEIGHT) { - setMaxHeight(height + topRounded - HEADER_HEIGHT); - } else if (topRounded > HEADER_HEIGHT) { - setMaxHeight(INITIAL_HEIGHT); - } - }); - const attrs = { className: 'c-tooltip-popup', hidden: !props.visible, }; - const contentStyle = - maxHeight === INITIAL_HEIGHT - ? {} - : { - maxHeight, - overflowY: 'scroll', - }; - const closeLabel = Translator.trans(/*@Desc("Close")*/ 'tooltip.close_label', {}, 'ibexa_content'); return (

{props.title}

-
- -
+ {props.subtitle &&
{props.subtitle}
}
-
+
{props.children}
{props.showFooter && (
- + {props.onConfirm && ( + + )} + {props.onClose && ( + + )}
)}
@@ -65,15 +48,35 @@ const TooltipPopupComponent = (props) => { TooltipPopupComponent.propTypes = { title: PropTypes.string.isRequired, + subtitle: PropTypes.string, children: PropTypes.node.isRequired, visible: PropTypes.bool.isRequired, onClose: PropTypes.func, + onConfirm: PropTypes.func, showFooter: PropTypes.bool, + confirmLabel: PropTypes.string, + closeLabel: PropTypes.string, + confirmBtnAttrs: PropTypes.object, + closeBtnAttrs: PropTypes.object, }; TooltipPopupComponent.defaultProps = { + subtitle: '', onClose: () => {}, + onConfirm: () => {}, showFooter: true, + confirmLabel: () => { + const Translator = getTranslator(); + + return Translator.trans(/*@Desc("Confirm")*/ 'tooltip.confirm_label', {}, 'ibexa_content'); + }, + closeLabel: () => { + const Translator = getTranslator(); + + return Translator.trans(/*@Desc("Close")*/ 'tooltip.close_label', {}, 'ibexa_content'); + }, + confirmBtnAttrs: {}, + closeBtnAttrs: {}, }; export default TooltipPopupComponent; diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/components/drop-area/drop.area.component.js b/src/bundle/ui-dev/src/modules/multi-file-upload/components/drop-area/drop.area.component.js index 726a13c27d..70c9874382 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/components/drop-area/drop.area.component.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/components/drop-area/drop.area.component.js @@ -1,16 +1,20 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; import { fileSizeToString } from '../../helpers/text.helper'; - -const { Translator } = window; - +import { createCssClassNames } from '../../../common/helpers/css.class.names'; +import Icon from '../../../common/icon/icon'; export default class DropAreaComponent extends Component { constructor(props) { super(props); this._refFileInput = null; + this.state = { + filesSizeExpanded: false, + }; + this.openFileSelector = this.openFileSelector.bind(this); this.handleUpload = this.handleUpload.bind(this); } @@ -42,6 +46,53 @@ export default class DropAreaComponent extends Component { event.currentTarget.value = null; } + renderMaxFileSizesMsg() { + const Translator = getTranslator(); + const maxFilesSizeListClassNames = createCssClassNames({ + 'c-drop-area__max-files-size': true, + 'c-drop-area__max-files-size--expanded': this.state.filesSizeExpanded, + }); + const isMaxFileSizesMultiMsg = this.props.maxFileSizes.length > 1; + + return ( + <> +
    + {isMaxFileSizesMultiMsg && ( +
  • + + {Translator.trans(/*@Desc("Max. file size")*/ 'max_file_size.message.general', {}, 'ibexa_multi_file_upload')} + +
  • + )} + {this.props.maxFileSizes.map((contentType) => ( +
  • + {!isMaxFileSizesMultiMsg && ( + + )} + {Translator.trans( + /*@Desc("%contentTypeName% max file size: %maxFileSize%")*/ 'max_file_size.message', + { + contentTypeName: contentType.name, + maxFileSize: fileSizeToString(contentType.maxFileSize), + }, + 'ibexa_multi_file_upload', + )} +
  • + ))} +
+ + ); + } + componentDidMount() { window.addEventListener('drop', this.props.preventDefaultAction, false); window.addEventListener('dragover', this.props.preventDefaultAction, false); @@ -53,7 +104,7 @@ export default class DropAreaComponent extends Component { } render() { - const maxFileSizeMessage = Translator.trans(/*@Desc("Max file size:")*/ 'max_file_size.message', {}, 'ibexa_multi_file_upload'); + const Translator = getTranslator(); const dropActionMessage = Translator.trans(/*@Desc("Drag and drop file")*/ 'drop_action.message', {}, 'ibexa_multi_file_upload'); const separatorMessage = Translator.trans(/*@Desc("or")*/ 'drop_action.separator', {}, 'ibexa_multi_file_upload'); const uploadBtnLabel = Translator.trans(/*@Desc("Upload file")*/ 'upload_btn.label', {}, 'ibexa_multi_file_upload'); @@ -70,9 +121,7 @@ export default class DropAreaComponent extends Component { > {uploadBtnLabel} -
- {maxFileSizeMessage} {fileSizeToString(this.props.maxFileSize)} -
+
{this.renderMaxFileSizesMsg()}
(this._refFileInput = ref)} @@ -89,7 +138,7 @@ export default class DropAreaComponent extends Component { } DropAreaComponent.propTypes = { - maxFileSize: PropTypes.number.isRequired, + maxFileSizes: PropTypes.object.isRequired, processUploadedFiles: PropTypes.func.isRequired, preventDefaultAction: PropTypes.func.isRequired, addItemsToUpload: PropTypes.func.isRequired, diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/components/progress-bar/progress.bar.component.js b/src/bundle/ui-dev/src/modules/multi-file-upload/components/progress-bar/progress.bar.component.js index 512c3dcfcb..aac05d225b 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/components/progress-bar/progress.bar.component.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/components/progress-bar/progress.bar.component.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -const { Translator } = window; +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; const ProgressBarComponent = (props) => { + const Translator = getTranslator(); const message = Translator.trans(/*@Desc("Uploading...")*/ 'upload.progress_bar.uploading', {}, 'ibexa_multi_file_upload'); return ( diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.item.component.js b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.item.component.js index d43345308e..7c67b007a6 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.item.component.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.item.component.js @@ -1,12 +1,13 @@ -import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import { getContentTypeIconUrl } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/content.type.helper'; +import { getRestInfo, getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; -import ProgressBarComponent from '../progress-bar/progress.bar.component'; -import { fileSizeToString } from '../../helpers/text.helper'; import { createCssClassNames } from '../../../common/helpers/css.class.names'; import Icon from '../../../common/icon/icon'; - -const { Translator, ibexa } = window; +import { fileSizeToString } from '../../helpers/text.helper'; +import ProgressBarComponent from '../progress-bar/progress.bar.component'; export default class UploadItemComponent extends Component { constructor(props) { @@ -24,7 +25,10 @@ export default class UploadItemComponent extends Component { this.handleLoadStart = this.handleLoadStart.bind(this); this.handleFileDeleted = this.handleFileDeleted.bind(this); this.abortUploading = this.abortUploading.bind(this); + this.initPublishFile = this.initPublishFile.bind(this); this.deleteFile = this.deleteFile.bind(this); + this.handleContentError = this.handleContentError.bind(this); + this.handleFileDeletedError = this.handleFileDeletedError.bind(this); this.contentInfoInput = null; this.contentVersionInfoInput = null; this.contentVersionNoInput = null; @@ -32,28 +36,27 @@ export default class UploadItemComponent extends Component { this.state = { uploading: false, uploaded: props.isUploaded, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, aborted: false, - failed: false, + failed: props.isFailed, deleted: false, progress: 0, xhr: null, - struct: props.data.struct || null, - totalSize: fileSizeToString(props.data.file.size), + struct: props.item.struct || null, + totalSize: fileSizeToString(props.item.file.size), uploadedSize: '0', + errorMsgs: props.item.errorMsgs || [], + isMultipleErrosExpanded: false, }; } componentDidMount() { const { - data, + item, adminUiConfig, parentInfo, createFileStruct, isUploaded, + isFailed, checkCanUpload, contentCreatePermissionsConfig, currentLanguage, @@ -64,7 +67,7 @@ export default class UploadItemComponent extends Component { this.contentVersionNoInput = window.document.querySelector('#form_subitems_content_edit_version_info_version_no'); this.contentEditBtn = window.document.querySelector('#form_subitems_content_edit_create'); - if (isUploaded) { + if (isUploaded || isFailed) { return; } @@ -78,10 +81,9 @@ export default class UploadItemComponent extends Component { contentTypeNotAllowedCallback: this.handleContentTypeNotAllowed, }; - if (!checkCanUpload(data.file, parentInfo, config, callbacks)) { + if (!checkCanUpload(item.file, parentInfo, config, callbacks)) { this.setState(() => ({ uploading: false, - disallowed: true, uploaded: false, aborted: false, failed: true, @@ -90,24 +92,18 @@ export default class UploadItemComponent extends Component { return; } - createFileStruct(data.file, { + const createFileStructParams = { parentInfo, config: adminUiConfig, languageCode: currentLanguage, - }).then(this.initPublishFile.bind(this, adminUiConfig)); + }; + + createFileStruct(item.file, createFileStructParams, this.handleContentError).then(this.initPublishFile); } - /** - * Initializes file-based content publishing - * - * @method initPublishFile - * @param {Object} restInfo config object containing token and siteaccess properties - * @param {Object} struct - * @memberof UploadItemComponent - */ - initPublishFile({ token, siteaccess }, struct) { + initPublishFile(struct) { this.props.publishFile( - { struct, token, siteaccess }, + struct, { upload: { onabort: this.handleUploadAbort, @@ -119,74 +115,70 @@ export default class UploadItemComponent extends Component { onerror: this.handleUploadError, }, this.handleUploadEnd, + this.handleContentError, ); } - /** - * Handles the case when a file cannot be upload because of file type - * - * @method handleFileTypeNotAllowed - * @memberof UploadItemComponent - */ - handleFileTypeNotAllowed() { - this.setState(() => ({ - uploading: false, - disallowed: true, - disallowedType: true, - disallowedSize: false, - disallowedContentType: false, - uploaded: false, - aborted: false, + handleFileDeletedError(errorMsg) { + this.setState((prevState) => ({ failed: true, + deleted: false, + errorMsgs: [...prevState.errorMsgs, errorMsg], })); } - /** - * Handles the case when a file cannot be upload because of file size - * - * @method handleFileSizeNotAllowed - * @memberof UploadItemComponent - */ - handleFileSizeNotAllowed() { - this.setState(() => ({ - uploading: false, - disallowed: true, - disallowedType: false, - disallowedSize: true, - disallowedContentType: false, - uploaded: false, - aborted: false, - failed: true, - })); + handleContentError(errorMsg) { + this.setState( + (prevState) => ({ + failed: true, + errorMsgs: [...prevState.errorMsgs, errorMsg], + }), + () => this.props.onCreateError({ ...this.props.item, errorMsgs: this.state.errorMsgs }), + ); } - handleContentTypeNotAllowed() { - this.setState(() => ({ - uploading: false, - disallowed: true, - disallowedType: false, - disallowedSize: false, - disallowedContentType: true, - uploaded: false, - aborted: false, - failed: true, - })); + handleFileTypeNotAllowed(errorMsg) { + this.setState( + (prevState) => ({ + uploading: false, + uploaded: false, + aborted: false, + failed: true, + errorMsgs: [...prevState.errorMsgs, errorMsg], + }), + () => this.props.onCreateError({ ...this.props.item, errorMsgs: this.state.errorMsgs }), + ); + } + + handleFileSizeNotAllowed(errorMsg) { + this.setState( + (prevState) => ({ + uploading: false, + uploaded: false, + aborted: false, + failed: true, + errorMsgs: [...prevState.errorMsgs, errorMsg], + }), + () => this.props.onCreateError({ ...this.props.item, errorMsgs: this.state.errorMsgs }), + ); + } + + handleContentTypeNotAllowed(errorMsg) { + this.setState( + (prevState) => ({ + uploading: false, + uploaded: false, + aborted: false, + failed: true, + errorMsgs: [...prevState.errorMsgs, errorMsg], + }), + () => this.props.onCreateError({ ...this.props.item, errorMsgs: this.state.errorMsgs }), + ); } - /** - * Handles the upload load start event - * - * @method handleLoadStart - * @param {Event} event - * @memberof UploadItemComponent - */ handleLoadStart(event) { this.setState(() => ({ uploading: true, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, uploaded: false, aborted: false, failed: false, @@ -194,81 +186,39 @@ export default class UploadItemComponent extends Component { })); } - /** - * Handles the upload abort event - * - * @method handleUploadAbort - * @memberof UploadItemComponent - */ handleUploadAbort() { this.setState(() => ({ uploading: false, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, uploaded: false, aborted: true, failed: false, })); } - /** - * Handles the upload error event - * - * @method handleUploadError - * @memberof UploadItemComponent - */ handleUploadError() { this.setState((state) => ({ uploading: false, - disallowed: state.disallowed, - disallowedSize: state.disallowedSize, - disallowedType: state.disallowedType, - disallowedContentType: state.disallowedContentType, uploaded: false, aborted: state.aborted, failed: true, })); } - /** - * Handles the upload load event - * - * @method handleUploadLoad - * @memberof UploadItemComponent - */ handleUploadLoad() { this.setState(() => ({ uploading: false, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, uploaded: true, aborted: false, failed: false, })); } - /** - * Handles the upload progress event - * - * @method handleUploadProgress - * @param {Event} event - * @memberof UploadItemComponent - */ handleUploadProgress(event) { const fraction = event.loaded / event.total; const progress = parseInt(fraction * 100, 10); - this.setState(() => ({ - uploadedSize: fileSizeToString(fraction * parseInt(this.props.data.file.size, 10)), + uploadedSize: fileSizeToString(fraction * parseInt(this.props.item.file.size, 10)), uploading: true, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, uploaded: false, aborted: false, failed: false, @@ -276,12 +226,6 @@ export default class UploadItemComponent extends Component { })); } - /** - * Handles the upload end event - * - * @method handleUploadEnd - * @memberof UploadItemComponent - */ handleUploadEnd() { this.setState( (state) => { @@ -290,196 +234,146 @@ export default class UploadItemComponent extends Component { return { struct, uploading: false, - disallowed: false, - disallowedType: false, - disallowedSize: false, - disallowedContentType: false, uploaded: true, aborted: false, failed: false, }; }, () => { - const { data } = this.props; + const { item } = this.props; - this.props.onAfterUpload({ ...data, struct: this.state.struct }); + this.props.onAfterUpload({ ...item, struct: this.state.struct }); }, ); } - /** - * Aborts file upload - * - * @method abortUploading - * @memberof UploadItemComponent - */ abortUploading() { this.state.xhr.abort(); - this.props.onAfterAbort(this.props.data); + this.props.onAfterAbort(this.props.item); } - /** - * Deletes a file - * - * @method deleteFile - * @memberof UploadItemComponent - */ deleteFile() { - this.setState( - () => ({ deleted: true }), - () => this.props.deleteFile(this.props.adminUiConfig, this.state.struct, this.handleFileDeleted), - ); + const { failed } = this.state; + const { item } = this.props; + + if (failed) { + this.props.removeItemsToUpload([item]); + this.handleFileDeleted(item); + } else { + this.props.deleteFile(this.state.struct, this.handleFileDeleted, this.handleFileDeletedError); + } } - /** - * Handles the file deleted event - * - * @method handleFileDeleted - * @memberof UploadItemComponent - */ handleFileDeleted() { - this.props.onAfterDelete(this.props.data); + this.props.onAfterDelete(this.props.item); + this.setState({ deleted: true }); } - /** - * Returns content type identifier - * based on Content object returned from server after upload - * - * @method getContentTypeIdentifier - * @memberof UploadItemComponent - * @returns {String|null} - */ getContentTypeIdentifier() { - const { contentTypesMap, data } = this.props; + const { contentTypesMap, item } = this.props; - if (!data.struct || !data.struct.Content) { + if (!item.struct || !item.struct.Content) { return null; } - const contentTypeHref = data.struct.Content.ContentType._href; + const contentTypeHref = item.struct.Content.ContentType._href; const contentType = contentTypesMap ? contentTypesMap[contentTypeHref] : null; const contentTypeIdentifier = contentType ? contentType.identifier : null; return contentTypeIdentifier; } - /** - * Renders an icon of a content type - * - * @method renderIcon - * @returns {JSX.Element|null} - */ renderIcon() { + const { failed } = this.state; const contentTypeIdentifier = this.getContentTypeIdentifier(); - if (!contentTypeIdentifier) { + if (!contentTypeIdentifier || failed) { return null; } - const contentTypeIconUrl = ibexa.helpers.contentType.getContentTypeIconUrl(contentTypeIdentifier); + const { instanceUrl } = getRestInfo(); + const contentTypeIconUrl = getContentTypeIconUrl(contentTypeIdentifier); + const [, iconName] = contentTypeIconUrl.split('#'); + const isStandaloneMode = window.origin !== instanceUrl; - return ; + return ( + <> + {isStandaloneMode ? ( + + ) : ( + + )} + + ); } - /** - * Renders a progress bar - * - * @method renderProgressBar - * @memberof UploadItemComponent - * @returns {null|Element} - */ renderProgressBar() { - const { uploaded, aborted, progress, totalSize, uploadedSize, disallowed } = this.state; + const { uploaded, aborted, progress, totalSize, uploadedSize, failed } = this.state; - if (this.props.isUploaded || uploaded || aborted || disallowed) { + if (this.props.isUploaded || uploaded || aborted || failed) { return null; } return ; } - /** - * Renders an error message - * - * @method renderErrorMessage - * @memberof UploadItemComponent - * @returns {null|Element} - */ - renderErrorMessage() { - const { uploaded, aborted, disallowedType, disallowedSize, failed, uploading, disallowedContentType } = this.state; - const isError = !uploaded && !aborted && (disallowedSize || disallowedType || disallowedContentType) && failed && !uploading; - const cannotUploadMessage = Translator.trans( - /*@Desc("Cannot upload file")*/ 'cannot_upload.message', - {}, - 'ibexa_multi_file_upload', - ); - const disallowedTypeMessage = Translator.trans( - /*@Desc("File type is not allowed")*/ 'disallowed_type.message', - {}, - 'ibexa_multi_file_upload', - ); - const disallowedSizeMessage = Translator.trans( - /*@Desc("File size is not allowed")*/ 'disallowed_size.message', - {}, - 'ibexa_multi_file_upload', - ); - const disallowedContentTypeMessage = Translator.trans( - /*@Desc("You do not have permission to create this Content item")*/ 'disallowed_content_type.message', - {}, - 'ibexa_multi_file_upload', - ); - let msg = cannotUploadMessage; - - if (disallowedType) { - msg = disallowedTypeMessage; - } + renderErrorInfo() { + const { failed, errorMsgs } = this.state; - if (disallowedSize) { - msg = disallowedSizeMessage; + if (!failed) { + return null; } - if (disallowedContentType) { - msg = disallowedContentTypeMessage; - } + const Translator = getTranslator(); + const isMultipleErros = errorMsgs.length > 1; + const label = isMultipleErros + ? Translator.trans(/*@Desc("Failed to upload ")*/ 'multierror.label', {}, 'ibexa_multi_file_upload') + : errorMsgs[0]; - return isError ? ( + return (
- - {msg} + + {label} + {isMultipleErros && ( + + )}
- ) : null; + ); } - /** - * Renders an error message - * - * @method renderErrorMessage - * @memberof UploadItemComponent - * @returns {null|Element} - */ renderSuccessMessage() { - const { uploaded, aborted, disallowedSize, disallowedType, failed, uploading } = this.state; - const isSuccess = uploaded && !aborted && !(disallowedSize || disallowedType) && !failed && !uploading; + const Translator = getTranslator(); + const { uploaded, aborted, failed, uploading } = this.state; + const isSuccess = uploaded && !aborted && !failed && !uploading; + + if (!isSuccess) { + return; + } + const message = Translator.trans(/*@Desc("100% Uploaded")*/ 'upload.success.message', {}, 'ibexa_multi_file_upload'); - return isSuccess ? ( + return (
- + {message}
- ) : null; + ); } - /** - * Renders an abort upload button - * - * @method renderAbortBtn - * @memberof UploadItemComponent - * @returns {null|Element} - */ renderAbortBtn() { - const { uploaded, aborted, disallowedSize, disallowedType, failed, uploading } = this.state; - const canAbort = !uploaded && !aborted && !disallowedSize && !disallowedType && !failed && uploading; + const Translator = getTranslator(); + const { uploaded, aborted, failed, uploading } = this.state; + const canAbort = !uploaded && !aborted && !failed && uploading; if (!canAbort) { return null; @@ -494,18 +388,11 @@ export default class UploadItemComponent extends Component { title={label} tabIndex="-1" > - +
); } - /** - * Handles the edit button click event. Fills in the hidden form to redirect a user to a correct content edit location. - * - * @method handleEditBtnClick - * @memberof UploadItemComponent - * @param {Event} event - */ handleEditBtnClick(event) { event.preventDefault(); @@ -518,22 +405,19 @@ export default class UploadItemComponent extends Component { this.contentInfoInput.value = contentId; this.contentVersionInfoInput.value = contentId; this.contentVersionNoInput.value = versionNo; + window.document.querySelector(`#form_subitems_content_edit_language_${languageCode}`).checked = true; + this.contentEditBtn.click(); } - /** - * Renders an edit content button - * - * @method renderEditBtn - * @memberof UploadItemComponent - * @returns {null|Element} - */ renderEditBtn() { - const { uploaded, aborted, disallowedSize, disallowedType, failed, uploading } = this.state; - const canEdit = this.props.isUploaded || (uploaded && !aborted && !(disallowedSize || disallowedType) && !failed && !uploading); + const Translator = getTranslator(); + const { instanceUrl } = getRestInfo(); + const { uploaded, aborted, failed, uploading } = this.state; + const canEdit = (this.props.isUploaded || (uploaded && !aborted && !uploading)) && !failed; - if (!canEdit) { + if (!canEdit || window.origin !== instanceUrl) { return null; } @@ -546,26 +430,20 @@ export default class UploadItemComponent extends Component { onClick={this.handleEditBtnClick} tabIndex="-1" > - + ); } - /** - * Renders an delete content button - * - * @method renderDeleteBtn - * @memberof UploadItemComponent - * @returns {null|Element} - */ renderDeleteBtn() { - const { uploaded, aborted, disallowedSize, disallowedType, failed, uploading } = this.state; - const canDelete = this.props.isUploaded || (uploaded && !aborted && !(disallowedSize || disallowedType) && !failed && !uploading); + const { uploaded, aborted, failed, uploading } = this.state; + const canDelete = this.props.isUploaded || (uploaded && !aborted && !uploading) || failed; if (!canDelete) { return null; } + const Translator = getTranslator(); const label = Translator.trans(/*@Desc("Delete")*/ 'delete.label', {}, 'ibexa_multi_file_upload'); return ( @@ -575,18 +453,18 @@ export default class UploadItemComponent extends Component { title={label} tabIndex="-1" > - + ); } render() { - const { uploaded, aborted, disallowedType, disallowedSize, failed, uploading, disallowedContentType, deleted, totalSize } = - this.state; - const isError = !uploaded && !aborted && (disallowedSize || disallowedType || disallowedContentType) && failed && !uploading; + const { failed, deleted, totalSize, errorMsgs, isMultipleErrosExpanded } = this.state; + const isMultipleErros = errorMsgs.length > 1; const wrapperClassName = createCssClassNames({ 'c-upload-list-item': true, - 'c-upload-list-item--errored': isError, + 'c-upload-list-item--errored': failed, + 'c-upload-list-item--expanded-multiple-errors': isMultipleErrosExpanded, }); if (deleted) { @@ -595,28 +473,37 @@ export default class UploadItemComponent extends Component { return (
-
{!isError && this.renderIcon()}
+
{this.renderIcon()}
-
{this.props.data.file.name}
-
{!isError && uploaded ? totalSize : ''}
+
{this.props.item.file.name}
+
{totalSize}
- {this.renderErrorMessage()} - {!isError && this.renderSuccessMessage()} - {!isError && this.renderProgressBar()} + {this.renderErrorInfo()} + {this.renderSuccessMessage()} + {this.renderProgressBar()}
- {!isError && this.renderAbortBtn()} - {!isError && this.renderEditBtn()} - {!isError && this.renderDeleteBtn()} + {this.renderAbortBtn()} + {this.renderEditBtn()} + {this.renderDeleteBtn()}
+ {isMultipleErros && ( +
    + {errorMsgs.map((errorMsg) => ( +
  • + {errorMsg} +
  • + ))} +
+ )}
); } } UploadItemComponent.propTypes = { - data: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, onAfterUpload: PropTypes.func.isRequired, onAfterAbort: PropTypes.func.isRequired, onAfterDelete: PropTypes.func.isRequired, @@ -644,10 +531,18 @@ UploadItemComponent.propTypes = { contentTypesMap: PropTypes.object.isRequired, currentLanguage: PropTypes.string, isUploaded: PropTypes.bool, + isFailed: PropTypes.bool, + removeItemsToUpload: PropTypes.func, + onCreateError: PropTypes.func, + errorMsgs: PropTypes.array, }; UploadItemComponent.defaultProps = { isUploaded: false, + isFailed: false, currentLanguage: '', contentCreatePermissionsConfig: {}, + removeItemsToUpload: () => {}, + onCreateError: () => {}, + errorMsgs: [], }; diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.list.component.js b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.list.component.js index dcf242f80e..bc5218f603 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.list.component.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-list/upload.list.component.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { createCssClassNames } from '../../../common/helpers/css.class.names'; import UploadItemComponent from './upload.item.component'; export default class UploadListComponent extends Component { @@ -9,6 +10,7 @@ export default class UploadListComponent extends Component { this.state = { items: [], + erroredItems: [], }; } @@ -16,13 +18,6 @@ export default class UploadListComponent extends Component { this.props.onAfterUpload(this.state.items); } - /** - * Handles after file upload event - * - * @method handleAfterUpload - * @param {Object} item - * @memberof UploadListComponent - */ handleAfterUpload(item) { this.props.removeItemsToUpload([item]); this.setState((state) => ({ @@ -30,13 +25,6 @@ export default class UploadListComponent extends Component { })); } - /** - * Handles after file upload abort event - * - * @method handleAfterAbort - * @param {Object} item - * @memberof UploadListComponent - */ handleAfterAbort(item) { this.props.removeItemsToUpload([item]); this.setState((state) => { @@ -46,29 +34,39 @@ export default class UploadListComponent extends Component { }); } - /** - * Handles after file delete event - * - * @method handleAfterDelete - * @param {Object} item - * @memberof UploadListComponent - */ handleAfterDelete(item) { - this.setState((state) => { - const items = state.items.filter((data) => data.id !== item.id); + this.setState( + (state) => { + const items = state.items.filter((data) => data.id !== item.id); + const erroredItems = state.erroredItems.filter((data) => data.id !== item.id); + + return { uploaded: items.length, items, erroredItems }; + }, + () => this.props.onAfterDelete(item), + ); + } - return { uploaded: items.length, items }; + handleCreateError(item) { + this.props.removeItemsToUpload([item]); + this.setState((state) => ({ + erroredItems: [...state.erroredItems, item], + })); + } + + removeErroredItems(items) { + const itemsIds = items.map((item) => item.id); + + this.setState((prevState) => { + const erroredItems = prevState.erroredItems.filter((stateItem) => !itemsIds.includes(stateItem.id)); + + if (erroredItems.length !== prevState.erroredItems.length) { + return { + erroredItems, + }; + } }); } - /** - * Renders an item to upload - * - * @method renderItemToUpload - * @param {Object} item - * @memberof UploadListComponent - * @returns {Element} - */ renderItemToUpload(item) { return this.renderItem(item, { isUploaded: false, @@ -76,18 +74,12 @@ export default class UploadListComponent extends Component { publishFile: this.props.publishFile, onAfterAbort: this.handleAfterAbort.bind(this), onAfterUpload: this.handleAfterUpload.bind(this), + onCreateError: this.handleCreateError.bind(this), checkCanUpload: this.props.checkCanUpload, + removeItemsToUpload: this.props.removeItemsToUpload, }); } - /** - * Renders an uploaded item - * - * @method renderUploadedItem - * @param {Object} item - * @memberof UploadListComponent - * @returns {Element} - */ renderUploadedItem(item) { return this.renderItem(item, { isUploaded: true, @@ -96,20 +88,19 @@ export default class UploadListComponent extends Component { }); } - /** - * Renders an item - * - * @method renderItem - * @param {Object} item - * @param {Object} customAttrs component's custom attrs - * @memberof UploadListComponent - * @returns {Element} - */ + renderErroredItem(item) { + return this.renderItem(item, { + isFailed: true, + deleteFile: this.props.deleteFile, + onAfterDelete: this.handleAfterDelete.bind(this), + }); + } + renderItem(item, customAttrs) { const { adminUiConfig, parentInfo, contentCreatePermissionsConfig, contentTypesMap, currentLanguage } = this.props; const attrs = { + item, key: item.id, - data: item, adminUiConfig, parentInfo, contentCreatePermissionsConfig, @@ -123,12 +114,16 @@ export default class UploadListComponent extends Component { render() { const { itemsToUpload } = this.props; - const { items } = this.state; + const { items, erroredItems } = this.state; + const uploadListClassName = createCssClassNames({ + 'c-upload-list': true, + }); return ( -
+
{itemsToUpload.map(this.renderItemToUpload.bind(this))} + {erroredItems.map(this.renderErroredItem.bind(this))} {items.map(this.renderUploadedItem.bind(this))}
@@ -163,9 +158,11 @@ UploadListComponent.propTypes = { contentTypesMap: PropTypes.object.isRequired, currentLanguage: PropTypes.string, removeItemsToUpload: PropTypes.func.isRequired, + onAfterDelete: PropTypes.func, }; UploadListComponent.defaultProps = { itemsToUpload: [], currentLanguage: '', + onAfterDelete: () => {}, }; diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-popup/upload.popup.component.js b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-popup/upload.popup.component.js index b927cb26ca..ec6a6bfc83 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-popup/upload.popup.component.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/components/upload-popup/upload.popup.component.js @@ -1,12 +1,14 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getTranslator, getRootNode } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { parse as parseTooltips } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/tooltips.helper'; +import { getContentTypeName } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/content.type.helper'; + import TooltipPopup from '../../../common/tooltip-popup/tooltip.popup.component'; import DropAreaComponent from '../drop-area/drop.area.component'; import UploadListComponent from '../upload-list/upload.list.component'; -const { Translator } = window; - const CLASS_SCROLL_DISABLED = 'ibexa-scroll-disabled'; export default class UploadPopupModule extends Component { @@ -14,32 +16,64 @@ export default class UploadPopupModule extends Component { super(props); this.refTooltip = React.createRef(); + this.rootNode = getRootNode(); } componentDidMount() { - window.document.body.classList.add(CLASS_SCROLL_DISABLED); - window.ibexa.helpers.tooltips.parse(this.refTooltip.current); + this.rootNode.classList.add(CLASS_SCROLL_DISABLED); + parseTooltips(this.refTooltip.current); } componentWillUnmount() { window.document.body.classList.remove(CLASS_SCROLL_DISABLED); } + getContentTypesMaxFileSize() { + const { locationMappings, defaultMappings, maxFileSize: defaultMaxFileSize } = this.props.adminUiConfig.multiFileUpload; + const mappings = locationMappings.length ? locationMappings : defaultMappings; + const contentTypeIdentifiers = Object.keys(this.props.contentCreatePermissionsConfig); + + return contentTypeIdentifiers.reduce((maxFileSizes, contentTypeIdentifier) => { + const contentTypeName = getContentTypeName(contentTypeIdentifier); + const contentTypeMapping = mappings.find((item) => item.contentTypeIdentifier === contentTypeIdentifier); + + maxFileSizes.push({ + name: contentTypeName, + maxFileSize: contentTypeMapping.maxFileSize || defaultMaxFileSize, + }); + + return maxFileSizes; + }, []); + } + render() { - const tooltipAttrs = this.props; + const Translator = getTranslator(); + const label = Translator.trans(/*@Desc("Upload")*/ 'upload_popup.label', {}, 'ibexa_multi_file_upload'); + const tooltipAttrs = { + ...this.props, + title: Translator.trans(/*@Desc("Multi-file upload")*/ 'upload_popup.title', {}, 'ibexa_multi_file_upload'), + confirmLabel: Translator.trans(/*@Desc("Confirm and close")*/ 'upload_popup.close_label', {}, 'ibexa_multi_file_upload'), + closeLabel: Translator.trans(/*@Desc("Cancel pending upload")*/ 'upload_popup.confirm_label', {}, 'ibexa_multi_file_upload'), + confirmBtnAttrs: { + disabled: this.props.itemsToUpload.length, + }, + closeBtnAttrs: { + disabled: !this.props.itemsToUpload.length, + }, + }; const listAttrs = { ...tooltipAttrs, itemsToUpload: this.props.itemsToUpload, removeItemsToUpload: this.props.removeItemsToUpload, }; - const title = Translator.trans(/*@Desc("Multi-file upload")*/ 'upload_popup.close', {}, 'ibexa_multi_file_upload'); return (
- + +
{label}
@@ -80,10 +114,12 @@ UploadPopupModule.propTypes = { currentLanguage: PropTypes.string, addItemsToUpload: PropTypes.func.isRequired, removeItemsToUpload: PropTypes.func.isRequired, + contentCreatePermissionsConfig: PropTypes.object, }; UploadPopupModule.defaultProps = { visible: true, itemsToUpload: [], currentLanguage: '', + contentCreatePermissionsConfig: {}, }; diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/config.loader.js b/src/bundle/ui-dev/src/modules/multi-file-upload/config.loader.js new file mode 100644 index 0000000000..687e350ecd --- /dev/null +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/config.loader.js @@ -0,0 +1,5 @@ +import MultiFileUploadModule from './multi.file.upload.module'; + +(function (ibexa) { + ibexa.addConfig('modules.MultiFileUpload', MultiFileUploadModule); +})(window.ibexa); diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/multi.file.upload.module.js b/src/bundle/ui-dev/src/modules/multi-file-upload/multi.file.upload.module.js index b9a8a2229d..4c3d6fa9f1 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/multi.file.upload.module.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/multi.file.upload.module.js @@ -1,19 +1,25 @@ import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; +import { getTranslator, getRootNode } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; + import UploadPopupComponent from './components/upload-popup/upload.popup.component'; import { createFileStruct, publishFile, deleteFile, checkCanUpload } from './services/multi.file.upload.service'; import Icon from '../common/icon/icon'; +import { createCssClassNames } from '../common/helpers/css.class.names'; -const { Translator, ibexa, document } = window; - +export const MODULES_CAN_TRIGGER_MFU_LIST = { + udw: 'UniversalDiscoveryModule', + subitems: 'SubItemsModule', +}; export default class MultiFileUploadModule extends Component { constructor(props) { super(props); let popupVisible = true; + this.configRootNode = getRootNode(); this._itemsUploaded = []; if (!props.itemsToUpload || !props.itemsToUpload.length) { @@ -22,8 +28,10 @@ export default class MultiFileUploadModule extends Component { this.handleDropOnWindow = this.handleDropOnWindow.bind(this); this.handleAfterUpload = this.handleAfterUpload.bind(this); + this.handleAfterDelete = this.handleAfterDelete.bind(this); this.showUploadPopup = this.showUploadPopup.bind(this); this.hidePopup = this.hidePopup.bind(this); + this.confirmPopup = this.confirmPopup.bind(this); this.processUploadedFiles = this.processUploadedFiles.bind(this); this.setUdwStateOpened = this.setUdwStateOpened.bind(this); this.setUdwStateClosed = this.setUdwStateClosed.bind(this); @@ -42,8 +50,8 @@ export default class MultiFileUploadModule extends Component { componentDidMount() { this.manageDropEvent(); - window.document.body.addEventListener('ibexa-udw-opened', this.setUdwStateOpened, false); - window.document.body.addEventListener('ibexa-udw-closed', this.setUdwStateClosed, false); + this.configRootNode.addEventListener('ibexa-udw-opened', this.setUdwStateOpened, false); + this.configRootNode.addEventListener('ibexa-udw-closed', this.setUdwStateClosed, false); } componentDidUpdate() { @@ -51,87 +59,49 @@ export default class MultiFileUploadModule extends Component { } componentWillUnmount() { - window.document.body.removeEventListener('ibexa-udw-opened', this.setUdwStateOpened, false); - window.document.body.removeEventListener('ibexa-udw-closed', this.setUdwStateClosed, false); + this.configRootNode.removeEventListener('ibexa-udw-opened', this.setUdwStateOpened, false); + this.configRootNode.removeEventListener('ibexa-udw-closed', this.setUdwStateClosed, false); } - /** - * Set udw state as open - * - * @method setUdwStateOpened - * @memberof MultiFileUploadModule - */ setUdwStateOpened() { this.setState({ udwOpened: true }); } - /** - * Set udw state as closed - * - * @method setUdwStateClosed - * @memberof MultiFileUploadModule - */ setUdwStateClosed() { this.setState({ udwOpened: false }); } - /** - * Attaches `drop` and `dragover` events handlers on window - * - * @method manageDropEvent - * @memberof MultiFileUploadModule - */ manageDropEvent() { const { uploadDisabled, popupVisible, itemsToUpload } = this.state; if (!uploadDisabled && !popupVisible && !itemsToUpload.length) { - window.addEventListener('drop', this.handleDropOnWindow, false); - window.addEventListener('dragover', this.preventDefaultAction, false); + this.configRootNode.addEventListener('drop', this.handleDropOnWindow, false); + this.configRootNode.addEventListener('dragover', this.preventDefaultAction, false); } } - /** - * Hides multi file upload popup - * - * @method hidePopup - * @memberof MultiFileUploadModule - */ hidePopup() { - this.setState((state) => ({ ...state, popupVisible: false })); - + this.setState((state) => ({ ...state, popupVisible: false, allowDropOnWindow: true })); this.props.onPopupClose(this._itemsUploaded); } - /** - * Displays multi file upload popup - * - * @method showUploadPopup - * @memberof MultiFileUploadModule - */ + confirmPopup() { + this.setState((state) => ({ ...state, popupVisible: false, allowDropOnWindow: true })); + this.props.onPopupConfirm(this._itemsUploaded); + } + showUploadPopup() { this.setState((state) => ({ ...state, popupVisible: true, itemsToUpload: [] })); } - /** - * Keeps information about uploaded files. - * We want to avoid component rerendering so it's stored in an object instance property. - * - * @method handleAfterUpload - * @param {Array} itemsUploaded - * @memberof MultiFileUploadModule - */ handleAfterUpload(itemsUploaded) { this._itemsUploaded = [...this._itemsUploaded, ...itemsUploaded]; } - /** - * Handles dropping on window. - * When file/files are dropped onto window the `drop` and `dragover` event handlers are removed. - * - * @method handleDropOnWindow - * @param {Event} event - * @memberof MultiFileUploadModule - */ + handleAfterDelete(deletedItem) { + this._itemsUploaded = this._itemsUploaded.filter((data) => data.id !== deletedItem.id); + } + handleDropOnWindow(event) { this.preventDefaultAction(event); @@ -143,20 +113,12 @@ export default class MultiFileUploadModule extends Component { return; } - window.removeEventListener('drop', this.handleDropOnWindow, false); - window.removeEventListener('dragover', this.preventDefaultAction, false); + this.configRootNode.removeEventListener('drop', this.handleDropOnWindow, false); + this.configRootNode.removeEventListener('dragover', this.preventDefaultAction, false); this.setState((state) => ({ ...state, itemsToUpload, popupVisible: true, allowDropOnWindow: false })); } - /** - * Extracts information about dropped files - * - * @method extractDroppedFilesList - * @param {Event} event - * @returns {undefined|Array} - * @memberof MultiFileUploadModule - */ extractDroppedFilesList(event) { let list; @@ -169,14 +131,6 @@ export default class MultiFileUploadModule extends Component { return list; } - /** - * Processes uploaded files and generates an unique file id - * - * @method processUploadedFiles - * @param {Event} event - * @returns {Array} - * @memberof MultiFileUploadModule - */ processUploadedFiles(event) { const list = this.extractDroppedFilesList(event); @@ -186,37 +140,28 @@ export default class MultiFileUploadModule extends Component { })); } - /** - * Prevents default event actions - * - * @method preventDefaultAction - * @param {Event} event - * @memberof MultiFileUploadModule - */ preventDefaultAction(event) { event.preventDefault(); event.stopPropagation(); } - /** - * Renders multi file upload button, - * that allows to open multi file upload popup. - * - * @method renderBtn - * @returns {null|Element} - * @memberof MultiFileUploadModule - */ renderBtn() { if (!this.props.withUploadButton) { return null; } + const Translator = getTranslator(); const { uploadDisabled } = this.state; const label = Translator.trans(/*@Desc("Upload")*/ 'multi_file_upload_open_btn.label', {}, 'ibexa_multi_file_upload'); - + const isTriggerBySubitems = this.props.triggeredBy === MODULES_CAN_TRIGGER_MFU_LIST['subitems']; + const buttonClassName = createCssClassNames({ + 'ibexa-btn btn': true, + 'ibexa-btn--secondary ibexa-btn--small': !isTriggerBySubitems, + 'ibexa-btn--ghost': isTriggerBySubitems, + }); return ( - ); } @@ -247,31 +192,35 @@ export default class MultiFileUploadModule extends Component { }); } - /** - * Renders a popup - * - * @method renderPopup - * @returns {null|Element} - * @memberof MultiFileUploadModule - */ renderPopup() { if (!this.state.popupVisible) { return null; } + const Translator = getTranslator(); + const subtitle = Translator.trans( + /*@Desc("Under %name%")*/ 'multi_file_upload_popup.subtitle', + { name: this.props.parentInfo.name }, + 'ibexa_multi_file_upload', + ); + const attrs = { ...this.props, + subtitle: this.props.parentInfo.name ? subtitle : '', visible: true, onClose: this.hidePopup, + onConfirm: this.confirmPopup, itemsToUpload: this.state.itemsToUpload, onAfterUpload: this.handleAfterUpload, + onAfterDelete: this.handleAfterDelete, preventDefaultAction: this.preventDefaultAction, processUploadedFiles: this.processUploadedFiles, addItemsToUpload: this.addItemsToUpload, removeItemsToUpload: this.removeItemsToUpload, + contentCreatePermissionsConfig: this.props.contentCreatePermissionsConfig, }; - return ReactDOM.createPortal(, document.body); + return createPortal(, this.configRootNode); } render() { @@ -284,8 +233,6 @@ export default class MultiFileUploadModule extends Component { } } -ibexa.addConfig('modules.MultiFileUpload', MultiFileUploadModule); - MultiFileUploadModule.propTypes = { adminUiConfig: PropTypes.shape({ multiFileUpload: PropTypes.shape({ @@ -294,25 +241,29 @@ MultiFileUploadModule.propTypes = { locationMappings: PropTypes.arrayOf(PropTypes.object).isRequired, maxFileSize: PropTypes.number.isRequired, }).isRequired, - token: PropTypes.string.isRequired, - siteaccess: PropTypes.string.isRequired, + token: PropTypes.string, + siteaccess: PropTypes.string, + accessToken: PropTypes.string, }).isRequired, parentInfo: PropTypes.shape({ contentTypeIdentifier: PropTypes.string.isRequired, contentTypeId: PropTypes.number.isRequired, locationPath: PropTypes.string.isRequired, language: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, }).isRequired, checkCanUpload: PropTypes.func, createFileStruct: PropTypes.func, deleteFile: PropTypes.func, onPopupClose: PropTypes.func, + onPopupConfirm: PropTypes.func, publishFile: PropTypes.func, itemsToUpload: PropTypes.array, withUploadButton: PropTypes.bool, contentCreatePermissionsConfig: PropTypes.object, contentTypesMap: PropTypes.object.isRequired, currentLanguage: PropTypes.string, + triggeredBy: PropTypes.string, }; MultiFileUploadModule.defaultProps = { @@ -320,9 +271,11 @@ MultiFileUploadModule.defaultProps = { createFileStruct, deleteFile, onPopupClose: () => {}, + onPopupConfirm: () => {}, publishFile, itemsToUpload: [], withUploadButton: true, currentLanguage: '', contentCreatePermissionsConfig: {}, + triggeredBy: MODULES_CAN_TRIGGER_MFU_LIST['subitems'], }; diff --git a/src/bundle/ui-dev/src/modules/multi-file-upload/services/multi.file.upload.service.js b/src/bundle/ui-dev/src/modules/multi-file-upload/services/multi.file.upload.service.js index 8312f52773..5699bc48d2 100644 --- a/src/bundle/ui-dev/src/modules/multi-file-upload/services/multi.file.upload.service.js +++ b/src/bundle/ui-dev/src/modules/multi-file-upload/services/multi.file.upload.service.js @@ -1,13 +1,6 @@ -const { Translator } = window; - -/** - * Handles ready state change of request - * - * @function handleOnReadyStateChange - * @param {XMLHttpRequest} xhr - * @param {Function} onSuccess - * @param {Function} onError - */ +import { getTranslator, getRestInfo } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { getRequestHeaders, getRequestMode } from '../../../../../Resources/public/js/scripts/helpers/request.helper'; + const handleOnReadyStateChange = (xhr, onSuccess, onError) => { if (xhr.readyState !== 4) { return; @@ -26,14 +19,6 @@ const handleOnReadyStateChange = (xhr, onSuccess, onError) => { onSuccess(JSON.parse(xhr.response)); }; - -/** - * Handles request response - * - * @function handleRequestResponse - * @param {Response} response - * @returns {String|Response} - */ const handleRequestResponse = (response) => { if (!response.ok) { throw Error(response.text()); @@ -41,140 +26,77 @@ const handleRequestResponse = (response) => { return response; }; - -/** - * Read file handler - * - * @function readFile - * @param {File} file - * @param {Function} resolve - * @param {Function} reject - */ const readFile = function (file, resolve, reject) { this.addEventListener('load', () => resolve({ fileReader: this, file }), false); this.addEventListener('error', () => reject(), false); this.readAsDataURL(file); }; - -/** - * Finds a content type mapping based on a file type - * - * @function findFileTypeMapping - * @param {Array} mappings - * @param {File} file - * @returns {Object|undefined} - */ const findFileTypeMapping = (mappings, file) => mappings.find((item) => item.mimeTypes.find((type) => type === file.type)); - -/** - * Checks if file's MIME Type is allowed - * - * @function isMimeTypeAllowed - * @param {Array} mappings - * @param {File} file - * @returns {Boolean} - */ const isMimeTypeAllowed = (mappings, file) => !!findFileTypeMapping(mappings, file); -/** - * Checks if file type is allowed - * - * @function checkFileTypeAllowed - * @param {File} file - * @param {Object} locationMapping - * @returns {Boolean} - */ const checkFileTypeAllowed = (file, locationMapping) => (!locationMapping ? true : isMimeTypeAllowed(locationMapping.mappings, file)); -/** - * Detects a content type for a given file - * - * @function detectContentTypeMapping - * @param {File} file - * @param {Object} parentInfo - * @param {Object} config - * @returns {Object} detected content type config - */ const detectContentTypeMapping = (file, parentInfo, config) => { const locationMapping = config.locationMappings.find((item) => item.contentTypeIdentifier === parentInfo.contentTypeIdentifier); const mappings = locationMapping ? locationMapping.mappings : config.defaultMappings; return findFileTypeMapping(mappings, file) || config.fallbackContentType; }; - -/** - * Gets content type identifier - * - * @function getContentTypeByIdentifier - * @param {Object} params params object containing token and siteaccess properties - * @param {String} identifier content type identifier - * @returns {Promise} - */ -const getContentTypeByIdentifier = ({ token, siteaccess }, identifier) => { - const request = new Request(`/api/ibexa/v2/content/types?identifier=${identifier}`, { +const getContentTypeByIdentifier = (identifier) => { + const { instanceUrl, token, siteaccess, accessToken } = getRestInfo(); + const request = new Request(`${instanceUrl}/api/ibexa/v2/content/types?identifier=${identifier}`, { method: 'GET', - headers: { - Accept: 'application/vnd.ibexa.api.ContentTypeInfoList+json', - 'X-Siteaccess': siteaccess, - 'X-CSRF-Token': token, - }, + headers: getRequestHeaders({ + token, + siteaccess, + accessToken, + extraHeaders: { + Accept: 'application/vnd.ibexa.api.ContentTypeInfoList+json', + }, + }), credentials: 'same-origin', - mode: 'cors', + mode: getRequestMode({ instanceUrl }), }); return fetch(request).then(handleRequestResponse); }; - -/** - * Get content type field definition by identifier - * - * @function getFieldDefinitionByIdentifier - * @param {Object} params params object containing token and siteaccess properties - * @param {Int} contentTypeId content type id - * @param {String} fieldIdentifier content type field identifier - * @returns {Promise} - */ -const getFieldDefinitionByIdentifier = ({ token, siteaccess }, contentTypeId, fieldIdentifier) => { - const request = new Request(`/api/ibexa/v2/content/types/${contentTypeId}/fieldDefinition/${fieldIdentifier}`, { +const getFieldDefinitionByIdentifier = (contentTypeId, fieldIdentifier) => { + const { instanceUrl, token, siteaccess, accessToken } = getRestInfo(); + const request = new Request(`${instanceUrl}/api/ibexa/v2/content/types/${contentTypeId}/fieldDefinition/${fieldIdentifier}`, { method: 'GET', - headers: { - Accept: 'application/vnd.ibexa.api.FieldDefinition+json', - 'X-Siteaccess': siteaccess, - 'X-CSRF-Token': token, - }, + headers: getRequestHeaders({ + token, + siteaccess, + accessToken, + extraHeaders: { + Accept: 'application/vnd.ibexa.api.FieldDefinition+json', + }, + }), credentials: 'same-origin', - mode: 'cors', + mode: getRequestMode({ instanceUrl }), }); return fetch(request).then(handleRequestResponse); }; - -/** - * Prepares a ContentCreate struct based on an uploaded file type - * - * @function prepareStruct - * @param {Object} params params object containing parentInfo and config properties - * @param {Object} data file data containing File object and FileReader object - * @returns {Promise} - */ -const prepareStruct = ({ parentInfo, config, languageCode }, data) => { +const prepareStruct = ({ parentInfo, config, languageCode }, data, contentErrorCallback) => { + const Translator = getTranslator(); let parentLocation = `/api/ibexa/v2/content/locations${parentInfo.locationPath}`; parentLocation = parentLocation.endsWith('/') ? parentLocation.slice(0, -1) : parentLocation; const mapping = detectContentTypeMapping(data.file, parentInfo, config.multiFileUpload); - return getContentTypeByIdentifier(config, mapping.contentTypeIdentifier) + return getContentTypeByIdentifier(mapping.contentTypeIdentifier) .then((response) => response.json()) - .catch(() => - window.ibexa.helpers.notification.showErrorNotification( + .catch(() => { + contentErrorCallback( Translator.trans( /*@Desc("Cannot get content type by identifier")*/ 'cannot_get_content_type_identifier.message', {}, 'ibexa_multi_file_upload', ), - ), - ) + ); + }) .then((response) => { const fileValue = { fileName: data.file.name, @@ -184,17 +106,17 @@ const prepareStruct = ({ parentInfo, config, languageCode }, data) => { const contentType = response.ContentTypeInfoList.ContentType[0]; const { contentFieldIdentifier } = mapping; - return getFieldDefinitionByIdentifier(config, contentType.id, contentFieldIdentifier) + return getFieldDefinitionByIdentifier(contentType.id, contentFieldIdentifier) .then((parsedResponse) => parsedResponse.json()) - .catch(() => - window.ibexa.helpers.notification.showErrorNotification( + .catch(() => { + contentErrorCallback( Translator.trans( /*@Desc("Cannot get content type by identifier")*/ 'cannot_get_content_type_identifier.message', {}, 'ibexa_multi_file_upload', ), - ), - ) + ); + }) .then((parsedResponse) => { const fieldDefinition = parsedResponse.FieldDefinition; @@ -222,47 +144,42 @@ const prepareStruct = ({ parentInfo, config, languageCode }, data) => { return struct; }) - .catch(() => - window.ibexa.helpers.notification.showErrorNotification( + .catch(() => { + contentErrorCallback( Translator.trans( /*@Desc("Cannot create content structure")*/ 'cannot_create_content_structure.message', {}, 'ibexa_multi_file_upload', ), - ), - ); + ); + }); }) - .catch(() => - window.ibexa.helpers.notification.showErrorNotification( + .catch(() => { + contentErrorCallback( Translator.trans( /*@Desc("Cannot create content structure")*/ 'cannot_create_content_structure.message', {}, 'ibexa_multi_file_upload', ), - ), - ); + ); + }); }; - -/** - * Creates a content draft - * - * @function createDraft - * @param {Object} params params object containing struct, token and siteaccess properties - * @param {Object} requestEventHandlers object containing a list of callbacks - * @returns {Promise} - */ -const createDraft = ({ struct, token, siteaccess }, requestEventHandlers) => { +const createDraft = (struct, requestEventHandlers) => { + const { instanceUrl, token, siteaccess, accessToken } = getRestInfo(); const xhr = new XMLHttpRequest(); const body = JSON.stringify(struct); - const headers = { - Accept: 'application/vnd.ibexa.api.Content+json', - 'Content-Type': 'application/vnd.ibexa.api.ContentCreate+json', - 'X-CSRF-Token': token, - 'X-Siteaccess': siteaccess, - }; + const headers = getRequestHeaders({ + token, + siteaccess, + accessToken, + extraHeaders: { + Accept: 'application/vnd.ibexa.api.Content+json', + 'Content-Type': 'application/vnd.ibexa.api.ContentCreate+json', + }, + }); return new Promise((resolve, reject) => { - xhr.open('POST', '/api/ibexa/v2/content/objects', true); + xhr.open('POST', `${instanceUrl}/api/ibexa/v2/content/objects`, true); xhr.onreadystatechange = handleOnReadyStateChange.bind(null, xhr, resolve, reject); @@ -290,43 +207,28 @@ const createDraft = ({ struct, token, siteaccess }, requestEventHandlers) => { xhr.send(body); }); }; - -/** - * Publishes a content draft - * - * @function publishDraft - * @param {Object} params params object containing token and siteaccess properties - * @param {Object} response object containing created draft struct - * @returns {Promise} - */ -const publishDraft = ({ token, siteaccess }, response) => { - if (!response || !Object.prototype.hasOwnProperty.call(response, 'Content')) { +const publishDraft = (data) => { + if (!data || !Object.prototype.hasOwnProperty.call(data, 'Content')) { return Promise.reject('Cannot publish content based on an uploaded file'); } - const request = new Request(response.Content.CurrentVersion.Version._href, { + const { instanceUrl, token, siteaccess, accessToken } = getRestInfo(); + const request = new Request(`${instanceUrl}${data.Content.CurrentVersion.Version._href}`, { method: 'POST', - headers: { - 'X-Siteaccess': siteaccess, - 'X-CSRF-Token': token, - 'X-HTTP-Method-Override': 'PUBLISH', - }, - mode: 'cors', + headers: getRequestHeaders({ + token, + siteaccess, + accessToken, + extraHeaders: { + 'X-HTTP-Method-Override': 'PUBLISH', + }, + }), + mode: getRequestMode({ instanceUrl }), credentials: 'same-origin', }); return fetch(request).then(handleRequestResponse); }; - -/** - * Checks whether a content based on an uploaded file can be created - * - * @function canCreateContent - * @param {File} file - * @param {Object} parentInfo parent info hash - * @param {Object} config multi file upload config - * @returns {Boolean} - */ const canCreateContent = (file, parentInfo, config) => { if (!Object.prototype.hasOwnProperty.call(config, 'contentCreatePermissionsConfig') || !config.contentCreatePermissionsConfig) { return true; @@ -336,87 +238,90 @@ const canCreateContent = (file, parentInfo, config) => { return config.contentCreatePermissionsConfig[contentTypeConfig.contentTypeIdentifier]; }; +const getMaxFileSize = (file, parentInfo, config) => { + const { maxFileSize: contentMaxFileSize } = detectContentTypeMapping(file, parentInfo, config); + + return contentMaxFileSize ? contentMaxFileSize : config.maxFileSize; +}; -/** - * Checks if a file can be uploaded - * - * @function checkCanUpload - * @param {File} file - * @param {Object} parentInfo parent info hash - * @param {Object} config multi file upload config - * @param {Object} callbacks a list of callbacks - * @returns {Boolean} - */ export const checkCanUpload = (file, parentInfo, config, callbacks) => { + const Translator = getTranslator(); const locationMapping = config.locationMappings.find((item) => item.contentTypeIdentifier === parentInfo.contentTypeIdentifier); + const maxFileSize = getMaxFileSize(file, parentInfo, config); if (!canCreateContent(file, parentInfo, config)) { - callbacks.contentTypeNotAllowedCallback(); + callbacks.contentTypeNotAllowedCallback( + Translator.trans( + /*@Desc("You do not have permission to create this Content item")*/ 'disallowed_content_type.message', + {}, + 'ibexa_multi_file_upload', + ), + ); return false; } - if (!checkFileTypeAllowed(file, locationMapping)) { - callbacks.fileTypeNotAllowedCallback(); + callbacks.fileTypeNotAllowedCallback( + Translator.trans(/*@Desc("File type is not allowed")*/ 'disallowed_type.message', {}, 'ibexa_multi_file_upload'), + ); return false; } - if (file.size > config.maxFileSize) { - callbacks.fileSizeNotAllowedCallback(); + if (file.size > maxFileSize) { + callbacks.fileSizeNotAllowedCallback( + Translator.trans(/*@Desc("File size is not allowed")*/ 'disallowed_size.message', {}, 'ibexa_multi_file_upload'), + ); return false; } return true; }; - -/** - * Creates a ContentCreate struct based on a file - * - * @function createFileStruct - * @param {File} file - * @param {Object} params struct params - * @returns {Promise} - */ -export const createFileStruct = (file, params) => new Promise(readFile.bind(new FileReader(), file)).then(prepareStruct.bind(null, params)); - -/** - * Publishes file - * - * @function publishFile - * @param {Object} data file data - * @param {Object} requestEventHandlers a list of request event handlers - * @param {Function} callback a success callback - */ -export const publishFile = (data, requestEventHandlers, callback) => { - createDraft(data, requestEventHandlers) - .then(publishDraft.bind(null, data)) - .then(callback) - .catch(() => window.ibexa.helpers.notification.showErrorNotification('An error occurred while publishing a file')); +export const createFileStruct = (file, params, contentErrorCallback) => { + return new Promise(readFile.bind(new FileReader(), file)).then((fileData) => prepareStruct(params, fileData, contentErrorCallback)); }; - -/** - * Deletes file - * - * @function deleteFile - * @param {Object} systemInfo system info containing: token and siteaccess info. - * @param {Object} struct Content struct - * @param {Function} callback file deleted callback - */ -export const deleteFile = ({ token, siteaccess }, struct, callback) => { - const request = new Request(struct.Content._href, { +export const publishFile = (data, requestEventHandlers, successCallback, contentErrorCallback) => { + createDraft(data, requestEventHandlers, contentErrorCallback) + .then(publishDraft) + .then(successCallback) + .catch(() => { + const Translator = getTranslator(); + + contentErrorCallback( + Translator.trans( + /*@Desc("An error occurred while publishing a file")*/ 'general.error.message', + {}, + 'ibexa_multi_file_upload', + ), + ); + }); +}; +export const deleteFile = (struct, callback, contentErrorCallback) => { + const { instanceUrl, token, siteaccess, accessToken } = getRestInfo(); + const request = new Request(`${instanceUrl}${struct.Content._href}`, { method: 'DELETE', - headers: { - 'X-Siteaccess': siteaccess, - 'X-CSRF-Token': token, - }, - mode: 'cors', + headers: getRequestHeaders({ + token, + siteaccess, + accessToken, + }), + mode: getRequestMode({ instanceUrl }), credentials: 'same-origin', }); fetch(request) .then(handleRequestResponse) .then(callback) - .catch(() => window.ibexa.helpers.notification.showErrorNotification('An error occurred while deleting a file')); + .catch(() => { + const Translator = getTranslator(); + + contentErrorCallback( + Translator.trans( + /*@Desc("An error occurred while deleting a file")*/ 'delete.error.message', + {}, + 'ibexa_multi_file_upload', + ), + ); + }); }; diff --git a/src/lib/UI/Config/Provider/ContentTypeMappings.php b/src/lib/UI/Config/Provider/ContentTypeMappings.php index f3a03624ec..86cfc89d2a 100644 --- a/src/lib/UI/Config/Provider/ContentTypeMappings.php +++ b/src/lib/UI/Config/Provider/ContentTypeMappings.php @@ -4,40 +4,42 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ - namespace Ibexa\AdminUi\UI\Config\Provider; use Ibexa\Contracts\AdminUi\UI\Config\ProviderInterface; +use Ibexa\Contracts\Core\Repository\ContentTypeService; /** * Class responsible for generating PlatformUI configuration for Multi File Upload functionality. */ class ContentTypeMappings implements ProviderInterface { - /** @var array */ - protected $locationMappings = []; + private ContentTypeService $contentTypeService; + + /** @var array */ + protected array $locationMappings = []; - /** @var array */ - protected $defaultMappings = []; + /** @var array */ + protected array $defaultMappings = []; - /** @var array */ - protected $fallbackContentType = []; + /** @var array */ + protected array $fallbackContentType = []; - /** @var int */ - protected $maxFileSize = 0; + protected int $maxFileSize = 0; /** - * @param array $locationMappings - * @param array $defaultMappings - * @param array $fallbackContentType - * @param int $maxFileSize + * @param array $locationMappings + * @param array $defaultMappings + * @param array $fallbackContentType */ public function __construct( + ContentTypeService $contentTypeService, array $locationMappings, array $defaultMappings, array $fallbackContentType, - $maxFileSize + int $maxFileSize ) { + $this->contentTypeService = $contentTypeService; $this->locationMappings = $locationMappings; $this->defaultMappings = $defaultMappings; $this->fallbackContentType = $fallbackContentType; @@ -45,9 +47,9 @@ public function __construct( } /** - * Returns configuration structure compatible with PlatformUI. + * Returns configuration structure compatible with AdminUI. * - * @return array + * @return array */ public function getConfig(): array { @@ -78,33 +80,68 @@ public function getConfig(): array } /** - * @param array $mappingGroup + * @param array $mappingGroup * - * @return array + * @return array */ - private function buildMappingGroupStructure(array $mappingGroup) + private function buildMappingGroupStructure(array $mappingGroup): array { + $contentTypeIdentifier = $mappingGroup['content_type_identifier']; + $contentFieldIdentifier = $mappingGroup['content_field_identifier']; + return [ 'mimeTypes' => $mappingGroup['mime_types'], - 'contentTypeIdentifier' => $mappingGroup['content_type_identifier'], - 'contentFieldIdentifier' => $mappingGroup['content_field_identifier'], + 'contentTypeIdentifier' => $contentTypeIdentifier, + 'contentFieldIdentifier' => $contentFieldIdentifier, 'nameFieldIdentifier' => $mappingGroup['name_field_identifier'], + 'maxFileSize' => $this->getContentTypeConfiguredMaxFileSize( + $contentTypeIdentifier, + $contentFieldIdentifier + ), ]; } /** - * @param array $fallbackContentType + * @param array $fallbackContentType * - * @return array + * @return array */ - private function buildFallbackContentTypeStructure(array $fallbackContentType) + private function buildFallbackContentTypeStructure(array $fallbackContentType): array { + $fallbackContentTypeIdentifier = $fallbackContentType['content_type_identifier']; + $fallbackContentFieldIdentifier = $fallbackContentType['content_field_identifier']; + return [ 'contentTypeIdentifier' => $fallbackContentType['content_type_identifier'], 'contentFieldIdentifier' => $fallbackContentType['content_field_identifier'], 'nameFieldIdentifier' => $fallbackContentType['name_field_identifier'], + 'maxFileSize' => $this->getContentTypeConfiguredMaxFileSize( + $fallbackContentTypeIdentifier, + $fallbackContentFieldIdentifier + ), ]; } + + private function getContentTypeConfiguredMaxFileSize( + string $contentTypeIdentifier, + string $imageFieldTypeIdentifier + ): int { + $contentType = $this->contentTypeService->loadContentTypeByIdentifier( + $contentTypeIdentifier + ); + + $imgFieldType = $contentType->getFieldDefinition($imageFieldTypeIdentifier); + if ($imgFieldType === null) { + return $this->maxFileSize; + } + + $validatorConfig = $imgFieldType->getValidatorConfiguration(); + if (isset($validatorConfig['FileSizeValidator']['maxFileSize'])) { + return (int)$validatorConfig['FileSizeValidator']['maxFileSize'] * 1024 * 1024; + } + + return $this->maxFileSize; + } } class_alias(ContentTypeMappings::class, 'EzSystems\EzPlatformAdminUi\UI\Config\Provider\ContentTypeMappings');