refactor(projects-detail/projektbaum): reuse the /projects tree component
m's 2026-05-08 21:28: "The Projektbaum inside a Project in the tab
with the Unterordner should just be the same as the Tree in Projects.
It has symbols, everything. That should be a shared component."
Drop the inline mini-tree renderer (renderTreeNode / loadProjectTree /
~50 lines of duplicate logic) in client/projects-detail.ts and mount
the existing client/project-tree.ts module into the tab's container.
The shared component carries:
- per-type icons (Mandant / Litigation / Patent / Case)
- pin star (touch-friendly)
- overdue / open-deadline badges with subtree counts
- status chip + type chip
- expand / collapse toggles
- inherited-visibility marking
- search highlighting (no-op when no search params are passed)
Current project highlight: set aria-current="true" on the matching
.projekt-tree-node after mount. The shared CSS already styles
.projekt-tree-node[aria-current="true"] > .projekt-tree-row with the
lime accent (global.css :5853).
Removed the now-dead mini-tree CSS block that was also accidentally
overriding .projekt-tree-title from the real tree (later-defined rule
won the cascade and erased the shared title weight).
loadChildren() still fetches /api/projects/<id>/children for the
empty-state gate ("Keine untergeordneten Projekte" when this node has
no direct children) and the create-link parent_id pre-fill — both
predicates depend on direct children, not the visible tree.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { initI18n, onLangChange, t, tDyn, getLang, translateEvent } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
import { initNotes } from "./notes";
|
||||
import { initProjectTree, refreshProjectTree, rerenderProjectTree } from "./project-tree";
|
||||
import {
|
||||
loadParentCandidates,
|
||||
initParentPicker,
|
||||
@@ -206,8 +207,9 @@ let deadlines: Deadline[] = [];
|
||||
let appointments: Appointment[] = [];
|
||||
let ancestors: ProjectMini[] = [];
|
||||
let children: ProjectMini[] = [];
|
||||
type TreeNode = ProjectMini & { children?: TreeNode[] };
|
||||
let projectTree: TreeNode[] = [];
|
||||
// projects-cards' /projects tree owns rendering inside the Projektbaum
|
||||
// tab too — see initProjectTreeTab below. Local children[] still feeds
|
||||
// the empty-state gate + the create-link parent_id pre-fill.
|
||||
let teamMembers: ProjectTeamMember[] = [];
|
||||
// t-paliad-139 — additional Team-tab sections.
|
||||
let descendantStaffed: ProjectTeamMember[] = [];
|
||||
@@ -1553,15 +1555,15 @@ async function loadChildren(id: string) {
|
||||
} catch {
|
||||
children = [];
|
||||
}
|
||||
// Full visible tree drives the rendering.
|
||||
try {
|
||||
const resp = await fetch(`/api/projects/tree?subtree_counts=false`);
|
||||
if (resp.ok) projectTree = ((await resp.json()) as TreeNode[]) ?? [];
|
||||
} catch {
|
||||
projectTree = [];
|
||||
}
|
||||
}
|
||||
|
||||
// renderChildren is the Projektbaum tab's mount point. m's 2026-05-08
|
||||
// 21:28: "should just be the same as the Tree in Projects. It has
|
||||
// symbols, everything." Reuse the /projects tree component
|
||||
// (project-tree.ts) verbatim — type icons, pin stars, deadline badges,
|
||||
// expand/collapse, search highlighting all come along for free. The
|
||||
// current project is highlighted via a CSS modifier we add to its
|
||||
// data-id row after the tree mounts.
|
||||
function renderChildren() {
|
||||
const root = document.getElementById("project-tree")!;
|
||||
const empty = document.getElementById("children-empty")!;
|
||||
@@ -1569,30 +1571,22 @@ function renderChildren() {
|
||||
// CTA next to the empty message is "create sub-project", which would be
|
||||
// misleading if the tree itself has other branches.
|
||||
empty.style.display = children.length ? "none" : "";
|
||||
if (!projectTree.length) {
|
||||
root.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const currentId = project?.id ?? "";
|
||||
root.innerHTML = `<ul class="projekt-tree-list">${projectTree.map((n) => renderTreeNode(n, currentId, 0)).join("")}</ul>`;
|
||||
}
|
||||
|
||||
function renderTreeNode(node: TreeNode, currentId: string, depth: number): string {
|
||||
const isCurrent = node.id === currentId;
|
||||
const kids = node.children ?? [];
|
||||
const childHTML = kids.length
|
||||
? `<ul class="projekt-tree-list">${kids.map((k) => renderTreeNode(k, currentId, depth + 1)).join("")}</ul>`
|
||||
: "";
|
||||
const itemClass = `projekt-tree-item${isCurrent ? " projekt-tree-item--current" : ""}`;
|
||||
const linkInner =
|
||||
`<span class="entity-type-chip entity-type-${esc(node.type)}">${esc(tDyn("projects.type." + node.type) || node.type)}</span>` +
|
||||
`<span class="projekt-tree-title">${esc(node.title)}</span>` +
|
||||
(node.reference ? `<span class="projekt-tree-ref">${esc(node.reference)}</span>` : "") +
|
||||
`<span class="entity-status-chip entity-status-${esc(node.status)}">${esc(tDyn("projects.filter.status." + node.status) || node.status)}</span>`;
|
||||
const row = isCurrent
|
||||
? `<span class="projekt-tree-link projekt-tree-link--current" aria-current="true">${linkInner}</span>`
|
||||
: `<a href="/projects/${esc(node.id)}" class="projekt-tree-link">${linkInner}</a>`;
|
||||
return `<li class="${itemClass}">${row}${childHTML}</li>`;
|
||||
// Mount the shared tree. initProjectTree fetches /api/projects/tree on
|
||||
// first call and caches; subsequent tab-switches re-render from cache.
|
||||
// Set aria-current on the row matching this project — the shared tree
|
||||
// already styles aria-current=true with a lime highlight (global.css
|
||||
// .projekt-tree-node[aria-current="true"] > .projekt-tree-row).
|
||||
void initProjectTree(root).then(() => {
|
||||
const currentId = project?.id ?? "";
|
||||
if (!currentId) return;
|
||||
root.querySelectorAll<HTMLLIElement>(".projekt-tree-node").forEach((li) => {
|
||||
if (li.dataset.id === currentId) {
|
||||
li.setAttribute("aria-current", "true");
|
||||
} else {
|
||||
li.removeAttribute("aria-current");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initChildAddLink() {
|
||||
|
||||
@@ -9748,63 +9748,13 @@ dialog.quick-add-sheet::backdrop {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* Projektbaum / Project Tree — full visible hierarchy on /projects/<id>. */
|
||||
.projekt-tree {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.projekt-tree-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.projekt-tree-list .projekt-tree-list {
|
||||
padding-left: 1.5rem;
|
||||
border-left: 1px dashed var(--color-border-subtle, #e4e4e7);
|
||||
margin-left: 0.6rem;
|
||||
}
|
||||
|
||||
.projekt-tree-item {
|
||||
margin: 0.15rem 0;
|
||||
}
|
||||
|
||||
.projekt-tree-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text, var(--hlc-midnight));
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.projekt-tree-link:hover,
|
||||
.projekt-tree-link:focus-visible {
|
||||
background: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
.projekt-tree-link--current {
|
||||
background: var(--color-cta-lime-soft, #f0fad9);
|
||||
border: 1px solid var(--color-cta-lime, #c6f41c);
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.projekt-tree-link--current:hover {
|
||||
background: var(--color-cta-lime-soft, #f0fad9);
|
||||
}
|
||||
|
||||
.projekt-tree-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.projekt-tree-ref {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.85em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
/* Projektbaum tab inside /projects/<id> reuses the /projects tree
|
||||
component (client/project-tree.ts) — see initProjectTree(root) in
|
||||
client/projects-detail.ts. The earlier inline mini-tree CSS at this
|
||||
block was dead code that also accidentally overrode .projekt-tree-title,
|
||||
so it was removed when m's "should be the same as the Tree in
|
||||
Projects" landed (2026-05-08 21:28). The current-node highlight
|
||||
already lives at .projekt-tree-node[aria-current="true"] above. */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.projekt-breadcrumb {
|
||||
|
||||
Reference in New Issue
Block a user