proje-react-panel 1.0.17 → 1.1.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/dist/components/Counter.d.ts +9 -0
- package/dist/components/DetailsPage.d.ts +7 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorComponent.d.ts +4 -0
- package/dist/components/LoadingScreen.d.ts +2 -0
- package/dist/components/Login.d.ts +13 -0
- package/dist/components/Panel.d.ts +1 -3
- package/dist/components/components/Checkbox.d.ts +3 -2
- package/dist/components/components/FormField.d.ts +5 -1
- package/dist/components/components/InnerForm.d.ts +8 -3
- package/dist/components/components/Label.d.ts +3 -2
- package/dist/components/components/Uploader.d.ts +8 -0
- package/dist/components/components/index.d.ts +1 -1
- package/dist/components/components/list/ListPage.d.ts +1 -1
- package/dist/components/form/Checkbox.d.ts +7 -0
- package/dist/components/form/FormField.d.ts +17 -0
- package/dist/components/form/FormPage.d.ts +6 -0
- package/dist/components/form/InnerForm.d.ts +10 -0
- package/dist/components/form/Label.d.ts +9 -0
- package/dist/components/form/Uploader.d.ts +8 -0
- package/dist/components/layout/Layout.d.ts +3 -4
- package/dist/components/layout/SideBar.d.ts +2 -3
- package/dist/components/list/CellField.d.ts +9 -0
- package/dist/components/list/Datagrid.d.ts +6 -8
- package/dist/components/list/FilterPopup.d.ts +7 -5
- package/dist/components/list/ListHeader.d.ts +11 -0
- package/dist/components/list/ListPage.d.ts +6 -0
- package/dist/components/pages/FormPage.d.ts +8 -2
- package/dist/decorators/details/Details.d.ts +11 -0
- package/dist/decorators/details/DetailsItem.d.ts +11 -0
- package/dist/decorators/details/getDetailsPageMeta.d.ts +8 -0
- package/dist/decorators/form/Form.d.ts +21 -5
- package/dist/decorators/form/Input.d.ts +7 -3
- package/dist/decorators/form/getFormPageMeta.d.ts +10 -0
- package/dist/decorators/list/Cell.d.ts +13 -1
- package/dist/decorators/list/List.d.ts +18 -1
- package/dist/decorators/list/cells/ImageCell.d.ts +9 -0
- package/dist/decorators/list/getListPageMeta.d.ts +8 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +12 -17
- package/dist/index.esm.js +1 -1
- package/dist/initPanel.d.ts +1 -1
- package/dist/store/store.d.ts +0 -3
- package/dist/types/AnyClass.d.ts +2 -1
- package/dist/types/getDetailsData.d.ts +1 -0
- package/dist/types/initPanelOptions.d.ts +0 -1
- package/package.json +1 -1
- package/src/assets/icons/svg/check.svg +4 -0
- package/src/assets/icons/svg/cross.svg +4 -0
- package/src/components/DetailsPage.tsx +55 -0
- package/src/components/{components/ErrorComponent.tsx → ErrorComponent.tsx} +1 -1
- package/src/components/{pages/Login.tsx → Login.tsx} +2 -2
- package/src/components/Panel.tsx +4 -5
- package/src/components/form/Checkbox.tsx +21 -0
- package/src/components/{components → form}/FormField.tsx +30 -22
- package/src/components/form/FormPage.tsx +32 -0
- package/src/components/form/InnerForm.tsx +85 -0
- package/src/components/form/Label.tsx +21 -0
- package/src/components/form/Uploader.tsx +66 -0
- package/src/components/layout/Layout.tsx +29 -32
- package/src/components/layout/SideBar.tsx +4 -13
- package/src/components/list/CellField.tsx +63 -0
- package/src/components/list/Datagrid.tsx +106 -0
- package/src/components/{components/list → list}/FilterPopup.tsx +13 -9
- package/src/components/list/ListHeader.tsx +47 -0
- package/src/components/{components/list → list}/ListPage.tsx +20 -82
- package/src/decorators/details/Details.ts +31 -0
- package/src/decorators/details/DetailsItem.ts +40 -0
- package/src/decorators/details/getDetailsPageMeta.ts +15 -0
- package/src/decorators/form/Form.ts +37 -12
- package/src/decorators/form/Input.ts +11 -4
- package/src/decorators/form/getFormPageMeta.ts +21 -0
- package/src/decorators/list/Cell.ts +41 -1
- package/src/decorators/list/List.ts +30 -6
- package/src/decorators/list/cells/ImageCell.ts +17 -0
- package/src/decorators/list/getListPageMeta.ts +16 -0
- package/src/index.ts +32 -24
- package/src/initPanel.ts +1 -4
- package/src/store/store.ts +0 -5
- package/src/styles/components/checkbox.scss +42 -0
- package/src/styles/components/uploader.scss +86 -0
- package/src/styles/details.scss +62 -0
- package/src/styles/form.scss +9 -11
- package/src/styles/index.scss +26 -12
- package/src/styles/list.scss +3 -1
- package/src/types/AnyClass.ts +2 -1
- package/src/types/initPanelOptions.ts +1 -3
- package/src/components/components/Checkbox.tsx +0 -9
- package/src/components/components/ImageUploader.tsx +0 -301
- package/src/components/components/InnerForm.tsx +0 -74
- package/src/components/components/Label.tsx +0 -15
- package/src/components/components/index.ts +0 -8
- package/src/components/components/list/Datagrid.tsx +0 -127
- package/src/components/pages/ControllerDetails.tsx +0 -37
- package/src/components/pages/FormPage.tsx +0 -34
- package/src/decorators/Crud.ts +0 -20
- package/src/decorators/form/FormOptions.ts +0 -8
- package/src/decorators/form/getFormFields.ts +0 -13
- package/src/decorators/list/GetCellFields.ts +0 -13
- package/src/decorators/list/ImageCell.ts +0 -13
- package/src/decorators/list/ListData.ts +0 -7
- package/src/decorators/list/getListFields.ts +0 -10
- package/src/styles/image-uploader.scss +0 -94
- package/src/types/Screen.ts +0 -4
- package/src/types/ScreenCreatorData.ts +0 -14
- package/src/utils/createScreens.ts +0 -5
- package/src/utils/getFields.ts +0 -22
- /package/src/components/{components/Counter.tsx → Counter.tsx} +0 -0
- /package/src/components/{components/ErrorBoundary.tsx → ErrorBoundary.tsx} +0 -0
- /package/src/components/{components/LoadingScreen.tsx → LoadingScreen.tsx} +0 -0
- /package/src/components/{components/list → list}/EmptyList.tsx +0 -0
- /package/src/components/{components/list → list}/Pagination.tsx +0 -0
- /package/src/components/{components/list → list}/index.ts +0 -0
@@ -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 {
|
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 '
|
7
|
-
import { getListFields } from '../../../decorators/list/getListFields';
|
6
|
+
import { AnyClass, AnyClassConstructor } from '../../types/AnyClass';
|
8
7
|
import { Pagination } from './Pagination';
|
9
|
-
import {
|
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({
|
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
|
-
[
|
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
|
-
|
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
|
-
|
98
|
+
listPageMeta={listPageMeta}
|
151
99
|
data={data}
|
152
|
-
onRemoveItem={async (
|
153
|
-
|
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
|
-
|
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,43 @@
|
|
1
|
-
import
|
2
|
-
import { AnyClass } from
|
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
|
6
|
+
const DETAILS_METADATA_KEY = 'DetailsMetaData';
|
7
|
+
export type OnSubmitFN<T> = (data: T | FormData) => Promise<T | FormData>;
|
5
8
|
|
6
|
-
|
9
|
+
interface FormOptions<T extends AnyClass> {
|
10
|
+
onSubmit: OnSubmitFN<T>;
|
11
|
+
getDetailsData?: GetDetailsDataFN<T>;
|
12
|
+
onSelectPreloader?: (
|
13
|
+
inputOptions: InputConfiguration
|
14
|
+
) => Promise<{ label: string; value: string }[]>;
|
15
|
+
redirectBackOnSuccess?: boolean;
|
16
|
+
type?: 'json' | 'formData';
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface FormConfiguration<T extends AnyClass> extends FormOptions<T> {
|
20
|
+
redirectBackOnSuccess: boolean;
|
21
|
+
type: 'json' | 'formData';
|
22
|
+
}
|
7
23
|
|
8
|
-
export function
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
24
|
+
export function Form<T extends AnyClass>(options?: FormOptions<T>): ClassDecorator {
|
25
|
+
return (target: Function) => {
|
26
|
+
if (options) {
|
27
|
+
Reflect.defineMetadata(DETAILS_METADATA_KEY, options, target);
|
28
|
+
}
|
29
|
+
};
|
14
30
|
}
|
15
31
|
|
16
|
-
export function getFormConfiguration
|
17
|
-
|
32
|
+
export function getFormConfiguration<T extends AnyClass, K extends AnyClassConstructor<T>>(
|
33
|
+
entityClass: K
|
34
|
+
): FormConfiguration<T> {
|
35
|
+
const formConfiguration = Reflect.getMetadata(DETAILS_METADATA_KEY, entityClass as Object);
|
36
|
+
if (!formConfiguration) {
|
37
|
+
throw new Error('Form decerator should be used on class');
|
38
|
+
}
|
39
|
+
return {
|
40
|
+
...formConfiguration,
|
41
|
+
type: formConfiguration.type ?? 'json',
|
42
|
+
};
|
18
43
|
}
|
@@ -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
|
|
@@ -15,7 +15,12 @@ export interface InputOptions {
|
|
15
15
|
placeholder?: string;
|
16
16
|
cancelPasswordValidationOnEdit?: boolean;
|
17
17
|
options?: { value: string; label: string }[];
|
18
|
-
|
18
|
+
optionsPreload?: boolean;
|
19
|
+
nestedFields?: InputConfiguration[];
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface InputConfiguration extends InputOptions {
|
23
|
+
name: string;
|
19
24
|
}
|
20
25
|
|
21
26
|
export function Input(options?: InputOptions): PropertyDecorator {
|
@@ -30,8 +35,10 @@ export function Input(options?: InputOptions): PropertyDecorator {
|
|
30
35
|
};
|
31
36
|
}
|
32
37
|
|
33
|
-
export function getInputFields<T extends AnyClass>(
|
34
|
-
|
38
|
+
export function getInputFields<T extends AnyClass>(
|
39
|
+
entityClass: AnyClassConstructor<T>
|
40
|
+
): InputConfiguration[] {
|
41
|
+
const prototype = (entityClass as any).prototype;
|
35
42
|
const inputFields: string[] = Reflect.getMetadata(INPUT_KEY, prototype) || [];
|
36
43
|
return inputFields.map(field => {
|
37
44
|
const fields = Reflect.getMetadata(`${INPUT_KEY.toString()}:${field}:options`, prototype) || {};
|
@@ -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
|
+
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import 'reflect-metadata';
|
2
|
+
import { AnyClass } from '../../types/AnyClass';
|
2
3
|
|
3
4
|
export const CELL_KEY = Symbol('cell');
|
4
5
|
|
@@ -11,15 +12,39 @@ export interface StaticSelectFilter extends Filter {
|
|
11
12
|
options: { value: string; label: string }[];
|
12
13
|
}
|
13
14
|
|
15
|
+
export type CellTypes = 'string' | 'date' | 'number' | 'boolean' | 'uuid';
|
16
|
+
export type ExtendedCellTypes = CellTypes | 'image';
|
17
|
+
|
14
18
|
export interface CellOptions {
|
15
19
|
name?: string;
|
16
20
|
title?: string;
|
17
|
-
type?:
|
21
|
+
type?: CellTypes;
|
18
22
|
placeHolder?: string;
|
19
23
|
filter?: Filter | StaticSelectFilter;
|
20
24
|
}
|
25
|
+
export interface ExtendedCellOptions extends Omit<CellOptions, 'type'> {
|
26
|
+
type?: ExtendedCellTypes;
|
27
|
+
}
|
28
|
+
|
29
|
+
export interface CellConfiguration extends Omit<CellOptions, 'type'> {
|
30
|
+
name: string;
|
31
|
+
type: ExtendedCellTypes;
|
32
|
+
}
|
21
33
|
|
22
34
|
export function Cell(options?: CellOptions): PropertyDecorator {
|
35
|
+
//TODO: reduce all similar code
|
36
|
+
return (target, propertyKey) => {
|
37
|
+
const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
|
38
|
+
Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
|
39
|
+
|
40
|
+
if (options) {
|
41
|
+
const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
|
42
|
+
Reflect.defineMetadata(keyString, options, target);
|
43
|
+
}
|
44
|
+
};
|
45
|
+
}
|
46
|
+
|
47
|
+
export function ExtendedCell(options?: ExtendedCellOptions): PropertyDecorator {
|
23
48
|
return (target, propertyKey) => {
|
24
49
|
const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
|
25
50
|
Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
|
@@ -30,3 +55,18 @@ export function Cell(options?: CellOptions): PropertyDecorator {
|
|
30
55
|
}
|
31
56
|
};
|
32
57
|
}
|
58
|
+
|
59
|
+
export function getCellFields<T extends AnyClass>(entityClass: T): CellConfiguration[] {
|
60
|
+
const prototype = (entityClass as any).prototype;
|
61
|
+
const inputFields: string[] = Reflect.getMetadata(CELL_KEY, prototype) || [];
|
62
|
+
|
63
|
+
return inputFields.map(field => {
|
64
|
+
const fields: CellOptions =
|
65
|
+
Reflect.getMetadata(`${CELL_KEY.toString()}:${field}:options`, prototype) || {};
|
66
|
+
return {
|
67
|
+
...fields,
|
68
|
+
name: fields.name || field,
|
69
|
+
type: fields.type as ExtendedCellTypes,
|
70
|
+
};
|
71
|
+
});
|
72
|
+
}
|
@@ -1,6 +1,22 @@
|
|
1
1
|
import 'reflect-metadata';
|
2
|
+
import { AnyClass } from '../../types/AnyClass';
|
2
3
|
|
3
|
-
const
|
4
|
+
const LIST_METADATA_KEY = 'ListMetaData';
|
5
|
+
|
6
|
+
export interface GetDataParams {
|
7
|
+
page?: number;
|
8
|
+
limit?: number;
|
9
|
+
filters?: Record<string, any>;
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface PaginatedResponse<T> {
|
13
|
+
data: T[];
|
14
|
+
total: number;
|
15
|
+
page: number;
|
16
|
+
limit: number;
|
17
|
+
}
|
18
|
+
|
19
|
+
export type GetDataForList<T> = (params: GetDataParams) => Promise<PaginatedResponse<T>>;
|
4
20
|
|
5
21
|
export interface ListHeaderOptions {
|
6
22
|
title?: string;
|
@@ -10,23 +26,31 @@ export interface ListHeaderOptions {
|
|
10
26
|
export interface ListCellOptions<T> {
|
11
27
|
details?: { path: string; label: string };
|
12
28
|
edit?: { path: string; label: string };
|
13
|
-
delete?: { label: string };
|
29
|
+
delete?: { label: string; onRemoveItem?: (item: T) => Promise<void> };
|
14
30
|
}
|
15
31
|
|
16
32
|
export interface ListOptions<T> {
|
33
|
+
getData: GetDataForList<T>;
|
17
34
|
headers?: ListHeaderOptions;
|
18
35
|
cells?: ((item: T) => ListCellOptions<T>) | ListCellOptions<T>;
|
19
36
|
}
|
20
37
|
|
38
|
+
export interface ListConfiguration<T> extends ListOptions<T> {}
|
39
|
+
|
21
40
|
export function List<T>(options?: ListOptions<T> | ((item: T) => ListOptions<T>)): ClassDecorator {
|
22
41
|
return (target: Function) => {
|
23
42
|
if (options) {
|
24
|
-
Reflect.defineMetadata(
|
43
|
+
Reflect.defineMetadata(LIST_METADATA_KEY, options, target);
|
25
44
|
}
|
26
45
|
};
|
27
46
|
}
|
28
47
|
|
29
|
-
export function
|
30
|
-
|
31
|
-
|
48
|
+
export function getListConfiguration<T extends AnyClass>(entityClass: T): ListConfiguration<T> {
|
49
|
+
const listConfiguration = Reflect.getMetadata(LIST_METADATA_KEY, entityClass);
|
50
|
+
if (!listConfiguration) {
|
51
|
+
throw new Error('List decerator should be used on class');
|
52
|
+
}
|
53
|
+
return {
|
54
|
+
...listConfiguration,
|
55
|
+
};
|
32
56
|
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import 'reflect-metadata';
|
2
|
+
import { Cell, CellConfiguration, CellOptions, ExtendedCell } from '../Cell';
|
3
|
+
|
4
|
+
export interface ImageCellOptions extends CellOptions {
|
5
|
+
baseUrl: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface ImageCellConfiguration extends CellConfiguration {
|
9
|
+
type: 'image';
|
10
|
+
}
|
11
|
+
|
12
|
+
export function ImageCell(options?: ImageCellOptions): PropertyDecorator {
|
13
|
+
return ExtendedCell({
|
14
|
+
...options,
|
15
|
+
type: 'image',
|
16
|
+
});
|
17
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { AnyClass } from '../../types/AnyClass';
|
2
|
+
import { getDetailsItemFields } from '../details/DetailsItem';
|
3
|
+
import { CellConfiguration, getCellFields } from './Cell';
|
4
|
+
import { getListConfiguration, ListConfiguration } from './List';
|
5
|
+
|
6
|
+
export interface ListPageMeta<T extends AnyClass> {
|
7
|
+
class: ListConfiguration<T>;
|
8
|
+
cells: CellConfiguration[];
|
9
|
+
}
|
10
|
+
|
11
|
+
export function getListPageMeta<T extends AnyClass>(entityClass: T): ListPageMeta<T> {
|
12
|
+
return {
|
13
|
+
class: getListConfiguration(entityClass),
|
14
|
+
cells: getCellFields<T>(entityClass),
|
15
|
+
};
|
16
|
+
}
|
package/src/index.ts
CHANGED
@@ -1,28 +1,36 @@
|
|
1
|
-
|
2
|
-
export
|
3
|
-
export type
|
4
|
-
export
|
5
|
-
export type { AnyClass } from './types/AnyClass';
|
6
|
-
export type {
|
7
|
-
GetDataForList,
|
8
|
-
PaginatedResponse,
|
9
|
-
GetDataParams,
|
10
|
-
} from './components/components/list/ListPage';
|
1
|
+
//DETAILS
|
2
|
+
export { DetailsPage } from './components/DetailsPage';
|
3
|
+
export { Details, type GetDetailsDataFN } from './decorators/details/Details';
|
4
|
+
export { DetailsItem } from './decorators/details/DetailsItem';
|
11
5
|
|
12
|
-
|
13
|
-
export {
|
14
|
-
export
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
export { List } from './decorators/list/List';
|
22
|
-
export { ImageCell } from './decorators/list/ImageCell';
|
23
|
-
export { Crud } from './decorators/Crud';
|
6
|
+
//LIST
|
7
|
+
export { ListPage } from './components/list/ListPage';
|
8
|
+
export {
|
9
|
+
List,
|
10
|
+
type GetDataForList,
|
11
|
+
type PaginatedResponse,
|
12
|
+
type GetDataParams,
|
13
|
+
} from './decorators/list/List';
|
14
|
+
export { ImageCell } from './decorators/list/cells/ImageCell';
|
24
15
|
export { Cell } from './decorators/list/Cell';
|
16
|
+
|
17
|
+
//FORM
|
18
|
+
export { FormPage } from './components/form/FormPage';
|
19
|
+
export { Form, type OnSubmitFN } from './decorators/form/Form';
|
25
20
|
export { Input } from './decorators/form/Input';
|
26
|
-
//
|
27
|
-
export { getFormFields } from './decorators/form/getFormFields';
|
21
|
+
//for nested form fields
|
28
22
|
export { getInputFields } from './decorators/form/Input';
|
23
|
+
|
24
|
+
//PANEL
|
25
|
+
export { Panel } from './components/Panel';
|
26
|
+
|
27
|
+
//DASHBOARD
|
28
|
+
export { Counter } from './components/Counter';
|
29
|
+
|
30
|
+
//LAYOUT
|
31
|
+
export { Layout } from './components/layout';
|
32
|
+
|
33
|
+
//AUTH
|
34
|
+
export { Login } from './components/Login';
|
35
|
+
|
36
|
+
//UTILS
|
package/src/initPanel.ts
CHANGED
@@ -1,6 +1,3 @@
|
|
1
1
|
import { InitPanelOptions } from './types/initPanelOptions';
|
2
|
-
import { useAppStore } from './store/store';
|
3
2
|
|
4
|
-
export function initPanel({
|
5
|
-
useAppStore.setState({ screenPaths });
|
6
|
-
}
|
3
|
+
export function initPanel({}: InitPanelOptions) {}
|
package/src/store/store.ts
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
2
2
|
import { createWithEqualityFn } from 'zustand/traditional';
|
3
3
|
import { shallow } from 'zustand/vanilla/shallow';
|
4
|
-
import { ScreenCreatorData } from '../types/ScreenCreatorData';
|
5
4
|
|
6
5
|
interface User {
|
7
6
|
username: string;
|
@@ -9,16 +8,12 @@ interface User {
|
|
9
8
|
|
10
9
|
interface AppState {
|
11
10
|
user: User | null;
|
12
|
-
screens: Record<string, ScreenCreatorData> | null;
|
13
|
-
screenPaths: Record<string, string>;
|
14
11
|
}
|
15
12
|
|
16
13
|
export const useAppStore = createWithEqualityFn<AppState>()(
|
17
14
|
persist(
|
18
15
|
_ => ({
|
19
|
-
screens: null,
|
20
16
|
user: null,
|
21
|
-
screenPaths: {},
|
22
17
|
}),
|
23
18
|
{
|
24
19
|
name: 'app-store-1',
|