refactor(web): improve data handling in DataTable and ObjectTable components

This commit refactors the DataTable and ObjectTable components to enhance state management. In DataTable, the column visibility state is now set more efficiently by defaulting to an empty object when no initial visibility is provided. In ObjectTable, local entries are introduced to maintain alignment with server updates, and a new callback for local value changes is added to EditableCell, improving the responsiveness of the UI during data edits. Additionally, the handling of row selection during bulk delete operations is updated to use local entries, ensuring consistency across the component's state.
This commit is contained in:
kumarabhirup 2026-03-03 17:58:45 -08:00
parent a1cb0b8372
commit 8542f07783
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
3 changed files with 39 additions and 8 deletions

View File

@ -192,9 +192,7 @@ export function DataTable<TData, TValue>({
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(initialColumnVisibility ?? {});
// Sync column visibility when the prop changes (e.g. loading a saved view)
useEffect(() => {
if (initialColumnVisibility) {
setColumnVisibility(initialColumnVisibility);
}
setColumnVisibility(initialColumnVisibility ?? {});
}, [initialColumnVisibility]);
const [internalRowSelection, setInternalRowSelection] = useState<Record<string, boolean>>({});
const [showColumnsMenu, setShowColumnsMenu] = useState(false);

View File

@ -180,6 +180,7 @@ function EditableCell({
members,
relationLabels,
onNavigate,
onLocalValueChange,
onSaved,
}: {
value: unknown;
@ -190,6 +191,7 @@ function EditableCell({
members?: Array<{ id: string; name: string }>;
relationLabels?: Record<string, Record<string, string>>;
onNavigate?: (objectName: string) => void;
onLocalValueChange?: (value: string) => void;
onSaved?: () => void;
}) {
const [editing, setEditing] = useState(false);
@ -212,6 +214,7 @@ function EditableCell({
const isRelation = field.type === "relation" && !!field.related_object_name;
const save = useCallback(async (val: string) => {
onLocalValueChange?.(val);
try {
await fetch(`/api/workspace/objects/${encodeURIComponent(objectName)}/entries/${encodeURIComponent(entryId)}`, {
method: "PATCH",
@ -220,7 +223,7 @@ function EditableCell({
});
onSaved?.();
} catch { /* ignore */ }
}, [objectName, entryId, fieldName, onSaved]);
}, [objectName, entryId, fieldName, onLocalValueChange, onSaved]);
const handleChange = (val: string) => {
setLocalValue(val);
@ -385,6 +388,25 @@ export function ObjectTable({
}: ObjectTableProps) {
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [showAddModal, setShowAddModal] = useState(false);
const [localEntries, setLocalEntries] = useState<EntryRow[]>(entries as EntryRow[]);
// Keep local rows aligned with server-paginated updates.
useEffect(() => {
setLocalEntries(entries as EntryRow[]);
}, [entries]);
const updateLocalEntryField = useCallback((entryId: string, fieldName: string, value: string) => {
setLocalEntries((prev) =>
prev.map((entry) => {
const eid = entry.entry_id;
const currentEntryId = String(
eid != null && typeof eid === "object" ? JSON.stringify(eid) : (eid ?? ""),
);
if (currentEntryId !== entryId) {return entry;}
return { ...entry, [fieldName]: value };
}),
);
}, []);
const activeReverseRelations = useMemo(() => {
if (!reverseRelations) {return [];}
@ -440,6 +462,7 @@ export function ObjectTable({
members={members}
relationLabels={relationLabels}
onNavigate={onNavigateToObject}
onLocalValueChange={(value) => updateLocalEntryField(entryId, field.name, value)}
onSaved={onRefresh}
/>
);
@ -485,7 +508,7 @@ export function ObjectTable({
const handleBulkDelete = useCallback(async () => {
const selectedIds = Object.keys(rowSelection)
.filter((k) => rowSelection[k])
.map((idx) => safeString(entries[Number(idx)]?.entry_id))
.map((idx) => safeString(localEntries[Number(idx)]?.entry_id))
.filter(Boolean);
if (selectedIds.length === 0) {return;}
@ -500,7 +523,7 @@ export function ObjectTable({
setRowSelection({});
onRefresh?.();
} catch { /* ignore */ }
}, [rowSelection, entries, objectName, onRefresh]);
}, [rowSelection, localEntries, objectName, onRefresh]);
// Single delete handler
const handleDeleteEntry = useCallback(async (entry: EntryRow) => {
@ -574,7 +597,7 @@ export function ObjectTable({
<>
<DataTable
columns={columns}
data={entries as EntryRow[]}
data={localEntries}
enableSorting
enableGlobalFilter
enableRowSelection

View File

@ -2223,6 +2223,10 @@ function ObjectView({
const [serverSearch, setServerSearch] = useState("");
const [sortRules, _setSortRules] = useState<SortRule[] | undefined>(undefined);
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasActiveServerQuery =
filters.rules.length > 0 ||
((sortRules?.length ?? 0) > 0) ||
serverSearch.trim().length > 0;
// Column visibility: maps field IDs to boolean (false = hidden)
const [viewColumns, setViewColumns] = useState<string[] | undefined>(undefined);
@ -2279,10 +2283,16 @@ function ObjectView({
}
}, [serverPage, serverPageSize, filters, sortRules, serverSearch, data.object.name]);
// Sync initial data from props (when parent refreshes via SSE)
// Sync incoming object data. If a server query is active (filters/search/sort),
// re-fetch with the active query instead of showing unfiltered parent entries.
useEffect(() => {
if (hasActiveServerQuery) {
void fetchEntries();
return;
}
setEntries(data.entries);
setTotalCount(data.totalCount ?? data.entries.length);
// eslint-disable-next-line react-hooks/exhaustive-deps -- only react to parent data updates
}, [data.entries, data.totalCount]);
// Sync saved views when data changes (e.g. SSE refresh from AI editing .object.yaml)