Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 0238d8bd53 fix: optimistically update metadata store after view filter/sort mutations
https://sonarly.com/issue/23699?type=bug

After saving view filters/sorts, the local metadata store is not updated optimistically — it waits for an asynchronous SSE event round-trip from the backend, causing the "Update view" button to remain visible and requiring a second click.

Fix: After each view save hook's mutations succeed, the metadata store (`metadataStoreState`) is now optimistically updated to reflect the new state immediately. This eliminates the dependency on the SSE round-trip for UI state reconciliation.

**What changed in each file:**

1. **`useSaveRecordFiltersToViewFilters.ts`** — After all create/update/delete filter mutations succeed, replaces the `viewFilters` entries in `metadataStoreState.current` for the current view with the computed `newViewFilters`.

2. **`useSaveRecordSortsToViewSorts.ts`** — Same pattern for `viewSorts` entries.

3. **`useSaveRecordFilterGroupsToViewFilterGroups.ts`** — Same pattern for `viewFilterGroups` entries.

4. **`useSaveAnyFieldFilterToView.ts`** — After the view update mutation succeeds, updates the `anyFieldFilterValue` field on the current view in the `views` metadata store entry.

Each optimistic update:
- Reads the current state from the metadata store (respecting draft-pending status)
- Filters out entries belonging to the current view
- Replaces them with the new computed values
- Writes directly to `.current` so the `viewsSelector` (which reads `.current`) immediately reflects the saved state

When the SSE event arrives later, the `addToDraft`/`applyChanges` pipeline will find the data already matches and skip unnecessary updates due to `isDeeplyEqual` checks.
2026-04-10 09:21:38 +00:00
4 changed files with 112 additions and 0 deletions
@@ -1,3 +1,5 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatView } from '@/metadata-store/types/FlatView';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
@@ -39,6 +41,26 @@ export const useSaveAnyFieldFilterToView = () => {
anyFieldFilterValue: currentAnyFieldFilterValue,
}),
});
// Optimistically update the metadata store so the UI reflects
// the saved state immediately, without waiting for the SSE round-trip.
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
const currentFlatViews = (
viewsEntry.status === 'draft-pending'
? viewsEntry.draft
: viewsEntry.current
) as FlatView[];
const updatedViews = currentFlatViews.map((view) =>
view.id === currentView.id
? { ...view, anyFieldFilterValue: currentAnyFieldFilterValue }
: view,
);
store.set(metadataStoreState.atomFamily('views'), {
...viewsEntry,
current: updatedViews,
});
}
}, [
store,
@@ -1,3 +1,5 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
@@ -70,6 +72,34 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete);
// Optimistically update the metadata store so the UI reflects
// the saved state immediately, without waiting for the SSE round-trip.
const viewFilterGroupsEntry = store.get(
metadataStoreState.atomFamily('viewFilterGroups'),
);
const currentFlatViewFilterGroups = (
viewFilterGroupsEntry.status === 'draft-pending'
? viewFilterGroupsEntry.draft
: viewFilterGroupsEntry.current
) as FlatViewFilterGroup[];
const otherViewFilterGroups = currentFlatViewFilterGroups.filter(
(filterGroup) => filterGroup.viewId !== currentView.id,
);
const updatedViewFilterGroups = [
...otherViewFilterGroups,
...newViewFilterGroups.map((filterGroup) => ({
...filterGroup,
viewId: currentView.id,
})),
];
store.set(metadataStoreState.atomFamily('viewFilterGroups'), {
...viewFilterGroupsEntry,
current: updatedViewFilterGroups,
});
}, [
canPersistChanges,
currentView,
@@ -1,4 +1,6 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -121,6 +123,34 @@ export const useSaveRecordFiltersToViewFilters = () => {
if (deleteResult.status === 'failed') {
return;
}
// Optimistically update the metadata store so the UI reflects
// the saved state immediately, without waiting for the SSE round-trip.
const viewFiltersEntry = store.get(
metadataStoreState.atomFamily('viewFilters'),
);
const currentFlatViewFilters = (
viewFiltersEntry.status === 'draft-pending'
? viewFiltersEntry.draft
: viewFiltersEntry.current
) as FlatViewFilter[];
const otherViewFilters = currentFlatViewFilters.filter(
(filter) => filter.viewId !== currentView.id,
);
const updatedViewFilters = [
...otherViewFilters,
...newViewFilters.map((filter) => ({
...filter,
viewId: currentView.id,
})),
];
store.set(metadataStoreState.atomFamily('viewFilters'), {
...viewFiltersEntry,
current: updatedViewFilters,
});
}, [
store,
canPersistChanges,
@@ -1,3 +1,5 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
@@ -91,6 +93,34 @@ export const useSaveRecordSortsToViewSorts = () => {
if (deleteResult.status === 'failed') {
return;
}
// Optimistically update the metadata store so the UI reflects
// the saved state immediately, without waiting for the SSE round-trip.
const viewSortsEntry = store.get(
metadataStoreState.atomFamily('viewSorts'),
);
const currentFlatViewSorts = (
viewSortsEntry.status === 'draft-pending'
? viewSortsEntry.draft
: viewSortsEntry.current
) as FlatViewSort[];
const otherViewSorts = currentFlatViewSorts.filter(
(sort) => sort.viewId !== currentView.id,
);
const updatedViewSorts = [
...otherViewSorts,
...newViewSorts.map((sort) => ({
...sort,
viewId: currentView.id,
})),
];
store.set(metadataStoreState.atomFamily('viewSorts'), {
...viewSortsEntry,
current: updatedViewSorts,
});
}, [
canPersistChanges,
currentView,