Merge branch 'feature/76688-general-tabulator-issues_76776-localization' into cis40_2026-02_rc_abgabetool_finetuning_76688

This commit is contained in:
Harald Bamberger
2026-07-08 07:17:24 +02:00
14 changed files with 936 additions and 200 deletions
@@ -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);
}
}
+20 -1
View File
@@ -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),
},
};
},
};
@@ -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*/ `
<div class="container-fluid text-break fhc-form" >
@@ -454,7 +452,6 @@ export default {
<!-- FUNKTIONEN TABELLE -->
<div class="col-12 mb-4" >
<core-filter-cmpt
v-if="arePhrasesPreloaded"
@tableBuilt="funktionenTableBuilt"
:title="$p.t('person','funktionen')"
ref="funktionenTable"
@@ -466,7 +463,6 @@ export default {
<!-- BETRIEBSMITTEL TABELLE -->
<div class="col-12 mb-4" >
<core-filter-cmpt
v-if="arePhrasesPreloaded"
@tableBuilt="betriebsmittelTableBuilt"
:title="$p.t('profil','entlehnteBetriebsmittel')"
ref="betriebsmittelTable"
@@ -20,7 +20,6 @@ export default {
data() {
return {
collapseIconFunktionen: true,
arePhrasesPreloaded: false,
funktionen_table_options: {
persistenceID: "filterTableMaViewProfilFunktionen",
persistence: {
@@ -31,6 +30,7 @@ export default {
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
locale: true,
columns: [
//? option when wanting to hide the collapsed list
@@ -45,25 +45,24 @@ export default {
visible: true,
},
{
title: Vue.computed(() => 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 {
<div class="row">
<!-- FIRST TABLE -->
<div class="col-12 mb-4" >
<core-filter-cmpt v-if="arePhrasesPreloaded" @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
</div>
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
</div>
@@ -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*/ `
<div class="container-fluid text-break fhc-form">
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal"
@@ -395,7 +388,6 @@ export default {
<div class="row">
<div class="col-12 mb-4" >
<core-filter-cmpt
v-if="arePhrasesPreloaded"
@tableBuilt="betriebsmittelTableBuilt"
:title="$p.t('profil','entlehnteBetriebsmittel')"
ref="betriebsmittelTable"
@@ -405,7 +397,6 @@ export default {
</div>
<div class="col-12 mb-4" >
<core-filter-cmpt
v-if="arePhrasesPreloaded"
@tableBuilt="zutrittsgruppenTableBuilt"
:title="$p.t('profil','zutrittsGruppen')"
ref="zutrittsgruppenTable"
+195 -29
View File
@@ -22,8 +22,14 @@ 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 { debounce } from "../../helpers/DebounceHelper.js";
import ApiFilter from '../../api/factory/filter.js';
import ApiPhrases from '../../api/factory/phrasen.js';
//
const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter';
@@ -84,8 +90,9 @@ export const CoreFilterCmpt = {
useSelectionSpan: {
type: Boolean,
default: true
}
},
},
inject: ["language"],
data: function() {
return {
uuid: 0,
@@ -120,7 +127,8 @@ export const CoreFilterCmpt = {
headerFilter: false,
group: false,
page: false,
}
},
collapsedHeadingLocalizationTimer: null,
};
},
computed: {
@@ -183,20 +191,25 @@ export const CoreFilterCmpt = {
return columns;
},
fieldIdsForVisibilty() {
if (!this.tableBuilt)
return [];
return this.tabulator.getColumns().filter(col => {
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
@@ -42,7 +42,7 @@ export default {
watch: {
selected(n) {
this.selectedFields = n;
}
},
},
methods: {
toggle(field) {
+103 -105
View File
@@ -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();
+7
View File
@@ -0,0 +1,7 @@
export const debounce = (callback, wait) => {
let timeoutId = null;
return (context) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => callback(), wait);
}
}
+47 -2
View File
@@ -1,4 +1,49 @@
export function capitalize(string) {
if (!string) return '';
if (!string) return "";
return string[0].toUpperCase() + string.slice(1);
}
}
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;
}
@@ -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"));
});
});
}
}
@@ -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);
}
}
@@ -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;
}
}
+382
View File
@@ -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
);