docs: Guide Notion Database & Tasks — analyse complète pour FlowDeck
- Anatomie des Databases Notion (Collection/CollectionView/Pages) - 21 types de propriétés, 6 types de vues, Relations & Rollups - Architecture des Tasks (sub-items auto-référencés, dépendances) - Plan d'implémentation en 6 phases avec SQL, Python et HTML - Rétrocompatibilité avec les boards Gitea existants
This commit is contained in:
@@ -0,0 +1,822 @@
|
||||
# Guide Technique — Recréer Database & Tasks de Notion dans FlowDeck
|
||||
|
||||
> **Version:** 1.0 · **Date:** 2026-07-10
|
||||
> **Auteur:** Hermes-Deepin · **Cible:** FlowDeck v1.2+
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Comprendre Notion : le modèle « Blocs & Collections »](#1-comprendre-notion--le-modèle-blocs--collections)
|
||||
3. [La Database Notion : anatomie complète](#2-la-database-notion--anatomie-complète)
|
||||
4. [Les Tasks Notion : un cas spécial de database](#3-les-tasks-notion--un-cas-spécial-de-database)
|
||||
5. [Comment Notion connecte Database et Tasks](#4-comment-notion-connecte-database-et-tasks)
|
||||
6. [Implémentation dans FlowDeck : plan pas à pas](#5-implémentation-dans-flowdeck--plan-pas-à-pas)
|
||||
7. [Références](#6-références)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
L'objectif est de **recréer dans FlowDeck** les deux fonctionnalités les plus puissantes de Notion :
|
||||
|
||||
- **Les Databases** — tables structurées avec propriétés typées, vues multiples, filtres et tris
|
||||
- **Les Tasks** — sous-tâches hiérarchiques avec dépendances, liées aux databases
|
||||
|
||||
Ces deux systèmes sont **profondément interconnectés** dans Notion : les tâches ne sont qu'une forme spécialisée de database, avec un parent auto-référencé et des propriétés de dépendance.
|
||||
|
||||
### État actuel de FlowDeck (v1.2)
|
||||
|
||||
FlowDeck a déjà une base solide :
|
||||
|
||||
```
|
||||
✅ Boards (Kanban lié à Gitea) → table `boards`
|
||||
✅ Cards (issues → colonnes) → table `cards`
|
||||
✅ Colonnes custom → `project_properties` + `property_values`
|
||||
✅ Checklists par issue → `checklists` + `checklist_items`
|
||||
✅ Vues multiples → Kanban, Table, Status, TeamLoad, Detailed
|
||||
✅ Notes Markdown → `notes`
|
||||
✅ Pages (ébauche Notion editor) → `pages` (content, parent_id, sort_order, content_format)
|
||||
⚠️ Database concept → ABSENT (pas de notion de collection/row)
|
||||
❌ Relations entre projets → ABSENT
|
||||
❌ Sub-tasks / Parent-item → ABSENT
|
||||
❌ Rollups / Formules → ABSENT
|
||||
❌ Dependencies (bloque/bloqué par) → ABSENT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Comprendre Notion : le modèle « Blocs & Collections »
|
||||
|
||||
### 1.1 Le principe fondateur : « Everything is a Block »
|
||||
|
||||
Dans Notion, **chaque élément de contenu est un bloc**. Un bloc peut être :
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ BLOC │
|
||||
│ ├─ id: UUID │
|
||||
│ ├─ type: "paragraph" │
|
||||
│ │ "heading_1" │
|
||||
│ │ "to_do" │
|
||||
│ │ "bulleted_list_item" │
|
||||
│ │ "child_page" │
|
||||
│ │ "child_database" │
|
||||
│ │ "collection_view_page" │ ← Database
|
||||
│ │ ... │
|
||||
│ ├─ content: [...] (blocs enfants) │
|
||||
│ ├─ properties: {} (metadata) │
|
||||
│ └─ parent: {type, id} │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 Les 3 couches d'une Database Notion
|
||||
|
||||
Une database Notion est un assemblage de **3 concepts distincts** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE NOTION │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────────┐ ┌────────────┐│
|
||||
│ │ COLLECTION │ │ COLLECTION_VIEW │ │ PAGES ││
|
||||
│ │ (schema) │◄───│ (présentation) │◄───│ (data) ││
|
||||
│ │ │ │ │ │ ││
|
||||
│ │ properties │ │ type: table │ │ row 1 ││
|
||||
│ │ - Name │ │ board │ │ row 2 ││
|
||||
│ │ - Status │ │ calendar │ │ row 3 ││
|
||||
│ │ - Priority │ │ gallery │ │ ... ││
|
||||
│ │ - Due Date │ │ list │ │ ││
|
||||
│ │ - Assignee │ │ timeline │ │ ││
|
||||
│ │ │ │ │ │ ││
|
||||
│ │ │ │ filters │ │ ││
|
||||
│ │ │ │ sorts │ │ ││
|
||||
│ │ │ │ grouping │ │ ││
|
||||
│ │ │ │ format options │ │ ││
|
||||
│ └─────────────┘ └──────────────────┘ └────────────┘│
|
||||
│ │
|
||||
│ 1 SCHÉMA N VUES (filtres N LIGNES │
|
||||
│ partagé + tris différents) (pages filles) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Concept clé** : Une database n'est PAS une table SQL. C'est une **collection de pages**, chaque page ayant un schéma de propriétés commun, affiché via des vues configurables.
|
||||
|
||||
### 1.3 Le cycle de vie d'une Database
|
||||
|
||||
```
|
||||
Création Consultation Modification
|
||||
───────── ──────────── ────────────
|
||||
|
||||
1. L'utilisateur choisit 4. Les vues lisent le 7. Édition inline :
|
||||
un type de vue schéma + les pages - click → édite valeur
|
||||
(table, board...) - drag → réorganise
|
||||
5. Filtres/sorts/tri - shortcuts → ajoute ligne
|
||||
2. Notion crée : appliqués
|
||||
- 1 Collection 8. Changement de vue =
|
||||
- 1 CollectionView 6. Rendu en temps réel même données,
|
||||
- 1 CollectionViewPage autre présentation
|
||||
- + pages vides optionnelles
|
||||
9. Ajout propriété =
|
||||
3. L'utilisateur définit nouveau champ sur
|
||||
les propriétés (schéma) TOUTES les pages
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. La Database Notion : anatomie complète
|
||||
|
||||
### 2.1 Propriétés (le schéma)
|
||||
|
||||
Notion supporte **21 types de propriétés**. Toutes sont définies au niveau de la Collection :
|
||||
|
||||
```
|
||||
Type │ Description │ Exemple de valeur
|
||||
──────────────────┼────────────────────────────┼──────────────────
|
||||
Title │ Le titre de la page (obligatoire, unique)
|
||||
Text │ Texte libre │ "Description..."
|
||||
Number │ Nombre (décimales, format) │ 42 / 12.5%
|
||||
Select │ Choix unique │ "En cours"
|
||||
Multi-select │ Choix multiples │ ["Frontend","Backend"]
|
||||
Status │ Select avec couleur forcée │ "Done" (vert)
|
||||
Date │ Date + heure optionnelle │ 2026-07-15
|
||||
Person │ Mention utilisateur │ ["bruno"]
|
||||
Files & media │ Uploads │ [url1, url2]
|
||||
Checkbox │ Booléen │ true / false
|
||||
URL │ Lien cliquable │ "https://..."
|
||||
Email │ Email cliquable │ "[email protected]"
|
||||
Phone │ Téléphone cliquable │ "+1..."
|
||||
Formula │ Calcul (JS-like) │ prop("Prix")*prop("Qté") ← CLÉ
|
||||
Relation │ Lien vers autre database │ [page_id_1, page_id_2] ← CLÉ
|
||||
Rollup │ Agrégation via relation │ sum, avg, count, min, max ← CLÉ
|
||||
Created time │ Date de création (auto) │ 2026-01-15T10:30:00Z
|
||||
Created by │ Auteur (auto) │ "bruno"
|
||||
Last edited time │ Dernière édition (auto) │ 2026-07-09T18:00:00Z
|
||||
Last edited by │ Dernier éditeur (auto) │ "bruno"
|
||||
Unique ID │ ID incrémental (auto) │ 42
|
||||
Button │ Déclenche action (nouveau) │ —
|
||||
AI Summary │ Résumé AI (nouveau) │ "Ce projet..."
|
||||
```
|
||||
|
||||
### 2.2 Views (les vues)
|
||||
|
||||
Une database peut avoir **N vues**, chaque vue avec :
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Vue "Kanban" │
|
||||
│ ├─ type: board │
|
||||
│ ├─ group_by: "Status" │ ← Groupement
|
||||
│ ├─ filter: {Status ≠ "Archivé"} │ ← Filtres
|
||||
│ ├─ sort: [Due Date ASC, Priority DESC] │ ← Tris
|
||||
│ ├─ visible_properties: [Name, Assignee, Due Date] │
|
||||
│ ├─ card_size: medium │
|
||||
│ └─ cover_image_property: "Files" │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**6 types de vues :**
|
||||
|
||||
| Vue | Usage | Particularité |
|
||||
|-----|-------|---------------|
|
||||
| **Table** | Liste classique | Colonnes = propriétés |
|
||||
| **Board** | Kanban | Groupé par propriété Select/Status |
|
||||
| **Timeline** | Gantt | Axe X = propriété Date |
|
||||
| **Calendar** | Calendrier | Groupé par propriété Date (jour/semaine/mois) |
|
||||
| **Gallery** | Cartes visuelles | Cover image + preview |
|
||||
| **List** | Liste compacte | Comme table mais 1 colonne + preview |
|
||||
|
||||
**Filtres avancés :**
|
||||
|
||||
```
|
||||
Filtre 1 : Status IS "En cours" → filtre simple
|
||||
Filtre 2 : Due Date IS BEFORE "today" → filtre date
|
||||
Filtre 3 : Priority CONTAINS "P1" → filtre multi-select
|
||||
Groupe : AND / OR entre les filtres
|
||||
Sous-groupe : (FiltreA OR FiltreB) AND FiltreC
|
||||
```
|
||||
|
||||
### 2.3 Relations et Rollups (les super-pouvoirs)
|
||||
|
||||
C'est LE mécanisme qui rend Notion puissant :
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌──────────────────────┐
|
||||
│ DATABASE A │ │ DATABASE B │
|
||||
│ « Projets » │ │ « Tâches » │
|
||||
│ │ │ │
|
||||
│ ┌─────────────┐ │ relation│ ┌──────────────┐ │
|
||||
│ │ Nom │ │◄───────►│ │ Titre │ │
|
||||
│ │ Description │ │ │ │ Priorité │ │
|
||||
│ │ Statut │ │ │ │ Projet ──────┼──┐ │
|
||||
│ └─────────────┘ │ │ │ Assignee │ │ │
|
||||
│ │ │ └──────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ ROLLUP « Nb tâches»│◄────────┼─── count(Tâches) │ │
|
||||
│ ROLLUP « Avancement│◄────────┼─── avg(%Complété) │ │
|
||||
│ ROLLUP « Prochaine │◄────────┼─── min(Due Date) │ │
|
||||
└─────────────────────┘ └───────────────────┘──┘
|
||||
```
|
||||
|
||||
**Relation** = pointeur bidirectionnel entre pages de 2 databases différentes
|
||||
**Rollup** = agrégation (COUNT, SUM, AVG, MIN, MAX, RANGE, UNIQUE) sur une propriété de la database liée
|
||||
|
||||
---
|
||||
|
||||
## 3. Les Tasks Notion : un cas spécial de database
|
||||
|
||||
### 3.1 Architecture des tâches
|
||||
|
||||
Contrairement à ce qu'on pourrait penser, **Notion n'a pas de module « Tasks » séparé**.
|
||||
|
||||
```
|
||||
Les tâches sont simplement des PAGES dans une DATABASE
|
||||
qui possède des propriétés spécifiques de type tâche.
|
||||
```
|
||||
|
||||
Voici comment Notion les distingue :
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE « Tasks » (ou n'importe quelle DB) │
|
||||
│ │
|
||||
│ Propriétés « tâche » │ Propriétés « standard » │
|
||||
│ ────────────────────────── │ ────────────────────────── │
|
||||
│ ✅ Sub-item (parent) │ • Titre (obligatoire) │
|
||||
│ ✅ Dependencies (bloque) │ • Status (Select) │
|
||||
│ ✅ Due Date │ • Priority (Select) │
|
||||
│ ✅ Assignee │ • Tags (Multi-select) │
|
||||
│ ✅ Reminder │ • ... │
|
||||
│ │
|
||||
│ La magie : ces propriétés SONT la database. │
|
||||
│ Pas de modèle « Task » séparé. │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Sub-items (sous-tâches)
|
||||
|
||||
Les sous-tâches utilisent une **relation auto-référencée** :
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Table « tasks » (dans la DB) │
|
||||
│ │
|
||||
│ id │ title │ parent_id │
|
||||
│ ───┼────────────────┼───────────────│
|
||||
│ 1 │ Refonte UI │ NULL │ ← Tâche parent
|
||||
│ 2 │ Nouveau header │ 1 │ ← Sous-tâche de 1
|
||||
│ 3 │ Dark mode │ 1 │ ← Sous-tâche de 1
|
||||
│ 4 │ Tests header │ 2 │ ← Sous-sous-tâche de 2
|
||||
│ 5 │ Déploiement │ NULL │ ← Autre tâche parent
|
||||
│ │
|
||||
│ Propriétés spéciales : │
|
||||
│ ├─ « Sub-item » = relation vers │
|
||||
│ │ la même database │
|
||||
│ └─ « Parent item » = l'inverse │
|
||||
│ (auto-créé par Notion) │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Comment ça fonctionne techniquement :**
|
||||
|
||||
1. L'utilisateur active « Sub-items » dans une database
|
||||
2. Notion crée automatiquement 2 propriétés de type **Relation** pointant vers la même database :
|
||||
- `Sub-item` (relation → même DB)
|
||||
- `Parent item` (relation inverse, auto-générée)
|
||||
3. Dans l'interface Kanban, les sous-tâches apparaissent imbriquées sous le parent
|
||||
4. La récursion est illimitée (tâche → sous-tâche → sous-sous-tâche → ...)
|
||||
|
||||
### 3.3 Dependencies (dépendances)
|
||||
|
||||
Les dépendances bloquantes sont une **seconde relation auto-référencée** :
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Relation « Bloque » │
|
||||
│ │
|
||||
│ Tâche A ──bloque──► Tâche B │
|
||||
│ Tâche B ──bloquée par──► Tâche A │
|
||||
│ │
|
||||
│ Règles métier : │
|
||||
│ ├─ B ne peut pas être « Done » │
|
||||
│ │ tant que A n'est pas « Done » │
|
||||
│ ├─ Si A est déplacée, B suit │
|
||||
│ │ (optionnel, selon config) │
|
||||
│ └─ Timeline affiche les flèches │
|
||||
│ A ────────► B │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.4 La vue « My Tasks » (vue agrégée)
|
||||
|
||||
La killer feature de Notion : un dashboard qui regroupe TOUTES les tâches, peu importe leur database d'origine.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ MY TASKS │
|
||||
│ ┌───────────────────────────────────────────┐ │
|
||||
│ │ Projet Alpha ┌──────────────────────┐│ │
|
||||
│ │ ├─ Design maquette│ Assignee: Bruno ││ │
|
||||
│ │ └─ Tests UI │ Due: 15 juillet ││ │
|
||||
│ │ └──────────────────────┘│ │
|
||||
│ │ Projet Beta │ │
|
||||
│ │ ├─ API endpoint │ │
|
||||
│ │ └─ Documentation │ │
|
||||
│ │ Daily Tasks │ │
|
||||
│ │ └─ Réunion standup │ │
|
||||
│ └───────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Agrège TOUTES les databases ayant : │
|
||||
│ ├─ Une propriété « Person » = current user │
|
||||
│ └─ Un Status ≠ « Done » │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Fonctionnement interne :**
|
||||
|
||||
1. Notion scanne TOUTES les databases du workspace
|
||||
2. Filtre les pages où `Assignee = current_user` ET `Status ≠ Done/Completed`
|
||||
3. Regroupe par database d'origine (ou par projet parent)
|
||||
4. Affiche avec tri par `Due Date` ASC
|
||||
5. Cette vue est **read-only logique** (pas une vraie database)
|
||||
|
||||
---
|
||||
|
||||
## 4. Comment Notion connecte Database et Tasks
|
||||
|
||||
### 4.1 Le diagramme de dépendance conceptuel
|
||||
|
||||
```
|
||||
DATABASE (concept racine)
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
COLLECTION PAGES (rows) COLLECTION_VIEWS
|
||||
(schéma) │ (présentations)
|
||||
│ │ │
|
||||
│ ├─ Title ├─ Table
|
||||
├─ Text ├─ Properties ├─ Board
|
||||
├─ Select │ (valeurs ├─ Calendar
|
||||
├─ Date │ par page) ├─ Timeline
|
||||
├─ Person │ ├─ Gallery
|
||||
├─ Relation ────┼──► SELF └─ List
|
||||
│ └── Sub-items │ ┌── Parent
|
||||
│ └── Deps │ └── Blocked by
|
||||
├─ Rollup ←──────┼──┘ (count, sum, avg via relation)
|
||||
├─ Formula │
|
||||
└─ ... │
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ TASKS │ (surcouche métier)
|
||||
│ │
|
||||
│ Ne sont PAS │
|
||||
│ un modèle │
|
||||
│ séparé — │
|
||||
│ ce sont des │
|
||||
│ PAGES avec │
|
||||
│ propriétés │
|
||||
│ spécifiques │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Les invariants du modèle
|
||||
|
||||
1. **Une database = 1 Collection** (schéma de propriétés)
|
||||
2. **1 Collection = N CollectionViews** (vues)
|
||||
3. **1 CollectionView = 1 type** (table | board | calendar | timeline | gallery | list)
|
||||
4. **Les pages d'une database sont des blocs** (comme toute page Notion)
|
||||
5. **Une propriété « Relation » pointe vers une autre database** (pas une table SQL)
|
||||
6. **Une propriété « Rollup » agrège via une Relation existante**
|
||||
7. **Les sub-items sont une Relation auto-référencée**
|
||||
8. **Les dépendances sont une Relation auto-référencée avec contrainte métier**
|
||||
|
||||
---
|
||||
|
||||
## 5. Implémentation dans FlowDeck : plan pas à pas
|
||||
|
||||
### 5.1 Phase 1 — Le concept de Database (semaine 1)
|
||||
|
||||
**Objectif :** Abstraire le concept de « board Gitea » en « database » réutilisable.
|
||||
|
||||
#### Nouvelle table : `collections`
|
||||
|
||||
```sql
|
||||
CREATE TABLE collections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
icon TEXT DEFAULT '📋',
|
||||
|
||||
-- Peut être liée à un projet Gitea (optionnel)
|
||||
gitea_owner TEXT,
|
||||
gitea_repo TEXT,
|
||||
|
||||
-- Schéma de propriétés en JSON (flexible, évolutif)
|
||||
schema_json TEXT NOT NULL DEFAULT '[]',
|
||||
-- Format: [{"name":"Status","type":"select","options":["Todo","Done"],"position":0}, ...]
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### Nouvelle table : `collection_pages` (remplace `cards`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE collection_pages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
|
||||
-- Peut être liée à une issue Gitea
|
||||
gitea_issue_id INTEGER,
|
||||
gitea_issue_number INTEGER,
|
||||
|
||||
-- Titre de la page (obligatoire)
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Position dans la collection
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Sub-items (auto-référence)
|
||||
parent_id INTEGER REFERENCES collection_pages(id),
|
||||
|
||||
-- Valeurs des propriétés en JSON
|
||||
property_values_json TEXT NOT NULL DEFAULT '{}',
|
||||
-- Format: {"Status":"Done","Priority":"P1","DueDate":"2026-07-15"}
|
||||
|
||||
-- Métadonnées temporelles
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cp_collection ON collection_pages(collection_id, position);
|
||||
CREATE INDEX idx_cp_parent ON collection_pages(parent_id);
|
||||
CREATE INDEX idx_cp_gitea ON collection_pages(gitea_issue_id);
|
||||
```
|
||||
|
||||
#### Nouvelle table : `collection_views`
|
||||
|
||||
```sql
|
||||
CREATE TABLE collection_views (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
|
||||
name TEXT NOT NULL DEFAULT 'Default View',
|
||||
view_type TEXT NOT NULL DEFAULT 'table', -- table, board, calendar, timeline, gallery, list
|
||||
|
||||
-- Configuration JSON
|
||||
config_json TEXT NOT NULL DEFAULT '{}',
|
||||
-- Format:
|
||||
-- {
|
||||
-- "group_by": "Status", ← board/calendar uniquement
|
||||
-- "filters": [ ← tous types
|
||||
-- {"property":"Status","operator":"is_not","value":"Done"},
|
||||
-- {"property":"DueDate","operator":"is_before","value":"today"}
|
||||
-- ],
|
||||
-- "filter_conjunction": "and", ← "and" | "or"
|
||||
-- "sorts": [ ← tous types
|
||||
-- {"property":"Priority","direction":"asc"},
|
||||
-- {"property":"DueDate","direction":"desc"}
|
||||
-- ],
|
||||
-- "visible_properties": ["Title","Status","Assignee","DueDate"],
|
||||
-- "card_size": "medium", ← board/gallery
|
||||
-- "cover_property": "Files", ← board/gallery
|
||||
-- "date_property": "DueDate", ← calendar/timeline
|
||||
-- "date_range_property": "EndDate" ← timeline (optionnel)
|
||||
-- }
|
||||
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 5.2 Phase 2 — Le système de propriétés (semaine 1-2)
|
||||
|
||||
#### Nouvelle table : `collection_properties` (remplace `project_properties`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE collection_properties (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
prop_type TEXT NOT NULL DEFAULT 'text',
|
||||
-- Types: title, text, number, select, multi_select, status,
|
||||
-- date, person, checkbox, url, email, phone,
|
||||
-- relation, rollup, formula,
|
||||
-- created_time, created_by, last_edited_time, last_edited_by
|
||||
|
||||
-- Options pour select/multi_select/status
|
||||
options_json TEXT DEFAULT '[]',
|
||||
-- Format: [{"name":"Todo","color":"gray"},{"name":"Done","color":"green"}]
|
||||
|
||||
-- Pour les relations
|
||||
related_collection_id INTEGER REFERENCES collections(id),
|
||||
reverse_name TEXT, -- nom de la relation inverse (auto-généré)
|
||||
|
||||
-- Pour les rollups
|
||||
relation_property_id INTEGER REFERENCES collection_properties(id),
|
||||
target_property_id INTEGER REFERENCES collection_properties(id),
|
||||
rollup_function TEXT, -- count, sum, avg, min, max, range, unique
|
||||
|
||||
-- Pour les formules
|
||||
formula_expression TEXT,
|
||||
|
||||
-- Position dans la liste des propriétés
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
required BOOLEAN NOT NULL DEFAULT 0,
|
||||
visible_in_views BOOLEAN NOT NULL DEFAULT 1,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(collection_id, name)
|
||||
);
|
||||
```
|
||||
|
||||
**Mapping des types Notion → FlowDeck :**
|
||||
|
||||
```python
|
||||
PROPERTY_TYPES = {
|
||||
"title": {
|
||||
"storage": "string",
|
||||
"validation": "max 2000 chars",
|
||||
"indexed": True,
|
||||
"unique_per_collection": False,
|
||||
},
|
||||
"text": {
|
||||
"storage": "string",
|
||||
"validation": "any text",
|
||||
},
|
||||
"number": {
|
||||
"storage": "float",
|
||||
"validation": "numeric",
|
||||
"format_options": ["number", "percent", "currency", "dollar", "euro", "pound", "yen"],
|
||||
},
|
||||
"select": {
|
||||
"storage": "string (from options_json)",
|
||||
"validation": "must match one option",
|
||||
},
|
||||
"multi_select": {
|
||||
"storage": "JSON array of strings",
|
||||
"validation": "each must match option",
|
||||
},
|
||||
"status": {
|
||||
"storage": "string (from options_json) + color",
|
||||
"validation": "must match one option",
|
||||
"colors": ["gray","brown","orange","yellow","green","blue","purple","pink","red"],
|
||||
},
|
||||
"date": {
|
||||
"storage": "ISO 8601 string or range",
|
||||
"validation": "valid datetime",
|
||||
"include_time": True, # configurable
|
||||
},
|
||||
"person": {
|
||||
"storage": "JSON array of {id, login, avatar_url}",
|
||||
"validation": "must be workspace user",
|
||||
},
|
||||
"checkbox": {
|
||||
"storage": "boolean",
|
||||
"validation": "true/false",
|
||||
},
|
||||
"relation": {
|
||||
"storage": "JSON array of page IDs",
|
||||
"validation": "must be existing page in related_collection",
|
||||
"bidirectional": True,
|
||||
},
|
||||
"rollup": {
|
||||
"storage": "computed — not stored directly",
|
||||
"functions": ["count","count_values","empty","not_empty",
|
||||
"sum","average","median","min","max","range"],
|
||||
"depends_on": ["relation_property_id","target_property_id"],
|
||||
},
|
||||
"formula": {
|
||||
"storage": "computed — not stored directly",
|
||||
"expression": "JavaScript-like DSL",
|
||||
"functions_available": [
|
||||
"prop()", "now()", "today()", "if()", "concat()", "round()",
|
||||
"dateAdd()", "dateSubtract()", "formatDate()", "toNumber()",
|
||||
"contains()", "length()", "replace()", "replaceAll()",
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Phase 3 — Sub-items et Dépendances (semaine 2)
|
||||
|
||||
#### Implémentation des sub-items (relation auto-référencée)
|
||||
|
||||
```python
|
||||
# Dans le schema d'une database, activer Sub-items crée automatiquement :
|
||||
|
||||
def enable_sub_items(collection_id: int):
|
||||
"""Active les sub-items sur une collection."""
|
||||
|
||||
# 1. Créer la propriété relation « Parent item » (si pas déjà)
|
||||
parent_prop = create_property(
|
||||
collection_id=collection_id,
|
||||
name="Parent item",
|
||||
prop_type="relation",
|
||||
related_collection_id=collection_id, # AUTO-RÉFÉRENCE
|
||||
reverse_name="Sub-item", # relation inverse
|
||||
visible_in_views=False, # cachée dans les vues
|
||||
)
|
||||
|
||||
# 2. Marquer la collection comme ayant des sub-items
|
||||
mark_collection_feature(collection_id, "has_sub_items", True)
|
||||
|
||||
# 3. Mettre à jour la vue board pour afficher l'imbrication
|
||||
# Le rendu Kanban doit maintenant supporter :
|
||||
# - Groupe parent → sous-tâches indentées
|
||||
# - Bouton « + Add sub-item » sous chaque carte
|
||||
# - État du parent = agrégation des enfants
|
||||
```
|
||||
|
||||
#### Rendu des sub-items dans le board
|
||||
|
||||
```html
|
||||
<!-- Dans board_fragment.html, modifier le rendu des cartes -->
|
||||
<div class="kanban-card" data-page-id="{{ page.id }}">
|
||||
<!-- Carte parent -->
|
||||
<div class="card-title">{{ page.title }}</div>
|
||||
|
||||
<!-- Sous-tâches -->
|
||||
{% for sub in page.sub_items %}
|
||||
<div class="sub-item" data-page-id="{{ sub.id }}">
|
||||
├─ {{ sub.title }}
|
||||
<span class="sub-status">{{ sub.properties.Status }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Bouton ajout sous-tâche -->
|
||||
<button hx-post="/api/pages/{{ page.id }}/sub-items"
|
||||
hx-swap="beforeend">
|
||||
+ Sub-item
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Implémentation des dépendances
|
||||
|
||||
```python
|
||||
def enable_dependencies(collection_id: int):
|
||||
"""Active les dépendances bloquantes."""
|
||||
|
||||
# Création de 2 propriétés relation
|
||||
blocks = create_property(
|
||||
collection_id=collection_id,
|
||||
name="Blocks",
|
||||
prop_type="relation",
|
||||
related_collection_id=collection_id,
|
||||
reverse_name="Blocked by",
|
||||
)
|
||||
|
||||
# Contrainte métier : au niveau applicatif
|
||||
# (pas de contrainte SQL pour garder la flexibilité)
|
||||
|
||||
def check_dependency_constraint(page_id: int, new_status: str):
|
||||
"""Vérifie qu'une page peut passer à 'Done'
|
||||
seulement si toutes les pages qu'elle bloque sont Done."""
|
||||
|
||||
blocked_pages = get_blocked_pages(page_id)
|
||||
if new_status == "Done" and blocked_pages:
|
||||
not_done = [p for p in blocked_pages
|
||||
if p.status not in ("Done", "Cancelled")]
|
||||
if not_done:
|
||||
raise DependencyError(
|
||||
f"Cannot mark as Done: still blocking {len(not_done)} tasks"
|
||||
)
|
||||
```
|
||||
|
||||
### 5.4 Phase 4 — « My Tasks » (vue agrégée) (semaine 2-3)
|
||||
|
||||
```python
|
||||
# GET /my-tasks
|
||||
async def my_tasks(request: Request):
|
||||
"""Dashboard personnel : toutes les tâches assignées à l'utilisateur."""
|
||||
|
||||
user = get_current_user(request)
|
||||
|
||||
# 1. Scanner TOUTES les collections
|
||||
collections = db.fetch_all("SELECT * FROM collections")
|
||||
|
||||
my_tasks = []
|
||||
for col in collections:
|
||||
schema = json.loads(col.schema_json)
|
||||
|
||||
# 2. Vérifier si la collection a une propriété Person
|
||||
person_props = [p for p in schema if p["type"] == "person"]
|
||||
if not person_props:
|
||||
continue
|
||||
|
||||
# 3. Récupérer les pages assignées à l'utilisateur
|
||||
pages = db.fetch_all("""
|
||||
SELECT cp.*, cpv.value as property_values
|
||||
FROM collection_pages cp
|
||||
JOIN collection_properties cprop
|
||||
ON cprop.collection_id = cp.collection_id
|
||||
AND cprop.prop_type = 'person'
|
||||
WHERE cp.collection_id = ?
|
||||
AND cp.property_values_json LIKE ?
|
||||
""", (col.id, f'%"{user.login}"%'))
|
||||
|
||||
my_tasks.extend([
|
||||
{"collection": col, "page": p, "schema": schema}
|
||||
for p in pages
|
||||
])
|
||||
|
||||
# 4. Grouper par collection
|
||||
grouped = {}
|
||||
for task in my_tasks:
|
||||
col_name = task["collection"].name
|
||||
if col_name not in grouped:
|
||||
grouped[col_name] = []
|
||||
grouped[col_name].append(task)
|
||||
|
||||
# 5. Trier par Due Date
|
||||
for tasks in grouped.values():
|
||||
tasks.sort(key=lambda t: t["page"].get_property("DueDate") or "9999")
|
||||
|
||||
return render_template("my_tasks.html", tasks=grouped)
|
||||
```
|
||||
|
||||
### 5.5 Phase 5 — Rétrocompatibilité avec Gitea
|
||||
|
||||
**Stratégie de migration progressive :**
|
||||
|
||||
```python
|
||||
# Phase 5a : Wrapper — les boards Gitea existants
|
||||
# deviennent des collections avec une couche de compatibilité
|
||||
|
||||
class GiteaBoardCompat:
|
||||
"""Adaptateur : ancien board Gitea → nouvelle Collection."""
|
||||
|
||||
@staticmethod
|
||||
def from_board(board_row):
|
||||
"""Convertit un board existant en collection."""
|
||||
return {
|
||||
"collection_id": f"gitea:{board_row.id}",
|
||||
"name": f"{board_row.project_owner}/{board_row.project_name}",
|
||||
"gitea_owner": board_row.project_owner,
|
||||
"gitea_repo": board_row.project_name,
|
||||
"schema": [
|
||||
{"name": "Title", "type": "title"},
|
||||
{"name": "Status", "type": "select",
|
||||
"options": json.loads(board_row.columns_json)},
|
||||
{"name": "Priority", "type": "select",
|
||||
"options": ["P1","P2","P3","P4"]},
|
||||
],
|
||||
"is_gitea_linked": True,
|
||||
}
|
||||
|
||||
# Phase 5b : Nouvelle route hybride
|
||||
# GET /board/{owner}/{repo} → détecte si c'est un board Gitea legacy
|
||||
# ou une collection pure, et route vers le bon renderer
|
||||
```
|
||||
|
||||
### 5.6 Résumé du plan d'implémentation
|
||||
|
||||
```
|
||||
Semaine 1 Semaine 2 Semaine 3
|
||||
──────── ───────── ─────────
|
||||
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Database │ │ Properties │ │ My Tasks │
|
||||
│ Concept │ ──► │ Avancées │ ──► │ Dashboard │
|
||||
│ │ │ │ │ │
|
||||
│ • collections│ │ • Relations │ │ • Vue agrégée│
|
||||
│ • pages │ │ • Rollups │ │ • Filtres │
|
||||
│ • views │ │ • Formules │ │ globaux │
|
||||
│ • filters │ │ • Auto-prop │ │ • Calendrier │
|
||||
│ • sorts │ │ computed │ │ intégré │
|
||||
│ │ │ │ │ │
|
||||
│ Sub-items │ │ Dependencies │ │ UI polish │
|
||||
│ (parent_id) │ ──► │ + contraintes│ ──► │ + tests │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Références
|
||||
|
||||
- [Notion API — Property Object](https://developers.notion.com/reference/property-object)
|
||||
- [Notion Help — Intro to Databases](https://www.notion.com/help/intro-to-databases)
|
||||
- [Notion Help — Tasks & Dependencies](https://www.notion.com/help/tasks-and-dependencies)
|
||||
- [Notion Help — Relations & Rollups](https://www.notion.com/help/relations-and-rollups)
|
||||
- [react-notion-x — Block types reference](https://github.com/NotionX/react-notion-x)
|
||||
- [Notion Data Sources update (2025)](https://www.notionapps.com/blog/notion-data-sources-update-2025/)
|
||||
|
||||
### Différences clés FlowDeck vs Notion
|
||||
|
||||
| Concept | Notion | FlowDeck (cible) |
|
||||
|---------|--------|------------------|
|
||||
| **Database** | Collection + Pages + Views | `collections` + `collection_pages` + `collection_views` |
|
||||
| **Schéma** | Properties sur la Collection | `collection_properties` au niveau collection |
|
||||
| **Valeurs** | Dans chaque page (bloc) | `property_values_json` dans chaque `collection_page` |
|
||||
| **Sub-items** | Relation auto-référencée | `parent_id` auto-référence dans `collection_pages` |
|
||||
| **Dependencies** | Relation auto-référencée + contrainte | Idem + check applicatif au changement de statut |
|
||||
| **My Tasks** | Scan cross-database + filtre | Route `/my-tasks` + JOIN sur `collection_pages` |
|
||||
| **Gitea sync** | N/A (pas de Gitea) | Les pages peuvent être liées à `gitea_issue_id` |
|
||||
| **Stockage** | Cloud distribué (PostgreSQL + cache) | SQLite local (1 fichier) |
|
||||
Reference in New Issue
Block a user