mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	refactor file drop widget
This commit is contained in:
		| @@ -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], | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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) }} | ||||
|     <div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div> | ||||
| {% endif %} | ||||
|  | ||||
| @@ -127,4 +129,4 @@ | ||||
|  | ||||
| {% block css %} | ||||
|     {{ encore_entry_link_tags('mod_pickentity_type') }} | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\ChillCollectionType; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
|  | ||||
| class CollectionStoredObjectType extends AbstractType | ||||
| { | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     { | ||||
|         $resolver | ||||
|             ->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; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Form\DataMapper; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Component\Form\DataMapperInterface; | ||||
| use Symfony\Component\Form\Exception; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
|  | ||||
| class StoredObjectDataMapper implements DataMapperInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private EntityManagerInterface $entityManager, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances | ||||
|      */ | ||||
|     public function mapDataToForms($viewData, $forms) | ||||
|     { | ||||
|         if (null === $viewData) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!$viewData instanceof StoredObject) { | ||||
|             throw new Exception\UnexpectedTypeException($viewData, StoredObject::class); | ||||
|         } | ||||
|  | ||||
|         $forms = iterator_to_array($forms); | ||||
|         if (array_key_exists('title', $forms)) { | ||||
|             $forms['title']->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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Form\DataTransformer; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer; | ||||
| use Symfony\Component\Form\DataTransformerInterface; | ||||
| use Symfony\Component\Form\Exception\UnexpectedTypeException; | ||||
| use Symfony\Component\Serializer\SerializerInterface; | ||||
|  | ||||
| class StoredObjectDataTransformer implements DataTransformerInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private SerializerInterface $serializer | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function transform(mixed $value): mixed | ||||
|     { | ||||
|         if (null === $value) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         if ($value instanceof StoredObject) { | ||||
|             return $this->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); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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: '<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>', | ||||
|         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<CollectionEventPayload>) => { | ||||
|     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<HTMLDivElement> = 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 {} | ||||
| @@ -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<void> | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div v-if="'ready' === props.storedObject.status" class="btn-group"> | ||||
|   <div v-if="'ready' === props.storedObject.status || 'stored_object_created' === props.storedObject.status" class="btn-group"> | ||||
|     <button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, 'btn-sm': props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|       Actions | ||||
|     </button> | ||||
| @@ -35,14 +35,14 @@ import DownloadButton from "./StoredObjectButton/DownloadButton.vue"; | ||||
| import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue"; | ||||
| import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers"; | ||||
| import { | ||||
|   StoredObject, | ||||
|   StoredObjectStatusChange, | ||||
|   WopiEditButtonExecutableBeforeLeaveFunction | ||||
|     StoredObject, StoredObjectCreated, | ||||
|     StoredObjectStatusChange, | ||||
|     WopiEditButtonExecutableBeforeLeaveFunction | ||||
| } from "../types"; | ||||
| import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue"; | ||||
|  | ||||
| interface DocumentActionButtonsGroupConfig { | ||||
|   storedObject: StoredObject, | ||||
|   storedObject: StoredObject|StoredObjectCreated, | ||||
|   small?: boolean, | ||||
|   canEdit?: boolean, | ||||
|   canDownload?: boolean, | ||||
| @@ -99,6 +99,7 @@ const checkForReady = function(): void { | ||||
|   if ( | ||||
|     'ready' === props.storedObject.status | ||||
|     || 'failure' === props.storedObject.status | ||||
|     || 'stored_object_created' === props.storedObject.status | ||||
|     // stop reloading if the page stays opened for a long time | ||||
|     || tryiesForReady > maxTryiesForReady | ||||
|   ) { | ||||
| @@ -111,6 +112,11 @@ const checkForReady = function(): void { | ||||
| }; | ||||
|  | ||||
| const onObjectNewStatusCallback = async function(): Promise<void> { | ||||
|  | ||||
|   if (props.storedObject.status === 'stored_object_created') { | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|  | ||||
|   const new_status = await is_object_ready(props.storedObject); | ||||
|   if (props.storedObject.status !== new_status.status) { | ||||
|     emit('onStoredObjectStatusChange', new_status); | ||||
|   | ||||
| @@ -0,0 +1,155 @@ | ||||
| <script setup lang="ts"> | ||||
|  | ||||
| import {StoredObject, StoredObjectCreated} from "../../types"; | ||||
| import {encryptFile, uploadFile} from "../_components/helper"; | ||||
| import {computed, ref, Ref} from "vue"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     existingDoc?: StoredObjectCreated|StoredObject, | ||||
| } | ||||
|  | ||||
| const props = defineProps<DropFileConfig>(); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     (e: 'addDocument', stored_object: StoredObjectCreated): void, | ||||
| }>(); | ||||
|  | ||||
| const is_dragging: Ref<boolean> = ref(false); | ||||
| const uploading: Ref<boolean> = ref(false); | ||||
|  | ||||
| const has_existing_doc = computed<boolean>(() => { | ||||
|     return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
| }); | ||||
|  | ||||
| const onDragOver = (e: Event) => { | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     is_dragging.value = true; | ||||
| } | ||||
|  | ||||
| const onDragLeave = (e: Event) => { | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     is_dragging.value = false; | ||||
| } | ||||
|  | ||||
| const onDrop = (e: DragEvent) => { | ||||
|     console.log('on drop', e); | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     const files = e.dataTransfer?.files; | ||||
|  | ||||
|     if (null === files || undefined === files) { | ||||
|         console.error("no files transferred", e.dataTransfer); | ||||
|         return; | ||||
|     } | ||||
|     if (files.length === 0) { | ||||
|         console.error("no files given"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     handleFile(files[0]) | ||||
| } | ||||
|  | ||||
| const onZoneClick = (e: Event) => { | ||||
|     e.stopPropagation(); | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     const input = document.createElement("input"); | ||||
|     input.type = "file"; | ||||
|     input.addEventListener("change", onFileChange); | ||||
|  | ||||
|     input.click(); | ||||
| } | ||||
|  | ||||
| const onFileChange = async (event: Event): Promise<void> => { | ||||
|     const input = event.target as HTMLInputElement; | ||||
|     console.log('event triggered', input); | ||||
|  | ||||
|     if (input.files && input.files[0]) { | ||||
|         console.log('file added', input.files[0]); | ||||
|         const file = input.files[0]; | ||||
|         await handleFile(file); | ||||
|  | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|  | ||||
|     throw 'No file given'; | ||||
| } | ||||
|  | ||||
| const handleFile = async (file: File): Promise<void> => { | ||||
|     uploading.value = true; | ||||
|     const type = file.type; | ||||
|     const buffer = await file.arrayBuffer(); | ||||
|     const [encrypted, iv, jsonWebKey] = await encryptFile(buffer); | ||||
|     const filename = await uploadFile(encrypted); | ||||
|  | ||||
|     console.log(iv, jsonWebKey); | ||||
|  | ||||
|     const storedObject: StoredObjectCreated = { | ||||
|         filename: filename, | ||||
|         iv, | ||||
|         keyInfos: jsonWebKey, | ||||
|         type: type, | ||||
|         status: "stored_object_created", | ||||
|     } | ||||
|  | ||||
|     emit('addDocument', storedObject); | ||||
|     uploading.value = false; | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <div class="drop-file"> | ||||
|         <div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop"> | ||||
|             <p v-if="has_existing_doc"> | ||||
|                 <i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i> | ||||
|                 <i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i> | ||||
|                 <i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i> | ||||
|                 <i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/msword'"></i> | ||||
|                 <i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i> | ||||
|                 <i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.ms-excel'"></i> | ||||
|                 <i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/jpeg'"></i> | ||||
|                 <i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/png'"></i> | ||||
|                 <i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i> | ||||
|                 <i class="fa fa-file-code-o" v-else ></i> | ||||
|             </p> | ||||
|             <!-- todo i18n --> | ||||
|             <p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p> | ||||
|             <p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p> | ||||
|         </div> | ||||
|         <div v-else class="waiting"> | ||||
|             <i class="fa fa-cog fa-spin fa-3x fa-fw"></i> | ||||
|             <span class="sr-only">Loading...</span> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .drop-file { | ||||
|     width: 100%; | ||||
|  | ||||
|     & > .area, & > .waiting { | ||||
|         width: 100%; | ||||
|         height: 8rem; | ||||
|  | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|     } | ||||
|  | ||||
|     & > .area { | ||||
|         border: 4px dashed #ccc; | ||||
|  | ||||
|         &.dragging { | ||||
|             border: 4px dashed blue; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| div.chill-collection ul.list-entry li.entry:nth-child(2n) { | ||||
|  | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,83 @@ | ||||
| <script setup lang="ts"> | ||||
|  | ||||
| import {StoredObject, StoredObjectCreated} from "../../types"; | ||||
| import {computed, ref, Ref} from "vue"; | ||||
| import DropFile from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFile.vue"; | ||||
| import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     allowRemove: boolean, | ||||
|     existingDoc?: StoredObjectCreated|StoredObject, | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DropFileConfig>(), { | ||||
|     allowRemove: false, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|     (e: 'addDocument', stored_object: StoredObjectCreated): void, | ||||
|     (e: 'removeDocument', stored_object: null): void | ||||
| }>(); | ||||
|  | ||||
| const has_existing_doc = computed<boolean>(() => { | ||||
|     return props.existingDoc !== undefined && props.existingDoc !== null; | ||||
| }); | ||||
|  | ||||
| const dav_link_expiration = computed<number|undefined>(() => { | ||||
|     if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|         return undefined; | ||||
|     } | ||||
|     if (props.existingDoc.status !== 'ready') { | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|     return props.existingDoc._links?.dav_link?.expiration; | ||||
| }); | ||||
|  | ||||
| const dav_link_href = computed<string|undefined>(() => { | ||||
|     if (props.existingDoc === undefined || props.existingDoc === null) { | ||||
|         return undefined; | ||||
|     } | ||||
|     if (props.existingDoc.status !== 'ready') { | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|     return props.existingDoc._links?.dav_link?.href; | ||||
| }) | ||||
|  | ||||
| const onAddDocument = (s: StoredObjectCreated): void => { | ||||
|     emit('addDocument', s); | ||||
| } | ||||
|  | ||||
| const onRemoveDocument = (e: Event): void => { | ||||
|     e.stopPropagation(); | ||||
|     e.preventDefault(); | ||||
|     emit('removeDocument', null); | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <div> | ||||
|         <drop-file :existingDoc="props.existingDoc" @addDocument="onAddDocument"></drop-file> | ||||
|  | ||||
|         <ul class="record_actions"> | ||||
|             <li v-if="has_existing_doc"> | ||||
|                 <document-action-buttons-group | ||||
|                     :stored-object="props.existingDoc" | ||||
|                     :can-edit="props.existingDoc?.status === 'ready'" | ||||
|                     :can-download="true" | ||||
|                     :dav-link="dav_link_href" | ||||
|                     :dav-link-expiration="dav_link_expiration" | ||||
|                 /> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <button v-if="allowRemove" class="btn btn-delete" @click="onRemoveDocument($event)" ></button> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
|  | ||||
| </style> | ||||
| @@ -10,10 +10,10 @@ | ||||
| import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers"; | ||||
| import mime from "mime"; | ||||
| import {reactive} from "vue"; | ||||
| import {StoredObject} from "../../types"; | ||||
| import {StoredObject, StoredObjectCreated} from "../../types"; | ||||
|  | ||||
| interface ConvertButtonConfig { | ||||
|   storedObject: StoredObject, | ||||
|   storedObject: StoredObject|StoredObjectCreated, | ||||
|   classes: { [key: string]: boolean}, | ||||
|   filename?: string, | ||||
| }; | ||||
|   | ||||
| @@ -13,10 +13,10 @@ | ||||
| import {reactive, ref, nextTick, onMounted} from "vue"; | ||||
| import {build_download_info_link, download_and_decrypt_doc} from "./helpers"; | ||||
| import mime from "mime"; | ||||
| import {StoredObject} from "../../types"; | ||||
| import {StoredObject, StoredObjectCreated} from "../../types"; | ||||
|  | ||||
| interface DownloadButtonConfig { | ||||
|     storedObject: StoredObject, | ||||
|     storedObject: StoredObject|StoredObjectCreated, | ||||
|     classes: { [k: string]: boolean }, | ||||
|     filename?: string, | ||||
| } | ||||
|   | ||||
| @@ -8,10 +8,10 @@ | ||||
| <script lang="ts" setup> | ||||
| import WopiEditButton from "./WopiEditButton.vue"; | ||||
| import {build_wopi_editor_link} from "./helpers"; | ||||
| import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types"; | ||||
| import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types"; | ||||
|  | ||||
| interface WopiEditButtonConfig { | ||||
|   storedObject: StoredObject, | ||||
|   storedObject: StoredObject|StoredObjectCreated, | ||||
|   returnPath?: string, | ||||
|   classes: {[k: string] : boolean}, | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, | ||||
|   | ||||
| @@ -0,0 +1,60 @@ | ||||
| import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; | ||||
| import {PostStoreObjectSignature} from "../../types"; | ||||
|  | ||||
| const algo = 'AES-CBC'; | ||||
|  | ||||
| const URL_POST = '/asyncupload/temp_url/generate/post'; | ||||
|  | ||||
| const keyDefinition = { | ||||
|     name: algo, | ||||
|     length: 256 | ||||
| }; | ||||
|  | ||||
| const createFilename = (): string => { | ||||
|     var text = ""; | ||||
|     var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||||
|  | ||||
|     for (let i = 0; i < 7; i++) { | ||||
|         text += possible.charAt(Math.floor(Math.random() * possible.length)); | ||||
|     } | ||||
|  | ||||
|     return text; | ||||
| }; | ||||
|  | ||||
| export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => { | ||||
|     const params = new URLSearchParams(); | ||||
|     params.append('expires_delay', "180"); | ||||
|     params.append('submit_delay', "180"); | ||||
|     const asyncData: PostStoreObjectSignature = await makeFetch("GET", URL_POST + "?" + params.toString()); | ||||
|     const suffix = createFilename(); | ||||
|     const filename = asyncData.prefix + suffix; | ||||
|     const formData = new FormData(); | ||||
|     formData.append("redirect", asyncData.redirect); | ||||
|     formData.append("max_file_size", asyncData.max_file_size.toString()); | ||||
|     formData.append("max_file_count", asyncData.max_file_count.toString()); | ||||
|     formData.append("expires", asyncData.expires.toString()); | ||||
|     formData.append("signature", asyncData.signature); | ||||
|     formData.append(filename, new Blob([uploadFile]), suffix); | ||||
|  | ||||
|     const response = await window.fetch(asyncData.url, { | ||||
|         method: "POST", | ||||
|         body: formData, | ||||
|     }) | ||||
|  | ||||
|     if (!response.ok) { | ||||
|         console.error("Error while sending file to store", response); | ||||
|         throw new Error(response.statusText); | ||||
|     } | ||||
|  | ||||
|     return Promise.resolve(filename); | ||||
| } | ||||
|  | ||||
| export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => { | ||||
|     console.log('encrypt', originalFile); | ||||
|     const iv = crypto.getRandomValues(new Uint8Array(16)); | ||||
|     const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]); | ||||
|     const exportedKey = await window.crypto.subtle.exportKey('jwk', key); | ||||
|     const encrypted = await window.crypto.subtle.encrypt({ name: algo, iv: iv}, key, originalFile); | ||||
|  | ||||
|     return Promise.resolve([encrypted, iv, exportedKey]); | ||||
| }; | ||||
| @@ -1,23 +1,7 @@ | ||||
| {% block stored_object_widget %} | ||||
|     {% if form.title is defined %} {{ form_row(form.title) }} {% endif %} | ||||
|     <div | ||||
|         data-stored-object="data-stored-object" | ||||
|         data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}" | ||||
|         data-label-quiet-button="{{ 'Download existing file'|trans|escape('html_attr') }}" | ||||
|         data-label-ready="{{ 'Ready to show'|trans|escape('html_attr') }}" | ||||
|         data-dict-file-too-big="{{ 'File too big'|trans|escape('html_attr') }}" | ||||
|         data-dict-default-message="{{ "Drop your file or click here"|trans|escape('html_attr') }}" | ||||
|         data-dict-remove-file="{{ 'Remove file in order to upload a new one'|trans|escape('html_attr') }}" | ||||
|         data-dict-max-files-exceeded="{{ 'Max files exceeded. Remove previous files'|trans|escape('html_attr') }}" | ||||
|         data-dict-cancel-upload="{{ 'Cancel upload'|trans|escape('html_attr') }}" | ||||
|         data-dict-cancel-upload-confirm="{{ 'Are you sure you want to cancel this upload ?'|trans|escape('html_attr') }}" | ||||
|         data-dict-upload-canceled="{{ 'Upload canceled'|trans|escape('html_attr') }}" | ||||
|         data-dict-remove="{{ 'Remove existing file'|trans|escape('html_attr') }}" | ||||
|         data-allow-remove="{% if required %}false{% else %}true{% endif %}" | ||||
|         data-temp-url-generator="{{ path('async_upload.generate_url', { 'method': 'GET' })|escape('html_attr') }}"> | ||||
|         {{ form_widget(form.filename) }} | ||||
|         {{ form_widget(form.keyInfos, { 'attr': { 'data-stored-object-key': 1 } }) }} | ||||
|         {{ form_widget(form.iv, { 'attr': { 'data-stored-object-iv': 1 } }) }} | ||||
|         {{ form_widget(form.type, { 'attr': { 'data-async-file-type': 1 } }) }} | ||||
|         data-stored-object="data-stored-object"> | ||||
|         {{ form_widget(form.stored_object, { 'attr': { 'data-stored-object': 1 } }) }} | ||||
|     </div> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -3,6 +3,6 @@ module.exports = function(encore) | ||||
|     encore.addAliases({ | ||||
|         ChillDocStoreAssets: __dirname + '/Resources/public' | ||||
|     }); | ||||
|     encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.js'); | ||||
|     encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts'); | ||||
|     encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index'); | ||||
| }; | ||||
|   | ||||
| @@ -1,13 +1,18 @@ | ||||
| services: | ||||
|   Chill\DocStoreBundle\Form\StoredObjectType: | ||||
|     arguments: | ||||
|       $em: '@Doctrine\ORM\EntityManagerInterface' | ||||
|     tags: | ||||
|       - { name: form.type } | ||||
|     _defaults: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|  | ||||
|   Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType: | ||||
|     class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType | ||||
|     arguments: | ||||
|       - "@chill.main.helper.translatable_string" | ||||
|     tags: | ||||
|       - { name: form.type, alias: chill_docstorebundle_form_document } | ||||
|     Chill\DocStoreBundle\Form\StoredObjectType: | ||||
|         tags: | ||||
|             - { name: form.type } | ||||
|  | ||||
|     Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType: | ||||
|         tags: | ||||
|             - { name: form.type, alias: chill_docstorebundle_form_document } | ||||
|  | ||||
|     Chill\DocStoreBundle\Form\DataMapper\: | ||||
|         resource: '../../Form/DataMapper' | ||||
|  | ||||
|     Chill\DocStoreBundle\Form\DataTransformer\: | ||||
|         resource: '../../Form/DataTransformer' | ||||
|   | ||||
| @@ -46,6 +46,9 @@ Are you sure you want to cancel this upload ?: Êtes-vous sûrs de vouloir annul | ||||
| Upload canceled: Téléversement annulé | ||||
| Remove existing file: Supprimer le document existant | ||||
|  | ||||
| stored_object: | ||||
|     Insert a document: Ajouter un document | ||||
|  | ||||
| # ROLES | ||||
| PersonDocument: Documents | ||||
| CHILL_PERSON_DOCUMENT_CREATE: Ajouter un document | ||||
|   | ||||
| @@ -35,6 +35,7 @@ class ChillCollectionType extends AbstractType | ||||
|         $view->vars['allow_add'] = (int) $options['allow_add']; | ||||
|         $view->vars['identifier'] = $options['identifier']; | ||||
|         $view->vars['empty_collection_explain'] = $options['empty_collection_explain']; | ||||
|         $view->vars['js_caller'] = $options['js_caller']; | ||||
|     } | ||||
|  | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
| @@ -45,6 +46,8 @@ class ChillCollectionType extends AbstractType | ||||
|                 'button_remove_label' => 'Remove entry', | ||||
|                 'identifier' => '', | ||||
|                 'empty_collection_explain' => '', | ||||
|                 'js_caller' => 'data-collection-regular', | ||||
|                 'delete_empty' => true, | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -41,8 +41,6 @@ require('./img/logo-chill-outil-accompagnement_white.png'); | ||||
|  * Some libs are only used in a few pages, they are loaded on a case by case basis | ||||
|  */ | ||||
|  | ||||
| require('../lib/collection/index.js'); | ||||
|  | ||||
| require('../lib/breadcrumb/index.js'); | ||||
| require('../lib/download-report/index.js'); | ||||
| require('../lib/select_interactive_loading/index.js'); | ||||
|   | ||||
| @@ -1,120 +0,0 @@ | ||||
| /** | ||||
|  * Javascript file which handle ChillCollectionType | ||||
|  * | ||||
|  * Two events are emitted by this module, both on window and on collection / ul. | ||||
|  * | ||||
|  * Collection (an UL element) and entry (a li element) are associated with those | ||||
|  * events. | ||||
|  * | ||||
|  * ``` | ||||
|  * window.addEventListener('collection-add-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * window.addEventListener('collection-remove-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * collection.addEventListener('collection-add-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * collection.addEventListener('collection-remove-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * ``` | ||||
|  */ | ||||
| require('./collection.scss'); | ||||
|  | ||||
| class CollectionEvent { | ||||
|     constructor(collection, entry) { | ||||
|         this.collection = collection; | ||||
|         this.entry = entry; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {type} button | ||||
|  * @returns {handleAdd} | ||||
|  */ | ||||
| var handleAdd = function(button) { | ||||
|     var | ||||
|         form_name = button.dataset.collectionAddTarget, | ||||
|         prototype = button.dataset.formPrototype, | ||||
|         collection = document.querySelector('ul[data-collection-name="'+form_name+'"]'), | ||||
|         empty_explain = collection.querySelector('li[data-collection-empty-explain]'), | ||||
|         entry = document.createElement('li'), | ||||
|         event = new CustomEvent('collection-add-entry', { detail: { collection: collection, entry: entry } }), | ||||
|         counter = collection.childNodes.length + parseInt(Math.random() * 1000000) | ||||
|         content | ||||
|         ; | ||||
|     content = prototype.replace(new RegExp('__name__', 'g'), counter); | ||||
|     entry.innerHTML = content; | ||||
|     entry.classList.add('entry'); | ||||
|     initializeRemove(collection, entry); | ||||
|     if (empty_explain !== null) { | ||||
|         empty_explain.remove(); | ||||
|     } | ||||
|     collection.appendChild(entry); | ||||
|  | ||||
|     collection.dispatchEvent(event); | ||||
|     window.dispatchEvent(event); | ||||
| }; | ||||
|  | ||||
| var initializeRemove = function(collection, entry) { | ||||
|     var | ||||
|         button = document.createElement('button'), | ||||
|         isPersisted = entry.dataset.collectionIsPersisted, | ||||
|         content = collection.dataset.collectionButtonRemoveLabel, | ||||
|         allowDelete = collection.dataset.collectionAllowDelete, | ||||
|         event = new CustomEvent('collection-remove-entry', { detail: { collection: collection, entry: entry } }) | ||||
|         ; | ||||
|  | ||||
|     if (allowDelete === '0' && isPersisted === '1') { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     button.classList.add('btn', 'btn-delete', 'remove-entry'); | ||||
|     button.textContent = content; | ||||
|  | ||||
|     button.addEventListener('click', function(e) { | ||||
|         e.preventDefault(); | ||||
|         entry.remove(); | ||||
|         collection.dispatchEvent(event); | ||||
|         window.dispatchEvent(event); | ||||
|     }); | ||||
|  | ||||
|     entry.appendChild(button); | ||||
| }; | ||||
|  | ||||
| window.addEventListener('load', function() { | ||||
|     var | ||||
|         addButtons = document.querySelectorAll("button[data-collection-add-target]"), | ||||
|         collections = document.querySelectorAll("ul[data-collection-name]") | ||||
|         ; | ||||
|  | ||||
|     for (let i = 0; i < addButtons.length; i ++) { | ||||
|         let addButton = addButtons[i]; | ||||
|         addButton.addEventListener('click', function(e) { | ||||
|             e.preventDefault(); | ||||
|             handleAdd(e.target); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     for (let i = 0; i < collections.length; i ++) { | ||||
|         let entries = collections[i].querySelectorAll(':scope > li'); | ||||
|  | ||||
|         for (let j = 0; j < entries.length; j ++) { | ||||
|             console.log(entries[j].dataset); | ||||
|             if (entries[j].dataset.collectionEmptyExplain === "1") { | ||||
|                 continue; | ||||
|             } | ||||
|             initializeRemove(collections[i], entries[j]); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| @@ -0,0 +1,128 @@ | ||||
| /** | ||||
|  * Javascript file which handle ChillCollectionType | ||||
|  * | ||||
|  * Two events are emitted by this module, both on window and on collection / ul. | ||||
|  * | ||||
|  * Collection (an UL element) and entry (a li element) are associated with those | ||||
|  * events. | ||||
|  * | ||||
|  * ``` | ||||
|  * window.addEventListener('collection-add-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * window.addEventListener('collection-remove-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * collection.addEventListener('collection-add-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * | ||||
|  * collection.addEventListener('collection-remove-entry', function(e) { | ||||
|  *   console.log(e.detail.collection); | ||||
|  *   console.log(e.detail.entry); | ||||
|  * }); | ||||
|  * ``` | ||||
|  */ | ||||
| import './collection.scss'; | ||||
|  | ||||
| export class CollectionEventPayload { | ||||
|     collection: HTMLUListElement; | ||||
|     entry: HTMLLIElement; | ||||
|  | ||||
|     constructor(collection: HTMLUListElement, entry: HTMLLIElement) { | ||||
|         this.collection = collection; | ||||
|         this.entry = entry; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const handleAdd = (button: any): void => { | ||||
|     let | ||||
|         form_name = button.dataset.collectionAddTarget, | ||||
|         prototype = button.dataset.formPrototype, | ||||
|         collection: HTMLUListElement | null = document.querySelector('ul[data-collection-name="' + form_name + '"]'); | ||||
|  | ||||
|     if (collection === null) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let | ||||
|         empty_explain: HTMLLIElement | null = collection.querySelector('li[data-collection-empty-explain]'), | ||||
|         entry = document.createElement('li'), | ||||
|         counter = collection.childNodes.length + 1, | ||||
|         content = prototype.replace(new RegExp('__name__', 'g'), counter.toString()), | ||||
|         event = new CustomEvent('collection-add-entry', {detail: new CollectionEventPayload(collection, entry)}); | ||||
|  | ||||
|     entry.innerHTML = content; | ||||
|     entry.classList.add('entry'); | ||||
|  | ||||
|     if ("dataCollectionRegular" in collection.dataset) { | ||||
|         initializeRemove(collection, entry); | ||||
|         if (empty_explain !== null) { | ||||
|             empty_explain.remove(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     collection.appendChild(entry); | ||||
|     collection.dispatchEvent(event); | ||||
|     window.dispatchEvent(event); | ||||
| }; | ||||
|  | ||||
| const initializeRemove = (collection: HTMLUListElement, entry: HTMLLIElement): void => { | ||||
|     const button = buildRemoveButton(collection, entry); | ||||
|     if (null === button) { | ||||
|         return; | ||||
|     } | ||||
|     entry.appendChild(button); | ||||
| }; | ||||
|  | ||||
| export const buildRemoveButton = (collection: HTMLUListElement, entry: HTMLLIElement): HTMLButtonElement|null => { | ||||
|  | ||||
|     let | ||||
|         button = document.createElement('button'), | ||||
|         isPersisted = entry.dataset.collectionIsPersisted || '', | ||||
|         content = collection.dataset.collectionButtonRemoveLabel || '', | ||||
|         allowDelete = collection.dataset.collectionAllowDelete || '', | ||||
|         event = new CustomEvent('collection-remove-entry', {detail: new CollectionEventPayload(collection, entry)}); | ||||
|  | ||||
|     if (allowDelete === '0' && isPersisted === '1') { | ||||
|         return null; | ||||
|     } | ||||
|     button.classList.add('btn', 'btn-delete', 'remove-entry'); | ||||
|     button.textContent = content; | ||||
|     button.addEventListener('click', (e: Event) => { | ||||
|         e.preventDefault(); | ||||
|         entry.remove(); | ||||
|         collection.dispatchEvent(event); | ||||
|         window.dispatchEvent(event); | ||||
|     }); | ||||
|  | ||||
|     return button; | ||||
| } | ||||
|  | ||||
| window.addEventListener('load', () => { | ||||
|     let | ||||
|         addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll("button[data-collection-add-target]"), | ||||
|         collections: NodeListOf<HTMLUListElement> = document.querySelectorAll("ul[data-collection-regular]"); | ||||
|  | ||||
|     for (let i = 0; i < addButtons.length; i++) { | ||||
|         let addButton = addButtons[i]; | ||||
|         addButton.addEventListener('click', (e: Event) => { | ||||
|             e.preventDefault(); | ||||
|             handleAdd(e.target); | ||||
|         }); | ||||
|     } | ||||
|     for (let i = 0; i < collections.length; i++) { | ||||
|         let entries: NodeListOf<HTMLLIElement> = collections[i].querySelectorAll(':scope > li'); | ||||
|         for (let j = 0; j < entries.length; j++) { | ||||
|             if (entries[j].dataset.collectionEmptyExplain === "1") { | ||||
|                 continue; | ||||
|             } | ||||
|             initializeRemove(collections[i], entries[j]); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| @@ -162,6 +162,7 @@ | ||||
| {% block chill_collection_widget %} | ||||
|     <div class="chill-collection"> | ||||
|         <ul class="list-entry" | ||||
|             {{ form.vars.js_caller }}="{{ form.vars.js_caller }}" | ||||
|             data-collection-name="{{ form.vars.name|escape('html_attr') }}" | ||||
|             data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}" | ||||
|             data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}" | ||||
| @@ -173,7 +174,7 @@ | ||||
|                         {{ form_widget(entry) }} | ||||
|                         {{ form_errors(entry) }} | ||||
|                     </div> | ||||
|                 </li>  | ||||
|                 </li> | ||||
|             {% else %} | ||||
|                 <li data-collection-empty-explain="1"> | ||||
|                     <span class="chill-no-data-statement">{{ form.vars.empty_collection_explain|default('No entities')|trans }}</span> | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|         window.addaddress = {{ add_address|json_encode|raw }}; | ||||
|     </script> | ||||
|  | ||||
|     {{ encore_entry_link_tags('mod_collection') }} | ||||
|     {{ encore_entry_link_tags('mod_bootstrap') }} | ||||
|     {{ encore_entry_link_tags('mod_forkawesome') }} | ||||
|     {{ encore_entry_link_tags('mod_ckeditor5') }} | ||||
| @@ -107,6 +108,7 @@ | ||||
|  | ||||
|     {{ include('@ChillMain/Layout/_footer.html.twig') }} | ||||
|  | ||||
|     {{ encore_entry_script_tags('mod_collection') }} | ||||
|     {{ encore_entry_script_tags('mod_bootstrap') }} | ||||
|     {{ encore_entry_script_tags('mod_forkawesome') }} | ||||
|     {{ encore_entry_script_tags('mod_ckeditor5') }} | ||||
|   | ||||
| @@ -62,6 +62,7 @@ module.exports = function(encore, entries) | ||||
|     buildCKEditor(encore); | ||||
|  | ||||
|     // Modules entrypoints | ||||
|     encore.addEntry('mod_collection', __dirname + '/Resources/public/module/collection/index.ts'); | ||||
|     encore.addEntry('mod_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js'); | ||||
|     encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js'); | ||||
|     encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js'); | ||||
|   | ||||
| @@ -135,8 +135,8 @@ | ||||
|                          :filename="d.title" | ||||
|                          :can-edit="true" | ||||
|                          :execute-before-leave="submitBeforeLeaveToEditor" | ||||
|                          :davLink="d.storedObject._links.dav_link.href" | ||||
|                          :davLinkExpiration="d.storedObject._links.dav_link.expiration" | ||||
|                          :davLink="d.storedObject._links?.dav_link.href" | ||||
|                          :davLinkExpiration="d.storedObject._links?.dav_link.expiration" | ||||
|                          @on-stored-object-status-change="onStatusDocumentChanged" | ||||
|                      ></document-action-buttons-group> | ||||
|                    </li> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user