Yayaw
Documentation
Afficher et explorer

État URL (Nuqs)

Tri, filtres, pagination et état des colonnes dans l'URL avec Nuqs

État URL (Nuqs)

La table conserve tri, filtres, pagination, visibilité, ordre et largeur des colonnes, mode d'affichage, groupement et pinning dans l'URL avec Nuqs. Cela apporte :

  • Liens partageables – Envoyez un lien et le destinataire voit la même vue (même tri, filtres, page).

  • Retour/avance navigateur – L'historique reflète l'état de la table.

  • Compatible SSR – La même URL peut être utilisée pour le rendu côté serveur ou le prefetch.

Utilisez table.syncUrl: false ou la prop de composant syncUrl={false} lorsque la page hôte contrôle la navigation. React et Vue conservent alors le même état de table dans une mémoire partagée et isolée par instance, sans lire ni écrire les paramètres de table dans l'URL. Les champs de formulaire tablePicker imbriqués utilisent ce mode par défaut afin de ne pas écraser les paramètres de la table parente ; activez leur état URL uniquement avec syncUrl: true explicite.

React ignore les écritures répétées d’un état de tableau équivalent, en mode URL comme en mémoire. Réappliquer un ordre de colonnes inchangé ne relance pas les abonnements du tableau pendant les actions de ligne ou l’ouverture d’un formulaire.

Précédent et suivant ne règlent que ce que l’URL a changé. Revenir à la même requête, ou à un autre ordre de colonnes, ne recharge ni les lignes ni les modes d’affichage (Arborescence, Fil, Calendrier, Graphique, Carte) : en React, et en Vue depuis v3.9.2.

Setup (Next.js App Router)

  1. Installez nuqs :

npm install nuqs
  1. Ajoutez NuqsAdapter dans votre layout racine :

// app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NuqsAdapter>
          {children}
        </NuqsAdapter>
      </body>
    </html>
  );
}

Aucune autre configuration n'est nécessaire ; la table utilise Nuqs en interne.

Ce qui est stocké dans l'URL

Les query params sont préfixés par table id (votre tableType, par ex. products), ou par l'instanceId si vous en définissez un. Noms typiques :

ParamExempleDescription
{tableId}-sortproducts-sortÉtat du tri (tableau de { id, desc }).
{tableId}-filtersproducts-filtersFiltres de colonnes.
{tableId}-advancedFiltersproducts-advancedFiltersRègles de filtres avancés ; les valeurs des règles de date sont des jours du calendrier (AAAA-MM-JJ). Les liens écrits par les versions antérieures à v3.8.0 portent des instants et s’ouvrent sur les mêmes jours.
{tableId}-qproducts-qRecherche globale.
{tableId}-pageproducts-pageIndex de page en base 0.
{tableId}-pageSizeproducts-pageSizeTaille de page.
{tableId}-visibilityproducts-visibilityVisibilité des colonnes (objet).
{tableId}-orderproducts-orderOrdre des colonnes. React l’écrit quand la personne déplace une colonne ou applique une vue ; sans lui, la table affiche columns.order.
{tableId}-sizingproducts-sizingLargeurs de colonnes définies par l'utilisateur, en pixels.
{tableId}-displayproducts-displayMode d'affichage : table, list, kanban, gallery, filetree, calendar, chart, feed, map, form ou gantt, parmi ceux que la table propose.
{tableId}-kanbanproducts-kanbanOverrides Kanban : lanes, colonne titre, propriétés de carte et libellés.
{tableId}-kanbanGroupByproducts-kanbanGroupByParamètre legacy de colonne de lanes Kanban. Encore lu en fallback si {tableId}-kanban.groupBy est absent.
{tableId}-galleryproducts-galleryOverrides Gallery : image, titre, propriétés, ratio média, fit, taille et libellés.
{tableId}-groupingproducts-groupingIDs des colonnes de regroupement.
{tableId}-pinningproducts-pinningColonnes épinglées (gauche/droite).

Les valeurs sont encodées en JSON ou en string simple. La table les lit et les écrit via Nuqs ; vous n'avez pas besoin de les lire vous-même sauf si vous voulez les exploiter côté serveur (par ex. pour SSR).

Plusieurs tables sur une page

Par défaut, les clés d'URL d'une table commencent par son identifiant (products-sort, products-q…), et la vue enregistrée active utilise la clé partagée view (React écrit aussi historyIndex). Deux tables de même identifiant, ou deux tables avec des vues enregistrées, sur une même page liraient et écriraient donc les mêmes clés. Donnez à chacune un instanceId :

two-task-tables.tsx
<DataTable tableType="tasks" instanceId="my-tasks" />
<DataTable tableType="tasks" instanceId="team-tasks" />
TwoTaskTables.vue
<template>
  <YayawDataTable :config="tasksConfig" :get-table-actions="() => tasksActions" instance-id="my-tasks" />
  <YayawDataTable :config="tasksConfig" :get-table-actions="() => tasksActions" instance-id="team-tasks" />
</template>

Avec instanceId, les clés de l'instance sont <instanceId>-view, <instanceId>-historyIndex (React) et <instanceId>-<clé>, comme my-tasks-sort, au lieu de view, historyIndex et <tableId>-<clé>. Elle ne lit que ses propres clés dans une URL reçue. En React, chaque instance garde aussi son état dans un store à elle, si bien que deux instances d'une même table ne partagent jamais filtres, sélection ou pagination ; les instances Vue gardent déjà leur état séparé. La config, les actions et les vues enregistrées restent celles de la table. Sans instanceId, les clés et l'état ne changent pas.

Une table intégrée sans barre d'outils, comme un panneau latéral ou un widget de tableau de bord, peut partir d'une vue enregistrée avec initialView: { id?, config } : une vue enregistrée (son id devient la vue active) ou une config de vue. Elle s'applique avant la première requête, si bien que le premier list porte déjà les filtres et le tri de la vue. initialView demande une synchronisation d'URL désactivée (table.syncUrl: false dans la config de la table) et est ignorée sinon ; avec la synchronisation d'URL, utilisez initialActiveViewId.

open-tasks-panel.tsx
const embeddedTasks = defineTableConfig({
  ...tasksConfig,
  table: { ...tasksConfig.table, syncUrl: false, showToolbar: false },
});

<DataTable
  tableType="tasks"
  getTableConfig={() => embeddedTasks}
  getTableActions={() => tasksActions}
  instanceId="open-tasks-panel"
  initialView={{ id: openTasksView.id, config: openTasksView.config }}
/>;

En Vue, passez les mêmes valeurs dans instance-id et :initial-view.

Vues enregistrées

Les vues locales ou distantes sauvegardées sont chargées au montage du tableau, même si initialViews est omis ou vide, y compris après un rechargement de la page.

Les vues enregistrées sont construites depuis le même état basé sur l'URL. Le gestionnaire de vues est activé par défaut et peut être désactivé avec enableViews: false. Utilisez allowViewSave: false quand les utilisateurs peuvent sélectionner des vues existantes mais ne doivent pas créer, mettre à jour ou supprimer des vues. Utilisez allowViewSharing: true pour afficher l'option “Partager avec l'équipe” au moment d'enregistrer une vue.

Quand une vue est enregistrée, Yayaw Table persiste ce snapshot adapté à la base de données :

  • Recherche globale ({tableId}-q)

  • Filtres de colonnes et filtres avancés

  • Tri

  • Visibilité et ordre des colonnes

  • Largeurs des colonnes

  • Mode d'affichage, overrides Kanban et overrides Gallery

  • Groupement

  • Pinning des colonnes

  • Taille de page

  • Densité effective du tableau (density)

density fait partie du snapshot sauvegardé et n’a pas de paramètre de requête indépendant. Sa modification marque une vue sauvegardée active comme modifiée. Appliquer une ancienne vue sans densité restaure la valeur par défaut du tableau ; les autres tableaux conservent leur propre densité.

La page courante, les lignes dépliées et l'index d'historique navigateur ne sont pas persistés. Appliquer une vue remet toujours {tableId}-page à 0, pour éviter qu'une vue filtrée ne s'ouvre sur une page hors limites.

Les vues Kanban et Gallery persistent uniquement les choix d'affichage. Kanban stocke les colonnes lane/titre/propriétés et l'affichage des libellés; Gallery stocke les colonnes image/titre/propriétés et les réglages de layout des cartes. Aucune vue ne stocke les données de lignes ni les données images.

Pour la persistance en production, exposez les actions de vues depuis getTableActions :

const getTableActions = (tableType: string) => {
  if (tableType !== "products") {
    return;
  }

  return {
    list: listProducts,
    views: {
      list: async ({ tableId }) => ({ data: await db.views.list(tableId) }),
      create: async (input) => await db.views.create(input), // input.isGlobal vaut true pour les vues partagées avec l'équipe
      update: async (id, input) => await db.views.update(id, input),
      delete: async (id, context) => await db.views.delete(id, context),
    },
  };
};

Si aucune action views n'est fournie, le composant copié utilise un fallback localStorage pour garder l'UI utilisable dans les prototypes. Les applications consommatrices doivent remplacer ce fallback par des actions adossées à la base quand les vues enregistrées doivent suivre les utilisateurs ou les équipes authentifiés.

Modèle de données recommandé

Utilisez une table générique table_views pour toutes les instances Yayaw Table plutôt qu'une table de vues par entité métier. Une vue enregistrée décrit un état UI, pas une table SQL; identifiez donc la table cible avec une clé applicative stable comme products, customers ou orders. Évitez d'utiliser un nom de table SQL comme identifiant long terme, car une table UI peut être alimentée par des joins, des index de recherche, des réponses API ou des tables renommées.

create table table_views (
  id uuid primary key,
  workspace_id uuid not null,
  table_key text not null,
  name text not null,
  config jsonb not null,
  visibility text not null default 'private',
  owner_user_id uuid,
  created_by_id uuid,
  is_system boolean not null default false,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  deleted_at timestamptz
);

Stockez la config normalisée de la vue dans config plutôt que la chaîne URL complète. Garder le snapshot structuré facilite la validation, les migrations, l'inspection et l'application sur plusieurs clients.

{
  "version": 1,
  "sorting": [],
  "columnFilters": [],
  "advancedFilters": [],
  "globalSearch": "samsung",
  "columnVisibility": {},
  "columnOrder": [],
  "columnSizing": { "name": 280, "price": 140 },
  "displayMode": "gallery",
  "kanban": {
    "groupBy": "status",
    "titleColumn": "name",
    "cardColumnIds": ["brand", "price"],
    "showCardLabels": false
  },
  "gallery": {
    "imageColumn": "imageUrl",
    "titleColumn": "name",
    "cardColumnIds": ["brand", "category", "price", "status"],
    "aspectRatio": "square",
    "imageFit": "cover",
    "cardSize": "medium"
  },
  "columnPinning": { "left": [], "right": [] },
  "grouping": [],
  "pageSize": 50
}

Utilisez visibility pour représenter si une vue est privée, partagée avec un workspace ou fournie par le système. Si les utilisateurs peuvent choisir leur propre vue par défaut, gardez cette préférence hors de table_views pour que le défaut reste non ambigu :

create table table_view_preferences (
  user_id uuid not null,
  workspace_id uuid not null,
  table_key text not null,
  default_view_id uuid references table_views(id),
  primary key (user_id, workspace_id, table_key)
);

Cela permet à une même vue partagée d'être la vue par défaut d'un utilisateur sans devenir la vue par défaut de tout le monde. Pour des défauts workspace-wide, stockez cela séparément comme préférence workspace ou ajoutez une colonne scope explicite à la table de préférences.

Partage et reset

  • Copier le lien – Le menu Données peut proposer “Copy link” / “Share” avec l'URL courante (tous les paramètres table y sont déjà).

  • Reset – Réinitialiser l'état de la table efface ces paramètres (ou restaure les valeurs par défaut), donc l'URL est mise à jour en conséquence.

Usage server-side de l'état URL

Si vous rendez la table côté serveur (par ex. dans un Server Component), vous pouvez lire les mêmes paramètres depuis searchParams et passer des données initiales ou les utiliser dans votre Server Action. Le client hydratera avec la même URL et Nuqs restera synchronisé.

Dépendances

  • nuqs – Requis pour l'état URL. La table utilise useQueryState et des parseurs custom.

  • Next.js – Utilisez nuqs/adapters/next/app pour l'App Router.

Voir aussi :

Compatibilité et gestion des vues sauvegardées

Le snapshot commun utilise globalSearch, columnFilters et columnPinning. Les anciens noms Vue search, filters et pinning restent acceptés ; les valeurs canoniques sont prioritaires si les deux existent. L'ancien regroupement Kanban devient grouping. Les filtres avancés conservent leur combinaison AND/OR et les règles inactives.

Dans Vue, la vue par défaut s'applique seulement si aucune vue demandée ou configuration explicite de l'URL n'est prioritaire. Le gestionnaire indique les changements non enregistrés et propose, selon la configuration, copie, mise à jour, renommage, choix par défaut et suppression. Les vues système sont protégées contre modification et suppression dans l'interface ; appliquez aussi les permissions côté serveur. Une erreur conserve le brouillon pour correction ou nouvelle tentative.

Le gestionnaire Vue utilise les traductions anglaises/françaises, des menus et dialogues accessibles au clavier, la restauration du focus et les couleurs de table dans les portails. La synchronisation URL Vue n'exige pas Nuqs ; la configuration Nuqs ci-dessus concerne React.

Vues partagées dans l'organisation

allowViewSharing: true affiche le choix de partage et isGlobal: true identifie une vue d'équipe dans le contrat de persistance. Ces options n'apportent ni appartenance à une organisation, ni stockage entre appareils, ni autorisation. Le fallback localStorage, utilisé aussi par la démonstration publique, reste limité au navigateur courant ; marquer une vue locale comme partagée ne la transmet pas aux collègues.

Un adaptateur views de production doit déduire l'organisation active et l'utilisateur authentifié côté serveur. views.list ne retourne que les vues privées de cet utilisateur et les vues partagées avec son organisation pour la table demandée. Création, modification et suppression doivent vérifier le même périmètre et les permissions. Vérifiez aussi l'accès à un ID de vue demandé ; une URL, un tableId ou un isGlobal envoyé par le client n'autorise pas l'accès aux données d'une autre organisation. Partager une présentation ne partage jamais les lignes : la requête de données conserve ses propres contrôles d'accès.

Le schéma de préférences ci-dessus est une proposition d’intégration applicative. Branchez views.getFavorite et views.setFavorite sur cet enregistrement distinct par utilisateur. Un favori peut référencer une vue privée, partagée ou système accessible sans modifier sa configuration ni imposer un défaut à tous les membres. Gardez les défauts d’organisation séparés.

Effacer les filtres depuis un résultat vide conserve l'ID de vue active et modifie uniquement le brouillon courant. L'action ne modifie ni ne supprime la vue enregistrée ; utilisez Enregistrer les modifications pour rendre la nouvelle configuration persistante.

Vue favorite à l’arrivée

La Vue par défaut intégrée propose aussi une étoile. Sélectionnez-la puis cliquez sur son étoile pour retirer la préférence personnelle avec setFavorite(null, context) ; aucune vue artificielle n’est créée. Son étoile est pleine dans la barre et le menu lorsqu’aucun favori enregistré accessible n’existe. Cliquer sur son étoile déjà pleine ne change rien. Une erreur conserve le favori précédent et permet de réessayer. Les ID de vues temporaires non enregistrées ne peuvent pas devenir favoris. La priorité à l’arrivée des vues partagées isDefault reste inchangée.

Lorsque les vues enregistrées sont activées, sélectionnez une vue et cliquez sur l’étoile à côté de son nom pour l’utiliser à l’arrivée. Pour une vue enregistrée, cliquez sur l’étoile pleine pour retirer le favori ; choisir un autre favori remplace le précédent. Sélectionner une autre vue ne change pas le favori. React et Vue proposent ce comportement, y compris pour les vues partagées ou système et avec allowViewSave: false.

Le favori référence la configuration enregistrée. Mettre en favori une vue modifiée n’enregistre pas son brouillon ; utilisez d’abord Enregistrer les modifications pour les inclure. Le favori ne change jamais les champs config, isDefault, isGlobal ou le propriétaire de la vue.

Un état de table explicite dans l’URL reste prioritaire si la synchronisation URL est activée. Sinon, l’ordre à l’arrivée est initialActiveViewId, le favori accessible, une vue isDefault, puis la configuration du catalogue. Seule l’URL d’arrivée compte comme état explicite : la table la lit une fois au montage, et une écriture que la table fait elle-même à l’arrivée n’annule jamais la vue. Depuis v3.9.2, React n’écrit aucun ordre de colonnes de lui-même, seulement quand la personne déplace une colonne ou applique une vue ; avant, une table React dont le columns.order différait de ses définitions écrivait l’ordre de ses définitions juste après le montage, et s’ouvrait sur la Vue par défaut au lieu du favori ou d’une vue isDefault. Un initialActiveViewId explicite invalide laisse la configuration normale ; un favori supprimé ou inaccessible laisse place au défaut. Les réponses tardives n’écrasent pas les modifications faites pendant le chargement. Retirer un favori conserve la vue courante jusqu’à la prochaine arrivée ; réinitialiser manuellement la vue ne réapplique pas immédiatement le favori.

Mettez à jour le registre copié pour obtenir cette API. Sans gestionnaires de préférence, elle utilise le localStorage du navigateur, avec une clé contenant le type et l’ID d’instance de table. Ce stockage accepte les vues chargées à distance, mais ne synchronise pas les appareils et n’isole pas automatiquement les comptes d’un même navigateur.

Fournissez les deux gestionnaires optionnels avec votre adaptateur CRUD complet :

views: {
  ...viewCrudActions,
  getFavorite: async (context) => ({
    success: true,
    data: { viewId: await readMyFavorite(context) },
  }),
  setFavorite: async (viewId, context) => {
    await saveMyFavorite(context, viewId);
    return { success: true, data: { viewId } };
  },
}

Tous deux reçoivent { tableId, tableType } ; setFavorite reçoit aussi l’ID de vue ou null pour retirer le favori. Retournez { success: true, data: { viewId: null } } sans préférence, ou { success: false, error: "Unable to save your favorite view" } en cas d’échec. React exige success ; Vue accepte aussi son omission. Les erreurs sont signalées sans remplacer la préférence enregistrée.

Implémentez readMyFavorite et saveMyFavorite côté serveur à partir de l’utilisateur authentifié et de l’organisation active. Utilisez une clé unique (organizationId, userId, tableType, tableId) et vérifiez l’accès à la vue cible. Retirez la référence si la vue est supprimée ou son accès révoqué. Délimitez l’ID d’instance client et le cache par utilisateur/organisation, remontez la table quand ce périmètre change et videz l’ancien cache React à la déconnexion ; les ID clients ne remplacent jamais les autorisations serveur. Les permissions CRUD des vues partagées restent indépendantes des préférences personnelles.