Home / Function/ SortPopoverPrimitive() — supabase Function Reference

SortPopoverPrimitive() — supabase Function Reference

Architecture documentation for the SortPopoverPrimitive() function in SortPopoverPrimitive.tsx from the supabase codebase.

Entity Profile

Relationship Graph

Source Code

apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx lines 46–328

export const SortPopoverPrimitive = ({
  buttonText,
  sorts,
  onApplySorts,
  defaultOpen = false,
  tableQueriesEnabled = true,
}: SortPopoverPrimitiveProps) => {
  const { ref } = useParams()
  const { filters } = useTableFilter()
  const { data: project } = useSelectedProjectQuery()
  const roleImpersonationState = useRoleImpersonationStateSnapshot()

  const snap = useTableEditorTableStateSnapshot()
  const tableName = snap.table?.name || ''
  const tableSchema = snap.table.schema || ''

  const [open, setOpen] = useState(defaultOpen)
  const [showWarning, setShowWarning] = useState(false)
  // Local state for draft sorts
  const [localSorts, setLocalSorts] = useState<Sort[]>(sorts)

  // Track the last props we received for comparison
  const lastSortsRef = useRef<Sort[]>(sorts)
  // Track if we're in the middle of applying our own changes
  const isApplyingRef = useRef(false)

  const { data: countData } = useTableRowsCountQuery(
    {
      projectRef: project?.ref,
      connectionString: project?.connectionString,
      tableId: snap.table.id,
      filters,
      enforceExactCount: snap.enforceExactCount,
      roleImpersonationState: roleImpersonationState as RoleImpersonationState,
    },
    { placeholderData: keepPreviousData, enabled: tableQueriesEnabled }
  )
  const isLargeTable = (countData?.count ?? 0) > THRESHOLD_COUNT

  // Fix: Use localSorts for button text, not sorts
  const displayButtonText =
    buttonText ??
    (localSorts.length > 0
      ? `Sorted by ${localSorts.length} rule${localSorts.length > 1 ? 's' : ''}`
      : 'Sort')

  // Filter available columns to exclude columns already in sorts
  const columns = useMemo(() => {
    if (!snap?.table?.columns) return []
    return snap.table.columns.filter((x) => {
      const found = localSorts.find((y) => y.column == x.name)
      return !found
    })
  }, [snap?.table?.columns, localSorts])

  // Format the columns for the dropdown
  const dropdownOptions = useMemo(() => {
    return (
      columns?.map((x) => ({
        value: x.name,
        label: x.name,
        postLabel: x.dataType,
        disabled: x.dataType === 'json' || x.dataType === 'jsonb',
        tooltip:
          x.dataType === 'json' || x.dataType === 'jsonb'
            ? 'Sorting on JSON-based columns is currently not supported'
            : '',
      })) || []
    )
  }, [columns])

  // Add a new sort
  const onAddSort = (columnName: string | number) => {
    const currentTableName = snap.table?.name
    if (currentTableName) {
      setLocalSorts([
        ...localSorts,
        { table: currentTableName, column: columnName as string, ascending: true },
      ])
    }
  }

  // Remove a sort by column name
  const onDeleteSort = useCallback((column: string) => {
    setLocalSorts((currentSorts) => currentSorts.filter((sort) => sort.column !== column))
  }, [])

  // Toggle ascending/descending for a column
  const onToggleSort = useCallback((column: string, ascending: boolean) => {
    setLocalSorts((currentSorts) => {
      const index = currentSorts.findIndex((x) => x.column === column)
      if (index === -1) return currentSorts
      const updatedSort = { ...currentSorts[index], ascending }
      return [...currentSorts.slice(0, index), updatedSort, ...currentSorts.slice(index + 1)]
    })
  }, [])

  // Handle drag-and-drop reordering
  const onDragSort = useCallback((dragIndex: number, hoverIndex: number) => {
    setLocalSorts((currentSort) => {
      if (
        dragIndex < 0 ||
        dragIndex >= currentSort.length ||
        hoverIndex < 0 ||
        hoverIndex >= currentSort.length
      ) {
        return currentSort
      }
      const itemToMove = currentSort[dragIndex]
      const remainingItems = [
        ...currentSort.slice(0, dragIndex),
        ...currentSort.slice(dragIndex + 1),
      ]
      return [
        ...remainingItems.slice(0, hoverIndex),
        itemToMove,
        ...remainingItems.slice(hoverIndex),
      ]
    })
  }, [])

  // Fix: Compare for meaningful changes (only column order and ascending)
  const hasChanges = useMemo(() => {
    if (localSorts.length !== sorts.length) return true

    // Compare each sort by relevant properties
    return localSorts.some((localSort, index) => {
      const propSort = sorts[index]
      return (
        !propSort ||
        localSort.column !== propSort.column ||
        localSort.ascending !== propSort.ascending
      )
    })
  }, [localSorts, sorts])

  // Apply the sorts to the parent component
  const onSelectApplySorts = () => {
    // Mark that we're applying our changes to prevent re-syncing
    isApplyingRef.current = true

    // Update our last sorts ref to the current local state
    lastSortsRef.current = [...localSorts]

    // Create deep copies to avoid reference issues
    const sortsCopy = localSorts.map((sort) => ({ ...sort }))

    // Apply the sorts
    onApplySorts(sortsCopy)
  }

  // Generate stable keys for SortRow components to avoid reconciliation issues
  const getSortRowKey = (sort: Sort, index: number) => {
    return `sort-${sort.table}-${sort.column}-${index}`
  }

  // Sync with props when they change, but in a smarter way
  useEffect(() => {
    // If we're in the middle of applying changes, don't sync from props
    if (isApplyingRef.current) {
      isApplyingRef.current = false
      return
    }

    // If the props changed unexpectedly (not due to our own actions)
    // then we should update our local state
    if (!isEqual(sorts, lastSortsRef.current)) {
      setLocalSorts(sorts)
      lastSortsRef.current = sorts
    }
  }, [sorts])

  return (
    <>
      <Popover_Shadcn_ modal={false} open={open} onOpenChange={setOpen}>
        <PopoverTrigger_Shadcn_ asChild>
          <Button type={localSorts.length > 0 ? 'link' : 'text'} icon={<List />}>
            {displayButtonText}
          </Button>
        </PopoverTrigger_Shadcn_>
        <PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="start">
          <div className="space-y-2 py-2">
            {localSorts.map((sort, index) => (
              <SortRow
                key={getSortRowKey(sort, index)}
                index={index}
                columnName={sort.column}
                sort={sort}
                onDelete={onDeleteSort}
                onToggle={onToggleSort}
                onDrag={onDragSort}
              />
            ))}
            {localSorts.length === 0 && (
              <div className="space-y-1 px-3">
                <h5 className="text-xs text-foreground-light">No sorts applied to this view</h5>
                <p className="text-xs text-foreground-lighter">
                  Add a column below to sort the view
                </p>
              </div>
            )}

            <PopoverSeparator_Shadcn_ />
            <div className="px-3 flex flex-row justify-between">
              {dropdownOptions && dropdownOptions.length > 0 ? (
                <DropdownControl
                  options={dropdownOptions}
                  onSelect={onAddSort}
                  side="bottom"
                  align="start"
                >
                  <Button
                    asChild
                    type="dashed"
                    iconRight={<ChevronDown size="14" className="text-foreground-light" />}
                    className="sb-grid-dropdown__item-trigger"
                    data-testid="table-editor-pick-column-to-sort-button"
                  >
                    <span>Pick {localSorts.length > 1 ? 'another' : 'a'} column to sort by</span>
                  </Button>
                </DropdownControl>
              ) : (
                <p className="text-sm text-foreground-light">All columns have been added</p>
              )}
              <div className="flex items-center">
                <Button
                  disabled={!hasChanges}
                  type="default"
                  onClick={() => {
                    if (isLargeTable && localSorts.length > 0) {
                      // [Joshen] Note we're only checking PKs - unable to check indexes properly
                      // as we are not able to deterministically figure out which columns the indexes are applied to
                      const hasSortNotPK = localSorts.some(
                        (x) => !snap.table.columns.find((y) => x.column === y.name)?.isPrimaryKey
                      )
                      if (hasSortNotPK) return setShowWarning(true)
                    }

                    onSelectApplySorts()
                  }}
                >
                  Apply sorting
                </Button>
              </div>
            </div>
          </div>
        </PopoverContent_Shadcn_>
      </Popover_Shadcn_>

      <ConfirmationModal
        size="medium"
        variant="warning"
        visible={showWarning}
        confirmLabel="Confirm"
        title="Sorting on a large table"
        onConfirm={() => {
          onSelectApplySorts()
          setShowWarning(false)
        }}
        onCancel={() => {
          setLocalSorts(sorts)
          setShowWarning(false)
        }}
        alert={{
          base: { variant: 'warning' },
          title: 'Be careful with sorting on unindexed columns',
          description:
            'This may adversely impact your database, in particular if your table has a large number of rows - use with caution.',
        }}
      >
        <p className="text-foreground-light text-sm">
          We highly recommend only sorting on columns which are{' '}
          <InlineLink
            href={`/project/${ref}/database/indexes?search=${tableName}&schema=${tableSchema}`}
          >
            indexed
          </InlineLink>
          , such as your primary key columns.
        </p>
      </ConfirmationModal>
    </>
  )
}

Subdomains

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free