diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index 7cc652c71..3e13bb61e 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -32,6 +32,8 @@ class Phrasen extends FHCAPI_Controller 'setLanguage' => self::PERM_ANONYMOUS, 'getLanguage' => self::PERM_ANONYMOUS, 'getAllLanguages' => self::PERM_ANONYMOUS, + 'getPhrases' => self::PERM_ANONYMOUS, + 'getTabulatorPhrases' => self::PERM_ANONYMOUS, ]); $this->load->helper('hlp_language'); @@ -92,4 +94,59 @@ class Phrasen extends FHCAPI_Controller $this->terminateWithSuccess($langs); } + public function getPhrases() + { + $postParams = $this->getPostJSON(); + + $languages = $postParams->languages; + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $phrasesGroupedByCategory = $postParams->phrasesGroupedByCategory; + $result = []; + foreach ($languages as $language) { + $phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language); + $result[$language] = $this->getDataOrTerminateWithError($phrases); + } + + $this->terminateWithSuccess($result); + } + + public function getTabulatorPhrases() + { + $languages = json_decode($this->input->get('languages')); + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $result = []; + foreach ($languages as $language) { + $tabulatorPhrases = $this->phraseModel->getPhrasesByCategoryAndLanguage(['tabulator'], $language); + $result[$language] = $this->getDataOrTerminateWithError($tabulatorPhrases); + } + + header('Pragma: private'); + header('Cache-Control: private, max-age=' . (60 * 60 * 24 * 30)); + $this->terminateWithSuccess($result); + } + } \ No newline at end of file diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index a87e4f4be..1f8e772ba 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -41,5 +41,24 @@ export default { method: 'get', url: '/api/frontend/v1/phrasen/getAllLanguages' }; - } + }, + getPhrases(phrasesGroupedByCategory, languages = []) { + return { + method: 'post', + url: '/api/frontend/v1/phrasen/getPhrases', + params: { + phrasesGroupedByCategory, + languages, + }, + }; + }, + getTabulatorPhrases(languages = []) { + return { + method: 'get', + url: '/api/frontend/v1/phrasen/getTabulatorPhrases', + params: { + languages: JSON.stringify(languages), + }, + }; + }, }; \ No newline at end of file diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index bccbf4eb5..9a6d12e3f 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -36,7 +36,6 @@ export default { return { showModal: false, editDataFilter: null, - arePhrasesPreloaded: false, // tabulator options funktionen_table_options: { persistenceID: "filterTableMaProfilFunktionen", @@ -49,6 +48,7 @@ export default { responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, responsiveLayoutCollapseStartOpen: false, + locale: true, columns: [ { title: @@ -63,7 +63,8 @@ export default { responsive: 0, }, { - title: Vue.computed(() => this.$p.t('ui/bezeichnung')), + title: "placeholder", + titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, @@ -71,7 +72,8 @@ export default { responsive: 0, }, { - title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')), + title: "placeholder", + titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, @@ -79,7 +81,8 @@ export default { responsive: 1, }, { - title: Vue.computed(() => this.$p.t('global/gueltigVon')), + title: "placeholder", + titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -91,7 +94,8 @@ export default { responsive: 4, }, { - title: Vue.computed(() => this.$p.t('global/gueltigBis')), + title: "placeholder", + titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -103,7 +107,8 @@ export default { responsive: 3, }, { - title: Vue.computed(() => this.$p.t('profil/wochenstunden')), + title: "placeholder", + titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -124,6 +129,7 @@ export default { responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}], responsiveLayoutCollapseStartOpen: false, + locale: true, columns: [ { title: @@ -138,7 +144,8 @@ export default { responsive: 0, }, { - title: Vue.computed(() => this.$p.t('profil/entlehnteBetriebsmittel')), + title: "placeholder", + titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, @@ -146,7 +153,8 @@ export default { responsive: 0, }, { - title: Vue.computed(() => this.$p.t('profil/inventarnummer')), + title: "placeholder", + titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -155,7 +163,8 @@ export default { responsive: 2, }, { - title: Vue.computed(() => this.$p.t('profil/ausgabedatum')), + title: "placeholder", + titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -230,10 +239,6 @@ export default { } }); }, - setTableColumnTitles() { // reevaluates computed phrasen - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns); - if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns); - }, datetimeFormatterParams: function() { const params = { inputFormat:"yyyy-MM-dd", @@ -316,10 +321,6 @@ export default { }, created() { - // preload phrasen - this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => { - this.arePhrasesPreloaded = true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, @@ -330,9 +331,6 @@ export default { 'data.mittel'(newVal) { if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal); }, - 'language.value'(newVal) { - this.setTableColumnTitles() - } }, template: /*html*/ `
@@ -454,7 +452,6 @@ export default {
this.$p.t("ui/bezeichnung")), + title: "placeholder", + titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true, }, { - title: Vue.computed(() => - this.$p.t("lehre/organisationseinheit"), - ), + title: "placeholder", + titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true, }, { - title: Vue.computed(() => - this.$p.t("global/gueltigVon"), - ), + title: "placeholder", + titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: "dates", headerFilter: dateFilter, @@ -74,9 +73,8 @@ export default { formatterParams: this.datetimeFormatterParams(), }, { - title: Vue.computed(() => - this.$p.t("global/gueltigBis"), - ), + title: "placeholder", + titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: "dates", headerFilter: dateFilter, @@ -87,9 +85,8 @@ export default { formatterParams: this.datetimeFormatterParams(), }, { - title: Vue.computed(() => - this.$p.t("profil/wochenstunden"), - ), + title: "placeholder", + titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -214,11 +211,6 @@ export default { return quickLinks; }, }, - created() { - this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => { - this.arePhrasesPreloaded = true; - }); - }, template: /*html*/ ` @@ -273,7 +265,7 @@ export default {
- +
diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index ac4f367b5..06b981b12 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -29,13 +29,12 @@ export default { QuickLinks, CalendarSync, }, - inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"], + inject: ["sortProfilUpdates", "collapseFunction", "isEditable"], data() { return { showModal: false, collapseIconBetriebsmittel: true, editDataFilter: null, - arePhrasesPreloaded: false, // tabulator options zutrittsgruppen_table_options: { persistenceID: "filterTableStudentProfilZutrittsgruppen", @@ -44,9 +43,9 @@ export default { }, minHeight: 200, layout: "fitColumns", - columns: [ - { - title: Vue.computed(() => this.$p.t('profil/zutrittsGruppen')), + columns: [{ + title: "placeholder", + titlePhrase: "profil/zutrittsGruppen", field: "bezeichnung" } ], @@ -62,6 +61,7 @@ export default { responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, responsiveLayoutCollapseStartOpen: false, + locale: true, columns: [ { title: @@ -75,7 +75,8 @@ export default { responsive: 0, }, { - title: Vue.computed(()=>this.$p.t('profil/entlehnteBetriebsmittel')), + title: "placeholder", + titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, @@ -83,7 +84,8 @@ export default { responsive: 0, }, { - title: Vue.computed(() => this.$p.t('profil/inventarnummer')) , + title: "placeholder", + titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -92,7 +94,8 @@ export default { responsive: 2, }, { - title: Vue.computed(() => this.$p.t('profil/ausgabedatum')) , + title: "placeholder", + titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -259,19 +262,9 @@ export default { }, }, created() { - // preload phrasen - this.$p.loadCategory('profil').then(() => { - this.arePhrasesPreloaded = true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, - watch: { - 'language.value'(newVal) { - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns) - if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns) - } - }, template: /*html*/ `
{ - let def = col.getDefinition(); - return !def.frozen && def.title && def.formatter != "responsiveCollapse"; - }).map(col => col.getField()); - }, - fieldNames() { - if (!this.tableBuilt) - return {}; - return this.tabulator.getColumns().reduce((res, col) => { - res[col.getField()] = col.getDefinition().title; - return res; - }, {}); + if (!this.tableBuilt) return []; + + const localizedColumnTitles = this.tabulator.getLang().columns; + const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale; + return this.tabulator + .getColumns() + .filter((col) => { + let def = col.getDefinition(); + let title = + isTabulatorLocalized && localizedColumnTitles[def.field] + ? localizedColumnTitles[def.field] + : def.title; + return ( + !def.frozen && + title && + def.formatter != "responsiveCollapse" + ); + }) + .map((col) => col.getField()); }, idExtra() { if (!this.uuid) @@ -204,14 +217,30 @@ export const CoreFilterCmpt = { return '-' + this.uuid; }, columnsForFilter() { - if (!this.filteredColumns || !this.datasetMetadata) - return []; - const filterTitles = this.filteredColumns.reduce((a,c) => { - a[c.field] = c.title; + if (!this.filteredColumns || !this.datasetMetadata) return []; + const localizedColumnTitles = this.tabulator.getLang().columns; + const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale; + const filterTitles = this.filteredColumns.reduce((a, c) => { + a[c.field] = + isTabulatorLocalized && localizedColumnTitles[c.field] + ? localizedColumnTitles[c.field] + : c.title; return a; }, {}); - return this.datasetMetadata.map(el => ({...el, ...{title: filterTitles[el.name]}})); - } + return this.datasetMetadata.map((el) => ({ + ...el, + ...{ title: filterTitles[el.name] }, + })); + }, + }, + watch: { + "language.value": { + handler(newSelectedLanguage) { + if (!this.$props.tabulatorOptions.locale) return; + + this.tabulator.setLocale(newSelectedLanguage); + }, + }, }, methods: { reloadTable() { @@ -267,6 +296,10 @@ export const CoreFilterCmpt = { persistence: this.persistence, }, ...(this.tabulatorOptions || {})}; + if (tabulatorOptions.locale) { + tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions); + } + // set default height if no height property is set if (tabulatorOptions.height === undefined && tabulatorOptions.minHeight === undefined && @@ -295,10 +328,10 @@ export const CoreFilterCmpt = { } // Start the tabulator with the build options - this.tabulator = new Tabulator( - this.$refs.table, - tabulatorOptions - ); + this.tabulator = new Tabulator(this.$refs.table, { + ...tabulatorOptions, + debugInvalidOptions: false, + }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) { @@ -306,7 +339,10 @@ export const CoreFilterCmpt = { for (let evt of this.tabulatorEvents) this.tabulator.on(evt.event, evt.handler); } - this.tabulator.on('tableBuilt', () => {this.tableBuilt = true; this.$emit('tableBuilt');}); + this.tabulator.on('tableBuilt', () => { + this.tableBuilt = true; + this.$emit('tableBuilt'); + }); this.tabulator.on("rowSelectionChanged", data => { this.selectedData = data; }); @@ -352,6 +388,121 @@ export const CoreFilterCmpt = { this.filterActive = filters.length > 0; this.$emit("headerFilterOn", this.filterActive); }); + this.tabulator.on("localized", () => { + this.tabulator.modules.responsiveLayout.generateCollapsedContent(); + }); + }, + async configureTabulatorLocalizations(tabulatorOptions) { + tabulatorOptions.locale = this.$p.user_language.value; + tabulatorOptions.langs = {}; + + const genericTabulatorPhrasesResponse = await this.$api.call( + ApiPhrases.getTabulatorPhrases(), + ); + const genericTabulatorPhrasesData = genericTabulatorPhrasesResponse.data; + Object.keys(genericTabulatorPhrasesData).forEach((language) => { + let genericTabulatorPhraseToTranslationMapper = {}; + genericTabulatorPhrasesData[language].forEach((phrase) => { + genericTabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text; + }); + + tabulatorOptions.langs[language] = { + data: { + loading: genericTabulatorPhraseToTranslationMapper["loading"], + error: genericTabulatorPhraseToTranslationMapper["error"], + }, + groups: { + item: genericTabulatorPhraseToTranslationMapper["item"], + items: genericTabulatorPhraseToTranslationMapper["items"], + }, + pagination: { + page_size: + genericTabulatorPhraseToTranslationMapper["page_size"], + page_title: + genericTabulatorPhraseToTranslationMapper["page_title"], + first: genericTabulatorPhraseToTranslationMapper["first"], + first_title: + genericTabulatorPhraseToTranslationMapper["first_title"], + last: genericTabulatorPhraseToTranslationMapper["last"], + last_title: + genericTabulatorPhraseToTranslationMapper["last_title"], + prev: genericTabulatorPhraseToTranslationMapper["prev"], + prev_title: + genericTabulatorPhraseToTranslationMapper["prev_title"], + next: genericTabulatorPhraseToTranslationMapper["next"], + next_title: + genericTabulatorPhraseToTranslationMapper["next_title"], + all: genericTabulatorPhraseToTranslationMapper["all"], + counter: { + showing: + genericTabulatorPhraseToTranslationMapper["showing"], + of: genericTabulatorPhraseToTranslationMapper["of"], + rows: genericTabulatorPhraseToTranslationMapper["rows"], + pages: genericTabulatorPhraseToTranslationMapper["pages"], + }, + }, + }; + }); + + let phrasesGroupedByCategoryRequestParam = {}; + tabulatorOptions.columns.forEach((column) => { + if (!column.titlePhrase?.length) return; + + let [category, phrase] = column.titlePhrase.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => { + let [category, phrase] = menuItemPhrase.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + + if (!Object.keys(phrasesGroupedByCategoryRequestParam).length) { + return tabulatorOptions; + } + + const phrasesResponse = await this.$api.call( + ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), + ); + const phrasesData = phrasesResponse.data; + + Object.keys(phrasesData).forEach((language) => { + let phraseToTranslationMapper = {}; + phrasesData[language].forEach((phrase) => { + phraseToTranslationMapper[ + phrase.category + "/" + phrase.phrase + ] = phrase.text; + }); + + let columnFieldToTranslationMapper = {}; + tabulatorOptions.columns.forEach((column) => { + if (!column.titlePhrase?.length) return; + + columnFieldToTranslationMapper[column.field] = + phraseToTranslationMapper[column.titlePhrase] ?? + "<< PHRASE " + column.titlePhrase + " >>"; + }); + tabulatorOptions.langs[language].columns = + columnFieldToTranslationMapper; + + let menuItemPhraseToTranslationMapper = {}; + tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => { + menuItemPhraseToTranslationMapper[menuItemPhrase] = + phraseToTranslationMapper[menuItemPhrase] ?? + "<< PHRASE " + menuItemPhrase + " >>"; + }); + tabulatorOptions.langs[language].menuItems = + menuItemPhraseToTranslationMapper; + }); + + return tabulatorOptions; }, updateTabulator() { if (this.tabulator) { @@ -659,7 +810,18 @@ export const CoreFilterCmpt = { // parent not found return false; - } + }, + getColumnNames() { + if (!this.tableBuilt) { + return {}; + } else if (this.tabulator.options.locale) { + return this.tabulator.getLang().columns; + } + return this.tabulator.getColumns().reduce((res, col) => { + res[col.getField()] = col.getDefinition().title; + return res; + }, {}); + }, }, beforeCreate() { if (!this.tableOnly == !this.filterType) @@ -670,6 +832,10 @@ export const CoreFilterCmpt = { alert('"nwNewEntry" listener is mandatory when sideMenu is true'); this.uuid = _uuid++; this.$emit('uuidDefined', this.uuid) + + Tabulator.registerModule(InternalToExternalEventBroadcastModule); + Tabulator.registerModule(MenuExtensionModule); + Tabulator.registerModule(ResponsiveLayoutExtensionModule); }, mounted() { this.initTabulator().then(() => { @@ -737,7 +903,7 @@ export const CoreFilterCmpt = { :data-bs-parent="'#filterCollapsables' + idExtra" :fields="fieldIdsForVisibilty" :selected="selectedFields" - :names="fieldNames" + :names="getColumnNames()" @hide="tabulator.hideColumn($event)" @show="tabulator.showColumn($event)" v-collapse-auto-close diff --git a/public/js/components/filter/Filter/Columns.js b/public/js/components/filter/Filter/Columns.js index be29e5f19..75bf2abab 100644 --- a/public/js/components/filter/Filter/Columns.js +++ b/public/js/components/filter/Filter/Columns.js @@ -42,7 +42,7 @@ export default { watch: { selected(n) { this.selectedFields = n; - } + }, }, methods: { toggle(field) { diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 6d5c5dd60..3b73c44f2 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -8,6 +8,7 @@ import dms from "./result/dms.js"; import cms from "./result/cms.js"; import mergedStudent from "./result/mergedstudent.js"; import mergedPerson from "./result/mergedperson.js"; +import { debounce } from "../../helpers/DebounceHelper.js"; export default { name: "FhcSearchbar", @@ -57,6 +58,107 @@ export default { settingsDropdown: null, lastQuery: "", isSearchShownInMobileView: false, + callsearchapi: debounce(() => { + this.error = null; + this.searchresult.splice(0, this.searchresult.length); + this.searching = true; + this.showsearchresult(); + if (this.searchsettings.types.length === 0) { + this.error = this.$p.t("search/error_missing_type"); + this.searching = false; + return; + } + + if (this.abortController) this.abortController.abort(); + + if (!this.searchsettings.searchstr?.length) return; + + this.abortController = new AbortController(); + + this.searchfunction(this.searchsettings, { + timeout: 50000, + signal: this.abortController.signal, + }) + .then((response) => { + if (!response.data) { + this.error = this.$p.t("search/error_general"); + } else { + let res = response.data.map((el) => + el.data ? { ...el, ...JSON.parse(el.data) } : el, + ); + this.lastQuery = response.meta.searchstring; + if (this.searchoptions.mergeResults) { + let counter = 0; + let mergeTypes = []; + let mergedType = "merged-"; + let mergeKey = ""; + + switch (this.searchoptions.mergeResults) { + case "student": + mergeTypes = ["student", "prestudent"]; + mergedType += + this.searchoptions.mergeResults; + mergeKey = "uid"; + break; + case "person": + mergeTypes = [ + "person", + "employee", + "student", + "prestudent", + ]; + mergedType += + this.searchoptions.mergeResults; + mergeKey = "person_id"; + break; + } + + if (mergeTypes.length) { + res = Object.values( + res.reduce((a, c) => { + if (!mergeTypes.includes(c.renderer)) { + a["nomerge" + counter++] = c; + } else if (c[mergeKey] === null) { + a["nomerge" + counter++] = c; + } else if ( + a[c[mergeKey]] === undefined + ) { + a[c[mergeKey]] = { + rank: c.rank, + renderer: mergedType, + type: mergedType, + list: [c], + }; + } else { + a[c[mergeKey]].list.push(c); + if (c.rank > a[c[mergeKey]].rank) + a[c[mergeKey]].rank = c.rank; + } + return a; + }, {}), + ).sort((a, b) => b.rank - a.rank); + } + } + this.searchresult = res; + this.searchmode = response.meta.mode; + } + this.searching = false; + this.retry = 0; + }) + .catch((error) => { + if (error.code == "ERR_CANCELED") { + return (this.retry = 0); + } + if (error.code == "ECONNABORTED" && this.retry) { + this.retry--; + return this.callsearchapi(); + } + + this.error = this.$p.t("search/error_general", error); + this.searching = false; + this.retry = 0; + }); + }, 500), }; }, computed: { @@ -345,122 +447,18 @@ export default { this.abort(); if (this.searchsettings.searchstr.length >= 2) { this.calcSearchResultExtent(); - this.searchtimer = setTimeout(this.callsearchapi, 500); + this.callsearchapi(); } else { this.showresult = false; } }, abort() { - if (this.searchtimer !== null) { - clearTimeout(this.searchtimer); - } if (this.abortController) { this.abortController.abort(); this.abortController = null; } this.searchresult = []; }, - callsearchapi: function () { - this.error = null; - this.searchresult.splice(0, this.searchresult.length); - this.searching = true; - this.showsearchresult(); - if (this.searchsettings.types.length === 0) { - this.error = this.$p.t("search/error_missing_type"); - this.searching = false; - return; - } - - if (this.abortController) this.abortController.abort(); - - if (!this.searchsettings.searchstr?.length) return; - - this.abortController = new AbortController(); - - this.searchfunction(this.searchsettings, { - timeout: 50000, - signal: this.abortController.signal, - }) - .then((response) => { - if (!response.data) { - this.error = this.$p.t("search/error_general"); - } else { - let res = response.data.map((el) => - el.data ? { ...el, ...JSON.parse(el.data) } : el, - ); - this.lastQuery = response.meta.searchstring; - if (this.searchoptions.mergeResults) { - let counter = 0; - let mergeTypes = []; - let mergedType = "merged-"; - let mergeKey = ""; - - switch (this.searchoptions.mergeResults) { - case "student": - mergeTypes = ["student", "prestudent"]; - mergedType += - this.searchoptions.mergeResults; - mergeKey = "uid"; - break; - case "person": - mergeTypes = [ - "person", - "employee", - "student", - "prestudent", - ]; - mergedType += - this.searchoptions.mergeResults; - mergeKey = "person_id"; - break; - } - - if (mergeTypes.length) { - res = Object.values( - res.reduce((a, c) => { - if (!mergeTypes.includes(c.renderer)) { - a["nomerge" + counter++] = c; - } else if (c[mergeKey] === null) { - a["nomerge" + counter++] = c; - } else if ( - a[c[mergeKey]] === undefined - ) { - a[c[mergeKey]] = { - rank: c.rank, - renderer: mergedType, - type: mergedType, - list: [c], - }; - } else { - a[c[mergeKey]].list.push(c); - if (c.rank > a[c[mergeKey]].rank) - a[c[mergeKey]].rank = c.rank; - } - return a; - }, {}), - ).sort((a, b) => b.rank - a.rank); - } - } - this.searchresult = res; - this.searchmode = response.meta.mode; - } - this.searching = false; - this.retry = 0; - }) - .catch((error) => { - if (error.code == "ERR_CANCELED") { - return (this.retry = 0); - } - if (error.code == "ECONNABORTED" && this.retry) { - this.retry--; - return this.callsearchapi(); - } - - this.error = this.$p.t("search/error_general", error); - this.searching = false; - this.retry = 0; - }); - }, refreshsearch: function () { this.search(); this.togglesettings(); diff --git a/public/js/helpers/DebounceHelper.js b/public/js/helpers/DebounceHelper.js new file mode 100644 index 000000000..af5e80019 --- /dev/null +++ b/public/js/helpers/DebounceHelper.js @@ -0,0 +1,7 @@ +export const debounce = (callback, wait) => { + let timeoutId = null; + return (context) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => callback(), wait); + } +} \ No newline at end of file diff --git a/public/js/helpers/StringHelpers.js b/public/js/helpers/StringHelpers.js index a67d0138f..e68d1d381 100644 --- a/public/js/helpers/StringHelpers.js +++ b/public/js/helpers/StringHelpers.js @@ -1,4 +1,49 @@ export function capitalize(string) { - if (!string) return ''; + if (!string) return ""; return string[0].toUpperCase() + string.slice(1); -} \ No newline at end of file +} + +export function convertCase(string, from, to) { + const possibleCases = ["kebab", "snake", "camel"]; + if ( + from === to || + !possibleCases.includes(from) || + !possibleCases.includes(to) + ) + return string; + + const individualWords = + from === "kebab" + ? string.split("-") + : from === "snake" + ? string.split("_") + : splitWordsInCamelCase(string); + + const formattedIndividualWords = ["snake", "kebab"].includes(to) + ? individualWords.map((word) => word.toLowerCase()) + : individualWords.map((word, index) => { + if (!index) return word.toLowerCase(); + return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase(); + }); + + const joinCharacter = to === "snake" ? "_" : to === "kebab" ? "-" : ""; + return formattedIndividualWords.join(joinCharacter); +} + +function splitWordsInCamelCase(string) { + const lastUpperCaseLetterIndex = string + .split("") + .findLastIndex((character) => { + return character.toUpperCase() === character; + }); + + if (lastUpperCaseLetterIndex < 1) { + return [string]; + } + + let splitWords = splitWordsInCamelCase( + string.slice(0, lastUpperCaseLetterIndex), + ); + splitWords.push(string.slice(lastUpperCaseLetterIndex)); + return splitWords; +} diff --git a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js new file mode 100644 index 000000000..e4d0b2811 --- /dev/null +++ b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js @@ -0,0 +1,31 @@ +// changes within tabulator are often not adequately broadcast to the external component +// this module exposes tabulator's internal events +// if external event name is not specified, the dispatched external event will be the camelCase version of the kebab-case internal event + +import Module from "../../../../vendor/olifolkerd/tabulator5/src/js/core/Module.js"; +import { convertCase } from "../../helpers/StringHelpers.js"; + +export default class InternalToExternalEventBroadcastModule extends Module { + static moduleName = "internalToExternalEventBroadcast"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + + this.eventsToBroadcast = [ + // example event + // { + // internal: "layout-refreshed", + // external: null, + // }, + ]; + } + + initialize() { + this.eventsToBroadcast.forEach((event) => { + this.subscribe(event.internal, () => { + this.dispatchExternal(event.external ?? convertCase(event.internal, "kebab", "camel")); + }); + }); + } +} diff --git a/public/js/tabulator/customModules/MenuExtensionModule.js b/public/js/tabulator/customModules/MenuExtensionModule.js new file mode 100644 index 000000000..883e82aeb --- /dev/null +++ b/public/js/tabulator/customModules/MenuExtensionModule.js @@ -0,0 +1,25 @@ +import Menu from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/Menu/Menu.js"; + +export default class MenuExtensionModule extends Menu { + static moduleName = "menu"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + } + + loadMenu(e, component, menu, parentEl, parentPopup){ + const isLocalizationEnabled = component.table.options.locale && component.table.options.locale !== "default"; + const menuItemTranslations = component.table.getLang().menuItems; + if (isLocalizationEnabled && menuItemTranslations) { + menu = menu.map((menuItem) => { + if (menuItem.phrase && menuItemTranslations[menuItem.phrase]) { + menuItem.label = menuItemTranslations[menuItem.phrase]; + } + return menuItem; + }); + } + + super.loadMenu(e, component, menu, parentEl, parentPopup); + } +} diff --git a/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js new file mode 100644 index 000000000..c0e66cff0 --- /dev/null +++ b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js @@ -0,0 +1,27 @@ +import ResponsiveLayout from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/ResponsiveLayout/ResponsiveLayout.js"; + +export default class ResponsiveLayoutExtensionModule extends ResponsiveLayout { + static moduleName = "responsiveLayout"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + } + + generateCollapsedRowData(row) { + let result = super.generateCollapsedRowData(row) + const isLocalizationEnabled = this.table.options.locale && this.table.options.locale !== "default"; + const columnHeadingTranslations = this.table?.getLang()?.columns; + + if (isLocalizationEnabled && columnHeadingTranslations) { + result = result.map((column) => { + if (columnHeadingTranslations[column.field]) { + column.title = columnHeadingTranslations[column.field]; + } + return column; + }); + } + + return result; + } +} diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index fbb664ed7..4b7140bd5 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -59958,6 +59958,388 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // ### End Notenstatistik + // generic tabulator phrases START + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'loading', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Lädt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Loading', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'error', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Fehler', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Error', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'item', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Element', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'item', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'items', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Elemente', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'items', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_size', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Seitengröße', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Page Size', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Seite anzeigen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Show Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Erste', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Erste Seite', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Letzte', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Letzte Seite', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'zurück', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Previous', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'vorherige Seite', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Previous Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nächste', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nächste Seite', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'all', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Alle', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'All', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'showing', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Angezeigt', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Showing', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'of', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'von', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'of', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'rows', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zeilen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'rows', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'pages', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Seiten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'pages', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // generic tabulator phrases END );