proje-react-panel 1.7.0 → 1.9.0

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.
Files changed (47) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/README.md +43 -3
  3. package/dist/__tests__/components/form/CustomField.test.d.ts +4 -0
  4. package/dist/__tests__/components/form/FormGroups.test.d.ts +1 -0
  5. package/dist/__tests__/components/form/RichTextField.test.d.ts +4 -0
  6. package/dist/__tests__/optionalTiptap.test.d.ts +4 -0
  7. package/dist/components/form/CustomField.d.ts +9 -0
  8. package/dist/components/form/RichTextField.d.ts +7 -0
  9. package/dist/decorators/form/Form.d.ts +8 -0
  10. package/dist/decorators/form/Input.d.ts +5 -1
  11. package/dist/decorators/form/inputs/CustomInput.d.ts +16 -0
  12. package/dist/decorators/form/inputs/RichTextInput.d.ts +13 -0
  13. package/dist/index.cjs.js +1 -1
  14. package/dist/index.d.ts +3 -1
  15. package/dist/index.esm.js +2 -2
  16. package/{DASHBOARD_GUIDE.md → guides/DASHBOARD_GUIDE.md} +1 -1
  17. package/guides/USAGE.md +85 -0
  18. package/how_to.md +302 -0
  19. package/jest.config.js +1 -0
  20. package/package.json +17 -1
  21. package/src/__tests__/components/form/CustomField.test.tsx +92 -0
  22. package/src/__tests__/components/form/FormGroups.test.tsx +194 -0
  23. package/src/__tests__/components/form/RichTextField.test.tsx +181 -0
  24. package/src/__tests__/optionalTiptap.test.tsx +89 -0
  25. package/src/components/form/CustomField.tsx +34 -0
  26. package/src/components/form/FormField.tsx +13 -2
  27. package/src/components/form/InnerForm.tsx +101 -19
  28. package/src/components/form/RichTextField.tsx +218 -0
  29. package/src/components/layout/Layout.tsx +18 -2
  30. package/src/components/list/ListPage.tsx +1 -1
  31. package/src/decorators/form/Form.ts +11 -0
  32. package/src/decorators/form/Input.ts +6 -1
  33. package/src/decorators/form/inputs/CustomInput.ts +26 -0
  34. package/src/decorators/form/inputs/RichTextInput.ts +38 -0
  35. package/src/index.ts +13 -1
  36. package/src/styles/components/rich-text.scss +115 -0
  37. package/src/styles/form.scss +48 -0
  38. package/src/styles/index.scss +1 -0
  39. package/dist/components/DashboardContainer.d.ts +0 -7
  40. package/dist/components/DashboardGrid.d.ts +0 -9
  41. package/dist/components/DashboardItem.d.ts +0 -10
  42. package/dist/components/ThemeSwitcher.d.ts +0 -7
  43. package/dist/store/themeStore.d.ts +0 -23
  44. /package/{AUTH_LAYOUT_EXAMPLE.md → guides/AUTH_LAYOUT_EXAMPLE.md} +0 -0
  45. /package/{AUTH_LAYOUT_GUIDE.md → guides/AUTH_LAYOUT_GUIDE.md} +0 -0
  46. /package/{COLOR_SYSTEM_GUIDE.md → guides/COLOR_SYSTEM_GUIDE.md} +0 -0
  47. /package/{IMPLEMENTATION_GUIDE.md → guides/IMPLEMENTATION_GUIDE.md} +0 -0
@@ -527,5 +527,5 @@ The Dashboard components are part of the `proje-react-panel` library and require
527
527
 
528
528
  ## Related Components
529
529
 
530
- - [Counter Component](./README.md#counter-component) - For displaying animated counters
530
+ - [Counter Component](../README.md#counter-component) - For displaying animated counters
531
531
  - [Layout Component](./AUTH_LAYOUT_GUIDE.md) - For page layouts with sidebar
@@ -0,0 +1,85 @@
1
+ # React Panel Library
2
+
3
+ A powerful and flexible React-based panel library for building administrative interfaces and data management systems.
4
+
5
+ ## Features
6
+
7
+ - **List Page Component**: Built-in list view with customizable cells and image handling
8
+ - **Form Page Component**: Flexible form creation and management
9
+ - **Login Component**: Authentication handling
10
+ - **Layout System**: Consistent layout management
11
+ - **Panel Component**: Core panel functionality
12
+ - **Counter Component**: Utility component for counting operations
13
+ - **Dashboard Components**: Flexible grid-based dashboard layout system
14
+ - **Color System**: Centralized color system with SCSS variables and CSS custom properties
15
+
16
+ ## Decorators
17
+
18
+ The library provides several decorators for enhanced functionality:
19
+
20
+ ### List Decorators
21
+
22
+ - `@List`: Main list decorator for creating list views
23
+ - `@ImageCell`: Specialized cell for handling images in lists
24
+ - `@Cell`: Base cell decorator for list items
25
+
26
+ ### Form Decorators
27
+
28
+ - `@Input`: Form input decorator
29
+ - `@Crud`: CRUD operations decorator
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install proje-react-panel
35
+ # or
36
+ yarn add proje-react-panel
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```typescript
42
+ import { Panel, ListPage, FormPage, Login, Layout, DashboardGrid, DashboardItem } from 'proje-react-panel';
43
+
44
+ // Initialize the panel
45
+ const panel = new Panel({
46
+ // configuration options
47
+ });
48
+
49
+ // Use components
50
+ <Layout>
51
+ <ListPage />
52
+ <FormPage />
53
+ <Login />
54
+ </Layout>
55
+
56
+ // Dashboard example
57
+ <DashboardGrid columns={3}>
58
+ <DashboardItem>Content 1</DashboardItem>
59
+ <DashboardItem>Content 2</DashboardItem>
60
+ <DashboardItem>Content 3</DashboardItem>
61
+ </DashboardGrid>
62
+ ```
63
+
64
+ ## Guides
65
+
66
+ - **[Dashboard Guide](./guides/DASHBOARD_GUIDE.md)** - Complete guide for using Dashboard components
67
+ - **[Auth Layout Guide](./guides/AUTH_LAYOUT_GUIDE.md)** - Guide for implementing authentication layouts with sidebar
68
+ - **[Auth Layout Example](./guides/AUTH_LAYOUT_EXAMPLE.md)** - Complete working example of AuthLayout implementation
69
+ - **[Color System Guide](./guides/COLOR_SYSTEM_GUIDE.md)** - Complete documentation of the color system, SCSS variables, and CSS custom properties
70
+ - **[Implementation Guide](./guides/IMPLEMENTATION_GUIDE.md)** - Comprehensive guide for implementing the library in your React applications
71
+
72
+ ## Type Definitions
73
+
74
+ The library includes TypeScript definitions for better development experience:
75
+
76
+ - `InitPanelOptions`: Configuration options for panel initialization
77
+ - `ScreenCreatorData`: Data structure for screen creation
78
+
79
+ ## Contributing
80
+
81
+ Contributions are welcome! Please feel free to submit a Pull Request.
82
+
83
+ ## License
84
+
85
+ This project is licensed under the MIT License - see the LICENSE file for details.
package/how_to.md ADDED
@@ -0,0 +1,302 @@
1
+ # How To — `proje-react-panel`
2
+
3
+ A decorator-driven admin-panel kit for React. You declare your resources as TypeScript classes (`@List`, `@Form`, `@Details`); the library renders the table / form / detail pages and talks to your REST backend.
4
+
5
+ For deep walkthroughs see [`guides/IMPLEMENTATION_GUIDE.md`](./guides/IMPLEMENTATION_GUIDE.md). For a runnable demo see [`examples/`](./examples).
6
+
7
+ ---
8
+
9
+ ## 1. What you're getting
10
+
11
+ - `<ListPage model={...} />`, `<FormPage model={...} />`, `<DetailsPage model={...} />` — page components driven by class metadata.
12
+ - `<Panel>`, `<Layout>`, `<Login>` — app shell, sidebar, and login page.
13
+ - `<Dashboard>`, `<DashboardGrid>`, `<DashboardItem>`, `<Counter>` — dashboard widgets.
14
+ - `initApi`, `setAuthToken`, `getAll`, `getOne`, `create`, `update`, `remove`, `createFormData`, `updateFormData` — backend wiring helpers.
15
+ - Decorators: `@List`, `@Form`, `@Details`, `@Cell`, `@LinkCell`, `@ImageCell`, `@DownloadCell`, `@Input`, `@SelectInput`, `@RichTextInput`, `@CustomInput`, `@DetailsItem`.
16
+
17
+ See the full export surface in [`src/index.ts`](./src/index.ts).
18
+
19
+ ---
20
+
21
+ ## 2. Backend expectations
22
+
23
+ The library is built against a plain REST backend with these conventions. Any backend that respects them will work — a compatible NestJS reference implementation is available as a sibling project; use it as a starting point if you don't have a backend yet.
24
+
25
+ | Concern | Shape |
26
+ | ----------- | ---------------------------------------------------------------------- |
27
+ | Auth header | `Authorization: Bearer <jwt>` on every protected request |
28
+ | Login | `POST /auth/login` → `{ access_token, admin: { id, email } }` |
29
+ | List | `GET /:resource?page=<n>&limit=<n>` → `{ total: number, data: T[] }` |
30
+ | Item | `GET /:resource/:id` → entity JSON (no envelope) |
31
+ | Mutations | `POST /:resource`, `PUT /:resource/:id`, `DELETE /:resource/:id` |
32
+ | File upload | `multipart/form-data`, field name `file` |
33
+ | Errors | `{ statusCode, message, error }` (NestJS default works out of the box) |
34
+ | 401 | The library auto-clears the token and redirects to `/login` |
35
+
36
+ ---
37
+
38
+ ## 3. Client-side data flow
39
+
40
+ ```mermaid
41
+ flowchart LR
42
+ Model["Model class<br/>(@List/@Form/@Details)"] -->|reflect-metadata| Page["ListPage / FormPage / DetailsPage"]
43
+ Page -->|getData / onSubmit / getDetailsData| DF["dataFetchers"]
44
+ DF -->|getAll/getOne/create/update/remove| Crud["CrudApi (axios)"]
45
+ Crud -->|REST + Bearer| API[("Your backend")]
46
+ API -->|"{ total, data }" or entity| Crud
47
+ Crud --> Page
48
+ Page --> UI["Rendered table / form / details"]
49
+
50
+ LS[("localStorage<br/>token")] -.read on boot.- Crud
51
+ API -.401.-> Logout["setAuthLogout()<br/>→ /login"]
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 4. Quick start (5 steps)
57
+
58
+ ### 4.1 Install
59
+
60
+ ```bash
61
+ npm install proje-react-panel \
62
+ react react-router react-hook-form \
63
+ zustand axios react-select use-sync-external-store
64
+ ```
65
+
66
+ Peer-dep versions: `react >= 19`, `react-router 7.3.0`, `react-hook-form >= 7.54.2`, `zustand >= 5.0.3`, `axios >= 1.0.0`, `react-select ^5.10.1`, `use-sync-external-store >= 1.4.0` (see [`package.json`](./package.json)).
67
+
68
+ Rich text (`@RichTextInput`) needs two more packages, and only if you use it — they are declared **optional** peer dependencies, so npm/yarn will not pull them into panels that have no richtext field:
69
+
70
+ ```bash
71
+ npm install @tiptap/react @tiptap/starter-kit
72
+ ```
73
+
74
+ ### 4.2 Initialize at app root
75
+
76
+ ```ts
77
+ import { Panel, initApi, initAuthToken, setAuthToken } from 'proje-react-panel';
78
+ import 'reflect-metadata';
79
+
80
+ initApi({ baseUrl: import.meta.env.VITE_API_BASE_URL });
81
+ initAuthToken();
82
+
83
+ export function App() {
84
+ return (
85
+ <Panel onInit={(appData) => { if (appData.token) setAuthToken(appData.token); }}>
86
+ <Router>{/* routes */}</Router>
87
+ </Panel>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ### 4.3 Wire data fetchers
93
+
94
+ ```ts
95
+ // src/api/dataFetchers.ts
96
+ import { getAll, getOne, create, update, remove } from 'proje-react-panel';
97
+ import { ProductList, CreateProductForm, EditProductForm, ProductDetails } from '../models/Product';
98
+
99
+ export const dataFetchers = Object.freeze({
100
+ products: {
101
+ getAll: getAll<ProductList>('products'),
102
+ details: getOne<ProductDetails>('products'),
103
+ create: create<CreateProductForm>('products'),
104
+ update: update<EditProductForm>('products'),
105
+ remove: remove('products', 'id'),
106
+ },
107
+ });
108
+ ```
109
+
110
+ ### 4.4 Declare a model with decorators
111
+
112
+ ```ts
113
+ // src/models/Product.ts
114
+ import { List, Cell, LinkCell, Form, Input, Details, DetailsItem } from 'proje-react-panel';
115
+ import { IsNotEmpty, MinLength } from 'class-validator';
116
+ import { dataFetchers } from '../api/dataFetchers';
117
+
118
+ @List({ getData: dataFetchers.products.getAll, primaryId: 'id' })
119
+ export class ProductList {
120
+ @Cell({ title: 'ID', type: 'uuid' }) id!: string;
121
+ @Cell({ title: 'Name' }) name!: string;
122
+ @LinkCell({ path: '/products/:id', placeHolder: 'open' }) open!: string;
123
+ }
124
+
125
+ @Form({ onSubmit: dataFetchers.products.create, redirectSuccessUrl: '/products' })
126
+ export class CreateProductForm {
127
+ @Input({ label: 'Name' })
128
+ @IsNotEmpty()
129
+ @MinLength(2)
130
+ name!: string;
131
+ }
132
+
133
+ @Details({ getDetailsData: dataFetchers.products.details, primaryId: 'id' })
134
+ export class ProductDetails {
135
+ @DetailsItem() id!: string;
136
+ @DetailsItem() name!: string;
137
+ }
138
+ ```
139
+
140
+ ### 4.5 Route the pages
141
+
142
+ ```tsx
143
+ import { ListPage, FormPage, DetailsPage } from 'proje-react-panel';
144
+ import { ProductList, CreateProductForm, ProductDetails } from './models/Product';
145
+
146
+ <Route path="products">
147
+ <Route path="" element={<ListPage model={ProductList} />} />
148
+ <Route path="create" element={<FormPage model={CreateProductForm} />} />
149
+ <Route path=":id" element={<DetailsPage model={ProductDetails} />} />
150
+ </Route>;
151
+ ```
152
+
153
+ That's it — you have a fully functional list / create / detail flow against `GET|POST|GET /products`.
154
+
155
+ ---
156
+
157
+ ## 5. File uploads
158
+
159
+ Swap `create` → `createFormData` (and `update` → `updateFormData`) in your `dataFetchers`, set `type: 'formData'` on the `@Form` decorator, and name the file field `file`. The library then sends `multipart/form-data` with the file under the `file` key — which is what the reference backend's upload endpoint expects.
160
+
161
+ A complete worked example lives in [`examples/src/models/Asset.ts`](./examples/src/models/Asset.ts) and [`examples/src/api/dataFetchers.ts`](./examples/src/api/dataFetchers.ts).
162
+
163
+ ---
164
+
165
+ ## 6. Form field types
166
+
167
+ Beyond `@Input`, a field can opt into a richer control. Each decorator is a drop-in replacement for `@Input` on the property.
168
+
169
+ ### 6.1 `@SelectInput` — dropdown
170
+
171
+ ```ts
172
+ @Form({ onSubmit: dataFetchers.products.create })
173
+ export class CreateProductForm {
174
+ @SelectInput({ label: 'Category', defaultOptions: [{ label: 'Books', value: 'books' }] })
175
+ category!: string;
176
+
177
+ @SelectInput({
178
+ label: 'Owner',
179
+ onSelectPreloader: async () => (await listUsers()).map(u => ({ label: u.email, value: u.id })),
180
+ })
181
+ ownerId!: string;
182
+ }
183
+ ```
184
+
185
+ | Option | Meaning |
186
+ | ------------------- | ----------------------------------------------------------------------- |
187
+ | `defaultOptions` | Options known up front |
188
+ | `onSelectPreloader` | `async () => { label, value }[]`, fetched once and cached across fields |
189
+
190
+ ### 6.2 `@RichTextInput` — rich text
191
+
192
+ Renders a TipTap editor and stores **HTML** in the field. Requires the optional peers from §4.1; without them the field renders an "install these packages" notice and the rest of the panel keeps working.
193
+
194
+ ```ts
195
+ import { Form, Input, RichTextInput } from 'proje-react-panel';
196
+
197
+ @Form({ onSubmit: dataFetchers.articles.create })
198
+ export class CreateArticleForm {
199
+ @Input({ label: 'Title' })
200
+ title!: string;
201
+
202
+ @RichTextInput({
203
+ label: 'Body',
204
+ placeholder: 'Write the article…',
205
+ toolbar: ['bold', 'italic', 'heading', 'bulletList', 'orderedList', 'link'],
206
+ minHeight: 240,
207
+ })
208
+ body!: string;
209
+ }
210
+ ```
211
+
212
+ | Option | Meaning |
213
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
214
+ | `toolbar` | Buttons to show, in order — any of `bold`, `italic`, `heading`, `bulletList`, `orderedList`, `link`. Defaults to all six |
215
+ | `minHeight` | Editor height in px (default `160`) |
216
+ | `placeholder` | Shown while the document is empty |
217
+
218
+ Notes:
219
+
220
+ - The value is an HTML string (`<p>…</p>`); an empty editor reports `''`, so `@IsNotEmpty()` behaves as expected.
221
+ - Values loaded through `@Form({ getDetailsData })` arrive after the form mounts; the editor picks them up without disturbing a caret that is already in the text.
222
+
223
+ ### 6.3 `@CustomInput` — anything else
224
+
225
+ ```ts
226
+ @CustomInput({ label: 'Color', render: ({ value, onChange }) => <ColorPicker value={value} onChange={onChange} /> })
227
+ color!: string;
228
+ ```
229
+
230
+ ---
231
+
232
+ ## 7. Grouping form fields
233
+
234
+ Long forms can be split into sections. Fields opt in with `group`, and `@Form({ groups })` decides the headings and their order:
235
+
236
+ ```ts
237
+ @Form({
238
+ onSubmit: dataFetchers.articles.create,
239
+ groups: [
240
+ { key: 'content', label: 'Content' },
241
+ { key: 'seo', label: 'SEO', collapsible: true },
242
+ { key: 'advanced', label: 'Advanced', collapsible: true, defaultCollapsed: true },
243
+ ],
244
+ })
245
+ export class CreateArticleForm {
246
+ @Input({ label: 'Title', group: 'content' })
247
+ title!: string;
248
+
249
+ @RichTextInput({ label: 'Body', group: 'content' })
250
+ body!: string;
251
+
252
+ @Input({ label: 'Meta description', type: 'textarea', rows: 4, group: 'seo' })
253
+ metaDescription!: string;
254
+
255
+ @Input({ label: 'Cache key', group: 'advanced' })
256
+ cacheKey!: string;
257
+ }
258
+ ```
259
+
260
+ | Group option | Meaning |
261
+ | ------------------ | ---------------------------------------------------------------- |
262
+ | `key` | Matched against `@Input({ group })` |
263
+ | `label` | Section heading |
264
+ | `collapsible` | Renders as `<details>/<summary>` instead of `<fieldset><legend>` |
265
+ | `defaultCollapsed` | Starts closed (collapsible groups only) |
266
+
267
+ Rules worth knowing:
268
+
269
+ - **A form where no field sets `group` renders exactly as before** — no extra wrapper element, so existing panel CSS is untouched.
270
+ - Fields without a `group` render first, above the sections, in declaration order.
271
+ - A `group` value missing from `@Form({ groups })` still renders: it is appended after the declared sections and labelled with its own key.
272
+ - `hidden` fields never go into a section.
273
+ - `rows` works on any `type: 'textarea'` field, grouped or not.
274
+
275
+ ---
276
+
277
+ ## 8. Login & 401
278
+
279
+ - `initAuthToken()` reads `localStorage["token"]` on boot and applies it to axios.
280
+ - `setAuthToken(token)` writes the token to `localStorage` and axios defaults — call it from your login form's success handler (or use the built-in `<Login model={DefaultLoginForm} />`).
281
+ - A 401 response anywhere triggers `setAuthLogout()` and a hard redirect to `/login`. See [`src/api/ApiConfig.ts`](./src/api/ApiConfig.ts) for the interceptor.
282
+
283
+ ---
284
+
285
+ ## 9. Where to next
286
+
287
+ | Topic | File |
288
+ | ------------------------------------------ | -------------------------------------------------------------------- |
289
+ | Step-by-step walkthrough (the deepest doc) | [`guides/IMPLEMENTATION_GUIDE.md`](./guides/IMPLEMENTATION_GUIDE.md) |
290
+ | Sidebar + authenticated layout | [`guides/AUTH_LAYOUT_GUIDE.md`](./guides/AUTH_LAYOUT_GUIDE.md) |
291
+ | Layout example code | [`guides/AUTH_LAYOUT_EXAMPLE.md`](./guides/AUTH_LAYOUT_EXAMPLE.md) |
292
+ | Dashboard widgets | [`guides/DASHBOARD_GUIDE.md`](./guides/DASHBOARD_GUIDE.md) |
293
+ | Colors / theme tokens | [`guides/COLOR_SYSTEM_GUIDE.md`](./guides/COLOR_SYSTEM_GUIDE.md) |
294
+ | Feature overview | [`guides/USAGE.md`](./guides/USAGE.md) |
295
+ | Live runnable example | [`examples/`](./examples) |
296
+ | Product/tech doc | [`PTD.md`](./PTD.md) |
297
+
298
+ ---
299
+
300
+ ## 10. Version & license
301
+
302
+ `proje-react-panel` v1.9.0 — ISC license. See [`package.json`](./package.json).
package/jest.config.js CHANGED
@@ -10,6 +10,7 @@ export default {
10
10
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
11
11
  moduleNameMapper: {
12
12
  '^@/(.*)$': '<rootDir>/src/$1',
13
+ '\\.svg$': '<rootDir>/jest/svgStub.js',
13
14
  },
14
15
  collectCoverage: true,
15
16
  coverageDirectory: 'coverage',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proje-react-panel",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "author": "SEFA DEMİR",
@@ -30,6 +30,10 @@
30
30
  "@rollup/plugin-commonjs": "^28.0.3",
31
31
  "@rollup/plugin-node-resolve": "^16.0.1",
32
32
  "@svgr/rollup": "^8.1.0",
33
+ "@testing-library/dom": "^10.4.1",
34
+ "@testing-library/react": "^16.3.2",
35
+ "@tiptap/react": "^3.29.2",
36
+ "@tiptap/starter-kit": "^3.29.2",
33
37
  "@types/react": "^19.0.10",
34
38
  "@typescript-eslint/eslint-plugin": "^8.26.1",
35
39
  "@typescript-eslint/parser": "^8.29.1",
@@ -41,9 +45,11 @@
41
45
  "eslint-plugin-react-hooks": "^5.2.0",
42
46
  "globals": "^16.0.0",
43
47
  "jest": "^29.7.0",
48
+ "jest-environment-jsdom": "^29.7.0",
44
49
  "prettier": "^3.5.3",
45
50
  "prettier-eslint": "^16.3.0",
46
51
  "react": "^19.0.0",
52
+ "react-dom": "19.0.0",
47
53
  "react-hook-form": "^7.54.2",
48
54
  "react-router": "^7.3.0",
49
55
  "react-select": "^5.10.1",
@@ -62,6 +68,8 @@
62
68
  "zustand": "^5.0.3"
63
69
  },
64
70
  "peerDependencies": {
71
+ "@tiptap/react": "^3.0.0",
72
+ "@tiptap/starter-kit": "^3.0.0",
65
73
  "axios": ">=1.0.0",
66
74
  "react": ">=19.0.0",
67
75
  "react-hook-form": ">=7.54.2",
@@ -69,5 +77,13 @@
69
77
  "react-select": "^5.10.1",
70
78
  "use-sync-external-store": ">=1.4.0",
71
79
  "zustand": ">=5.0.3"
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "@tiptap/react": {
83
+ "optional": true
84
+ },
85
+ "@tiptap/starter-kit": {
86
+ "optional": true
87
+ }
72
88
  }
73
89
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import 'reflect-metadata';
5
+ import React from 'react';
6
+ import { describe, expect, it, beforeEach } from '@jest/globals';
7
+ import { render, screen, fireEvent } from '@testing-library/react';
8
+ import { FormProvider, useForm } from 'react-hook-form';
9
+ import { CustomInput, CustomRenderProps } from '../../../decorators/form/inputs/CustomInput';
10
+ import { Input, getInputFields } from '../../../decorators/form/Input';
11
+ import { FormField } from '../../../components/form/FormField';
12
+
13
+ // Every call the library makes into the consumer supplied render prop.
14
+ let renderCalls: CustomRenderProps[] = [];
15
+
16
+ function renderControl(props: CustomRenderProps) {
17
+ renderCalls.push(props);
18
+ return (
19
+ <button type="button" data-testid="custom-control" onClick={() => props.onChange('picked-42')}>
20
+ {String(props.value ?? '')}
21
+ </button>
22
+ );
23
+ }
24
+
25
+ class AssetForm {
26
+ @Input({ label: 'Title' })
27
+ title: string;
28
+
29
+ @CustomInput({ label: 'Asset', render: renderControl })
30
+ asset: unknown;
31
+ }
32
+
33
+ // The values react-hook-form currently holds, mirrored out of the harness.
34
+ let formValues: Record<string, unknown> = {};
35
+
36
+ function Harness() {
37
+ const form = useForm({ defaultValues: { title: '', asset: 'initial' } });
38
+ formValues = form.watch();
39
+ const inputs = getInputFields(AssetForm);
40
+ return (
41
+ <FormProvider {...form}>
42
+ {inputs.map(input => (
43
+ <FormField key={input.name} input={input} register={form.register} />
44
+ ))}
45
+ </FormProvider>
46
+ );
47
+ }
48
+
49
+ describe('CustomInput', () => {
50
+ beforeEach(() => {
51
+ renderCalls = [];
52
+ formValues = {};
53
+ });
54
+
55
+ it('carries the render function through the metadata', () => {
56
+ const asset = getInputFields(AssetForm).find(input => input.name === 'asset');
57
+ expect(asset?.type).toBe('custom');
58
+ expect(typeof (asset as unknown as { render?: unknown })?.render).toBe('function');
59
+ });
60
+
61
+ it('calls the render prop with the field name and current value', () => {
62
+ render(<Harness />);
63
+
64
+ expect(renderCalls.length).toBeGreaterThan(0);
65
+ expect(renderCalls[0].fieldName).toBe('asset');
66
+ expect(renderCalls[0].value).toBe('initial');
67
+ expect(renderCalls[0].error).toBeUndefined();
68
+ expect(screen.getByTestId('custom-control').textContent).toBe('initial');
69
+ });
70
+
71
+ it('renders the label like any other field', () => {
72
+ render(<Harness />);
73
+
74
+ expect(screen.getByText('Asset:')).toBeTruthy();
75
+ });
76
+
77
+ it('updates the form value when the render prop calls onChange', () => {
78
+ render(<Harness />);
79
+
80
+ fireEvent.click(screen.getByTestId('custom-control'));
81
+
82
+ expect(formValues.asset).toBe('picked-42');
83
+ expect(screen.getByTestId('custom-control').textContent).toBe('picked-42');
84
+ });
85
+
86
+ it('leaves the other field types untouched', () => {
87
+ render(<Harness />);
88
+
89
+ expect(screen.getByText('Title:')).toBeTruthy();
90
+ expect(formValues.title).toBe('');
91
+ });
92
+ });