Merge branch 'add-wysiwyg' into 'master'

Applique l'éditeur de texte sur plusieurs instances + rendu des "Comment Embeddable"

See merge request Chill-Projet/chill-bundles!7
This commit is contained in:
Julien Fastré 2021-04-02 12:11:39 +00:00
commit 36cdf4c220
23 changed files with 393 additions and 64 deletions

View File

@ -0,0 +1,121 @@
.. Copyright (C) 2016 Champs Libres Cooperative SCRLFS
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
Embeddable comments
###################
Those embeddable comments is a comment with some metadata:
* the one who updated the comment (the comment itself, and not the whole entity);
* the date and time for the last update (again, the comment itself, and not the whole entity).
We make usage of `embeddables <https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/tutorials/embeddables.html>`_.
Embed the comment
=================
The comment may be embedded into the entity:
.. code-block:: php
namespace Chill\ActivityBundle\Entity;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Activity
*
* @ORM\Entity()
*/
class Activity
{
/**
* @var integer
*
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
*/
private $comment;
/**
* @return \Chill\MainBundle\Entity\Embeddalbe\CommentEmbeddable
*/
public function getComment()
{
return $this->comment;
}
/**
* @param \Chill\MainBundle\Entity\Embeddalbe\CommentEmbeddable $comment
*/
public function setComment($comment)
{
$this->comment = $comment;
}
}
Note on relation to :class:`User`
=================================
The embeddable proposed by Doctrine does not support relationship to other entities. The entity Comment is able to render a user's id, but not an User object.
.. code-block:: php
$activity->getComment()->getUserId(); // return user id of the last author
$activity->getComment()->getUser(); // does not work !
Usage into form
===============
Use the :class:`Chill\MainBundle\Form\Type\CommentType` to load the form widget:
.. code-block:: php
namespace Chill\ActivityBundle\Form;
use Chill\MainBundle\Form\Type\CommentType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ActivityType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('comment', CommentType::class, [
'required' => false,
])
;
}
}
Render the comment
==================
.. code-block:: twig
{{ activity.comment|chill_entity_render_box }}

View File

@ -29,6 +29,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
Searching <searching.rst>
Timelines <timelines.rst>
Exports <exports.rst>
Embeddable comments <embeddable-comments.rst>
Testing <make-test-working.rst>
Useful snippets <useful-snippets.rst>
manual/index.rst
@ -40,6 +41,7 @@ Layout and UI
.. toctree::
:maxdepth: 2
Render entities automatically <render-entity.rst>
Layout / Template usage <user-interface/layout-template-usage.rst>
Classes and mixins <user-interface/css-classes.rst>
Widgets <user-interface/widgets.rst>

View File

@ -0,0 +1,154 @@
Rendering entity automatically
##############################
Some entity need to be rendered automatically for a couple of times: a person, a user, ...
One can use some twig filter to render those entities:
.. code-block:: twig
{{ person|chill_entity_render_box }}
Define a renderer
=================
By default, the object passed through the renderer will be rendered using the :code:`__toString()` method. To customize this behaviour, you have to define a service and tag it using :code:`chill.render_entity`.
The rendered is implemented using :class:`Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface`. This interface has 3 methods:
* :code:`public function supports($entity, array $options): bool`: return true if the :code:`$entity` given in parameter, with custom options, is supported by this renderer;
* :code:`public function renderString($entity, array $options): string`: render the entity as a single string, for instance in a select list;
* :code:`public function renderBox($entity, array $options): string`: render the entity in an html box.
.. warning::
The HTML returned by :code:`renderBox` **MUST BE SAFE** of any XSS injection.
:class:`Chill\MainBundle\Templating\Entity\AbstractChillEntityRender` provides some useful methods to get the opening and closing boxes that should be used.
Usage about rendering comment:
.. code-block:: php
namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\AbstractChillEntityRender;
use Symfony\Component\Templating\EngineInterface;
class CommentRender extends AbstractChillEntityRender
{
/**
* @var \Chill\MainBundle\Repository\UserRepository
*/
private $userRepository;
/**
*
* @var EngineInterface
*/
private $engine;
public function __construct(
UserRepository $userRepository,
EngineInterface $engine
) {
$this->userRepository = $userRepository;
$this->engine = $engine;
}
/**
* @param CommentEmbeddable $entity
* @param array $options
*
* @return string
*/
public function renderBox($entity, array $options): string
{
// default options
$options = \array_merge([
'user' => [],
'disable_markdown' => false,
'limit_lines' => null,
'metadata' => true
], $options);
if ($entity->getUserId()) {
$user = $this->userRepository->find($entity->getUserId());
}
return $this->engine
->render(
'@ChillMain/Entity/CommentEmbeddable.html.twig',
[
'opening_box' => $this->getDefaultOpeningBox('comment-embeddable'),
'closing_box' => $this->getDefaultClosingBox(),
'user' => $user ?? NULL,
'comment' => $entity,
'options' => $options
]
);
}
/**
* @param CommentEmbeddable $entity
* @param array $options
*
* @return string
*/
public function renderString($entity, array $options): string
{
return $entity->getComment();
}
public function supports($entity, array $options): bool
{
return $entity instanceof CommentEmbeddable;
}
}
Logic inside the template:
.. code-block:: twig
{{ opening_box|raw }}
<div>
{# logic for rendering #}
</div>
{{ closing_box|raw }}
Usage in templates
==================
For rendering entity as a box:
.. code-block:: twig
{{ entity|chill_entity_render_box }}
For rendering entity as a string:
.. code-block:: twig
{{ entity|chill_entity_render_string }}
Available renderer and options
==============================
:code:`Person` (Person Bundle)
------------------------------
* no options
:code:`CommentEmbeddable` (Main Bundle)
---------------------------------------
Options:
* :code:`user`: options which will be passed to "user" renderer
* :code:`disable_markdown`: disable markdown renderer, default to :code:`FALSE`
* :code:`limit_lines` (integer) limit the number of lines. Default to :code:`NULL`. May be an integer.
* :code:`metadata` (boolean): show the last updating user and last updating date. Default to :code:`TRUE`.

View File

@ -15,7 +15,6 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTra
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Chill\ActivityBundle\Form\Type\TranslatableActivityType;
use Chill\ActivityBundle\Form\Type\TranslatableActivityReason;
use Chill\MainBundle\Form\Type\UserPickerType;

View File

@ -46,12 +46,7 @@
<td>{{ activity.durationTime|date('H:i') }}</td>
<td>
{% if activity.comment.comment is not empty %}
<blockquote class="chill-user-quote">
{{ activity.comment.comment|slice(0, 250) }} {#
sf4 check: if 'slice' could replace 'truncate' filter ?
truncate come with twig-extensions, in conflict with twig 3
#}
</blockquote>
{{ activity.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
{% endif %}
{%- if activity.reasons is empty -%}
{{ 'No reason associated'|trans }}

View File

@ -13,7 +13,7 @@
{% if is_granted(constant('Chill\\ActivityBundle\\Security\\Authorization\\ActivityVoter::SEE_DETAILS'), activity) %}
<dl class="chill_view_data">
<dd>{% if activity.comment.comment is empty %}{{ 'No comments'|trans }}{% else %}<blockquote class="chill-user-quote">{{ activity.comment.comment|nl2br }}</blockquote>{% endif %}</dd>
<dd>{% if activity.comment.comment is empty %}{{ 'No comments'|trans }}{% else %}{{ activity.comment|chill_entity_render_box({'metadata': false}) }}{% endif %}</dd>
<dt class="inline">{{ 'Reasons'|trans }}</dt>
{%- if activity.reasons is empty -%}

View File

@ -4,7 +4,6 @@ namespace Chill\DocStoreBundle\Form;
use Chill\DocStoreBundle\Entity\Document;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\AbstractType;
@ -12,15 +11,13 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
use Chill\MainBundle\Form\Type\AppendScopeChoiceTypeTrait;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\Persistence\ObjectManager;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
class PersonDocumentType extends AbstractType
@ -62,7 +59,7 @@ class PersonDocumentType extends AbstractType
{
$builder
->add('title', TextType::class)
->add('description', TextareaType::class, [
->add('description', ChillTextareaType::class, [
'required' => false
])
->add('object', StoredObjectType::class, [

View File

@ -1,5 +1,5 @@
var algo = 'AES-CBC';
var Dropzone = require('dropzone');
import Dropzone from 'dropzone';
var initializeDownload = require('./downloader.js');
@ -121,7 +121,7 @@ var createZone = (zone, zoneData) => {
created.classList.add('dropzone');
initMessage.classList.add('dz-message');
initMessage.appendChild(document.createTextNode(initContent));
console.log(Dropzone);
dropzoneI = new Dropzone(created, {
url: function(files) {
return getUploadUrl(zoneData, files);

View File

@ -45,7 +45,7 @@
<span class="chill-no-data-statement">{{ 'Any description'|trans }}</span>
{% else %}
<blockquote class="chill-user-quote">
{{ document.description }}
{{ document.description|chill_markdown_to_html }}
</blockquote>
{% endif %}
</dd>

View File

@ -20,7 +20,7 @@ namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
@ -44,8 +44,8 @@ class CommentType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('comment', TextareaType::class, [
->add('comment', ChillTextareaType::class, [
'disable_editor' => $options['disable_editor']
])
;
@ -72,8 +72,12 @@ class CommentType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CommentEmbeddable::class,
]);
$resolver
->setDefined('disable_editor')
->setAllowedTypes('disable_editor', 'bool')
->setDefaults([
'data_class' => CommentEmbeddable::class,
'disable_editor' => false
]);
}
}

View File

@ -36,6 +36,7 @@ require('./modules/breadcrumb/index.js');
require('./modules/download-report/index.js');
require('./modules/select_interactive_loading/index.js');
require('./modules/export-list/export-list.scss');
require('./modules/entity/index.js');
/*
* load img

View File

@ -1,4 +1,8 @@
// set min height for ckeditor
.ck-editor__editable {
min-height: 150px;
}
.ck-editor.ck-reset {
margin-bottom: 1.5em;
}

View File

@ -0,0 +1,6 @@
.chill-entity__comment-embeddable {
.chill-entity__comment-embeddable__metadata {
font-size: smaller;
color: var(--chill-light-gray);
}
}

View File

@ -0,0 +1,2 @@
// css classes to render entities
require('./comment_embeddable.scss');

View File

@ -0,0 +1,36 @@
{{ opening_box|raw }}
<div class="">
<div class="comment-embeddable_comment">
{%- if options['limit_lines'] is not null -%}
{% set content = comment.comment|split('\n')|slice(0, options['limit_lines'])|join('\n') %}
{%- else -%}
{% set content = comment.comment %}
{%- endif -%}
<blockquote class="chill-user-quote">
{% if options['disable_markdown'] %}
{{ content|nl2br }}
{% else %}
{{ content|chill_markdown_to_html }}
{% endif %}
</blockquote>
</div>
</div>
{% if options['metadata'] %}
<div class="chill-entity__comment-embeddable__metadata">
{% if user is not empty %}
<span class="chill-entity__comment-embeddable__user">
{{ 'Last updated by'| trans }} {{ user|chill_entity_render_box(options['user']) }}
</span>';
{% endif %}
{% if comment.date is not empty %}
<span class="chill-entity__comment-embeddable__date">
{% if user is empty %}{{ 'Last updated on'|trans ~ ' ' }}{% else %}{{ 'on'|trans ~ ' ' }}{% endif %}
{{ comment.date|format_datetime("medium", "short") }}
</span>
{% endif %}
</div>
{% endif %}
{{ closing_box|raw }}

View File

@ -21,9 +21,9 @@
namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\AbstractChillEntityRender;
use Symfony\Component\Templating\EngineInterface;
class CommentRender extends AbstractChillEntityRender
{
@ -31,10 +31,19 @@ class CommentRender extends AbstractChillEntityRender
* @var \Chill\MainBundle\Repository\UserRepository
*/
private $userRepository;
/**
*
* @var EngineInterface
*/
private $engine;
public function __construct(UserRepository $userRepository)
{
public function __construct(
UserRepository $userRepository,
EngineInterface $engine
) {
$this->userRepository = $userRepository;
$this->engine = $engine;
}
/**
@ -45,35 +54,29 @@ class CommentRender extends AbstractChillEntityRender
*/
public function renderBox($entity, array $options): string
{
$username = '';
// default options
$options = \array_merge([
'user' => [],
'disable_markdown' => false,
'limit_lines' => null,
'metadata' => true
], $options);
if ($entity->getUserId()) {
$user = $this->userRepository->find($entity->getUserId());
if ($user instanceof User) {
$username = $user->getUsername();
}
}
$str = $this->getDefaultOpeningBox('comment-embeddable').
'<span class="comment_comment">'.
nl2br($entity->getComment()).
'</span>';
if ($entity->getDate() instanceof \DateTime) {
$str .= '<span class="comment_date">'.
$entity->getDate()->format('d/m/Y H:i');
'</span>';
}
if (strlen($username) > 0) {
$str .= '<span class="comment_user">'.
$username.
'</span>';
}
$str .= $this->getDefaultClosingBox();
return $str;
return $this->engine
->render(
'@ChillMain/Entity/CommentEmbeddable.html.twig',
[
'opening_box' => $this->getDefaultOpeningBox('comment-embeddable'),
'closing_box' => $this->getDefaultClosingBox(),
'user' => $user ?? NULL,
'comment' => $entity,
'options' => $options
]
);
}
/**

View File

@ -34,6 +34,7 @@ services:
Chill\MainBundle\Templating\Entity\CommentRender:
arguments:
- '@chill.main.user_repository'
- '@Symfony\Component\Templating\EngineInterface'
tags:
- { name: 'chill.render_entity' }

View File

@ -35,6 +35,9 @@ This form contains errors: Ce formulaire contient des erreurs
Choose an user: Choisir un utilisateur
'You are going to leave a page with unsubmitted data. Are you sure you want to leave ?': "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?"
No value: Aucune information
Last updated by: Dernière mise à jour par
Last updated on: Dernière mise à jour le
on: le
Edit: Modifier
Update: Mettre à jour

View File

@ -6,17 +6,16 @@ use Chill\MainBundle\Entity\Center;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Form\Type\UserPickerType;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Form\Type\ClosingMotivePickerType;
/**
* Class AccompanyingPeriodType
*
@ -89,7 +88,7 @@ class AccompanyingPeriodType extends AbstractType
]);
}
$builder->add('remark', TextareaType::class, [
$builder->add('remark', ChillTextareaType::class, [
'required' => false
]);
}

View File

@ -48,8 +48,13 @@
</td>
{% endif %}
<td>
{{ accompanying_period.remark|chill_print_or_message('No remark', 'blockquote') }}
{% if accompanying_period is not empty %}
<blockquote class="chill-user-quote">
{{ accompanying_period.remark|chill_markdown_to_html }}
</blockquote>
{% else %}
{{ null|chill_print_or_message('No remark', 'blockquote') }}
{% endif %}
</td>
<td>
<ul class="record_actions">

View File

@ -22,15 +22,12 @@ use Symfony\Component\Form\FormBuilderInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Entity\Center;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Chill\MainBundle\Form\Type\UserPickerType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Role\Role;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
use Chill\MainBundle\Form\Type\DateIntervalType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
/**
*
@ -43,7 +40,7 @@ class SingleTaskType extends AbstractType
{
$builder
->add('title', TextType::class)
->add('description', TextareaType::class, [
->add('description', ChillTextareaType::class, [
'required' => false
])
->add('assignee', UserPickerType::class, [

View File

@ -37,7 +37,7 @@
<span class="chill-no-data-statement">{{"No description"|trans}}</span>
{% else %}
<blockquote class="chill-user-quote">
{{ task.description }}
{{ task.description|chill_markdown_to_html }}
</blockquote>
{% endif %}
</dd>

View File

@ -18,7 +18,7 @@
<dt class="inline">{{ 'Description'|trans }}</dt>
<dd>
<blockquote class="chill-user-quote">
{{ event.task.description }}
{{ event.task.description|chill_markdown_to_html }}
</blockquote>
</dd>
{% endif %}