@proteinjs/ui 1.0.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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/dist/generated/index.d.ts +17 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +40 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/src/container/AccountIconButton.d.ts +20 -0
- package/dist/src/container/AccountIconButton.d.ts.map +1 -0
- package/dist/src/container/AccountIconButton.js +112 -0
- package/dist/src/container/AccountIconButton.js.map +1 -0
- package/dist/src/container/NavMenu.d.ts +16 -0
- package/dist/src/container/NavMenu.d.ts.map +1 -0
- package/dist/src/container/NavMenu.js +51 -0
- package/dist/src/container/NavMenu.js.map +1 -0
- package/dist/src/container/PageContainer.d.ts +31 -0
- package/dist/src/container/PageContainer.d.ts.map +1 -0
- package/dist/src/container/PageContainer.js +75 -0
- package/dist/src/container/PageContainer.js.map +1 -0
- package/dist/src/form/Field.d.ts +34 -0
- package/dist/src/form/Field.d.ts.map +1 -0
- package/dist/src/form/Field.js +29 -0
- package/dist/src/form/Field.js.map +1 -0
- package/dist/src/form/Form.d.ts +46 -0
- package/dist/src/form/Form.d.ts.map +1 -0
- package/dist/src/form/Form.js +377 -0
- package/dist/src/form/Form.js.map +1 -0
- package/dist/src/form/FormButton.d.ts +28 -0
- package/dist/src/form/FormButton.d.ts.map +1 -0
- package/dist/src/form/FormButton.js +18 -0
- package/dist/src/form/FormButton.js.map +1 -0
- package/dist/src/form/FunctionalForm.d.ts +1 -0
- package/dist/src/form/FunctionalForm.d.ts.map +1 -0
- package/dist/src/form/FunctionalForm.js +328 -0
- package/dist/src/form/FunctionalForm.js.map +1 -0
- package/dist/src/form/container/FormPage.d.ts +4 -0
- package/dist/src/form/container/FormPage.d.ts.map +1 -0
- package/dist/src/form/container/FormPage.js +29 -0
- package/dist/src/form/container/FormPage.js.map +1 -0
- package/dist/src/form/container/FormPaper.d.ts +4 -0
- package/dist/src/form/container/FormPaper.d.ts.map +1 -0
- package/dist/src/form/container/FormPaper.js +27 -0
- package/dist/src/form/container/FormPaper.js.map +1 -0
- package/dist/src/form/fields/TextField.d.ts +6 -0
- package/dist/src/form/fields/TextField.d.ts.map +1 -0
- package/dist/src/form/fields/TextField.js +70 -0
- package/dist/src/form/fields/TextField.js.map +1 -0
- package/dist/src/list/NestedList.d.ts +14 -0
- package/dist/src/list/NestedList.d.ts.map +1 -0
- package/dist/src/list/NestedList.js +44 -0
- package/dist/src/list/NestedList.js.map +1 -0
- package/dist/src/router/Page.d.ts +28 -0
- package/dist/src/router/Page.d.ts.map +1 -0
- package/dist/src/router/Page.js +7 -0
- package/dist/src/router/Page.js.map +1 -0
- package/dist/src/router/Router.d.ts +14 -0
- package/dist/src/router/Router.d.ts.map +1 -0
- package/dist/src/router/Router.js +66 -0
- package/dist/src/router/Router.js.map +1 -0
- package/dist/src/router/createUrlParams.d.ts +4 -0
- package/dist/src/router/createUrlParams.d.ts.map +1 -0
- package/dist/src/router/createUrlParams.js +19 -0
- package/dist/src/router/createUrlParams.js.map +1 -0
- package/dist/src/router/withRouter.d.ts +7 -0
- package/dist/src/router/withRouter.d.ts.map +1 -0
- package/dist/src/router/withRouter.js +28 -0
- package/dist/src/router/withRouter.js.map +1 -0
- package/dist/src/table/Table.d.ts +14 -0
- package/dist/src/table/Table.d.ts.map +1 -0
- package/dist/src/table/Table.js +170 -0
- package/dist/src/table/Table.js.map +1 -0
- package/dist/src/table/TableButton.d.ts +12 -0
- package/dist/src/table/TableButton.d.ts.map +1 -0
- package/dist/src/table/TableButton.js +3 -0
- package/dist/src/table/TableButton.js.map +1 -0
- package/dist/src/table/TableLoader.d.ts +16 -0
- package/dist/src/table/TableLoader.d.ts.map +1 -0
- package/dist/src/table/TableLoader.js +57 -0
- package/dist/src/table/TableLoader.js.map +1 -0
- package/dist/src/table/TableToolbar.d.ts +10 -0
- package/dist/src/table/TableToolbar.d.ts.map +1 -0
- package/dist/src/table/TableToolbar.js +59 -0
- package/dist/src/table/TableToolbar.js.map +1 -0
- package/generated/index.ts +37 -0
- package/index.ts +20 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/src/container/AccountIconButton.tsx +118 -0
- package/src/container/NavMenu.tsx +91 -0
- package/src/container/PageContainer.tsx +108 -0
- package/src/form/Field.tsx +51 -0
- package/src/form/Form.tsx +405 -0
- package/src/form/FormButton.tsx +30 -0
- package/src/form/FunctionalForm.tsx +364 -0
- package/src/form/container/FormPage.tsx +23 -0
- package/src/form/container/FormPaper.tsx +16 -0
- package/src/form/fields/TextField.tsx +71 -0
- package/src/list/NestedList.tsx +61 -0
- package/src/router/Page.ts +28 -0
- package/src/router/Router.tsx +76 -0
- package/src/router/createUrlParams.ts +16 -0
- package/src/router/withRouter.tsx +21 -0
- package/src/table/Table.tsx +195 -0
- package/src/table/TableButton.ts +12 -0
- package/src/table/TableLoader.ts +19 -0
- package/src/table/TableToolbar.tsx +99 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// import React from 'react';
|
|
2
|
+
// import { Button, Container, Grid, IconButton, Typography, LinearProgress } from '@material-ui/core';
|
|
3
|
+
// import { makeStyles, styled } from '@material-ui/core/styles';
|
|
4
|
+
// import { compose, sizing, spacing } from '@material-ui/system';
|
|
5
|
+
// import queryString from 'query-string';
|
|
6
|
+
// import { history } from '../util/history';
|
|
7
|
+
// import { Field, FieldComponent, Fields } from './Field';
|
|
8
|
+
// import { FormButton, FormButtons } from './FormButton';
|
|
9
|
+
|
|
10
|
+
// const useStyles = makeStyles(theme => ({
|
|
11
|
+
// root: {
|
|
12
|
+
// padding: 0
|
|
13
|
+
// },
|
|
14
|
+
// title: {
|
|
15
|
+
// marginBottom: theme.spacing(4),
|
|
16
|
+
// },
|
|
17
|
+
// status: {
|
|
18
|
+
// marginBottom: theme.spacing(2)
|
|
19
|
+
// },
|
|
20
|
+
// button: {
|
|
21
|
+
// margin: theme.spacing(6, 0, 2),
|
|
22
|
+
// }
|
|
23
|
+
// }));
|
|
24
|
+
|
|
25
|
+
// export const RootBox = styled('div')(
|
|
26
|
+
// compose(
|
|
27
|
+
// sizing,
|
|
28
|
+
// spacing
|
|
29
|
+
// )
|
|
30
|
+
// );
|
|
31
|
+
|
|
32
|
+
// export type FormProps<F extends Fields, B extends FormButtons<F>> = {
|
|
33
|
+
// name?: string,
|
|
34
|
+
// documentation?: React.ComponentType,
|
|
35
|
+
// createFields: () => F,
|
|
36
|
+
// fieldLayout?: (keyof F)[]|(keyof F)[][],
|
|
37
|
+
// buttons: B,
|
|
38
|
+
// onLoad?: (fields: F, buttons: B) => Promise<void>,
|
|
39
|
+
// onLoadProgressMessage?: string
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// export function Form<F extends Fields, B extends FormButtons<F>>(props: FormProps<F, B>) {
|
|
43
|
+
// const classes = useStyles(props);
|
|
44
|
+
// const [status, setStatus] = React.useState<{ message?: string, isError?: boolean }>();
|
|
45
|
+
// const [fields, setFields] = React.useState<F>();
|
|
46
|
+
// const [progress, setProgress] = React.useState<{ visible?: boolean, message?: string }>();
|
|
47
|
+
// const [fieldRenderKeys, setFieldRenderKeys] = React.useState<{[fieldName: string]: number}>({});
|
|
48
|
+
// // const [fieldRenderKeys, dispatchFieldRenderKeys] = React.useReducer((state: {[fieldName: string]: number}, action: any) => {
|
|
49
|
+
// // // const counter = typeof state[action.field] !== 'undefined' ? state[action.field] + 1 : 1;
|
|
50
|
+
// // // const fieldUpdate: any = {};
|
|
51
|
+
// // // fieldUpdate[action.field] = counter;
|
|
52
|
+
// // // return Object.assign(state, fieldUpdate);
|
|
53
|
+
// // state[action.field] = typeof state[action.field] !== 'undefined' ? state[action.field] + 1 : 1;
|
|
54
|
+
// // return Object.assign({}, state);
|
|
55
|
+
// // }, {});
|
|
56
|
+
// const [renderFields, setRenderFields] = React.useState(0);
|
|
57
|
+
// const [focusedField, setFocusedField] = React.useState<string>();
|
|
58
|
+
// React.useEffect(() => {
|
|
59
|
+
// onLoad();
|
|
60
|
+
// }, []);
|
|
61
|
+
|
|
62
|
+
// async function onLoad() {
|
|
63
|
+
// setProgress({ visible: true, message: props.onLoadProgressMessage });
|
|
64
|
+
// const newFields = props.createFields();
|
|
65
|
+
// try {
|
|
66
|
+
// for (const fieldPropertyName in newFields) {
|
|
67
|
+
// const field = newFields[fieldPropertyName].field;
|
|
68
|
+
// if (!field)
|
|
69
|
+
// continue;
|
|
70
|
+
|
|
71
|
+
// if (field.onLoad)
|
|
72
|
+
// await field.onLoad(newFields);
|
|
73
|
+
|
|
74
|
+
// if (!field.accessibility)
|
|
75
|
+
// field.accessibility = {};
|
|
76
|
+
// Object.defineProperty(field.accessibility, 'required', {
|
|
77
|
+
// get: function() {
|
|
78
|
+
// return this._required;
|
|
79
|
+
// },
|
|
80
|
+
// set: function(value) {
|
|
81
|
+
// console.log(`running set value for field: ${field.name}`);
|
|
82
|
+
// // const renderKeyUpdate: any = {};
|
|
83
|
+
// // renderKeyUpdate[field.name] = typeof renderKeyUpdate[field.name] !== 'undefined' ? renderKeyUpdate[field.name] + 1 : 1;
|
|
84
|
+
// // setFieldRenderKeys(Object.assign(fieldRenderKeys, renderKeyUpdate));
|
|
85
|
+
// fieldRenderKeys[field.name] = typeof fieldRenderKeys[field.name] !== 'undefined' ? fieldRenderKeys[field.name] + 1 : 1;
|
|
86
|
+
// setFieldRenderKeys(Object.assign({}, fieldRenderKeys));
|
|
87
|
+
// // dispatchFieldRenderKeys({ field: field.name });
|
|
88
|
+
// // if (typeof fieldRenderKeys[field.name] !== 'number')
|
|
89
|
+
// // fieldRenderKeys[field.name] = 0;
|
|
90
|
+
|
|
91
|
+
// // fieldRenderKeys[field.name] = fieldRenderKeys[field.name] + 1;
|
|
92
|
+
// this._required = value;
|
|
93
|
+
// }
|
|
94
|
+
// });
|
|
95
|
+
// }
|
|
96
|
+
|
|
97
|
+
// if (props.onLoad)
|
|
98
|
+
// await props.onLoad(newFields, props.buttons);
|
|
99
|
+
// } catch (error) {
|
|
100
|
+
// console.error(`Failed while running onLoad functions`, error);
|
|
101
|
+
// }
|
|
102
|
+
|
|
103
|
+
// setStatus({});
|
|
104
|
+
// setFields(newFields);
|
|
105
|
+
// setProgress({ visible: false });
|
|
106
|
+
// }
|
|
107
|
+
|
|
108
|
+
// async function onChange(field: Field<any, any>, value: any, setFieldStatus: (message: string, isError: boolean) => void) {
|
|
109
|
+
// field.value = value;
|
|
110
|
+
// if (field.onChange) {
|
|
111
|
+
// try {
|
|
112
|
+
// await field.onChange(field.value, fields, setFieldStatus);
|
|
113
|
+
// } catch (error) {
|
|
114
|
+
// console.error(`Failed while running onChange for field: ${field.name}`, error);
|
|
115
|
+
// }
|
|
116
|
+
// }
|
|
117
|
+
|
|
118
|
+
// setFocusedField(field.name);
|
|
119
|
+
// // setFields(Object.assign({}, fields));
|
|
120
|
+
// // setFieldRenderKeys(Object.assign({}, fieldRenderKeys));
|
|
121
|
+
// // dispatchFieldRenderKeys({ type: 'update' })
|
|
122
|
+
// // setRenderFields(renderFields +1);
|
|
123
|
+
// }
|
|
124
|
+
|
|
125
|
+
// async function onClick(button: FormButton<any>) {
|
|
126
|
+
// if (button.onClick) {
|
|
127
|
+
// setProgress({ visible: true, message: button.progressMessage ? button.progressMessage(fields) : undefined });
|
|
128
|
+
// try {
|
|
129
|
+
// const successMessage = await button.onClick(fields, props.buttons);
|
|
130
|
+
// if (successMessage)
|
|
131
|
+
// setStatus({ message: successMessage, isError: false});
|
|
132
|
+
// } catch (error) {
|
|
133
|
+
// setStatus({ message: error.message, isError: true});
|
|
134
|
+
// console.error(`Error when clicking button: ${button.name}`, error);
|
|
135
|
+
// }
|
|
136
|
+
// setProgress({ visible: false });
|
|
137
|
+
// }
|
|
138
|
+
|
|
139
|
+
// if (button.redirect) {
|
|
140
|
+
// let path = button.redirect.path;
|
|
141
|
+
// if (button.redirect.props)
|
|
142
|
+
// path += `?${queryString.stringify(button.redirect.props)}`;
|
|
143
|
+
// history.push(path);
|
|
144
|
+
// return;
|
|
145
|
+
// }
|
|
146
|
+
|
|
147
|
+
// if (button.clearFormOnClick)
|
|
148
|
+
// await onLoad();
|
|
149
|
+
// }
|
|
150
|
+
|
|
151
|
+
// return (
|
|
152
|
+
// <Container>
|
|
153
|
+
// <form autoComplete='off'>
|
|
154
|
+
// <Title />
|
|
155
|
+
// <Documentation />
|
|
156
|
+
// <Status />
|
|
157
|
+
// <Fields />
|
|
158
|
+
// <Progress />
|
|
159
|
+
// <Buttons />
|
|
160
|
+
// </form>
|
|
161
|
+
// </Container>
|
|
162
|
+
// );
|
|
163
|
+
|
|
164
|
+
// function Title() {
|
|
165
|
+
// if (!props.name)
|
|
166
|
+
// return null;
|
|
167
|
+
|
|
168
|
+
// return (
|
|
169
|
+
// <Grid container justify='flex-start' alignItems='flex-start' spacing={2}>
|
|
170
|
+
// <Grid item>
|
|
171
|
+
// <Typography variant='h5' className={classes.title}>
|
|
172
|
+
// {props.name}
|
|
173
|
+
// </Typography>
|
|
174
|
+
// </Grid>
|
|
175
|
+
// </Grid>
|
|
176
|
+
// );
|
|
177
|
+
// }
|
|
178
|
+
|
|
179
|
+
// function Documentation() {
|
|
180
|
+
// if (!props.documentation)
|
|
181
|
+
// return null;
|
|
182
|
+
|
|
183
|
+
// return (
|
|
184
|
+
// <Grid container justify='flex-start' alignItems='flex-start'>
|
|
185
|
+
// <Grid item>
|
|
186
|
+
// <props.documentation />
|
|
187
|
+
// </Grid>
|
|
188
|
+
// </Grid>
|
|
189
|
+
// );
|
|
190
|
+
// }
|
|
191
|
+
|
|
192
|
+
// function Status() {
|
|
193
|
+
// if (!status || !status.message)
|
|
194
|
+
// return null;
|
|
195
|
+
|
|
196
|
+
// return (
|
|
197
|
+
// <Grid container>
|
|
198
|
+
// <Grid item xs={12} className={classes.status}>
|
|
199
|
+
// <Typography
|
|
200
|
+
// variant='subtitle1'
|
|
201
|
+
// color={status.isError ? 'error' : 'primary'}
|
|
202
|
+
// >
|
|
203
|
+
// {status.message}
|
|
204
|
+
// </Typography>
|
|
205
|
+
// </Grid>
|
|
206
|
+
// </Grid>
|
|
207
|
+
// );
|
|
208
|
+
// }
|
|
209
|
+
|
|
210
|
+
// function Fields() {
|
|
211
|
+
// return (
|
|
212
|
+
// <Grid container spacing={8}>
|
|
213
|
+
// {
|
|
214
|
+
// getFieldRows().map((fieldComponents, index) => {
|
|
215
|
+
// if (!fieldRowVisible(fieldComponents))
|
|
216
|
+
// return null;
|
|
217
|
+
|
|
218
|
+
// return (
|
|
219
|
+
// <Grid item xs={12} key={index}>
|
|
220
|
+
// <Grid container spacing={8}>
|
|
221
|
+
// {
|
|
222
|
+
// fieldComponents.filter((fieldComponent) => {
|
|
223
|
+
// if (fieldComponent.field.accessibility?.hidden)
|
|
224
|
+
// return false;
|
|
225
|
+
|
|
226
|
+
// return true;
|
|
227
|
+
// }).map((fieldComponent, key) => {
|
|
228
|
+
// const fieldKey = fieldRenderKeys[fieldComponent.field.name] ? `${fieldComponent.field.name}_${fieldRenderKeys[fieldComponent.field.name]}` : `${fieldComponent.field.name}`;
|
|
229
|
+
// console.log(fieldKey);
|
|
230
|
+
// return (
|
|
231
|
+
// <Grid item xs={12} sm={fieldComponent.field.layout?.width as 1} key={fieldRenderKeys[fieldComponent.field.name] ? `${fieldComponent.field.name}_${fieldRenderKeys[fieldComponent.field.name]}` : `${fieldComponent.field.name}`}>
|
|
232
|
+
// <fieldComponent.component
|
|
233
|
+
// field={fieldComponent.field}
|
|
234
|
+
// onChange={onChange}
|
|
235
|
+
// // focused={focusedField == fieldComponent.field.name}
|
|
236
|
+
// />
|
|
237
|
+
// </Grid>
|
|
238
|
+
// )})
|
|
239
|
+
// }
|
|
240
|
+
// </Grid>
|
|
241
|
+
// </Grid>
|
|
242
|
+
// );
|
|
243
|
+
// })
|
|
244
|
+
// }
|
|
245
|
+
// </Grid>
|
|
246
|
+
// );
|
|
247
|
+
// }
|
|
248
|
+
|
|
249
|
+
// function getFieldRows(): FieldComponent<any, any>[][] {
|
|
250
|
+
// const rows: FieldComponent<any, any>[][] = [];
|
|
251
|
+
// if (!fields)
|
|
252
|
+
// return rows;
|
|
253
|
+
|
|
254
|
+
// if (props.fieldLayout) {
|
|
255
|
+
// if (typeof props.fieldLayout[0] === 'string') {
|
|
256
|
+
// for (let i = 0; i < props.fieldLayout.length; i++) {
|
|
257
|
+
// const fieldPropertyName = props.fieldLayout[i] as string;
|
|
258
|
+
// const fieldComponent = fields[fieldPropertyName];
|
|
259
|
+
// fieldComponent.field.layout = { row: i, width: 12 };
|
|
260
|
+
// rows.push([fieldComponent]);
|
|
261
|
+
// }
|
|
262
|
+
// } else {
|
|
263
|
+
// for (let i = 0; i < props.fieldLayout.length; i++) {
|
|
264
|
+
// const row = props.fieldLayout[i] as string[];
|
|
265
|
+
// const columns = row.length;
|
|
266
|
+
// if (columns > 6)
|
|
267
|
+
// throw new Error(`When using FormProps.fieldLayout, the maximum number of fields per row is 6, provided: ${columns}. For more granular layout control use Field.layout`);
|
|
268
|
+
|
|
269
|
+
// const currentRow: FieldComponent<any, any>[] = [];
|
|
270
|
+
// for (const fieldPropertyName of row ) {
|
|
271
|
+
// const fieldComponent = fields[fieldPropertyName];
|
|
272
|
+
// fieldComponent.field.layout = { row: i, width: columns == 5 ? 2 : 12/columns as 1 };
|
|
273
|
+
// currentRow.push(fieldComponent);
|
|
274
|
+
// }
|
|
275
|
+
// rows.push(currentRow);
|
|
276
|
+
// }
|
|
277
|
+
// }
|
|
278
|
+
// } else {
|
|
279
|
+
// for (const fieldPropertyName in fields) {
|
|
280
|
+
// const fieldComponent: FieldComponent<any, any> = fields[fieldPropertyName];
|
|
281
|
+
// const field = fieldComponent.field;
|
|
282
|
+
// if (!field.layout)
|
|
283
|
+
// throw new Error(`Unless using FormProps.fieldLayout, Field.layout must be provided; layout not provided for field: ${field.name}`);
|
|
284
|
+
|
|
285
|
+
// if (typeof rows[field.layout.row] === 'undefined')
|
|
286
|
+
// rows[field.layout.row] = [];
|
|
287
|
+
|
|
288
|
+
// if (field.layout.column && rows[field.layout.row].length >= field.layout.column) {
|
|
289
|
+
// rows[field.layout.row].splice(field.layout.column - 1, 0, fieldComponent);
|
|
290
|
+
// } else
|
|
291
|
+
// rows[field.layout.row].push(fieldComponent);
|
|
292
|
+
|
|
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
|
+
// if (rowWidth > 12)
|
|
295
|
+
// throw new Error(`Width of row exceeds maximum width (12), row width: ${rowWidth}, row index: ${field.layout.row}`);
|
|
296
|
+
// }
|
|
297
|
+
// }
|
|
298
|
+
|
|
299
|
+
// return rows;
|
|
300
|
+
// }
|
|
301
|
+
|
|
302
|
+
// function fieldRowVisible(fieldComponents: FieldComponent<any, any>[]): boolean {
|
|
303
|
+
// for (const fieldComponent of fieldComponents) {
|
|
304
|
+
// if (!fieldComponent.field.accessibility || !fieldComponent.field.accessibility?.hidden)
|
|
305
|
+
// return true;
|
|
306
|
+
// }
|
|
307
|
+
|
|
308
|
+
// return false;
|
|
309
|
+
// }
|
|
310
|
+
|
|
311
|
+
// function Progress() {
|
|
312
|
+
// if (!progress || !progress.visible)
|
|
313
|
+
// return null;
|
|
314
|
+
|
|
315
|
+
// return (
|
|
316
|
+
// <Grid container justify='center' alignItems='center' spacing={2}>
|
|
317
|
+
// <Grid item>
|
|
318
|
+
// <LinearProgress variant='indeterminate' color='primary' />
|
|
319
|
+
// </Grid>
|
|
320
|
+
// </Grid>
|
|
321
|
+
// );
|
|
322
|
+
// }
|
|
323
|
+
|
|
324
|
+
// function Buttons() {
|
|
325
|
+
// return (
|
|
326
|
+
// <Grid container justify='flex-end' alignItems='flex-end' spacing={2}>
|
|
327
|
+
// {
|
|
328
|
+
// Object.keys(props.buttons).map((buttonPropertyName) => props.buttons[buttonPropertyName]).filter((button) => {
|
|
329
|
+
// if (button.accessibility?.hidden)
|
|
330
|
+
// return false;
|
|
331
|
+
|
|
332
|
+
// return true;
|
|
333
|
+
// }).map((button, key) => {
|
|
334
|
+
// return (
|
|
335
|
+
// <Grid item key={key}>
|
|
336
|
+
// { button.style.icon ?
|
|
337
|
+
// <RootBox className={classes.button}>
|
|
338
|
+
// <IconButton
|
|
339
|
+
// disabled={button.accessibility?.disabled}
|
|
340
|
+
// onClick={event => onClick(button)}
|
|
341
|
+
// >
|
|
342
|
+
// <button.style.icon />
|
|
343
|
+
// </IconButton>
|
|
344
|
+
// </RootBox>
|
|
345
|
+
// :
|
|
346
|
+
// <RootBox className={classes.button}>
|
|
347
|
+
// <Button
|
|
348
|
+
// color={button.style.color}
|
|
349
|
+
// variant={button.style.variant ? 'contained' : 'text'}
|
|
350
|
+
// disabled={button.accessibility?.disabled}
|
|
351
|
+
// onClick={event => onClick(button)}
|
|
352
|
+
// >
|
|
353
|
+
// {button.name}
|
|
354
|
+
// </Button>
|
|
355
|
+
// </RootBox>
|
|
356
|
+
// }
|
|
357
|
+
// </Grid>
|
|
358
|
+
// )
|
|
359
|
+
// })
|
|
360
|
+
// }
|
|
361
|
+
// </Grid>
|
|
362
|
+
// );
|
|
363
|
+
// }
|
|
364
|
+
// }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Grid, PaperProps } from '@mui/material';
|
|
3
|
+
import { FormPaper } from './FormPaper';
|
|
4
|
+
|
|
5
|
+
export function FormPage(props: PaperProps) {
|
|
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}>
|
|
18
|
+
{props.children}
|
|
19
|
+
</FormPaper>
|
|
20
|
+
</Grid>
|
|
21
|
+
</Grid>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PaperProps, Paper } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
export function FormPaper(props: PaperProps) {
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { InputAdornment, TextField as MuiTextField, IconButton } from '@mui/material';
|
|
3
|
+
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
|
4
|
+
import { Field, FieldComponent, FieldComponentProps, fieldDisplayValue, fieldLabel, Fields } from '../Field';
|
|
5
|
+
|
|
6
|
+
export type TextFieldProps<T, F extends Fields> = Field<T, F> & {
|
|
7
|
+
isPassword?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function textField<F extends Fields>(props: TextFieldProps<string, F>): FieldComponent<string, F> {
|
|
11
|
+
const { isPassword } = props;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
field: props,
|
|
15
|
+
component: TextField
|
|
16
|
+
};
|
|
17
|
+
|
|
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);
|
|
23
|
+
|
|
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(() => { // setFieldStatus may not be called in Field.onChange, but we still want to run after Form.onChange
|
|
47
|
+
if (!errorReceived)
|
|
48
|
+
setError(false);
|
|
49
|
+
|
|
50
|
+
if (!messageReceived)
|
|
51
|
+
setStatusMessage(undefined);
|
|
52
|
+
})
|
|
53
|
+
}}
|
|
54
|
+
InputProps={{
|
|
55
|
+
endAdornment: (
|
|
56
|
+
isPassword &&
|
|
57
|
+
<InputAdornment position='end'>
|
|
58
|
+
<IconButton
|
|
59
|
+
aria-label='Toggle password visibility'
|
|
60
|
+
onClick={event => setPasswordVisible(!passwordVisible)}
|
|
61
|
+
>
|
|
62
|
+
{passwordVisible ? <VisibilityOff /> : <Visibility />}
|
|
63
|
+
</IconButton>
|
|
64
|
+
</InputAdornment>
|
|
65
|
+
)
|
|
66
|
+
}}
|
|
67
|
+
{...other}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { ReactElement } from 'react'
|
|
2
|
+
import { Box } from '@mui/material'
|
|
3
|
+
|
|
4
|
+
export type NestedListItem = { element: ReactElement, children?: NestedListItem[], renderChildren?: boolean }
|
|
5
|
+
type NestedListProps = { items: NestedListItem[], level?: number, levelColors?: string[] }
|
|
6
|
+
const blue = '#1976d2';
|
|
7
|
+
const green = '#76d275';
|
|
8
|
+
const orange = '#ffa726';
|
|
9
|
+
const purple = '#ab47bc';
|
|
10
|
+
const defaultLevelColors = [blue, green, orange, purple];
|
|
11
|
+
|
|
12
|
+
export const NestedList = ({ items, level, levelColors }: NestedListProps) => {
|
|
13
|
+
const isRoot = typeof level === 'undefined';
|
|
14
|
+
const resolvedLevel = level ? level : 0;
|
|
15
|
+
const resolvedLevelColors = levelColors ? levelColors : defaultLevelColors;
|
|
16
|
+
const flattenedItems = flattenItems(items);
|
|
17
|
+
|
|
18
|
+
function flattenItems(items: NestedListItem[]): (NestedListItem|NestedListItem[])[] {
|
|
19
|
+
const flattened = [];
|
|
20
|
+
for (let item of items) {
|
|
21
|
+
flattened.push(item);
|
|
22
|
+
if (item.children && (item.renderChildren || typeof item.renderChildren === 'undefined'))
|
|
23
|
+
flattened.push(item.children);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return flattened;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box
|
|
31
|
+
component='ul'
|
|
32
|
+
sx={{
|
|
33
|
+
padding: isRoot ? 0 : undefined,
|
|
34
|
+
margin: isRoot ? 0 : undefined,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{
|
|
38
|
+
flattenedItems.map((item, index) => (Array.isArray(item)) ?
|
|
39
|
+
<NestedList
|
|
40
|
+
key={index}
|
|
41
|
+
items={item}
|
|
42
|
+
level={resolvedLevel + 1}
|
|
43
|
+
/>
|
|
44
|
+
:
|
|
45
|
+
<Box
|
|
46
|
+
key={index}
|
|
47
|
+
component='li'
|
|
48
|
+
sx={(theme) => ({
|
|
49
|
+
borderLeft: 3,
|
|
50
|
+
borderLeftColor: resolvedLevelColors[resolvedLevel%resolvedLevelColors.length],
|
|
51
|
+
listStyleType: 'none',
|
|
52
|
+
marginTop: !(isRoot && index == 0) ? theme.spacing(1) : undefined,
|
|
53
|
+
})}
|
|
54
|
+
>
|
|
55
|
+
{item.element}
|
|
56
|
+
</Box>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SxProps } from '@mui/material';
|
|
3
|
+
import { Loadable, SourceRepository } from '@proteinjs/reflection';
|
|
4
|
+
import { NavigateFunction } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
export const getPages = () => SourceRepository.get().objects<Page>('@proteinjs/ui/Page');
|
|
7
|
+
|
|
8
|
+
export type PageComponentProps = {
|
|
9
|
+
urlParams: {[key: string]: string},
|
|
10
|
+
navigate: NavigateFunction,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Page extends Loadable {
|
|
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?: SxProps;
|
|
28
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Route, Routes } from 'react-router';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter, useNavigate, NavigateFunction } from 'react-router-dom';
|
|
5
|
+
import { CssBaseline } from '@mui/material';
|
|
6
|
+
import { Page, getPages } from './Page';
|
|
7
|
+
import { createUrlParams } from './createUrlParams';
|
|
8
|
+
|
|
9
|
+
export type AppOptions = {
|
|
10
|
+
pageContainer?: React.ComponentType<{ page: Page }>,
|
|
11
|
+
pageNotFound?: React.ComponentType
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadApp(options: AppOptions = {}) {
|
|
15
|
+
const container = document.getElementById('app');
|
|
16
|
+
const root = createRoot(container!);
|
|
17
|
+
root.render(<Router pages={getPages()} options={options} />);
|
|
18
|
+
}
|
|
19
|
+
|
|
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();
|
|
33
|
+
return (
|
|
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} /> );
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function PageNotFound(props: { pageNotFound: AppOptions['pageNotFound'] }) {
|
|
64
|
+
if (props.pageNotFound)
|
|
65
|
+
return ( <props.pageNotFound /> );
|
|
66
|
+
|
|
67
|
+
return <h1>404: Page not found</h1>;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getPath(path: string) {
|
|
72
|
+
if (path.startsWith('/'))
|
|
73
|
+
return path;
|
|
74
|
+
|
|
75
|
+
return `/${path}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useLocation } from 'react-router';
|
|
2
|
+
|
|
3
|
+
export function createUrlParams() {
|
|
4
|
+
const urlParams: {[paramName: string]: string} = {};
|
|
5
|
+
const location = useLocation();
|
|
6
|
+
const serializedParams = location.search.substring(1).split('&');
|
|
7
|
+
for (let serializedParam of serializedParams) {
|
|
8
|
+
if (serializedParam === '')
|
|
9
|
+
continue;
|
|
10
|
+
|
|
11
|
+
const kvp = serializedParam.split('=');
|
|
12
|
+
urlParams[decodeURIComponent(kvp[0])] = decodeURIComponent(kvp[1]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return urlParams;
|
|
16
|
+
}
|