Moving away from Azure Devops #1
9 changed files with 187 additions and 249 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { Container } from '@novadi/core';
|
import { Container } from '@novadi/core';
|
||||||
import { IGroupingRenderer } from './core/IGroupingRenderer';
|
import { Renderer } from './core/IGroupingRenderer';
|
||||||
import { IGroupingStore } from './core/IGroupingStore';
|
import { IGroupingStore } from './core/IGroupingStore';
|
||||||
import { DateRenderer } from './features/date/DateRenderer';
|
import { DateRenderer } from './features/date/DateRenderer';
|
||||||
import { DateService } from './core/DateService';
|
import { DateService } from './core/DateService';
|
||||||
|
|
@ -75,10 +75,10 @@ export function createV2Container(): Container {
|
||||||
// Features
|
// Features
|
||||||
builder.registerType(EventRenderer).as<EventRenderer>();
|
builder.registerType(EventRenderer).as<EventRenderer>();
|
||||||
|
|
||||||
// Renderers - registreres som IGroupingRenderer (array injection til CalendarOrchestrator)
|
// Renderers - registreres som Renderer (array injection til CalendarOrchestrator)
|
||||||
builder.registerType(DateRenderer).as<IGroupingRenderer>();
|
builder.registerType(DateRenderer).as<Renderer>();
|
||||||
builder.registerType(ResourceRenderer).as<IGroupingRenderer>();
|
builder.registerType(ResourceRenderer).as<Renderer>();
|
||||||
builder.registerType(TeamRenderer).as<IGroupingRenderer>();
|
builder.registerType(TeamRenderer).as<Renderer>();
|
||||||
|
|
||||||
// Stores - registreres som IGroupingStore
|
// Stores - registreres som IGroupingStore
|
||||||
builder.registerType(MockTeamStore).as<IGroupingStore>();
|
builder.registerType(MockTeamStore).as<IGroupingStore>();
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,14 @@
|
||||||
import { from } from 'ts-linq-light';
|
import { Renderer, RenderContext } from './IGroupingRenderer';
|
||||||
import { ViewConfig } from './ViewConfig';
|
import { buildPipeline } from './RenderBuilder';
|
||||||
import { IGroupingRenderer, RenderContext } from './IGroupingRenderer';
|
|
||||||
import { IGroupingStore } from './IGroupingStore';
|
|
||||||
import { RenderBuilder } from './RenderBuilder';
|
|
||||||
import { EventRenderer } from '../features/event/EventRenderer';
|
import { EventRenderer } from '../features/event/EventRenderer';
|
||||||
|
import { ViewConfig } from './ViewConfig';
|
||||||
|
|
||||||
export class CalendarOrchestrator {
|
export class CalendarOrchestrator {
|
||||||
constructor(
|
constructor(
|
||||||
private renderers: IGroupingRenderer[],
|
private allRenderers: Renderer[],
|
||||||
private stores: IGroupingStore[],
|
|
||||||
private eventRenderer: EventRenderer
|
private eventRenderer: EventRenderer
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getRenderer(type: string): IGroupingRenderer | undefined {
|
|
||||||
return from(this.renderers).firstOrDefault(r => r.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getStore(type: string): IGroupingStore | undefined {
|
|
||||||
return from(this.stores).firstOrDefault(s => s.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
|
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
|
||||||
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
||||||
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
||||||
|
|
@ -27,53 +16,42 @@ export class CalendarOrchestrator {
|
||||||
throw new Error('Missing swp-calendar-header or swp-day-columns');
|
throw new Error('Missing swp-calendar-header or swp-day-columns');
|
||||||
}
|
}
|
||||||
|
|
||||||
const context: RenderContext = { headerContainer, columnContainer };
|
// Byg filter fra viewConfig
|
||||||
|
const filter: Record<string, string[]> = {};
|
||||||
|
for (const grouping of viewConfig.groupings) {
|
||||||
|
filter[grouping.type] = grouping.values;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear containers
|
const context: RenderContext = { headerContainer, columnContainer, filter };
|
||||||
|
|
||||||
|
// Clear
|
||||||
headerContainer.innerHTML = '';
|
headerContainer.innerHTML = '';
|
||||||
columnContainer.innerHTML = '';
|
columnContainer.innerHTML = '';
|
||||||
|
|
||||||
// Set header levels
|
// Vælg renderers baseret på groupings types
|
||||||
const types = from(viewConfig.groupings).select(g => g.type).toArray();
|
const activeRenderers = this.selectRenderers(viewConfig);
|
||||||
headerContainer.dataset.levels = types.join(' ');
|
|
||||||
|
|
||||||
// Byg renderer chain
|
// Beregn total kolonner dynamisk
|
||||||
const builder = new RenderBuilder(context);
|
const totalColumns = this.calculateTotalColumns(viewConfig);
|
||||||
|
|
||||||
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));
|
container.style.setProperty('--grid-columns', String(totalColumns));
|
||||||
|
|
||||||
builder.build();
|
// Byg og kør pipeline
|
||||||
|
const pipeline = buildPipeline(activeRenderers);
|
||||||
|
pipeline.run(context);
|
||||||
|
|
||||||
// Render events
|
// Events
|
||||||
const visibleDates = this.extractVisibleDates(viewConfig);
|
const dates = filter['date'] || [];
|
||||||
await this.eventRenderer.render(container, visibleDates);
|
await this.eventRenderer.render(container, dates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getItems(type: string, viewConfig: ViewConfig): ReturnType<typeof from> {
|
private selectRenderers(viewConfig: ViewConfig): Renderer[] {
|
||||||
const grouping = from(viewConfig.groupings).firstOrDefault(g => g.type === type);
|
const types = viewConfig.groupings.map(g => g.type);
|
||||||
if (!grouping) return from([]);
|
return this.allRenderers.filter(r => types.includes(r.type));
|
||||||
|
|
||||||
if (type === 'date') {
|
|
||||||
return from(grouping.values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.getStore(type);
|
private calculateTotalColumns(viewConfig: ViewConfig): number {
|
||||||
if (!store) return from([]);
|
const dateCount = viewConfig.groupings.find(g => g.type === 'date')?.values.length || 1;
|
||||||
|
const resourceCount = viewConfig.groupings.find(g => g.type === 'resource')?.values.length || 1;
|
||||||
return from(store.getByIds(grouping.values));
|
return dateCount * resourceCount;
|
||||||
}
|
|
||||||
|
|
||||||
private extractVisibleDates(viewConfig: ViewConfig): string[] {
|
|
||||||
return from(viewConfig.groupings).firstOrDefault(g => g.type === 'date')?.values || [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import { IEnumerable } from 'ts-linq-light';
|
|
||||||
import { NextFunction } from './RenderBuilder';
|
|
||||||
|
|
||||||
export interface RenderContext {
|
export interface RenderContext {
|
||||||
headerContainer: HTMLElement;
|
headerContainer: HTMLElement;
|
||||||
columnContainer: HTMLElement;
|
columnContainer: HTMLElement;
|
||||||
|
filter: Record<string, string[]>; // { team: ['alpha'], resource: ['alice', 'bob'], date: [...] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupingRenderer<T = unknown> {
|
export interface Renderer {
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
render(
|
next: Renderer | null;
|
||||||
items: IEnumerable<T>,
|
render(context: RenderContext): void;
|
||||||
next: NextFunction,
|
|
||||||
context: RenderContext
|
|
||||||
): void;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,20 @@
|
||||||
import { from, IEnumerable } from 'ts-linq-light';
|
import { Renderer, RenderContext } from './IGroupingRenderer';
|
||||||
import { IGroupingRenderer, RenderContext } from './IGroupingRenderer';
|
|
||||||
|
|
||||||
export interface NextFunction {
|
export interface Pipeline {
|
||||||
count(items: IEnumerable<unknown>): number;
|
run(context: RenderContext): void;
|
||||||
render(items: IEnumerable<unknown>): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderLevel {
|
export function buildPipeline(renderers: Renderer[]): Pipeline {
|
||||||
renderer: IGroupingRenderer;
|
// Link renderers
|
||||||
items: IEnumerable<unknown>;
|
for (let i = 0; i < renderers.length - 1; i++) {
|
||||||
}
|
renderers[i].next = renderers[i + 1];
|
||||||
|
|
||||||
export class RenderBuilder {
|
|
||||||
private levels: RenderLevel[] = [];
|
|
||||||
|
|
||||||
constructor(private context: RenderContext) {}
|
|
||||||
|
|
||||||
add(renderer: IGroupingRenderer, items: IEnumerable<unknown>): this {
|
|
||||||
this.levels.push({ renderer, items });
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalCount(): number {
|
const first = renderers[0] ?? null;
|
||||||
if (this.levels.length === 0) return 0;
|
|
||||||
|
|
||||||
const chain = this.buildChain(0);
|
|
||||||
return chain.count(this.levels[0].items);
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): void {
|
|
||||||
if (this.levels.length === 0) return;
|
|
||||||
|
|
||||||
const chain = this.buildChain(0);
|
|
||||||
chain.render(this.levels[0].items);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildChain(index: number): NextFunction {
|
|
||||||
if (index >= this.levels.length) {
|
|
||||||
// Leaf - ingen flere levels
|
|
||||||
return {
|
|
||||||
count: (items) => from(items).count(),
|
|
||||||
render: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const level = this.levels[index];
|
|
||||||
const nextChain = this.buildChain(index + 1);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: (items) => {
|
run(context: RenderContext) {
|
||||||
let total = 0;
|
if (first) first.render(context);
|
||||||
for (const item of items) {
|
|
||||||
const childItems = this.getChildItems(index, item);
|
|
||||||
total += nextChain.count(childItems);
|
|
||||||
}
|
|
||||||
return total || from(items).count();
|
|
||||||
},
|
|
||||||
render: (items) => {
|
|
||||||
level.renderer.render(items, nextChain, this.context);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private getChildItems(levelIndex: number, _parentItem: unknown): IEnumerable<unknown> {
|
|
||||||
// Returnerer næste levels items - rendereren selv filtrerer baseret på parent
|
|
||||||
const nextLevel = this.levels[levelIndex + 1];
|
|
||||||
if (!nextLevel) {
|
|
||||||
return from([]);
|
|
||||||
}
|
|
||||||
return nextLevel.items;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import { NavigationAnimator } from '../core/NavigationAnimator';
|
||||||
import { DateService } from '../core/DateService';
|
import { DateService } from '../core/DateService';
|
||||||
import { ScrollManager } from '../core/ScrollManager';
|
import { ScrollManager } from '../core/ScrollManager';
|
||||||
import { HeaderDrawerManager } from '../core/HeaderDrawerManager';
|
import { HeaderDrawerManager } from '../core/HeaderDrawerManager';
|
||||||
import { ViewConfig } from '../core/ViewConfig';
|
|
||||||
import { IndexedDBContext } from '../storage/IndexedDBContext';
|
import { IndexedDBContext } from '../storage/IndexedDBContext';
|
||||||
import { DataSeeder } from '../workers/DataSeeder';
|
import { DataSeeder } from '../workers/DataSeeder';
|
||||||
|
import { ViewConfig } from '../core/ViewConfig';
|
||||||
|
|
||||||
export class DemoApp {
|
export class DemoApp {
|
||||||
private animator!: NavigationAnimator;
|
private animator!: NavigationAnimator;
|
||||||
private container!: HTMLElement;
|
private container!: HTMLElement;
|
||||||
private weekOffset = 0;
|
private weekOffset = 0;
|
||||||
private views!: Record<string, ViewConfig>;
|
private currentView: 'simple' | 'resource' | 'team' = 'team';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private orchestrator: CalendarOrchestrator,
|
private orchestrator: CalendarOrchestrator,
|
||||||
|
|
@ -41,27 +41,6 @@ export class DemoApp {
|
||||||
document.querySelector('swp-content-track') as HTMLElement
|
document.querySelector('swp-content-track') as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
// View configs
|
|
||||||
const dates = this.dateService.getWeekDates();
|
|
||||||
this.views = {
|
|
||||||
simple: { templateId: 'simple', groupings: [{ type: 'date', values: dates }] },
|
|
||||||
resource: {
|
|
||||||
templateId: 'resource',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'resource', values: ['alice', 'bob', 'carol'] },
|
|
||||||
{ type: 'date', values: dates.slice(0, 3) }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
templateId: 'team',
|
|
||||||
groupings: [
|
|
||||||
{ type: 'team', values: ['alpha', 'beta'] },
|
|
||||||
{ type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' },
|
|
||||||
{ type: 'date', values: dates.slice(0, 3) }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render time axis (06:00 - 18:00)
|
// Render time axis (06:00 - 18:00)
|
||||||
this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement, 6, 18);
|
this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement, 6, 18);
|
||||||
|
|
||||||
|
|
@ -73,36 +52,78 @@ export class DemoApp {
|
||||||
|
|
||||||
// Setup event handlers
|
// Setup event handlers
|
||||||
this.setupNavigation();
|
this.setupNavigation();
|
||||||
this.setupViewSwitchers();
|
|
||||||
this.setupDrawerToggle();
|
this.setupDrawerToggle();
|
||||||
|
this.setupViewSwitching();
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
this.orchestrator.render(this.views.simple, this.container);
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async render(): Promise<void> {
|
||||||
|
const viewConfig = this.buildViewConfig();
|
||||||
|
await this.orchestrator.render(viewConfig, this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildViewConfig(): ViewConfig {
|
||||||
|
const dates = this.dateService.getWeekDates(this.weekOffset);
|
||||||
|
|
||||||
|
switch (this.currentView) {
|
||||||
|
case 'simple':
|
||||||
|
return {
|
||||||
|
templateId: 'simple',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'date', values: dates }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'resource':
|
||||||
|
return {
|
||||||
|
templateId: 'resource',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'resource', values: ['res1', 'res2', 'res3'] },
|
||||||
|
{ type: 'date', values: dates }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'team':
|
||||||
|
return {
|
||||||
|
templateId: 'team',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'team', values: ['team1', 'team2'] },
|
||||||
|
{ type: 'resource', values: ['res1', 'res2', 'res3'] },
|
||||||
|
{ type: 'date', values: dates }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupNavigation(): void {
|
private setupNavigation(): void {
|
||||||
document.getElementById('btn-prev')!.onclick = () => {
|
document.getElementById('btn-prev')!.onclick = () => {
|
||||||
this.weekOffset--;
|
this.weekOffset--;
|
||||||
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
this.animator.slide('right', () => this.render());
|
||||||
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('btn-next')!.onclick = () => {
|
document.getElementById('btn-next')!.onclick = () => {
|
||||||
this.weekOffset++;
|
this.weekOffset++;
|
||||||
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
this.animator.slide('left', () => this.render());
|
||||||
this.animator.slide('left', () => this.orchestrator.render(this.views.simple, this.container));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupViewSwitchers(): void {
|
private setupViewSwitching(): void {
|
||||||
document.getElementById('btn-simple')!.onclick = () =>
|
document.getElementById('btn-simple')?.addEventListener('click', () => {
|
||||||
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
this.currentView = 'simple';
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-resource')!.onclick = () =>
|
document.getElementById('btn-resource')?.addEventListener('click', () => {
|
||||||
this.animator.slide('left', () => this.orchestrator.render(this.views.resource, this.container));
|
this.currentView = 'resource';
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-team')!.onclick = () =>
|
document.getElementById('btn-team')?.addEventListener('click', () => {
|
||||||
this.animator.slide('left', () => this.orchestrator.render(this.views.team, this.container));
|
this.currentView = 'team';
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupDrawerToggle(): void {
|
private setupDrawerToggle(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,38 @@
|
||||||
import { IEnumerable } from 'ts-linq-light';
|
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
|
||||||
import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer';
|
|
||||||
import { NextFunction, RenderData } from '../../core/RenderBuilder';
|
|
||||||
import { DateService } from '../../core/DateService';
|
import { DateService } from '../../core/DateService';
|
||||||
|
|
||||||
export class DateRenderer implements IGroupingRenderer<string> {
|
export class DateRenderer implements Renderer {
|
||||||
readonly type = 'date';
|
readonly type = 'date';
|
||||||
|
next: Renderer | null = null;
|
||||||
|
|
||||||
constructor(private dateService: DateService) {}
|
constructor(private dateService: DateService) {}
|
||||||
|
|
||||||
render(
|
render(context: RenderContext): void {
|
||||||
dates: IEnumerable<string>,
|
const dates = context.filter['date'] || [];
|
||||||
_data: RenderData,
|
const resourceCount = context.filter['resource']?.length || 1;
|
||||||
_next: NextFunction,
|
|
||||||
context: RenderContext
|
// Render dates for HVER resource (resourceCount gange)
|
||||||
): void {
|
for (let r = 0; r < resourceCount; r++) {
|
||||||
for (const dateStr of dates) {
|
for (const dateStr of dates) {
|
||||||
const date = this.dateService.parseISO(dateStr);
|
const date = this.dateService.parseISO(dateStr);
|
||||||
|
|
||||||
const headerCell = document.createElement('swp-day-header');
|
// Header
|
||||||
headerCell.dataset.date = dateStr;
|
const header = document.createElement('swp-day-header');
|
||||||
headerCell.innerHTML = `
|
header.dataset.date = dateStr;
|
||||||
|
header.innerHTML = `
|
||||||
<swp-day-name>${this.dateService.getDayName(date, 'short')}</swp-day-name>
|
<swp-day-name>${this.dateService.getDayName(date, 'short')}</swp-day-name>
|
||||||
<swp-day-date>${date.getDate()}</swp-day-date>
|
<swp-day-date>${date.getDate()}</swp-day-date>
|
||||||
`;
|
`;
|
||||||
context.headerContainer.appendChild(headerCell);
|
context.headerContainer.appendChild(header);
|
||||||
|
|
||||||
|
// Column
|
||||||
const column = document.createElement('swp-day-column');
|
const column = document.createElement('swp-day-column');
|
||||||
column.dataset.date = dateStr;
|
column.dataset.date = dateStr;
|
||||||
column.innerHTML = '<swp-events-layer></swp-events-layer>';
|
column.innerHTML = '<swp-events-layer></swp-events-layer>';
|
||||||
context.columnContainer.appendChild(column);
|
context.columnContainer.appendChild(column);
|
||||||
|
|
||||||
// Leaf renderer - ingen next.render() kald
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaf - ingen next
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import { from, IEnumerable } from 'ts-linq-light';
|
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
|
||||||
import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer';
|
|
||||||
import { NextFunction, RenderData } from '../../core/RenderBuilder';
|
|
||||||
|
|
||||||
interface Resource {
|
interface Resource {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResourceRenderer implements IGroupingRenderer<Resource> {
|
export class ResourceRenderer implements Renderer {
|
||||||
readonly type = 'resource';
|
readonly type = 'resource';
|
||||||
|
next: Renderer | null = null;
|
||||||
|
|
||||||
render(
|
// Hardcoded data
|
||||||
resources: IEnumerable<Resource>,
|
private resources: Resource[] = [
|
||||||
data: RenderData,
|
{ id: 'res1', name: 'Anders' },
|
||||||
next: NextFunction,
|
{ id: 'res2', name: 'Bente' },
|
||||||
context: RenderContext
|
{ id: 'res3', name: 'Carsten' }
|
||||||
): void {
|
];
|
||||||
const dates = data.dates || from([]);
|
|
||||||
|
|
||||||
for (const resource of resources) {
|
render(context: RenderContext): void {
|
||||||
const colspan = next.count(dates);
|
const allowedIds = context.filter['resource'] || [];
|
||||||
|
const filteredResources = this.resources.filter(r => allowedIds.includes(r.id));
|
||||||
|
|
||||||
const cell = document.createElement('swp-resource-header');
|
const dateCount = context.filter['date']?.length || 1;
|
||||||
cell.dataset.resourceId = resource.id;
|
|
||||||
cell.textContent = resource.name || resource.id;
|
|
||||||
|
|
||||||
if (colspan > 1) {
|
// Render ALLE resource headers
|
||||||
cell.style.gridColumn = `span ${colspan}`;
|
for (const resource of filteredResources) {
|
||||||
|
const header = document.createElement('swp-resource-header');
|
||||||
|
header.dataset.resourceId = resource.id;
|
||||||
|
header.textContent = resource.name;
|
||||||
|
header.style.gridColumn = `span ${dateCount}`;
|
||||||
|
context.headerContainer.appendChild(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.headerContainer.appendChild(cell);
|
// Derefter kald next ÉN gang
|
||||||
|
if (this.next) this.next.render(context);
|
||||||
next.render(dates);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,42 @@
|
||||||
import { from, IEnumerable } from 'ts-linq-light';
|
import { Renderer, RenderContext } from '../../core/IGroupingRenderer';
|
||||||
import { IGroupingRenderer, RenderContext } from '../../core/IGroupingRenderer';
|
|
||||||
import { NextFunction, RenderData } from '../../core/RenderBuilder';
|
|
||||||
|
|
||||||
interface Team {
|
interface Team {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name: string;
|
||||||
|
resourceIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Resource {
|
export class TeamRenderer implements Renderer {
|
||||||
id: string;
|
|
||||||
teamId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TeamRenderer implements IGroupingRenderer<Team> {
|
|
||||||
readonly type = 'team';
|
readonly type = 'team';
|
||||||
|
next: Renderer | null = null;
|
||||||
|
|
||||||
render(
|
// Hardcoded data
|
||||||
teams: IEnumerable<Team>,
|
private teams: Team[] = [
|
||||||
data: RenderData,
|
{ id: 'team1', name: 'Team Alpha', resourceIds: ['res1', 'res2'] },
|
||||||
next: NextFunction,
|
{ id: 'team2', name: 'Team Beta', resourceIds: ['res3'] }
|
||||||
context: RenderContext
|
];
|
||||||
): void {
|
|
||||||
const resources = data.resources as IEnumerable<Resource> || from([]);
|
|
||||||
const resourcesByTeam = from(resources).groupBy((r: Resource) => r.teamId || '');
|
|
||||||
|
|
||||||
for (const team of teams) {
|
render(context: RenderContext): void {
|
||||||
const teamResources = from(resourcesByTeam)
|
const allowedIds = context.filter['team'] || [];
|
||||||
.firstOrDefault(g => g.key === team.id);
|
const filteredTeams = this.teams.filter(t => allowedIds.includes(t.id));
|
||||||
|
|
||||||
const teamResourcesEnum = teamResources ? from(teamResources) : from([]);
|
const dateCount = context.filter['date']?.length || 1;
|
||||||
const colspan = next.count(teamResourcesEnum);
|
const resourceIds = context.filter['resource'] || [];
|
||||||
|
|
||||||
const cell = document.createElement('swp-team-header');
|
// Render ALLE team headers først
|
||||||
cell.dataset.teamId = team.id;
|
for (const team of filteredTeams) {
|
||||||
cell.textContent = team.name || team.id;
|
// Tæl resources der tilhører dette team OG er i filter
|
||||||
|
const teamResourceCount = team.resourceIds.filter(id => resourceIds.includes(id)).length;
|
||||||
|
const colspan = teamResourceCount * dateCount;
|
||||||
|
|
||||||
if (colspan > 1) {
|
const header = document.createElement('swp-team-header');
|
||||||
cell.style.gridColumn = `span ${colspan}`;
|
header.dataset.teamId = team.id;
|
||||||
|
header.textContent = team.name;
|
||||||
|
header.style.setProperty('--team-cols', String(colspan));
|
||||||
|
context.headerContainer.appendChild(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.headerContainer.appendChild(cell);
|
// Derefter kald next ÉN gang
|
||||||
|
if (this.next) this.next.render(context);
|
||||||
next.render(teamResourcesEnum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Core exports
|
// Core exports
|
||||||
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
|
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
|
||||||
export { IGroupingRenderer, RenderContext } from './core/IGroupingRenderer';
|
export { Renderer, RenderContext } from './core/IGroupingRenderer';
|
||||||
export { IGroupingStore } from './core/IGroupingStore';
|
export { IGroupingStore } from './core/IGroupingStore';
|
||||||
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||||
export { NavigationAnimator } from './core/NavigationAnimator';
|
export { NavigationAnimator } from './core/NavigationAnimator';
|
||||||
export { RenderBuilder, NextFunction, RenderData } from './core/RenderBuilder';
|
export { buildPipeline, Pipeline } from './core/RenderBuilder';
|
||||||
|
|
||||||
// Feature exports
|
// Feature exports
|
||||||
export { DateRenderer } from './features/date';
|
export { DateRenderer } from './features/date';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue