Skip to content

Commit

Permalink
Added ZSCustomField
Browse files Browse the repository at this point in the history
- Added ZSCustomField to replace data-name-field attribute
- Added proper support for booleans (fixed some issues)
- Allow custom types in MapFieldTypeToElement
- Added globalClassName to easily style all forms
- Cleaned up form-config by moving queryClient
  • Loading branch information
kirankunigiri committed Dec 2, 2024
1 parent 67dc529 commit 677ee34
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 114 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ model Item {
}
```

This is all the code you need to generate the forms seen in the demo! Any change in the zmodel schema will automatically reflect in the UI. There are many customization options as well, including creating form templates and custom elements. Read the docs to learn more.
This is all the code you need to generate the forms seen in the demo! Any change in the zmodel schema will automatically reflect in the UI.

```tsx
// Create form
Expand All @@ -39,4 +39,22 @@ This is all the code you need to generate the forms seen in the demo! Any change
<ZSUpdateForm model={modelNames.item} id={0} />
```

You can see the results in the demo!
If you want to customize your forms, you can use slot placeholders or custom elements. This allows you to build complete templates with your own components. See the example below:
```tsx
<ZSUpdateForm model={modelNames.room} id={id}>
{/* A placeholder example. This gets replaced by an input component */}
<ZSFieldSlot className="grow" fieldName={roomFields.description} />

{/* A custom element example. This will be directly used by the form */}
<ZSCustomField fieldName={roomFields.aiSummary}>
<Textarea
className="grow"
autosize
label="AI Summary"
placeholder="AI Summary"
/>
</ZSCustomField>
</ZSUpdateForm>
```

You can see the results in the demo! There are many customization options as well, read the docs to learn more.
8 changes: 7 additions & 1 deletion example-client/src/form/form-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Checkbox, MultiSelect, NumberInput, TextInput } from '@mantine/core';
import { Button } from '@mantine/core';

import { SearchableSelect } from '~client/form/lib/searchable-select';
import { queryClient } from '~client/utils/query-client';
import * as hooks from '~zenstack/hooks';
import metadata from '~zenstack/hooks/__model_meta';
import * as schemas from '~zenstack/zod/models';
Expand All @@ -19,6 +20,9 @@ const mapFieldTypeToElement: MapFieldTypeToElement = {
[FieldType.Enum]: SearchableSelect,
[FieldType.ReferenceSingle]: SearchableSelect,
[FieldType.ReferenceMultiple]: MultiSelect,

// Example of adding custom types
MyCustomType: () => <div>My Custom Type</div>,
};

const CreateButton = ({ model, ...props }: SubmitButtonProps) => {
Expand Down Expand Up @@ -96,11 +100,13 @@ meta.models.item.fields.ownerName.filter = (itemFields: typeof meta.models.item.
// --------------------------------------------------------------------------------
// Export config
// --------------------------------------------------------------------------------
export const baseZenstackUIConfig = {
export const zenstackUIConfig: ZenstackUIConfigType = {
hooks,
schemas,
metadata: meta,
elementMap: mapFieldTypeToElement,
submitButtons: submitButtonMap,
enumLabelTransformer: (label: string) => label.replace(/_/g, ' '),
globalClassName: 'flex flex-col gap-2',
queryClient,
};
60 changes: 7 additions & 53 deletions example-client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import '@mantine/core/styles.css';
import '~client/index.css';
import '~client/styles/button.css';
import '~client/styles/input.css';
import '~client/styles/modal.css';
import '~client/styles/tooltip.css';
import '~client/styles/button.css';
import '~client/styles/form.css';

import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { createRouter, RouterProvider } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { httpLink, TRPCClientError } from '@trpc/client';
import { httpLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { toast } from 'sonner';

import { baseZenstackUIConfig } from '~client/form/form-config';
import { zenstackUIConfig } from '~client/form/form-config';
import { theme } from '~client/routes/-mantine-theme';
import { routeTree } from '~client/routeTree.gen';
import { queryClient } from '~client/utils/query-client';
import type { AppRouter } from '~server/api';
import { Provider as ZenStackHooksProvider } from '~zenstack/hooks';
import { type ZenstackUIConfigType, ZenstackUIProvider } from '~zenstack-ui/utils/provider';
import { ZenstackUIProvider } from '~zenstack-ui/utils/provider';

// --------------------------------------------------------------------------------
// TanStack Router Setup
Expand Down Expand Up @@ -72,53 +73,6 @@ const trpcLinks = [httpLink({ url: `${serverUrl}/trpc` })];
export const trpc = createTRPCReact<AppRouter>();
const trpcClient = trpc.createClient({ links: trpcLinks });

// Query Client with UI notification logging
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
},
},
queryCache: new QueryCache({
onError: (error, query) => {
if (error instanceof TRPCClientError) {
console.error('trpc query error');
console.dir(error);
toast.error(`TRPC Server Error: ${error.message}`);
} else {
console.error('ZenStack query error');
// @ts-expect-error Need to find correct type for error
const message = error['info']['message'];
console.error('ZenStack Server Error:', message);
toast.error(`ZenStack Server Error: ${message}`);
}
},
}),
mutationCache: new MutationCache({
onError: (error, mutation) => {
if (error instanceof TRPCClientError) {
console.error('trpc mutation error');
console.error('TRPC Server Error:', error.message);
toast.error(`TRPC Server Error: ${error.message}`);
} else {
console.error('ZenStack mutation error');
// @ts-expect-error Need to find correct type for error
const message = error['info']['message'];
console.error('ZenStack Server Error:', message);
toast.error(`ZenStack Server Error: ${message}`);
}
},
}),
});

// ZenstackUIConfig with queryClient
const zenstackUIConfig: ZenstackUIConfigType = {
...baseZenstackUIConfig,
queryClient,
};

// --------------------------------------------------------------------------------
// Render
const rootElement = document.getElementById('root')!;
Expand Down
35 changes: 20 additions & 15 deletions 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 { ZSFieldSlot, type ZSFormRef } from '~zenstack-ui/index';
import { ZSCustomField, ZSFieldSlot, type ZSFormRef } from '~zenstack-ui/index';

export const Route = createFileRoute('/rooms/$id')({
component: PeopleDetail,
Expand All @@ -31,13 +31,14 @@ function PeopleDetail() {
{/* <ZSFieldSlot className="grow" fieldName={roomFields.description} /> */}

{/* A custom element example. This will be directly used by the form */}
{/* <Textarea
className="grow"
autosize
data-field-name={roomFields.aiSummary}
label="AI Summary"
placeholder="AI Summary"
/> */}
{/* <ZSCustomField fieldName={roomFields.aiSummary}>
<Textarea
className="grow"
autosize
label="AI Summary"
placeholder="AI Summary"
/>
</ZSCustomField> */}

<ZenstackTest />
</div>
Expand All @@ -50,14 +51,18 @@ function PeopleDetail() {
const ZenstackTest = () => {
return (
<>
{/* A placeholder example. This gets replaced by an input component */}
<ZSFieldSlot className="grow" fieldName={roomFields.description} />
<Textarea
className="grow"
autosize
data-field-name={roomFields.aiSummary}
label="AI Summary"
placeholder="AI Summary"
/>

{/* A custom element example. This will be directly used by the form */}
<ZSCustomField fieldName={roomFields.aiSummary}>
<Textarea
className="grow"
autosize
label="AI Summary"
placeholder="AI Summary"
/>
</ZSCustomField>
</>
);
};
5 changes: 5 additions & 0 deletions example-client/src/styles/form.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
form {
.mantine-Checkbox-root {
@apply my-2;
}
}
5 changes: 5 additions & 0 deletions example-client/src/styles/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
}
}

/* Remove extra gap for input fields */
.mantine-InputBase-root {
margin-top: -8px;
}

/* -------------------------------------------------------------------------------- */
/* Custom SearchableSelect */
/* -------------------------------------------------------------------------------- */
Expand Down
44 changes: 44 additions & 0 deletions example-client/src/utils/query-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query';
import { TRPCClientError } from '@trpc/client';
import { toast } from 'sonner';

// Query Client with UI notification logging
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: true,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 1 day
},
},
queryCache: new QueryCache({
onError: (error, query) => {
if (error instanceof TRPCClientError) {
console.error('trpc query error');
console.dir(error);
toast.error(`TRPC Server Error: ${error.message}`);
} else {
console.error('ZenStack query error');
// @ts-expect-error Need to find correct type for error
const message = error['info']['message'];
console.error('ZenStack Server Error:', message);
toast.error(`ZenStack Server Error: ${message}`);
}
},
}),
mutationCache: new MutationCache({
onError: (error, mutation) => {
if (error instanceof TRPCClientError) {
console.error('trpc mutation error');
console.error('TRPC Server Error:', error.message);
toast.error(`TRPC Server Error: ${error.message}`);
} else {
console.error('ZenStack mutation error');
// @ts-expect-error Need to find correct type for error
const message = error['info']['message'];
console.error('ZenStack Server Error:', message);
toast.error(`ZenStack Server Error: ${message}`);
}
},
}),
});
1 change: 1 addition & 0 deletions example-server/schema.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ model Person {
model Item {
id Int @id @default(autoincrement())
name String
shareable Boolean @default(false)
category ITEM_CATEGORY
room Room @relation(fields: [roomId], references: [id])
roomId Int
Expand Down
Loading

0 comments on commit 677ee34

Please sign in to comment.