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
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
<a
|
|
19
|
+
className="util-cell-link"
|
|
20
|
+
onClick={() => {
|
|
21
|
+
linkConfiguration.onClick?.(item as T);
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{value?.toString()}
|
|
25
|
+
</a>
|
|
26
|
+
) : (
|
|
27
|
+
value?.toString() || linkConfiguration.placeHolder
|
|
28
|
+
)}
|
|
29
|
+
</Link>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -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 UUIDCellProps {
|
|
4
|
-
|
|
6
|
+
item: AnyClass;
|
|
7
|
+
configuration: CellConfiguration;
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
export function UUIDCell({
|
|
10
|
+
export function UUIDCell({ item, configuration }: UUIDCellProps) {
|
|
11
|
+
const value = item[configuration.name];
|
|
8
12
|
if (!value || typeof value !== 'string' || value.length < 6) return <>-</>;
|
|
9
13
|
|
|
10
14
|
return <>{`${value.slice(0, 3)}...${value.slice(-3)}`}</>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MinLength } from 'class-validator';
|
|
2
|
+
import { Form } from '../form/Form';
|
|
3
|
+
import { Input } from '../form/Input';
|
|
4
|
+
import { LoginForm as LoginFormInterface, LoginResponse } from '../../types/Login';
|
|
5
|
+
import { setAuthToken } from '../../api/ApiConfig';
|
|
6
|
+
import { login } from '../../utils/login';
|
|
7
|
+
import { User } from '../../types/User';
|
|
8
|
+
import { authLogin } from '../..';
|
|
9
|
+
|
|
10
|
+
@Form<LoginFormInterface, LoginResponse<User>>({
|
|
11
|
+
onSubmit: authLogin,
|
|
12
|
+
onSubmitSuccess: (data: LoginResponse<User>) => {
|
|
13
|
+
setAuthToken(data.access_token);
|
|
14
|
+
login(data.admin as User, data.access_token, () => {
|
|
15
|
+
window.location.href = '/';
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
type: 'formData',
|
|
19
|
+
})
|
|
20
|
+
export class DefaultLoginForm {
|
|
21
|
+
@MinLength(3)
|
|
22
|
+
@Input({
|
|
23
|
+
label: 'Username',
|
|
24
|
+
})
|
|
25
|
+
username: string;
|
|
26
|
+
|
|
27
|
+
@Input({
|
|
28
|
+
label: 'Password',
|
|
29
|
+
inputType: 'password',
|
|
30
|
+
})
|
|
31
|
+
password: string;
|
|
32
|
+
}
|
|
@@ -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;
|
|
@@ -22,6 +22,10 @@ export interface CellOptions {
|
|
|
22
22
|
type?: CellTypes;
|
|
23
23
|
placeHolder?: string;
|
|
24
24
|
filter?: Filter | StaticSelectFilter;
|
|
25
|
+
style?: {
|
|
26
|
+
minWidth?: string;
|
|
27
|
+
width?: string;
|
|
28
|
+
};
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface CellConfiguration extends Omit<CellOptions, 'type'> {
|
|
@@ -25,7 +25,8 @@ export interface ListHeaderOptions {
|
|
|
25
25
|
create?: { path: string; label: string };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface
|
|
28
|
+
export interface ListActionOptions<T> {
|
|
29
|
+
customActions?: { label: string; onClick: (item: T) => void; icon?: string }[];
|
|
29
30
|
details?: { path: string; label: string };
|
|
30
31
|
edit?: { path: string; label: string };
|
|
31
32
|
delete?: { label: string; onRemoveItem?: (item: T) => Promise<void> };
|
|
@@ -34,13 +35,12 @@ export interface ListCellOptions<T> {
|
|
|
34
35
|
export interface ListOptions<T> {
|
|
35
36
|
getData: GetDataForList<T>;
|
|
36
37
|
headers?: ListHeaderOptions;
|
|
37
|
-
|
|
38
|
-
primaryId
|
|
38
|
+
actions?: ((item: T) => ListActionOptions<T>) | ListActionOptions<T>;
|
|
39
|
+
primaryId?: string;
|
|
39
40
|
key?: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export type ListConfiguration<T> = ListOptions<T> & {
|
|
43
|
-
primaryId: string;
|
|
44
44
|
key: string;
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -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
|
|
|
@@ -36,6 +37,31 @@ export { Layout } from './components/layout';
|
|
|
36
37
|
export { Login } from './components/Login';
|
|
37
38
|
export { login } from './utils/login';
|
|
38
39
|
export { logout } from './utils/logout';
|
|
40
|
+
export { DefaultLoginForm } from './decorators/auth/DefaultLoginForm';
|
|
41
|
+
|
|
42
|
+
//API
|
|
43
|
+
export {
|
|
44
|
+
initApi,
|
|
45
|
+
initAuthToken,
|
|
46
|
+
setAuthToken,
|
|
47
|
+
setAuthLogout,
|
|
48
|
+
getAxiosInstance,
|
|
49
|
+
axiosInstance,
|
|
50
|
+
} from './api/ApiConfig';
|
|
51
|
+
export {
|
|
52
|
+
getAll,
|
|
53
|
+
getOne,
|
|
54
|
+
create,
|
|
55
|
+
createFormData,
|
|
56
|
+
update,
|
|
57
|
+
updateFormData,
|
|
58
|
+
updateSimple,
|
|
59
|
+
remove,
|
|
60
|
+
} from './api/CrudApi';
|
|
61
|
+
export { login as authLogin } from './api/AuthApi';
|
|
62
|
+
|
|
63
|
+
//TYPES
|
|
64
|
+
export type { LoginForm, LoginResponse } from './types/Login';
|
|
39
65
|
|
|
40
66
|
//SERVICES
|
|
41
67
|
export { updateDetailsData } from './services/DataService';
|
|
@@ -9,13 +9,15 @@ export function updateDetailsData<T extends AnyClass>(
|
|
|
9
9
|
) {
|
|
10
10
|
const { class: detailsClass } = getDetailsPageMeta(model);
|
|
11
11
|
const key = detailsClass.key;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!data[id]) {
|
|
15
|
-
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');
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
|
|
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);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function updateListData<T extends AnyClass>(
|
|
@@ -24,11 +26,13 @@ export function updateListData<T extends AnyClass>(
|
|
|
24
26
|
) {
|
|
25
27
|
const { class: listClass } = getListPageMeta(model);
|
|
26
28
|
const key = listClass.key;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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`);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
useAppStore.getState().updateListData(key, data[
|
|
37
|
+
useAppStore.getState().updateListData(key, data[listClass.primaryId]?.toString(), data);
|
|
34
38
|
}
|
package/src/store/store.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.panel-button {
|
|
2
|
+
padding: 0.5rem 1rem;
|
|
3
|
+
border: 1px solid #444444;
|
|
4
|
+
border-radius: 4px;
|
|
5
|
+
background-color: cornflowerblue;
|
|
6
|
+
color: #ffffff;
|
|
7
|
+
font-size: 0.875rem;
|
|
8
|
+
font-weight: 500;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
transition: all 0.2s ease;
|
|
11
|
+
&:hover {
|
|
12
|
+
transform: scale(1.01);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/styles/index.scss
CHANGED
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;
|
|
@@ -80,14 +86,18 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
.datagrid-table {
|
|
83
|
-
width: 100%;
|
|
89
|
+
min-width: 100%;
|
|
90
|
+
table-layout: fixed;
|
|
84
91
|
border-collapse: collapse;
|
|
92
|
+
position: relative;
|
|
85
93
|
|
|
86
94
|
th,
|
|
87
95
|
td {
|
|
88
96
|
padding: 12px 16px;
|
|
89
97
|
text-align: left;
|
|
90
98
|
border-bottom: 1px solid #444444;
|
|
99
|
+
text-overflow: ellipsis;
|
|
100
|
+
white-space: nowrap;
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
th {
|
|
@@ -158,6 +168,11 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
|
158
168
|
fill: #ff0000;
|
|
159
169
|
stroke: none;
|
|
160
170
|
}
|
|
171
|
+
&.icon-down {
|
|
172
|
+
fill: none;
|
|
173
|
+
stroke: currentColor;
|
|
174
|
+
stroke-width: 2;
|
|
175
|
+
}
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
.util-cell-label {
|
|
@@ -209,3 +224,51 @@ $datagrid-height: calc(100vh - #{$header-height} - #{$footer-height});
|
|
|
209
224
|
transition: background-color 0.2s ease;
|
|
210
225
|
}
|
|
211
226
|
}
|
|
227
|
+
.util-cell-actions {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
position: relative;
|
|
232
|
+
&:hover {
|
|
233
|
+
.util-cell-actions-list {
|
|
234
|
+
display: flex;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
.util-cell-actions-label {
|
|
239
|
+
font-size: 14px;
|
|
240
|
+
font-weight: 500;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
white-space: nowrap;
|
|
243
|
+
text-decoration: underline;
|
|
244
|
+
}
|
|
245
|
+
.util-cell-actions-list {
|
|
246
|
+
position: absolute;
|
|
247
|
+
top: 100%;
|
|
248
|
+
right: 0;
|
|
249
|
+
display: none;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
list-style: none;
|
|
252
|
+
margin: 0;
|
|
253
|
+
padding: 0;
|
|
254
|
+
background-color: #2b2b2b;
|
|
255
|
+
border: 1px solid #444444;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
z-index: 1000;
|
|
258
|
+
> li {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
padding: 12px 16px;
|
|
262
|
+
border-bottom: 1px solid #444444;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
&:hover {
|
|
265
|
+
background-color: #444444;
|
|
266
|
+
}
|
|
267
|
+
&:last-child {
|
|
268
|
+
border-bottom: none;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
.util-cell-link {
|
|
273
|
+
cursor: pointer;
|
|
274
|
+
}
|
package/src/utils/logout.ts
CHANGED