+
{snap.editable && (
<>
diff --git a/apps/studio/components/grid/hooks/README.md b/apps/studio/components/grid/hooks/README.md
new file mode 100644
index 00000000000..ffbb3c5d97f
--- /dev/null
+++ b/apps/studio/components/grid/hooks/README.md
@@ -0,0 +1,126 @@
+# Table Filtering and Sorting Developer notes
+
+## Overview
+
+The table filtering and sorting system uses a URL-based state persistence pattern combined with custom hooks that abstract the implementation details from consuming components. This architecture provides several benefits:
+
+- **Persistent state**: Filters and sorts are stored in URL parameters, enabling bookmarking and sharing
+- **Separation of concerns**: Logic is separated from UI components
+- **Draft-then-apply pattern**: UI components maintain draft state until explicitly applied
+
+## Core Hooks
+
+### `useTableFilter`
+
+```typescript
+// Returns: { filters, urlFilters, onApplyFilters }
+```
+
+The `useTableFilter` hook manages filter state with these responsibilities:
+
+- Retrieves raw filter parameters from URL
+- Formats filter parameters into usable Filter[] objects
+- Provides a callback to apply new filters
+- Persists changes to URL parameters
+- Triggers side effects through `saveFiltersAndTriggerSideEffects`
+
+Key design aspects:
+
+- No direct snapshot interaction, keeping it focused solely on filter management
+- Uses URL parameters as the source of truth
+- Forwards filter changes to URL and triggers application-specific side effects
+
+### `useTableSort`
+
+```typescript
+// Returns: { sorts, urlSorts, onApplySorts }
+```
+
+The `useTableSort` hook manages sort state with these responsibilities:
+
+- Retrieves raw filter parameters from URL
+- Formats sort parameters into usable Sort[] objects (needs table name)
+- Provides a callback to apply new sorts
+- Persists changes to URL parameters
+- Triggers side effects through `saveSortsAndTriggerSideEffects`
+
+Key design aspects:
+
+- Uses safe snapshot ONLY to get table name for formatting
+- Handles applying table name to sort objects
+- Maintains URL parameters as source of truth
+- Forwards sort changes to URL and triggers application-specific side effects
+
+### `useSafeTableEditorSnapshot`
+
+```typescript
+// Returns: Partial
+```
+
+This hook exists is to satisfy React's rules of hooks:
+
+React hooks CANNOT be called conditionally. Since `useTableSort` is a hook that needs table state, we cannot write:
+
+```typescript
+// THIS VIOLATES REACT'S RULES - NOT ALLOWED
+function useTableSort() {
+ const { sorts: urlSorts } = useTableEditorFiltersSort()
+
+ // Conditional hook usage is forbidden in React!
+ const snap = hasTableContext ? useTableEditorTableStateSnapshot() : null
+
+ // ...rest of hook
+}
+```
+
+Instead, we created `useSafeTableEditorSnapshot` which unconditionally calls the original snapshot hook and handles error cases. This allows `useTableSort` to always call the hook safely regardless of context.
+
+## Component Implementation
+
+### FilterPopoverPrimitive and SortPopoverPrimitive
+
+These components follow a "draft and apply" pattern:
+
+1. **Local state management**: Both components maintain a local state copy of filters/sorts
+2. **Edit operations**: Changes like adding, modifying, or deleting are made to the local state
+3. **Apply operations**: Only when the user clicks "Apply" are the changes committed via the callback
+4. **Synchronization**: Local state is synchronized with props when external changes occur
+
+## Data Flow
+
+1. URL parameters store the raw filter/sort state
+2. Hooks read and format these parameters into usable objects
+3. UI components receive formatted objects and callbacks
+4. Components maintain draft state for editing
+5. When "Apply" is clicked, callbacks update URL parameters
+6. Side effects are triggered via dedicated save hooks
+
+## Component Usage
+
+Components using these hooks should follow this pattern:
+
+```tsx
+function TableComponent() {
+ // Get filter data and callbacks
+ const { filters, onApplyFilters } = useTableFilter()
+
+ // Get sort data and callbacks
+ const { sorts, onApplySorts } = useTableSort()
+
+ return (
+ <>
+
+
+
+
+ {/* Table rendering with filters and sorts applied */}
+ >
+ )
+}
+```
+
+## Implementation Notes
+
+- Filter and sort parameters are stored in URL using specific formats
+- Conversion utilities (`formatFilterURLParams`, `formatSortURLParams`, etc.) handle translation between URL strings and typed objects
+- Side effect hooks manage database persistence and related operations
diff --git a/apps/studio/components/grid/hooks/useTableSort.ts b/apps/studio/components/grid/hooks/useTableSort.ts
index b40352a25b4..9ba34d77d62 100644
--- a/apps/studio/components/grid/hooks/useTableSort.ts
+++ b/apps/studio/components/grid/hooks/useTableSort.ts
@@ -44,9 +44,58 @@ export function useTableSort() {
[snap, setParams, saveSortsAndTriggerSideEffects]
)
+ /**
+ * Adds a new sort for a column or updates the direction of an existing one.
+ * New sorts are added to the beginning of the array (highest precedence).
+ * If the column already exists, its `ascending` direction is updated.
+ * Calls `onApplySorts` to update URL parameters and trigger side effects.
+ *
+ * @param columnKey The key/name of the column to sort.
+ * @param ascending The sort direction (true for ascending, false for descending).
+ */
+ const addOrUpdateSort = useCallback(
+ (columnKey: string, ascending: boolean) => {
+ if (!tableName || !columnKey) return
+
+ // Use the derived 'sorts' state from the hook
+ const existingSortIndex = sorts.findIndex((s) => s.column === columnKey)
+ let newSorts = [...sorts] // Create a mutable copy
+
+ if (existingSortIndex !== -1) {
+ // Column already exists in sorts: Update the existing sort (toggle handled by removeSort)
+ newSorts[existingSortIndex] = { ...newSorts[existingSortIndex], ascending: ascending }
+ } else {
+ // Column doesn't exist in sorts: Add it to the beginning
+ newSorts.unshift({ table: tableName, column: columnKey, ascending: ascending })
+ }
+
+ onApplySorts(newSorts)
+ },
+ [tableName, sorts, onApplySorts] // Depend on derived sorts and callback
+ )
+
+ /**
+ * Removes a sort criterion for a specific column.
+ * Calls `onApplySorts` with the filtered array to update URL parameters and trigger side effects.
+ *
+ * @param columnKey The key/name of the column to remove from sorting.
+ */
+ const removeSort = useCallback(
+ (columnKey: string) => {
+ if (!tableName || !columnKey) return
+
+ // Use the derived 'sorts' state from the hook
+ const newSorts = sorts.filter((s) => s.column !== columnKey)
+ onApplySorts(newSorts)
+ },
+ [tableName, sorts, onApplySorts] // Depend on derived sorts and callback
+ )
+
return {
sorts,
urlSorts,
onApplySorts,
+ addOrUpdateSort,
+ removeSort,
}
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
index 221e6bfa353..e873f620a08 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
@@ -4,15 +4,9 @@ import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { useParams } from 'common'
-import {
- filtersToUrlParams,
- formatFilterURLParams,
- formatSortURLParams,
- sortsToUrlParams,
-} from 'components/grid/SupabaseGrid.utils'
import RefreshButton from 'components/grid/components/header/RefreshButton'
-import FilterPopover from 'components/grid/components/header/filter/FilterPopover'
-import { SortPopover } from 'components/grid/components/header/sort'
+import FilterPopoverPrimitive from 'components/grid/components/header/filter/FilterPopoverPrimitive'
+import { SortPopoverPrimitive } from 'components/grid/components/header/sort'
import type { Filter } from 'components/grid/types'
import { Sort } from 'components/grid/types'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -65,9 +59,9 @@ const ForeignRowSelector = ({
id: tableId,
})
- const [{ sort: sorts, filter: filters }, setParams] = useState<{
- filter: string[]
- sort: string[]
+ const [{ sort: sorts, filter: filters }, setFiltersAndSorts] = useState<{
+ filter: Filter[]
+ sort: Sort[]
}>({ filter: [], sort: [] })
const onApplyFilters = (appliedFilters: Filter[]) => {
@@ -76,19 +70,19 @@ const ForeignRowSelector = ({
setPage(1)
}
- setParams((prevParams) => {
+ setFiltersAndSorts((prevParams) => {
return {
...prevParams,
- filter: filtersToUrlParams(appliedFilters),
+ filter: appliedFilters,
}
})
}
const onApplySorts = (appliedSorts: Sort[]) => {
- setParams((prevParams) => {
+ setFiltersAndSorts((prevParams) => {
return {
...prevParams,
- sort: sortsToUrlParams(appliedSorts),
+ sort: appliedSorts,
}
})
}
@@ -103,8 +97,8 @@ const ForeignRowSelector = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: table?.id,
- sorts: formatSortURLParams(table?.name || '', sorts),
- filters: formatFilterURLParams(filters),
+ sorts,
+ filters,
page,
limit: rowsPerPage,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
@@ -160,13 +154,17 @@ const ForeignRowSelector = ({