Compare commits

...

43 Commits

Author SHA1 Message Date
2ca9de2f60 If value is '' and multiple is true then return an empty array 2025-01-07 16:41:14 +01:00
184bb095d8 Update chill-bundles version to 3.5.2 2024-12-19 10:45:07 +01:00
78c8e94765 Merge branch '345-export-activity-bug' into 'master'
Fix the filtering of users that have been associated to an activity between certain dates

Closes #345

See merge request Chill-Projet/chill-bundles!773
2024-12-19 09:41:21 +00:00
c8d1a91953 Fix the filtering of users that have been associated to an activity between certain dates 2024-12-19 09:41:21 +00:00
3e8e2b0fa8 Merge branch 'add-employment-status' into 'master'
add an employmentStatus property to the person entity

See merge request Chill-Projet/chill-bundles!763
2024-12-17 15:15:57 +00:00
Christophe Siraut
2c9c700ca7 composer.json: specify doctrine/data-fixtures version 2024-12-16 17:35:22 +01:00
Christophe Siraut
110db30748 ChillPersonBundle: add employmentStatus property to Person 2024-12-16 17:35:19 +01:00
Christophe Siraut
1f96f76f87 ChillPersonBundle: add a visibility parameter to addFieldNode() 2024-12-16 16:54:18 +01:00
Christophe Siraut
65902ea231 ChillCUstomFieldsBundle: Symfony\Component\Frm\FormFactory::createNamedBuilder(): Argument #1 () must be of type string 2024-12-16 16:54:18 +01:00
Christophe Siraut
88c0b1570d composer: require-dev php-cs-fixer 2024-12-16 16:54:18 +01:00
4933d2251c fix tests about PdfSignedMessageHandler.php 2024-12-16 16:50:07 +01:00
c939ff4a4e Release v3.5.1 2024-12-16 15:45:29 +01:00
82fb98348b Wrap PdfSignedMessage handling in a transaction
Ensure atomicity when writing stored objects and marking signatures as signed by wrapping these operations in a database transaction. This reduces the risk of partial updates and improves data consistency.
2024-12-16 15:44:40 +01:00
d56d00421a Add convetion on database table and add translation file in english for the conventions 2024-12-12 11:37:54 +01:00
de11fa86c6 Add database table naming convention 2024-12-12 10:52:39 +01:00
4cc254a3e5 Add script to package.json for eslint + take ts files into account with new baseline 2024-12-11 14:24:53 +01:00
22ce16ef49 Fix display of gender label in the filiation 2024-12-11 14:12:54 +01:00
1e52a93cbe ignore certain folders for eslint 2024-12-11 12:26:41 +01:00
16fe07cce7 More automatic eslint fixes, update baseline and eslint docs 2024-12-11 11:32:29 +01:00
e8962782ed Merge branch 'implement_eslint' into 'master'
Implement ESLint

See merge request Chill-Projet/chill-bundles!752
2024-12-11 10:26:35 +00:00
64a853fb6d Renew baseline after applying prettier 2024-12-11 10:50:01 +01:00
ebfd48e41f Apply prettier to files 2024-12-11 10:49:11 +01:00
a6aa2a81c2 Resolve merge with master 2024-12-11 10:46:06 +01:00
98d29c2134 Add baseline to eslint 2024-12-11 10:38:55 +01:00
54e10cacd3 git use apk instead of apt-get to install jq 2024-12-05 16:25:40 +01:00
3ca126804b Implement baseline comparison for eslint in the CI 2024-12-05 16:13:07 +01:00
40d733c290 Add a baseline for eslint 2024-12-05 16:08:42 +01:00
aa0785fc71 Apply prettier rules 2024-11-14 18:47:38 +01:00
610227815a Add prettier to eslint to format vue, js and ts code + add script to run it locally 2024-11-14 18:46:22 +01:00
9d9f062417 add eslint to ci gitlab 2024-11-14 18:37:37 +01:00
0454e5d758 eslint corrections in mainbundle 2024-11-14 16:46:07 +01:00
2d6d2a1f58 eslint corrections in personbundle 2024-11-14 16:45:50 +01:00
c971e34675 JS correction in ChillWopiBundle required by ESLint 2024-11-12 15:39:52 +01:00
be2a119163 JS correction in ChillThirdpartyBundle required by ESLint 2024-11-12 15:39:31 +01:00
22ecb11227 JS corrections in ChillPersonBundle required by ESLint 2024-11-12 15:38:46 +01:00
dd854ea339 Switch back to using .mjs extension otherwise webpack doesn't compile well with type:module specified in package.json file 2024-11-05 15:48:48 +01:00
f05c25853c Fix of errors: automatic and some manual 2024-11-04 19:56:03 +01:00
90798b12e5 Add ts parser and adjust config 2024-11-04 19:55:27 +01:00
f91f5ce27e Add eslint documentation to developers manual 2024-11-04 17:57:13 +01:00
96f73b419d Eslint correction always return something in computed property 2024-10-31 19:11:49 +01:00
273f91fd00 Add config for eslint 2024-10-31 19:11:16 +01:00
27ecae4486 first eslint corrections 2024-10-31 17:28:02 +01:00
f90f1c7ef8 Install eslint and eslint vue plugin + config file 2024-10-31 17:27:45 +01:00
340 changed files with 31077 additions and 22957 deletions

4
.changes/v3.5.1.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.5.1 - 2024-12-16
### Fixed
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions

3
.changes/v3.5.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.5.2 - 2024-12-19
### Fixed
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"

2386
.eslint-baseline.json Normal file

File diff suppressed because it is too large Load Diff

5
.gitignore vendored
View File

@@ -51,3 +51,8 @@ phpstan.neon
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###

View File

@@ -5,6 +5,7 @@ cache:
paths:
- /vendor/
- .cache
- node_modules/
# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# See http://docs.gitlab.com/ee/ci/services/README.html for examples.
@@ -103,6 +104,32 @@ rector_tests:
paths:
- vendor/
lint:
stage: Tests
image: node:20-alpine
before_script:
- apk add --no-cache python3 make g++ py3-setuptools
- export PYTHON="$(which python3)"
- export PATH="./node_modules/.bin:$PATH"
script:
- yarn install --ignore-optional
- npx eslint-baseline "**/*.{js,vue}"
cache:
paths:
- node_modules/
# psalm_tests:
# stage: Tests
# image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
# script:
# - bin/psalm
# allow_failure: true
# artifacts:
# expire_in: 30 min
# paths:
# - bin
# - tests/app/vendor/
unit_tests:
stage: Tests
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82

View File

@@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.5.2 - 2024-12-19
### Fixed
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"
## v3.5.1 - 2024-12-16
### Fixed
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions
## v3.5.0 - 2024-12-09
### Feature
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app

386
CONVENTIONS-en.md Normal file
View File

@@ -0,0 +1,386 @@
# Chill Conventions
In Progress
## Translations
Per bundle, all translations for Twig pages are located in a single file: translations/messages.fr.yaml.
## File Locations
Controllers, form types, and Twig templates are placed in the root folders Controller, Form, and Resources/views, respectively.
Admin pages are no longer placed in subfolders under Admin.
## Assets: Entrypoint Naming
There are three types of entry points:
* Vue application-specific (often for a single page): Prefixed with vue_.
* Reusable JavaScript/CSS code:
Examples include:
* ckeditor
* async_upload (used for forms)
* bootstrap
* chill.js
* ...
=> We prefix with `mod_`
* Page-specific CSS/JS:
* Often reuses functionalities like ShowHide.
=> We prefix with `page_`.
## Folder Structure
````
# Under Resources/public:
- chill/ => Contains the theme (Chill).
- chillmain.scss: Compiled into the chill entrypoint.
- lib/ => Libraries never used as entrypoints but reused elsewhere.
- Examples: ShowHide, Collection, Select2.
- module/: Ends up in reusable entrypoints (mod_).
- bootstrap
- custom.scss
- custom/
- variable.scss
- ...
- AsyncUpload.
- vue/ => Vue applications only (vue_).
- Examples: _components, app.
- page/ => Page-specific assets (page_).
- login
- person
- personvendee
- household_edit_metadata
- index.js
````
# Stylesheet Organization
1. The mod_bootstrap (module bootstrap) entry point is the first level. All parts (modules) of bootstrap are included in the bootstrap.js file located in ChillMainBundle/Resources/public/module/bootstrap.
* At the beginning, this file imports the variables.scss file, which determines most of the bootstrap settings as customized. This file overrides the original, and many variables are adjusted for Chill.
* Care must be taken to ensure this file can always be compared to the original bootstrap file. In the event of a bootstrap update, a diff must be generated and this diff applied to the variable file of the new version.
* At the end, it imports the custom.scss file, which includes bootstrap adaptations to prepare it for our Chill theme.
* This `custom.scss` file can be split into smaller files using `@import 'custom/...'`.
* The idea is that this first bootstrap layer sets an important part of the applications styles, particularly those related to layout positioning, responsive breakpoints, and the default margins and spacing applied to elements being manipulated.
2. The chill entry point is the second level. It contains the Chill theme, which is recognizable in the application.
* Each bundle has a `Resources/public/chill` folder containing a main sass file, which can optionally be split using `@imports`. All these files are compiled into a single Chill entry point, which serves as the applications theme and overrides bootstrap.
* The chillmain.scss file should contain the most general style cascades, those applied to many areas of the application.
* The chillperson.scss file also includes styles specific to different contexts related to people: person, household, and accompanying course.
* Some smaller bundles contain only styles specific to their functionality.
3. The vue_ entry points are used for Vue components. Vue files can contain an scss style block. These styles are specific to the component and its inheritance, with the scoped tag precisely defining their scope (see the documentation).
4. The page_ entry points are used to add assets specific to certain pages, most often scripts and styles.
## HTML Tagging and Style Cascades
The following example shows how to tag a code element without overdoing it. Note that:
* It is not necessary to tag all inner classes.
* The parent class should not be repeated in all child classes. Sass cascading allows for flexible HTML structuring without overloading the tag hierarchy.
* Often, the first class will have variations created with additional classes that start in the same way: bloc-dark simply adds the dark version of bloc. We do not use bloc dark because we dont want the dark class of bloc to interact with the same dark class of table. As a result, we will have an element bloc bloc-dark and another element table table-dark.
```html
<div class="bloc bloc-dark my-bloc">
<h3>My Title</h3>
<ul class="record_actions">
<li>
<a class="btn btn-edit"></a>
</li>
</ul>
</div>
```
Finally, it is important to define what a block, an action zone, and a button are. These three elements exist independently and are the only ones we tag.
For example, to style the title, we simply specify h3 within the block cascade.
```scss
div.bloc {
// un bloc générique, utilisé à plusieurs endroits
&.bloc-dark {
// la version sombre du bloc
}
h3 {}
ul {
// une liste standard dans bloc
li {
// des items de liste standard dans bloc
}
}
}
div.mon-bloc {
// des exceptions spécifiques à mon-bloc,
// qui sont des adaptations de bloc
}
ul.record_actions {
// va uniformiser tous les record_actions de l'application
li {
//...
}
}
.btn {
// les boutons de bootstrap
.btn-edit {
// chill étends les boutons bootstrap pour ses propres besoins
}
}
</style>
```
## Render box
## URL
## Route Naming Conventions
:::warning
These rules have not always been followed in the past. They are desired for the future.
:::
Routes follow this structure:
`chill_(api|crud)_bundle_(api)_entity_action`
1. First, chill_ (for all Chill modules).
2. Then, crud or api, optional, automatically added if the route is generated by the configuration.
3. Then, a string indicating the bundle (`main`, `person`, `activity`, ...).
4. Then, api, if the route is an API route.
5. Then, a string indicating the entity the route targets, and possibly the sub-entities.
6. Then, an action (`list`, `view`, `edit`, `new`, ...).
Indicating `api` in the fourth position allows distinguishing API routes generated by the configuration (which are all prefixed with `chill_api`) from those generated manually. (For example: `chill_api_household__index` and `chill_person_api_household_members_move`).
If points 4 and 5 are missing, they are replaced by other elements to ensure the uniqueness of the route and its clear understanding.
#### HTML pages
:::warning
These rules have not always been followed in the past. They are desired for the future.
:::
Syntaxe:
```
/{_locale}/bundle/entity/{id}/action
/{_locale}/bundle/entity/sub-entity/{id}/action
```
The following elements should be included in the list:
1. The locale;
2. An identifier for the bundle;
3. The entity it relates to;
4. Any sub-entities that the URL refers to;
5. The action.
```
# list of activities for a person
/fr/activity/person/25/activity/list
# new activity
/fr/activity/activity/new?person_id=25
```
#### Pour les API
:::info
Automatically generated routes are prefixed with `chill_api`
:::
Syntaxe:
```
/api/1.0/bundle/entity/{id}/action
/api/1.0/bundle/entity/sub-entity/{id}/action
```
The following elements should be included in the list:
1. The string `/api/` followed by the version (e.g., 1.0);
2. An identifier for the bundle;
3. The entity it relates to;
4. Any sub-entities that the URL refers to;
5. The action.
These elements may be interspersed with the entity identifier. In this case, the identifier should be placed immediately after the entity it relates to.
#### URLS for admin pages
Same conventions as for other html pages, **but `admin` is added in second position**. Soit:
`/{_locale}/admin/bundle/entity/{id}/action`
## Database table naming convention
When creating a new entity and the corresponding database table, we follow the following naming convention for the database table:
`chill_{bundle_identifier}_{entity_name}`.
Eg. chill_person_spoken_languages
## UI Rules
### Page Titles
#### Every page must have a title (in the head tag and page header).
Each page contains a title in the <head> tag. This title is typically the same as the header title on the page.
Tip: It is possible to use the block function in Twig for this.
Example:
```htmlmixed=
{% block title "Titre de la page" %}
{% block content %}
<h1>
{{ block('title')}}
</h1>
{% endblock %}
```
### entity_render usage
#### in Twig
Always use chill_entity_render_box for rendering entities like:
* User
* Person
* SocialAction
* SocialIssue
* Address
* Thirdparty
* ...
Example:
```
address|chill_entity_render_box
```
Justification:
1. Customization by installation: Some elements are sometimes customized during installation (for example, the name of each user will be followed by the name of the service).
2. To streamline and make displays consistent: Ensures uniformity in the way information is displayed across different pages or sections.
3. To simplify Twig code: By using blocks and centralizing title logic, it reduces repetition and makes the Twig code easier to maintain.
* Three rendering modes:
* inline
* block
* item.
#### In Vue
There is always a renderbox equivalent in vue.
#### HTML Links to Sections
Always include links/icons for accessing records, such as person or household details, if the user has access.
### Form Guidelines
#### Vocabulary:
* `Create` in a `bt bt-create` for links to the form to create an entity (to access the form).
* `Save` in a `bt bt-save` for "Save" buttons (in either an edit or create form).
* `Save and new`
* `Save and view`
* `Edit` in a `bt bt-edit` for links to the edit form.
* `Duplicate` (specify where it can be seen).
* `Cancel` for leaving an edit page with a link to the list, or the `returnPath`.
#### After Saving:
Redirect to the returnPath if available; otherwise, redirect to the view page.
### Sticky Form Buttons:
Buttons like "Cancel" and "Save" must be within a "sticky-form" bar at the bottom of the form.
If relevant:
* The banner contains a "Cancel" button that returns to the previous page. It is mandatory for forms, but optional for lists or "summary" pages.
* This "Cancel" button is always on the left.
```
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_entity_return_path('route_name' { 'route': 'option' } )}}">{{ return_path_label }}</a>
</li>
<li>
<!-- action 1 -->
</li>
</ul>
```
### Flash Messages
#### Display a flash message upon entity creation:
Everytime an entity has been created
> "The entity has been created."
The name of the element can be replaced with something more relevant:
> * The activity has been created
> * The appointment has been created
> * ...
#### On Saving an Entity
Each time an entity is saved, a flash message should appear:
> The data has been modified
#### Form Error (Validation Error)
At the top of the form, a flash message should indicate that validations have failed:
> This form contains errors
Errors should appear attached to the field they concern. However, it is acceptable to display errors at the root of the form if it is technically difficult to attach errors.
### Return Links
Each time a link is provided, check whether the function chill_return_path, chill_forward_return_path, or chill_return_path_or should be used.
* From the list page, to the opening of an element, or the creation button => use chill_path_add_return_path
* In these edit pages:
* use chill_return_path_or in the "Cancel" button;
* for the "Save and view" and "Save and close" buttons => ?
### Assets for Suggestion Lists
Create a list of suggestions to add (the entire item is clickable)
```html
<ul class="list-suggest add-items">
<li>
<span>item</span>
</li>
</ul>
```
Create a list of suggestions to remove (with a clickable red cross, the anchor <a> is empty)
```html
<ul class="list-suggest remove-items">
<li>
<span>
item
</span>
</li>
</ul>
```
Create a removable title (with a clickable red cross, the anchor <a> is empty)
```html
<div class="item-title">
<span>title</span>
</div>
```
The classes `cols` or `inline` can be added alongside `list-suggest` to modify the layout of the list. In the last example, add a `removable` class to the `<span>` if you want to make the item removable.

View File

@@ -4,7 +4,6 @@ en cours de rédaction
## Translations
Par bundle, toutes les traductions des pages twig se trouvent dans un seul fichier `translations/messages.fr.yaml`.
## Emplacement des fichiers
@@ -142,7 +141,6 @@ ul.record_actions {
## Render box
## URL
### Nommage des routes
@@ -234,6 +232,13 @@ Même conventions que dans les autres pages html de l'application, **mais `admin
`/{_locale}/admin/bundle/entity/{id}/action`
### Nommage des tables de base de donnée
Lors de la création d'une nouvelle entité et de la table de base de données correspondante, nous suivons la convention d'appellation suivante pour la table de base de données :
`chill_{bundle_identifier}_{nom_de_l'entité}`.
Par exemple : chill_person_spoken_languages
## Règles UI chill
@@ -293,8 +298,6 @@ A prevoir:
> quand on passe loption render: bloc, on peut placer le render_box dans une boucle for plus large qui fonctionne avec la classe flex-table ou la classe flex-bloc, ce qui donnera un affichage en rangée (table) ou en blocs. [name=Mathieu]
#### En vue
Il existe systématiquement une "box" équivalente en vue.

View File

@@ -16,6 +16,7 @@
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7",
@@ -85,6 +86,7 @@
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13",
"friendsofphp/php-cs-fixer": "3.65.0",
"jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15",

View File

@@ -0,0 +1,69 @@
ESLint
======
To improve the quality of our JS and VueJS code, ESLint and eslint-plugin-vue are implemented within the chill-bundles project.
Commands
--------
To run ESLint, you can simply use the ``eslint`` command within the chill-bundles directory.
This runs eslint **not** taking the baseline into account, thus showing all existing errors in the project.
A script was also added to package.json allowing you to execute ``yarn run eslint``.
This will run eslint, but **taking the baseline into account**, thus only alerting to newly created errors.
Interesting options that can be used in combination with eslint are:
- ``--quiet`` to only get errors and silence the warnings
- ``--fix`` to have ESLint fix what it can, automatically. This will not fix everything.
Baseline
--------
To allow us the time to fix linting errors/warnings a baseline has been created using the following command
- ``npx eslint-baseline "**/*.{js,vue}"``
The baseline has been commited and the gitlab CI setup to only fail upon new errors/warnings being created.
When fixing errors/warnings manually, please update the baseline.
1. Delete the current baseline file
2. Run the above command locally, this will automatically create a new baseline that should be commited
Rules
-----
We use Vue 3, so the rules can be configured as follows within the ``eslint.config.mjs`` file:
- ``.configs["flat/base"]`` ... Settings and rules to enable correct ESLint parsing.
Configurations for using Vue.js 3.x:
- ``.configs["flat/essential"]`` : Base rules plus rules to prevent errors or unintended behavior.
- ``.configs["flat/strongly-recommended"]`` ... Above, plus rules to considerably improve code readability and/or dev experience.
- ``.configs["flat/recommended"]`` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
Detailed information about which rules each set includes can be found here:
`https://eslint.vuejs.org/rules/ <https://eslint.vuejs.org/rules/>`_
Manual Rule Configuration
-------------------------
We can also manually configure certain rules or override rules that are part of the ruleset specified above.
For example, if we want to turn off a certain rule, we can do so as follows:
.. code-block:: javascript
rules: {
'vue/multi-word-component': 'off'
}
We could also change the severity of a certain rule from 'error' to 'warning', for example.
Within specific ``.js`` or ``.vue`` files, we can also override a certain rule only for that specific file by adding a comment:
.. code-block:: javascript
/* eslint multi-word-component: "off", no-child-content: "error"
--------
Here's a description about why this configuration is necessary. */

View File

@@ -31,6 +31,7 @@ As Chill relies on the `symfony <http://symfony.com>`_ framework, reading the fr
Exports <exports.rst>
Embeddable comments <embeddable-comments.rst>
Run tests <run-tests.rst>
ESLint <es-lint.rst>
Useful snippets <useful-snippets.rst>
manual/index.rst
Assets <assets.rst>

View File

@@ -1,19 +1,18 @@
import { ShowHide } from 'ShowHide/show_hide.js';
import { ShowHide } from "ShowHide/show_hide.js";
var
div_accompagnement = document.getElementById("form_accompagnement"),
div_accompagnement_comment = document.getElementById("form_accompagnement_comment"),
var div_accompagnement = document.getElementById("form_accompagnement"),
div_accompagnement_comment = document.getElementById(
"form_accompagnement_comment",
),
div_caf_id = document.getElementById("cafId"),
div_caf_inscription_date = document.getElementById("cafInscriptionDate"),
;
div_caf_inscription_date = document.getElementById("cafInscriptionDate");
// let show/hide the div_accompagnement_comment if the input with value `'autre'` is checked
new ShowHide({
"froms": [div_accompagnement],
"test": function(froms, event) {
froms: [div_accompagnement],
test: function (froms, event) {
for (let el of froms.values()) {
for (let input of el.querySelectorAll('input').values()) {
if (input.value === 'autre') {
for (let input of el.querySelectorAll("input").values()) {
if (input.value === "autre") {
return input.checked;
}
}
@@ -21,7 +20,7 @@ new ShowHide({
return false;
},
"container": [div_accompagnement_comment]
container: [div_accompagnement_comment],
});
// let show the date input only if the the id is filled
@@ -34,6 +33,5 @@ new ShowHide({
},
container: [div_caf_inscription_date],
// using this option, we use the event `input` instead of `change`
event_name: 'input'
event_name: "input",
});

39
eslint.config.mjs Normal file
View File

@@ -0,0 +1,39 @@
import eslintPluginVue from "eslint-plugin-vue";
import ts from "typescript-eslint";
import eslintPluginPrettier from "eslint-plugin-prettier";
export default ts.config(
...ts.configs.recommended,
...ts.configs.stylistic,
...eslintPluginVue.configs["flat/essential"],
{
files: ["**/*.vue"],
languageOptions: {
parserOptions: {
parser: "@typescript-eslint/parser",
},
},
},
{
ignores: [
"**/vendor/*",
"**/import-png.d.ts",
"**/chill.webpack.config.js",
"**/var/*",
"**/docker/*",
"**/node_modules/*",
"**/public/build/*"
],
},
{
plugins: {
prettier: eslintPluginPrettier,
},
rules: {
"prettier/prettier": "error",
// override/add rules settings here, such as:
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-require-imports": "off",
},
},
);

View File

@@ -13,26 +13,36 @@
"@ckeditor/ckeditor5-markdown-gfm": "^41.4.2",
"@ckeditor/ckeditor5-theme-lark": "^41.4.2",
"@ckeditor/ckeditor5-vue": "^5.1.0",
"@eslint/js": "^9.14.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5",
"@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.12.2",
"bindings": "^1.5.0",
"bootstrap": "5.2.3",
"chokidar": "^3.5.1",
"dompurify": "^3.1.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.30.0",
"fork-awesome": "^1.1.7",
"jquery": "^3.6.0",
"marked": "^12.0.1",
"node-sass": "^8.0.0",
"popper.js": "^1.16.1",
"postcss-loader": "^7.0.2",
"prettier": "^3.3.3",
"raw-loader": "^4.0.2",
"sass-loader": "^14.0.0",
"select2": "^4.0.13",
"select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
"typescript": "^5.4.5",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"vue-loader": "^17.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
@@ -68,8 +78,10 @@
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"prettier": "prettier --write \"**/*.{js,ts,vue}\"",
"watch": "encore dev --watch",
"build": "encore production --progress"
"build": "encore production --progress",
"eslint": "npx eslint-baseline \"**/*.{js,ts,vue}\""
},
"private": true
}

View File

@@ -55,6 +55,8 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
.' AND '
.'(person_person_having_activity.id = person.id OR person MEMBER OF activity_person_having_activity.persons)');
$sqb->andWhere('activity_person_having_activity.id = activity.id');
if (isset($data['reasons']) && [] !== $data['reasons']) {
// add clause activity reason
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');

View File

@@ -1 +1 @@
require('./chillactivity.scss');
require("./chillactivity.scss");

View File

@@ -1,21 +1,21 @@
<template>
<concerned-groups v-if="hasPerson"></concerned-groups>
<social-issues-acc v-if="hasSocialIssues"></social-issues-acc>
<location v-if="hasLocation"></location>
<concerned-groups v-if="hasPerson" />
<social-issues-acc v-if="hasSocialIssues" />
<location v-if="hasLocation" />
</template>
<script>
import ConcernedGroups from './components/ConcernedGroups.vue';
import SocialIssuesAcc from './components/SocialIssuesAcc.vue';
import Location from './components/Location.vue';
import ConcernedGroups from "./components/ConcernedGroups.vue";
import SocialIssuesAcc from "./components/SocialIssuesAcc.vue";
import Location from "./components/Location.vue";
export default {
name: "App",
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
props: ["hasSocialIssues", "hasLocation", "hasPerson"],
components: {
ConcernedGroups,
SocialIssuesAcc,
Location
}
}
Location,
},
};
</script>

View File

@@ -1,37 +1,38 @@
import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api.js';
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods';
import { getSocialIssues } from "ChillPersonAssets/vuejs/AccompanyingCourse/api.js";
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
/*
* Load socialActions by socialIssue (id)
*/
const getSocialActionByIssue = (id) => {
const url = `/api/1.0/person/social/social-action/by-social-issue/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
const getLocations = () => fetchResults('/api/1.0/main/location.json');
const getLocations = () => fetchResults("/api/1.0/main/location.json");
const getLocationTypes = () => fetchResults('/api/1.0/main/location-type.json');
const getLocationTypes = () => fetchResults("/api/1.0/main/location-type.json");
const getUserCurrentLocation =
() => fetch('/api/1.0/main/user-current-location.json')
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
const getUserCurrentLocation = () =>
fetch("/api/1.0/main/user-current-location.json").then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
/*
* Load Location Type by defaultFor
* @param {string} entity - can be "person" or "thirdparty"
*/
const getLocationTypeByDefaultFor = (entity) => {
return getLocationTypes().then(results =>
results.filter(t => t.defaultFor === entity)[0]
return getLocationTypes().then(
(results) => results.filter((t) => t.defaultFor === entity)[0],
);
};
@@ -45,15 +46,16 @@ const getLocationTypeByDefaultFor = (entity) => {
const postLocation = (body) => {
const url = `/api/1.0/main/location.json`;
return fetch(url, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json;charset=utf-8'
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
@@ -64,5 +66,5 @@ export {
getLocationTypes,
getLocationTypeByDefaultFor,
postLocation,
getUserCurrentLocation
getUserCurrentLocation,
};

View File

@@ -1,19 +1,27 @@
<template>
<teleport to="#add-persons" v-if="isComponentVisible">
<div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc
v-for="bloc in contextPersonsBlocs"
v-bind:key="bloc.key"
v-bind:bloc="bloc"
v-bind:blocWidth="getBlocWidth"
v-bind:setPersonsInBloc="setPersonsInBloc">
</persons-bloc>
:key="bloc.key"
:bloc="bloc"
:bloc-width="getBlocWidth"
:set-persons-in-bloc="setPersonsInBloc"
/>
</div>
<div v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0">
<div
v-if="
getContext === 'accompanyingCourse' &&
suggestedEntities.length > 0
"
>
<ul class="list-suggest add-items inline">
<li v-for="(p, i) in suggestedEntities" @click="addSuggestedEntity(p)" :key="`suggestedEntities-${i}`">
<person-text v-if="p.type === 'person'" :person="p"></person-text>
<li
v-for="(p, i) in suggestedEntities"
@click="addSuggestedEntity(p)"
:key="`suggestedEntities-${i}`"
>
<person-text v-if="p.type === 'person'" :person="p" />
<span v-else>{{ p.text }}</span>
</li>
</ul>
@@ -22,114 +30,126 @@
<ul class="record_actions">
<li class="add-persons">
<add-persons
buttonTitle="activity.add_persons"
modalTitle="activity.add_persons"
v-bind:key="addPersons.key"
v-bind:options="addPersonsOptions"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
button-title="activity.add_persons"
modal-title="activity.add_persons"
:key="addPersons.key"
:options="addPersonsOptions"
@add-new-persons="addNewPersons"
ref="addPersons"
/>
</li>
</ul>
</teleport>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonsBloc from './ConcernedGroups/PersonsBloc.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
import { mapState, mapGetters } from "vuex";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
export default {
name: "ConcernedGroups",
components: {
AddPersons,
PersonsBloc,
PersonText
PersonText,
},
data() {
return {
personsBlocs: [
{ key: 'persons',
title: 'activity.bloc_persons',
{
key: "persons",
title: "activity.bloc_persons",
persons: [],
included: false
included: false,
},
{ key: 'personsAssociated',
title: 'activity.bloc_persons_associated',
{
key: "personsAssociated",
title: "activity.bloc_persons_associated",
persons: [],
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{ key: 'personsNotAssociated',
title: 'activity.bloc_persons_not_associated',
{
key: "personsNotAssociated",
title: "activity.bloc_persons_not_associated",
persons: [],
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{ key: 'thirdparty',
title: 'activity.bloc_thirdparty',
{
key: "thirdparty",
title: "activity.bloc_thirdparty",
persons: [],
included: window.activity ? window.activity.activityType.thirdPartiesVisible !== 0 : true
included: window.activity
? window.activity.activityType.thirdPartiesVisible !== 0
: true,
},
{ key: 'users',
title: 'activity.bloc_users',
{
key: "users",
title: "activity.bloc_users",
persons: [],
included: window.activity ? window.activity.activityType.usersVisible !== 0 : true
included: window.activity
? window.activity.activityType.usersVisible !== 0
: true,
},
],
addPersons: {
key: 'activity'
}
}
key: "activity",
},
};
},
computed: {
isComponentVisible() {
return window.activity
? (window.activity.activityType.personsVisible !== 0 || window.activity.activityType.thirdPartiesVisible !== 0 || window.activity.activityType.usersVisible !== 0)
: true
? window.activity.activityType.personsVisible !== 0 ||
window.activity.activityType.thirdPartiesVisible !== 0 ||
window.activity.activityType.usersVisible !== 0
: true;
},
...mapState({
persons: state => state.activity.persons,
thirdParties: state => state.activity.thirdParties,
users: state => state.activity.users,
accompanyingCourse: state => state.activity.accompanyingPeriod
persons: (state) => state.activity.persons,
thirdParties: (state) => state.activity.thirdParties,
users: (state) => state.activity.users,
accompanyingCourse: (state) => state.activity.accompanyingPeriod,
}),
...mapGetters([
'suggestedEntities'
]),
...mapGetters(["suggestedEntities"]),
getContext() {
return (this.accompanyingCourse) ? "accompanyingCourse" : "person";
return this.accompanyingCourse ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter(bloc => bloc.included !== false);
return this.personsBlocs.filter((bloc) => bloc.included !== false);
},
addPersonsOptions() {
let optionsType = [];
if (window.activity) {
if (window.activity.activityType.personsVisible !== 0) {
optionsType.push('person')
optionsType.push("person");
}
if (window.activity.activityType.thirdPartiesVisible !== 0) {
optionsType.push('thirdparty')
optionsType.push("thirdparty");
}
if (window.activity.activityType.usersVisible !== 0) {
optionsType.push('user')
optionsType.push("user");
}
} else {
optionsType = ['person', 'thirdparty', 'user'];
optionsType = ["person", "thirdparty", "user"];
}
return {
type: optionsType,
priority: null,
uniq: false,
button: {
size: 'btn-sm'
}
}
size: "btn-sm",
},
};
},
getBlocWidth() {
return Math.round(100/(this.contextPersonsBlocs.length)) + '%';
}
return Math.round(100 / this.contextPersonsBlocs.length) + "%";
},
},
mounted() {
this.setPersonsInBloc();
@@ -140,31 +160,31 @@ export default {
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach(bloc => {
this.personsBlocs.forEach((bloc) => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case 'personsAssociated':
case "personsAssociated":
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case 'personsNotAssociated':
case "personsNotAssociated":
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else {
switch (bloc.key) {
case 'persons':
case "persons":
bloc.persons = this.persons;
bloc.included = true;
break;
}
}
switch (bloc.key) {
case 'thirdparty':
case "thirdparty":
bloc.persons = this.thirdParties;
break;
case 'users':
case "users":
bloc.persons = this.users;
break;
}
@@ -174,23 +194,25 @@ export default {
let personsAssociated = [];
let personsNotAssociated = this.persons;
let participations = this.getCourseParticipations();
this.persons.forEach(person => {
participations.forEach(participation => {
this.persons.forEach((person) => {
participations.forEach((participation) => {
if (person.id === participation.id) {
//console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(p => p !== person);
personsNotAssociated = personsNotAssociated.filter(
(p) => p !== person,
);
}
});
});
return {
'personsAssociated': personsAssociated,
'personsNotAssociated': personsNotAssociated
personsAssociated: personsAssociated,
personsNotAssociated: personsNotAssociated,
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach(participation => {
this.accompanyingCourse.participations.forEach((participation) => {
if (!participation.endDate) {
participations.push(participation.person);
}
@@ -198,22 +220,23 @@ export default {
return participations;
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
console.log("@@@ CLICK button addNewPersons", selected);
selected.forEach((item) => {
this.$store.dispatch('addPersonsInvolved', item);
}, this
);
this.$store.dispatch("addPersonsInvolved", item);
}, this);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
},
addSuggestedEntity(person) {
this.$store.dispatch('addPersonsInvolved', { result: person, type: 'person' });
this.$store.dispatch("addPersonsInvolved", {
result: person,
type: "person",
});
this.setPersonsInBloc();
},
}
}
},
};
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>

View File

@@ -2,21 +2,20 @@
<li>
<span :title="person.text" @click.prevent="$emit('remove', person)">
<span class="chill_denomination">
<person-text :person="person" :isCut="true"></person-text>
<person-text :person="person" :is-cut="true" />
</span>
</span>
</li>
</template>
<script>
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
export default {
name: "PersonBadge",
props: ['person'],
props: ["person"],
components: {
PersonText
PersonText,
},
// computed: {
// textCutted() {
@@ -24,9 +23,8 @@ export default {
// return this.person.text.slice(0,15) + more;
// }
// },
emits: ['remove'],
}
emits: ["remove"],
};
</script>
<style lang="css" scoped>
</style>
<style lang="css" scoped></style>

View File

@@ -8,10 +8,10 @@
<ul class="list-suggest remove-items">
<person-badge
v-for="person in bloc.persons"
v-bind:key="person.id"
v-bind:person="person"
@remove="removePerson">
</person-badge>
:key="person.id"
:person="person"
@remove="removePerson"
/>
</ul>
</div>
</div>
@@ -19,23 +19,21 @@
</template>
<script>
import PersonBadge from './PersonBadge.vue';
import PersonBadge from "./PersonBadge.vue";
export default {
name: "PersonsBloc",
components: {
PersonBadge
PersonBadge,
},
props: ['bloc', 'setPersonsInBloc', 'blocWidth'],
props: ["bloc", "setPersonsInBloc", "blocWidth"],
methods: {
removePerson(item) {
console.log('@@ CLICK remove person: item', item);
this.$store.dispatch('removePersonInvolved', item);
console.log("@@ CLICK remove person: item", item);
this.$store.dispatch("removePersonInvolved", item);
this.setPersonsInBloc();
}
}
}
},
},
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -22,9 +22,8 @@
group-values="locations"
group-label="locationGroup"
v-model="location"
>
</VueMultiselect>
<new-location v-bind:availableLocations="availableLocations"></new-location>
/>
<new-location :available-locations="availableLocations" />
</div>
</div>
</teleport>
@@ -43,9 +42,8 @@ export default {
},
data() {
return {
locationClassList:
`col-form-label col-sm-4 ${document.querySelector('input#chill_activitybundle_activity_location').getAttribute('required') ? 'required' : ''}`,
}
locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`,
};
},
computed: {
...mapState(["activity", "availableLocations"]),
@@ -61,16 +59,16 @@ export default {
},
methods: {
labelAccompanyingCourseLocation(value) {
return `${value.address.text} (${value.locationType.title.fr})`
return `${value.address.text} (${value.locationType.title.fr})`;
},
customLabel(value) {
return value.locationType
? value.name
? value.name === '__AccompanyingCourseLocation__'
? value.name === "__AccompanyingCourseLocation__"
? this.labelAccompanyingCourseLocation(value)
: `${value.name} (${value.locationType.title.fr})`
: value.locationType.title.fr
: '';
: "";
},
},
};

View File

@@ -3,84 +3,129 @@
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-create" @click="openModal">
{{ $t('activity.create_new_location') }}
{{ $t("activity.create_new_location") }}
</a>
</li>
</ul>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('activity.create_new_location') }}</h3>
<modal
v-if="modal.showModal"
:modal-dialog-class="modal.modalDialogClass"
@close="modal.showModal = false"
>
<template #header>
<h3 class="modal-title">
{{ $t("activity.create_new_location") }}
</h3>
</template>
<template v-slot:body>
<template #body>
<form>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
<li v-for="(e, i) in errors" :key="i">
{{ e }}
</li>
</ul>
</div>
<div class="form-floating mb-3">
<select class="form-select form-select-lg" id="type" required v-model="selectType">
<option selected disabled value="">{{ $t('activity.choose_location_type') }}</option>
<option v-for="t in locationTypes" :value="t" :key="t.id">
<select
class="form-select form-select-lg"
id="type"
required
v-model="selectType"
>
<option selected disabled value="">
{{ $t("activity.choose_location_type") }}
</option>
<option
v-for="t in locationTypes"
:value="t"
:key="t.id"
>
{{ t.title.fr }}
</option>
</select>
<label>{{ $t('activity.location_fields.type') }}</label>
<label>{{
$t("activity.location_fields.type")
}}</label>
</div>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="name" v-model="inputName" placeholder />
<label for="name">{{ $t('activity.location_fields.name') }}</label>
<input
class="form-control form-control-lg"
id="name"
v-model="inputName"
placeholder
/>
<label for="name">{{
$t("activity.location_fields.name")
}}</label>
</div>
<add-address
:context="addAddress.context"
:options="addAddress.options"
:addressChangedCallback="submitNewAddress"
:address-changed-callback="submitNewAddress"
v-if="showAddAddress"
ref="addAddress">
</add-address>
ref="addAddress"
/>
<div class="form-floating mb-3" v-if="showContactData">
<input class="form-control form-control-lg" id="phonenumber1" v-model="inputPhonenumber1" placeholder />
<label for="phonenumber1">{{ $t('activity.location_fields.phonenumber1') }}</label>
<input
class="form-control form-control-lg"
id="phonenumber1"
v-model="inputPhonenumber1"
placeholder
/>
<label for="phonenumber1">{{
$t("activity.location_fields.phonenumber1")
}}</label>
</div>
<div class="form-floating mb-3" v-if="hasPhonenumber1">
<input class="form-control form-control-lg" id="phonenumber2" v-model="inputPhonenumber2" placeholder />
<label for="phonenumber2">{{ $t('activity.location_fields.phonenumber2') }}</label>
<input
class="form-control form-control-lg"
id="phonenumber2"
v-model="inputPhonenumber2"
placeholder
/>
<label for="phonenumber2">{{
$t("activity.location_fields.phonenumber2")
}}</label>
</div>
<div class="form-floating mb-3" v-if="showContactData">
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder />
<label for="email">{{ $t('activity.location_fields.email') }}</label>
<input
class="form-control form-control-lg"
id="email"
v-model="inputEmail"
placeholder
/>
<label for="email">{{
$t("activity.location_fields.email")
}}</label>
</div>
</form>
</template>
<template v-slot:footer>
<button class="btn btn-save"
<template #footer>
<button
class="btn btn-save"
@click.prevent="saveNewLocation"
>
{{ $t('action.save') }}
{{ $t("action.save") }}
</button>
</template>
</modal>
</teleport>
</div>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { mapState } from "vuex";
import { getLocationTypes } from "../../api";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export default {
name: "NewLocation",
@@ -88,7 +133,7 @@ export default {
Modal,
AddAddress,
},
props: ['availableLocations'],
props: ["availableLocations"],
data() {
return {
errors: [],
@@ -103,35 +148,42 @@ export default {
locationTypes: [],
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
modalDialogClass: "modal-dialog-scrollable modal-xl",
},
addAddress: {
options: {
button: {
text: { create: 'activity.create_address', edit: 'activity.edit_address' },
size: 'btn-sm'
text: {
create: "activity.create_address",
edit: "activity.edit_address",
},
size: "btn-sm",
},
title: {
create: "activity.create_address",
edit: "activity.edit_address",
},
title: { create: 'activity.create_address', edit: 'activity.edit_address' },
},
context: {
target: { //name, id
target: {
//name, id
},
edit: false,
addressId: null,
defaults: window.addaddress
}
}
}
defaults: window.addaddress,
},
},
};
},
computed: {
...mapState(['activity']),
...mapState(["activity"]),
selectType: {
get() {
return this.selected.type;
},
set(value) {
this.selected.type = value;
}
},
},
inputName: {
get() {
@@ -139,7 +191,7 @@ export default {
},
set(value) {
this.selected.name = value;
}
},
},
inputEmail: {
get() {
@@ -147,7 +199,7 @@ export default {
},
set(value) {
this.selected.email = value;
}
},
},
inputPhonenumber1: {
get() {
@@ -155,7 +207,7 @@ export default {
},
set(value) {
this.selected.phonenumber1 = value;
}
},
},
inputPhonenumber2: {
get() {
@@ -163,15 +215,18 @@ export default {
},
set(value) {
this.selected.phonenumber2 = value;
}
},
},
hasPhonenumber1() {
return this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== "";
return (
this.selected.phonenumber1 !== null &&
this.selected.phonenumber1 !== ""
);
},
showAddAddress() {
let cond = false;
if (this.selected.type) {
if (this.selected.type.addressRequired !== 'never') {
if (this.selected.type.addressRequired !== "never") {
cond = true;
}
}
@@ -180,7 +235,7 @@ export default {
showContactData() {
let cond = false;
if (this.selected.type) {
if (this.selected.type.contactData !== 'never') {
if (this.selected.type.contactData !== "never") {
cond = true;
}
}
@@ -195,28 +250,39 @@ export default {
let cond = true;
this.errors = [];
if (!this.selected.type) {
this.errors.push('Type de localisation requis');
this.errors.push("Type de localisation requis");
cond = false;
} else {
if (this.selected.type.addressRequired === 'required' && !this.selected.addressId) {
this.errors.push('Adresse requise');
if (
this.selected.type.addressRequired === "required" &&
!this.selected.addressId
) {
this.errors.push("Adresse requise");
cond = false;
}
if (this.selected.type.contactData === 'required' && !this.selected.phonenumber1) {
this.errors.push('Numéro de téléphone requis');
if (
this.selected.type.contactData === "required" &&
!this.selected.phonenumber1
) {
this.errors.push("Numéro de téléphone requis");
cond = false;
}
if (this.selected.type.contactData === 'required' && !this.selected.email) {
this.errors.push('Adresse email requise');
if (
this.selected.type.contactData === "required" &&
!this.selected.email
) {
this.errors.push("Adresse email requise");
cond = false;
}
}
return cond;
},
getLocationTypesList() {
getLocationTypes().then(results => {
this.locationTypes = results.filter(t => t.availableForUsers === true);
})
getLocationTypes().then((results) => {
this.locationTypes = results.filter(
(t) => t.availableForUsers === true,
);
});
},
openModal() {
this.modal.showModal = true;
@@ -224,11 +290,11 @@ export default {
saveNewLocation() {
if (this.checkForm()) {
let body = {
type: 'location',
type: "location",
name: this.selected.name,
locationType: {
id: this.selected.type.id,
type: 'location-type'
type: "location-type",
},
phonenumber1: this.selected.phonenumber1,
phonenumber2: this.selected.phonenumber2,
@@ -237,36 +303,36 @@ export default {
if (this.selected.addressId) {
body = Object.assign(body, {
address: {
id: this.selected.addressId
}
id: this.selected.addressId,
},
});
}
makeFetch('POST', '/api/1.0/main/location.json', body)
.then(response => {
this.$store.dispatch('addAvailableLocationGroup', {
locationGroup: 'Localisations nouvellement créées',
locations: [response]
makeFetch("POST", "/api/1.0/main/location.json", body)
.then((response) => {
this.$store.dispatch("addAvailableLocationGroup", {
locationGroup: "Localisations nouvellement créées",
locations: [response],
});
this.$store.dispatch('updateLocation', response);
this.$store.dispatch("updateLocation", response);
this.modal.showModal = false;
})
.catch((error) => {
if (error.name === 'ValidationException') {
if (error.name === "ValidationException") {
for (let v of error.violations) {
this.errors.push(v);
}
} else {
this.errors.push('An error occurred');
this.errors.push("An error occurred");
}
});
}
})
};
},
submitNewAddress(payload) {
this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true;
}
}
}
},
},
};
</script>

View File

@@ -1,19 +1,19 @@
<template>
<teleport to="#social-issues-acc">
<div class="mb-3 row">
<div class="col-4">
<label :class="socialIssuesClassList">{{ $t('activity.social_issues') }}</label>
<label :class="socialIssuesClassList">{{
$t("activity.social_issues")
}}</label>
</div>
<div class="col-8">
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected">
</check-social-issue>
@update-selected="updateIssuesSelected"
/>
<div class="my-3">
<VueMultiselect
@@ -33,72 +33,81 @@
:loading="issueIsLoading"
:placeholder="$t('activity.choose_other_social_issue')"
:options="socialIssuesOther"
@select="addIssueInList">
</VueMultiselect>
@select="addIssueInList"
/>
</div>
</div>
</div>
<div class="mb-3 row">
<div class="col-4">
<label :class="socialActionsClassList">{{ $t('activity.social_actions') }}</label>
<label :class="socialActionsClassList">{{
$t("activity.social_actions")
}}</label>
</div>
<div class="col-8">
<div v-if="actionIsLoading === true">
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i>
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg" />
</div>
<span v-else-if="socialIssuesSelected.length === 0" class="inline-choice chill-no-data-statement mt-3">
{{ $t('activity.select_first_a_social_issue') }}
<span
v-else-if="socialIssuesSelected.length === 0"
class="inline-choice chill-no-data-statement mt-3"
>
{{ $t("activity.select_first_a_social_issue") }}
</span>
<template v-else-if="socialActionsList.length > 0">
<div
v-if="
socialIssuesSelected.length ||
socialActionsSelected.length
"
>
<check-social-action
v-if="socialIssuesSelected.length || socialActionsSelected.length"
v-for="action in socialActionsList"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
@update-selected="updateActionsSelected"
/>
</div>
</template>
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
{{ $t('activity.social_action_list_empty') }}
<span
v-else-if="
actionAreLoaded && socialActionsList.length === 0
"
class="inline-choice chill-no-data-statement mt-3"
>
{{ $t("activity.social_action_list_empty") }}
</span>
</div>
</div>
</teleport>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
import CheckSocialIssue from './SocialIssuesAcc/CheckSocialIssue.vue';
import CheckSocialAction from './SocialIssuesAcc/CheckSocialAction.vue';
import { getSocialIssues, getSocialActionByIssue } from '../api.js';
import VueMultiselect from "vue-multiselect";
import CheckSocialIssue from "./SocialIssuesAcc/CheckSocialIssue.vue";
import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue";
import { getSocialIssues, getSocialActionByIssue } from "../api.js";
export default {
name: "SocialIssuesAcc",
components: {
CheckSocialIssue,
CheckSocialAction,
VueMultiselect
VueMultiselect,
},
data() {
return {
issueIsLoading: false,
actionIsLoading: false,
actionAreLoaded: false,
socialIssuesClassList:
`col-form-label ${document.querySelector('input#chill_activitybundle_activity_socialIssues').getAttribute('required') ? 'required' : ''}`,
socialActionsClassList:
`col-form-label ${document.querySelector('input#chill_activitybundle_activity_socialActions').getAttribute('required') ? 'required' : ''}`,
}
socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
};
},
computed: {
socialIssuesList() {
@@ -115,109 +124,115 @@ export default {
},
socialActionsSelected() {
return this.$store.state.activity.socialActions;
}
},
},
mounted() {
/* Load others issues in multiselect
*/
this.issueIsLoading = true;
this.actionAreLoaded = false;
getSocialIssues().then(response => new Promise((resolve, reject) => {
this.$store.commit('updateIssuesOther', response.results);
getSocialIssues().then(
(response) =>
new Promise((resolve, reject) => {
this.$store.commit("updateIssuesOther", response.results);
/* Add in list the issues already associated (if not yet listed)
*/
this.socialIssuesSelected.forEach(issue => {
if (this.socialIssuesList.filter(i => i.id === issue.id).length !== 1) {
this.$store.commit('addIssueInList', issue);
this.socialIssuesSelected.forEach((issue) => {
if (
this.socialIssuesList.filter(
(i) => i.id === issue.id,
).length !== 1
) {
this.$store.commit("addIssueInList", issue);
}
}, this);
/* Remove from multiselect the issues that are not yet in checkbox list
*/
this.socialIssuesList.forEach(issue => {
this.$store.commit('removeIssueInOther', issue);
this.socialIssuesList.forEach((issue) => {
this.$store.commit("removeIssueInOther", issue);
}, this);
/* Filter issues
*/
this.$store.commit('filterList', 'issues');
this.$store.commit("filterList", "issues");
/* Add in list the actions already associated (if not yet listed)
*/
this.socialActionsSelected.forEach(action => {
this.$store.commit('addActionInList', action);
this.socialActionsSelected.forEach((action) => {
this.$store.commit("addActionInList", action);
}, this);
/* Filter issues
*/
this.$store.commit('filterList', 'actions');
this.$store.commit("filterList", "actions");
this.issueIsLoading = false;
this.actionAreLoaded = true;
this.updateActionsList();
resolve();
}));
}),
);
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected),
remove it from multiselect, and add socialActions concerned
*/
addIssueInList(value) {
//console.log('addIssueInList', value);
this.$store.commit('addIssueInList', value);
this.$store.commit('removeIssueInOther', value);
this.$store.dispatch('addIssueSelected', value);
this.$store.commit("addIssueInList", value);
this.$store.commit("removeIssueInOther", value);
this.$store.dispatch("addIssueSelected", value);
this.updateActionsList();
},
/* Update value for selected issues checkboxes
*/
updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues);
this.$store.dispatch('updateIssuesSelected', issues);
this.$store.dispatch("updateIssuesSelected", issues);
this.updateActionsList();
},
/* Update value for selected actions checkboxes
*/
updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions);
this.$store.dispatch('updateActionsSelected', actions);
this.$store.dispatch("updateActionsSelected", actions);
},
/* Add socialActions concerned: after reset, loop on each issue selected
to get social actions concerned
*/
updateActionsList() {
this.resetActionsList();
this.socialIssuesSelected.forEach(item => {
this.socialIssuesSelected.forEach((item) => {
this.actionIsLoading = true;
getSocialActionByIssue(item.id)
.then(actions => new Promise((resolve, reject) => {
actions.results.forEach(action => {
this.$store.commit('addActionInList', action);
getSocialActionByIssue(item.id).then(
(actions) =>
new Promise((resolve, reject) => {
actions.results.forEach((action) => {
this.$store.commit("addActionInList", action);
}, this);
this.$store.commit('filterList', 'actions');
this.$store.commit("filterList", "actions");
this.actionIsLoading = false;
this.actionAreLoaded = true;
resolve();
}));
}),
);
}, this);
},
/* Reset socialActions List: flush list and restore selected actions
*/
resetActionsList() {
this.$store.commit('resetActionsList');
this.$store.commit("resetActionsList");
this.actionAreLoaded = false;
this.socialActionsSelected.forEach(item => {
this.$store.commit('addActionInList', item);
this.socialActionsSelected.forEach((item) => {
this.$store.commit("addActionInList", item);
}, this);
}
}
}
},
},
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,18 +1,17 @@
<template>
<span class="inline-choice">
<div class="form-check">
<input class="form-check-input"
<input
class="form-check-input"
type="checkbox"
v-model="selected"
name="action"
v-bind:id="action.id"
v-bind:value="action"
:id="action.id"
:value="action"
/>
<label class="form-check-label" v-bind:for="action.id">
<label class="form-check-label" :for="action.id">
<span class="badge bg-light text-dark">{{ action.text }}</span>
</label>
</div>
</span>
</template>
@@ -20,25 +19,25 @@
<script>
export default {
name: "CheckSocialAction",
props: [ 'action', 'selection' ],
emits: [ 'updateSelected' ],
props: ["action", "selection"],
emits: ["updateSelected"],
computed: {
selected: {
set(value) {
this.$emit('updateSelected', value);
this.$emit("updateSelected", value);
},
get() {
return this.selection;
}
}
}
}
},
},
},
};
</script>
<style lang="scss" scoped>
@import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins';
@import 'ChillMainAssets/chill/scss/chill_variables';
@import "ChillMainAssets/module/bootstrap/shared";
@import "ChillPersonAssets/chill/scss/mixins";
@import "ChillMainAssets/chill/scss/chill_variables";
span.badge {
@include badge_social($social-action-color);
font-size: 95%;

View File

@@ -1,18 +1,19 @@
<template>
<span class="inline-choice">
<div class="form-check">
<input class="form-check-input"
<input
class="form-check-input"
type="checkbox"
v-model="selected"
name="issue"
v-bind:id="issue.id"
v-bind:value="issue"
:id="issue.id"
:value="issue"
/>
<label class="form-check-label" v-bind:for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span>
<label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{
issue.text
}}</span>
</label>
</div>
</span>
</template>
@@ -20,25 +21,25 @@
<script>
export default {
name: "CheckSocialIssue",
props: [ 'issue', 'selection' ],
emits: [ 'updateSelected' ],
props: ["issue", "selection"],
emits: ["updateSelected"],
computed: {
selected: {
set(value) {
this.$emit('updateSelected', value);
this.$emit("updateSelected", value);
},
get() {
return this.selection;
}
}
}
}
},
},
},
};
</script>
<style lang="scss" scoped>
@import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins';
@import 'ChillMainAssets/chill/scss/chill_variables';
@import "ChillMainAssets/module/bootstrap/shared";
@import "ChillPersonAssets/chill/scss/mixins";
@import "ChillMainAssets/chill/scss/chill_variables";
span.badge {
@include badge_social($social-issue-color);
font-size: 95%;

View File

@@ -1,5 +1,5 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n'
import { personMessages } from "ChillPersonAssets/vuejs/_js/i18n";
import { multiSelectMessages } from "ChillMainAssets/vuejs/_js/i18n";
const activityMessages = {
fr: {
@@ -9,7 +9,8 @@ const activityMessages = {
social_issues: "Problématiques sociales",
choose_other_social_issue: "Ajouter une autre problématique sociale...",
social_actions: "Actions d'accompagnement",
select_first_a_social_issue: "Sélectionnez d'abord une problématique sociale",
select_first_a_social_issue:
"Sélectionnez d'abord une problématique sociale",
social_action_list_empty: "Aucune action sociale disponible",
//
@@ -32,14 +33,12 @@ const activityMessages = {
phonenumber2: "Autre téléphone",
email: "Adresse courriel",
},
create_address: 'Créer une adresse',
edit_address: "Modifier l'adresse"
}
}
}
create_address: "Créer une adresse",
edit_address: "Modifier l'adresse",
},
},
};
Object.assign(activityMessages.fr, personMessages.fr, multiSelectMessages.fr);
export {
activityMessages
};
export { activityMessages };

View File

@@ -1,19 +1,19 @@
import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { activityMessages } from './i18n'
import store from './store'
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {fetchTemplates} from 'ChillDocGeneratorAssets/api/pickTemplate.js';
import { createApp } from "vue";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { activityMessages } from "./i18n";
import store from "./store";
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
import App from './App.vue';
import App from "./App.vue";
const i18n = _createI18n(activityMessages);
// app for activity
const hasSocialIssues = document.querySelector('#social-issues-acc') !== null;
const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null;
const hasSocialIssues = document.querySelector("#social-issues-acc") !== null;
const hasLocation = document.querySelector("#location") !== null;
const hasPerson = document.querySelector("#add-persons") !== null;
const app = createApp({
template: `<app
@@ -27,20 +27,19 @@ const app = createApp({
hasLocation,
hasPerson,
};
}
},
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#activity');
.component("app", App)
.mount("#activity");
// app for picking template
const i18nGendoc = _createI18n({});
document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => {
fetchTemplates(el.dataset.entityClass).then(templates => {
document.querySelectorAll("div[data-docgen-template-picker]").forEach((el) => {
fetchTemplates(el.dataset.entityClass).then((templates) => {
const picker = {
template:
'<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" ' +
@@ -52,35 +51,36 @@ document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => {
return {
templates: templates,
entityId: el.dataset.entityId,
}
};
},
methods: {
generateDoc({ event, link, template }) {
console.log('generateDoc');
console.log('link', link);
console.log('template', template);
console.log("generateDoc");
console.log("link", link);
console.log("template", template);
let hiddenInput = document.querySelector("input[data-template-id]");
if (hiddenInput === null) {
console.error('hidden input not found');
console.error("hidden input not found");
return;
}
hiddenInput.value = template;
let form = document.querySelector('form[name="chill_activitybundle_activity"');
let form = document.querySelector(
'form[name="chill_activitybundle_activity"',
);
if (form === null) {
console.error('form not found');
console.error("form not found");
return;
}
form.submit();
}
}
},
},
};
createApp(picker).use(i18nGendoc).mount(el);
})
});
});

View File

@@ -1,21 +1,21 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postLocation } from './api';
import prepareLocations from './store.locations.js';
import "es6-promise/auto";
import { createStore } from "vuex";
import { postLocation } from "./api";
import prepareLocations from "./store.locations.js";
const debug = process.env.NODE_ENV !== 'production';
const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity);
const addIdToValue = (string, id) => {
let array = string ? string.split(',') : [];
let array = string ? string.split(",") : [];
array.push(id.toString());
let str = array.join();
return str;
};
const removeIdFromValue = (string, id) => {
let array = string.split(',');
array = array.filter(el => el !== id.toString());
let array = string.split(",");
array = array.filter((el) => el !== id.toString());
let str = array.join();
return str;
};
@@ -30,7 +30,10 @@ const store = createStore({
},
getters: {
suggestedEntities(state) {
if (typeof state.activity.accompanyingPeriod === "undefined" || state.activity.accompanyingPeriod === null) {
if (
typeof state.activity.accompanyingPeriod === "undefined" ||
state.activity.accompanyingPeriod === null
) {
return [];
}
const allEntities = [
@@ -44,7 +47,7 @@ const store = createStore({
];
return Array.from(
uniqueIds,
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0]
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0],
);
},
suggestedPersons(state) {
@@ -62,7 +65,7 @@ const store = createStore({
}
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id
(p) => p.id,
);
return [state.activity.accompanyingPeriod.requestor].filter(
@@ -72,7 +75,7 @@ const store = createStore({
state.activity.activityType.personsVisible !== 0) ||
(r.type === "thirdparty" &&
!existingThirdPartyIds.includes(r.id) &&
state.activity.activityType.thirdPartiesVisible !== 0)
state.activity.activityType.thirdPartiesVisible !== 0),
);
},
suggestedUser(state) {
@@ -80,14 +83,14 @@ const store = createStore({
return state.activity.activityType.usersVisible === 0
? []
: [state.activity.accompanyingPeriod.user].filter(
(u) => u !== null && !existingUserIds.includes(u.id)
(u) => u !== null && !existingUserIds.includes(u.id),
);
},
suggestedResources(state) {
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id
(p) => p.id,
);
return state.activity.accompanyingPeriod.resources
.map((r) => r.resource)
@@ -98,11 +101,13 @@ const store = createStore({
state.activity.activityType.personsVisible !== 0) ||
(r.type === "thirdparty" &&
!existingThirdPartyIds.includes(r.id) &&
state.activity.activityType.thirdPartiesVisible !== 0)
state.activity.activityType.thirdPartiesVisible !== 0),
);
},
socialActionsListSorted(state) {
return [ ...state.socialActionsList].sort((a, b) => a.ordering - b.ordering);
return [...state.socialActionsList].sort(
(a, b) => a.ordering - b.ordering,
);
},
},
mutations: {
@@ -126,7 +131,7 @@ const store = createStore({
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(
(i) => i.id !== issue.id
(i) => i.id !== issue.id,
);
},
resetActionsList(state) {
@@ -145,18 +150,15 @@ const store = createStore({
// remove duplicates entries
list = list.filter(
(value, index) =>
list.findIndex((array) => array.id === value.id) ===
index
list.findIndex((array) => array.id === value.id) === index,
);
// alpha sort
list.sort((a, b) =>
a.text > b.text ? 1 : b.text > a.text ? -1 : 0
);
list.sort((a, b) => (a.text > b.text ? 1 : b.text > a.text ? -1 : 0));
return list;
};
if (list === "issues") {
state.activity.accompanyingPeriod.socialIssues = filterList(
state.activity.accompanyingPeriod.socialIssues
state.activity.accompanyingPeriod.socialIssues,
);
}
if (list === "actions") {
@@ -184,18 +186,17 @@ const store = createStore({
switch (payload.type) {
case "person":
state.activity.persons = state.activity.persons.filter(
(person) => person !== payload
(person) => person !== payload,
);
break;
case "thirdparty":
state.activity.thirdParties =
state.activity.thirdParties.filter(
(thirdparty) => thirdparty !== payload
state.activity.thirdParties = state.activity.thirdParties.filter(
(thirdparty) => thirdparty !== payload,
);
break;
case "user":
state.activity.users = state.activity.users.filter(
(user) => user !== payload
(user) => user !== payload,
);
break;
}
@@ -206,39 +207,33 @@ const store = createStore({
},
addAvailableLocationGroup(state, group) {
state.availableLocations.push(group);
}
},
},
actions: {
addIssueSelected({ commit }, issue) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues"
"chill_activitybundle_activity_socialIssues",
);
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id);
commit("addIssueSelected", issue);
},
updateIssuesSelected({ commit }, payload) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues"
"chill_activitybundle_activity_socialIssues",
);
aSocialIssues.value = "";
payload.forEach((item) => {
aSocialIssues.value = addIdToValue(
aSocialIssues.value,
item.id
);
aSocialIssues.value = addIdToValue(aSocialIssues.value, item.id);
});
commit("updateIssuesSelected", payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById(
"chill_activitybundle_activity_socialActions"
"chill_activitybundle_activity_socialActions",
);
aSocialActions.value = "";
payload.forEach((item) => {
aSocialActions.value = addIdToValue(
aSocialActions.value,
item.id
);
aSocialActions.value = addIdToValue(aSocialActions.value, item.id);
});
commit("updateActionsSelected", payload);
},
@@ -250,30 +245,24 @@ const store = createStore({
switch (payload.result.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = addIdToValue(
aPersons.value,
payload.result.id
"chill_activitybundle_activity_persons",
);
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
"chill_activitybundle_activity_thirdParties",
);
aThirdParties.value = addIdToValue(
aThirdParties.value,
payload.result.id
payload.result.id,
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
);
aUsers.value = addIdToValue(
aUsers.value,
payload.result.id
"chill_activitybundle_activity_users",
);
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
break;
}
commit("addPersonsInvolved", payload);
@@ -283,25 +272,22 @@ const store = createStore({
switch (payload.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = removeIdFromValue(
aPersons.value,
payload.id
"chill_activitybundle_activity_persons",
);
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
"chill_activitybundle_activity_thirdParties",
);
aThirdParties.value = removeIdFromValue(
aThirdParties.value,
payload.id
payload.id,
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
"chill_activitybundle_activity_users",
);
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
@@ -311,32 +297,30 @@ const store = createStore({
updateLocation({ commit }, value) {
console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location"
"chill_activitybundle_activity_location",
);
if (value.onthefly) {
const body = {
"type": "location",
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
"locationType": {
"id": value.locationType.id,
"type": "location-type"
}
type: "location",
name:
value.name === "__AccompanyingCourseLocation__" ? null : value.name,
locationType: {
id: value.locationType.id,
type: "location-type",
},
};
if (value.address.id) {
Object.assign(body, {
"address": {
"id": value.address.id
address: {
id: value.address.id,
},
})
});
}
postLocation(body)
.then(
location => hiddenLocation.value = location.id
).catch(
err => {
.then((location) => (hiddenLocation.value = location.id))
.catch((err) => {
console.log(err.message);
}
);
});
} else {
hiddenLocation.value = value.id;
}

View File

@@ -1,41 +1,41 @@
import {getLocations, getLocationTypeByDefaultFor, getUserCurrentLocation} from "./api";
import {
getLocations,
getLocationTypeByDefaultFor,
getUserCurrentLocation,
} from "./api";
const makeConcernedPersonsLocation = (locationType, store) => {
let locations = [];
store.getters.suggestedEntities.forEach(
(e) => {
if (e.type === 'person' && e.current_household_address !== null){
store.getters.suggestedEntities.forEach((e) => {
if (e.type === "person" && e.current_household_address !== null) {
locations.push({
type: 'location',
type: "location",
id: -store.getters.suggestedEntities.indexOf(e) * 10,
onthefly: true,
name: e.text,
address: {
id: e.current_household_address.address_id,
},
locationType: locationType
locationType: locationType,
});
}
}
)
});
return locations;
};
const makeConcernedThirdPartiesLocation = (locationType, store) => {
let locations = [];
store.getters.suggestedEntities.forEach(
(e) => {
if (e.type === 'thirdparty' && e.address !== null){
store.getters.suggestedEntities.forEach((e) => {
if (e.type === "thirdparty" && e.address !== null) {
locations.push({
type: 'location',
type: "location",
id: -store.getters.suggestedEntities.indexOf(e) * 10,
onthefly: true,
name: e.text,
address: { id: e.address.address_id },
locationType: locationType
locationType: locationType,
});
}
}
)
});
return locations;
};
const makeAccompanyingPeriodLocation = (locationType, store) => {
@@ -44,78 +44,86 @@ const makeAccompanyingPeriodLocation = (locationType, store) => {
}
const accPeriodLocation = store.state.activity.accompanyingPeriod.location;
return {
type: 'location',
type: "location",
id: -1,
onthefly: true,
name: '__AccompanyingCourseLocation__',
name: "__AccompanyingCourseLocation__",
address: {
id: accPeriodLocation.address_id,
text: `${accPeriodLocation.text} - ${accPeriodLocation.postcode.code} ${accPeriodLocation.postcode.name}`
text: `${accPeriodLocation.text} - ${accPeriodLocation.postcode.code} ${accPeriodLocation.postcode.name}`,
},
locationType: locationType
}
locationType: locationType,
};
};
export default function prepareLocations(store) {
// find the locations
let allLocations = getLocations().then(
(results) => {
store.commit('addAvailableLocationGroup', {
locationGroup: 'Autres localisations',
locations: results
let allLocations = getLocations().then((results) => {
store.commit("addAvailableLocationGroup", {
locationGroup: "Autres localisations",
locations: results,
});
});
}
);
let currentLocation = getUserCurrentLocation().then(
userCurrentLocation => {
let currentLocation = getUserCurrentLocation().then((userCurrentLocation) => {
if (null !== userCurrentLocation) {
store.commit('addAvailableLocationGroup', {
locationGroup: 'Ma localisation',
locations: [userCurrentLocation]
store.commit("addAvailableLocationGroup", {
locationGroup: "Ma localisation",
locations: [userCurrentLocation],
});
}
}
);
});
let partiesLocations = [], partyPromise;
['person', 'thirdparty'].forEach(kind => {
let partiesLocations = [],
partyPromise;
["person", "thirdparty"].forEach((kind) => {
partyPromise = getLocationTypeByDefaultFor(kind).then(
(kindLocationType) => {
if (kindLocationType) {
let concernedKindLocations;
if (kind === 'person') {
concernedKindLocations = makeConcernedPersonsLocation(kindLocationType, store);
if (kind === "person") {
concernedKindLocations = makeConcernedPersonsLocation(
kindLocationType,
store,
);
// add location for the parcours into suggestions
const personLocation = makeAccompanyingPeriodLocation(kindLocationType, store);
store.commit('addAvailableLocationGroup', {
locationGroup: 'Localisation du parcours',
locations: [personLocation]
const personLocation = makeAccompanyingPeriodLocation(
kindLocationType,
store,
);
store.commit("addAvailableLocationGroup", {
locationGroup: "Localisation du parcours",
locations: [personLocation],
});
} else {
concernedKindLocations = makeConcernedThirdPartiesLocation(kindLocationType, store);
concernedKindLocations = makeConcernedThirdPartiesLocation(
kindLocationType,
store,
);
}
store.commit('addAvailableLocationGroup', {
locationGroup: kind === 'person' ? 'Usagers concernés' : 'Tiers concernés',
store.commit("addAvailableLocationGroup", {
locationGroup:
kind === "person" ? "Usagers concernés" : "Tiers concernés",
locations: concernedKindLocations,
});
}
}
},
);
partiesLocations.push(partyPromise);
});
// when all location are loaded
Promise.all([allLocations, currentLocation, ...partiesLocations]).then(() => {
console.log('current location in activity', store.state.activity.location);
console.log('default loation id', window.default_location_id);
console.log("current location in activity", store.state.activity.location);
console.log("default loation id", window.default_location_id);
if (window.default_location_id) {
for (let group of store.state.availableLocations) {
let location = group.locations.find((l) => l.id === window.default_location_id);
let location = group.locations.find(
(l) => l.id === window.default_location_id,
);
if (location !== undefined && store.state.activity.location === null) {
store.dispatch('updateLocation', location);
store.dispatch("updateLocation", location);
break;
}
}

View File

@@ -1,13 +1,18 @@
// this file loads all assets from the Chill person bundle
module.exports = function(encore, entries)
{
entries.push(__dirname + '/Resources/public/chill/index.js');
module.exports = function (encore, entries) {
entries.push(__dirname + "/Resources/public/chill/index.js");
encore.addAliases({
ChillActivityAssets: __dirname + '/Resources/public'
ChillActivityAssets: __dirname + "/Resources/public",
});
encore.addEntry('page_edit_activity', __dirname + '/Resources/public/page/edit_activity/index.scss');
encore.addEntry(
"page_edit_activity",
__dirname + "/Resources/public/page/edit_activity/index.scss",
);
encore.addEntry('vue_activity', __dirname + '/Resources/public/vuejs/Activity/index.js');
encore.addEntry(
"vue_activity",
__dirname + "/Resources/public/vuejs/Activity/index.js",
);
};

View File

@@ -1 +1 @@
require('./chillbudget.scss');
require("./chillbudget.scss");

View File

@@ -1,9 +1,8 @@
// this file loads all assets from the Chill budget bundle
module.exports = function(encore, entries)
{
module.exports = function (encore, entries) {
encore.addAliases({
ChillBudgetAssets: __dirname + '/Resources/public'
ChillBudgetAssets: __dirname + "/Resources/public",
});
encore.addEntry('page_budget', __dirname + '/Resources/public/page/index.js');
encore.addEntry("page_budget", __dirname + "/Resources/public/page/index.js");
};

View File

@@ -1,2 +1,2 @@
import './scss/badge.scss';
import './scss/calendar-list.scss';
import "./scss/badge.scss";
import "./scss/calendar-list.scss";

View File

@@ -1 +1 @@
require('./scss/calendar.scss');
require("./scss/calendar.scss");

View File

@@ -1,14 +1,13 @@
import { createApp } from 'vue';
import Answer from 'ChillCalendarAssets/vuejs/Invite/Answer';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { createApp } from "vue";
import Answer from "ChillCalendarAssets/vuejs/Invite/Answer";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
const i18n = _createI18n({});
document.addEventListener('DOMContentLoaded', function (e) {
console.log('dom loaded answer');
document.querySelectorAll('div[invite-answer]').forEach(function (el) {
console.log('element found', el);
document.addEventListener("DOMContentLoaded", function (e) {
console.log("dom loaded answer");
document.querySelectorAll("div[invite-answer]").forEach(function (el) {
console.log("element found", el);
const app = createApp({
components: {
@@ -18,14 +17,15 @@ document.addEventListener('DOMContentLoaded', function (e) {
return {
status: el.dataset.status,
calendarId: Number.parseInt(el.dataset.calendarId),
}
};
},
template: '<answer :calendarId="calendarId" :status="status" @statusChanged="onStatusChanged"></answer>',
template:
'<answer :calendarId="calendarId" :status="status" @statusChanged="onStatusChanged"></answer>',
methods: {
onStatusChanged: function (newStatus) {
this.$data.status = newStatus;
},
}
},
});
app.use(i18n).mount(el);

View File

@@ -1,5 +1,10 @@
import {EventInput} from '@fullcalendar/core';
import {DateTime, Location, User, UserAssociatedInterface} from '../../../ChillMainBundle/Resources/public/types' ;
import { EventInput } from "@fullcalendar/core";
import {
DateTime,
Location,
User,
UserAssociatedInterface,
} from "../../../ChillMainBundle/Resources/public/types";
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
export interface CalendarRange {
@@ -22,8 +27,8 @@ export interface CalendarRangeCreate {
}
export interface CalendarRangeEdit {
startDate?: DateTime,
endDate?: DateTime
startDate?: DateTime;
endDate?: DateTime;
location?: Location;
}
@@ -49,19 +54,23 @@ export interface CalendarRemote {
}
export type EventInputCalendarRange = EventInput & {
id: string,
userId: number,
userLabel: string,
calendarRangeId: number,
locationId: number,
locationName: string,
start: string,
end: string,
is: "range"
id: string;
userId: number;
userLabel: string;
calendarRangeId: number;
locationId: number;
locationName: string;
start: string;
end: string;
is: "range";
};
export function isEventInputCalendarRange(toBeDetermined: EventInputCalendarRange | EventInput): toBeDetermined is EventInputCalendarRange {
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
export function isEventInputCalendarRange(
toBeDetermined: EventInputCalendarRange | EventInput,
): toBeDetermined is EventInputCalendarRange {
return (
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
);
}
export {};

View File

@@ -1,63 +1,82 @@
<template>
<teleport to="#mainUser">
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" ></calendar-active>
<calendar-active :user="this.$store.getters.getMainUser" />
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
:removableIfSet="false"
:displayPicked="false"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
: []
"
:removable-if-set="false"
:display-picked="false"
:suggested="this.suggestedUsers"
:label="'main_user'"
@addNewEntity="setMainUser"
></pick-entity>
@add-new-entity="setMainUser"
/>
</div>
</div>
</teleport>
<concerned-groups></concerned-groups>
<concerned-groups />
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
<label class="col-form-label col-sm-4">Date</label>
<div class="col-sm-8">
{{ $d(activity.startDate, 'long') }} - {{ $d(activity.endDate, 'hoursOnly') }}
<span v-if="activity.calendarRange === null">(Pas de plage de disponibilité sélectionnée)</span>
{{ $d(activity.startDate, "long") }} -
{{ $d(activity.endDate, "hoursOnly") }}
<span v-if="activity.calendarRange === null"
>(Pas de plage de disponibilité sélectionnée)</span
>
<span v-else>(Une plage de disponibilité sélectionnée)</span>
</div>
</div>
</teleport>
<location></location>
<location />
<teleport to="#fullCalendar">
<div class="calendar-actives">
<template class="" v-for="u in getActiveUsers" :key="u.id">
<calendar-active :user="u" :invite="this.$store.getters.getInviteForUser(u)"></calendar-active>
<template v-for="u in getActiveUsers" :key="u.id">
<calendar-active
:user="u"
:invite="this.$store.getters.getInviteForUser(u)"
/>
</template>
</div>
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
<div
class="display-options row justify-content-between"
style="margin-top: 1rem"
>
<div class="col-sm-9 col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
<select v-model="slotDuration" id="slotDuration" class="form-select">
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
<select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
@@ -73,7 +92,11 @@
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
<select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
@@ -94,21 +117,44 @@
<div class="float-end">
<div class="form-check input-group">
<span class="input-group-text">
<input id="showHideWE" class="mt-0" type="checkbox" v-model="hideWeekends">
<input
id="showHideWE"
class="mt-0"
type="checkbox"
v-model="hideWeekends"
/>
</span>
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
<label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label
>
</div>
</div>
</div>
</div>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template v-slot:eventContent='arg'>
<template #eventContent="arg">
<span>
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} {{ arg.event.extendedProps.locationName }} <small>{{ arg.event.extendedProps.userLabel }}</small></b>
<b v-else-if="arg.event.extendedProps.is === 'current'">{{ arg.timeText }} {{ $t('current_selected')}} </b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
<b v-else>{{ arg.timeText }} {{ $t('current_selected')}} </b>
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }}
{{ arg.event.extendedProps.locationName }}
<small>{{
arg.event.extendedProps.userLabel
}}</small></b
>
<b v-else-if="arg.event.extendedProps.is === 'current'"
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
}}</b>
<b v-else
>{{ arg.timeText }} {{ $t("current_selected") }}
</b>
</span>
</template>
</FullCalendar>
@@ -116,16 +162,16 @@
</template>
<script>
import ConcernedGroups from 'ChillActivityAssets/vuejs/Activity/components/ConcernedGroups.vue';
import Location from 'ChillActivityAssets/vuejs/Activity/components/Location.vue';
import frLocale from '@fullcalendar/core/locales/fr';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list';
import CalendarActive from './Components/CalendarActive';
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
import ConcernedGroups from "ChillActivityAssets/vuejs/Activity/components/ConcernedGroups.vue";
import Location from "ChillActivityAssets/vuejs/Activity/components/Location.vue";
import frLocale from "@fullcalendar/core/locales/fr";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import listPlugin from "@fullcalendar/list";
import CalendarActive from "./Components/CalendarActive";
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
import { mapGetters, mapState } from "vuex";
export default {
@@ -141,24 +187,30 @@ export default {
return {
errorMsg: [],
showMyCalendar: false,
slotDuration: '00:05:00',
slotMinTime: '09:00:00',
slotMaxTime: '18:00:00',
slotDuration: "00:05:00",
slotMinTime: "09:00:00",
slotMaxTime: "18:00:00",
hideWeekEnds: true,
previousUser: [],
}
};
},
computed: {
...mapGetters(['getMainUser']),
...mapState(['activity']),
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
events() {
return this.$store.getters.getEventSources;
},
calendarOptions() {
return {
locale: frLocale,
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, dayGridPlugin, listPlugin],
initialView: 'timeGridWeek',
plugins: [
dayGridPlugin,
interactionPlugin,
timeGridPlugin,
dayGridPlugin,
listPlugin,
],
initialView: "timeGridWeek",
initialDate: this.$store.getters.getInitialDate,
eventSources: this.events,
selectable: true,
@@ -173,9 +225,9 @@ export default {
editable: true,
weekends: !this.hideWeekEnds,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'timeGridWeek,timeGridDay,listWeek',
left: "prev,next today",
center: "title",
right: "timeGridWeek,timeGridDay,listWeek",
},
views: {
timeGrid: {
@@ -195,9 +247,9 @@ export default {
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach(u => {
this.$data.previousUser.forEach((u) => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u)
suggested.push(u);
}
});
@@ -207,81 +259,120 @@ export default {
methods: {
setMainUser({ entity }) {
const user = entity;
console.log('setMainUser APP', entity);
console.log("setMainUser APP", entity);
if (user.id !== this.$store.getters.getMainUser && (
this.$store.state.activity.calendarRange !== null
|| this.$store.state.activity.startDate !== null
|| this.$store.state.activity.endDate !== null
if (
user.id !== this.$store.getters.getMainUser &&
(this.$store.state.activity.calendarRange !== null ||
this.$store.state.activity.startDate !== null ||
this.$store.state.activity.endDate !== null)
) {
if (
!window.confirm(
this.$t("change_main_user_will_reset_event_data"),
)
) {
if (!window.confirm(this.$t('change_main_user_will_reset_event_data'))) {
return;
}
}
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(this.$data.previousUser.map(u => u.id));
const suggestedUids = new Set(
this.$data.previousUser.map((u) => u.id),
);
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
this.$data.previousUser.push(this.$store.getters.getMainUser);
this.$data.previousUser.push(
this.$store.getters.getMainUser,
);
}
}
this.$store.dispatch('setMainUser', user);
this.$store.commit('showUserOnCalendar', {user, ranges: true, remotes: true});
this.$store.dispatch("setMainUser", user);
this.$store.commit("showUserOnCalendar", {
user,
ranges: true,
remotes: true,
});
},
removeMainUser(user) {
console.log('removeMainUser APP', user);
console.log("removeMainUser APP", user);
window.alert(this.$t('main_user_is_mandatory'));
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log('onDatesSet', event);
this.$store.dispatch('setCurrentDatesView', {start: event.start, end: event.end});
console.log("onDatesSet", event);
this.$store.dispatch("setCurrentDatesView", {
start: event.start,
end: event.end,
});
},
onDateSelect(payload) {
console.log('onDateSelect', payload);
console.log("onDateSelect", payload);
// show an alert if changing mainUser
if ((this.$store.getters.getMainUser !== null
&& this.$store.state.me.id !== this.$store.getters.getMainUser.id)
|| this.$store.getters.getMainUser === null) {
if (!window.confirm(this.$t('will_change_main_user_for_me'))) {
if (
(this.$store.getters.getMainUser !== null &&
this.$store.state.me.id !==
this.$store.getters.getMainUser.id) ||
this.$store.getters.getMainUser === null
) {
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
return;
} else {
this.$store.commit('showUserOnCalendar', {user: this.$store.state.me, remotes: true, ranges: true})
this.$store.commit("showUserOnCalendar", {
user: this.$store.state.me,
remotes: true,
ranges: true,
});
}
}
this.$store.dispatch('setEventTimes', {start: payload.start, end: payload.end});
this.$store.dispatch("setEventTimes", {
start: payload.start,
end: payload.end,
});
},
onEventChange(payload) {
console.log('onEventChange', payload);
console.log("onEventChange", payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error("not allowed to edit a calendar associated with a calendar range");
throw new Error(
"not allowed to edit a calendar associated with a calendar range",
);
}
this.$store.dispatch('setEventTimes', {start: payload.event.start, end: payload.event.end});
this.$store.dispatch("setEventTimes", {
start: payload.event.start,
end: payload.event.end,
});
},
onEventClick(payload) {
if (payload.event.extendedProps.is !== 'range') {
if (payload.event.extendedProps.is !== "range") {
// do nothing when clicking on remote
return;
}
// show an alert if changing mainUser
if (this.$store.getters.getMainUser !== null
&& payload.event.extendedProps.userId !== this.$store.getters.getMainUser.id) {
if (!window.confirm(this.$t('this_calendar_range_will_change_main_user'))) {
if (
this.$store.getters.getMainUser !== null &&
payload.event.extendedProps.userId !==
this.$store.getters.getMainUser.id
) {
if (
!window.confirm(
this.$t("this_calendar_range_will_change_main_user"),
)
) {
return;
}
}
this.$store.dispatch('associateCalendarToRange', {range: payload.event});
this.$store.dispatch("associateCalendarToRange", {
range: payload.event,
});
},
},
}
};
</script>
<style>

View File

@@ -3,68 +3,107 @@
<span class="badge-user">
{{ user.text }}
<template v-if="invite !== null">
<i v-if="invite.status === 'accepted'" class="fa fa-check"></i>
<i v-else-if="invite.status === 'declined'" class="fa fa-times"></i>
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o"></i>
<i v-else-if="invite.status === 'tentative'" class="fa fa-question"></i>
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
<i
v-else-if="invite.status === 'declined'"
class="fa fa-times"
/>
<i
v-else-if="invite.status === 'pending'"
class="fa fa-question-o"
/>
<i
v-else-if="invite.status === 'tentative'"
class="fa fa-question"
/>
<span v-else="">{{ invite.status }}</span>
</template>
</span>
<span class="form-check-inline form-switch">
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="rangeShow">
&nbsp;<label class="form-check-label" for="flexSwitchCheckDefault" title="Disponibilités"><i class="fa fa-calendar-check-o"></i></label>
<input
class="form-check-input"
type="checkbox"
id="flexSwitchCheckDefault"
v-model="rangeShow"
/>
&nbsp;<label
class="form-check-label"
for="flexSwitchCheckDefault"
title="Disponibilités"
><i class="fa fa-calendar-check-o"
/></label>
</span>
<span class="form-check-inline form-switch">
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="remoteShow">
&nbsp;<label class="form-check-label" for="flexSwitchCheckDefault" title="Agenda"><i class="fa fa-calendar"></i></label>
<input
class="form-check-input"
type="checkbox"
id="flexSwitchCheckDefault"
v-model="remoteShow"
/>
&nbsp;<label
class="form-check-label"
for="flexSwitchCheckDefault"
title="Agenda"
><i class="fa fa-calendar"
/></label>
</span>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import { mapGetters } from "vuex";
export default {
name: "CalendarActive",
props: {
user: {
type: Object,
required: true
required: true,
},
invite: {
type: Object,
required: false,
default: null,
}
},
},
computed: {
style() {
return {
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
backgroundColor: this.$store.getters.getUserData(this.user)
.mainColor,
};
},
rangeShow: {
set(value) {
this.$store.commit('showUserOnCalendar', {user: this.user, ranges: value});
this.$store.commit("showUserOnCalendar", {
user: this.user,
ranges: value,
});
},
get() {
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
}
return this.$store.getters.isRangeShownOnCalendarForUser(
this.user,
);
},
},
remoteShow: {
set(value) {
this.$store.commit('showUserOnCalendar', {user: this.user, remotes: value});
this.$store.commit("showUserOnCalendar", {
user: this.user,
remotes: value,
});
},
get() {
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
}
return this.$store.getters.isRemoteShownOnCalendarForUser(
this.user,
);
},
}
}
},
},
};
</script>
<style scoped lang="scss">
.calendar-active {
margin: 0 0.25rem 0.25rem 0;
padding: 0.5rem;

View File

@@ -1,7 +1,7 @@
import {fetchResults} from '../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods';
import {datetimeToISO} from '../../../../../ChillMainBundle/Resources/public/chill/js/date';
import {User} from '../../../../../ChillMainBundle/Resources/public/types';
import {CalendarLight, CalendarRange, CalendarRemote} from '../../types';
import { fetchResults } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { datetimeToISO } from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
import { User } from "../../../../../ChillMainBundle/Resources/public/types";
import { CalendarLight, CalendarRange, CalendarRemote } from "../../types";
// re-export whoami
export { whoami } from "../../../../../ChillMainBundle/Resources/public/lib/api/user";
@@ -13,26 +13,38 @@ export {whoami} from "../../../../../ChillMainBundle/Resources/public/lib/api/us
* @param Date end
* @return Promise
*/
export const fetchCalendarRangeForUser = (user: User, start: Date, end: Date): Promise<CalendarRange[]> => {
export const fetchCalendarRangeForUser = (
user: User,
start: Date,
end: Date,
): Promise<CalendarRange[]> => {
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end);
return fetchResults<CalendarRange>(uri, { dateFrom, dateTo });
}
};
export const fetchCalendarRemoteForUser = (user: User, start: Date, end: Date): Promise<CalendarRemote[]> => {
export const fetchCalendarRemoteForUser = (
user: User,
start: Date,
end: Date,
): Promise<CalendarRemote[]> => {
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end);
return fetchResults<CalendarRemote>(uri, { dateFrom, dateTo });
}
};
export const fetchCalendarLocalForUser = (user: User, start: Date, end: Date): Promise<CalendarLight[]> => {
export const fetchCalendarLocalForUser = (
user: User,
start: Date,
end: Date,
): Promise<CalendarLight[]> => {
const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`;
const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end);
return fetchResults<CalendarLight>(uri, { dateFrom, dateTo });
}
};

View File

@@ -1,19 +1,17 @@
const COLORS = [ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
'#8dd3c7',
'#ffffb3',
'#bebada',
'#fb8072',
'#80b1d3',
'#fdb462',
'#b3de69',
'#fccde5',
'#d9d9d9',
'#bc80bd',
'#ccebc5',
'#ffed6f'
const COLORS = [
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
"#8dd3c7",
"#ffffb3",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#b3de69",
"#fccde5",
"#d9d9d9",
"#bc80bd",
"#ccebc5",
"#ffed6f",
];
export {
COLORS,
};
export { COLORS };

View File

@@ -1,6 +1,6 @@
import {personMessages} from 'ChillPersonAssets/vuejs/_js/i18n'
import {calendarUserSelectorMessages} from '../_components/CalendarUserSelector/js/i18n';
import {activityMessages} from 'ChillActivityAssets/vuejs/Activity/i18n';
import { personMessages } from "ChillPersonAssets/vuejs/_js/i18n";
import { calendarUserSelectorMessages } from "../_components/CalendarUserSelector/js/i18n";
import { activityMessages } from "ChillActivityAssets/vuejs/Activity/i18n";
const appMessages = {
fr: {
@@ -13,20 +13,22 @@ const appMessages = {
bloc_thirdparty: "Tiers professionnels",
bloc_users: "T(M)S",
},
this_calendar_range_will_change_main_user: "Cette plage de disponibilité n'est pas celle de l'utilisateur principal. Si vous continuez, l'utilisateur principal sera adapté. Êtes-vous sûr·e ?",
will_change_main_user_for_me: "Vous ne pouvez pas écrire dans le calendrier d'un autre utilisateur. Voulez-vous être l'utilisateur principal de ce rendez-vous ?",
main_user_is_mandatory: "L'utilisateur principal est requis. Vous pouvez le modifier, mais pas le supprimer",
change_main_user_will_reset_event_data: "Modifier l'utilisateur principal nécessite de choisir une autre plage de disponibilité ou un autre horaire. Ces informations seront perdues. Êtes-vous sûr·e de vouloir continuer ?",
list_three_days: 'Liste 3 jours',
current_selected: 'Rendez-vous fixé',
this_calendar_range_will_change_main_user:
"Cette plage de disponibilité n'est pas celle de l'utilisateur principal. Si vous continuez, l'utilisateur principal sera adapté. Êtes-vous sûr·e ?",
will_change_main_user_for_me:
"Vous ne pouvez pas écrire dans le calendrier d'un autre utilisateur. Voulez-vous être l'utilisateur principal de ce rendez-vous ?",
main_user_is_mandatory:
"L'utilisateur principal est requis. Vous pouvez le modifier, mais pas le supprimer",
change_main_user_will_reset_event_data:
"Modifier l'utilisateur principal nécessite de choisir une autre plage de disponibilité ou un autre horaire. Ces informations seront perdues. Êtes-vous sûr·e de vouloir continuer ?",
list_three_days: "Liste 3 jours",
current_selected: "Rendez-vous fixé",
main_user: "Utilisateur principal",
}
}
},
};
Object.assign(appMessages.fr, personMessages.fr);
Object.assign(appMessages.fr, calendarUserSelectorMessages.fr);
Object.assign(appMessages.fr, activityMessages.fr);
export {
appMessages
};
export { appMessages };

View File

@@ -1,9 +1,9 @@
import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { appMessages } from './i18n'
import store from './store'
import { createApp } from "vue";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { appMessages } from "./i18n";
import store from "./store";
import App from './App.vue';
import App from "./App.vue";
const i18n = _createI18n(appMessages);
@@ -12,5 +12,5 @@ const app = createApp({
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#calendar');
.component("app", App)
.mount("#calendar");

View File

@@ -1,14 +1,11 @@
import {
addIdToValue,
removeIdFromValue,
} from './utils';
import { addIdToValue, removeIdFromValue } from "./utils";
import {
fetchCalendarRangeForUser,
fetchCalendarRemoteForUser,
fetchCalendarLocalForUser,
} from './../api';
import {datetimeToISO} from 'ChillMainAssets/chill/js/date';
import {postLocation} from 'ChillActivityAssets/vuejs/Activity/api';
} from "./../api";
import { datetimeToISO } from "ChillMainAssets/chill/js/date";
import { postLocation } from "ChillActivityAssets/vuejs/Activity/api";
/**
* This will store a unique key for each value, and prevent to launch the same
@@ -25,9 +22,9 @@ const fetchings = new Set();
export default {
setCurrentDatesView({ commit, dispatch }, { start, end }) {
commit('setCurrentDatesView', {start, end});
commit("setCurrentDatesView", { start, end });
return dispatch('fetchCalendarEvents');
return dispatch("fetchCalendarEvents");
},
fetchCalendarEvents({ state, getters, dispatch }) {
if (state.currentView.start === null && state.currentView.end === null) {
@@ -39,29 +36,32 @@ export default {
let unique = `${uid}, ${state.currentView.start.toISOString()}, ${state.currentView.end.toISOString()}`;
if (fetchings.has(unique)) {
console.log('prevent from fetching for a user', unique);
console.log("prevent from fetching for a user", unique);
continue;
}
fetchings.add(unique);
promises.push(
dispatch(
'fetchCalendarRangeForUser',
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
)
dispatch("fetchCalendarRangeForUser", {
user: state.usersData.get(uid).user,
start: state.currentView.start,
end: state.currentView.end,
}),
);
promises.push(
dispatch(
'fetchCalendarRemotesForUser',
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
)
dispatch("fetchCalendarRemotesForUser", {
user: state.usersData.get(uid).user,
start: state.currentView.start,
end: state.currentView.end,
}),
);
promises.push(
dispatch(
'fetchCalendarLocalsForUser',
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
)
dispatch("fetchCalendarLocalsForUser", {
user: state.usersData.get(uid).user,
start: state.currentView.start,
end: state.currentView.end,
}),
);
}
@@ -70,7 +70,7 @@ export default {
fetchCalendarRangeForUser({ commit, getters }, { user, start, end }) {
if (!getters.isCalendarRangeLoadedForUser({ user, start, end })) {
return fetchCalendarRangeForUser(user, start, end).then((ranges) => {
commit('addCalendarRangesForUser', {user, ranges, start, end});
commit("addCalendarRangesForUser", { user, ranges, start, end });
return Promise.resolve();
});
@@ -79,7 +79,7 @@ export default {
fetchCalendarRemotesForUser({ commit, getters }, { user, start, end }) {
if (!getters.isCalendarRemoteLoadedForUser({ user, start, end })) {
return fetchCalendarRemoteForUser(user, start, end).then((remotes) => {
commit('addCalendarRemotesForUser', {user, remotes, start, end});
commit("addCalendarRemotesForUser", { user, remotes, start, end });
return Promise.resolve();
});
@@ -88,52 +88,72 @@ export default {
fetchCalendarLocalsForUser({ commit, getters }, { user, start, end }) {
if (!getters.isCalendarRemoteLoadedForUser({ user, start, end })) {
return fetchCalendarLocalForUser(user, start, end).then((locals) => {
commit('addCalendarLocalsForUser', {user, locals, start, end});
commit("addCalendarLocalsForUser", { user, locals, start, end });
return Promise.resolve();
});
}
},
addPersonsInvolved({ commit, dispatch }, payload) {
console.log('### action addPersonsInvolved', payload.result.type);
console.log('### action addPersonsInvolved payload result', payload.result);
console.log("### action addPersonsInvolved", payload.result.type);
console.log("### action addPersonsInvolved payload result", payload.result);
switch (payload.result.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons",
);
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
break;
case 'thirdparty':
let aThirdParties = document.getElementById("chill_activitybundle_activity_professionals");
aThirdParties.value = addIdToValue(aThirdParties.value, payload.result.id);
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_professionals",
);
aThirdParties.value = addIdToValue(
aThirdParties.value,
payload.result.id,
);
break;
case 'user':
let aUsers = document.getElementById("chill_activitybundle_activity_users");
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users",
);
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
commit('showUserOnCalendar', {user: payload.result, ranges: false, remotes: true});
dispatch('fetchCalendarEvents');
commit("showUserOnCalendar", {
user: payload.result,
ranges: false,
remotes: true,
});
dispatch("fetchCalendarEvents");
break;
}
;
commit('addPersonsInvolved', payload);
commit("addPersonsInvolved", payload);
},
removePersonInvolved({ commit }, payload) {
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons",
);
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
break;
case 'thirdparty':
let aThirdParties = document.getElementById("chill_activitybundle_activity_professionals");
aThirdParties.value = removeIdFromValue(aThirdParties.value, payload.id);
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_professionals",
);
aThirdParties.value = removeIdFromValue(
aThirdParties.value,
payload.id,
);
break;
case 'user':
let aUsers = document.getElementById("chill_activitybundle_activity_users");
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users",
);
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
}
;
commit('removePersonInvolved', payload);
commit("removePersonInvolved", payload);
},
// Calendar
@@ -149,30 +169,48 @@ export default {
* @param end
*/
setEventTimes({ commit, state, getters }, { start, end }) {
console.log('### action createEvent', {start, end});
let startDateInput = document.getElementById("chill_activitybundle_activity_startDate");
startDateInput.value = null !== start ? datetimeToISO(start) : '';
let endDateInput = document.getElementById("chill_activitybundle_activity_endDate");
endDateInput.value = null !== end ? datetimeToISO(end) : '';
let calendarRangeInput = document.getElementById("chill_activitybundle_activity_calendarRange");
console.log("### action createEvent", { start, end });
let startDateInput = document.getElementById(
"chill_activitybundle_activity_startDate",
);
startDateInput.value = null !== start ? datetimeToISO(start) : "";
let endDateInput = document.getElementById(
"chill_activitybundle_activity_endDate",
);
endDateInput.value = null !== end ? datetimeToISO(end) : "";
let calendarRangeInput = document.getElementById(
"chill_activitybundle_activity_calendarRange",
);
calendarRangeInput.value = "";
if (getters.getMainUser === null || getters.getMainUser.id !== state.me.id) {
let mainUserInput = document.getElementById("chill_activitybundle_activity_mainUser");
if (
getters.getMainUser === null ||
getters.getMainUser.id !== state.me.id
) {
let mainUserInput = document.getElementById(
"chill_activitybundle_activity_mainUser",
);
mainUserInput.value = state.me.id;
commit('setMainUser', state.me);
commit("setMainUser", state.me);
}
commit('setEventTimes', {start, end});
commit("setEventTimes", { start, end });
},
associateCalendarToRange({ state, commit, dispatch, getters }, { range }) {
console.log('### action associateCAlendarToRange', range);
let startDateInput = document.getElementById("chill_activitybundle_activity_startDate");
console.log("### action associateCAlendarToRange", range);
let startDateInput = document.getElementById(
"chill_activitybundle_activity_startDate",
);
startDateInput.value = null !== range ? datetimeToISO(range.start) : "";
let endDateInput = document.getElementById("chill_activitybundle_activity_endDate");
let endDateInput = document.getElementById(
"chill_activitybundle_activity_endDate",
);
endDateInput.value = null !== range ? datetimeToISO(range.end) : "";
let calendarRangeInput = document.getElementById("chill_activitybundle_activity_calendarRange");
calendarRangeInput.value = null !== range ? Number(range.extendedProps.calendarRangeId) : "";
let calendarRangeInput = document.getElementById(
"chill_activitybundle_activity_calendarRange",
);
calendarRangeInput.value =
null !== range ? Number(range.extendedProps.calendarRangeId) : "";
if (null !== range) {
let location = getters.getLocationById(range.extendedProps.locationId);
@@ -181,63 +219,68 @@ export default {
console.error("location not found!", range.extendedProps.locationId);
}
dispatch('updateLocation', location);
dispatch("updateLocation", location);
const userId = range.extendedProps.userId;
if (state.activity.mainUser !== null && state.activity.mainUser.id !== userId) {
dispatch('setMainUser', state.usersData.get(userId).user);
if (
state.activity.mainUser !== null &&
state.activity.mainUser.id !== userId
) {
dispatch("setMainUser", state.usersData.get(userId).user);
// TODO: remove persons involved with this user
}
}
commit('associateCalendarToRange', {range});
commit("associateCalendarToRange", { range });
return Promise.resolve();
},
setMainUser({ commit, dispatch, state }, mainUser) {
console.log('setMainUser', mainUser);
console.log("setMainUser", mainUser);
let mainUserInput = document.getElementById("chill_activitybundle_activity_mainUser");
let mainUserInput = document.getElementById(
"chill_activitybundle_activity_mainUser",
);
mainUserInput.value = Number(mainUser.id);
return dispatch('associateCalendarToRange', { range: null }).then(() => {
commit('setMainUser', mainUser);
return dispatch("associateCalendarToRange", { range: null }).then(() => {
commit("setMainUser", mainUser);
return dispatch('fetchCalendarEvents');
return dispatch("fetchCalendarEvents");
});
},
// Location
updateLocation({ commit }, value) {
console.log('### action: updateLocation', value);
let hiddenLocation = document.getElementById("chill_activitybundle_activity_location");
console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location",
);
if (value.onthefly) {
const body = {
"type": "location",
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
"locationType": {
"id": value.locationType.id,
"type": "location-type"
}
type: "location",
name:
value.name === "__AccompanyingCourseLocation__" ? null : value.name,
locationType: {
id: value.locationType.id,
type: "location-type",
},
};
if (value.address.id) {
Object.assign(body, {
"address": {
"id": value.address.id
address: {
id: value.address.id,
},
})
});
}
postLocation(body)
.then(
location => hiddenLocation.value = location.id
).catch(
err => {
.then((location) => (hiddenLocation.value = location.id))
.catch((err) => {
console.log(err.message);
}
);
});
} else {
hiddenLocation.value = value.id;
}
commit("updateLocation", value);
}
}
},
};

View File

@@ -18,7 +18,7 @@ export default {
if (null === state.activity.start) {
return new Date();
}
throw 'transform date to object ?';
throw "transform date to object ?";
},
/**
* Compute the event sources to show on the FullCalendar
@@ -33,7 +33,7 @@ export default {
// current calendar
if (state.activity.startDate !== null && state.activity.endDate !== null) {
const s = {
id: 'current',
id: "current",
events: [
{
title: "Rendez-vous",
@@ -41,8 +41,8 @@ export default {
end: state.activity.endDate,
allDay: false,
is: "current",
classNames: ['iscurrent'],
}
classNames: ["iscurrent"],
},
],
editable: state.activity.calendarRange === null,
};
@@ -52,7 +52,7 @@ export default {
for (const [userId, kinds] of state.currentView.users.entries()) {
if (!state.usersData.has(userId)) {
console.log('try to get events on a user which not exists', userId);
console.log("try to get events on a user which not exists", userId);
continue;
}
@@ -61,11 +61,16 @@ export default {
if (kinds.ranges && userData.calendarRanges.length > 0) {
const s = {
id: `ranges_${userId}`,
events: userData.calendarRanges.filter(r => state.activity.calendarRange === null || r.calendarRangeId !== state.activity.calendarRange.calendarRangeId),
events: userData.calendarRanges.filter(
(r) =>
state.activity.calendarRange === null ||
r.calendarRangeId !==
state.activity.calendarRange.calendarRangeId,
),
color: userData.mainColor,
classNames: ['isrange'],
backgroundColor: 'white',
textColor: 'black',
classNames: ["isrange"],
backgroundColor: "white",
textColor: "black",
editable: false,
};
@@ -74,10 +79,10 @@ export default {
if (kinds.remotes && userData.remotes.length > 0) {
const s = {
'id': `remote_${userId}`,
id: `remote_${userId}`,
events: userData.remotes,
color: userData.mainColor,
textColor: 'black',
textColor: "black",
editable: false,
};
@@ -87,10 +92,12 @@ export default {
// if remotes is checked, we display also the locals calendars
if (kinds.remotes && userData.locals.length > 0) {
const s = {
'id': `local_${userId}`,
events: userData.locals.filter(l => l.originId !== state.activity.id),
id: `local_${userId}`,
events: userData.locals.filter(
(l) => l.originId !== state.activity.id,
),
color: userData.mainColor,
textColor: 'black',
textColor: "black",
editable: false,
};
@@ -104,7 +111,7 @@ export default {
return state.activity.startDate;
},
getInviteForUser: (state) => (user) => {
return state.activity.invites.find(i => i.user.id === user.id);
return state.activity.invites.find((i) => i.user.id === user.id);
},
/**
* get the user data for a specific user
@@ -138,7 +145,9 @@ export default {
* @param getters
* @returns {(function({user: *, start: *, end: *}): (boolean))|*}
*/
isCalendarRangeLoadedForUser: (state, getters) => ({user, start, end}) => {
isCalendarRangeLoadedForUser:
(state, getters) =>
({ user, start, end }) => {
if (!getters.hasUserData(user)) {
return false;
}
@@ -159,7 +168,9 @@ export default {
* @param getters
* @returns {(function({user: *, start: *, end: *}): (boolean))|*}
*/
isCalendarRemoteLoadedForUser: (state, getters) => ({user, start, end}) => {
isCalendarRemoteLoadedForUser:
(state, getters) =>
({ user, start, end }) => {
if (!getters.hasUserData(user)) {
return false;
}
@@ -180,8 +191,10 @@ export default {
*/
isRangeShownOnCalendarForUser: (state) => (user) => {
const k = state.currentView.users.get(user.id);
if (typeof k === 'undefined') {
console.error('try to determinate if calendar range is shown and user is not in currentView');
if (typeof k === "undefined") {
console.error(
"try to determinate if calendar range is shown and user is not in currentView",
);
return false;
}
@@ -195,8 +208,10 @@ export default {
*/
isRemoteShownOnCalendarForUser: (state) => (user) => {
const k = state.currentView.users.get(user.id);
if (typeof k === 'undefined') {
console.error('try to determinate if calendar range is shown and user is not in currentView');
if (typeof k === "undefined") {
console.error(
"try to determinate if calendar range is shown and user is not in currentView",
);
return false;
}
@@ -205,8 +220,8 @@ export default {
getLocationById: (state) => (id) => {
for (let group of state.availableLocations) {
console.log('group', group);
const found = group.locations.find(l => l.id === id);
console.log("group", group);
const found = group.locations.find((l) => l.id === id);
if (typeof found !== "undefined") {
return found;
}
@@ -216,57 +231,60 @@ export default {
},
suggestedEntities(state, getters) {
if (typeof (state.activity.accompanyingPeriod) === 'undefined') {
if (typeof state.activity.accompanyingPeriod === "undefined") {
return [];
}
const allEntities = [
...getters.suggestedPersons,
...getters.suggestedRequestor,
...getters.suggestedUser,
...getters.suggestedResources
...getters.suggestedResources,
];
const uniqueIds = [...new Set(allEntities.map(i => `${i.type}-${i.id}`))];
return Array.from(uniqueIds, id => allEntities.filter(r => `${r.type}-${r.id}` === id)[0]);
const uniqueIds = [...new Set(allEntities.map((i) => `${i.type}-${i.id}`))];
return Array.from(
uniqueIds,
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0],
);
},
suggestedPersons(state) {
const existingPersonIds = state.activity.persons.map(p => p.id);
const existingPersonIds = state.activity.persons.map((p) => p.id);
return state.activity.accompanyingPeriod.participations
.filter(p => p.endDate === null)
.map(p => p.person)
.filter(p => !existingPersonIds.includes(p.id))
.filter((p) => p.endDate === null)
.map((p) => p.person)
.filter((p) => !existingPersonIds.includes(p.id));
},
suggestedRequestor(state) {
if (state.activity.accompanyingPeriod.requestor === null) {
return [];
}
const existingPersonIds = state.activity.persons.map(p => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
return [state.activity.accompanyingPeriod.requestor]
.filter(r =>
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map((p) => p.id);
return [state.activity.accompanyingPeriod.requestor].filter(
(r) =>
(r.type === "person" && !existingPersonIds.includes(r.id)) ||
(r.type === "thirdparty" && !existingThirdPartyIds.includes(r.id)),
);
},
suggestedUser(state) {
if (null === state.activity.users) {
return [];
}
const existingUserIds = state.activity.users.map(p => p.id);
return [state.activity.accompanyingPeriod.user]
.filter(
u => u !== null && !existingUserIds.includes(u.id)
const existingUserIds = state.activity.users.map((p) => p.id);
return [state.activity.accompanyingPeriod.user].filter(
(u) => u !== null && !existingUserIds.includes(u.id),
);
},
suggestedResources(state) {
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map(p => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map((p) => p.id);
return state.activity.accompanyingPeriod.resources
.map(r => r.resource)
.filter(r =>
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
.map((r) => r.resource)
.filter(
(r) =>
(r.type === "person" && !existingPersonIds.includes(r.id)) ||
(r.type === "thirdparty" && !existingThirdPartyIds.includes(r.id)),
);
}
}
},
};

View File

@@ -1,14 +1,14 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postLocation } from 'ChillActivityAssets/vuejs/Activity/api';
import getters from './getters';
import actions from './actions';
import mutations from './mutations';
import { mapEntity } from './utils';
import { whoami } from '../api';
import "es6-promise/auto";
import { createStore } from "vuex";
import { postLocation } from "ChillActivityAssets/vuejs/Activity/api";
import getters from "./getters";
import actions from "./actions";
import mutations from "./mutations";
import { mapEntity } from "./utils";
import { whoami } from "../api";
import prepareLocations from "ChillActivityAssets/vuejs/Activity/store.locations";
const debug = process.env.NODE_ENV !== 'production';
const debug = process.env.NODE_ENV !== "production";
const store = createStore({
strict: debug,
@@ -42,16 +42,24 @@ const store = createStore({
actions,
});
whoami().then(me => {
store.commit('setWhoAmiI', me);
whoami().then((me) => {
store.commit("setWhoAmiI", me);
});
if (null !== store.getters.getMainUser) {
store.commit('showUserOnCalendar', {ranges: true, remotes: true, user: store.getters.getMainUser});
store.commit("showUserOnCalendar", {
ranges: true,
remotes: true,
user: store.getters.getMainUser,
});
}
for (let u of store.state.activity.users) {
store.commit('showUserOnCalendar', {ranges: false, remotes: false, user: u});
store.commit("showUserOnCalendar", {
ranges: false,
remotes: false,
user: u,
});
}
prepareLocations(store);

View File

@@ -3,7 +3,7 @@ import {
calendarRangeToFullCalendarEvent,
remoteToFullCalendarEvent,
localsToFullCalendarEvent,
} from './utils';
} from "./utils";
export default {
setWhoAmiI(state, me) {
@@ -20,13 +20,10 @@ export default {
const cur = state.currentView.users.get(user.id);
state.currentView.users.set(
user.id,
{
ranges: typeof ranges !== 'undefined' ? ranges : cur.ranges,
remotes: typeof remotes !== 'undefined' ? remotes : cur.remotes,
}
);
state.currentView.users.set(user.id, {
ranges: typeof ranges !== "undefined" ? ranges : cur.ranges,
remotes: typeof remotes !== "undefined" ? remotes : cur.remotes,
});
},
/**
* Set the event start and end to the given start and end,
@@ -49,7 +46,7 @@ export default {
* @param range
*/
associateCalendarToRange(state, { range }) {
console.log('associateCalendarToRange', range);
console.log("associateCalendarToRange", range);
if (null === range) {
state.activity.calendarRange = null;
@@ -59,22 +56,25 @@ export default {
return;
}
console.log('userId', range.extendedProps.userId);
console.log("userId", range.extendedProps.userId);
const r = state.usersData.get(range.extendedProps.userId).calendarRanges
.find(r => r.calendarRangeId === range.extendedProps.calendarRangeId);
const r = state.usersData
.get(range.extendedProps.userId)
.calendarRanges.find(
(r) => r.calendarRangeId === range.extendedProps.calendarRangeId,
);
if (typeof r === 'undefined') {
throw Error('Could not find managed calendar range');
if (typeof r === "undefined") {
throw Error("Could not find managed calendar range");
}
console.log('range found', r);
console.log("range found", r);
state.activity.startDate = range.start;
state.activity.endDate = range.end;
state.activity.calendarRange = r;
console.log('activity', state.activity);
console.log("activity", state.activity);
},
setMainUser(state, user) {
@@ -85,32 +85,36 @@ export default {
addPersonsInvolved(state, payload) {
//console.log('### mutation addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case 'person':
case "person":
state.activity.persons.push(payload.result);
break;
case 'thirdparty':
case "thirdparty":
state.activity.thirdParties.push(payload.result);
break;
case 'user':
case "user":
state.activity.users.push(payload.result);
break;
}
;
},
removePersonInvolved(state, payload) {
//console.log('### mutation removePersonInvolved', payload.type);
switch (payload.type) {
case 'person':
state.activity.persons = state.activity.persons.filter(person => person !== payload);
case "person":
state.activity.persons = state.activity.persons.filter(
(person) => person !== payload,
);
break;
case 'thirdparty':
state.activity.thirdParties = state.activity.thirdParties.filter(thirdparty => thirdparty !== payload);
case "thirdparty":
state.activity.thirdParties = state.activity.thirdParties.filter(
(thirdparty) => thirdparty !== payload,
);
break;
case 'user':
state.activity.users = state.activity.users.filter(user => user !== payload);
case "user":
state.activity.users = state.activity.users.filter(
(user) => user !== payload,
);
break;
}
;
},
/**
* Add CalendarRange object for an user
@@ -131,13 +135,13 @@ export default {
}
const eventRanges = ranges
.filter(r => !state.existingEvents.has(`range_${r.id}`))
.map(r => {
.filter((r) => !state.existingEvents.has(`range_${r.id}`))
.map((r) => {
// add to existing ids
state.existingEvents.add(`range_${r.id}`);
return r;
})
.map(r => calendarRangeToFullCalendarEvent(r));
.map((r) => calendarRangeToFullCalendarEvent(r));
userData.calendarRanges = userData.calendarRanges.concat(eventRanges);
userData.calendarRangesLoaded.push({ start, end });
@@ -152,13 +156,13 @@ export default {
}
const eventRemotes = remotes
.filter(r => !state.existingEvents.has(`remote_${r.id}`))
.map(r => {
.filter((r) => !state.existingEvents.has(`remote_${r.id}`))
.map((r) => {
// add to existing ids
state.existingEvents.add(`remote_${r.id}`);
return r;
})
.map(r => remoteToFullCalendarEvent(r));
.map((r) => remoteToFullCalendarEvent(r));
userData.remotes = userData.remotes.concat(eventRemotes);
userData.remotesLoaded.push({ start, end });
@@ -173,20 +177,20 @@ export default {
}
const eventRemotes = locals
.filter(r => !state.existingEvents.has(`locals_${r.id}`))
.map(r => {
.filter((r) => !state.existingEvents.has(`locals_${r.id}`))
.map((r) => {
// add to existing ids
state.existingEvents.add(`locals_${r.id}`);
return r;
})
.map(r => localsToFullCalendarEvent(r));
.map((r) => localsToFullCalendarEvent(r));
userData.locals = userData.locals.concat(eventRemotes);
userData.localsLoaded.push({ start, end });
},
// Location
updateLocation(state, value) {
console.log('### mutation: updateLocation', value);
console.log("### mutation: updateLocation", value);
state.activity.location = value;
},
addAvailableLocationGroup(state, group) {

View File

@@ -1,32 +1,35 @@
import {COLORS} from '../const';
import {ISOToDatetime} from '../../../../../../ChillMainBundle/Resources/public/chill/js/date';
import {DateTime, User} from '../../../../../../ChillMainBundle/Resources/public/types';
import {CalendarLight, CalendarRange, CalendarRemote} from '../../../types';
import type {EventInputCalendarRange} from '../../../types';
import {EventInput} from '@fullcalendar/core';
import { COLORS } from "../const";
import { ISOToDatetime } from "../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import {
DateTime,
User,
} from "../../../../../../ChillMainBundle/Resources/public/types";
import { CalendarLight, CalendarRange, CalendarRemote } from "../../../types";
import type { EventInputCalendarRange } from "../../../types";
import { EventInput } from "@fullcalendar/core";
export interface UserData {
user: User,
calendarRanges: CalendarRange[],
calendarRangesLoaded: {}[],
remotes: CalendarRemote[],
remotesLoaded: {}[],
locals: CalendarRemote[],
localsLoaded: {}[],
mainColor: string,
user: User;
calendarRanges: CalendarRange[];
calendarRangesLoaded: {}[];
remotes: CalendarRemote[];
remotesLoaded: {}[];
locals: CalendarRemote[];
localsLoaded: {}[];
mainColor: string;
}
export const addIdToValue = (string: string, id: number): string => {
let array = string ? string.split(',') : [];
const array = string ? string.split(",") : [];
array.push(id.toString());
let str = array.join();
const str = array.join();
return str;
};
export const removeIdFromValue = (string: string, id: number) => {
let array = string.split(',');
array = array.filter(el => el !== id.toString());
let str = array.join();
let array = string.split(",");
array = array.filter((el) => el !== id.toString());
const str = array.join();
return str;
};
@@ -34,7 +37,7 @@ export const removeIdFromValue = (string: string, id: number) => {
* Assign missing keys for the ConcernedGroups component
*/
export const mapEntity = (entity: EventInput): EventInput => {
let calendar = { ...entity};
const calendar = { ...entity };
Object.assign(calendar, { thirdParties: entity.professionals });
if (entity.startDate !== null) {
@@ -64,11 +67,13 @@ export const createUserData = (user: User, colorIndex: number): UserData => {
locals: [],
localsLoaded: [],
mainColor: COLORS[colorId],
}
}
};
};
// TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app
export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInputCalendarRange => {
export const calendarRangeToFullCalendarEvent = (
entity: CalendarRange,
): EventInputCalendarRange => {
return {
id: `range_${entity.id}`,
title: "(" + entity.user.text + ")",
@@ -80,29 +85,33 @@ export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventIn
calendarRangeId: entity.id,
locationId: entity.location.id,
locationName: entity.location.name,
is: 'range',
is: "range",
};
};
}
export const remoteToFullCalendarEvent = (entity: CalendarRemote): EventInput & {id: string} => {
export const remoteToFullCalendarEvent = (
entity: CalendarRemote,
): EventInput & { id: string } => {
return {
id: `range_${entity.id}`,
title: entity.title,
start: entity.startDate.datetime8601,
end: entity.endDate.datetime8601,
allDay: entity.isAllDay,
is: 'remote',
is: "remote",
};
};
}
export const localsToFullCalendarEvent = (entity: CalendarLight): EventInput & {id: string; originId: number;} => {
export const localsToFullCalendarEvent = (
entity: CalendarLight,
): EventInput & { id: string; originId: number } => {
return {
id: `local_${entity.id}`,
title: entity.persons.map(p => p.text).join(', '),
title: entity.persons.map((p) => p.text).join(", "),
originId: entity.id,
start: entity.startDate.datetime8601,
end: entity.endDate.datetime8601,
allDay: false,
is: 'local',
is: "local",
};
};
}

View File

@@ -1,50 +1,81 @@
<template>
<div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn btn-misc dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<button
id="btnGroupDrop1"
type="button"
class="btn btn-misc dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<template v-if="status === Statuses.PENDING">
<span class="fa fa-hourglass"></span> {{ $t('Give_an_answer')}}
<span class="fa fa-hourglass"></span> {{ $t("Give_an_answer") }}
</template>
<template v-else-if="status === Statuses.ACCEPTED">
<span class="fa fa-check"></span> {{ $t('Accepted')}}
<span class="fa fa-check"></span> {{ $t("Accepted") }}
</template>
<template v-else-if="status === Statuses.DECLINED">
<span class="fa fa-times"></span> {{ $t('Declined')}}
<span class="fa fa-times"></span> {{ $t("Declined") }}
</template>
<template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
<span class="fa fa-question"></span> {{ $t('Tentative')}}
<span class="fa fa-question"></span> {{ $t("Tentative") }}
</template>
</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<li v-if="status !== Statuses.ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"><i class="fa fa-check" aria-hidden="true"></i> {{ $t('Accept') }}</a></li>
<li v-if="status !== Statuses.DECLINED"><a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"><i class="fa fa-times" aria-hidden="true"></i> {{ $t('Decline') }}</a></li>
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"><i class="fa fa-question"></i> {{ $t('Tentatively_accept') }}</a></li>
<li v-if="status !== Statuses.PENDING"><a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"><i class="fa fa-hourglass-o"></i> {{ $t('Set_pending') }}</a></li>
<li v-if="status !== Statuses.ACCEPTED">
<a
class="dropdown-item"
@click="changeStatus(Statuses.ACCEPTED)"
><i class="fa fa-check" aria-hidden="true"></i>
{{ $t("Accept") }}</a
>
</li>
<li v-if="status !== Statuses.DECLINED">
<a
class="dropdown-item"
@click="changeStatus(Statuses.DECLINED)"
><i class="fa fa-times" aria-hidden="true"></i>
{{ $t("Decline") }}</a
>
</li>
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
<a
class="dropdown-item"
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
><i class="fa fa-question"></i>
{{ $t("Tentatively_accept") }}</a
>
</li>
<li v-if="status !== Statuses.PENDING">
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
><i class="fa fa-hourglass-o"></i>
{{ $t("Set_pending") }}</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import {defineComponent, PropType} from 'vue';
import { defineComponent, PropType } from "vue";
const ACCEPTED = 'accepted';
const DECLINED = 'declined';
const PENDING = 'pending';
const TENTATIVELY_ACCEPTED = 'tentative';
const ACCEPTED = "accepted";
const DECLINED = "declined";
const PENDING = "pending";
const TENTATIVELY_ACCEPTED = "tentative";
const i18n = {
messages: {
fr: {
"Give_an_answer": "Répondre",
"Accepted": "Accepté",
"Declined": "Refusé",
"Tentative": "Accepté provisoirement",
"Accept": "Accepter",
"Decline": "Refuser",
"Tentatively_accept": "Accepter provisoirement",
"Set_pending": "Ne pas répondre",
}
}
Give_an_answer: "Répondre",
Accepted: "Accepté",
Declined: "Refusé",
Tentative: "Accepté provisoirement",
Accept: "Accepter",
Decline: "Refuser",
Tentatively_accept: "Accepter provisoirement",
Set_pending: "Ne pas répondre",
},
},
};
export default defineComponent({
@@ -52,10 +83,17 @@ export default defineComponent({
i18n,
props: {
calendarId: { type: Number, required: true },
status: {type: String as PropType<"accepted" | "declined" | "pending" | "tentative">, required: true},
status: {
type: String as PropType<
"accepted" | "declined" | "pending" | "tentative"
>,
required: true,
},
},
emits: {
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
statusChanged(
payload: "accepted" | "declined" | "pending" | "tentative",
) {
return true;
},
},
@@ -67,29 +105,29 @@ export default defineComponent({
PENDING,
TENTATIVELY_ACCEPTED,
},
}
};
},
methods: {
changeStatus: function (newStatus: "accepted" | "declined" | "pending" | "tentative") {
console.log('changeStatus', newStatus);
changeStatus: function (
newStatus: "accepted" | "declined" | "pending" | "tentative",
) {
console.log("changeStatus", newStatus);
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
window.fetch(url, {
method: 'POST',
}).then((r: Response) => {
window
.fetch(url, {
method: "POST",
})
.then((r: Response) => {
if (!r.ok) {
console.error('could not confirm answer', newStatus);
console.error("could not confirm answer", newStatus);
return;
}
console.log('answer sent', newStatus);
this.$emit('statusChanged', newStatus);
console.log("answer sent", newStatus);
this.$emit("statusChanged", newStatus);
});
},
}
})
},
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -23,14 +23,22 @@
<label class="input-group-text" for="slotDuration"
>Durée des créneaux</label
>
<select v-model="slotDuration" id="slotDuration" class="form-select">
<select
v-model="slotDuration"
id="slotDuration"
class="form-select"
>
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
</select>
<label class="input-group-text" for="slotMinTime">De</label>
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
<select
v-model="slotMinTime"
id="slotMinTime"
class="form-select"
>
<option value="00:00:00">0h</option>
<option value="01:00:00">1h</option>
<option value="02:00:00">2h</option>
@@ -46,7 +54,11 @@
<option value="12:00:00">12h</option>
</select>
<label class="input-group-text" for="slotMaxTime">À</label>
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
<select
v-model="slotMaxTime"
id="slotMaxTime"
class="form-select"
>
<option value="12:00:00">12h</option>
<option value="13:00:00">13h</option>
<option value="14:00:00">14h</option>
@@ -74,7 +86,9 @@
v-model="showWeekends"
/>
</span>
<label for="showHideWE" class="form-check-label input-group-text"
<label
for="showHideWE"
class="form-check-label input-group-text"
>Week-ends</label
>
</div>
@@ -88,7 +102,8 @@
arg.event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b
>{{ arg.timeText }} -
{{ arg.event.extendedProps.locationName }}</b
>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
@@ -106,29 +121,45 @@
<div id="copy-widget">
<div class="container mt-2 mb-2">
<div class="row justify-content-between align-items-center mb-4">
<div class="col-xs-12 col-sm-3 col-md-2">
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
</div>
<div class="col-xs-12 col-sm-9 col-md-2">
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select">
<select
v-model="dayOrWeek"
id="dayOrWeek"
class="form-select"
>
<option value="day">{{ $t("from_day_to_day") }}</option>
<option value="week">{{ $t("from_week_to_week") }}</option>
<option value="week">
{{ $t("from_week_to_week") }}
</option>
</select>
</div>
<template v-if="dayOrWeek === 'day'">
<div class="col-xs-12 col-sm-3 col-md-3">
<input class="form-control" type="date" v-model="copyFrom" />
<input
class="form-control"
type="date"
v-model="copyFrom"
/>
</div>
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
<i class="fa fa-angle-double-right"></i>
</div>
<div class="col-xs-12 col-sm-3 col-md-3">
<input class="form-control" type="date" v-model="copyTo" />
<input
class="form-control"
type="date"
v-model="copyTo"
/>
</div>
<div class="col-xs-12 col-sm-5 col-md-1">
<button class="btn btn-action float-end" @click="copyDay">
<button
class="btn btn-action float-end"
@click="copyDay"
>
{{ $t("copy_range") }}
</button>
</div>
@@ -140,7 +171,11 @@
id="copyFromWeek"
class="form-select"
>
<option v-for="w in lastWeeks" :value="w.value" :key="w.value">
<option
v-for="w in lastWeeks"
:value="w.value"
:key="w.value"
>
{{ w.text }}
</option>
</select>
@@ -149,21 +184,31 @@
<i class="fa fa-angle-double-right"></i>
</div>
<div class="col-xs-12 col-sm-3 col-md-3">
<select v-model="copyToWeek" id="copyToWeek" class="form-select">
<option v-for="w in nextWeeks" :value="w.value" :key="w.value">
<select
v-model="copyToWeek"
id="copyToWeek"
class="form-select"
>
<option
v-for="w in nextWeeks"
:value="w.value"
:key="w.value"
>
{{ w.text }}
</option>
</select>
</div>
<div class="col-xs-12 col-sm-5 col-md-1">
<button class="btn btn-action float-end" @click="copyWeek">
<button
class="btn btn-action float-end"
@click="copyWeek"
>
{{ $t("copy_range") }}
</button>
</div>
</template>
</div>
</div>
</div>
<!-- not directly seen, but include in a modal -->
@@ -224,7 +269,7 @@ interface Weeks {
const getMonday = (week: number): Date => {
const lastMonday = new Date();
lastMonday.setDate(
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7,
);
return lastMonday;
};
@@ -243,7 +288,7 @@ const lastWeeks = computed((): Weeks[] =>
value: dateToISO(lastMonday),
text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`,
};
})
}),
);
const nextWeeks = computed((): Weeks[] =>
@@ -253,7 +298,7 @@ const nextWeeks = computed((): Weeks[] =>
value: dateToISO(nextMonday),
text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`,
};
})
}),
);
const baseOptions = ref<CalendarOptions>({
@@ -298,7 +343,9 @@ const pickedLocation = computed<Location | null>({
);
},
set(newLocation: Location | null): void {
store.commit("locations/setLocationPicked", newLocation, { root: true });
store.commit("locations/setLocationPicked", newLocation, {
root: true,
});
},
});
@@ -354,7 +401,7 @@ function onDatesSet(event: DatesSetArg): void {
function onDateSelect(event: DateSelectArg): void {
if (null === pickedLocation.value) {
window.alert(
"Indiquez une localisation avant de créer une période de disponibilité."
"Indiquez une localisation avant de créer une période de disponibilité.",
);
return;
}
@@ -376,7 +423,7 @@ function onClickDelete(event: EventApi): void {
store.dispatch(
"calendarRanges/deleteRange",
event.extendedProps.calendarRangeId
event.extendedProps.calendarRangeId,
);
}

View File

@@ -1,24 +1,26 @@
<template>
<component :is="Teleport" to="body">
<modal v-if="showModal"
@close="closeModal">
<modal v-if="showModal" @close="closeModal">
<template v-slot:header>
<h3>{{ 'Modifier le lieu' }}</h3>
<h3>{{ "Modifier le lieu" }}</h3>
</template>
<template v-slot:body>
<div>
</div>
<div></div>
<label>Localisation</label>
<vue-multiselect v-model="location" :options="locations" :label="'name'" :track-by="'id'"></vue-multiselect>
<vue-multiselect
v-model="location"
:options="locations"
:label="'name'"
:track-by="'id'"
></vue-multiselect>
</template>
<template v-slot:footer>
<button class="btn btn-save" @click="saveAndClose">{{ 'Enregistrer' }}</button>
<button class="btn btn-save" @click="saveAndClose">
{{ "Enregistrer" }}
</button>
</template>
</modal>
</component>
</template>
@@ -34,18 +36,11 @@ import VueMultiselect from "vue-multiselect";
//import type {Teleport} from "vue";
// see https://github.com/vuejs/core/issues/2855
import {
Teleport as teleport_,
TeleportProps,
VNodeProps
} from 'vue'
const Teleport = teleport_ as {
new (): {
$props: VNodeProps & TeleportProps
}
}
import { Teleport as teleport_, TeleportProps, VNodeProps } from "vue";
const Teleport = teleport_ as new () => {
$props: VNodeProps & TeleportProps;
};
const store = useStore(key);
@@ -59,30 +54,36 @@ const locations = computed<Location[]>(() => {
});
const startEdit = function (event: EventApi): void {
console.log('startEditing', event);
console.log("startEditing", event);
calendarRangeId.value = event.extendedProps.calendarRangeId;
location.value = store.getters['locations/getLocationById'](event.extendedProps.locationId) || null;
location.value =
store.getters["locations/getLocationById"](
event.extendedProps.locationId,
) || null;
console.log('new location value', location.value);
console.log('calendar range id', calendarRangeId.value);
console.log("new location value", location.value);
console.log("calendar range id", calendarRangeId.value);
showModal.value = true;
}
};
const saveAndClose = function (e: Event): void {
console.log('saveEditAndClose', e);
console.log("saveEditAndClose", e);
store.dispatch('calendarRanges/patchRangeLocation', {location: location.value, calendarRangeId: calendarRangeId.value})
.then(_ => {showModal.value = false;})
}
store
.dispatch("calendarRanges/patchRangeLocation", {
location: location.value,
calendarRangeId: calendarRangeId.value,
})
.then((_) => {
showModal.value = false;
});
};
const closeModal = function (_: any): void {
showModal.value = false;
}
};
defineExpose({ startEdit });
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -8,7 +8,8 @@ const appMessages = {
copy_range_from_to: "Copier les plages",
from_day_to_day: "d'un jour à l'autre",
from_week_to_week: "d'une semaine à l'autre",
copy_range_how_to: "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
copy_range_how_to:
"Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
new_range_to_save: "Nouvelles plages à enregistrer",
update_range_to_save: "Plages à modifier",
delete_range_to_save: "Plages à supprimer",
@@ -20,9 +21,7 @@ const appMessages = {
week: "Semaine",
month: "Mois",
today: "Aujourd'hui",
}
}
export {
appMessages
},
};
export { appMessages };

View File

@@ -1,9 +1,9 @@
import { createApp } from 'vue';
import { _createI18n } from '../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n'
import { appMessages } from './i18n'
import futureStore, {key} from './store/index'
import { createApp } from "vue";
import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import { appMessages } from "./i18n";
import futureStore, { key } from "./store/index";
import App2 from './App2.vue';
import App2 from "./App2.vue";
import { useI18n } from "vue-i18n";
futureStore().then((store) => {
@@ -14,6 +14,6 @@ futureStore().then((store) => {
})
.use(store, key)
.use(i18n)
.component('app', App2)
.mount('#myCalendar');
.component("app", App2)
.mount("#myCalendar");
});

View File

@@ -1,24 +1,26 @@
import 'es6-promise/auto';
import {Store, createStore} from 'vuex';
import "es6-promise/auto";
import { Store, createStore } from "vuex";
import { InjectionKey } from "vue";
import me, {MeState} from './modules/me';
import fullCalendar, {FullCalendarState} from './modules/fullcalendar';
import calendarRanges, {CalendarRangesState} from './modules/calendarRanges';
import calendarRemotes, {CalendarRemotesState} from './modules/calendarRemotes';
import me, { MeState } from "./modules/me";
import fullCalendar, { FullCalendarState } from "./modules/fullcalendar";
import calendarRanges, { CalendarRangesState } from "./modules/calendarRanges";
import calendarRemotes, {
CalendarRemotesState,
} from "./modules/calendarRemotes";
import { whoami } from "../../../../../../ChillMainBundle/Resources/public/lib/api/user";
import {User} from '../../../../../../ChillMainBundle/Resources/public/types';
import { User } from "../../../../../../ChillMainBundle/Resources/public/types";
import locations, { LocationState } from "./modules/location";
import calendarLocals, { CalendarLocalsState } from "./modules/calendarLocals";
const debug = process.env.NODE_ENV !== 'production';
const debug = process.env.NODE_ENV !== "production";
export interface State {
calendarRanges: CalendarRangesState,
calendarRemotes: CalendarRemotesState,
calendarLocals: CalendarLocalsState,
fullCalendar: FullCalendarState,
me: MeState,
locations: LocationState
calendarRanges: CalendarRangesState;
calendarRemotes: CalendarRemotesState;
calendarLocals: CalendarLocalsState;
fullCalendar: FullCalendarState;
me: MeState;
locations: LocationState;
}
export const key: InjectionKey<Store<State>> = Symbol();
@@ -35,16 +37,20 @@ const futureStore = function(): Promise<Store<State>> {
calendarLocals,
locations,
},
mutations: {}
mutations: {},
});
store.commit('me/setWhoAmi', user, {root: true});
store.dispatch('locations/getLocations', null, {root: true}).then(_ => {
return store.dispatch('locations/getCurrentLocation', null, {root: true});
store.commit("me/setWhoAmi", user, { root: true });
store
.dispatch("locations/getLocations", null, { root: true })
.then((_) => {
return store.dispatch("locations/getCurrentLocation", null, {
root: true,
});
});
return Promise.resolve(store);
});
}
};
export default futureStore;

View File

@@ -1,33 +1,38 @@
import {State} from './../index';
import {ActionContext, Module} from 'vuex';
import {CalendarLight} from '../../../../types';
import {fetchCalendarLocalForUser} from '../../../Calendar/api';
import {EventInput} from '@fullcalendar/core';
import { State } from "./../index";
import { ActionContext, Module } from "vuex";
import { CalendarLight } from "../../../../types";
import { fetchCalendarLocalForUser } from "../../../Calendar/api";
import { EventInput } from "@fullcalendar/core";
import { localsToFullCalendarEvent } from "../../../Calendar/store/utils";
import { TransportExceptionInterface } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { COLORS } from "../../../Calendar/const";
export interface CalendarLocalsState {
locals: EventInput[],
localsLoaded: {start: number, end: number}[],
localsIndex: Set<string>,
key: number
locals: EventInput[];
localsLoaded: { start: number; end: number }[];
localsIndex: Set<string>;
key: number;
}
type Context = ActionContext<CalendarLocalsState, State>;
export default <Module<CalendarLocalsState, State>> {
export default {
namespaced: true,
state: (): CalendarLocalsState => ({
locals: [],
localsLoaded: [],
localsIndex: new Set<string>(),
key: 0
key: 0,
}),
getters: {
isLocalsLoaded: (state: CalendarLocalsState) => ({start, end}: {start: Date, end: Date}): boolean => {
for (let range of state.localsLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) {
isLocalsLoaded:
(state: CalendarLocalsState) =>
({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.localsLoaded) {
if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true;
}
}
@@ -37,11 +42,11 @@ export default <Module<CalendarLocalsState, State>> {
},
mutations: {
addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) {
console.log('addLocals', ranges);
console.log("addLocals", ranges);
const toAdd = ranges
.map(cr => localsToFullCalendarEvent(cr))
.filter(r => !state.localsIndex.has(r.id));
.map((cr) => localsToFullCalendarEvent(cr))
.filter((r) => !state.localsIndex.has(r.id));
toAdd.forEach((r) => {
state.localsIndex.add(r.id);
@@ -49,16 +54,25 @@ export default <Module<CalendarLocalsState, State>> {
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarLocalsState, payload: {start: Date, end: Date}) {
state.localsLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
addLoaded(
state: CalendarLocalsState,
payload: { start: Date; end: Date },
) {
state.localsLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
},
actions: {
fetchLocals(ctx: Context, payload: {start: Date, end: Date}): Promise<null> {
fetchLocals(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters['me/getMe'] === null) {
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(null);
}
@@ -66,23 +80,30 @@ export default <Module<CalendarLocalsState, State>> {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit('addLoaded', {
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarLocalForUser(
ctx.rootGetters['me/getMe'],
ctx.rootGetters["me/getMe"],
start,
end
end,
)
.then((remotes: CalendarLight[]) => {
// to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes);
const inputs = remotes
.map(cr => localsToFullCalendarEvent(cr))
.map(cr => ({...cr, backgroundColor: COLORS[0], textColor: 'black', editable: false}))
ctx.commit('calendarRanges/addExternals', inputs, {root: true});
.map((cr) => localsToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: COLORS[0],
textColor: "black",
editable: false,
}));
ctx.commit("calendarRanges/addExternals", inputs, {
root: true,
});
return Promise.resolve(null);
})
.catch((e: TransportExceptionInterface) => {
@@ -90,6 +111,6 @@ export default <Module<CalendarLocalsState, State>> {
return Promise.resolve(null);
});
}
}
};
},
},
} as Module<CalendarLocalsState, State>;

View File

@@ -1,52 +1,65 @@
import {State} from './../index';
import {ActionContext, Module} from 'vuex';
import {CalendarRange, CalendarRangeCreate, CalendarRangeEdit, isEventInputCalendarRange} from "../../../../types";
import { State } from "./../index";
import { ActionContext, Module } from "vuex";
import {
CalendarRange,
CalendarRangeCreate,
CalendarRangeEdit,
isEventInputCalendarRange,
} from "../../../../types";
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import {fetchCalendarRangeForUser} from '../../../Calendar/api';
import {calendarRangeToFullCalendarEvent} from '../../../Calendar/store/utils';
import {EventInput} from '@fullcalendar/core';
import { fetchCalendarRangeForUser } from "../../../Calendar/api";
import { calendarRangeToFullCalendarEvent } from "../../../Calendar/store/utils";
import { EventInput } from "@fullcalendar/core";
import { makeFetch } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import {
datetimeToISO,
dateToISO,
ISOToDatetime
ISOToDatetime,
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type { EventInputCalendarRange } from "../../../../types";
export interface CalendarRangesState {
ranges: (EventInput | EventInputCalendarRange) [],
rangesLoaded: { start: number, end: number }[],
rangesIndex: Set<string>,
key: number
ranges: (EventInput | EventInputCalendarRange)[];
rangesLoaded: { start: number; end: number }[];
rangesIndex: Set<string>;
key: number;
}
type Context = ActionContext<CalendarRangesState, State>;
export default <Module<CalendarRangesState, State>>{
export default {
namespaced: true,
state: (): CalendarRangesState => ({
ranges: [],
rangesLoaded: [],
rangesIndex: new Set<string>(),
key: 0
key: 0,
}),
getters: {
isRangeLoaded: (state: CalendarRangesState) => ({start, end}: { start: Date, end: Date }): boolean => {
for (let range of state.rangesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) {
isRangeLoaded:
(state: CalendarRangesState) =>
({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.rangesLoaded) {
if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true;
}
}
return false;
},
getRangesOnDate: (state: CalendarRangesState) => (date: Date): EventInputCalendarRange[] => {
getRangesOnDate:
(state: CalendarRangesState) =>
(date: Date): EventInputCalendarRange[] => {
const founds = [];
const dateStr = <string>dateToISO(date);
const dateStr = dateToISO(date) as string;
for (let range of state.ranges) {
if (isEventInputCalendarRange(range)
&& range.start.startsWith(dateStr)
for (const range of state.ranges) {
if (
isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr)
) {
founds.push(range);
}
@@ -54,15 +67,18 @@ export default <Module<CalendarRangesState, State>>{
return founds;
},
getRangesOnWeek: (state: CalendarRangesState) => (mondayDate: Date): EventInputCalendarRange[] => {
getRangesOnWeek:
(state: CalendarRangesState) =>
(mondayDate: Date): EventInputCalendarRange[] => {
const founds = [];
for (let d of Array.from(Array(7).keys())) {
for (const d of Array.from(Array(7).keys())) {
const dateOfWeek = new Date(mondayDate);
dateOfWeek.setDate(mondayDate.getDate() + d);
const dateStr = <string>dateToISO(dateOfWeek);
for (let range of state.ranges) {
if (isEventInputCalendarRange(range)
&& range.start.startsWith(dateStr)
const dateStr = dateToISO(dateOfWeek) as string;
for (const range of state.ranges) {
if (
isEventInputCalendarRange(range) &&
range.start.startsWith(dateStr)
) {
founds.push(range);
}
@@ -75,12 +91,14 @@ export default <Module<CalendarRangesState, State>>{
mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
const toAdd = ranges
.map(cr => calendarRangeToFullCalendarEvent(cr))
.map(cr => ({
...cr, backgroundColor: 'white', borderColor: '#3788d8',
textColor: 'black'
.map((cr) => calendarRangeToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
}))
.filter(r => !state.rangesIndex.has(r.id));
.filter((r) => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
@@ -89,8 +107,9 @@ export default <Module<CalendarRangesState, State>>{
state.key = state.key + toAdd.length;
},
addExternals(state, externalEvents: (EventInput & { id: string })[]) {
const toAdd = externalEvents
.filter(r => !state.rangesIndex.has(r.id));
const toAdd = externalEvents.filter(
(r) => !state.rangesIndex.has(r.id),
);
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
@@ -98,24 +117,43 @@ export default <Module<CalendarRangesState, State>>{
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarRangesState, payload: { start: Date, end: Date }) {
state.rangesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
addLoaded(
state: CalendarRangesState,
payload: { start: Date; end: Date },
) {
state.rangesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
addRange(state: CalendarRangesState, payload: CalendarRange) {
const asEvent = calendarRangeToFullCalendarEvent(payload);
state.ranges.push({...asEvent, backgroundColor: 'white', borderColor: '#3788d8', textColor: 'black'});
state.ranges.push({
...asEvent,
backgroundColor: "white",
borderColor: "#3788d8",
textColor: "black",
});
state.rangesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRange(state: CalendarRangesState, calendarRangeId: number) {
const found = state.ranges.find(r => r.calendarRangeId === calendarRangeId && r.is === "range");
const found = state.ranges.find(
(r) =>
r.calendarRangeId === calendarRangeId && r.is === "range",
);
if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range")
(r) =>
!(
r.calendarRangeId === calendarRangeId &&
r.is === "range"
),
);
if (typeof found.id === "string") { // should always be true
if (typeof found.id === "string") {
// should always be true
state.rangesIndex.delete(found.id);
}
@@ -123,7 +161,9 @@ export default <Module<CalendarRangesState, State>>{
}
},
updateRange(state, range: CalendarRange) {
const found = state.ranges.find(r => r.calendarRangeId === range.id && r.is === "range");
const found = state.ranges.find(
(r) => r.calendarRangeId === range.id && r.is === "range",
);
const newEvent = calendarRangeToFullCalendarEvent(range);
if (found !== undefined) {
@@ -134,14 +174,17 @@ export default <Module<CalendarRangesState, State>>{
}
state.key = state.key + 1;
}
},
},
actions: {
fetchRanges(ctx: Context, payload: { start: Date, end: Date }): Promise<null> {
fetchRanges(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters['me/getMe'] === null) {
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(ctx.getters.getRangeSource);
}
@@ -149,49 +192,58 @@ export default <Module<CalendarRangesState, State>>{
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit('addLoaded', {
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRangeForUser(
ctx.rootGetters['me/getMe'],
ctx.rootGetters["me/getMe"],
start,
end
)
.then((ranges: CalendarRange[]) => {
ctx.commit('addRanges', ranges);
end,
).then((ranges: CalendarRange[]) => {
ctx.commit("addRanges", ranges);
return Promise.resolve(null);
});
},
createRange(ctx, {start, end, location}: { start: Date, end: Date, location: Location }): Promise<null> {
createRange(
ctx,
{
start,
end,
location,
}: { start: Date; end: Date; location: Location },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error('user is currently null');
throw new Error("user is currently null");
}
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user"
type: "user",
},
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end)
datetime: datetimeToISO(end),
},
location: {
id: location.id,
type: "location"
}
type: "location",
},
} as CalendarRangeCreate;
return makeFetch<CalendarRangeCreate, CalendarRange>('POST', url, body)
return makeFetch<CalendarRangeCreate, CalendarRange>(
"POST",
url,
body,
)
.then((newRange) => {
ctx.commit('addRange', newRange);
ctx.commit("addRange", newRange);
return Promise.resolve(null);
})
@@ -199,88 +251,127 @@ export default <Module<CalendarRangesState, State>>{
console.error(error);
throw error;
})
});
},
deleteRange(ctx, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
makeFetch<undefined, never>('DELETE', url)
.then((_) => {
ctx.commit('removeRange', calendarRangeId);
makeFetch<undefined, never>("DELETE", url).then((_) => {
ctx.commit("removeRange", calendarRangeId);
});
},
patchRangeTime(ctx, {calendarRangeId, start, end}: {calendarRangeId: number, start: Date, end: Date}): Promise<null> {
patchRangeTime(
ctx,
{
calendarRangeId,
start,
end,
}: { calendarRangeId: number; start: Date; end: Date },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
startDate: {
datetime: datetimeToISO(start)
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end)
datetime: datetimeToISO(end),
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>('PATCH', url, body)
return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => {
ctx.commit('updateRange', range);
ctx.commit("updateRange", range);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
return Promise.resolve(null);
})
});
},
patchRangeLocation(ctx, {location, calendarRangeId}: {location: Location, calendarRangeId: number}): Promise<null> {
patchRangeLocation(
ctx,
{
location,
calendarRangeId,
}: { location: Location; calendarRangeId: number },
): Promise<null> {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
const body = {
location: {
id: location.id,
type: "location"
type: "location",
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>('PATCH', url, body)
return makeFetch<CalendarRangeEdit, CalendarRange>(
"PATCH",
url,
body,
)
.then((range) => {
ctx.commit('updateRange', range);
ctx.commit("updateRange", range);
return Promise.resolve(null);
})
.catch((error) => {
console.error(error);
return Promise.resolve(null);
})
});
},
copyFromDayToAnotherDay(ctx, {from, to}: {from: Date, to: Date}): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnDate'](from);
copyFromDayToAnotherDay(
ctx,
{ from, to }: { from: Date; to: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnDate"](from);
const promises = [];
for (let r of rangesToCopy) {
let start = new Date(<Date>ISOToDatetime(r.start));
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
let end = new Date(<Date>ISOToDatetime(r.end));
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
start.setFullYear(
to.getFullYear(),
to.getMonth(),
to.getDate(),
);
const end = new Date(ISOToDatetime(r.end) as Date);
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch('createRange', {start, end, location}));
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(_ => Promise.resolve(null));
return Promise.all(promises).then((_) => Promise.resolve(null));
},
copyFromWeekToAnotherWeek(ctx, {fromMonday, toMonday}: {fromMonday: Date, toMonday: Date}): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnWeek'](fromMonday);
copyFromWeekToAnotherWeek(
ctx,
{ fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
): Promise<null> {
const rangesToCopy: EventInputCalendarRange[] =
ctx.getters["getRangesOnWeek"](fromMonday);
const promises = [];
const diffTime = toMonday.getTime() - fromMonday.getTime();
for (let r of rangesToCopy) {
let start = new Date(<Date>ISOToDatetime(r.start));
let end = new Date(<Date>ISOToDatetime(r.end));
for (const r of rangesToCopy) {
const start = new Date(ISOToDatetime(r.start) as Date);
const end = new Date(ISOToDatetime(r.end) as Date);
start.setTime(start.getTime() + diffTime);
end.setTime(end.getTime() + diffTime);
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
const location = ctx.rootGetters["locations/getLocationById"](
r.locationId,
);
promises.push(ctx.dispatch('createRange', {start, end, location}));
promises.push(
ctx.dispatch("createRange", { start, end, location }),
);
}
return Promise.all(promises).then(_ => Promise.resolve(null));
}
}
};
return Promise.all(promises).then((_) => Promise.resolve(null));
},
},
} as Module<CalendarRangesState, State>;

View File

@@ -1,33 +1,38 @@
import {State} from './../index';
import {ActionContext, Module} from 'vuex';
import {CalendarRemote} from '../../../../types';
import {fetchCalendarRemoteForUser} from '../../../Calendar/api';
import {EventInput} from '@fullcalendar/core';
import { State } from "./../index";
import { ActionContext, Module } from "vuex";
import { CalendarRemote } from "../../../../types";
import { fetchCalendarRemoteForUser } from "../../../Calendar/api";
import { EventInput } from "@fullcalendar/core";
import { remoteToFullCalendarEvent } from "../../../Calendar/store/utils";
import { TransportExceptionInterface } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { COLORS } from "../../../Calendar/const";
export interface CalendarRemotesState {
remotes: EventInput[],
remotesLoaded: {start: number, end: number}[],
remotesIndex: Set<string>,
key: number
remotes: EventInput[];
remotesLoaded: { start: number; end: number }[];
remotesIndex: Set<string>;
key: number;
}
type Context = ActionContext<CalendarRemotesState, State>;
export default <Module<CalendarRemotesState, State>> {
export default {
namespaced: true,
state: (): CalendarRemotesState => ({
remotes: [],
remotesLoaded: [],
remotesIndex: new Set<string>(),
key: 0
key: 0,
}),
getters: {
isRemotesLoaded: (state: CalendarRemotesState) => ({start, end}: {start: Date, end: Date}): boolean => {
for (let range of state.remotesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) {
isRemotesLoaded:
(state: CalendarRemotesState) =>
({ start, end }: { start: Date; end: Date }): boolean => {
for (const range of state.remotesLoaded) {
if (
start.getTime() === range.start &&
end.getTime() === range.end
) {
return true;
}
}
@@ -37,11 +42,11 @@ export default <Module<CalendarRemotesState, State>> {
},
mutations: {
addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
console.log('addRemotes', ranges);
console.log("addRemotes", ranges);
const toAdd = ranges
.map(cr => remoteToFullCalendarEvent(cr))
.filter(r => !state.remotesIndex.has(r.id));
.map((cr) => remoteToFullCalendarEvent(cr))
.filter((r) => !state.remotesIndex.has(r.id));
toAdd.forEach((r) => {
state.remotesIndex.add(r.id);
@@ -49,16 +54,25 @@ export default <Module<CalendarRemotesState, State>> {
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarRemotesState, payload: {start: Date, end: Date}) {
state.remotesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
addLoaded(
state: CalendarRemotesState,
payload: { start: Date; end: Date },
) {
state.remotesLoaded.push({
start: payload.start.getTime(),
end: payload.end.getTime(),
});
},
},
actions: {
fetchRemotes(ctx: Context, payload: {start: Date, end: Date}): Promise<null> {
fetchRemotes(
ctx: Context,
payload: { start: Date; end: Date },
): Promise<null> {
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters['me/getMe'] === null) {
if (ctx.rootGetters["me/getMe"] === null) {
return Promise.resolve(null);
}
@@ -66,23 +80,30 @@ export default <Module<CalendarRemotesState, State>> {
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit('addLoaded', {
ctx.commit("addLoaded", {
start: start,
end: end,
});
return fetchCalendarRemoteForUser(
ctx.rootGetters['me/getMe'],
ctx.rootGetters["me/getMe"],
start,
end
end,
)
.then((remotes: CalendarRemote[]) => {
// to be add when reactivity problem will be solve ?
//ctx.commit('addRemotes', remotes);
const inputs = remotes
.map(cr => remoteToFullCalendarEvent(cr))
.map(cr => ({...cr, backgroundColor: COLORS[0], textColor: 'black', editable: false}))
ctx.commit('calendarRanges/addExternals', inputs, {root: true});
.map((cr) => remoteToFullCalendarEvent(cr))
.map((cr) => ({
...cr,
backgroundColor: COLORS[0],
textColor: "black",
editable: false,
}));
ctx.commit("calendarRanges/addExternals", inputs, {
root: true,
});
return Promise.resolve(null);
})
.catch((e: TransportExceptionInterface) => {
@@ -90,6 +111,6 @@ export default <Module<CalendarRemotesState, State>> {
return Promise.resolve(null);
});
}
}
};
},
},
} as Module<CalendarRemotesState, State>;

View File

@@ -1,12 +1,12 @@
import {State} from './../index';
import {ActionContext} from 'vuex';
import { State } from "./../index";
import { ActionContext } from "vuex";
export interface FullCalendarState {
currentView: {
start: Date|null,
end: Date|null,
},
key: number
start: Date | null;
end: Date | null;
};
key: number;
}
type Context = ActionContext<FullCalendarState, State>;
@@ -21,36 +21,58 @@ export default {
key: 0,
}),
mutations: {
setCurrentDatesView: function(state: FullCalendarState, payload: {start: Date, end: Date}): void {
setCurrentDatesView: function (
state: FullCalendarState,
payload: { start: Date; end: Date },
): void {
state.currentView.start = payload.start;
state.currentView.end = payload.end;
},
increaseKey: function (state: FullCalendarState): void {
state.key = state.key + 1;
}
},
},
actions: {
setCurrentDatesView(ctx: Context, {start, end}: {start: Date|null, end: Date|null}): Promise<null> {
console.log('dispatch setCurrentDatesView', {start, end});
setCurrentDatesView(
ctx: Context,
{ start, end }: { start: Date | null; end: Date | null },
): Promise<null> {
console.log("dispatch setCurrentDatesView", { start, end });
if (ctx.state.currentView.start !== start || ctx.state.currentView.end !== end) {
ctx.commit('setCurrentDatesView', {start, end});
if (
ctx.state.currentView.start !== start ||
ctx.state.currentView.end !== end
) {
ctx.commit("setCurrentDatesView", { start, end });
}
if (start !== null && end !== null) {
return Promise.all([
ctx.dispatch('calendarRanges/fetchRanges', {start, end}, {root: true}).then(_ => Promise.resolve(null)),
ctx.dispatch('calendarRemotes/fetchRemotes', {start, end}, {root: true}).then(_ => Promise.resolve(null)),
ctx.dispatch('calendarLocals/fetchLocals', {start, end}, {root: true}).then(_ => Promise.resolve(null))
]
).then(_ => Promise.resolve(null));
ctx
.dispatch(
"calendarRanges/fetchRanges",
{ start, end },
{ root: true },
)
.then((_) => Promise.resolve(null)),
ctx
.dispatch(
"calendarRemotes/fetchRemotes",
{ start, end },
{ root: true },
)
.then((_) => Promise.resolve(null)),
ctx
.dispatch(
"calendarLocals/fetchLocals",
{ start, end },
{ root: true },
)
.then((_) => Promise.resolve(null)),
]).then((_) => Promise.resolve(null));
} else {
return Promise.resolve(null);
}
},
}
}
},
};

View File

@@ -1,6 +1,6 @@
import { Location } from "../../../../../../../ChillMainBundle/Resources/public/types";
import {State} from '../index';
import {Module} from 'vuex';
import { State } from "../index";
import { Module } from "vuex";
import { getLocations } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/locations";
import { whereami } from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user";
@@ -10,18 +10,20 @@ export interface LocationState {
currentLocation: Location | null;
}
export default <Module<LocationState, State>>{
export default {
namespaced: true,
state: (): LocationState => {
return {
locations: [],
locationPicked: null,
currentLocation: null,
}
};
},
getters: {
getLocationById: (state) => (id: number): Location|undefined => {
return state.locations.find(l => l.id === id);
getLocationById:
(state) =>
(id: number): Location | undefined => {
return state.locations.find((l) => l.id === id);
},
},
mutations: {
@@ -34,7 +36,8 @@ export default <Module<LocationState, State>>{
return;
}
state.locationPicked = state.locations.find(l => l.id === location.id) || null;
state.locationPicked =
state.locations.find((l) => l.id === location.id) || null;
},
setCurrentLocation(state, location: Location | null): void {
if (null === location) {
@@ -42,21 +45,21 @@ export default <Module<LocationState, State>>{
return;
}
state.currentLocation = state.locations.find(l => l.id === location.id) || null;
}
state.currentLocation =
state.locations.find((l) => l.id === location.id) || null;
},
},
actions: {
getLocations(ctx): Promise<void> {
return getLocations().then(locations => {
ctx.commit('setLocations', locations);
return getLocations().then((locations) => {
ctx.commit("setLocations", locations);
return Promise.resolve();
});
},
getCurrentLocation(ctx): Promise<void> {
return whereami().then(location => {
ctx.commit('setCurrentLocation', location);
})
}
}
}
return whereami().then((location) => {
ctx.commit("setCurrentLocation", location);
});
},
},
} as Module<LocationState, State>;

View File

@@ -1,9 +1,9 @@
import {State} from './../index';
import {User} from '../../../../../../../ChillMainBundle/Resources/public/types';
import {ActionContext} from 'vuex';
import { State } from "./../index";
import { User } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { ActionContext } from "vuex";
export interface MeState {
me: User|null,
me: User | null;
}
type Context = ActionContext<MeState, State>;
@@ -22,8 +22,5 @@ export default {
setWhoAmi(state: MeState, me: User) {
state.me = me;
},
}
},
};

View File

@@ -6,20 +6,22 @@
const fetchCalendarRanges = () => {
return Promise.resolve([]);
const url = `/api/1.0/calendar/calendar-range-available.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
const fetchCalendarRangesByUser = (userId) => {
return Promise.resolve([]);
const url = `/api/1.0/calendar/calendar-range-available.json?user=${userId}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
@@ -31,14 +33,14 @@ const fetchCalendarRangesByUser = (userId) => {
const fetchCalendar = (mainUserId) => {
return Promise.resolve([]);
const url = `/api/1.0/calendar/calendar.json?main_user=${mainUserId}&item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
/*
* Endpoint chill_api_single_calendar_range__entity_create
* method POST, post CalendarRange entity
@@ -46,14 +48,16 @@ const fetchCalendar = (mainUserId) => {
const postCalendarRange = (body) => {
const url = `/api/1.0/calendar/calendar-range.json?`;
return fetch(url, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json;charset=utf-8'
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
@@ -62,17 +66,19 @@ const postCalendarRange = (body) => {
* method PATCH, patch CalendarRange entity
*/
const patchCalendarRange = (id, body) => {
console.log(body)
console.log(body);
const url = `/api/1.0/calendar/calendar-range/${id}.json`;
return fetch(url, {
method: 'PATCH',
method: "PATCH",
headers: {
'Content-Type': 'application/json;charset=utf-8'
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
@@ -83,13 +89,15 @@ const patchCalendarRange = (id, body) => {
const deleteCalendarRange = (id) => {
const url = `/api/1.0/calendar/calendar-range/${id}.json`;
return fetch(url, {
method: 'DELETE',
method: "DELETE",
headers: {
'Content-Type': 'application/json;charset=utf-8'
"Content-Type": "application/json;charset=utf-8",
},
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
@@ -99,5 +107,5 @@ export {
fetchCalendarRangesByUser,
postCalendarRange,
patchCalendarRange,
deleteCalendarRange
deleteCalendarRange,
};

View File

@@ -1,6 +1,8 @@
<template>
<div>
<h2 class="chill-red">{{ $t('choose_your_calendar_user') }}</h2>
<h2 class="chill-red">
{{ $t("choose_your_calendar_user") }}
</h2>
<VueMultiselect
name="field"
id="calendarUserSelector"
@@ -19,50 +21,70 @@
@select="selectUsers"
@remove="unSelectUsers"
@close="coloriseSelectedValues"
:options="options">
</VueMultiselect>
:options="options"
/>
</div>
<div class="form-check">
<input type="checkbox" id="myCalendar" class="form-check-input" v-model="showMyCalendarWidget" />
<label class="form-check-label" for="myCalendar">{{ $t('show_my_calendar') }}</label>
<input
type="checkbox"
id="myCalendar"
class="form-check-input"
v-model="showMyCalendarWidget"
/>
<label class="form-check-label" for="myCalendar">{{
$t("show_my_calendar")
}}</label>
</div>
<div class="form-check">
<input type="checkbox" id="weekends" class="form-check-input" @click="toggleWeekends" />
<label class="form-check-label" for="weekends">{{ $t('show_weekends') }}</label>
<input
type="checkbox"
id="weekends"
class="form-check-input"
@click="toggleWeekends"
/>
<label class="form-check-label" for="weekends">{{
$t("show_weekends")
}}</label>
</div>
</template>
<script>
import { fetchCalendarRanges, fetchCalendar } from "../../_api/api";
import VueMultiselect from "vue-multiselect";
import { whoami } from "ChillPersonAssets/vuejs/AccompanyingCourse/api";
import { fetchCalendarRanges, fetchCalendar } from '../../_api/api'
import VueMultiselect from 'vue-multiselect';
import { whoami } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api';
const COLORS = [ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
'#8dd3c7',
'#ffffb3',
'#bebada',
'#fb8072',
'#80b1d3',
'#fdb462',
'#b3de69',
'#fccde5',
'#d9d9d9',
'#bc80bd',
'#ccebc5',
'#ffed6f'
const COLORS = [
/* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
"#8dd3c7",
"#ffffb3",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#b3de69",
"#fccde5",
"#d9d9d9",
"#bc80bd",
"#ccebc5",
"#ffed6f",
];
export default {
name: 'CalendarUserSelector',
name: "CalendarUserSelector",
components: { VueMultiselect },
props: ['users', 'updateEventsSource', 'calendarEvents', 'showMyCalendar', 'toggleMyCalendar', 'toggleWeekends'],
props: [
"users",
"updateEventsSource",
"calendarEvents",
"showMyCalendar",
"toggleMyCalendar",
"toggleWeekends",
],
data() {
return {
errorMsg: [],
value: [],
options: []
}
options: [],
};
},
computed: {
showMyCalendarWidget: {
@@ -72,86 +94,113 @@ export default {
},
get() {
return this.showMyCalendar;
}
}
},
},
},
methods: {
init() {
this.fetchData()
this.fetchData();
},
fetchData() {
fetchCalendarRanges().then(calendarRanges => new Promise((resolve, reject) => {
fetchCalendarRanges()
.then(
(calendarRanges) =>
new Promise((resolve, reject) => {
let results = calendarRanges.results;
let users = [];
results.forEach(i => {
if (!(users.some(j => i.user.id === j.id))){
let ratio = Math.floor(users.length / COLORS.length);
let colorIndex = users.length - ratio * COLORS.length;
results.forEach((i) => {
if (!users.some((j) => i.user.id === j.id)) {
let ratio = Math.floor(
users.length / COLORS.length,
);
let colorIndex =
users.length - ratio * COLORS.length;
users.push({
id: i.user.id,
username: i.user.username,
color: COLORS[colorIndex]
})
color: COLORS[colorIndex],
});
}
});
let calendarEvents = [];
users.forEach(u => {
let arr = results.filter(i => i.user.id === u.id).map(i =>
({
users.forEach((u) => {
let arr = results
.filter((i) => i.user.id === u.id)
.map((i) => ({
start: i.startDate.datetime,
end: i.endDate.datetime,
calendarRangeId: i.id,
sourceColor: u.color
sourceColor: u.color,
//display: 'background' // can be an option for the disponibility
})
);
}));
calendarEvents.push({
events: arr,
color: u.color,
textColor: '#444444',
textColor: "#444444",
editable: false,
id: u.id
})
})
id: u.id,
});
});
this.users.loaded = users;
this.options = users;
this.calendarEvents.loaded = calendarEvents;
whoami().then(me => new Promise((resolve, reject) => {
whoami().then(
(me) =>
new Promise((resolve, reject) => {
this.users.logged = me;
let currentUser = users.find(u => u.id === me.id);
let currentUser = users.find(
(u) => u.id === me.id,
);
this.value = currentUser;
fetchCalendar(currentUser.id).then(calendar => new Promise((resolve, reject) => {
let results = calendar.results;
let events = results.map(i =>
({
start: i.startDate.datetime,
end: i.endDate.datetime,
})
fetchCalendar(currentUser.id).then(
(calendar) =>
new Promise(
(resolve, reject) => {
let results =
calendar.results;
let events =
results.map(
(i) => ({
start: i
.startDate
.datetime,
end: i
.endDate
.datetime,
}),
);
let calendarEventsCurrentUser = {
let calendarEventsCurrentUser =
{
events: events,
color: 'darkblue',
color: "darkblue",
id: 1000,
editable: false
editable: false,
};
this.calendarEvents.user = calendarEventsCurrentUser;
this.calendarEvents.user =
calendarEventsCurrentUser;
this.selectUsers(currentUser);
this.selectUsers(
currentUser,
);
resolve();
}));
},
),
);
resolve();
}));
}),
);
resolve();
}))
}),
)
.catch((error) => {
this.errorMsg.push(error.message);
});
@@ -160,29 +209,32 @@ export default {
return `${value.username}`;
},
coloriseSelectedValues() {
let tags = document.querySelectorAll('div.multiselect__tags-wrap')[0];
let tags = document.querySelectorAll(
"div.multiselect__tags-wrap",
)[0];
if (tags.hasChildNodes()) {
let children = tags.childNodes;
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (child.nodeType === Node.ELEMENT_NODE) {
this.users.selected.forEach(u => {
this.users.selected.forEach((u) => {
if (child.hasChildNodes()) {
if (child.firstChild.innerText == u.username) {
child.style.background = u.color;
child.firstChild.style.color = '#444444';
child.firstChild.style.color = "#444444";
}
}
}
)
});
}
}
}
},
selectEvents() {
let selectedUsersId = this.users.selected.map(a => a.id);
this.calendarEvents.selected = this.calendarEvents.loaded.filter(a => selectedUsersId.includes(a.id));
let selectedUsersId = this.users.selected.map((a) => a.id);
this.calendarEvents.selected = this.calendarEvents.loaded.filter(
(a) => selectedUsersId.includes(a.id),
);
},
selectUsers(value) {
this.users.selected.push(value);
@@ -191,16 +243,17 @@ export default {
this.updateEventsSource();
},
unSelectUsers(value) {
this.users.selected = this.users.selected.filter(a => a.id != value.id);
this.users.selected = this.users.selected.filter(
(a) => a.id != value.id,
);
this.selectEvents();
this.updateEventsSource();
}
},
},
mounted() {
this.init();
}
}
},
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,17 +1,14 @@
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n'
import { multiSelectMessages } from "ChillMainAssets/vuejs/_js/i18n";
const calendarUserSelectorMessages = {
fr: {
choose_your_calendar_user: "Afficher les plages de disponibilités",
select_user: "Sélectionnez des calendriers",
show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends"
}
show_weekends: "Afficher les week-ends",
},
};
Object.assign(calendarUserSelectorMessages.fr, multiSelectMessages.fr);
export {
calendarUserSelectorMessages
};
export { calendarUserSelectorMessages };

View File

@@ -1,14 +1,25 @@
// this file loads all assets from the Chill calendar bundle
module.exports = function (encore, entries) {
entries.push(__dirname + '/Resources/public/chill/chill.js');
entries.push(__dirname + "/Resources/public/chill/chill.js");
encore.addAliases({
ChillCalendarAssets: __dirname + '/Resources/public'
ChillCalendarAssets: __dirname + "/Resources/public",
});
encore.addEntry('vue_calendar', __dirname + '/Resources/public/vuejs/Calendar/index.js');
encore.addEntry('vue_mycalendarrange', __dirname + '/Resources/public/vuejs/MyCalendarRange/index2.ts');
encore.addEntry('page_calendar', __dirname + '/Resources/public/chill/index.js');
encore.addEntry('mod_answer', __dirname + '/Resources/public/module/Invite/answer.js');
encore.addEntry(
"vue_calendar",
__dirname + "/Resources/public/vuejs/Calendar/index.js",
);
encore.addEntry(
"vue_mycalendarrange",
__dirname + "/Resources/public/vuejs/MyCalendarRange/index2.ts",
);
encore.addEntry(
"page_calendar",
__dirname + "/Resources/public/chill/index.js",
);
encore.addEntry(
"mod_answer",
__dirname + "/Resources/public/module/Invite/answer.js",
);
};

View File

@@ -298,7 +298,7 @@ class CustomFieldsGroupController extends AbstractController
->setCustomFieldsGroup($customFieldsGroup);
$builder = $this->get('form.factory')
->createNamedBuilder(null, FormType::class, $customfield, [
->createNamedBuilder('', FormType::class, $customfield, [
'method' => 'GET',
'action' => $this->generateUrl('customfield_new'),
'csrf_protection' => false,

View File

@@ -3,8 +3,6 @@ import { fetchResults } from "ChillMainAssets/lib/api/apiMethods.ts";
const fetchTemplates = (entityClass) => {
let fqdnEntityClass = encodeURI(entityClass);
return fetchResults(`/api/1.0/docgen/templates/by-entity/${fqdnEntityClass}`);
}
export {
fetchTemplates
};
export { fetchTemplates };

View File

@@ -1,12 +1,11 @@
const buildLink = function (templateId, entityId, entityClass) {
const
entityIdEncoded = encodeURI(entityId),
returnPath = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash),
const entityIdEncoded = encodeURI(entityId),
returnPath = encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash,
),
entityClassEncoded = encodeURI(entityClass),
url = `/fr/doc/gen/generate/from/${templateId}/for/${entityClassEncoded}/${entityIdEncoded}?returnPath=${returnPath}`
;
console.log('computed Url');
url = `/fr/doc/gen/generate/from/${templateId}/for/${entityClassEncoded}/${entityIdEncoded}?returnPath=${returnPath}`;
console.log("computed Url");
return url;
};

View File

@@ -1,15 +1,15 @@
import {createApp} from 'vue';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {fetchTemplates} from 'ChillDocGeneratorAssets/api/pickTemplate.js';
import {_createI18n} from 'ChillMainAssets/vuejs/_js/i18n';
import { createApp } from "vue";
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
const i18n = _createI18n({});
document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => {
fetchTemplates(el.dataset.entityClass).then(templates => {
let
picker = {
template: '<pick-template :templates="this.templates" :entityId="this.entityId"></pick-template>',
document.querySelectorAll("div[data-docgen-template-picker]").forEach((el) => {
fetchTemplates(el.dataset.entityClass).then((templates) => {
let picker = {
template:
'<pick-template :templates="this.templates" :entityId="this.entityId"></pick-template>',
components: {
PickTemplate,
},
@@ -17,11 +17,9 @@ document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => {
return {
templates: templates,
entityId: el.dataset.entityId,
}
};
},
}
;
};
createApp(picker).use(i18n).mount(el);
})
});
});

View File

@@ -2,26 +2,44 @@
<div>
<template v-if="templates.length > 0">
<slot name="title">
<h2>{{ $t('generate_document')}}</h2>
<h2>{{ $t("generate_document") }}</h2>
</slot>
<div class="container">
<div class="row">
<div class="col-md-4">
<slot name="label">
<label>{{ $t('select_a_template') }}</label>
<label>{{ $t("select_a_template") }}</label>
</slot>
</div>
<div class="col-md-8">
<div class="input-group mb-3">
<select class="form-select" v-model="template">
<option disabled selected value="">{{ $t('choose_a_template') }}</option>
<option disabled selected value="">
{{ $t("choose_a_template") }}
</option>
<template v-for="t in templates" :key="t.id">
<option :value="t.id" >{{ t.name.fr || 'Aucun nom défini' }}</option>
<option :value="t.id">
{{ t.name.fr || "Aucun nom défini" }}
</option>
</template>
</select>
<a v-if="canGenerate" class="btn btn-update btn-sm change-icon" :href="buildUrlGenerate" @click.prevent="clickGenerate($event, buildUrlGenerate)"><i class="fa fa-fw fa-cog"></i></a>
<a v-else class="btn btn-update btn-sm change-icon" href="#" disabled ><i class="fa fa-fw fa-cog"></i></a>
<a
v-if="canGenerate"
class="btn btn-update btn-sm change-icon"
:href="buildUrlGenerate"
@click.prevent="
clickGenerate($event, buildUrlGenerate)
"
><i class="fa fa-fw fa-cog"
/></a>
<a
v-else
class="btn btn-update btn-sm change-icon"
href="#"
disabled
><i class="fa fa-fw fa-cog"
/></a>
</div>
</div>
</div>
@@ -30,16 +48,13 @@
<p>{{ getDescription }}</p>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
export default {
name: "PickTemplate",
@@ -57,13 +72,13 @@ export default {
type: Boolean,
required: false,
default: false,
}
},
emits: ['goToGenerateDocument'],
},
emits: ["goToGenerateDocument"],
data() {
return {
template: null,
}
};
},
computed: {
canGenerate() {
@@ -78,21 +93,21 @@ export default {
},
getDescription() {
if (null === this.template) {
return '';
return "";
}
let desc = this.templates.find(t => t.id === this.template);
let desc = this.templates.find((t) => t.id === this.template);
if (null === desc) {
return '';
return "";
}
return desc.description || '';
return desc.description || "";
},
buildUrlGenerate() {
if (null === this.template) {
return '#';
return "#";
}
return buildLink(this.template, this.entityId, this.entityClass);
}
},
},
methods: {
clickGenerate(event, link) {
@@ -100,21 +115,23 @@ export default {
window.location.assign(link);
}
this.$emit('goToGenerateDocument', {event, link, template: this.template});
this.$emit("goToGenerateDocument", {
event,
link,
template: this.template,
});
},
},
i18n: {
messages: {
fr: {
generate_document: 'Générer un document',
select_a_template: 'Choisir un modèle',
choose_a_template: 'Choisir',
}
}
}
}
generate_document: "Générer un document",
select_a_template: "Choisir un modèle",
choose_a_template: "Choisir",
},
},
},
};
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,8 +1,11 @@
// this file loads all assets from the Chill DocGenerator bundle
module.exports = function (encore, entries) {
encore.addAliases({
ChillDocGeneratorAssets: __dirname + '/Resources/public'
ChillDocGeneratorAssets: __dirname + "/Resources/public",
});
encore.addEntry('mod_docgen_picktemplate', __dirname + '/Resources/public/module/PickTemplate/index.js');
encore.addEntry(
"mod_docgen_picktemplate",
__dirname + "/Resources/public/module/PickTemplate/index.js",
);
};

View File

@@ -1,18 +1,19 @@
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { PostStoreObjectSignature, StoredObject } from "../../types";
const algo = 'AES-CBC';
const algo = "AES-CBC";
const URL_POST = '/asyncupload/temp_url/generate/post';
const URL_POST = "/asyncupload/temp_url/generate/post";
const keyDefinition = {
name: algo,
length: 256
length: 256,
};
const createFilename = (): string => {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let text = "";
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 7; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
@@ -29,14 +30,22 @@ const createFilename = (): string => {
* @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject.
*/
export const fetchNewStoredObject = async (): Promise<StoredObject> => {
return makeFetch("POST", '/api/1.0/doc-store/stored-object/create', null);
}
return makeFetch("POST", "/api/1.0/doc-store/stored-object/create", null);
};
export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: StoredObject): Promise<string> => {
export const uploadVersion = async (
uploadFile: ArrayBuffer,
storedObject: StoredObject,
): Promise<string> => {
const params = new URLSearchParams();
params.append('expires_delay', "180");
params.append('submit_delay', "180");
const asyncData: PostStoreObjectSignature = await makeFetch("GET", `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + "?" + params.toString());
params.append("expires_delay", "180");
params.append("submit_delay", "180");
const asyncData: PostStoreObjectSignature = await makeFetch(
"GET",
`/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` +
"?" +
params.toString(),
);
const suffix = createFilename();
const filename = asyncData.prefix + suffix;
const formData = new FormData();
@@ -50,7 +59,7 @@ export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: Store
const response = await window.fetch(asyncData.url, {
method: "POST",
body: formData,
})
});
if (!response.ok) {
console.error("Error while sending file to store", response);
@@ -58,13 +67,22 @@ export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: Store
}
return Promise.resolve(filename);
}
};
export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
export const encryptFile = async (
originalFile: ArrayBuffer,
): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
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);
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]);
};

View File

@@ -1,10 +1,9 @@
var mime = require('mime');
var mime = require("mime");
var algo = 'AES-CBC';
var algo = "AES-CBC";
var initializeButtons = (root) => {
var
buttons = root.querySelectorAll('a[data-download-button]');
var buttons = root.querySelectorAll("a[data-download-button]");
for (let i = 0; i < buttons.length; i++) {
initialize(buttons[i]);
@@ -12,18 +11,17 @@ var initializeButtons = (root) => {
};
var initialize = (button) => {
button.addEventListener('click', onClick);
button.addEventListener("click", onClick);
};
var onClick = e => download(e.target);
var onClick = (e) => download(e.target);
var download = (button) => {
var
keyData = JSON.parse(button.dataset.key),
var keyData = JSON.parse(button.dataset.key),
ivData = JSON.parse(button.dataset.iv),
iv = new Uint8Array(ivData),
urlGenerator = button.dataset.tempUrlGetGenerator,
hasFilename = 'filename' in button.dataset,
hasFilename = "filename" in button.dataset,
filename = button.dataset.filename,
labelPreparing = button.dataset.labelPreparing,
labelReady = button.dataset.labelReady,
@@ -31,65 +29,70 @@ var download = (button) => {
extension = mime.getExtension(mimeType),
decryptError = "Error while decrypting file",
fetchError = "Error while fetching file",
key, url
;
key,
url;
button.textContent = labelPreparing;
window.fetch(urlGenerator)
window
.fetch(urlGenerator)
.then((r) => {
if (r.ok) {
return r.json();
} else {
throw new Error("error while downloading url " + r.status + " " + r.statusText);
throw new Error(
"error while downloading url " + r.status + " " + r.statusText,
);
}
})
.then(data => {
.then((data) => {
return window.fetch(data.url);
})
.then(response => {
.then((response) => {
if (response.ok) {
return response.arrayBuffer();
}
throw new Error(response.status + response.statusText);
})
.then(buffer => {
.then((buffer) => {
if (keyData.alg !== undefined) {
return window.crypto.subtle
.importKey('jwk', keyData, { name: algo, iv: iv}, false, ['decrypt'])
.then(key => {
return window.crypto.subtle.decrypt({ name: algo, iv: iv }, key, buffer);
.importKey("jwk", keyData, { name: algo, iv: iv }, false, ["decrypt"])
.then((key) => {
return window.crypto.subtle.decrypt(
{ name: algo, iv: iv },
key,
buffer,
);
});
}
return Promise.resolve(buffer);
})
.then(decrypted => {
var
blob = new Blob([decrypted], { type: mimeType }),
url = window.URL.createObjectURL(blob)
;
.then((decrypted) => {
var blob = new Blob([decrypted], { type: mimeType }),
url = window.URL.createObjectURL(blob);
button.href = url;
button.target = '_blank';
button.target = "_blank";
button.type = mimeType;
button.textContent = labelReady;
if (hasFilename) {
button.download = filename;
if (extension !== false) {
button.download = button.download + '.' + extension;
button.download = button.download + "." + extension;
}
}
button.removeEventListener('click', onClick);
button.removeEventListener("click", onClick);
button.click();
})
.catch(error => {
.catch((error) => {
button.textContent = "";
button.appendChild(document.createTextNode("error while handling decrypted file"));
})
;
button.appendChild(
document.createTextNode("error while handling decrypted file"),
);
});
};
window.addEventListener('load', function(e) {
window.addEventListener("load", function (e) {
initializeButtons(e.target);
});

View File

@@ -1,2 +1,2 @@
require('./uploader.js');
require('./downloader.js');
require("./uploader.js");
require("./downloader.js");

View File

@@ -1,15 +1,19 @@
import { CollectionEventPayload } from "../../../../../ChillMainBundle/Resources/public/module/collection";
import { createApp } from "vue";
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue"
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue";
import { StoredObject, StoredObjectVersion } 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]");
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');
throw new Error("input to stored object not found");
}
let existingDoc: StoredObject | null = null;
@@ -20,69 +24,87 @@ const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElemen
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>',
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({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void {
console.log('object added', stored_object);
console.log('version added', stored_object_version);
addDocument: function ({
stored_object,
stored_object_version,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
}): void {
console.log("object added", stored_object);
console.log("version added", stored_object_version);
this.$data.existingDoc = stored_object;
this.$data.existingDoc.currentVersion = stored_object_version;
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
input_stored_object.value = JSON.stringify(
this.$data.existingDoc,
);
},
removeDocument: function (object: StoredObject): void {
console.log('catch remove document', object);
console.log("catch remove document", object);
input_stored_object.value = "";
this.$data.existingDoc = undefined;
console.log('collectionEntry', collectionEntry);
console.log("collectionEntry", collectionEntry);
if (null !== collectionEntry) {
console.log('will remove collection');
console.log("will remove collection");
collectionEntry.remove();
}
}
}
},
},
});
app.use(i18n).mount(app_container);
}
window.addEventListener('collection-add-entry', ((e: CustomEvent<CollectionEventPayload>) => {
};
window.addEventListener("collection-add-entry", ((
e: CustomEvent<CollectionEventPayload>,
) => {
const detail = e.detail;
const divElement: null|HTMLDivElement = detail.entry.querySelector('div[data-stored-object]');
const divElement: null | HTMLDivElement = detail.entry.querySelector(
"div[data-stored-object]",
);
if (null === divElement) {
throw new Error('div[data-stored-object] not found');
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]');
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);
const parent = input.parentElement;
console.log("parent", parent);
if (null !== parent) {
let grandParent = parent.parentElement;
console.log('grandParent', grandParent);
const grandParent = parent.parentElement;
console.log("grandParent", grandParent);
if (null !== grandParent) {
if (grandParent.tagName.toLowerCase() === 'li' && grandParent.classList.contains('entry')) {
if (
grandParent.tagName.toLowerCase() === "li" &&
grandParent.classList.contains("entry")
) {
collectionEntry = grandParent as HTMLLIElement;
}
}
}
startApp(input, collectionEntry);
})
});
});
export {}
export {};

View File

@@ -1,7 +1,6 @@
var algo = 'AES-CBC';
import Dropzone from 'dropzone';
import { initializeButtons } from './downloader.js';
var algo = "AES-CBC";
import Dropzone from "dropzone";
import { initializeButtons } from "./downloader.js";
/**
*
@@ -23,8 +22,8 @@ import { initializeButtons } from './downloader.js';
// load css
//require('dropzone/dist/basic.css');
require('dropzone/dist/dropzone.css');
require('./index.scss');
require("dropzone/dist/dropzone.css");
require("./index.scss");
//
// disable dropzone autodiscover
@@ -32,22 +31,21 @@ Dropzone.autoDiscover = false;
var keyDefinition = {
name: algo,
length: 256
length: 256,
};
var searchForZones = function (root) {
var zones = root.querySelectorAll('div[data-stored-object]');
var zones = root.querySelectorAll("div[data-stored-object]");
for (let i = 0; i < zones.length; i++) {
initialize(zones[i]);
}
};
var getUploadUrl = function (zoneData, files) {
var
generateTempUrlPost = zoneData.zone.querySelector('input[data-async-file-upload]').dataset.generateTempUrlPost,
oReq = new XMLHttpRequest()
;
var generateTempUrlPost = zoneData.zone.querySelector(
"input[data-async-file-upload]",
).dataset.generateTempUrlPost,
oReq = new XMLHttpRequest();
// arg, dropzone, you cannot handle async upload...
oReq.open("GET", generateTempUrlPost, false);
oReq.send();
@@ -62,32 +60,37 @@ var getUploadUrl = function(zoneData, files) {
};
var encryptFile = function (originalFile, zoneData, done) {
var
iv = crypto.getRandomValues(new Uint8Array(16)),
var iv = crypto.getRandomValues(new Uint8Array(16)),
reader = new FileReader(),
jsKey, rawKey
;
jsKey,
rawKey;
zoneData.originalType = originalFile.type;
reader.onload = e => {
window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ])
.then(key => {
reader.onload = (e) => {
window.crypto.subtle
.generateKey(keyDefinition, true, ["encrypt", "decrypt"])
.then((key) => {
jsKey = key;
// we register the key somwhere
return window.crypto.subtle.exportKey('jwk', key);
}).then(exportedKey => {
return window.crypto.subtle.exportKey("jwk", key);
})
.then((exportedKey) => {
rawKey = exportedKey;
// we start encryption
return window.crypto.subtle.encrypt({ name: algo, iv: iv}, jsKey, e.target.result);
return window.crypto.subtle.encrypt(
{ name: algo, iv: iv },
jsKey,
e.target.result,
);
})
.then(encrypted => {
.then((encrypted) => {
zoneData.crypto = {
jsKey: jsKey,
rawKey: rawKey,
iv: iv
iv: iv,
};
done(new File([encrypted], zoneData.suffix));
@@ -98,12 +101,11 @@ var encryptFile = function(originalFile, zoneData, done) {
};
var addBelowButton = (btn, zone, zoneData) => {
let
belowZone = zone.querySelector('.chill-dropzone__below-zone');
let belowZone = zone.querySelector(".chill-dropzone__below-zone");
if (belowZone === null) {
belowZone = document.createElement('div');
belowZone.classList.add('chill-dropzone__below-zone');
belowZone = document.createElement("div");
belowZone.classList.add("chill-dropzone__below-zone");
zone.appendChild(belowZone);
}
@@ -111,14 +113,13 @@ var addBelowButton = (btn, zone, zoneData) => {
};
var createZone = (zone, zoneData) => {
var
created = document.createElement('div'),
initMessage = document.createElement('div'),
var created = document.createElement("div"),
initMessage = document.createElement("div"),
initContent = zone.dataset.labelInitMessage,
dropzoneI;
created.classList.add('dropzone');
initMessage.classList.add('dz-message');
created.classList.add("dropzone");
initMessage.classList.add("dz-message");
initMessage.appendChild(document.createTextNode(initContent));
console.log(Dropzone);
dropzoneI = new Dropzone(created, {
@@ -139,7 +140,7 @@ var createZone = (zone, zoneData) => {
},
renameFile: function (file) {
return zoneData.suffix;
}
},
});
dropzoneI.on("sending", function (file, xhr, formData) {
@@ -156,7 +157,7 @@ var createZone = (zone, zoneData) => {
});
dropzoneI.on("addedfile", function (file) {
if (zoneData.hasOwnProperty('currentFile')) {
if (zoneData.hasOwnProperty("currentFile")) {
dropzoneI.removeFile(zoneData.currentFile);
}
});
@@ -170,19 +171,20 @@ var createZone = (zone, zoneData) => {
let event = new CustomEvent("chill_dropzone_initialized", {
detail: {
dropzone: dropzoneI,
zoneData: zoneData
}
zoneData: zoneData,
},
});
window.dispatchEvent(event);
};
var initialize = function (zone) {
var
allowRemove = zone.dataset.allowRemove,
zoneData = { zone: zone, suffix: createFilename(), allowRemove: allowRemove, old: null }
;
var allowRemove = zone.dataset.allowRemove,
zoneData = {
zone: zone,
suffix: createFilename(),
allowRemove: allowRemove,
old: null,
};
if (hasDataInForm(zone, zoneData)) {
insertRemoveButton(zone, zoneData);
insertDownloadButton(zone, zoneData);
@@ -193,7 +195,8 @@ var initialize = function(zone) {
var createFilename = () => {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 7; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
@@ -203,13 +206,10 @@ var createFilename = () => {
};
var storeDataInForm = (zone, zoneData) => {
var
inputKey = zone.querySelector('input[data-stored-object-key]'),
inputIv = zone.querySelector('input[data-stored-object-iv]'),
inputObject = zone.querySelector('input[data-async-file-upload]'),
inputType = zone.querySelector('input[data-async-file-type]')
;
var inputKey = zone.querySelector("input[data-stored-object-key]"),
inputIv = zone.querySelector("input[data-stored-object-iv]"),
inputObject = zone.querySelector("input[data-async-file-upload]"),
inputType = zone.querySelector("input[data-async-file-type]");
inputKey.value = JSON.stringify(zoneData.crypto.rawKey);
inputIv.value = JSON.stringify(Array.from(zoneData.crypto.iv));
inputType.value = zoneData.originalType;
@@ -219,15 +219,12 @@ var storeDataInForm = (zone, zoneData) => {
};
const restoreDataInForm = (zone, zoneData) => {
var
inputKey = zone.querySelector('input[data-stored-object-key]'),
inputIv = zone.querySelector('input[data-stored-object-iv]'),
inputObject = zone.querySelector('input[data-async-file-upload]'),
inputType = zone.querySelector('input[data-async-file-type]')
;
var inputKey = zone.querySelector("input[data-stored-object-key]"),
inputIv = zone.querySelector("input[data-stored-object-iv]"),
inputObject = zone.querySelector("input[data-async-file-upload]"),
inputType = zone.querySelector("input[data-async-file-type]");
if (zoneData.old === null) {
console.log('should not have restored data');
console.log("should not have restored data");
return;
}
@@ -240,27 +237,21 @@ const restoreDataInForm = (zone, zoneData) => {
};
const hasDataInForm = (zone, zoneData) => {
var
inputObject = zone.querySelector('input[data-async-file-upload]')
;
var inputObject = zone.querySelector("input[data-async-file-upload]");
return inputObject.value.length > 0;
};
var removeDataInForm = (zone, zoneData) => {
var
inputKey = zone.querySelector('input[data-stored-object-key]'),
inputIv = zone.querySelector('input[data-stored-object-iv]'),
inputObject = zone.querySelector('input[data-async-file-upload]'),
inputType = zone.querySelector('input[data-async-file-type]')
;
var inputKey = zone.querySelector("input[data-stored-object-key]"),
inputIv = zone.querySelector("input[data-stored-object-iv]"),
inputObject = zone.querySelector("input[data-async-file-upload]"),
inputType = zone.querySelector("input[data-async-file-type]");
// store data for future usage
zoneData.old = {
key: inputKey.value,
iv: inputIv.value,
obj: inputObject.value,
type: inputType.value
type: inputType.value,
};
// set blank values
inputKey.value = "";
@@ -272,30 +263,27 @@ var removeDataInForm = (zone, zoneData) => {
};
var insertRemoveButton = (zone, zoneData) => {
var
removeButton = document.createElement('a'),
cancelButton = document.createElement('a'),
var removeButton = document.createElement("a"),
cancelButton = document.createElement("a"),
labelRemove = zone.dataset.dictRemove,
labelCancel = 'Restaurer'
;
removeButton.classList.add('btn', 'btn-delete');
labelCancel = "Restaurer";
removeButton.classList.add("btn", "btn-delete");
removeButton.textContent = labelRemove;
cancelButton.classList.add('btn', 'btn-cancel');
cancelButton.classList.add("btn", "btn-cancel");
cancelButton.textContent = labelCancel;
removeButton.addEventListener('click', (e) => {
removeButton.addEventListener("click", (e) => {
e.preventDefault();
if (zoneData.allowRemove === 'true') {
if (zoneData.allowRemove === "true") {
removeDataInForm(zone, zoneData);
cancelButton.addEventListener('click', (e) => {
cancelButton.addEventListener("click", (e) => {
e.preventDefault();
restoreDataInForm(zone, zoneData);
cancelButton.remove();
zone.querySelector('.dropzone').remove();
zone.querySelector(".dropzone").remove();
initialize(zone);
});
@@ -311,10 +299,7 @@ var insertRemoveButton = (zone, zoneData) => {
};
const removeDownloadButton = (zone, zoneData) => {
var
existingButtons = zone.querySelectorAll('a[data-download-button]')
;
var existingButtons = zone.querySelectorAll("a[data-download-button]");
// remove existing
existingButtons.forEach(function (b) {
b.remove();
@@ -322,30 +307,27 @@ const removeDownloadButton = (zone, zoneData) => {
};
var insertDownloadButton = (zone, zoneData) => {
var
existingButtons = zone.querySelectorAll('a[data-download-button]'),
newButton = document.createElement('a'),
inputKey = zone.querySelector('input[data-stored-object-key]'),
inputIv = zone.querySelector('input[data-stored-object-iv]'),
inputObject = zone.querySelector('input[data-async-file-upload]'),
inputType = zone.querySelector('input[data-async-file-type]'),
var existingButtons = zone.querySelectorAll("a[data-download-button]"),
newButton = document.createElement("a"),
inputKey = zone.querySelector("input[data-stored-object-key]"),
inputIv = zone.querySelector("input[data-stored-object-iv]"),
inputObject = zone.querySelector("input[data-async-file-upload]"),
inputType = zone.querySelector("input[data-async-file-type]"),
labelPreparing = zone.dataset.labelPreparing,
labelQuietButton = zone.dataset.labelQuietButton,
labelReady = zone.dataset.labelReady,
tempUrlGenerator = zone.dataset.tempUrlGenerator,
tempUrlGeneratorParams = new URLSearchParams()
;
tempUrlGeneratorParams = new URLSearchParams();
// remove existing
existingButtons.forEach(function (b) {
b.remove();
});
if (inputObject.value === '') {
if (inputObject.value === "") {
return;
}
tempUrlGeneratorParams.append('object_name', inputObject.value);
tempUrlGeneratorParams.append("object_name", inputObject.value);
newButton.dataset.downloadButton = true;
newButton.dataset.key = inputKey.value;
@@ -353,8 +335,9 @@ var insertDownloadButton = (zone, zoneData) => {
newButton.dataset.mimeType = inputType.value;
newButton.dataset.labelPreparing = labelPreparing;
newButton.dataset.labelReady = labelReady;
newButton.dataset.tempUrlGetGenerator = tempUrlGenerator + '?' + tempUrlGeneratorParams.toString();
newButton.classList.add('btn', 'btn-download', 'dz-bt-below-dropzone');
newButton.dataset.tempUrlGetGenerator =
tempUrlGenerator + "?" + tempUrlGeneratorParams.toString();
newButton.classList.add("btn", "btn-download", "dz-bt-below-dropzone");
newButton.textContent = labelQuietButton;
addBelowButton(newButton, zone, zoneData);
@@ -362,11 +345,11 @@ var insertDownloadButton = (zone, zoneData) => {
initializeButtons(zone);
};
window.addEventListener('load', function(e) {
window.addEventListener("load", function (e) {
searchForZones(document);
});
window.addEventListener('collection-add-entry', function(e) {
window.addEventListener("collection-add-entry", function (e) {
searchForZones(e.detail.entry);
});

View File

@@ -6,20 +6,27 @@ import {defineComponent} from "vue";
import DownloadButton from "../../vuejs/StoredObjectButton/DownloadButton.vue";
import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({});
window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll<HTMLDivElement>('div[data-download-button-single]').forEach((el) => {
const storedObject = JSON.parse(el.dataset.storedObject as string) as StoredObject;
window.addEventListener("DOMContentLoaded", function (e) {
document
.querySelectorAll<HTMLDivElement>("div[data-download-button-single]")
.forEach((el) => {
const storedObject = JSON.parse(
el.dataset.storedObject as string,
) as StoredObject;
const title = el.dataset.title as string;
const app = createApp({
components: { DownloadButton },
data() {
return {storedObject, title, classes: {btn: true, "btn-outline-primary": true}};
return {
storedObject,
title,
classes: { btn: true, "btn-outline-primary": true },
};
},
template: '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
template:
'<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
});
app.use(i18n).use(ToastPlugin).mount(el);

View File

@@ -7,48 +7,67 @@ import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({});
window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll<HTMLDivElement>('div[data-download-buttons]').forEach((el) => {
window.addEventListener("DOMContentLoaded", function (e) {
document
.querySelectorAll<HTMLDivElement>("div[data-download-buttons]")
.forEach((el) => {
const app = createApp({
components: { DocumentActionButtonsGroup },
data() {
const datasets = el.dataset as {
filename: string,
canEdit: string,
storedObject: string,
buttonSmall: string,
davLink: string,
davLinkExpiration: string,
filename: string;
canEdit: string;
storedObject: string;
buttonSmall: string;
davLink: string;
davLinkExpiration: string;
};
const
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
const storedObject = JSON.parse(
datasets.storedObject,
) as StoredObject,
filename = datasets.filename,
canEdit = datasets.canEdit === '1',
small = datasets.buttonSmall === '1',
davLink = 'davLink' in datasets && datasets.davLink !== '' ? datasets.davLink : null,
davLinkExpiration = 'davLinkExpiration' in datasets ? Number.parseInt(datasets.davLinkExpiration) : null
;
return { storedObject, filename, canEdit, small, davLink, davLinkExpiration };
canEdit = datasets.canEdit === "1",
small = datasets.buttonSmall === "1",
davLink =
"davLink" in datasets && datasets.davLink !== ""
? datasets.davLink
: null,
davLinkExpiration =
"davLinkExpiration" in datasets
? Number.parseInt(datasets.davLinkExpiration)
: null;
return {
storedObject,
filename,
canEdit,
small,
davLink,
davLinkExpiration,
};
},
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
template:
'<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
methods: {
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
onStoredObjectStatusChange: function (
newStatus: StoredObjectStatusChange,
): void {
this.$data.storedObject.status = newStatus.status;
this.$data.storedObject.filename = newStatus.filename;
this.$data.storedObject.type = newStatus.type;
// remove eventual div which inform pending status
document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`)
document
.querySelectorAll(
`[data-docgen-is-pending="${this.$data.storedObject.id}"]`,
)
.forEach(function (el) {
el.remove();
});
}
}
},
},
});
app.use(i18n).use(ToastPlugin).mount(el);
})
});
});

View File

@@ -64,7 +64,8 @@ export interface StoredObjectStatusChange {
type: string;
}
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
export interface StoredObjectVersionWithPointInTime
extends StoredObjectVersionPersisted {
"point-in-times": StoredObjectPointInTime[];
"from-restored": StoredObjectVersionPersisted | null;
}
@@ -72,15 +73,13 @@ export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionP
export interface StoredObjectPointInTime {
id: number;
byUser: User | null;
reason: 'keep-before-conversion'|'keep-by-user';
reason: "keep-before-conversion" | "keep-by-user";
}
/**
* Function executed by the WopiEditButton component.
*/
export type WopiEditButtonExecutableBeforeLeaveFunction = {
(): Promise<void>;
};
export type WopiEditButtonExecutableBeforeLeaveFunction = () => Promise<void>;
/**
* Object containing information for performering a POST request to a swift object store
@@ -135,7 +134,7 @@ export interface ZoomLevel {
id: number;
zoom: number;
label: {
fr?: string,
nl?: string
fr?: string;
nl?: string;
};
}

View File

@@ -1,23 +1,58 @@
<template>
<div v-if="isButtonGroupDisplayable" 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
</button>
<ul class="dropdown-menu">
<li v-if="isEditableOnline">
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
<wopi-edit-button
:stored-object="props.storedObject"
:classes="{ 'dropdown-item': true }"
:execute-before-leave="props.executeBeforeLeave"
></wopi-edit-button>
</li>
<li v-if="isEditableOnDesktop">
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
<desktop-edit-button
:classes="{ 'dropdown-item': true }"
:edit-link="props.davLink"
:expiration-link="props.davLinkExpiration"
></desktop-edit-button>
</li>
<li v-if="isConvertibleToPdf">
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
<convert-button
:stored-object="props.storedObject"
:filename="filename"
:classes="{ 'dropdown-item': true }"
></convert-button>
</li>
<li v-if="isDownloadable">
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}" :display-action-string-in-button="true"></download-button>
<download-button
:stored-object="props.storedObject"
:at-version="props.storedObject.currentVersion"
:filename="filename"
:classes="{ 'dropdown-item': true }"
:display-action-string-in-button="true"
></download-button>
</li>
<li v-if="isHistoryViewable">
<history-button :stored-object="props.storedObject" :can-edit="canEdit && props.storedObject._permissions.canEdit"></history-button>
<history-button
:stored-object="props.storedObject"
:can-edit="
canEdit && props.storedObject._permissions.canEdit
"
></history-button>
</li>
</ul>
</div>
@@ -27,31 +62,34 @@
<div v-else-if="'failure' === props.storedObject.status">
<div class="btn btn-outline-danger">La génération a échoué</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from "vue";
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
import {
is_extension_editable,
is_extension_viewable,
is_object_ready,
} from "./StoredObjectButton/helpers";
import {
StoredObject,
StoredObjectStatusChange, StoredObjectVersion,
WopiEditButtonExecutableBeforeLeaveFunction
StoredObjectStatusChange,
StoredObjectVersion,
WopiEditButtonExecutableBeforeLeaveFunction,
} from "../types";
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
interface DocumentActionButtonsGroupConfig {
storedObject: StoredObject,
small?: boolean,
canEdit?: boolean,
canDownload?: boolean,
canConvertPdf?: boolean,
returnPath?: string,
storedObject: StoredObject;
small?: boolean;
canEdit?: boolean;
canDownload?: boolean;
canConvertPdf?: boolean;
returnPath?: string;
/**
* Will be the filename displayed to the user when he·she download the document
@@ -59,34 +97,41 @@ interface DocumentActionButtonsGroupConfig {
*
* If not set, 'document' will be used.
*/
filename?: string,
filename?: string;
/**
* If set, will execute this function before leaving to the editor
*/
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
/**
* a link to download and edit file using webdav
*/
davLink?: string,
davLink?: string;
/**
* the expiration date of the download, as a unix timestamp
*/
davLinkExpiration?: number,
davLinkExpiration?: number;
}
const emit = defineEmits<{
(e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
}>();
const emit =
defineEmits<
(
e: "onStoredObjectStatusChange",
newStatus: StoredObjectStatusChange,
) => void
>();
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
small: false,
canEdit: true,
canDownload: true,
canConvertPdf: true,
returnPath: window.location.pathname + window.location.search + window.location.hash
returnPath:
window.location.pathname +
window.location.search +
window.location.hash,
});
/**
@@ -100,22 +145,32 @@ let tryiesForReady = 0;
const maxTryiesForReady = 120;
const isButtonGroupDisplayable = computed<boolean>(() => {
return isDownloadable.value || isEditableOnline.value || isEditableOnDesktop.value || isConvertibleToPdf.value;
return (
isDownloadable.value ||
isEditableOnline.value ||
isEditableOnDesktop.value ||
isConvertibleToPdf.value
);
});
const isDownloadable = computed<boolean>(() => {
return props.storedObject.status === 'ready'
return (
props.storedObject.status === "ready" ||
// happens when the stored object version is just added, but not persisted
|| (props.storedObject.currentVersion !== null && props.storedObject.status === 'empty')
(props.storedObject.currentVersion !== null &&
props.storedObject.status === "empty")
);
});
const isEditableOnline = computed<boolean>(() => {
return props.storedObject.status === 'ready'
&& props.storedObject._permissions.canEdit
&& props.canEdit
&& props.storedObject.currentVersion !== null
&& is_extension_editable(props.storedObject.currentVersion.type)
&& props.storedObject.currentVersion.persisted !== false;
return (
props.storedObject.status === "ready" &&
props.storedObject._permissions.canEdit &&
props.canEdit &&
props.storedObject.currentVersion !== null &&
is_extension_editable(props.storedObject.currentVersion.type) &&
props.storedObject.currentVersion.persisted !== false
);
});
const isEditableOnDesktop = computed<boolean>(() => {
@@ -123,26 +178,28 @@ const isEditableOnDesktop = computed<boolean>(() => {
});
const isConvertibleToPdf = computed<boolean>(() => {
return props.storedObject.status === 'ready'
&& props.storedObject._permissions.canSee
&& props.canConvertPdf
&& props.storedObject.currentVersion !== null
&& is_extension_viewable(props.storedObject.currentVersion.type)
&& props.storedObject.currentVersion.type !== 'application/pdf'
&& props.storedObject.currentVersion.persisted !== false;
return (
props.storedObject.status === "ready" &&
props.storedObject._permissions.canSee &&
props.canConvertPdf &&
props.storedObject.currentVersion !== null &&
is_extension_viewable(props.storedObject.currentVersion.type) &&
props.storedObject.currentVersion.type !== "application/pdf" &&
props.storedObject.currentVersion.persisted !== false
);
});
const isHistoryViewable = computed<boolean>(() => {
return props.storedObject.status === 'ready';
return props.storedObject.status === "ready";
});
const checkForReady = function (): void {
if (
'ready' === props.storedObject.status
|| 'empty' === props.storedObject.status
|| 'failure' === props.storedObject.status
"ready" === props.storedObject.status ||
"empty" === props.storedObject.status ||
"failure" === props.storedObject.status ||
// stop reloading if the page stays opened for a long time
|| tryiesForReady > maxTryiesForReady
tryiesForReady > maxTryiesForReady
) {
return;
}
@@ -153,20 +210,19 @@ const checkForReady = function(): void {
};
const onObjectNewStatusCallback = async function (): Promise<void> {
if (props.storedObject.status === 'stored_object_created') {
if (props.storedObject.status === "stored_object_created") {
return Promise.resolve();
}
const new_status = await is_object_ready(props.storedObject);
if (props.storedObject.status !== new_status.status) {
emit('onStoredObjectStatusChange', new_status);
emit("onStoredObjectStatusChange", new_status);
return Promise.resolve();
} else if ('failure' === new_status.status) {
} else if ("failure" === new_status.status) {
return Promise.resolve();
}
if ('ready' !== new_status.status) {
if ("ready" !== new_status.status) {
// we check for new status, unless it is ready
checkForReady();
}
@@ -176,9 +232,7 @@ const onObjectNewStatusCallback = async function(): Promise<void> {
onMounted(() => {
checkForReady();
})
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -27,7 +27,9 @@
</modal>
</teleport>
<div class="col-12 m-auto sticky-top">
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
<div
class="row justify-content-center border-bottom pdf-tools d-md-none"
>
<div class="col-5 text-center turn-page">
<select
class="form-select form-select-sm"
@@ -89,7 +91,10 @@
class="col-5 p-0 text-center turnSignature"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
@@ -97,7 +102,9 @@
</button>
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
:disabled="
userSignatureZone?.index >= signature.zones.length - 1
"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
@@ -135,7 +142,10 @@
:title="$t('add_sign_zone')"
>
<template v-if="canvasEvent === 'add'">
<div class="spinner-border spinner-border-sm" role="status">
<div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</template>
@@ -190,7 +200,10 @@
class="col-4 d-xl-none text-center turnSignature p-0"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
@@ -198,7 +211,9 @@
</button>
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
:disabled="
userSignatureZone?.index >= signature.zones.length - 1
"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
@@ -210,7 +225,10 @@
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
@@ -218,7 +236,9 @@
</button>
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
:disabled="
userSignatureZone?.index >= signature.zones.length - 1
"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
@@ -259,7 +279,10 @@
</template>
<template v-else>
{{ $t("click_on_document") }}
<div class="spinner-border spinner-border-sm" role="status">
<div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</template>
@@ -336,7 +359,10 @@ console.log(PdfWorker); // incredible but this is needed
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButton/helpers";
import {
download_and_decrypt_doc,
download_doc_as_pdf,
} from "../StoredObjectButton/helpers";
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
@@ -479,12 +505,18 @@ const addCanvasEvents = () => {
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false
false,
);
});
} else {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
const canvas = document.querySelectorAll(
"canvas",
)[0] as HTMLCanvasElement;
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false,
);
}
};
@@ -511,7 +543,7 @@ const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
const hitSignature = (
zone: SignatureZone,
xy: number[],
canvas: HTMLCanvasElement
canvas: HTMLCanvasElement,
) =>
scaleXToCanvas(zone.x, canvas.width, zone.PDFPage.width) < xy[0] &&
xy[0] <
@@ -520,7 +552,11 @@ const hitSignature = (
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
xy[1] &&
xy[1] <
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
scaleYToCanvas(
zone.height - zone.y,
canvas.height,
zone.PDFPage.height,
) +
zone.PDFPage.height * zoom.value;
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
@@ -536,8 +572,9 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones
.filter(
(z) =>
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
(z.PDFPage.index + 1 === getCanvasId(canvas) &&
multiPage.value) ||
(z.PDFPage.index + 1 === page.value && !multiPage.value),
)
.map((z) => {
if (hitSignature(z, [e.offsetX, e.offsetY], canvas)) {
@@ -590,7 +627,7 @@ const drawZone = (
zone: SignatureZone,
ctx: CanvasRenderingContext2D,
canvasWidth: number,
canvasHeight: number
canvasHeight: number,
) => {
const unselectedBlue = "#007bff";
const selectedBlue = "#034286";
@@ -605,7 +642,7 @@ const drawZone = (
zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height),
);
ctx.font = `bold ${16 * zoom.value}px serif`;
ctx.textAlign = "center";
@@ -633,7 +670,7 @@ const drawAllZones = () => {
.filter(
(z) =>
multiPage.value ||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
(z.PDFPage.index + 1 === page.value && !multiPage.value),
)
.map((z) => {
const canvas = getCanvas(z.PDFPage.index + 1);
@@ -663,7 +700,7 @@ const checkSignature = () => {
signedState.value = "error";
console.log("Error while checking the signature", error);
$toast.error(
`Erreur lors de la vérification de la signature: ${error.txt}`
`Erreur lors de la vérification de la signature: ${error.txt}`,
);
});
};
@@ -682,7 +719,7 @@ const checkForReady = () => {
tryForReady = 0;
console.log("Reached the maximum number of tentative to try signing");
$toast.error(
"Le nombre maximum de tentatives pour essayer de signer est atteint"
"Le nombre maximum de tentatives pour essayer de signer est atteint",
);
}
if (signedState.value === "rejected") {
@@ -724,7 +761,7 @@ const confirmSign = () => {
console.log("Error while posting the signature", error);
stopTrySigning();
$toast.error(
`Erreur lors de la soumission de la signature: ${error.txt}`
`Erreur lors de la soumission de la signature: ${error.txt}`,
);
});
};
@@ -780,8 +817,8 @@ const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
const getReturnPath = () =>
window.location.search
? window.location.search.split("?returnPath=")[1] ??
window.location.pathname
? (window.location.search.split("?returnPath=")[1] ??
window.location.pathname)
: window.location.pathname;
init();
@@ -842,4 +879,3 @@ div.signature-modal-body {
height: 8rem;
}
</style>

View File

@@ -5,29 +5,29 @@ import App from "./App.vue";
const appMessages = {
fr: {
yes: 'Oui',
are_you_sure: 'Êtes-vous sûr·e?',
you_are_going_to_sign: 'Vous allez signer le document',
signature_confirmation: 'Confirmation de la signature',
sign: 'Signer',
choose_another_signature: 'Choisir une autre zone',
cancel: 'Annuler',
last_sign_zone: 'Zone de signature précédente',
next_sign_zone: 'Zone de signature suivante',
add_sign_zone: 'Ajouter une zone de signature',
click_on_document: 'Cliquer sur le document',
last_zone: 'Zone précédente',
next_zone: 'Zone suivante',
add_zone: 'Ajouter une zone',
another_zone: 'Autre zone',
electronic_signature_in_progress: 'Signature électronique en cours...',
loading: 'Chargement...',
remove_sign_zone: 'Enlever la zone',
return: 'Retour',
see_all_pages: 'Voir toutes les pages',
all_pages: 'Toutes les pages',
}
}
yes: "Oui",
are_you_sure: "Êtes-vous sûr·e?",
you_are_going_to_sign: "Vous allez signer le document",
signature_confirmation: "Confirmation de la signature",
sign: "Signer",
choose_another_signature: "Choisir une autre zone",
cancel: "Annuler",
last_sign_zone: "Zone de signature précédente",
next_sign_zone: "Zone de signature suivante",
add_sign_zone: "Ajouter une zone de signature",
click_on_document: "Cliquer sur le document",
last_zone: "Zone précédente",
next_zone: "Zone suivante",
add_zone: "Ajouter une zone",
another_zone: "Autre zone",
electronic_signature_in_progress: "Signature électronique en cours...",
loading: "Chargement...",
remove_sign_zone: "Enlever la zone",
return: "Retour",
see_all_pages: "Voir toutes les pages",
all_pages: "Toutes les pages",
},
};
const i18n = _createI18n(appMessages);

View File

@@ -1,19 +1,31 @@
<script setup lang="ts">
import { StoredObject, StoredObjectVersionCreated } from "../../types";
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
import {
encryptFile,
fetchNewStoredObject,
uploadVersion,
} from "../../js/async-upload/uploader";
import { computed, ref, Ref } from "vue";
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
interface DropFileConfig {
existingDoc?: StoredObject,
existingDoc?: StoredObject;
}
const props = withDefaults(defineProps<DropFileConfig>(), {existingDoc: null});
const props = withDefaults(defineProps<DropFileConfig>(), {
existingDoc: null,
});
const emit = defineEmits<{
(e: 'addDocument', {stored_object_version: StoredObjectVersionCreated, stored_object: StoredObject}): void,
}>();
const emit =
defineEmits<
(
e: "addDocument",
{
stored_object_version: StoredObjectVersionCreated,
stored_object: StoredObject,
},
) => void
>();
const is_dragging: Ref<boolean> = ref(false);
const uploading: Ref<boolean> = ref(false);
@@ -27,13 +39,13 @@ const onDragOver = (e: Event) => {
e.preventDefault();
is_dragging.value = true;
}
};
const onDragLeave = (e: Event) => {
e.preventDefault();
is_dragging.value = false;
}
};
const onDrop = (e: DragEvent) => {
e.preventDefault();
@@ -49,8 +61,8 @@ const onDrop = (e: DragEvent) => {
return;
}
handleFile(files[0])
}
handleFile(files[0]);
};
const onZoneClick = (e: Event) => {
e.stopPropagation();
@@ -61,21 +73,21 @@ const onZoneClick = (e: Event) => {
input.addEventListener("change", onFileChange);
input.click();
}
};
const onFileChange = async (event: Event): Promise<void> => {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
console.log('file added', 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';
}
throw "No file given";
};
const handleFile = async (file: File): Promise<void> => {
uploading.value = true;
@@ -100,25 +112,39 @@ const handleFile = async (file: File): Promise<void> => {
keyInfos: jsonWebKey,
type: type,
persisted: false,
}
};
emit('addDocument', {stored_object, stored_object_version});
emit("addDocument", { stored_object, stored_object_version });
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">
<div
v-if="!uploading"
:class="{ area: true, dragging: is_dragging }"
@click="onZoneClick"
@dragover="onDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<p v-if="has_existing_doc" class="file-icon">
<file-icon :type="props.existingDoc?.type"></file-icon>
</p>
<p v-if="display_filename !== null" class="display-filename">{{ display_filename }}</p>
<p v-if="display_filename !== null" class="display-filename">
{{ display_filename }}
</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>
<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>
@@ -140,7 +166,8 @@ const handleFile = async (file: File): Promise<void> => {
font-weight: 200;
}
& > .area, & > .waiting {
& > .area,
& > .waiting {
width: 100%;
height: 10rem;
@@ -163,5 +190,4 @@ const handleFile = async (file: File): Promise<void> => {
}
}
}
</style>

View File

@@ -1,14 +1,13 @@
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { StoredObject, StoredObjectVersion } from "../../types";
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
import { computed, reactive } from "vue";
import {useToast} from 'vue-toast-notification';
import { useToast } from "vue-toast-notification";
interface DropFileConfig {
allowRemove: boolean,
existingDoc?: StoredObject,
allowRemove: boolean;
existingDoc?: StoredObject;
}
const props = withDefaults(defineProps<DropFileConfig>(), {
@@ -16,8 +15,14 @@ const props = withDefaults(defineProps<DropFileConfig>(), {
});
const emit = defineEmits<{
(e: 'addDocument', {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void,
(e: 'removeDocument'): void
(
e: "addDocument",
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
},
): void;
(e: "removeDocument"): void;
}>();
const $toast = useToast();
@@ -26,23 +31,30 @@ const state = reactive({showModal: false});
const modalClasses = { "modal-dialog-centered": true, "modal-md": true };
const buttonState = computed<'add'|'replace'>(() => {
const buttonState = computed<"add" | "replace">(() => {
if (props.existingDoc === undefined || props.existingDoc === null) {
return 'add';
return "add";
}
return 'replace';
})
return "replace";
});
function onAddDocument({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void {
const message = buttonState.value === 'add' ? "Document ajouté" : "Document remplacé";
function onAddDocument({
stored_object,
stored_object_version,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
}): void {
const message =
buttonState.value === "add" ? "Document ajouté" : "Document remplacé";
$toast.success(message);
emit('addDocument', {stored_object_version, stored_object});
emit("addDocument", { stored_object_version, stored_object });
state.showModal = false;
}
function onRemoveDocument(): void {
emit('removeDocument');
emit("removeDocument");
}
function openModal(): void {
@@ -55,15 +67,30 @@ function closeModal(): void {
</script>
<template>
<button v-if="buttonState === 'add'" @click="openModal" class="btn btn-create">Ajouter un document</button>
<button v-else @click="openModal" class="btn btn-edit">Remplacer le document</button>
<modal v-if="state.showModal" :modal-dialog-class="modalClasses" @close="closeModal">
<button
v-if="buttonState === 'add'"
@click="openModal"
class="btn btn-create"
>
Ajouter un document
</button>
<button v-else @click="openModal" class="btn btn-edit">
Remplacer le document
</button>
<modal
v-if="state.showModal"
:modal-dialog-class="modalClasses"
@close="closeModal"
>
<template v-slot:body>
<drop-file-widget :existing-doc="existingDoc" :allow-remove="allowRemove" @add-document="onAddDocument" @remove-document="onRemoveDocument" ></drop-file-widget>
<drop-file-widget
:existing-doc="existingDoc"
:allow-remove="allowRemove"
@add-document="onAddDocument"
@remove-document="onRemoveDocument"
></drop-file-widget>
</template>
</modal>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { StoredObject, StoredObjectVersion } 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?: StoredObject,
allowRemove: boolean;
existingDoc?: StoredObject;
}
const props = withDefaults(defineProps<DropFileConfig>(), {
@@ -15,8 +14,14 @@ const props = withDefaults(defineProps<DropFileConfig>(), {
});
const emit = defineEmits<{
(e: 'addDocument', {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void,
(e: 'removeDocument'): void
(
e: "addDocument",
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
},
): void;
(e: "removeDocument"): void;
}>();
const has_existing_doc = computed<boolean>(() => {
@@ -27,7 +32,7 @@ const dav_link_expiration = computed<number|undefined>(() => {
if (props.existingDoc === undefined || props.existingDoc === null) {
return undefined;
}
if (props.existingDoc.status !== 'ready') {
if (props.existingDoc.status !== "ready") {
return undefined;
}
@@ -38,28 +43,36 @@ const dav_link_href = computed<string|undefined>(() => {
if (props.existingDoc === undefined || props.existingDoc === null) {
return undefined;
}
if (props.existingDoc.status !== 'ready') {
if (props.existingDoc.status !== "ready") {
return undefined;
}
return props.existingDoc._links?.dav_link?.href;
})
});
const onAddDocument = ({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void => {
emit('addDocument', {stored_object, stored_object_version});
}
const onAddDocument = ({
stored_object,
stored_object_version,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
}): void => {
emit("addDocument", { stored_object, stored_object_version });
};
const onRemoveDocument = (e: Event): void => {
e.stopPropagation();
e.preventDefault();
emit('removeDocument');
}
emit("removeDocument");
};
</script>
<template>
<div>
<drop-file :existingDoc="props.existingDoc" @addDocument="onAddDocument"></drop-file>
<drop-file
:existingDoc="props.existingDoc"
@addDocument="onAddDocument"
></drop-file>
<ul class="record_actions">
<li v-if="has_existing_doc">
@@ -72,12 +85,14 @@ const onRemoveDocument = (e: Event): void => {
/>
</li>
<li>
<button v-if="allowRemove" class="btn btn-delete" @click="onRemoveDocument($event)" ></button>
<button
v-if="allowRemove"
class="btn btn-delete"
@click="onRemoveDocument($event)"
></button>
</li>
</ul>
</div>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -4,22 +4,43 @@ interface FileIconConfig {
}
const props = defineProps<FileIconConfig>();
</script>
<template>
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"></i>
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/msword'"></i>
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.ms-excel'"></i>
<i
class="fa fa-file-word-o"
v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"
></i>
<i
class="fa fa-file-word-o"
v-else-if="
props.type ===
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
"
></i>
<i
class="fa fa-file-word-o"
v-else-if="props.type === 'application/msword'"
></i>
<i
class="fa fa-file-excel-o"
v-else-if="
props.type ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
"
></i>
<i
class="fa fa-file-excel-o"
v-else-if="props.type === 'application/vnd.ms-excel'"
></i>
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i>
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
<i class="fa fa-file-archive-o" v-else-if="props.type === 'application/x-zip-compressed'"></i>
<i
class="fa fa-file-archive-o"
v-else-if="props.type === 'application/x-zip-compressed'"
></i>
<i class="fa fa-file-code-o" v-else></i>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -6,20 +6,23 @@
</template>
<script lang="ts" setup>
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 { reactive, ref } from "vue";
import { StoredObject } from "../../types";
interface ConvertButtonConfig {
storedObject: StoredObject,
classes: { [key: string]: boolean},
filename?: string,
};
storedObject: StoredObject;
classes: Record<string, boolean>;
filename?: string;
}
interface DownloadButtonState {
content: null|string
content: null | string;
}
const props = defineProps<ConvertButtonConfig>();
@@ -32,13 +35,15 @@ async function download_and_open(event: Event): Promise<void> {
if (null === state.content) {
event.preventDefault();
const raw = await download_doc(build_convert_link(props.storedObject.uuid));
const raw = await download_doc(
build_convert_link(props.storedObject.uuid),
);
state.content = window.URL.createObjectURL(raw);
button.href = window.URL.createObjectURL(raw);
button.type = 'application/pdf';
button.type = "application/pdf";
button.download = (props.filename + '.pdf') || 'document.pdf';
button.download = props.filename + ".pdf" || "document.pdf";
}
button.click();
@@ -47,11 +52,10 @@ async function download_and_open(event: Event): Promise<void> {
function reset_state(): void {
state.content = null;
btn.value?.removeAttribute('download');
btn.value?.removeAttribute('href');
btn.value?.removeAttribute('type');
btn.value?.removeAttribute("download");
btn.value?.removeAttribute("href");
btn.value?.removeAttribute("type");
}
</script>
<style scoped lang="scss">

View File

@@ -1,23 +1,24 @@
<script setup lang="ts">
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { computed, reactive } from "vue";
export interface DesktopEditButtonConfig {
editLink: null,
classes: { [k: string]: boolean },
expirationLink: number|Date,
editLink: null;
classes: Record<string, boolean>;
expirationLink: number | Date;
}
interface DesktopEditButtonState {
modalOpened: boolean
};
modalOpened: boolean;
}
const state: DesktopEditButtonState = reactive({ modalOpened: false });
const props = defineProps<DesktopEditButtonConfig>();
const buildCommand = computed<string>(() => 'vnd.libreoffice.command:ofe|u|' + props.editLink);
const buildCommand = computed<string>(
() => "vnd.libreoffice.command:ofe|u|" + props.editLink,
);
const editionUntilFormatted = computed<string>(() => {
let d;
@@ -29,9 +30,11 @@ const editionUntilFormatted = computed<string>(() => {
}
console.log(props.expirationLink);
return (new Intl.DateTimeFormat(undefined, {'dateStyle': 'long', 'timeStyle': 'medium'})).format(d);
return new Intl.DateTimeFormat(undefined, {
dateStyle: "long",
timeStyle: "medium",
}).format(d);
});
</script>
<template>
@@ -39,16 +42,40 @@ const editionUntilFormatted = computed<string>(() => {
<modal v-if="state.modalOpened" @close="state.modalOpened = false">
<template v-slot:body>
<div class="desktop-edit">
<p class="center">Veuillez enregistrer vos modifications avant le</p>
<p><strong>{{ editionUntilFormatted }}</strong></p>
<p class="center">
Veuillez enregistrer vos modifications avant le
</p>
<p>
<strong>{{ editionUntilFormatted }}</strong>
</p>
<p><a class="btn btn-primary" :href="buildCommand">Ouvrir le document pour édition</a></p>
<p>
<a class="btn btn-primary" :href="buildCommand"
>Ouvrir le document pour édition</a
>
</p>
<p><small>Le document peut être édité uniquement en utilisant Libre Office.</small></p>
<p>
<small
>Le document peut être édité uniquement en utilisant
Libre Office.</small
>
</p>
<p><small>En cas d'échec lors de l'enregistrement, sauver le document sur le poste de travail avant de le déposer à nouveau ici.</small></p>
<p>
<small
>En cas d'échec lors de l'enregistrement, sauver le
document sur le poste de travail avant de le déposer
à nouveau ici.</small
>
</p>
<p><small>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small></p>
<p>
<small
>Vous pouvez naviguez sur d'autres pages pendant
l'édition.</small
>
</p>
</div>
</template>
</modal>

View File

@@ -1,9 +1,23 @@
<template>
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="T&#233;l&#233;charger">
<a
v-if="!state.is_ready"
:class="props.classes"
@click="download_and_open()"
title="T&#233;l&#233;charger"
>
<i class="fa fa-download"></i>
<template v-if="displayActionStringInButton">Télécharger</template>
</a>
<a v-else :class="props.classes" target="_blank" :type="props.atVersion.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
<a
v-else
:class="props.classes"
target="_blank"
:type="props.atVersion.type"
:download="buildDocumentName()"
:href="state.href_url"
ref="open_button"
title="Ouvrir"
>
<i class="fa fa-external-link"></i>
<template v-if="displayActionStringInButton">Ouvrir</template>
</a>
@@ -16,43 +30,50 @@ import mime from "mime";
import { StoredObject, StoredObjectVersion } from "../../types";
interface DownloadButtonConfig {
storedObject: StoredObject,
atVersion: StoredObjectVersion,
classes: { [k: string]: boolean },
filename?: string,
storedObject: StoredObject;
atVersion: StoredObjectVersion;
classes: Record<string, boolean>;
filename?: string;
/**
* if true, display the action string into the button. If false, displays only
* the icon
*/
displayActionStringInButton?: boolean,
displayActionStringInButton?: boolean;
/**
* if true, will download directly the file on load
*/
directDownload?: boolean,
directDownload?: boolean;
}
interface DownloadButtonState {
is_ready: boolean,
is_running: boolean,
href_url: string,
is_ready: boolean;
is_running: boolean;
href_url: string;
}
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
const props = withDefaults(defineProps<DownloadButtonConfig>(), {
displayActionStringInButton: true,
directDownload: false,
});
const state: DownloadButtonState = reactive({
is_ready: false,
is_running: false,
href_url: "#",
});
const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
let document_name = props.filename ?? props.storedObject.title;
if ('' === document_name) {
document_name = 'document';
if ("" === document_name) {
document_name = "document";
}
const ext = mime.getExtension(props.atVersion.type);
if (null !== ext) {
return document_name + '.' + ext;
return document_name + "." + ext;
}
return document_name;
@@ -60,21 +81,24 @@ function buildDocumentName(): string {
async function download_and_open(): Promise<void> {
if (state.is_running) {
console.log('state is running, aborting');
console.log("state is running, aborting");
return;
}
state.is_running = true;
if (state.is_ready) {
console.log('state is ready. This should not happens');
console.log("state is ready. This should not happens");
return;
}
let raw;
try {
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion);
raw = await download_and_decrypt_doc(
props.storedObject,
props.atVersion,
);
} catch (e) {
console.error("error while downloading and decrypting document");
console.error(e);
@@ -89,13 +113,13 @@ async function download_and_open(): Promise<void> {
await nextTick();
open_button.value?.click();
console.log('open button should have been clicked');
console.log("open button should have been clicked");
setTimeout(reset_state, 45000);
}
}
function reset_state(): void {
state.href_url = '#';
state.href_url = "#";
state.is_ready = false;
state.is_running = false;
}

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../types";
import {
StoredObject,
StoredObjectVersionWithPointInTime,
} from "./../../types";
import { computed, reactive, ref, useTemplateRef } from "vue";
import { get_versions } from "./HistoryButton/api";
@@ -17,7 +19,7 @@ interface HistoryButtonState {
const props = defineProps<HistoryButtonConfig>();
const state = reactive<HistoryButtonState>({ versions: [], loaded: false });
const modal = useTemplateRef<typeof HistoryButtonModal>('modal');
const modal = useTemplateRef<typeof HistoryButtonModal>("modal");
const download_version_and_open_modal = async function (): Promise<void> {
if (null !== modal.value) {
@@ -34,17 +36,26 @@ const download_version_and_open_modal = async function (): Promise<void> {
}
state.loaded = true;
}
}
};
const onRestoreVersion = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
const onRestoreVersion = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
state.versions.unshift(newVersion);
}
};
</script>
<template>
<a @click="download_version_and_open_modal" class="dropdown-item">
<history-button-modal ref="modal" :versions="state.versions" :stored-object="storedObject" :can-edit="canEdit" @restore-version="onRestoreVersion"></history-button-modal>
<history-button-modal
ref="modal"
:versions="state.versions"
:stored-object="storedObject"
:can-edit="canEdit"
@restore-version="onRestoreVersion"
></history-button-modal>
<i class="fa fa-history"></i>
Historique
</a>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts">
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
import {
StoredObject,
StoredObjectVersionWithPointInTime,
} from "./../../../types";
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
import { computed, reactive } from "vue";
@@ -10,8 +13,8 @@ interface HistoryButtonListConfig {
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
}>()
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
interface HistoryButtonListState {
/**
@@ -22,12 +25,14 @@ interface HistoryButtonListState {
const props = defineProps<HistoryButtonListConfig>();
const state = reactive<HistoryButtonListState>({restored: -1})
const state = reactive<HistoryButtonListState>({ restored: -1 });
const higher_version = computed<number>(() => props.versions.reduce(
(accumulator: number, version: StoredObjectVersionWithPointInTime) => Math.max(accumulator, version.version),
-1
)
const higher_version = computed<number>(() =>
props.versions.reduce(
(accumulator: number, version: StoredObjectVersionWithPointInTime) =>
Math.max(accumulator, version.version),
-1,
),
);
/**
@@ -35,11 +40,14 @@ const higher_version = computed<number>(() => props.versions.reduce(
*
* internally, keep track of the newly restored version
*/
const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
const onRestored = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
state.restored = newVersion.version;
emit('restoreVersion', {newVersion});
}
emit("restoreVersion", { newVersion });
};
</script>
<template>
@@ -59,9 +67,6 @@ const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTim
<template v-else>
<p>Chargement des versions</p>
</template>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import {StoredObject, StoredObjectPointInTime, StoredObjectVersionWithPointInTime} from "./../../../types";
import {
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
} from "./../../../types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
@@ -15,47 +19,120 @@ interface HistoryButtonListItemConfig {
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
}>()
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
const props = defineProps<HistoryButtonListItemConfig>();
const onRestore = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
emit('restoreVersion', {newVersion});
}
const onRestore = ({
newVersion,
}: {
newVersion: StoredObjectVersionWithPointInTime;
}) => {
emit("restoreVersion", { newVersion });
};
const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) => accumulator || "keep-before-conversion" === pit.reason,
false
const isKeptBeforeConversion = computed<boolean>(() =>
props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
),
);
const isRestored = computed<boolean>(() => props.version.version > 0 && null !== props.version["from-restored"]);
const isRestored = computed<boolean>(
() => props.version.version > 0 && null !== props.version["from-restored"],
);
const isDuplicated = computed<boolean>(() => props.version.version === 0 && null !== props.version["from-restored"]);
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
const isDuplicated = computed<boolean>(
() =>
props.version.version === 0 && null !== props.version["from-restored"],
);
const classes = computed<{
row: true;
"row-hover": true;
"blinking-1": boolean;
"blinking-2": boolean;
}>(() => ({
row: true,
"row-hover": true,
"blinking-1": props.isRestored && 0 === props.version.version % 2,
"blinking-2": props.isRestored && 1 === props.version.version % 2,
}));
</script>
<template>
<div :class="classes">
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version + 1 }}</span>
<span class="badge bg-info" v-if="isDuplicated">Dupliqué depuis un autre document</span>
<div
class="col-12 tags"
v-if="
isCurrent ||
isKeptBeforeConversion ||
isRestored ||
isDuplicated
"
>
<span class="badge bg-success" v-if="isCurrent"
>Version actuelle</span
>
<span class="badge bg-info" v-if="isKeptBeforeConversion"
>Conservée avant conversion dans un autre format</span
>
<span class="badge bg-info" v-if="isRestored"
>Restaurée depuis la version
{{ version["from-restored"]?.version + 1 }}</span
>
<span class="badge bg-info" v-if="isDuplicated"
>Dupliqué depuis un autre document</span
>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <template v-if="version.createdBy !== null && version.createdAt !== null"><strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template><template v-if="version.createdBy === null && version.createdAt !== null"><strong v-if="version.version == 0">Créé le</strong><strong v-else>modifié le</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template>
<file-icon :type="version.type"></file-icon>
<span
><strong>#{{ version.version + 1 }}</strong></span
>
<template
v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé par</strong
><strong v-else>modifié par</strong>
<span class="badge-user"
><UserRenderBoxBadge
:user="version.createdBy"
></UserRenderBoxBadge
></span>
<strong>à</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
><template
v-if="version.createdBy === null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé le</strong
><strong v-else>modifié le</strong>
{{
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
}}</template
>
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
<li v-if="canEdit && !isCurrent">
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
<restore-version-button
:stored-object-version="props.version"
@restore-version="onRestore"
></restore-version-button>
</li>
<li>
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true, 'btn-sm': true}" :display-action-string-in-button="false"></download-button>
<download-button
:stored-object="storedObject"
:at-version="version"
:classes="{
btn: true,
'btn-outline-primary': true,
'btn-sm': true,
}"
:display-action-string-in-button="false"
></download-button>
</li>
</ul>
</div>

View File

@@ -2,7 +2,10 @@
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { reactive } from "vue";
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
import {
StoredObject,
StoredObjectVersionWithPointInTime,
} from "./../../../types";
interface HistoryButtonListConfig {
versions: StoredObjectVersionWithPointInTime[];
@@ -11,8 +14,8 @@ interface HistoryButtonListConfig {
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
}>()
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
interface HistoryButtonModalState {
opened: boolean;
@@ -23,10 +26,9 @@ const state = reactive<HistoryButtonModalState>({opened: false});
const open = () => {
state.opened = true;
}
};
defineExpose({ open });
</script>
<template>
<Teleport to="body">
@@ -36,12 +38,17 @@ defineExpose({open});
</template>
<template v-slot:body>
<p>Les versions sont conservées pendant 90 jours.</p>
<history-button-list :versions="props.versions" :can-edit="canEdit" :stored-object="storedObject" @restore-version="(payload) => emit('restoreVersion', payload)"></history-button-list>
<history-button-list
:versions="props.versions"
:can-edit="canEdit"
:stored-object="storedObject"
@restore-version="
(payload) => emit('restoreVersion', payload)
"
></history-button-list>
</template>
</modal>
</Teleport>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,17 +1,20 @@
<script setup lang="ts">
import {StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
import {
StoredObjectVersionPersisted,
StoredObjectVersionWithPointInTime,
} from "../../../types";
import { useToast } from "vue-toast-notification";
import { restore_version } from "./api";
interface RestoreVersionButtonProps {
storedObjectVersion: StoredObjectVersionPersisted,
storedObjectVersion: StoredObjectVersionPersisted;
}
const emit = defineEmits<{
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
}>()
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime];
}>();
const props = defineProps<RestoreVersionButtonProps>()
const props = defineProps<RestoreVersionButtonProps>();
const $toast = useToast();
@@ -19,14 +22,18 @@ const restore_version_fn = async () => {
const newVersion = await restore_version(props.storedObjectVersion);
$toast.success("Version restaurée");
emit('restoreVersion', {newVersion});
}
emit("restoreVersion", { newVersion });
};
</script>
<template>
<button class="btn btn-outline-action" @click="restore_version_fn" title="Restaurer"><i class="fa fa-rotate-left"></i> Restaurer</button>
<button
class="btn btn-outline-action"
@click="restore_version_fn"
title="Restaurer"
>
<i class="fa fa-rotate-left"></i> Restaurer
</button>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,12 +1,33 @@
import {StoredObject, StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
import {fetchResults, makeFetch} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import {
StoredObject,
StoredObjectVersionPersisted,
StoredObjectVersionWithPointInTime,
} from "../../../types";
import {
fetchResults,
makeFetch,
} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
export const get_versions = async (storedObject: StoredObject): Promise<StoredObjectVersionWithPointInTime[]> => {
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`);
export const get_versions = async (
storedObject: StoredObject,
): Promise<StoredObjectVersionWithPointInTime[]> => {
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(
`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`,
);
return versions.sort((a: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime) => b.version - a.version);
}
return versions.sort(
(
a: StoredObjectVersionWithPointInTime,
b: StoredObjectVersionWithPointInTime,
) => b.version - a.version,
);
};
export const restore_version = async (version: StoredObjectVersionPersisted): Promise<StoredObjectVersionWithPointInTime> => {
return await makeFetch<null, StoredObjectVersionWithPointInTime>("POST", `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`);
}
export const restore_version = async (
version: StoredObjectVersionPersisted,
): Promise<StoredObjectVersionWithPointInTime> => {
return await makeFetch<null, StoredObjectVersionWithPointInTime>(
"POST",
`/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`,
);
};

View File

@@ -1,5 +1,11 @@
<template>
<a :class="Object.assign(props.classes, {'btn': true})" @click="beforeLeave($event)" :href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)">
<a
:class="Object.assign(props.classes, { btn: true })"
@click="beforeLeave($event)"
:href="
build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
"
>
<i class="fa fa-paragraph"></i>
Editer en ligne
</a>
@@ -8,13 +14,16 @@
<script lang="ts" setup>
import WopiEditButton from "./WopiEditButton.vue";
import { build_wopi_editor_link } from "./helpers";
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
import {
StoredObject,
WopiEditButtonExecutableBeforeLeaveFunction,
} from "../../types";
interface WopiEditButtonConfig {
storedObject: StoredObject,
returnPath?: string,
classes: {[k: string] : boolean},
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
storedObject: StoredObject;
returnPath?: string;
classes: Record<string, boolean>;
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction;
}
const props = defineProps<WopiEditButtonConfig>();
@@ -43,4 +52,3 @@ i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@@ -1,108 +1,111 @@
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange, StoredObjectVersion} from "../../types";
import {
StoredObject,
StoredObjectStatus,
StoredObjectStatusChange,
StoredObjectVersion,
} from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
const MIMES_EDIT = new Set([
'application/vnd.ms-powerpoint',
'application/vnd.ms-excel',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-flat-xml',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-flat-xml',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-flat-xml',
'application/vnd.oasis.opendocument.chart',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/x-dif-document',
'text/spreadsheet',
'text/csv',
'application/x-dbase',
'text/rtf',
'text/plain',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
"application/vnd.ms-powerpoint",
"application/vnd.ms-excel",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-flat-xml",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-flat-xml",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-flat-xml",
"application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-flat-xml",
"application/vnd.oasis.opendocument.chart",
"application/msword",
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-word.document.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"application/vnd.ms-excel.sheet.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
"application/x-dif-document",
"text/spreadsheet",
"text/csv",
"application/x-dbase",
"text/rtf",
"text/plain",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
]);
const MIMES_VIEW = new Set([
...MIMES_EDIT,
[
'image/svg+xml',
'application/vnd.sun.xml.writer',
'application/vnd.sun.xml.calc',
'application/vnd.sun.xml.impress',
'application/vnd.sun.xml.draw',
'application/vnd.sun.xml.writer.global',
'application/vnd.sun.xml.writer.template',
'application/vnd.sun.xml.calc.template',
'application/vnd.sun.xml.impress.template',
'application/vnd.sun.xml.draw.template',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.text-master-template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.wordperfect',
'application/x-aportisdoc',
'application/x-hwp',
'application/vnd.ms-works',
'application/x-mswrite',
'application/vnd.lotus-1-2-3',
'image/cgm',
'image/vnd.dxf',
'image/x-emf',
'image/x-wmf',
'application/coreldraw',
'application/vnd.visio2013',
'application/vnd.visio',
'application/vnd.ms-visio.drawing',
'application/x-mspublisher',
'application/x-sony-bbeb',
'application/x-gnumeric',
'application/macwriteii',
'application/x-iwork-numbers-sffnumbers',
'application/vnd.oasis.opendocument.text-web',
'application/x-pagemaker',
'application/x-fictionbook+xml',
'application/clarisworks',
'image/x-wpg',
'application/x-iwork-pages-sffpages',
'application/x-iwork-keynote-sffkey',
'application/x-abiword',
'image/x-freehand',
'application/vnd.sun.xml.chart',
'application/x-t602',
'image/bmp',
'image/png',
'image/gif',
'image/tiff',
'image/jpg',
'image/jpeg',
'application/pdf',
]
])
"image/svg+xml",
"application/vnd.sun.xml.writer",
"application/vnd.sun.xml.calc",
"application/vnd.sun.xml.impress",
"application/vnd.sun.xml.draw",
"application/vnd.sun.xml.writer.global",
"application/vnd.sun.xml.writer.template",
"application/vnd.sun.xml.calc.template",
"application/vnd.sun.xml.impress.template",
"application/vnd.sun.xml.draw.template",
"application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-master-template",
"application/vnd.oasis.opendocument.spreadsheet-template",
"application/vnd.oasis.opendocument.presentation-template",
"application/vnd.oasis.opendocument.graphics-template",
"application/vnd.ms-word.template.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.ms-excel.template.macroEnabled.12",
"application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.ms-powerpoint.template.macroEnabled.12",
"application/vnd.wordperfect",
"application/x-aportisdoc",
"application/x-hwp",
"application/vnd.ms-works",
"application/x-mswrite",
"application/vnd.lotus-1-2-3",
"image/cgm",
"image/vnd.dxf",
"image/x-emf",
"image/x-wmf",
"application/coreldraw",
"application/vnd.visio2013",
"application/vnd.visio",
"application/vnd.ms-visio.drawing",
"application/x-mspublisher",
"application/x-sony-bbeb",
"application/x-gnumeric",
"application/macwriteii",
"application/x-iwork-numbers-sffnumbers",
"application/vnd.oasis.opendocument.text-web",
"application/x-pagemaker",
"application/x-fictionbook+xml",
"application/clarisworks",
"image/x-wpg",
"application/x-iwork-pages-sffpages",
"application/x-iwork-keynote-sffkey",
"application/x-abiword",
"image/x-freehand",
"application/vnd.sun.xml.chart",
"application/x-t602",
"image/bmp",
"image/png",
"image/gif",
"image/tiff",
"image/jpg",
"image/jpeg",
"application/pdf",
],
]);
export interface SignedUrlGet {
method: 'GET'|'HEAD',
url: string,
expires: number,
object_name: string,
method: "GET" | "HEAD";
url: string;
expires: number;
object_name: string;
}
function is_extension_editable(mimeType: string): boolean {
@@ -117,43 +120,56 @@ function build_convert_link(uuid: string) {
return `/chill/wopi/convert/${uuid}`;
}
function build_download_info_link(storedObject: StoredObject, atVersion: null|StoredObjectVersion): string {
function build_download_info_link(
storedObject: StoredObject,
atVersion: null | StoredObjectVersion,
): string {
const url = `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/get`;
if (null !== atVersion) {
const params = new URLSearchParams({ version: atVersion.filename });
return url + '?' + params.toString();
return url + "?" + params.toString();
}
return url;
}
async function download_info_link(storedObject: StoredObject, atVersion: null|StoredObjectVersion): Promise<SignedUrlGet> {
return makeFetch('GET', build_download_info_link(storedObject, atVersion));
async function download_info_link(
storedObject: StoredObject,
atVersion: null | StoredObjectVersion,
): Promise<SignedUrlGet> {
return makeFetch("GET", build_download_info_link(storedObject, atVersion));
}
function build_wopi_editor_link(uuid: string, returnPath?: string) {
if (returnPath === undefined) {
returnPath = window.location.pathname + window.location.search + window.location.hash;
returnPath =
window.location.pathname +
window.location.search +
window.location.hash;
}
return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath);
return (
`/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath)
);
}
function download_doc(url: string): Promise<Blob> {
return window.fetch(url).then(r => {
return window.fetch(url).then((r) => {
if (r.ok) {
return r.blob()
return r.blob();
}
throw new Error('Could not download document');
throw new Error("Could not download document");
});
}
async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: null|StoredObjectVersion): Promise<Blob>
{
const algo = 'AES-CBC';
async function download_and_decrypt_doc(
storedObject: StoredObject,
atVersion: null | StoredObjectVersion,
): Promise<Blob> {
const algo = "AES-CBC";
const atVersionToDownload = atVersion ?? storedObject.currentVersion;
@@ -162,18 +178,29 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
}
// sometimes, the downloadInfo may be embedded into the storedObject
console.log('storedObject', storedObject);
console.log("storedObject", storedObject);
let downloadInfo;
if (typeof storedObject._links !== 'undefined' && typeof storedObject._links.downloadLink !== 'undefined') {
if (
typeof storedObject._links !== "undefined" &&
typeof storedObject._links.downloadLink !== "undefined"
) {
downloadInfo = storedObject._links.downloadLink;
} else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
downloadInfo = await download_info_link(
storedObject,
atVersionToDownload,
);
}
const rawResponse = await window.fetch(downloadInfo.url);
if (!rawResponse.ok) {
throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText);
throw new Error(
"error while downloading raw file " +
rawResponse.status +
" " +
rawResponse.statusText,
);
}
if (atVersionToDownload.iv.length === 0) {
@@ -182,15 +209,23 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
const rawBuffer = await rawResponse.arrayBuffer();
try {
const key = await window.crypto.subtle
.importKey('jwk', atVersionToDownload.keyInfos, { name: algo }, false, ['decrypt']);
const key = await window.crypto.subtle.importKey(
"jwk",
atVersionToDownload.keyInfos,
{ name: algo },
false,
["decrypt"],
);
const iv = Uint8Array.from(atVersionToDownload.iv);
const decrypted = await window.crypto.subtle
.decrypt({ name: algo, iv: iv }, key, rawBuffer);
const decrypted = await window.crypto.subtle.decrypt(
{ name: algo, iv: iv },
key,
rawBuffer,
);
return Promise.resolve(new Blob([decrypted]));
} catch (e) {
console.error('encounter error while keys and decrypt operations');
console.error("encounter error while keys and decrypt operations");
console.error(e);
throw e;
@@ -203,14 +238,16 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
* If the document is already in a pdf on the server side, the document is retrieved "as is" from the usual
* storage.
*/
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob>
{
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
if (null === storedObject.currentVersion) {
throw new Error("the stored object does not count any version");
}
if (storedObject.currentVersion?.type === 'application/pdf') {
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
if (storedObject.currentVersion?.type === "application/pdf") {
return download_and_decrypt_doc(
storedObject,
storedObject.currentVersion,
);
}
const convertLink = build_convert_link(storedObject.uuid);
@@ -223,10 +260,12 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob>
return response.blob();
}
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
{
const new_status_response = await window
.fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`);
async function is_object_ready(
storedObject: StoredObject,
): Promise<StoredObjectStatusChange> {
const new_status_response = await window.fetch(
`/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`,
);
if (!new_status_response.ok) {
throw new Error("could not fetch the new status");

View File

@@ -51,11 +51,11 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
throw new \RuntimeException('no stored object found');
}
$this->entityManager->wrapInTransaction(function () use ($storedObject, $message, $signature) {
$this->storedObjectManager->write($storedObject, $message->content);
$this->signatureStepStateChanger->markSignatureAsSigned($signature, $message->signatureZoneIndex);
});
$this->entityManager->flush();
$this->entityManager->clear();
}
}

View File

@@ -25,6 +25,8 @@ use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
/**
@@ -34,6 +36,8 @@ use Psr\Log\NullLogger;
*/
class PdfSignedMessageHandlerTest extends TestCase
{
use ProphecyTrait;
public function testThatObjectIsWrittenInStoredObjectManagerHappyScenario(): void
{
// a dummy stored object
@@ -91,10 +95,19 @@ class PdfSignedMessageHandlerTest extends TestCase
private function buildEntityManager(bool $willFlush): EntityManagerInterface
{
$em = $this->createMock(EntityManagerInterface::class);
$em->expects($willFlush ? $this->once() : $this->never())->method('flush');
$em->expects($willFlush ? $this->once() : $this->never())->method('clear');
$em = $this->prophesize(EntityManagerInterface::class);
$clear = $em->clear();
$wrap = $em->wrapInTransaction(Argument::type('callable'))->will(function ($args) {
$callable = $args[0];
return $em;
return call_user_func($callable);
});
if ($willFlush) {
$clear->shouldBeCalled();
$wrap->shouldBeCalled();
}
return $em->reveal();
}
}

View File

@@ -1,10 +1,21 @@
module.exports = function(encore)
{
module.exports = function (encore) {
encore.addAliases({
ChillDocStoreAssets: __dirname + '/Resources/public'
ChillDocStoreAssets: __dirname + "/Resources/public",
});
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_download_button', __dirname + '/Resources/public/module/button_download/index');
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index');
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_download_button",
__dirname + "/Resources/public/module/button_download/index",
);
encore.addEntry(
"vue_document_signature",
__dirname + "/Resources/public/vuejs/DocumentSignature/index",
);
};

View File

@@ -1 +1 @@
require('./chillevent.scss');
require("./chillevent.scss");

Some files were not shown because too many files have changed in this diff Show More