proje-react-panel 1.4.0 → 1.5.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/.vscode/launch.json +9 -0
- package/dist/components/list/CellField.d.ts +2 -2
- package/dist/components/list/cells/LinkCell.d.ts +8 -0
- package/dist/decorators/details/Details.d.ts +1 -1
- package/dist/decorators/list/Cell.d.ts +1 -1
- package/dist/decorators/list/List.d.ts +5 -1
- package/dist/decorators/list/cells/LinkCell.d.ts +13 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +1 -1
- package/dist/services/DataService.d.ts +1 -0
- package/dist/store/store.d.ts +2 -0
- package/package.json +1 -1
- package/src/assets/icons/svg/down-arrow-backup-2.svg +3 -0
- package/src/components/DetailsPage.tsx +5 -1
- package/src/components/list/CellField.tsx +8 -2
- package/src/components/list/Datagrid.tsx +62 -41
- package/src/components/list/ListPage.tsx +3 -0
- package/src/components/list/cells/ImageCell.tsx +1 -0
- package/src/components/list/cells/LinkCell.tsx +30 -0
- package/src/decorators/details/Details.ts +1 -1
- package/src/decorators/list/Cell.ts +1 -1
- package/src/decorators/list/List.ts +8 -2
- package/src/decorators/list/cells/LinkCell.ts +22 -0
- package/src/index.ts +3 -1
- package/src/services/DataService.ts +25 -5
- package/src/store/store.ts +7 -0
- package/src/styles/list.scss +56 -0
@@ -33,10 +33,14 @@ export function DetailsPage<T extends AnyClass>({ model, CustomHeader }: Details
|
|
33
33
|
}, [params, detailsClass.getDetailsData, detailsClass]);
|
34
34
|
|
35
35
|
useEffect(() => {
|
36
|
+
if (!detailsClass.primaryId) {
|
37
|
+
return;
|
38
|
+
}
|
39
|
+
|
36
40
|
setData(data => {
|
37
41
|
if (data) {
|
38
42
|
const detailsData =
|
39
|
-
allDetailsData?.[detailsClass.key]?.[data[detailsClass.primaryId] as string] ??
|
43
|
+
allDetailsData?.[detailsClass.key]?.[data[detailsClass.primaryId!] as string] ??
|
40
44
|
({} as Partial<T>);
|
41
45
|
|
42
46
|
return { ...data, ...detailsData };
|
@@ -7,18 +7,21 @@ import { ImageCell } from './cells/ImageCell';
|
|
7
7
|
import { UUIDCell } from './cells/UUIDCell';
|
8
8
|
import { DefaultCell } from './cells/DefaultCell';
|
9
9
|
import { DownloadCell } from './cells/DownloadCell';
|
10
|
+
import { LinkCell } from './cells/LinkCell';
|
10
11
|
|
11
12
|
interface CellFieldProps<T extends AnyClass> {
|
12
13
|
configuration: CellConfiguration;
|
13
|
-
|
14
|
+
item: T;
|
14
15
|
}
|
15
16
|
|
16
17
|
export function CellField<T extends AnyClass>({
|
17
18
|
configuration,
|
18
|
-
|
19
|
+
item,
|
19
20
|
}: CellFieldProps<T>): React.ReactElement {
|
20
21
|
let render;
|
21
22
|
|
23
|
+
const value = item[configuration.name];
|
24
|
+
|
22
25
|
switch (configuration.type) {
|
23
26
|
case 'boolean':
|
24
27
|
render = <BooleanCell value={value} />;
|
@@ -35,6 +38,9 @@ export function CellField<T extends AnyClass>({
|
|
35
38
|
case 'download':
|
36
39
|
render = <DownloadCell value={value} configuration={configuration} />;
|
37
40
|
break;
|
41
|
+
case 'link':
|
42
|
+
render = <LinkCell item={item} configuration={configuration} />;
|
43
|
+
break;
|
38
44
|
default:
|
39
45
|
render = <DefaultCell value={value} configuration={configuration} />;
|
40
46
|
break;
|
@@ -3,11 +3,13 @@ import { Link } from 'react-router';
|
|
3
3
|
import { EmptyList } from './EmptyList';
|
4
4
|
import SearchIcon from '../../assets/icons/svg/search.svg';
|
5
5
|
import PencilIcon from '../../assets/icons/svg/pencil.svg';
|
6
|
+
import DownArrowIcon from '../../assets/icons/svg/down-arrow-backup-2.svg';
|
6
7
|
import TrashIcon from '../../assets/icons/svg/trash.svg';
|
7
8
|
import { ListPageMeta } from '../../decorators/list/getListPageMeta';
|
8
9
|
import { AnyClass } from '../../types/AnyClass';
|
9
10
|
import { CellField } from './CellField';
|
10
11
|
import { CellConfiguration } from '../../decorators/list/Cell';
|
12
|
+
import { useAppStore } from '../../store/store';
|
11
13
|
|
12
14
|
interface DatagridProps<T extends AnyClass> {
|
13
15
|
data: T[];
|
@@ -21,6 +23,7 @@ export function Datagrid<T extends AnyClass>({
|
|
21
23
|
onRemoveItem,
|
22
24
|
}: DatagridProps<T>) {
|
23
25
|
const cells = listPageMeta.cells;
|
26
|
+
const listData = useAppStore(state => state.listData[listPageMeta.class.key]);
|
24
27
|
const listGeneralCells = data?.[0]
|
25
28
|
? typeof listPageMeta.class.cells === 'function'
|
26
29
|
? listPageMeta.class.cells?.(data[0])
|
@@ -38,9 +41,9 @@ export function Datagrid<T extends AnyClass>({
|
|
38
41
|
{cells.map(cellOptions => (
|
39
42
|
<th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
|
40
43
|
))}
|
41
|
-
{listGeneralCells?.details
|
42
|
-
|
43
|
-
|
44
|
+
{(listGeneralCells?.details ||
|
45
|
+
listGeneralCells?.edit ||
|
46
|
+
listGeneralCells?.delete) && <th>Actions</th>}
|
44
47
|
</tr>
|
45
48
|
</thead>
|
46
49
|
<tbody>
|
@@ -50,55 +53,73 @@ export function Datagrid<T extends AnyClass>({
|
|
50
53
|
? listPageMeta.class.cells?.(item)
|
51
54
|
: listPageMeta.class.cells
|
52
55
|
: null;
|
56
|
+
const listDataItem = listPageMeta.class.primaryId
|
57
|
+
? (listData?.[item[listPageMeta.class.primaryId!] as string] as
|
58
|
+
| Record<string, unknown>
|
59
|
+
| undefined)
|
60
|
+
: null;
|
53
61
|
return (
|
54
62
|
<tr key={index}>
|
55
63
|
{cells.map((configuration: CellConfiguration) => {
|
56
|
-
const value = item[configuration.name];
|
57
64
|
return (
|
58
65
|
<CellField
|
59
66
|
key={configuration.name}
|
67
|
+
item={{
|
68
|
+
...(listDataItem ?? {}),
|
69
|
+
...item,
|
70
|
+
}}
|
60
71
|
configuration={configuration}
|
61
|
-
value={value}
|
62
72
|
/>
|
63
73
|
);
|
64
74
|
})}
|
65
|
-
{listCells?.details && (
|
66
|
-
<td>
|
67
|
-
<Link to={listCells.details.path} className="util-cell-link">
|
68
|
-
<SearchIcon className="icon icon-search" />
|
69
|
-
<span className="util-cell-label">{listCells.details.label}</span>
|
70
|
-
</Link>
|
71
|
-
</td>
|
72
|
-
)}
|
73
|
-
{listCells?.edit && (
|
75
|
+
{(listCells?.details || listCells?.edit || listCells?.delete) && (
|
74
76
|
<td>
|
75
|
-
<
|
76
|
-
<
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
77
|
+
<div className="util-cell-actions">
|
78
|
+
<p className="util-cell-actions-label">
|
79
|
+
Actions <DownArrowIcon className="icon icon-down" />
|
80
|
+
</p>
|
81
|
+
<ul className="util-cell-actions-list">
|
82
|
+
{listCells?.details && (
|
83
|
+
<li>
|
84
|
+
<Link to={listCells.details.path} className="util-cell-link">
|
85
|
+
<SearchIcon className="icon icon-search" />
|
86
|
+
<span className="util-cell-label">{listCells.details.label}</span>
|
87
|
+
</Link>
|
88
|
+
</li>
|
89
|
+
)}
|
90
|
+
{listCells?.edit && (
|
91
|
+
<li>
|
92
|
+
<Link to={listCells.edit.path} className="util-cell-link">
|
93
|
+
<PencilIcon className="icon icon-pencil" />
|
94
|
+
<span className="util-cell-label">{listCells.edit.label}</span>
|
95
|
+
</Link>
|
96
|
+
</li>
|
97
|
+
)}
|
98
|
+
{listCells?.delete && (
|
99
|
+
<li>
|
100
|
+
<a
|
101
|
+
onClick={() => {
|
102
|
+
listCells.delete
|
103
|
+
?.onRemoveItem?.(item)
|
104
|
+
.then(() => {
|
105
|
+
onRemoveItem?.(item);
|
106
|
+
})
|
107
|
+
.catch((e: unknown) => {
|
108
|
+
console.error(e);
|
109
|
+
const message =
|
110
|
+
e instanceof Error ? e.message : 'Error deleting item';
|
111
|
+
alert(message);
|
112
|
+
});
|
113
|
+
}}
|
114
|
+
className="util-cell-link util-cell-link-remove"
|
115
|
+
>
|
116
|
+
<TrashIcon className="icon icon-trash" />
|
117
|
+
<span className="util-cell-label">{listCells.delete.label}</span>
|
118
|
+
</a>
|
119
|
+
</li>
|
120
|
+
)}
|
121
|
+
</ul>
|
122
|
+
</div>
|
102
123
|
</td>
|
103
124
|
)}
|
104
125
|
</tr>
|
@@ -106,6 +106,9 @@ export function ListPage<T extends AnyClass>({
|
|
106
106
|
/>
|
107
107
|
<div className="list-footer">
|
108
108
|
<Pagination pagination={pagination} onPageChange={fetchData} />
|
109
|
+
<p className="list-footer-total">
|
110
|
+
TOTAL: {pagination.total} / SHOWING: {pagination.limit}
|
111
|
+
</p>
|
109
112
|
</div>
|
110
113
|
<FilterPopup
|
111
114
|
isOpen={isFilterOpen}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { CellConfiguration } from '../../../decorators/list/Cell';
|
3
|
+
import { LinkCellConfiguration } from '../../../decorators/list/cells/LinkCell';
|
4
|
+
import { Link } from 'react-router';
|
5
|
+
|
6
|
+
interface LinkCellProps<T> {
|
7
|
+
item: T;
|
8
|
+
configuration: CellConfiguration;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function LinkCell<T>({ item, configuration }: LinkCellProps<T>) {
|
12
|
+
const linkConfiguration = configuration as LinkCellConfiguration<T>;
|
13
|
+
const value = item[configuration.name as keyof T] ?? 'Link';
|
14
|
+
|
15
|
+
return (
|
16
|
+
<Link to={linkConfiguration.path ?? linkConfiguration.url ?? ''}>
|
17
|
+
{linkConfiguration.onClick ? (
|
18
|
+
<button
|
19
|
+
onClick={() => {
|
20
|
+
linkConfiguration.onClick?.(item as T);
|
21
|
+
}}
|
22
|
+
>
|
23
|
+
{value?.toString()}
|
24
|
+
</button>
|
25
|
+
) : (
|
26
|
+
value?.toString() || linkConfiguration.placeHolder
|
27
|
+
)}
|
28
|
+
</Link>
|
29
|
+
);
|
30
|
+
}
|
@@ -7,7 +7,7 @@ export type GetDetailsDataFN<T> = (param: Record<string, string>) => Promise<T>;
|
|
7
7
|
interface DetailsOptions<T extends AnyClass> {
|
8
8
|
getDetailsData: GetDetailsDataFN<T>;
|
9
9
|
key?: string;
|
10
|
-
primaryId
|
10
|
+
primaryId?: keyof T;
|
11
11
|
}
|
12
12
|
|
13
13
|
export type DetailsConfiguration<T extends AnyClass> = DetailsOptions<T> & {
|
@@ -14,7 +14,7 @@ export interface StaticSelectFilter extends Filter {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
export type CellTypes = 'string' | 'date' | 'number' | 'boolean' | 'uuid';
|
17
|
-
export type ExtendedCellTypes = CellTypes | 'image' | 'download';
|
17
|
+
export type ExtendedCellTypes = CellTypes | 'image' | 'download' | 'link';
|
18
18
|
|
19
19
|
export interface CellOptions {
|
20
20
|
name?: string;
|
@@ -35,9 +35,13 @@ export interface ListOptions<T> {
|
|
35
35
|
getData: GetDataForList<T>;
|
36
36
|
headers?: ListHeaderOptions;
|
37
37
|
cells?: ((item: T) => ListCellOptions<T>) | ListCellOptions<T>;
|
38
|
+
primaryId?: string;
|
39
|
+
key?: string;
|
38
40
|
}
|
39
41
|
|
40
|
-
export type ListConfiguration<T> = ListOptions<T
|
42
|
+
export type ListConfiguration<T> = ListOptions<T> & {
|
43
|
+
key: string;
|
44
|
+
};
|
41
45
|
|
42
46
|
export function List<T>(options?: ListOptions<T> | ((item: T) => ListOptions<T>)): ClassDecorator {
|
43
47
|
return (target: object) => {
|
@@ -50,11 +54,13 @@ export function List<T>(options?: ListOptions<T> | ((item: T) => ListOptions<T>)
|
|
50
54
|
export function getListConfiguration<T extends AnyClass>(
|
51
55
|
entityClass: AnyClassConstructor<T>
|
52
56
|
): ListConfiguration<T> {
|
53
|
-
const listConfiguration = Reflect.getMetadata(LIST_METADATA_KEY, entityClass);
|
57
|
+
const listConfiguration: ListOptions<T> = Reflect.getMetadata(LIST_METADATA_KEY, entityClass);
|
54
58
|
if (!listConfiguration) {
|
55
59
|
throw new Error('List decerator should be used on class');
|
56
60
|
}
|
57
61
|
return {
|
58
62
|
...listConfiguration,
|
63
|
+
primaryId: listConfiguration.primaryId,
|
64
|
+
key: listConfiguration.key || entityClass.name,
|
59
65
|
};
|
60
66
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { CellConfiguration, CellOptions } from '../Cell';
|
2
|
+
import { ExtendedCell } from '../ExtendedCell';
|
3
|
+
|
4
|
+
export interface LinkCellOptions<T> extends Omit<CellOptions, 'type'> {
|
5
|
+
url?: string;
|
6
|
+
path?: string;
|
7
|
+
onClick?: (data: T) => void;
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface LinkCellConfiguration<T> extends CellConfiguration {
|
11
|
+
type: 'link';
|
12
|
+
url?: string;
|
13
|
+
path?: string;
|
14
|
+
onClick?: (data: T) => void;
|
15
|
+
}
|
16
|
+
|
17
|
+
export function LinkCell<T>(options?: LinkCellOptions<T>): PropertyDecorator {
|
18
|
+
return ExtendedCell(options, (_, options) => ({
|
19
|
+
...options,
|
20
|
+
type: 'link',
|
21
|
+
}));
|
22
|
+
}
|
package/src/index.ts
CHANGED
@@ -13,13 +13,14 @@ export {
|
|
13
13
|
} from './decorators/list/List';
|
14
14
|
export { ImageCell } from './decorators/list/cells/ImageCell';
|
15
15
|
export { Cell } from './decorators/list/Cell';
|
16
|
+
export { DownloadCell } from './decorators/list/cells/DownloadCell';
|
17
|
+
export { LinkCell } from './decorators/list/cells/LinkCell';
|
16
18
|
|
17
19
|
//FORM
|
18
20
|
export { FormPage } from './components/form/FormPage';
|
19
21
|
export { Form, type OnSubmitFN } from './decorators/form/Form';
|
20
22
|
export { Input } from './decorators/form/Input';
|
21
23
|
export { SelectInput } from './decorators/form/inputs/SelectInput';
|
22
|
-
export { DownloadCell } from './decorators/list/cells/DownloadCell';
|
23
24
|
//for nested form fields
|
24
25
|
export { getInputFields } from './decorators/form/Input';
|
25
26
|
|
@@ -39,3 +40,4 @@ export { logout } from './utils/logout';
|
|
39
40
|
|
40
41
|
//SERVICES
|
41
42
|
export { updateDetailsData } from './services/DataService';
|
43
|
+
export { updateListData } from './services/DataService';
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { getDetailsPageMeta } from '../decorators/details/getDetailsPageMeta';
|
2
2
|
import { AnyClass, AnyClassConstructor } from '../types/AnyClass';
|
3
3
|
import { useAppStore } from '../store/store';
|
4
|
+
import { getListPageMeta } from '../decorators/list/getListPageMeta';
|
4
5
|
|
5
6
|
export function updateDetailsData<T extends AnyClass>(
|
6
7
|
model: AnyClassConstructor<T>,
|
@@ -8,11 +9,30 @@ export function updateDetailsData<T extends AnyClass>(
|
|
8
9
|
) {
|
9
10
|
const { class: detailsClass } = getDetailsPageMeta(model);
|
10
11
|
const key = detailsClass.key;
|
11
|
-
|
12
|
-
|
13
|
-
if (!data[id]) {
|
14
|
-
throw new Error(`Id ${id} not found in data`);
|
12
|
+
if (!detailsClass.primaryId) {
|
13
|
+
throw new Error('Primary id is required to use this utility function');
|
15
14
|
}
|
16
15
|
|
17
|
-
|
16
|
+
if (!data[detailsClass.primaryId]) {
|
17
|
+
throw new Error(`Id ${detailsClass.primaryId} not found in data`);
|
18
|
+
}
|
19
|
+
|
20
|
+
useAppStore.getState().updateDetailsData(key, data[detailsClass.primaryId]?.toString(), data);
|
21
|
+
}
|
22
|
+
|
23
|
+
export function updateListData<T extends AnyClass>(
|
24
|
+
model: AnyClassConstructor<T>,
|
25
|
+
data: Partial<T>
|
26
|
+
) {
|
27
|
+
const { class: listClass } = getListPageMeta(model);
|
28
|
+
const key = listClass.key;
|
29
|
+
if (!listClass.primaryId) {
|
30
|
+
throw new Error('Primary id is required to use this utility function');
|
31
|
+
}
|
32
|
+
|
33
|
+
if (!data[listClass.primaryId]) {
|
34
|
+
throw new Error(`Id ${listClass.primaryId} not found in data`);
|
35
|
+
}
|
36
|
+
|
37
|
+
useAppStore.getState().updateListData(key, data[listClass.primaryId]?.toString(), data);
|
18
38
|
}
|
package/src/store/store.ts
CHANGED
@@ -6,6 +6,8 @@ import { User } from '../types/User';
|
|
6
6
|
interface AppState {
|
7
7
|
detailsData: Record<string, Record<string, unknown>>;
|
8
8
|
updateDetailsData: (key: string, id: string, data: unknown) => void;
|
9
|
+
listData: Record<string, Record<string, unknown>>;
|
10
|
+
updateListData: (key: string, id: string, data: unknown) => void;
|
9
11
|
user: User | null;
|
10
12
|
login: (user: User) => void;
|
11
13
|
logout: () => void;
|
@@ -25,6 +27,11 @@ export const useAppStore = createWithEqualityFn<AppState>()(
|
|
25
27
|
[key]: { ...get().detailsData[key], [id]: data },
|
26
28
|
},
|
27
29
|
}),
|
30
|
+
listData: {},
|
31
|
+
updateListData: (key: string, id: string, data: unknown) =>
|
32
|
+
set({
|
33
|
+
listData: { ...get().listData, [key]: { ...get().listData[key], [id]: data } },
|
34
|
+
}),
|
28
35
|
}),
|
29
36
|
{
|
30
37
|
name: 'app-store-1',
|
package/src/styles/list.scss
CHANGED
@@ -64,10 +64,16 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
64
64
|
align-items: center;
|
65
65
|
height: $footer-height;
|
66
66
|
font-size: 24px;
|
67
|
+
padding-right: 12px;
|
67
68
|
font-weight: bold;
|
68
69
|
text-align: center;
|
69
70
|
color: #ffffff;
|
70
71
|
}
|
72
|
+
.list-footer-total {
|
73
|
+
font-size: 12px;
|
74
|
+
font-weight: 500;
|
75
|
+
color: #ffffff;
|
76
|
+
}
|
71
77
|
.datagrid {
|
72
78
|
padding: 0 0 0 8px;
|
73
79
|
height: $datagrid-height;
|
@@ -158,6 +164,11 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
158
164
|
fill: #ff0000;
|
159
165
|
stroke: none;
|
160
166
|
}
|
167
|
+
&.icon-down {
|
168
|
+
fill: none;
|
169
|
+
stroke: currentColor;
|
170
|
+
stroke-width: 2;
|
171
|
+
}
|
161
172
|
}
|
162
173
|
|
163
174
|
.util-cell-label {
|
@@ -209,3 +220,48 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
209
220
|
transition: background-color 0.2s ease;
|
210
221
|
}
|
211
222
|
}
|
223
|
+
.util-cell-actions {
|
224
|
+
display: flex;
|
225
|
+
flex-direction: column;
|
226
|
+
gap: 8px;
|
227
|
+
position: relative;
|
228
|
+
&:hover {
|
229
|
+
.util-cell-actions-list {
|
230
|
+
display: flex;
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
.util-cell-actions-label {
|
235
|
+
font-size: 14px;
|
236
|
+
font-weight: 500;
|
237
|
+
cursor: pointer;
|
238
|
+
white-space: nowrap;
|
239
|
+
text-decoration: underline;
|
240
|
+
}
|
241
|
+
.util-cell-actions-list {
|
242
|
+
position: absolute;
|
243
|
+
top: 100%;
|
244
|
+
right: 0;
|
245
|
+
display: none;
|
246
|
+
flex-direction: column;
|
247
|
+
list-style: none;
|
248
|
+
margin: 0;
|
249
|
+
padding: 0;
|
250
|
+
background-color: #2b2b2b;
|
251
|
+
border: 1px solid #444444;
|
252
|
+
border-radius: 4px;
|
253
|
+
z-index: 1000;
|
254
|
+
> li {
|
255
|
+
display: flex;
|
256
|
+
align-items: center;
|
257
|
+
padding: 12px 16px;
|
258
|
+
border-bottom: 1px solid #444444;
|
259
|
+
cursor: pointer;
|
260
|
+
&:hover {
|
261
|
+
background-color: #444444;
|
262
|
+
}
|
263
|
+
&:last-child {
|
264
|
+
border-bottom: none;
|
265
|
+
}
|
266
|
+
}
|
267
|
+
}
|