You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
21KB

  1. {# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
  2. {# @var entities \EasyCorp\Bundle\EasyAdminBundle\Collection\EntityDtoCollection #}
  3. {# @var paginator \EasyCorp\Bundle\EasyAdminBundle\Orm\EntityPaginator #}
  4. {% extends ea.templatePath('layout') %}
  5. {% trans_default_domain ea.i18n.translationDomain %}
  6. {% block body_id entities|length > 0 ? 'ea-index-' ~ entities|first.name : '' %}
  7. {% block body_class 'index' ~ (entities|length > 0 ? ' index-' ~ entities|first.name : '') %}
  8. {% block content_title %}
  9. {%- apply spaceless -%}
  10. {% set default_title = ea.crud.defaultPageTitle('index')|trans(ea.i18n.translationParameters, 'EasyAdminBundle') %}
  11. {{ ea.crud.customPageTitle is null ? default_title|raw : ea.crud.customPageTitle('index')|trans(ea.i18n.translationParameters)|raw }}
  12. {%- endapply -%}
  13. {% endblock %}
  14. {% set has_batch_actions = batch_actions|length > 0 %}
  15. {% block page_actions %}
  16. {% block global_actions %}
  17. <div class="global-actions">
  18. {% for action in global_actions %}
  19. {{ include(action.templatePath, { action: action }, with_context = false) }}
  20. {% endfor %}
  21. </div>
  22. {% endblock global_actions %}
  23. {% block batch_actions %}
  24. {% if has_batch_actions %}
  25. <div class="batch-actions" style="display: none">
  26. {% for action in batch_actions %}
  27. {{ include(action.templatePath, { action: action }, with_context = false) }}
  28. {% endfor %}
  29. </div>
  30. {% endif %}
  31. {% endblock %}
  32. {% endblock page_actions %}
  33. {% block main %}
  34. {# sort can be multiple; let's consider the sorting field the first one #}
  35. {% set sort_field_name = app.request.get('sort')|keys|first %}
  36. {% set sort_order = app.request.get('sort')|first %}
  37. {% set some_results_are_hidden = false %}
  38. {% set has_footer = entities|length != 0 %}
  39. {% set has_search = ea.crud.isSearchEnabled %}
  40. {% set has_filters = filters|length > 0 %}
  41. {% set has_datagrid_tools = has_search or has_filters %}
  42. <div class="card card-table card-outline card-primary">
  43. <div class="card-header">
  44. <span data-toggle="tooltip" class="badge badge-light" data-original-title="Total" title="Total">
  45. {{ paginator.numResults }} résultats
  46. </span>
  47. </div>
  48. <div class="card-body">
  49. <div class="table-responsive">
  50. <table class="table table-bordered table-hover table-striped">
  51. <thead>
  52. {% block table_head %}
  53. <tr>
  54. {% if has_batch_actions %}
  55. <th class=""><span><input type="checkbox"
  56. class="form-check-input m-0 align-middle form-batch-checkbox-all"></span>
  57. </th>
  58. {% endif %}
  59. {% set ea_sort_asc = constant('EasyCorp\\Bundle\\EasyAdminBundle\\Config\\Option\\SortOrder::ASC') %}
  60. {% set ea_sort_desc = constant('EasyCorp\\Bundle\\EasyAdminBundle\\Config\\Option\\SortOrder::DESC') %}
  61. {% for field in entities|first.fields ?? [] %}
  62. {% set is_sorting_field = ea.search.isSortingField(field.property) %}
  63. {% set next_sort_direction = is_sorting_field ? (ea.search.sortDirection(field.property) == ea_sort_desc ? ea_sort_asc : ea_sort_desc) : ea_sort_desc %}
  64. {% set column_icon = is_sorting_field ? (next_sort_direction == ea_sort_desc ? 'fa-arrow-up' : 'fa-arrow-down') : 'fa-sort' %}
  65. <th class="{{ is_sorting_field ? 'sorted' }} {{ field.isVirtual ? 'field-virtual' }} {% if field.textAlign %}text-{{ field.textAlign }}{% endif %}"
  66. dir="{{ ea.i18n.textDirection }}">
  67. {% if field.isSortable %}
  68. <a href="{{ ea_url({ page: 1, sort: { (field.property): next_sort_direction } }).includeReferrer() }}">
  69. {{ field.label|raw }} <i class="fa fa-fw {{ column_icon }}"></i>
  70. </a>
  71. {% else %}
  72. <span>{{ field.label|raw }}</span>
  73. {% endif %}
  74. </th>
  75. {% endfor %}
  76. <th class="w-1" {% if ea.crud.showEntityActionsAsDropdown %}width="10px"{% endif %} dir="{{ ea.i18n.textDirection }}">
  77. <span class="sr-only">{{ 'action.entity_actions'|trans(ea.i18n.translationParameters, 'EasyAdminBundle') }}</span>
  78. </th>
  79. </tr>
  80. {% endblock table_head %}
  81. </thead>
  82. <tbody>
  83. {% block table_body %}
  84. {% for entity in entities %}
  85. {% if not entity.isAccessible %}
  86. {% set some_results_are_hidden = true %}
  87. {% else %}
  88. <tr data-id="{{ entity.primaryKeyValueAsString }}">
  89. {% if has_batch_actions %}
  90. <td><input type="checkbox" class="form-batch-checkbox"
  91. value="{{ entity.primaryKeyValue }}"></td>
  92. {% endif %}
  93. {% for field in entity.fields %}
  94. <td class="{{ field.property == sort_field_name ? 'sorted' }} text-{{ field.textAlign }} {{ field.cssClass }}"
  95. dir="{{ ea.i18n.textDirection }}">
  96. {{ include(field.templatePath, { field: field, entity: entity }, with_context = false) }}
  97. </td>
  98. {% endfor %}
  99. {% block entity_actions %}
  100. <td class="actions">
  101. {% if not ea.crud.showEntityActionsAsDropdown %}
  102. {% for action in entity.actions %}
  103. {{ include(action.templatePath, { action: action, entity: entity, isIncludedInDropdown: ea.crud.showEntityActionsAsDropdown }, with_context = false) }}
  104. {% endfor %}
  105. {% else %}
  106. <div class="dropdown dropdown-actions">
  107. <a class="dropdown-toggle btn btn-secondary btn-sm" href="#"
  108. role="button" data-toggle="dropdown" aria-haspopup="true"
  109. aria-expanded="false">
  110. <i class="fa fa-fw fa-ellipsis-h"></i>
  111. </a>
  112. <div class="dropdown-menu dropdown-menu-right">
  113. {% for action in entity.actions %}
  114. {{ include(action.templatePath, { action: action, isIncludedInDropdown: ea.crud.showEntityActionsAsDropdown }, with_context = false) }}
  115. {% endfor %}
  116. </div>
  117. </div>
  118. {% endif %}
  119. </td>
  120. {% endblock entity_actions %}
  121. </tr>
  122. {% endif %}
  123. {% else %}
  124. <tr>
  125. <td class="no-results" colspan="100">
  126. {{ 'datagrid.no_results'|trans(ea.i18n.translationParameters, 'EasyAdminBundle') }}
  127. </td>
  128. </tr>
  129. {% endfor %}
  130. {% if some_results_are_hidden %}
  131. <tr class="datagrid-row-empty">
  132. <td class="text-center" colspan="{{ entities|first.fields|length + 1 }}">
  133. <span class="datagrid-row-empty-message"><i
  134. class="fa fa-lock mr-1"></i> {{ 'datagrid.hidden_results'|trans({}, 'EasyAdminBundle') }}</span>
  135. </td>
  136. </tr>
  137. {% endif %}
  138. {% endblock table_body %}
  139. </tbody>
  140. </table>
  141. </div>
  142. </div>
  143. {% if entities|length > 0 %}
  144. <div class="card-footer">
  145. <div class="row">
  146. {% block paginator %}
  147. {{ include(ea.templatePath('crud/paginator')) }}
  148. {% endblock paginator %}
  149. </div>
  150. </div>
  151. {% endif %}
  152. </div>
  153. {% block delete_form %}
  154. {{ include('@EasyAdmin/crud/includes/_delete_form.html.twig', with_context = false) }}
  155. {% endblock delete_form %}
  156. {% if has_filters %}
  157. {{ include('@EasyAdmin/crud/includes/_filters_modal.html.twig') }}
  158. {% endif %}
  159. {% if has_batch_actions %}
  160. {{ include('@EasyAdmin/crud/includes/_batch_action_modal.html.twig', {}, with_context = false) }}
  161. {% endif %}
  162. {% endblock main %}
  163. {% block body_javascript %}
  164. {{ parent() }}
  165. <script type="text/javascript">
  166. $(function () {
  167. const customSwitches = document.querySelectorAll('td.field-boolean .custom-control.custom-switch input[type="checkbox"]');
  168. for (i = 0; i < customSwitches.length; i++) {
  169. customSwitches[i].addEventListener('change', function () {
  170. const customSwitch = this;
  171. const newValue = this.checked;
  172. const oldValue = !newValue;
  173. const toggleUrl = this.getAttribute('data-toggle-url') + "&newValue=" + newValue.toString();
  174. let toggleRequest = $.ajax({type: "GET", url: toggleUrl, data: {}});
  175. toggleRequest.done(function (result) {
  176. });
  177. toggleRequest.fail(function () {
  178. // in case of error, restore the original value and disable the toggle
  179. customSwitch.checked = oldValue;
  180. customSwitch.disabled = true;
  181. customSwitch.closest('.custom-switch').classList.add('disabled');
  182. });
  183. });
  184. }
  185. $('.action-delete').on('click', function (e) {
  186. e.preventDefault();
  187. const formAction = $(this).attr('formaction');
  188. $('#modal-delete').modal({backdrop: true, keyboard: true})
  189. .off('click', '#modal-delete-button')
  190. .on('click', '#modal-delete-button', function () {
  191. let deleteForm = $('#delete-form');
  192. deleteForm.attr('action', formAction);
  193. deleteForm.submit();
  194. });
  195. });
  196. {% if filters|length > 0 %}
  197. const filterModal = document.querySelector('#modal-filters');
  198. const removeFilter = function (field) {
  199. field.closest('form').querySelectorAll('input[name^="filters[' + field.dataset.filterProperty + ']"]').forEach(hidden => {
  200. hidden.remove();
  201. });
  202. field.remove();
  203. }
  204. document.querySelector('#modal-clear-button').addEventListener('click', function () {
  205. filterModal.querySelectorAll('.filter-field').forEach(filterField => {
  206. removeFilter(filterField);
  207. });
  208. filterModal.querySelector('form').submit();
  209. });
  210. document.querySelector('#modal-apply-button').addEventListener('click', function () {
  211. filterModal.querySelectorAll('.filter-checkbox:not(:checked)').forEach(notAppliedFilter => {
  212. removeFilter(notAppliedFilter.closest('.filter-field'));
  213. });
  214. filterModal.querySelector('form').submit();
  215. });
  216. // HTML5 specifies that a <script> tag inserted with innerHTML should not execute
  217. // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#Security_considerations
  218. // That's why we can't use just 'innerHTML'. See https://stackoverflow.com/a/47614491/2804294
  219. let setInnerHTML = function (element, htmlContent) {
  220. element.innerHTML = htmlContent;
  221. Array.from(element.querySelectorAll('script')).forEach(oldScript => {
  222. const newScript = document.createElement('script');
  223. Array.from(oldScript.attributes)
  224. .forEach(attr => newScript.setAttribute(attr.name, attr.value));
  225. newScript.appendChild(document.createTextNode(oldScript.innerHTML));
  226. oldScript.parentNode.replaceChild(newScript, oldScript);
  227. });
  228. };
  229. let filterButton = document.querySelector('.action-filters-button');
  230. filterButton.addEventListener('click', function (event) {
  231. let filterModal = document.querySelector(filterButton.dataset.modal);
  232. let filterModalBody = filterModal.querySelector('.modal-body');
  233. $(filterModal).modal({backdrop: true, keyboard: true});
  234. filterModalBody.innerHTML = '<div class="fa-3x px-3 py-3 text-muted text-center"><i class="fas fa-circle-notch fa-spin"></i></div>';
  235. $.get(filterButton.getAttribute('href'), function (response) {
  236. setInnerHTML(filterModalBody, response);
  237. });
  238. event.preventDefault();
  239. event.stopPropagation();
  240. });
  241. filterButton.setAttribute('href', filterButton.getAttribute('data-href'));
  242. filterButton.removeAttribute('data-href');
  243. filterButton.classList.remove('disabled');
  244. {% endif %}
  245. {% if has_batch_actions %}
  246. const titleContent = $('.content-header-title > .title').html();
  247. $(document).on('click', '.deselect-batch-button', function () {
  248. $(this).closest('.content').find(':checkbox.form-batch-checkbox-all').prop('checked', false).trigger('change');
  249. });
  250. $(document).on('change', '.form-batch-checkbox-all', function () {
  251. $(this).closest('.content').find(':checkbox.form-batch-checkbox').prop('checked', $(this).prop('checked')).trigger('change');
  252. });
  253. $(document).on('change', '.form-batch-checkbox', function () {
  254. const $content = $(this).closest('.content');
  255. let $input = $content.find(':hidden#batch_form_entityIds');
  256. let ids = $input.val() ? $input.val().split(',') : [];
  257. const id = $(this).val();
  258. if ($(this).prop('checked')) {
  259. $(this).closest('tr').addClass('selected-row');
  260. if (-1 === ids.indexOf(id)) {
  261. ids.push(id);
  262. }
  263. } else {
  264. $(this).closest('tr').removeClass('selected-row');
  265. ids = ids.filter(function (value) {
  266. return value !== id
  267. });
  268. $content.find(':checkbox.form-batch-checkbox-all').prop('checked', false);
  269. }
  270. if (0 === ids.length) {
  271. $content.find('.global-actions').show();
  272. $content.find('.batch-actions').hide();
  273. $content.find('table').removeClass('table-batch');
  274. } else {
  275. $content.find('.batch-actions').show();
  276. $content.find('.global-actions').hide();
  277. $content.find('table').addClass('table-batch');
  278. }
  279. $input.val(ids.join(','));
  280. $content.find('.content-header-title > .title').html(0 === ids.length ? titleContent : '');
  281. });
  282. let modalTitle = $('#batch-action-confirmation-title');
  283. const titleContentWithPlaceholders = modalTitle.text();
  284. $('[data-action-batch]').on('click', function (event) {
  285. event.preventDefault();
  286. event.stopPropagation();
  287. let $actionElement = $(this);
  288. const actionName = $actionElement.attr('data-action-name');
  289. const selectedItems = $('input[type="checkbox"].form-batch-checkbox:checked');
  290. modalTitle.text(titleContentWithPlaceholders
  291. .replace('%action_name%', actionName)
  292. .replace('%num_items%', selectedItems.length));
  293. $('#modal-batch-action').modal({backdrop: true, keyboard: true})
  294. .off('click', '#modal-batch-action-button')
  295. .on('click', '#modal-batch-action-button', function () {
  296. $actionElement.unbind('click');
  297. $form = document.createElement('form');
  298. $form.setAttribute('action', $actionElement.attr('data-action-url'));
  299. $form.setAttribute('method', 'POST');
  300. $actionNameInput = document.createElement('input');
  301. $actionNameInput.setAttribute('type', 'hidden');
  302. $actionNameInput.setAttribute('name', 'batchActionName');
  303. $actionNameInput.setAttribute('value', $actionElement.attr('data-action-name'));
  304. $form.appendChild($actionNameInput);
  305. $entityFqcnInput = document.createElement('input');
  306. $entityFqcnInput.setAttribute('type', 'hidden');
  307. $entityFqcnInput.setAttribute('name', 'entityFqcn');
  308. $entityFqcnInput.setAttribute('value', $actionElement.attr('data-entity-fqcn'));
  309. $form.appendChild($entityFqcnInput);
  310. $actionUrlInput = document.createElement('input');
  311. $actionUrlInput.setAttribute('type', 'hidden');
  312. $actionUrlInput.setAttribute('name', 'batchActionUrl');
  313. $actionUrlInput.setAttribute('value', $actionElement.attr('data-action-url'));
  314. $form.appendChild($actionUrlInput);
  315. $csrfTokenInput = document.createElement('input');
  316. $csrfTokenInput.setAttribute('type', 'hidden');
  317. $csrfTokenInput.setAttribute('name', 'batchActionCsrfToken');
  318. $csrfTokenInput.setAttribute('value', $actionElement.attr('data-action-csrf-token'));
  319. $form.appendChild($csrfTokenInput);
  320. selectedItems.each((i, item) => {
  321. $entityIdInput = document.createElement('input');
  322. $entityIdInput.setAttribute('type', 'hidden');
  323. $entityIdInput.setAttribute('name', `batchActionEntityIds[${i}]`);
  324. $entityIdInput.setAttribute('value', item.value);
  325. $form.appendChild($entityIdInput);
  326. });
  327. document.body.appendChild($form);
  328. //modalTitle.text(titleContentWithPlaceholders);
  329. $form.submit();
  330. });
  331. });
  332. {% endif %}
  333. });
  334. </script>
  335. {% if app.request.get('query') is not empty %}
  336. <script type="text/javascript">
  337. const search_query = "{{ ea.search.query|default('')|e('js') }}";
  338. // the original query is prepended to allow matching exact phrases in addition to single words
  339. $('#main').find('table tbody td:not(.actions)').highlight($.merge([search_query], search_query.split(' ')));
  340. </script>
  341. {% endif %}
  342. {% endblock %}