mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
merge upgrade-sf5 branch to have latest fixes
This commit is contained in:
commit
635b1ee537
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: |
|
||||||
|
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||||
|
time: 2024-05-30T16:00:03.440767606+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: |
|
||||||
|
Upgrade CKEditor and refactor configuration with use of typescript
|
||||||
|
time: 2024-05-31T19:02:42.776662753+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
@ -27,6 +27,7 @@ $config
|
|||||||
->setRiskyAllowed(true)
|
->setRiskyAllowed(true)
|
||||||
->setCacheFile('.cache/php-cs-fixer.cache')
|
->setCacheFile('.cache/php-cs-fixer.cache')
|
||||||
->setUsingCache(true)
|
->setUsingCache(true)
|
||||||
|
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
|
||||||
;
|
;
|
||||||
|
|
||||||
$rules = $config->getRules();
|
$rules = $config->getRules();
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
"phpstan/phpstan-deprecation-rules": "^1.1",
|
"phpstan/phpstan-deprecation-rules": "^1.1",
|
||||||
"phpstan/phpstan-strict-rules": "^1.0",
|
"phpstan/phpstan-strict-rules": "^1.0",
|
||||||
"phpunit/phpunit": ">= 7.5",
|
"phpunit/phpunit": ">= 7.5",
|
||||||
"rector/rector": "^1.0.0",
|
"rector/rector": "^1.1.0",
|
||||||
"symfony/debug-bundle": "^5.4",
|
"symfony/debug-bundle": "^5.4",
|
||||||
"symfony/dotenv": "^5.4",
|
"symfony/dotenv": "^5.4",
|
||||||
"symfony/maker-bundle": "^1.20",
|
"symfony/maker-bundle": "^1.20",
|
||||||
@ -151,6 +151,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd"
|
"cache:clear": "symfony-cmd"
|
||||||
}
|
},
|
||||||
|
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,352 @@
|
|||||||
Installation & Usage
|
Installation & Usage
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
You will learn here how to install a new symfony project with chill, and configure it.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
The installation is tested on a Debian-like linux distribution. The installation on other operating systems is not documented.
|
||||||
|
|
||||||
|
You have to install the following tools on your computer:
|
||||||
|
|
||||||
|
- `PHP <https://www.php.net/>`_, version 8.3+, with the following extensions: pdo_pgsql, intl, mbstring, zip, bcmath, exif, sockets, redis, ast, gd;
|
||||||
|
- `composer <https://getcomposer.org/>`_;
|
||||||
|
- `symfony cli <https://symfony.com/download>`_;
|
||||||
|
- `node, we encourage you to use nvm to configure the correct version <https://github.com/nvm-sh/nvm>`_. The project contains an
|
||||||
|
:code:`.nvmrc` file which selects automatically the required version of node (if present).
|
||||||
|
- `yarn <https://classic.yarnpkg.com/lang/en/docs/install/>`_. We use the version 1.22+ for now.
|
||||||
|
- `docker and the plugin compose <https://docker.com>`_ to run the database
|
||||||
|
|
||||||
|
Chill needs a redis server and a postgresql database, and a few other things like a "relatorio service" which will
|
||||||
|
generate documents from templates. **All these things are available through docker using the plugin compose**. We do not provide
|
||||||
|
information on how to run this without docker compose.
|
||||||
|
|
||||||
|
|
||||||
|
Install a new project
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Initialize project and dependencies
|
||||||
|
***********************************
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony new --version=5.4 my_chill_project
|
||||||
|
cd my_chill_project
|
||||||
|
|
||||||
|
We strongly encourage you to initialize a git repository at this step, to track further changes.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# add the flex endpoints required for custom recipes
|
||||||
|
cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json
|
||||||
|
# install chill and some dependencies
|
||||||
|
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released
|
||||||
|
symfony composer require chill-project/chill-bundles v3.0.0-alpha1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
|
||||||
|
|
||||||
|
We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database.
|
||||||
|
You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually).
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# fix some configuration
|
||||||
|
./post-install-chill.sh
|
||||||
|
# install node dependencies
|
||||||
|
yarn install
|
||||||
|
# and compile assets
|
||||||
|
yarn run encore production
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you encounter this error during assets compilation (:code:`yarn run encore production`) (repeated multiple times):
|
||||||
|
|
||||||
|
.. code-block:: txt
|
||||||
|
|
||||||
|
[tsl] ERROR in /tmp/chill/v1/public/bundles/chillcalendar/types.ts(2,65)
|
||||||
|
TS2307: Cannot find module '../../../ChillMainBundle/Resources/public/types' or its corresponding type declarations.
|
||||||
|
|
||||||
|
run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
rm -rf public/bundles/*
|
||||||
|
|
||||||
|
Then restart the compilation of assets (:code:```yarn run encore production```)
|
||||||
|
|
||||||
|
Configure your project
|
||||||
|
**********************
|
||||||
|
|
||||||
|
You should read the configuration files in :code:`chill/config/packages` carefully, especially if you have
|
||||||
|
custom developments. But most of the time, this should be fine.
|
||||||
|
|
||||||
|
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
|
||||||
|
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
|
||||||
|
or in the :code:`.env.local` file, which should not be commited to the git repository.
|
||||||
|
|
||||||
|
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
|
||||||
|
by the symfony server, from the docker compose services.
|
||||||
|
|
||||||
|
The only required variable is the :code:`ADMIN_PASSWORD`. You can generate a hashed and salted admin password using the command
|
||||||
|
:code:`symfony console security:hash-password <your password> 'Symfony\Component\Security\Core\User\User'`. Then,
|
||||||
|
you can either:
|
||||||
|
|
||||||
|
- add this password to the :code:`.env.local` file, you must escape the character :code:`$`: if the generated password
|
||||||
|
is :code:`$2y$13$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm`, your :code:`.env.local` file will be:
|
||||||
|
|
||||||
|
.. code-block:: env
|
||||||
|
|
||||||
|
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm
|
||||||
|
|
||||||
|
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
|
||||||
|
not the password in clear text).
|
||||||
|
|
||||||
|
Prepare migrations and other tools
|
||||||
|
**********************************
|
||||||
|
|
||||||
|
To continue the installation process, you will have to run migrations:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# start databases and other services
|
||||||
|
docker compose up -d
|
||||||
|
# the first start, it may last some seconds, you can check with docker compose ps
|
||||||
|
# run migrations
|
||||||
|
symfony console doctrine:migrations:migrate
|
||||||
|
# setup messenger
|
||||||
|
symfony console messenger:setup-transports
|
||||||
|
# prepare some views
|
||||||
|
symfony console chill:db:sync-views
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If you encounter an error while running :code:`symfony console messenger:setup-transports`, you can set up the messenger
|
||||||
|
transport to redis, by adding this in the :code:`.env.local` or :code:`.env` file:
|
||||||
|
|
||||||
|
.. code-block:: env
|
||||||
|
|
||||||
|
MESSENGER_TRANSPORT_DSN=redis://${REDIS_HOST}:${REDIS_PORT}/messages
|
||||||
|
|
||||||
|
Start your web server locally
|
||||||
|
*****************************
|
||||||
|
|
||||||
|
At this step, Chill will be ready to be served locally, but without any configuration. You can run the project
|
||||||
|
locally using the `local symfony server <https://symfony.com/doc/current/setup/symfony_server.html>`_:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# see the whole possibilities at https://symfony.com/doc/current/setup/symfony_server.html
|
||||||
|
symfony server:start -d
|
||||||
|
|
||||||
|
|
||||||
|
If you need to test the instance with accounts and some basic configuration, please install the fixtures (see below).
|
||||||
|
|
||||||
|
|
||||||
|
Add capabilities for dev
|
||||||
|
========================
|
||||||
|
|
||||||
|
If you need to add custom bundles, you can develop them in the `src/` directory, like for any other symfony project. You
|
||||||
|
can rely on the whole chill framework, meaning there is no need to add them to the original `chill-bundles`.
|
||||||
|
|
||||||
|
You will require some bundles to have the following development tools:
|
||||||
|
|
||||||
|
- add fixtures
|
||||||
|
- add profiler and var-dumper to debug
|
||||||
|
|
||||||
|
Install fixtures
|
||||||
|
****************
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# generate fixtures for chill
|
||||||
|
symfony composer require --dev doctrine/doctrine-fixtures-bundle nelmio/alice
|
||||||
|
# now, you can generate fixtures (this will reset your database)
|
||||||
|
symfony console doctrine:fixtures:load
|
||||||
|
|
||||||
|
This will generate user accounts, centers, and some basic configuration.
|
||||||
|
|
||||||
|
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
|
||||||
|
visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
|
||||||
|
|
||||||
|
The password is always :code:`password`.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The fixtures are not fully functional. See the `corresponding issue <https://gitlab.com/Chill-Projet/chill-bundles/-/issues/280>`_.
|
||||||
|
|
||||||
|
Add web profiler and debugger
|
||||||
|
*****************************
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
|
||||||
|
|
||||||
|
Working on chill bundles
|
||||||
|
************************
|
||||||
|
|
||||||
|
If you plan to improve the chill-bundles repository, that's great!
|
||||||
|
|
||||||
|
You will have to download chill-bundles as a git repository (and not as an archive, which is barely editable).
|
||||||
|
|
||||||
|
In your :code:`composer.json` file, add these lines:
|
||||||
|
|
||||||
|
.. code-block:: diff
|
||||||
|
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
+ "preferred-install": {
|
||||||
|
+ "chill-project/chill-bundles": "source",
|
||||||
|
"*": "dist"
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
Then, run :code:`symfony composer reinstall chill-project/chill-bundles` to re-install the package from source.
|
||||||
|
|
||||||
|
Code style, code quality and other tools
|
||||||
|
****************************************
|
||||||
|
|
||||||
|
For development, you will also have to install:
|
||||||
|
|
||||||
|
- `php-cs-fixer <https://cs.symfony.com/>`_
|
||||||
|
|
||||||
|
We also encourage you to use tools like `phpstan <https://phpstan.org>`_ and `rector <https://getrector.com>`_.
|
||||||
|
|
||||||
|
Commit and share your project
|
||||||
|
=============================
|
||||||
|
|
||||||
|
If multiple developers work on a project, you can commit your symfony project and share it with other people.
|
||||||
|
|
||||||
|
When another developer clones your project, they will have to:
|
||||||
|
|
||||||
|
- run :code:`symfony composer install` and :code:`yarn install` to install the same dependencies as the initial developer;
|
||||||
|
- run :code:`yarn run encore production` to compile assets;
|
||||||
|
- copy any possible variables from the :code:`.env.local` files;
|
||||||
|
- start the docker compose stack, using :code:`docker compose`, and run migrations, set up transports, and prepare chill db views
|
||||||
|
(see the corresponding command above)
|
||||||
|
|
||||||
|
Update
|
||||||
|
======
|
||||||
|
|
||||||
|
In order to update your app, you must update dependencies:
|
||||||
|
|
||||||
|
- for chill-bundles, you can `set the last version <https://gitlab.com/Chill-Projet/chill-bundles/-/releases>`_ manually
|
||||||
|
in the :code:`composer.json` file, or set the version to `^3.0.0` and run :code:`symfony composer update` regularly
|
||||||
|
- run :code:`composer update` and :code:`yarn update` to maintain your dependencies up-to-date.
|
||||||
|
|
||||||
|
After each update, you must update your database schema:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony console doctrine:migrations:migrate
|
||||||
|
symfony console chill:db:sync-views
|
||||||
|
|
||||||
|
Operations
|
||||||
|
==========
|
||||||
|
|
||||||
|
Build assets
|
||||||
|
************
|
||||||
|
|
||||||
|
run those commands:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# for production (or in dev, when you don't need to work on your assets and need some speed)
|
||||||
|
yarn run encore production
|
||||||
|
# in dev, when you wan't to reload the assets on each changes
|
||||||
|
yarn run encore dev --watch
|
||||||
|
|
||||||
|
How to execute the console ?
|
||||||
|
****************************
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# start the console with all required variables
|
||||||
|
symfony console
|
||||||
|
# you can add your command after that:
|
||||||
|
symfony console list
|
||||||
|
|
||||||
|
How to generate documents
|
||||||
|
*************************
|
||||||
|
|
||||||
|
Documents are generated asynchronously by `"consuming messages" <https://symfony.com/doc/current/messenger.html#consuming-messages-running-the-worker>`_.
|
||||||
|
|
||||||
|
You must generate them using a dedicated process:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony console messenger:consume async priority
|
||||||
|
|
||||||
|
To avoid memory issues, we encourage you to also use the :code:`--limit` parameter of the command.
|
||||||
|
|
||||||
|
How to read emails sent by the program ?
|
||||||
|
*******************************************
|
||||||
|
|
||||||
|
In development, there is a built-in "mail catcher". Open it with :code:`symfony open:local:webmail`
|
||||||
|
|
||||||
|
How to run cron-jobs ?
|
||||||
|
**********************
|
||||||
|
|
||||||
|
Some commands must be executed in :ref:`cron jobs <cronjob>`. To execute them:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
symfony console chill:cron-job:execute
|
||||||
|
|
||||||
|
What about materialized views ?
|
||||||
|
*******************************
|
||||||
|
|
||||||
|
There are some materialized views in chill, to speed up some complex computations in the database.
|
||||||
|
|
||||||
|
In order to refresh them, run a cron job or refresh them manually in your database.
|
||||||
|
|
||||||
|
How to run tests for chill-bundles
|
||||||
|
**********************************
|
||||||
|
|
||||||
|
Tests reside inside the installed bundles. You must `cd` into that directory, download the required packages, and execute them from this place.
|
||||||
|
|
||||||
|
**Note**: some bundles require the fixtures to be executed. See the dedicated _how-tos_.
|
||||||
|
|
||||||
|
Example, for running a unit test inside `main` bundle:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# cd into main directory
|
||||||
|
cd vendor/chill-project/chill-bundles
|
||||||
|
composer install
|
||||||
|
# run tests
|
||||||
|
bin/phpunit src/Bundle/path/to/your/test
|
||||||
|
|
||||||
|
Or for running tests to check code style and php conventions with csfixer and phpstan:
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
===============
|
||||||
|
|
||||||
|
Error `An exception has been thrown during the rendering of a template ("Asset manifest file "/var/www/app/web/build/manifest.json" does not exist.").` on first run
|
||||||
|
********************************************************************************************************************************************************************
|
||||||
|
|
||||||
|
Build assets, see above.
|
||||||
|
|
||||||
|
Running in production
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Currently, to run this software in production, the *state of the art* is the following :
|
||||||
|
|
||||||
|
1. Run the software locally and tweak the configuration to your needs ;
|
||||||
|
2. Build the image and store it in a private container registry.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
In production, you **must** set these variables:
|
||||||
|
|
||||||
|
* ``APP_ENV`` to ``prod``
|
||||||
|
* ``APP_DEBUG`` to ``false``
|
||||||
|
|
||||||
|
There are security issues if you keep the same variables as for production.
|
||||||
|
|
||||||
|
|
||||||
|
Going further
|
||||||
|
=============
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
@ -21,386 +367,3 @@ Installation & Usage
|
|||||||
load-addresses.rst
|
load-addresses.rst
|
||||||
prod-calendar-sms-sending.rst
|
prod-calendar-sms-sending.rst
|
||||||
msgraph-configure.rst
|
msgraph-configure.rst
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
************
|
|
||||||
|
|
||||||
- This project use `docker <https://docker.com>`_ to be run. As a developer, use `docker-compose <https://docs.docker.com/compose/overview/>`_ to bootstrap a dev environment in a glance. You do not need any other dependencies ;
|
|
||||||
- Make is used to automate scripts.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
************
|
|
||||||
|
|
||||||
If you plan to run chill in production:
|
|
||||||
|
|
||||||
1. install it locally first, and check if everything is ok on your local machine;
|
|
||||||
2. once ready, build the image from your local machine, and deploy them.
|
|
||||||
|
|
||||||
If you want to develop some bundles, the first step is sufficient (until you deploy on production).
|
|
||||||
|
|
||||||
|
|
||||||
1. Get the code
|
|
||||||
===============
|
|
||||||
|
|
||||||
Clone or download the chill-skeleton project and `cd` into the main directory.
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
git clone https://gitea.champs-libres.be/Chill-project/chill-skeleton-basic.git
|
|
||||||
cd chill-skeleton-basic
|
|
||||||
|
|
||||||
|
|
||||||
As a developer, the code will stay on your computer and will be executed in docker container. To avoid permission problem, the code should be run with the same uid/gid from your current user. This is why we get your current user id with the command ``id -u`` in each following scripts.
|
|
||||||
|
|
||||||
2. Prepare composer to download the sources
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
As you are running in dev, you must configure an auth token for getting the source code.
|
|
||||||
|
|
||||||
.. warning
|
|
||||||
|
|
||||||
If you skip this part, the code will be downloaded from dist instead of source (with git repository). You will probably replace the source manually, but the next time you will run ```composer update```, your repository will be replaced and you might loose something.
|
|
||||||
|
|
||||||
1. Create a personal access token from https://gitlab.com/-/profile/personal_access_tokens, with the `read_api` scope.
|
|
||||||
2. add a file called ```.composer/auth.json``` with this content:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"gitlab-token": {
|
|
||||||
"gitlab.com": "glXXX-XXXXXXXXXXXXXXXXXXXX"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2. Prepare your variables and environment
|
|
||||||
=========================================
|
|
||||||
|
|
||||||
Copy ```docker-compose.override.dev.yml``` into ```docker-compose.override.yml```
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
cp docker-compose.override.dev.template.yml docker-compose.override.yml
|
|
||||||
|
|
||||||
2. Prepare your variables and docker-compose
|
|
||||||
============================================
|
|
||||||
|
|
||||||
Have a look at the variable in ``.env`` and check if you need to adapt them. If they do not adapt with your need, or if some are missing:
|
|
||||||
|
|
||||||
1. copy the file as ``.env.local``: ``cp .env .env.local``
|
|
||||||
2. you may replace some variables inside ``.env``
|
|
||||||
|
|
||||||
Prepare also you docker-compose installation, and adapt it to your needs:
|
|
||||||
|
|
||||||
1. If you plan to deploy on dev, copy the file ``docker-compose.override.dev.template.yml`` to ``docker-compose.override.yml``.
|
|
||||||
2. adapt to your needs.
|
|
||||||
|
|
||||||
|
|
||||||
**Note**: If you intend to use the bundle ``Chill-Doc-Store``, you will need to configure and install an openstack object storage container with temporary url middleware. You will have to configure `secret keys <https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html#secret-keys>`_.
|
|
||||||
|
|
||||||
3. Run the bootstrap script
|
|
||||||
===========================
|
|
||||||
|
|
||||||
This script can be run using `make`
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
make init
|
|
||||||
|
|
||||||
This script will :
|
|
||||||
|
|
||||||
1. force docker-compose to, eventually, pull the base images and build the image used by this project ;
|
|
||||||
2. run an install script to download `composer <https://getcomposer.org>`_ ;
|
|
||||||
3. install the php dependencies
|
|
||||||
4. build assets
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
The script will work only if the binary ``docker-compose`` is located into your ``PATH``. If you use ``compose`` as a docker plugin,
|
|
||||||
you can simulate this binary by creating this file at (for instance), ``/usr/local/bin/docker-compose`` (and run ``chmod +x /usr/local/bin/docker-compose``):
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
/usr/bin/docker compose "$@"
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
In some cases it can happen that an old image (chill_base_php82 or chill_php82) stored in the docker cache will make the script fail. To solve this problem you have to delete the image and the container, before the make init :
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
docker-compose images php
|
|
||||||
docker rmi -f chill_php82:prod
|
|
||||||
docker-compose rm php
|
|
||||||
|
|
||||||
|
|
||||||
4. Start the project
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
docker-compose up
|
|
||||||
|
|
||||||
**On the first run** (and after each upgrade), you must execute *post update commands* and run database migrations. With a container up and running, execute the following commands:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# mount into to container
|
|
||||||
./docker-php.sh
|
|
||||||
bin/console chill:db:sync-views
|
|
||||||
# and load fixtures
|
|
||||||
bin/console doctrine:migrations:migrate
|
|
||||||
|
|
||||||
|
|
||||||
Chill will be available at ``http://localhost:8001.`` Currently, there isn't any user or data. To add fixtures, run
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# mount into to container
|
|
||||||
./docker-php.sh
|
|
||||||
# and load fixtures (do not this for production)
|
|
||||||
bin/console doctrine:fixtures:load --purge-with-truncate
|
|
||||||
|
|
||||||
There are several users available:
|
|
||||||
|
|
||||||
- ``center a_social``
|
|
||||||
- ``center b_social``
|
|
||||||
|
|
||||||
The password is always ``password``.
|
|
||||||
|
|
||||||
Now, read `Operations` below. For running in production, read `prod_`.
|
|
||||||
|
|
||||||
|
|
||||||
Operations
|
|
||||||
**********
|
|
||||||
|
|
||||||
Build assets
|
|
||||||
============
|
|
||||||
|
|
||||||
run those commands:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
make build-assets
|
|
||||||
|
|
||||||
How to execute the console ?
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php bin/console
|
|
||||||
|
|
||||||
How to create the database schema (= run migrations) ?
|
|
||||||
======================================================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
bin/console doctrine:migrations:migrate
|
|
||||||
bin/console chill:db:sync-views
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php bin/console doctrine:migrations:migrate
|
|
||||||
docker-compose run --user $(id -u) php bin/console chill:db:sync-views
|
|
||||||
|
|
||||||
|
|
||||||
How to read the email sent by the program ?
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Go at ``http://localhost:8005`` and you should have access to mailcatcher.
|
|
||||||
|
|
||||||
In case of you should click on a link in the email, be aware that you should remove the "s" from https.
|
|
||||||
|
|
||||||
How to load fixtures ? (development mode only)
|
|
||||||
==============================================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
bin/console doctrine:fixtures:load
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php bin/console doctrine:fixtures:load
|
|
||||||
|
|
||||||
How to open a terminal in the project
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php /bin/bash
|
|
||||||
|
|
||||||
How to run cron-jobs ?
|
|
||||||
======================
|
|
||||||
|
|
||||||
Some command must be executed in :ref:`cron jobs <cronjob>`. To execute them:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
bin/console chill:cron-job:execute
|
|
||||||
# some of them are executed only during the night. So, we have to force the execution during the day:
|
|
||||||
bin/console chill:cron-job:execute 'name-of-the-cron'
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php bin/console chill:cron-job:execute
|
|
||||||
# some of them are executed only during the night. So, we have to force the execution during the day:
|
|
||||||
docker-compose run --user $(id -u) php bin/console chill:cron-job:execute 'name-of-the-cron'
|
|
||||||
|
|
||||||
How to run composer ?
|
|
||||||
=====================
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# if a container is running
|
|
||||||
./docker-php.sh
|
|
||||||
composer
|
|
||||||
# if not
|
|
||||||
docker-compose run --user $(id -u) php composer
|
|
||||||
|
|
||||||
How to access to PGADMIN ?
|
|
||||||
==========================
|
|
||||||
|
|
||||||
Pgadmin is installed with docker-compose, and is available **only if you uncomment the appropriate lines into ``docker-compose.override.yml``.
|
|
||||||
|
|
||||||
You can access it at ``http://localhost:8002``.
|
|
||||||
|
|
||||||
Credentials:
|
|
||||||
|
|
||||||
- login: from the variable you set into ``docker-composer.override.yml``
|
|
||||||
- password: same :-)
|
|
||||||
|
|
||||||
How to run tests ?
|
|
||||||
==================
|
|
||||||
|
|
||||||
Tests reside inside the installed bundles. You must `cd` into that directory, download the required packages, and execute them from this place.
|
|
||||||
|
|
||||||
**Note**: some bundle require the fixture to be executed. See the dedicated _how-tos_.
|
|
||||||
|
|
||||||
Exemple, for running unit test inside `main` bundle:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# mount into the php image
|
|
||||||
./docker-php.sh
|
|
||||||
# cd into main directory
|
|
||||||
cd vendor/chill-project/chill-bundles
|
|
||||||
# download deps
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
composer install
|
|
||||||
# run tests
|
|
||||||
bin/phpunit src/Bundle/path/to/your/test
|
|
||||||
|
|
||||||
Or for running tests to check code style and php conventions with csfixer and phpstan:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# run code style fixer
|
|
||||||
bin/grumphp run --tasks=phpcsfixer
|
|
||||||
# run phpstan
|
|
||||||
bin/grumphp run --tasks=phpstan
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
To avoid phpstan block your commits:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
git commit -n ...
|
|
||||||
|
|
||||||
To avoid phpstan block your commits permanently:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
./bin/grumphp git:deinit
|
|
||||||
|
|
||||||
How to run webpack interactively
|
|
||||||
================================
|
|
||||||
|
|
||||||
Executing :code:`bash docker-node.sh` will open a terminal in a node container, with volumes mounted.
|
|
||||||
|
|
||||||
How to switch the branch for chill-bundles, and get new dependencies
|
|
||||||
====================================================================
|
|
||||||
|
|
||||||
During development, you will switch to new branches for chill-bundles. As long as the dependencies are equals, this does not cause any problem. But sometimes, a new branch introduces a new dependency, and you must download it.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Ensure that you have gitlab-token ready before updating your branches. See above.
|
|
||||||
|
|
||||||
In order to do that without pain, use those steps:
|
|
||||||
|
|
||||||
0. Ensuire you have a token, set
|
|
||||||
1. at the app's root, update the ``composer.json`` to your current branch:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"require": {
|
|
||||||
"chill-bundles": "dev-<my-branch>@dev"
|
|
||||||
}
|
|
||||||
|
|
||||||
2. mount into the php container (``./docker-php.sh``), and run ``composer update``
|
|
||||||
|
|
||||||
Error `An exception has been thrown during the rendering of a template ("Asset manifest file "/var/www/app/web/build/manifest.json" does not exist.").` on first run
|
|
||||||
====================================================================================================================================================================
|
|
||||||
|
|
||||||
Run :code:`make build-assets`
|
|
||||||
|
|
||||||
Running in production
|
|
||||||
*********************
|
|
||||||
|
|
||||||
Currently, to run this software in production, the *state of the art* is the following :
|
|
||||||
|
|
||||||
1. Run the software locally and tweak the configuration to your needs ;
|
|
||||||
2. Build the image and store them into a private container registry. This can be done using :code:`make build-and-push-image`.
|
|
||||||
|
|
||||||
To be sure to target the correct container registry, you have to adapt the image names into your ``docker-compose.override.yml`` file.
|
|
||||||
3. Push the image on your registry, or upload them to the destination machine using ``docker image save`` and ``docker image load``.
|
|
||||||
3. Run the image on your production server, using docker-compose or eventually docker stack. You have to customize the variable set in docker-compose.
|
|
||||||
|
|
||||||
See also the :ref:`running-production-tips-and-tricks` below.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
In production, you **must** set those variables:
|
|
||||||
|
|
||||||
* ``APP_ENV`` to ``prod``
|
|
||||||
* ``APP_DEBUG`` to ``false``
|
|
||||||
|
|
||||||
There are security issues if you keep the same variable than for production.
|
|
||||||
|
|
||||||
|
|
||||||
.. _running-production-tips-and-tricks:
|
|
||||||
|
|
||||||
Tips and tricks
|
|
||||||
===============
|
|
||||||
|
|
||||||
Operation on database (backups, running custom sql, replication) are easier to set when run outside of a container. If you run into a container, take care of the volume where data are stored.
|
|
||||||
|
|
||||||
The PHP sessions are stored inside redis. This is useful if you distribute the traffic amongst different php server: they will share same sessions if a request goes into a different instance of the container.
|
|
||||||
|
|
||||||
When the PHP servers are shared across multiple instances, take care that some data is stored into redis: the same redis server should be reachable by all instances.
|
|
||||||
|
|
||||||
It is worth having an eye on the configuration of logstash container.
|
|
||||||
|
|
||||||
|
|
||||||
Design principles
|
|
||||||
*****************
|
|
||||||
|
|
||||||
Why the DB URL is also set in environment, and not in .env file ?
|
|
||||||
=================================================================
|
|
||||||
|
|
||||||
Because, at startup, a script does check the db is up and, if not, wait for a couple of seconds before running ``entrypoint.sh``. For avoiding double configuration, the configuration of the PHP app takes his configuration from environment also (and it will be standard in future releases, with symfony 4.0).
|
|
||||||
|
|
||||||
|
15
package.json
15
package.json
@ -6,15 +6,16 @@
|
|||||||
"@apidevtools/swagger-cli": "^4.0.4",
|
"@apidevtools/swagger-cli": "^4.0.4",
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@ckeditor/ckeditor5-build-classic": "^35.3.2",
|
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
|
||||||
"@ckeditor/ckeditor5-dev-utils": "^31.1.13",
|
"@ckeditor/ckeditor5-dev-utils": "^40.2.0",
|
||||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
|
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
|
||||||
"@ckeditor/ckeditor5-markdown-gfm": "^35.3.2",
|
"@ckeditor/ckeditor5-dev-translations": "^40.2.0",
|
||||||
"@ckeditor/ckeditor5-theme-lark": "^35.3.2",
|
"@ckeditor/ckeditor5-markdown-gfm": "^41.4.2",
|
||||||
"@ckeditor/ckeditor5-vue": "^4.0.1",
|
"@ckeditor/ckeditor5-theme-lark": "^41.4.2",
|
||||||
|
"@ckeditor/ckeditor5-vue": "^5.1.0",
|
||||||
"@symfony/webpack-encore": "^4.1.0",
|
"@symfony/webpack-encore": "^4.1.0",
|
||||||
"@tsconfig/node14": "^1.0.1",
|
"@tsconfig/node14": "^1.0.1",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"bindings": "^1.5.0",
|
"bindings": "^1.5.0",
|
||||||
"bootstrap": "5.2.3",
|
"bootstrap": "5.2.3",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
@ -31,7 +32,7 @@
|
|||||||
"select2-bootstrap-theme": "0.1.0-beta.10",
|
"select2-bootstrap-theme": "0.1.0-beta.10",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-loader": "^9.3.1",
|
"ts-loader": "^9.3.1",
|
||||||
"typescript": "^4.7.2",
|
"typescript": "^5.4.5",
|
||||||
"vue-loader": "^17.0.0",
|
"vue-loader": "^17.0.0",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
|
14
phpstan-baseline-2024-05.neon
Normal file
14
phpstan-baseline-2024-05.neon
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
parameters:
|
||||||
|
ignoreErrors:
|
||||||
|
-
|
||||||
|
message: "#^Parameter \\#1 \\$records of method League\\\\Csv\\\\Writer\\:\\:insertAll\\(\\) expects iterable\\<array\\<float\\|int\\|string\\|Stringable\\|null\\>\\>, iterable\\<array\\<string, bool\\|int\\|string\\>\\> given\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Bundle/ChillMainBundle/Controller/UserExportController.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: """
|
||||||
|
#^Fetching deprecated class constant ASC of class Doctrine\\\\Common\\\\Collections\\\\Criteria\\:
|
||||||
|
use Order\\:\\:Ascending instead$#
|
||||||
|
"""
|
||||||
|
count: 1
|
||||||
|
path: src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
|
@ -33,3 +33,5 @@ includes:
|
|||||||
- phpstan-baseline-level-5.neon
|
- phpstan-baseline-level-5.neon
|
||||||
- phpstan-deprecations-sf54.neon
|
- phpstan-deprecations-sf54.neon
|
||||||
- phpstan-baseline-deprecations-doctrine-orm.neon
|
- phpstan-baseline-deprecations-doctrine-orm.neon
|
||||||
|
- phpstan-baseline-2024-05.neon
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
@ -46,9 +46,7 @@ final readonly class WebdavController
|
|||||||
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['GET', 'HEAD'], name: 'chill_docstore_dav_directory_get')]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
|
|
||||||
*/
|
|
||||||
public function getDirectory(StoredObject $storedObject, string $access_token): Response
|
public function getDirectory(StoredObject $storedObject, string $access_token): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -63,9 +61,7 @@ final readonly class WebdavController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['OPTIONS'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"})
|
|
||||||
*/
|
|
||||||
public function optionsDirectory(StoredObject $storedObject): Response
|
public function optionsDirectory(StoredObject $storedObject): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -82,9 +78,7 @@ final readonly class WebdavController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/', methods: ['PROPFIND'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"})
|
|
||||||
*/
|
|
||||||
public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
|
public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -119,9 +113,7 @@ final readonly class WebdavController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', name: 'chill_docstore_dav_document_get', methods: ['GET'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
|
|
||||||
*/
|
|
||||||
public function getDocument(StoredObject $storedObject): Response
|
public function getDocument(StoredObject $storedObject): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -132,9 +124,7 @@ final readonly class WebdavController
|
|||||||
->setEtag($this->storedObjectManager->etag($storedObject));
|
->setEtag($this->storedObjectManager->etag($storedObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['HEAD'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"})
|
|
||||||
*/
|
|
||||||
public function headDocument(StoredObject $storedObject): Response
|
public function headDocument(StoredObject $storedObject): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -154,9 +144,7 @@ final readonly class WebdavController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['OPTIONS'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"})
|
|
||||||
*/
|
|
||||||
public function optionsDocument(StoredObject $storedObject): Response
|
public function optionsDocument(StoredObject $storedObject): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -172,9 +160,7 @@ final readonly class WebdavController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PROPFIND'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"})
|
|
||||||
*/
|
|
||||||
public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
|
public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
@ -206,9 +192,7 @@ final readonly class WebdavController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Route(path: '/dav/{access_token}/get/{uuid}/d', methods: ['PUT'])]
|
||||||
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"})
|
|
||||||
*/
|
|
||||||
public function putDocument(StoredObject $storedObject, Request $request): Response
|
public function putDocument(StoredObject $storedObject, Request $request): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
||||||
|
@ -39,15 +39,15 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
final public const STATUS_PENDING = 'pending';
|
final public const STATUS_PENDING = 'pending';
|
||||||
final public const STATUS_FAILURE = 'failure';
|
final public const STATUS_FAILURE = 'failure';
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'datas')]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'datas')]
|
||||||
private array $datas = [];
|
private array $datas = [];
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
||||||
private string $filename = '';
|
private string $filename = '';
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||||
@ -56,23 +56,23 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
/**
|
/**
|
||||||
* @var int[]
|
* @var int[]
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
|
||||||
private array $iv = [];
|
private array $iv = [];
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
|
||||||
private array $keyInfos = [];
|
private array $keyInfos = [];
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'title')]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'title')]
|
||||||
private string $title = '';
|
private string $title = '';
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
|
||||||
private string $type = '';
|
private string $type = '';
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
#[ORM\Column(type: 'uuid', unique: true)]
|
#[ORM\Column(type: 'uuid', unique: true)]
|
||||||
private UuidInterface $uuid;
|
private UuidInterface $uuid;
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
* @param StoredObject::STATUS_* $status
|
* @param StoredObject::STATUS_* $status
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Serializer\Groups(['read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])]
|
||||||
private string $status = 'ready'
|
private string $status = 'ready'
|
||||||
) {
|
) {
|
||||||
$this->uuid = Uuid::uuid4();
|
$this->uuid = Uuid::uuid4();
|
||||||
@ -114,7 +114,7 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['read', 'write'])]
|
#[Serializer\Groups(['write'])]
|
||||||
public function getCreationDate(): \DateTime
|
public function getCreationDate(): \DateTime
|
||||||
{
|
{
|
||||||
if (null === $this->createdAt) {
|
if (null === $this->createdAt) {
|
||||||
|
@ -24,7 +24,7 @@ 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
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||||
|
@ -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,73 @@
|
|||||||
|
<?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 Symfony\Component\Form\DataMapperInterface;
|
||||||
|
use Symfony\Component\Form\Exception;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
|
||||||
|
class StoredObjectDataMapper implements DataMapperInterface
|
||||||
|
{
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances
|
||||||
|
*/
|
||||||
|
public function mapDataToForms($viewData, \Traversable $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(\Traversable $forms, &$viewData)
|
||||||
|
{
|
||||||
|
$forms = iterator_to_array($forms);
|
||||||
|
|
||||||
|
if (!(null === $viewData || $viewData instanceof StoredObject)) {
|
||||||
|
throw new Exception\UnexpectedTypeException($viewData, StoredObject::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
<?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 readonly 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((string) $value, true, 10, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
}
|
@ -11,11 +11,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocStoreBundle\Form;
|
namespace Chill\DocStoreBundle\Form;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\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,9 +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(private readonly EntityManagerInterface $em) {}
|
public function __construct(
|
||||||
|
private readonly StoredObjectDataTransformer $storedObjectDataTransformer,
|
||||||
|
private readonly StoredObjectDataMapper $storedObjectDataMapper,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
@ -37,30 +39,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)
|
||||||
@ -72,43 +53,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 %}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<?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\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StoredObjectNormalizer.
|
||||||
|
*
|
||||||
|
* Normalizes a StoredObject entity to an array of data.
|
||||||
|
*/
|
||||||
|
final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||||
|
{
|
||||||
|
use NormalizerAwareTrait;
|
||||||
|
public const ADD_DAV_SEE_LINK_CONTEXT = 'dav-see-link-context';
|
||||||
|
public const ADD_DAV_EDIT_LINK_CONTEXT = 'dav-edit-link-context';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
||||||
|
private readonly UrlGeneratorInterface $urlGenerator
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
|
{
|
||||||
|
/** @var StoredObject $object */
|
||||||
|
$datas = [
|
||||||
|
'datas' => $object->getDatas(),
|
||||||
|
'filename' => $object->getFilename(),
|
||||||
|
'id' => $object->getId(),
|
||||||
|
'iv' => $object->getIv(),
|
||||||
|
'keyInfos' => $object->getKeyInfos(),
|
||||||
|
'title' => $object->getTitle(),
|
||||||
|
'type' => $object->getType(),
|
||||||
|
'uuid' => $object->getUuid(),
|
||||||
|
'status' => $object->getStatus(),
|
||||||
|
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||||
|
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||||
|
];
|
||||||
|
|
||||||
|
// deprecated property
|
||||||
|
$datas['creationDate'] = $datas['createdAt'];
|
||||||
|
|
||||||
|
$canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||||
|
$canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||||
|
|
||||||
|
if ($canDavSee || $canDavEdit) {
|
||||||
|
$accessToken = $this->JWTDavTokenProvider->createToken(
|
||||||
|
$object,
|
||||||
|
$canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
|
);
|
||||||
|
|
||||||
|
$datas['_links'] = [
|
||||||
|
'dav_link' => [
|
||||||
|
'href' => $this->urlGenerator->generate(
|
||||||
|
'chill_docstore_dav_document_get',
|
||||||
|
[
|
||||||
|
'uuid' => $object->getUuid(),
|
||||||
|
'access_token' => $accessToken,
|
||||||
|
],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL,
|
||||||
|
),
|
||||||
|
'expiration' => $this->JWTDavTokenProvider->getTokenExpiration($accessToken)->format('U'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsNormalization($data, ?string $format = null)
|
||||||
|
{
|
||||||
|
return $data instanceof StoredObject && 'json' === $format;
|
||||||
|
}
|
||||||
|
}
|
@ -405,4 +405,6 @@ class MockedStoredObjectManager implements StoredObjectManagerInterface
|
|||||||
{
|
{
|
||||||
return 'ab56b4d92b40713acc5af89985d4b786';
|
return 'ab56b4d92b40713acc5af89985d4b786';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clearCache(): void {}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
<?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\Tests\Form;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
|
||||||
|
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
|
||||||
|
use Chill\DocStoreBundle\Form\StoredObjectType;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
|
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Form\PreloadedExtension;
|
||||||
|
use Symfony\Component\Form\Test\TypeTestCase;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||||
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class StoredObjectTypeTest extends TypeTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testChangeTitleValue(): void
|
||||||
|
{
|
||||||
|
$formData = ['title' => $newTitle = 'new title', 'stored_object' => <<<'JSON'
|
||||||
|
{"datas":[],"filename":"","id":null,"iv":[],"keyInfos":[],"title":"","type":"","uuid":"3c6a28fe-f913-40b9-a201-5eccc4f2d312","status":"ready","createdAt":null,"createdBy":null,"creationDate":null,"_links":{"dav_link":{"href":"http:\/\/url\/fake","expiration":"1716889578"}}}
|
||||||
|
JSON];
|
||||||
|
$model = new StoredObject();
|
||||||
|
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
||||||
|
|
||||||
|
$form->submit($formData);
|
||||||
|
|
||||||
|
$this->assertTrue($form->isSynchronized());
|
||||||
|
|
||||||
|
$this->assertEquals($newTitle, $model->getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReplaceByAnotherObject(): void
|
||||||
|
{
|
||||||
|
$formData = ['title' => $newTitle = 'new title', 'stored_object' => <<<'JSON'
|
||||||
|
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
|
||||||
|
JSON];
|
||||||
|
$model = new StoredObject();
|
||||||
|
$originalObjectId = spl_object_id($model);
|
||||||
|
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
||||||
|
|
||||||
|
$form->submit($formData);
|
||||||
|
|
||||||
|
$this->assertTrue($form->isSynchronized());
|
||||||
|
$model = $form->getData();
|
||||||
|
$this->assertNotEquals($originalObjectId, spl_object_hash($model));
|
||||||
|
$this->assertEquals('abcdef', $model->getFilename());
|
||||||
|
$this->assertEquals([10, 15, 20, 30], $model->getIv());
|
||||||
|
$this->assertEquals('text/html', $model->getType());
|
||||||
|
$this->assertEquals($newTitle, $model->getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getExtensions()
|
||||||
|
{
|
||||||
|
$jwtTokenProvider = $this->prophesize(JWTDavTokenProviderInterface::class);
|
||||||
|
$jwtTokenProvider->createToken(Argument::type(StoredObject::class), Argument::type(StoredObjectRoleEnum::class))
|
||||||
|
->willReturn('token');
|
||||||
|
$jwtTokenProvider->getTokenExpiration('token')->willReturn(new \DateTimeImmutable());
|
||||||
|
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||||
|
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
|
||||||
|
->willReturn('http://url/fake');
|
||||||
|
|
||||||
|
$serializer = new Serializer(
|
||||||
|
[
|
||||||
|
new StoredObjectNormalizer(
|
||||||
|
$jwtTokenProvider->reveal(),
|
||||||
|
$urlGenerator->reveal(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new JsonEncoder(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$dataTransformer = new StoredObjectDataTransformer($serializer);
|
||||||
|
$dataMapper = new StoredObjectDataMapper();
|
||||||
|
$type = new StoredObjectType(
|
||||||
|
$dataTransformer,
|
||||||
|
$dataMapper,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
new PreloadedExtension([$type], []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -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');
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -211,7 +211,7 @@ class SearchController extends AbstractController
|
|||||||
$builder = $this
|
$builder = $this
|
||||||
->get('form.factory')
|
->get('form.factory')
|
||||||
->createNamedBuilder(
|
->createNamedBuilder(
|
||||||
null,
|
'',
|
||||||
FormType::class,
|
FormType::class,
|
||||||
$data,
|
$data,
|
||||||
['method' => Request::METHOD_POST]
|
['method' => Request::METHOD_POST]
|
||||||
|
@ -71,6 +71,7 @@ final readonly class UserExportController
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
$csv->addFormatter(fn (array $row) => null !== ($row['absenceStart'] ?? null) ? array_merge($row, ['absenceStart' => $row['absenceStart']->format('Y-m-d')]) : $row);
|
$csv->addFormatter(fn (array $row) => null !== ($row['absenceStart'] ?? null) ? array_merge($row, ['absenceStart' => $row['absenceStart']->format('Y-m-d')]) : $row);
|
||||||
|
/* @phpstan-ignore-next-line as phpstan seem to ignore that we transform datetime into string */
|
||||||
$csv->insertAll($users);
|
$csv->insertAll($users);
|
||||||
|
|
||||||
return new StreamedResponse(
|
return new StreamedResponse(
|
||||||
|
@ -85,7 +85,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
*/
|
*/
|
||||||
#[Assert\Valid(traverse: true)]
|
#[Assert\Valid(traverse: true)]
|
||||||
#[ORM\OneToMany(targetEntity: EntityWorkflowStep::class, mappedBy: 'entityWorkflow', orphanRemoval: true, cascade: ['persist'])]
|
#[ORM\OneToMany(targetEntity: EntityWorkflowStep::class, mappedBy: 'entityWorkflow', orphanRemoval: true, cascade: ['persist'])]
|
||||||
#[ORM\OrderBy(['transitionAt' => Order::Ascending, 'id' => 'ASC'])]
|
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
|
||||||
private Collection $steps;
|
private Collection $steps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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');
|
||||||
|
@ -59,6 +59,10 @@ export const ISOToDatetime = (str: string|null): Date|null => {
|
|||||||
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
|
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ('0000' === timezone) {
|
||||||
|
return new Date(Date.UTC(year, month-1, date, hours, minutes, seconds));
|
||||||
|
}
|
||||||
|
|
||||||
return new Date(year, month-1, date, hours, minutes, seconds);
|
return new Date(year, month-1, date, hours, minutes, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -15,9 +15,9 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var mime = require('mime')
|
import mime from 'mime';
|
||||||
|
|
||||||
var download_report = (url, container) => {
|
export const download_report = (url, container) => {
|
||||||
var download_text = container.dataset.downloadText,
|
var download_text = container.dataset.downloadText,
|
||||||
alias = container.dataset.alias;
|
alias = container.dataset.alias;
|
||||||
|
|
||||||
@ -63,5 +63,3 @@ var download_report = (url, container) => {
|
|||||||
.replaceChild(problem_text, container.firstChild);
|
.replaceChild(problem_text, container.firstChild);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = download_report;
|
|
||||||
|
@ -39,23 +39,5 @@ ClassicEditor.defaultConfig = {
|
|||||||
'redo'
|
'redo'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
language: 'fr'
|
language: 'fr',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let Fields = [];
|
|
||||||
Fields.push.apply(Fields, document.querySelectorAll('textarea[ckeditor]'));
|
|
||||||
// enable for custom fields
|
|
||||||
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));
|
|
||||||
|
|
||||||
Fields.forEach(function(field) {
|
|
||||||
ClassicEditor
|
|
||||||
.create( field )
|
|
||||||
.then( editor => {
|
|
||||||
//console.log( 'CkEditor was initialized', editor );
|
|
||||||
})
|
|
||||||
.catch( error => {
|
|
||||||
console.error( error.stack );
|
|
||||||
})
|
|
||||||
;
|
|
||||||
});
|
|
@ -0,0 +1,15 @@
|
|||||||
|
import ClassicEditor from "./editor_config";
|
||||||
|
|
||||||
|
const ckeditorFields: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll('textarea[ckeditor]');
|
||||||
|
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
|
||||||
|
ClassicEditor
|
||||||
|
.create( field )
|
||||||
|
.then( editor => {
|
||||||
|
//console.log( 'CkEditor was initialized', editor );
|
||||||
|
})
|
||||||
|
.catch( error => {
|
||||||
|
console.error( error.stack );
|
||||||
|
})
|
||||||
|
;
|
||||||
|
});
|
||||||
|
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
import {download_report} from "../../lib/download-report/download-report";
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", function(e) {
|
||||||
|
const export_generate_url = window.export_generate_url;
|
||||||
|
|
||||||
|
if (typeof export_generate_url === 'undefined') {
|
||||||
|
console.error('Alias not found!');
|
||||||
|
throw new Error('Alias not found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = window.location.search,
|
||||||
|
container = document.querySelector("#download_container")
|
||||||
|
;
|
||||||
|
|
||||||
|
download_report(export_generate_url + "?" + query.toString(), container);
|
||||||
|
});
|
@ -0,0 +1,306 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
SASS Assets Catalogue
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style media="screen">
|
||||||
|
h2 { margin: 1.5em 0; }
|
||||||
|
div.flex-table ul, div.flex-bloc ul { padding-left: 1rem; }
|
||||||
|
div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }
|
||||||
|
div.flex-bloc div.item-bloc { flex-basis: 50%; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-md-10">
|
||||||
|
<h1 class="display-4">{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
<b>Voir aussi: </b>
|
||||||
|
<a href="{{ path('sass_assets_test1') }}">Test 1</a> |
|
||||||
|
<a href="{{ path('sass_assets_test2') }}">Test 2</a>
|
||||||
|
|
||||||
|
<h2>Flex-table et flex-bloc</h2>
|
||||||
|
<p>Base d'un placement flex alternatif à l'usage des tables.
|
||||||
|
Flex-table et flex-bloc utilisent la même structure html (seul la root class change).
|
||||||
|
Le placement est responsive.
|
||||||
|
La bordure utilise box-shadow pour simuler border-collapse (table).
|
||||||
|
</p>
|
||||||
|
<p>Une classe separator peut être appliquée sur item-row</p>
|
||||||
|
|
||||||
|
<xmp>
|
||||||
|
<div class="flex-table">
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col"></div>
|
||||||
|
<div class="item-col"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col"></div>
|
||||||
|
<div class="item-col"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col"></div>
|
||||||
|
<div class="item-col"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col"></div>
|
||||||
|
<div class="item-col"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xmp>
|
||||||
|
|
||||||
|
<h3>Flex-table</h3>
|
||||||
|
<p>On fixe manuellement la largeur de la première colonne :
|
||||||
|
<pre>div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }</pre>
|
||||||
|
</p>
|
||||||
|
<div class="flex-table debug">
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row1</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col">Title row2</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row3</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row1</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row2</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Flex-bloc</h3>
|
||||||
|
<p>On fixe manuellement la largeur des blocs :
|
||||||
|
<pre>div.flex-bloc div.item-bloc { flex-basis: 50%; }</pre>
|
||||||
|
</p>
|
||||||
|
<div class="flex-bloc debug">
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row1</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col">Title row2</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row3</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row1</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">Title row2</div>
|
||||||
|
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Wrap-list</h2>
|
||||||
|
<p>Une liste inline qui s'aligne, puis glisse sous son titre.</p>
|
||||||
|
<div class="wrap-list debug">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">Usagers concernés</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item"><a href="#">Gaston Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Alain Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Corentine Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Justin Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Michel Sardou</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Carine Rousseau</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Mohamed Martin</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">Problématiques sociales</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item"><a href="#">Gaston Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Alain Bah</a></p>
|
||||||
|
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<xmp>
|
||||||
|
<div class="wrap-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">title</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item">item</p>
|
||||||
|
<p class="wl-item">item</p>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</xmp>
|
||||||
|
|
||||||
|
<h2>Wrap-header</h2>
|
||||||
|
<p>Réglage d'une zone de titre sur 2 lignes.</p>
|
||||||
|
<div class="wrap-header debug">
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">
|
||||||
|
<span class="h3"><b>Title</b></span>
|
||||||
|
<span class="badge rounded-pill bg-danger">badge</span>
|
||||||
|
</div>
|
||||||
|
<div class="wh-col">
|
||||||
|
<span class="badge rounded-pill bg-primary">badge</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">from startdate to enddate</div>
|
||||||
|
<div class="wh-col">text</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<xmp>
|
||||||
|
<div class="wrap-header">
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">line1 left</div>
|
||||||
|
<div class="wh-col">line1 right</div>
|
||||||
|
</div>
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">line2 left</div>
|
||||||
|
<div class="wh-col">line2 right</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xmp>
|
||||||
|
|
||||||
|
<h2>Float-button top</h2>
|
||||||
|
<p>Une zone de bouton flotte à droite d'un contenu. On peut voir en faisant varier la largeur que celui-ci vient s'adapter harmonieusement autour des boutons.</p>
|
||||||
|
<div class="float-button top debug">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<xmp>
|
||||||
|
<div class="float-button top">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
floating button
|
||||||
|
</div>
|
||||||
|
content ...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xmp>
|
||||||
|
|
||||||
|
<h2>Float-button bottom</h2>
|
||||||
|
<p>Avec la même structure, on accroche la zone de bouton en bas, toujours à droite. Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">source</a>. </p>
|
||||||
|
<div class="float-button bottom debug">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<xmp>
|
||||||
|
<div class="float-button bottom">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
floating button
|
||||||
|
</div>
|
||||||
|
content ...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xmp>
|
||||||
|
|
||||||
|
|
||||||
|
<h1>Buttons</h1>
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><a href="#" class="btn btn-submit">submit</a></li>
|
||||||
|
<li><a href="#" class="btn btn-save">save</a></li>
|
||||||
|
<li><a href="#" class="btn btn-create">create</a></li>
|
||||||
|
<li><a href="#" class="btn btn-new">new</a></li>
|
||||||
|
<li><a href="#" class="btn btn-duplicate">duplicate</a></li>
|
||||||
|
<li><a href="#" class="btn btn-not-duplicate">not-duplicate</a></li>
|
||||||
|
<li><a href="#" class="btn btn-reset">reset</a></li>
|
||||||
|
<li><a href="#" class="btn btn-delete">delete</a></li>
|
||||||
|
<li><a href="#" class="btn btn-danger">danger</a></li>
|
||||||
|
<li><a href="#" class="btn btn-remove">remove</a></li>
|
||||||
|
<li><a href="#" class="btn btn-unlink">unlink</a></li>
|
||||||
|
<li><a href="#" class="btn btn-action">action</a></li>
|
||||||
|
<li><a href="#" class="btn btn-edit">edit</a></li>
|
||||||
|
<li><a href="#" class="btn btn-update">update</a></li>
|
||||||
|
<li><a href="#" class="btn btn-show">show</a></li>
|
||||||
|
<li><a href="#" class="btn btn-view">view</a></li>
|
||||||
|
<li><a href="#" class="btn btn-misc">misc</a></li>
|
||||||
|
<li><a href="#" class="btn btn-cancel">cancel</a></li>
|
||||||
|
<li><a href="#" class="btn btn-choose">choose</a></li>
|
||||||
|
<li><a href="#" class="btn btn-notify">notify</a></li>
|
||||||
|
<li><a href="#" class="btn btn-tpchild">tpchild</a></li>
|
||||||
|
<li><a href="#" class="btn btn-chill-beige">my button</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Variants of <pre>record_actions</pre></h2>
|
||||||
|
|
||||||
|
<h3><pre>small</pre></h3>
|
||||||
|
|
||||||
|
<ul class="record_actions small">
|
||||||
|
<li><a href="#" class="btn btn-create"></a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3><pre>inline</pre></h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
This is inline and small
|
||||||
|
|
||||||
|
<ul class="record_actions small inline">
|
||||||
|
<li><a href="#" class="btn btn-create"></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<xmp><a class="btn btn-submit">Text</a></xmp>
|
||||||
|
Toutes les classes btn-* de bootstrap sont fonctionnelles
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,84 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
SASS Assets Tests - page 1
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style media="screen">
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1>CSS Tests - page 1 : float-button</h1>
|
||||||
|
|
||||||
|
<h2>1) avec des li</h2>
|
||||||
|
<div class="float-button bottom debug">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul class="list-content fa-ul">
|
||||||
|
<li><i class="fa fa-li fa-file-text-o"></i>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus.</li>
|
||||||
|
<li><i class="fa fa-li fa-map-marker"></i>
|
||||||
|
<div class="chill-entity entity-address my-3" data-v-8b2170ec="">
|
||||||
|
<div class="address multiline" data-v-8b2170ec="">
|
||||||
|
<p class="street" data-v-8b2170ec="">97, chemin Franck Julien, </p>
|
||||||
|
<p class="postcode" data-v-8b2170ec="">1000 Bruxelles</p>
|
||||||
|
<p class="country" data-v-8b2170ec="">Belgique</p>
|
||||||
|
</div>
|
||||||
|
<div class="address-more" data-v-8b2170ec="">
|
||||||
|
<div data-v-8b2170ec="">
|
||||||
|
<span class="corridor" data-v-8b2170ec="">
|
||||||
|
<b data-v-8b2170ec="">Couloir</b>: 3
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><i class="fa fa-li fa-mobile"></i><a href="tel: +33 8 27 17 12 19">+33 8 27 17 12 19</a></li>
|
||||||
|
<li><i class="fa fa-li fa-envelope-o"></i><a href="mailto: gusikowski.yesenia@hotmail.com">gusikowski.yesenia@hotmail.com</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2) avec des p</h2>
|
||||||
|
|
||||||
|
<div class="float-button bottom debug">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</p>
|
||||||
|
<p>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam. Curabitur sem eros, consectetur a auctor vel, facilisis sit amet sem.</p>
|
||||||
|
<p>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>3) avec des div</h2>
|
||||||
|
<div class="float-button bottom debug">
|
||||||
|
<div class="box">
|
||||||
|
<div class="action">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</div>
|
||||||
|
<div>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam.
|
||||||
|
<a href="#">Curabitur</a> sem eros, consectetur a auctor vel, facilisis sit amet sem.</div>
|
||||||
|
<div>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,78 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
SASS Assets Tests - page 2
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-md-10">
|
||||||
|
<h1>CSS Tests - page 2: grid layout</h1>
|
||||||
|
|
||||||
|
<h2>1) mgrid 1-2: avec grid-column et grid-row</h2>
|
||||||
|
|
||||||
|
<div class="mgrid debug">
|
||||||
|
<div class="area1">
|
||||||
|
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
<div class="area2">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>2) lgrid 3-4: avec grid-template-areas</h2>
|
||||||
|
|
||||||
|
<div class="lgrid debug">
|
||||||
|
<div class="area3">
|
||||||
|
<i>La zone qu'on crée avec les noms doit être rectangulaires. Actuellement, il n'existe pas de méthode pour créer une zone avec une forme de L (bien que la spécification indique qu'une prochaine version pourrait couvrir cette fonctionnalité).
|
||||||
|
[...] Si des zones ne sont pas rectangulaires, cela sera également considéré comme invalide.</i>
|
||||||
|
Voir sur MDN: <a target="_blank" href="https://developer.mozilla.org/fr/docs/Web/CSS/CSS_Grid_Layout/Grid_Template_Areas#occuper_plusieurs_cellules">Définir des zones sur une grille</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="area4">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button type="button" name="button">Annuler</button></li>
|
||||||
|
<li><button type="button" name="button">Voir</button></li>
|
||||||
|
<li><button type="button" name="button">Enregistrer</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>3) cgrid 5-6-7-8: avec masonry</h2>
|
||||||
|
<p>Expérimental: dans FF <i>about:config</i>, il faut mettre <i>layout.css.grid-template-masonry-value.enabled = true</i></p>
|
||||||
|
|
||||||
|
<div class="cgrid debug">
|
||||||
|
<div class="item">
|
||||||
|
1 Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
2 Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
3 Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
4 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
5 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
6 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
7 Proin hendrerit arcu velit, eu ultrices dui interdum eget.
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
8 Eu ultrices dui interdum eget.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -22,15 +22,13 @@
|
|||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.addEventListener("DOMContentLoaded", function(e) {
|
window.export_generate_url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}";
|
||||||
var url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}",
|
|
||||||
query = window.location.search,
|
|
||||||
container = document.querySelector("#download_container")
|
|
||||||
;
|
|
||||||
|
|
||||||
chill.download_report(url+query, container);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
{{ encore_entry_link_tags('page_download_exports') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ encore_entry_script_tags('page_download_exports') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -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') }}
|
||||||
|
@ -18,7 +18,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||||||
|
|
||||||
class AddressReferenceBEFromBestAddress
|
class AddressReferenceBEFromBestAddress
|
||||||
{
|
{
|
||||||
private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.0.0';
|
private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.1.1';
|
||||||
|
|
||||||
public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $baseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) {}
|
public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $baseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) {}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
|
|
||||||
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
|
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
|
||||||
|
const {CKEditorTranslationsPlugin} = require("@ckeditor/ckeditor5-dev-translations");
|
||||||
|
|
||||||
buildCKEditor = function(encore)
|
buildCKEditor = function(encore)
|
||||||
{
|
{
|
||||||
encore
|
encore
|
||||||
.addPlugin( new CKEditorWebpackPlugin( {
|
.addPlugin( new CKEditorTranslationsPlugin( {
|
||||||
language: 'fr',
|
language: 'fr',
|
||||||
addMainLanguageTranslationsToAllAssets: true,
|
addMainLanguageTranslationsToAllAssets: true,
|
||||||
verbose: !encore.isProduction(),
|
verbose: !encore.isProduction(),
|
||||||
@ -52,19 +52,22 @@ module.exports = function(encore, entries)
|
|||||||
Tabs: __dirname + '/Resources/public/lib/tabs'
|
Tabs: __dirname + '/Resources/public/lib/tabs'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Page entrypoints
|
// Page entrypoints
|
||||||
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
|
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
|
||||||
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
|
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
|
||||||
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
|
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
|
||||||
encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js');
|
encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js');
|
||||||
encore.addEntry('page_export', __dirname + '/Resources/public/page/export/index.js');
|
encore.addEntry('page_export', __dirname + '/Resources/public/page/export/index.js');
|
||||||
|
encore.addEntry('page_download_exports', __dirname + '/Resources/public/page/export/download-export.js');
|
||||||
|
|
||||||
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');
|
||||||
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
|
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
|
||||||
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
||||||
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
||||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Controller;
|
namespace Chill\PersonBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||||
@ -115,7 +116,7 @@ final class AccompanyingCourseWorkController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
|
$this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
|
||||||
|
|
||||||
$json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]);
|
$json = $this->serializer->normalize($work, 'json', ['groups' => ['read', StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT]]);
|
||||||
|
|
||||||
return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [
|
return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [
|
||||||
'accompanyingCourse' => $work->getAccompanyingPeriod(),
|
'accompanyingCourse' => $work->getAccompanyingPeriod(),
|
||||||
|
@ -592,14 +592,15 @@ class Household implements HasCentersInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
public function validate(ExecutionContextInterface $context, $payload)
|
public function validate(ExecutionContextInterface $context, $payload): void
|
||||||
{
|
{
|
||||||
$addresses = $this->getAddresses();
|
$addresses = $this->getAddressesOrdered();
|
||||||
$cond = true;
|
|
||||||
|
|
||||||
for ($i = 0; \count($addresses) - 1 > $i; ++$i) {
|
for ($i = 0; \count($addresses) - 1 > $i; ++$i) {
|
||||||
if ($addresses[$i]->getValidFrom() !== $addresses[$i + 1]->getValidTo()) {
|
if (0 === $i) {
|
||||||
$cond = false;
|
continue;
|
||||||
|
}
|
||||||
|
if ($addresses[$i - 1]->getValidTo() !== $addresses[$i]->getValidFrom()) {
|
||||||
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
|
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
|
||||||
->atPath('addresses')
|
->atPath('addresses')
|
||||||
->addViolation();
|
->addViolation();
|
||||||
|
@ -28,11 +28,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
|
|
||||||
final readonly class GeographicalUnitStatAggregator implements AggregatorInterface
|
final readonly class GeographicalUnitStatAggregator implements AggregatorInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, private TranslatableStringHelperInterface $translatableStringHelper, private RollingDateConverterInterface $rollingDateConverter) {}
|
||||||
private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository,
|
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
|
||||||
private RollingDateConverterInterface $rollingDateConverter
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
{
|
{
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
|
import ClassicEditor from "../../../../../../ChillMainBundle/Resources/public/module/ckeditor5/editor_config";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
|
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
|
||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
|
import ClassicEditor from "ChillMainAssets/module/ckeditor5/editor_config";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "WriteComment",
|
name: "WriteComment",
|
||||||
|
@ -331,7 +331,7 @@
|
|||||||
import {mapState, mapGetters,} from 'vuex';
|
import {mapState, mapGetters,} from 'vuex';
|
||||||
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
|
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
|
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
|
||||||
import AddResult from './components/AddResult.vue';
|
import AddResult from './components/AddResult.vue';
|
||||||
import AddEvaluation from './components/AddEvaluation.vue';
|
import AddEvaluation from './components/AddEvaluation.vue';
|
||||||
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
|
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
|
||||||
|
@ -135,6 +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"
|
||||||
|
: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>
|
||||||
@ -193,7 +195,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
|
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
|
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
|
||||||
import { mapGetters, mapState } from 'vuex';
|
import { mapGetters, mapState } from 'vuex';
|
||||||
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
|
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
|
||||||
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
|
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
|
||||||
|
@ -75,7 +75,7 @@ div.participation-details {
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
|
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
|
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MemberDetails',
|
name: 'MemberDetails',
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||||
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
|
import ClassicEditor from "ChillMainAssets/module/ckeditor5/editor_config";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PersonComment.vue",
|
name: "PersonComment.vue",
|
||||||
|
@ -15,7 +15,7 @@ const personMessages = {
|
|||||||
person: {
|
person: {
|
||||||
firstname: "Prénom",
|
firstname: "Prénom",
|
||||||
lastname: "Nom",
|
lastname: "Nom",
|
||||||
born: (ctx) => {
|
born: (ctx: {gender: "man"|"woman"|"unknown"}) => {
|
||||||
if (ctx.gender === 'man') {
|
if (ctx.gender === 'man') {
|
||||||
return 'Né le';
|
return 'Né le';
|
||||||
} else if (ctx.gender === 'woman') {
|
} else if (ctx.gender === 'woman') {
|
@ -19,7 +19,6 @@
|
|||||||
<h1 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h1>
|
<h1 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h1>
|
||||||
|
|
||||||
<div class="col-md col-xxl">
|
<div class="col-md col-xxl">
|
||||||
<h3 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h3>
|
|
||||||
{% include "@ChillPerson/PersonResource/form.html.twig" %}
|
{% include "@ChillPerson/PersonResource/form.html.twig" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -250,9 +250,9 @@ Concerned scopes: Services concernés
|
|||||||
# person resource
|
# person resource
|
||||||
person_resources_menu: "Personnes ressources"
|
person_resources_menu: "Personnes ressources"
|
||||||
Person resources: "Personnes ressources de l'usager"
|
Person resources: "Personnes ressources de l'usager"
|
||||||
Add a person resource: "Ajouter une person ressource"
|
Add a person resource: "Ajouter une personne ressource"
|
||||||
edit resource: "Modifier la ressource"
|
edit resource: "Modifier la personne ressource"
|
||||||
Remove resource: "Supprimer la ressource"
|
Remove resource: "Supprimer la personne ressource"
|
||||||
Are you sure you want to remove the resource for "%name%" ?: Étes-vous sûr de vouloir supprimer cette ressource de %name%?
|
Are you sure you want to remove the resource for "%name%" ?: Étes-vous sûr de vouloir supprimer cette ressource de %name%?
|
||||||
The resource has been successfully removed.: "La ressource a été supprimée."
|
The resource has been successfully removed.: "La ressource a été supprimée."
|
||||||
List of resources: "Liste des personnes ressources"
|
List of resources: "Liste des personnes ressources"
|
||||||
@ -293,7 +293,7 @@ residential_address_new_address_explanation: Créer une nouvelle adresse. L'adre
|
|||||||
New residential address: Nouvelle adresse de résidence
|
New residential address: Nouvelle adresse de résidence
|
||||||
Host person: Choisir l'adresse d'un usager
|
Host person: Choisir l'adresse d'un usager
|
||||||
The new residential address was created successfully: La nouvelle adresse de résidence a été créée
|
The new residential address was created successfully: La nouvelle adresse de résidence a été créée
|
||||||
Edit a residential address: Modifier l'addresse de résidence
|
Edit a residential address: Modifier l'adresse de résidence
|
||||||
The residential address was updated successfully: L'adresse de résidence a été mise à jour
|
The residential address was updated successfully: L'adresse de résidence a été mise à jour
|
||||||
Residential addresses: Adresses de résidence
|
Residential addresses: Adresses de résidence
|
||||||
Address of: Adresse de
|
Address of: Adresse de
|
||||||
|
@ -598,10 +598,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function setCenters(Collection $centers): self
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setCenters(Collection $centers)
|
|
||||||
{
|
{
|
||||||
$this->centers = $centers;
|
$this->centers = $centers;
|
||||||
|
|
||||||
|
@ -43,6 +43,4 @@ return [
|
|||||||
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
|
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
|
||||||
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
||||||
\Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
\Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
Chill\EventBundle\ChillEventBundle::class => ['all' => true],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -38,3 +38,5 @@ when@test:
|
|||||||
test: true
|
test: true
|
||||||
session:
|
session:
|
||||||
storage_factory_id: session.storage.factory.mock_file
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
|
assets:
|
||||||
|
json_manifest_path: null
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
framework:
|
|
||||||
test: true
|
|
||||||
session:
|
|
||||||
storage_id: session.storage.mock_file
|
|
||||||
assets:
|
|
||||||
json_manifest_path: NULL
|
|
Loading…
x
Reference in New Issue
Block a user