Bugzilla – Attachment 147902 Details for
Bug 33066
We need a KohaTable Vue component
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 33066: Introduce a KohaTable Vue component
Bug-33066-Introduce-a-KohaTable-Vue-component.patch (text/plain), 52.84 KB, created by
Jonathan Druart
on 2023-03-08 12:25:58 UTC
(
hide
)
Description:
Bug 33066: Introduce a KohaTable Vue component
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2023-03-08 12:25:58 UTC
Size:
52.84 KB
patch
obsolete
>From 9f8849730d84df89c04178db17e7fb0c92b6280c Mon Sep 17 00:00:00 2001 >From: Agustin Moyano <agustinmoyano@theke.io> >Date: Wed, 1 Mar 2023 15:25:56 -0300 >Subject: [PATCH] Bug 33066: Introduce a KohaTable Vue component > >The way we deal with DataTables in Vue component is not nice, especially when we >need to add buttons/link in the cell and interact with the rest of the Vue app from >there. > >When I started to work on Vue last year there was no good solution from DataTables, >now there is a Vue component. It is not perfect, you still cannot add Vue component >in the DT component, but it brings something to follow. Agustin implemented something >on theke/import_source_vue, but he went too far, and it will need to rewrite the >whole ERM module. Additionally he didn't provide a solution that has the same features >as what we have now. > >The goal of this patch is to not duplicate the code in datatables.js, we >don't want to maintain two version of this code (one is enough already!) >We split the huge function in datatables.js in small ones to make them >reusable from the Vue component. > >This is quite ugly, and it needs to lot more addition, but it's a first >start! > >Help, ideas, and feedback welcome (and needed!) >--- > koha-tmpl/intranet-tmpl/prog/js/datatables.js | 738 +++++++++--------- > .../js/vue/components/ERM/AgreementsList.vue | 198 ++++- > .../prog/js/vue/components/KohaTable.vue | 142 ++++ > package.json | 2 + > yarn.lock | 28 + > 5 files changed, 719 insertions(+), 389 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >index 9b32ab2460b..81a900e0841 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >@@ -504,6 +504,381 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > console.log(message); > }; > >+function _dt_default_ajax (params){ >+ let default_filters = params.default_filters; >+ let options = params.options; >+ >+ if(!options.criteria || ['contains', 'starts_with', 'ends_with', 'exact'].indexOf(options.criteria.toLowerCase()) === -1) options.criteria = 'contains'; >+ options.criteria = options.criteria.toLowerCase(); >+ >+ return { >+ 'type': 'GET', >+ 'cache': true, >+ 'dataSrc': 'data', >+ 'beforeSend': function(xhr, settings) { >+ this._xhr = xhr; >+ if(options.embed) { >+ xhr.setRequestHeader('x-koha-embed', Array.isArray(options.embed)?options.embed.join(','):options.embed); >+ } >+ }, >+ 'dataFilter': function(data, type) { >+ var json = {data: JSON.parse(data)}; >+ if (total = this._xhr.getResponseHeader('x-total-count')) { >+ json.recordsTotal = total; >+ json.recordsFiltered = total; >+ } >+ if (total = this._xhr.getResponseHeader('x-base-total-count')) { >+ json.recordsTotal = total; >+ } >+ if (draw = this._xhr.getResponseHeader('x-koha-request-id')) { >+ json.draw = draw; >+ } >+ >+ return JSON.stringify(json); >+ }, >+ 'data': function( data, settings ) { >+ var length = data.length; >+ var start = data.start; >+ >+ var dataSet = { >+ _page: Math.floor(start/length) + 1, >+ _per_page: length >+ }; >+ >+ function build_query(col, value){ >+ >+ var parts = []; >+ var attributes = col.data.split(':'); >+ for (var i=0;i<attributes.length;i++){ >+ var part = {}; >+ var attr = attributes[i]; >+ let criteria = options.criteria; >+ if ( value.match(/^\^(.*)\$$/) ) { >+ value = value.replace(/^\^/, '').replace(/\$$/, ''); >+ criteria = "exact"; >+ } else { >+ // escape SQL LIKE special characters % >+ value = value.replace(/(\%|\\)/g, "\\$1"); >+ } >+ part[!attr.includes('.')?'me.'+attr:attr] = criteria === 'exact' >+ ? value >+ : {like: (['contains', 'ends_with'].indexOf(criteria) !== -1?'%':'') + value + (['contains', 'starts_with'].indexOf(criteria) !== -1?'%':'')}; >+ parts.push(part); >+ } >+ return parts; >+ } >+ >+ var filter = data.search.value; >+ // Build query for each column filter >+ var and_query_parameters = settings.aoColumns >+ .filter(function(col) { >+ return col.bSearchable && typeof col.data == 'string' && data.columns[col.idx].search.value != '' >+ }) >+ .map(function(col) { >+ var value = data.columns[col.idx].search.value; >+ return build_query(col, value) >+ }) >+ .map(function r(e){ >+ return ($.isArray(e) ? $.map(e, r) : e); >+ }); >+ >+ // Build query for the global search filter >+ var or_query_parameters = settings.aoColumns >+ .filter(function(col) { >+ return col.bSearchable && filter != '' >+ }) >+ .map(function(col) { >+ var value = filter; >+ return build_query(col, value) >+ }) >+ .map(function r(e){ >+ return ($.isArray(e) ? $.map(e, r) : e); >+ }); >+ >+ if ( default_filters ) { >+ let additional_filters = {}; >+ for ( f in default_filters ) { >+ let k; let v; >+ if ( typeof(default_filters[f]) === 'function' ) { >+ let val = default_filters[f](); >+ if ( val != undefined && val != "" ) { >+ k = f; v = val; >+ } >+ } else { >+ k = f; v = default_filters[f]; >+ } >+ >+ // Pass to -or if you want a separate OR clause >+ // It's not the usual DBIC notation! >+ if ( f == '-or' ) { >+ if (v) or_query_parameters.push(v) >+ } else if ( f == '-and' ) { >+ if (v) and_query_parameters.push(v) >+ } else if ( v ) { >+ additional_filters[k] = v; >+ } >+ } >+ if ( Object.keys(additional_filters).length ) { >+ and_query_parameters.push(additional_filters); >+ } >+ } >+ query_parameters = and_query_parameters; >+ if ( or_query_parameters.length) { >+ query_parameters.push(or_query_parameters); >+ } >+ >+ if(query_parameters.length) { >+ query_parameters = JSON.stringify(query_parameters.length === 1?query_parameters[0]:{"-and": query_parameters}); >+ dataSet.q = query_parameters; >+ delete options.query_parameters; >+ } else { >+ delete options.query_parameters; >+ } >+ >+ dataSet._match = options.criteria; >+ >+ if ( data["draw"] !== undefined ) { >+ settings.ajax.headers = { 'x-koha-request-id': data.draw } >+ } >+ >+ if(options.columns) { >+ var order = data.order; >+ var orderArray = new Array(); >+ order.forEach(function (e,i) { >+ var order_col = e.column; >+ var order_by = options.columns[order_col].data; >+ order_by = order_by.split(':'); >+ var order_dir = e.dir == 'asc' ? '+' : '-'; >+ Array.prototype.push.apply(orderArray,order_by.map(x => order_dir + (!x.includes('.')?'me.'+x:x))); >+ }); >+ dataSet._order_by = orderArray.filter((v, i, a) => a.indexOf(v) === i).join(','); >+ } >+ >+ return dataSet; >+ } >+ } >+} >+ >+function _dt_buttons(params){ >+ let included_ids = params.included_ids || []; >+ let settings = params.settings || {}; >+ let table_settings = params.table_settings || {}; >+ >+ var exportColumns = ":visible:not(.noExport)"; >+ if( settings.hasOwnProperty("exportColumns") ){ >+ // A custom buttons configuration has been passed from the page >+ exportColumns = settings["exportColumns"]; >+ } >+ >+ var export_format = { >+ body: function ( data, row, column, node ) { >+ var newnode = $(node); >+ >+ if ( newnode.find(".noExport").length > 0 ) { >+ newnode = newnode.clone(); >+ newnode.find(".noExport").remove(); >+ } >+ >+ return newnode.text().replace( /\n/g, ' ' ).trim(); >+ } >+ } >+ >+ var export_buttons = [ >+ { >+ extend: 'excelHtml5', >+ text: __("Excel"), >+ exportOptions: { >+ columns: exportColumns, >+ format: export_format >+ }, >+ }, >+ { >+ extend: 'csvHtml5', >+ text: __("CSV"), >+ exportOptions: { >+ columns: exportColumns, >+ format: export_format >+ }, >+ }, >+ { >+ extend: 'copyHtml5', >+ text: __("Copy"), >+ exportOptions: { >+ columns: exportColumns, >+ format: export_format >+ }, >+ }, >+ { >+ extend: 'print', >+ text: __("Print"), >+ exportOptions: { >+ columns: exportColumns, >+ format: export_format >+ }, >+ } >+ ]; >+ >+ let buttons = []; >+ buttons.push( >+ { >+ fade: 100, >+ className: "dt_button_clear_filter", >+ titleAttr: __("Clear filter"), >+ enabled: false, >+ text: '<i class="fa fa-lg fa-remove"></i> <span class="dt-button-text">' + __("Clear filter") + '</span>', >+ action: function ( e, dt, node, config ) { >+ dt.search( "" ).draw("page"); >+ node.addClass("disabled"); >+ } >+ } >+ ); >+ >+ if( included_ids.length > 0 ){ >+ buttons.push( >+ { >+ extend: 'colvis', >+ fade: 100, >+ columns: included_ids, >+ className: "columns_controls", >+ titleAttr: __("Columns settings"), >+ text: '<i class="fa fa-lg fa-gear"></i> <span class="dt-button-text">' + __("Columns") + '</span>', >+ exportOptions: { >+ columns: exportColumns >+ } >+ } >+ ); >+ } >+ >+ buttons.push( >+ { >+ extend: 'collection', >+ autoClose: true, >+ fade: 100, >+ className: "export_controls", >+ titleAttr: __("Export or print"), >+ text: '<i class="fa fa-lg fa-download"></i> <span class="dt-button-text">' + __("Export") + '</span>', >+ buttons: export_buttons >+ } >+ ); >+ >+ if ( table_settings && CAN_user_parameters_manage_column_config ) { >+ buttons.push( >+ { >+ className: "dt_button_configure_table", >+ fade: 100, >+ titleAttr: __("Configure table"), >+ text: '<i class="fa fa-lg fa-wrench"></i> <span class="dt-button-text">' + __("Configure") + '</span>', >+ action: function() { >+ window.location = '/cgi-bin/koha/admin/columns_settings.pl?module=' + table_settings['module'] + '&page=' + table_settings['page'] + '&table=' + table_settings['table']; >+ }, >+ } >+ ); >+ } >+ >+ return buttons; >+} >+ >+function _dt_visibility(table_settings, settings){ >+ var counter = 0; >+ let hidden_ids = []; >+ let included_ids = []; >+ if ( table_settings ) { >+ var columns_settings = table_settings['columns']; >+ $(columns_settings).each( function() { >+ var named_id = $( 'thead th[data-colname="' + this.columnname + '"]', this ).index( 'th' ); >+ var used_id = settings.bKohaColumnsUseNames ? named_id : counter; >+ if ( used_id == -1 ) return; >+ >+ if ( this['is_hidden'] == "1" ) { >+ hidden_ids.push( used_id ); >+ } >+ if ( this['cannot_be_toggled'] == "0" ) { >+ included_ids.push( used_id ); >+ } >+ counter++; >+ }); >+ } >+ return [hidden_ids, included_ids]; >+} >+ >+function _dt_on_visibility(add_filters, table_node, table_dt){ >+ if ( add_filters ) { >+ let visible_columns = table_dt.columns().visible(); >+ $(table_node).find('thead tr:eq(1) th').each( function (i) { >+ let th_id = $(this).data('th-id'); >+ if ( visible_columns[th_id] == false ) { >+ $(this).hide(); >+ } else { >+ $(this).show(); >+ } >+ }); >+ } >+ >+ if( typeof columnsInit == 'function' ){ >+ // This function can be created separately and used to trigger >+ // an event after the DataTable has loaded AND column visibility >+ // has been updated according to the table's configuration >+ columnsInit(); >+ } >+} >+ >+function _dt_add_filters(table_node, table_dt) { >+ $(table_node).find('thead tr').clone().appendTo( $(table_node).find('thead') ); >+ >+ $(table_node).find('thead tr:eq(1) th').each( function (i) { >+ var is_searchable = table_dt.settings()[0].aoColumns[i].bSearchable; >+ $(this).removeClass('sorting').removeClass("sorting_asc").removeClass("sorting_desc"); >+ $(this).data('th-id', i); >+ if ( is_searchable ) { >+ let input_type = 'input'; >+ if ( $(this).data('filter') ) { >+ input_type = 'select' >+ let filter_type = $(this).data('filter'); >+ var existing_search = table_dt.column(i).search(); >+ let select = $('<select><option value=""></option></select'); >+ >+ // FIXME eval here is bad and dangerous, how do we workaround that? >+ $(eval(filter_type)).each(function(){ >+ let o = $('<option value="%s">%s</option>'.format(this._id, this._str)); >+ if ( existing_search === this._id ) { >+ o.prop("selected", "selected"); >+ } >+ o.appendTo(select); >+ }); >+ $(this).html( select ); >+ } else { >+ var title = $(this).text(); >+ var existing_search = table_dt.column(i).search(); >+ if ( existing_search ) { >+ $(this).html( '<input type="text" value="%s" style="width: 100%" />'.format(existing_search) ); >+ } else { >+ var search_title = _("%s search").format(title); >+ $(this).html( '<input type="text" placeholder="%s" style="width: 100%" />'.format(search_title) ); >+ } >+ } >+ >+ $( input_type, this ).on( 'keyup change', function () { >+ if ( table_dt.column(i).search() !== this.value ) { >+ if ( input_type == "input" ) { >+ table_dt >+ .column(i) >+ .search( this.value ) >+ .draw(); >+ } else { >+ table_dt >+ .column(i) >+ .search( this.value.length ? '^'+this.value+'$' : '', true, false ) >+ .draw(); >+ } >+ } >+ } ); >+ } else { >+ $(this).html(''); >+ } >+ } ); >+} >+ >+ > (function($) { > > /** >@@ -522,9 +897,6 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > var settings = null; > > if(options) { >- if(!options.criteria || ['contains', 'starts_with', 'ends_with', 'exact'].indexOf(options.criteria.toLowerCase()) === -1) options.criteria = 'contains'; >- options.criteria = options.criteria.toLowerCase(); >- > // Don't redefine the default initComplete > if ( options.initComplete ) { > let our_initComplete = options.initComplete; >@@ -544,286 +916,14 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > 'language': { > 'emptyTable': (options.emptyTable) ? options.emptyTable : __("No data available in table") > }, >- 'ajax': { >- 'type': 'GET', >- 'cache': true, >- 'dataSrc': 'data', >- 'beforeSend': function(xhr, settings) { >- this._xhr = xhr; >- if(options.embed) { >- xhr.setRequestHeader('x-koha-embed', Array.isArray(options.embed)?options.embed.join(','):options.embed); >- } >- }, >- 'dataFilter': function(data, type) { >- var json = {data: JSON.parse(data)}; >- if (total = this._xhr.getResponseHeader('x-total-count')) { >- json.recordsTotal = total; >- json.recordsFiltered = total; >- } >- if (total = this._xhr.getResponseHeader('x-base-total-count')) { >- json.recordsTotal = total; >- } >- if (draw = this._xhr.getResponseHeader('x-koha-request-id')) { >- json.draw = draw; >- } >- >- return JSON.stringify(json); >- }, >- 'data': function( data, settings ) { >- var length = data.length; >- var start = data.start; >- >- var dataSet = { >- _page: Math.floor(start/length) + 1, >- _per_page: length >- }; >- >- function build_query(col, value){ >- >- var parts = []; >- var attributes = col.data.split(':'); >- for (var i=0;i<attributes.length;i++){ >- var part = {}; >- var attr = attributes[i]; >- let criteria = options.criteria; >- if ( value.match(/^\^(.*)\$$/) ) { >- value = value.replace(/^\^/, '').replace(/\$$/, ''); >- criteria = "exact"; >- } else { >- // escape SQL LIKE special characters % >- value = value.replace(/(\%|\\)/g, "\\$1"); >- } >- part[!attr.includes('.')?'me.'+attr:attr] = criteria === 'exact' >- ? value >- : {like: (['contains', 'ends_with'].indexOf(criteria) !== -1?'%':'') + value + (['contains', 'starts_with'].indexOf(criteria) !== -1?'%':'')}; >- parts.push(part); >- } >- return parts; >- } >- >- var filter = data.search.value; >- // Build query for each column filter >- var and_query_parameters = settings.aoColumns >- .filter(function(col) { >- return col.bSearchable && typeof col.data == 'string' && data.columns[col.idx].search.value != '' >- }) >- .map(function(col) { >- var value = data.columns[col.idx].search.value; >- return build_query(col, value) >- }) >- .map(function r(e){ >- return ($.isArray(e) ? $.map(e, r) : e); >- }); >- >- // Build query for the global search filter >- var or_query_parameters = settings.aoColumns >- .filter(function(col) { >- return col.bSearchable && filter != '' >- }) >- .map(function(col) { >- var value = filter; >- return build_query(col, value) >- }) >- .map(function r(e){ >- return ($.isArray(e) ? $.map(e, r) : e); >- }); >- >- if ( default_filters ) { >- let additional_filters = {}; >- for ( f in default_filters ) { >- let k; let v; >- if ( typeof(default_filters[f]) === 'function' ) { >- let val = default_filters[f](); >- if ( val != undefined && val != "" ) { >- k = f; v = val; >- } >- } else { >- k = f; v = default_filters[f]; >- } >- >- // Pass to -or if you want a separate OR clause >- // It's not the usual DBIC notation! >- if ( f == '-or' ) { >- if (v) or_query_parameters.push(v) >- } else if ( f == '-and' ) { >- if (v) and_query_parameters.push(v) >- } else if ( v ) { >- additional_filters[k] = v; >- } >- } >- if ( Object.keys(additional_filters).length ) { >- and_query_parameters.push(additional_filters); >- } >- } >- query_parameters = and_query_parameters; >- if ( or_query_parameters.length) { >- query_parameters.push(or_query_parameters); >- } >- >- if(query_parameters.length) { >- query_parameters = JSON.stringify(query_parameters.length === 1?query_parameters[0]:{"-and": query_parameters}); >- dataSet.q = query_parameters; >- delete options.query_parameters; >- } else { >- delete options.query_parameters; >- } >- >- dataSet._match = options.criteria; >- >- if ( data["draw"] !== undefined ) { >- settings.ajax.headers = { 'x-koha-request-id': data.draw } >- } >- >- if(options.columns) { >- var order = data.order; >- var orderArray = new Array(); >- order.forEach(function (e,i) { >- var order_col = e.column; >- var order_by = options.columns[order_col].data; >- order_by = order_by.split(':'); >- var order_dir = e.dir == 'asc' ? '+' : '-'; >- Array.prototype.push.apply(orderArray,order_by.map(x => order_dir + (!x.includes('.')?'me.'+x:x))); >- }); >- dataSet._order_by = orderArray.filter((v, i, a) => a.indexOf(v) === i).join(','); >- } >- >- return dataSet; >- } >- } >+ 'ajax': _dt_default_ajax({default_filters, options}), > }, options); > } > >- var counter = 0; >- var hidden_ids = []; >- var included_ids = []; >- >- >- if ( table_settings ) { >- var columns_settings = table_settings['columns']; >- $(columns_settings).each( function() { >- var named_id = $( 'thead th[data-colname="' + this.columnname + '"]', this ).index( 'th' ); >- var used_id = settings.bKohaColumnsUseNames ? named_id : counter; >- if ( used_id == -1 ) return; >- >- if ( this['is_hidden'] == "1" ) { >- hidden_ids.push( used_id ); >- } >- if ( this['cannot_be_toggled'] == "0" ) { >- included_ids.push( used_id ); >- } >- counter++; >- }); >- } >- >- var exportColumns = ":visible:not(.noExport)"; >- if( settings.hasOwnProperty("exportColumns") ){ >- // A custom buttons configuration has been passed from the page >- exportColumns = settings["exportColumns"]; >- } >- >- var export_format = { >- body: function ( data, row, column, node ) { >- var newnode = $(node); >- >- if ( newnode.find(".noExport").length > 0 ) { >- newnode = newnode.clone(); >- newnode.find(".noExport").remove(); >- } >- >- return newnode.text().replace( /\n/g, ' ' ).trim(); >- } >- } >- >- var export_buttons = [ >- { >- extend: 'excelHtml5', >- text: __("Excel"), >- exportOptions: { >- columns: exportColumns, >- format: export_format >- }, >- }, >- { >- extend: 'csvHtml5', >- text: __("CSV"), >- exportOptions: { >- columns: exportColumns, >- format: export_format >- }, >- }, >- { >- extend: 'copyHtml5', >- text: __("Copy"), >- exportOptions: { >- columns: exportColumns, >- format: export_format >- }, >- }, >- { >- extend: 'print', >- text: __("Print"), >- exportOptions: { >- columns: exportColumns, >- format: export_format >- }, >- } >- ]; >- >- settings[ "buttons" ] = [ >- { >- fade: 100, >- className: "dt_button_clear_filter", >- titleAttr: __("Clear filter"), >- enabled: false, >- text: '<i class="fa fa-lg fa-remove"></i> <span class="dt-button-text">' + __("Clear filter") + '</span>', >- action: function ( e, dt, node, config ) { >- dt.search( "" ).draw("page"); >- node.addClass("disabled"); >- } >- } >- ]; >- >- if( included_ids.length > 0 ){ >- settings[ "buttons" ].push( >- { >- extend: 'colvis', >- fade: 100, >- columns: included_ids, >- className: "columns_controls", >- titleAttr: __("Columns settings"), >- text: '<i class="fa fa-lg fa-gear"></i> <span class="dt-button-text">' + __("Columns") + '</span>', >- exportOptions: { >- columns: exportColumns >- } >- } >- ); >- } >- >- settings[ "buttons" ].push( >- { >- extend: 'collection', >- autoClose: true, >- fade: 100, >- className: "export_controls", >- titleAttr: __("Export or print"), >- text: '<i class="fa fa-lg fa-download"></i> <span class="dt-button-text">' + __("Export") + '</span>', >- buttons: export_buttons >- } >- ); >+ let hidden_ids, included_ids; >+ [hidden_ids, included_ids] = _dt_visibility(table_settings, settings) > >- if ( table_settings && CAN_user_parameters_manage_column_config ) { >- settings[ "buttons" ].push( >- { >- className: "dt_button_configure_table", >- fade: 100, >- titleAttr: __("Configure table"), >- text: '<i class="fa fa-lg fa-wrench"></i> <span class="dt-button-text">' + __("Configure") + '</span>', >- action: function() { >- window.location = '/cgi-bin/koha/admin/columns_settings.pl?module=' + table_settings['module'] + '&page=' + table_settings['page'] + '&table=' + table_settings['table']; >- }, >- } >- ); >- } >+ settings["buttons"] = _dt_buttons({included_ids, settings, table_settings}); > > $(".dt_button_clear_filter, .columns_controls, .export_controls, .dt_button_configure_table").tooltip(); > >@@ -842,85 +942,13 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > > var table = $(this).dataTable(settings); > >- >+ var table_dt = table.DataTable(); > if ( add_filters ) { >- var table_dt = table.DataTable(); >- >- $(this).find('thead tr').clone().appendTo( $(this).find('thead') ); >- >- $(this).find('thead tr:eq(1) th').each( function (i) { >- var is_searchable = table_dt.settings()[0].aoColumns[i].bSearchable; >- $(this).removeClass('sorting').removeClass("sorting_asc").removeClass("sorting_desc"); >- $(this).data('th-id', i); >- if ( is_searchable ) { >- let input_type = 'input'; >- if ( $(this).data('filter') ) { >- input_type = 'select' >- let filter_type = $(this).data('filter'); >- var existing_search = table_dt.column(i).search(); >- let select = $('<select><option value=""></option></select'); >- >- // FIXME eval here is bad and dangerous, how do we workaround that? >- $(eval(filter_type)).each(function(){ >- let o = $('<option value="%s">%s</option>'.format(this._id, this._str)); >- if ( existing_search === this._id ) { >- o.prop("selected", "selected"); >- } >- o.appendTo(select); >- }); >- $(this).html( select ); >- } else { >- var title = $(this).text(); >- var existing_search = table_dt.column(i).search(); >- if ( existing_search ) { >- $(this).html( '<input type="text" value="%s" style="width: 100%" />'.format(existing_search) ); >- } else { >- var search_title = __("%s search").format(title); >- $(this).html( '<input type="text" placeholder="%s" style="width: 100%" />'.format(search_title) ); >- } >- } >- >- $( input_type, this ).on( 'keyup change', function () { >- if ( table_dt.column(i).search() !== this.value ) { >- if ( input_type == "input" ) { >- table_dt >- .column(i) >- .search( this.value ) >- .draw(); >- } else { >- table_dt >- .column(i) >- .search( this.value.length ? '^'+this.value+'$' : '', true, false ) >- .draw(); >- } >- } >- } ); >- } else { >- $(this).html(''); >- } >- } ); >+ _dt_add_filters(this, table_dt); > } > >- table.DataTable().on("column-visibility.dt", function(){ >- if ( add_filters ) { >- let visible_columns = table_dt.columns().visible(); >- $(table).find('thead tr:eq(1) th').each( function (i) { >- let th_id = $(this).data('th-id'); >- if ( visible_columns[th_id] == false ) { >- $(this).hide(); >- } else { >- $(this).show(); >- } >- }); >- } >- >- if( typeof columnsInit == 'function' ){ >- // This function can be created separately and used to trigger >- // an event after the DataTable has loaded AND column visibility >- // has been updated according to the table's configuration >- columnsInit(); >- } >- }).columns( hidden_ids ).visible( false ); >+ table.DataTable().on("column-visibility.dt", function(){_dt_on_visibility(add_filters, table, table_dt);}) >+ .columns( hidden_ids ).visible( false ); > > return table; > }; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >index e14b1142498..ecfed99d21b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >@@ -33,7 +33,13 @@ > /> > </fieldset> > <div v-if="agreements.length" class="page-section"> >- <table :id="table_id"></table> >+ <KohaTable >+ ref="table" >+ v-bind="tableOptions" >+ @show="doShow" >+ @edit="doEdit" >+ @delete="doDelete" >+ ></KohaTable> > </div> > <div v-else-if="initialized" class="dialog message"> > {{ $__("There are no agreements defined") }} >@@ -44,10 +50,11 @@ > <script> > import flatPickr from "vue-flatpickr-component" > import Toolbar from "./AgreementsToolbar.vue" >-import { inject, createVNode, render } from "vue" >+import { inject, createVNode, render, ref } from "vue" > import { APIClient } from "../../fetch/api-client.js" > import { storeToRefs } from "pinia" >-import { useDataTable, build_url } from "../../composables/datatables" >+import { build_url } from "../../composables/datatables" >+import KohaTable from "../KohaTable.vue" > > export default { > setup() { >@@ -59,17 +66,18 @@ export default { > > const { setConfirmationDialog, setMessage } = inject("mainStore") > >- const table_id = "agreement_list" >- useDataTable(table_id) >+ const table = ref() > > return { > vendors, > get_lib_from_av, > map_av_dt_filter, >- table_id, > logged_in_user, >+ table, > setConfirmationDialog, > setMessage, >+ escape_str, >+ agreement_table_settings, > } > }, > data: function () { >@@ -85,6 +93,16 @@ export default { > }, > before_route_entered: false, > building_table: false, >+ tableOptions: { >+ columns: this.getTableColumns(), >+ url: () => this.table_url(), >+ table_settings: this.agreement_table_settings, >+ add_filters: true, >+ actions: { >+ 0: ["show"], >+ "-1": ["edit", "delete"], >+ }, >+ }, > } > }, > beforeRouteEnter(to, from, next) { >@@ -92,19 +110,11 @@ export default { > vm.before_route_entered = true // FIXME This is ugly, but we need to distinguish when it's used as main component or child component (from EHoldingsEBSCOPAckagesShow for instance) > if (!vm.building_table) { > vm.building_table = true >- vm.getAgreements().then(() => vm.build_datatable()) >+ vm.getAgreements() >+ vm.initialized = true > } > }) > }, >- computed: { >- datatable_url() { >- let url = "/api/v1/erm/agreements" >- if (this.filters.by_expired) >- url += >- "?max_expiration_date=" + this.filters.max_expiration_date >- return url >- }, >- }, > methods: { > async getAgreements() { > const client = APIClient.erm >@@ -116,19 +126,22 @@ export default { > error => {} > ) > }, >- show_agreement: function (agreement_id) { >- this.$router.push("/cgi-bin/koha/erm/agreements/" + agreement_id) >+ doShow: function (agreement, dt, event) { >+ event.preventDefault() >+ this.$router.push( >+ "/cgi-bin/koha/erm/agreements/" + agreement.agreement_id >+ ) > }, >- edit_agreement: function (agreement_id) { >+ doEdit: function (agreement, dt, event) { > this.$router.push( >- "/cgi-bin/koha/erm/agreements/edit/" + agreement_id >+ "/cgi-bin/koha/erm/agreements/edit/" + agreement.agreement_id > ) > }, >- delete_agreement: function (agreement_id, agreement_name) { >+ doDelete: function (agreement, dt, event) { > this.setConfirmationDialog( > { > title: this.$__( >- "Are you sure you want to remove this agreement?" >+ "Are you sure you want to delete this agreement?" > ), > message: agreement_name, > accept_label: this.$__("Yes, delete"), >@@ -136,7 +149,7 @@ export default { > }, > () => { > const client = APIClient.erm >- client.agreements.delete(agreement_id).then( >+ client.agreements.delete(agreement.agreement_id).then( > success => { > this.setMessage( > this.$__("Agreement %s deleted").format( >@@ -144,23 +157,24 @@ export default { > ), > true > ) >- this.refresh_table() >+ dt.draw() > }, > error => {} > ) > } > ) > }, >+ table_url: function () { >+ let url = "/api/v1/erm/agreements" >+ if (this.filters.by_expired) >+ url += >+ "?max_expiration_date=" + this.filters.max_expiration_date >+ return url >+ }, > select_agreement: function (agreement_id) { > this.$emit("select-agreement", agreement_id) > this.$emit("close") > }, >- refresh_table: function () { >- $("#" + this.table_id) >- .DataTable() >- .ajax.url(this.datatable_url) >- .draw() >- }, > filter_table: async function () { > if (this.before_route_entered) { > let new_route = build_url( >@@ -175,9 +189,8 @@ export default { > .toISOString() > .substring(0, 10) > } >- this.refresh_table() >+ this.$refs.table.redraw(this.table_url()) > }, >- table_url: function () {}, > build_datatable: function () { > let show_agreement = this.show_agreement > let edit_agreement = this.edit_agreement >@@ -477,14 +490,131 @@ export default { > additional_filters > ) > }, >+ getTableColumns: function () { >+ let get_lib_from_av = this.get_lib_from_av >+ let escape_str = this.escape_str >+ window["vendors"] = this.vendors.map(e => { >+ e["_id"] = e["id"] >+ e["_str"] = e["name"] >+ return e >+ }) >+ let vendors_map = this.vendors.reduce((map, e) => { >+ map[e.id] = e >+ return map >+ }, {}) >+ let avs = [ >+ "av_agreement_statuses", >+ "av_agreement_closure_reasons", >+ "av_agreement_renewal_priorities", >+ ] >+ let c = this >+ avs.forEach(function (av_cat) { >+ window[av_cat] = c.map_av_dt_filter(av_cat) >+ }) >+ >+ window["av_agreement_is_perpetual"] = [ >+ { _id: 0, _str: _("No") }, >+ { _id: 1, _str: _("Yes") }, >+ ] >+ return [ >+ { >+ title: __("Name"), >+ data: "me.agreement_id:me.name", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ // Rendering done in drawCallback >+ return ( >+ '<a href="/cgi-bin/koha/erm/agreements/' + >+ row.agreement_id + >+ '" class="show">show</a>' >+ ) >+ }, >+ }, >+ { >+ title: __("Vendor"), >+ data: "vendor_id", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return row.vendor_id != undefined >+ ? escape_str(vendors_map[row.vendor_id].name) >+ : "" >+ }, >+ }, >+ { >+ title: __("Description"), >+ data: "description", >+ searchable: true, >+ orderable: true, >+ }, >+ { >+ title: __("Status"), >+ data: "status", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return escape_str( >+ get_lib_from_av("av_agreement_statuses", row.status) >+ ) >+ }, >+ }, >+ { >+ title: __("Closure reason"), >+ data: "closure_reason", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return escape_str( >+ get_lib_from_av( >+ "av_agreement_closure_reasons", >+ row.closure_reason >+ ) >+ ) >+ }, >+ }, >+ { >+ title: __("Is perpetual"), >+ data: "is_perpetual", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return escape_str(row.is_perpetual ? _("Yes") : _("No")) >+ }, >+ }, >+ { >+ title: __("Renewal priority"), >+ data: "renewal_priority", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return escape_str( >+ get_lib_from_av( >+ "av_agreement_renewal_priorities", >+ row.renewal_priority >+ ) >+ ) >+ }, >+ }, >+ { >+ title: __("Actions"), >+ data: function (row, type, val, meta) { >+ return '<div class="actions"></div>' >+ }, >+ className: "actions noExport", >+ searchable: false, >+ orderable: false, >+ }, >+ ] >+ }, > }, > mounted() { > if (!this.building_table) { > this.building_table = true >- this.getAgreements().then(() => this.build_datatable()) >+ this.getAgreements() > } > }, >- components: { flatPickr, Toolbar }, >+ components: { flatPickr, Toolbar, KohaTable }, > name: "AgreementsList", > emits: ["select-agreement", "close"], > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue >new file mode 100644 >index 00000000000..5f93005108a >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue >@@ -0,0 +1,142 @@ >+<template> >+ <DataTable >+ :columns="tableColumns" >+ :options="{ ...dataTablesDefaults, ...allOptions }" >+ :data="data" >+ ref="table" >+ > >+ <slot></slot> >+ </DataTable> >+</template> >+ >+<script> >+import DataTable from "datatables.net-vue3" >+import DataTablesLib from "datatables.net" >+import "datatables.net-buttons" >+import "datatables.net-buttons/js/buttons.html5" >+import "datatables.net-buttons/js/buttons.print" >+DataTable.use(DataTablesLib) >+ >+export default { >+ name: "KohaTable", >+ data() { >+ return { >+ data: [], >+ tableColumns: this.columns, >+ allOptions: { >+ deferRender: true, >+ paging: true, >+ serverSide: true, >+ searching: true, >+ pagingType: "full_numbers", >+ processing: true, >+ ajax: { >+ url: typeof this.url === "function" ? this.url() : this.url, >+ ..._dt_default_ajax({ options: this.options }), >+ }, >+ buttons: _dt_buttons({ table_settings: this.table_settings }), >+ default_search: this.$route.query.q, >+ }, >+ } >+ }, >+ setup() { >+ return { dataTablesDefaults } >+ }, >+ methods: { >+ redraw: function (url) { >+ this.$refs.table.dt().ajax.url(url).draw() >+ }, >+ }, >+ beforeMount() { >+ if (this.actions.hasOwnProperty("-1")) { >+ let actions = this.actions["-1"] >+ this.tableColumns = [ >+ ...this.tableColumns, >+ { >+ name: "actions", >+ title: this.$__("Actions"), >+ searchable: false, >+ render: (data, type, row) => { >+ let content = [] >+ this.actions["-1"].forEach(a => { >+ if (a == "edit") { >+ content.push( >+ '<a class="edit btn btn-default btn-xs" role="button"><i class="fa fa-pencil"></i>' + >+ this.$__("Edit") + >+ "</a>" >+ ) >+ } else if (a == "delete") { >+ content.push( >+ '<a class="delete btn btn-default btn-xs" role="button"><i class="fa fa-trash"></i>' + >+ this.$__("Delete") + >+ "</a>" >+ ) >+ } >+ }) >+ return content.join(" ") >+ }, >+ }, >+ ] >+ } >+ }, >+ mounted() { >+ if (Object.keys(this.actions).length) { >+ const dt = this.$refs.table.dt() >+ const self = this >+ dt.on("draw", () => { >+ const dataSet = dt.rows().data() >+ Object.entries(this.actions).forEach(([col_id, actions]) => { >+ dt.column(col_id) >+ .nodes() >+ .to$() >+ .each(function (idx) { >+ const data = dataSet[idx] >+ actions.forEach(action => { >+ $("." + action, this).on("click", e => { >+ self.$emit(action, data, dt, e) >+ }) >+ }) >+ }) >+ }) >+ }) >+ } >+ }, >+ beforeUnmount() { >+ const dt = this.$refs.table.dt() >+ dt.destroy() >+ }, >+ components: { >+ DataTable, >+ }, >+ props: { >+ url: { >+ type: [String, Function], >+ default: "", >+ }, >+ columns: { >+ type: Array, >+ default: [], >+ }, >+ actions: { >+ type: Object, >+ default: {}, >+ }, >+ options: { >+ type: Object, >+ default: {}, >+ }, >+ default_filters: { >+ type: Object, >+ required: false, >+ }, >+ table_settings: { >+ type: Object, >+ required: false, >+ }, >+ default_search: { >+ type: String, >+ required: false, >+ }, >+ }, >+} >+</script> >diff --git a/package.json b/package.json >index 046755dbb8c..1a4d20e98ec 100644 >--- a/package.json >+++ b/package.json >@@ -18,6 +18,8 @@ > "bootstrap": "^4.5.2", > "css-loader": "^6.6.0", > "cypress": "^9.5.2", >+ "datatables.net-buttons": "^2.3.4", >+ "datatables.net-vue3": "^2.0.0", > "gulp": "^4.0.2", > "gulp-autoprefixer": "^4.0.0", > "gulp-concat-po": "^1.0.0", >diff --git a/yarn.lock b/yarn.lock >index 5c2947a4427..cefae980247 100644 >--- a/yarn.lock >+++ b/yarn.lock >@@ -3518,6 +3518,29 @@ dashdash@^1.12.0: > dependencies: > assert-plus "^1.0.0" > >+datatables.net-buttons@^2.3.4: >+ version "2.3.4" >+ resolved "https://registry.yarnpkg.com/datatables.net-buttons/-/datatables.net-buttons-2.3.4.tgz#85b88baed81d380cb04c06608d549c8868326ece" >+ integrity sha512-1fe/aiKBdKbwJ5j0OobP2dzhbg/alGOphnTfLFGaqlP5yVxDCfcZ9EsuglYeHRJ/KnU7DZ8BgsPFiTE0tOFx8Q== >+ dependencies: >+ datatables.net ">=1.12.1" >+ jquery ">=1.7" >+ >+datatables.net-vue3@^2.0.0: >+ version "2.0.0" >+ resolved "https://registry.yarnpkg.com/datatables.net-vue3/-/datatables.net-vue3-2.0.0.tgz#01b20c68201f49f57920dea62a5f742b7119880b" >+ integrity sha512-auwLfwqebGZ0gFnU8C/HWQYpkVtU64x8T+gYs5i7/Jqyo3YNTDU2M/lWwp7rJ+VSlolkDICrKpfmmo/Rz6ZBFw== >+ dependencies: >+ datatables.net "^1.13.1" >+ jquery "^3.6.0" >+ >+datatables.net@>=1.12.1, datatables.net@^1.13.1: >+ version "1.13.2" >+ resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.2.tgz#48f7035b1696a29cb70909db1f2e0ebd5f946f3e" >+ integrity sha512-u5nOU+C9SBp1SyPmd6G+niozZtrBwo1E8xzdOk3JJaAkFYgX/KxF3Gd79R8YLbUfmIs2OLnLe5gaz/qs5U8UDA== >+ dependencies: >+ jquery ">=1.7" >+ > dayjs@^1.10.4: > version "1.11.5" > resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" >@@ -5935,6 +5958,11 @@ joi@^17.4.0: > "@sideway/formula" "^3.0.0" > "@sideway/pinpoint" "^2.0.0" > >+jquery@>=1.7, jquery@^3.6.0: >+ version "3.6.3" >+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.3.tgz#23ed2ffed8a19e048814f13391a19afcdba160e6" >+ integrity sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg== >+ > js-message@1.0.7: > version "1.0.7" > resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47" >-- >2.25.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 33066
:
147352
|
147353
|
147354
|
147692
|
147693
|
147694
|
147713
|
147846
|
147847
|
147848
|
147849
|
147850
|
147851
|
147852
|
147853
|
147855
|
147856
|
147857
|
147858
|
147862
|
147865
|
147902
|
147903
|
147904
|
147905
|
147906
|
147907
|
147908
|
147909
|
147910
|
147911
|
147912
|
147913
|
147914
|
147915
|
147916
|
147917
|
147918
|
147921
|
147922
|
147923
|
147925
|
147926
|
147928
|
147957
|
147960
|
147971
|
147972
|
148718
|
149158
|
149159
|
149160
|
149161
|
149162
|
149163
|
149164
|
149165
|
149166
|
149167
|
149168
|
149169
|
149170
|
149171
|
149172
|
149173
|
149174
|
149175
|
149176
|
149177
|
149178
|
149179
|
149180
|
149181
|
149182
|
149183