mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 01:17:31 +00:00
- Remove outdated feature requests from .github/CONTRIBUTING.md (GitLab support already exists) and add commit message format section - Improve PR template with type-of-change checkboxes and checklist - Update commit guideline links in README and CONTRIBUTING.md to reference the project's own format instead of external angular.js docs - Add "only edit en.json" rule to TRANSLATING.md and clarify workflow - Update add-new-integration.md provider list to match codebase (add Trello, ClickUp, Linear, Azure DevOps, Nextcloud Deck; note GitHub plugin migration; fix type name to BuiltInIssueProviderKey) - Add cross-references between mac certificate docs and remove 240-line duplicate section from update-mac-certificates.md - Clean up update-android-app.md (specify npm version args, collapse deprecated workflow, translate German UI labels to English) - Add context to howto-refresh-snap-credentials.md - Fix fine-grained token note in github-access-token-instructions.md - Fix absolute URL to relative path in gitlab-access-token-instructions.md - Fix grammar in i18n-script-usage.md - Add status headers to all 19 long-term plan files (Planned, Completed, Archived with reason, Investigation Complete) - Fix broken relative link in hybrid-manifest-architecture.md - Delete supersync-scenarios-simplified.md (duplicate of supersync-scenarios.md; known issues already covered there) - Rename vector-clock-pruning-research.md to vector-clock-history-and-alternatives.md for clarity
531 lines
14 KiB
Markdown
531 lines
14 KiB
Markdown
# Plugin System: View-Adapter API for Task Grouping
|
|
|
|
> **Status: Planned**
|
|
|
|
**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
|