Refactor calendar rendering with RenderBuilder

Simplifies calendar rendering process by introducing a new RenderBuilder pattern

Improves flexibility and modularity of grouping renderer chain
Decouples rendering logic from specific grouping types
Enables dynamic column span calculation and recursive rendering

Reduces complexity in CalendarOrchestrator
Enhances type safety and simplifies renderer interfaces
This commit is contained in:
Janus C. H. Knudsen 2025-12-09 00:51:41 +01:00
parent 27561750f8
commit a3a1b9a421
9 changed files with 230 additions and 155 deletions

View file

@ -1,23 +1,10 @@
import { from } from 'ts-linq-light';
import { ViewConfig, GroupingConfig } from './ViewConfig';
import { RenderContext } from './RenderContext';
import { IGroupingRenderer } from './IGroupingRenderer';
import { ViewConfig } from './ViewConfig';
import { IGroupingRenderer, RenderContext } from './IGroupingRenderer';
import { IGroupingStore } from './IGroupingStore';
import { RenderBuilder, RenderData } from './RenderBuilder';
import { EventRenderer } from '../features/event/EventRenderer';
interface HierarchyNode {
type: string;
id: string;
data: unknown;
parentId?: string;
children: HierarchyNode[];
}
interface GroupingData {
items: { id: string; data: unknown }[];
byParent: Map<string, { id: string; data: unknown }[]> | null;
}
export class CalendarOrchestrator {
constructor(
private renderers: IGroupingRenderer[],
@ -40,115 +27,60 @@ export class CalendarOrchestrator {
throw new Error('Missing swp-calendar-header or swp-day-columns');
}
const groupingData = this.fetchAllData(viewConfig.groupings);
const hierarchy = this.buildHierarchy(viewConfig.groupings, groupingData);
const totalColumns = this.countLeaves(hierarchy);
container.style.setProperty('--grid-columns', String(totalColumns));
const types = from(viewConfig.groupings).select(g => g.type).toArray();
headerContainer.dataset.levels = types.join(' ');
const context: RenderContext = { headerContainer, columnContainer };
// Clear containers
headerContainer.innerHTML = '';
columnContainer.innerHTML = '';
this.renderHierarchy(hierarchy, headerContainer, columnContainer);
// Set header levels
const types = from(viewConfig.groupings).select(g => g.type).toArray();
headerContainer.dataset.levels = types.join(' ');
// Render events from IndexedDB
// Hent alt data
const data: RenderData = {
teams: this.getItems('team', viewConfig),
resources: this.getItems('resource', viewConfig),
dates: this.getItems('date', viewConfig)
};
// Byg renderer chain
const builder = new RenderBuilder(context, data);
for (const grouping of viewConfig.groupings) {
const renderer = this.getRenderer(grouping.type);
if (renderer) {
const items = this.getItems(grouping.type, viewConfig);
builder.add(renderer, items);
}
}
// Beregn total columns og render
const totalColumns = builder.getTotalCount();
container.style.setProperty('--grid-columns', String(totalColumns));
builder.build();
// Render events
const visibleDates = this.extractVisibleDates(viewConfig);
await this.eventRenderer.render(container, visibleDates);
}
private getItems(type: string, viewConfig: ViewConfig): ReturnType<typeof from> {
const grouping = from(viewConfig.groupings).firstOrDefault(g => g.type === type);
if (!grouping) return from([]);
if (type === 'date') {
return from(grouping.values);
}
const store = this.getStore(type);
if (!store) return from([]);
return from(store.getByIds(grouping.values));
}
private extractVisibleDates(viewConfig: ViewConfig): string[] {
return from(viewConfig.groupings).firstOrDefault(g => g.type === 'date')?.values || [];
}
private fetchAllData(groupings: GroupingConfig[]): Map<string, GroupingData> {
const result = new Map<string, GroupingData>();
for (const g of groupings) {
if (g.type === 'date') {
result.set(g.type, {
items: from(g.values).select(v => ({ id: v, data: { id: v } })).toArray(),
byParent: null
});
continue;
}
const store = this.getStore(g.type);
if (!store) continue;
const rawItems = store.getByIds(g.values);
const items = from(rawItems).select((item: any) => ({ id: item.id, data: item })).toArray();
let byParent: Map<string, { id: string; data: unknown }[]> | null = null;
if (g.parentKey) {
byParent = new Map();
const grouped = from(items)
.where(item => (item.data as any)[g.parentKey!] != null)
.groupBy(item => (item.data as any)[g.parentKey!] as string);
for (const group of grouped) {
byParent.set(group.key, group.toArray());
}
}
result.set(g.type, { items, byParent });
}
return result;
}
private buildHierarchy(
groupings: GroupingConfig[],
data: Map<string, GroupingData>,
level = 0,
parentId?: string
): HierarchyNode[] {
if (level >= groupings.length) return [];
const g = groupings[level];
const gData = data.get(g.type);
if (!gData) return [];
const items = parentId && gData.byParent
? gData.byParent.get(parentId) ?? []
: gData.items;
return from(items).select(item => ({
type: g.type,
id: item.id,
data: item.data,
parentId,
children: this.buildHierarchy(groupings, data, level + 1, item.id)
})).toArray();
}
private countLeaves(nodes: HierarchyNode[]): number {
return nodes.reduce((sum, n) =>
sum + (n.children.length ? this.countLeaves(n.children) : 1), 0);
}
private renderHierarchy(
nodes: HierarchyNode[],
headerContainer: HTMLElement,
columnContainer: HTMLElement,
headerRow = 1
): void {
for (const node of nodes) {
const renderer = this.getRenderer(node.type);
const colspan = this.countLeaves([node]) || 1;
renderer?.render({
headerContainer,
columnContainer,
values: [node.id],
headerRow,
columnIndex: 0, // Not used - grid auto-places
colspan,
parentId: node.parentId
} as RenderContext);
if (node.children.length) {
this.renderHierarchy(node.children, headerContainer, columnContainer, headerRow + 1);
}
}
}
}