Adds EventId type for robust event ID handling
Introduces type-safe EventId with centralized normalization logic for clone and standard event IDs Refactors event ID management across multiple components to use consistent ID transformation methods Improves type safety and reduces potential ID-related bugs in drag-and-drop and event rendering
This commit is contained in:
parent
d53af317bb
commit
73e284660f
5 changed files with 82 additions and 29 deletions
55
CLAUDE.md
55
CLAUDE.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
Calendar Plantempus is a professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities.
|
||||
Calendar Plantempus is a professional TypeScript calendar component with offline-first architecture, drag-and-drop functionality, and real-time synchronization capabilities. Supports both date-based (day/week/month) and resource-based (people, rooms) calendar views.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
|
|
@ -21,18 +21,18 @@ npm run clean
|
|||
# Type check only
|
||||
npx tsc --noEmit
|
||||
|
||||
# Run all tests
|
||||
# Run all tests (watch mode)
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test
|
||||
|
||||
# Run tests once and exit
|
||||
npm run test:run
|
||||
|
||||
# Run tests with UI
|
||||
npm run test:ui
|
||||
|
||||
# Run single test file
|
||||
npm test -- <test-file-name>
|
||||
|
||||
# CSS Development
|
||||
npm run css:build # Build CSS
|
||||
npm run css:watch # Watch and rebuild CSS
|
||||
|
|
@ -57,6 +57,21 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o
|
|||
- Components subscribe via `eventBus.on(CoreEvents.EVENT_NAME, handler)`
|
||||
- Never call methods directly between managers - always use events
|
||||
|
||||
### Calendar Modes: Date vs Resource
|
||||
|
||||
The calendar supports two column modes, configured at initialization in `src/index.ts`:
|
||||
|
||||
**Date Mode** (`DateColumnDataSource`):
|
||||
- Columns represent dates (day/week/month views)
|
||||
- Uses `DateHeaderRenderer` and `DateColumnRenderer`
|
||||
|
||||
**Resource Mode** (`ResourceColumnDataSource`):
|
||||
- Columns represent resources (people, rooms, equipment)
|
||||
- Uses `ResourceHeaderRenderer` and `ResourceColumnRenderer`
|
||||
- Events filtered per-resource via `IColumnInfo.events`
|
||||
|
||||
Both modes implement `IColumnDataSource` interface, allowing polymorphic column handling.
|
||||
|
||||
### Manager Hierarchy
|
||||
|
||||
**CalendarManager** (`src/managers/CalendarManager.ts`) - Top-level coordinator
|
||||
|
|
@ -67,7 +82,6 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o
|
|||
**Key Managers**:
|
||||
- **EventManager** - Event CRUD operations, data loading from repository
|
||||
- **GridManager** - Renders time grid structure
|
||||
- **ViewManager** - Handles view switching (day/week/month)
|
||||
- **NavigationManager** - Date navigation and period calculations
|
||||
- **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
|
||||
- **ResizeHandleManager** - Event resizing with visual feedback
|
||||
|
|
@ -78,13 +92,20 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o
|
|||
|
||||
### Repository Pattern
|
||||
|
||||
Event data access is abstracted through the **IEventRepository** interface (`src/repositories/IEventRepository.ts`):
|
||||
- **IndexedDBEventRepository** - Primary: Local storage with offline support
|
||||
- **ApiEventRepository** - Sends changes to backend API
|
||||
- **MockEventRepository** - Legacy: Loads from JSON file
|
||||
Data access is abstracted through **IApiRepository<T>** interface (`src/repositories/IApiRepository.ts`):
|
||||
- **MockEventRepository**, **MockBookingRepository**, **MockCustomerRepository**, **MockResourceRepository** - Development: Load from JSON files
|
||||
- **ApiEventRepository**, **ApiBookingRepository**, **ApiCustomerRepository**, **ApiResourceRepository** - Production: Backend API calls
|
||||
|
||||
All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates.
|
||||
|
||||
### Entity Service Pattern
|
||||
|
||||
**IEntityService<T>** (`src/storage/IEntityService.ts`) provides polymorphic entity handling:
|
||||
- `EventService`, `BookingService`, `CustomerService`, `ResourceService` implement this interface
|
||||
- Encapsulates sync status manipulation (SyncManager delegates, never manipulates directly)
|
||||
- Uses `entityType` discriminator for runtime routing without switch statements
|
||||
- Enables polymorphic operations: `Array<IEntityService<any>>` works across all entity types
|
||||
|
||||
### Offline-First Sync Architecture
|
||||
|
||||
**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization:
|
||||
|
|
@ -154,16 +175,19 @@ When dropping events, snap to time grid:
|
|||
### Testing with Vitest
|
||||
|
||||
Tests use **Vitest** with **jsdom** environment:
|
||||
- Config: `vitest.config.ts`
|
||||
- Setup file: `test/setup.ts`
|
||||
- Test helpers: `test/helpers/dom-helpers.ts`
|
||||
- Test helpers: `test/helpers/dom-helpers.ts`, `test/helpers/config-helpers.ts`
|
||||
- Run single test: `npm test -- <test-file-name>`
|
||||
|
||||
## Key Files to Know
|
||||
|
||||
- `src/index.ts` - DI container setup and initialization
|
||||
- `src/index.ts` - DI container setup and initialization (also sets calendar mode: date vs resource)
|
||||
- `src/core/EventBus.ts` - Central event dispatcher
|
||||
- `src/constants/CoreEvents.ts` - All event type constants
|
||||
- `src/constants/CoreEvents.ts` - All event type constants (~34 core events)
|
||||
- `src/types/CalendarTypes.ts` - Core type definitions
|
||||
- `src/types/ColumnDataSource.ts` - Column abstraction for date/resource modes
|
||||
- `src/storage/IEntityService.ts` - Entity service interface for polymorphic sync
|
||||
- `src/managers/CalendarManager.ts` - Main coordinator
|
||||
- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs
|
||||
- `src/configurations/CalendarConfig.ts` - Configuration schema
|
||||
|
|
@ -207,12 +231,15 @@ Access debug interface in browser console:
|
|||
window.calendarDebug.eventBus.getEventLog()
|
||||
window.calendarDebug.calendarManager
|
||||
window.calendarDebug.eventManager
|
||||
window.calendarDebug.auditService
|
||||
window.calendarDebug.syncManager
|
||||
window.calendarDebug.app // Full DI container
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **@novadi/core** - Dependency injection framework
|
||||
- **date-fns** / **date-fns-tz** - Date manipulation and timezone support
|
||||
- **dayjs** - Date manipulation and formatting
|
||||
- **fuse.js** - Fuzzy search for event filtering
|
||||
- **esbuild** - Fast bundler for development
|
||||
- **vitest** - Testing framework
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Configuration } from '../configurations/CalendarConfig';
|
|||
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { EventId } from '../types/EventId';
|
||||
|
||||
/**
|
||||
* Base class for event elements
|
||||
|
|
@ -172,7 +173,7 @@ export class SwpEventElement extends BaseSwpEventElement {
|
|||
const clone = this.cloneNode(true) as SwpEventElement;
|
||||
|
||||
// Apply "clone-" prefix to ID
|
||||
clone.dataset.eventId = `clone-${this.eventId}`;
|
||||
clone.dataset.eventId = EventId.toCloneId(this.eventId as EventId);
|
||||
|
||||
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||
clone.style.pointerEvents = 'none';
|
||||
|
|
@ -343,7 +344,7 @@ export class SwpAllDayEventElement extends BaseSwpEventElement {
|
|||
const clone = this.cloneNode(true) as SwpAllDayEventElement;
|
||||
|
||||
// Apply "clone-" prefix to ID
|
||||
clone.dataset.eventId = `clone-${this.eventId}`;
|
||||
clone.dataset.eventId = EventId.toCloneId(this.eventId as EventId);
|
||||
|
||||
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||
clone.style.pointerEvents = 'none';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { EventManager } from './EventManager';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { EventId } from '../types/EventId';
|
||||
|
||||
/**
|
||||
* AllDayManager - Handles all-day row height animations and management
|
||||
|
|
@ -500,7 +501,7 @@ export class AllDayManager {
|
|||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||
|
||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||
const eventId = clone.eventId.replace('clone-', '');
|
||||
const eventId = EventId.from(clone.eventId);
|
||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||
|
||||
// Determine target date based on mode
|
||||
|
|
@ -568,7 +569,7 @@ export class AllDayManager {
|
|||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||
|
||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||
const eventId = clone.eventId.replace('clone-', '');
|
||||
const eventId = EventId.from(clone.eventId);
|
||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||
|
||||
// Determine target date based on mode
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPa
|
|||
import { DateService } from '../utils/DateService';
|
||||
import { EventStackManager } from '../managers/EventStackManager';
|
||||
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
||||
import { EventId } from '../types/EventId';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -165,22 +166,14 @@ export class DateEventRenderer implements IEventRenderer {
|
|||
* Handle drag end event
|
||||
*/
|
||||
public handleDragEnd(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void {
|
||||
if (!draggedClone || !originalElement) {
|
||||
console.warn('Missing draggedClone or originalElement');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Only fade out and remove if it's a swp-event (not swp-allday-event)
|
||||
// AllDayManager handles removal of swp-allday-event elements
|
||||
if (originalElement.tagName === 'SWP-EVENT') {
|
||||
this.fadeOutAndRemove(originalElement);
|
||||
}
|
||||
|
||||
// Remove clone prefix and normalize clone to be a regular event
|
||||
const cloneId = draggedClone.dataset.eventId;
|
||||
if (cloneId && cloneId.startsWith('clone-')) {
|
||||
draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
||||
}
|
||||
draggedClone.dataset.eventId = EventId.from(draggedClone.dataset.eventId!);
|
||||
|
||||
// Fully normalize the clone to be a regular event
|
||||
draggedClone.classList.remove('dragging');
|
||||
|
|
@ -192,7 +185,7 @@ export class DateEventRenderer implements IEventRenderer {
|
|||
|
||||
|
||||
// Clean up any remaining day event clones
|
||||
const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${cloneId}"]`);
|
||||
const dayEventClone = document.querySelector(`swp-event[data-event-id="${draggedClone.dataset.eventId}"]`);
|
||||
if (dayEventClone) {
|
||||
dayEventClone.remove();
|
||||
}
|
||||
|
|
|
|||
31
src/types/EventId.ts
Normal file
31
src/types/EventId.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Branded type for Event IDs
|
||||
* Ensures type-safety and centralizes ID normalization logic
|
||||
*/
|
||||
export type EventId = string & { readonly __brand: 'EventId' };
|
||||
|
||||
/**
|
||||
* EventId utility functions
|
||||
*/
|
||||
export const EventId = {
|
||||
/**
|
||||
* Create EventId from string, normalizing clone- prefix
|
||||
*/
|
||||
from(id: string): EventId {
|
||||
return id.replace('clone-', '') as EventId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if raw ID is a clone
|
||||
*/
|
||||
isClone(id: string): boolean {
|
||||
return id.startsWith('clone-');
|
||||
},
|
||||
|
||||
/**
|
||||
* Create clone ID string from EventId
|
||||
*/
|
||||
toCloneId(id: EventId): string {
|
||||
return `clone-${id}`;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue