proje-react-panel 1.6.0 → 1.8.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.
- package/.cursor/rules.md +11 -1
- package/.vscode/settings.json +1 -1
- package/README.md +57 -1
- package/dist/__tests__/components/form/CustomField.test.d.ts +4 -0
- package/dist/api/ApiConfig.d.ts +11 -0
- package/dist/api/AuthApi.d.ts +2 -5
- package/dist/api/CrudApi.d.ts +11 -12
- package/dist/components/DashboardContainer.d.ts +7 -0
- package/dist/components/DashboardGrid.d.ts +9 -0
- package/dist/components/DashboardItem.d.ts +10 -0
- package/dist/components/ThemeSwitcher.d.ts +7 -0
- package/dist/components/dashboard/Dashboard.d.ts +7 -0
- package/dist/components/dashboard/DashboardGrid.d.ts +7 -0
- package/dist/components/dashboard/DashboardItem.d.ts +6 -0
- package/dist/components/dashboard/index.d.ts +3 -0
- package/dist/components/form/CustomField.d.ts +9 -0
- package/dist/decorators/auth/DefaultLoginForm.d.ts +4 -0
- package/dist/decorators/form/Input.d.ts +1 -1
- package/dist/decorators/form/inputs/CustomInput.d.ts +16 -0
- package/dist/index.cjs.js +15 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +15 -1
- package/dist/store/themeStore.d.ts +23 -0
- package/dist/types/Login.d.ts +8 -0
- package/guides/AUTH_LAYOUT_EXAMPLE.md +343 -0
- package/guides/AUTH_LAYOUT_GUIDE.md +819 -0
- package/guides/COLOR_SYSTEM_GUIDE.md +296 -0
- package/guides/DASHBOARD_GUIDE.md +531 -0
- package/guides/IMPLEMENTATION_GUIDE.md +899 -0
- package/guides/USAGE.md +85 -0
- package/how_to.md +184 -0
- package/package.json +7 -1
- package/src/__tests__/components/form/CustomField.test.tsx +92 -0
- package/src/api/ApiConfig.ts +63 -0
- package/src/api/AuthApi.ts +8 -0
- package/src/api/CrudApi.ts +96 -60
- package/src/components/dashboard/Dashboard.tsx +11 -0
- package/src/components/dashboard/DashboardGrid.tsx +14 -0
- package/src/components/dashboard/DashboardItem.tsx +9 -0
- package/src/components/dashboard/index.ts +3 -0
- package/src/components/form/CustomField.tsx +34 -0
- package/src/components/form/FormField.tsx +5 -1
- package/src/components/layout/Layout.tsx +18 -2
- package/src/components/list/ListPage.tsx +1 -1
- package/src/decorators/auth/DefaultLoginForm.ts +32 -0
- package/src/decorators/form/Input.ts +1 -1
- package/src/decorators/form/inputs/CustomInput.ts +26 -0
- package/src/index.ts +31 -0
- package/src/styles/base/_variables.scss +45 -0
- package/src/styles/components/button.scss +3 -3
- package/src/styles/components/checkbox.scss +6 -6
- package/src/styles/components/form-header.scss +21 -19
- package/src/styles/components/uploader.scss +15 -37
- package/src/styles/counter.scss +25 -33
- package/src/styles/dashboard.scss +9 -0
- package/src/styles/details.scss +6 -15
- package/src/styles/error-boundary.scss +75 -74
- package/src/styles/filter-popup.scss +29 -27
- package/src/styles/form.scss +16 -15
- package/src/styles/index.scss +8 -4
- package/src/styles/layout.scss +9 -8
- package/src/styles/list.scss +29 -27
- package/src/styles/loading-screen.scss +4 -4
- package/src/styles/login.scss +3 -3
- package/src/styles/pagination.scss +13 -13
- package/src/styles/sidebar.scss +24 -22
- package/src/styles/utils/scrollbar.scss +4 -3
- package/src/types/Login.ts +9 -0
- package/src/utils/logout.ts +2 -0
- package/dist/components/components/Checkbox.d.ts +0 -7
- package/dist/components/components/Counter.d.ts +0 -9
- package/dist/components/components/ErrorBoundary.d.ts +0 -16
- package/dist/components/components/ErrorComponent.d.ts +0 -4
- package/dist/components/components/FormField.d.ts +0 -17
- package/dist/components/components/ImageUploader.d.ts +0 -15
- package/dist/components/components/InnerForm.d.ts +0 -17
- package/dist/components/components/Label.d.ts +0 -9
- package/dist/components/components/LoadingScreen.d.ts +0 -2
- package/dist/components/components/Uploader.d.ts +0 -8
- package/dist/components/components/index.d.ts +0 -8
- package/dist/components/components/list/Datagrid.d.ts +0 -9
- package/dist/components/components/list/EmptyList.d.ts +0 -2
- package/dist/components/components/list/FilterPopup.d.ts +0 -11
- package/dist/components/components/list/ListPage.d.ts +0 -20
- package/dist/components/components/list/Pagination.d.ts +0 -11
- package/dist/components/components/list/index.d.ts +0 -0
- package/dist/components/pages/ControllerDetails.d.ts +0 -5
- package/dist/components/pages/FormPage.d.ts +0 -18
- package/dist/components/pages/ListPage.d.ts +0 -18
- package/dist/components/pages/Login.d.ts +0 -13
- package/dist/decorators/Crud.d.ts +0 -6
- package/dist/decorators/form/FormOptions.d.ts +0 -7
- package/dist/decorators/form/getFormFields.d.ts +0 -3
- package/dist/decorators/list/GetCellFields.d.ts +0 -2
- package/dist/decorators/list/ImageCell.d.ts +0 -6
- package/dist/decorators/list/ListData.d.ts +0 -6
- package/dist/decorators/list/getListFields.d.ts +0 -2
- package/dist/initPanel.d.ts +0 -2
- package/dist/types/Screen.d.ts +0 -4
- package/dist/types/ScreenCreatorData.d.ts +0 -13
- package/dist/types/getDetailsData.d.ts +0 -1
- package/dist/types/initPanelOptions.d.ts +0 -2
- package/dist/utils/createScreens.d.ts +0 -1
- package/dist/utils/getFields.d.ts +0 -3
package/guides/USAGE.md
ADDED
|
@@ -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,184 @@
|
|
|
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`, `@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
|
+
### 4.2 Initialize at app root
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { Panel, initApi, initAuthToken, setAuthToken } from 'proje-react-panel';
|
|
72
|
+
import 'reflect-metadata';
|
|
73
|
+
|
|
74
|
+
initApi({ baseUrl: import.meta.env.VITE_API_BASE_URL });
|
|
75
|
+
initAuthToken();
|
|
76
|
+
|
|
77
|
+
export function App() {
|
|
78
|
+
return (
|
|
79
|
+
<Panel onInit={(appData) => { if (appData.token) setAuthToken(appData.token); }}>
|
|
80
|
+
<Router>{/* routes */}</Router>
|
|
81
|
+
</Panel>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4.3 Wire data fetchers
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// src/api/dataFetchers.ts
|
|
90
|
+
import { getAll, getOne, create, update, remove } from 'proje-react-panel';
|
|
91
|
+
import { ProductList, CreateProductForm, EditProductForm, ProductDetails } from '../models/Product';
|
|
92
|
+
|
|
93
|
+
export const dataFetchers = Object.freeze({
|
|
94
|
+
products: {
|
|
95
|
+
getAll: getAll<ProductList>('products'),
|
|
96
|
+
details: getOne<ProductDetails>('products'),
|
|
97
|
+
create: create<CreateProductForm>('products'),
|
|
98
|
+
update: update<EditProductForm>('products'),
|
|
99
|
+
remove: remove('products', 'id'),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4.4 Declare a model with decorators
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// src/models/Product.ts
|
|
108
|
+
import { List, Cell, LinkCell, Form, Input, Details, DetailsItem } from 'proje-react-panel';
|
|
109
|
+
import { IsNotEmpty, MinLength } from 'class-validator';
|
|
110
|
+
import { dataFetchers } from '../api/dataFetchers';
|
|
111
|
+
|
|
112
|
+
@List({ getData: dataFetchers.products.getAll, primaryId: 'id' })
|
|
113
|
+
export class ProductList {
|
|
114
|
+
@Cell({ title: 'ID', type: 'uuid' }) id!: string;
|
|
115
|
+
@Cell({ title: 'Name' }) name!: string;
|
|
116
|
+
@LinkCell({ path: '/products/:id', placeHolder: 'open' }) open!: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Form({ onSubmit: dataFetchers.products.create, redirectSuccessUrl: '/products' })
|
|
120
|
+
export class CreateProductForm {
|
|
121
|
+
@Input({ label: 'Name' })
|
|
122
|
+
@IsNotEmpty()
|
|
123
|
+
@MinLength(2)
|
|
124
|
+
name!: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Details({ getDetailsData: dataFetchers.products.details, primaryId: 'id' })
|
|
128
|
+
export class ProductDetails {
|
|
129
|
+
@DetailsItem() id!: string;
|
|
130
|
+
@DetailsItem() name!: string;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 4.5 Route the pages
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { ListPage, FormPage, DetailsPage } from 'proje-react-panel';
|
|
138
|
+
import { ProductList, CreateProductForm, ProductDetails } from './models/Product';
|
|
139
|
+
|
|
140
|
+
<Route path="products">
|
|
141
|
+
<Route path="" element={<ListPage model={ProductList} />} />
|
|
142
|
+
<Route path="create" element={<FormPage model={CreateProductForm} />} />
|
|
143
|
+
<Route path=":id" element={<DetailsPage model={ProductDetails} />} />
|
|
144
|
+
</Route>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
That's it — you have a fully functional list / create / detail flow against `GET|POST|GET /products`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 5. File uploads
|
|
152
|
+
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
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).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 6. Login & 401
|
|
160
|
+
|
|
161
|
+
- `initAuthToken()` reads `localStorage["token"]` on boot and applies it to axios.
|
|
162
|
+
- `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} />`).
|
|
163
|
+
- A 401 response anywhere triggers `setAuthLogout()` and a hard redirect to `/login`. See [`src/api/ApiConfig.ts`](./src/api/ApiConfig.ts) for the interceptor.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 7. Where to next
|
|
168
|
+
|
|
169
|
+
| Topic | File |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| Step-by-step walkthrough (the deepest doc) | [`guides/IMPLEMENTATION_GUIDE.md`](./guides/IMPLEMENTATION_GUIDE.md) |
|
|
172
|
+
| Sidebar + authenticated layout | [`guides/AUTH_LAYOUT_GUIDE.md`](./guides/AUTH_LAYOUT_GUIDE.md) |
|
|
173
|
+
| Layout example code | [`guides/AUTH_LAYOUT_EXAMPLE.md`](./guides/AUTH_LAYOUT_EXAMPLE.md) |
|
|
174
|
+
| Dashboard widgets | [`guides/DASHBOARD_GUIDE.md`](./guides/DASHBOARD_GUIDE.md) |
|
|
175
|
+
| Colors / theme tokens | [`guides/COLOR_SYSTEM_GUIDE.md`](./guides/COLOR_SYSTEM_GUIDE.md) |
|
|
176
|
+
| Feature overview | [`guides/USAGE.md`](./guides/USAGE.md) |
|
|
177
|
+
| Live runnable example | [`examples/`](./examples) |
|
|
178
|
+
| Product/tech doc | [`PTD.md`](./PTD.md) |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 8. Version & license
|
|
183
|
+
|
|
184
|
+
`proje-react-panel` v1.7.0 — ISC license. See [`package.json`](./package.json).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proje-react-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "SEFA DEMİR",
|
|
@@ -30,9 +30,12 @@
|
|
|
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",
|
|
33
35
|
"@types/react": "^19.0.10",
|
|
34
36
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
35
37
|
"@typescript-eslint/parser": "^8.29.1",
|
|
38
|
+
"axios": "^1.12.2",
|
|
36
39
|
"class-transformer": "^0.5.1",
|
|
37
40
|
"class-validator": "^0.14.1",
|
|
38
41
|
"eslint": "^9.24.0",
|
|
@@ -40,9 +43,11 @@
|
|
|
40
43
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
41
44
|
"globals": "^16.0.0",
|
|
42
45
|
"jest": "^29.7.0",
|
|
46
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
43
47
|
"prettier": "^3.5.3",
|
|
44
48
|
"prettier-eslint": "^16.3.0",
|
|
45
49
|
"react": "^19.0.0",
|
|
50
|
+
"react-dom": "19.0.0",
|
|
46
51
|
"react-hook-form": "^7.54.2",
|
|
47
52
|
"react-router": "^7.3.0",
|
|
48
53
|
"react-select": "^5.10.1",
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
"zustand": "^5.0.3"
|
|
62
67
|
},
|
|
63
68
|
"peerDependencies": {
|
|
69
|
+
"axios": ">=1.0.0",
|
|
64
70
|
"react": ">=19.0.0",
|
|
65
71
|
"react-hook-form": ">=7.54.2",
|
|
66
72
|
"react-router": "7.3.0",
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
|
|
3
|
+
let axiosInstance: AxiosInstance;
|
|
4
|
+
|
|
5
|
+
export interface ApiConfig {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function initApi(config: ApiConfig): void {
|
|
10
|
+
axiosInstance = axios.create({
|
|
11
|
+
baseURL: config.baseUrl,
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
axiosInstance.interceptors.response.use(
|
|
18
|
+
response => response,
|
|
19
|
+
error => {
|
|
20
|
+
if (error.response && error.response.status === 401) {
|
|
21
|
+
setAuthLogout();
|
|
22
|
+
window.location.href = '/login';
|
|
23
|
+
}
|
|
24
|
+
return Promise.reject(error);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function initAuthToken(): void {
|
|
30
|
+
if (!axiosInstance) {
|
|
31
|
+
throw new Error('API not initialized. Call initApi first.');
|
|
32
|
+
}
|
|
33
|
+
const token = localStorage.getItem('token');
|
|
34
|
+
if (token) {
|
|
35
|
+
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setAuthToken(token: string): void {
|
|
40
|
+
if (!axiosInstance) {
|
|
41
|
+
throw new Error('API not initialized. Call initApi first.');
|
|
42
|
+
}
|
|
43
|
+
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
44
|
+
localStorage.setItem('token', token);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setAuthLogout(): void {
|
|
48
|
+
if (!axiosInstance) {
|
|
49
|
+
throw new Error('API not initialized. Call initApi first.');
|
|
50
|
+
}
|
|
51
|
+
axiosInstance.defaults.headers.common['Authorization'] = null;
|
|
52
|
+
localStorage.removeItem('token');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getAxiosInstance(): AxiosInstance {
|
|
56
|
+
if (!axiosInstance) {
|
|
57
|
+
throw new Error('API not initialized. Call initApi first.');
|
|
58
|
+
}
|
|
59
|
+
return axiosInstance;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Export the axios instance for external use
|
|
63
|
+
export { axiosInstance };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LoginForm, LoginResponse } from '../types/Login';
|
|
2
|
+
import { getAxiosInstance } from './ApiConfig';
|
|
3
|
+
|
|
4
|
+
export async function login<T>(data: LoginForm | FormData): Promise<LoginResponse<T>> {
|
|
5
|
+
const axiosInstance = getAxiosInstance();
|
|
6
|
+
const response = await axiosInstance.post<LoginResponse<T>>('/auth/login', data);
|
|
7
|
+
return response.data;
|
|
8
|
+
}
|
package/src/api/CrudApi.ts
CHANGED
|
@@ -1,65 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
import { GetDataForList, PaginatedResponse, GetDataParams } from '../decorators/list/List';
|
|
2
|
+
import { OnSubmitFN } from '../decorators/form/Form';
|
|
3
|
+
import { GetDetailsDataFN } from '../decorators/details/Details';
|
|
4
|
+
import { getAxiosInstance } from './ApiConfig';
|
|
5
|
+
import { AxiosError } from 'axios';
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
throw res;
|
|
7
|
+
export function getAll<T>(endpoint: string): GetDataForList<T> {
|
|
8
|
+
return async (params: GetDataParams): Promise<PaginatedResponse<T>> => {
|
|
9
|
+
const axiosInstance = getAxiosInstance();
|
|
10
|
+
const { page = 1, limit = 10 } = params;
|
|
11
|
+
const response = await axiosInstance.get<{
|
|
12
|
+
data: T[];
|
|
13
|
+
total: number;
|
|
14
|
+
}>(`/${endpoint}`, {
|
|
15
|
+
params: { page, limit, ...(params.filters ?? {}) },
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
return {
|
|
18
|
+
data: response.data.data,
|
|
19
|
+
total: response.data.total,
|
|
20
|
+
page,
|
|
21
|
+
limit,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getOne<T>(endpoint: string, key = 'id'): GetDetailsDataFN<T> {
|
|
27
|
+
return async (params: Record<string, string>): Promise<T> => {
|
|
28
|
+
const axiosInstance = getAxiosInstance();
|
|
29
|
+
const response = await axiosInstance.get<T>(`/${endpoint}/${params[key]}`);
|
|
30
|
+
return response.data;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
export function create<T>(endpoint: string): OnSubmitFN<T> {
|
|
35
|
+
return async (data: T | FormData): Promise<T> => {
|
|
36
|
+
const axiosInstance = getAxiosInstance();
|
|
37
|
+
const response = await axiosInstance.post<T>(`/${endpoint}`, data);
|
|
38
|
+
return response.data;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createFormData<T>(endpoint: string): OnSubmitFN<T> {
|
|
43
|
+
return async (data: T | FormData): Promise<T> => {
|
|
44
|
+
const axiosInstance = getAxiosInstance();
|
|
45
|
+
const response = await axiosInstance.post<T>(`/${endpoint}`, data, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'multipart/form-data',
|
|
48
|
+
},
|
|
41
49
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
return response.data;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function update<T>(endpoint: string, key = 'id'): OnSubmitFN<T> {
|
|
55
|
+
return async (data: T | FormData): Promise<T> => {
|
|
56
|
+
const axiosInstance = getAxiosInstance();
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const id = (data as any)[key];
|
|
59
|
+
const response = await axiosInstance.put<T>(`/${endpoint}/${id}`, data);
|
|
60
|
+
return response.data;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateFormData<T>(endpoint: string, key = 'id'): OnSubmitFN<T> {
|
|
65
|
+
return async (data: T | FormData): Promise<T> => {
|
|
66
|
+
const axiosInstance = getAxiosInstance();
|
|
67
|
+
const id = (data as any)[key];
|
|
68
|
+
const response = await axiosInstance.put<T>(`/${endpoint}/${id}`, data, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'multipart/form-data',
|
|
71
|
+
},
|
|
63
72
|
});
|
|
64
|
-
|
|
65
|
-
};
|
|
73
|
+
return response.data;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function updateSimple<T>(endpoint: string): OnSubmitFN<T> {
|
|
78
|
+
return async (data: T | FormData): Promise<T> => {
|
|
79
|
+
const axiosInstance = getAxiosInstance();
|
|
80
|
+
const response = await axiosInstance.put<T>(`/${endpoint}`, data);
|
|
81
|
+
return response.data;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function remove<T>(endpoint: string, key = 'id'): (data: T) => Promise<void> {
|
|
86
|
+
return async (data: T): Promise<void> => {
|
|
87
|
+
const axiosInstance = getAxiosInstance();
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
const id = (data as any)[key];
|
|
90
|
+
await axiosInstance
|
|
91
|
+
.delete<T>(`/${endpoint}/${id}`)
|
|
92
|
+
.then((res: any) => res.data)
|
|
93
|
+
.catch((err: AxiosError) => {
|
|
94
|
+
const messageError = err.response?.data as { message: string };
|
|
95
|
+
if (messageError?.message) {
|
|
96
|
+
throw new Error(messageError.message);
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DashboardGrid } from './DashboardGrid';
|
|
3
|
+
|
|
4
|
+
interface DashboardProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
columns?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Dashboard({ children, columns }: DashboardProps) {
|
|
10
|
+
return <DashboardGrid columns={columns}>{children}</DashboardGrid>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface DashboardGridProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
columns?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function DashboardGrid({ children, columns = 3 }: DashboardGridProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="dashboard-grid" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|