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
|
## 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
|
## Build & Development Commands
|
||||||
|
|
||||||
|
|
@ -21,18 +21,18 @@ npm run clean
|
||||||
# Type check only
|
# Type check only
|
||||||
npx tsc --noEmit
|
npx tsc --noEmit
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests (watch mode)
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
# Run tests in watch mode
|
|
||||||
npm run test
|
|
||||||
|
|
||||||
# Run tests once and exit
|
# Run tests once and exit
|
||||||
npm run test:run
|
npm run test:run
|
||||||
|
|
||||||
# Run tests with UI
|
# Run tests with UI
|
||||||
npm run test:ui
|
npm run test:ui
|
||||||
|
|
||||||
|
# Run single test file
|
||||||
|
npm test -- <test-file-name>
|
||||||
|
|
||||||
# CSS Development
|
# CSS Development
|
||||||
npm run css:build # Build CSS
|
npm run css:build # Build CSS
|
||||||
npm run css:watch # Watch and rebuild 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)`
|
- Components subscribe via `eventBus.on(CoreEvents.EVENT_NAME, handler)`
|
||||||
- Never call methods directly between managers - always use events
|
- 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
|
### Manager Hierarchy
|
||||||
|
|
||||||
**CalendarManager** (`src/managers/CalendarManager.ts`) - Top-level coordinator
|
**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**:
|
**Key Managers**:
|
||||||
- **EventManager** - Event CRUD operations, data loading from repository
|
- **EventManager** - Event CRUD operations, data loading from repository
|
||||||
- **GridManager** - Renders time grid structure
|
- **GridManager** - Renders time grid structure
|
||||||
- **ViewManager** - Handles view switching (day/week/month)
|
|
||||||
- **NavigationManager** - Date navigation and period calculations
|
- **NavigationManager** - Date navigation and period calculations
|
||||||
- **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
|
- **DragDropManager** - Advanced drag-and-drop with smooth animations, type conversion (timed ↔ all-day), scroll compensation
|
||||||
- **ResizeHandleManager** - Event resizing with visual feedback
|
- **ResizeHandleManager** - Event resizing with visual feedback
|
||||||
|
|
@ -78,13 +92,20 @@ The application uses a **centralized EventBus** (`src/core/EventBus.ts`) built o
|
||||||
|
|
||||||
### Repository Pattern
|
### Repository Pattern
|
||||||
|
|
||||||
Event data access is abstracted through the **IEventRepository** interface (`src/repositories/IEventRepository.ts`):
|
Data access is abstracted through **IApiRepository<T>** interface (`src/repositories/IApiRepository.ts`):
|
||||||
- **IndexedDBEventRepository** - Primary: Local storage with offline support
|
- **MockEventRepository**, **MockBookingRepository**, **MockCustomerRepository**, **MockResourceRepository** - Development: Load from JSON files
|
||||||
- **ApiEventRepository** - Sends changes to backend API
|
- **ApiEventRepository**, **ApiBookingRepository**, **ApiCustomerRepository**, **ApiResourceRepository** - Production: Backend API calls
|
||||||
- **MockEventRepository** - Legacy: Loads from JSON file
|
|
||||||
|
|
||||||
All repository methods accept an `UpdateSource` parameter ('local' | 'remote') to distinguish user actions from remote updates.
|
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
|
### Offline-First Sync Architecture
|
||||||
|
|
||||||
**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization:
|
**SyncManager** (`src/workers/SyncManager.ts`) provides background synchronization:
|
||||||
|
|
@ -154,16 +175,19 @@ When dropping events, snap to time grid:
|
||||||
### Testing with Vitest
|
### Testing with Vitest
|
||||||
|
|
||||||
Tests use **Vitest** with **jsdom** environment:
|
Tests use **Vitest** with **jsdom** environment:
|
||||||
|
- Config: `vitest.config.ts`
|
||||||
- Setup file: `test/setup.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>`
|
- Run single test: `npm test -- <test-file-name>`
|
||||||
|
|
||||||
## Key Files to Know
|
## 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/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/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/CalendarManager.ts` - Main coordinator
|
||||||
- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs
|
- `src/managers/DragDropManager.ts` - Detailed drag-drop architecture docs
|
||||||
- `src/configurations/CalendarConfig.ts` - Configuration schema
|
- `src/configurations/CalendarConfig.ts` - Configuration schema
|
||||||
|
|
@ -207,12 +231,15 @@ Access debug interface in browser console:
|
||||||
window.calendarDebug.eventBus.getEventLog()
|
window.calendarDebug.eventBus.getEventLog()
|
||||||
window.calendarDebug.calendarManager
|
window.calendarDebug.calendarManager
|
||||||
window.calendarDebug.eventManager
|
window.calendarDebug.eventManager
|
||||||
|
window.calendarDebug.auditService
|
||||||
|
window.calendarDebug.syncManager
|
||||||
|
window.calendarDebug.app // Full DI container
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **@novadi/core** - Dependency injection framework
|
- **@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
|
- **fuse.js** - Fuzzy search for event filtering
|
||||||
- **esbuild** - Fast bundler for development
|
- **esbuild** - Fast bundler for development
|
||||||
- **vitest** - Testing framework
|
- **vitest** - Testing framework
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Configuration } from '../configurations/CalendarConfig';
|
||||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||||
import { PositionUtils } from '../utils/PositionUtils';
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for event elements
|
* Base class for event elements
|
||||||
|
|
@ -172,7 +173,7 @@ export class SwpEventElement extends BaseSwpEventElement {
|
||||||
const clone = this.cloneNode(true) as SwpEventElement;
|
const clone = this.cloneNode(true) as SwpEventElement;
|
||||||
|
|
||||||
// Apply "clone-" prefix to ID
|
// 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
|
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||||
clone.style.pointerEvents = 'none';
|
clone.style.pointerEvents = 'none';
|
||||||
|
|
@ -343,7 +344,7 @@ export class SwpAllDayEventElement extends BaseSwpEventElement {
|
||||||
const clone = this.cloneNode(true) as SwpAllDayEventElement;
|
const clone = this.cloneNode(true) as SwpAllDayEventElement;
|
||||||
|
|
||||||
// Apply "clone-" prefix to ID
|
// 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
|
// Disable pointer events on clone so it doesn't interfere with hover detection
|
||||||
clone.style.pointerEvents = 'none';
|
clone.style.pointerEvents = 'none';
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { EventManager } from './EventManager';
|
import { EventManager } from './EventManager';
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllDayManager - Handles all-day row height animations and management
|
* AllDayManager - Handles all-day row height animations and management
|
||||||
|
|
@ -500,7 +501,7 @@ export class AllDayManager {
|
||||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
const eventId = EventId.from(clone.eventId);
|
||||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||||
|
|
||||||
// Determine target date based on mode
|
// Determine target date based on mode
|
||||||
|
|
@ -568,7 +569,7 @@ export class AllDayManager {
|
||||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
||||||
|
|
||||||
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
||||||
const eventId = clone.eventId.replace('clone-', '');
|
const eventId = EventId.from(clone.eventId);
|
||||||
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
||||||
|
|
||||||
// Determine target date based on mode
|
// Determine target date based on mode
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPa
|
||||||
import { DateService } from '../utils/DateService';
|
import { DateService } from '../utils/DateService';
|
||||||
import { EventStackManager } from '../managers/EventStackManager';
|
import { EventStackManager } from '../managers/EventStackManager';
|
||||||
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
||||||
|
import { EventId } from '../types/EventId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -165,22 +166,14 @@ export class DateEventRenderer implements IEventRenderer {
|
||||||
* Handle drag end event
|
* Handle drag end event
|
||||||
*/
|
*/
|
||||||
public handleDragEnd(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void {
|
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)
|
// Only fade out and remove if it's a swp-event (not swp-allday-event)
|
||||||
// AllDayManager handles removal of swp-allday-event elements
|
// AllDayManager handles removal of swp-allday-event elements
|
||||||
if (originalElement.tagName === 'SWP-EVENT') {
|
if (originalElement.tagName === 'SWP-EVENT') {
|
||||||
this.fadeOutAndRemove(originalElement);
|
this.fadeOutAndRemove(originalElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove clone prefix and normalize clone to be a regular event
|
draggedClone.dataset.eventId = EventId.from(draggedClone.dataset.eventId!);
|
||||||
const cloneId = draggedClone.dataset.eventId;
|
|
||||||
if (cloneId && cloneId.startsWith('clone-')) {
|
|
||||||
draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fully normalize the clone to be a regular event
|
// Fully normalize the clone to be a regular event
|
||||||
draggedClone.classList.remove('dragging');
|
draggedClone.classList.remove('dragging');
|
||||||
|
|
@ -192,7 +185,7 @@ export class DateEventRenderer implements IEventRenderer {
|
||||||
|
|
||||||
|
|
||||||
// Clean up any remaining day event clones
|
// 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) {
|
if (dayEventClone) {
|
||||||
dayEventClone.remove();
|
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