Spunto CMS
Concepts

Le registre des propriétés

Dix types de propriétés, déclarés une fois par espace de travail, validés à l'écriture — et ce que chacun permet de trier, filtrer ou grouper.

Les propriétés se déclarent par espace de travail, pas par liste. Un seul registre, partagé par toutes les listes de l'espace.

C'est pour cela qu'une carte garde ses valeurs quand elle passe d'une liste à l'autre : la propriété dont elle porte une valeur est le même objet avant et après. Un schéma par liste aurait fait de « déplacer cette carte » un « abandonner les champs que la destination n'a pas » — une perte de données déguisée en navigation.

Quelles propriétés une liste donnée affiche est une autre question — c'est displayedFieldIds sur la liste, et c'est de la présentation. C'est aussi ce que montre la fiche d'une carte : les propriétés de sa liste, puis celles des listes où elle est rattachée — pas tout le registre. Une valeur renseignée hors de ces listes n'est pas cachée, elle est rangée sous « Autres propriétés ». Et « Ajouter une propriété » depuis une carte l'ajoute à sa liste. Côté API, chaque liste de ItemDetail.collections porte sa sélection (fieldIds), et POST /workspaces/{workspaceId}/collections/{collectionId}/fields y ajoute une propriété sans remplacer les autres. Une liste qui n'a rien choisi (displayedFieldIds vide) montre tout le registre.

Les dix types

TypeContientValidé contreTri / groupement
textUne chaîne—oui
numberUn nombre—oui
selectUne optionconfig.options, si déclaréesoui
dateUne date ISO—oui
checkboxUn booléen—oui
urlUne URL absolue http, https ou mailtole schéma d'URLoui
personUn membre de cet espace de travailla table membershipoui
fileUn fichier envoyé dans cet espace de travailla table fileoui
multi_selectPlusieurs optionsconfig.options, si déclaréesnon
multi_personPlusieurs membresla table membershipnon

L'API ne restreint pas les opérateurs de filtre par type — le même jeu (eq, neq, lt, lte, gt, gte, contains, starts_with, in, not_in, is_empty, is_not_empty) se compile contre n'importe quelle propriété scalaire, et son sens découle du type. L'interface propose, pour chaque type, un sous-ensemble plus restreint et raisonnable ; un client qui pilote directement l'API n'y est pas tenu. Les types tableau sont l'exception, et mieux vaut savoir pourquoi avant de concevoir autour d'eux.

Types tableau

multi_select et multi_person contiennent une liste. Ils se filtrent par inclusion — eq et contains veulent tous deux dire « a cette valeur parmi ses valeurs », parce que c'est ce que signifie filtrer sur une étiquette — et ils ne peuvent servir ni au tri ni au groupement. Demander l'un ou l'autre renvoie un 400 qui le dit.

C'est un refus, pas un manque. Postgres les trierait volontiers ; il les trierait selon l'ordre des documents d'un tableau JSON, ce qui ferait afficher à l'interface un contrôle de tri produisant un ordre qu'aucun utilisateur ne pourrait prévoir ni expliquer. Un contrôle qui ment est pire qu'un contrôle absent.

multi_person plutôt qu'une colonne

Une propriété « Assignés » est un multi_person. Elle n'a délibérément pas été ajoutée comme colonne assignee_ids sur la carte, et la raison se généralise :

l'invariant d'un kanban est que la somme des colonnes vaut la vue. Une carte à deux assignés, groupée par assigné, apparaîtrait dans deux colonnes — le groupement par type tableau est donc refusé, ce qui veut dire qu'un type tableau n'a rien à faire en colonne promue que tout le reste suppose scalaire.

Une fois cet argument posé, il a aussi tranché le singulier : « Assigné » est une propriété person déclarée, pas une colonne non plus.

Les trois qui dépendent d'une autre table

person, multi_person et file ne sont valides que si la valeur désigne quelque chose qui existe — une appartenance à cet espace de travail, ou un fichier de cet espace. C'est vérifié à l'écriture, pas à la lecture.

La validation se fait à l'écriture

Une valeur qui ne correspond pas à son type déclaré est refusée à l'écriture, avec un 400. Elle n'atteint jamais la base.

Cette barrière ne peut pas être déplacée à la lecture, et la raison mérite d'être dite franchement : une valeur mal typée ne casse pas sa propre ligne. Elle casse la requête entière de toute vue qui convertit cette propriété — un seul nombre invalide dans une propriété number empêche toute une liste de se charger, pour tout le monde. Le seul endroit où l'arrêter, c'est la porte.

Un PATCH sur une carte fusionne properties : modifier une propriété n'efface pas les autres. Retirer une propriété d'une carte est une opération explicite, pas l'effet de bord d'une omission.

Les options et leurs couleurs

Un select ou un multi_select peut déclarer des options — les valeurs permises. Laissez-les vides pour du texte libre.

Il peut aussi déclarer optionColors : une table à part, indexée par la valeur de l'option, puisée dans la même palette de huit jetons que les statuts. Une seule palette, pour qu'une étiquette verte soit du même vert que le statut vert d'à côté.

Que optionColors soit une table à part plutôt qu'un objet option enrichi a deux conséquences :

  • la valeur écrite sur une carte est la chaîne. C'est elle que compare la validation et que cherche le filtre d'inclusion. options dit ce qui est permis, optionColors comment ça se présente ;
  • la couleur ne se compile nulle part — ni dans le tri, ni dans le filtre, ni dans la validité. Une entrée qui nomme une option disparue est ignorée ; une option sans entrée s'affiche en neutre. Rien n'est déduit à l'affichage.

Une option nouvelle écrite sans couleur en reçoit une d'après son libellé : le même mot a toujours la même couleur, dans toutes les listes et tous les espaces de travail. L'API la choisit une fois et l'enregistre dans optionColors : elle se lit ensuite comme n'importe quelle autre couleur, et vous pouvez la changer ou la rendre neutre après coup. Une option qui existe déjà n'est jamais recolorée, et passer color: null à la création la garde neutre.

`config` est remplacé en bloc

options et optionColors vivent tous deux dans config, et une écriture remplace l'objet entier. Envoyez les couleurs avec les options, sinon elles disparaissent. C'est aussi pourquoi l'emplacement d'affichage d'une propriété vit dans une tout autre colonne — sinon déplacer un champ dans la barre latérale aurait effacé ses options sans rien dire.

Pour ajouter une option, ou changer la couleur d'une seule, sans renvoyer les autres, utilisez POST /workspaces/{workspaceId}/fields/{fieldId}/options avec { value, color }. La fusion se fait côté serveur : deux personnes qui ajoutent une étiquette au même moment gardent chacune la leur. Omettre color conserve la couleur actuelle (ou, pour une option nouvelle, lui donne la couleur de son libellé) ; null la rend neutre. Dans l'app, c'est ce que fait la saisie d'une nouvelle valeur dans un select : le menu propose Créer « x », et chaque option affiche un bouton de palette au survol.

Champs système

Certaines choses sont adressables dans une vue sans être des propriétés du registre : sys:title, sys:status, sys:order, sys:created_at, etc.

L'API en publie la liste sous systemFields sur la liste, pour qu'aucun client n'ait à tenir sa propre copie de ce qu'accepte le compilateur de vues.

Un piège connu

Un select se trie par ordre alphabétique, pas dans l'ordre de ses options. Une vue triée « par priorité » renvoie critical, high, low, normal.

C'est connu, c'est documenté, et ce n'est pas un bug à corriger à la légère — l'index de base de données sur lequel s'appuie le tri dépend de cet ordre. Si vous avez besoin de l'ordre des priorités, encodez-le : 1 — Basse, 2 — Normale.

En ajouter une ne coûte rien structurellement

Créer une propriété, c'est une ligne dans un registre. Cela ne modifie aucune table, ne verrouille rien et ne réécrit aucune carte existante — les cartes sans valeur n'ont simplement pas de clé pour elle.

Trier ou filtrer dessus à grande échelle peut demander un index. L'API vous dira lequel : POST …/index-hints indique ce dont une vue donnée bénéficierait.

Sur cette page