feat(tags): is_parent-Flag zur Unterscheidung von Gruppierungs-Tags und zuordenbaren Tags
Parent-Tags (is_parent=true) dienen als Gruppierungsknoten und können nicht Gesprächsnotizen zugeordnet werden. Echte Tags (is_parent=false) sind zuordenbar. Die Bearbeitungsmaske bietet eine Checkbox für das Flag, die Eltern-Auswahl ist auf Parent-Tags beschränkt. Die Tag-Liste zeigt Parent-Tags mit Folder-Icon, echte Tags mit Label-Icon. Die Suche filtert den gesamten Teilbaum — nur Knoten mit Self-Match oder einem matchenden Nachfahren werden angezeigt. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
3361ef16dd
commit
b44ea806ef
1 file changed
+142
-25
+142
-25
@@ -22,6 +22,7 @@ export interface iTag {
|
||||
description: string; // Beschreibung
|
||||
color: string; // Farbcode (z. B. "#ff0000")
|
||||
parentId: number | null; // Referenz auf den Eltern-Tag (null = Wurzelebene)
|
||||
is_parent: boolean; // true = Gruppierungs-Knoten (nicht zuordenbar), false = echter Tag (zuordenbar)
|
||||
icon?: string; // Optional: Icon-Name oder URL
|
||||
order?: number; // Optional: Sortierreihenfolge innerhalb der Ebene
|
||||
}
|
||||
@@ -49,6 +50,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Hochpriorisierte Aufgaben",
|
||||
color: "#ff0000",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
icon: "priority_high",
|
||||
order: 1,
|
||||
},
|
||||
@@ -58,6 +60,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Kundenbezogene Tags",
|
||||
color: "#00ff00",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
icon: "person",
|
||||
order: 2,
|
||||
},
|
||||
@@ -67,6 +70,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Projektbezogene Tags",
|
||||
color: "#0000ff",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
icon: "folder",
|
||||
order: 3,
|
||||
},
|
||||
@@ -76,6 +80,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Nachverfolgung erforderlich",
|
||||
color: "#ffff00",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
icon: "schedule",
|
||||
order: 4,
|
||||
},
|
||||
@@ -85,6 +90,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Archivierte Einträge",
|
||||
color: "#808080",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
icon: "archive",
|
||||
order: 5,
|
||||
},
|
||||
@@ -96,6 +102,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Server-seitige Technologien und Frameworks",
|
||||
color: "#4285F4",
|
||||
parentId: null,
|
||||
is_parent: true,
|
||||
icon: "server",
|
||||
order: 6,
|
||||
},
|
||||
@@ -105,6 +112,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Client-seitige Technologien und Frameworks",
|
||||
color: "#34A853",
|
||||
parentId: null,
|
||||
is_parent: true,
|
||||
icon: "browser",
|
||||
order: 7,
|
||||
},
|
||||
@@ -114,6 +122,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Infrastruktur, Deployment und CI/CD",
|
||||
color: "#EA4335",
|
||||
parentId: null,
|
||||
is_parent: true,
|
||||
icon: "cog",
|
||||
order: 8,
|
||||
},
|
||||
@@ -123,6 +132,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Datenbanken, Analyse und KI",
|
||||
color: "#FBBC04",
|
||||
parentId: null,
|
||||
is_parent: true,
|
||||
icon: "database",
|
||||
order: 9,
|
||||
},
|
||||
@@ -134,6 +144,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Allgemeine Python-Entwicklung",
|
||||
color: "#3776AB",
|
||||
parentId: 100,
|
||||
is_parent: true,
|
||||
icon: "python",
|
||||
order: 1,
|
||||
},
|
||||
@@ -143,6 +154,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Java und JVM-Ökosystem",
|
||||
color: "#007396",
|
||||
parentId: 100,
|
||||
is_parent: false,
|
||||
icon: "java",
|
||||
order: 2,
|
||||
},
|
||||
@@ -152,6 +164,7 @@ const tagDaten: iTag[] = [
|
||||
description: "JavaScript auf dem Server",
|
||||
color: "#339933",
|
||||
parentId: 100,
|
||||
is_parent: true,
|
||||
icon: "nodejs",
|
||||
order: 3,
|
||||
},
|
||||
@@ -163,6 +176,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Mikro-Webframework für Python",
|
||||
color: "#000000",
|
||||
parentId: 200,
|
||||
is_parent: false,
|
||||
icon: "flask",
|
||||
order: 1,
|
||||
},
|
||||
@@ -172,6 +186,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Full-Stack Webframework für Python",
|
||||
color: "#092E20",
|
||||
parentId: 200,
|
||||
is_parent: false,
|
||||
icon: "django",
|
||||
order: 2,
|
||||
},
|
||||
@@ -181,6 +196,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Moderne API-Entwicklung mit Python",
|
||||
color: "#005577",
|
||||
parentId: 200,
|
||||
is_parent: false,
|
||||
icon: "fastapi",
|
||||
order: 3,
|
||||
},
|
||||
@@ -192,6 +208,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Minimalistisches Webframework für Node.js",
|
||||
color: "#000000",
|
||||
parentId: 202,
|
||||
is_parent: false,
|
||||
icon: "express",
|
||||
order: 1,
|
||||
},
|
||||
@@ -201,6 +218,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Progressives Node.js-Framework für Enterprise-Anwendungen",
|
||||
color: "#E0234E",
|
||||
parentId: 202,
|
||||
is_parent: false,
|
||||
icon: "nestjs",
|
||||
order: 2,
|
||||
},
|
||||
@@ -212,6 +230,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Backend-Datenbank-Systeme",
|
||||
color: "#336791",
|
||||
parentId: 100,
|
||||
is_parent: true,
|
||||
icon: "database",
|
||||
order: 4,
|
||||
},
|
||||
@@ -221,6 +240,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Relationale Datenbank mit erweiterten Features",
|
||||
color: "#336791",
|
||||
parentId: 210,
|
||||
is_parent: false,
|
||||
icon: "postgres",
|
||||
order: 1,
|
||||
},
|
||||
@@ -230,6 +250,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Dokumentenorientierte NoSQL-Datenbank",
|
||||
color: "#47A248",
|
||||
parentId: 210,
|
||||
is_parent: false,
|
||||
icon: "mongodb",
|
||||
order: 2,
|
||||
},
|
||||
@@ -241,6 +262,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Die Hauptsprache des Web",
|
||||
color: "#F7DF1E",
|
||||
parentId: 101,
|
||||
is_parent: true,
|
||||
icon: "js",
|
||||
order: 1,
|
||||
},
|
||||
@@ -250,6 +272,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Typsicheres JavaScript",
|
||||
color: "#3178C6",
|
||||
parentId: 101,
|
||||
is_parent: false,
|
||||
icon: "ts",
|
||||
order: 2,
|
||||
},
|
||||
@@ -261,6 +284,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Komponentenbasiertes UI-Framework",
|
||||
color: "#61DAFB",
|
||||
parentId: 220,
|
||||
is_parent: false,
|
||||
icon: "react",
|
||||
order: 1,
|
||||
},
|
||||
@@ -270,6 +294,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Progressives JavaScript-Framework",
|
||||
color: "#4FC08D",
|
||||
parentId: 220,
|
||||
is_parent: false,
|
||||
icon: "vue",
|
||||
order: 2,
|
||||
},
|
||||
@@ -279,6 +304,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Full-Stack Framework von Google",
|
||||
color: "#DD1B16",
|
||||
parentId: 220,
|
||||
is_parent: false,
|
||||
icon: "angular",
|
||||
order: 3,
|
||||
},
|
||||
@@ -290,6 +316,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Stildefinition für Webseiten",
|
||||
color: "#1572B6",
|
||||
parentId: 101,
|
||||
is_parent: true,
|
||||
icon: "css",
|
||||
order: 3,
|
||||
},
|
||||
@@ -299,6 +326,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Utility-first CSS-Framework",
|
||||
color: "#06B6D4",
|
||||
parentId: 230,
|
||||
is_parent: false,
|
||||
icon: "tailwind",
|
||||
order: 1,
|
||||
},
|
||||
@@ -310,6 +338,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Containerisierung von Anwendungen",
|
||||
color: "#2496ED",
|
||||
parentId: 102,
|
||||
is_parent: false,
|
||||
icon: "docker",
|
||||
order: 1,
|
||||
},
|
||||
@@ -319,6 +348,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Orchestrierung von Containern",
|
||||
color: "#326CE5",
|
||||
parentId: 102,
|
||||
is_parent: false,
|
||||
icon: "kubernetes",
|
||||
order: 2,
|
||||
},
|
||||
@@ -328,6 +358,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Continuous Integration und Deployment",
|
||||
color: "#FFA500",
|
||||
parentId: 102,
|
||||
is_parent: false,
|
||||
icon: "cicd",
|
||||
order: 3,
|
||||
},
|
||||
@@ -339,6 +370,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Machine Learning und KI-Technologien",
|
||||
color: "#8B00FF",
|
||||
parentId: 103,
|
||||
is_parent: false,
|
||||
icon: "ai",
|
||||
order: 1,
|
||||
},
|
||||
@@ -348,6 +380,7 @@ const tagDaten: iTag[] = [
|
||||
description: "Verarbeitung großer Datenmengen",
|
||||
color: "#FF6B6B",
|
||||
parentId: 103,
|
||||
is_parent: false,
|
||||
icon: "bigdata",
|
||||
order: 2,
|
||||
},
|
||||
@@ -417,6 +450,45 @@ function getDescendants(tagId: number): iTag[] {
|
||||
return descendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle echten (zuordenbaren) Tags, d. h. alle Tags, die keine
|
||||
* Parent-Tags sind (is_parent === false).
|
||||
*/
|
||||
function getRealTags(): iTag[] {
|
||||
return tagDaten.filter((tag) => !tag.is_parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle echten (zuordenbaren) Tags, die Nachfahren eines Parent-Tags
|
||||
* sind (flach, rekursiv). Parent-Tags selbst werden herausgefiltert.
|
||||
*/
|
||||
function getRealDescendants(parentId: number): iTag[] {
|
||||
return getDescendants(parentId).filter((tag) => !tag.is_parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst eine Tag-Auswahl auf echte (zuordenbare) Tag-IDs auf.
|
||||
* Wurde ein Parent-Tag ausgewählt, werden alle echten Nachfahren geliefert.
|
||||
* Wurde ein echter Tag ausgewählt, wird dessen eigene ID geliefert.
|
||||
* Rückgabe der Tag-IDs (für Filter/Auswertungen).
|
||||
*/
|
||||
function resolveToRealTagIds(tagId: number): number[] {
|
||||
const tag = findTagById(tagId);
|
||||
if (!tag) return [];
|
||||
if (tag.is_parent) {
|
||||
return getRealDescendants(tagId).map((t) => t.id);
|
||||
}
|
||||
return [tagId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Tag zuordenbar ist, d. h. ein echter Tag (kein Parent-Tag).
|
||||
*/
|
||||
function isAssignable(tagId: number): boolean {
|
||||
const tag = findTagById(tagId);
|
||||
return tag ? !tag.is_parent : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Vorfahren (Eltern, Großeltern, ...) eines Tags zurück.
|
||||
*/
|
||||
@@ -444,7 +516,7 @@ function searchTagsByName(query: string): iTag[] {
|
||||
/**
|
||||
* Fügt einen neuen Tag hinzu (mit automatischer ID-Vergabe).
|
||||
*/
|
||||
function addTag(name: string, parentId: number | null = null, description: string = "", color: string = "#808080", icon?: string, order?: number): iTag {
|
||||
function addTag(name: string, parentId: number | null = null, description: string = "", color: string = "#808080", icon?: string, order?: number, is_parent: boolean = false): iTag {
|
||||
const newId = tagDaten.length > 0 ? Math.max(...tagDaten.map(t => t.id)) + 1 : 1;
|
||||
const tag: iTag = {
|
||||
id: newId,
|
||||
@@ -452,6 +524,7 @@ function addTag(name: string, parentId: number | null = null, description: strin
|
||||
description,
|
||||
color,
|
||||
parentId,
|
||||
is_parent,
|
||||
icon,
|
||||
order,
|
||||
};
|
||||
@@ -501,29 +574,41 @@ export const Tag: m.ClosureComponent = (vnode) => {
|
||||
description: "",
|
||||
color: "",
|
||||
parentId: null,
|
||||
is_parent: false,
|
||||
};
|
||||
|
||||
const setColor = (color: string) => {
|
||||
currentTag.color = color;
|
||||
};
|
||||
|
||||
// Liste aller Tags für die Eltern-Auswahl
|
||||
const allTags = tagDaten.filter(t => t.id !== currentTag.id); // Ausschließen des aktuellen Tags
|
||||
// Liste aller Parent-Tags für die Eltern-Auswahl (nur Parent-Tags können übergeordnet sein)
|
||||
let allParentTags: iTag[] = [];
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param();
|
||||
if (routeParams) {
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1;
|
||||
if (id !== -1) {
|
||||
// Bestehenden Tag aus den Daten laden (frisch, nicht aus Route-Params)
|
||||
const tag = findTagById(id);
|
||||
if (tag) {
|
||||
currentTag = {...tag};
|
||||
}
|
||||
} else if (routeParams) {
|
||||
// Neuer Tag: vorbelegte Werte aus Route-Params übernehmen
|
||||
currentTag = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
id: -1,
|
||||
name: routeParams.name || "",
|
||||
description: routeParams.description || "",
|
||||
color: routeParams.color || "",
|
||||
parentId: routeParams.parentId ? parseInt(routeParams.parentId) : null,
|
||||
is_parent: routeParams.is_parent ? routeParams.is_parent === "true" : false,
|
||||
icon: routeParams.icon || undefined,
|
||||
order: routeParams.order ? parseInt(routeParams.order) : undefined,
|
||||
};
|
||||
}
|
||||
// Parent-Tag-Liste erst hier berechnen, da currentTag.id jetzt bekannt ist
|
||||
allParentTags = tagDaten.filter(t => t.id !== currentTag.id && t.is_parent);
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-auto.form-view-container",
|
||||
@@ -533,14 +618,29 @@ export const Tag: m.ClosureComponent = (vnode) => {
|
||||
m("label", "Tag-Name", m(Input, {name: "name", value: currentTag.name, type: "text"} as iInput)),
|
||||
m("label", "Beschreibung", m(Input, {name: "description", value: currentTag.description, type: "text"} as iInput)),
|
||||
|
||||
// Parent-Tag-Umschalter: true = Gruppierungs-Knoten (nicht zuordenbar)
|
||||
m("div", {style: {'margin-bottom': '5px'}},
|
||||
m("label",
|
||||
m("input.w3-check", {
|
||||
type: "checkbox",
|
||||
name: "is_parent",
|
||||
checked: currentTag.is_parent,
|
||||
style: {'margin-right': '8px'},
|
||||
onclick: (e: Event) => {
|
||||
currentTag.is_parent = (e.target as HTMLInputElement).checked;
|
||||
}
|
||||
}),
|
||||
"Nur zur Gruppierung, kann nicht zugeordnet werden"
|
||||
)
|
||||
),
|
||||
|
||||
// Eltern-Tag-Auswahl
|
||||
m("label", "Übergeordneter Tag (optional)",
|
||||
m("label", "Übergeordneter Tag (optional)"),
|
||||
m("select.w3-select", {name: "parentId", value: currentTag.parentId || ""},
|
||||
m("option", {value: ""}, "-- Kein übergeordneter Tag --"),
|
||||
allTags.map(tag =>
|
||||
allParentTags.map(tag =>
|
||||
m("option", {value: tag.id, selected: currentTag.parentId === tag.id}, tag.name)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
m("label", "Farbe"),
|
||||
@@ -559,9 +659,6 @@ export const Tag: m.ClosureComponent = (vnode) => {
|
||||
})
|
||||
)
|
||||
),
|
||||
m(Input, {name: "color", value: currentTag.color, type: "text"} as iInput),
|
||||
m("label", "Icon (optional)", m(Input, {name: "icon", value: currentTag.icon || "", type: "text"} as iInput)),
|
||||
m("label", "Sortierreihenfolge (optional)", m(Input, {name: "order", value: currentTag.order?.toString() || "", type: "number"} as iInput)),
|
||||
])
|
||||
)
|
||||
);
|
||||
@@ -585,22 +682,26 @@ export const TagList: m.ClosureComponent = (vnode) => {
|
||||
|
||||
const editTag = (tag: iTag, e: Event) => {
|
||||
e.stopPropagation();
|
||||
m.route.set("/tag", {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
description: tag.description,
|
||||
color: tag.color,
|
||||
parentId: tag.parentId,
|
||||
icon: tag.icon,
|
||||
order: tag.order
|
||||
});
|
||||
m.route.set("/tag", {id: tag.id});
|
||||
};
|
||||
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let tag: iTag = node.nodevalue;
|
||||
|
||||
// Parent-Tags erhalten ein "folder"-Icon, echte Tags ein "label"-Icon
|
||||
const tagIcon = tag.is_parent ? "folder" : "label";
|
||||
const tagTitle = tag.is_parent ? "Parent-Tag (nicht zuordenbar)" : "";
|
||||
|
||||
return [
|
||||
m("span",
|
||||
m("i.material-icons", {
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '6px',
|
||||
fontSize: '18px'
|
||||
},
|
||||
title: tagTitle
|
||||
}, tagIcon),
|
||||
m("span", {
|
||||
style: {
|
||||
backgroundColor: tag.color,
|
||||
@@ -610,7 +711,8 @@ export const TagList: m.ClosureComponent = (vnode) => {
|
||||
fontWeight: 'bold',
|
||||
display: 'inline-block',
|
||||
marginRight: '8px'
|
||||
}
|
||||
},
|
||||
title: tagTitle
|
||||
}, tag.name)
|
||||
),
|
||||
m("span", tag.description),
|
||||
@@ -625,10 +727,21 @@ export const TagList: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let tag: iTag = node.nodevalue;
|
||||
let searchLower = searchvalue.toLowerCase();
|
||||
return tag.name.toLowerCase().includes(searchLower) ||
|
||||
tag.description.toLowerCase().includes(searchLower) ||
|
||||
tag.color.toLowerCase().includes(searchLower) ||
|
||||
(tag.icon && tag.icon.toLowerCase().includes(searchLower));
|
||||
|
||||
// Prüft, ob der Tag selbst auf den Suchbegriff passt (nur Name und Beschreibung)
|
||||
const selfMatch = tag.name.toLowerCase().includes(searchLower) ||
|
||||
tag.description.toLowerCase().includes(searchLower);
|
||||
|
||||
// Wenn der Tag selbst passt, anzeigen
|
||||
if (selfMatch) return true;
|
||||
|
||||
// Wenn der Tag Kinder hat, prüfen, ob ein Nachfahne passt
|
||||
if (searchLower === "") return true;
|
||||
const descendants = getDescendants(tag.id);
|
||||
return descendants.some((d) =>
|
||||
d.name.toLowerCase().includes(searchLower) ||
|
||||
d.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -675,4 +788,8 @@ export {
|
||||
addTag,
|
||||
updateTag,
|
||||
getRootTags,
|
||||
getRealTags,
|
||||
getRealDescendants,
|
||||
resolveToRealTagIds,
|
||||
isAssignable,
|
||||
};
|
||||
Reference in new issue
Block a user