diff --git a/.agents/skills/data-client-react/references/useQuery.md b/.agents/skills/data-client-react/references/useQuery.md index 9dd95fe6f748..327f12aba574 100644 --- a/.agents/skills/data-client-react/references/useQuery.md +++ b/.agents/skills/data-client-react/references/useQuery.md @@ -189,15 +189,18 @@ interface Queryable { ### Sorting & Filtering -[Query](https://dataclient.io/rest/api/Query) provides programmatic access to the Reactive Data Client store. +[Query](https://dataclient.io/rest/api/Query) sorts and filters what is already in the store, so picking another column +re-sorts the table without fetching again. -```ts title="UserResource" +```ts title="resources/User" import { Entity, resource } from '@data-client/rest'; export class User extends Entity { id = ''; name = ''; isAdmin = false; + posts = 0; + joined = 0; static key = 'User'; } @@ -205,38 +208,171 @@ export const UserResource = resource({ path: '/users/:id', schema: User, }); + +/** A users list's order: by one column, reversed when `desc` */ +export type SortBy = 'name' | 'posts' | 'joined'; +export interface Sort { + sortBy: SortBy; + desc: boolean; +} +``` + +```tsx title="RoleFilter" +const roles = { All: undefined, Admins: true, Members: false }; + +export default function RoleFilter({ isAdmin, onChange }: Props) { + return ( +
+ {Object.entries(roles).map(([label, value]) => ( + + ))} +
+ ); +} +interface Props { + isAdmin: boolean | undefined; + onChange: (isAdmin: boolean | undefined) => void; +} +``` + +```tsx title="UsersTableView" +import React from 'react'; +import { type Sort, type SortBy, type User } from './resources/User'; + +export default function UsersTableView({ users, sort, onSort }: Props) { + return ( + + + + + Name + + + + Posts + + + Joined + + + + + {users.map(user => ( + + + + + + + ))} + +
Role
+ + {user.name} + + + {user.isAdmin ? 'Admin' : 'Member'} + + {user.posts}{user.joined}
+ ); +} +interface Props { + users: User[]; + sort: Sort; + onSort: (sort: Sort) => void; +} + +/** A column's header: sorts by it, and a second click reverses it */ +function SortHeader({ column, sort, onSort, numeric, children }: HeaderProps) { + const sorted = sort.sortBy === column; + return ( + + + + ); +} +interface HeaderProps { + column: SortBy; + sort: Sort; + onSort: (sort: Sort) => void; + numeric?: boolean; + children: React.ReactNode; +} ``` -```tsx title="UsersPage" {22} +```tsx title="UsersTable" {27} import { All, Query } from '@data-client/rest'; -import { useQuery, useFetch } from '@data-client/react'; -import { UserResource, User } from './UserResource'; +import { useQuery, useSuspense } from '@data-client/react'; +import { type Sort, User, UserResource } from './resources/User'; +import UsersTableView from './UsersTableView'; -interface Args { - asc: boolean; - isAdmin?: boolean; +interface Args extends Sort { + /** admins only (true), members only (false), or everyone */ + isAdmin: boolean | undefined; } + const sortedUsers = new Query( new All(User), - (entries, { asc, isAdmin }: Args = { asc: false }) => { - let sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name)); - if (isAdmin !== undefined) - sorted = sorted.filter(user => user.isAdmin === isAdmin); - if (asc) return sorted; - return sorted.reverse(); + (entries, { sortBy, desc, isAdmin }: Args) => { + const users = entries + .filter(user => isAdmin === undefined || user.isAdmin === isAdmin) + .sort((a, b) => + sortBy === 'name' ? + a.name.localeCompare(b.name) + : a[sortBy] - b[sortBy], + ); + return desc ? users.reverse() : users; }, ); +export default function UsersTable({ sort, isAdmin, onSort }: Props) { + useSuspense(UserResource.getList); + const users = useQuery(sortedUsers, { ...sort, isAdmin }) ?? []; + return ; +} +interface Props { + sort: Sort; + isAdmin: boolean | undefined; + onSort: (sort: Sort) => void; +} +``` + +```tsx title="UsersPage" +import React from 'react'; +import RoleFilter from './RoleFilter'; +import { type Sort } from './resources/User'; +import UsersTable from './UsersTable'; + function UsersPage() { - useFetch(UserResource.getList); - const users = useQuery(sortedUsers, { asc: true }); - if (!users) return
No users in cache yet
; + const [sort, setSort] = React.useState({ + sortBy: 'name', + desc: false, + }); + const [isAdmin, setIsAdmin] = React.useState(); return ( -
- {users.map(user => ( -
{user.name}
- ))} -
+ <> +
+

Team

+ +
+ + ); } render(); diff --git a/.agents/skills/data-client-schema/references/Scalar.md b/.agents/skills/data-client-schema/references/Scalar.md index 9bc31ebf755d..8257c3ff82cb 100644 --- a/.agents/skills/data-client-schema/references/Scalar.md +++ b/.agents/skills/data-client-schema/references/Scalar.md @@ -69,26 +69,26 @@ import { type Company } from './api/Company'; export default function CompanyGrid({ companies }: { companies: Company[] }) { return ( - +
- - - - + + + + {companies.map(c => ( - - + -
NamePrice% EquitySharesNamePrice% EquityShares
{c.name}${c.price.toFixed(2)} + ${c.price.toFixed(2)} {c.pct_equity === undefined ? : formatPercent(c.pct_equity)} + {c.shares === undefined ? : c.shares.toLocaleString()} diff --git a/.agents/skills/data-client-schema/references/Scalar.vue.md b/.agents/skills/data-client-schema/references/Scalar.vue.md index 9b074a3853d4..ada76d888082 100644 --- a/.agents/skills/data-client-schema/references/Scalar.vue.md +++ b/.agents/skills/data-client-schema/references/Scalar.vue.md @@ -76,24 +76,24 @@ export const getPortfolioColumns = new RestEndpoint({