From 775535e68384e210980fa45e3991aa60fd43442b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 27 May 2024 22:32:03 +0200 Subject: [PATCH] refactor file drop widget --- .../ChillActivityBundle/Form/ActivityType.php | 12 +- .../Resources/views/Activity/edit.html.twig | 6 +- .../Form/AccompanyingCourseDocumentType.php | 32 +--- .../Form/CollectionStoredObjectType.php | 37 +++++ .../DataMapper/StoredObjectDataMapper.php | 82 +++++++++ .../StoredObjectDataTransformer.php | 52 ++++++ .../Form/StoredObjectType.php | 85 ++-------- .../async_upload/{index.js => index-old.js} | 0 .../public/module/async_upload/index.ts | 86 ++++++++++ .../Resources/public/types.ts | 29 ++++ .../vuejs/DocumentActionButtonsGroup.vue | 16 +- .../public/vuejs/DropFileWidget/DropFile.vue | 155 ++++++++++++++++++ .../vuejs/DropFileWidget/DropFileWidget.vue | 83 ++++++++++ .../StoredObjectButton/ConvertButton.vue | 4 +- .../StoredObjectButton/DownloadButton.vue | 4 +- .../StoredObjectButton/WopiEditButton.vue | 4 +- .../public/vuejs/_components/helper.ts | 60 +++++++ .../Resources/views/Form/fields.html.twig | 20 +-- .../chill.webpack.config.js | 2 +- .../config/services/form.yaml | 27 +-- .../translations/messages.fr.yml | 3 + .../Form/Type/ChillCollectionType.php | 3 + .../Resources/public/chill/index.js | 2 - .../Resources/public/lib/collection/index.js | 120 -------------- .../collection/collection.scss | 0 .../public/module/collection/index.ts | 128 +++++++++++++++ .../Resources/views/Form/fields.html.twig | 3 +- .../Resources/views/layout.html.twig | 2 + .../ChillMainBundle/chill.webpack.config.js | 1 + .../components/FormEvaluation.vue | 4 +- 30 files changed, 780 insertions(+), 282 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php create mode 100644 src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php create mode 100644 src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php rename src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/{index.js => index-old.js} (100%) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DropFileWidget/DropFile.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DropFileWidget/DropFileWidget.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/helper.ts delete mode 100644 src/Bundle/ChillMainBundle/Resources/public/lib/collection/index.js rename src/Bundle/ChillMainBundle/Resources/public/{lib => module}/collection/collection.scss (100%) create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/collection/index.ts diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index fbcf60bf0..9e2358e8b 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -15,11 +15,10 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Form\Type\PickActivityReasonType; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; -use Chill\DocStoreBundle\Form\StoredObjectType; +use Chill\DocStoreBundle\Form\CollectionStoredObjectType; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\PickUserDynamicType; @@ -276,16 +275,9 @@ class ActivityType extends AbstractType } if ($activityType->isVisible('documents')) { - $builder->add('documents', ChillCollectionType::class, [ - 'entry_type' => StoredObjectType::class, + $builder->add('documents', CollectionStoredObjectType::class, [ 'label' => $activityType->getLabel('documents'), 'required' => $activityType->isRequired('documents'), - 'allow_add' => true, - 'allow_delete' => true, - 'button_add_label' => 'activity.Insert a document', - 'button_remove_label' => 'activity.Remove a document', - 'empty_collection_explain' => 'No documents', - 'entry_options' => ['has_title' => true], ]); } diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig index a000b0c7e..d986f9150 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig @@ -92,7 +92,9 @@ {% endif %} {%- if edit_form.documents is defined -%} - {{ form_row(edit_form.documents) }} + {{ form_label(edit_form.documents) }} + {{ form_errors(edit_form.documents) }} + {{ form_widget(edit_form.documents) }}
{% endif %} @@ -127,4 +129,4 @@ {% block css %} {{ encore_entry_link_tags('mod_pickentity_type') }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php b/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php index 0fced39d3..331e3cb98 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php +++ b/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php @@ -14,47 +14,21 @@ namespace Chill\DocStoreBundle\Form; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\Document; use Chill\DocStoreBundle\Entity\DocumentCategory; -use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillTextareaType; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectManager; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class AccompanyingCourseDocumentType extends AbstractType +final class AccompanyingCourseDocumentType extends AbstractType { - /** - * @var AuthorizationHelper - */ - protected $authorizationHelper; - - /** - * @var ObjectManager - */ - protected $om; - - /** - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - /** - * the user running this form. - * - * @var User - */ - protected $user; - public function __construct( - TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelperInterface $translatableStringHelper ) { - $this->translatableStringHelper = $translatableStringHelper; } public function buildForm(FormBuilderInterface $builder, array $options) diff --git a/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php b/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php new file mode 100644 index 000000000..fd14897bf --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php @@ -0,0 +1,37 @@ +setDefault('entry_type', StoredObjectType::class) + ->setDefault('allow_add', true) + ->setDefault('allow_delete', true) + ->setDefault('button_add_label', 'stored_object.Insert a document') + ->setDefault('button_remove_label', 'stored_object.Remove a document') + ->setDefault('empty_collection_explain', 'No documents') + ->setDefault('entry_options', ['has_title' => true]) + ->setDefault('js_caller', 'data-collection-stored-object'); + } + + public function getParent() + { + return ChillCollectionType::class; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php new file mode 100644 index 000000000..2869522a0 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php @@ -0,0 +1,82 @@ +setData($viewData->getTitle()); + } + $forms['stored_object']->setData($viewData); + } + + /** + * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances + */ + public function mapFormsToData($forms, &$viewData) + { + $forms = iterator_to_array($forms); + + if (!(null === $viewData || $viewData instanceof StoredObject)) { + throw new Exception\UnexpectedTypeException($viewData, StoredObject::class); + } + + dump($forms['stored_object']->getData(), $viewData); + + if (null === $forms['stored_object']->getData()) { + + return; + } + + /** @var StoredObject $viewData */ + if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { + // we do not want to erase the previous object + $viewData = new StoredObject(); + } + + $viewData->setFilename($forms['stored_object']->getData()['filename']); + $viewData->setIv($forms['stored_object']->getData()['iv']); + $viewData->setKeyInfos($forms['stored_object']->getData()['keyInfos']); + $viewData->setType($forms['stored_object']->getData()['type']); + + if (array_key_exists('title', $forms)) { + $viewData->setTitle($forms['title']->getData()); + } + + dump($viewData); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php new file mode 100644 index 000000000..dfa1b6d4e --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php @@ -0,0 +1,52 @@ +serializer->serialize($value, 'json', [ + 'groups' => [ + StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT, + ], + ]); + } + + throw new UnexpectedTypeException($value, StoredObject::class); + } + + public function reverseTransform(mixed $value): mixed + { + if ('' === $value || null === $value) { + return null; + } + + return json_decode($value, true, 10, JSON_THROW_ON_ERROR); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php b/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php index 77484208f..5badc458d 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php +++ b/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php @@ -11,11 +11,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Form; -use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType; use Chill\DocStoreBundle\Entity\StoredObject; -use Doctrine\ORM\EntityManagerInterface; +use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper; +use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -24,16 +23,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** * Form type which allow to join a document. */ -class StoredObjectType extends AbstractType +final class StoredObjectType extends AbstractType { - /** - * @var EntityManagerInterface - */ - protected $em; - - public function __construct(EntityManagerInterface $em) - { - $this->em = $em; + public function __construct( + private readonly StoredObjectDataTransformer $storedObjectDataTransformer, + private readonly StoredObjectDataMapper $storedObjectDataMapper, + ) { } public function buildForm(FormBuilderInterface $builder, array $options) @@ -45,30 +40,9 @@ class StoredObjectType extends AbstractType ]); } - $builder - ->add('filename', AsyncUploaderType::class) - ->add('type', HiddenType::class) - ->add('keyInfos', HiddenType::class) - ->add('iv', HiddenType::class); - - $builder - ->get('keyInfos') - ->addModelTransformer(new CallbackTransformer( - $this->transform(...), - $this->reverseTransform(...) - )); - $builder - ->get('iv') - ->addModelTransformer(new CallbackTransformer( - $this->transform(...), - $this->reverseTransform(...) - )); - - $builder - ->addModelTransformer(new CallbackTransformer( - $this->transformObject(...), - $this->reverseTransformObject(...) - )); + $builder->add('stored_object', HiddenType::class); + $builder->get('stored_object')->addModelTransformer($this->storedObjectDataTransformer); + $builder->setDataMapper($this->storedObjectDataMapper); } public function configureOptions(OptionsResolver $resolver) @@ -80,43 +54,4 @@ class StoredObjectType extends AbstractType ->setDefault('has_title', false) ->setAllowedTypes('has_title', ['bool']); } - - public function reverseTransform($value) - { - if (null === $value) { - return null; - } - - return \json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR); - } - - public function reverseTransformObject($object) - { - if (null === $object) { - return null; - } - - if (null === $object->getFilename()) { - // remove the original object - $this->em->remove($object); - - return null; - } - - return $object; - } - - public function transform($object) - { - if (null === $object) { - return null; - } - - return \json_encode($object, JSON_THROW_ON_ERROR); - } - - public function transformObject($object = null) - { - return $object; - } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.js b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index-old.js similarity index 100% rename from src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.js rename to src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index-old.js diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts new file mode 100644 index 000000000..b7df11323 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts @@ -0,0 +1,86 @@ +import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection"; +import {createApp} from "vue"; +import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue" +import {StoredObject, StoredObjectCreated} from "../../types"; +import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; +const i18n = _createI18n({}); + +const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElement): void => { + console.log('app started', divElement); + const input_stored_object: HTMLInputElement|null = divElement.querySelector("input[data-stored-object]"); + if (null === input_stored_object) { + throw new Error('input to stored object not found'); + } + + let existingDoc: StoredObject|null = null; + if (input_stored_object.value !== "") { + existingDoc = JSON.parse(input_stored_object.value); + } + const app_container = document.createElement("div"); + divElement.appendChild(app_container); + + const app = createApp({ + template: '', + data(vm) { + return { + existingDoc: existingDoc, + } + }, + components: { + DropFileWidget, + }, + methods: { + addDocument: function(object: StoredObjectCreated): void { + console.log('object added', object); + this.$data.existingDoc = object; + input_stored_object.value = JSON.stringify(object); + }, + removeDocument: function(object: StoredObject): void { + console.log('catch remove document', object); + input_stored_object.value = ""; + this.$data.existingDoc = null; + console.log('collectionEntry', collectionEntry); + + if (null !== collectionEntry) { + console.log('will remove collection'); + collectionEntry.remove(); + } + } + } + }); + + app.use(i18n).mount(app_container); +} +window.addEventListener('collection-add-entry', ((e: CustomEvent) => { + const detail = e.detail; + const divElement: null|HTMLDivElement = detail.entry.querySelector('div[data-stored-object]'); + + if (null === divElement) { + throw new Error('div[data-stored-object] not found'); + } + + startApp(divElement, detail.entry); +}) as EventListener); + +window.addEventListener('DOMContentLoaded', () => { + const upload_inputs: NodeListOf = document.querySelectorAll('div[data-stored-object]'); + + upload_inputs.forEach((input: HTMLDivElement): void => { + // test for a parent to check if this is a collection entry + let collectionEntry: null|HTMLLIElement = null; + let parent = input.parentElement; + console.log('parent', parent); + if (null !== parent) { + let grandParent = parent.parentElement; + console.log('grandParent', grandParent); + if (null !== grandParent) { + if (grandParent.tagName.toLowerCase() === 'li' && grandParent.classList.contains('entry')) { + collectionEntry = grandParent as HTMLLIElement; + } + } + } + startApp(input, collectionEntry); + }) +}); + +export {} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 825055973..25d956312 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -17,6 +17,20 @@ export interface StoredObject { type: string, uuid: string, status: StoredObjectStatus, + _links?: { + dav_link?: { + href: string + expiration: number + }, + } +} + +export interface StoredObjectCreated { + status: "stored_object_created", + filename: string, + iv: Uint8Array, + keyInfos: object, + type: string, } export interface StoredObjectStatusChange { @@ -33,3 +47,18 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = { (): Promise } +/** + * Object containing information for performering a POST request to a swift object store + */ +export interface PostStoreObjectSignature { + method: "POST", + max_file_size: number, + max_file_count: 1, + expires: number, + submit_delay: 180, + redirect: string, + prefix: string, + url: string, + signature: string, +} + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue index 284ae0f1f..192cdd271 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -1,5 +1,5 @@