@proteinjs/ui 2.0.2 → 2.0.3
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/.eslintrc.js +20 -0
- package/.prettierignore +3 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +10 -19
- package/LICENSE +21 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/src/container/AccountIconButton.d.ts +1 -1
- package/dist/src/container/AccountIconButton.d.ts.map +1 -1
- package/dist/src/container/AccountIconButton.js +13 -6
- package/dist/src/container/AccountIconButton.js.map +1 -1
- package/dist/src/container/NavMenu.d.ts.map +1 -1
- package/dist/src/container/NavMenu.js +9 -9
- package/dist/src/container/NavMenu.js.map +1 -1
- package/dist/src/container/PageContainer.d.ts +1 -1
- package/dist/src/container/PageContainer.d.ts.map +1 -1
- package/dist/src/container/PageContainer.js +12 -7
- package/dist/src/container/PageContainer.js.map +1 -1
- package/dist/src/form/Field.d.ts.map +1 -1
- package/dist/src/form/Field.js +4 -2
- package/dist/src/form/Field.js.map +1 -1
- package/dist/src/form/Form.d.ts.map +1 -1
- package/dist/src/form/Form.js +75 -40
- package/dist/src/form/Form.js.map +1 -1
- package/dist/src/form/FormButton.d.ts.map +1 -1
- package/dist/src/form/FormButton.js +1 -1
- package/dist/src/form/FormButton.js.map +1 -1
- package/dist/src/form/container/FormPage.d.ts.map +1 -1
- package/dist/src/form/container/FormPage.js.map +1 -1
- package/dist/src/form/container/FormPaper.js +1 -1
- package/dist/src/form/container/FormPaper.js.map +1 -1
- package/dist/src/form/fields/TextField.d.ts.map +1 -1
- package/dist/src/form/fields/TextField.js +8 -6
- package/dist/src/form/fields/TextField.js.map +1 -1
- package/dist/src/list/NestedList.d.ts.map +1 -1
- package/dist/src/list/NestedList.js +10 -10
- package/dist/src/list/NestedList.js.map +1 -1
- package/dist/src/router/Page.d.ts.map +1 -1
- package/dist/src/router/Router.d.ts.map +1 -1
- package/dist/src/router/Router.js +11 -7
- package/dist/src/router/Router.js.map +1 -1
- package/dist/src/router/createUrlParams.d.ts.map +1 -1
- package/dist/src/router/createUrlParams.js +2 -1
- package/dist/src/router/createUrlParams.js.map +1 -1
- package/dist/src/router/withRouter.d.ts.map +1 -1
- package/dist/src/router/withRouter.js +1 -1
- package/dist/src/router/withRouter.js.map +1 -1
- package/dist/src/table/Table.d.ts +1 -1
- package/dist/src/table/Table.d.ts.map +1 -1
- package/dist/src/table/Table.js +31 -23
- package/dist/src/table/Table.js.map +1 -1
- package/dist/src/table/TableButton.d.ts.map +1 -1
- package/dist/src/table/TableLoader.d.ts.map +1 -1
- package/dist/src/table/TableToolbar.d.ts.map +1 -1
- package/dist/src/table/TableToolbar.js +10 -7
- package/dist/src/table/TableToolbar.js.map +1 -1
- package/generated/index.ts +1 -1
- package/index.ts +1 -1
- package/jest.config.js +8 -17
- package/package.json +56 -50
- package/src/container/AccountIconButton.tsx +103 -102
- package/src/container/NavMenu.tsx +20 -21
- package/src/container/PageContainer.tsx +110 -80
- package/src/form/Field.tsx +42 -35
- package/src/form/Form.tsx +370 -351
- package/src/form/FormButton.tsx +24 -22
- package/src/form/FunctionalForm.tsx +15 -15
- package/src/form/container/FormPage.tsx +16 -18
- package/src/form/container/FormPaper.tsx +12 -12
- package/src/form/fields/TextField.tsx +63 -61
- package/src/list/NestedList.tsx +18 -21
- package/src/router/Page.ts +18 -18
- package/src/router/Router.tsx +66 -52
- package/src/router/createUrlParams.ts +8 -7
- package/src/router/withRouter.tsx +6 -11
- package/src/table/Table.tsx +96 -80
- package/src/table/TableButton.ts +9 -9
- package/src/table/TableLoader.ts +6 -6
- package/src/table/TableToolbar.tsx +52 -59
- package/tsconfig.json +17 -17
package/src/form/FormButton.tsx
CHANGED
|
@@ -2,29 +2,31 @@ import React from 'react';
|
|
|
2
2
|
import { Fields } from './Field';
|
|
3
3
|
|
|
4
4
|
export type FormButton<F extends Fields> = {
|
|
5
|
-
name: string
|
|
5
|
+
name: string;
|
|
6
6
|
accessibility?: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
};
|
|
10
|
+
style: {
|
|
11
|
+
color?: 'inherit' | 'primary' | 'success' | 'warning' | 'secondary' | 'error' | 'info';
|
|
12
|
+
variant?: 'text' | 'outlined' | 'contained';
|
|
13
|
+
icon?: React.ComponentType;
|
|
14
|
+
};
|
|
15
|
+
clearFormOnClick?: boolean;
|
|
16
|
+
redirect?: (fields: F, buttons: FormButtons<F>) => Promise<{ path: string; props?: { [key: string]: any } }>;
|
|
17
|
+
onClick?: (fields: F, buttons: FormButtons<F>) => Promise<string | void>;
|
|
18
|
+
progressMessage?: (fields: F) => string;
|
|
19
|
+
};
|
|
20
20
|
|
|
21
|
-
export abstract class FormButtons<F extends Fields> {
|
|
21
|
+
export abstract class FormButtons<F extends Fields> {
|
|
22
|
+
[name: string]: FormButton<F>;
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
export const clearButton: FormButton<any> = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
26
|
+
name: 'Clear',
|
|
27
|
+
style: {
|
|
28
|
+
color: 'primary',
|
|
29
|
+
variant: 'text',
|
|
30
|
+
},
|
|
31
|
+
clearFormOnClick: true,
|
|
32
|
+
};
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
// </Grid>
|
|
189
189
|
// );
|
|
190
190
|
// }
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
// function Status() {
|
|
193
193
|
// if (!status || !status.message)
|
|
194
194
|
// return null;
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
// </Grid>
|
|
207
207
|
// );
|
|
208
208
|
// }
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
// function Fields() {
|
|
211
211
|
// return (
|
|
212
212
|
// <Grid container spacing={8}>
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
// getFieldRows().map((fieldComponents, index) => {
|
|
215
215
|
// if (!fieldRowVisible(fieldComponents))
|
|
216
216
|
// return null;
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
// return (
|
|
219
219
|
// <Grid item xs={12} key={index}>
|
|
220
220
|
// <Grid container spacing={8}>
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
// fieldComponents.filter((fieldComponent) => {
|
|
223
223
|
// if (fieldComponent.field.accessibility?.hidden)
|
|
224
224
|
// return false;
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
// return true;
|
|
227
227
|
// }).map((fieldComponent, key) => {
|
|
228
228
|
// const fieldKey = fieldRenderKeys[fieldComponent.field.name] ? `${fieldComponent.field.name}_${fieldRenderKeys[fieldComponent.field.name]}` : `${fieldComponent.field.name}`;
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
// </Grid>
|
|
246
246
|
// );
|
|
247
247
|
// }
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
// function getFieldRows(): FieldComponent<any, any>[][] {
|
|
250
250
|
// const rows: FieldComponent<any, any>[][] = [];
|
|
251
251
|
// if (!fields)
|
|
@@ -281,33 +281,33 @@
|
|
|
281
281
|
// const field = fieldComponent.field;
|
|
282
282
|
// if (!field.layout)
|
|
283
283
|
// throw new Error(`Unless using FormProps.fieldLayout, Field.layout must be provided; layout not provided for field: ${field.name}`);
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
// if (typeof rows[field.layout.row] === 'undefined')
|
|
286
286
|
// rows[field.layout.row] = [];
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
// if (field.layout.column && rows[field.layout.row].length >= field.layout.column) {
|
|
289
289
|
// rows[field.layout.row].splice(field.layout.column - 1, 0, fieldComponent);
|
|
290
290
|
// } else
|
|
291
291
|
// rows[field.layout.row].push(fieldComponent);
|
|
292
|
-
|
|
292
|
+
|
|
293
293
|
// const rowWidth = rows[field.layout.row].map((fieldComponent) => fieldComponent.field.layout?.width ? fieldComponent.field.layout.width as number : 0).reduce((accumulator, currentWidth) => accumulator + currentWidth);
|
|
294
294
|
// if (rowWidth > 12)
|
|
295
295
|
// throw new Error(`Width of row exceeds maximum width (12), row width: ${rowWidth}, row index: ${field.layout.row}`);
|
|
296
296
|
// }
|
|
297
297
|
// }
|
|
298
|
-
|
|
298
|
+
|
|
299
299
|
// return rows;
|
|
300
300
|
// }
|
|
301
|
-
|
|
301
|
+
|
|
302
302
|
// function fieldRowVisible(fieldComponents: FieldComponent<any, any>[]): boolean {
|
|
303
303
|
// for (const fieldComponent of fieldComponents) {
|
|
304
304
|
// if (!fieldComponent.field.accessibility || !fieldComponent.field.accessibility?.hidden)
|
|
305
305
|
// return true;
|
|
306
306
|
// }
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
// return false;
|
|
309
309
|
// }
|
|
310
|
-
|
|
310
|
+
|
|
311
311
|
// function Progress() {
|
|
312
312
|
// if (!progress || !progress.visible)
|
|
313
313
|
// return null;
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
// </Grid>
|
|
321
321
|
// );
|
|
322
322
|
// }
|
|
323
|
-
|
|
323
|
+
|
|
324
324
|
// function Buttons() {
|
|
325
325
|
// return (
|
|
326
326
|
// <Grid container justify='flex-end' alignItems='flex-end' spacing={2}>
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
// Object.keys(props.buttons).map((buttonPropertyName) => props.buttons[buttonPropertyName]).filter((button) => {
|
|
329
329
|
// if (button.accessibility?.hidden)
|
|
330
330
|
// return false;
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
// return true;
|
|
333
333
|
// }).map((button, key) => {
|
|
334
334
|
// return (
|
|
@@ -361,4 +361,4 @@
|
|
|
361
361
|
// </Grid>
|
|
362
362
|
// );
|
|
363
363
|
// }
|
|
364
|
-
// }
|
|
364
|
+
// }
|
|
@@ -3,21 +3,19 @@ import { Grid, PaperProps } from '@mui/material';
|
|
|
3
3
|
import { FormPaper } from './FormPaper';
|
|
4
4
|
|
|
5
5
|
export function FormPage(props: PaperProps) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
23
|
-
}
|
|
6
|
+
return (
|
|
7
|
+
<Grid
|
|
8
|
+
container
|
|
9
|
+
sx={(theme) => ({
|
|
10
|
+
marginTop: theme.spacing(4),
|
|
11
|
+
})}
|
|
12
|
+
direction='row'
|
|
13
|
+
justifyContent='center'
|
|
14
|
+
alignItems='center'
|
|
15
|
+
>
|
|
16
|
+
<Grid item>
|
|
17
|
+
<FormPaper {...props}>{props.children}</FormPaper>
|
|
18
|
+
</Grid>
|
|
19
|
+
</Grid>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -2,15 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import { PaperProps, Paper } from '@mui/material';
|
|
3
3
|
|
|
4
4
|
export function FormPaper(props: PaperProps) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
5
|
+
return (
|
|
6
|
+
<Paper
|
|
7
|
+
sx={(theme) => ({
|
|
8
|
+
padding: theme.spacing(2, 2, 1),
|
|
9
|
+
width: 'fit-content',
|
|
10
|
+
})}
|
|
11
|
+
{...props}
|
|
12
|
+
>
|
|
13
|
+
{props.children}
|
|
14
|
+
</Paper>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -4,68 +4,70 @@ import { Visibility, VisibilityOff } from '@mui/icons-material';
|
|
|
4
4
|
import { Field, FieldComponent, FieldComponentProps, fieldDisplayValue, fieldLabel, Fields } from '../Field';
|
|
5
5
|
|
|
6
6
|
export type TextFieldProps<T, F extends Fields> = Field<T, F> & {
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
isPassword?: boolean;
|
|
8
|
+
};
|
|
9
9
|
|
|
10
10
|
export function textField<F extends Fields>(props: TextFieldProps<string, F>): FieldComponent<string, F> {
|
|
11
|
-
|
|
11
|
+
const { isPassword } = props;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
field: props,
|
|
15
|
+
component: TextField,
|
|
16
|
+
};
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
function TextField(props: FieldComponentProps<string, F>) {
|
|
19
|
+
const { field, onChange, ...other } = props;
|
|
20
|
+
const [error, setError] = React.useState(false);
|
|
21
|
+
const [statusMessage, setStatusMessage] = React.useState<string>();
|
|
22
|
+
const [passwordVisible, setPasswordVisible] = React.useState(false);
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
24
|
+
return (
|
|
25
|
+
<MuiTextField
|
|
26
|
+
sx={{
|
|
27
|
+
display: 'flex',
|
|
28
|
+
flexWrap: 'nowrap',
|
|
29
|
+
}}
|
|
30
|
+
key={field.name}
|
|
31
|
+
label={fieldLabel(field)}
|
|
32
|
+
type={isPassword && !passwordVisible ? 'password' : 'text'}
|
|
33
|
+
value={fieldDisplayValue(field)}
|
|
34
|
+
error={error}
|
|
35
|
+
helperText={statusMessage ? statusMessage : field.description}
|
|
36
|
+
required={field.accessibility?.required}
|
|
37
|
+
disabled={field.accessibility?.readonly}
|
|
38
|
+
onChange={(event) => {
|
|
39
|
+
let errorReceived = false;
|
|
40
|
+
let messageReceived: string;
|
|
41
|
+
onChange(field, event.target.value, (message: string, isError: boolean) => {
|
|
42
|
+
setError(isError);
|
|
43
|
+
setStatusMessage(message);
|
|
44
|
+
errorReceived = isError;
|
|
45
|
+
messageReceived = message;
|
|
46
|
+
}).then(() => {
|
|
47
|
+
// setFieldStatus may not be called in Field.onChange, but we still want to run after Form.onChange
|
|
48
|
+
if (!errorReceived) {
|
|
49
|
+
setError(false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!messageReceived) {
|
|
53
|
+
setStatusMessage(undefined);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}}
|
|
57
|
+
InputProps={{
|
|
58
|
+
endAdornment: isPassword && (
|
|
59
|
+
<InputAdornment position='end'>
|
|
60
|
+
<IconButton
|
|
61
|
+
aria-label='Toggle password visibility'
|
|
62
|
+
onClick={(event) => setPasswordVisible(!passwordVisible)}
|
|
63
|
+
>
|
|
64
|
+
{passwordVisible ? <VisibilityOff /> : <Visibility />}
|
|
65
|
+
</IconButton>
|
|
66
|
+
</InputAdornment>
|
|
67
|
+
),
|
|
68
|
+
}}
|
|
69
|
+
{...other}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/list/NestedList.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React, { ReactElement } from 'react'
|
|
2
|
-
import { Box } from '@mui/material'
|
|
1
|
+
import React, { ReactElement } from 'react';
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
3
|
|
|
4
|
-
export type NestedListItem = { element: ReactElement
|
|
5
|
-
type NestedListProps = { items: NestedListItem[]
|
|
4
|
+
export type NestedListItem = { element: ReactElement; children?: NestedListItem[]; renderChildren?: boolean };
|
|
5
|
+
type NestedListProps = { items: NestedListItem[]; level?: number; levelColors?: string[] };
|
|
6
6
|
const blue = '#1976d2';
|
|
7
7
|
const green = '#76d275';
|
|
8
8
|
const orange = '#ffa726';
|
|
@@ -15,39 +15,36 @@ export const NestedList = ({ items, level, levelColors }: NestedListProps) => {
|
|
|
15
15
|
const resolvedLevelColors = levelColors ? levelColors : defaultLevelColors;
|
|
16
16
|
const flattenedItems = flattenItems(items);
|
|
17
17
|
|
|
18
|
-
function flattenItems(items: NestedListItem[]): (NestedListItem|NestedListItem[])[] {
|
|
18
|
+
function flattenItems(items: NestedListItem[]): (NestedListItem | NestedListItem[])[] {
|
|
19
19
|
const flattened = [];
|
|
20
|
-
for (
|
|
20
|
+
for (const item of items) {
|
|
21
21
|
flattened.push(item);
|
|
22
|
-
if (item.children && (item.renderChildren || typeof item.renderChildren === 'undefined'))
|
|
22
|
+
if (item.children && (item.renderChildren || typeof item.renderChildren === 'undefined')) {
|
|
23
23
|
flattened.push(item.children);
|
|
24
|
+
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
return flattened;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
<Box
|
|
31
|
-
component='ul'
|
|
32
|
-
sx={{
|
|
31
|
+
<Box
|
|
32
|
+
component='ul'
|
|
33
|
+
sx={{
|
|
33
34
|
padding: isRoot ? 0 : undefined,
|
|
34
35
|
margin: isRoot ? 0 : undefined,
|
|
35
36
|
}}
|
|
36
37
|
>
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
<NestedList
|
|
40
|
-
|
|
41
|
-
items={item}
|
|
42
|
-
level={resolvedLevel + 1}
|
|
43
|
-
/>
|
|
44
|
-
:
|
|
38
|
+
{flattenedItems.map((item, index) =>
|
|
39
|
+
Array.isArray(item) ? (
|
|
40
|
+
<NestedList key={index} items={item} level={resolvedLevel + 1} />
|
|
41
|
+
) : (
|
|
45
42
|
<Box
|
|
46
43
|
key={index}
|
|
47
44
|
component='li'
|
|
48
45
|
sx={(theme) => ({
|
|
49
46
|
borderLeft: 3,
|
|
50
|
-
borderLeftColor: resolvedLevelColors[resolvedLevel%resolvedLevelColors.length],
|
|
47
|
+
borderLeftColor: resolvedLevelColors[resolvedLevel % resolvedLevelColors.length],
|
|
51
48
|
listStyleType: 'none',
|
|
52
49
|
marginTop: !(isRoot && index == 0) ? theme.spacing(1) : undefined,
|
|
53
50
|
})}
|
|
@@ -55,7 +52,7 @@ export const NestedList = ({ items, level, levelColors }: NestedListProps) => {
|
|
|
55
52
|
{item.element}
|
|
56
53
|
</Box>
|
|
57
54
|
)
|
|
58
|
-
}
|
|
55
|
+
)}
|
|
59
56
|
</Box>
|
|
60
57
|
);
|
|
61
|
-
}
|
|
58
|
+
};
|
package/src/router/Page.ts
CHANGED
|
@@ -6,23 +6,23 @@ import { NavigateFunction } from 'react-router-dom';
|
|
|
6
6
|
export const getPages = () => SourceRepository.get().objects<Page>('@proteinjs/ui/Page');
|
|
7
7
|
|
|
8
8
|
export type PageComponentProps = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
9
|
+
urlParams: { [key: string]: string };
|
|
10
|
+
navigate: NavigateFunction;
|
|
11
|
+
};
|
|
12
12
|
|
|
13
13
|
export interface Page extends Loadable {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
14
|
+
name: string;
|
|
15
|
+
path: string | string[];
|
|
16
|
+
component: React.ComponentType<PageComponentProps>;
|
|
17
|
+
/** Render component on its own without any additional, top-level container */
|
|
18
|
+
noPageContainer?: boolean;
|
|
19
|
+
auth?: {
|
|
20
|
+
/** If true, the user does not need to be logged in or have any roles to access this page. If blank, defaults to false. */
|
|
21
|
+
public?: boolean;
|
|
22
|
+
/** If true, the user does not need to have any roles to access this page, but must be logged in. If blank, defaults to false. */
|
|
23
|
+
allUsers?: boolean;
|
|
24
|
+
/** The user must be logged in and have these roles to access this page. If blank, defaults to requiring the 'admin' role. */
|
|
25
|
+
roles?: string[];
|
|
26
|
+
};
|
|
27
|
+
pageContainerSxProps?: (theme: Theme) => SxProps;
|
|
28
|
+
}
|
package/src/router/Router.tsx
CHANGED
|
@@ -7,70 +7,84 @@ import { Page, getPages } from './Page';
|
|
|
7
7
|
import { createUrlParams } from './createUrlParams';
|
|
8
8
|
|
|
9
9
|
export type AppOptions = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
10
|
+
pageContainer?: React.ComponentType<{ page: Page }>;
|
|
11
|
+
pageNotFound?: React.ComponentType;
|
|
12
|
+
};
|
|
13
13
|
|
|
14
14
|
export function loadApp(options: AppOptions = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const container = document.getElementById('app');
|
|
16
|
+
const root = createRoot(container!);
|
|
17
|
+
root.render(<Router pages={getPages()} options={options} />);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function Router(props: { pages: Page[]
|
|
21
|
-
|
|
20
|
+
export function Router(props: { pages: Page[]; options: AppOptions }) {
|
|
21
|
+
const { pages, options } = props;
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<CssBaseline />
|
|
25
|
+
<BrowserRouter>
|
|
26
|
+
<RoutesComponent />
|
|
27
|
+
</BrowserRouter>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function RoutesComponent() {
|
|
32
|
+
const navigate = useNavigate();
|
|
22
33
|
return (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
<Routes>
|
|
35
|
+
{(() => {
|
|
36
|
+
const routes = [];
|
|
37
|
+
let key = 0;
|
|
38
|
+
for (const page of pages) {
|
|
39
|
+
if (typeof page.path === 'string') {
|
|
40
|
+
routes.push(
|
|
41
|
+
<Route
|
|
42
|
+
key={key++}
|
|
43
|
+
path={getPath(page.path)}
|
|
44
|
+
element={<ContainerizedComponent options={options} page={page} navigate={navigate} />}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
const paths = page.path as string[];
|
|
49
|
+
for (const path of paths) {
|
|
50
|
+
routes.push(
|
|
51
|
+
<Route
|
|
52
|
+
key={key++}
|
|
53
|
+
path={getPath(path)}
|
|
54
|
+
element={<ContainerizedComponent options={options} page={page} navigate={navigate} />}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return routes;
|
|
61
|
+
})()}
|
|
62
|
+
<Route element={<PageNotFound pageNotFound={options.pageNotFound} />} />
|
|
63
|
+
</Routes>
|
|
29
64
|
);
|
|
65
|
+
}
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<Routes>
|
|
35
|
-
{
|
|
36
|
-
(() => {
|
|
37
|
-
const routes = [];
|
|
38
|
-
let key = 0;
|
|
39
|
-
for (const page of pages) {
|
|
40
|
-
if (typeof page.path === 'string')
|
|
41
|
-
routes.push(<Route key={key++} path={getPath(page.path)} element={<ContainerizedComponent options={options} page={page} navigate={navigate} />} />);
|
|
42
|
-
else {
|
|
43
|
-
const paths = page.path as string[];
|
|
44
|
-
for (const path of paths)
|
|
45
|
-
routes.push(<Route key={key++} path={getPath(path)} element={<ContainerizedComponent options={options} page={page} navigate={navigate} />} />);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return routes;
|
|
49
|
-
})()
|
|
50
|
-
}
|
|
51
|
-
<Route element={<PageNotFound pageNotFound={options.pageNotFound} />} />
|
|
52
|
-
</Routes>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function ContainerizedComponent(props: { options: AppOptions, page: Page, navigate: NavigateFunction}) {
|
|
57
|
-
if (props.options.pageContainer && !props.page.noPageContainer)
|
|
58
|
-
return ( <props.options.pageContainer page={props.page} /> );
|
|
59
|
-
|
|
60
|
-
return ( <props.page.component urlParams={createUrlParams()} navigate={props.navigate} /> );
|
|
67
|
+
function ContainerizedComponent(props: { options: AppOptions; page: Page; navigate: NavigateFunction }) {
|
|
68
|
+
if (props.options.pageContainer && !props.page.noPageContainer) {
|
|
69
|
+
return <props.options.pageContainer page={props.page} />;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return ( <props.pageNotFound /> );
|
|
72
|
+
return <props.page.component urlParams={createUrlParams()} navigate={props.navigate} />;
|
|
73
|
+
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
function PageNotFound(props: { pageNotFound: AppOptions['pageNotFound'] }) {
|
|
76
|
+
if (props.pageNotFound) {
|
|
77
|
+
return <props.pageNotFound />;
|
|
68
78
|
}
|
|
79
|
+
|
|
80
|
+
return <h1>404: Page not found</h1>;
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
function getPath(path: string) {
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
if (path.startsWith('/')) {
|
|
86
|
+
return path;
|
|
87
|
+
}
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
}
|
|
89
|
+
return `/${path}`;
|
|
90
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useLocation } from 'react-router';
|
|
2
2
|
|
|
3
3
|
export function createUrlParams() {
|
|
4
|
-
const urlParams: {[paramName: string]: string} = {};
|
|
4
|
+
const urlParams: { [paramName: string]: string } = {};
|
|
5
5
|
const location = useLocation();
|
|
6
6
|
const serializedParams = location.search.substring(1).split('&');
|
|
7
|
-
for (
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
for (const serializedParam of serializedParams) {
|
|
8
|
+
if (serializedParam === '') {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const kvp = serializedParam.split('=');
|
|
13
|
+
urlParams[decodeURIComponent(kvp[0])] = decodeURIComponent(kvp[1]);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
return urlParams;
|
|
16
|
-
}
|
|
17
|
+
}
|