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 :
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.
Données de démonstration. Les modifications restent dans cet aperçu.
import { defineTableConfig } from "@/components/ui/yayaw-table";
import { projectConfig } from "./project-config";
export const exampleConfig = defineTableConfig({
...projectConfig,
// Newest posts first; without a sort, the feed also sorts by its date column.
columns: { ...projectConfig.columns, sort: [{ id: "postedAt", desc: true }] },
table: {
...projectConfig.table,
displayModes: ["feed", "table"],
defaultDisplayMode: "feed",
feed: {
titleColumn: "name",
authorColumn: "author",
dateColumn: "postedAt",
bodyColumn: "update",
propertyColumnIds: ["status", "category", "start"],
bodyLines: 3,
pageSize: 5,
},
},
});Données de démonstration. Les modifications restent dans cet aperçu.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue";
import { projectConfig } from "./project-config";
export const exampleConfig = defineTableConfig({
...projectConfig,
// Newest posts first; without a sort, the feed also sorts by its date column.
columns: { ...projectConfig.columns, sort: [{ id: "postedAt", desc: true }] },
table: {
...projectConfig.table,
displayModes: ["feed", "table"],
defaultDisplayMode: "feed",
feed: {
titleColumn: "name",
authorColumn: "author",
dateColumn: "postedAt",
bodyColumn: "update",
propertyColumnIds: ["status", "category", "start"],
bodyLines: 3,
pageSize: 5,
},
},
});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 }(labelouemailservent 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 comme2026-09-24se comparent par jour calendaire local. MettezdateDisplay: "absolute"pour afficher la date complète à la place.Contenu : du texte brut dont les retours à la ligne sont conservés, limité à
bodyLineslignes. Voir plus et Voir moins sont des boutons (avecaria-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",widthetheight) : 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. Quandtable.gallery.media.enabledcouvre 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églage | Par défaut | Description |
|---|---|---|
titleColumn | Première colonne texte | Le titre de la publication. |
authorColumn | Deviné d’après le nom de la colonne (auteur, owner, created by…) | L’auteur. null n’en affiche aucun. |
dateColumn | Deviné d’après le nom de la colonne (publié, créé, posted…), sinon la première colonne date | La 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). |
bodyColumn | Deviné d’après le nom de la colonne (contenu, body, update, description…) parmi les colonnes texte | Le contenu de la publication. null n’en affiche aucun. |
mediaColumn | Première colonne d’images ou de fichiers | Les images et fichiers affichés sous le contenu. null n’en affiche aucun. |
propertyColumnIds | Les 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. |
showPropertyLabels | false | Afficher le nom de la colonne avant chaque valeur de propriété. |
bodyLines | 4 | Lignes 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). |
pageSize | 10 | Publications demandées par page, de 1 à 100. Le panneau propose 5, 10, 20 et 50. |
infiniteScroll | true | Charger 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) etpageSize(aussi envoyé commelimit) ;quand la vue n’a pas de tri propre, la colonne de date, les plus récentes d’abord :
sorting: [{ id: dateColumn, desc: true }]etorderBy: { [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.
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 :
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 },
},
});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,mediaetvideo(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.