BUG: Fix random issues found during testing

This commit is contained in:
Ivymaster
2026-06-27 17:54:24 +02:00
parent 4ed87b5c2b
commit db5339e4a5
5 changed files with 234 additions and 64 deletions
@@ -1,7 +1,13 @@
import { CoreFilterCmpt } from "../../filter/Filter.js";
import ListNew from "./List/New.js";
import CoreTag from "../../Tag/Tag.js";
import { buildTagHeaderFilterExpression, buildTagOptionsFromRows, customTagFilter, setTagHeaderFilterValue, tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js";
import {
buildTagHeaderFilterExpression,
buildTagOptionsFromRows,
customTagFilter,
setTagHeaderFilterValue,
tagHeaderFilter,
} from "../../../tabulator/filters/extendedHeaderFilter.js";
import {
addTagInTable,
deleteTagInTable,
@@ -68,6 +74,15 @@ export default {
initialOptions: [],
selectedOptions: [],
},
tagFilterLabels: {
tag: "Tag",
clear: "Clear",
connectors: {
AND: "AND",
OR: "OR",
NOT: "NOT",
},
},
selectedColumnValues: [],
tabulatorOptions: {
columns: [
@@ -383,7 +398,15 @@ export default {
selectableRows: true,
selectableRowsRangeMode: "click",
index: "prestudent_id",
persistence: false,
persistence: {
sort: true,
columns: ["width", "visible"],
filter: false,
headerFilter: false,
group: false,
page: false,
},
persistenceID: "stv-studentenverwaltung-list-2",
},
tabulatorEvents: [
{
@@ -401,7 +424,6 @@ export default {
{
event: "dataProcessed",
handler: (data) => {
//console.log("dataProcessed", data);
this.getAllRows();
this.autoSelectRows(data);
},
@@ -409,7 +431,6 @@ export default {
{
event: "dataLoaded",
handler: (data) => {
//console.log("dataLoaded", data);
if (Array.isArray(data)) {
this.count = data.length;
this.allPrestudents = data.map((item) => item.prestudent_id);
@@ -417,14 +438,16 @@ export default {
this.count = 0;
this.allPrestudents = [];
}
this.refreshTagHeaderFilterInitialOptions(Array.isArray(data) ? data : []);
this.refreshTagHeaderFilterInitialOptions(
Array.isArray(data) ? data : [],
);
},
},
{
event: "dataFiltered",
handler: (filters, rows) => {
//console.log("dataFiltered", filters, rows);
this.filteredcount = rows.length;
this.syncSelectedTagOptionsWithHeaderFilters(filters);
},
},
{
@@ -434,14 +457,12 @@ export default {
{
event: "dataTreeRowExpanded",
handler: (data) => {
//console.log("dataTreeRowExpanded", data);
this.getExpandedRows();
},
},
{
event: "dataTreeRowCollapsed",
handler: (data) => {
//console.log("dataTreeRowCollapsed", data);
this.getExpandedRows();
},
},
@@ -452,7 +473,6 @@ export default {
{
event: "columnWidth",
handler: (column) => {
//console.log("columnWidth", column);
if (column.getField() !== "tags") return;
column.getCells().forEach((cell) => {
@@ -558,29 +578,6 @@ export default {
);
},
},
created: function () {
if (this.tagsEnabled) {
const coltags = {
title: "Tags",
field: "tags",
tooltip: false,
headerFilter: customTagFilter,
headerFilterParams: {
listOnEmpty: true,
autocomplete: true,
sort: "asc",
initialOptions: this.tagFilterState.initialOptions,
selectedOptions: this.tagFilterState.selectedOptions,
},
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: { field: "tags" },
formatter: (cell, formatterParams, onRendered) =>
tagFormatter(cell, this.$refs.tagComponent, onRendered),
width: 150,
};
this.tabulatorOptions.columns.splice(2, 0, coltags);
}
},
watch: {
"$p.user_language.value"(n, o) {
if (n !== o && o !== undefined && this.$refs.table.tableBuilt) {
@@ -590,9 +587,8 @@ export default {
"tagFilterState.selectedOptions": {
handler() {
const selectedOptions = this.tagFilterState.selectedOptions;
const combinedFilterStatement = buildTagHeaderFilterExpression(
selectedOptions,
);
const combinedFilterStatement =
buildTagHeaderFilterExpression(selectedOptions);
setTagHeaderFilterValue(
this.$refs.table.tabulator,
@@ -616,6 +612,8 @@ export default {
"stv",
])
.then(() => {
this.updateTagFilterLabels();
const translations = {
uid: capitalize(this.$p.t("person/uid")),
titelpre: capitalize(this.$p.t("person/titelpre")),
@@ -694,13 +692,11 @@ export default {
this.$refs.new.open();
},
rowSelectionChanged(data, rows, selected, deselected) {
//console.log("rowSelectionChanged", data, rows, selected, deselected);
this.selectedcount = data.length;
if (selected.length > 0 || deselected.length > 0) {
this.lastSelected = this.selected;
//for tags
this.selectedRows = this.$refs.table.tabulator.getSelectedRows();
this.selectedColumnValues = this.selectedRows
.filter(
@@ -738,12 +734,16 @@ export default {
this.$emit("filterActive", filter);
this.updateUrl();
},
updateTagFilterLabels() {
this.tagFilterLabels.clear = this.$p.t("ui/filterdelete");
this.tagFilterLabels.tag = 'Tag';
this.tagFilterLabels.connectors.AND = this.$p.t("ui/andCondition").toUpperCase();
this.tagFilterLabels.connectors.OR = this.$p.t("ui/orCondition").toUpperCase();
this.tagFilterLabels.connectors.NOT = this.$p.t("ui/notCondition").toUpperCase();
},
updateUrl(endpoint, first) {
this.lastSelected = first ? undefined : this.selected;
/* //console.log('function param endpoint: ' + JSON.stringify(endpoint));
//console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/
if (endpoint === undefined && this.currentEndpoint === null) {
endpoint = { url: "" };
} else if (endpoint === undefined) {
@@ -847,7 +847,6 @@ export default {
}
},
handleRowClick(e, row) {
//console.log("handleRowClick", e, row);
// TODO(chris): this should be in the filter component
if (this.focusObj) {
let el = row.getElement();
@@ -864,7 +863,6 @@ export default {
this.refreshTagHeaderFilterInitialOptions();
},
deletedTag(deletedTag) {
console.log("deletedTag", deletedTag);
deleteTagInTable(deletedTag, this.allRows);
this.refreshTagHeaderFilterInitialOptions();
},
@@ -873,8 +871,7 @@ export default {
this.refreshTagHeaderFilterInitialOptions();
},
refreshTagHeaderFilterInitialOptions(rows) {
const data =
rows || this.$refs.table?.tabulator?.getData() || [];
const data = rows || this.$refs.table?.tabulator?.getData() || [];
const options = buildTagOptionsFromRows(data);
this.tagFilterState.initialOptions.splice(
@@ -884,8 +881,14 @@ export default {
);
const availableValues = new Set(options.map((option) => option.value));
for (let i = this.tagFilterState.selectedOptions.length - 1; i >= 0; i--) {
if (!availableValues.has(this.tagFilterState.selectedOptions[i].value)) {
for (
let i = this.tagFilterState.selectedOptions.length - 1;
i >= 0;
i--
) {
if (
!availableValues.has(this.tagFilterState.selectedOptions[i].value)
) {
this.tagFilterState.selectedOptions.splice(i, 1);
}
}
@@ -894,14 +897,37 @@ export default {
this.allRows = this.$refs.table.tabulator.getRows();
},
resetFilter() {
this.clearSelectedTagHeaderFilters();
this.$refs.listfilter.resetFilter();
this.$refs.table.clearFilters();
},
clearSelectedTagHeaderFilters() {
this.tagFilterState.selectedOptions.splice(
0,
this.tagFilterState.selectedOptions.length,
);
},
syncSelectedTagOptionsWithHeaderFilters(filters) {
if (!this.tagsEnabled || !this.tagFilterState.selectedOptions.length) {
return;
}
const hasActiveTagFilter = filters.some(
(filter) =>
filter.field === "tags" &&
filter.value !== "" &&
filter.value !== null &&
filter.value !== undefined,
);
if (!hasActiveTagFilter) {
this.clearSelectedTagHeaderFilters();
}
},
handleHeaderFilter(filterActive) {
this.headerFilterActive = filterActive;
},
handleMouseDown(e, row) {
//console.log("handleMouseDown", e, row);
let data = row.getData();
let id = data.uid ?? data.prestudent_id ?? data.person_id;
@@ -913,12 +939,10 @@ export default {
isAlreadySelected && this.selected?.length ? this.selected : [data];
},
handleDataLoading() {
//console.log("handleDataLoading");
this.oldScrollLeft = this.$refs.table.tabulator.rowManager.scrollLeft;
this.oldScrollTop = this.$refs.table.tabulator.rowManager.scrollTop;
},
handleRenderComplete() {
//console.log("handleRenderComplete");
const table = this.$refs.table.tabulator.element.querySelector(
".tabulator-tableholder",
);
@@ -935,6 +959,33 @@ export default {
}
},
},
created: function () {
if (this.tagsEnabled) {
const coltags = {
title: "Tags",
field: "tags",
tooltip: false,
headerFilter: customTagFilter,
headerFilterParams: {
listOnEmpty: true,
autocomplete: true,
sort: "asc",
initialOptions: this.tagFilterState.initialOptions,
selectedOptions: this.tagFilterState.selectedOptions,
labels: this.tagFilterLabels,
},
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: { field: "tags" },
formatter: (cell, formatterParams, onRendered) =>
tagFormatter(cell, this.$refs.tagComponent, onRendered),
width: 150,
};
this.tabulatorOptions.columns.splice(2, 0, coltags);
this.$p.loadCategory("ui").then(() => {
this.updateTagFilterLabels();
});
}
},
// TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component
// TODO(chris): filter component column chooser has no accessibilty features
template: `
+18 -4
View File
@@ -15,7 +15,12 @@ export function addTagInTable(addedTag, rows, matchKey, tagsKey = "tags")
let tags;
try {
tags = JSON.parse(rowData[tagsKey] || "[]");
let initialTags = rowData[tagsKey] || "[]";
if (typeof initialTags === 'string') {
tags = JSON.parse(initialTags);
} else {
tags = initialTags;
}
} catch (e) {
tags = [];
}
@@ -53,7 +58,12 @@ export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags'])
let tags;
try {
tags = JSON.parse(rowData[key] || "[]");
let initialTags = rowData[key] || "[]";
if (typeof initialTags === 'string') {
tags = JSON.parse(initialTags);
} else {
tags = initialTags;
}
} catch (e) {
tags = [];
}
@@ -62,7 +72,6 @@ export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags'])
return;
let filtered = tags.filter(tag => tag?.id !== deletedTag);
if (filtered.length !== tags.length)
{
updates[key] = JSON.stringify(filtered);
@@ -95,7 +104,12 @@ export function updateTagInTable(updatedTag, rows, fields = ['tags'])
let fieldData;
try {
fieldData = JSON.parse(rowData[field] || "[]");
let initialTags = rowData[field] || "[]";
if (typeof initialTags === 'string') {
fieldData = JSON.parse(initialTags);
} else {
fieldData = initialTags;
}
} catch (e) {
return;
}
@@ -22,6 +22,10 @@ const TAG_FILTER_CONNECTOR_ORDER = [
TAG_FILTER_CONNECTORS.NOT,
];
function getConnectorLabel(connector, labels) {
return labels?.connectors?.[connector.key] || connector.label;
}
export function setTagHeaderFilterValue(tabulator, value) {
tabulator?.setHeaderFilterValue("tags", value);
@@ -147,7 +151,7 @@ export function customTagFilter(cell, onRendered, success, cancel, params) {
closeDropdown();
};
const clearFilter = () => {
const clearFilter = ({ closeDropdownAfterClear = true } = {}) => {
input.value = "";
if (Array.isArray(params.selectedOptions)) {
@@ -158,7 +162,10 @@ export function customTagFilter(cell, onRendered, success, cancel, params) {
}
success("");
closeDropdown();
if (closeDropdownAfterClear) {
closeDropdown();
}
};
input.addEventListener("input", () => {
@@ -184,6 +191,8 @@ export function customTagFilter(cell, onRendered, success, cancel, params) {
dropdownMenu = getRebuiltDropdown(
params.initialOptions,
params.selectedOptions,
params.labels,
clearFilter,
);
dropdownMenu.addEventListener("mousedown", (event) => {
event.stopPropagation();
@@ -207,21 +216,48 @@ export function customTagFilter(cell, onRendered, success, cancel, params) {
return container;
}
function getRebuiltDropdown(initialOptions, selectedOptions) {
let dropdownTable = generateDropdownTable(
function getRebuiltDropdown(
initialOptions,
selectedOptions,
labels,
clearFilter,
) {
const dropdownTable = generateDropdownTable(
initialOptions || [],
selectedOptions || [],
labels,
);
const menu = document.createElement("div");
menu.className = "dropdown-menu show";
menu.className = "dropdown-menu show py-3 px-1";
const clearButton = document.createElement("button");
clearButton.type = "button";
clearButton.className =
"btn btn-link btn-sm p-0 position-absolute top-0 end-0 m-1";
clearButton.title = labels?.clear || "Clear";
clearButton.setAttribute("aria-label", labels?.clear || "Clear");
const clearIcon = document.createElement("i");
clearIcon.className = "fa-solid fa-filter-circle-xmark";
clearButton.appendChild(clearIcon);
clearButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
clearFilter({ closeDropdownAfterClear: false });
menu.querySelectorAll("input[type='checkbox']").forEach((checkbox) => {
checkbox.checked = false;
});
});
menu.appendChild(clearButton);
menu.appendChild(dropdownTable);
return menu;
}
function generateDropdownTable(options, selectedTagOptions) {
function generateDropdownTable(options, selectedTagOptions, labels) {
const table = document.createElement("table");
table.className = "table table-sm mb-0";
@@ -230,13 +266,21 @@ function generateDropdownTable(options, selectedTagOptions) {
const headRow = document.createElement("tr");
const headers = [
"Tag",
...TAG_FILTER_CONNECTOR_ORDER.map((connector) => connector.label),
labels?.tag || "Tag",
...TAG_FILTER_CONNECTOR_ORDER.map((connector) =>
getConnectorLabel(connector, labels),
),
];
headers.forEach((h) => {
headers.forEach((h, index) => {
const th = document.createElement("th");
th.innerHTML = `<strong>${h}</strong>`;
if (index > 0) {
th.className = "text-center";
}
const strong = document.createElement("strong");
strong.textContent = h;
th.appendChild(strong);
headRow.appendChild(th);
});
@@ -260,6 +304,8 @@ function generateDropdownTable(options, selectedTagOptions) {
const td = document.createElement("td");
const cb = document.createElement("input");
td.className = "text-center align-middle";
cb.type = "checkbox";
cb.style.cursor = "pointer";
-1
View File
@@ -122,7 +122,6 @@ export function tagFormatter(cell, tagComponent, onRendered) {
});
}
console.log("tagFormatter called with:")
renderTags();
return container;
}
+60
View File
@@ -58377,6 +58377,66 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'andCondition',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'und',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'and',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'orCondition',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'oder',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'or',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'notCondition',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'nicht',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'not',
'description' => '',
'insertvon' => 'system'
)
)
),
);