Corban-Lee Jones 9cbb4fe586
Some checks failed
Build and Push Docker Image / build (push) Failing after 6m59s
share & delete btn disabled
2024-11-04 23:05:39 +00:00

616 lines
18 KiB
JavaScript

// region Init Table
function initializeDataTable(tableId, columns) {
$(tableId).DataTable({
info: false,
paging: false,
ordering: false,
searching: false,
autoWidth: false,
order: [],
language: {
emptyTable: "No results found"
},
select: {
style: "multi+shift",
selector: 'th:first-child input[type="checkbox"]'
},
columnDefs: [
{ orderable: false, targets: "no-sort" },
{
targets: 0,
checkboxes: { selectRow: true }
},
],
columns: [
{
// Select row checkbox column
title: '<input type="checkbox" class="form-check-input table-select-all" />',
data: null,
orderable: false,
className: "col-checkbox text-center",
render: function() {
return '<input type="checkbox" class="form-check-input table-select-row" />'
}
},
{ data: "id", visible: false },
...columns
]
});
bindTablePagination(tableId);
bindTablePageSizer(tableId);
bindTableSearch(tableId);
bindRefreshButton(tableId);
bindTableSelectColumn(tableId);
}
// region Filters & Ordering
// Filter methods
var _tableFilters = {};
function getTableFilters(tableId) {
return _tableFilters[tableId];
}
function setTableFilter(tableId, key, value) {
if (!_tableFilters[tableId]) {
_tableFilters[tableId] = {};
}
_tableFilters[tableId][key] = value;
}
// Sort methods
var _tableOrdering = {};
function getTableOrdering(tableId) {
return _tableOrdering[tableId];
}
function setTableOrdering(tableId, value) {
_tableOrdering[tableId] = value;
}
// Clear all kinds of sorting and filtering when changing servers
$(document).on("selectedServerChange", function() {
_tableFilters = {};
_tableOrdering = {};
$(".table-search-input").val("");
});
// region Load & Clear Data
function wipeTable(tableId) {
$(`${tableId} thead .table-select-all`).prop("checked", false).prop("indeterminate", false);
$(tableId).DataTable().clear().draw(false)
}
function populateTable(tableId, data) {
$(tableId).DataTable().rows.add(data.results).draw(false);
updateTablePagination(tableId, data);
updateTableTotalCount(tableId, data);
}
async function loadTableData(tableId, url, method) {
fixTablePagination(tableId);
disableTableControls(tableId);
// Create querystring for filtering against the API
const filters = getTableFilters(tableId);
const ordering = getTableOrdering(tableId);
const querystring = makeQuerystring(filters, ordering);
// API request
const data = await ajaxRequest(url + querystring, method);
// Update table with new data
wipeTable(tableId);
populateTable(tableId, data);
enableTableControls(tableId);
}
// region Pagination
function bindTablePagination(tableId) {
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
$paginationArea.on("click", ".page-link", function() {
let currentPage = parseInt($paginationArea.data("page"));
let wantedPage;
if ($(this).hasClass("page-prev")) {
wantedPage = currentPage - 1;
}
else if ($(this).hasClass("page-next")) {
wantedPage = currentPage + 1;
}
else {
wantedPage = $(this).attr("data-page")
}
setTableFilter(tableId, "page", wantedPage);
$(tableId).trigger("doDataLoad");
});
}
function fixTablePagination(tableId) {
let filters = getTableFilters(tableId);
let $pageSizer = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find(".table-page-sizer");
if (!("page" in filters)) {
setTableFilter(tableId, "page", 1);
}
if (!("page_size" in filters)) {
setTableFilter(tableId, "page_size", $($pageSizer).val())
}
}
function updateTablePagination(tableId, data) {
let filters = getTableFilters(tableId);
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
$paginationArea.data("page", filters.page); // store the page for later
// Remove existing buttons for specific pages
$paginationArea.find(".page-pick").remove();
// Enable/disable 'next' and 'prev' buttons
$paginationArea.find(".page-prev").toggleClass("disabled", !data.previous);
$paginationArea.find(".page-next").toggleClass("disabled", !data.next);
const pagesToShow = Math.max(Math.ceil(data.count / filters.page_size), 1);
const maxPagesToShow = 10;
let startPage = 1;
let endPage;
let page = parseInt(filters.page);
// Determine the start and end page
if (pagesToShow <= maxPagesToShow) {
endPage = pagesToShow;
}
else {
const halfVisible = Math.floor(maxPagesToShow / 2);
if (page <= halfVisible) {
endPage = maxPagesToShow;
}
else if (page + halfVisible >= pagesToShow) {
startPage = pagesToShow - maxPagesToShow + 1;
endPage = pagesToShow;
}
else {
startPage = page - halfVisible;
endPage = page + halfVisible;
}
}
// Add buttons
if (startPage > 1) {
AddTablePageButton($paginationArea, 1);
if (startPage > 2) {
AddTablePageEllipsis($paginationArea);
}
}
for (i = startPage; i <= endPage; i++) {
AddTablePageButton($paginationArea, i, page);
}
if (endPage < pagesToShow) {
if (endPage < pagesToShow - 1) {
AddTablePageEllipsis($paginationArea);
}
AddTablePageButton($paginationArea, pagesToShow);
}
}
function AddTablePageButton($paginationArea, number, currentPage) {
let pageItem = $("<li>").addClass("page-item");
let pageLink = $("<button>")
.attr("type", "button")
.attr("data-page", number)
.addClass("page-link page-pick")
.text(number);
if (number === parseInt(currentPage)) {
pageLink.addClass("disabled").attr("tabindex", -1);
}
pageItem.append(pageLink);
$paginationArea.find(".page-next").parent().before(pageItem);
}
function AddTablePageEllipsis($paginationArea) {
let ellipsisItem = $("<li>").addClass("page-item disabled");
let ellipsisLink = $("<span>").addClass("page-link page-pick").text("...");
ellipsisItem.append(ellipsisLink);
$paginationArea.find(".page-next").parent().before(ellipsisItem);
}
// region Page Sizer
function updateTableTotalCount(tableId, data) {
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
$tableControls.find('.pageinfo-total').text(data.count);
}
function bindTablePageSizer(tableId) {
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
$tableControls.on("change", ".table-page-sizer", function() {
setTableFilter(tableId, "page", "1");
setTableFilter(tableId, "page_size", $(this).val());
$(tableId).trigger("doDataLoad");
});
}
// region Search Filters
_searchTimeouts = {};
function bindTableSearch(tableId) {
const $tableFilters = $(tableId).closest('.js-tableBody').siblings('.js-tableFilters');
$tableFilters.on("input", ".table-search-input", function() {
$(this).data("was-focused", true);
clearTimeout(_searchTimeouts[tableId]);
const searchString = $(this).val();
setTableFilter(tableId, "search", searchString);
setTableFilter(tableId, "page", 1); // back to first page, as desired page no. might not exist after filtering
_searchTimeouts[tableId] = setTimeout(function() {
$(tableId).trigger("doDataLoad");
}, 300);
});
}
// region Button Controls
function bindRefreshButton(tableId) {
$controls = getTableFiltersComponent(tableId);
$controls.on("click", ".js-tableRefreshBtn", function() {
$controls.find(".js-tableDeleteBtn").prop("disabled", true);
$controls.find(".js-tableShareBtn").prop("disabled", true);
$(tableId).trigger("doDataLoad");
})
}
// region Select Checkboxes
function bindTableSelectColumn(tableId) {
$(tableId).on("change", "tbody tr .table-select-row", function() {
let selected = $(this).prop("checked");
let rowIndex = $(this).closest("tr").index();
let row = $(tableId).DataTable().row(rowIndex);
selected === true ? row.select() : row.deselect();
determineSelectAllState(tableId);
});
$(tableId).on("change", "thead .table-select-all", function() {
let selected = $(this).prop("checked");
let table = $(tableId).DataTable();
$(tableId).find("tbody tr").each(function(rowIndex) {
let row = table.row(rowIndex);
selected === true ? row.select() : row.deselect();
$(this).find(".table-select-row").prop("checked", selected);
});
determineSelectAllState(tableId);
});
}
function determineSelectAllState(tableId) {
let table = $(tableId).DataTable();
let selectedRowsCount = table.rows(".selected").data().toArray().length;
let allRowsCount = table.rows().data().toArray().length;
let doCheck = selectedRowsCount === allRowsCount;
let doIndeterminate = !doCheck && selectedRowsCount > 0;
$checkbox = $(tableId).find("thead .table-select-all");
$checkbox.prop("checked", doCheck);
$checkbox.prop("indeterminate", doIndeterminate);
const selectionExists = doCheck || doIndeterminate;
const $controls = getTableFiltersComponent(tableId);
$controls.find(".js-tableShareBtn").prop("disabled", !selectionExists);
$controls.find(".js-tableDeleteBtn").prop("disabled", !selectionExists);
}
// region On/Off Controls
function enableTableControls(tableId) {
setTableControlsUsability(tableId, false);
}
function disableTableControls(tableId) {
setTableControlsUsability(tableId, true);
}
function setTableControlsUsability(tableId, disabled) {
const $table = $(tableId);
const $tableBody = $table.closest(".js-tableBody");
const $tableFilters = $tableBody.siblings(".js-tableFilters");
const $tableControls = $tableBody.siblings(".tableControls");
$tableBody.find(".disable-while-loading").prop("disabled", disabled);
$tableFilters.find(".disable-while-loading").prop("disabled", disabled);
$tableControls.find(".disable-while-loading").prop("disabled", disabled);
// Re-focus search bar if used
const $search = $tableFilters.find(".disable-while-loading[type=search]");
$search.data("was-focused") ? $search.focus() : null;
}
// region Modals
async function openDataModal(modalId, pk, url) {
$modal = $(modalId);
$modal.data("primary-key", pk);
clearValidation($modal);
if (parseInt(pk) === -1) {
$modal.find(".form-create").show();
$modal.find(".form-edit").hide();
setDefaultModalData($modal);
}
else {
$modal.find(".form-create").hide();
$modal.find(".form-edit").show();
await loadModalData($modal, url);
}
$modal.modal("show");
}
function setDefaultModalData($modal) {
$modal.find("[data-field]").each(function() {
const type = $(this).attr("type");
const defaultVal = $(this).attr("data-default") || "";
if (type === "checkbox") {
$(this).prop("checked", defaultVal === "true");
}
else if (type === "datetime-local") {
$(this).val(getCurrentDateTime());
}
else if ($(this).is("select") && defaultVal === "firstOption") {
$(this).val($(this).find("option:first").val()).change();
}
else {
$(this).val(defaultVal).change();
}
});
}
function clearValidation($modal) {
$modal.find(".invalid-feedback").remove();
$modal.find(".is-invalid").removeClass("is-invalid");
}
async function loadModalData($modal, url) {
const data = await ajaxRequest(url, "GET");
$modal.find("[data-field]").each(function() {
const key = $(this).attr("data-field");
const value = data[key];
if (typeof value === "boolean") {
$(this).prop("checked", value);
}
else if (isISODateTimeString(value)) {
$(this).val(value.split('+')[0].substring(0, 16));
}
else if ($(this).attr("type") === "color") {
$(this).val(`#${value}`);
}
else {
$(this).val(value).change();
}
});
}
async function onModalSubmit($modal, $table, url) {
if (!selectedServer) {
return;
}
clearValidation($modal);
let data = { server: selectedServer.id };
$modal.find("[data-field]").each(function() {
const type = $(this).attr("type");
const key = $(this).attr("data-field");
if (!key) {
return;
}
let value;
switch (type) {
case "checkbox":
value = $(this).prop("checked");
break;
case "color":
value = $(this).val();
value = value ? value.replace("#", "") : value;
break;
default:
value = $(this).val();
break;
}
data[key] = value;
});
const formData = objectToFormData(data);
const id = $modal.data("primary-key");
const isNewItem = parseInt(id) !== -1;
const method = isNewItem ? "PUT" : "POST";
url = isNewItem ? url + `${id}/` : url;
ajaxRequest(url, method, formData)
.then(response => {
$table.trigger("doDataLoad");
$modal.modal("hide");
})
.catch(error => {
logError(error);
if (typeof error === "object" && "responseJSON" in error) {
renderErrorMessages($modal, error.responseJSON);
}
});
}
// region Modal Error Msgs
function renderErrorMessages($modal, errorObj) {
for (const key in errorObj) {
const value = errorObj[key];
const $input = $modal.find(`[data-field="${key}"]`);
$input.addClass("is-invalid");
$input.nextAll(".form-text").last().after(
`<div class="invalid-feedback">${value}</div>`
)
}
}
// region Table Col Types
function renderEditColumn(data) {
const name = sanitise(data);
return `<span class="act-as-link edit-modal" role="button">${name}</span>`
}
function renderAnchorColumn(name, href) {
name = sanitise(name);
href = sanitise(href);
return `<a href="${href}" class="act-as-link">${name}</a>`;
}
function renderBooleanColumn(data) {
const iconClass = data ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-danger";
return `<i class="bi ${iconClass}"></i>`;
}
function renderBadgeColumn(data, colour=null) {
let badge = $(`<span class="badge text-bg-secondary rounded-1 border border-1">${data}</span>`)
if (colour) {
badge[0].style.setProperty("border-color", `#${colour}`, "important");
}
return badge.prop("outerHTML");
}
function renderArrayBadgesColumn(data) {
let badges = $("<div>");
data.forEach((item, index) => {
let badge = $(`<span class="badge text-bg-secondary rounded-1">${item}</span>`);
if (index > 0) { badge.addClass("ms-2") }
badges.append(badge);
});
return badges.html();
}
function renderArrayDropdownColumn(data, icon, headerText) {
if (!data.length) {
return "";
}
let $dropdown = $(`
<div class="dropdown">
<button type="button" class="dropdown-toggle" data-bs-toggle="dropdown">
<i class="fs-5 bi ${icon}"></i>
</button>
<div class="dropdown-menu">
<li><h6 class="dropdown-header">${headerText} (${data.length})</h6></li>
</div>
</div>
`);
let $dropdownItems = $dropdown.find(".dropdown-menu")
data.forEach(item => {
let $itemBtn = $(`<li><button type="button" class="dropdown-item">${item}</button></li>`);
$dropdownItems.append($itemBtn);
});
return $dropdown.prop("outerHTML");
}
const renderHexColourColumn = data => {
const hexWithHashtag = `#${data}`.toUpperCase();
let icon = $("<div>");
icon.addClass("col-hex-icon");
icon.css("background-color", hexWithHashtag);
return $(`<span data-bs-toggle="tooltip" data-bs-title="${hexWithHashtag}">${icon.prop("outerHTML")}</span>`).tooltip()[0];
}
function renderMutatorColumn(data) {
if (!("id" in data)) {
return "";
}
return $(`<span class="badge text-bg-secondary rounded-1">${data.name}</span>`).prop("outerHTML");
}
const renderLinkToStyleColumn = style => {
const hexWithHashtag = `#${style.colour}`.toUpperCase();
let icon = $("<div>");
icon.addClass("col-hex-icon js-openSubStyle");
icon.css("background-color", hexWithHashtag);
return $(`<span data-bs-toggle="tooltip" data-bs-title="${hexWithHashtag}" role="button">${icon.prop("outerHTML")}</span>`).tooltip()[0];
}
const renderPopoverBadgesColumn = (items, iconClass) => {
if (!items.length) {
return "";
}
let $span = $("<span>");
$span.attr("data-bs-toggle", "popover");
$span.attr("data-bs-trigger", "hover focus");
$span.attr("data-bs-custom-class", "table-badge-popover")
$span.attr("data-bs-html", "true");
let $placeholderContainer = $("<div>");
items.forEach(item => {
let $badge = $("<div>");
$badge.addClass("badge text-bg-secondary rounded-1 me-2 mb-2 text-wrap mw-100 text-start");
$badge.text(item);
$placeholderContainer.append($badge);
});
$span.attr("data-bs-content", $placeholderContainer.html());
$span.html(`<i class="bi ${iconClass} fs-5"></i>`)
return $span.popover()[0];
}
// region Get Table Parts
function getTableFiltersComponent(tableId) {
return $(tableId).closest(".js-tableBody").siblings(".js-tableFilters");
}
function getTableControlsComponent(tableId) {
return $(tableId).closest(".js-tableBody").siblings(".js-tableControls");
}
function getSelectedTableRows(tableId) {
return $(tableId).DataTable().rows(".selected").data().toArray();
}