mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	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".
This commit is contained in:
		| @@ -168,7 +168,7 @@ | ||||
|                     {% if entity.documents|length > 0 %} | ||||
|                         <ul> | ||||
|                             {% for d in entity.documents %} | ||||
|                                 <li>{{ d.title }}{{ m.download_button(d) }}</li> | ||||
|                                 <li>{{ d.title }} {{ d|chill_document_button_group() }}</li> | ||||
|                             {% endfor %} | ||||
|                         </ul> | ||||
|                     {% else %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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<HTMLDivElement>('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: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small"></document-action-buttons-group>', | ||||
|      }); | ||||
|  | ||||
|      app.use(i18n).mount(el); | ||||
|   }) | ||||
| }); | ||||
							
								
								
									
										25
									
								
								src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<void> | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,63 @@ | ||||
| <template> | ||||
|   <div class="dropdown"> | ||||
|     <button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, small: props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|       Actions | ||||
|     </button> | ||||
|     <ul class="dropdown-menu"> | ||||
|       <li v-if="props.canEdit && is_extension_editable(props.storedObject.type)"> | ||||
|         <wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button> | ||||
|       </li> | ||||
|       <li v-if="props.storedObject.type != 'application/pdf' && props.canConvertPdf"> | ||||
|         <convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button> | ||||
|       </li> | ||||
|       <li v-if="props.canDownload"> | ||||
|         <download-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></download-button> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|  | ||||
| import ConvertButton from "./StoredObjectButton/ConvertButton.vue"; | ||||
| import DownloadButton from "./StoredObjectButton/DownloadButton.vue"; | ||||
| import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue"; | ||||
| import {is_extension_editable} from "./StoredObjectButton/helpers"; | ||||
| import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../types"; | ||||
|  | ||||
| interface DocumentActionButtonsGroupConfig { | ||||
|   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 | ||||
|    * (the document will be saved on his disk with this name) | ||||
|    * | ||||
|    * If not set, 'document' will be used. | ||||
|    */ | ||||
|   filename?: string, | ||||
|  | ||||
|   /** | ||||
|    * If set, will execute this function before leaving to the editor | ||||
|    */ | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), { | ||||
|   small: false, | ||||
|   canEdit: true, | ||||
|   canDownload: true, | ||||
|   canConvertPdf: true, | ||||
|   returnPath: window.location.pathname + window.location.search + window.location.hash, | ||||
| }); | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|  | ||||
| </style> | ||||
| @@ -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. | ||||
| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
|   <a :class="props.classes" @click="download_and_open($event)"> | ||||
|     <i class="fa fa-file-pdf-o"></i> | ||||
|     Télécharger en pdf | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|  | ||||
| import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers"; | ||||
| import mime from "mime"; | ||||
| import {reactive} from "vue"; | ||||
| import {StoredObject} from "../../types"; | ||||
|  | ||||
| interface ConvertButtonConfig { | ||||
|   storedObject: StoredObject, | ||||
|   classes: { [key: string]: boolean}, | ||||
|   filename?: string, | ||||
| }; | ||||
|  | ||||
| interface DownloadButtonState { | ||||
|   content: null|string | ||||
| } | ||||
|  | ||||
| const props = defineProps<ConvertButtonConfig>(); | ||||
| const state: DownloadButtonState = reactive({content: null}); | ||||
|  | ||||
| async function download_and_open(event: Event): Promise<void> { | ||||
|   const button = event.target as HTMLAnchorElement; | ||||
|  | ||||
|   if (null === state.content) { | ||||
|     event.preventDefault(); | ||||
|  | ||||
|     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.download = (props.filename + '.pdf') || 'document.pdf'; | ||||
|   } | ||||
|  | ||||
|   button.click(); | ||||
| } | ||||
|  | ||||
| </script> | ||||
| @@ -0,0 +1,53 @@ | ||||
| <template> | ||||
|   <a :class="props.classes" @click="download_and_open($event)"> | ||||
|     <i class="fa fa-download"></i> | ||||
|     Télécharger | ||||
|   </a> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import {reactive} from "vue"; | ||||
| import {build_download_info_link, download_and_decrypt_doc} from "./helpers"; | ||||
| import mime from "mime"; | ||||
| import {StoredObject} from "../../types"; | ||||
|  | ||||
| interface DownloadButtonConfig { | ||||
|   storedObject: StoredObject, | ||||
|   classes: {[k: string]: boolean}, | ||||
|   filename?: string, | ||||
| } | ||||
|  | ||||
| interface DownloadButtonState { | ||||
|   content: null|string | ||||
| } | ||||
|  | ||||
| const props = defineProps<DownloadButtonConfig>(); | ||||
| const state: DownloadButtonState = reactive({content: null}); | ||||
|  | ||||
| async function download_and_open(event: Event): Promise<void> { | ||||
|   const button = event.target as HTMLAnchorElement; | ||||
|  | ||||
|   if (null === state.content) { | ||||
|     event.preventDefault(); | ||||
|  | ||||
|     const urlInfo = build_download_info_link(props.storedObject.filename); | ||||
|  | ||||
|     const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv)); | ||||
|     state.content = window.URL.createObjectURL(raw); | ||||
|  | ||||
|     button.href = window.URL.createObjectURL(raw); | ||||
|     button.type = props.storedObject.type; | ||||
|  | ||||
|     if (props.filename !== undefined) { | ||||
|       button.download = props.filename || 'document'; | ||||
|  | ||||
|       const ext = mime.getExtension(props.storedObject.type); | ||||
|       if (null !== ext) { | ||||
|         button.download = button.download + '.' + ext; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   button.click(); | ||||
| } | ||||
| </script> | ||||
| @@ -0,0 +1,44 @@ | ||||
| <template> | ||||
|   <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> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import WopiEditButton from "./WopiEditButton.vue"; | ||||
| import {build_wopi_editor_link} from "./helpers"; | ||||
| import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types"; | ||||
|  | ||||
| interface WopiEditButtonConfig { | ||||
|   storedObject: StoredObject, | ||||
|   returnPath?: string, | ||||
|   classes: {[k: string] : boolean}, | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, | ||||
| } | ||||
|  | ||||
| const props = defineProps<WopiEditButtonConfig>(); | ||||
|  | ||||
| let executed = false; | ||||
|  | ||||
| async function beforeLeave(event: Event): Promise<true> { | ||||
|   console.log(executed); | ||||
|   if (props.executeBeforeLeave === undefined || executed === true) { | ||||
|     return Promise.resolve(true); | ||||
|   } | ||||
|  | ||||
|   event.preventDefault(); | ||||
|  | ||||
|   await props.executeBeforeLeave(); | ||||
|   executed = true; | ||||
|  | ||||
|   const link = event.target as HTMLAnchorElement; | ||||
|   link.click(); | ||||
|  | ||||
|   return Promise.resolve(true); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="sass"> | ||||
| </style> | ||||
|  | ||||
| @@ -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<Blob> { | ||||
|   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<Blob> | ||||
| { | ||||
|    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, | ||||
| }; | ||||
| @@ -46,21 +46,8 @@ | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             <li> | ||||
|                 {{ m.download_button(document.object, document.title) }} | ||||
|                 {{ document.object|chill_document_button_group(document.title, not freezed) }} | ||||
|             </li> | ||||
|             {% if chill_document_is_editable(document.object) %} | ||||
|                 {% if not freezed %} | ||||
|                 <li> | ||||
|                     {{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }} | ||||
|                 </li> | ||||
|                 {% else %} | ||||
|                     <li> | ||||
|                         <a class="btn btn-wopilink disabled" href="#" title="{{ 'workflow.freezed document'|trans }}"> | ||||
|                             {{ 'Update document'|trans }} | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|             {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %} | ||||
|                 <li> | ||||
|                     <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a> | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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 @@ | ||||
|             </li> | ||||
|         {% endif %} | ||||
|         <li> | ||||
|             {{ m.download_button(document.object, document.title) }} | ||||
|             {{ document.object|chill_document_button_group(document.title) }} | ||||
|         </li> | ||||
|         {% if chill_document_is_editable(document.object) %} | ||||
|             <li> | ||||
|                 {{ document.object|chill_document_edit_button }} | ||||
|             </li> | ||||
|         {% endif %} | ||||
|         {% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %} | ||||
|         {% if workflows_frame is not empty %} | ||||
|             <li> | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| {%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} | ||||
| <div | ||||
|     data-download-buttons | ||||
|     data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" | ||||
|     data-can-edit="{{ can_edit ? '1' : '0' }}" | ||||
|     {% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %} | ||||
|     {% if title|default(document.title)|default(null) is not null %}data-filename="{{ title|default(document.title)|escape('html_attr') }}"{% endif %}></div> | ||||
| @@ -53,15 +53,10 @@ | ||||
|                     <li> | ||||
|                         <a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a> | ||||
|                     </li> | ||||
|                     {% if chill_document_is_editable(document.object) %} | ||||
|                         <li> | ||||
|                             {{ document.object|chill_document_edit_button }} | ||||
|                         </li> | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|                 {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} | ||||
|                     <li> | ||||
|                         {{ m.download_button(document.object, document.title) }} | ||||
|                         {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }} | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a> | ||||
| @@ -80,15 +75,10 @@ | ||||
|                     <li> | ||||
|                         <a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a> | ||||
|                     </li> | ||||
|                     {% if chill_document_is_editable(document.object) %} | ||||
|                         <li> | ||||
|                             {{ document.object|chill_document_edit_button }} | ||||
|                         </li> | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|                 {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} | ||||
|                     <li> | ||||
|                         {{ m.download_button(document.object, document.title) }} | ||||
|                         {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} | ||||
|                     </li> | ||||
|                     <li> | ||||
|                         <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a> | ||||
|   | ||||
| @@ -2,13 +2,13 @@ | ||||
|     {% if storedObject is null %} | ||||
|         <!-- No document to download --> | ||||
|     {% else %} | ||||
|         <a class="btn btn-download"  | ||||
|            data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}"  | ||||
|            data-label-ready="{{ 'Ready to show'|trans|escape('html_attr') }}"  | ||||
|            data-download-button  | ||||
|            data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}"  | ||||
|            data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}"  | ||||
|            data-temp-url-get-generator="{{ storedObject|generate_url|escape('html_attr') }}"  | ||||
|         <a class="btn btn-download" | ||||
|            data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}" | ||||
|            data-label-ready="{{ 'Ready to show'|trans|escape('html_attr') }}" | ||||
|            data-download-button | ||||
|            data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}" | ||||
|            data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}" | ||||
|            data-temp-url-get-generator="{{ storedObject|generate_url|escape('html_attr') }}" | ||||
|            data-mime-type="{{ storedObject.type|escape('html_attr') }}" {% if filename is not null %}data-filename="{{ filename|escape('html_attr') }}"{% endif %}> | ||||
|             {{ 'Download'|trans }}</a> | ||||
|     {% 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 }}</a> | ||||
|     {% endif %} | ||||
| {% endmacro %} | ||||
| {% endmacro %} | ||||
|  | ||||
| {% macro download_button_group(storedObject, canEdit = true, filename = null, options = {}) %} | ||||
|     {% if storedObject is null %} | ||||
|         <!-- No document to download --> | ||||
|     {% else %} | ||||
|         <div | ||||
|             data-download-buttons | ||||
|             data-uuid="{{ storedObject.uuid|escape('html_attr') }}" | ||||
|             data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}" | ||||
|             data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}" | ||||
|             data-temp-url-generator="{{ storedObject|generate_url|escape('html_attr') }}" | ||||
|             data-mime-type="{{ storedObject.type|escape('html_attr') }}" | ||||
|             data-can-edit="{{ canEdit ? '1' : '0' }}" | ||||
|             {% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %} | ||||
|             {% if filename|default(storedObject.title)|default(null) is not null %}data-filename="{{ filename|default(storedObject.title)|escape('html_attr') }}"{% endif %}></div> | ||||
|     {% endif %} | ||||
| {% endmacro %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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 @@ | ||||
|             </li> | ||||
|         {% endif %} | ||||
|  | ||||
|         <li> | ||||
|             {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} | ||||
|         </li> | ||||
|  | ||||
|         {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} | ||||
|             <li> | ||||
|                 <a href="{{ path('person_document_edit', {'id': document.id, 'person': person.id}) }}" class="btn btn-edit"> | ||||
| @@ -77,16 +85,4 @@ | ||||
|                 </a> | ||||
|             </li> | ||||
|         {% endif %} | ||||
|  | ||||
|         <li> | ||||
|             {{ m.download_button(document.object, document.title) }} | ||||
|         </li> | ||||
|  | ||||
|         {% if chill_document_is_editable(document.object) %} | ||||
|             <li> | ||||
|                 {{ document.object|chill_document_edit_button }} | ||||
|             </li> | ||||
|         {% endif %} | ||||
|  | ||||
|         {# {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} #} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -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'], | ||||
|             ]), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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, [ | ||||
|   | ||||
| @@ -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'); | ||||
| }; | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -111,14 +111,12 @@ | ||||
|                      </add-async-upload> | ||||
|                    </li> | ||||
|                    <li> | ||||
|                      <add-async-upload-downloader | ||||
|                         :buttonTitle="$t('download')" | ||||
|                         :storedObject="d.storedObject" | ||||
|                      > | ||||
|                      </add-async-upload-downloader> | ||||
|                    </li> | ||||
|                    <li v-if="canEditDocument(d)"> | ||||
|                      <a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a> | ||||
|                      <document-action-buttons-group | ||||
|                          :stored-object="d.storedObject" | ||||
|                          :filename="d.title" | ||||
|                          :can-edit="true" | ||||
|                          :execute-before-leave="submitBeforeLeaveToEditor" | ||||
|                      ></document-action-buttons-group> | ||||
|                    </li> | ||||
|                    <li v-if="d.workflows.length === 0"> | ||||
|                      <a class="btn btn-delete" @click="removeDocument(d)"> | ||||
| @@ -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; | ||||
|   | ||||
| @@ -142,7 +142,7 @@ | ||||
|                                             {{ mm.mimeIcon(d.storedObject.type) }} | ||||
|                                         </div> | ||||
|                                         <div class="col col-lg-4 text-end"> | ||||
|                                             {{ 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}) }} | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 {% endfor %} | ||||
|   | ||||
| @@ -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 %} | ||||
|     <div class="accompanying-course-work"> | ||||
|         <h1>{{ block('title') }}</h1> | ||||
|      | ||||
|  | ||||
|         <div class="flex-table mt-4"> | ||||
|             {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { | ||||
|                 'w': work, | ||||
| @@ -29,7 +29,7 @@ | ||||
|             } %} | ||||
|             <div class="p-3 mt-3">{{ macro.metadata(work) }}</div> | ||||
|         </div> | ||||
|      | ||||
|  | ||||
|         <ul class="record_actions sticky-form-buttons"> | ||||
|             <li class="cancel"> | ||||
|                 <a href="{{ path('chill_person_accompanying_period_work_list', { 'id': accompanyingCourse.id }) }}" | ||||
| @@ -51,7 +51,7 @@ | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|         </ul> | ||||
|          | ||||
|  | ||||
|     </div> | ||||
| {% endblock %} | ||||
|  | ||||
|   | ||||
| @@ -120,20 +120,13 @@ | ||||
|     </div> | ||||
|  | ||||
|     {% if display_action is defined and display_action == true %} | ||||
|         {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork) %} | ||||
|         <ul class="record_actions"> | ||||
|             <li>{{ m.download_button(doc.storedObject, doc.title) }}</li> | ||||
|             {% if chill_document_is_editable(doc.storedObject) %} | ||||
|                 <li> | ||||
|                     {{ doc.storedObject|chill_document_edit_button }} | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             <li>{{ doc.storedObject|chill_document_button_group(doc.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork)) }}</li> | ||||
|             <li> | ||||
|                 <a class="btn btn-show" href="{{ path('chill_person_accompanying_period_work_edit', {'id': evaluation.accompanyingPeriodWork.id}) }}"> | ||||
|                     {{ 'Show'|trans }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|         {% endif %} | ||||
|     {% endif %} | ||||
| {% endif %} | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/Bundle/ChillWopiBundle/src/Controller/Convert.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/Bundle/ChillWopiBundle/src/Controller/Convert.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\WopiBundle\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManager; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Mime\Part\DataPart; | ||||
| use Symfony\Component\Mime\Part\Multipart\FormDataPart; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | ||||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | ||||
| use Symfony\Contracts\HttpClient\ResponseInterface; | ||||
|  | ||||
| class Convert | ||||
| { | ||||
|     private const LOG_PREFIX = '[convert] '; | ||||
|  | ||||
|     private string $collaboraDomain; | ||||
|  | ||||
|     private HttpClientInterface $httpClient; | ||||
|  | ||||
|     private LoggerInterface $logger; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private StoredObjectManagerInterface $storedObjectManager; | ||||
|  | ||||
|     /** | ||||
|      * @param StoredObjectManager $storedObjectManager | ||||
|      */ | ||||
|     public function __construct( | ||||
|         HttpClientInterface $httpClient, | ||||
|         Security $security, | ||||
|         StoredObjectManagerInterface $storedObjectManager, | ||||
|         LoggerInterface $logger, | ||||
|         ParameterBagInterface $parameters | ||||
|     ) { | ||||
|         $this->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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										92
									
								
								src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/Bundle/ChillWopiBundle/tests/Controller/ConverTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\WopiBundle\Tests\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\WopiBundle\Controller\Convert; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; | ||||
| use Symfony\Component\HttpClient\MockHttpClient; | ||||
| use Symfony\Component\HttpClient\Response\MockResponse; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| final class ConverTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testConversionFailed(): void | ||||
|     { | ||||
|         $storedObject = (new StoredObject())->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()); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user