@famgia/omnify-laravel 0.0.88 → 0.0.89
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/dist/{chunk-YVVAJA3T.js → chunk-V7LWJ6OM.js} +178 -12
- package/dist/chunk-V7LWJ6OM.js.map +1 -0
- package/dist/index.cjs +180 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +5 -1
- package/dist/plugin.cjs +176 -11
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +5 -5
- package/scripts/postinstall.js +29 -36
- package/stubs/ai-guides/claude-agents/architect.md.stub +150 -0
- package/stubs/ai-guides/claude-agents/developer.md.stub +190 -0
- package/stubs/ai-guides/claude-agents/reviewer.md.stub +134 -0
- package/stubs/ai-guides/claude-agents/tester.md.stub +196 -0
- package/stubs/ai-guides/claude-checklists/backend.md.stub +112 -0
- package/stubs/ai-guides/claude-omnify/antdesign-guide.md.stub +401 -0
- package/stubs/ai-guides/claude-omnify/config-guide.md.stub +253 -0
- package/stubs/ai-guides/claude-omnify/japan-guide.md.stub +186 -0
- package/stubs/ai-guides/claude-omnify/laravel-guide.md.stub +61 -0
- package/stubs/ai-guides/claude-omnify/schema-guide.md.stub +115 -0
- package/stubs/ai-guides/claude-omnify/typescript-guide.md.stub +310 -0
- package/stubs/ai-guides/claude-rules/naming.md.stub +364 -0
- package/stubs/ai-guides/claude-rules/performance.md.stub +251 -0
- package/stubs/ai-guides/claude-rules/security.md.stub +159 -0
- package/stubs/ai-guides/claude-workflows/bug-fix.md.stub +201 -0
- package/stubs/ai-guides/claude-workflows/code-review.md.stub +164 -0
- package/stubs/ai-guides/claude-workflows/new-feature.md.stub +327 -0
- package/stubs/ai-guides/cursor/laravel-controller.mdc.stub +391 -0
- package/stubs/ai-guides/cursor/laravel-request.mdc.stub +112 -0
- package/stubs/ai-guides/cursor/laravel-resource.mdc.stub +73 -0
- package/stubs/ai-guides/cursor/laravel-review.mdc.stub +69 -0
- package/stubs/ai-guides/cursor/laravel-testing.mdc.stub +138 -0
- package/stubs/ai-guides/cursor/laravel.mdc.stub +82 -0
- package/stubs/ai-guides/laravel/README.md.stub +59 -0
- package/stubs/ai-guides/laravel/architecture.md.stub +424 -0
- package/stubs/ai-guides/laravel/controller.md.stub +484 -0
- package/stubs/ai-guides/laravel/datetime.md.stub +334 -0
- package/stubs/ai-guides/laravel/openapi.md.stub +369 -0
- package/stubs/ai-guides/laravel/request.md.stub +450 -0
- package/stubs/ai-guides/laravel/resource.md.stub +516 -0
- package/stubs/ai-guides/laravel/service.md.stub +503 -0
- package/stubs/ai-guides/laravel/testing.md.stub +1504 -0
- package/ai-guides/laravel-guide.md +0 -461
- package/dist/chunk-YVVAJA3T.js.map +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Tester Agent
|
|
2
|
+
|
|
3
|
+
> Agent for writing tests and ensuring test coverage.
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
|
|
7
|
+
**Testing Specialist** - Writes comprehensive tests covering all scenarios.
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- Writing tests for new features
|
|
12
|
+
- Adding missing test coverage
|
|
13
|
+
- Verifying test completeness
|
|
14
|
+
- Test debugging
|
|
15
|
+
|
|
16
|
+
## Persona
|
|
17
|
+
|
|
18
|
+
### Style
|
|
19
|
+
|
|
20
|
+
- Thorough and systematic
|
|
21
|
+
- Edge-case aware
|
|
22
|
+
- Clear test naming
|
|
23
|
+
- Real-world scenarios
|
|
24
|
+
|
|
25
|
+
### Core Principles
|
|
26
|
+
|
|
27
|
+
1. **正常系 + 異常系**: Cover both success and failure paths
|
|
28
|
+
2. **API-First Data**: Create test data via API when possible
|
|
29
|
+
3. **PEST Syntax**: Use PEST, not PHPUnit
|
|
30
|
+
4. **Descriptive Names**: `正常:` and `異常:` prefixes
|
|
31
|
+
5. **No Bias**: Test real behavior, not implementation
|
|
32
|
+
|
|
33
|
+
## Context to Read
|
|
34
|
+
|
|
35
|
+
Before writing tests, read these:
|
|
36
|
+
|
|
37
|
+
| Priority | File | Purpose |
|
|
38
|
+
| ------------- | ---------------------------------------------------------- | ----------------------- |
|
|
39
|
+
| **Required** | [/guides/laravel/testing.md](../guides/laravel/testing.md) | Full testing guide |
|
|
40
|
+
| **Required** | [/rules/naming.md](../rules/naming.md) | Test naming conventions |
|
|
41
|
+
| **Reference** | [/checklists/backend.md](../checklists/backend.md) | Test checklist |
|
|
42
|
+
|
|
43
|
+
## Test Coverage Matrix
|
|
44
|
+
|
|
45
|
+
### Per Endpoint
|
|
46
|
+
|
|
47
|
+
| Endpoint | 正常系 (Normal) | 異常系 (Abnormal) |
|
|
48
|
+
| ----------- | ---------------------------- | ---------------------------- |
|
|
49
|
+
| **index** | List, filter, sort, paginate | Empty result, invalid params |
|
|
50
|
+
| **store** | Creates → 201 | 422 (each field), duplicate |
|
|
51
|
+
| **show** | Returns → 200 | 404 not found |
|
|
52
|
+
| **update** | Full update, partial | 404, 422 |
|
|
53
|
+
| **destroy** | Deletes → 204 | 404 |
|
|
54
|
+
|
|
55
|
+
### Auth Endpoints (if protected)
|
|
56
|
+
|
|
57
|
+
| Scenario | Expected |
|
|
58
|
+
| ------------- | -------- |
|
|
59
|
+
| No token | 401 |
|
|
60
|
+
| Invalid token | 401 |
|
|
61
|
+
| No permission | 403 |
|
|
62
|
+
|
|
63
|
+
### Japanese Field Validation
|
|
64
|
+
|
|
65
|
+
| Field | Valid | Invalid |
|
|
66
|
+
| ------------- | ------------ | ---------------- |
|
|
67
|
+
| `name_kana_*` | カタカナ | hiragana, romaji |
|
|
68
|
+
| Max length | Within limit | Exceeds limit |
|
|
69
|
+
|
|
70
|
+
## Test Naming Convention
|
|
71
|
+
|
|
72
|
+
```php
|
|
73
|
+
// 正常系 (Normal cases) - success behavior
|
|
74
|
+
it('正常: returns paginated users')
|
|
75
|
+
it('正常: creates user with valid data')
|
|
76
|
+
it('正常: updates user with partial data')
|
|
77
|
+
|
|
78
|
+
// 異常系 (Abnormal cases) - failure behavior
|
|
79
|
+
it('異常: fails to create user with missing email')
|
|
80
|
+
it('異常: fails to create user with invalid kana format')
|
|
81
|
+
it('異常: returns 404 when user not found')
|
|
82
|
+
it('異常: returns 401 when not authenticated')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Test Template
|
|
86
|
+
|
|
87
|
+
```php
|
|
88
|
+
describe('POST /api/users', function () {
|
|
89
|
+
// ================================================================
|
|
90
|
+
// 正常系 (Normal Cases)
|
|
91
|
+
// ================================================================
|
|
92
|
+
|
|
93
|
+
it('正常: creates user with valid data', function () {
|
|
94
|
+
$data = [
|
|
95
|
+
'name_lastname' => '田中',
|
|
96
|
+
'name_firstname' => '太郎',
|
|
97
|
+
'name_kana_lastname' => 'タナカ',
|
|
98
|
+
'name_kana_firstname' => 'タロウ',
|
|
99
|
+
'email' => 'test@example.com',
|
|
100
|
+
'password' => 'password123',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
$response = $this->postJson('/api/users', $data);
|
|
104
|
+
|
|
105
|
+
$response->assertCreated()
|
|
106
|
+
->assertJsonPath('data.email', 'test@example.com');
|
|
107
|
+
|
|
108
|
+
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ================================================================
|
|
112
|
+
// 異常系 (Abnormal Cases)
|
|
113
|
+
// ================================================================
|
|
114
|
+
|
|
115
|
+
// Required fields
|
|
116
|
+
it('異常: fails to create user with missing email', function () {
|
|
117
|
+
$data = validUserData();
|
|
118
|
+
unset($data['email']);
|
|
119
|
+
|
|
120
|
+
$this->postJson('/api/users', $data)
|
|
121
|
+
->assertUnprocessable()
|
|
122
|
+
->assertJsonValidationErrors(['email']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Format validation
|
|
126
|
+
it('異常: fails to create user with invalid email format', function () {
|
|
127
|
+
$data = validUserData(['email' => 'not-an-email']);
|
|
128
|
+
|
|
129
|
+
$this->postJson('/api/users', $data)
|
|
130
|
+
->assertUnprocessable()
|
|
131
|
+
->assertJsonValidationErrors(['email']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Japanese field validation
|
|
135
|
+
it('異常: fails to create user with invalid kana format', function () {
|
|
136
|
+
$data = validUserData(['name_kana_lastname' => 'たなか']); // hiragana
|
|
137
|
+
|
|
138
|
+
$this->postJson('/api/users', $data)
|
|
139
|
+
->assertUnprocessable()
|
|
140
|
+
->assertJsonValidationErrors(['name_kana_lastname']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Unique constraint
|
|
144
|
+
it('異常: fails to create user with duplicate email', function () {
|
|
145
|
+
User::factory()->create(['email' => 'existing@example.com']);
|
|
146
|
+
$data = validUserData(['email' => 'existing@example.com']);
|
|
147
|
+
|
|
148
|
+
$this->postJson('/api/users', $data)
|
|
149
|
+
->assertUnprocessable()
|
|
150
|
+
->assertJsonValidationErrors(['email']);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('GET /api/users/{id}', function () {
|
|
155
|
+
it('正常: returns user by id', function () {
|
|
156
|
+
$user = User::factory()->create();
|
|
157
|
+
|
|
158
|
+
$this->getJson("/api/users/{$user->id}")
|
|
159
|
+
->assertOk()
|
|
160
|
+
->assertJsonPath('data.id', $user->id);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('異常: returns 404 when user not found', function () {
|
|
164
|
+
$this->getJson('/api/users/99999')
|
|
165
|
+
->assertNotFound();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Debugging Failed Tests
|
|
171
|
+
|
|
172
|
+
```mermaid
|
|
173
|
+
flowchart TD
|
|
174
|
+
Fail[Test Failed] --> Check1{Endpoint correct?}
|
|
175
|
+
Check1 -->|No| FixEndpoint[Fix URL/method]
|
|
176
|
+
Check1 -->|Yes| Check2{Data valid?}
|
|
177
|
+
Check2 -->|No| FixData[Fix test data]
|
|
178
|
+
Check2 -->|Yes| Check3{Assertion correct?}
|
|
179
|
+
Check3 -->|No| FixAssert[Fix assertion]
|
|
180
|
+
Check3 -->|Yes| CodeBug[Code has bug - fix code]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Example Interaction
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
User: Write tests for OrderController
|
|
187
|
+
|
|
188
|
+
Tester Agent:
|
|
189
|
+
1. Read /guides/laravel/testing.md
|
|
190
|
+
2. Identify endpoints (index, store, show, update, destroy)
|
|
191
|
+
3. For each endpoint:
|
|
192
|
+
- Write 正常系 tests
|
|
193
|
+
- Write 異常系 tests (validation, 404, 401, 403)
|
|
194
|
+
4. Add Japanese field tests if applicable
|
|
195
|
+
5. Output complete test file with describe/it blocks
|
|
196
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Backend Checklist
|
|
2
|
+
|
|
3
|
+
> Quick reference. For details, see linked guides.
|
|
4
|
+
|
|
5
|
+
## New Resource
|
|
6
|
+
|
|
7
|
+
| Step | Action | Guide |
|
|
8
|
+
| ---- | --------------------- | ----------------------------------------------------------- |
|
|
9
|
+
| 1 | Create schema | [guides/omnify/schema-guide.md](../guides/omnify/schema-guide.md) |
|
|
10
|
+
| 2 | `npx omnify generate` | |
|
|
11
|
+
| 3 | `./artisan migrate` | |
|
|
12
|
+
| 4 | Model (extend base) | [guides/laravel/README.md](../guides/laravel/README.md) |
|
|
13
|
+
| 5 | Controller (thin) | |
|
|
14
|
+
| 6 | Resource | |
|
|
15
|
+
| 7 | Routes | |
|
|
16
|
+
| 8 | **Tests** | [guides/laravel/testing.md](../guides/laravel/testing.md) |
|
|
17
|
+
| 9 | `./artisan test` | |
|
|
18
|
+
| 10 | OpenAPI | [guides/laravel/openapi.md](../guides/laravel/openapi.md) |
|
|
19
|
+
|
|
20
|
+
> **Full workflow**: [workflows/new-feature.md](../workflows/new-feature.md)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Before Commit
|
|
25
|
+
|
|
26
|
+
### Code
|
|
27
|
+
- [ ] No `dd()`, `dump()`, `console.log`
|
|
28
|
+
- [ ] No commented-out code
|
|
29
|
+
- [ ] Type hints on methods
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
- [ ] `$fillable` defined in Model
|
|
33
|
+
- [ ] `$request->validated()` (not `all()`)
|
|
34
|
+
- [ ] Sensitive data in `$hidden`
|
|
35
|
+
|
|
36
|
+
### API
|
|
37
|
+
- [ ] Dates use `->toISOString()`
|
|
38
|
+
- [ ] Return Resource (not Model)
|
|
39
|
+
- [ ] Proper HTTP status codes
|
|
40
|
+
|
|
41
|
+
### Performance
|
|
42
|
+
- [ ] `with()` for relations
|
|
43
|
+
- [ ] `whenLoaded()` in Resources
|
|
44
|
+
- [ ] `paginate()` for lists
|
|
45
|
+
|
|
46
|
+
> **Details**: [rules/](../rules/)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
### Coverage Required
|
|
53
|
+
|
|
54
|
+
| Endpoint | 正常系 | 異常系 |
|
|
55
|
+
| ----------- | ------------------ | --------------------- |
|
|
56
|
+
| **index** | List, filter, sort | Empty, invalid params |
|
|
57
|
+
| **store** | Creates → 201 | 422 (validation) |
|
|
58
|
+
| **show** | Returns → 200 | 404 |
|
|
59
|
+
| **update** | Updates → 200 | 404, 422 |
|
|
60
|
+
| **destroy** | Deletes → 204 | 404 |
|
|
61
|
+
|
|
62
|
+
### Auth Tests (if protected)
|
|
63
|
+
- 401: Not authenticated
|
|
64
|
+
- 403: Not authorized
|
|
65
|
+
|
|
66
|
+
> **Full guide**: [guides/laravel/testing.md](../guides/laravel/testing.md)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## OpenAPI
|
|
71
|
+
|
|
72
|
+
1. Check `OmnifyBase/*RequestBase.php` for request fields
|
|
73
|
+
2. Check `OmnifyBase/*ResourceBase.php` for response fields
|
|
74
|
+
3. Use `$ref` from `Schemas.php`
|
|
75
|
+
4. Run `./artisan l5-swagger:generate`
|
|
76
|
+
|
|
77
|
+
> **Full guide**: [guides/laravel/openapi.md](../guides/laravel/openapi.md)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Controller Methods
|
|
82
|
+
|
|
83
|
+
### index
|
|
84
|
+
```php
|
|
85
|
+
return UserResource::collection(
|
|
86
|
+
User::with('relation')
|
|
87
|
+
->when($request->search, fn($q, $s) => $q->where('name', 'like', "%{$s}%"))
|
|
88
|
+
->paginate($request->input('per_page', 15))
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### store
|
|
93
|
+
```php
|
|
94
|
+
return new UserResource(User::create($request->validated()));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### show
|
|
98
|
+
```php
|
|
99
|
+
return new UserResource($user->load('relation'));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### update
|
|
103
|
+
```php
|
|
104
|
+
$user->update($request->validated());
|
|
105
|
+
return new UserResource($user);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### destroy
|
|
109
|
+
```php
|
|
110
|
+
$user->delete();
|
|
111
|
+
return response()->noContent();
|
|
112
|
+
```
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Omnify + Ant Design Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to use Omnify-generated validation rules with Ant Design Forms.
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
Omnify generates validation rules in `rules/` directory:
|
|
8
|
+
- `rules/{Model}.rules.ts` - Validation rules for each model
|
|
9
|
+
|
|
10
|
+
## File Structure
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// rules/User.rules.ts
|
|
14
|
+
|
|
15
|
+
// Display name for model (multi-locale)
|
|
16
|
+
export const UserDisplayName: LocaleMap = {
|
|
17
|
+
ja: 'ユーザー',
|
|
18
|
+
en: 'User',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Display names for properties (multi-locale)
|
|
22
|
+
export const UserPropertyDisplayNames: Record<string, LocaleMap> = {
|
|
23
|
+
name: { ja: '名前', en: 'Name' },
|
|
24
|
+
email: { ja: 'メールアドレス', en: 'Email' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Validation rules with multi-locale messages
|
|
28
|
+
export const UserRules: Record<string, ValidationRule[]> = {
|
|
29
|
+
name: [
|
|
30
|
+
{ required: true, message: { ja: '名前は必須です', en: 'Name is required' } },
|
|
31
|
+
{ max: 100, message: { ja: '名前は100文字以内で入力してください', en: 'Name must be at most 100 characters' } },
|
|
32
|
+
],
|
|
33
|
+
email: [
|
|
34
|
+
{ required: true, message: { ja: 'メールアドレスは必須です', en: 'Email is required' } },
|
|
35
|
+
{ type: 'email', message: { ja: 'メールアドレスの形式が正しくありません', en: 'Email is not a valid email address' } },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Helper functions
|
|
40
|
+
export function getUserRules(locale: string): Record<string, Rule[]>;
|
|
41
|
+
export function getUserDisplayName(locale: string): string;
|
|
42
|
+
export function getUserPropertyDisplayName(property: string, locale: string): string;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Basic Usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Form, Input, Button } from 'antd';
|
|
49
|
+
import {
|
|
50
|
+
getUserRules,
|
|
51
|
+
getUserDisplayName,
|
|
52
|
+
getUserPropertyDisplayName
|
|
53
|
+
} from '@/types/model/rules/User.rules';
|
|
54
|
+
|
|
55
|
+
interface UserFormProps {
|
|
56
|
+
locale?: string;
|
|
57
|
+
onSubmit: (values: User) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function UserForm({ locale = 'ja', onSubmit }: UserFormProps) {
|
|
61
|
+
const [form] = Form.useForm();
|
|
62
|
+
const rules = getUserRules(locale);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Form
|
|
66
|
+
form={form}
|
|
67
|
+
layout="vertical"
|
|
68
|
+
onFinish={onSubmit}
|
|
69
|
+
>
|
|
70
|
+
<Form.Item
|
|
71
|
+
name="name"
|
|
72
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
73
|
+
rules={rules.name}
|
|
74
|
+
>
|
|
75
|
+
<Input placeholder={getUserPropertyDisplayName('name', locale)} />
|
|
76
|
+
</Form.Item>
|
|
77
|
+
|
|
78
|
+
<Form.Item
|
|
79
|
+
name="email"
|
|
80
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
81
|
+
rules={rules.email}
|
|
82
|
+
>
|
|
83
|
+
<Input type="email" />
|
|
84
|
+
</Form.Item>
|
|
85
|
+
|
|
86
|
+
<Form.Item>
|
|
87
|
+
<Button type="primary" htmlType="submit">
|
|
88
|
+
{locale === 'ja' ? '送信' : 'Submit'}
|
|
89
|
+
</Button>
|
|
90
|
+
</Form.Item>
|
|
91
|
+
</Form>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## With Edit Mode (Initial Values)
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Form, Input, Button, Spin } from 'antd';
|
|
100
|
+
import { getUserRules, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
101
|
+
import { User } from '@/types/model';
|
|
102
|
+
|
|
103
|
+
interface UserEditFormProps {
|
|
104
|
+
user: User;
|
|
105
|
+
locale?: string;
|
|
106
|
+
onSubmit: (values: Partial<User>) => void;
|
|
107
|
+
loading?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function UserEditForm({ user, locale = 'ja', onSubmit, loading }: UserEditFormProps) {
|
|
111
|
+
const [form] = Form.useForm();
|
|
112
|
+
const rules = getUserRules(locale);
|
|
113
|
+
|
|
114
|
+
// Set initial values when user data changes
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
form.setFieldsValue(user);
|
|
117
|
+
}, [user, form]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Spin spinning={loading}>
|
|
121
|
+
<Form
|
|
122
|
+
form={form}
|
|
123
|
+
layout="vertical"
|
|
124
|
+
initialValues={user}
|
|
125
|
+
onFinish={onSubmit}
|
|
126
|
+
>
|
|
127
|
+
<Form.Item
|
|
128
|
+
name="name"
|
|
129
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
130
|
+
rules={rules.name}
|
|
131
|
+
>
|
|
132
|
+
<Input />
|
|
133
|
+
</Form.Item>
|
|
134
|
+
|
|
135
|
+
<Form.Item
|
|
136
|
+
name="email"
|
|
137
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
138
|
+
rules={rules.email}
|
|
139
|
+
>
|
|
140
|
+
<Input type="email" />
|
|
141
|
+
</Form.Item>
|
|
142
|
+
|
|
143
|
+
<Form.Item>
|
|
144
|
+
<Button type="primary" htmlType="submit" loading={loading}>
|
|
145
|
+
{locale === 'ja' ? '更新' : 'Update'}
|
|
146
|
+
</Button>
|
|
147
|
+
</Form.Item>
|
|
148
|
+
</Form>
|
|
149
|
+
</Spin>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Dynamic Locale Switching
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Form, Input, Select } from 'antd';
|
|
158
|
+
import { useState, useMemo } from 'react';
|
|
159
|
+
import { getUserRules, getUserPropertyDisplayName, getUserDisplayName } from '@/types/model/rules/User.rules';
|
|
160
|
+
|
|
161
|
+
export function UserFormWithLocale() {
|
|
162
|
+
const [locale, setLocale] = useState('ja');
|
|
163
|
+
const [form] = Form.useForm();
|
|
164
|
+
|
|
165
|
+
// Memoize rules to avoid recalculation
|
|
166
|
+
const rules = useMemo(() => getUserRules(locale), [locale]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<Select value={locale} onChange={setLocale} style={{ marginBottom: 16 }}>
|
|
171
|
+
<Select.Option value="ja">日本語</Select.Option>
|
|
172
|
+
<Select.Option value="en">English</Select.Option>
|
|
173
|
+
<Select.Option value="vi">Tiếng Việt</Select.Option>
|
|
174
|
+
</Select>
|
|
175
|
+
|
|
176
|
+
<h2>{getUserDisplayName(locale)}</h2>
|
|
177
|
+
|
|
178
|
+
<Form form={form} layout="vertical">
|
|
179
|
+
<Form.Item
|
|
180
|
+
name="name"
|
|
181
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
182
|
+
rules={rules.name}
|
|
183
|
+
>
|
|
184
|
+
<Input />
|
|
185
|
+
</Form.Item>
|
|
186
|
+
|
|
187
|
+
<Form.Item
|
|
188
|
+
name="email"
|
|
189
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
190
|
+
rules={rules.email}
|
|
191
|
+
>
|
|
192
|
+
<Input type="email" />
|
|
193
|
+
</Form.Item>
|
|
194
|
+
</Form>
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## With Table Columns
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Table, TableColumnsType } from 'antd';
|
|
204
|
+
import { getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
205
|
+
import { User } from '@/types/model';
|
|
206
|
+
|
|
207
|
+
interface UserTableProps {
|
|
208
|
+
users: User[];
|
|
209
|
+
locale?: string;
|
|
210
|
+
loading?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function UserTable({ users, locale = 'ja', loading }: UserTableProps) {
|
|
214
|
+
const columns: TableColumnsType<User> = [
|
|
215
|
+
{
|
|
216
|
+
title: getUserPropertyDisplayName('name', locale),
|
|
217
|
+
dataIndex: 'name',
|
|
218
|
+
key: 'name',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
title: getUserPropertyDisplayName('email', locale),
|
|
222
|
+
dataIndex: 'email',
|
|
223
|
+
key: 'email',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Table
|
|
229
|
+
columns={columns}
|
|
230
|
+
dataSource={users}
|
|
231
|
+
rowKey="id"
|
|
232
|
+
loading={loading}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Custom Form Hook
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { Form, FormInstance } from 'antd';
|
|
242
|
+
import { useMemo } from 'react';
|
|
243
|
+
|
|
244
|
+
// Generic hook for any model's rules
|
|
245
|
+
export function useModelForm<T>(
|
|
246
|
+
getRules: (locale: string) => Record<string, any[]>,
|
|
247
|
+
locale: string = 'ja'
|
|
248
|
+
): {
|
|
249
|
+
form: FormInstance<T>;
|
|
250
|
+
rules: Record<string, any[]>;
|
|
251
|
+
} {
|
|
252
|
+
const [form] = Form.useForm<T>();
|
|
253
|
+
const rules = useMemo(() => getRules(locale), [getRules, locale]);
|
|
254
|
+
|
|
255
|
+
return { form, rules };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Usage
|
|
259
|
+
import { getUserRules } from '@/types/model/rules/User.rules';
|
|
260
|
+
|
|
261
|
+
function MyComponent() {
|
|
262
|
+
const { form, rules } = useModelForm(getUserRules, 'ja');
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Form form={form}>
|
|
266
|
+
<Form.Item name="name" rules={rules.name}>
|
|
267
|
+
<Input />
|
|
268
|
+
</Form.Item>
|
|
269
|
+
</Form>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Modal Form
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { Modal, Form, Input, message } from 'antd';
|
|
278
|
+
import { getUserRules, getUserDisplayName, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
279
|
+
import { User } from '@/types/model';
|
|
280
|
+
|
|
281
|
+
interface UserModalFormProps {
|
|
282
|
+
open: boolean;
|
|
283
|
+
onClose: () => void;
|
|
284
|
+
onSubmit: (values: Partial<User>) => Promise<void>;
|
|
285
|
+
initialValues?: Partial<User>;
|
|
286
|
+
locale?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function UserModalForm({
|
|
290
|
+
open,
|
|
291
|
+
onClose,
|
|
292
|
+
onSubmit,
|
|
293
|
+
initialValues,
|
|
294
|
+
locale = 'ja'
|
|
295
|
+
}: UserModalFormProps) {
|
|
296
|
+
const [form] = Form.useForm();
|
|
297
|
+
const [loading, setLoading] = useState(false);
|
|
298
|
+
const rules = getUserRules(locale);
|
|
299
|
+
|
|
300
|
+
const handleSubmit = async () => {
|
|
301
|
+
try {
|
|
302
|
+
const values = await form.validateFields();
|
|
303
|
+
setLoading(true);
|
|
304
|
+
await onSubmit(values);
|
|
305
|
+
message.success(locale === 'ja' ? '保存しました' : 'Saved successfully');
|
|
306
|
+
form.resetFields();
|
|
307
|
+
onClose();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof Error) {
|
|
310
|
+
message.error(error.message);
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
setLoading(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Modal
|
|
319
|
+
title={getUserDisplayName(locale)}
|
|
320
|
+
open={open}
|
|
321
|
+
onCancel={onClose}
|
|
322
|
+
onOk={handleSubmit}
|
|
323
|
+
confirmLoading={loading}
|
|
324
|
+
okText={locale === 'ja' ? '保存' : 'Save'}
|
|
325
|
+
cancelText={locale === 'ja' ? 'キャンセル' : 'Cancel'}
|
|
326
|
+
>
|
|
327
|
+
<Form
|
|
328
|
+
form={form}
|
|
329
|
+
layout="vertical"
|
|
330
|
+
initialValues={initialValues}
|
|
331
|
+
>
|
|
332
|
+
<Form.Item
|
|
333
|
+
name="name"
|
|
334
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
335
|
+
rules={rules.name}
|
|
336
|
+
>
|
|
337
|
+
<Input />
|
|
338
|
+
</Form.Item>
|
|
339
|
+
|
|
340
|
+
<Form.Item
|
|
341
|
+
name="email"
|
|
342
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
343
|
+
rules={rules.email}
|
|
344
|
+
>
|
|
345
|
+
<Input type="email" />
|
|
346
|
+
</Form.Item>
|
|
347
|
+
</Form>
|
|
348
|
+
</Modal>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Validation Rule Types
|
|
354
|
+
|
|
355
|
+
Omnify generates these rule types based on schema:
|
|
356
|
+
|
|
357
|
+
| Schema | Generated Rule |
|
|
358
|
+
|--------|---------------|
|
|
359
|
+
| `required: true` | `{ required: true, message: {...} }` |
|
|
360
|
+
| `type: Email` | `{ type: 'email', message: {...} }` |
|
|
361
|
+
| `maxLength: N` | `{ max: N, message: {...} }` |
|
|
362
|
+
| `minLength: N` | `{ min: N, message: {...} }` |
|
|
363
|
+
| `max: N` (numeric) | `{ max: N, type: 'number', message: {...} }` |
|
|
364
|
+
| `min: N` (numeric) | `{ min: N, type: 'number', message: {...} }` |
|
|
365
|
+
| `pattern: regex` | `{ pattern: /regex/, message: {...} }` |
|
|
366
|
+
|
|
367
|
+
## Built-in Validation Messages
|
|
368
|
+
|
|
369
|
+
Omnify includes templates for 5 languages:
|
|
370
|
+
- Japanese (ja)
|
|
371
|
+
- English (en)
|
|
372
|
+
- Vietnamese (vi)
|
|
373
|
+
- Korean (ko)
|
|
374
|
+
- Chinese (zh)
|
|
375
|
+
|
|
376
|
+
Custom templates can be configured in `omnify.config.ts`:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
export default defineConfig({
|
|
380
|
+
output: {
|
|
381
|
+
typescript: {
|
|
382
|
+
validationTemplates: {
|
|
383
|
+
required: {
|
|
384
|
+
ja: '${displayName}を入力してください',
|
|
385
|
+
en: '${displayName} is required',
|
|
386
|
+
vi: '${displayName} là bắt buộc',
|
|
387
|
+
},
|
|
388
|
+
maxLength: {
|
|
389
|
+
ja: '${displayName}は${max}文字以内です',
|
|
390
|
+
en: '${displayName} must be at most ${max} characters',
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Available placeholders:
|
|
399
|
+
- `${displayName}` - Property display name
|
|
400
|
+
- `${min}` - Minimum value/length
|
|
401
|
+
- `${max}` - Maximum value/length
|