Gramplets.fr - dsblank/gramps-connect GitHub Wiki
🌐 English · Deutsch · Español · Nederlands · 简体中文
Les Gramplets sont le système d'extensions de Gramps Connect : de
petits programmes Python qui interrogent l'arbre et affichent un
résultat — un tableau, un diagramme, du texte brut, ou du HTML. Il en
existe deux sortes : les Gramplets, que vous lancez depuis le menu
Gramplets dans leur propre fenêtre, et les Gramplets de vue, qui
se placent dans un panneau sous une liste (Individu, Famille, ou tout
autre type d'objet) et suivent ce que vous y sélectionnez. Ce qui les
rend inhabituels, c'est où ils s'exécutent : chacun
est du vrai Python, s'exécutant directement dans votre onglet de
navigateur sous Pyodide (CPython compilé en
WebAssembly), contre des wheels construits localement du propre modèle
de données gramps.gen.lib de Gramps. Rien n'est installé sur votre
machine, et rien ne s'exécute sur un serveur — voir
Architecture.
Cela veut aussi dire que chaque Gramplet est modifiable. En ouvrir un depuis l'éditeur de Gramplets de l'application, et vous regardez (et pouvez changer) sa véritable source — pas une boîte noire derrière un panneau de réglages. Changer l'apparence d'un diagramme, ajuster ce qu'une recherche cherche, ou en écrire une nouvelle de zéro, tout cela sans quitter l'application.
Toute personne connectée à l'arbre peut exécuter ses Gramplets, depuis le menu Gramplets ou dans le panneau « Gramplets de vue » d'une liste. Seul le propriétaire de l'arbre ou un administrateur du site peut les écrire : en créer un, modifier le code d'un Gramplet, ou en installer, mettre à jour ou retirer un via la boutique de Gramplets. Pour tous les autres, la boutique, Nouveau Gramplet… et les crayons de modification n'apparaissent pas du tout.
| Rôle | Exécuter des Gramplets | Écrire / installer des Gramplets |
|---|---|---|
| Invité | ✓ (sauf les privés) | — |
| Membre | ✓ | — |
| Contributeur | ✓ | — |
| Éditeur | ✓ | — |
| Propriétaire | ✓ | ✓ |
| Admin | ✓ | ✓ |
Écrire un Gramplet demande plus que les droits habituels d'un éditeur, car son code s'exécute dans le navigateur de tous ceux qui l'utilisent : le modifier affecte donc l'arbre pour tout le monde, pas seulement vos propres modifications. Un Gramplet est stocké comme un objet média ordinaire portant l'étiquette Gramplet, donc les règles de confidentialité habituelles s'appliquent : s'il est marqué privé, les invités ne le verront pas.
Il en va de même pour Autoriser les modifications de données (voir Modifier des données) : seul un propriétaire ou un administrateur peut l'activer pour un Gramplet. N'importe qui peut faire un essai à blanc d'un tel Gramplet, qui n'enregistre rien ; enregistrer ses modifications demande des droits d'éditeur ou supérieurs (les propres permissions d'ajout, de modification et de suppression du serveur), donc pour les invités, membres et contributeurs, le bouton Exécuter de la fenêtre est désactivé.
La boutique de Gramplets est un catalogue consultable à l'intérieur de
l'application — ouvrez-la depuis Gramplets → Boutique de Gramplets….
Elle comporte deux onglets, Gramplets et Gramplets de vue ;
installer, mettre à jour, ou retirer n'importe quelle entrée en un clic.
Un Gramplet installé apparaît aussitôt dans le menu Gramplets (regroupé
par catégorie) ; un Gramplet de vue installé s'ajoute à une liste avec
le + du panneau « Gramplets de vue » de cette liste. Elle est publiée indépendamment des propres versions
de Gramps Connect — le catalogue est du contenu statique
(gramplet-store/
dans le dépôt) récupéré par l'application en cours d'exécution au
moment voulu, pas intégré au build de l'application, de nouveaux
Gramplets peuvent donc apparaître dans la boutique sans que personne ne
mette à jour Gramps Connect lui-même.
Des exemples déjà présents dans le catalogue incluent ces Gramplets, qui se lancent depuis le menu Gramplets :
| Gramplet | Ce qu'il fait |
|---|---|
| Age at Death by Decade | Diagramme de l'âge médian au décès pour chaque décennie de naissance ou de décès |
| Mark Unsourced People Private | Marque comme privées toutes les personnes sans aucune source — modifie des données, donc l'ouvrir fait un essai à blanc qui liste qui changerait, et Exécuter l'enregistre (voir Modifier des données) |
| People Without Sources | Toutes les personnes sans citation sur elles-mêmes ni sur aucun de leurs événements (un parcours de tout l'arbre avec barre de progression) |
| Possible Duplicate People | Personnes de même nom nées à quelques années d'écart, à vérifier et fusionner |
| Tag People | Ajoute une étiquette choisie dans une liste à toutes les personnes du filtre Personnes actuel — modifie des données, donc c'est un essai à blanc jusqu'à ce que vous cliquiez sur Exécuter |
et ces Gramplets de vue :
| Gramplet de vue | Ce qu'il fait |
|---|---|
| Age-at-Death Histogram | Histogramme plotly interactif de l'âge au décès sur tout l'arbre (ou la liste actuellement filtrée) |
| All Relationships | Calculs de relations sur tout l'arbre |
| Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes | Panneaux de détail dans le style des Gramplets de bureau pour la fiche sélectionnée |
| Born In | Recherche par lieu de naissance |
| Crossing Relationships | Trouve les relations qui croisent les générations de façon inhabituelle |
| GOQL Query | Taper une clause where GOQL brute, et choisir des expressions de champs comme colonnes |
| Hello Table | L'exemple de départ minimal |
| People Search | Une recherche d'Individus en direct, au fil de la frappe, par nom, sexe et date de décès |
| Phonebook Surname Sort | Tri des noms de famille insensible aux accents (par ex. « Müller » se trie à côté de « Muller ») |
| Relationship / Relationships | Comment deux personnes sélectionnées sont apparentées |
| Selected Record | Réagit à la fiche actuellement ouverte |
| To-Do / Todo Tracker | Suit les tâches de recherche à faire sur tout l'arbre |
| Tree Statistics | Un tableau de bord des comptes de fiches et pourcentages dérivés |
Dans chaque vue en liste (Personnes, Familles, Événements, …), les Gramplets de vue que vous avez ajoutés à cette vue apparaissent sous forme d'onglets dans un panneau Gramplets de vue repliable, sous le tableau. Le panneau est replié au départ, et un Gramplet de vue ne s'exécute que lorsque le panneau est ouvert : le déplier exécute l'onglet sélectionné. La première exécution d'une session charge aussi l'environnement Python du navigateur et peut donc prendre quelques secondes ; les suivantes sont rapides. Un panneau replié ne coûte rien, puisque Python n'est chargé qu'à son ouverture.
Un Gramplet (par opposition à un Gramplet de vue) n'est lié à aucune liste. Choisissez-en un dans le menu Gramplets — regroupés en sous-menus par catégorie dès qu'il y a plus d'une catégorie — et il s'ouvre dans une fenêtre flottante :
- Il s'exécute à l'ouverture. Un Gramplet qui a d'abord besoin d'une saisie — un nom de famille, un nombre de générations — dessine ses propres champs et un bouton Exécuter, avec les mêmes widgets que les Gramplets de vue.
- Les Gramplets qui modifient des données (marqués ✎ dans le menu) font un essai à blanc à l'ouverture et ont des boutons Essai à blanc / Exécuter et Annuler — voir Modifier des données.
- Il voit la liste derrière lui. Si une liste est ouverte dans la vue principale, le Gramplet voit son enregistrement sélectionné et son filtre actuel, tels qu'ils étaient au début de l'exécution ; naviguer ailleurs ensuite ne change pas une exécution en cours.
- Progression et Arrêter. Un Gramplet plus long affiche une barre de progression pendant qu'il travaille ; Arrêter l'interrompt aussitôt et conserve ce qu'il avait produit jusque-là.
- Continuer à travailler en attendant. Réduisez une fenêtre (▾) et elle devient une petite pastille en bas de l'écran qui montre où elle en est ; l'exécution continue. Quand un Gramplet réduit se termine, vous recevez une notification avec un bouton Afficher. Plusieurs Gramplets peuvent tourner en même temps (jusqu'à cinq fenêtres), chacun indépendamment, et ils restent ouverts quand vous passez d'une vue à l'autre.
- Déplacer, redimensionner, agrandir. Faites glisser une fenêtre par sa barre de titre, redimensionnez-la par son coin, ou agrandissez-la (⤢, ou double-clic sur le titre). Chaque Gramplet mémorise la taille et la position de sa fenêtre dans votre navigateur.
- Modification. Pour le propriétaire de l'arbre ou un administrateur du site (voir Qui peut utiliser les Gramplets), la barre de titre de la fenêtre comporte un crayon (✎) qui ouvre l'éditeur de Gramplets ; enregistrer relance la fenêtre avec vos modifications.
- Fermeture. Pour relancer un Gramplet depuis le début, fermez sa fenêtre et choisissez-le à nouveau dans le menu. Fermer une fenêtre encore en cours d'exécution demande d'abord confirmation, de même que quitter ou recharger la page — fermer l'onglet du navigateur arrête toutes les exécutions.
Les résultats ne sont affichés que dans la fenêtre ; ils ne sont encore enregistrés nulle part.
Le code d'un Gramplet s'exécute dans un bac à sable avec un ensemble de
fonctions et d'objets déjà disponibles — aucun import nécessaire pour
atteindre l'arbre. Le répertoire
gramplet_examples/
du dépôt est un parcours numéroté à travers l'API, du plus simple au
plus avancé ; la même référence est disponible depuis l'application
elle-même via le bouton d'aide (i) « Écrire un Gramplet » dans
l'éditeur de Gramplets.
matches = people(
"gender == Person.MALE and birth.date.sortval >= Date('Jan 1, 1900')",
limit=25,
)
columns("Person")
for person in matches:
row(person)-
people(where=None, what=None, order=None, limit=50)récupère les Individus correspondant à une requête, comme fiches complètes, en un seul appel — ou, avecwhat=["gramps_id", "primary_name"], seulement ces champs — et il existe unfamilies()/events()/places()/repositories()/sources()/citations()/media()/notes()/tags()pour chaque autre type d'objet, même signature, juste une table différente. -
where=s'écrit en GOQL — exactement la même syntaxe que le champ de recherche de n'importe quelle vue de liste, y compris atteindre à travers des relations (birth.date.sortval) même quand le champ filtré n'est pas un de ceux affichés. - Pas besoin de
awaitbien que ce soit un véritable appel réseau sous le capot — il est inséré automatiquement. -
row(*values)ajoute une ligne au tableau. Passer un objet Person/Event/Place/… entier (plutôt qu'un champ choisi à la main) le représente comme un lien cliquable qui montre déjà son nom/titre complet et son ID Gramps — cliquer dessus ouvre une fenêtre pour voir cette fiche en Liste, Carte, Graphe, ou Chronologie. -
name_of(person)est le nom d'un individu sous forme de texte — prénom, puis noms de famille (« Root Ancestor »), commerow()les étiquette — pour un titre ou une phrase. Accepte aussi l'un de ses noms, p. ex.name_of(person.alternate_names[0]).
Chaque fiche renvoyée par people()/families()/etc. est un
« DataDict » Gramps — un dict ordinaire des champs de l'objet, mais
avec un accès par point en plus : person.primary_name.first_name est
la même valeur que person["primary_name"]["first_name"]. Y recourir
chaque fois qu'il faut calculer quelque chose à partir d'un champ ou
montrer quelque chose que la propre représentation de row() ne fait
pas apparaître. row(), html(), et print() peuvent chacun être
appelés autant de fois que voulu, dans n'importe quel ordre — tout
apparaît dans l'ordre où cela a été ajouté.
Afficher une fiche n'échoue jamais : print(person) ou f"{person}"
montre une fiche entière comme row() l'étiquette (« Root Ancestor
[I0001] »), une date comme une date, et tout le reste — un résultat de
filter(), person.primary_name — comme ses champs bruts.
total_people = db.get_number_of_people()
women = db.get_number_of("person", where="gender == Person.FEMALE")
no_birth_date = db.get_number_of("person", where="birth.date.sortval is None")db.get_number_of_<type>() est un comptage économique sur tout
l'arbre — le total revient dans un en-tête de réponse, aucune fiche
n'est réellement téléchargée. db.get_number_of(object_type, where=...) est l'équivalent conditionnel, pour quand le comptage
lui-même a besoin d'un filtre.
Une condition where= peut atteindre à travers une relation pour
filtrer dessus — l'événement de naissance d'une personne, le père
d'une famille, le lieu d'un événement — même si people()/
families()/etc. renvoient toujours un objet entier, indépendant, sans
moyen de redemander directement un champ lié :
born_and_died_same_place = people(
"birth.place.title == death.place.title and birth.place.title is not None",
order=[{"column": "surname", "direction": "asc"}],
limit=50,
)Récupérer un champ lié pour l'affichage (plutôt que simplement
filtrer dessus) est une recherche séparée, à la main, via les propres
méthodes de db — db.get_event_from_handle(...), et ainsi de
suite — suivant la même indirection par handle que le propre modèle de
données de Gramps utilise en interne.
Les collections fonctionnent aussi : any(c.given_name == 'Steve' for c in children) correspond à une famille si un enfant quelconque satisfait
une condition, et len([c for c in children if c.gender == Person.MALE]) > 1 compte seulement les fils. Voir
GOQL pour l'histoire complète des
collections, y compris sa limite actuelle (un saut dans une collection,
pas d'enchaînement supplémentaire au-delà de ce qu'elle contient).
record = get_selected()
if record is not None:
if isinstance(record, Person):
...get_selected() renvoie la fiche actuellement ouverte dans le propre
panneau de détail de la vue — None quand rien n'est sélectionné, ou
en s'exécutant depuis l'aperçu de l'éditeur autonome (qui n'a aucun
contexte de vue du tout). Un seul appel réseau au premier appel d'une
exécution, puis mémorisé pour le reste. isinstance(record, Person)
(etc.) indique quel type de fiche a été obtenu, ce qui importe pour un
Gramplet attaché aux vues « Toutes ». Pour que cela se mette à jour en
direct en cliquant à travers la liste, activer « Réexécuter
automatiquement quand la fiche sélectionnée change » dans le propre
éditeur du Gramplet — désactivé (par défaut), get_selected()
fonctionne toujours, mais ne reflète que ce qui était sélectionné la
dernière fois que le Gramplet s'est exécuté pour une autre raison.
Un Gramplet peut superposer sa propre requête à tout filtre actuellement appliqué à la vue — soit tapé dans le propre champ de recherche de la vue, soit appliqué via le sélecteur « Filtres » (un filtre enregistré ou une Règle personnalisée) — filtrer la liste pour la réduire à une branche de l'arbre restreint donc la propre sortie du Gramplet de la même façon :
rows = filter(
"person",
where=and_filters(get_filter(), "birth.date.sortval is not None and death.date.sortval is not None"),
)Cela nécessite views: [...] défini dans le manifeste (get_filter()
ne renvoie qu'un where_expr correspondant au propre type d'objet de
la vue) et listensToFilter: true pour que le Gramplet se réexécute
réellement quand le filtre change.
pygal, matplotlib et plotly sont tous préinstallés — un simple import
(ou, pour plotly, from plotly.subplots import make_subplots)
fonctionne hors ligne, aucun %pip install nécessaire. print(fig)
sur une Figure plotly, une chaîne SVG de pygal, ou une figure
matplotlib est reconnu automatiquement et rendu comme un diagramme —
aucun to_html()/intégration manuel nécessaire.
Pour les données de diagramme, passer what=[...] quand seuls un ou
deux champs sont nécessaires — people() seul télécharge chaque champ
de chaque fiche, ce qui est gaspillé pour un diagramme qui ne trace,
disons, que l'âge au décès.
col1, col2 = st.columns([2, 1])
with col1:
row("Colonne plus large")
with col2:
row("Colonne plus étroite")st.columns(spec) dispose spec régions côte à côte — un entier pour
ce nombre de colonnes de largeur égale, ou une liste de poids
(st.columns([2, 1])) pour des largeurs proportionnelles — et renvoie
une région par colonne. Tout ce qui est écrit à l'intérieur de
with col: (row()/html()/print(), ou un autre appel st.*, y
compris un st.columns() imbriqué) atterrit dans cette colonne plutôt
qu'au niveau supérieur ; col.write(x) fonctionne de la même façon
sans bloc with.
Gramplets → Nouveau Gramplet… ouvre l'éditeur de Gramplets prêt
pour cette sorte (pour un Gramplet existant, réglez Sorte sur
Gramplet — s'ouvre dans une fenêtre depuis le menu Gramplets). À la
place des réglages de Gramplet de vue, il y a un champ Catégorie,
qui le regroupe avec d'autres dans le menu Gramplets (par ex. chart) ;
vide, il est rangé sous Autres.
Le code s'exécute dès que la fenêtre s'ouvre. Pour demander d'abord une
saisie, dessinez-la avec les widgets st.* et faites le vrai travail
derrière un st.button. Ils suivent les noms et arguments de
Streamlit :
st.button, st.text_input, st.number_input, st.checkbox,
st.selectbox, st.radio, st.columns, st.progress, les boîtes de
message st.info / st.success / st.warning / st.error, et
st.session_state. Ils fonctionnent de la même façon dans les Gramplets
de vue. Pour un Gramplet de longue durée, affichez une barre avec
st.progress(value, text), comme dans Streamlit : value va de 0 à 100
(ou de 0.0 à 1.0), et le .progress(...) de la barre renvoyée la
déplace sur place tandis que .empty() la retire — assez peu coûteux
pour être appelé une fois par ligne. Cela fonctionne aussi dans les
Gramplets de vue. people(..., limit=None) (et les autres fonctions de
liste) renvoient toutes les correspondances au lieu des 50 premières :
surname = st.text_input("Surname", "Smith")
if st.button("Run"):
everyone = people(f"primary_name.surname_list[0].surname == '{surname}'", limit=None)
bar = st.progress(0, text="Checking people...")
for i, person in enumerate(everyone):
bar.progress((i + 1) / len(everyone))
...
bar.empty()%pip install unidecode
from unidecode import unidecode%pip install est une syntaxe magique de Jupyter, réécrite en un
véritable appel await micropip.install([...]) avant l'exécution du
code — cela doit être sur sa propre ligne, tout en haut, exactement
comme dans un notebook (l'écrire à l'intérieur d'un if, par exemple,
ne fonctionnera pas). Cela ne fonctionne que pour les paquets Python
pur, sans code compilé/extension C ; pygal/matplotlib/plotly n'en ont
pas besoin car ils sont préinstallés, mais la plupart des petits
utilitaires Python purs sur PyPI s'installent bien de cette façon.
Un Gramplet (pas un Gramplet de vue — ceux-là ne modifient jamais de données) peut modifier et supprimer des enregistrements une fois Autoriser les modifications de données activé dans l'éditeur de Gramplets. L'API est celle qu'utilise Gram.py Script dans Gramps de bureau : on modifie un enregistrement comme n'importe quel objet Python.
for person in people("len(citation_list) == 0", limit=None):
person.private = TrueMark Unsourced People Private, dans la boutique, est un exemple
complet : il trouve d'abord les personnes à moindre coût avec
filter(), puis ne récupère et ne modifie que celles qui en ont besoin,
au sein d'un groupe begin_changes() nommé. Tag People montre
l'ajout à une liste (person.tag_list.append(tag.handle) — les
enregistrements stockent les étiquettes sous forme de handles, pas de
noms) et un st.selectbox() dans lequel vous devez choisir avant que
quoi que ce soit ne change.
-
record.field = valuemodifie un champ, y compris imbriqué (person.primary_name.first_name = "Chris"). De même pour les modifications de listes (.append(),.pop(), ...) et toute méthodeset_*(), par ex.person.set_privacy(True). -
delete(record)supprime un enregistrement. Cela ne retire pas les références à celui-ci depuis d'autres enregistrements. -
begin_changes("message")/end_changes()regroupent les modifications intermédiaires sous ce message. Facultatif : sans eux, toute l'exécution forme un seul groupe, nommé d'après le Gramplet.
Les enregistrements issus de people() et des autres fonctions au
pluriel, de get_raw_object(), get_object(), get_selected() et
db.* peuvent être modifiés. Les résultats de filter() ne le peuvent
pas, puisqu'ils ne contiennent que certains champs d'un enregistrement.
Demander deux fois le même enregistrement au cours d'une exécution
renvoie le même objet, avec vos modifications dedans.
D'abord un essai à blanc. Dans l'éditeur de Gramplets, chaque
exécution est un essai à blanc, et ouvrir la fenêtre du Gramplet depuis
le menu Gramplets en fait un aussi. Rien n'est enregistré ; sous la
sortie, un encadré jaune Essai à blanc liste chaque enregistrement
qui changerait, avec l'ancienne et la nouvelle valeur de chaque champ
modifié, et chaque enregistrement qui serait supprimé, avec le nombre
d'autres enregistrements qui y font encore référence. Chaque
enregistrement modifié est d'abord vérifié par rapport à son schéma
Gramps, la même vérification que fait le serveur ; une valeur invalide
arrête l'exécution et nomme le champ. Un point où un essai à blanc peut
différer de l'exécution réelle : les recherches (filter(), count(),
where=) s'exécutent sur le serveur, elles voient donc encore l'arbre
tel qu'il était avant les modifications de l'exécution.
Ensuite, Exécuter. La fenêtre du Gramplet a des boutons Essai à
blanc et Exécuter (le menu marque ces Gramplets d'un ✎).
Exécuter enregistre chaque groupe de modifications au moment où le
code le ferme — à end_changes(), ou à la fin de l'exécution — en une
seule transaction, si bien qu'il apparaît comme une seule entrée dans
l'historique de l'arbre. Tant que le résultat d'une exécution réelle est
affiché, la barre de titre indique Enregistrement des modifications,
et l'encadré sous la sortie devient vert et marque chaque groupe comme
enregistré. Si le serveur refuse un groupe — très probablement parce
que quelqu'un d'autre a modifié l'un de ses enregistrements après que le
Gramplet l'a lu —, rien de ce groupe n'est enregistré, l'exécution
s'arrête avec une erreur, et les groupes enregistrés avant lui le
restent. Les widgets de la sortie (st.button("Apply"), etc.)
réexécutent le code dans le mode d'où provient le résultat actuel, de
sorte qu'un Gramplet peut montrer ce qu'il changerait lors d'un essai à
blanc et l'appliquer après Exécuter.
Annuler. Après une exécution réelle, la fenêtre indique combien de groupes elle a enregistrés et affiche un bouton Annuler, qui les défait, le dernier en premier. Annuler refuse un groupe dont quelqu'un a modifié les enregistrements depuis, plutôt que d'écraser ces modifications. Il n'est proposé que pour le résultat actuel de la fenêtre : relancer le Gramplet, ou fermer sa fenêtre, y met fin.
Un dossier par Gramplet dans
gramplet-store/,
nommé d'après son id (le nom du dossier et le propre champ id du
manifeste doivent correspondre) :
gramplet-store/
<slug>/
manifest.json # requis
code.py # requis -- le code source Python du Gramplet
icon.png # optionnel (png/jpg/jpeg/svg/webp)
Champs de manifest.json :
| Champ | Requis | Signification |
|---|---|---|
id |
oui | Doit correspondre au nom du dossier. |
name |
oui | Affiché comme titre de l'entrée dans la boutique. |
description |
oui | Affichée sous le nom dans la boutique. |
version |
oui | Semver simple, par ex. "1.0.0" — l'incrémenter à chaque changement de code.py ou du manifeste, pour que les copies installées puissent détecter qu'une mise à jour est disponible. |
author |
oui | Affiché dans la boutique. |
category |
oui | Une courte étiquette ("example", "detail", "utility", "chart", …) utilisée pour grouper/filtrer les entrées. |
views |
non | À quelles vues de type d'objet ("person", "family", …) ce Gramplet peut être ajouté. Omettre pour « tous les types ». |
listensToSelection |
non | Si ce Gramplet doit se réexécuter quand la fiche sélectionnée change. |
listensToFilter |
non | Si ce Gramplet doit se réexécuter quand le filtre actif change (champ de recherche ou sélecteur Filtres). |
kind |
non |
"view" (par défaut) pour un Gramplet de vue ; "window" pour un Gramplet lancé depuis le menu Gramplets. views/listensTo* ne s'appliquent qu'aux Gramplets de vue. |
allowChanges |
non |
true pour l'installer avec Autoriser les modifications de données activé (la boutique affiche un badge Modifie vos données). Autorisé seulement avec "kind": "window". |
Après avoir ajouté ou modifié une entrée, régénérer catalog.json —
le seul fichier que l'application récupère réellement :
npm --prefix app run build:gramplet-catalogCela valide chaque entrée (champs requis présents, id correspond à
son dossier, version ressemble à du semver, code.py non vide, kind valide) et
échoue bruyamment au premier problème plutôt que de publier un
catalogue défectueux.
Les Gramplets peuvent modifier et supprimer des enregistrements (voir Modifier des données) mais ne peuvent pas encore en créer de nouveaux. Il n'y a pas non plus encore d'équivalent pour les autres types d'add-ons de Gramps (outils, rapports). Voir Feuille de route et limitations connues pour l'état actuel de tout cela.