docs: add plan

This commit is contained in:
Johannes Millan 2026-01-21 12:39:32 +01:00
parent 9b2afbe109
commit 0b266bebc5

View file

@ -0,0 +1,529 @@
# Plugin System: View-Adapter API for Task Grouping
**Date:** 2026-01-20
**Approach:** Option B - Simpler view-adapter API (not full wrapping system)
**Estimated Complexity:** ~500 lines of code
## Overview
Enable plugins to provide custom task grouping (e.g., sections, kanban boards) while core handles all rendering. Plugins provide **grouping logic**, core provides **UI rendering**.
**Key Benefits:**
- Simple: Reuses existing TaskViewCustomizerService infrastructure
- Performant: No iframe-per-slot, just function calls
- Sync-compatible: Plugin state via existing persistDataSynced()
- Type-safe: Full TypeScript support via PluginAPI
## Architecture
```
Plugin (JS code)
↓ registerTaskGrouping({ id, label, groupFn })
PluginTaskGroupingService (new)
↓ exposes groupingOptions signal
TaskViewCustomizerService (modified)
↓ calls plugin groupFn when selected
WorkViewComponent (minimal template changes)
↓ renders groups using existing <collapsible> + <task-list>
```
## Implementation Plan
### Task 1: Create Core Grouping Service
**File:** `src/app/plugins/plugin-task-grouping.service.ts` (NEW, ~150 lines)
**What it does:**
- Stores plugin grouping registrations in a signal
- Exposes `groupingOptions()` for UI integration
- Provides `applyPluginGrouping(id, tasks)` to execute grouping
- Implements caching (1 second) and timeout (5 seconds) for performance
- Cleans up when plugins are unloaded
**Key types:**
```typescript
interface PluginTaskGrouping {
id: string;
label: string;
icon?: string;
groupFn: (tasks: Task[]) => Promise<PluginTaskGroup[]> | PluginTaskGroup[];
getGroupMetadata?: (groupKey: string) => PluginGroupMetadata;
}
interface PluginTaskGroup {
key: string;
label: string;
tasks: Task[];
icon?: string;
color?: string;
order?: number;
}
```
**Verification:**
- Unit test: Register grouping, verify it appears in groupingOptions()
- Unit test: applyPluginGrouping returns correct format
- Unit test: Timeout protection (mock slow groupFn)
- Unit test: Caching works (same tasks = cached result)
---
### Task 2: Extend PluginAPI
**Files to modify:**
- `src/app/plugins/plugin-api.ts` (~30 lines)
- `src/app/plugins/plugin-bridge.service.ts` (~20 lines)
- `packages/plugin-api/src/types.ts` (~40 lines)
**Changes:**
1. **Add method to PluginAPI class:**
```typescript
registerTaskGrouping(grouping: PluginTaskGrouping): void {
this._sendMessage({
type: 'API_CALL',
method: 'registerTaskGrouping',
args: [this._pluginId, grouping],
});
}
unregisterTaskGrouping(id: string): void {
// ...
}
```
2. **Add to PluginBridgeService.createBoundMethods():**
```typescript
registerTaskGrouping: (grouping: PluginTaskGrouping) => {
this._pluginTaskGroupingService.registerGrouping(pluginId, grouping);
},
```
3. **Export types in packages/plugin-api:**
```typescript
export interface PluginTaskGrouping {
/* ... */
}
export interface PluginTaskGroup {
/* ... */
}
export interface PluginGroupMetadata {
/* ... */
}
```
**Verification:**
- Build plugin-api package: `npm run build:plugin-api`
- TypeScript types are exported
- Integration test: Plugin can call registerTaskGrouping()
---
### Task 3: Integrate with TaskViewCustomizerService
**File:** `src/app/features/task-view-customizer/task-view-customizer.service.ts` (~50 lines modified)
**Changes:**
1. **Inject PluginTaskGroupingService:**
```typescript
private _pluginGroupingService = inject(PluginTaskGroupingService);
```
2. **Expose combined grouping options:**
```typescript
public availableGroupOptions = computed(() => {
const builtIn = OPTIONS.group.list;
const pluginOptions = this._pluginGroupingService.groupingOptions();
return [...builtIn, ...pluginOptions];
});
```
3. **Update applyGrouping to handle plugin groupings:**
```typescript
private async applyGrouping(
tasks: TaskWithSubTasks[],
groupType: GROUP_OPTION_TYPE | null,
pluginGroupingId?: string,
): Promise<Record<string, TaskWithSubTasks[]>> {
if (groupType === GROUP_OPTION_TYPE.plugin && pluginGroupingId) {
return this._pluginGroupingService.applyPluginGrouping(
pluginGroupingId,
tasks,
);
}
// Existing built-in grouping logic unchanged...
}
```
**Files to modify:**
- `src/app/features/task-view-customizer/types.ts` (~10 lines)
- Add `plugin` to GROUP_OPTION_TYPE enum
- Add `pluginId?` and `pluginGroupingId?` to GroupOption interface
**Verification:**
- Start app with test plugin
- Plugin grouping appears in customizer dropdown
- Selecting plugin grouping applies grouping correctly
- Console shows no errors
---
### Task 4: Update Work View Template
**File:** `src/app/features/work-view/work-view.component.html` (~10 lines modified)
**Changes:**
1. **Create metadata pipe** (NEW file: `src/app/ui/pipes/plugin-group-metadata.pipe.ts`, ~40 lines):
```typescript
@Pipe({ name: 'pluginGroupMetadata', standalone: true })
export class PluginGroupMetadataPipe implements PipeTransform {
transform(groupKey: string): { label: string; icon?: string } {
// Gets metadata from plugin or falls back to groupKey
}
}
```
2. **Update template to use metadata:**
```html
@for (group of customized.grouped | keyvalue; track group.key) { @let metadata = group.key
| pluginGroupMetadata;
<collapsible
[title]="metadata.label"
[icon]="metadata.icon"
[isIconBefore]="true"
[isExpanded]="true"
>
<task-list
[tasks]="group.value"
listId="PARENT"
listModelId="UNDONE"
></task-list>
</collapsible>
}
```
**Verification:**
- Plugin-provided group labels render correctly
- Icons appear if provided by plugin
- Built-in groups still work (backward compatibility)
---
### Task 5: Plugin Cleanup Integration
**File:** `src/app/plugins/plugin-cleanup.service.ts` (~10 lines)
**Changes:**
Add cleanup of groupings when plugin is unloaded:
```typescript
unload(pluginId: string): void {
// ... existing cleanup ...
this._pluginTaskGroupingService.cleanupPlugin(pluginId);
}
```
**Verification:**
- Disable plugin in settings
- Plugin grouping option disappears from UI
- No memory leaks (check with Chrome DevTools)
---
### Task 6: Documentation & Example Plugin
**Files to create:**
1. **`docs/plugin-api-task-grouping.md`** (~100 lines)
- API reference for registerTaskGrouping
- Type definitions
- Best practices (performance, state management)
- Complete sections plugin example
2. **`packages/plugin-dev/sections-plugin-example/`** (example plugin)
- `manifest.json`
- `plugin.js` - Implements sections grouping
- `README.md` - Usage instructions
- Demonstrates:
- Registering grouping
- Persisting section assignments via persistDataSynced()
- Using ANY_TASK_UPDATE hook to refresh grouping
**Verification:**
- Build example plugin
- Install in app
- Create sections, assign tasks
- Verify sections sync across browser tabs (persistDataSynced)
- Verify sections work after app reload
---
## Critical Files Summary
**New files (~290 lines):**
- `src/app/plugins/plugin-task-grouping.service.ts` (~150 lines)
- `src/app/ui/pipes/plugin-group-metadata.pipe.ts` (~40 lines)
- `docs/plugin-api-task-grouping.md` (~100 lines)
**Modified files (~200 lines changes):**
- `src/app/plugins/plugin-api.ts` (~30 lines)
- `src/app/plugins/plugin-bridge.service.ts` (~20 lines)
- `src/app/features/task-view-customizer/task-view-customizer.service.ts` (~50 lines)
- `src/app/features/task-view-customizer/types.ts` (~10 lines)
- `src/app/features/work-view/work-view.component.html` (~10 lines)
- `src/app/plugins/plugin-cleanup.service.ts` (~10 lines)
- `packages/plugin-api/src/types.ts` (~40 lines)
**Total: ~490 lines** ✓
---
## Example: Sections Plugin
```typescript
// sections-plugin.js
let taskSections = {}; // { taskId: sectionName }
// Load persisted section assignments
plugin.loadPersistedData().then((data) => {
taskSections = data ? JSON.parse(data) : {};
});
// Register grouping
plugin.registerTaskGrouping({
id: 'sections',
label: 'By Section',
icon: 'category',
groupFn: async (tasks) => {
const groups = new Map();
const sectionOrder = ['Urgent', 'Today', 'This Week', 'Backlog'];
for (const task of tasks) {
const section = taskSections[task.id] || 'Uncategorized';
if (!groups.has(section)) {
groups.set(section, []);
}
groups.get(section).push(task);
}
return Array.from(groups.entries()).map(([key, tasks]) => ({
key,
label: key,
tasks,
icon: key === 'Urgent' ? 'priority_high' : 'folder',
order: sectionOrder.indexOf(key),
}));
},
getGroupMetadata: (groupKey) => ({
label: groupKey,
icon: groupKey === 'Urgent' ? 'priority_high' : 'folder',
}),
});
// Helper: Assign task to section
async function setTaskSection(taskId, sectionName) {
taskSections[taskId] = sectionName;
await plugin.persistDataSynced(JSON.stringify(taskSections));
}
// Provide UI to move tasks (via header button)
plugin.registerHeaderButton({
label: 'Manage Sections',
icon: 'category',
onClick: () => {
plugin.showIndexHtmlAsView(); // Show section management UI
},
});
```
---
## Testing Strategy
### Unit Tests
**PluginTaskGroupingService:**
- Registration adds grouping to signal
- applyPluginGrouping executes groupFn correctly
- Timeout protection (5s limit)
- Caching works (same task IDs = cached result)
- Cleanup removes all groupings for plugin
**PluginGroupMetadataPipe:**
- Returns metadata for plugin groups
- Falls back for built-in groups
- Handles missing metadata gracefully
### Integration Tests
**E2E test:** `e2e/tests/plugins/task-grouping.spec.ts`
- Load test plugin with grouping
- Select plugin grouping in customizer
- Verify tasks are grouped correctly in UI
- Verify groups have correct labels/icons
- Disable plugin → grouping option disappears
### Manual Testing Checklist
- [ ] Plugin grouping appears in customizer dropdown
- [ ] Selecting grouping shows tasks in groups
- [ ] Group labels and icons render correctly
- [ ] Collapsible groups work (expand/collapse)
- [ ] Drag-drop resets grouping (existing behavior)
- [ ] Plugin data syncs across browser tabs
- [ ] Groups persist after page reload (via persistDataSynced)
- [ ] Disabling plugin removes grouping option
- [ ] No console errors or warnings
- [ ] Performance: 100+ tasks group in < 1 second
---
## Performance Considerations
**Timeout protection:**
- groupFn execution limited to 5 seconds
- Prevents slow plugins from freezing UI
- Falls back to "All Tasks" group on timeout
**Caching:**
- Results cached for 1 second
- Cache invalidated when task list changes (compare task IDs)
- Prevents re-running expensive grouping on every render
**Memory:**
- Cache cleared after 1 second
- Max ~10KB per grouping
- Cleanup on plugin unload
---
## Sync & Operation Log
**What syncs:**
- Plugin state (via `persistDataSynced()`) → creates operation in op-log
- Syncs across all devices running the same plugin
**What doesn't sync:**
- Grouping function code (plugin installed locally)
- Selected grouping option (local UI preference)
- Collapsed/expanded state (local UI state)
**Cross-device behavior:**
- Device A: Plugin installed, assigns tasks to sections
- Device B (with plugin): Loads synced section data, grouping works
- Device B (no plugin): Data syncs but has no effect, tasks visible in default view
---
## Risks & Mitigations
| Risk | Impact | Mitigation |
| ------------------------------------ | ------ | ----------------------------------------------------------- |
| Slow groupFn blocks UI | Medium | 5s timeout, caching, performance guidelines in docs |
| Plugin state corruption | Low | try/catch + fallback to "All Tasks" group |
| Drag-drop UX unclear | Low | Use existing behavior (reset grouping), document limitation |
| Plugin not installed on other device | Low | Tasks still accessible, just not grouped |
---
## Future Enhancements (Not in MVP)
These can be added later without breaking changes:
1. **Drag-between-groups:**
- Add optional `onTaskMoved(taskId, fromGroup, toGroup)` callback
- Plugin updates state when task dragged to different group
2. **Context menu integration:**
- New hook: `TASK_CONTEXT_MENU_OPEN`
- Plugins can add "Move to Section" menu items
3. **Loading states:**
- Show spinner while groupFn executes
- Better UX for slow grouping functions
4. **Group statistics:**
- Show task count per group in header
- Optional metadata field: `count?: number`
5. **Nested groups:**
- Support hierarchical grouping (e.g., Project → Section → Priority)
- `PluginTaskGroup.subGroups?: PluginTaskGroup[]`
---
## Success Criteria
✅ Plugins can register custom grouping via `registerTaskGrouping()`
✅ Plugin groupings appear in customizer UI
✅ Core renders groups using existing components
✅ Plugin state syncs via `persistDataSynced()`
✅ Performance: < 5% overhead with plugin grouping
✅ ~500 lines of implementation code
✅ Type-safe plugin development
✅ Backward compatible (no breaking changes)
✅ Example sections plugin works end-to-end
✅ All tests passing
---
## Open Questions
None - design is ready for implementation.
---
## Confidence: 90%
**Strengths:**
- Reuses existing architecture (TaskViewCustomizerService, signals, persistence)
- Simple implementation (~500 lines)
- No performance concerns (timeout + caching)
- Clean plugin API
**Potential Issues:**
- Drag-drop UX limitation (resets grouping) - but acceptable for MVP
- Plugin must handle async state loading - documented in example
**Side Effects:**
- Minimal: just extends existing customizer infrastructure
- No breaking changes to core or existing plugins