diff --git a/application/controllers/api/frontend/v1/TabulatorPresets.php b/application/controllers/api/frontend/v1/TabulatorPresets.php new file mode 100644 index 000000000..c38cce033 --- /dev/null +++ b/application/controllers/api/frontend/v1/TabulatorPresets.php @@ -0,0 +1,162 @@ +. + */ + +if (! defined('BASEPATH')) exit('No direct script access allowed'); + +/** + * This controller operates between (interface) the JS (GUI) and the PhrasesLib (back-end) + * Provides data to the ajax get calls about the Phrasen plugin + * This controller works with JSON calls on the HTTP GET and the output is always JSON + */ +class TabulatorPresets extends FHCAPI_Controller +{ + public function __construct() + { + parent::__construct([ + 'getTabulatorPresets' => self::PERM_LOGGED, + 'createTabulatorPreset' => self::PERM_LOGGED, + 'updateTabulatorPreset' => self::PERM_LOGGED, + 'deleteTabulatorPreset' => self::PERM_LOGGED, + ]); + + $this->load->model('tabulator/Tabulator_preset_model', 'TabulatorPresetModel'); + + $this->loadPhrases([ + 'global', + 'tabulator_presets' + ]); + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + public function getTabulatorPresets() + { + $tableName = $this->input->get("tableName"); + if (!$tableName) { + $this->terminateWithError(error($this->p->t("global", "invalid_params_err_msg"))); + } + + $uid = getAuthUID(); + $presets = $this->TabulatorPresetModel->getTabulatorPresetsByUserAndTable($uid, $tableName); + $presetsData = $this->getDataOrTerminateWithError($presets) ?? []; + + $this->terminateWithSuccess($presetsData); + } + + public function createTabulatorPreset() + { + $tableName = $this->input->post("tableName"); + $presetName = $this->input->post("presetName"); + $preset = $this->input->post("preset"); + + if (!$tableName || !$presetName || !$preset) { + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); + } + + $uid = getAuthUID(); + + $existingPresetsResult = $this->TabulatorPresetModel->getTabulatorPresetsByUserAndTable($uid, $tableName); + $existingPresets = $this->getDataOrTerminateWithError($existingPresetsResult) ?? []; + + if (count($existingPresets) > 19) { + $this->terminateWithError($this->p->t("tabulator_presets", "max_presets_count_err_msg"), "general", 409); + } + + $existingPresetNames = array_map(function($presetInfo) { + return $presetInfo->preset_name; + }, $existingPresets); + + if (in_array($presetName, $existingPresetNames)) { + $this->terminateWithError($this->p->t("tabulator_presets", "preset_name_duplicate_err_msg"), "general", 409); + } + + $presetCreationResult = $this->TabulatorPresetModel->createTabulatorPreset($uid, $tableName, $presetName, $preset); + + if (isError($presetCreationResult)) { + $this->terminateWithError($presetCreationResult->retval); + } + + $newPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetCreationResult->retval); + $newPresetArray = $this->getDataOrTerminateWithError($newPresetData) ?? [null]; + $newPreset = $newPresetArray[0]; + + $this->terminateWithSuccess($newPreset); + } + + public function updateTabulatorPreset() + { + $presetId = $this->input->post("presetId"); + $preset = $this->input->post("preset"); + + if (!$presetId || !$preset) { + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); + } + + $existingPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $existingPresetArray = $this->getDataOrTerminateWithError($existingPresetData) ?? [null]; + $existingPreset = $existingPresetArray[0]; + if (!$existingPreset) { + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_found_err_msg"), "general", 404); + } + + $uid = getAuthUID(); + if ($existingPreset->benutzer_uid !== $uid) { + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_own_update_err_msg"), "general", 403); + } + + $presetUpdateResult = $this->TabulatorPresetModel->updateTabulatorPreset($presetId, $preset); + if (isError($presetUpdateResult)) { + $this->terminateWithError($presetUpdateResult->retval); + } + + $updatedPresetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $updatedPresetArray = $this->getDataOrTerminateWithError($updatedPresetData) ?? [null]; + $updatedPreset = $updatedPresetArray[0]; + + $this->terminateWithSuccess($updatedPreset); + } + + public function deleteTabulatorPreset() + { + $presetId = $this->input->post("presetId"); + if (!$presetId) { + $this->terminateWithError($this->p->t("global", "invalid_params_err_msg"), "general", 400); + } + + $presetData = $this->TabulatorPresetModel->getTabulatorPreset($presetId); + $presetArray = $this->getDataOrTerminateWithError($presetData) ?? [null]; + $preset = $presetArray[0]; + if (!$preset) { + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_found_err_msg"), "general", 404); + } + + $uid = getAuthUID(); + if ($preset->benutzer_uid !== $uid) { + $this->terminateWithError($this->p->t("tabulator_presets", "preset_not_own_delete_err_msg"), "general", 403); + } + + $presetDeletionResult = $this->TabulatorPresetModel->deleteTabulatorPreset($presetId); + if (isError($presetDeletionResult)) { + $this->terminateWithError($presetDeletionResult->retval); + } + + $this->terminateWithSuccess(); + } + +} \ No newline at end of file diff --git a/application/models/tabulator/Tabulator_preset_model.php b/application/models/tabulator/Tabulator_preset_model.php new file mode 100644 index 000000000..a3c45739d --- /dev/null +++ b/application/models/tabulator/Tabulator_preset_model.php @@ -0,0 +1,83 @@ +dbTable = 'public.tbl_tabulator_presets'; + $this->pk = ['preset_id']; + $this->hasSequence = true; + + + $this->load->model('person/Benutzer_model', 'BenutzerModel'); + } + + public function getTabulatorPreset($presetId) + { + $query = " + SELECT * + FROM public.tbl_tabulator_presets + WHERE preset_id = ? + LIMIT 1 + "; + return $this->execQuery($query, array($presetId)); + } + + public function getTabulatorPresetsByUserAndTable($uid, $tableName) + { + $query = ' + SELECT * + FROM public.tbl_tabulator_presets + WHERE benutzer_uid = ? + AND table_name = ? + ORDER BY preset_id ASC + '; + return $this->execQuery($query, array($uid, $tableName)); + } + + public function createTabulatorPreset($uid, $tableName, $presetName, $preset) + { + $presetCreationResult = $this->insert([ + "benutzer_uid" => $uid, + "table_name" => $tableName, + "preset_name" => $presetName, + "preset_json" => json_encode($preset), + ]); + + if (isError($presetCreationResult)) + { + return error('Something went wrong during preset creation!'); + } + + return success($presetCreationResult->retval); + } + + public function updateTabulatorPreset($presetId, $preset) + { + $presetUpdateResult = $this->update(["preset_id" => $presetId], ["preset_json" => json_encode($preset)]); + + if (isError($presetUpdateResult)) + { + return error('Something went wrong during preset update!'); + } + + return success($presetUpdateResult->retval); + } + + public function deleteTabulatorPreset($presetId) + { + $presetDeletionReseult = $this->delete(["preset_id" => $presetId]); + + if (isError($presetDeletionReseult)) + { + return error('Something went wrong during preset deletion!'); + } + + return success($presetDeletionReseult->retval); + } + +} diff --git a/public/js/api/factory/tabulatorPresets.js b/public/js/api/factory/tabulatorPresets.js new file mode 100644 index 000000000..1653ccb40 --- /dev/null +++ b/public/js/api/factory/tabulatorPresets.js @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getTabulatorPresets(tableName) { + return { + method: "get", + url: + "/api/frontend/v1/TabulatorPresets/getTabulatorPresets?tableName=" + + tableName, + }; + }, + createTabulatorPreset(tableName, presetName, preset) { + return { + method: "post", + url: "/api/frontend/v1/TabulatorPresets/createTabulatorPreset", + params: { + tableName, + presetName, + preset, + }, + }; + }, + updateTabulatorPreset(presetId, preset) { + return { + method: "post", + url: "/api/frontend/v1/TabulatorPresets/updateTabulatorPreset", + params: { + presetId, + preset, + }, + }; + }, + deleteTabulatorPreset(tabulatorPresetId) { + return { + method: "post", + url: + "/api/frontend/v1/TabulatorPresets/deleteTabulatorPreset", + params: { + presetId: tabulatorPresetId, + }, + }; + }, +}; diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 9a6d12e3f..68a3a0d75 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -469,6 +469,7 @@ export default { :tabulator-options="betriebsmittel_table_options" tableOnly :sideMenu="false" + :isUsingPresets="true" /> diff --git a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js index 07a21fcca..3d23e7714 100644 --- a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js @@ -43,6 +43,7 @@ export default { maxWidth: 40, headerClick: this.collapseFunction, visible: true, + responsive: 0, }, { title: "placeholder", @@ -51,6 +52,7 @@ export default { headerFilter: true, minWidth: 200, visible: true, + responsive: 0, }, { title: "placeholder", @@ -59,6 +61,7 @@ export default { headerFilter: true, minWidth: 200, visible: true, + responsive: 1, }, { title: "placeholder", @@ -71,6 +74,7 @@ export default { visible: true, formatter: "datetime", formatterParams: this.datetimeFormatterParams(), + responsive: 4, }, { title: "placeholder", @@ -83,6 +87,7 @@ export default { visible: true, formatter: "datetime", formatterParams: this.datetimeFormatterParams(), + responsive: 3, }, { title: "placeholder", @@ -91,6 +96,7 @@ export default { headerFilter: true, minWidth: 200, visible: true, + responsive: 2, }, ], }, diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index ab7eb0f3d..cba34951c 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -176,7 +176,6 @@ export const Profil = { this.data = data.profil_data.data; this.calendarSyncUrls = data.calendar_sync_urls ?? []; this.authPermissions = data.permissions; - console.log(data.profil_data); }, zustellAdressenCount() { if (!this.data || !this.data.adressen) { diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index b1a6b2c53..b1209e1fd 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -15,25 +15,26 @@ * along with this program. If not, see . */ -import {CoreFetchCmpt} from '../../components/Fetch.js'; -import FilterConfig from './Filter/Config.js'; -import FilterColumns from './Filter/Columns.js'; -import TableDownload from './Table/Download.js'; -import collapseAutoClose from '../../directives/collapseAutoClose.js'; +import { CoreFetchCmpt } from "../../components/Fetch.js"; +import FilterConfig from "./Filter/Config.js"; +import FilterColumns from "./Filter/Columns.js"; +import TablePresets from "./Filter/TablePresets.js"; +import TableDownload from "./Table/Download.js"; +import collapseAutoClose from "../../directives/collapseAutoClose.js"; -import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; -import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js" -import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js" -import ResponsiveLayoutExtensionModule from "../../tabulator/customModules/ResponsiveLayoutExtensionModule.js" +import moduleLayoutFitDataStretchFrozen from "../../tabulator/layouts/fitDataStretchFrozen.js"; +import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js"; +import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js"; +import ResponsiveLayoutExtensionModule from "../../tabulator/customModules/ResponsiveLayoutExtensionModule.js"; import { debounce } from "../../helpers/DebounceHelper.js"; -import ApiFilter from '../../api/factory/filter.js'; -import ApiPhrases from '../../api/factory/phrasen.js'; +import ApiFilter from "../../api/factory/filter.js"; +import ApiPhrases from "../../api/factory/phrasen.js"; // -const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter'; -const FILTER_COMPONENT_NEW_FILTER_TYPE = 'Filter Component New Filter Type'; +const FILTER_COMPONENT_NEW_FILTER = "Filter Component New Filter"; +const FILTER_COMPONENT_NEW_FILTER_TYPE = "Filter Component New Filter Type"; var _uuid = 0; @@ -45,17 +46,18 @@ export const CoreFilterCmpt = { CoreFetchCmpt, FilterConfig, FilterColumns, - TableDownload + TablePresets, + TableDownload, }, directives: { - collapseAutoClose + collapseAutoClose, }, emits: [ - 'nwNewEntry', - 'click:new', - 'tableBuilt', - 'uuidDefined', - 'headerFilterOn' + "nwNewEntry", + "click:new", + "tableBuilt", + "uuidDefined", + "headerFilterOn", ], props: { onNwNewEntry: Function, // NOTE(chris): Hack to get the nwNewEntry listener into $props @@ -63,20 +65,20 @@ export const CoreFilterCmpt = { description: String, sideMenu: { type: Boolean, - default: true + default: true, }, filterType: { - type: String + type: String, }, tabulatorOptions: Object, tabulatorEvents: Array, tableOnly: Boolean, - noColumnFilter:Boolean, + noColumnFilter: Boolean, reload: Boolean, reloadBtnInfotext: String, download: { type: [Boolean, String, Function, Array, Object], - default: false + default: false, }, newBtnShow: Boolean, newBtnClass: [String, Array, Object], @@ -89,11 +91,23 @@ export const CoreFilterCmpt = { countOnly: Boolean, useSelectionSpan: { type: Boolean, - default: true + default: true, + }, + isUsingPresets: { + Boolean, + default: false, + }, + presetsId: { + String, + default: "", + }, + generalPresets: { + type: Array, + default: [], }, }, inject: ["language"], - data: function() { + data: function () { return { uuid: 0, // FilterCmpt properties @@ -129,41 +143,48 @@ export const CoreFilterCmpt = { page: false, }, collapsedHeadingLocalizationTimer: null, + hasPresetBeenModified: false, + appliedPreset: null, }; }, computed: { notSelectedFields() { - if (!this.fields || !this.selectedFields) - return null; - return this.fields.filter(x => this.selectedFields.indexOf(x) === -1) + if (!this.fields || !this.selectedFields) return null; + return this.fields.filter( + (x) => this.selectedFields.indexOf(x) === -1, + ); }, filteredData() { - if (!this.dataset) - return []; + if (!this.dataset) return []; return JSON.parse(JSON.stringify(this.dataset)); }, filteredColumns() { let fields = JSON.parse(JSON.stringify(this.fields)) || []; - let selectedFields = JSON.parse(JSON.stringify(this.selectedFields)) || []; + let selectedFields = + JSON.parse(JSON.stringify(this.selectedFields)) || []; let columns = null; // If the tabulator options has been provided and it contains the property columns - if (this.tabulatorOptions && this.tabulatorOptions.hasOwnProperty('columns')) + if ( + this.tabulatorOptions && + this.tabulatorOptions.hasOwnProperty("columns") + ) columns = this.tabulatorOptions.columns; // If columns is not an array or it is an array with less elements then the array fields - if (!Array.isArray(columns) || (Array.isArray(columns) && columns.length < fields.length)) - { + if ( + !Array.isArray(columns) || + (Array.isArray(columns) && columns.length < fields.length) + ) { columns = []; // set it as an empty array // Loop throught all the retrieved columns from database - for (let field of fields) - { + for (let field of fields) { // Create a new column having the title equal to the field name let column = { title: field, - field: field + field: field, }; // If the column has to be displayed or not @@ -172,18 +193,18 @@ export const CoreFilterCmpt = { // Add the new column to the list of columns columns.push(column); } - } - else // the property columns has been provided in the tabulator options + } else // the property columns has been provided in the tabulator options { // Loop throught the property columns of the tabulator options - for (let col of columns) - { + for (let col of columns) { // If the column has to be displayed or not /* fields.indexOf(col.field) == -1; ensures displaying formatter colums e.g. column with rowSelection checkboxes or with custom formatted action buttons */ - col.visible = selectedFields.indexOf(col.field) >= 0 || fields.indexOf(col.field) == -1; + col.visible = + selectedFields.indexOf(col.field) >= 0 || + fields.indexOf(col.field) == -1; - if (col.hasOwnProperty('resizable')) + if (col.hasOwnProperty("resizable")) col.resizable = col.visible; } } @@ -212,9 +233,8 @@ export const CoreFilterCmpt = { .map((col) => col.getField()); }, idExtra() { - if (!this.uuid) - return ''; - return '-' + this.uuid; + if (!this.uuid) return ""; + return "-" + this.uuid; }, columnsForFilter() { if (!this.filteredColumns || !this.datasetMetadata) return []; @@ -252,66 +272,90 @@ export const CoreFilterCmpt = { }, methods: { reloadTable() { - if (this.tableOnly) - this.tabulator.setData(); - else - this.getFilter(); + if (this.tableOnly) this.tabulator.setData(); + else this.getFilter(); }, setSelectedFields() { const cols = this.tabulator.getColumns(); - this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); + this.selectedFields = cols + .filter((col) => col.isVisible()) + .map((col) => col.getField()); }, async initTabulator() { - let placeholder = '< Phrasen Plugin not loaded! >'; + let placeholder = "< Phrasen Plugin not loaded! >"; if (this.$p) { - await this.$p.loadCategory('ui'); - placeholder = this.$p.t('ui/keineDatenVorhanden'); + await this.$p.loadCategory("ui"); + placeholder = this.$p.t("ui/keineDatenVorhanden"); } if (!this.tableOnly) { // prefetch data to get fields & selectedFields for filteredColumns & filteredData - await new Promise(resolve => { - const filterId = window.location.hash ? window.location.hash.slice(1) : null; + await new Promise((resolve) => { + const filterId = window.location.hash + ? window.location.hash.slice(1) + : null; - const resolvePromiseFunc = data => { + const resolvePromiseFunc = (data) => { this.setRenderData(data); resolve(); }; // get the filter data if (filterId === null) this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.getFilter(wsParams)), + (wsParams) => + this.$api.call(ApiFilter.getFilter(wsParams)), null, - resolvePromiseFunc + resolvePromiseFunc, ); else this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.getFilterById(wsParams)), + (wsParams) => + this.$api.call( + ApiFilter.getFilterById(wsParams), + ), { filterId }, - resolvePromiseFunc + resolvePromiseFunc, ); }); } // Define a default tabulator options in case it was not provided - let tabulatorOptions = {...{ + let tabulatorOptions = { + ...{ layout: "fitDataStretchFrozen", movableColumns: true, - columnDefaults:{ - tooltip: true + columnDefaults: { + tooltip: true, }, placeholder, persistence: this.persistence, - }, ...(this.tabulatorOptions || {})}; + }, + ...(this.tabulatorOptions || {}), + }; + + if ( + this.$props.isUsingPresets && + "tabulatorPreset-" + this.$props.presetsId in + window.localStorage + ) { + tabulatorOptions.persistence.columns = false; + tabulatorOptions.persistence.headerFilter = false; + tabulatorOptions.persistence.sort = false; + } if (tabulatorOptions.locale) { - tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions); + tabulatorOptions = + await this.configureTabulatorLocalizations( + tabulatorOptions, + ); } // set default height if no height property is set - if (tabulatorOptions.height === undefined && + if ( + tabulatorOptions.height === undefined && tabulatorOptions.minHeight === undefined && - tabulatorOptions.maxHeight === undefined) { + tabulatorOptions.maxHeight === undefined + ) { tabulatorOptions.height = 500; } @@ -319,13 +363,19 @@ export const CoreFilterCmpt = { tabulatorOptions.data = this.filteredData; tabulatorOptions.columns = this.filteredColumns; } else { - tabulatorOptions.columns.forEach(col => { - if (col.visible === undefined) - col.visible = true; + tabulatorOptions.columns.forEach((col) => { + if (col.visible === undefined) col.visible = true; }); } - if (tabulatorOptions.selectable || tabulatorOptions.selectableRows || (tabulatorOptions.columns && tabulatorOptions.columns.filter(el => el.formatter == 'rowSelection').length)) + if ( + tabulatorOptions.selectable || + tabulatorOptions.selectableRows || + (tabulatorOptions.columns && + tabulatorOptions.columns.filter( + (el) => el.formatter == "rowSelection", + ).length) + ) this.tabulatorHasSelector = true; if (this.idField) { @@ -341,60 +391,70 @@ export const CoreFilterCmpt = { debugInvalidOptions: false, }); // If event handlers have been provided - if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) - { + if ( + Array.isArray(this.tabulatorEvents) && + this.tabulatorEvents.length > 0 + ) { // Attach all the provided event handlers to the started tabulator for (let evt of this.tabulatorEvents) this.tabulator.on(evt.event, evt.handler); } - this.tabulator.on('tableBuilt', () => { + this.tabulator.on("tableBuilt", () => { this.tableBuilt = true; - this.$emit('tableBuilt'); + this.$emit("tableBuilt"); }); - this.tabulator.on("rowSelectionChanged", data => { + this.tabulator.on("rowSelectionChanged", (data) => { this.selectedData = data; }); + this.tabulator.on("columnMoved", (column, columns) => { + this.checkAgainstAppliedPreset(); + }); + this.tabulator.on("dataSorted", (sorters, rows) => { + this.checkAgainstAppliedPreset(); + }); // if nested tabulator, restructure data if (this.parentIdField && this.idField) { - this.tabulator.on("dataLoading", data => { + this.tabulator.on("dataLoading", (data) => { let toDelete = []; // loop through all data - for (let childIdx = 0; childIdx < data.length; childIdx++) - { + for (let childIdx = 0; childIdx < data.length; childIdx++) { let child = data[childIdx]; // if it has parent id, it is a child - if (child[this.parentIdField]) - { + if (child[this.parentIdField]) { // append the child on the right place. If parent found, mark original sw child on 0 level for deleting - if (this.appendChild(data, child)) toDelete.push(childIdx); + if (this.appendChild(data, child)) + toDelete.push(childIdx); } } // delete the marked children from 0 level - for (let counter = 0; counter < toDelete.length; counter++) - { + for ( + let counter = 0; + counter < toDelete.length; + counter++ + ) { // decrease index by counter as index of data array changes after every deletion data.splice(toDelete[counter] - counter, 1); } }); } if (this.tableOnly) { - this.tabulator.on('tableBuilt', () => { + this.tabulator.on("tableBuilt", () => { const cols = this.tabulator.getColumns(); - this.fields = cols.map(col => col.getField()); + this.fields = cols.map((col) => col.getField()); this.setSelectedFields(); if (this.tabulator.options.persistence.headerFilter) this._setHeaderFilter(); }); - } - this.tabulator.on("dataFiltered", filters => { + this.tabulator.on("dataFiltered", (filters) => { this.filterActive = filters.length > 0; this.$emit("headerFilterOn", this.filterActive); + this.checkAgainstAppliedPreset(); }); this.tabulator.on("localized", () => { this.tabulator.modules.responsiveLayout.generateCollapsedContent(); @@ -407,46 +467,75 @@ export const CoreFilterCmpt = { const genericTabulatorPhrasesResponse = await this.$api.call( ApiPhrases.getTabulatorPhrases(), ); - const genericTabulatorPhrasesData = genericTabulatorPhrasesResponse.data; + const genericTabulatorPhrasesData = + genericTabulatorPhrasesResponse.data; Object.keys(genericTabulatorPhrasesData).forEach((language) => { let genericTabulatorPhraseToTranslationMapper = {}; genericTabulatorPhrasesData[language].forEach((phrase) => { - genericTabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text; + genericTabulatorPhraseToTranslationMapper[phrase.phrase] = + phrase.text; }); tabulatorOptions.langs[language] = { data: { - loading: genericTabulatorPhraseToTranslationMapper["loading"], - error: genericTabulatorPhraseToTranslationMapper["error"], + loading: + genericTabulatorPhraseToTranslationMapper[ + "loading" + ], + error: genericTabulatorPhraseToTranslationMapper[ + "error" + ], }, groups: { item: genericTabulatorPhraseToTranslationMapper["item"], - items: genericTabulatorPhraseToTranslationMapper["items"], + items: genericTabulatorPhraseToTranslationMapper[ + "items" + ], }, pagination: { page_size: - genericTabulatorPhraseToTranslationMapper["page_size"], + genericTabulatorPhraseToTranslationMapper[ + "page_size" + ], page_title: - genericTabulatorPhraseToTranslationMapper["page_title"], - first: genericTabulatorPhraseToTranslationMapper["first"], + genericTabulatorPhraseToTranslationMapper[ + "page_title" + ], + first: genericTabulatorPhraseToTranslationMapper[ + "first" + ], first_title: - genericTabulatorPhraseToTranslationMapper["first_title"], + genericTabulatorPhraseToTranslationMapper[ + "first_title" + ], last: genericTabulatorPhraseToTranslationMapper["last"], last_title: - genericTabulatorPhraseToTranslationMapper["last_title"], + genericTabulatorPhraseToTranslationMapper[ + "last_title" + ], prev: genericTabulatorPhraseToTranslationMapper["prev"], prev_title: - genericTabulatorPhraseToTranslationMapper["prev_title"], + genericTabulatorPhraseToTranslationMapper[ + "prev_title" + ], next: genericTabulatorPhraseToTranslationMapper["next"], next_title: - genericTabulatorPhraseToTranslationMapper["next_title"], + genericTabulatorPhraseToTranslationMapper[ + "next_title" + ], all: genericTabulatorPhraseToTranslationMapper["all"], counter: { showing: - genericTabulatorPhraseToTranslationMapper["showing"], + genericTabulatorPhraseToTranslationMapper[ + "showing" + ], of: genericTabulatorPhraseToTranslationMapper["of"], - rows: genericTabulatorPhraseToTranslationMapper["rows"], - pages: genericTabulatorPhraseToTranslationMapper["pages"], + rows: genericTabulatorPhraseToTranslationMapper[ + "rows" + ], + pages: genericTabulatorPhraseToTranslationMapper[ + "pages" + ], }, }, }; @@ -514,49 +603,51 @@ export const CoreFilterCmpt = { }, updateTabulator() { if (this.tabulator) { - if (this.tableBuilt) - this._updateTabulator(); - else - this.tabulator.on('tableBuilt', this._updateTabulator); + if (this.tableBuilt) this._updateTabulator(); + else this.tabulator.on("tableBuilt", this._updateTabulator); } }, _updateTabulator() { - this.tabulatorHasSelector = this.tabulatorOptions.selectable || this.tabulatorOptions.selectableRows || this.filteredColumns.filter(el => el.formatter == 'rowSelection').length; + this.tabulatorHasSelector = + this.tabulatorOptions.selectable || + this.tabulatorOptions.selectableRows || + this.filteredColumns.filter( + (el) => el.formatter == "rowSelection", + ).length; this.tabulator.setColumns(this.filteredColumns); this.tabulator.setData(this.filteredData); - this._setHeaderFilter() + this._setHeaderFilter(); }, - clearFilters() - { + clearFilters() { let existingFilters = this.tabulator.getHeaderFilters(); - existingFilters.forEach(filter => { + existingFilters.forEach((filter) => { this.tabulator.setHeaderFilterValue(filter.field, ""); }); this.tabulator.clearFilter(); this.filterActive = false; - this.$emit('headerFilterOn', this.filterActive) + this.$emit("headerFilterOn", this.filterActive); }, - _setHeaderFilter() - { + _setHeaderFilter() { const existingFilters = this.tabulator.getHeaderFilters(); - existingFilters.forEach(filter => { + existingFilters.forEach((filter) => { this.tabulator.setHeaderFilterValue(filter.field, filter.value); }); }, getFilter() { if (this.selectedFilter === null) this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.getFilter(wsParams)), + (wsParams) => this.$api.call(ApiFilter.getFilter(wsParams)), null, - this.render + this.render, ); else this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.getFilterById(wsParams)), + (wsParams) => + this.$api.call(ApiFilter.getFilterById(wsParams)), { - filterId: this.selectedFilter + filterId: this.selectedFilter, }, - this.render + this.render, ); }, setRenderData(data) { @@ -569,12 +660,9 @@ export const CoreFilterCmpt = { this.selectedFields = data.selectedFields; this.filterFields = []; - for (let i = 0; i < data.datasetMetadata.length; i++) - { - for (let j = 0; j < data.filters.length; j++) - { - if (data.datasetMetadata[i].name == data.filters[j].name) - { + for (let i = 0; i < data.datasetMetadata.length; i++) { + for (let j = 0; j < data.filters.length; j++) { + if (data.datasetMetadata[i].name == data.filters[j].name) { let filter = data.filters[j]; filter.type = data.datasetMetadata[i].type; @@ -593,11 +681,9 @@ export const CoreFilterCmpt = { this.setRenderData(data); // If the side menu is active - if (this.sideMenu === true) - { + if (this.sideMenu === true) { this.setSideMenu(data); - } - else // otherwise use the dropdown in the filter options + } else // otherwise use the dropdown in the filter options { this.setDropDownMenu(data); } @@ -611,51 +697,57 @@ export const CoreFilterCmpt = { let personalFilters = data.sideMenu.personalFilters; let filtersArray = []; - for (let filtersCount = 0; filtersCount < filters.length; filtersCount++) - { + for ( + let filtersCount = 0; + filtersCount < filters.length; + filtersCount++ + ) { let link = filters[filtersCount].link; - if (link == null) link = '#'; + if (link == null) link = "#"; filtersArray[filtersArray.length] = { id: filters[filtersCount].filter_id, link: link + filters[filtersCount].filter_id, description: filters[filtersCount].desc, sort: filtersCount, - onClickCall: this.handlerGetFilterById + onClickCall: this.handlerGetFilterById, }; } - for (let filtersCount = 0; filtersCount < personalFilters.length; filtersCount++) - { + for ( + let filtersCount = 0; + filtersCount < personalFilters.length; + filtersCount++ + ) { let link = personalFilters[filtersCount].link; - if (link == null) link = '#'; + if (link == null) link = "#"; filtersArray[filtersArray.length] = { id: personalFilters[filtersCount].filter_id, link: link + personalFilters[filtersCount].filter_id, description: personalFilters[filtersCount].desc, - subscriptDescription: personalFilters[filtersCount].subscriptDescription, - subscriptLinkClass: personalFilters[filtersCount].subscriptLinkClass, - subscriptLinkValue: personalFilters[filtersCount].subscriptLinkValue, + subscriptDescription: + personalFilters[filtersCount].subscriptDescription, + subscriptLinkClass: + personalFilters[filtersCount].subscriptLinkClass, + subscriptLinkValue: + personalFilters[filtersCount].subscriptLinkValue, sort: filtersCount, onClickCall: this.handlerGetFilterById, - onClickSubscriptCall: this.handlerRemoveCustomFilter + onClickSubscriptCall: this.handlerRemoveCustomFilter, }; } this.availableFilters = filtersArray; - this.$emit( - 'nwNewEntry', - { - link: "#", - description: "Filters", - icon: "filter", - children: filtersArray - } - ); + this.$emit("nwNewEntry", { + link: "#", + description: "Filters", + icon: "filter", + children: filtersArray, + }); }, /** * Set the drop down menu @@ -665,29 +757,35 @@ export const CoreFilterCmpt = { let personalFilters = data.sideMenu.personalFilters; let filtersArray = []; - for (let filtersCount = 0; filtersCount < filters.length; filtersCount++) - { + for ( + let filtersCount = 0; + filtersCount < filters.length; + filtersCount++ + ) { let link = filters[filtersCount].link; - if (link == null) link = '#'; + if (link == null) link = "#"; filtersArray[filtersArray.length] = { id: filters[filtersCount].filter_id, option: filters[filtersCount].filter_id, - description: filters[filtersCount].desc + description: filters[filtersCount].desc, }; } - for (let filtersCount = 0; filtersCount < personalFilters.length; filtersCount++) - { + for ( + let filtersCount = 0; + filtersCount < personalFilters.length; + filtersCount++ + ) { let link = personalFilters[filtersCount].link; - if (link == null) link = '#'; + if (link == null) link = "#"; filtersArray[filtersArray.length] = { id: personalFilters[filtersCount].filter_id, option: personalFilters[filtersCount].filter_id, - description: personalFilters[filtersCount].desc + description: personalFilters[filtersCount].desc, }; } @@ -696,7 +794,11 @@ export const CoreFilterCmpt = { /** * Used to start/refresh the FetchCmpt */ - startFetchCmpt(apiFunction, apiFunctionParameters, dataFetchedCallback) { + startFetchCmpt( + apiFunction, + apiFunctionParameters, + dataFetchedCallback, + ) { // Assign the function api of the FetchCmpt binded property this.fetchCmptApiFunction = apiFunction; @@ -704,11 +806,14 @@ export const CoreFilterCmpt = { if (apiFunctionParameters == null) apiFunctionParameters = {}; // Always needed parameters - apiFunctionParameters.filterUniqueId = FHC_JS_DATA_STORAGE_OBJECT.called_path + "/" + FHC_JS_DATA_STORAGE_OBJECT.called_method; + apiFunctionParameters.filterUniqueId = + FHC_JS_DATA_STORAGE_OBJECT.called_path + + "/" + + FHC_JS_DATA_STORAGE_OBJECT.called_method; apiFunctionParameters.filterType = this.filterType; if (this.uniqueId) - apiFunctionParameters.filterUniqueId += '_' + this.uniqueId; + apiFunctionParameters.filterUniqueId += "_" + this.uniqueId; // Assign parameters to the FetchCmpt binded properties this.fetchCmptApiFunctionParams = apiFunctionParameters; @@ -716,7 +821,9 @@ export const CoreFilterCmpt = { this.fetchCmptDataFetched = dataFetchedCallback; // Set the FetchCmpt binded property refresh to have the component to refresh // NOTE: this should be the last one to be called because it triggers the FetchCmpt to start to refresh - this.fetchCmptRefresh === true ? this.fetchCmptRefresh = false : this.fetchCmptRefresh = true; + this.fetchCmptRefresh === true + ? (this.fetchCmptRefresh = false) + : (this.fetchCmptRefresh = true); }, // ------------------------------------------------------------------------------------------------------------------ @@ -729,47 +836,46 @@ export const CoreFilterCmpt = { this.selectedFilter = null; // this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.saveCustomFilter(wsParams)), + (wsParams) => + this.$api.call(ApiFilter.saveCustomFilter(wsParams)), { - customFilterName + customFilterName, }, - this.getFilter + this.getFilter, ); }, /** * */ handlerRemoveCustomFilter(event) { - let filterId = event.currentTarget.getAttribute("href").substring(1); - if (filterId === this.selectedFilter) - this.selectedFilter = null; + let filterId = event.currentTarget + .getAttribute("href") + .substring(1); + if (filterId === this.selectedFilter) this.selectedFilter = null; // this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.removeCustomFilter(wsParams)), + (wsParams) => + this.$api.call(ApiFilter.removeCustomFilter(wsParams)), { - filterId: filterId + filterId: filterId, }, - this.getFilter + this.getFilter, ); }, /* * */ - handlerGetFilterById: function(event) { - + handlerGetFilterById: function (event) { let filterId = null; // Get the attribute href if side menu is rendered let attr = event.currentTarget.getAttribute("href"); // Otherwise get the value of the drop down menu - if (attr == null) - { + if (attr == null) { filterId = event.currentTarget.value; - } - else - { + } else { filterId = attr.substring(1); } @@ -782,11 +888,12 @@ export const CoreFilterCmpt = { applyFilterConfig(filterFields) { this.selectedFilter = null; this.startFetchCmpt( - wsParams => this.$api.call(ApiFilter.applyFilterFields(wsParams)), + (wsParams) => + this.$api.call(ApiFilter.applyFilterFields(wsParams)), { - filterFields + filterFields, }, - this.getFilter + this.getFilter, ); }, @@ -796,24 +903,24 @@ export const CoreFilterCmpt = { let parentId = child[this.parentIdField]; // loop thorugh all data - for (let parentIdx = 0; parentIdx < data.length; parentIdx++) - { + for (let parentIdx = 0; parentIdx < data.length; parentIdx++) { let parent = data[parentIdx]; // if it's the parent - if (parent[this.idField] == parentId) - { + if (parent[this.idField] == parentId) { // create children array if not done yet if (!parent._children) parent._children = []; // if child is not included in children array, append the child - if (!parent._children.includes(child)) parent._children.push(child); + if (!parent._children.includes(child)) + parent._children.push(child); // parent found return true; } // search children for parents - else if (parent._children) this.appendChild(parent._children, child); + else if (parent._children) + this.appendChild(parent._children, child); } // parent not found @@ -830,16 +937,122 @@ export const CoreFilterCmpt = { return res; }, {}); }, + afterTablePresetApplied(preset) { + this.selectedFields = [...preset.displayedColumns]; + this.appliedPreset = { ...preset }; + this.hasPresetBeenModified = false; + }, + updateTablePresetsSubCollapsibles() { + if ( + !this.$refs.tablePresets.$el + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.$refs.tablePresets?.hidePresetInfo(); + this.$refs.tablePresets?.hideNewPresetForm(); + } + }, + hideColumn(column) { + this.tabulator.hideColumn(column); + this.checkAgainstAppliedPreset(); + }, + showColumn(column) { + this.tabulator.showColumn(column); + this.checkAgainstAppliedPreset(); + }, + checkAgainstAppliedPreset() { + if (!this.appliedPreset) return; + + let hasPresetBeenModified = false; + + const displayedColumns = this.tabulator.columnManager.columns + .filter((column) => column.field !== "collapse") + .filter((column) => column.visible) + .map((column) => column.field); + + const displayedColumnsLength = displayedColumns.length; + const presetDisplayedColumnsLength = + this.appliedPreset.displayedColumns?.length ?? 0; + + if ( + displayedColumns.length !== + (this.appliedPreset.displayedColumns?.length ?? 0) + ) { + hasPresetBeenModified = true; + } + + displayedColumns.forEach((displayedColumn, index) => { + if ( + this.appliedPreset.displayedColumns[index] !== + displayedColumn + ) { + hasPresetBeenModified = true; + } + }); + + const unparsedHeaderFilters = + this.tabulator.modules.filter.headerFilters; + const headerFilterFieldValuePairs = Object.entries( + unparsedHeaderFilters, + ).map(([columnField, filter]) => [columnField, filter.value]); + const headerFiltersFieldValuePairsExcludingHiddenColumns = + headerFilterFieldValuePairs.filter( + ([columnField, filterValue]) => { + return displayedColumns.includes(columnField); + }, + ); + const headerFilters = Object.fromEntries( + headerFiltersFieldValuePairsExcludingHiddenColumns, + ); + const headerFilterKeys = Object.keys(headerFilters); + const appliedHeaderFilterKeys = this.appliedPreset.headerFilters + ? Object.keys(this.appliedPreset.headerFilters) + : []; + + if (headerFilterKeys.length !== appliedHeaderFilterKeys.length) { + hasPresetBeenModified = true; + } + + headerFilterKeys.forEach((key) => { + if ( + !(key in this.appliedPreset.headerFilters) || + this.appliedPreset.headerFilters[key] !== headerFilters[key] + ) { + hasPresetBeenModified = true; + } + }); + + const activeSort = this.tabulator.getSorters()[0]; + const sort = activeSort + ? { + column: activeSort.field, + direction: activeSort.dir, + } + : null; + if ( + sort?.column !== this.appliedPreset.sort?.column || + sort?.direction !== this.appliedPreset.sort?.direction + ) { + hasPresetBeenModified = true; + } + + this.hasPresetBeenModified = hasPresetBeenModified; + }, }, beforeCreate() { if (!this.tableOnly == !this.filterType) - alert('You can not have a filter-type in table-only mode!'); + alert("You can not have a filter-type in table-only mode!"); }, created() { - if (this.sideMenu && (!this.$props.onNwNewEntry || !(this.$props.onNwNewEntry instanceof Function))) + if ( + this.sideMenu && + (!this.$props.onNwNewEntry || + !(this.$props.onNwNewEntry instanceof Function)) + ) alert('"nwNewEntry" listener is mandatory when sideMenu is true'); this.uuid = _uuid++; - this.$emit('uuidDefined', this.uuid) + this.$emit("uuidDefined", this.uuid); Tabulator.registerModule(InternalToExternalEventBroadcastModule); Tabulator.registerModule(MenuExtensionModule); @@ -848,13 +1061,14 @@ export const CoreFilterCmpt = { mounted() { this.initTabulator().then(() => { if (!this.tableOnly) { - this.selectedFilter = window.location.hash ? window.location.hash.slice(1) : null; + this.selectedFilter = window.location.hash + ? window.location.hash.slice(1) + : null; this.render(this.fetchResult); } }); - }, - template: ` + template: /*html*/ ` + + + + + {{ appliedPreset.name }} + {{ " [" + $p.t("global/modified") + "]" }} + + + @@ -944,5 +1183,5 @@ export const CoreFilterCmpt = {
- ` + `, }; diff --git a/public/js/components/filter/Filter/TablePresets.js b/public/js/components/filter/Filter/TablePresets.js new file mode 100644 index 000000000..861dc03c6 --- /dev/null +++ b/public/js/components/filter/Filter/TablePresets.js @@ -0,0 +1,565 @@ +/** + * Copyright (C) 2022 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import ApiTabulatorPresets from "../../../api/factory/tabulatorPresets.js"; + +/** + * + */ +export default { + name: "TablePresets", + props: { + presetsId: { type: String }, + tabulator: { type: Object }, + generalPresets: { type: Array, default: [] }, + }, + emits: ["tablePresetApplied"], + data: function () { + return { + customUserPresets: [], + presetInfo: null, + newPreset: null, + newPresetName: "", + }; + }, + computed: { + allPresets() { + return this.$props.generalPresets.concat(this.customUserPresets); + }, + presetInfoFormattedStrings() { + if (!this.presetInfo) return null; + + const columnTitles = this.getColumnTitles(); + + let displayedColumns = + this.presetInfo.displayedColumns?.map( + (columnField) => columnTitles[columnField] ?? "", + ) ?? []; + displayedColumns = displayedColumns.filter( + (string) => string.length, + ); + const displayedColumnsString = displayedColumns.length + ? this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + + displayedColumns.join(", ") + : ""; + + let headerFilters = this.presetInfo.headerFilters + ? Object.keys(this.presetInfo.headerFilters).map((column) => + columnTitles[column] + ? columnTitles[column] + + '["' + + this.presetInfo.headerFilters[column] + + '"]' + : "", + ) + : []; + headerFilters = headerFilters.filter((string) => string.length); + const headerFiltersString = headerFilters.length + ? this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", ") + : ""; + + const sortColumn = this.presetInfo.sort?.column + ? columnTitles[this.presetInfo.sort.column] + : null; + const sortDirection = + this.presetInfo.sort?.direction === "desc" + ? this.$p.t("tabulator_presets/desc") + : this.$p.t("tabulator_presets/asc"); + const sortString = sortColumn + ? this.$p.t("tabulator_presets/sort") + + ": " + + sortColumn + + " (" + + sortDirection + + ")" + : ""; + + return { + displayedColumns: displayedColumnsString, + headerFilters: headerFiltersString, + sort: sortString, + }; + }, + newPresetFormattedStrings() { + if (!this.newPreset) return null; + + const columnTitles = this.getColumnTitles(); + + const displayedColumns = this.newPreset.displayedColumns.map( + (columnField) => columnTitles[columnField], + ); + const displayedColumnsString = displayedColumns.length + ? this.$p.t("tabulator_presets/displayed_columns") + + " (" + + this.$p.t("tabulator_presets/in_order") + + "): " + + displayedColumns.join(", ") + : ""; + + const headerFilters = Object.keys(this.newPreset.headerFilters).map( + (column) => + columnTitles[column] + + '["' + + this.newPreset.headerFilters[column] + + '"]', + ); + const headerFiltersString = headerFilters.length + ? this.$p.t("tabulator_presets/column_header_filters") + + ": " + + headerFilters.join(", ") + : ""; + + const sortColumn = this.newPreset.sort?.column + ? columnTitles[this.newPreset.sort.column] + : null; + const sortDirection = + this.newPreset.sort?.direction === "desc" + ? this.$p.t("tabulator_presets/desc") + : this.$p.t("tabulator_presets/asc"); + const sortString = sortColumn + ? this.$p.t("tabulator_presets/sort") + + ": " + + sortColumn + + " (" + + sortDirection + + ")" + : ""; + + return { + displayedColumns: displayedColumnsString, + headerFilters: headerFiltersString, + sort: sortString, + }; + }, + }, + watch: { + allPresets: { + handler() { + this.hidePresetInfo(); + this.hideNewPresetForm(); + }, + deep: true, + }, + }, + methods: { + async showPresetInfo(preset) { + let shouldUpdatePresetInfo; + if (!this.presetInfo) { + shouldUpdatePresetInfo = true; + } else if (preset.id) { + shouldUpdatePresetInfo = preset.id !== this.presetInfo.id; + } else { + shouldUpdatePresetInfo = preset.name !== this.presetInfo.name; + } + + if (shouldUpdatePresetInfo) { + this.presetInfo = {...preset}; + } + + this.hideNewPresetForm(); + + await this.$nextTick(); + + if ( + !this.$refs.presetInfoCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.togglePresetInfoCollapsible(); + } + }, + async hidePresetInfo() { + if ( + this.$refs.presetInfoCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.togglePresetInfoCollapsible(); + } + await this.$nextTick(); + this.presetInfo = null; + }, + togglePresetInfoCollapsible() { + new bootstrap.Collapse( + "#presetInfoCollapsible_" + this.$props.presetsId, + { toggle: true }, + ); + }, + async showNewPresetForm() { + this.generateNewPreset(); + + this.hidePresetInfo(); + + await this.$nextTick(); + + if ( + !this.$refs.newPresetFormCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.toggleNewPresetFormCollapsible(); + } + }, + generateNewPreset() { + const columns = this.tabulator.columnManager.columns.filter( + (column) => column.field !== "collapse", + ); + + const displayedColumns = columns + .filter((column) => column.visible) + .map((column) => column.field); + + const unparsedHeaderFilters = + this.tabulator.modules.filter.headerFilters; + const headerFilterFieldValuePairs = Object.entries( + unparsedHeaderFilters, + ).map(([columnField, filter]) => [columnField, filter.value]); + const headerFiltersFieldValuePairsExcludingHiddenColumns = + headerFilterFieldValuePairs.filter( + ([columnField, filterValue]) => { + return displayedColumns.includes(columnField); + }, + ); + const headerFilters = Object.fromEntries( + headerFiltersFieldValuePairsExcludingHiddenColumns, + ); + + const activeSort = this.tabulator.getSorters()[0]; + const sort = activeSort + ? { + column: activeSort.field, + direction: activeSort.dir, + } + : null; + + this.newPreset = { + displayedColumns, + headerFilters, + sort, + }; + }, + hideNewPresetForm() { + if ( + this.$refs.newPresetFormCollapsible + .getAttribute("class") + .split(" ") + .includes("show") + ) { + this.toggleNewPresetFormCollapsible(); + } + }, + toggleNewPresetFormCollapsible() { + this.newPresetName = ""; + new bootstrap.Collapse( + "#newPresetFormCollapsible_" + this.$props.presetsId, + { toggle: true }, + ); + }, + async createPreset() { + if (!this.newPresetName?.length) return; + + const presetCreationResponse = await this.$api.call( + ApiTabulatorPresets.createTabulatorPreset( + this.$props.presetsId, + this.newPresetName, + this.newPreset, + ), + ); + + if (presetCreationResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + this.hideNewPresetForm(); + } + }, + async deletePreset(preset) { + if (!preset.id) return; + + if ( + window.confirm( + this.$p + .t("tabulator_presets/preset_deletion_confirmation") + .replace("(((presetName)))", preset.name), + ) + ) { + const presetDeletionResponse = await this.$api.call( + ApiTabulatorPresets.deleteTabulatorPreset(preset.id), + ); + + if (presetDeletionResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + } + } + }, + syncPresetWithConfig(preset) { + const validColumns = this.$props.tabulator.getColumnDefinitions(); + let isUpdateNecessary = false; + + if (preset.displayedColumns) { + const initialNumberOfDisplayedColumns = + preset.displayedColumns.length; + preset.displayedColumns = preset.displayedColumns.filter( + (columnField) => { + return validColumns.some( + (column) => column.field === columnField, + ); + }, + ); + const updatedNumberOfDisplayedColumns = + preset.displayedColumns.length; + + isUpdateNecessary = + updatedNumberOfDisplayedColumns < + initialNumberOfDisplayedColumns; + } + + if (preset.headerFilters) { + const initialNumberOfHeaderFilters = Object.keys( + preset.headerFilters, + ).length; + Object.keys(preset.headerFilters).forEach((columnField) => { + if ( + !validColumns.some( + (column) => + column.field === columnField && + column.headerFilter, + ) || + !preset.displayedColumns.includes(columnField) + ) { + delete preset.headerFilters[columnField]; + } + }); + const updatedNumberOfHeaderFilters = Object.keys( + preset.headerFilters, + ).length; + + isUpdateNecessary = + isUpdateNecessary || + updatedNumberOfHeaderFilters < initialNumberOfHeaderFilters; + } + + if (preset.sort) { + if ( + !validColumns.some( + (column) => + column.field === preset.sort.column && + column.headerSort, + ) + ) { + preset.sort = null; + isUpdateNecessary = true; + } + } + + if (isUpdateNecessary) { + this.updatePreset(preset); + } + + return preset; + }, + async updatePreset(preset) { + if (!preset.id) return; + + const tabulatorPresetUpdateResponse = await this.$api.call( + ApiTabulatorPresets.updateTabulatorPreset(preset.id, { + displayedColumns: preset.displayedColumns, + headerFilters: preset.headerFilters, + sort: preset.sort, + }), + ); + + if (tabulatorPresetUpdateResponse.meta.status === "success") { + this.fetchCustomUserTabulatorPresets(); + } + }, + async applyPreset(preset) { + preset = this.syncPresetWithConfig(preset); + window.localStorage["tabulatorPreset-" + this.$props.presetsId] = + preset.id ? "custom:" + preset.id : "general:" + preset.name; + + let columns = this.$props.tabulator + .getColumnDefinitions() + .filter((column) => column.field !== "collapse"); + columns.forEach((column) => + this.$props.tabulator.hideColumn(column.field), + ); + + preset.displayedColumns?.forEach( + (columnField, index, displayedColumns) => { + this.$props.tabulator.showColumn(columnField); + if (index === 0) return; + this.$props.tabulator.moveColumn( + columnField, + displayedColumns[index - 1], + true, + ); + }, + ); + + this.$props.tabulator.clearHeaderFilter(); + if (preset.headerFilters) { + Object.entries(preset.headerFilters).forEach( + ([columnField, filterValue]) => { + this.$props.tabulator.setHeaderFilterValue( + columnField, + filterValue, + ); + }, + ); + } + + this.$props.tabulator.clearSort(); + if (preset.sort?.column) { + this.$props.tabulator.setSort( + preset.sort.column, + preset.sort.direction ?? "asc", + ); + } + + this.$emit("tablePresetApplied", { preset }); + }, + async fetchCustomUserTabulatorPresets() { + let tabulatorPresetsResponse = await this.$api.call( + ApiTabulatorPresets.getTabulatorPresets(this.$props.presetsId), + ); + + if (tabulatorPresetsResponse.meta.status === "success") { + this.customUserPresets = tabulatorPresetsResponse.data.map( + (presetInfo) => { + return { + id: presetInfo.preset_id, + name: presetInfo.preset_name, + ...JSON.parse(presetInfo.preset_json), + }; + }, + ); + } + }, + getColumnTitles() { + const columnLocalizations = + this.$props.tabulator.getLocale() !== "default" + ? this.$props.tabulator.getLang().columns + : {}; + const columnFieldTitlePairs = this.$props.tabulator + .getColumnDefinitions() + .map((column) => [ + column.field, + columnLocalizations[column.field] ?? column.title, + ]); + return Object.fromEntries(columnFieldTitlePairs); + }, + applyStoredPreset() { + let storedPreset = + window.localStorage["tabulatorPreset-" + this.$props.presetsId]; + if (!storedPreset) return; + + let preset = null; + + if (storedPreset.search("custom:") === 0) { + const customPresetId = parseInt(storedPreset.slice(7)); + preset = this.customUserPresets.find( + (customPreset) => customPreset.id === customPresetId, + ); + } else if (storedPreset.search("general:") === 0) { + const generalPresetName = storedPreset.slice(8); + preset = this.$props.generalPresets.find( + (generalPreset) => generalPreset.name === generalPresetName, + ); + } + + if (!preset) return; + + this.applyPreset(preset); + }, + }, + async created() { + await this.$p.loadCategory(["tabulator_presets", "global"]); + await this.fetchCustomUserTabulatorPresets(); + this.applyStoredPreset(); + }, + template: /*html*/ ` +
+
+
{{ $p.t("tabulator_presets/table_presets") }}
+
+
+
+
{{ preset.name }}
+
+
+ + +
+
+
+
+
+ + {{ $p.t("tabulator_presets/save_current_config") }} +
+
+
+
+
+
+
+
+ {{ presetInfo?.name }} + +
+ {{ presetInfoFormattedStrings.displayedColumns }} + {{ presetInfoFormattedStrings.headerFilters }} + {{ presetInfoFormattedStrings.sort }} +
+
+
+
+
+
+
+
+ {{ $p.t("tabulator_presets/save_current_config") }} + +
+ {{ newPresetFormattedStrings.displayedColumns }} + {{ newPresetFormattedStrings.headerFilters }} + {{ newPresetFormattedStrings.sort }} +
+ +
+ {{ $p.t('global/speichern') }} +
+
+
+
+
+
+
+
+ `, +}; diff --git a/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js index c0e66cff0..038bef527 100644 --- a/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js +++ b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js @@ -9,9 +9,13 @@ export default class ResponsiveLayoutExtensionModule extends ResponsiveLayout { } generateCollapsedRowData(row) { - let result = super.generateCollapsedRowData(row) - const isLocalizationEnabled = this.table.options.locale && this.table.options.locale !== "default"; - const columnHeadingTranslations = this.table?.getLang()?.columns; + let result = super.generateCollapsedRowData(row); + const isLocalizationEnabled = + this.table.options.locale && + this.table.options.locale !== "default"; + const columnHeadingTranslations = this.table?.initialized + ? this.table.getLang()?.columns + : null; if (isLocalizationEnabled && columnHeadingTranslations) { result = result.map((column) => { diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 1bf411143..c57f88984 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -99,6 +99,7 @@ require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php'); require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php'); require_once('dbupdate_3.4/75959_StudVw_Automatische_Tags.php'); require_once('dbupdate_3.4/76160_lvv_favorites.php'); +require_once('dbupdate_3.4/77080_tabulator_presets_table.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

Pruefe Tabellen und Attribute!

'; @@ -396,6 +397,7 @@ $tabellen=array( "public.tbl_studiengangstyp" => array("typ","bezeichnung","beschreibung","bezeichnung_mehrsprachig"), "public.tbl_studienjahr" => array("studienjahr_kurzbz","bezeichnung"), "public.tbl_studiensemester" => array("studiensemester_kurzbz","bezeichnung","start","ende","studienjahr_kurzbz","ext_id","beschreibung","onlinebewerbung"), + "public.tbl_tabulator_presets" => array("preset_id", "benutzer_uid", "table_name", "preset_json"), "public.tbl_tag" => array("tag"), "public.tbl_variable" => array("name","uid","wert"), "public.tbl_variablenname" => array("name","defaultwert"), diff --git a/system/dbupdate_3.4/77080_tabulator_presets_table.php b/system/dbupdate_3.4/77080_tabulator_presets_table.php new file mode 100644 index 000000000..bef240b17 --- /dev/null +++ b/system/dbupdate_3.4/77080_tabulator_presets_table.php @@ -0,0 +1,55 @@ +db_query("SELECT 1 FROM public.tbl_tabulator_presets LIMIT 1")) { + $qry = "CREATE TABLE public.tbl_tabulator_presets ( + preset_id int NOT NULL, + benutzer_uid varchar(33) NOT NULL, + table_name varchar(255) NOT NULL, + preset_name varchar(255) NOT NULL, + preset_json varchar(10000) NOT NULL, + CONSTRAINT tbl_tabulator_presets_preset_id_pk PRIMARY KEY(preset_id), + CONSTRAINT tbl_tabulator_presets_benutzer_uid_fk FOREIGN KEY (benutzer_uid) REFERENCES public.tbl_benutzer(uid) + );"; + + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
public.tbl_tabulator_presets: table created'; + + + $db->db_query('CREATE SEQUENCE IF NOT EXISTS public.seq_tbl_tabulator_presets_preset_id + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1;'); + + $db->db_query("ALTER TABLE public.tbl_tabulator_presets ALTER COLUMN preset_id SET DEFAULT nextval('public.seq_tbl_tabulator_presets_preset_id');"); + + + $qry = 'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.tbl_tabulator_presets TO web;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to web on public.tbl_tabulator_presets'; + + $qry = 'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.tbl_tabulator_presets TO vilesci;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to vilesci on public.tbl_tabulator_presets'; + + $qry = 'GRANT USAGE ON public.seq_tbl_tabulator_presets_preset_id TO web;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to web on public.tbl_tabulator_presets'; + + $qry = 'GRANT USAGE ON public.seq_tbl_tabulator_presets_preset_id TO vilesci;'; + if (!$db->db_query($qry)) + echo 'public.tbl_tabulator_presets: ' . $db->db_last_error() . '
'; + else + echo '
Granted privileges to vilesci on public.tbl_tabulator_presets'; +} diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index a5983fcbc..a8e3f9c7e 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -61721,6 +61721,368 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), //End Finetuning StudVW + // tabulator presets section phrases START + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'displayed_columns', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Angezeigte Spalten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Displayed columns', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'in_order', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'in Reihenfolge', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'in order', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'column_header_filters', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Spalten Header Filter', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Column header filters', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'asc', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'aufsteigend', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'ascending', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'desc', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'absteigend', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'descending', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'sort', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sortierung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Sort', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_deletion_confirmation', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sind Sie sicher, dass Sie die Voreinstellung (((presetName))) löschen möchten?', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Are you sure you want to delete preset (((presetName)))?', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'table_presets', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tabelle Voreinstellungen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Table presets', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'save_current_config', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Aktuelle Einstellung speichern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Save current configuration', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_name', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Voreinstellung Name', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset name', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'max_presets_count_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Maximale Anzahl an Voreinstellungen erreicht!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Maximum number of presets reached!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_name_duplicate_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Doppelte Voreinstellunge Namen sind nicht zulässig!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset name duplicate not allowed!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_found_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Voreinstellung nicht gefunden!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Preset not found!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_own_update_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sie dürfen diese Voreinstellung nicht ändern!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'You are not allowed to update this preset!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_not_own_delete_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sie dürfen diese Voreinstellung nicht löschen!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'You are not allowed to delete this preset!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_creation_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bei der Erstellung der Voreinstellung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset creation!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_update_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bei der Voreinstellungsänderung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset update!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator_presets', + 'phrase' => 'preset_deletion_generic_err_msg', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Beim Löschen der Voreinstellung ist ein Fehler aufgetreten!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Something went wrong during preset deletion!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // tabulator presets section phrases END );