mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
refactor file drop widget
This commit is contained in:
parent
47a928a6cd
commit
775535e683
@ -15,11 +15,10 @@ use Chill\ActivityBundle\Entity\Activity;
|
|||||||
use Chill\ActivityBundle\Entity\ActivityPresence;
|
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||||
use Chill\ActivityBundle\Form\Type\PickActivityReasonType;
|
use Chill\ActivityBundle\Form\Type\PickActivityReasonType;
|
||||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
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\Center;
|
||||||
use Chill\MainBundle\Entity\Location;
|
use Chill\MainBundle\Entity\Location;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
use Chill\MainBundle\Form\Type\CommentType;
|
use Chill\MainBundle\Form\Type\CommentType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
@ -276,16 +275,9 @@ class ActivityType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($activityType->isVisible('documents')) {
|
if ($activityType->isVisible('documents')) {
|
||||||
$builder->add('documents', ChillCollectionType::class, [
|
$builder->add('documents', CollectionStoredObjectType::class, [
|
||||||
'entry_type' => StoredObjectType::class,
|
|
||||||
'label' => $activityType->getLabel('documents'),
|
'label' => $activityType->getLabel('documents'),
|
||||||
'required' => $activityType->isRequired('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 %}
|
{% endif %}
|
||||||
|
|
||||||
{%- if edit_form.documents is defined -%}
|
{%- 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>
|
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -14,47 +14,21 @@ namespace Chill\DocStoreBundle\Form;
|
|||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
use Chill\DocStoreBundle\Entity\Document;
|
use Chill\DocStoreBundle\Entity\Document;
|
||||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
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(
|
public function __construct(
|
||||||
TranslatableStringHelper $translatableStringHelper
|
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||||
) {
|
) {
|
||||||
$this->translatableStringHelper = $translatableStringHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
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;
|
namespace Chill\DocStoreBundle\Form;
|
||||||
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
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\AbstractType;
|
||||||
use Symfony\Component\Form\CallbackTransformer;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@ -24,16 +23,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
/**
|
/**
|
||||||
* Form type which allow to join a document.
|
* Form type which allow to join a document.
|
||||||
*/
|
*/
|
||||||
class StoredObjectType extends AbstractType
|
final class StoredObjectType extends AbstractType
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* @var EntityManagerInterface
|
private readonly StoredObjectDataTransformer $storedObjectDataTransformer,
|
||||||
*/
|
private readonly StoredObjectDataMapper $storedObjectDataMapper,
|
||||||
protected $em;
|
) {
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
|
||||||
{
|
|
||||||
$this->em = $em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
@ -45,30 +40,9 @@ class StoredObjectType extends AbstractType
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder
|
$builder->add('stored_object', HiddenType::class);
|
||||||
->add('filename', AsyncUploaderType::class)
|
$builder->get('stored_object')->addModelTransformer($this->storedObjectDataTransformer);
|
||||||
->add('type', HiddenType::class)
|
$builder->setDataMapper($this->storedObjectDataMapper);
|
||||||
->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(...)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
@ -80,43 +54,4 @@ class StoredObjectType extends AbstractType
|
|||||||
->setDefault('has_title', false)
|
->setDefault('has_title', false)
|
||||||
->setAllowedTypes('has_title', ['bool']);
|
->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,
|
type: string,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
status: StoredObjectStatus,
|
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 {
|
export interface StoredObjectStatusChange {
|
||||||
@ -33,3 +47,18 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
|||||||
(): Promise<void>
|
(): 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>
|
<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">
|
<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
|
Actions
|
||||||
</button>
|
</button>
|
||||||
@ -35,14 +35,14 @@ import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
|||||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||||
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
||||||
import {
|
import {
|
||||||
StoredObject,
|
StoredObject, StoredObjectCreated,
|
||||||
StoredObjectStatusChange,
|
StoredObjectStatusChange,
|
||||||
WopiEditButtonExecutableBeforeLeaveFunction
|
WopiEditButtonExecutableBeforeLeaveFunction
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||||
|
|
||||||
interface DocumentActionButtonsGroupConfig {
|
interface DocumentActionButtonsGroupConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject|StoredObjectCreated,
|
||||||
small?: boolean,
|
small?: boolean,
|
||||||
canEdit?: boolean,
|
canEdit?: boolean,
|
||||||
canDownload?: boolean,
|
canDownload?: boolean,
|
||||||
@ -99,6 +99,7 @@ const checkForReady = function(): void {
|
|||||||
if (
|
if (
|
||||||
'ready' === props.storedObject.status
|
'ready' === props.storedObject.status
|
||||||
|| 'failure' === props.storedObject.status
|
|| 'failure' === props.storedObject.status
|
||||||
|
|| 'stored_object_created' === props.storedObject.status
|
||||||
// stop reloading if the page stays opened for a long time
|
// stop reloading if the page stays opened for a long time
|
||||||
|| tryiesForReady > maxTryiesForReady
|
|| tryiesForReady > maxTryiesForReady
|
||||||
) {
|
) {
|
||||||
@ -111,6 +112,11 @@ const checkForReady = function(): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onObjectNewStatusCallback = async function(): Promise<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);
|
const new_status = await is_object_ready(props.storedObject);
|
||||||
if (props.storedObject.status !== new_status.status) {
|
if (props.storedObject.status !== new_status.status) {
|
||||||
emit('onStoredObjectStatusChange', new_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 {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import {StoredObject} from "../../types";
|
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||||
|
|
||||||
interface ConvertButtonConfig {
|
interface ConvertButtonConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject|StoredObjectCreated,
|
||||||
classes: { [key: string]: boolean},
|
classes: { [key: string]: boolean},
|
||||||
filename?: string,
|
filename?: string,
|
||||||
};
|
};
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
import {reactive, ref, nextTick, onMounted} from "vue";
|
import {reactive, ref, nextTick, onMounted} from "vue";
|
||||||
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
|
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import {StoredObject} from "../../types";
|
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||||
|
|
||||||
interface DownloadButtonConfig {
|
interface DownloadButtonConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject|StoredObjectCreated,
|
||||||
classes: { [k: string]: boolean },
|
classes: { [k: string]: boolean },
|
||||||
filename?: string,
|
filename?: string,
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import WopiEditButton from "./WopiEditButton.vue";
|
import WopiEditButton from "./WopiEditButton.vue";
|
||||||
import {build_wopi_editor_link} from "./helpers";
|
import {build_wopi_editor_link} from "./helpers";
|
||||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||||
|
|
||||||
interface WopiEditButtonConfig {
|
interface WopiEditButtonConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject|StoredObjectCreated,
|
||||||
returnPath?: string,
|
returnPath?: string,
|
||||||
classes: {[k: string] : boolean},
|
classes: {[k: string] : boolean},
|
||||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
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 %}
|
{% block stored_object_widget %}
|
||||||
{% if form.title is defined %} {{ form_row(form.title) }} {% endif %}
|
{% if form.title is defined %} {{ form_row(form.title) }} {% endif %}
|
||||||
<div
|
<div
|
||||||
data-stored-object="data-stored-object"
|
data-stored-object="data-stored-object">
|
||||||
data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}"
|
{{ form_widget(form.stored_object, { 'attr': { 'data-stored-object': 1 } }) }}
|
||||||
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 } }) }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,6 +3,6 @@ module.exports = function(encore)
|
|||||||
encore.addAliases({
|
encore.addAliases({
|
||||||
ChillDocStoreAssets: __dirname + '/Resources/public'
|
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');
|
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
services:
|
services:
|
||||||
Chill\DocStoreBundle\Form\StoredObjectType:
|
_defaults:
|
||||||
arguments:
|
autowire: true
|
||||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
autoconfigure: true
|
||||||
tags:
|
|
||||||
- { name: form.type }
|
|
||||||
|
|
||||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
Chill\DocStoreBundle\Form\StoredObjectType:
|
||||||
class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType
|
tags:
|
||||||
arguments:
|
- { name: form.type }
|
||||||
- "@chill.main.helper.translatable_string"
|
|
||||||
tags:
|
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
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é
|
Upload canceled: Téléversement annulé
|
||||||
Remove existing file: Supprimer le document existant
|
Remove existing file: Supprimer le document existant
|
||||||
|
|
||||||
|
stored_object:
|
||||||
|
Insert a document: Ajouter un document
|
||||||
|
|
||||||
# ROLES
|
# ROLES
|
||||||
PersonDocument: Documents
|
PersonDocument: Documents
|
||||||
CHILL_PERSON_DOCUMENT_CREATE: Ajouter un document
|
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['allow_add'] = (int) $options['allow_add'];
|
||||||
$view->vars['identifier'] = $options['identifier'];
|
$view->vars['identifier'] = $options['identifier'];
|
||||||
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
||||||
|
$view->vars['js_caller'] = $options['js_caller'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
@ -45,6 +46,8 @@ class ChillCollectionType extends AbstractType
|
|||||||
'button_remove_label' => 'Remove entry',
|
'button_remove_label' => 'Remove entry',
|
||||||
'identifier' => '',
|
'identifier' => '',
|
||||||
'empty_collection_explain' => '',
|
'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
|
* 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/breadcrumb/index.js');
|
||||||
require('../lib/download-report/index.js');
|
require('../lib/download-report/index.js');
|
||||||
require('../lib/select_interactive_loading/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 %}
|
{% block chill_collection_widget %}
|
||||||
<div class="chill-collection">
|
<div class="chill-collection">
|
||||||
<ul class="list-entry"
|
<ul class="list-entry"
|
||||||
|
{{ form.vars.js_caller }}="{{ form.vars.js_caller }}"
|
||||||
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
||||||
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
||||||
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
window.addaddress = {{ add_address|json_encode|raw }};
|
window.addaddress = {{ add_address|json_encode|raw }};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{{ encore_entry_link_tags('mod_collection') }}
|
||||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||||
{{ encore_entry_link_tags('mod_ckeditor5') }}
|
{{ encore_entry_link_tags('mod_ckeditor5') }}
|
||||||
@ -107,6 +108,7 @@
|
|||||||
|
|
||||||
{{ include('@ChillMain/Layout/_footer.html.twig') }}
|
{{ include('@ChillMain/Layout/_footer.html.twig') }}
|
||||||
|
|
||||||
|
{{ encore_entry_script_tags('mod_collection') }}
|
||||||
{{ encore_entry_script_tags('mod_bootstrap') }}
|
{{ encore_entry_script_tags('mod_bootstrap') }}
|
||||||
{{ encore_entry_script_tags('mod_forkawesome') }}
|
{{ encore_entry_script_tags('mod_forkawesome') }}
|
||||||
{{ encore_entry_script_tags('mod_ckeditor5') }}
|
{{ encore_entry_script_tags('mod_ckeditor5') }}
|
||||||
|
@ -62,6 +62,7 @@ module.exports = function(encore, entries)
|
|||||||
buildCKEditor(encore);
|
buildCKEditor(encore);
|
||||||
|
|
||||||
// Modules entrypoints
|
// 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_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js');
|
||||||
encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js');
|
encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js');
|
||||||
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
|
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
|
||||||
|
@ -135,8 +135,8 @@
|
|||||||
:filename="d.title"
|
:filename="d.title"
|
||||||
:can-edit="true"
|
:can-edit="true"
|
||||||
:execute-before-leave="submitBeforeLeaveToEditor"
|
:execute-before-leave="submitBeforeLeaveToEditor"
|
||||||
:davLink="d.storedObject._links.dav_link.href"
|
:davLink="d.storedObject._links?.dav_link.href"
|
||||||
:davLinkExpiration="d.storedObject._links.dav_link.expiration"
|
:davLinkExpiration="d.storedObject._links?.dav_link.expiration"
|
||||||
@on-stored-object-status-change="onStatusDocumentChanged"
|
@on-stored-object-status-change="onStatusDocumentChanged"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</li>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user