proje-react-panel 1.0.17 → 1.1.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.
Files changed (119) hide show
  1. package/dist/components/Counter.d.ts +9 -0
  2. package/dist/components/DetailsPage.d.ts +7 -0
  3. package/dist/components/ErrorBoundary.d.ts +16 -0
  4. package/dist/components/ErrorComponent.d.ts +4 -0
  5. package/dist/components/LoadingScreen.d.ts +2 -0
  6. package/dist/components/Login.d.ts +13 -0
  7. package/dist/components/Panel.d.ts +1 -3
  8. package/dist/components/components/Checkbox.d.ts +3 -2
  9. package/dist/components/components/FormField.d.ts +5 -1
  10. package/dist/components/components/InnerForm.d.ts +8 -3
  11. package/dist/components/components/Label.d.ts +3 -2
  12. package/dist/components/components/Uploader.d.ts +8 -0
  13. package/dist/components/components/index.d.ts +1 -1
  14. package/dist/components/components/list/ListPage.d.ts +1 -1
  15. package/dist/components/form/Checkbox.d.ts +7 -0
  16. package/dist/components/form/FormField.d.ts +17 -0
  17. package/dist/components/form/FormPage.d.ts +6 -0
  18. package/dist/components/form/InnerForm.d.ts +10 -0
  19. package/dist/components/form/Label.d.ts +9 -0
  20. package/dist/components/form/Select.d.ts +8 -0
  21. package/dist/components/form/SelectStyles.d.ts +7 -0
  22. package/dist/components/form/Uploader.d.ts +8 -0
  23. package/dist/components/layout/Layout.d.ts +3 -4
  24. package/dist/components/layout/SideBar.d.ts +2 -3
  25. package/dist/components/list/CellField.d.ts +9 -0
  26. package/dist/components/list/Datagrid.d.ts +6 -8
  27. package/dist/components/list/FilterPopup.d.ts +7 -5
  28. package/dist/components/list/ListHeader.d.ts +11 -0
  29. package/dist/components/list/ListPage.d.ts +6 -0
  30. package/dist/components/pages/FormPage.d.ts +8 -2
  31. package/dist/decorators/details/Details.d.ts +11 -0
  32. package/dist/decorators/details/DetailsItem.d.ts +11 -0
  33. package/dist/decorators/details/getDetailsPageMeta.d.ts +8 -0
  34. package/dist/decorators/form/Form.d.ts +16 -5
  35. package/dist/decorators/form/Input.d.ts +14 -9
  36. package/dist/decorators/form/getFormPageMeta.d.ts +10 -0
  37. package/dist/decorators/form/inputs/SelectInput.d.ts +23 -0
  38. package/dist/decorators/list/Cell.d.ts +13 -1
  39. package/dist/decorators/list/List.d.ts +18 -1
  40. package/dist/decorators/list/cells/ImageCell.d.ts +9 -0
  41. package/dist/decorators/list/getListPageMeta.d.ts +8 -0
  42. package/dist/index.cjs.js +1 -1
  43. package/dist/index.d.ts +13 -17
  44. package/dist/index.esm.js +1 -1
  45. package/dist/initPanel.d.ts +1 -1
  46. package/dist/store/store.d.ts +0 -3
  47. package/dist/types/AnyClass.d.ts +2 -1
  48. package/dist/types/getDetailsData.d.ts +1 -0
  49. package/dist/types/initPanelOptions.d.ts +0 -1
  50. package/package.json +1 -1
  51. package/src/assets/icons/svg/check.svg +4 -0
  52. package/src/assets/icons/svg/cross.svg +4 -0
  53. package/src/components/DetailsPage.tsx +55 -0
  54. package/src/components/{components/ErrorComponent.tsx → ErrorComponent.tsx} +1 -1
  55. package/src/components/{pages/Login.tsx → Login.tsx} +2 -2
  56. package/src/components/Panel.tsx +4 -5
  57. package/src/components/form/Checkbox.tsx +21 -0
  58. package/src/components/{components → form}/FormField.tsx +22 -30
  59. package/src/components/form/FormPage.tsx +32 -0
  60. package/src/components/form/InnerForm.tsx +84 -0
  61. package/src/components/form/Label.tsx +21 -0
  62. package/src/components/form/Select.tsx +51 -0
  63. package/src/components/form/SelectStyles.ts +73 -0
  64. package/src/components/form/Uploader.tsx +66 -0
  65. package/src/components/layout/Layout.tsx +29 -32
  66. package/src/components/layout/SideBar.tsx +4 -13
  67. package/src/components/list/CellField.tsx +63 -0
  68. package/src/components/list/Datagrid.tsx +106 -0
  69. package/src/components/{components/list → list}/FilterPopup.tsx +13 -9
  70. package/src/components/list/ListHeader.tsx +47 -0
  71. package/src/components/{components/list → list}/ListPage.tsx +20 -82
  72. package/src/decorators/details/Details.ts +31 -0
  73. package/src/decorators/details/DetailsItem.ts +40 -0
  74. package/src/decorators/details/getDetailsPageMeta.ts +15 -0
  75. package/src/decorators/form/Form.ts +38 -12
  76. package/src/decorators/form/Input.ts +31 -9
  77. package/src/decorators/form/getFormPageMeta.ts +21 -0
  78. package/src/decorators/form/inputs/SelectInput.ts +19 -0
  79. package/src/decorators/list/Cell.ts +41 -1
  80. package/src/decorators/list/List.ts +30 -6
  81. package/src/decorators/list/cells/ImageCell.ts +17 -0
  82. package/src/decorators/list/getListPageMeta.ts +16 -0
  83. package/src/index.ts +33 -24
  84. package/src/initPanel.ts +1 -4
  85. package/src/store/store.ts +0 -5
  86. package/src/styles/components/checkbox.scss +42 -0
  87. package/src/styles/components/uploader.scss +86 -0
  88. package/src/styles/details.scss +62 -0
  89. package/src/styles/form.scss +9 -11
  90. package/src/styles/index.scss +26 -12
  91. package/src/styles/list.scss +3 -1
  92. package/src/types/AnyClass.ts +2 -1
  93. package/src/types/initPanelOptions.ts +1 -3
  94. package/src/components/components/Checkbox.tsx +0 -9
  95. package/src/components/components/ImageUploader.tsx +0 -301
  96. package/src/components/components/InnerForm.tsx +0 -74
  97. package/src/components/components/Label.tsx +0 -15
  98. package/src/components/components/index.ts +0 -8
  99. package/src/components/components/list/Datagrid.tsx +0 -127
  100. package/src/components/pages/ControllerDetails.tsx +0 -37
  101. package/src/components/pages/FormPage.tsx +0 -34
  102. package/src/decorators/Crud.ts +0 -20
  103. package/src/decorators/form/FormOptions.ts +0 -8
  104. package/src/decorators/form/getFormFields.ts +0 -13
  105. package/src/decorators/list/GetCellFields.ts +0 -13
  106. package/src/decorators/list/ImageCell.ts +0 -13
  107. package/src/decorators/list/ListData.ts +0 -7
  108. package/src/decorators/list/getListFields.ts +0 -10
  109. package/src/styles/image-uploader.scss +0 -94
  110. package/src/types/Screen.ts +0 -4
  111. package/src/types/ScreenCreatorData.ts +0 -14
  112. package/src/utils/createScreens.ts +0 -5
  113. package/src/utils/getFields.ts +0 -22
  114. /package/src/components/{components/Counter.tsx → Counter.tsx} +0 -0
  115. /package/src/components/{components/ErrorBoundary.tsx → ErrorBoundary.tsx} +0 -0
  116. /package/src/components/{components/LoadingScreen.tsx → LoadingScreen.tsx} +0 -0
  117. /package/src/components/{components/list → list}/EmptyList.tsx +0 -0
  118. /package/src/components/{components/list → list}/Pagination.tsx +0 -0
  119. /package/src/components/{components/list → list}/index.ts +0 -0
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router';
3
+ import { EmptyList } from './EmptyList';
4
+ import SearchIcon from '../../assets/icons/svg/search.svg';
5
+ import PencilIcon from '../../assets/icons/svg/pencil.svg';
6
+ import TrashIcon from '../../assets/icons/svg/trash.svg';
7
+ import { ListPageMeta } from '../../decorators/list/getListPageMeta';
8
+ import { ImageCellOptions } from '../../decorators/list/cells/ImageCell';
9
+ import { AnyClass } from '../../types/AnyClass';
10
+ import { CellField } from './CellField';
11
+
12
+ interface DatagridProps<T extends AnyClass> {
13
+ data: T[];
14
+ listPageMeta: ListPageMeta<T>;
15
+ onRemoveItem?: (item: T) => Promise<void>;
16
+ }
17
+
18
+ export function Datagrid<T extends AnyClass>({
19
+ data,
20
+ listPageMeta,
21
+ onRemoveItem,
22
+ }: DatagridProps<T>) {
23
+ const cells = listPageMeta.cells;
24
+ const listGeneralCells = data?.[0]
25
+ ? typeof listPageMeta.class.cells === 'function'
26
+ ? listPageMeta.class.cells?.(data[0])
27
+ : listPageMeta.class.cells
28
+ : null;
29
+
30
+ return (
31
+ <div className="datagrid">
32
+ {!data || data.length === 0 ? (
33
+ <EmptyList />
34
+ ) : (
35
+ <table className="datagrid-table">
36
+ <thead>
37
+ <tr>
38
+ {cells.map(cellOptions => (
39
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
40
+ ))}
41
+ {listGeneralCells?.details && <th>Details</th>}
42
+ {listGeneralCells?.edit && <th>Edit</th>}
43
+ {listGeneralCells?.delete && <th>Delete</th>}
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ {data.map((item, index) => {
48
+ const listCells = item
49
+ ? typeof listPageMeta.class.cells === 'function'
50
+ ? listPageMeta.class.cells?.(item)
51
+ : listPageMeta.class.cells
52
+ : null;
53
+ return (
54
+ <tr key={index}>
55
+ {cells.map(cellOptions => {
56
+ // @ts-ignore
57
+ const value = item[cellOptions.name];
58
+ return (
59
+ <CellField
60
+ key={cellOptions.name}
61
+ cellOptions={cellOptions}
62
+ item={item}
63
+ value={value}
64
+ />
65
+ );
66
+ })}
67
+ {listCells?.details && (
68
+ <td>
69
+ <Link to={listCells.details.path} className="util-cell-link">
70
+ <SearchIcon className="icon icon-search" />
71
+ <span className="util-cell-label">{listCells.details.label}</span>
72
+ </Link>
73
+ </td>
74
+ )}
75
+ {listCells?.edit && (
76
+ <td>
77
+ <Link to={listCells.edit.path} className="util-cell-link">
78
+ <PencilIcon className="icon icon-pencil" />
79
+ <span className="util-cell-label">{listCells.edit.label}</span>
80
+ </Link>
81
+ </td>
82
+ )}
83
+ {listCells?.delete && (
84
+ <td>
85
+ <a
86
+ onClick={() => {
87
+ listCells.delete?.onRemoveItem?.(item).then(() => {
88
+ onRemoveItem?.(item);
89
+ });
90
+ }}
91
+ className="util-cell-link util-cell-link-remove"
92
+ >
93
+ <TrashIcon className="icon icon-trash" />
94
+ <span className="util-cell-label">{listCells.delete.label}</span>
95
+ </a>
96
+ </td>
97
+ )}
98
+ </tr>
99
+ );
100
+ })}
101
+ </tbody>
102
+ </table>
103
+ )}
104
+ </div>
105
+ );
106
+ }
@@ -1,18 +1,19 @@
1
1
  import React, { useEffect, useMemo, useRef } from 'react';
2
- import { ListData } from '../../../decorators/list/ListData';
3
- import { CellOptions, StaticSelectFilter } from '../../../decorators/list/Cell';
2
+ import { CellConfiguration, StaticSelectFilter } from '../../decorators/list/Cell';
4
3
  import Select from 'react-select';
4
+ import { ListPageMeta } from '../../decorators/list/getListPageMeta';
5
+ import { AnyClass } from '../../types/AnyClass';
5
6
 
6
- interface FilterPopupProps<T> {
7
+ interface FilterPopupProps<T extends AnyClass> {
7
8
  isOpen: boolean;
8
9
  onClose: () => void;
9
10
  onApplyFilters: (filters: Record<string, string>) => void;
10
- listData: ListData<T>;
11
+ listPageMeta: ListPageMeta<T>;
11
12
  activeFilters?: Record<string, string>;
12
13
  }
13
14
 
14
15
  interface FilterFieldProps {
15
- field: CellOptions;
16
+ field: CellConfiguration;
16
17
  value: string;
17
18
  onChange: (value: string) => void;
18
19
  }
@@ -126,16 +127,19 @@ function FilterField({ field, value, onChange }: FilterFieldProps): React.ReactE
126
127
  }
127
128
  }
128
129
 
129
- export function FilterPopup<T>({
130
+ export function FilterPopup<T extends AnyClass>({
130
131
  isOpen,
131
132
  onClose,
132
133
  onApplyFilters,
133
- listData,
134
+ listPageMeta,
134
135
  activeFilters,
135
136
  }: FilterPopupProps<T>): React.ReactElement | null {
136
137
  const [filters, setFilters] = React.useState<Record<string, any>>(activeFilters ?? {});
137
138
  const popupRef = useRef<HTMLDivElement>(null);
138
- const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
139
+ const fields = useMemo(
140
+ () => listPageMeta.cells.filter(cell => !!cell.filter),
141
+ [listPageMeta.cells]
142
+ );
139
143
 
140
144
  useEffect(() => {
141
145
  const handleClickOutside = (event: MouseEvent) => {
@@ -177,7 +181,7 @@ export function FilterPopup<T>({
177
181
  </button>
178
182
  </div>
179
183
  <div className="filter-popup-content">
180
- {fields.map((field: CellOptions) => (
184
+ {fields.map((field: CellConfiguration) => (
181
185
  <div key={field.name} className="filter-field">
182
186
  <label htmlFor={field.name}>{field.title || field.name}</label>
183
187
  <FilterField
@@ -0,0 +1,47 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Link } from 'react-router';
3
+ import { AnyClass } from '../../types/AnyClass';
4
+ import CreateIcon from '../../assets/icons/svg/create.svg';
5
+ import FilterIcon from '../../assets/icons/svg/filter.svg';
6
+ import { ListPageMeta } from '../../decorators/list/getListPageMeta';
7
+
8
+ interface ListHeaderProps<T extends AnyClass> {
9
+ listPageMeta: ListPageMeta<T>;
10
+ filtered: boolean;
11
+ onFilterClick: () => void;
12
+ customHeader?: React.ReactNode;
13
+ }
14
+
15
+ export function ListHeader<T extends AnyClass>({
16
+ listPageMeta,
17
+ filtered,
18
+ onFilterClick,
19
+ customHeader,
20
+ }: ListHeaderProps<T>) {
21
+ const fields = useMemo(
22
+ () => listPageMeta.cells.filter(cell => !!cell.filter),
23
+ [listPageMeta.cells]
24
+ );
25
+
26
+ const header = listPageMeta.class.headers;
27
+ return (
28
+ <div className="list-header">
29
+ <div className="header-title">{header?.title || 'List'}</div>
30
+ {customHeader && <div className="header-custom">{customHeader}</div>}
31
+ <div className="header-actions">
32
+ {!!fields.length && (
33
+ <button onClick={onFilterClick} className="filter-button">
34
+ <FilterIcon className={`icon icon-filter ${filtered ? 'active' : ''}`} />
35
+ Filter
36
+ </button>
37
+ )}
38
+ {header?.create && (
39
+ <Link to={header.create.path} className="create-button">
40
+ <CreateIcon className="icon icon-create" />
41
+ {header.create.label}
42
+ </Link>
43
+ )}
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -1,85 +1,30 @@
1
1
  import React, { useMemo, useCallback, useEffect, useState } from 'react';
2
- import { Link, useParams, useNavigate } from 'react-router';
2
+ import { useParams, useNavigate } from 'react-router';
3
3
  import { Datagrid } from './Datagrid';
4
4
  import { ErrorComponent } from '../ErrorComponent';
5
5
  import { LoadingScreen } from '../LoadingScreen';
6
- import { AnyClass } from '../../../types/AnyClass';
7
- import { getListFields } from '../../../decorators/list/getListFields';
6
+ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
8
7
  import { Pagination } from './Pagination';
9
- import { ListData } from '../../../decorators/list/ListData';
10
- import CreateIcon from '../../../assets/icons/svg/create.svg';
11
- import FilterIcon from '../../../assets/icons/svg/filter.svg';
8
+ import { ListHeader } from './ListHeader';
12
9
  import { FilterPopup } from './FilterPopup';
13
-
14
- export interface GetDataParams {
15
- page?: number;
16
- limit?: number;
17
- filters?: Record<string, any>;
18
- }
19
-
20
- export interface PaginatedResponse<T> {
21
- data: T[];
22
- total: number;
23
- page: number;
24
- limit: number;
25
- }
26
-
27
- export type GetDataForList<T> = (params: GetDataParams) => Promise<PaginatedResponse<T>>;
28
-
29
- const ListHeader = <T extends AnyClass>({
30
- listData,
31
- filtered,
32
- onFilterClick,
33
- customHeader,
34
- }: {
35
- listData: ListData<T>;
36
- filtered: boolean;
37
- onFilterClick: () => void;
38
- customHeader?: React.ReactNode;
39
- }) => {
40
- const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
41
-
42
- const header = listData.list?.headers;
43
- return (
44
- <div className="list-header">
45
- <div className="header-title">{header?.title || 'List'}</div>
46
- {customHeader && <div className="header-custom">{customHeader}</div>}
47
- <div className="header-actions">
48
- {!!fields.length && (
49
- <button onClick={onFilterClick} className="filter-button">
50
- <FilterIcon className={`icon icon-filter ${filtered ? 'active' : ''}`} />
51
- Filter
52
- </button>
53
- )}
54
- {header?.create && (
55
- <Link to={header.create.path} className="create-button">
56
- <CreateIcon className="icon icon-create" />
57
- {header.create.label}
58
- </Link>
59
- )}
60
- </div>
61
- </div>
62
- );
63
- };
10
+ import { getListPageMeta } from '../../decorators/list/getListPageMeta';
64
11
 
65
12
  export function ListPage<T extends AnyClass>({
66
13
  model,
67
- getData,
68
- onRemoveItem,
69
14
  customHeader,
70
15
  }: {
71
- model: T;
72
- getData: GetDataForList<T>;
16
+ model: AnyClassConstructor<T>;
73
17
  customHeader?: React.ReactNode;
74
- onRemoveItem?: (item: T) => Promise<void>;
75
18
  }) {
19
+ const listPageMeta = useMemo(() => getListPageMeta(model), [model]);
20
+
76
21
  const [loading, setLoading] = useState(true);
77
- const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
78
22
  const [data, setData] = useState<any>(null);
79
23
  const [error, setError] = useState<unknown>(null);
24
+
25
+ const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
80
26
  const [isFilterOpen, setIsFilterOpen] = useState(false);
81
27
  const [activeFilters, setActiveFilters] = useState<Record<string, string>>();
82
- const listData = useMemo(() => getListFields(model), [model]);
83
28
  const params = useParams();
84
29
  const navigate = useNavigate();
85
30
 
@@ -87,7 +32,10 @@ export function ListPage<T extends AnyClass>({
87
32
  async (page: number, filters?: Record<string, string>) => {
88
33
  setLoading(true);
89
34
  try {
90
- const result = await getData({ page, filters: filters ?? activeFilters ?? {} });
35
+ const result = await listPageMeta.class.getData({
36
+ page,
37
+ filters: filters ?? activeFilters ?? {},
38
+ });
91
39
  setData(result.data);
92
40
  setPagination({
93
41
  total: result.total,
@@ -101,7 +49,7 @@ export function ListPage<T extends AnyClass>({
101
49
  setLoading(false);
102
50
  }
103
51
  },
104
- [getData, activeFilters]
52
+ [activeFilters, listPageMeta.class.getData]
105
53
  );
106
54
 
107
55
  useEffect(() => {
@@ -117,7 +65,7 @@ export function ListPage<T extends AnyClass>({
117
65
  if (activeFilters) {
118
66
  fetchData(parseInt(params.page as string) || 1, activeFilters);
119
67
  }
120
- }, [fetchData, params.page, activeFilters]);
68
+ }, [fetchData, params.page, activeFilters, listPageMeta.class.getData]);
121
69
 
122
70
  const handleFilterApply = (filters: Record<string, any>) => {
123
71
  setActiveFilters(filters);
@@ -141,26 +89,16 @@ export function ListPage<T extends AnyClass>({
141
89
  return (
142
90
  <div className="list">
143
91
  <ListHeader
144
- listData={listData}
92
+ listPageMeta={listPageMeta}
145
93
  filtered={!!(activeFilters && !!Object.keys(activeFilters).length)}
146
94
  onFilterClick={() => setIsFilterOpen(true)}
147
95
  customHeader={customHeader}
148
96
  />
149
97
  <Datagrid
150
- listData={listData}
98
+ listPageMeta={listPageMeta}
151
99
  data={data}
152
- onRemoveItem={async (item: T) => {
153
- if (onRemoveItem) {
154
- alert({
155
- title: 'Are you sure you want to delete this item?',
156
- message: 'This action cannot be undone.',
157
- onConfirm: async () => {
158
- await onRemoveItem(item);
159
- //setData(data.filter((d: T) => d.id !== item.id));
160
- await fetchData(pagination.page);
161
- },
162
- });
163
- }
100
+ onRemoveItem={async () => {
101
+ await fetchData(pagination.page);
164
102
  }}
165
103
  />
166
104
  <div className="list-footer">
@@ -171,7 +109,7 @@ export function ListPage<T extends AnyClass>({
171
109
  activeFilters={activeFilters}
172
110
  onClose={() => setIsFilterOpen(false)}
173
111
  onApplyFilters={handleFilterApply}
174
- listData={listData}
112
+ listPageMeta={listPageMeta}
175
113
  />
176
114
  </div>
177
115
  );
@@ -0,0 +1,31 @@
1
+ import 'reflect-metadata';
2
+ import { AnyClass } from '../../types/AnyClass';
3
+
4
+ const DETAILS_METADATA_KEY = 'DetailsMetaData';
5
+ export type GetDetailsDataFN<T> = (param: Record<string, string>) => Promise<T>;
6
+
7
+ interface DetailsOptions<T extends AnyClass> {
8
+ getDetailsData: GetDetailsDataFN<T>;
9
+ }
10
+
11
+ export interface DetailsConfiguration<T extends AnyClass> extends DetailsOptions<T> {}
12
+
13
+ export function Details<T extends AnyClass>(options?: DetailsOptions<T>): ClassDecorator {
14
+ return (target: Function) => {
15
+ if (options) {
16
+ Reflect.defineMetadata(DETAILS_METADATA_KEY, options, target);
17
+ }
18
+ };
19
+ }
20
+
21
+ export function getDetailsConfiguration<T extends AnyClass>(
22
+ entityClass: T
23
+ ): DetailsConfiguration<T> {
24
+ const detailsConfiguration = Reflect.getMetadata(DETAILS_METADATA_KEY, entityClass);
25
+ if (!detailsConfiguration) {
26
+ throw new Error('Details decerator should be used on class');
27
+ }
28
+ return {
29
+ ...detailsConfiguration,
30
+ };
31
+ }
@@ -0,0 +1,40 @@
1
+ import 'reflect-metadata';
2
+ import { AnyClass } from '../../types/AnyClass';
3
+
4
+ const DETAILS_ITEM_KEY = Symbol('detailsItem');
5
+
6
+ interface DetailsItemOptions {
7
+ //NOTE: all optional to support autoFields merging
8
+ name?: string;
9
+ }
10
+ export interface DetailsItemConfiguration extends DetailsItemOptions {
11
+ name: string;
12
+ }
13
+
14
+ export function DetailsItem(options?: DetailsItemOptions): PropertyDecorator {
15
+ return (target, propertyKey) => {
16
+ const existingInputs: string[] = Reflect.getMetadata(DETAILS_ITEM_KEY, target) || [];
17
+ Reflect.defineMetadata(DETAILS_ITEM_KEY, [...existingInputs, propertyKey.toString()], target);
18
+
19
+ if (options) {
20
+ const keyString = `${DETAILS_ITEM_KEY.toString()}:${propertyKey.toString()}:options`;
21
+ Reflect.defineMetadata(keyString, options, target);
22
+ }
23
+ };
24
+ }
25
+
26
+ export function getDetailsItemFields<T extends AnyClass>(
27
+ entityClass: T
28
+ ): DetailsItemConfiguration[] {
29
+ const prototype = (entityClass as any).prototype;
30
+ const inputFields: string[] = Reflect.getMetadata(DETAILS_ITEM_KEY, prototype) || [];
31
+
32
+ return inputFields.map(field => {
33
+ const fields: DetailsItemOptions =
34
+ Reflect.getMetadata(`${DETAILS_ITEM_KEY.toString()}:${field}:options`, prototype) || {};
35
+ return {
36
+ ...fields,
37
+ name: fields.name || field,
38
+ };
39
+ });
40
+ }
@@ -0,0 +1,15 @@
1
+ import { AnyClass } from '../../types/AnyClass';
2
+ import { DetailsItemConfiguration, getDetailsItemFields } from './DetailsItem';
3
+ import { DetailsConfiguration, getDetailsConfiguration } from './Details';
4
+
5
+ export interface DetailsPageMeta<T extends AnyClass> {
6
+ class: DetailsConfiguration<T>;
7
+ items: DetailsItemConfiguration[];
8
+ }
9
+
10
+ export function getDetailsPageMeta<T extends AnyClass>(entityClass: T): DetailsPageMeta<T> {
11
+ return {
12
+ class: getDetailsConfiguration(entityClass),
13
+ items: getDetailsItemFields<T>(entityClass),
14
+ };
15
+ }
@@ -1,18 +1,44 @@
1
- import "reflect-metadata";
2
- import { AnyClass } from "../../types/AnyClass";
1
+ import 'reflect-metadata';
2
+ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
3
+ import { GetDetailsDataFN } from '../details/Details';
4
+ import { InputConfiguration } from './Input';
3
5
 
4
- const FORM_METADATA_KEY = "FormMetadata"; // More descriptive name indicating it's a metadata key
6
+ const DETAILS_METADATA_KEY = 'DetailsMetaData';
7
+ export type OnSubmitFN<T> = (data: T | FormData) => Promise<T | FormData>;
5
8
 
6
- export interface FormConfiguration {} // Better describes that this is configuration/options for the form
9
+ interface FormOptions<T extends AnyClass> {
10
+ onSubmit: OnSubmitFN<T>;
11
+ getDetailsData?: GetDetailsDataFN<T>;
12
+ redirectBackOnSuccess?: boolean;
13
+ type?: 'json' | 'formData';
14
+ }
15
+
16
+ export interface FormConfiguration<T extends AnyClass> extends FormOptions<T> {
17
+ redirectBackOnSuccess: boolean;
18
+ type: 'json' | 'formData';
19
+ }
7
20
 
8
- export function FormDecorator(options?: FormConfiguration): ClassDecorator {
9
- return (target: Function) => {
10
- if (options) {
11
- Reflect.defineMetadata(FORM_METADATA_KEY, options, target);
12
- }
13
- };
21
+ export function Form<T extends AnyClass>(options?: FormOptions<T>): ClassDecorator {
22
+ return (target: Function) => {
23
+ if (options) {
24
+ Reflect.defineMetadata(DETAILS_METADATA_KEY, options, target);
25
+ }
26
+ };
14
27
  }
15
28
 
16
- export function getFormConfiguration(entityClass: AnyClass): FormConfiguration | undefined {
17
- return Reflect.getMetadata(FORM_METADATA_KEY, entityClass);
29
+ export function getFormConfiguration<T extends AnyClass, K extends AnyClassConstructor<T>>(
30
+ entityClass: K
31
+ ): FormConfiguration<T> {
32
+ const formOptions: FormOptions<T> = Reflect.getMetadata(
33
+ DETAILS_METADATA_KEY,
34
+ entityClass as Object
35
+ );
36
+ if (!formOptions) {
37
+ throw new Error('Form decerator should be used on class');
38
+ }
39
+ return {
40
+ ...formOptions,
41
+ type: formOptions.type ?? 'json',
42
+ redirectBackOnSuccess: formOptions.redirectBackOnSuccess ?? true,
43
+ };
18
44
  }
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import { AnyClass } from '../../types/AnyClass';
2
+ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
3
3
 
4
4
  const INPUT_KEY = Symbol('input');
5
5
 
@@ -7,15 +7,25 @@ const isFieldSensitive = (fieldName: string): boolean => {
7
7
  return ['password'].some(term => fieldName.toLowerCase().includes(term));
8
8
  };
9
9
 
10
+ export type InputTypes = 'input' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden' | 'nested';
11
+ export type ExtendedInputTypes = InputTypes | 'select';
12
+
10
13
  export interface InputOptions {
11
- type?: 'input' | 'select' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden' | 'nested';
14
+ type?: InputTypes;
12
15
  inputType?: 'text' | 'email' | 'tel' | 'password' | 'number' | 'date';
13
16
  name?: string;
14
17
  label?: string;
15
18
  placeholder?: string;
16
- cancelPasswordValidationOnEdit?: boolean;
17
- options?: { value: string; label: string }[];
18
- nestedFields?: InputOptions[];
19
+ nestedFields?: InputConfiguration[];
20
+ }
21
+
22
+ export interface ExtendedInputOptions extends Omit<InputOptions, 'type'> {
23
+ type: ExtendedInputTypes;
24
+ }
25
+
26
+ export interface InputConfiguration extends Omit<InputOptions, 'type'> {
27
+ name: string;
28
+ type: ExtendedInputTypes;
19
29
  }
20
30
 
21
31
  export function Input(options?: InputOptions): PropertyDecorator {
@@ -30,8 +40,22 @@ export function Input(options?: InputOptions): PropertyDecorator {
30
40
  };
31
41
  }
32
42
 
33
- export function getInputFields<T extends AnyClass>(entityClass: T): InputOptions[] {
34
- const prototype = entityClass.prototype;
43
+ export function ExtendedInput(options?: ExtendedInputOptions): PropertyDecorator {
44
+ return (target, propertyKey) => {
45
+ const existingInputs: string[] = Reflect.getMetadata(INPUT_KEY, target) || [];
46
+ Reflect.defineMetadata(INPUT_KEY, [...existingInputs, propertyKey.toString()], target);
47
+
48
+ if (options) {
49
+ const keyString = `${INPUT_KEY.toString()}:${propertyKey.toString()}:options`;
50
+ Reflect.defineMetadata(keyString, options, target);
51
+ }
52
+ };
53
+ }
54
+
55
+ export function getInputFields<T extends AnyClass>(
56
+ entityClass: AnyClassConstructor<T>
57
+ ): InputConfiguration[] {
58
+ const prototype = (entityClass as any).prototype;
35
59
  const inputFields: string[] = Reflect.getMetadata(INPUT_KEY, prototype) || [];
36
60
  return inputFields.map(field => {
37
61
  const fields = Reflect.getMetadata(`${INPUT_KEY.toString()}:${field}:options`, prototype) || {};
@@ -46,8 +70,6 @@ export function getInputFields<T extends AnyClass>(entityClass: T): InputOptions
46
70
  inputType: inputType,
47
71
  type: fields?.type ?? 'input',
48
72
  selectOptions: fields?.selectOptions ?? [],
49
- cancelPasswordValidationOnEdit:
50
- fields?.cancelPasswordValidationOnEdit ?? inputType === 'password',
51
73
  };
52
74
  });
53
75
  }
@@ -0,0 +1,21 @@
1
+ import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
2
+ import { getInputFields, InputConfiguration } from './Input';
3
+ import { FormConfiguration, getFormConfiguration } from './Form';
4
+ import { classValidatorResolver } from '@hookform/resolvers/class-validator';
5
+ import { Resolver } from 'react-hook-form';
6
+
7
+ export interface FormPageMeta<T extends AnyClass> {
8
+ resolver: Resolver<T>;
9
+ class: FormConfiguration<T>;
10
+ inputs: InputConfiguration[];
11
+ }
12
+
13
+ export function getFormPageMeta<T extends AnyClass, K extends AnyClassConstructor<T>>(
14
+ entityClass: K
15
+ ): FormPageMeta<T> {
16
+ return {
17
+ resolver: classValidatorResolver(entityClass),
18
+ class: getFormConfiguration(entityClass),
19
+ inputs: getInputFields(entityClass),
20
+ };
21
+ }
@@ -0,0 +1,19 @@
1
+ import { ExtendedInput, ExtendedInputOptions, InputConfiguration, InputOptions } from '../Input';
2
+
3
+ export interface SelectInputOptions extends InputOptions {
4
+ onSelectPreloader?: () => Promise<{ label: string; value: any }[]>;
5
+ defaultOptions?: { value: any; label: string }[];
6
+ }
7
+
8
+ export interface SelectInputConfiguration extends InputConfiguration {
9
+ type: 'select';
10
+ onSelectPreloader?: () => Promise<{ label: string; value: string }[]>;
11
+ defaultOptions?: { value: any; label: string }[];
12
+ }
13
+
14
+ export function SelectInput(options?: SelectInputOptions): PropertyDecorator {
15
+ return ExtendedInput({
16
+ ...options,
17
+ type: 'select',
18
+ } as ExtendedInputOptions);
19
+ }