diff --git a/app/code/Magento/Customer/Controller/Address/File/Upload.php b/app/code/Magento/Customer/Controller/Address/File/Upload.php
new file mode 100644
index 0000000000000..dbbea482b0e52
--- /dev/null
+++ b/app/code/Magento/Customer/Controller/Address/File/Upload.php
@@ -0,0 +1,146 @@
+fileUploaderFactory = $fileUploaderFactory;
+ $this->addressMetadataService = $addressMetadataService;
+ $this->logger = $logger;
+ $this->fileProcessorFactory = $fileProcessorFactory;
+ parent::__construct($context);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute()
+ {
+ try {
+ $requestedFiles = $this->getRequest()->getFiles('custom_attributes');
+ if (empty($requestedFiles)) {
+ $result = $this->processError(__('No files for upload.'));
+ } else {
+ $attributeCode = key($requestedFiles);
+ $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode);
+
+ /** @var FileUploader $fileUploader */
+ $fileUploader = $this->fileUploaderFactory->create([
+ 'attributeMetadata' => $attributeMetadata,
+ 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
+ 'scope' => CustomAttributesDataInterface::CUSTOM_ATTRIBUTES,
+ ]);
+
+ $errors = $fileUploader->validate();
+ if (true !== $errors) {
+ $errorMessage = implode('', $errors);
+ $result = $this->processError(($errorMessage));
+ } else {
+ $result = $fileUploader->upload();
+ }
+ }
+ } catch (LocalizedException $e) {
+ $result = $this->processError($e->getMessage(), $e->getCode());
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ $result = $this->processError($e->getMessage(), $e->getCode());
+ }
+
+ $this->moveTmpFileToSuitableFolder($result);
+ /** @var \Magento\Framework\Controller\Result\Json $resultJson */
+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+ $resultJson->setData($result);
+ return $resultJson;
+ }
+
+ /**
+ * Move file from temporary folder to the 'customer_address' media folder
+ *
+ * @param array $fileInfo
+ * @throws LocalizedException
+ */
+ private function moveTmpFileToSuitableFolder(&$fileInfo)
+ {
+ $fileName = $fileInfo['file'];
+ $fileProcessor = $this->fileProcessorFactory
+ ->create(['entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS]);
+
+ $newFilePath = $fileProcessor->moveTemporaryFile($fileName);
+ $fileInfo['file'] = $newFilePath;
+ $fileInfo['url'] = $fileProcessor->getViewUrl(
+ $newFilePath,
+ 'file'
+ );
+ }
+
+ /**
+ * Prepare result array for errors
+ *
+ * @param string $message
+ * @param int $code
+ * @return array
+ */
+ private function processError($message, $code = 0)
+ {
+ $result = [
+ 'error' => $message,
+ 'errorcode' => $code,
+ ];
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php
index 6a8472758c169..e7e235b31fe49 100644
--- a/app/code/Magento/Customer/Model/FileProcessor.php
+++ b/app/code/Magento/Customer/Model/FileProcessor.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Customer\Model;
+/**
+ * Processor class for work with uploaded files
+ */
class FileProcessor
{
/**
@@ -232,7 +235,7 @@ public function moveTemporaryFile($fileName)
);
}
- $fileName = $dispersionPath . '/' . $fileName;
+ $fileName = $dispersionPath . '/' . $destinationFileName;
return $fileName;
}
diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js
index 2c5bc1159dd3a..f28569caa0053 100644
--- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js
+++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js
@@ -16,7 +16,8 @@ define([
'Magento_Ui/js/form/element/abstract',
'mage/backend/notification',
'mage/translate',
- 'jquery/file-uploader'
+ 'jquery/file-uploader',
+ 'mage/adminhtml/tools'
], function ($, _, utils, uiAlert, validator, Element, notification, $t) {
'use strict';
diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js
index 0ae09f14fa946..b490ac557e71b 100644
--- a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js
+++ b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js
@@ -11,8 +11,7 @@ define([
'Magento_Ui/js/modal/alert',
'Magento_Ui/js/lib/validation/validator',
'Magento_Ui/js/form/element/file-uploader',
- 'mage/adminhtml/browser',
- 'mage/adminhtml/tools'
+ 'mage/adminhtml/browser'
], function ($, _, utils, uiAlert, validator, Element, browser) {
'use strict';
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html
new file mode 100644
index 0000000000000..226ad2915bb61
--- /dev/null
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less
index 3ea1f5b7f6842..ac5ab0d87bf62 100644
--- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less
+++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less
@@ -7,6 +7,8 @@
// Variables
// _____________________________________________
+@import 'fields/_file-uploader.less';
+
@checkout-wrapper__margin: @indent__base;
@checkout-wrapper__columns: 16;
diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less
new file mode 100644
index 0000000000000..7b06186ef9ad3
--- /dev/null
+++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less
@@ -0,0 +1,450 @@
+// /**
+// * Copyright © Magento, Inc. All rights reserved.
+// * See COPYING.txt for license details.
+// */
+
+//
+// Components -> Single File Uploader
+// _____________________________________________
+
+//
+// Variables
+// ---------------------------------------------
+
+@icon-delete__content: '\e604';
+@icon-file__content: '\e626';
+
+
+@file-uploader-preview__border-color: @color-lighter-grayish;
+@file-uploader-preview__background-color: @color-white;
+@file-uploader-preview-focus__color: @color-blue2;
+
+@file-uploader-document-icon__color: @color-gray80;
+@file-uploader-document-icon__size: 7rem;
+@file-uploader-document-icon__z-index: @data-grid-file-uploader-image__z-index + 1;
+
+@file-uploader-video-icon__color: @color-gray80;
+@file-uploader-video-icon__size: 4rem;
+@file-uploader-video-icon__z-index: @data-grid-file-uploader-image__z-index + 1;
+
+@file-uploader-placeholder-icon__color: @color-gray80;
+@file-uploader-placeholder-icon__z-index: @data-grid-file-uploader-image__z-index + 1;
+
+@file-uploader-delete-icon__color: @color-brownie;
+@file-uploader-delete-icon__hover__color: @color-brownie-vanilla;
+@file-uploader-delete-icon__font-size: 1.6rem;
+
+@file-uploader-muted-text__color: @color-gray62;
+
+@file-uploader-preview__width: 150px;
+@file-uploader-preview__height: @file-uploader-preview__width;
+@file-uploader-preview__opacity: .7;
+
+@file-uploader-spinner-dimensions: 15px;
+
+@file-uploader-dragover__background: @color-gray83;
+@file-uploader-dragover-focus__color: @color-blue2;
+
+// Grid uploader
+
+@data-grid-file-uploader-image__size: 5rem;
+@data-grid-file-uploader-image__z-index: 1;
+
+@data-grid-file-uploader-menu-button__width: 2rem;
+
+@data-grid-file-uploader-upload-icon__color: @color-darkie-gray;
+@data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray;
+@data-grid-file-uploader-upload-icon__line-height: 48px;
+
+@data-grid-file-uploader-wrapper__size: @data-grid-file-uploader-image__size + 2rem;
+
+//
+// Single file uploader
+// ---------------------------------------------
+
+.file-uploader-area {
+ position: relative;
+
+ input[type='file'] {
+ cursor: pointer;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ visibility: hidden;
+ width: 0;
+
+ &:focus {
+ + .file-uploader-button {
+ box-shadow: 0 0 0 1px @file-uploader-preview-focus__color;
+ }
+ }
+
+ &:disabled {
+ + .file-uploader-button {
+ cursor: default;
+ opacity: .5;
+ pointer-events: none;
+ }
+ }
+ }
+}
+
+.file-uploader-summary {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.file-uploader-button {
+ background: @color-gray-darken0;
+ border: 1px solid @color-gray_light;
+ box-sizing: border-box;
+ color: @color-black_dark;
+ cursor: pointer;
+ display: inline-block;
+ font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 1.4rem;
+ font-weight: 600;
+ line-height: 1.6rem;
+ margin: 0;
+ padding: 7px 15px;
+ vertical-align: middle;
+
+ &._is-dragover {
+ background: @file-uploader-dragover__background;
+ border: 1px solid @file-uploader-preview-focus__color;
+ }
+}
+
+.file-uploader-spinner {
+ background-image: url('@{baseDir}images/loader-1.gif');
+ background-position: 50%;
+ background-repeat: no-repeat;
+ background-size: @file-uploader-spinner-dimensions;
+ display: none;
+ height: 30px;
+ margin-left: @indent__s;
+ vertical-align: top;
+ width: @file-uploader-spinner-dimensions;
+}
+
+.file-uploader-preview {
+ .action-remove {
+ &:extend(.abs-action-reset all);
+ .lib-icon-font (
+ @icon-delete__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: @file-uploader-delete-icon__font-size,
+ @_icon-font-color: @file-uploader-delete-icon__color,
+ @_icon-font-color-hover: @file-uploader-delete-icon__hover__color,
+ @_icon-font-text-hide: true,
+ @_icon-font-display: block
+ );
+ bottom: 4px;
+ cursor: pointer;
+ display: block;
+ height: 27px;
+ left: 6px;
+ padding: 2px;
+ position: absolute;
+ text-decoration: none;
+ width: 25px;
+ z-index: 2;
+ }
+
+ &:hover {
+ .preview-image img,
+ .preview-link:before {
+ opacity: @file-uploader-preview__opacity;
+ }
+ }
+
+ .preview-link {
+ display: block;
+ height: 100%;
+ }
+
+ .preview-image img {
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ max-height: 100%;
+ max-width: 100%;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 1;
+ }
+
+ .preview-video {
+ .lib-icon-font(
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: @file-uploader-video-icon__size,
+ @_icon-font-color: @file-uploader-video-icon__color,
+ @_icon-font-color-hover: @file-uploader-video-icon__color
+ );
+
+ &:before {
+ left: 0;
+ margin-top: -@file-uploader-video-icon__size / 2;
+ position: absolute;
+ right: 0;
+ top: 50%;
+ z-index: @file-uploader-video-icon__z-index;
+ }
+ }
+
+ .preview-document {
+ .lib-icon-font(
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: @file-uploader-document-icon__size,
+ @_icon-font-color: @file-uploader-document-icon__color,
+ @_icon-font-color-hover: @file-uploader-document-icon__color
+ );
+
+ &:before {
+ left: 0;
+ margin-top: -@file-uploader-document-icon__size / 2;
+ position: absolute;
+ right: 0;
+ top: 50%;
+ z-index: @file-uploader-document-icon__z-index;
+ }
+ }
+}
+
+.file-uploader-preview,
+.file-uploader-placeholder {
+ background: @file-uploader-preview__background-color;
+ border: 1px solid @file-uploader-preview__border-color;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: block;
+ height: @file-uploader-preview__height;
+ line-height: 1;
+ margin: @indent__s @indent__m @indent__s 0;
+ overflow: hidden;
+ position: relative;
+ width: @file-uploader-preview__width;
+}
+
+.file-uploader {
+ &._loading {
+ .file-uploader-spinner {
+ display: inline-block;
+ }
+ }
+
+ .admin__field-note,
+ .admin__field-error {
+ margin-bottom: @indent__s;
+ }
+
+ .file-uploader-filename {
+ .lib-text-overflow();
+ max-width: @file-uploader-preview__width;
+ word-break: break-all;
+
+ &:first-child {
+ margin-bottom: @indent__s;
+ }
+ }
+
+ .file-uploader-meta {
+ color: @file-uploader-muted-text__color;
+ }
+
+ .admin__field-fallback-reset {
+ margin-left: @indent__s;
+ }
+
+ ._keyfocus & .action-remove {
+ &:focus {
+ box-shadow: 0 0 0 1px @file-uploader-preview-focus__color;
+ }
+ }
+}
+
+// Placeholder for multiple uploader
+.file-uploader-placeholder {
+ &.placeholder-document {
+ .lib-icon-font(
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: 5rem,
+ @_icon-font-color: @file-uploader-placeholder-icon__color,
+ @_icon-font-color-hover: @file-uploader-placeholder-icon__color
+ );
+
+ &:before {
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 20px;
+ z-index: @file-uploader-placeholder-icon__z-index;
+ }
+ }
+
+ &.placeholder-image {
+ .lib-icon-font(
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: 5rem,
+ @_icon-font-color: @file-uploader-placeholder-icon__color,
+ @_icon-font-color-hover: @file-uploader-placeholder-icon__color
+ );
+
+ &:before {
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 20px;
+ z-index: @file-uploader-placeholder-icon__z-index;
+ }
+ }
+
+ &.placeholder-video {
+ .lib-icon-font(
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: 3rem,
+ @_icon-font-color: @file-uploader-placeholder-icon__color,
+ @_icon-font-color-hover: @file-uploader-placeholder-icon__color
+ );
+
+ &:before {
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 30px;
+ z-index: @file-uploader-placeholder-icon__z-index;
+ }
+ }
+}
+
+.file-uploader-placeholder-text {
+ bottom: 0;
+ color: @color-blue-dodger;
+ font-size: 1.1rem;
+ left: 0;
+ line-height: @line-height__base;
+ margin-bottom: 15%;
+ padding: 0 @indent__base;
+ position: absolute;
+ right: 0;
+ text-align: center;
+}
+
+//
+// Grid image uploader
+// ---------------------------------------------
+
+.data-grid-file-uploader {
+ min-width: @data-grid-file-uploader-wrapper__size;
+
+ &._loading {
+ .file-uploader-spinner {
+ display: block;
+ }
+
+ .file-uploader-button {
+ &:before {
+ display: none;
+ }
+ }
+ }
+
+ .file-uploader-image {
+ background: transparent;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ max-height: 100%;
+ max-width: 100%;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: @data-grid-file-uploader-image__z-index;
+
+ + .file-uploader-area {
+ .file-uploader-button {
+ &:before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .file-uploader-area {
+ z-index: @data-grid-file-uploader-image__z-index + 1;
+ }
+
+ .file-uploader-spinner {
+ height: 100%;
+ margin: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ }
+
+ .file-uploader-button {
+ display: block;
+ height: @data-grid-file-uploader-upload-icon__line-height;
+ text-align: center;
+
+ .lib-icon-font (
+ @icon-file__content,
+ @_icon-font: @icons__font-name,
+ @_icon-font-size: 1.3rem,
+ @_icon-font-line-height: @data-grid-file-uploader-upload-icon__line-height,
+ @_icon-font-color: @data-grid-file-uploader-upload-icon__color,
+ @_icon-font-color-hover: @data-grid-file-uploader-upload-icon__hover__color,
+ @_icon-font-text-hide: true,
+ @_icon-font-display: block
+ );
+ }
+
+ .action-select-wrap {
+ float: left;
+
+ .action-select {
+ border: 1px solid @file-uploader-preview__border-color;
+ display: block;
+ height: @data-grid-file-uploader-image__size;
+ margin-left: -1px;
+ padding: 0;
+ width: @data-grid-file-uploader-menu-button__width;
+
+ &:after {
+ border-color: @data-grid-file-uploader-upload-icon__color transparent transparent transparent;
+ left: 50%;
+ margin: 0 0 0 -5px;
+ }
+
+ &:hover {
+ &:after {
+ border-color: @data-grid-file-uploader-upload-icon__hover__color transparent transparent transparent;
+ }
+ }
+
+ > span {
+ display: none;
+ }
+ }
+
+ .action-menu {
+ left: 4rem;
+ right: auto;
+ z-index: @data-grid-file-uploader-image__z-index + 1;
+ }
+ }
+}
+
+.data-grid-file-uploader-inner {
+ border: 1px solid @file-uploader-preview__border-color;
+ float: left;
+ height: @data-grid-file-uploader-image__size;
+ position: relative;
+ width: @data-grid-file-uploader-image__size;
+}
diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less
index 98dd57dead74c..b92a2b5070b8f 100644
--- a/app/design/frontend/Magento/luma/web/css/source/_forms.less
+++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less
@@ -104,6 +104,10 @@
.select-styling();
}
+ select.admin__control-multiselect {
+ height: auto;
+ }
+
.field-error,
div.mage-error[generated] {
margin-top: 7px;
diff --git a/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php b/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php
new file mode 100644
index 0000000000000..0f8743f057601
--- /dev/null
+++ b/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php
@@ -0,0 +1,29 @@
+typeProcessor = $typeProcessor;
$this->objectManager = $objectManager;
@@ -101,6 +109,7 @@ public function __construct(
?: \Magento\Framework\App\ObjectManager::getInstance()->get(ServiceTypeToEntityTypeMap::class);
$this->config = $config
?: \Magento\Framework\App\ObjectManager::getInstance()->get(ConfigInterface::class);
+ $this->customAttributePreprocessors = $customAttributePreprocessors;
}
/**
@@ -289,12 +298,11 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat
$dataObjectClassName = ltrim($dataObjectClassName, '\\');
foreach ($customAttributesValueArray as $key => $customAttribute) {
+ $this->runCustomAttributePreprocessors($key, $customAttribute);
if (!is_array($customAttribute)) {
$customAttribute = [AttributeValue::ATTRIBUTE_CODE => $key, AttributeValue::VALUE => $customAttribute];
}
-
list($customAttributeCode, $customAttributeValue) = $this->processCustomAttribute($customAttribute);
-
$entityType = $this->serviceTypeToEntityTypeMap->getEntityType($dataObjectClassName);
if ($entityType) {
$type = $this->customAttributeTypeLocator->getType(
@@ -331,6 +339,22 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat
return $result;
}
+ /**
+ * Prepare attribute value by loaded attribute preprocessors
+ *
+ * @param string $key
+ * @param mixed $customAttribute
+ * @return bool
+ */
+ private function runCustomAttributePreprocessors($key, &$customAttribute)
+ {
+ foreach ($this->customAttributePreprocessors as $attributePreprocessor) {
+ if ($attributePreprocessor->shouldBePrepared($key, $customAttribute)) {
+ $attributePreprocessor->prepare($key, $customAttribute);
+ }
+ }
+ }
+
/**
* Derive the custom attribute code and value.
*
diff --git a/lib/web/css/source/lib/variables/_colors.less b/lib/web/css/source/lib/variables/_colors.less
index 9c694468e9f62..ffb0e8e797d81 100644
--- a/lib/web/css/source/lib/variables/_colors.less
+++ b/lib/web/css/source/lib/variables/_colors.less
@@ -7,9 +7,13 @@
// Color variables
// _____________________________________________
+@color-blue-dodger: #008bdb;
+@color-black_dark: #333333;
+
@color-white: #fff;
@color-black: #000;
+@color-darkie-gray: #8a837f;
@color-gray19: #303030;
@color-gray20: #333;
@color-gray34: #575757;
@@ -29,12 +33,16 @@
@color-gray79: #c9c9c9;
@color-gray80: #ccc;
@color-gray82: #d1d1d1;
+@color-gray83: #d4d4d4;
@color-gray89: #e3e3e3;
@color-gray90: #e5e5e5;
@color-gray91: #e8e8e8;
@color-gray92: #ebebeb;
@color-gray94: #f0f0f0;
@color-gray95: #f2f2f2;
+@color-gray_light: #cccccc;
+@color-lighter-grayish: #cacaca;
+@color-very-dark-gray: #666;
@color-white-smoke: #f5f5f5;
@color-white-dark-smoke: #efefef;
@color-white-fog: #f8f8f8;
@@ -80,6 +88,8 @@
@color-pink1: #fae5e5;
@color-dark-pink1: #800080; // Legacy pink
+@color-brownie: #514943;
+@color-brownie-vanilla: #736963;
@color-brownie1: #6f4400;
@color-brownie-light1: #c07600;