Yayaw
Documentation
Vues

Vue Fil

Afficher une vue comme un fil de publications, chargé page par page depuis votre serveur, les plus récentes d’abord.

La vue Fil fonctionne comme le Fil de Notion : une vue de la table affiche chaque fiche comme une publication dans une colonne centrée (720 px de large au plus), avec son titre, son auteur et sa date, un contenu, des médias et une ligne de propriétés. Les publications sont chargées page par page depuis votre serveur, les plus récentes d’abord, et suivent la recherche, les filtres et le regroupement de la vue. React et Vue se comportent de la même façon.

Activer

Le mode Fil est livré avec les éléments de la table et n’ajoute aucune dépendance. Ajoutez "feed" à displayModes :

feed-config.ts
export const updatesConfig = defineTableConfig({
  ...projectConfig,
  table: {
    ...projectConfig.table,
    displayModes: ["table", "feed"],
    feed: {
      titleColumn: "name",
      authorColumn: "author",
      dateColumn: "postedAt",
      bodyColumn: "update",
      propertyColumnIds: ["status", "category", "dueDate"],
    },
  },
});

table.feed est optionnel : un objet de réglages par défaut (la forme ci-dessous) dont part chaque vue Fil, plus le hook renderBody, ou false pour désactiver le mode. Avec false, "feed" n’est pas proposé même s’il figure dans displayModes, et un lien qui le demande revient au mode par défaut. Un renderer passé dans displayModeRenderers.feed remplace celui intégré.

Essayer le fil

Chaque publication est la dernière actualité d’un projet, les plus récentes d’abord et cinq par page : le titre, le responsable avec ses initiales, une date relative, un texte limité à trois lignes avec Afficher plus, puis le statut, la catégorie et la date de début. Ouvrez Paramètres de la vue › Réglages des cartes pour changer les colonnes, le nombre de lignes ou la taille des pages. L’aperçu utilise les projets d’ouverture de magasins communs à tous les guides de vues ; voir les données des exemples pour project-config.ts et l’hôte en mémoire.

Agrandir ↗

Données de démonstration. Les modifications restent dans cet aperçu.

Agrandir ↗

Données de démonstration. Les modifications restent dans cet aperçu.

Publications

Chaque publication affiche :

  • Titre : un bouton qui ouvre la vue de la fiche, comme un clic sur une ligne. Un titre vide affiche « Sans titre ».

  • Auteur : une valeur texte, ou une valeur de type personne { name, avatarUrl } (label ou email servent aussi de nom). Sans avatar, les initiales de l’auteur sont affichées.

  • Date : relative par défaut dans la langue de la table (« il y a 3 h », « hier », « 3 hr. ago »), avec la date complète au survol et dans un élément <time datetime>. Les dates sans heure comme 2026-09-24 se comparent par jour calendaire local. Mettez dateDisplay: "absolute" pour afficher la date complète à la place.

  • Contenu : du texte brut dont les retours à la ligne sont conservés, limité à bodyLines lignes. Voir plus et Voir moins sont des boutons (avec aria-expanded), proposés seulement quand le contenu dépasse, et utilisables au clavier.

  • Médias : jusqu’à quatre images de la colonne de médias (URL, listes d’URL ou objets { url, name, type }, avec la même règle d’URL sûre que la galerie), avec un compteur « +N » pour les autres. Les autres fichiers sont listés sous forme de liens. Les images se chargent à la demande dans des cadres de proportions fixes (loading="lazy", width et height) : rien ne bouge quand elles arrivent. Les vidéos affichent leur image d’aperçu avec les contrôles natifs, ne démarrent jamais seules et ne chargent rien avant la lecture. Quand table.gallery.media.enabled couvre la colonne de médias du fil, la source qu’il résout est utilisée.

  • Propriétés : les colonnes d’options en étiquettes (les réglages d’étiquettes colorées s’appliquent), les nombres et dates formatés comme dans la table, les liens sûrs, et du texte sinon. Les valeurs vides sont omises.

Quand une vue est regroupée (un niveau), les publications sont affichées en sections sous le libellé du groupe et son nombre, dans l’ordre d’apparition.

Réglages du fil

Choisissez Fil dans Paramètres de la vue › Mode d’affichage, puis ouvrez Réglages des cartes dans le même menu. Les réglages sont enregistrés avec la vue (config.feed) et dans la clé d’URL <tableId>-feed, par exemple ?projects-display=feed&projects-feed={"bodyLines":8}. Réinitialiser revient à table.feed, puis aux valeurs par défaut ci-dessous.

RéglagePar défautDescription
titleColumnPremière colonne texteLe titre de la publication.
authorColumnDeviné d’après le nom de la colonne (auteur, owner, created by…)L’auteur. null n’en affiche aucun.
dateColumnDeviné d’après le nom de la colonne (publié, créé, posted…), sinon la première colonne dateLa date de publication, qui sert aussi à trier le fil. null n’en affiche aucune.
dateDisplay"relative""relative" (« il y a 3 h », date complète au survol) ou "absolute" (date complète).
bodyColumnDeviné d’après le nom de la colonne (contenu, body, update, description…) parmi les colonnes texteLe contenu de la publication. null n’en affiche aucun.
mediaColumnPremière colonne d’images ou de fichiersLes images et fichiers affichés sous le contenu. null n’en affiche aucun.
propertyColumnIdsLes colonnes d’options (sélection, sélection multiple, étiquettes)Les colonnes affichées en bas de chaque publication, dans l’ordre. Les colonnes de titre, d’auteur, de date, de contenu, de médias et de regroupement ne sont jamais répétées.
showPropertyLabelsfalseAfficher le nom de la colonne avant chaque valeur de propriété.
bodyLines4Lignes de contenu affichées avant Voir plus, de 0 à 20. 0 affiche toujours le texte entier. Le panneau propose 2, 3, 4, 6, 8, 12 et le texte entier.
density"comfortable"Cartes "comfortable" (confortables) ou "compact" (compactes).
pageSize10Publications demandées par page, de 1 à 100. Le panneau propose 5, 10, 20 et 50.
infiniteScrolltrueCharger la page suivante quand la fin du fil arrive à moins d’un écran de la zone visible. false garde seulement Charger plus.

Les colonnes qui n’existent plus sont ignorées et devinées à nouveau.

table.feed.windowing est une option de la table, jamais enregistrée dans les vues. Au-delà de 60 publications chargées (ou du nombre que vous indiquez), seules les publications proches de la zone visible sont affichées ; les autres gardent leur hauteur mesurée, donc la position de défilement ne bouge pas. La publication qui a le focus et celles dont une vidéo est en lecture restent affichées. false affiche toutes les publications, par exemple pour que la recherche dans la page du navigateur les trouve toutes.

Charger depuis votre serveur

Le fil demande à actions.list une page à la fois, avec la requête de la vue (recherche, filtres, filtres avancés) et :

  • page (à partir de 1) et pageSize (aussi envoyé comme limit) ;

  • quand la vue n’a pas de tri propre, la colonne de date, les plus récentes d’abord : sorting: [{ id: dateColumn, desc: true }] et orderBy: { [dateColumn]: "desc" } ;

  • grouping: [columnId] quand la vue est regroupée.

{
  search: "",
  filters: {},
  sorting: [{ id: "postedAt", desc: true }],
  orderBy: { postedAt: "desc" },
  page: 2,
  pageSize: 10,
  limit: 10,
}

Renvoyez uniquement les lignes de cette page dans data, comme dans la réponse de list, avec meta.pageCount ou meta.totalCount pour que le fil sache quand il est arrivé au bout. Sans l’un ni l’autre, une page plus courte que pageSize termine le fil. Un list qui renvoie toutes les lignes d’un coup les affiche toutes dès la première page.

server/list-updates.ts
export async function listUpdates(params: Record<string, unknown>) {
  const page = Math.max(1, Number(params.page) || 1);
  const pageSize = Math.min(100, Math.max(1, Number(params.pageSize) || 10));
  const [rows, totalCount] = await Promise.all([
    db.updates.findMany({
      where: buildWhere(params), // vos filtres de list
      orderBy: buildOrderBy(params.sorting), // par ex. postedAt desc
      skip: (page - 1) * pageSize,
      take: pageSize,
    }),
    db.updates.count({ where: buildWhere(params) }),
  ]);
  return {
    data: rows,
    meta: { pageCount: Math.ceil(totalCount / pageSize), totalCount },
  };
}

Par défaut (infiniteScroll), la page suivante se charge quand la fin du fil arrive à moins d’un écran de la zone visible, ou du plus proche parent qui défile, comme un widget de tableau de bord. Les pages se chargent une requête à la fois, sans répéter les fiches déjà affichées. Charger plus reste la solution de repli pour le clavier et les technologies d’assistance : il est dans l’ordre de tabulation, s’affiche quand il a le focus, et reste visible quand infiniteScroll vaut false, sans IntersectionObserver et après une erreur. Chaque page chargée est annoncée, par exemple « 10 publications de plus chargées, 30 affichées. ». Une page en échec garde les publications affichées, le signale et suspend le chargement jusqu’à Réessayer. Au bout du fil s’affiche « Vous êtes à jour ». Après une création, une modification ou une suppression, les pages déjà affichées sont rechargées. Sans action list, les lignes locales de la table sont paginées et triées de la même façon.

Pendant le chargement de la première page, des publications squelettes sont affichées ; un résultat vide affiche l’état vide de la table, et un échec de la première page affiche une erreur avec Réessayer. Le code de la vue Fil se charge avec le premier fil affiché (lazy en React, defineAsyncComponent en Vue), avec le squelette en attendant.

Contenus markdown et HTML

Les contenus sont du texte brut par défaut, et rien n’est jamais inséré comme HTML. Pour afficher du markdown ou du texte enrichi, définissez table.feed.renderBody(value, row) : React renvoie un nœud, Vue un VNode ou une chaîne. Le contenu affiché est limité en hauteur avec le même bouton Voir plus. renderBody est un hook d’exécution : il n’est jamais enregistré dans les vues ni dans les URL.

Si vous affichez du HTML, nettoyez-le d’abord. Les contenus viennent de vos fiches : du HTML non nettoyé permettrait à quiconque peut modifier une fiche d’injecter des scripts dans la page de tous les lecteurs. Cet exemple convertit le markdown avec marked et nettoie le résultat avec DOMPurify :

feed-body.tsx
import DOMPurify from "dompurify";
import { marked } from "marked";

function renderBody(value: unknown) {
  const markdown = typeof value === "string" ? value : "";
  // Nettoyer avant d’afficher : le contenu des fiches n’est pas fiable.
  const html = DOMPurify.sanitize(marked.parse(markdown, { async: false }));
  return (
    <div className="prose prose-sm" dangerouslySetInnerHTML={{ __html: html }} />
  );
}

export const updatesConfig = defineTableConfig({
  ...projectConfig,
  table: {
    ...projectConfig.table,
    displayModes: ["table", "feed"],
    feed: { bodyColumn: "update", renderBody },
  },
});
feed-body.ts
import DOMPurify from "dompurify";
import { marked } from "marked";
import { h } from "vue";

function renderBody(value: unknown) {
  const markdown = typeof value === "string" ? value : "";
  // Nettoyer avant d’afficher : le contenu des fiches n’est pas fiable.
  const html = DOMPurify.sanitize(marked.parse(markdown, { async: false }));
  return h("div", { class: "prose prose-sm", innerHTML: html });
}

export const updatesConfig = defineTableConfig({
  ...projectConfig,
  table: {
    ...projectConfig.table,
    displayModes: ["table", "feed"],
    feed: { bodyColumn: "update", renderBody },
  },
});

Un composant markdown qui n’affiche aucun HTML brut, comme react-markdown sans rehype-raw, n’a pas besoin de nettoyage : renvoyez-le directement depuis renderBody.

Traductions

Le fil a des libellés anglais et français intégrés ; le français est utilisé quand la locale commence par fr. Remplacez-en n’importe lequel avec des clés plates feed.<clé> dans les traductions de la table, en React comme en Vue, par exemple "feed.loadMore": "Publications plus anciennes".

  • Fil : showMore, showLess, loadMore, loading, loadingMore, error, loadMoreError, retry, end, loadedOne (avec {total}), loadedMany (avec {count} et {total}), untitled, noValue, yes, no, moreImages (avec {count}), by, media et video (avec {title}).

  • Réglages : titleColumn, authorColumn, dateColumn, dateDisplay, relative, absolute, bodyColumn, mediaColumn, properties, showPropertyLabels, bodyLines, lines (avec {count}), noLimit, density, comfortable, compact, pageSize, infiniteScroll, on, off, none.

Le bouton Réinitialiser lit common.reset. Le nom du mode dans le sélecteur de mode d’affichage est views.display.feed en React et display.feed en Vue.

Notes de compatibilité

TableDisplayMode inclut désormais "feed". Si votre code garde une table exhaustive Record<TableDisplayMode, …>, comme des icônes ou des libellés par mode, ajoutez une entrée feed pour qu’il compile toujours :

const modeLabels: Record<TableDisplayMode, string> = {
  // ...
  feed: "Fil",
};

Les contextes des renderers gagnent groupBy, que les renderers de mode d’affichage personnalisés peuvent utiliser de la même façon.