@tailor-platform/erp-kit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -3
- package/package.json +1 -1
- package/schemas/app-compose/business-flow.yml +3 -0
- package/schemas/app-compose/story.yml +1 -1
- package/skills/erp-kit-app-1-requirements/SKILL.md +6 -12
- package/skills/erp-kit-app-2-breakdown/SKILL.md +3 -10
- package/skills/erp-kit-app-3-doc-review/SKILL.md +1 -5
- package/skills/{erp-kit-app-6-impl-spec → erp-kit-app-4-impl-spec}/SKILL.md +10 -21
- package/skills/erp-kit-app-5-implementation/SKILL.md +149 -0
- package/skills/erp-kit-app-5-implementation/references/backend.md +232 -0
- package/skills/erp-kit-app-5-implementation/references/frontend.md +242 -0
- package/src/module.ts +38 -0
- package/skills/erp-kit-app-1-requirements/references/structure.md +0 -27
- package/skills/erp-kit-app-2-breakdown/references/screen-detailview.md +0 -106
- package/skills/erp-kit-app-2-breakdown/references/screen-form.md +0 -139
- package/skills/erp-kit-app-2-breakdown/references/screen-listview.md +0 -153
- package/skills/erp-kit-app-2-breakdown/references/structure.md +0 -27
- package/skills/erp-kit-app-3-doc-review/references/structure.md +0 -27
- package/skills/erp-kit-app-4-design/SKILL.md +0 -256
- package/skills/erp-kit-app-4-design/references/component.md +0 -50
- package/skills/erp-kit-app-4-design/references/screen-detailview.md +0 -106
- package/skills/erp-kit-app-4-design/references/screen-form.md +0 -139
- package/skills/erp-kit-app-4-design/references/screen-listview.md +0 -153
- package/skills/erp-kit-app-4-design/references/structure.md +0 -27
- package/skills/erp-kit-app-5-design-review/SKILL.md +0 -290
- package/skills/erp-kit-app-5-design-review/references/component.md +0 -50
- package/skills/erp-kit-app-5-design-review/references/screen-detailview.md +0 -106
- package/skills/erp-kit-app-5-design-review/references/screen-form.md +0 -139
- package/skills/erp-kit-app-5-design-review/references/screen-listview.md +0 -153
- package/skills/erp-kit-app-6-impl-spec/references/auth.md +0 -72
- package/skills/erp-kit-app-6-impl-spec/references/structure.md +0 -27
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Frontend Implementation Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Frontend Scaffold](#frontend-scaffold)
|
|
6
|
+
- [Frontend Pages](#frontend-pages)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Frontend Scaffold
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
frontend/
|
|
14
|
+
├── src/
|
|
15
|
+
│ ├── main.tsx, App.tsx, index.css
|
|
16
|
+
│ ├── graphql/
|
|
17
|
+
│ │ ├── index.ts # gql.tada setup
|
|
18
|
+
│ │ └── generated/ # Placeholder until backend deploy
|
|
19
|
+
│ ├── lib/
|
|
20
|
+
│ │ ├── auth-client.ts
|
|
21
|
+
│ │ └── utils.ts # cn() utility
|
|
22
|
+
│ ├── providers/
|
|
23
|
+
│ │ └── graphql-provider.tsx # urql client with DPoP auth
|
|
24
|
+
│ ├── components/
|
|
25
|
+
│ │ ├── ui/ # shadcn/Radix primitives
|
|
26
|
+
│ │ └── composed/ # empty-state, error-fallback, loading
|
|
27
|
+
│ └── pages/
|
|
28
|
+
│ └── <domain>/
|
|
29
|
+
│ ├── page.tsx # Module redirect (REQUIRED)
|
|
30
|
+
│ └── <entity>/
|
|
31
|
+
│ ├── page.tsx # ListView
|
|
32
|
+
│ ├── components/<entities>-table.tsx
|
|
33
|
+
│ ├── create/page.tsx + components/create-<entity>-form.tsx
|
|
34
|
+
│ └── [id]/
|
|
35
|
+
│ ├── page.tsx # DetailView
|
|
36
|
+
│ ├── components/<entity>-detail.tsx, <entity>-actions.tsx
|
|
37
|
+
│ └── edit/page.tsx + components/edit-<entity>-form.tsx
|
|
38
|
+
├── scripts/generate-graphql.mjs
|
|
39
|
+
├── package.json, vite.config.ts, tsconfig.json, eslint.config.js
|
|
40
|
+
└── components.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`package.json` key dependencies and scripts:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"type": "module",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"dev": "vite",
|
|
50
|
+
"build": "tsc -b && vite build",
|
|
51
|
+
"generate": "node --env-file=.env ./scripts/generate-graphql.mjs",
|
|
52
|
+
"lint": "eslint .",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@tailor-platform/app-shell": "...",
|
|
57
|
+
"gql.tada": "...",
|
|
58
|
+
"urql": "...",
|
|
59
|
+
"react-hook-form": "...",
|
|
60
|
+
"@hookform/resolvers": "...",
|
|
61
|
+
"zod": "...",
|
|
62
|
+
"tailwindcss": "...",
|
|
63
|
+
"lucide-react": "..."
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@tailor-platform/app-shell-vite-plugin": "...",
|
|
67
|
+
"@tailor-platform/sdk": "...",
|
|
68
|
+
"@tailwindcss/vite": "...",
|
|
69
|
+
"vite": "...",
|
|
70
|
+
"typescript": "..."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`scripts/generate-graphql.mjs` — fetches the GraphQL schema from the deployed backend:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { execSync } from "node:child_process";
|
|
79
|
+
const url = process.env.VITE_TAILOR_APP_URL;
|
|
80
|
+
execSync(`pnpm gql-tada generate schema "${url}/query"`);
|
|
81
|
+
execSync("pnpm gql-tada generate output");
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Before deployment, create empty placeholders for `src/graphql/generated/schema.graphql` and `graphql-env.d.ts` so the project compiles. After backend deployment, configure `.env` with `VITE_TAILOR_APP_URL` and `VITE_TAILOR_CLIENT_ID`, then run `pnpm generate` to regenerate them.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Frontend Pages
|
|
89
|
+
|
|
90
|
+
### GraphQL Setup
|
|
91
|
+
|
|
92
|
+
All operations use gql.tada for type safety:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { graphql, type FragmentOf, readFragment } from "@/graphql";
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Built-in queries (`gqlOperations: "query"`) generate:
|
|
99
|
+
|
|
100
|
+
- `<Entity>(id: ID!)` — get by ID
|
|
101
|
+
- `<entities>` — list with connection pattern (`edges { node { ... } }`)
|
|
102
|
+
|
|
103
|
+
Mutations are defined by custom resolvers.
|
|
104
|
+
|
|
105
|
+
### ListView
|
|
106
|
+
|
|
107
|
+
Key points:
|
|
108
|
+
|
|
109
|
+
- Query uses connection pattern: `edges { node { ...Fragment } }`
|
|
110
|
+
- Two-level fragments: row fragment for items, table fragment for the connection
|
|
111
|
+
- `EmptyState` when `edges.length === 0`
|
|
112
|
+
- Columns map to screen spec's "Required Columns"
|
|
113
|
+
- `Layout` with title and create action button
|
|
114
|
+
|
|
115
|
+
### DetailView
|
|
116
|
+
|
|
117
|
+
Key points:
|
|
118
|
+
|
|
119
|
+
- Two-column `Layout`: detail on left, actions on right
|
|
120
|
+
- `useParams()` to get entity ID from route
|
|
121
|
+
- `DescriptionCard` from `@tailor-platform/app-shell` for key-value fields:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<DescriptionCard
|
|
125
|
+
data={resource}
|
|
126
|
+
title="Overview"
|
|
127
|
+
columns={3}
|
|
128
|
+
fields={[
|
|
129
|
+
{ key: "name", label: "Name", meta: { copyable: true } },
|
|
130
|
+
{
|
|
131
|
+
key: "status",
|
|
132
|
+
label: "Status",
|
|
133
|
+
type: "badge",
|
|
134
|
+
meta: { badgeVariantMap: { ACTIVE: "success" } },
|
|
135
|
+
},
|
|
136
|
+
{ key: "createdAt", label: "Created At", type: "date", meta: { dateFormat: "medium" } },
|
|
137
|
+
]}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
|
|
142
|
+
|
|
143
|
+
### Form Pattern
|
|
144
|
+
|
|
145
|
+
Create and edit forms share the same structure:
|
|
146
|
+
|
|
147
|
+
- **React Hook Form + Zod** for validation with `zodResolver`
|
|
148
|
+
- **urql `useMutation`** for GraphQL mutations
|
|
149
|
+
- **Navigate `".."`** to go back to list after success
|
|
150
|
+
- Edit form pre-fills `defaultValues` from existing entity via fragment
|
|
151
|
+
|
|
152
|
+
#### Field Type Mapping
|
|
153
|
+
|
|
154
|
+
| Field Type | Component | Zod Schema |
|
|
155
|
+
| ---------- | ------------------------- | ------------------------------- |
|
|
156
|
+
| Text | `<Input />` | `z.string()` |
|
|
157
|
+
| Textarea | `<textarea />` | `z.string()` |
|
|
158
|
+
| Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
|
|
159
|
+
| Date | `<Input type="date" />` | `z.string()` (ISO format) |
|
|
160
|
+
| Number | `<Input type="number" />` | `z.coerce.number()` |
|
|
161
|
+
| Checkbox | `<Checkbox />` | `z.boolean()` |
|
|
162
|
+
|
|
163
|
+
#### Validation
|
|
164
|
+
|
|
165
|
+
- **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
|
|
166
|
+
- **Required: No** → `.optional()`
|
|
167
|
+
|
|
168
|
+
### Shared Component Patterns
|
|
169
|
+
|
|
170
|
+
#### Fragment Collocation
|
|
171
|
+
|
|
172
|
+
Each component defines and exports its own GraphQL fragment. The parent page imports it and includes it in the query:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// components/user-card.tsx
|
|
176
|
+
export const UserCardFragment = graphql(`
|
|
177
|
+
fragment UserCard on User {
|
|
178
|
+
id
|
|
179
|
+
name
|
|
180
|
+
email
|
|
181
|
+
}
|
|
182
|
+
`);
|
|
183
|
+
|
|
184
|
+
export const UserCard = ({ user }: { user: FragmentOf<typeof UserCardFragment> }) => {
|
|
185
|
+
const data = readFragment(UserCardFragment, user);
|
|
186
|
+
return <div>{data.name}</div>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// page.tsx
|
|
190
|
+
const UserQuery = graphql(
|
|
191
|
+
`
|
|
192
|
+
query User($id: ID!) {
|
|
193
|
+
user(id: $id) {
|
|
194
|
+
...UserCard
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`,
|
|
198
|
+
[UserCardFragment],
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### App.tsx
|
|
203
|
+
|
|
204
|
+
- `AuthGuard` is not exported from app-shell — implement it yourself, pass via `guardComponent` prop
|
|
205
|
+
- `GraphQLProvider` needs `authClient` prop to attach DPoP auth headers
|
|
206
|
+
- Required env vars: `VITE_TAILOR_APP_URL`, `VITE_TAILOR_CLIENT_ID`
|
|
207
|
+
- Auth client: create once in `src/lib/auth-client.ts` using `createAuthClient({ appUri, clientId })`
|
|
208
|
+
- Sidebar: use `SidebarGroup` + `SidebarItem` for custom ordering
|
|
209
|
+
|
|
210
|
+
#### Module-level page.tsx (required)
|
|
211
|
+
|
|
212
|
+
Every module directory must have a `page.tsx` that redirects to the first child. Use **absolute paths** to avoid double-path bugs:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
void navigate("/item-management/item", { replace: true });
|
|
217
|
+
}, [navigate]);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Page conventions
|
|
221
|
+
|
|
222
|
+
Every page component must:
|
|
223
|
+
|
|
224
|
+
1. Be the **default export** of `page.tsx`
|
|
225
|
+
2. Set `appShellPageProps` with at least `meta.title`
|
|
226
|
+
|
|
227
|
+
#### Common imports
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import {
|
|
231
|
+
Layout,
|
|
232
|
+
Link,
|
|
233
|
+
useParams,
|
|
234
|
+
useNavigate,
|
|
235
|
+
type AppShellPageProps,
|
|
236
|
+
} from "@tailor-platform/app-shell";
|
|
237
|
+
import { useQuery, useMutation } from "urql";
|
|
238
|
+
import { graphql, type FragmentOf, readFragment } from "@/graphql";
|
|
239
|
+
import { useForm } from "react-hook-form";
|
|
240
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
241
|
+
import { z } from "zod";
|
|
242
|
+
```
|
package/src/module.ts
CHANGED
|
@@ -85,3 +85,41 @@ export { type RevokePermissionFromRoleInput } from "./modules/user-management/co
|
|
|
85
85
|
export { type AssignRoleToUserInput } from "./modules/user-management/command/assignRoleToUser";
|
|
86
86
|
export { type RevokeRoleFromUserInput } from "./modules/user-management/command/revokeRoleFromUser";
|
|
87
87
|
export { type LogAuditEventInput } from "./modules/user-management/command/logAuditEvent";
|
|
88
|
+
// item-management
|
|
89
|
+
export { defineModule as defineItemManagementModule } from "./modules/item-management/module";
|
|
90
|
+
export {
|
|
91
|
+
permissions as itemManagementPermissions,
|
|
92
|
+
own as itemManagementOwn,
|
|
93
|
+
all as itemManagementAll,
|
|
94
|
+
} from "./modules/item-management/lib/permissions.generated";
|
|
95
|
+
export { ItemStatus } from "./modules/item-management/generated/enums";
|
|
96
|
+
export {
|
|
97
|
+
ItemNotFoundError,
|
|
98
|
+
DuplicateSkuError,
|
|
99
|
+
DuplicateBarcodeError,
|
|
100
|
+
UnitNotFoundError as ItemUnitNotFoundError,
|
|
101
|
+
SkuImmutableError,
|
|
102
|
+
UomLockedError,
|
|
103
|
+
NoFieldsToUpdateError,
|
|
104
|
+
InvalidStateTransitionError,
|
|
105
|
+
DeleteNonDraftError,
|
|
106
|
+
NodeNotFoundError,
|
|
107
|
+
DuplicateNodeCodeError,
|
|
108
|
+
ParentNodeNotFoundError,
|
|
109
|
+
MaxDepthExceededError,
|
|
110
|
+
CodeImmutableError,
|
|
111
|
+
MissingRequiredFieldsError,
|
|
112
|
+
CircularReferenceError,
|
|
113
|
+
NodeHasChildrenError,
|
|
114
|
+
NodeHasAssignmentsError,
|
|
115
|
+
DuplicateAssignmentError,
|
|
116
|
+
AssignmentNotFoundError as ItemAssignmentNotFoundError,
|
|
117
|
+
} from "./modules/item-management/lib/errors.generated";
|
|
118
|
+
export { type GetItemInput } from "./modules/item-management/query/getItem";
|
|
119
|
+
export { type GetTaxonomyNodeInput } from "./modules/item-management/query/getTaxonomyNode";
|
|
120
|
+
export { type CreateItemInput } from "./modules/item-management/command/createItem";
|
|
121
|
+
export { type UpdateItemInput } from "./modules/item-management/command/updateItem";
|
|
122
|
+
export { type ActivateItemInput } from "./modules/item-management/command/activateItem";
|
|
123
|
+
export { type DeactivateItemInput } from "./modules/item-management/command/deactivateItem";
|
|
124
|
+
export { type CreateTaxonomyNodeInput } from "./modules/item-management/command/createTaxonomyNode";
|
|
125
|
+
export { type AssignItemToTaxonomyInput } from "./modules/item-management/command/assignItemToTaxonomy";
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Application Directory Structure
|
|
2
|
-
|
|
3
|
-
```
|
|
4
|
-
{app_name}/
|
|
5
|
-
├── backend/
|
|
6
|
-
│ ├── src/
|
|
7
|
-
│ │ ├── modules.ts # Declaring module usage
|
|
8
|
-
│ │ ├── modules/
|
|
9
|
-
│ │ │ └── {module-name}/ # Module-specific directory
|
|
10
|
-
│ │ │ ├── resolvers/ # API Definition to expose graphql apis
|
|
11
|
-
│ │ │ └── executors/ # PubSub Automation (one file per declaration)
|
|
12
|
-
│ │ └── generated/ # Auto-generated code (do not edit)
|
|
13
|
-
│ └── tailor.config.ts # tailor application config
|
|
14
|
-
│
|
|
15
|
-
└── frontend/
|
|
16
|
-
└── src/
|
|
17
|
-
├── pages/ # File-based routing (auto-discovered by Vite plugin)
|
|
18
|
-
│ └── {page-path}/
|
|
19
|
-
│ ├── page.tsx
|
|
20
|
-
│ └── {page-path}/
|
|
21
|
-
│ ├── components/
|
|
22
|
-
│ └── page.tsx
|
|
23
|
-
├── components/
|
|
24
|
-
│ └── ui/ # Generic UI components
|
|
25
|
-
├── graphql/ # gql.tada settings
|
|
26
|
-
└── providers/ # react providers
|
|
27
|
-
```
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# DetailView Screen Implementation
|
|
2
|
-
|
|
3
|
-
Implementation pattern for screens with `Screen Type: DetailView`.
|
|
4
|
-
Assumes `page.md` and `component.md` rules.
|
|
5
|
-
|
|
6
|
-
## File Structure
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
{screen-path}/[id]/
|
|
10
|
-
├── components/
|
|
11
|
-
│ ├── {screen-name}-detail.tsx # Main content (left column)
|
|
12
|
-
│ └── {screen-name}-actions.tsx # Action sidebar (right column)
|
|
13
|
-
├── edit/
|
|
14
|
-
│ ├── components/
|
|
15
|
-
│ │ └── edit-{screen-name}-form.tsx
|
|
16
|
-
│ └── page.tsx
|
|
17
|
-
└── page.tsx
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Layout
|
|
21
|
-
|
|
22
|
-
- Two-column layout: main content on the left, actions on the right.
|
|
23
|
-
|
|
24
|
-
```tsx
|
|
25
|
-
const ResourcePage = () => {
|
|
26
|
-
const { id } = useParams();
|
|
27
|
-
const [{ data, error, fetching }, reexecuteQuery] = useQuery({
|
|
28
|
-
query: ResourceQuery,
|
|
29
|
-
variables: { id: id! },
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (fetching) return <Loading />;
|
|
33
|
-
if (error || !data?.resource) return <ErrorFallback ... />;
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<Layout columns={2} title="Resource Detail">
|
|
37
|
-
<Layout.Column>
|
|
38
|
-
<ResourceDetail resource={data.resource} />
|
|
39
|
-
</Layout.Column>
|
|
40
|
-
<Layout.Column>
|
|
41
|
-
<ResourceActions resource={data.resource} />
|
|
42
|
-
</Layout.Column>
|
|
43
|
-
</Layout>
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Left Column: Detail Component
|
|
49
|
-
|
|
50
|
-
Stack `DescriptionCard` and related tables vertically with `space-y-6`.
|
|
51
|
-
|
|
52
|
-
- `DescriptionCard` (`@tailor-platform/app-shell`): renders key-value fields declaratively.
|
|
53
|
-
- Complex content (tables, timelines): wrap in `<div className="rounded-lg border bg-card p-6">`.
|
|
54
|
-
|
|
55
|
-
### DescriptionCard
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
<DescriptionCard
|
|
59
|
-
data={resource}
|
|
60
|
-
title="Overview"
|
|
61
|
-
columns={3}
|
|
62
|
-
fields={[
|
|
63
|
-
{ key: "name", label: "Name", meta: { copyable: true } },
|
|
64
|
-
{
|
|
65
|
-
key: "status",
|
|
66
|
-
label: "Status",
|
|
67
|
-
type: "badge",
|
|
68
|
-
meta: { badgeVariantMap: { ACTIVE: "success", PENDING: "warning" } },
|
|
69
|
-
},
|
|
70
|
-
{ type: "divider" },
|
|
71
|
-
{
|
|
72
|
-
key: "createdAt",
|
|
73
|
-
label: "Created At",
|
|
74
|
-
type: "date",
|
|
75
|
-
meta: { dateFormat: "medium" },
|
|
76
|
-
},
|
|
77
|
-
]}
|
|
78
|
-
/>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
|
|
82
|
-
|
|
83
|
-
## Right Column: Actions Component
|
|
84
|
-
|
|
85
|
-
Wrap in a `Card` component. Use `Button variant="ghost"` for each action item.
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
<Card>
|
|
89
|
-
<CardHeader>
|
|
90
|
-
<CardTitle>Actions</CardTitle>
|
|
91
|
-
</CardHeader>
|
|
92
|
-
<CardContent className="space-y-2">
|
|
93
|
-
<Button variant="ghost" className="w-full justify-start gap-2" asChild>
|
|
94
|
-
<Link to="edit">✎ Edit</Link>
|
|
95
|
-
</Button>
|
|
96
|
-
<Button variant="ghost" className="w-full justify-start gap-2" onClick={handler}>
|
|
97
|
-
✓ Approve
|
|
98
|
-
</Button>
|
|
99
|
-
</CardContent>
|
|
100
|
-
</Card>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- Navigation: `<Button variant="ghost" asChild><Link to="...">`
|
|
104
|
-
- Mutation: `<Button variant="ghost" onClick={handler}>` with custom resolvers (see `backend/resolvers.md`)
|
|
105
|
-
- Conditional: show/hide based on status
|
|
106
|
-
- Multiple cards: stack with `<div className="space-y-6">`
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# Form Screen Implementation
|
|
2
|
-
|
|
3
|
-
Implementation pattern for screens with `Screen Type: Form`.
|
|
4
|
-
Assumes `page.md` and `component.md` rules.
|
|
5
|
-
|
|
6
|
-
## File Structure
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
{screen-path}/
|
|
10
|
-
├── components/
|
|
11
|
-
│ └── {screen-name}-form.tsx # Form component with validation
|
|
12
|
-
└── page.tsx
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Page Component (page.tsx)
|
|
16
|
-
|
|
17
|
-
Form pages delegate mutation logic to the form component.
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
const ScreenNamePage = () => (
|
|
21
|
-
<Layout columns={1} title="Screen Title">
|
|
22
|
-
<Layout.Column>
|
|
23
|
-
<ScreenNameForm />
|
|
24
|
-
</Layout.Column>
|
|
25
|
-
</Layout>
|
|
26
|
-
);
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
For edit forms that need existing data, co-locate data fetching in the page component:
|
|
30
|
-
|
|
31
|
-
```tsx
|
|
32
|
-
const EditPage = () => {
|
|
33
|
-
const { id } = useParams();
|
|
34
|
-
const [{ data, error, fetching }] = useQuery({
|
|
35
|
-
query: ResourceQuery,
|
|
36
|
-
variables: { id: id! },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
if (fetching) return <Loading />;
|
|
40
|
-
if (error || !data?.resource) return <ErrorFallback ... />;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Layout columns={1} title="Edit Resource">
|
|
44
|
-
<Layout.Column>
|
|
45
|
-
<EditResourceForm resource={data.resource} />
|
|
46
|
-
</Layout.Column>
|
|
47
|
-
</Layout>
|
|
48
|
-
);
|
|
49
|
-
};
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Form Component (components/{screen-name}-form.tsx)
|
|
53
|
-
|
|
54
|
-
### Technology Stack
|
|
55
|
-
|
|
56
|
-
- `react-hook-form` — form state management
|
|
57
|
-
- `zod` + `@hookform/resolvers/zod` — validation
|
|
58
|
-
- `useMutation` (urql) — GraphQL mutation
|
|
59
|
-
- `useNavigate` (@tailor-platform/app-shell) — post-submit navigation
|
|
60
|
-
|
|
61
|
-
### Pattern
|
|
62
|
-
|
|
63
|
-
```tsx
|
|
64
|
-
const formSchema = z.object({
|
|
65
|
-
title: z.string().min(1, "Title is required"),
|
|
66
|
-
description: z.string().optional(),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
type FormValues = z.infer<typeof formSchema>;
|
|
70
|
-
|
|
71
|
-
export const ScreenNameForm = () => {
|
|
72
|
-
const navigate = useNavigate();
|
|
73
|
-
const [, createResource] = useMutation(CreateMutation);
|
|
74
|
-
|
|
75
|
-
const form = useForm<FormValues>({
|
|
76
|
-
resolver: zodResolver(formSchema),
|
|
77
|
-
defaultValues: { title: "", description: "" },
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const onSubmit = (values: FormValues) => {
|
|
81
|
-
void createResource({ input: values }).then((result) => {
|
|
82
|
-
if (!result.error) {
|
|
83
|
-
void navigate("..");
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<Form {...form}>
|
|
90
|
-
<form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)} className="max-w-md space-y-4">
|
|
91
|
-
<FormField
|
|
92
|
-
control={form.control}
|
|
93
|
-
name="title"
|
|
94
|
-
render={({ field }) => (
|
|
95
|
-
<FormItem>
|
|
96
|
-
<FormLabel>Title</FormLabel>
|
|
97
|
-
<FormControl>
|
|
98
|
-
<Input placeholder="Enter title" {...field} />
|
|
99
|
-
</FormControl>
|
|
100
|
-
<FormMessage />
|
|
101
|
-
</FormItem>
|
|
102
|
-
)}
|
|
103
|
-
/>
|
|
104
|
-
<div className="flex gap-2">
|
|
105
|
-
<Button type="submit">Create</Button>
|
|
106
|
-
<Button type="button" variant="outline" onClick={() => void navigate("..")}>
|
|
107
|
-
Cancel
|
|
108
|
-
</Button>
|
|
109
|
-
</div>
|
|
110
|
-
</form>
|
|
111
|
-
</Form>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Field Type Mapping
|
|
117
|
-
|
|
118
|
-
| Field Type | Component | Zod Schema |
|
|
119
|
-
| ---------- | ------------------------------ | ------------------------------- |
|
|
120
|
-
| Text | `<Input />` | `z.string()` |
|
|
121
|
-
| Textarea | `<textarea className="..." />` | `z.string()` |
|
|
122
|
-
| Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
|
|
123
|
-
| Date | `<Input type="date" />` | `z.string()` (ISO format) |
|
|
124
|
-
| Number | `<Input type="number" />` | `z.coerce.number()` |
|
|
125
|
-
| Email | `<Input type="email" />` | `z.string().email()` |
|
|
126
|
-
| Checkbox | `<Checkbox />` | `z.boolean()` |
|
|
127
|
-
| Radio | `<RadioGroup />` | `z.enum([...])` |
|
|
128
|
-
|
|
129
|
-
## Validation Mapping
|
|
130
|
-
|
|
131
|
-
- **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
|
|
132
|
-
- **Required: No** → `.optional()`
|
|
133
|
-
|
|
134
|
-
## Key Points
|
|
135
|
-
|
|
136
|
-
- Set `defaultValues` for all fields (empty string, false, etc.)
|
|
137
|
-
- Navigate to `".."` after successful mutation
|
|
138
|
-
- Cancel button must use `type="button"` to prevent form submit
|
|
139
|
-
- For edit forms, accept fragment data as props and pre-fill `defaultValues`
|