diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js index 3079eb080..32791850f 100644 --- a/public/js/components/Stv/Studentenverwaltung/List.js +++ b/public/js/components/Stv/Studentenverwaltung/List.js @@ -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: ` diff --git a/public/js/helpers/TagHelper.js b/public/js/helpers/TagHelper.js index 9282aa167..3d287c792 100644 --- a/public/js/helpers/TagHelper.js +++ b/public/js/helpers/TagHelper.js @@ -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; } diff --git a/public/js/tabulator/filters/extendedHeaderFilter.js b/public/js/tabulator/filters/extendedHeaderFilter.js index d9f6faf7a..79fd19743 100644 --- a/public/js/tabulator/filters/extendedHeaderFilter.js +++ b/public/js/tabulator/filters/extendedHeaderFilter.js @@ -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 = `${h}`; + 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"; diff --git a/public/js/tabulator/formatter/tags.js b/public/js/tabulator/formatter/tags.js index 48c360b89..2200ca669 100644 --- a/public/js/tabulator/formatter/tags.js +++ b/public/js/tabulator/formatter/tags.js @@ -122,7 +122,6 @@ export function tagFormatter(cell, tagComponent, onRendered) { }); } - console.log("tagFormatter called with:") renderTags(); return container; } diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 05bdfbbef..e9ae6a554 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -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' + ) + ) + ), );