From 9f5b11e6cc87605381dcf72c5e2fbe65bd1f4130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 31 Jan 2023 16:30:19 +0000 Subject: [PATCH] Feature: allow to convert to PDF from Chill and group action button on document BREAKING CHANGE: avoid using the macro for download button. To keep the UI clean, use always the new "group of action buttons". --- .../Resources/views/Activity/show.html.twig | 2 +- .../Activity/showAccompanyingCourse.html.twig | 2 + .../views/Activity/showPerson.html.twig | 4 +- .../document_action_buttons_group/index.ts | 35 ++++ .../Resources/public/types.ts | 25 +++ .../vuejs/DocumentActionButtonsGroup.vue | 63 +++++++ .../Resources/public/vuejs/README.md | 5 + .../StoredObjectButton/ConvertButton.vue | 46 +++++ .../StoredObjectButton/DownloadButton.vue | 53 ++++++ .../StoredObjectButton/WopiEditButton.vue | 44 +++++ .../vuejs/StoredObjectButton/helpers.ts | 164 ++++++++++++++++++ .../_workflow.html.twig | 15 +- .../index.html.twig | 4 +- .../AccompanyingCourseDocument/show.html.twig | 9 +- .../views/Button/button_group.html.twig | 7 + .../Resources/views/List/list_item.html.twig | 14 +- .../Resources/views/Macro/macro.html.twig | 33 +++- .../views/PersonDocument/index.html.twig | 4 +- .../views/PersonDocument/show.html.twig | 22 +-- .../Templating/WopiEditTwigExtension.php | 4 + .../WopiEditTwigExtensionRuntime.php | 37 +++- .../chill.webpack.config.js | 1 + .../Resources/views/Workflow/index.html.twig | 2 + .../components/FormEvaluation.vue | 111 +++--------- .../_objectifs_results_evaluations.html.twig | 2 +- .../AccompanyingCourseWork/show.html.twig | 10 +- .../Workflow/_evaluation_document.html.twig | 9 +- .../src/Controller/Convert.php | 108 ++++++++++++ .../src/Resources/config/routes/routes.php | 4 + .../tests/Controller/ConverTest.php | 92 ++++++++++ 30 files changed, 770 insertions(+), 161 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/types.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig create mode 100644 src/Bundle/ChillWopiBundle/src/Controller/Convert.php create mode 100644 src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig index 53bd3e8a7..1ec0ab274 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig @@ -168,7 +168,7 @@ {% if entity.documents|length > 0 %} {% else %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig index fbd7b20b4..3486f47bc 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig @@ -8,12 +8,14 @@ {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig index 5776eddb5..43a8eb86b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig @@ -7,13 +7,13 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts new file mode 100644 index 000000000..ec1d50a86 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts @@ -0,0 +1,35 @@ +import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; +import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue"; +import {createApp} from "vue"; +import {StoredObject} from "../../types"; + +const i18n = _createI18n({}); + +window.addEventListener('DOMContentLoaded', function (e) { + document.querySelectorAll('div[data-download-buttons]').forEach((el) => { + const app = createApp({ + components: {DocumentActionButtonsGroup}, + data() { + + const datasets = el.dataset as { + filename: string, + canEdit: string, + storedObject: string, + small: string, + }; + + const + storedObject = JSON.parse(datasets.storedObject), + filename = datasets.filename, + canEdit = datasets.canEdit === '1', + small = datasets.small === '1' + ; + + return { storedObject, filename, canEdit, small }; + }, + template: '', + }); + + app.use(i18n).mount(el); + }) +}); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts new file mode 100644 index 000000000..918526117 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -0,0 +1,25 @@ +import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; + +export interface StoredObject { + id: number, + + /** + * filename of the object in the object storage + */ + filename: string, + creationDate: DateTime, + datas: object, + iv: number[], + keyInfos: object, + title: string, + type: string, + uuid: string +} + +/** + * Function executed by the WopiEditButton component. + */ +export type WopiEditButtonExecutableBeforeLeaveFunction = { + (): Promise +} + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue new file mode 100644 index 000000000..cb72fa5cd --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md new file mode 100644 index 000000000..2d10dace8 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md @@ -0,0 +1,5 @@ +# About buttons and components available + +## DocumentActionButtonsGroup + +This is an component to use to render a group of button with actions linked to a document. diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue new file mode 100644 index 000000000..aa99a223f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue new file mode 100644 index 000000000..ca6a0f618 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue new file mode 100644 index 000000000..d68f60f86 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue @@ -0,0 +1,44 @@ + + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts new file mode 100644 index 000000000..dc746ace7 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts @@ -0,0 +1,164 @@ + +const SUPPORTED_MIMES = new Set([ + 'image/svg+xml', + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-excel', + 'application/vnd.sun.xml.writer', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-flat-xml', + 'application/vnd.sun.xml.calc', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', + 'application/vnd.sun.xml.impress', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-flat-xml', + 'application/vnd.sun.xml.draw', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-flat-xml', + 'application/vnd.oasis.opendocument.chart', + 'application/vnd.sun.xml.writer.global', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.sun.xml.writer.template', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.oasis.opendocument.text-master-template', + 'application/vnd.sun.xml.calc.template', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.sun.xml.impress.template', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.sun.xml.draw.template', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/msword', + '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.wordprocessingml.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.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/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/x-dif-document', + 'text/spreadsheet', + 'text/csv', + 'application/x-dbase', + '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', + 'text/rtf', + 'text/plain', + 'application/x-fictionbook+xml', + 'application/clarisworks', + 'image/x-wpg', + 'application/x-iwork-pages-sffpages', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + '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', +]); + +function is_extension_editable(mimeType: string): boolean { + return SUPPORTED_MIMES.has(mimeType); +} + +function build_convert_link(uuid: string) { + return `/chill/wopi/convert/${uuid}`; +} + +function build_download_info_link(object_name: string) { + return `/asyncupload/temp_url/generate/GET?object_name=${object_name}`; +} + +function build_wopi_editor_link(uuid: string, returnPath?: string) { + if (returnPath === undefined) { + returnPath = window.location.pathname + window.location.search + window.location.hash; + } + + return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath); +} + +function download_doc(url: string): Promise { + return window.fetch(url).then(r => { + if (r.ok) { + return r.blob() + } + + throw new Error('Could not download document'); + }); +} + +async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKey, iv: Uint8Array): Promise +{ + const algo = 'AES-CBC'; + // get an url to download the object + const downloadInfoResponse = await window.fetch(urlGenerator); + + if (!downloadInfoResponse.ok) { + throw new Error("error while downloading url " + downloadInfoResponse.status + " " + downloadInfoResponse.statusText); + } + + const downloadInfo = await downloadInfoResponse.json() as {url: string}; + const rawResponse = await window.fetch(downloadInfo.url); + + if (!rawResponse.ok) { + throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText); + } + + const rawBuffer = await rawResponse.arrayBuffer(); + + try { + const key = await window.crypto.subtle + .importKey('jwk', keyData, { name: algo }, false, ['decrypt']); + const decrypted = await window.crypto.subtle + .decrypt({ name: algo, iv: iv }, key, rawBuffer); + + return Promise.resolve(new Blob([decrypted])); + } catch (e) { + console.error('get error while keys and decrypt operations'); + console.error(e); + + throw e; + } +} + +export { + build_convert_link, + build_download_info_link, + build_wopi_editor_link, + download_and_decrypt_doc, + download_doc, + is_extension_editable, +}; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig index f914bd7f5..58d4903b4 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig @@ -46,21 +46,8 @@ {% endif %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, not freezed) }}
  • - {% if chill_document_is_editable(document.object) %} - {% if not freezed %} -
  • - {{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }} -
  • - {% else %} -
  • - - {{ 'Update document'|trans }} - -
  • - {% endif %} - {% endif %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig index df18efa71..7a013260c 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig @@ -8,16 +8,16 @@ {% block js %} {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_docgen_picktemplate') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_docgen_picktemplate') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig index 45ed3988b..3c62451a9 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig @@ -14,6 +14,7 @@ {{ parent() }} {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} @@ -61,13 +62,8 @@
  • {% endif %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title) }}
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %} {% if workflows_frame is not empty %}
  • @@ -86,4 +82,5 @@ {{ parent() }} {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig new file mode 100644 index 000000000..f83cafd51 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig @@ -0,0 +1,7 @@ +{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} +
    diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 3963b0715..4cbcb7bef 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -53,15 +53,10 @@
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% endif %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
  • @@ -80,15 +75,10 @@
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% endif %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig index 0e739d94b..199a86c15 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig @@ -2,13 +2,13 @@ {% if storedObject is null %} {% else %} - {{ 'Download'|trans }} {% endif %} @@ -28,4 +28,21 @@ data-mime-type="{{ storedObject.type|escape('html_attr') }}" {% if filename is not null %}data-filename="{{ filename|escape('html_attr') }}"{% endif %}> {{ 'Download'|trans }} {% endif %} -{% endmacro %} \ No newline at end of file +{% endmacro %} + +{% macro download_button_group(storedObject, canEdit = true, filename = null, options = {}) %} + {% if storedObject is null %} + + {% else %} +
    + {% endif %} +{% endmacro %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig index 5dc359fa8..8d201605e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig @@ -27,16 +27,16 @@ {% block js %} {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_docgen_picktemplate') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_docgen_picktemplate') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig index b26aa4b8b..c276e067e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig @@ -24,7 +24,11 @@ {% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} {% block js %} - {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} @@ -70,6 +74,10 @@
  • {% endif %} +
  • + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} +
  • + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
  • @@ -77,16 +85,4 @@
  • {% endif %} - -
  • - {{ m.download_button(document.object, document.title) }} -
  • - - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} - - {# {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} #} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php index 43dc28f15..754c3fb3c 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php @@ -24,6 +24,10 @@ class WopiEditTwigExtension extends AbstractExtension 'needs_environment' => true, 'is_safe' => ['html'], ]), + new TwigFilter('chill_document_button_group', [WopiEditTwigExtensionRuntime::class, 'renderButtonGroup'], [ + 'needs_environment' => true, + 'is_safe' => ['html'], + ]), ]; } diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index f53d6336b..bae436b0a 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php @@ -13,6 +13,8 @@ namespace Chill\DocStoreBundle\Templating; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use Chill\DocStoreBundle\Entity\StoredObject; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; use Twig\Extension\RuntimeExtensionInterface; @@ -112,20 +114,53 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface 'application/pdf', ]; + private const DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP = [ + 'small' => false, + ]; + private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig'; + private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig'; + private DiscoveryInterface $discovery; - public function __construct(DiscoveryInterface $discovery) + private NormalizerInterface $normalizer; + + public function __construct(DiscoveryInterface $discovery, NormalizerInterface $normalizer) { $this->discovery = $discovery; + $this->normalizer = $normalizer; } + /** + * return true if the document is editable. + * + * **NOTE**: as the Vue button does have similar test, this is not required if in use with + * the dedicated Vue component (GroupDownloadButton.vue, WopiEditButton.vue) + */ public function isEditable(StoredObject $document): bool { return in_array($document->getType(), self::SUPPORTED_MIMES, true); } + /** + * @param array{small: boolean} $options + * + * @throws \Twig\Error\LoaderError + * @throws \Twig\Error\RuntimeError + * @throws \Twig\Error\SyntaxError + */ + public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string + { + return $environment->render(self::TEMPLATE_BUTTON_GROUP, [ + 'document' => $document, + 'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]), + 'title' => $title, + 'can_edit' => $canEdit, + 'options' => array_merge($options, self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP), + ]); + } + public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string { return $environment->render(self::TEMPLATE, [ diff --git a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js index c9b2b8877..3499fcf55 100644 --- a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js +++ b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js @@ -4,4 +4,5 @@ module.exports = function(encore) ChillDocStoreAssets: __dirname + '/Resources/public' }); encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.js'); + encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index'); }; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig index 1a6f3103b..782eae136 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -11,6 +11,7 @@ {{ encore_entry_script_tags('mod_entity_workflow_subscribe') }} {{ encore_entry_script_tags('page_workflow_show') }} {{ encore_entry_script_tags('mod_wopi_link') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} @@ -19,6 +20,7 @@ {{ encore_entry_link_tags('mod_entity_workflow_subscribe') }} {{ encore_entry_link_tags('page_workflow_show') }} {{ encore_entry_link_tags('mod_wopi_link') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue index 66918ad71..4bf9e1a8a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue @@ -111,14 +111,12 @@
  • - - -
  • -
  • - +
  • @@ -174,6 +172,7 @@ import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue'; import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue'; import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; +import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; const i18n = { messages: { @@ -212,6 +211,7 @@ export default { AddAsyncUpload, AddAsyncUploadDownloader, ListWorkflowModal, + DocumentActionButtonsGroup, }, i18n, data() { @@ -223,78 +223,6 @@ export default { maxPostSize: 15000000, required: false, }, - mime: [ - // TODO temporary hardcoded. to be replaced by twig extension or a collabora server query - 'application/clarisworks', - 'application/coreldraw', - 'application/macwriteii', - 'application/msword', - 'application/vnd.lotus-1-2-3', - 'application/vnd.ms-excel', - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - 'application/vnd.ms-excel.sheet.macroEnabled.12', - 'application/vnd.ms-excel.template.macroEnabled.12', - 'application/vnd.ms-powerpoint', - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', - 'application/vnd.ms-powerpoint.template.macroEnabled.12', - 'application/vnd.ms-visio.drawing', - 'application/vnd.ms-word.document.macroEnabled.12', - 'application/vnd.ms-word.template.macroEnabled.12', - 'application/vnd.ms-works', - 'application/vnd.oasis.opendocument.chart', - 'application/vnd.oasis.opendocument.formula', - 'application/vnd.oasis.opendocument.graphics', - 'application/vnd.oasis.opendocument.graphics-flat-xml', - 'application/vnd.oasis.opendocument.graphics-template', - 'application/vnd.oasis.opendocument.presentation', - 'application/vnd.oasis.opendocument.presentation-flat-xml', - 'application/vnd.oasis.opendocument.presentation-template', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', - 'application/vnd.oasis.opendocument.spreadsheet-template', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.oasis.opendocument.text-flat-xml', - 'application/vnd.oasis.opendocument.text-master', - 'application/vnd.oasis.opendocument.text-master-template', - 'application/vnd.oasis.opendocument.text-template', - 'application/vnd.oasis.opendocument.text-web', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'application/vnd.sun.xml.calc', - 'application/vnd.sun.xml.calc.template', - 'application/vnd.sun.xml.chart', - 'application/vnd.sun.xml.draw', - 'application/vnd.sun.xml.draw.template', - 'application/vnd.sun.xml.impress', - 'application/vnd.sun.xml.impress.template', - 'application/vnd.sun.xml.math', - 'application/vnd.sun.xml.writer', - 'application/vnd.sun.xml.writer.global', - 'application/vnd.sun.xml.writer.template', - 'application/vnd.visio', - 'application/vnd.visio2013', - 'application/vnd.wordperfect', - 'application/x-abiword', - 'application/x-aportisdoc', - 'application/x-dbase', - 'application/x-dif-document', - 'application/x-fictionbook+xml', - 'application/x-gnumeric', - 'application/x-hwp', - 'application/x-iwork-keynote-sffkey', - 'application/x-iwork-numbers-sffnumbers', - 'application/x-iwork-pages-sffpages', - 'application/x-mspublisher', - 'application/x-mswrite', - 'application/x-pagemaker', - 'application/x-sony-bbeb', - 'application/x-t602', - ] } }, computed: { @@ -343,10 +271,6 @@ export default { }, methods: { ISOToDatetime, - canEditDocument(document) { - return 'storedObject' in document ? - this.mime.includes(document.storedObject.type) : false; - }, listAllStatus() { console.log('load all status'); let url = `/api/`; @@ -359,10 +283,25 @@ export default { }) ; }, - buildEditLink(storedObject) { - return `/chill/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent( + buildEditLink(document) { + return `/chill/wopi/edit/${document.storedObject.uuid}?returnPath=` + encodeURIComponent( window.location.pathname + window.location.search + window.location.hash); }, + submitBeforeLeaveToEditor() { + console.log('submit beore edit 2'); + // empty callback + const callback = () => null; + return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; }); + }, + submitBeforeEdit(storedObject) { + const callback = (data) => { + let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key); + let document = evaluation.documents.find(d => d.storedObject.id === storedObject.id); + //console.log('=> document', document); + window.location.assign(this.buildEditLink(document)); + }; + return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; }); + }, submitBeforeGenerate({template}) { const callback = (data) => { let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id; diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/_objectifs_results_evaluations.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/_objectifs_results_evaluations.html.twig index aa3838348..63e4ff638 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/_objectifs_results_evaluations.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/_objectifs_results_evaluations.html.twig @@ -142,7 +142,7 @@ {{ mm.mimeIcon(d.storedObject.type) }}
    - {{ m.download_button_small(d.storedObject, d.title) }} + {{ d.storedObject|chill_document_button_group(d.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w), {'small': true}) }}
    {% endfor %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/show.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/show.html.twig index 9e47b61d1..bddffebdf 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/show.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/show.html.twig @@ -6,20 +6,20 @@ {% block css %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block js %} {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %}
    {% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig index 5ab93896a..e5d8f9c9a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation_document.html.twig @@ -120,20 +120,13 @@ {% if display_action is defined and display_action == true %} - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork) %} - {% endif %} {% endif %} {% endif %} diff --git a/src/Bundle/ChillWopiBundle/src/Controller/Convert.php b/src/Bundle/ChillWopiBundle/src/Controller/Convert.php new file mode 100644 index 000000000..0a4dc8762 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Controller/Convert.php @@ -0,0 +1,108 @@ +httpClient = $httpClient; + $this->security = $security; + $this->storedObjectManager = $storedObjectManager; + $this->logger = $logger; + $this->collaboraDomain = $parameters->get('wopi')['server']; + } + + public function __invoke(StoredObject $storedObject): Response + { + if (!$this->security->getUser() instanceof User) { + throw new AccessDeniedHttpException('User must be authenticated'); + } + + $content = $this->storedObjectManager->read($storedObject); + + try { + $url = sprintf('%s/cool/convert-to/pdf', $this->collaboraDomain); + $form = new FormDataPart([ + 'data' => new DataPart($content, $storedObject->getUuid()->toString(), $storedObject->getType()), + ]); + $response = $this->httpClient->request('POST', $url, [ + 'headers' => $form->getPreparedHeaders()->toArray(), + 'body' => $form->bodyToString(), + 'timeout' => 10, + ]); + + return new Response($response->getContent(), Response::HTTP_OK, [ + 'Content-Type' => 'application/pdf', + ]); + } catch (ClientExceptionInterface $exception) { + return $this->onConversionFailed($url, $response); + } catch (RedirectionExceptionInterface $e) { + return $this->onConversionFailed($url, $response); + } catch (ServerExceptionInterface $e) { + return $this->onConversionFailed($url, $response); + } catch (TransportExceptionInterface $e) { + return $this->onConversionFailed($url, $response); + } + } + + private function onConversionFailed(string $url, ResponseInterface $response): JsonResponse + { + $this->logger->error(self::LOG_PREFIX . ' could not convert document', [ + 'response_status' => $response->getStatusCode(), + 'message' => $response->getContent(false), + 'server' => $this->collaboraDomain, + 'url' => $url, + ]); + + return new JsonResponse(['message' => 'conversion failed : ' . $response->getContent(false)], Response::HTTP_SERVICE_UNAVAILABLE); + } +} diff --git a/src/Bundle/ChillWopiBundle/src/Resources/config/routes/routes.php b/src/Bundle/ChillWopiBundle/src/Resources/config/routes/routes.php index ddf4d5531..141799207 100644 --- a/src/Bundle/ChillWopiBundle/src/Resources/config/routes/routes.php +++ b/src/Bundle/ChillWopiBundle/src/Resources/config/routes/routes.php @@ -16,4 +16,8 @@ return static function (RoutingConfigurator $routes) { $routes ->add('chill_wopi_file_edit', '/edit/{fileId}') ->controller(Editor::class); + + $routes + ->add('chill_wopi_object_convert', '/convert/{uuid}') + ->controller(\Chill\WopiBundle\Controller\Convert::class); }; diff --git a/src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php b/src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php new file mode 100644 index 000000000..27d162a34 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php @@ -0,0 +1,92 @@ +setType('application/vnd.oasis.opendocument.text'); + + $httpClient = new MockHttpClient([ + new MockResponse('not authorized', ['http_code' => 401]), + ], 'http://collabora:9980'); + + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn(new User()); + + $storeManager = $this->prophesize(StoredObjectManagerInterface::class); + $storeManager->read($storedObject)->willReturn('content'); + + $parameterBag = new ParameterBag(['wopi' => ['server' => 'http://collabora:9980']]); + + $convert = new Convert( + $httpClient, + $security->reveal(), + $storeManager->reveal(), + new NullLogger(), + $parameterBag + ); + + $response = $convert($storedObject); + + $this->assertNotEquals(200, $response->getStatusCode()); + } + + public function testEverythingWentFine(): void + { + $storedObject = (new StoredObject())->setType('application/vnd.oasis.opendocument.text'); + + $httpClient = new MockHttpClient([ + new MockResponse('1234', ['http_code' => 200]), + ], 'http://collabora:9980'); + + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn(new User()); + + $storeManager = $this->prophesize(StoredObjectManagerInterface::class); + $storeManager->read($storedObject)->willReturn('content'); + + $parameterBag = new ParameterBag(['wopi' => ['server' => 'http://collabora:9980']]); + + $convert = new Convert( + $httpClient, + $security->reveal(), + $storeManager->reveal(), + new NullLogger(), + $parameterBag + ); + + $response = $convert($storedObject); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('1234', $response->getContent()); + } +}