Skip to content

Commit

Permalink
Improved list component
Browse files Browse the repository at this point in the history
- pass typed prisma queries to list component, and use typed model in render function
- example of wrapper for list component
- list search example
- env validation
  • Loading branch information
kirankunigiri committed Dec 3, 2024
1 parent e204128 commit a9c0df3
Show file tree
Hide file tree
Showing 17 changed files with 222 additions and 103 deletions.
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions example-client/src/form/lib/list-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export function ListHeader({ title, model, ...overrideProps }: ListHeaderProps)
</Modal>

{/* List Header */}
<div className="left-list__header">
<p className="left-list__title">{title}</p>
<div className="mb-2 mt-3 flex items-center justify-between font-semibold">
<p className="text-lg font-semibold">{title}</p>
<ActionIcon size="sm" variant="light" onClick={openCreateModal}>
<LuPlus size={12} />
</ActionIcon>
Expand Down
43 changes: 43 additions & 0 deletions example-client/src/form/lib/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ScrollArea } from '@mantine/core';
import { Link } from '@tanstack/react-router';

import ListSkeleton from '~client/form/lib/skeleton';
import type { RouteFullPaths } from '~client/routes/-sidebar';
import { ZSList, type ZSListProps } from '~zenstack-ui/list/list';

interface ListWrapperProps<T> extends ZSListProps<T> {
route: RouteFullPaths
itemId?: number | string
search?: string
}

function ListWrapper<T extends Record<string, any>>({ itemId, search, route, ...zsListProps }: ListWrapperProps<T>) {
return (
<ScrollArea className="list-scrollarea">
<ZSList<T>
{...zsListProps}

// Defaults
skeleton={zsListProps.skeleton ?? <ListSkeleton />}
noResults={zsListProps.noResults ?? <p className="text-gray-500">No results found</p>}

// Render
render={(item: T, id: string | number) => (
<Link
key={id}
to={route}
params={{ id: id.toString() }}
className="list-item"
data-selected={id === itemId}
search={search}
>
{zsListProps.render(item, id)}
</Link>
)}
/>
</ScrollArea>
);
}

export default ListWrapper;
33 changes: 9 additions & 24 deletions example-client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ body {
/* Temp */
--color-hover: light-dark(#eeeeef, #1e1e20);
--focus-ring-color: light-dark(#A1A1AA, #D4D4D8);

/* Layout */
--list-margin: 1rem
}

/* Add custom focus ring color for Mantine (used in theme object) */
Expand All @@ -51,30 +54,21 @@ div:has(> .tsqd-open-btn) { @apply hidden; }
.page {
@apply flex size-full;

.left-list {
@apply flex w-2/5 min-w-[40%] max-w-[800px] flex-col border-r border-r-bd-light px-4;

.left-list__header {
@apply flex items-center justify-between mb-2 mt-3 font-semibold;
}
.list-margin {
@apply mx-[var(--list-margin)];
}

.left-list__title {
/* @apply mx-4 mb-1 mt-3 text-lg font-semibold; */
}
.left-list {
@apply flex w-2/5 min-w-[40%] max-w-[800px] flex-col border-r border-r-bd-light;
}

.right-detail {
@apply flex flex-col h-full grow p-4 pb-0 size-full;
}

.form-container {
@apply flex grow flex-col justify-between;
}

}

.list-scrollarea {
@apply h-full p-4 py-0;
@apply px-[var(--list-margin)] h-full py-0;
}

.list-item {
Expand All @@ -84,12 +78,3 @@ div:has(> .tsqd-open-btn) { @apply hidden; }
@apply bg-bd-light;
}
}

/* Form Section Gap */
.form-section > div:not(:first-child) {
@apply mt-4;
}

.custom-form .mantine-Checkbox-root {
@apply mt-2;
}
6 changes: 3 additions & 3 deletions example-client/src/routes/items/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function ItemsDetail() {
const id = Number(params.id);

return (
<div className="form-container">
<div className="flex grow flex-col justify-between">
<div>
<DetailHeader model={modelNames.item} id={id} route="/items" />
<MZSUpdateForm model={modelNames.item} id={id} route="/items/$id" schemaOverride={CustomItemSchema} />
Expand All @@ -48,8 +48,8 @@ function ItemsDetail() {
return (
<div>
<DetailHeader model="Item" id={id} route="/items" />
<ZenstackUpdateForm model="Item" id={id} />
<DetailHeader model={modelNames.item} id={id} route="/items" />
<ZSUpdateForm model={modelNames.item} id={id} />
</div>
);
}
Expand Down
56 changes: 38 additions & 18 deletions example-client/src/routes/items/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
import { ScrollArea, TextInput } from '@mantine/core';
import { createFileRoute, Link, Outlet } from '@tanstack/react-router';

import { modelNames } from '~client/form/form-config';
import ListWrapper from '~client/form/lib/list';
import { ListHeader } from '~client/form/lib/list-header';
import ListSkeleton from '~client/form/lib/skeleton';
import type { ItemSchema } from '~zenstack/zod/models';
import { ZenstackList } from '~zenstack-ui/list/list';
import { validateSearch } from '~client/utils/utils';
import type { Prisma } from '~zenstack/models';
import { ZSList } from '~zenstack-ui/list/list';

export const Route = createFileRoute('/items')({
component: ItemsLayout,
validateSearch,
});

function ItemsLayout() {
const params = Route.useParams() as { id?: number };
const itemId = Number(params.id);
const search = Route.useSearch();
const navigate = Route.useNavigate();

const itemQuery = {
include: {},
where: { name: { contains: search.search, mode: 'insensitive' } },
} satisfies Prisma.ItemFindManyArgs;
type ItemPayload = Prisma.ItemGetPayload<typeof itemQuery>;

return (
<div className="page">

{/* List View */}
<div className="left-list">

{/* Header */}
<ListHeader title="Items" model="Item" />
<div className="list-margin">
{/* Header */}
<ListHeader title="Items" model={modelNames.item} />

{/* Search Input */}
<TextInput
placeholder="Search"
value={search.search || ''}
onChange={e => navigate({ search: { search: e.target.value } })}
className="mb-4"
/>
</div>

{/* This is not much better than manually calling the hook */}
{/* It will be improved when we add automatic filters, infinite scroll, etc. */}
<ZenstackList<typeof ItemSchema._type>
model="Item"
skeleton={<ListSkeleton />}
{/* List */}
<ListWrapper<ItemPayload>
model={modelNames.item}
query={itemQuery}
route="/items/$id"
itemId={itemId}
search={search.search}
render={item => (
<Link
key={item.id}
to="/items/$id"
params={{ id: item.id.toString() }}
className="list-item"
data-selected={item.id === itemId}
>
<>
<p className="text-sm">{item.name}</p>
<p className="text-sm text-gray-500">{item.category}</p>
</Link>
<p className="text-sm text-gray-500"> {item.ownerName}, {item.category}</p>
</>
)}
/>

</div>

{/* Detail View */}
Expand Down
2 changes: 2 additions & 0 deletions example-client/src/routes/people/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { createFileRoute } from '@tanstack/react-router';
import { modelNames } from '~client/form/form-config';
import { DetailHeader } from '~client/form/lib/detail-header';
import MZSUpdateForm from '~client/form/lib/mantine-update-form';
import { validateSearch } from '~client/utils/utils';

export const Route = createFileRoute('/people/$id')({
component: PeopleDetail,
validateSearch,
});

function PeopleDetail() {
Expand Down
85 changes: 71 additions & 14 deletions example-client/src/routes/people/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,95 @@
import { Button, ScrollArea, TextInput } from '@mantine/core';
import { createFileRoute, Link, Outlet } from '@tanstack/react-router';

import { modelNames } from '~client/form/form-config';
import ListWrapper from '~client/form/lib/list';
import { ListHeader } from '~client/form/lib/list-header';
import ListSkeleton from '~client/form/lib/skeleton';
import type { PersonSchema } from '~zenstack/zod/models';
import { ZenstackList } from '~zenstack-ui/list/list';
import { validateSearch } from '~client/utils/utils';
import { useCreateManyPerson } from '~zenstack/hooks';
import { type Prisma } from '~zenstack/models';
import { ZSList } from '~zenstack-ui/list/list';

export const Route = createFileRoute('/people')({
component: PeopleLayout,
validateSearch,
});

function PeopleLayout() {
const params = Route.useParams() as { id?: string };
const name = params.id;
const search = Route.useSearch();
const navigate = Route.useNavigate();

const createManyPerson = useCreateManyPerson();

const personQuery = {
include: {},
where: { name: { contains: search.search, mode: 'insensitive' } },
} satisfies Prisma.PersonFindManyArgs;
type PersonPayload = Prisma.PersonGetPayload<typeof personQuery>;

return (
<div className="page">
{/* List View */}
<div className="left-list">
<ListHeader title="People" model="Person" />
<ZenstackList<typeof PersonSchema._type>
model="Person"
skeleton={<ListSkeleton />}

<div className="list-margin">
{/* Header */}
<ListHeader title="People" model={modelNames.person} />

{/* Search Input */}
<TextInput
placeholder="Search"
value={search.search || ''}
onChange={e => navigate({ search: { search: e.target.value } })}
className="mb-4"
/>

{/* Generator */}
<Button
className="min-h-8"
onClick={() => {
const randomNames = [
'Liam', 'Emma', 'Noah', 'Olivia', 'Ethan', 'Ava', 'Mason', 'Sophia', 'Lucas', 'Isabella',
'Oliver', 'Mia', 'Elijah', 'Charlotte', 'William', 'Amelia', 'James', 'Harper', 'Benjamin', 'Evelyn',
'Henry', 'Abigail', 'Alexander', 'Emily', 'Sebastian', 'Elizabeth', 'Jack', 'Sofia', 'Daniel', 'Avery',
'Michael', 'Ella', 'Samuel', 'Scarlett', 'David', 'Victoria', 'Joseph', 'Madison', 'Carter', 'Luna',
'Owen', 'Grace', 'Wyatt', 'Chloe', 'John', 'Penelope', 'Luke', 'Layla', 'Gabriel', 'Riley',
'Anthony', 'Zoey', 'Isaac', 'Nora', 'Grayson', 'Lily', 'Julian', 'Eleanor', 'Matthew', 'Hannah',
'Leo', 'Lillian', 'Nathan', 'Addison', 'Thomas', 'Aubrey', 'Caleb', 'Ellie', 'Josh', 'Stella',
'Ryan', 'Natalie', 'Adrian', 'Zoe', 'Adam', 'Leah', 'Ian', 'Hazel', 'Eric', 'Violet',
'Wesley', 'Aurora', 'Austin', 'Savannah', 'Jordan', 'Audrey', 'Colin', 'Brooklyn', 'Blake', 'Bella',
'Steven', 'Claire', 'Miles', 'Skylar', 'Robert', 'Lucy', 'Roman', 'Paisley', 'Carson', 'Everly',
'Cooper', 'Anna', 'Kyle', 'Caroline', 'Parker', 'Nova', 'Marcus', 'Genesis', 'Vincent', 'Emilia',
];

createManyPerson.mutateAsync({
skipDuplicates: true,
data: randomNames.map(name => ({
name,
roomId: 29,
})),
});
}}
>Generate 100 Names
</Button>
</div>

{/* List */}
<ListWrapper<PersonPayload>
model={modelNames.person}
query={personQuery}
route="/people/$id"
itemId={name}
search={search.search}
render={person => (
<Link
key={person.name}
to="/people/$id"
params={{ id: person.name }}
className="list-item"
data-selected={person.name === name}
>
<>
<p className="text-sm">{person.name}</p>
</Link>
</>
)}
/>

</div>

{/* Detail View */}
Expand Down
2 changes: 1 addition & 1 deletion example-client/src/routes/rooms/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRef } from 'react';
import { modelNames, typedModelFields } from '~client/form/form-config';
import { DetailHeader } from '~client/form/lib/detail-header';
import MZSUpdateForm from '~client/form/lib/mantine-update-form';
import { CustomRoomCreateSchema, CustomRoomUpdateSchema } from '~server/schemas';
import { CustomRoomUpdateSchema } from '~server/schemas';
import { ZSCustomField, ZSFieldSlot, type ZSFormRef } from '~zenstack-ui/index';

export const Route = createFileRoute('/rooms/$id')({
Expand Down
6 changes: 3 additions & 3 deletions example-client/src/routes/rooms/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { ListHeader } from '~client/form/lib/list-header';
import ListSkeleton from '~client/form/lib/skeleton';
import { trpc } from '~client/main';
import { CustomRoomCreateSchema } from '~server/schemas';
import { type HouseRoomSchema } from '~zenstack/zod/models';
import { ZenstackList } from '~zenstack-ui/list/list';
import { type HouseRoom } from '~zenstack/models';
import { ZSList } from '~zenstack-ui/list/list';

export const Route = createFileRoute('/rooms')({
component: RoomsLayout,
Expand All @@ -32,7 +32,7 @@ function RoomsLayout() {

<ListHeader title="Rooms" model={modelNames.houseRoom} schemaOverride={CustomRoomCreateSchema} overrideSubmit={createRoom.mutateAsync} metadataOverride={metadataOverride} />

<ZenstackList<typeof HouseRoomSchema._type>
<ZSList<HouseRoom>
model={modelNames.houseRoom}
skeleton={<ListSkeleton />}
render={room => (
Expand Down
5 changes: 5 additions & 0 deletions example-client/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Tanstack Router generic search params
export interface SearchParams {
search: string | undefined
}
export const validateSearch = (search: SearchParams): SearchParams => ({ search: search.search || undefined });
Loading

0 comments on commit a9c0df3

Please sign in to comment.