Refactors renderer interfaces and implementations

Converts renderer interfaces to use 'I' prefix for better type clarity
Adds async support for rendering pipeline
Updates resource rendering to use ResourceService
Removes hardcoded resource data

Improves type safety and flexibility of rendering system
This commit is contained in:
Janus C. H. Knudsen 2025-12-09 23:16:13 +01:00
parent 7f6279a6f3
commit 400de8c9d5
10 changed files with 46 additions and 47 deletions

View file

@ -1,5 +1,5 @@
import { Container } from '@novadi/core';
import { Renderer } from './core/IGroupingRenderer';
import { IRenderer } from './core/IGroupingRenderer';
import { IGroupingStore } from './core/IGroupingStore';
import { DateRenderer } from './features/date/DateRenderer';
import { DateService } from './core/DateService';
@ -111,9 +111,9 @@ export function createV2Container(): Container {
builder.registerType(EventRenderer).as<EventRenderer>();
// Renderers - registreres som Renderer (array injection til CalendarOrchestrator)
builder.registerType(DateRenderer).as<Renderer>();
builder.registerType(ResourceRenderer).as<Renderer>();
builder.registerType(TeamRenderer).as<Renderer>();
builder.registerType(DateRenderer).as<IRenderer>();
builder.registerType(ResourceRenderer).as<IRenderer>();
builder.registerType(TeamRenderer).as<IRenderer>();
// Stores - registreres som IGroupingStore
builder.registerType(MockTeamStore).as<IGroupingStore>();

View file

@ -1,11 +1,11 @@
import { Renderer, RenderContext } from './IGroupingRenderer';
import { IRenderer, IRenderContext } from './IGroupingRenderer';
import { buildPipeline } from './RenderBuilder';
import { EventRenderer } from '../features/event/EventRenderer';
import { ViewConfig } from './ViewConfig';
export class CalendarOrchestrator {
constructor(
private allRenderers: Renderer[],
private allRenderers: IRenderer[],
private eventRenderer: EventRenderer
) {}
@ -22,7 +22,7 @@ export class CalendarOrchestrator {
filter[grouping.type] = grouping.values;
}
const context: RenderContext = { headerContainer, columnContainer, filter };
const context: IRenderContext = { headerContainer, columnContainer, filter };
// Clear
headerContainer.innerHTML = '';
@ -41,18 +41,18 @@ export class CalendarOrchestrator {
// Byg og kør pipeline
const pipeline = buildPipeline(activeRenderers);
pipeline.run(context);
await pipeline.run(context);
// Render events med hele filter (date + resource)
await this.eventRenderer.render(container, filter);
}
private selectRenderers(viewConfig: ViewConfig): Renderer[] {
private selectRenderers(viewConfig: ViewConfig): IRenderer[] {
const types = viewConfig.groupings.map(g => g.type);
// Sortér renderers i samme rækkefølge som viewConfig.groupings
return types
.map(type => this.allRenderers.find(r => r.type === type))
.filter((r): r is Renderer => r !== undefined);
.filter((r): r is IRenderer => r !== undefined);
}
private calculateTotalColumns(viewConfig: ViewConfig): number {

View file

@ -1,10 +1,10 @@
export interface RenderContext {
export interface IRenderContext {
headerContainer: HTMLElement;
columnContainer: HTMLElement;
filter: Record<string, string[]>; // { team: ['alpha'], resource: ['alice', 'bob'], date: [...] }
}
export interface Renderer {
export interface IRenderer {
readonly type: string;
render(context: RenderContext): void;
render(context: IRenderContext): void | Promise<void>;
}

View file

@ -1,14 +1,14 @@
import { Renderer, RenderContext } from './IGroupingRenderer';
import { IRenderer, IRenderContext } from './IGroupingRenderer';
export interface Pipeline {
run(context: RenderContext): void;
run(context: IRenderContext): Promise<void>;
}
export function buildPipeline(renderers: Renderer[]): Pipeline {
export function buildPipeline(renderers: IRenderer[]): Pipeline {
return {
run(context: RenderContext) {
async run(context: IRenderContext) {
for (const renderer of renderers) {
renderer.render(context);
await renderer.render(context);
}
}
};

View file

@ -73,7 +73,7 @@ export class DemoApp {
return {
templateId: 'day',
groupings: [
{ type: 'resource', values: ['res1', 'res2'] },
{ type: 'resource', values: ['EMP001', 'EMP002'] },
{ type: 'date', values: today }
]
};
@ -90,7 +90,7 @@ export class DemoApp {
return {
templateId: 'resource',
groupings: [
{ type: 'resource', values: ['res1', 'res2'] },
{ type: 'resource', values: ['EMP001', 'EMP002'] },
{ type: 'date', values: dates }
]
};

View file

@ -1,12 +1,12 @@
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
import { IRenderer, IRenderContext } from '../../core/IGroupingRenderer';
import { DateService } from '../../core/DateService';
export class DateRenderer implements Renderer {
export class DateRenderer implements IRenderer {
readonly type = 'date';
constructor(private dateService: DateService) {}
render(context: RenderContext): void {
render(context: IRenderContext): void {
const dates = context.filter['date'] || [];
const resourceIds = context.filter['resource'] || [];

View file

@ -1,31 +1,20 @@
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
import { IRenderer, IRenderContext } from '../../core/IGroupingRenderer';
import { ResourceService } from '../../storage/resources/ResourceService';
interface Resource {
id: string;
name: string;
}
export class ResourceRenderer implements Renderer {
export class ResourceRenderer implements IRenderer {
readonly type = 'resource';
// Hardcoded data
private resources: Resource[] = [
{ id: 'res1', name: 'Anders' },
{ id: 'res2', name: 'Bente' },
{ id: 'res3', name: 'Carsten' }
];
render(context: RenderContext): void {
const allowedIds = context.filter['resource'] || [];
const filteredResources = this.resources.filter(r => allowedIds.includes(r.id));
constructor(private resourceService: ResourceService) {}
async render(context: IRenderContext): Promise<void> {
const resourceIds = context.filter['resource'] || [];
const resources = await this.resourceService.getByIds(resourceIds);
const dateCount = context.filter['date']?.length || 1;
// Render ALLE resource headers
for (const resource of filteredResources) {
for (const resource of resources) {
const header = document.createElement('swp-resource-header');
header.dataset.resourceId = resource.id;
header.textContent = resource.name;
header.textContent = resource.displayName;
header.style.gridColumn = `span ${dateCount}`;
context.headerContainer.appendChild(header);
}

View file

@ -1,4 +1,4 @@
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
import { IRenderer, IRenderContext } from '../../core/IGroupingRenderer';
interface Team {
id: string;
@ -6,7 +6,7 @@ interface Team {
resourceIds: string[];
}
export class TeamRenderer implements Renderer {
export class TeamRenderer implements IRenderer {
readonly type = 'team';
// Hardcoded data
@ -15,7 +15,7 @@ export class TeamRenderer implements Renderer {
{ id: 'team2', name: 'Team Beta', resourceIds: ['res3'] }
];
render(context: RenderContext): void {
render(context: IRenderContext): void {
const allowedIds = context.filter['team'] || [];
const filteredTeams = this.teams.filter(t => allowedIds.includes(t.id));

View file

@ -1,6 +1,6 @@
// Core exports
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
export { Renderer, RenderContext } from './core/IGroupingRenderer';
export { IRenderer as Renderer, IRenderContext as RenderContext } from './core/IGroupingRenderer';
export { IGroupingStore } from './core/IGroupingStore';
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
export { NavigationAnimator } from './core/NavigationAnimator';

View file

@ -22,6 +22,16 @@ export class ResourceService extends BaseEntityService<IResource> {
return all.filter(r => r.isActive !== false);
}
/**
* Get resources by IDs
*/
async getByIds(ids: string[]): Promise<IResource[]> {
if (ids.length === 0) return [];
const results = await Promise.all(ids.map(id => this.get(id)));
return results.filter((r): r is IResource => r !== null);
}
/**
* Get resources by type
*/