@bit.rhplus/shared-grid-form
v0.0.10
Published
Komponenta pro sledování a správu změn v AG Grid tabulkách s field-level trackingem. Funguje analogicky k `shared-form` (pro formulářová pole), ale je navržena pro řádkové operace v AG Grid.
Readme
shared-grid-form
Komponenta pro sledování a správu změn v AG Grid tabulkách s field-level trackingem. Funguje analogicky k shared-form (pro formulářová pole), ale je navržena pro řádkové operace v AG Grid.
Architektura
Komponenta používá two-state systém postavený na Jotai atomech:
Original Items (itemsAtomFamily) Data ze serveru, nikdy se přímo nemění
+
Pending Changes (pendingChangesAtomFamily) Field-level změny per řádek
↓
Effective Items (computed) Merged výsledek pro zobrazení v gridu
↓
Tlačítko "Uložit" → getPendingChanges() Strukturovaný diff pro APIPrincip fungování
- Originální data (
items) se načtou ze serveru a uloží do Jotai atomu - Uživatel edituje buňky v gridu → změny se ukládají do
pendingChangesatomu - Grid zobrazuje
effectiveItems= originální data + aplikované změny - Při uložení se zavolá
getPendingChanges()→ vrátí strukturovaný diff{ added, updated, deleted } - Po úspěšném uložení se zavolá
clearPendingChanges()→ vyčistí pending stav
Struktura pending changes
pendingChanges = {
// Editovaný existující řádek - uchovává pouze změněná pole
"row-id-1": {
operation: "UPDATE",
changes: { quantity: 15, price: 200 },
originalData: { id: "row-id-1", quantity: 10, price: 100, name: "Produkt A" }
},
// Nově přidaný řádek - dočasné ID
"temp-1708012345-4567": {
operation: "ADD",
changes: { name: "Nový produkt", quantity: 1, price: 0 },
originalData: null
},
// Řádek označený k mazání
"row-id-2": {
operation: "DELETE",
changes: null,
originalData: { id: "row-id-2", quantity: 5, price: 50, name: "Produkt B" }
}
}Soubory
| Soubor | Popis |
|--------|-------|
| atoms.js | Jotai atomy a hlavní hook useSharedGridItems |
| SharedGridForm.jsx | React Context provider obalující AG Grid |
| index.js | Exporty |
| styles.css | CSS třídy pro vizuální indikátory změn |
API Reference
useSharedGridItems(instanceId)
Hlavní hook pro správu grid dat. Vrací kompletní API pro práci se změnami.
import { useSharedGridItems } from '@bit.rhplus/shared-grid-form';
const {
// --- Data ---
items, // any[] originální items ze serveru
effectiveItems, // any[] items + pending changes (pro grid rowData)
pendingChanges, // object raw pending changes objekt
// --- Computed stav ---
isDirty, // boolean existují nesmazané změny?
dirtyRowIds, // string[] pole ID řádků se změnami
// --- Settery ---
setItems, // (items) => void nastavení originálních items (z Detail)
setPendingChanges, // (changes) => void přímý přístup k pending atomu
// --- Mutační metody ---
updateCell, // (rowId, field, value) => void sleduje změnu jedné buňky
addRow, // (rowData) => string přidá řádek, vrací tempId
deleteRow, // (rowId) => void označí řádek k mazání
resetRow, // (rowId) => void vrátí řádek do původního stavu
resetAll, // () => void vrátí všechny řádky do původního stavu
// --- Pro ukládání ---
getPendingChanges, // () => { added, updated, deleted } strukturované změny
clearPendingChanges,// () => void vyčistí po úspěšném uložení
} = useSharedGridItems(instanceId);updateCell(rowId, field, value)
Sleduje změnu jedné buňky. Automaticky:
- Vytvoří UPDATE záznam při první změně existujícího řádku (uloží originální data pro undo)
- Sloučí s existujícími změnami při dalších editacích
- Ignoruje editace na smazaných řádcích
updateCell("row-123", "quantity", 15);
updateCell("row-123", "price", 200);
// → pendingChanges["row-123"].changes = { quantity: 15, price: 200 }addRow(rowData)
Přidá nový řádek s automaticky vygenerovaným dočasným ID (temp-{timestamp}-{random}). Vrací toto ID.
const tempId = addRow({ name: "Nový produkt", quantity: 1, price: 0 });
// tempId = "temp-1708012345-4567"deleteRow(rowId)
Označí řádek k mazání. Speciální chování:
- Existující řádek → uloží do pendingChanges jako DELETE (uschová originalData pro undo)
- Nově přidaný řádek (ADD) → úplně odstraní z pendingChanges (ADD + DELETE se vzájemně ruší)
deleteRow("row-123"); // existující → označen DELETE
deleteRow("temp-456"); // nový → odstraněn z pending (jako by nikdy nebyl přidán)resetRow(rowId)
Vrátí konkrétní řádek do původního stavu - odstraní jeho záznam z pendingChanges. Funguje pro všechny typy operací:
- UPDATE → řádek se vrátí na originální hodnoty
- DELETE → řádek se znovu zobrazí
- ADD → nový řádek zmizí
resetRow("row-123"); // vrátí řádek do origináluresetAll()
Zahodí všechny pending changes. Vše se vrátí na originální data ze serveru.
resetAll(); // isDirty = false, effectiveItems = itemsgetPendingChanges()
Vrátí strukturovaný objekt změn pro odeslání na API:
const { added, updated, deleted } = getPendingChanges();
// added = [{ id: "temp-123", name: "Nový", quantity: 1 }, ...]
// updated = [{ id: "row-1", quantity: 15, price: 200 }, ...]
// deleted = ["row-2", "row-3"]clearPendingChanges()
Vyčistí všechny pending changes. Volá se po úspěšném uložení na server.
<SharedGridForm>
React Context provider, který obaluje AG Grid a zpřístupňuje API skrze kontext.
<SharedGridForm
instanceId="detail_invoices_123" // povinné - unikátní ID instance
editableDefault={true} // výchozí editovatelnost buněk (default: true)
>
<Grid columnDefs={columnDefs} />
</SharedGridForm>Props
| Prop | Typ | Default | Popis |
|------|-----|---------|-------|
| instanceId | string | povinné | Unikátní identifikátor instance gridu. Musí odpovídat instanceId v Detail komponentě. |
| editableDefault | boolean | true | Výchozí editovatelnost pro sloupce bez explicitního editable nastavení. |
| children | ReactNode | povinné | AG Grid komponenta a další obsah. |
Co SharedGridForm zajišťuje automaticky
- Aktualizace gridu při změně
effectiveItems(volágridApi.setGridOption('rowData', ...)) - Blokování editace smazaných řádků (
isCellEditablevracífalsepro DELETE operace) - Zpětná kompatibilita -
updateItemwrapper detekuje starý i nový formát volání
useSharedGrid()
Hook pro přístup ke kontextu z dceřiných komponent uvnitř <SharedGridForm>. Používá se primárně interně v Grid komponentě.
import { useSharedGrid } from '@bit.rhplus/shared-grid-form';
const {
gridApi, // AG Grid API reference
items, // effectiveItems (pro grid rowData)
pendingChanges, // raw pending changes
isDirty, // boolean
dirtyRowIds, // string[]
// Metody (s aliasy pro zpětnou kompatibilitu)
addItem, // alias pro addRow
updateItem, // wrapper: podporuje (id, field, value) i (id, {changes})
deleteItem, // alias pro deleteRow
resetRow,
resetAll,
getPendingChanges,
clearPendingChanges,
// Grid integrace
onGridReady, // callback pro AG Grid
isCellEditable, // funkce pro kontrolu editovatelnosti
} = useSharedGrid();OperationType
Enum s typy operací:
import { OperationType } from '@bit.rhplus/shared-grid-form';
OperationType.ADD // "ADD"
OperationType.UPDATE // "UPDATE"
OperationType.DELETE // "DELETE"createTempId()
Utility pro generování dočasných ID pro nové řádky:
import { createTempId } from '@bit.rhplus/shared-grid-form';
const id = createTempId(); // "temp-1708012345-4567"Vizuální indikátory
Komponenta poskytuje CSS třídy pro barevné zvýraznění řádků s čekajícími změnami. Tyto třídy se aplikují automaticky přes rowClassRules v Grid komponentě.
| CSS třída | Barva | Význam |
|-----------|-------|--------|
| .row-pending-add | Světle zelená | Nově přidaný řádek |
| .row-pending-update | Světle žlutá | Editovaný řádek |
| .row-pending-delete | Světle červená + přeškrtnutí | Řádek označený k mazání |
Import stylů:
import '@bit.rhplus/shared-grid-form/styles.css';Použití
Základní příklad - sekce Items v Detail komponentě
import React, { useCallback } from 'react';
import Button from 'antd/es/button';
import { PlusOutlined } from '@ant-design/icons';
import { SharedGridForm, useSharedGridItems } from '@bit.rhplus/shared-grid-form';
import Grid, { ColumnBuilder } from '@bit.rhplus/ui.grid';
const Items = ({ instanceId }) => {
const { addRow } = useSharedGridItems(instanceId);
const columnDefs = React.useMemo(() => {
return new ColumnBuilder()
.addColumn({ headerName: 'Produkt', field: 'product', width: 200 })
.addColumn({ headerName: 'Množství', field: 'quantity', width: 120, editable: true })
.addCurrencyColumn({ headerName: 'Cena', field: 'price', width: 150, editable: true })
.build();
}, []);
const handleAddRow = useCallback(() => {
addRow({ product: '', quantity: 1, price: 0 });
}, [addRow]);
return (
<SharedGridForm instanceId={instanceId}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddRow}>
Přidat položku
</Button>
<div className="ag-theme-alpine" style={{ height: 400 }}>
<Grid columnDefs={columnDefs} />
</div>
</SharedGridForm>
);
};Práce s dirty stavem a undo
import { useSharedGridItems } from '@bit.rhplus/shared-grid-form';
const GridToolbar = ({ instanceId }) => {
const { isDirty, dirtyRowIds, resetAll, getPendingChanges } = useSharedGridItems(instanceId);
return (
<div>
{isDirty && (
<span>Neuložené změny: {dirtyRowIds.length} řádků</span>
)}
<Button onClick={resetAll} disabled={!isDirty}>
Zahodit změny
</Button>
<Button onClick={() => {
const changes = getPendingChanges();
console.log('Přidáno:', changes.added);
console.log('Upraveno:', changes.updated);
console.log('Smazáno:', changes.deleted);
}}>
Zobrazit změny
</Button>
</div>
);
};Undo pro konkrétní řádek (např. v kontextovém menu)
import { useSharedGridItems } from '@bit.rhplus/shared-grid-form';
const RowActions = ({ instanceId, rowId }) => {
const { deleteRow, resetRow, pendingChanges } = useSharedGridItems(instanceId);
const hasChanges = !!pendingChanges[rowId];
return (
<Space>
<Button danger onClick={() => deleteRow(rowId)}>
Smazat
</Button>
{hasChanges && (
<Button onClick={() => resetRow(rowId)}>
Vrátit změny
</Button>
)}
</Space>
);
};Integrace s Detail komponentou
Detail komponenta (src/shared/detail/) koordinuje formulářová data (shared-form) a grid data (shared-grid-form) při manuálním ukládání.
Datový tok při ukládání
Uživatel klikne "Uložit"
↓
Detail (useDetailSave)
├── formData ← useSharedForm(instanceId).data
├── gridChanges ← useSharedGridItems(instanceId).getPendingChanges()
│ → { added: [...], updated: [...], deleted: [...] }
└── allData = { ...formData, items: gridChanges }
↓
API call (saveApi nebo onSave callback)
↓
Úspěch → clearPendingGridChanges() + refetch()
Chyba → pending changes zůstávají (uživatel může opravit a zkusit znovu)Jak Detail používá shared-grid-form
// src/shared/detail/desktop/index.jsx (zjednodušeno)
const Detail = (props) => {
const { data: formData, setData } = useSharedForm(instanceId);
const {
items: gridItems,
setItems,
getPendingChanges: getPendingGridChanges,
clearPendingChanges: clearPendingGridChanges,
} = useSharedGridItems(instanceId);
// useDetailData načte data ze serveru a zavolá setData() + setItems()
const { displayData, isLoading, refetch } = useDetailData({
useQueryHook, id, setData, setItems, prepareDataAfterLoad
});
// useDetailSave sbírá formData + gridChanges a posílá na API
const { handleSave, isSaving } = useDetailSave({
formData,
gridItems, // fallback na plný seznam
getPendingGridChanges, // preferované: strukturované změny
clearPendingGridChanges, // vyčistí po úspěšném uložení
id, saveApi, refetch, ...
});
return (
<Layout>
<DetailContent sections={sections} instanceId={instanceId} />
<DetailFooter handleSave={handleSave} isSaving={isSaving} />
</Layout>
);
};Edge cases
Smazání nově přidaného řádku
Pokud uživatel přidá řádek (addRow) a poté ho smaže (deleteRow), operace se vzájemně ruší - řádek je kompletně odstraněn z pendingChanges bez zanechání stopy.
Editace smazaného řádku
Pokus o updateCell na řádku s operací DELETE je ignorován (no-op). Navíc isCellEditable automaticky vrací false pro smazané řádky, takže AG Grid nedovolí editaci.
Vícenásobná editace stejného pole
Při opakované editaci stejného pole (např. quantity: 10 → 15 → 20) se v changes uchovává vždy jen poslední hodnota (20). Originální hodnota (10) zůstává v originalData pro případný undo.
Reset smazaného řádku
Volání resetRow na řádek s operací DELETE ho obnoví - odstraní DELETE záznam z pendingChanges a řádek se znovu objeví v effectiveItems s originálními daty.
Vztah ke shared-form
| Vlastnost | shared-form | shared-grid-form |
|-----------|-------------|-------------------|
| Účel | Formulářová pole (Ant Design Form) | Tabulkové řádky (AG Grid) |
| Stav | data + changedData | items + pendingChanges |
| Granularita | Field-level (per pole) | Field-level (per buňka v řádku) |
| Operace | Pouze UPDATE | ADD, UPDATE, DELETE |
| Undo | Ne | Ano (resetRow, resetAll) |
| Ukládání | Manuální (Detail) | Manuální (Detail) |
| State management | Jotai atomFamily | Jotai atomFamily |
| Computed merge | allData = { ...data, ...changedData } | effectiveItems (useMemo) |
Struktura souborů
src/bit/shared-grid-form/
├── atoms.js # Jotai atomy, useSharedGridItems hook, createTempId, OperationType
├── SharedGridForm.jsx # React Context provider, useSharedGrid hook
├── index.js # Exporty
├── styles.css # CSS pro vizuální indikátory (row-pending-add/update/delete)
└── README.md # Tento soubor