proje-react-panel 1.4.1 → 1.6.0-test-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.
- package/.cursor/rules.md +11 -1
- package/.vscode/launch.json +9 -0
- package/AUTH_LAYOUT_EXAMPLE.md +343 -0
- package/AUTH_LAYOUT_GUIDE.md +819 -0
- package/IMPLEMENTATION_GUIDE.md +899 -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/list/CellField.d.ts +2 -2
- package/dist/components/list/cells/BooleanCell.d.ts +5 -2
- package/dist/components/list/cells/DateCell.d.ts +5 -2
- package/dist/components/list/cells/DefaultCell.d.ts +3 -2
- package/dist/components/list/cells/DownloadCell.d.ts +3 -2
- package/dist/components/list/cells/ImageCell.d.ts +3 -2
- package/dist/components/list/cells/LinkCell.d.ts +8 -0
- package/dist/components/list/cells/UUIDCell.d.ts +5 -2
- package/dist/decorators/auth/DefaultLoginForm.d.ts +4 -0
- package/dist/decorators/details/Details.d.ts +1 -1
- package/dist/decorators/list/Cell.d.ts +5 -1
- package/dist/decorators/list/List.d.ts +8 -4
- package/dist/decorators/list/cells/LinkCell.d.ts +13 -0
- package/dist/index.cjs.js +15 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.esm.js +15 -1
- package/dist/types/Login.d.ts +8 -0
- package/package.json +3 -1
- package/src/api/ApiConfig.ts +63 -0
- package/src/api/AuthApi.ts +8 -0
- package/src/api/CrudApi.ts +96 -60
- 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 +25 -10
- package/src/components/list/Datagrid.tsx +83 -53
- package/src/components/list/ListPage.tsx +3 -0
- package/src/components/list/cells/BooleanCell.tsx +7 -2
- package/src/components/list/cells/DateCell.tsx +6 -2
- package/src/components/list/cells/DefaultCell.tsx +4 -4
- package/src/components/list/cells/DownloadCell.tsx +4 -2
- package/src/components/list/cells/ImageCell.tsx +5 -2
- package/src/components/list/cells/LinkCell.tsx +31 -0
- package/src/components/list/cells/UUIDCell.tsx +6 -2
- package/src/decorators/auth/DefaultLoginForm.ts +32 -0
- package/src/decorators/details/Details.ts +1 -1
- package/src/decorators/list/Cell.ts +5 -1
- package/src/decorators/list/List.ts +4 -4
- package/src/decorators/list/cells/LinkCell.ts +22 -0
- package/src/index.ts +27 -1
- package/src/services/DataService.ts +14 -10
- package/src/store/store.ts +1 -1
- package/src/styles/components/button.scss +14 -0
- package/src/styles/index.scss +1 -1
- package/src/styles/list.scss +64 -1
- package/src/types/Login.ts +9 -0
- package/src/utils/logout.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proje-react-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-test-1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "SEFA DEMİR",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@types/react": "^19.0.10",
|
|
34
34
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
35
35
|
"@typescript-eslint/parser": "^8.29.1",
|
|
36
|
+
"axios": "^1.12.2",
|
|
36
37
|
"class-transformer": "^0.5.1",
|
|
37
38
|
"class-validator": "^0.14.1",
|
|
38
39
|
"eslint": "^9.24.0",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"zustand": "^5.0.3"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
65
|
+
"axios": ">=1.0.0",
|
|
64
66
|
"react": ">=19.0.0",
|
|
65
67
|
"react-hook-form": ">=7.54.2",
|
|
66
68
|
"react-router": "7.3.0",
|
|
@@ -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,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M903.232 256l56.768 50.432L512 768 64 306.432 120.768 256 512 659.072z" fill="#000000" /></svg>
|
|
@@ -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,38 +7,53 @@ 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
|
|
|
22
23
|
switch (configuration.type) {
|
|
23
24
|
case 'boolean':
|
|
24
|
-
render = <BooleanCell
|
|
25
|
+
render = <BooleanCell item={item} configuration={configuration} />;
|
|
25
26
|
break;
|
|
26
27
|
case 'date':
|
|
27
|
-
render = <DateCell
|
|
28
|
+
render = <DateCell item={item} configuration={configuration} />;
|
|
28
29
|
break;
|
|
29
30
|
case 'image':
|
|
30
|
-
render = <ImageCell
|
|
31
|
+
render = <ImageCell item={item} configuration={configuration} />;
|
|
31
32
|
break;
|
|
32
33
|
case 'uuid':
|
|
33
|
-
render = <UUIDCell
|
|
34
|
+
render = <UUIDCell item={item} configuration={configuration} />;
|
|
34
35
|
break;
|
|
35
36
|
case 'download':
|
|
36
|
-
render = <DownloadCell
|
|
37
|
+
render = <DownloadCell item={item} configuration={configuration} />;
|
|
38
|
+
break;
|
|
39
|
+
case 'link':
|
|
40
|
+
render = <LinkCell item={item} configuration={configuration} />;
|
|
37
41
|
break;
|
|
38
42
|
default:
|
|
39
|
-
render = <DefaultCell
|
|
43
|
+
render = <DefaultCell item={item} configuration={configuration} />;
|
|
40
44
|
break;
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const width = configuration.style?.width;
|
|
47
|
+
const minWidth = configuration.style?.minWidth;
|
|
48
|
+
return (
|
|
49
|
+
<td
|
|
50
|
+
key={configuration.name}
|
|
51
|
+
style={{
|
|
52
|
+
minWidth,
|
|
53
|
+
width,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{render}
|
|
57
|
+
</td>
|
|
58
|
+
);
|
|
44
59
|
}
|
|
@@ -3,6 +3,7 @@ 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';
|
|
@@ -23,10 +24,10 @@ export function Datagrid<T extends AnyClass>({
|
|
|
23
24
|
}: DatagridProps<T>) {
|
|
24
25
|
const cells = listPageMeta.cells;
|
|
25
26
|
const listData = useAppStore(state => state.listData[listPageMeta.class.key]);
|
|
26
|
-
const
|
|
27
|
-
? typeof listPageMeta.class.
|
|
28
|
-
? listPageMeta.class.
|
|
29
|
-
: listPageMeta.class.
|
|
27
|
+
const listActions = data?.[0]
|
|
28
|
+
? typeof listPageMeta.class.actions === 'function'
|
|
29
|
+
? listPageMeta.class.actions?.(data[0])
|
|
30
|
+
: listPageMeta.class.actions
|
|
30
31
|
: null;
|
|
31
32
|
|
|
32
33
|
return (
|
|
@@ -38,72 +39,101 @@ export function Datagrid<T extends AnyClass>({
|
|
|
38
39
|
<thead>
|
|
39
40
|
<tr>
|
|
40
41
|
{cells.map(cellOptions => (
|
|
41
|
-
<th
|
|
42
|
+
<th
|
|
43
|
+
key={cellOptions.name}
|
|
44
|
+
style={{ width: cellOptions.style?.width, minWidth: cellOptions.style?.minWidth }}
|
|
45
|
+
>
|
|
46
|
+
{cellOptions.title ?? cellOptions.name}
|
|
47
|
+
</th>
|
|
42
48
|
))}
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
{(listActions?.details ||
|
|
50
|
+
listActions?.edit ||
|
|
51
|
+
listActions?.delete ||
|
|
52
|
+
listActions?.customActions?.length) && <th style={{ width: '30px' }}>Actions</th>}
|
|
46
53
|
</tr>
|
|
47
54
|
</thead>
|
|
48
55
|
<tbody>
|
|
49
56
|
{data.map((item, index) => {
|
|
50
57
|
const listCells = item
|
|
51
|
-
? typeof listPageMeta.class.
|
|
52
|
-
? listPageMeta.class.
|
|
53
|
-
: listPageMeta.class.
|
|
58
|
+
? typeof listPageMeta.class.actions === 'function'
|
|
59
|
+
? listPageMeta.class.actions?.(item)
|
|
60
|
+
: listPageMeta.class.actions
|
|
61
|
+
: null;
|
|
62
|
+
//TODO: memoize this
|
|
63
|
+
const listDataItem = listPageMeta.class.primaryId
|
|
64
|
+
? (listData?.[item[listPageMeta.class.primaryId!] as string] as
|
|
65
|
+
| Record<string, unknown>
|
|
66
|
+
| undefined)
|
|
54
67
|
: null;
|
|
55
|
-
const listDataItem = listData?.[item[listPageMeta.class.primaryId]] as
|
|
56
|
-
| Record<string, unknown>
|
|
57
|
-
| undefined;
|
|
58
68
|
return (
|
|
59
69
|
<tr key={index}>
|
|
60
70
|
{cells.map((configuration: CellConfiguration) => {
|
|
61
|
-
const value = listDataItem?.[configuration.name] ?? item[configuration.name];
|
|
62
71
|
return (
|
|
63
72
|
<CellField
|
|
64
73
|
key={configuration.name}
|
|
74
|
+
item={{
|
|
75
|
+
...(listDataItem ?? {}),
|
|
76
|
+
...item,
|
|
77
|
+
}}
|
|
65
78
|
configuration={configuration}
|
|
66
|
-
value={value}
|
|
67
79
|
/>
|
|
68
80
|
);
|
|
69
81
|
})}
|
|
70
|
-
{listCells?.details && (
|
|
71
|
-
<td>
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
82
|
+
{(listCells?.details || listCells?.edit || listCells?.delete) && (
|
|
83
|
+
<td style={{ width: '30px' }}>
|
|
84
|
+
<div className="util-cell-actions">
|
|
85
|
+
<p className="util-cell-actions-label">
|
|
86
|
+
Actions <DownArrowIcon className="icon icon-down" />
|
|
87
|
+
</p>
|
|
88
|
+
<ul className="util-cell-actions-list">
|
|
89
|
+
{listCells?.details && (
|
|
90
|
+
<li>
|
|
91
|
+
<Link to={listCells.details.path} className="util-cell-link">
|
|
92
|
+
<SearchIcon className="icon icon-search" />
|
|
93
|
+
<span className="util-cell-label">{listCells.details.label}</span>
|
|
94
|
+
</Link>
|
|
95
|
+
</li>
|
|
96
|
+
)}
|
|
97
|
+
{listCells?.edit && (
|
|
98
|
+
<li>
|
|
99
|
+
<Link to={listCells.edit.path} className="util-cell-link">
|
|
100
|
+
<PencilIcon className="icon icon-pencil" />
|
|
101
|
+
<span className="util-cell-label">{listCells.edit.label}</span>
|
|
102
|
+
</Link>
|
|
103
|
+
</li>
|
|
104
|
+
)}
|
|
105
|
+
{listCells?.delete && (
|
|
106
|
+
<li>
|
|
107
|
+
<a
|
|
108
|
+
onClick={() => {
|
|
109
|
+
listCells.delete
|
|
110
|
+
?.onRemoveItem?.(item)
|
|
111
|
+
.then(() => {
|
|
112
|
+
onRemoveItem?.(item);
|
|
113
|
+
})
|
|
114
|
+
.catch((e: unknown) => {
|
|
115
|
+
console.error(e);
|
|
116
|
+
const message =
|
|
117
|
+
e instanceof Error ? e.message : 'Error deleting item';
|
|
118
|
+
alert(message);
|
|
119
|
+
});
|
|
120
|
+
}}
|
|
121
|
+
className="util-cell-link util-cell-link-remove"
|
|
122
|
+
>
|
|
123
|
+
<TrashIcon className="icon icon-trash" />
|
|
124
|
+
<span className="util-cell-label">{listCells.delete.label}</span>
|
|
125
|
+
</a>
|
|
126
|
+
</li>
|
|
127
|
+
)}
|
|
128
|
+
{listCells?.customActions?.map(action => (
|
|
129
|
+
<li key={action.label}>
|
|
130
|
+
<a onClick={() => action.onClick(item)} className="util-cell-link">
|
|
131
|
+
<span className="util-cell-label">{action.label}</span>
|
|
132
|
+
</a>
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
</div>
|
|
107
137
|
</td>
|
|
108
138
|
)}
|
|
109
139
|
</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}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import CheckIcon from '../../../assets/icons/svg/check.svg';
|
|
3
3
|
import CrossIcon from '../../../assets/icons/svg/cross.svg';
|
|
4
|
+
import { AnyClass } from '../../../types/AnyClass';
|
|
5
|
+
import { CellConfiguration } from '../../../decorators/list/Cell';
|
|
4
6
|
|
|
5
7
|
interface BooleanCellProps {
|
|
6
|
-
|
|
8
|
+
item: AnyClass;
|
|
9
|
+
configuration: CellConfiguration;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
export function BooleanCell({
|
|
12
|
+
export function BooleanCell({ item, configuration }: BooleanCellProps) {
|
|
13
|
+
const value = item[configuration.name];
|
|
14
|
+
|
|
10
15
|
return value ? (
|
|
11
16
|
<CheckIcon className="icon icon-true" />
|
|
12
17
|
) : (
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { AnyClass } from '../../../types/AnyClass';
|
|
3
|
+
import { CellConfiguration } from '../../../decorators/list/Cell';
|
|
2
4
|
|
|
3
5
|
interface DateCellProps {
|
|
4
|
-
|
|
6
|
+
item: AnyClass;
|
|
7
|
+
configuration: CellConfiguration;
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
export function DateCell({
|
|
10
|
+
export function DateCell({ item, configuration }: DateCellProps) {
|
|
11
|
+
const value = item[configuration.name];
|
|
8
12
|
if (!value) return <>-</>;
|
|
9
13
|
|
|
10
14
|
const date = new Date(value);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { CellConfiguration } from '../../../decorators/list/Cell';
|
|
3
|
+
import { AnyClass } from '../../../types/AnyClass';
|
|
3
4
|
|
|
4
5
|
interface DefaultCellProps {
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
value: any;
|
|
6
|
+
item: AnyClass;
|
|
8
7
|
configuration: CellConfiguration;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export function DefaultCell({
|
|
10
|
+
export function DefaultCell({ item, configuration }: DefaultCellProps): React.ReactElement {
|
|
11
|
+
const value = item[configuration.name];
|
|
12
12
|
return <>{value ? value.toString() : configuration.placeHolder}</>;
|
|
13
13
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { CellConfiguration } from '../../../decorators/list/Cell';
|
|
3
3
|
import { DownloadCellConfiguration } from '../../../decorators/list/cells/DownloadCell';
|
|
4
|
+
import { AnyClass } from '../../../types/AnyClass';
|
|
4
5
|
|
|
5
6
|
interface DownloadCellProps {
|
|
6
|
-
|
|
7
|
+
item: AnyClass;
|
|
7
8
|
configuration: CellConfiguration;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export function DownloadCell({
|
|
11
|
+
export function DownloadCell({ item, configuration }: DownloadCellProps): React.ReactElement {
|
|
12
|
+
const value = item[configuration.name];
|
|
11
13
|
if (!value) return <>-</>;
|
|
12
14
|
|
|
13
15
|
const downloadConfiguration = configuration as DownloadCellConfiguration;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { CellConfiguration } from '../../../decorators/list/Cell';
|
|
3
3
|
import { ImageCellConfiguration } from '../../../decorators/list/cells/ImageCell';
|
|
4
|
+
import { AnyClass } from '../../../types/AnyClass';
|
|
5
|
+
|
|
4
6
|
interface ImageCellProps {
|
|
5
|
-
|
|
7
|
+
item: AnyClass;
|
|
6
8
|
configuration: CellConfiguration;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function ImageCell({
|
|
11
|
+
export function ImageCell({ item, configuration }: ImageCellProps) {
|
|
10
12
|
const imageConfiguration = configuration as ImageCellConfiguration;
|
|
13
|
+
const value = item[configuration.name];
|
|
11
14
|
if (!value) return <>-</>;
|
|
12
15
|
|
|
13
16
|
return (
|