mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
Merge branch 'feature-77417/studvw-tags-formatter-and-header-filter' into studvw_lvvw_2026_08_rc
This commit is contained in:
@@ -37,6 +37,7 @@ class Tag extends API_Controller
|
||||
|
||||
if (isset($tag))
|
||||
{
|
||||
$this->TagModel->addJoin('public.tbl_notiz_typ', 'typ_kurzbz', 'typ_kurzbz');
|
||||
$result = $this->TagModel->load($tag);
|
||||
|
||||
$this->response($result, REST_Controller::HTTP_OK);
|
||||
|
||||
@@ -73,6 +73,7 @@ class Tag_Controller extends FHCAPI_Controller
|
||||
tbl_notiz.notiz_id,
|
||||
tbl_notiz_typ.style,
|
||||
tbl_notiz_typ.automatisiert,
|
||||
tbl_notiz_typ.prioritaet,
|
||||
tbl_notiz.erledigt as done,
|
||||
tbl_notiz.insertamum,
|
||||
tbl_notiz.updateamum,
|
||||
@@ -107,7 +108,8 @@ class Tag_Controller extends FHCAPI_Controller
|
||||
style,
|
||||
beschreibung,
|
||||
tag,
|
||||
automatisiert
|
||||
automatisiert,
|
||||
prioritaet
|
||||
"
|
||||
);
|
||||
$this->NotiztypModel->addOrder('prioritaet');
|
||||
@@ -317,6 +319,7 @@ class Tag_Controller extends FHCAPI_Controller
|
||||
tbl_notiz.notiz_id,
|
||||
tbl_notiz_typ.style,
|
||||
tbl_notiz_typ.automatisiert,
|
||||
tbl_notiz_typ.prioritaet,
|
||||
tbl_notiz.erledigt as done,
|
||||
tbl_notiz.insertamum,
|
||||
tbl_notiz.updateamum,
|
||||
@@ -336,6 +339,8 @@ class Tag_Controller extends FHCAPI_Controller
|
||||
|
||||
$this->NotizModel->addJoin('public.tbl_notizzuordnung notizzuordnung', 'tbl_notiz.notiz_id = notizzuordnung.notiz_id');
|
||||
|
||||
$this->NotizModel->addOrder('done');
|
||||
$this->NotizModel->addOrder('tbl_notiz_typ.prioritaet');
|
||||
$notiz = $this->NotizModel->loadWhere(array('prestudent_id' => $prestudent_id));
|
||||
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ class StudentListLib
|
||||
$subQueryTag = "(
|
||||
SELECT
|
||||
tag.prestudent_id,
|
||||
COALESCE(json_agg(tag ORDER BY tag.done), '[]'::json) AS tags
|
||||
COALESCE(json_agg(tag ORDER BY tag.done, tag.prioritaet), '[]'::json) AS tags
|
||||
FROM (
|
||||
SELECT DISTINCT ON (n.notiz_id)
|
||||
n.notiz_id AS id,
|
||||
@@ -355,6 +355,8 @@ class StudentListLib
|
||||
n.text AS notiz,
|
||||
nt.style,
|
||||
n.erledigt AS done,
|
||||
nt.prioritaet AS prioritaet,
|
||||
nt.automatisiert AS automatisiert,
|
||||
nz.prestudent_id,
|
||||
n.start,
|
||||
n.ende
|
||||
|
||||
@@ -719,7 +719,7 @@ EOSQL;
|
||||
return "tag_data_agg AS (
|
||||
SELECT
|
||||
lehreinheit_id,
|
||||
COALESCE(json_agg(tag ORDER BY done), '[]'::json) AS tags
|
||||
COALESCE(json_agg(tag ORDER BY done, prioritaet), '[]'::json) AS tags
|
||||
FROM (
|
||||
SELECT DISTINCT ON (public.tbl_notiz.notiz_id)
|
||||
tbl_notiz.notiz_id AS id,
|
||||
@@ -728,7 +728,9 @@ EOSQL;
|
||||
text AS notiz,
|
||||
style,
|
||||
erledigt AS done,
|
||||
lehreinheit_id
|
||||
lehreinheit_id,
|
||||
prioritaet,
|
||||
automatisiert
|
||||
FROM public.tbl_notizzuordnung
|
||||
JOIN public.tbl_notiz ON tbl_notizzuordnung.notiz_id = tbl_notiz.notiz_id
|
||||
JOIN public.tbl_notiz_typ ON tbl_notiz.typ = tbl_notiz_typ.typ_kurzbz
|
||||
|
||||
@@ -5,8 +5,7 @@ return [
|
||||
field: "lv_kurzbz",
|
||||
headerFilterFuncParams: { field: 'lv_kurzbz' },
|
||||
headerFilter: true,
|
||||
formatter(cell,
|
||||
formatterParams) {
|
||||
formatter(cell, formatterParams) {
|
||||
const rowData = cell.getRow().getData();
|
||||
const iconKey = (rowData.lehrtyp_kurzbz || '').toLowerCase();
|
||||
const lvkurzbz = (cell.getValue()).toUpperCase();
|
||||
@@ -22,8 +21,7 @@ return [
|
||||
|
||||
return parentspan;
|
||||
},
|
||||
cellClick(e,
|
||||
cell) {
|
||||
cellClick(e, cell) {
|
||||
cell.getRow().treeToggle();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -268,9 +268,9 @@ return [
|
||||
},
|
||||
headerFilter: "tickCross",
|
||||
headerFilterParams: {
|
||||
"tristate": true,
|
||||
tristate: true,
|
||||
elementAttributes: {
|
||||
"value": "true"
|
||||
value: "true"
|
||||
}
|
||||
},
|
||||
headerFilterEmptyCheck(value) {
|
||||
@@ -326,9 +326,9 @@ return [
|
||||
},
|
||||
headerFilter: "tickCross",
|
||||
headerFilterParams: {
|
||||
"tristate": true,
|
||||
tristate: true,
|
||||
elementAttributes: {
|
||||
"value": "true"
|
||||
value: "true"
|
||||
}
|
||||
},
|
||||
headerFilterEmptyCheck(value) {
|
||||
|
||||
@@ -298,7 +298,15 @@ export default {
|
||||
return this.$api
|
||||
.call(ApiTag.getAllTagsPrestudent({prestudent_id}))
|
||||
.then(result => {
|
||||
this.tagData = result.data;
|
||||
this.tagData = result.data.sort((a, b) => {
|
||||
let adone = a.done ? 1 : 0;
|
||||
let bdone = b.done ? 1 : 0;
|
||||
|
||||
if (adone !== bdone) {
|
||||
return adone - bdone;
|
||||
}
|
||||
return a.prioritaet - b.prioritaet;
|
||||
});
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
@@ -2,8 +2,16 @@ import {CoreFilterCmpt} from "../../filter/Filter.js";
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import DetailsForm from "../Details/Form.js";
|
||||
import CoreTag from '../../Tag/Tag.js';
|
||||
import { tagHeaderFilter } from '../../../../js/tabulator/filters/extendedHeaderFilter.js';
|
||||
import { extendedHeaderFilter } from "../../../../js/tabulator/filters/extendedHeaderFilter.js";
|
||||
import {
|
||||
buildTagHeaderFilterExpression,
|
||||
buildTagOptionsFromRows,
|
||||
customTagFilter,
|
||||
setTagHeaderFilterValue,
|
||||
tagHeaderFilter,
|
||||
extendedHeaderFilter,
|
||||
syncTagHeaderFilterOptions,
|
||||
syncSelectedTagOptionsWithHeaderFilters
|
||||
} from "../../../../js/tabulator/filters/extendedHeaderFilter.js";
|
||||
import { tagFormatter } from "../../../../js/tabulator/formatter/tags.js";
|
||||
import { addTagInTable, deleteTagInTable, updateTagInTable } from "../../../../js/helpers/TagHelper.js";
|
||||
|
||||
@@ -69,7 +77,20 @@ export default {
|
||||
this.lv_info_default.studiensemester_kurzbz = newVal
|
||||
this.lv_info = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
"tagFilterState.selectedOptions": {
|
||||
handler() {
|
||||
const selectedOptions = this.tagFilterState.selectedOptions;
|
||||
const combinedFilterStatement =
|
||||
buildTagHeaderFilterExpression(selectedOptions);
|
||||
|
||||
setTagHeaderFilterValue(
|
||||
combinedFilterStatement,
|
||||
this.$refs.table.tabulator
|
||||
);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -100,8 +121,37 @@ export default {
|
||||
handler: (data) => {
|
||||
this.getExpandedRows()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "dataLoaded",
|
||||
handler: (data) => {
|
||||
syncTagHeaderFilterOptions(
|
||||
Array.isArray(data) ? data : [],
|
||||
this.tagFilterState.initialOptions,
|
||||
this.tagFilterState.selectedOptions,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
event: "dataFiltered",
|
||||
handler: (filters, rows) => {
|
||||
syncSelectedTagOptionsWithHeaderFilters(
|
||||
filters,
|
||||
this.tagFilterState.selectedOptions,
|
||||
this.tagsEnabled,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
event: "columnWidth",
|
||||
handler: (column) => {
|
||||
if (column.getField() !== "tags") return;
|
||||
|
||||
column.getCells().forEach((cell) => {
|
||||
cell.getElement().firstElementChild?.fitTags?.();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: {},
|
||||
lv_info: false,
|
||||
@@ -115,7 +165,20 @@ export default {
|
||||
raumtypalternativ: this.lehreinheitRaumtypAlternativeDefault,
|
||||
lehrfach_id: ''
|
||||
|
||||
}
|
||||
},
|
||||
tagFilterState: {
|
||||
initialOptions: [],
|
||||
selectedOptions: [],
|
||||
},
|
||||
tagFilterLabels: {
|
||||
tag: "Tag",
|
||||
clear: "Clear",
|
||||
connectors: {
|
||||
AND: "AND",
|
||||
OR: "OR",
|
||||
NOT: "NOT",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -146,6 +209,14 @@ export default {
|
||||
layout: 'fitDataStretch',
|
||||
height: '100%',
|
||||
persistenceID: 'lehrveranstaltungen_2026_07_15_v1',
|
||||
persistence: {
|
||||
sort: true,
|
||||
columns: ["width", "visible"],
|
||||
filter: false,
|
||||
headerFilter: false,
|
||||
group: false,
|
||||
page: false,
|
||||
},
|
||||
selectableRowsRangeMode: 'click',
|
||||
selectableRows: true,
|
||||
rowContextMenu: (component, e) => {
|
||||
@@ -204,11 +275,25 @@ export default {
|
||||
created() {
|
||||
Tabulator.extendModule("format", "formatters", {
|
||||
tagHeaderFilter,
|
||||
tagFormatter: cell => tagFormatter(cell, this.$refs.tagComponent),
|
||||
tagFormatter: (cell, params, onRendered) => tagFormatter(cell, this.$refs.tagComponent, onRendered),
|
||||
ja_nein: cell => cell.getValue()
|
||||
? this.toUpperCase(this.$p.t('ui', 'ja'))
|
||||
: this.toUpperCase(this.$p.t('ui', 'nein'))
|
||||
});
|
||||
LvvColumns.foreach(col => {
|
||||
if (col.headerFilter == 'input' && col.headerFilterFunc == 'tagHeaderFilter') {
|
||||
col.headerFilter = customTagFilter;
|
||||
col.headerFilterParams = {
|
||||
listOnEmpty: true,
|
||||
autocomplete: true,
|
||||
sort: "asc",
|
||||
initialOptions: this.tagFilterState.initialOptions,
|
||||
selectedOptions: this.tagFilterState.selectedOptions,
|
||||
labels: this.tagFilterLabels,
|
||||
};
|
||||
col.headerSort = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
if (this.shouldAutoLoad())
|
||||
@@ -319,16 +404,31 @@ export default {
|
||||
},
|
||||
addedTag(addedTag)
|
||||
{
|
||||
addTagInTable(addedTag, this.allRows, 'lehreinheit_id')
|
||||
addTagInTable(addedTag, this.allRows, 'lehreinheit_id');
|
||||
syncTagHeaderFilterOptions(
|
||||
this.$refs.table?.tabulator?.getData() || [],
|
||||
this.tagFilterState.initialOptions,
|
||||
this.tagFilterState.selectedOptions,
|
||||
);
|
||||
},
|
||||
deletedTag(deletedTag)
|
||||
{
|
||||
deleteTagInTable(deletedTag, this.allRows);
|
||||
syncTagHeaderFilterOptions(
|
||||
this.$refs.table?.tabulator?.getData() || [],
|
||||
this.tagFilterState.initialOptions,
|
||||
this.tagFilterState.selectedOptions,
|
||||
);
|
||||
},
|
||||
|
||||
updatedTag(updatedTag)
|
||||
{
|
||||
updateTagInTable(updatedTag, this.allRows);
|
||||
syncTagHeaderFilterOptions(
|
||||
this.$refs.table?.tabulator?.getData() || [],
|
||||
this.tagFilterState.initialOptions,
|
||||
this.tagFilterState.selectedOptions,
|
||||
);
|
||||
},
|
||||
async copyLehreinheit(row, art)
|
||||
{
|
||||
@@ -502,4 +602,4 @@ export default {
|
||||
</bs-modal>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,8 @@ export default {
|
||||
updatevon: "",
|
||||
response: "",
|
||||
start: "",
|
||||
ende: ""
|
||||
ende: "",
|
||||
prioritaet: 100,
|
||||
},
|
||||
mode: "create"
|
||||
};
|
||||
@@ -94,7 +95,8 @@ export default {
|
||||
this.tagData.automatisiert = item.automatisiert;
|
||||
this.tagData.start = this.formatDateTimeDay(item.start);
|
||||
this.tagData.ende = this.formatDateTimeDay(item.ende);
|
||||
|
||||
this.tagData.prioritaet = item.prioritaet || 100;
|
||||
|
||||
if (item && item.notiz_id)
|
||||
{
|
||||
this.selectedTagId = item.notiz_id;
|
||||
@@ -192,7 +194,8 @@ export default {
|
||||
updateamum: "",
|
||||
bearbeiter: "",
|
||||
response: "",
|
||||
readonly: false
|
||||
readonly: false,
|
||||
prioritaet: 100,
|
||||
};
|
||||
this.selectedTagId = null;
|
||||
this.mode = "create";
|
||||
|
||||
@@ -59,7 +59,7 @@ export function idTagFormatter (id, tagData, tagComponent, typeId, semesterStart
|
||||
let bdone = b.done ? 1 : 0;
|
||||
|
||||
if (adone !== bdone) return adone - bdone;
|
||||
return b.id - a.id;
|
||||
return a.prioritaet - b.prioritaet;
|
||||
});
|
||||
|
||||
const tagsToShow = expanded
|
||||
|
||||
@@ -20,25 +20,37 @@ export function addTagInTable(addedTag, rows, matchKey, tagsKey = "tags")
|
||||
for (const tag of addedTag.response)
|
||||
{
|
||||
if (rowData[matchKey] !== tag[matchKey])
|
||||
continue;
|
||||
return;
|
||||
|
||||
//avoid double inserts
|
||||
if (tags.some(x => x.id === tag.id))
|
||||
continue;
|
||||
let tags;
|
||||
try {
|
||||
let initialTags = rowData[tagsKey] || "[]";
|
||||
if (typeof initialTags === 'string') {
|
||||
tags = JSON.parse(initialTags);
|
||||
} else {
|
||||
tags = initialTags;
|
||||
}
|
||||
} catch (e) {
|
||||
tags = [];
|
||||
}
|
||||
|
||||
const newTag = {
|
||||
id: tag.id,
|
||||
prestudent_id: tag.prestudent_id,
|
||||
//add also information of addedTag
|
||||
beschreibung: addedTag.beschreibung,
|
||||
notiz: addedTag.notiz ?? "",
|
||||
style: addedTag.style,
|
||||
done: addedTag.done ?? false,
|
||||
typ_kurzbz: addedTag.tag_typ_kurzbz ?? addedTag.typ_kurzbz, //here seem to be 2 variations
|
||||
automatisiert: addedTag.automatisiert
|
||||
};
|
||||
if (!Array.isArray(tags))
|
||||
tags = [];
|
||||
|
||||
tags.unshift(newTag);
|
||||
if (tags.some(t => t?.id === tag.id))
|
||||
return;
|
||||
|
||||
let newTag = { ...addedTag, id: tag.id };
|
||||
|
||||
tags.push(newTag);
|
||||
tags.sort((a, b) => {
|
||||
let adone = a.done ? 1 : 0;
|
||||
let bdone = b.done ? 1 : 0;
|
||||
|
||||
if (adone !== bdone) return adone - bdone;
|
||||
return a.prioritaet - b.prioritaet;
|
||||
});
|
||||
rowData[tagsKey] = JSON.stringify(tags);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
@@ -69,10 +81,11 @@ export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags'])
|
||||
let tags = [];
|
||||
|
||||
try {
|
||||
if (typeof raw === "string") {
|
||||
tags = JSON.parse(raw || "[]");
|
||||
} else if (Array.isArray(raw)) {
|
||||
tags = [...raw];
|
||||
let initialTags = rowData[key] || "[]";
|
||||
if (typeof initialTags === 'string') {
|
||||
tags = JSON.parse(initialTags);
|
||||
} else {
|
||||
tags = initialTags;
|
||||
}
|
||||
} catch (e) {
|
||||
tags = [];
|
||||
@@ -113,7 +126,12 @@ export function updateTagInTable(updatedTag, rows, fields = ['tags'])
|
||||
|
||||
let fieldData;
|
||||
try {
|
||||
fieldData = JSON.parse(rowData[field] || "[]");
|
||||
let initialTags = rowData[field] || "[]";
|
||||
if (typeof initialTags === 'string') {
|
||||
fieldData = JSON.parse(initialTags);
|
||||
} else {
|
||||
fieldData = initialTags;
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
@@ -126,6 +144,14 @@ export function updateTagInTable(updatedTag, rows, fields = ['tags'])
|
||||
if (index !== -1)
|
||||
{
|
||||
fieldData[index] = { ...updatedTag };
|
||||
fieldData.sort((a, b) => {
|
||||
let adone = a.done ? 1 : 0;
|
||||
let bdone = b.done ? 1 : 0;
|
||||
|
||||
if (adone !== bdone) return adone - bdone;
|
||||
return a.prioritaet - b.prioritaet;
|
||||
});
|
||||
|
||||
let updatedFieldData = JSON.stringify(fieldData);
|
||||
|
||||
if (updatedFieldData !== rowData[field])
|
||||
|
||||
@@ -1,163 +1,617 @@
|
||||
function parseFilterExpression(expression)
|
||||
{
|
||||
const collections = [];
|
||||
let activeTagDropdownCleanup = null;
|
||||
const TAG_FILTER_CONNECTORS = {
|
||||
AND: {
|
||||
key: "AND",
|
||||
label: "AND",
|
||||
operator: "&&",
|
||||
isVisible: false,
|
||||
},
|
||||
OR: {
|
||||
key: "OR",
|
||||
label: "OR",
|
||||
operator: "||",
|
||||
isVisible: true,
|
||||
},
|
||||
NOT: {
|
||||
key: "NOT",
|
||||
label: "NOT",
|
||||
operator: "!",
|
||||
isVisible: true,
|
||||
},
|
||||
};
|
||||
const TAG_FILTER_CONNECTOR_ORDER = [
|
||||
//TAG_FILTER_CONNECTORS.AND,
|
||||
TAG_FILTER_CONNECTORS.OR,
|
||||
TAG_FILTER_CONNECTORS.NOT,
|
||||
];
|
||||
|
||||
try {
|
||||
const orParts = expression.split('||').map(part => part.trim());
|
||||
|
||||
orParts.forEach(part => {
|
||||
|
||||
const andParts = part.split('&&').map(p => p.trim());
|
||||
|
||||
const collection = { positives: [], negatives: [] };
|
||||
|
||||
andParts.forEach(term => {
|
||||
|
||||
const comparisonMatch = term.match(/^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/);
|
||||
|
||||
if (comparisonMatch)
|
||||
{
|
||||
const operator = comparisonMatch[1];
|
||||
const numberStr = comparisonMatch[2].replace(',', '.');
|
||||
const number = parseFloat(numberStr);
|
||||
collection.positives.push({ type: 'comparison', operator, number });
|
||||
}
|
||||
else if (term.startsWith('!'))
|
||||
{
|
||||
const excludeTerm = term.substring(1).trim().replace(/\*/g, '.*');
|
||||
collection.negatives.push({ type: 'regex', regex: new RegExp(excludeTerm, 'i') });
|
||||
}
|
||||
else
|
||||
{
|
||||
const includeTerm = term.replace(/\*/g, '.*');
|
||||
collection.positives.push({ type: 'regex', regex: new RegExp(includeTerm, 'i') });
|
||||
}
|
||||
});
|
||||
collections.push(collection);
|
||||
});
|
||||
} catch (e) {}
|
||||
return collections;
|
||||
function getConnectorLabel(connector, labels) {
|
||||
return labels?.connectors?.[connector.key] || connector.label;
|
||||
}
|
||||
|
||||
export function extendedHeaderFilter(headerValue, rowValue, rowData, filterParams)
|
||||
{
|
||||
const fields = Array.isArray(filterParams?.field)
|
||||
? filterParams.field
|
||||
: [filterParams?.field];
|
||||
export function setTagHeaderFilterValue(value, tabulator, columnField = "tags") {
|
||||
tabulator?.setHeaderFilterValue(columnField, value);
|
||||
|
||||
if (fields.length > 1 && rowData)
|
||||
{
|
||||
rowValue = fields
|
||||
.map(f => rowData[f] ?? '')
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
if (typeof headerValue === 'boolean')
|
||||
{
|
||||
return rowValue === headerValue;
|
||||
}
|
||||
const input = tabulator
|
||||
?.getColumn(columnField)
|
||||
?.getElement()
|
||||
?.querySelector(".tabulator-header-filter input");
|
||||
|
||||
const collections = parseFilterExpression(headerValue);
|
||||
|
||||
function matchValue(value)
|
||||
{
|
||||
try {
|
||||
return collections.some(collection => {
|
||||
|
||||
let positives = collection.positives.length === 0 || collection.positives.every(condition => {
|
||||
|
||||
if (condition.type === 'comparison')
|
||||
{
|
||||
let value = parseFloat(rowValue);
|
||||
if (isNaN(value)) return false;
|
||||
|
||||
switch (condition.operator) {
|
||||
case '<':
|
||||
return value < condition.number;
|
||||
case '>':
|
||||
return value > condition.number;
|
||||
case '<=':
|
||||
return value <= condition.number;
|
||||
case '>=':
|
||||
return value >= condition.number;
|
||||
case '=':
|
||||
return value === condition.number;
|
||||
case '!=':
|
||||
return value !== condition.number;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (condition.type === 'regex')
|
||||
{
|
||||
return condition.regex.test(rowValue);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
let negatives = collection.negatives.every(condition => {
|
||||
return !condition.regex.test(rowValue);
|
||||
});
|
||||
|
||||
return positives && negatives;
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (matchValue(rowValue))
|
||||
return true;
|
||||
|
||||
if (rowData && filterParams)
|
||||
{
|
||||
const childrenField = filterParams?.children || '_children';
|
||||
const field = filterParams?.field;
|
||||
|
||||
const children = rowData[childrenField];
|
||||
if (Array.isArray(children))
|
||||
{
|
||||
for (let child of children)
|
||||
{
|
||||
let childValue = child[field];
|
||||
if (extendedHeaderFilter(headerValue, childValue, child, filterParams))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (input) {
|
||||
input.value = value ?? "";
|
||||
}
|
||||
}
|
||||
export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams)
|
||||
{
|
||||
|
||||
let data;
|
||||
export function buildTagHeaderFilterExpression(selectedTagOptions) {
|
||||
let expression = null;
|
||||
|
||||
try {
|
||||
data = typeof rowValue === 'string' ? JSON.parse(rowValue) : rowValue;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
selectedTagOptions.forEach(({ value, connector }) => {
|
||||
const connectorDef = TAG_FILTER_CONNECTORS[connector];
|
||||
if (!connectorDef) return;
|
||||
|
||||
let combinedText;
|
||||
if (connectorDef.key === TAG_FILTER_CONNECTORS.NOT.key) {
|
||||
expression = expression
|
||||
? `${expression} ${TAG_FILTER_CONNECTORS.AND.operator} ${connectorDef.operator}${value}`
|
||||
: `${connectorDef.operator}${value}`;
|
||||
} else {
|
||||
expression = expression
|
||||
? `${expression} ${connectorDef.operator} ${value}`
|
||||
: value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Array.isArray(data))
|
||||
{
|
||||
combinedText = data
|
||||
.filter(item => item?.done !== true)
|
||||
.map(item => `${item?.beschreibung} ${item?.notiz}`)
|
||||
.join(' ');
|
||||
}
|
||||
else if (typeof data === 'object' && data !== null)
|
||||
{
|
||||
combinedText = data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : '';
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedText = String(data);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams)
|
||||
}
|
||||
export function buildTagOptionsFromRows(rows, field = "tags") {
|
||||
let parsedTags = [
|
||||
...new Map(
|
||||
getRawTagValuesFromRows(rows, field)
|
||||
.filter((tag) => tag && tag.done !== true)
|
||||
.sort((a, b) => {
|
||||
return a.prioritaet - b.prioritaet;
|
||||
})
|
||||
.map((tag) => {
|
||||
return {
|
||||
label: tag.beschreibung || tag.typ_kurzbz,
|
||||
value: tag.beschreibung || tag.typ_kurzbz,
|
||||
style: tag.style,
|
||||
automatisiert: tag.automatisiert,
|
||||
done: tag.done,
|
||||
};
|
||||
})
|
||||
.map((obj) => [obj.value, obj]),
|
||||
).values(),
|
||||
];
|
||||
if (!Array.isArray(parsedTags)) return;
|
||||
|
||||
return parsedTags;
|
||||
}
|
||||
|
||||
export function syncTagHeaderFilterOptions(
|
||||
rows = [],
|
||||
initialOptions = [],
|
||||
selectedOptions = [],
|
||||
field = "tags"
|
||||
) {
|
||||
const options = buildTagOptionsFromRows(rows, field) || [];
|
||||
|
||||
if (Array.isArray(initialOptions)) {
|
||||
initialOptions.splice(0, initialOptions.length, ...options);
|
||||
}
|
||||
|
||||
if (Array.isArray(selectedOptions)) {
|
||||
const availableValues = new Set(options.map((option) => option.value));
|
||||
|
||||
for (let i = selectedOptions.length - 1; i >= 0; i--) {
|
||||
if (!availableValues.has(selectedOptions[i].value)) {
|
||||
selectedOptions.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function syncSelectedTagOptionsWithHeaderFilters(
|
||||
filters = [],
|
||||
selectedOptions = [],
|
||||
tagsEnabled = true,
|
||||
field = "tags",
|
||||
) {
|
||||
if (
|
||||
!tagsEnabled ||
|
||||
!Array.isArray(selectedOptions) ||
|
||||
!selectedOptions.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasActiveTagFilter = filters.some(
|
||||
(filter) =>
|
||||
filter.field === field &&
|
||||
filter.value !== "" &&
|
||||
filter.value !== null &&
|
||||
filter.value !== undefined,
|
||||
);
|
||||
|
||||
if (hasActiveTagFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedOptions.splice(0, selectedOptions.length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getRawTagValuesFromRows(rows = [], field = "tags") {
|
||||
let parsedTags = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (!row || (!row[field]?.length && !row._children?.length)) return;
|
||||
|
||||
let newTags = row[field]?.length
|
||||
? [row[field]]
|
||||
: getRawTagValuesFromRows(row._children, field);
|
||||
|
||||
newTags = newTags
|
||||
.map((newTag) => {
|
||||
if (typeof newTag === "string") {
|
||||
try {
|
||||
newTag = JSON.parse(newTag);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return newTag;
|
||||
})
|
||||
.filter((newTag) => newTag !== null);
|
||||
|
||||
parsedTags = parsedTags.concat(newTags);
|
||||
});
|
||||
|
||||
return parsedTags.flat();
|
||||
}
|
||||
|
||||
export function customTagFilter(cell, onRendered, success, cancel, params) {
|
||||
const container = document.createElement("div");
|
||||
container.style.width = "100%";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.style.width = "100%";
|
||||
input.type = "text";
|
||||
let dropdownMenu = null;
|
||||
let suppressScrollCloseUntil = 0;
|
||||
|
||||
Object.defineProperty(container, "value", {
|
||||
get: () => input.value,
|
||||
set: (value) => {
|
||||
input.value = value ?? "";
|
||||
},
|
||||
});
|
||||
|
||||
const positionDropdown = () => {
|
||||
if (!dropdownMenu) return;
|
||||
|
||||
const rect = input.getBoundingClientRect();
|
||||
const width = Math.min(Math.max(rect.width, 360), window.innerWidth - 16);
|
||||
const left = Math.min(
|
||||
Math.max(rect.left, 8),
|
||||
window.innerWidth - width - 8,
|
||||
);
|
||||
|
||||
dropdownMenu.style.position = "fixed";
|
||||
dropdownMenu.style.left = `${left}px`;
|
||||
dropdownMenu.style.top = `${rect.bottom + 2}px`;
|
||||
dropdownMenu.style.width = `${width}px`;
|
||||
dropdownMenu.style.maxHeight = "50vh";
|
||||
dropdownMenu.style.overflowY = "auto";
|
||||
dropdownMenu.style.zIndex = "2000";
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
if (dropdownMenu) {
|
||||
dropdownMenu.remove();
|
||||
dropdownMenu = null;
|
||||
}
|
||||
|
||||
if (activeTagDropdownCleanup === closeDropdown) {
|
||||
activeTagDropdownCleanup = null;
|
||||
}
|
||||
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
window.removeEventListener("resize", positionDropdown);
|
||||
document.removeEventListener("mousedown", handleOutsideClick, true);
|
||||
};
|
||||
|
||||
const suppressScrollClose = () => {
|
||||
suppressScrollCloseUntil = Date.now() + 300;
|
||||
};
|
||||
|
||||
const handleScroll = (event) => {
|
||||
const target = event.target;
|
||||
const isDropdownScroll =
|
||||
target instanceof Node && dropdownMenu?.contains(target);
|
||||
|
||||
if (isDropdownScroll || Date.now() < suppressScrollCloseUntil) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleOutsideClick = (event) => {
|
||||
if (
|
||||
dropdownMenu?.contains(event.target) ||
|
||||
container.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const clearFilter = ({ closeDropdownAfterClear = true } = {}) => {
|
||||
input.value = "";
|
||||
|
||||
if (Array.isArray(params.selectedOptions)) {
|
||||
params.selectedOptions.splice(0, params.selectedOptions.length);
|
||||
}
|
||||
|
||||
success("");
|
||||
|
||||
if (closeDropdownAfterClear) {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const openDropdown = () => {
|
||||
if (activeTagDropdownCleanup) {
|
||||
activeTagDropdownCleanup();
|
||||
}
|
||||
|
||||
dropdownMenu = getRebuiltDropdown(
|
||||
params.initialOptions,
|
||||
params.selectedOptions,
|
||||
params.labels,
|
||||
clearFilter,
|
||||
);
|
||||
dropdownMenu.addEventListener("mousedown", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
dropdownMenu.addEventListener("click", suppressScrollClose, true);
|
||||
|
||||
document.body.appendChild(dropdownMenu);
|
||||
activeTagDropdownCleanup = closeDropdown;
|
||||
|
||||
positionDropdown();
|
||||
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
window.addEventListener("resize", positionDropdown);
|
||||
document.addEventListener("mousedown", handleOutsideClick, true);
|
||||
};
|
||||
|
||||
const stopHeaderFilterGesture = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
input.addEventListener("pointerdown", stopHeaderFilterGesture);
|
||||
input.addEventListener("mousedown", stopHeaderFilterGesture);
|
||||
input.addEventListener("touchstart", stopHeaderFilterGesture);
|
||||
input.addEventListener("click", stopHeaderFilterGesture);
|
||||
input.addEventListener("input", () => {
|
||||
success(input.value);
|
||||
});
|
||||
input.addEventListener("change", () => {
|
||||
success(input.value);
|
||||
});
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Escape") return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
clearFilter({ closeDropdownAfterClear: false });
|
||||
|
||||
openDropdown();
|
||||
});
|
||||
input.addEventListener("focus", (e) => {
|
||||
e.stopPropagation();
|
||||
openDropdown();
|
||||
});
|
||||
|
||||
container.appendChild(input);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function getRebuiltDropdown(
|
||||
initialOptions,
|
||||
selectedOptions,
|
||||
labels,
|
||||
clearFilter,
|
||||
) {
|
||||
const dropdownTable = generateDropdownTable(
|
||||
initialOptions || [],
|
||||
selectedOptions || [],
|
||||
labels,
|
||||
);
|
||||
|
||||
const menu = document.createElement("div");
|
||||
menu.className = "dropdown-menu show py-3 px-1";
|
||||
|
||||
const clearButton = document.createElement("button");
|
||||
clearButton.type = "button";
|
||||
clearButton.className =
|
||||
"btn btn-link btn-sm p-0 position-absolute top-0 end-0 m-1";
|
||||
clearButton.title = labels?.clear || "Clear";
|
||||
clearButton.setAttribute("aria-label", labels?.clear || "Clear");
|
||||
|
||||
const clearIcon = document.createElement("i");
|
||||
clearIcon.className = "fa-solid fa-filter-circle-xmark";
|
||||
clearButton.appendChild(clearIcon);
|
||||
|
||||
clearButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearFilter({ closeDropdownAfterClear: false });
|
||||
menu.querySelectorAll("input[type='checkbox']").forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
menu.appendChild(clearButton);
|
||||
menu.appendChild(dropdownTable);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
function generateDropdownTable(options, selectedTagOptions, labels) {
|
||||
const table = document.createElement("table");
|
||||
table.className = "table table-sm mb-0";
|
||||
|
||||
// HEADER
|
||||
const thead = document.createElement("thead");
|
||||
const headRow = document.createElement("tr");
|
||||
|
||||
const headers = [
|
||||
labels?.tag || "Tag",
|
||||
...TAG_FILTER_CONNECTOR_ORDER.map((connector) =>
|
||||
getConnectorLabel(connector, labels),
|
||||
),
|
||||
];
|
||||
|
||||
headers.forEach((h, index) => {
|
||||
const th = document.createElement("th");
|
||||
if (index > 0) {
|
||||
th.className = "text-center";
|
||||
}
|
||||
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = h;
|
||||
th.appendChild(strong);
|
||||
headRow.appendChild(th);
|
||||
});
|
||||
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
// BODY
|
||||
const tbody = document.createElement("tbody");
|
||||
options.forEach((option) => {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
// LABEL
|
||||
const labelTd = document.createElement("td");
|
||||
const label = document.createElement("span");
|
||||
label.className = "tag " + option.style;
|
||||
label.innerText = option.label;
|
||||
|
||||
if (option.automatisiert) {
|
||||
label.innerHTML = "<i class='fa-solid fa-lock'></i> " + option.label;
|
||||
label.className += " tag_auto";
|
||||
}
|
||||
|
||||
labelTd.appendChild(label);
|
||||
row.appendChild(labelTd);
|
||||
|
||||
function makeCheckbox(connector) {
|
||||
const td = document.createElement("td");
|
||||
const cb = document.createElement("input");
|
||||
|
||||
td.className = "text-center align-middle";
|
||||
|
||||
cb.type = "checkbox";
|
||||
cb.style.cursor = "pointer";
|
||||
|
||||
cb.checked = selectedTagOptions.some(
|
||||
(t) => t.value === option.value && t.connector === connector.key,
|
||||
);
|
||||
|
||||
cb.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.target.checked) {
|
||||
selectedTagOptions.push({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
connector: connector.key,
|
||||
});
|
||||
} else {
|
||||
const idx = selectedTagOptions.findIndex(
|
||||
(t) => t.value === option.value && t.connector === connector.key,
|
||||
);
|
||||
|
||||
if (idx !== -1) {
|
||||
selectedTagOptions.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
td.appendChild(cb);
|
||||
return td;
|
||||
}
|
||||
|
||||
TAG_FILTER_CONNECTOR_ORDER.forEach((connector) => {
|
||||
row.appendChild(makeCheckbox(connector));
|
||||
});
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
function parseFilterExpression(expression) {
|
||||
const collections = [];
|
||||
|
||||
try {
|
||||
const orParts = expression.split("||").map((part) => part.trim());
|
||||
|
||||
orParts.forEach((part) => {
|
||||
const andParts = part.split("&&").map((p) => p.trim());
|
||||
|
||||
const collection = { positives: [], negatives: [] };
|
||||
|
||||
andParts.forEach((term) => {
|
||||
const comparisonMatch = term.match(
|
||||
/^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/,
|
||||
);
|
||||
|
||||
if (comparisonMatch) {
|
||||
const operator = comparisonMatch[1];
|
||||
const numberStr = comparisonMatch[2].replace(",", ".");
|
||||
const number = parseFloat(numberStr);
|
||||
collection.positives.push({ type: "comparison", operator, number });
|
||||
} else if (term.startsWith("!")) {
|
||||
const excludeTerm = term.substring(1).trim().replace(/\*/g, ".*");
|
||||
collection.negatives.push({
|
||||
type: "regex",
|
||||
regex: new RegExp(excludeTerm, "i"),
|
||||
});
|
||||
} else {
|
||||
const includeTerm = term.replace(/\*/g, ".*");
|
||||
collection.positives.push({
|
||||
type: "regex",
|
||||
regex: new RegExp(includeTerm, "i"),
|
||||
});
|
||||
}
|
||||
});
|
||||
collections.push(collection);
|
||||
});
|
||||
} catch (e) {}
|
||||
return collections;
|
||||
}
|
||||
|
||||
export function extendedHeaderFilter(
|
||||
headerValue,
|
||||
rowValue,
|
||||
rowData,
|
||||
filterParams,
|
||||
) {
|
||||
const fields = Array.isArray(filterParams?.field)
|
||||
? filterParams.field
|
||||
: [filterParams?.field];
|
||||
|
||||
if (fields.length > 1 && rowData) {
|
||||
rowValue = fields
|
||||
.map((f) => rowData[f] ?? "")
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
if (typeof headerValue === "boolean") {
|
||||
return rowValue === headerValue;
|
||||
}
|
||||
|
||||
const collections = parseFilterExpression(headerValue);
|
||||
|
||||
function matchValue(value) {
|
||||
try {
|
||||
return collections.some((collection) => {
|
||||
let positives =
|
||||
collection.positives.length === 0 ||
|
||||
collection.positives.every((condition) => {
|
||||
if (condition.type === "comparison") {
|
||||
let value = parseFloat(rowValue);
|
||||
if (isNaN(value)) return false;
|
||||
|
||||
switch (condition.operator) {
|
||||
case "<":
|
||||
return value < condition.number;
|
||||
case ">":
|
||||
return value > condition.number;
|
||||
case "<=":
|
||||
return value <= condition.number;
|
||||
case ">=":
|
||||
return value >= condition.number;
|
||||
case "=":
|
||||
return value === condition.number;
|
||||
case "!=":
|
||||
return value !== condition.number;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else if (condition.type === "regex") {
|
||||
return condition.regex.test(rowValue);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
let negatives = collection.negatives.every((condition) => {
|
||||
return !condition.regex.test(rowValue);
|
||||
});
|
||||
|
||||
return positives && negatives;
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (matchValue(rowValue)) return true;
|
||||
|
||||
if (rowData && filterParams) {
|
||||
const childrenField = filterParams?.children || "_children";
|
||||
const field = filterParams?.field;
|
||||
|
||||
const children = rowData[childrenField];
|
||||
if (Array.isArray(children)) {
|
||||
for (let child of children) {
|
||||
let childValue = child[field];
|
||||
if (
|
||||
extendedHeaderFilter(headerValue, childValue, child, filterParams)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = typeof rowValue === "string" ? JSON.parse(rowValue) : rowValue;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let combinedText;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
combinedText = data
|
||||
.filter((item) => item?.done !== true)
|
||||
.map((item) => `${item?.beschreibung} ${item?.notiz}`)
|
||||
.join(" ");
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
combinedText =
|
||||
data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : "";
|
||||
} else {
|
||||
combinedText = String(data);
|
||||
}
|
||||
|
||||
return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams);
|
||||
}
|
||||
|
||||
@@ -1,90 +1,128 @@
|
||||
export function tagFormatter(cell, tagComponent)
|
||||
{
|
||||
const mappedData = tagComponent.tags.map(tag => ({
|
||||
typ_kurzbz: tag.tag_typ_kurzbz,
|
||||
automatisiert: tag.automatisiert
|
||||
}));
|
||||
export function tagFormatter(cell, tagComponent, onRendered, tagKey = "tags") {
|
||||
const mappedData = tagComponent[tagKey]?.map((tag) => ({
|
||||
typ_kurzbz: tag.tag_typ_kurzbz,
|
||||
automatisiert: tag.automatisiert,
|
||||
}));
|
||||
|
||||
let tags = cell.getValue();
|
||||
if (!tags) return;
|
||||
|
||||
let tags = cell.getValue();
|
||||
if (!tags) return;
|
||||
let container = document.createElement("div");
|
||||
container.className = "d-flex gap-1";
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.className = "d-flex gap-1";
|
||||
let parsedTags = [];
|
||||
if (typeof tags === "string") {
|
||||
parsedTags = JSON.parse(tags);
|
||||
} else if (Array.isArray(tags)) {
|
||||
parsedTags = tags;
|
||||
}
|
||||
|
||||
let parsedTags = [];
|
||||
if (typeof tags === 'string') {
|
||||
parsedTags = JSON.parse(tags);
|
||||
} else if (Array.isArray(tags)) {
|
||||
parsedTags = tags;
|
||||
}
|
||||
let maxVisibleTags = 3;
|
||||
|
||||
let maxVisibleTags = 2;
|
||||
const rowData = cell.getRow().getData();
|
||||
if (rowData._tagExpanded === undefined) {
|
||||
rowData._tagExpanded = false;
|
||||
}
|
||||
|
||||
const rowData = cell.getRow().getData();
|
||||
if (rowData._tagExpanded === undefined) {
|
||||
rowData._tagExpanded = false;
|
||||
}
|
||||
const renderTags = () => {
|
||||
container.innerHTML = "";
|
||||
parsedTags = parsedTags.filter((item) => item !== null);
|
||||
|
||||
const renderTags = () => {
|
||||
container.innerHTML = '';
|
||||
parsedTags = parsedTags.filter(item => item !== null);
|
||||
parsedTags.sort((a, b) => {
|
||||
let adone = a.done ? 1 : 0;
|
||||
let bbone = b.done ? 1 : 0;
|
||||
|
||||
parsedTags.sort((a, b) => {
|
||||
let adone = a.done ? 1 : 0;
|
||||
let bbone = b.done ? 1 : 0;
|
||||
if (adone !== bbone) {
|
||||
return adone - bbone;
|
||||
}
|
||||
return a.prioritaet - b.prioritaet;
|
||||
});
|
||||
const tagsToShow = rowData._tagExpanded
|
||||
? parsedTags
|
||||
: parsedTags.slice(0, maxVisibleTags);
|
||||
|
||||
if (adone !== bbone)
|
||||
{
|
||||
return adone - bbone;
|
||||
}
|
||||
return b.id - a.id;
|
||||
});
|
||||
const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags);
|
||||
tagsToShow.forEach((tag) => {
|
||||
if (!tag) return;
|
||||
let tagElement = document.createElement("span");
|
||||
tagElement.innerText = tag.beschreibung;
|
||||
tagElement.title = tag.notiz;
|
||||
tagElement.className = "tag " + tag.style;
|
||||
if (tag.done) tagElement.className += " tag_done";
|
||||
|
||||
tagsToShow.forEach(tag => {
|
||||
if (!tag) return;
|
||||
let tagElement = document.createElement('span');
|
||||
tagElement.innerText = tag.beschreibung;
|
||||
tagElement.title = tag.notiz;
|
||||
tagElement.className = "tag " + tag.style;
|
||||
if (tag.done) tagElement.className += " tag_done";
|
||||
if (tag.automatisiert){
|
||||
tagElement.className += " tag_auto";
|
||||
tagElement.innerHTML = "<i class='fa-solid fa-lock'></i> " + tag.beschreibung;
|
||||
}
|
||||
const tagDef = mappedData?.find((t) => t.typ_kurzbz === tag.typ_kurzbz);
|
||||
|
||||
const tagDef = mappedData.find(t => t.typ_kurzbz === tag.typ_kurzbz);
|
||||
if (
|
||||
(!tagDef && tag.typ_kurzbz?.includes("_auto")) ||
|
||||
tagDef?.automatisiert
|
||||
) {
|
||||
tagElement.className += " tag_auto";
|
||||
tagElement.innerHTML =
|
||||
"<i class='fa-solid fa-lock'></i> " + tag.beschreibung;
|
||||
}
|
||||
|
||||
if (!tagDef && tag.typ_kurzbz?.includes("_auto") || tagDef?.automatisiert) {
|
||||
tagElement.className += " tag_auto";
|
||||
tagElement.innerHTML = "<i class='fa-solid fa-lock'></i> " + tag.beschreibung;
|
||||
}
|
||||
tagElement.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
tagComponent.editTag(tag.id);
|
||||
});
|
||||
|
||||
tagElement.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
tagComponent.editTag(tag.id)
|
||||
});
|
||||
container.appendChild(tagElement);
|
||||
});
|
||||
|
||||
container.appendChild(tagElement);
|
||||
});
|
||||
if (parsedTags.length > maxVisibleTags) {
|
||||
let toggle = document.createElement("button");
|
||||
toggle.innerText =
|
||||
(rowData._tagExpanded ? "- " : "+ ") +
|
||||
(parsedTags.length - maxVisibleTags);
|
||||
toggle.className = "display_all";
|
||||
toggle.title = rowData._tagExpanded
|
||||
? "Tags ausblenden"
|
||||
: "Tags einblenden";
|
||||
|
||||
if (parsedTags.length > maxVisibleTags) {
|
||||
let toggle = document.createElement('button');
|
||||
toggle.innerText = (rowData._tagExpanded ? '- ' : '+ ') + (parsedTags.length - maxVisibleTags);
|
||||
toggle.className = "display_all";
|
||||
toggle.title = rowData._tagExpanded ? "Tags ausblenden" : "Tags einblenden";
|
||||
toggle.addEventListener("click", () => {
|
||||
rowData._tagExpanded = !rowData._tagExpanded;
|
||||
renderTags();
|
||||
});
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
rowData._tagExpanded = !rowData._tagExpanded;
|
||||
renderTags();
|
||||
});
|
||||
container.appendChild(toggle);
|
||||
}
|
||||
};
|
||||
|
||||
container.appendChild(toggle);
|
||||
}
|
||||
};
|
||||
const fitTags = () => {
|
||||
if (rowData._tagExpanded) {
|
||||
renderTags();
|
||||
return;
|
||||
}
|
||||
|
||||
renderTags();
|
||||
return container;
|
||||
}
|
||||
let widthBuffer = 10;
|
||||
maxVisibleTags = parsedTags.length;
|
||||
renderTags();
|
||||
|
||||
while (
|
||||
maxVisibleTags > 0 &&
|
||||
container.scrollWidth > (container.clientWidth + widthBuffer)
|
||||
) {
|
||||
maxVisibleTags--;
|
||||
renderTags();
|
||||
}
|
||||
};
|
||||
|
||||
let animationFrame = null;
|
||||
container.fitTags = () => {
|
||||
if (animationFrame !== null) return;
|
||||
|
||||
animationFrame = requestAnimationFrame(() => {
|
||||
animationFrame = null;
|
||||
fitTags();
|
||||
});
|
||||
};
|
||||
|
||||
if (onRendered) {
|
||||
onRendered(() => {
|
||||
container.fitTags();
|
||||
});
|
||||
}
|
||||
|
||||
renderTags();
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -61539,6 +61539,66 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'andCondition',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'und',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'and',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'orCondition',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'oder',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'or',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'notCondition',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'nicht',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'not',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/cypress/videos
|
||||
/cypress/screenshots
|
||||
/results
|
||||
Reference in New Issue
Block a user