Skip to main content
OrderBySelect is an auto-generated component for selecting sort criteria in list pages. It’s generated based on the backend’s OrderBy enum with auto-mapped labels.

Core Features

Auto-generated

Auto-generated from Entity definitionOrderBy enum based

Localized Labels

Auto label mappingβ€œID Latest”, β€œCreated Ascending”, etc.

useListParams Integration

register pattern supportAuto URL sync

Customizable

Scaffolding fileFreely modifiable

Auto-generation Conditions

OrderBySelect is auto-generated when the OrderBy enum is defined in the backend.

Backend Definition

// user.types.ts (backend)
import { z } from "zod";

export const UserOrderBy = z.enum([
  "id-desc",
  "id-asc",
  "created_at-desc",
  "created_at-asc",
  "username-asc",
  "username-desc",
]);

export const UserListParams = UserBaseListParams.extend({
  orderBy: UserOrderBy.optional(),
});

Auto-generated Files

Location: web/src/components/user/UserOrderBySelect.tsx Auto-generated labels: web/src/services/sonamu.generated.ts
export const UserOrderByLabel = {
  "id-desc": "ID Latest",
  "id-asc": "ID Oldest",
  "created_at-desc": "Created Latest",
  "created_at-asc": "Created Oldest",
  "username-asc": "Name Ascending",
  "username-desc": "Name Descending",
};
Label Generation Rules
  • id-desc β†’ β€œID Latest”
  • created_at-asc β†’ β€œCreated Oldest”
  • username-desc β†’ β€œName Descending”
Generated by combining field name and sort direction (asc/desc).

Basic Usage

Using with useListParams

import { useListParams } from "@sonamu-kit/react-components";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
import { UserOrderBy } from "@/services/sonamu.generated";

export function UserListPage() {
  const { register } = useListParams(UserListParams, {
    num: 24,
    page: 1,
    orderBy: UserOrderBy.options[0],  // Default: "id-desc"
  });

  return (
    <div className="flex gap-2">
      <UserOrderBySelect {...register("orderBy")} />
    </div>
  );
}
How it works:
  1. User selects sort option
  2. register("orderBy") automatically updates URL
  3. listParams.orderBy is passed when calling API
  4. Backend applies sorting

Props

export type UserOrderBySelectProps = {
  value?: string;
  onValueChange?: (value: string | null | undefined) => void;
  placeholder?: string;
  textPrefix?: string;
  clearable?: boolean;
  disabled?: boolean;
  className?: string;
};

Basic Usage (without register)

import { useState } from "react";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";

export function UserListPage() {
  const [orderBy, setOrderBy] = useState<string>("id-desc");

  return (
    <UserOrderBySelect
      value={orderBy}
      onValueChange={(value) => setOrderBy(value ?? "id-desc")}
      placeholder="Sort by"
    />
  );
}

Props Details

placeholder

Text displayed when nothing is selected.
<UserOrderBySelect
  {...register("orderBy")}
  placeholder="Select sort"  // Default: "Sort"
/>

textPrefix

Prefix added before each option.
<UserOrderBySelect
  {...register("orderBy")}
  textPrefix="Sort: "
/>

// Rendered result:
// - Sort: ID Latest
// - Sort: ID Oldest
// - Sort: Created Latest
textPrefix Use CaseUseful when you want to clearly display current sort at the top of list pages:
[Sort: ID Latest β–Ό]

clearable

Adds an β€œAll” option to reset sorting.
<UserOrderBySelect
  {...register("orderBy")}
  clearable
/>

// Rendered result:
// - All           ← Added by clearable
// - ID Latest
// - ID Oldest
// - ...
Use scenarios:
  • Want to return to default sorting
  • Want to see data in database order without sorting

disabled

Disables the component.
<UserOrderBySelect
  {...register("orderBy")}
  disabled={isLoading}  // Can't change while loading
/>

className

Add Tailwind CSS classes.
<UserOrderBySelect
  {...register("orderBy")}
  className="w-[200px] h-8 bg-white border-gray-300 text-xs"
/>

Real-world Examples

Complete List Filter

import { useListParams } from "@sonamu-kit/react-components";
import { Input } from "@sonamu-kit/react-components/components";
import { UserService } from "@/services/services.generated";
import { UserOrderBySelect } from "@/components/user/UserOrderBySelect";
import { UserSearchFieldSelect } from "@/components/user/UserSearchFieldSelect";
import { UserRoleSelect } from "@/components/user/UserRoleSelect";

export function UserListPage() {
  const { listParams, register } = useListParams(UserListParams, {
    num: 24,
    page: 1,
    search: "id" as const,
    keyword: "",
    orderBy: "id-desc" as const,
    role: undefined,
  });

  const { data } = UserService.useUsers("A", listParams);

  return (
    <div>
      {/* Filter section */}
      <div className="flex gap-2 mb-4">
        {/* Search */}
        <UserSearchFieldSelect
          {...register("search")}
          className="w-32"
        />
        <Input
          {...register("keyword")}
          placeholder="Keyword"
          className="w-64"
        />

        {/* Role filter */}
        <UserRoleSelect
          {...register("role")}
          clearable
        />

        {/* Sort */}
        <UserOrderBySelect
          {...register("orderBy")}
          textPrefix="Sort: "
          className="w-[200px]"
        />
      </div>

      {/* Table */}
      <table>
        {/* ... */}
      </table>
    </div>
  );
}

Mobile Responsive

<div className="flex flex-col sm:flex-row gap-2">
  <UserOrderBySelect
    {...register("orderBy")}
    className="w-full sm:w-[200px]"  // Mobile: full width, Desktop: 200px
  />
</div>

Display Number of Sort Options

import { UserOrderBy } from "@/services/sonamu.generated";

<div className="flex items-center gap-2">
  <span className="text-sm text-gray-500">
    {UserOrderBy.options.length} sort options
  </span>
  <UserOrderBySelect {...register("orderBy")} />
</div>

Customization

OrderBySelect is a scaffolding file, so you can modify it freely.

Change Labels

// Modify UserOrderBySelect.tsx
import { UserOrderByLabel } from "@/services/sonamu.generated";

// Create custom label object
const customLabels = {
  ...UserOrderByLabel,
  "id-desc": "Latest First",  // Default: "ID Latest"
  "created_at-asc": "Oldest",  // Default: "Created Oldest"
};

export function UserOrderBySelect({ ... }) {
  return (
    <Select ...>
      <SelectContent>
        {validOptions.map((key) => (
          <SelectItem key={key} value={key}>
            {(textPrefix ?? "") + customLabels[key]}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}

Add Icons

import ArrowUpIcon from "~icons/lucide/arrow-up";
import ArrowDownIcon from "~icons/lucide/arrow-down";

const getIcon = (key: string) => {
  if (key.endsWith("-asc")) return <ArrowUpIcon className="w-3 h-3" />;
  if (key.endsWith("-desc")) return <ArrowDownIcon className="w-3 h-3" />;
  return null;
};

<SelectItem key={key} value={key}>
  <div className="flex items-center gap-2">
    {getIcon(key)}
    {customLabels[key]}
  </div>
</SelectItem>

Hide Specific Options

// Show only "username" sort options
const visibleOptions = validOptions.filter(key =>
  key.includes("username")
);

<SelectContent>
  {visibleOptions.map((key) => (
    <SelectItem key={key} value={key}>
      {UserOrderByLabel[key]}
    </SelectItem>
  ))}
</SelectContent>

Grouping

const groupedOptions = {
  "Basic": ["id-desc", "id-asc"],
  "Date": ["created_at-desc", "created_at-asc"],
  "Name": ["username-asc", "username-desc"],
};

<SelectContent>
  {Object.entries(groupedOptions).map(([group, options]) => (
    <Fragment key={group}>
      <SelectLabel>{group}</SelectLabel>
      {options.map((key) => (
        <SelectItem key={key} value={key}>
          {UserOrderByLabel[key]}
        </SelectItem>
      ))}
    </Fragment>
  ))}
</SelectContent>

Backend Processing

The value selected in OrderBySelect is passed to the backend for actual sorting.

Handling in Model

// user.model.ts (backend)
async findMany<T extends UserSubsetKey, LP extends UserListParams>(
  subset: T,
  rawParams?: LP,
): Promise<ListResult<LP, UserSubsetMapping[T]>> {
  const params = {
    num: 24,
    page: 1,
    orderBy: "id-desc" as const,
    ...rawParams,
  };

  const { qb } = this.getSubsetQueries(subset);

  // orderBy processing
  if (params.orderBy === "id-desc") {
    qb.orderBy("users.id", "desc");
  } else if (params.orderBy === "id-asc") {
    qb.orderBy("users.id", "asc");
  } else if (params.orderBy === "created_at-desc") {
    qb.orderBy("users.created_at", "desc");
  } else if (params.orderBy === "created_at-asc") {
    qb.orderBy("users.created_at", "asc");
  } else if (params.orderBy === "username-asc") {
    qb.orderBy("users.username", "asc");
  } else if (params.orderBy === "username-desc") {
    qb.orderBy("users.username", "desc");
  } else {
    exhaustive(params.orderBy);  // Ensure type safety
  }

  return this.executeSubsetQuery({ subset, qb, params });
}

exhaustive Pattern

exhaustive() validates at compile time that all cases are handled.
import { exhaustive } from "sonamu";

// Adding new option "email-asc" to OrderBy enum
// Compile error occurs β†’ Developer immediately knows about omission
else {
  exhaustive(params.orderBy);  // ❌ Type error: "email-asc" not handled
}

Regeneration

OrderBySelect is not auto-regenerated after initial generation.

When Regeneration is Needed

  1. Component file was deleted
  2. Entity’s OrderBy enum significantly changed

Regeneration Method

# Delete component
rm web/src/components/user/UserOrderBySelect.tsx

# Restart dev server (auto-regenerates)
pnpm dev
Caution on RegenerationCustomized content will be lost on regeneration. Back up or document important modifications separately.

Troubleshooting

OrderBySelect Not Generated

Cause: OrderBy enum not defined in backend Solution:
// Add to user.types.ts (backend)
export const UserOrderBy = z.enum([
  "id-desc",
  "id-asc",
]);

export const UserListParams = UserBaseListParams.extend({
  orderBy: UserOrderBy.optional(),
});

Labels Not Showing in Localized Language

Cause: sonamu.generated.ts not regenerated Solution:
# Delete sonamu.lock and re-sync
rm api/sonamu.lock
pnpm sonamu sync

Sorting Not Applied Despite Selection

Cause: orderBy handling missing in backend Model Solution: Handle params.orderBy in user.model.ts’s findMany.