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.

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