|
|
@@ -0,0 +1,409 @@
|
|
|
+# Wekan Data Persistence Architecture - 2025-12-23
|
|
|
+
|
|
|
+**Status**: ✅ Latest Current
|
|
|
+**Updated**: 2025-12-23
|
|
|
+**Scope**: All data persistence related to swimlanes, lists, cards, checklists, checklistItems positioning and user preferences
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Executive Summary
|
|
|
+
|
|
|
+Wekan's data persistence architecture distinguishes between:
|
|
|
+- **Board-Level Data**: Shared across all users on a board (positions, widths, heights, order)
|
|
|
+- **Per-User Data**: Private to each user, not visible to others (collapse state, label visibility)
|
|
|
+
|
|
|
+This document defines the authoritative source of truth for all persistence decisions.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Data Classification Matrix
|
|
|
+
|
|
|
+### ✅ PER-BOARD LEVEL (Shared - Stored in MongoDB Documents)
|
|
|
+
|
|
|
+| Entity | Property | Storage | Format | Scope |
|
|
|
+|--------|----------|---------|--------|-------|
|
|
|
+| **Swimlane** | Title | MongoDB | String | Board |
|
|
|
+| **Swimlane** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
|
+| **Swimlane** | Background | MongoDB | Object {color} | Board |
|
|
|
+| **Swimlane** | Height | MongoDB | Number (-1=auto, 50-2000) | Board |
|
|
|
+| **Swimlane** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
+| **List** | Title | MongoDB | String | Board |
|
|
|
+| **List** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
|
+| **List** | Background | MongoDB | Object {color} | Board |
|
|
|
+| **List** | Width | MongoDB | Number (100-1000) | Board |
|
|
|
+| **List** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
+| **List** | WIP Limit | MongoDB | Object {enabled, value, soft} | Board |
|
|
|
+| **List** | Starred | MongoDB | Boolean | Board |
|
|
|
+| **Card** | Title | MongoDB | String | Board |
|
|
|
+| **Card** | Color | MongoDB | String (ALLOWED_COLORS) | Board |
|
|
|
+| **Card** | Background | MongoDB | Object {color} | Board |
|
|
|
+| **Card** | Description | MongoDB | String | Board |
|
|
|
+| **Card** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
+| **Card** | ListId | MongoDB | String | Board |
|
|
|
+| **Card** | SwimlaneId | MongoDB | String | Board |
|
|
|
+| **Checklist** | Title | MongoDB | String | Board |
|
|
|
+| **Checklist** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
+| **Checklist** | hideCheckedItems | MongoDB | Boolean | Board |
|
|
|
+| **Checklist** | hideAllItems | MongoDB | Boolean | Board |
|
|
|
+| **ChecklistItem** | Title | MongoDB | String | Board |
|
|
|
+| **ChecklistItem** | isFinished | MongoDB | Boolean | Board |
|
|
|
+| **ChecklistItem** | Position/Sort | MongoDB | Number (decimal) | Board |
|
|
|
+
|
|
|
+### 🔒 PER-USER ONLY (Private - User Profile or localStorage)
|
|
|
+
|
|
|
+| Entity | Property | Storage | Format | Users |
|
|
|
+|--------|----------|---------|--------|-------|
|
|
|
+| **User** | Collapsed Swimlanes | User Profile / Cookie | Object {boardId: {swimlaneId: boolean}} | Single |
|
|
|
+| **User** | Collapsed Lists | User Profile / Cookie | Object {boardId: {listId: boolean}} | Single |
|
|
|
+| **User** | Hide Minicard Label Text | User Profile / localStorage | Object {boardId: boolean} | Single |
|
|
|
+| **User** | Collapse Card Details View | Cookie | Boolean | Single |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Implementation Details
|
|
|
+
|
|
|
+### 1. Swimlanes Schema (swimlanes.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+Swimlanes.attachSchema(
|
|
|
+ new SimpleSchema({
|
|
|
+ title: { type: String }, // ✅ Per-board
|
|
|
+ color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
|
+ // background: { ...color properties... } // ✅ Per-board (for future use)
|
|
|
+ height: { // ✅ Per-board (NEW)
|
|
|
+ type: Number,
|
|
|
+ optional: true,
|
|
|
+ defaultValue: -1, // -1 means auto-height
|
|
|
+ custom() {
|
|
|
+ const h = this.value;
|
|
|
+ if (h !== -1 && (h < 50 || h > 2000)) {
|
|
|
+ return 'heightOutOfRange';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
|
+ boardId: { type: String }, // ✅ Per-board
|
|
|
+ archived: { type: Boolean }, // ✅ Per-board
|
|
|
+ // NOTE: Collapse state is per-user only, stored in:
|
|
|
+ // - User profile: profile.collapsedSwimlanes[boardId][swimlaneId] = boolean
|
|
|
+ // - Non-logged-in: Cookie 'wekan-collapsed-swimlanes'
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 2. Lists Schema (lists.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+Lists.attachSchema(
|
|
|
+ new SimpleSchema({
|
|
|
+ title: { type: String }, // ✅ Per-board
|
|
|
+ color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
|
+ // background: { ...color properties... } // ✅ Per-board (for future use)
|
|
|
+ width: { // ✅ Per-board (NEW)
|
|
|
+ type: Number,
|
|
|
+ optional: true,
|
|
|
+ defaultValue: 272, // default width in pixels
|
|
|
+ custom() {
|
|
|
+ const w = this.value;
|
|
|
+ if (w < 100 || w > 1000) {
|
|
|
+ return 'widthOutOfRange';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
|
+ swimlaneId: { type: String, optional: true }, // ✅ Per-board
|
|
|
+ boardId: { type: String }, // ✅ Per-board
|
|
|
+ archived: { type: Boolean }, // ✅ Per-board
|
|
|
+ wipLimit: { type: Object, optional: true }, // ✅ Per-board
|
|
|
+ starred: { type: Boolean, optional: true }, // ✅ Per-board
|
|
|
+ // NOTE: Collapse state is per-user only, stored in:
|
|
|
+ // - User profile: profile.collapsedLists[boardId][listId] = boolean
|
|
|
+ // - Non-logged-in: Cookie 'wekan-collapsed-lists'
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 3. Cards Schema (cards.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+Cards.attachSchema(
|
|
|
+ new SimpleSchema({
|
|
|
+ title: { type: String, optional: true }, // ✅ Per-board
|
|
|
+ color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
|
|
|
+ // background: { ...color properties... } // ✅ Per-board (for future use)
|
|
|
+ description: { type: String, optional: true }, // ✅ Per-board
|
|
|
+ sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
|
|
|
+ swimlaneId: { type: String }, // ✅ Per-board (REQUIRED)
|
|
|
+ listId: { type: String, optional: true }, // ✅ Per-board
|
|
|
+ boardId: { type: String, optional: true }, // ✅ Per-board
|
|
|
+ archived: { type: Boolean }, // ✅ Per-board
|
|
|
+ // ... other fields are all per-board
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 4. Checklists Schema (checklists.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+Checklists.attachSchema(
|
|
|
+ new SimpleSchema({
|
|
|
+ title: { type: String }, // ✅ Per-board
|
|
|
+ sort: { type: Number, decimal: true }, // ✅ Per-board
|
|
|
+ hideCheckedChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
|
|
|
+ hideAllChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
|
|
|
+ cardId: { type: String }, // ✅ Per-board
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 5. ChecklistItems Schema (checklistItems.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+ChecklistItems.attachSchema(
|
|
|
+ new SimpleSchema({
|
|
|
+ title: { type: String }, // ✅ Per-board
|
|
|
+ sort: { type: Number, decimal: true }, // ✅ Per-board
|
|
|
+ isFinished: { type: Boolean }, // ✅ Per-board
|
|
|
+ checklistId: { type: String }, // ✅ Per-board
|
|
|
+ cardId: { type: String }, // ✅ Per-board
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 6. User Schema - Per-User Data (users.js)
|
|
|
+
|
|
|
+```javascript
|
|
|
+// User.profile structure for per-user data
|
|
|
+user.profile = {
|
|
|
+ // Collapse states - per-user, per-board
|
|
|
+ collapsedSwimlanes: {
|
|
|
+ 'boardId123': {
|
|
|
+ 'swimlaneId456': true, // swimlane is collapsed for this user
|
|
|
+ 'swimlaneId789': false
|
|
|
+ },
|
|
|
+ 'boardId999': { ... }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Collapse states - per-user, per-board
|
|
|
+ collapsedLists: {
|
|
|
+ 'boardId123': {
|
|
|
+ 'listId456': true, // list is collapsed for this user
|
|
|
+ 'listId789': false
|
|
|
+ },
|
|
|
+ 'boardId999': { ... }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Label visibility - per-user, per-board
|
|
|
+ hideMiniCardLabelText: {
|
|
|
+ 'boardId123': true, // hide minicard labels on this board
|
|
|
+ 'boardId999': false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Client-Side Storage (Non-Logged-In Users)
|
|
|
+
|
|
|
+For users not logged in, collapse state is persisted via cookies (localStorage alternative):
|
|
|
+
|
|
|
+```javascript
|
|
|
+// Cookie: wekan-collapsed-swimlanes
|
|
|
+{
|
|
|
+ 'boardId123': {
|
|
|
+ 'swimlaneId456': true,
|
|
|
+ 'swimlaneId789': false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Cookie: wekan-collapsed-lists
|
|
|
+{
|
|
|
+ 'boardId123': {
|
|
|
+ 'listId456': true,
|
|
|
+ 'listId789': false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Cookie: wekan-card-collapsed
|
|
|
+{
|
|
|
+ 'state': false // is card details view collapsed
|
|
|
+}
|
|
|
+
|
|
|
+// localStorage: wekan-hide-minicard-label-{boardId}
|
|
|
+true or false
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Data Flow
|
|
|
+
|
|
|
+### ✅ Board-Level Data Flow (Swimlane Height Example)
|
|
|
+
|
|
|
+```
|
|
|
+1. User resizes swimlane in UI
|
|
|
+2. Client calls: Swimlanes.update(swimlaneId, { $set: { height: 300 } })
|
|
|
+3. MongoDB receives update
|
|
|
+4. Schema validation: height must be -1 or 50-2000
|
|
|
+5. Update stored in swimlanes collection: { _id, title, height: 300, ... }
|
|
|
+6. Update reflected in Swimlanes collection reactive
|
|
|
+7. All users viewing board see updated height
|
|
|
+8. Persists across page reloads
|
|
|
+9. Persists across browser restarts
|
|
|
+```
|
|
|
+
|
|
|
+### ✅ Per-User Data Flow (Collapse State Example)
|
|
|
+
|
|
|
+```
|
|
|
+1. User collapses swimlane in UI
|
|
|
+2. Client detects LOGGED-IN or NOT-LOGGED-IN
|
|
|
+3. If LOGGED-IN:
|
|
|
+ a. Client calls: Meteor.call('setCollapsedSwimlane', boardId, swimlaneId, true)
|
|
|
+ b. Server updates user profile: { profile: { collapsedSwimlanes: { ... } } }
|
|
|
+ c. Stored in users collection
|
|
|
+4. If NOT-LOGGED-IN:
|
|
|
+ a. Client writes to cookie: wekan-collapsed-swimlanes
|
|
|
+ b. Stored in browser cookies
|
|
|
+5. On next page load:
|
|
|
+ a. Client reads from profile (logged-in) or cookie (not logged-in)
|
|
|
+ b. UI restored to saved state
|
|
|
+6. Collapse state NOT visible to other users
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Validation Rules
|
|
|
+
|
|
|
+### Swimlane Height Validation
|
|
|
+- **Allowed Values**: -1 (auto) or 50-2000 pixels
|
|
|
+- **Default**: -1 (auto)
|
|
|
+- **Trigger**: On insert/update
|
|
|
+- **Action**: Reject if invalid
|
|
|
+
|
|
|
+### List Width Validation
|
|
|
+- **Allowed Values**: 100-1000 pixels
|
|
|
+- **Default**: 272 pixels
|
|
|
+- **Trigger**: On insert/update
|
|
|
+- **Action**: Reject if invalid
|
|
|
+
|
|
|
+### Collapse State Validation
|
|
|
+- **Allowed Values**: true or false
|
|
|
+- **Storage**: Only boolean values allowed
|
|
|
+- **Trigger**: On read/write to profile
|
|
|
+- **Action**: Remove if corrupted
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Migration Strategy
|
|
|
+
|
|
|
+### For Existing Installations
|
|
|
+
|
|
|
+1. **Add new fields to schemas**
|
|
|
+ - `Swimlanes.height` (default: -1)
|
|
|
+ - `Lists.width` (default: 272)
|
|
|
+
|
|
|
+2. **Populate existing data**
|
|
|
+ - For swimlanes without height: set to -1 (auto)
|
|
|
+ - For lists without width: set to 272 (default)
|
|
|
+
|
|
|
+3. **Remove per-user storage if present**
|
|
|
+ - Check user.profile.swimlaneHeights → migrate to swimlane.height
|
|
|
+ - Check user.profile.listWidths → migrate to list.width
|
|
|
+ - Remove old fields from user profile
|
|
|
+
|
|
|
+4. **Validation migration**
|
|
|
+ - Ensure all swimlaneIds are valid (no orphaned data)
|
|
|
+ - Ensure all widths/heights are in valid range
|
|
|
+ - Clean corrupted per-user data
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Security Implications
|
|
|
+
|
|
|
+### Per-User Data (🔒 Private)
|
|
|
+- Collapse state is per-user → User A's collapse setting doesn't affect User B's view
|
|
|
+- Hide label setting is per-user → User A's label visibility doesn't affect User B
|
|
|
+- Stored in user profile → Only accessible to that user
|
|
|
+- Cookies for non-logged-in → Stored locally, not transmitted
|
|
|
+
|
|
|
+### Per-Board Data (✅ Shared)
|
|
|
+- Heights/widths are shared → All users see same swimlane/list sizes
|
|
|
+- Positions are shared → All users see same card order
|
|
|
+- Colors are shared → All users see same visual styling
|
|
|
+- Stored in MongoDB → All users can query and receive updates
|
|
|
+
|
|
|
+### No Cross-User Leakage
|
|
|
+- User A's preferences never stored in User B's profile
|
|
|
+- User A's preferences never affect User B's view
|
|
|
+- Each user has isolated per-user data space
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Testing Checklist
|
|
|
+
|
|
|
+### Per-Board Data Tests
|
|
|
+- [ ] Resize swimlane height → all users see change
|
|
|
+- [ ] Resize list width → all users see change
|
|
|
+- [ ] Move card between lists → all users see change
|
|
|
+- [ ] Change card color → all users see change
|
|
|
+- [ ] Reload page → changes persist
|
|
|
+- [ ] Different browser → changes persist
|
|
|
+
|
|
|
+### Per-User Data Tests
|
|
|
+- [ ] User A collapses swimlane → User B sees it expanded
|
|
|
+- [ ] User A hides labels → User B sees labels
|
|
|
+- [ ] User A scrolls away → User B can collapse same swimlane
|
|
|
+- [ ] Logout → cookies maintain collapse state
|
|
|
+- [ ] Login as different user → previous collapse state not visible
|
|
|
+- [ ] Reload page → collapse state restored for user
|
|
|
+
|
|
|
+### Validation Tests
|
|
|
+- [ ] Set swimlane height = 25 → rejected (< 50)
|
|
|
+- [ ] Set swimlane height = 3000 → rejected (> 2000)
|
|
|
+- [ ] Set list width = 50 → rejected (< 100)
|
|
|
+- [ ] Set list width = 2000 → rejected (> 1000)
|
|
|
+- [ ] Corrupt localStorage height → cleaned on startup
|
|
|
+- [ ] Corrupt user profile height → cleaned on startup
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Related Files
|
|
|
+
|
|
|
+| File | Purpose |
|
|
|
+|------|---------|
|
|
|
+| [models/swimlanes.js](../../../models/swimlanes.js) | Swimlane model with height field |
|
|
|
+| [models/lists.js](../../../models/lists.js) | List model with width field |
|
|
|
+| [models/cards.js](../../../models/cards.js) | Card model with position tracking |
|
|
|
+| [models/checklists.js](../../../models/checklists.js) | Checklist model |
|
|
|
+| [models/checklistItems.js](../../../models/checklistItems.js) | ChecklistItem model |
|
|
|
+| [models/users.js](../../../models/users.js) | User model with per-user settings |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Glossary
|
|
|
+
|
|
|
+| Term | Definition |
|
|
|
+|------|-----------|
|
|
|
+| **Per-Board** | Stored in swimlane/list/card document, visible to all users |
|
|
|
+| **Per-User** | Stored in user profile/cookie, visible only to that user |
|
|
|
+| **Sort** | Decimal number determining visual order of entity |
|
|
|
+| **Height** | Pixel measurement of swimlane vertical size |
|
|
|
+| **Width** | Pixel measurement of list horizontal size |
|
|
|
+| **Collapse** | Hiding swimlane/list/card from view (per-user preference) |
|
|
|
+| **Position** | Combination of swimlaneId/listId and sort value |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Change Log
|
|
|
+
|
|
|
+| Date | Change | Impact |
|
|
|
+|------|--------|--------|
|
|
|
+| 2025-12-23 | Created comprehensive architecture document | Documentation |
|
|
|
+| 2025-12-23 | Added height field to Swimlanes | Per-board storage |
|
|
|
+| 2025-12-23 | Added width field to Lists | Per-board storage |
|
|
|
+| 2025-12-23 | Defined per-user data as collapse + label visibility | Architecture |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**Status**: ✅ Complete and Current
|
|
|
+**Next Review**: Upon next architectural change
|
|
|
+
|