diff --git a/CHANGELOG-AK7.md b/CHANGELOG-AK7.md new file mode 100644 index 00000000..73d4437d --- /dev/null +++ b/CHANGELOG-AK7.md @@ -0,0 +1,131 @@ +## Added default Label & Image properties + +He now have default properties that reflect Reference Entities... + +You have to enable them to use them... + +Here is an example with Color + +```yaml +## grid example color +# */ReferenceDataBundle/Resources/config/datagrid/color.yml + +datagrid: + color: + options: + entityHint: color + manageFilters: false + source: + type: pim_datasource_default + entity: Induxx\Bundle\ReferenceDataBundle\Entity\ReferenceIcon + repository_method: createDatagridQueryBuilder + columns: + code: + label: pim_custom_entity.form.tab.code.title + label: + label: pim_custom_entity.form.tab.label.title + filters: + columns: + code: + type: string + label: pim_custom_entity.form.tab.code.title + data_name: rd.code + label: + type: search + label: pim_custom_entity.form.tab.label.title + data_name: rd.label + sorters: + columns: + code: + data_name: rd.code + label: + data_name: rd.label + default: + code: '%oro_datagrid.extension.orm_sorter.class%::DIRECTION_ASC' + properties: + id: ~ + edit_link: + type: url + route: pim_customentity_rest_get + params: + - id + - customEntityName + delete_link: + type: url + route: pim_customentity_rest_delete + params: + - id + - customEntityName + actions: + edit: + type: navigate + label: grid.action.label.edit + icon: edit + link: edit_link + rowAction: true + delete: + type: delete + label: grid.action.label.delete + icon: trash + link: delete_link +``` + +```yaml +## add label and image +# */ReferenceDataBundle/Resources/config/form_extensions/color/edit.yml +... + -color-edit-form-properties-label: + module: pim/form/common/fields/text + parent: -color-edit-form-properties-common + targetZone: content + position: 90 + config: + fieldName: label + label: pim_custom_entity.form.tab.label.title + + -color-edit-form-properties-image: + module: referencedata/media-field + parent: -color-edit-form-properties-common + targetZone: content + position: 130 + config: + fieldName: image + label: pim_custom_entity.form.tab.image.title + required: false + readOnly: false +``` + +```yaml +# */ReferenceDataBundle/Resources/config/doctrine/Color.orm.yml +Induxx\Bundle\ReferenceDataBundle\Entity\Color: + type: entity + table: refdata_reference_color + changeTrackingPolicy: DEFERRED_EXPLICIT + repositoryClass: Pim\Bundle\CustomEntityBundle\Entity\Repository\CustomEntityRepository + fields: + id: + type: integer + id: true + generator: + strategy: AUTO + code: + type: string + length: 255 + nullable: false + unique: true + label: + type: string + length: 255 + nullable: true + oneToOne: + image: + targetEntity: Akeneo\Tool\Component\FileStorage\Model\FileInfoInterface + joinColumn: + name: Image + referencedColumnName: id + onDelete: 'SET NULL' + cascade: + - persist +``` +After this you will have to generate a custom DB Integration which is a Case by Case Scenario. +Some might already have and Image or a Label ... diff --git a/Controller/Rest/AbstractRestApiAction.php b/Controller/Rest/AbstractRestApiAction.php new file mode 100644 index 00000000..10eb3c42 --- /dev/null +++ b/Controller/Rest/AbstractRestApiAction.php @@ -0,0 +1,146 @@ +actionFactory = $actionFactory; + $this->eventManager = $eventManager; + $this->managerRegistry = $managerRegistry; + $this->referenceEntityCodes = $referenceEntityCodes; + $this->referenceEntityNames = $referenceEntityNames; + } + + public function setConfiguration(ConfigurationInterface $configuration): void + { + $this->configuration = $configuration; + $resolver = new OptionsResolver(); + $this->setDefaultOptions($resolver); + $this->eventManager->dipatchConfigureEvent($this, $resolver); + $this->options = $resolver->resolve($configuration->getActionOptions($this->getType())); + } + + public function getConfiguration(): ConfigurationInterface + { + return $this->configuration; + } + + public function getOptions(): array + { + return $this->options; + } + + protected function getOption($optionKey) + { + if (isset($this->options[$optionKey])) { + return $this->options[$optionKey]; + } else { + throw new \LogicException( + sprintf('Option "%s" is not defined', $optionKey) + ); + } + } + + public function execute(Request $request): Response + { + throw new \Exception('WE DON\'T USE EXECUTE, WE BYPASS IT'); + } + + protected function findEntity(Request $request): AbstractCustomEntity + { + $entity = $this->getManager()->find( + $this->configuration->getEntityClass(), + $request->attributes->get('id'), + $this->options['find_options'] + ); + + if (null === $entity) { + throw new NotFoundHttpException(); + } + + return $entity; + } + + protected function normalize(AbstractCustomEntity $entity): array + { + $manager = $this->getManager(); + $entityName = $this->configuration->getName(); + $editFormExtension = $this->configuration->getOptions()['edit_form_extension']; + $context = [ + 'customEntityName' => $entityName, + 'form' => $editFormExtension, + ]; + + return $manager->normalize($entity, 'standard', $context); + } + + protected function getDecodedContent($content): array + { + $decodedContent = \json_decode($content, true); + + if (null === $decodedContent) { + throw new BadRequestHttpException('Invalid json message received'); + } + + return $decodedContent; + } + + protected function getManager(): ManagerInterface + { + return $this->managerRegistry->getFromConfiguration($this->configuration); + } + + protected function setDefaultOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults(['find_options' => []]); + } + + /** + * @param string $referenceEntityCode + * @return string + */ + protected function getEntityClass(string $referenceEntityCode): ?string + { + return $this->referenceEntityCodes[$referenceEntityCode] ?? null; + } + + /** + * TODO extract these params outside the class + */ + protected function getConfigurationAlias(string $referenceEntityCode): ?string + { + return $this->referenceEntityNames[$referenceEntityCode] ?? null; + } +} \ No newline at end of file diff --git a/Controller/Rest/PartialUpdateAction.php b/Controller/Rest/PartialUpdateAction.php new file mode 100644 index 00000000..41f37e8c --- /dev/null +++ b/Controller/Rest/PartialUpdateAction.php @@ -0,0 +1,110 @@ +validator = $validator; + $this->violationNormalizer = $violationNormalizer; + $this->router = $router; + $this->entityManager = $entityManager; + $this->configurationRegistry = $configurationRegistry; + } + + public function __invoke(Request $request, string $referenceCode, string $code): Response + { + $referenceEntityClass = $this->getEntityClass($referenceCode); + if (null === $referenceEntityClass) { + throw new NotFoundHttpException(sprintf('Reference entity "%s" does not exist.', $referenceCode)); + } + + $this->setConfiguration($this->configurationRegistry->get($this->getConfigurationAlias($referenceCode))); + $manager = $this->getManager(); + $data = $this->getDecodedContent($request->getContent()); + unset($data['reference-code']); + + $entityRepository = $this->entityManager->getRepository($referenceEntityClass); + + $referenceEntityRecord = $entityRepository->findOneByIdentifier($code); + $isCreation = null === $referenceEntityRecord; + + if ($isCreation) { + $referenceEntityRecord = $manager->create($referenceEntityClass, $data); + } else { + $manager->update($referenceEntityRecord, $data); + } + + $errors = $this->validator->validate($referenceEntityRecord); + if (count($errors) > 0) { + $normalizedViolations = []; + foreach ($errors as $error) { + $normalizedViolations[] = $this->violationNormalizer->normalize( + $error, + 'standard' + ); + } + + return new JsonResponse(['values' => $normalizedViolations], Response::HTTP_BAD_REQUEST); + } + + $manager->save($referenceEntityRecord); + $status = $isCreation ? Response::HTTP_CREATED : Response::HTTP_NO_CONTENT; + + return $this->getResponse($referenceCode, $code, $status); + } + + private function getResponse(string $referenceCode, string $code, int $status): Response + { + $response = new Response(null, $status); + $route = $this->router->generate( + 'api_reference_entity_get', + [ + 'referenceCode' => $referenceCode, + 'code' => $code, + ], + UrlGeneratorInterface::ABSOLUTE_URL + ); + $response->headers->set('Location', $route); + + return $response; + } + + public function getType(): string + { + return 'rest_create'; + } +} \ No newline at end of file diff --git a/Entity/DefaultLocalizableLabelsTrait.php b/Entity/DefaultLocalizableLabelsTrait.php new file mode 100644 index 00000000..0ecba56a --- /dev/null +++ b/Entity/DefaultLocalizableLabelsTrait.php @@ -0,0 +1,67 @@ + $content) { + $this->setLabel($locale, $content); + } + } + + public function setLabel(string $locale, string $content = null): void + { + $this->pop(); + Assert::stringNotEmpty($locale); + + // Validate the locale code format + if (!preg_match('/^[a-z]{2}_[A-Z]{2}$/', $locale)) { + throw new InvalidArgumentException('Invalid locale code format. Expected format is xx_YY.'); + } + + if (empty($content)) { + unset($labels[$locale]); + return; + } + + $this->labels[$locale] = $content; + } + + public function getLabel(string $locale):? string + { + return $this->labels[$locale] ?? null; + } + + public function getLabels(): array + { + return $this->labels; + } + + private function unpop(): void + { + if ($this->labels === null) { + $this->labels = []; + + if (is_string($this->label)) { + $this->labels = json_decode($this->label, true); + } + } + } + + private function pop(): void + { + $this->label = json_encode($this->labels); + } + + public function __destruct() + { + $this->pop(); + } +} \ No newline at end of file diff --git a/Entity/DefaultValuesTrait.php b/Entity/DefaultValuesTrait.php new file mode 100644 index 00000000..8da52cc4 --- /dev/null +++ b/Entity/DefaultValuesTrait.php @@ -0,0 +1,34 @@ +label; + } + + public function setLabel(?string $label): void + { + $this->label = $label; + } + + public function getImage(): ?FileInfoInterface + { + return $this->image; + } + + public function setImage(?FileInfoInterface $image): void + { + $this->image = $image; + } +} \ No newline at end of file diff --git a/Normalizer/Standard/DefaultValuesStandardNormalizer.php b/Normalizer/Standard/DefaultValuesStandardNormalizer.php new file mode 100644 index 00000000..d5685faf --- /dev/null +++ b/Normalizer/Standard/DefaultValuesStandardNormalizer.php @@ -0,0 +1,51 @@ + $entity->getId(), + 'code' => $entity->getCode(), + 'image' => $this->getFileData($entity->getImage()), + 'label' => $entity->getLabel(), + ]; + + return $normalizedEntity; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null): bool + { + return $data instanceof AbstractCustomEntity && in_array($format, $this->supportedFormats); + } + + protected function getFileData(?FileInfoInterface $fileInfo): array + { + if (!$fileInfo) { + return []; + } + return [ + 'originalFilename' => $fileInfo->getOriginalFilename(), + 'filePath' => $fileInfo->getKey() + ]; + } +} diff --git a/Resources/config/connectors.yml b/Resources/config/connectors.yml index eb5d6820..0dcd2d03 100644 --- a/Resources/config/connectors.yml +++ b/Resources/config/connectors.yml @@ -7,8 +7,6 @@ parameters: pim_custom_entity.array_converter.flat_to_standard.reference_data.class: Pim\Bundle\CustomEntityBundle\Connector\ArrayConverter\FlatToStandard\ReferenceData - referenced_data_entity_mappings: [] - services: # Readers pim_custom_entity.reader.file.csv: diff --git a/Resources/config/controllers.yml b/Resources/config/controllers.yml index b6514753..7cf57f72 100644 --- a/Resources/config/controllers.yml +++ b/Resources/config/controllers.yml @@ -2,6 +2,9 @@ parameters: pim_ui.controller.ajax_option.class: Pim\Bundle\CustomEntityBundle\Controller\AjaxOptionController pimee_product_asset.controller.ajax_option.class: Pim\Bundle\CustomEntityBundle\Controller\EnterpriseAjaxOptionController pim_custom_entity.rest_controller.class: Pim\Bundle\CustomEntityBundle\Controller\RestController + + referenced_data_entity_mappings: [ ] + referenced_data_entity_names: [ ] services: pim_custom_entity.controller: @@ -29,4 +32,19 @@ services: - '@pim_api.pagination.parameter_validator' - '@fos_oauth_server.entity_manager' - '%pim_api.configuration%' - - '%referenced_data_entity_mappings%' \ No newline at end of file + - '%referenced_data_entity_mappings%' + + Pim\Bundle\CustomEntityBundle\Controller\Rest\PartialUpdateAction: + shared: false + public: true + arguments: + - '@pim_custom_entity.action.factory' + - '@pim_custom_entity.action_event_manager' + - '@pim_custom_entity.manager.registry' + - '@pim_custom_entity.configuration.registry' + - '@router' + - '@fos_oauth_server.entity_manager' + - '@validator' + - '@pim_enrich.normalizer.violation' + - '%referenced_data_entity_mappings%' + - '%referenced_data_entity_names%' diff --git a/Resources/config/requirejs.yml b/Resources/config/requirejs.yml index dc3ba2e6..c6582b44 100644 --- a/Resources/config/requirejs.yml +++ b/Resources/config/requirejs.yml @@ -14,6 +14,9 @@ config: custom_entity/remover/reference-data: pimcustomentity/js/remover/reference-data-remover oro/datagrid/delete-action: pimcustomentity/js/datagrid/action/delete-action + referencedata/media-field: pimcustomentity/js/field/media-field + referencedata/template/field/media-field: pimcustomentity/templates/field/media-field.html + config: pim/fetcher-registry: fetchers: diff --git a/Resources/config/routing/reference_data_api.yml b/Resources/config/routing/reference_data_api.yml index 9a6c609d..6b3d2187 100644 --- a/Resources/config/routing/reference_data_api.yml +++ b/Resources/config/routing/reference_data_api.yml @@ -9,6 +9,11 @@ api_reference_entity_get: defaults: { _controller: pim_custom_entity.api_controller.reference_entity:getAction, _format: json } methods: [GET] +api_reference_entity_patch: + path: /reference-entities/{referenceCode}/records/{code} + defaults: { _controller: Pim\Bundle\CustomEntityBundle\Controller\Rest\PartialUpdateAction, _format: json } + methods: [PATCH] + api_media_file_list: path: /reference-entities-media-files/{code} defaults: { _controller: pim_api.controller.media_file:getAction, _format: json } diff --git a/Resources/config/serializer.yml b/Resources/config/serializer.yml index ab4bf9c7..53e414e6 100644 --- a/Resources/config/serializer.yml +++ b/Resources/config/serializer.yml @@ -20,6 +20,12 @@ services: tags: - { name: pim_serializer.normalizer, priority: 150 } + pim_custom_entity.normalizer.standard.default: + public: false + class: Pim\Bundle\CustomEntityBundle\Normalizer\Standard\DefaultValuesStandardNormalizer + tags: + - { name: pim_serializer.normalizer, priority: 160 } + pim_custom_entity.normalizer.flat.reference_data: public: false class: '%pim_custom_entity.normalizer.flat.reference_data.class%' diff --git a/Resources/public/js/field/media-field.js b/Resources/public/js/field/media-field.js new file mode 100644 index 00000000..d85ab94b --- /dev/null +++ b/Resources/public/js/field/media-field.js @@ -0,0 +1,152 @@ +'use strict'; +/** + * Media field + */ +define([ + 'jquery', + 'pim/form/common/fields/field', + 'underscore', + 'routing', + 'referencedata/template/field/media-field', + 'pim/common/property', + 'oro/mediator', + 'oro/messenger', + 'pim/media-url-generator', + 'jquery.slimbox' + ], + function ($, Field, _, Routing, fieldTemplate, propertyAccessor, mediator, messenger, MediaUrlGenerator) { + + return Field.extend({ + fieldTemplate: _.template(fieldTemplate), + ready: true, + events: { + 'change input': function (event) { + this.errors = []; + this.updateModel(event); + }, + 'click .open-media': 'previewImage', + 'click .clear-field': 'clearField' + }, + uploadContext: {}, + + renderInput: function (context) { + return this.fieldTemplate(_.extend(context, { + value: this.getModelValue() + })); + }, + + getTemplateContext: function () { + return Field.prototype.getTemplateContext.apply(this, arguments) + .then(function (templateContext) { + templateContext.inUpload = !this.isReady(); + templateContext.mediaUrlGenerator = MediaUrlGenerator; + + return templateContext; + }.bind(this)); + }, + + updateModel: function (event) { + if (!this.isReady()) { + console.log('not ready'); + } + + var input = event.target; + if (!input || 0 === input.files.length) { + return; + } + var formData = new FormData(); + formData.append('file', input.files[0]); + this.setReady(false); + + $.ajax({ + url: Routing.generate('pim_enrich_media_rest_post'), + type: 'POST', + data: formData, + contentType: false, + cache: false, + processData: false, + xhr: function () { + var myXhr = $.ajaxSettings.xhr(); + if (myXhr.upload) { + myXhr.upload.addEventListener('progress', this.handleProcess.bind(this), false); + } + + return myXhr; + }.bind(this) + }) + .done(function (data) { + this.setUploadContextValue(data); + }.bind(this)) + .fail(function (xhr) { + var message = xhr.responseJSON && xhr.responseJSON.message ? + xhr.responseJSON.message : + _.__('pim_enrich.entity.product.error.upload'); + messenger.enqueueMessage('error', message); + }) + .always(function () { + this.$('> .akeneo-media-uploader-field .progress').css({opacity: 0}); + this.setReady(true); + this.uploadContext = {}; + }.bind(this)); + }, + + setCurrentValue: function(value){ + const data = this.getFormData(); + propertyAccessor.updateProperty(data, this.fieldName, value); + + this.setData(data); + this.render(); + }, + + clearField: function (event) { + var value = { + filePath: null, + originalFilename: null + }; + + this.setCurrentValue(value); + }, + + handleProcess: function (e) { + this.$('> .akeneo-media-uploader-field .progress').css({opacity: 1}); + this.$('> .akeneo-media-uploader-field .progress .bar').css({ + width: ((e.loaded / e.total) * 100) + '%' + }); + }, + + getCurrentValue: function () { + return propertyAccessor.accessProperty(this.getFormData(),this.fieldName); + }, + + previewImage: function () { + var mediaUrl = MediaUrlGenerator.getMediaShowUrl(this.getCurrentValue().filePath, 'preview'); + if (mediaUrl) { + $.slimbox(mediaUrl, '', {overlayOpacity: 0.3}); + } + }, + + setUploadContextValue: function (value) { + this.setCurrentValue(value); + + mediator.trigger('pim_enrich:form:entity:post_update'); + }, + /** + * Set this field as ready + * + * @param {boolean} ready + */ + setReady: function (ready) { + this.ready = ready; + }, + + /** + * Return whether this field is ready + * + * @returns {boolean} + */ + isReady: function () { + return this.ready; + } + }); + } +); diff --git a/Resources/public/templates/field/media-field.html b/Resources/public/templates/field/media-field.html new file mode 100644 index 00000000..5de164aa --- /dev/null +++ b/Resources/public/templates/field/media-field.html @@ -0,0 +1,35 @@ +
+ <% if (!value || value.filePath === null || value.length === 0) { %> + +
+ + <%- _.__('pim_enrich.entity.product.media.upload')%> +
+ <% } else { %> +
+ <% mediaThumbnailUrl = mediaUrlGenerator.getMediaShowUrl(value.filePath, 'thumbnail_small') %> + <% mediaPreviewUrl = mediaUrlGenerator.getMediaShowUrl(value.filePath, 'preview') %> + <% mediaDownloadUrl = mediaUrlGenerator.getMediaDownloadUrl(value.filePath) %> + <% if (null != mediaThumbnailUrl) { %> +
+ <% } else { %> +
+ +
+ <% } %> +
+
<%- value.originalFilename %>
+
+ <% if (null != mediaPreviewUrl) { %> + + <% } %> + + +
+
+
+ <% } %> +
+
+
+
\ No newline at end of file diff --git a/Resources/translations/jsmessages.en.yml b/Resources/translations/jsmessages.en.yml index e022a7aa..1e7ee9d9 100644 --- a/Resources/translations/jsmessages.en.yml +++ b/Resources/translations/jsmessages.en.yml @@ -3,6 +3,13 @@ pim_menu: item.reference_data: Reference data navigation.reference_data: Reference data +grid: + action: + label: + edit: Edit this Reference + show: Show this Reference + delete: Delete this Reference + pim_custom_entity: index_title: Overview create_popin.title: Create @@ -15,6 +22,8 @@ pim_custom_entity: section: common: Common label_translations: Labels + image.title: Image + label.title: Label button: create: Create message: