Spunto CMS
Concepts

Listes et cartes

Les listes, les cartes qu'elles contiennent, les deux identifiants de chaque carte, et la différence entre là où une carte vit et là où elle apparaît.

Une collection est une liste. Une carte (un item, dans l'API) en est une ligne. C'est toute la forme du modèle, et presque tout le reste du produit n'est qu'une façon de regarder ces deux-là.

Une carte

Structurellement, une carte porte très peu de choses en propre :

  • un titre ;
  • une icône — un emoji, ou rien ;
  • une image de couverture (coverFileId) — un fichier image envoyé, affiché sur la page de la carte en bandeau paysage au-dessus du titre (par défaut) ou en carré à sa gauche — un réglage de l'espace de travail, coverLayout — et en haut de la carte sur un kanban ; ou rien ;
  • un corps — un document en blocs ;
  • un statut — l'un des statuts de l'espace de travail ;
  • sa place dans un arbre — une carte peut avoir une carte parente, donc les sous-tâches et les sous-pages sont elles aussi des cartes ;
  • son rang dans l'ordre manuel ;
  • une clé lisible, comme DOC-12 ;
  • qui l'a créée (createdBy), figé à la création.

Tout le reste — assigné, échéance, priorité, étiquettes — est une propriété, déclarée dans le registre de l'espace de travail et stockée sur la carte en JSON.

Pourquoi si peu de choses en dur

Un assigné et une échéance semblent universels, jusqu'au jour où l'on se sert du produit pour autre chose que du suivi de tâches. Un catalogue de contenu n'a ni l'un ni l'autre ; un CRM en veut d'autres. Des colonnes que seuls certains espaces remplissent, c'est un schéma qui se fait passer pour un domaine. La règle est donc : une carte ne porte en dur que ce qui porte une structure — ce dont dépendent l'arbre, l'ordre ou la clé — et rien d'autre. Être courant n'est pas une raison de promotion.

Ce n'est pas théorique : assignee_id, due_date et start_date ont été de vraies colonnes de la carte, et ont été retirées. Elles reviennent aujourd'hui comme des propriétés déclarées ordinaires, par le même compilateur, le même curseur et les mêmes conversions de type.

L'icône et l'auteur passent le même test, pour des raisons différentes. Une icône ne décrit pas une carte, elle la nomme : elle apparaît partout où le titre apparaît, et nulle part ailleurs. Une propriété déclarée aurait obligé chacun de ces endroits à résoudre une clé du registre pour dessiner deux caractères. L'auteur est une colonne parce que le journal d'événements est partitionné par mois et se purge : un auteur lu dans un journal disparaît à la première politique de rétention, sur les cartes les plus anciennes d'abord — justement celles dont on finit par se demander d'où elles viennent.

La couverture suit l'icône : elle présente la carte plutôt qu'elle ne la décrit, et un kanban doit savoir quelle image dessiner sans deviner quelle propriété file tient ce rôle.

Ce que l'icône n'est pas

Elle n'est ni triable, ni filtrable, ni groupable. Une vue qui la nomme est refusée, plutôt que de trier silencieusement les cartes par point de code Unicode — un ordre que personne ne peut prévoir ni expliquer.

createdBy vaut null sur les cartes créées par un import, un seed ou une tâche de fond, ainsi que sur toutes les cartes antérieures à la colonne : elle n'a pas été rétro-remplie, faute de quoi que ce soit de vrai à y mettre. L'interface masque alors la ligne plutôt que d'afficher « inconnu ».

Deux identifiants

Chaque carte en a deux, et ils ne sont pas interchangeables :

id est un UUIDv7. C'est lui que l'API attend dans les chemins, et il est ordonné dans le temps, ce qui rend la pagination keyset peu coûteuse.

key est l'identifiant lisible — DOC-12. Il est composé par l'API à partir du préfixe de clé de la liste d'origine de la carte et de son numéro.

Ne composez jamais une clé côté client. Une vue peut inclure des cartes d'autres listes, donc le préfixe devant un numéro n'est pas forcément celui de la liste que vous regardez. Et les numéros ne sont jamais recyclés : supprimez DOC-12, la carte suivante sera DOC-13.

Liste d'origine, et listes où la carte apparaît

Une carte appartient à exactement une liste d'origine — celle où elle a été créée et d'où vient sa clé.

Elle peut aussi être rattachée à d'autres listes. Un rattachement ne déplace ni ne copie la carte : il fait apparaître la même carte dans une seconde liste. Une carte, un seul jeu de valeurs, plusieurs endroits où la trouver.

Les vues décident si elles suivent ces rattachements, avec includeSecondaryLists dans la définition de vue. La vue par défaut — celle d'une liste sans vue enregistrée — les inclut.

Un vrai bug que cela a causé

Ce défaut était autrefois réimplémenté dans chaque client au lieu d'être publié par l'API. L'app web incluait les rattachements ; l'app iOS non. Une liste dont toutes les cartes arrivent par rattachement paraissait donc pleine sur le web et vide sur le téléphone, sans que rien ne soit faux nulle part. L'API la publie désormais sous CollectionDetail.defaultView. Un défaut recopié ne casse pas — il diverge.

Lire une liste entière en un appel

GET /workspaces/{workspaceId}/collections/{collectionId} renvoie d'un coup tout ce dont un écran a besoin : la liste, le registre des propriétés, les champs système, les statuts, les vues enregistrées, la vue par défaut, les membres et votre rôle.

Les cartes n'y sont pas. Elles viennent de POST …/query, parce que les cartes voulues dépendent de la vue que vous regardez.

Statuts

Les statuts appartiennent à l'espace de travail, pas à une liste : une carte garde donc son statut quand elle change de liste. Chacun a :

  • un libellé — le nom que vous voulez ;
  • une catégorie — todo, doing ou done ;
  • une position — l'ordre par défaut de l'espace de travail ;
  • une couleur — l'un des huit jetons d'une palette fermée.

La catégorie est la partie qui a un sens. C'est elle qui permet à une vue de dire « pas encore terminé » et de rester juste dans un espace dont les statuts portent de tout autres noms. La couleur, elle, ne veut rien dire et ne se compile nulle part — elle existe pour distinguer d'un coup d'œil deux statuts doing, et pour qu'un changement de catégorie ne repeigne pas le kanban.

La palette est fermée (gray, red, orange, yellow, green, blue, purple, pink) à dessein. Une couleur libre serait illisible une fois sur trois, et obligerait chaque surface qui affiche un statut à inventer un contraste.

Les groupes de la barre latérale

Les listes peuvent être rangées dans des groupes, et les groupes dans un niveau de sous-groupes. Deux niveaux, pas trois.

C'est cette borne qui fait de la barre latérale une simple boucle sur une liste plate, et d'un déplacement la mise à jour d'une seule ligne — ni chemins matérialisés, ni réécriture de sous-arbre, ni requête récursive.

Un groupe est une étiquette, pas un contenant : en supprimer un détache ses listes dans la même transaction et ne supprime rien. Il n'a pas de page à lui, et c'est pourquoi rien dans l'API ne désigne un groupe comme un emplacement.

Une carte porte pourtant son rangement : chaque entrée de ItemDetail.collections a un groupPath — les groupes où est rangée cette liste, du plus extérieur au plus intérieur, [] pour une liste à la racine. Le serveur le résout par deux jointures externes plutôt que de laisser les clients réassembler une hiérarchie à partir de la liste plate des groupes, pour la même raison qu'il compose key : la borne à deux niveaux est une règle du service, et un client qui la reconstruit la reconstruira de travers le jour où elle changera.

Archiver ou supprimer

PATCH { "archived": true } retire une carte de toutes les vues et de la recherche, et se défait.

DELETE est définitif et emporte tout le sous-arbre de la carte.

L'interface propose le premier. Le second existe pour quand vous le voulez vraiment. Les deux se trouvent dans le même menu ⋯, en haut à droite d'une carte ouverte, à côté du seul geste de ce menu qui ajoute au lieu de retirer : la dupliquer.

Sur cette page