Various fixes to tables

- Fixed errors related to how sortBy changes when column visibility is toggled.
- Fixed player page links in extension tables
- All Extension tables are now colored DataTables instead of being different based on row count.
This commit is contained in:
Aurora Lahtela 2023-10-15 18:16:23 +03:00
parent b076308ec3
commit dae9ebe362
4 changed files with 31 additions and 100 deletions

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat;
import org.apache.commons.text.StringEscapeUtils;
import java.util.ArrayList;
import java.util.Arrays;
@ -76,7 +77,7 @@ public class TableDto {
case DATE_SECOND:
return Formatters.getInstance().secondLong().apply(Long.parseLong(value.toString()));
case PLAYER_NAME:
return Html.LINK.create("../player/" + Html.encodeToURL(value.toString()));
return Html.LINK.create("../player/" + Html.encodeToURL(value.toString()), StringEscapeUtils.escapeHtml4(value.toString()));
default:
return value.toString();
}

View File

@ -201,7 +201,9 @@ public class PlayersTableJSONCreator {
Html link = openPlayerPageInNewTab ? Html.LINK_EXTERNAL : Html.LINK;
putDataEntry(dataJson, link.create(url, StringUtils.replace(StringEscapeUtils.escapeHtml4(name), "\\", "\\\\") /* Backslashes escaped to prevent json errors */), "name");
/* Backslashes escaped to prevent json errors */
String escapedName = StringUtils.replace(StringEscapeUtils.escapeHtml4(name), "\\", "\\\\");
putDataEntry(dataJson, link.create(url, escapedName, escapedName), "name");
putDataEntry(dataJson, activityIndex.getValue(), activityString, "index");
putDataEntry(dataJson, activePlaytime, numberFormatters.get(FormatType.TIME_MILLISECONDS).apply(activePlaytime), "activePlaytime");
putDataEntry(dataJson, loginTimes, "sessions");

View File

@ -1,15 +1,21 @@
import React, {useCallback, useState} from 'react';
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import React, {useState} from 'react';
import ExtensionIcon from "./ExtensionIcon";
import DataTablesTable from "../table/DataTablesTable";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faSort, faSortDown, faSortUp} from "@fortawesome/free-solid-svg-icons";
import ColoredText from "../text/ColoredText";
import {baseAddress} from "../../service/backendConfiguration";
const ExtensionDataTable = ({table}) => {
const [id] = useState("extension-table-" + new Date().getTime() + "-" + (Math.floor(Math.random() * 100000)));
const mapToCell = (value, j) => {
if (String(value).startsWith("<a class=\"link\" href=\"")) {
let linkHtml = value || String(value);
linkHtml = linkHtml.replace("../", baseAddress + '/');
return <span dangerouslySetInnerHTML={{__html: linkHtml}}/>;
}
return <ColoredText text={value || String(value)}/>
};
const data = {
columns: table.table.columns.map((column, i) => {
return {
@ -24,7 +30,7 @@ const ExtensionDataTable = ({table}) => {
const dataRow = {};
row.forEach((cell, j) => {
dataRow[`col${j}Value`] = cell['valueUnformatted'] || cell.value || cell;
dataRow[`col${j}Display`] = cell.value || cell;
dataRow[`col${j}Display`] = mapToCell(cell.value || cell, j);
});
return dataRow;
})
@ -37,96 +43,17 @@ const ExtensionDataTable = ({table}) => {
pagingType: "numbers",
order: [[1, "desc"]]
}
return (
<DataTablesTable id={id} options={options}/>
)
}
const sortComparator = (columnIndex) => (rowA, rowB) => {
const a = rowA[columnIndex].valueUnformatted;
const b = rowB[columnIndex].valueUnformatted;
if (a === b) return 0;
if (isNaN(Number(a)) || isNaN(Number(b))) {
return String(a).toLowerCase().localeCompare(String(b).toLowerCase());
} else {
const numA = Number(a);
const numB = Number(b);
if (numA < numB) return -1;
if (numA > numB) return 1;
return 0;
const rowKeyFunction = (row, column) => {
return JSON.stringify(Object.entries(row).filter(e => e[0].includes('Value'))) + "-" + JSON.stringify(column?.data?._);
}
}
const sortRows = (rows, sortIndex, sortReversed) => {
if (sortIndex === undefined) return rows;
const comparator = sortComparator(sortIndex);
const sorted = rows.sort(comparator);
if (sortReversed) return rows.reverse();
return sorted;
}
const ExtensionColoredTable = ({table}) => {
const {nightModeEnabled} = useTheme();
const {t} = useTranslation();
const [sortBy, setSortBy] = useState(undefined);
const [sortReverse, setSortReverse] = useState(false);
const changeSort = useCallback(index => {
if (index === sortBy) {
setSortReverse(!sortReverse);
} else {
setSortBy(index);
setSortReverse(false);
}
}, [sortBy, setSortBy, sortReverse, setSortReverse]);
const mapToCell = (value, j) => {
if (String(value?.value).startsWith("<a class=\"link\" href=\"")) {
return <td key={JSON.stringify(value)} dangerouslySetInnerHTML={{__html: value.value || String(value)}}/>;
}
return <td key={JSON.stringify(value)}>
<ColoredText text={value.value || String(value)}/>
</td>;
};
const rows = table.table.rows.length ? sortRows(table.table.rows, sortBy, sortReverse)
.map((row, i) => <tr key={JSON.stringify(row)}>{row.map(mapToCell)}</tr>) :
<tr>{table.table.columns.map((column, i) =>
<td key={JSON.stringify(column)}>{i === 0 ? t('generic.noData') : '-'}</td>)}
</tr>
return (
<table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead className={table.tableColorClass}>
<tr>
{table.table.columns.map((column, i) => <th className={'extension-table-header'}
key={JSON.stringify(column)} onClick={() => changeSort(i)}>
<ExtensionIcon icon={table.table.icons[i]}/>
&nbsp;
{column}
&nbsp;
<FontAwesomeIcon className={sortBy === i ? '' : 'opacity-50'}
icon={sortBy === i ? (sortReverse ? faSortDown : faSortUp) : faSort}/>
</th>)}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<DataTablesTable id={id} options={options} rowKeyFunction={rowKeyFunction} colorClass={table.tableColorClass}/>
)
}
const ExtensionTable = ({table}) => {
const tableLength = table.table.rows.length;
if (tableLength > 10) {
return <ExtensionDataTable table={table}/>
} else {
return <ExtensionColoredTable table={table}/>
}
return <ExtensionDataTable table={table}/>;
}
export default ExtensionTable

View File

@ -79,7 +79,7 @@ const VisibleColumnsSelector = ({columns, visibleColumnIndexes, toggleColumn}) =
)
}
const DataTablesTable = ({id, rowKeyFunction, options}) => {
const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
@ -88,22 +88,23 @@ const DataTablesTable = ({id, rowKeyFunction, options}) => {
const [sortReversed, setSortReversed] = useState(options.order[0][1] === 'asc');
const [visibleColumnIndexes, setVisibleColumnIndexes] = useState(columns.map((_, i) => i));
const toggleColumn = useCallback(index => {
const currentSortIndex = visibleColumnIndexes[sortBy];
if (visibleColumnIndexes.includes(index)) {
if (visibleColumnIndexes.length === 1) return;
const newVisible = visibleColumnIndexes.filter(i => i !== index);
newVisible.sort((a, b) => a - b);
setVisibleColumnIndexes(newVisible);
if (sortBy === index) {
if (currentSortIndex === index) {
setSortBy(0);
setSortReversed(false);
} else if (index < sortBy) {
} else if (index <= currentSortIndex) {
setSortBy(sortBy - 1); // Keep the current sort
}
} else {
const newVisible = [index, ...visibleColumnIndexes];
newVisible.sort((a, b) => a - b);
setVisibleColumnIndexes(newVisible);
if (sortBy >= index) {
if (currentSortIndex >= index) {
setSortBy(sortBy + 1); // Keep the current sort
}
}
@ -198,14 +199,14 @@ const DataTablesTable = ({id, rowKeyFunction, options}) => {
<div className={"float-end"}>
<SearchField className={"dataTables_filter"} value={filter} setValue={setFilter}/>
</div>
<div className={"float-start dataTables_columns"}>
{columns.length > 2 && <div className={"float-start dataTables_columns"}>
<VisibleColumnsSelector columns={columns} visibleColumnIndexes={visibleColumnIndexes}
toggleColumn={toggleColumn}/>
</div>
</div>}
<table id={id}
className={"datatable table table-bordered table-striped" + (nightModeEnabled ? " table-dark" : '')}
style={{width: "100%"}}>
<thead id={id + '-head'}>
<thead id={id + '-head'} className={colorClass}>
<tr>
{visibleColumns.map((column, i) => <th key={JSON.stringify(column.data)}>
<button onClick={() => changeSort(i)}>
@ -268,7 +269,7 @@ const DataTablesTable = ({id, rowKeyFunction, options}) => {
</React.Fragment>)}
</tbody>
</table>
<p className={"dataTables_info float-start"}>
<p className={"dataTables_info float-start"} style={{maxWidth: "40%", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
<Trans i18nKey={"html.label.table.showNofM"}
defaults={"Showing {{n}} of {{m}} entries"}
values={{
@ -276,7 +277,7 @@ const DataTablesTable = ({id, rowKeyFunction, options}) => {
m: matchingData.length
}}/>
</p>
<div className={"float-end"}>
<div className={"float-end"} style={{maxWidth: "60%"}}>
<Pagination page={page} setPage={setPage} maxPage={maxPage}/>
</div>
</div>