Yayaw
Documentation
Recettes

Recettes

Six parcours complets avec un socle de code commun et consultable.

Les quatre parcours guidés sont Catalogue → Stock → Pipeline → Administration. Commande et Fiche prolongent ces bases. Les extraits spécifiques utilisent les onglets React/Vue synchronisés ; les données et validations partagées sont identiques. Copiez l’adaptateur view-actions.ts depuis Vues enregistrées lorsque vous utilisez editable-actions.ts.

Partez du démarrage React ou Vue. Conservez shared/products.ts, product-config.ts et list-records.ts ; les recettes ajoutent les fichiers ci-dessous. Ces exemples fonctionnent en mémoire dans le navigateur pour observer le comportement avant de brancher un backend. Recharger réinitialise modifications, vues et activité.

Choisir un parcours

RecetteRésultat
CatalogueRecherche, filtres, galerie et CSV filtré
StockValidation en cellule et totaux
PipelineChangements de statut en Kanban
CommandeRelation client et lignes imbriquées
AdministrationSélection entre pages et édition groupée par champ
FicheConsultation, historique, annulation et vues

Stockage des modifications

store.ts
import { z } from "zod";
import { type Product, products } from "./products";

const productPatch = z
  .object({
    name: z.string().trim().min(1),
    price: z.number().finite().min(0),
    stock: z.number().int().min(0),
    status: z.enum(["draft", "active", "archived"]),
    active: z.boolean(),
    tags: z.array(z.string()),
  })
  .partial()
  .strict();

export interface Activity {
  action: string;
  actor: { name: string };
  at: string;
  changes: { field: string; before: unknown; after: unknown }[];
  id: string;
  reversible?: boolean;
  reverts?: string;
}

/** Browser-local learning data. Replace this store with authenticated server actions. */
export function createProductStore() {
  const rows: Product[] = structuredClone(products);
  const history = new Map<string, Activity[]>();
  const activity = (id: string) => history.get(id) ?? [];
  const update = (id: string, input: Record<string, unknown>) => {
    const row = rows.find((item) => item.id === id);
    if (!row) {
      return { success: false, error: "Product no longer exists." };
    }
    // Undefined transport fields are omitted from a patch; explicit unknown values still fail.
    const patch = Object.fromEntries(
      Object.entries(input).filter(([, value]) => value !== undefined)
    );
    const parsed = productPatch.safeParse(patch);
    if (!parsed.success) {
      return {
        success: false,
        error: "Check the highlighted fields.",
        fieldErrors: Object.fromEntries(
          parsed.error.issues.map((issue) => [
            String(issue.path[0]),
            issue.message,
          ])
        ),
      };
    }
    const changes = Object.entries(parsed.data)
      .filter(
        ([field, value]) => JSON.stringify(row[field]) !== JSON.stringify(value)
      )
      .map(([field, after]) => ({
        field,
        before: structuredClone(row[field]),
        after: structuredClone(after),
      }));
    if (changes.length) {
      const at = new Date().toISOString();
      history.set(id, [
        ...activity(id),
        {
          id: crypto.randomUUID(),
          actor: { name: "Demo editor" },
          at,
          action: "Updated product",
          changes,
        },
      ]);
      Object.assign(row, parsed.data, { updatedAt: at });
    }
    return { success: true, data: structuredClone(row) };
  };
  const bulkUpdate = (ids: string[], patch: Record<string, unknown>) => {
    const failedIds: string[] = [];
    const data: Product[] = [];
    for (const id of new Set(ids)) {
      const result = update(id, patch);
      if (result.success && result.data) {
        data.push(result.data);
      } else {
        failedIds.push(id);
      }
    }
    return {
      success: failedIds.length === 0,
      data,
      failedIds,
      ...(failedIds.length
        ? { error: "Some products could not be updated." }
        : {}),
    };
  };
  const revert = (id: string, eventId: string) => {
    const events = activity(id);
    const entry = events.find((event) => event.id === eventId);
    const row = rows.find((item) => item.id === id);
    if (
      !(entry && row) ||
      entry.reverts ||
      events.some((event) => event.reverts === eventId)
    ) {
      return { success: false, error: "This change is no longer reversible." };
    }
    // Check current values again at the write boundary, even when the UI enables Undo.
    if (
      entry.changes.some(
        (change) =>
          JSON.stringify(row[change.field]) !== JSON.stringify(change.after)
      )
    ) {
      return {
        success: false,
        error: "A newer change must be reviewed first.",
      };
    }
    const at = new Date().toISOString();
    const changes = entry.changes.map((change) => ({
      field: change.field,
      before: change.after,
      after: change.before,
    }));
    for (const change of changes) {
      row[change.field] = structuredClone(change.after);
    }
    row.updatedAt = at;
    history.set(id, [
      ...events,
      {
        id: crypto.randomUUID(),
        actor: { name: "Demo editor" },
        at,
        action: "Undid product change",
        reverts: eventId,
        reversible: false,
        changes,
      },
    ]);
    return { success: true };
  };
  return { rows, update, bulkUpdate, activity, revert };
}
export const productStore = createProductStore();

Adaptateur des actions

editable-actions.ts
import type { TableActions } from "@/components/ui/yayaw-table/providers/table-provider";
import { productStore } from "../shared/store";
import { listRecords } from "./list-records";
import { viewActions } from "./view-actions";

export const editableActions: TableActions = {
  views: viewActions,
  list: (params) => listRecords(productStore.rows, params),
  update: (id, patch) => Promise.resolve(productStore.update(id, patch)),
  bulkUpdate: (ids, patch) =>
    Promise.resolve(productStore.bulkUpdate(ids, patch)),
};
export const getEditableActions = (tableType: string) =>
  tableType === "products" ? editableActions : undefined;
editable-actions.ts
import type { TableActions } from "@/components/ui/yayaw-table-vue/types";
import { productStore } from "../shared/store";
import { listRecords } from "./list-records";
import { viewActions } from "./view-actions";

export const editableActions: TableActions = {
  views: viewActions,
  list: (params) => listRecords(productStore.rows, params),
  update: (id, patch) => Promise.resolve(productStore.update(id, patch)),
  bulkUpdate: (ids, patch) =>
    Promise.resolve(productStore.bulkUpdate(ids, patch)),
};
export const getEditableActions = (tableType: string) =>
  tableType === "products" ? editableActions : undefined;

Éditeur de produit commun

Copiez product-form.ts et get-form-config.ts depuis Formulaires. Chaque entrée React inclut un QueryClient et un adaptateur Nuqs pour fonctionner seule. Dans une application existante, utilisez ses providers partagés. Les recettes Vue utilisent la racine VueQueryPlugin du démarrage : remplacez le composant Products importé par la recette choisie et montez un Toaster de vue-sonner dans cette racine.

Brancher un vrai backend

Remplacez le stockage en mémoire par des actions authentifiées ou des endpoints HTTP. Validez les modifications partielles, autorisez chaque identifiant et champ, écrivez l’activité dans la transaction de modification et invalidez le client partagé. N’utilisez pas des tableaux de module comme stockage serveur. Le playground existant permet l’exploration interactive ; la documentation présente des sources copiables.