304 lines
14 KiB
Twig
304 lines
14 KiB
Twig
|
|
{#
|
||
|
|
/**
|
||
|
|
* Copyright (C) 2020 Xibo Signage Ltd
|
||
|
|
*
|
||
|
|
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||
|
|
*
|
||
|
|
* This file is part of Xibo.
|
||
|
|
*
|
||
|
|
* Xibo is free software: you can redistribute it and/or modify
|
||
|
|
* it under the terms of the GNU Affero General Public License as published by
|
||
|
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
|
* any later version.
|
||
|
|
*
|
||
|
|
* Xibo is distributed in the hope that it will be useful,
|
||
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
* GNU Affero General Public License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU Affero General Public License
|
||
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
*/
|
||
|
|
#}
|
||
|
|
{% extends "authed.twig" %}
|
||
|
|
{% import "inline.twig" as inline %}
|
||
|
|
{% set dataSetName = dataSet.dataSet %}
|
||
|
|
|
||
|
|
{% block title %}{% trans %}Data Entry for {{ dataSetName }}{% endtrans %} | {% endblock %}
|
||
|
|
|
||
|
|
{% block actionMenu %}
|
||
|
|
<div class="widget-action-menu pull-right">
|
||
|
|
<button class="btn btn-success XiboFormButton addRowButton" title="{% trans "Add a row to the end of this DataSet" %}" href="{{ url_for("dataSet.data.add.form", {"id": dataSet.dataSetId}) }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Row" %}</button>
|
||
|
|
<button class="btn btn-primary toggleMultiSelectMode" id="toggleMultiSelectMode" title="{% trans "Click to toggle between Data Edit and Multi Select modes" %}"><i class="fa fa-object-group" aria-hidden="true"></i> <span class="button-text">{% trans "Multi Select Mode" %}</span></button>
|
||
|
|
<button class="btn btn-danger d-none deleteSelectedRows" id="deleteSelectedRows" title="{% trans "Click to delete selected rows" %}" disabled="disabled"><i class="fa fa-trash" aria-hidden="true"></i> <span class="button-text">{% trans "Delete Rows" %}</span></button>
|
||
|
|
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("dataSet.column.view", {"id": dataSet.dataSetId}) }}"><i class="fa fa-columns" aria-hidden="true"></i> {% trans "View Columns" %}</button>
|
||
|
|
<button class="btn btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
|
||
|
|
{% block pageContent %}
|
||
|
|
{% set widgetTitle %}{% trans %}Data Entry for {{ dataSetName }}{% endtrans %}{% endset %}
|
||
|
|
<div class="widget">
|
||
|
|
<div class="widget-title">{{ widgetTitle }} | <strong class="dataset-edit-title-mode widget-sub-title">{% trans "Edit Mode" %}</strong><span title="{% trans "Click on any row to edit" %}" class="badge badge-pill badge-secondary px-1 mx-1">?</span></div>
|
||
|
|
<div class="widget-body">
|
||
|
|
<div class="XiboGrid" id="{{ random() }}">
|
||
|
|
<div class="XiboFilter">
|
||
|
|
<div class="FilterDiv card-body" id="Filter">
|
||
|
|
<form class="form-inline">
|
||
|
|
{% for col in dataSet.getColumn() %}
|
||
|
|
{% if col.dataSetColumnTypeId == 1 and (col.dataTypeId == 1 or col.dataTypeId == 2) and col.showFilter == 1 %}
|
||
|
|
{{ inline.input(col.heading, col.heading) }}
|
||
|
|
{% endif %}
|
||
|
|
{% endfor %}
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="XiboData card pt-3">
|
||
|
|
<table id="datasets" class="table table-striped" data-image-url="{{ url_for("library.download", {"id":":id"}) }}?preview=1&width=150&height=150">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>{% trans "ID" %}</th>
|
||
|
|
{% for col in dataSet.getColumn() %}
|
||
|
|
<th>{{ col.heading }}</th>
|
||
|
|
{% endfor %}
|
||
|
|
<th></th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block javaScript %}
|
||
|
|
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||
|
|
|
||
|
|
var imageUrl = $("#datasets").data("imageUrl");
|
||
|
|
var cols = [];
|
||
|
|
var editMode = true;
|
||
|
|
var editModeTitleTrans = "{% trans "Edit Mode" %}";
|
||
|
|
var multiSelectTitleTrans = "{% trans "Multi Select Mode" %}";
|
||
|
|
var editModeHelpTrans = "{% trans "Click on any row to edit" %}";
|
||
|
|
var multiSelectHelpTrans = "{% trans "Select one or more rows to delete" %}";
|
||
|
|
const entityMap = {
|
||
|
|
'&': '&',
|
||
|
|
'<': '<',
|
||
|
|
'>': '>',
|
||
|
|
'"': '"',
|
||
|
|
"'": ''',
|
||
|
|
'/': '/',
|
||
|
|
'`': '`',
|
||
|
|
'=': '='
|
||
|
|
};
|
||
|
|
|
||
|
|
function sanitizeHtml(string) {
|
||
|
|
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
|
||
|
|
return entityMap[s];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function validateHTMLData(str) {
|
||
|
|
let doc = new DOMParser().parseFromString(str, "text/html");
|
||
|
|
|
||
|
|
// If valid html, sanitize and format as a code
|
||
|
|
if (Array.from(doc.body.childNodes).some(node => node.nodeType === 1)) {
|
||
|
|
return `<code>${sanitizeHtml(str)}</code>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return str;
|
||
|
|
}
|
||
|
|
|
||
|
|
cols.push({ "name": "id", "data": "id" });
|
||
|
|
{% for col in dataSet.getColumn() %}
|
||
|
|
{% if col.dataTypeId == 3 %}
|
||
|
|
cols.push({ "data": "{{ col.heading }}", "orderable": {% if col.showSort == 1 %}true{% else %}false{% endif %}, "render": dataTableDateFromIso });
|
||
|
|
{% elseif col.dataTypeId == 5 %}
|
||
|
|
cols.push({ "data": "{{ col.heading }}", "orderable": {% if col.showSort == 1 %}true{% else %}false{% endif %}, "render": function(data, type, row) {
|
||
|
|
if (type != "display")
|
||
|
|
return data;
|
||
|
|
|
||
|
|
if (data == null)
|
||
|
|
return "";
|
||
|
|
|
||
|
|
return '<img src="' + imageUrl.replace(":id", data) + '"/>';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
{% else %}
|
||
|
|
cols.push({
|
||
|
|
"data": "{{ col.heading }}",
|
||
|
|
"orderable": {% if col.showSort == 1 %}true{% else %}false{% endif %},
|
||
|
|
"render": function(data) {
|
||
|
|
return validateHTMLData(data);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
{% endif %}
|
||
|
|
{% endfor %}
|
||
|
|
|
||
|
|
cols.push({
|
||
|
|
"orderable": false,
|
||
|
|
"data": function(data, type, row, meta) {
|
||
|
|
if (type != "display")
|
||
|
|
return "";
|
||
|
|
|
||
|
|
var url = "{{ url_for("dataSet.data.delete.form", {"id": dataSet.dataSetId, "rowId":':rowId'}) }}".replace(":rowId", data.id);
|
||
|
|
|
||
|
|
return "<a href=\"" + url + "\" class=\"XiboFormButton\"><span class=\"fa fa-times\"></span></a>";
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
var table = $("#datasets").DataTable({ "language": dataTablesLanguage,
|
||
|
|
dom: dataTablesTemplate,
|
||
|
|
serverSide: true, stateSave: true, stateDuration: 0,
|
||
|
|
filter: false,
|
||
|
|
responsive: true,
|
||
|
|
searchDelay: 3000,
|
||
|
|
"order": [[ 0, "asc"]],
|
||
|
|
ajax: {
|
||
|
|
"url": "{{ url_for("dataSet.data.search", {id: dataSet.dataSetId}) }}",
|
||
|
|
"data": function(dataToSend) {
|
||
|
|
var data = {};
|
||
|
|
data.draw = dataToSend.draw;
|
||
|
|
data.length = dataToSend.length;
|
||
|
|
data.start = dataToSend.start;
|
||
|
|
data.order = dataToSend.order;
|
||
|
|
data.columns = [];
|
||
|
|
$.each(dataToSend.columns, function (index, e) {
|
||
|
|
var col = {};
|
||
|
|
col.data = e.data;
|
||
|
|
if (e.orderable) {
|
||
|
|
data.columns.push(col);
|
||
|
|
} else {
|
||
|
|
data.columns.push({});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
$.extend(data, $("#datasets").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||
|
|
return data;
|
||
|
|
},
|
||
|
|
complete: function (response) {
|
||
|
|
|
||
|
|
if (response.responseJSON.data.exception != '' && response.responseJSON.data.exception != undefined) {
|
||
|
|
var error = response.responseJSON.data.exception;
|
||
|
|
$("#datasets tbody").html(error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"columns": cols
|
||
|
|
});
|
||
|
|
|
||
|
|
table.on('draw', dataTableDraw);
|
||
|
|
table.on('processing.dt', dataTableProcessing);
|
||
|
|
dataTableAddButtons(table, $('#datasets_wrapper').find('.dataTables_buttons'));
|
||
|
|
|
||
|
|
$("#refreshGrid").click(function () {
|
||
|
|
table.ajax.reload();
|
||
|
|
});
|
||
|
|
|
||
|
|
var dataSetEditFormOpen = _.debounce(function() {
|
||
|
|
var data = table.row($(this)).data();
|
||
|
|
|
||
|
|
// Load the edit form.
|
||
|
|
var editUrl = "{{ url_for("dataSet.data.edit.form", {"id": dataSet.dataSetId, "rowId":':rowId'}) }}".replace(":rowId", data.id);
|
||
|
|
XiboFormRender(editUrl);
|
||
|
|
}, 500, true);
|
||
|
|
|
||
|
|
// default is Edit mode, hence this should be our onclick event and deleteRows button should be hidden.
|
||
|
|
$("#datasets tbody").on('click', 'tr', dataSetEditFormOpen);
|
||
|
|
|
||
|
|
// Toggle between Edit and Multi Select modes
|
||
|
|
// we also switch between two different onclick events depending on the mode and show/hide deleteRows button.
|
||
|
|
$('#toggleMultiSelectMode').on("click", function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
if (editMode) {
|
||
|
|
// switch to multi select mode
|
||
|
|
editMode = false;
|
||
|
|
$(this).find('.button-text').text(editModeTitleTrans);
|
||
|
|
$(this).find('i').removeClass('fa-object-group').addClass('fa-edit');
|
||
|
|
$('.dataset-edit-title-mode').text(multiSelectTitleTrans);
|
||
|
|
$('.widget-title .badge').attr('title', multiSelectHelpTrans);
|
||
|
|
$("#datasets tbody").off('click', 'tr');
|
||
|
|
$('#datasets tbody tr.selected').removeClass('selected');
|
||
|
|
$(".deleteSelectedRows").removeClass('d-none');
|
||
|
|
$('#datasets tbody').on('click', 'tr', function(ev) {
|
||
|
|
|
||
|
|
// See if element has class already
|
||
|
|
var hasClass = $(ev.currentTarget).hasClass('selected')
|
||
|
|
|
||
|
|
// Add class to the clicked one ( or remove it if there was already there )
|
||
|
|
$(ev.currentTarget).toggleClass('selected', !hasClass);
|
||
|
|
|
||
|
|
// Change Delete button state according to the number of selected rows
|
||
|
|
if ($('#datasets tbody tr.selected').length > 0) {
|
||
|
|
$(".deleteSelectedRows").removeAttr('disabled');
|
||
|
|
} else {
|
||
|
|
$(".deleteSelectedRows").attr('disabled', 'disabled');
|
||
|
|
}
|
||
|
|
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// switch to edit mode
|
||
|
|
editMode = true;
|
||
|
|
$(this).find('.button-text').text(multiSelectTitleTrans);
|
||
|
|
$(this).find('i').removeClass('fa-edit').addClass('fa-object-group');
|
||
|
|
$('.dataset-edit-title-mode').text(editModeTitleTrans);
|
||
|
|
$('.widget-title .badge').attr('title', editModeHelpTrans);
|
||
|
|
$(".deleteSelectedRows").addClass('d-none');
|
||
|
|
$("#datasets tbody").off('click', 'tr');
|
||
|
|
$('#datasets tbody tr.selected').removeClass('selected');
|
||
|
|
$(".deleteSelectedRows").attr('disabled', 'disabled');
|
||
|
|
$("#datasets tbody").on('click', 'tr', dataSetEditFormOpen);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// get selected rows and their ids then pass it to dataSet data delete ajax call and reload the grid when done.
|
||
|
|
$('#deleteSelectedRows').on("click", function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
var rows = [];
|
||
|
|
var rowIds = [];
|
||
|
|
var processedRows = 0;
|
||
|
|
|
||
|
|
$('#datasets tbody tr.selected').each(function(idx, ele) {
|
||
|
|
rows.push(table.row(ele).data());
|
||
|
|
});
|
||
|
|
|
||
|
|
for (var i = 0; i < rows.length; i++) {
|
||
|
|
rowIds.push(rows[i].id);
|
||
|
|
}
|
||
|
|
|
||
|
|
rowIds.forEach(function(id) {
|
||
|
|
$.ajax({
|
||
|
|
url: "{{ url_for("dataSet.data.delete", {"id": dataSet.dataSetId, "rowId":':rowId'}) }}".replace(":rowId", id),
|
||
|
|
type: "DELETE",
|
||
|
|
success: function (data) {
|
||
|
|
processedRows++;
|
||
|
|
if (processedRows === rowIds.length) {
|
||
|
|
table.ajax.reload();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// handle add row button when we don't have any value columns in the dataset
|
||
|
|
var valueColumns = {{ dataSet.columns|filter(column => column.dataSetColumnTypeId == '1')|json_encode|raw }};
|
||
|
|
var valueColumnsWarningMessage = "{% trans "No value columns have been configured for this dataset. Please configure your columns accordingly." %}";
|
||
|
|
if(valueColumns.length === 0) {
|
||
|
|
// remove class xibo form open to prevent opening the form
|
||
|
|
$('.addRowButton')
|
||
|
|
.addClass('disabled')
|
||
|
|
.removeClass('XiboFormButton')
|
||
|
|
.on('click', function(e) {
|
||
|
|
// show toast notification
|
||
|
|
toastr.error(valueColumnsWarningMessage);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|