Koha/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt
Owen Leonard 169758fb56
Bug 31014: Minor UI problems in QOTD editor tool
This patch makes some cosmetic and functional updates to Quote of the
Day template, including simplifying some JavaScript, adding consisten
styles to buttons and page sections, and adding visible buttons to
jEditable text inputs.

The patch also improves the Quotes page breadcrumbs to add context
on the single quote entry page.

The patch removes quotes.css which is obsolete.

To test, apply the page and go to Tools -> Quote editor.

- The breadcrumbs should read "Tools -> Quote editor."
- Click the "New quote" button. The breadcrumbs should read "Tools ->
  Quote editor -> Add quote"
- Go back and click the "Import quotes" button.
- Confirm that the layout of the page looks good and that the upload
  instructions are shown.
- Upload a CSV file of quotes
  - The instructions should now show information about editing and
    saving quotes.
  - The "Save quotes" button should be styled consistently with other
    submit buttons.
  - Confirm that you can click in the "Source" and "Quote" table cells
    to trigger the display of a textarea where you can edit the text.
    Confirm that both the "Save" and "Cancel" buttons work.
  - Click the "Save quotes" button. A message should appear which shows
    how many quotes were imported.*
  - Confirm that your quotes are saved correctly, including any changes
    you made.

* This process could be improved: When the quotes are done importing the
toolbar doesn't change, and there isn't an indication that the import is
complete.

Signed-off-by: Sam Lau <samalau@gmail.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
2023-09-01 11:23:20 -03:00

413 lines
17 KiB
Text

[% USE raw %]
[% USE Asset %]
[% SET footerjs = 1 %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Quote uploader &rsaquo; Tools &rsaquo; Koha</title>
[% INCLUDE 'doc-head-close.inc' %]
[% Asset.css("css/uploader.css") | $raw %]
<style>
.jeditable-text {
display: block;
height: 5em;
margin-bottom: .5em;
min-width: 20em;
width: 70%;
}
</style>
</head>
<body id="tools_quotes" class="tools">
[% WRAPPER 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
[% END %]
[% WRAPPER 'sub-header.inc' %]
[% WRAPPER breadcrumbs %]
[% WRAPPER breadcrumb_item %]
<a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a>
[% END %]
[% WRAPPER breadcrumb_item %]
<a href="/cgi-bin/koha/tools/quotes.pl">Quote editor</a>
[% END %]
[% WRAPPER breadcrumb_item bc_active= 1 %]
<span>Quote uploader</span>
[% END %]
[% END #/ WRAPPER breadcrumbs %]
[% END #/ WRAPPER sub-header.inc %]
<div class="main container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<main>
<div id="toolbar" class="btn-toolbar" style="display:none">
<div class="btn-group"><a class="btn btn-primary" id="save_quotes" href="#"><i class="fa fa-save"></i> Save quotes</a></div>
<div class="btn-group"><a href="/cgi-bin/koha/tools/quotes-upload.pl" id="cancel_quotes" class="btn btn-default"><i class="fa fa-times"></i> Cancel import</a></div>
</div>
<h1>Quote uploader</h1>
<div id="messages" style="display: none;">
<div class="import_success dialog message" style="display: none;"></div>
<div class="import_error dialog alert" style="display: none;"></div>
</div>
<div id="instructions" class="page-section">
<h2>Instructions</h2>
<div id="file_uploader_inst">
<ul>
<li>The quote uploader accepts standard csv files with two columns: "source","text"</li>
<li>Click the "Choose file" button and select the csv file to be uploaded.</li>
<li>The file will be imported into an editable table for review prior to saving.</li>
</ul>
</div>
<div id="file_editor_inst" style="display:none">
<ul>
<li>Click on any field to edit the contents; Press the &lt;Enter&gt; key to save edit.</li>
<li>Click the 'Save quotes' button in the toolbar to save the entire batch of quotes.</li>
</ul>
</div>
</div>
<fieldset id="file_uploader" class="rows">
<legend>Upload quotes</legend>
<ol>
<li>
<label for="uploadfile">Select the file to upload: </label>
<div id="file_upload">
<input type="file" name="file" />
<button id="cancel_upload" style="display:none">Cancel upload</button>
<div id="progress_bar"><div class="percent">0%</div></div>
</div>
</li>
</ol>
</fieldset>
<div class="page-section">
<table id="quotes_editor" style="display:none">
</table>
</div>
<fieldset id="footer" class="action" style="display:none">
</fieldset>
</main>
</div> <!-- /.col-sm-10.col-sm-push-2 -->
<div class="col-sm-2 col-sm-pull-10">
<aside>
[% INCLUDE 'tools-menu.inc' %]
</aside>
</div> <!-- /.col-sm-2.col-sm-pull-10 -->
</div> <!-- /.row -->
[% MACRO jsinclude BLOCK %]
[% Asset.js("js/tools-menu.js") | $raw %]
[% INCLUDE 'datatables.inc' %]
[% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
<script>
var oTable; //DataTable object
$(document).ready(function() {
$("#cancel_upload").on("click",function(e){
e.preventDefault();
fnAbortRead();
});
$("#cancel_quotes").on("click",function(){
return confirm( _("Are you sure you want to cancel this import?") );
});
// Credits:
// FileReader() code copied and hacked from:
// http://www.html5rocks.com/en/tutorials/file/dndfiles/
// fnCSVToArray() gratefully borrowed from:
// http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
var reader;
var progress = document.querySelector('.percent');
$("#server_response").hide();
function yuiGetData() {
fnGetData(document.getElementById('quotes_editor'));
}
function fnAbortRead() {
reader.abort();
}
function fnErrorHandler(evt) {
switch(evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
alert(_("File not found!"));
break;
case evt.target.error.NOT_READABLE_ERR:
alert(_("File is not readable"));
break;
case evt.target.error.ABORT_ERR:
break; // noop
default:
alert(_("An error occurred reading this file."));
};
}
function fnUpdateProgress(evt) {
// evt is an ProgressEvent.
if (evt.lengthComputable) {
var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
// Increase the progress bar length.
if (percentLoaded < 100) {
progress.style.width = percentLoaded + '%';
progress.textContent = percentLoaded + '%';
}
}
}
function fnCSVToArray( strData, strDelimiter ){
// This will parse a delimited string into an array of
// arrays. The default delimiter is the comma, but this
// can be overriden in the second argument.
// Check to see if the delimiter is defined. If not,
// then default to comma.
strDelimiter = (strDelimiter || ",");
strData = escape_str(strData);
// Create a regular expression to parse the CSV values.
var objPattern = new RegExp(
(
// Delimiters.
"(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
// Quoted fields.
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
// Standard fields.
"([^\"\\" + strDelimiter + "\\r\\n]*))"
),
"gi"
);
// Create an array to hold our data. Give the array
// a default empty first row.
var arrData = [[]];
// Create an array to hold our individual pattern
// matching groups.
var arrMatches = null;
// Keep looping over the regular expression matches
// until we can no longer find a match.
while (arrMatches = objPattern.exec( strData )){
// Get the delimiter that was found.
var strMatchedDelimiter = arrMatches[ 1 ];
// Check to see if the given delimiter has a length
// (is not the start of string) and if it matches
// field delimiter. If it does not, then we know
// that this delimiter is a row delimiter.
if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){
// Since we have reached a new row of data,
// add an empty row to our data array.
// Note: if there is not more data, we will have to remove this row later
arrData.push( [] );
}
// Now that we have our delimiter out of the way,
// let's check to see which kind of value we
// captured (quoted or unquoted).
if (arrMatches[ 2 ]){
// We found a quoted value. When we capture
// this value, unescape any double quotes.
var strMatchedValue = arrMatches[ 2 ].replace(
new RegExp( "\"\"", "g" ),
"\""
);
} else if (arrMatches[3]){
// We found a non-quoted value.
var strMatchedValue = arrMatches[ 3 ];
} else {
// There is no more valid data so remove the row we added earlier
// Is there a better way? Perhaps a look-ahead regexp?
arrData.splice(arrData.length-1, 1);
}
// Now that we have our value string, let's add
// it to the data array.
if ( arrData[ arrData.length - 1 ] ) {
arrData[ arrData.length - 1 ].push( strMatchedValue );
} else {
$("#messages .import_error").text(_("Something went wrong, check your CSV file."));
}
}
// Return the parsed data.
return( arrData );
}
function fnDataTable(aaData) {
for(var i=0; i<aaData.length; i++) {
aaData[i].unshift(i+1); // Add a column w/quote number
}
/* Transition from the quote file uploader to the quote file editor interface */
$('#toolbar').show();
$('#file_editor_inst').show();
$('#file_uploader_inst').hide();
$('#save_quotes').show();
$('#file_uploader').hide();
$('#quotes_editor').show();
$("#save_quotes").on("click", yuiGetData);
oTable = $('#quotes_editor').dataTable( {
"bAutoWidth" : false,
"bPaginate" : true,
"bSort" : false,
"sPaginationType" : "full_numbers",
"sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">',
"aaData" : aaData,
"pageLength": 20,
"fixedHeader": true,
"aoColumns" : [
{
"sTitle" : _("Number"),
"sWidth" : "2%",
},
{
"sTitle" : _("Source"),
"sWidth" : "15%",
},
{
"sTitle" : _("Quote"),
"sWidth" : "83%",
},
],
"fnPreDrawCallback": function(oSettings) {
return true;
},
"fnRowCallback": function( nRow, aData, iDisplayIndex ) {
/* do foo on various cells in the current row */
var quoteNum = $('td', nRow)[0].innerHTML;
$(nRow).attr("id", quoteNum); /* set row ids to quote number */
/* apply no_edit id to noEditFields */
noEditFields = [0]; /* number */
for (i=0; i<noEditFields.length; i++) {
$('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
}
return nRow;
},
"fnDrawCallback": function(oSettings) {
/* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
$('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) {
value = escape_str(value);
var cellPosition = oTable.fnGetPosition( this );
oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false);
return(value);
},
{
"callback" : function( sValue, y ) {
oTable.fnDraw(false); /* no filter/sort or we lose our pagination */
},
"cancel" : _("Cancel"),
"height": "none",
"inputcssclass": "jeditable-text",
"onblur": "ignore",
"style": "",
"submit" : _("Save"),
"type": "textarea",
"width": "none",
}
);
},
});
$('#footer').show();
}
function fnHandleFileSelect(evt) {
// Reset progress indicator on new file selection.
progress.style.width = '0%';
progress.textContent = '0%';
reader = new FileReader();
reader.onerror = fnErrorHandler;
reader.onprogress = fnUpdateProgress;
reader.onabort = function(e) {
alert(_("File read cancelled"));
parent.location='quotes-upload.pl';
};
reader.onloadstart = function(e) {
$('#cancel_upload').show();
$('#progress_bar').addClass("loading");
};
reader.onload = function(e) {
// Ensure that the progress bar displays 100% at the end.
progress.style.width = '100%';
progress.textContent = '100%';
$('#cancel_upload').hide();
quotes = fnCSVToArray(e.target.result, ',');
fnDataTable(quotes);
}
// perform various sanity checks on the target file prior to uploading...
var fileType = evt.target.files[0].type || 'unknown';
var fileSizeInK = Math.round(evt.target.files[0].size/1024);
if (!fileType.match(/comma-separated-values|csv|excel|calc/i)) {
alert(_("Uploads limited to csv. Incorrect filetype: %s").format(fileType));
parent.location='quotes-upload.pl';
return;
}
if (fileSizeInK > 512) {
if (!confirm(_("%s %s KB Do you really want to upload this file?").format(evt.target.files[0].name, fileSizeInK))) {
parent.location='quotes-upload.pl';
return;
}
}
// Read in the image file as a text string.
reader.readAsText(evt.target.files[0]);
}
$('#file_upload').on('change', fnHandleFileSelect);
var MSG_IMPORT_SUCCESS = _("%s quotes imported successfully");
var MSG_IMPORT_ERROR = _("%s quotes have not been imported. An error occurred");
function fnGetData(element) {
var lines = oTable.fnGetData();
$(lines).each(function(line){
var s = this[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
var t = this[2].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
var data = {source: s, text: t};
var success = 0; var error = 0;
$.ajax({
url : "/api/v1/quotes",
method : "POST",
data : JSON.stringify(data),
dataType : "application/json",
success : function(data) {
$("#messages").addClass("message").show();
var import_success = $("#messages .import_success");
import_success.show();
import_success.data("nb")
nb_success = import_success.data("nb") || 0;
nb_success++;
$("#messages .import_success").text(MSG_IMPORT_SUCCESS.format(nb_success));
import_success.data("nb", nb_success);
},
error : function(xhr) {
if (xhr.status==201) { this.success(null, "Created", xhr); return; }
$("#messages").addClass("alert").show();
var import_error = $("#messages .import_error");
import_error.show();
import_error.data("nb")
nb_error = import_error.data("nb") || 0;
nb_error++;
$("#messages .import_error").text(MSG_IMPORT_ERROR.format(nb_error));
import_error.data("nb", nb_error);
},
});
});
}
}); // $(document).ready
</script>
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]