@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/dist/generated/index.d.ts +17 -0
  4. package/dist/generated/index.d.ts.map +1 -0
  5. package/dist/generated/index.js +40 -0
  6. package/dist/generated/index.js.map +1 -0
  7. package/dist/index.d.ts +15 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +31 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/src/container/AccountIconButton.d.ts +20 -0
  12. package/dist/src/container/AccountIconButton.d.ts.map +1 -0
  13. package/dist/src/container/AccountIconButton.js +112 -0
  14. package/dist/src/container/AccountIconButton.js.map +1 -0
  15. package/dist/src/container/NavMenu.d.ts +16 -0
  16. package/dist/src/container/NavMenu.d.ts.map +1 -0
  17. package/dist/src/container/NavMenu.js +51 -0
  18. package/dist/src/container/NavMenu.js.map +1 -0
  19. package/dist/src/container/PageContainer.d.ts +31 -0
  20. package/dist/src/container/PageContainer.d.ts.map +1 -0
  21. package/dist/src/container/PageContainer.js +75 -0
  22. package/dist/src/container/PageContainer.js.map +1 -0
  23. package/dist/src/form/Field.d.ts +34 -0
  24. package/dist/src/form/Field.d.ts.map +1 -0
  25. package/dist/src/form/Field.js +29 -0
  26. package/dist/src/form/Field.js.map +1 -0
  27. package/dist/src/form/Form.d.ts +46 -0
  28. package/dist/src/form/Form.d.ts.map +1 -0
  29. package/dist/src/form/Form.js +377 -0
  30. package/dist/src/form/Form.js.map +1 -0
  31. package/dist/src/form/FormButton.d.ts +28 -0
  32. package/dist/src/form/FormButton.d.ts.map +1 -0
  33. package/dist/src/form/FormButton.js +18 -0
  34. package/dist/src/form/FormButton.js.map +1 -0
  35. package/dist/src/form/FunctionalForm.d.ts +1 -0
  36. package/dist/src/form/FunctionalForm.d.ts.map +1 -0
  37. package/dist/src/form/FunctionalForm.js +328 -0
  38. package/dist/src/form/FunctionalForm.js.map +1 -0
  39. package/dist/src/form/container/FormPage.d.ts +4 -0
  40. package/dist/src/form/container/FormPage.d.ts.map +1 -0
  41. package/dist/src/form/container/FormPage.js +29 -0
  42. package/dist/src/form/container/FormPage.js.map +1 -0
  43. package/dist/src/form/container/FormPaper.d.ts +4 -0
  44. package/dist/src/form/container/FormPaper.d.ts.map +1 -0
  45. package/dist/src/form/container/FormPaper.js +27 -0
  46. package/dist/src/form/container/FormPaper.js.map +1 -0
  47. package/dist/src/form/fields/TextField.d.ts +6 -0
  48. package/dist/src/form/fields/TextField.d.ts.map +1 -0
  49. package/dist/src/form/fields/TextField.js +70 -0
  50. package/dist/src/form/fields/TextField.js.map +1 -0
  51. package/dist/src/list/NestedList.d.ts +14 -0
  52. package/dist/src/list/NestedList.d.ts.map +1 -0
  53. package/dist/src/list/NestedList.js +44 -0
  54. package/dist/src/list/NestedList.js.map +1 -0
  55. package/dist/src/router/Page.d.ts +28 -0
  56. package/dist/src/router/Page.d.ts.map +1 -0
  57. package/dist/src/router/Page.js +7 -0
  58. package/dist/src/router/Page.js.map +1 -0
  59. package/dist/src/router/Router.d.ts +14 -0
  60. package/dist/src/router/Router.d.ts.map +1 -0
  61. package/dist/src/router/Router.js +66 -0
  62. package/dist/src/router/Router.js.map +1 -0
  63. package/dist/src/router/createUrlParams.d.ts +4 -0
  64. package/dist/src/router/createUrlParams.d.ts.map +1 -0
  65. package/dist/src/router/createUrlParams.js +19 -0
  66. package/dist/src/router/createUrlParams.js.map +1 -0
  67. package/dist/src/router/withRouter.d.ts +7 -0
  68. package/dist/src/router/withRouter.d.ts.map +1 -0
  69. package/dist/src/router/withRouter.js +28 -0
  70. package/dist/src/router/withRouter.js.map +1 -0
  71. package/dist/src/table/Table.d.ts +14 -0
  72. package/dist/src/table/Table.d.ts.map +1 -0
  73. package/dist/src/table/Table.js +170 -0
  74. package/dist/src/table/Table.js.map +1 -0
  75. package/dist/src/table/TableButton.d.ts +12 -0
  76. package/dist/src/table/TableButton.d.ts.map +1 -0
  77. package/dist/src/table/TableButton.js +3 -0
  78. package/dist/src/table/TableButton.js.map +1 -0
  79. package/dist/src/table/TableLoader.d.ts +16 -0
  80. package/dist/src/table/TableLoader.d.ts.map +1 -0
  81. package/dist/src/table/TableLoader.js +57 -0
  82. package/dist/src/table/TableLoader.js.map +1 -0
  83. package/dist/src/table/TableToolbar.d.ts +10 -0
  84. package/dist/src/table/TableToolbar.d.ts.map +1 -0
  85. package/dist/src/table/TableToolbar.js +59 -0
  86. package/dist/src/table/TableToolbar.js.map +1 -0
  87. package/generated/index.ts +37 -0
  88. package/index.ts +20 -0
  89. package/jest.config.js +18 -0
  90. package/package.json +52 -0
  91. package/src/container/AccountIconButton.tsx +118 -0
  92. package/src/container/NavMenu.tsx +91 -0
  93. package/src/container/PageContainer.tsx +108 -0
  94. package/src/form/Field.tsx +51 -0
  95. package/src/form/Form.tsx +405 -0
  96. package/src/form/FormButton.tsx +30 -0
  97. package/src/form/FunctionalForm.tsx +364 -0
  98. package/src/form/container/FormPage.tsx +23 -0
  99. package/src/form/container/FormPaper.tsx +16 -0
  100. package/src/form/fields/TextField.tsx +71 -0
  101. package/src/list/NestedList.tsx +61 -0
  102. package/src/router/Page.ts +28 -0
  103. package/src/router/Router.tsx +76 -0
  104. package/src/router/createUrlParams.ts +16 -0
  105. package/src/router/withRouter.tsx +21 -0
  106. package/src/table/Table.tsx +195 -0
  107. package/src/table/TableButton.ts +12 -0
  108. package/src/table/TableLoader.ts +19 -0
  109. package/src/table/TableToolbar.tsx +99 -0
  110. package/tsconfig.json +18 -0
@@ -0,0 +1,405 @@
1
+ import React from 'react';
2
+ import { Button, Container, Grid, IconButton, Typography, LinearProgress } from '@mui/material';
3
+ import queryString from 'query-string';
4
+ import { Field, FieldComponent, Fields } from './Field';
5
+ import { FormButton, FormButtons } from './FormButton';
6
+ import { withRouter, WithRouterProps } from '../router/withRouter';
7
+
8
+ export type FormProps<F extends Fields, B extends FormButtons<F>> = {
9
+ name?: string,
10
+ documentation?: React.ComponentType,
11
+ createFields: () => F,
12
+ fieldLayout?: (keyof F)[]|(keyof F)[][],
13
+ buttons: B,
14
+ onLoad?: (fields: F, buttons: B) => Promise<void>,
15
+ onLoadProgressMessage?: string
16
+ } & Partial<WithRouterProps>
17
+
18
+ export type FormState<F extends Fields> = {
19
+ status?: { message?: string, isError?: boolean },
20
+ fields: F,
21
+ progress?: { visible?: boolean, message?: string },
22
+ onLoadExecuted?: boolean
23
+ }
24
+
25
+ export class FormComponent<F extends Fields, B extends FormButtons<F>> extends React.Component<FormProps<F, B>, FormState<F>> {
26
+ constructor(props: FormProps<F, B>) {
27
+ super(props);
28
+ this.state = { fields: props.createFields() };
29
+ }
30
+
31
+ componentDidMount() {
32
+ if (this.state.onLoadExecuted)
33
+ return;
34
+
35
+ this.onLoad();
36
+ }
37
+
38
+ private async onLoad() {
39
+ this.setState({ progress: { visible: true, message: this.props.onLoadProgressMessage }});
40
+ const newFields = this.props.createFields();
41
+ try {
42
+ for (const fieldPropertyName in newFields) {
43
+ const field = newFields[fieldPropertyName].field;
44
+ if (!field)
45
+ continue;
46
+
47
+ if (field.onLoad)
48
+ await field.onLoad(newFields);
49
+
50
+ if (!field.accessibility)
51
+ field.accessibility = {};
52
+ }
53
+
54
+ if (this.props.onLoad)
55
+ await this.props.onLoad(newFields, this.props.buttons);
56
+ } catch (error) {
57
+ console.error(`Failed while running onLoad functions`, error);
58
+ }
59
+
60
+ this.setState({
61
+ status: {},
62
+ fields: newFields,
63
+ progress: { visible: false },
64
+ onLoadExecuted: true
65
+ });
66
+ }
67
+
68
+ private async onChange(field: Field<any, any>, value: any, setFieldStatus: (message: string, isError: boolean) => void) {
69
+ field.value = value;
70
+ if (field.onChange) {
71
+ try {
72
+ await field.onChange(field.value, this.state.fields, setFieldStatus);
73
+ } catch (error) {
74
+ console.error(`Failed while running onChange for field: ${field.name}`, error);
75
+ }
76
+ }
77
+
78
+ this.setState(this.state);
79
+ }
80
+
81
+ private async onClick(button: FormButton<any>) {
82
+ if (button.onClick) {
83
+ this.setState({ progress: { visible: true, message: button.progressMessage ? button.progressMessage(this.state.fields) : undefined }});
84
+ try {
85
+ const successMessage = await button.onClick(this.state.fields, this.props.buttons);
86
+ if (successMessage)
87
+ this.setState({ status: { message: successMessage, isError: false}});
88
+ } catch (error: any) {
89
+ this.setState({ status: { message: error.message, isError: true}});
90
+ console.error(`Error when clicking button: ${button.name}`, error);
91
+ }
92
+ this.setState({ progress: { visible: false }});
93
+ }
94
+
95
+ if (button.redirect) {
96
+ const redirect = await button.redirect(this.state.fields, this.props.buttons);
97
+ let path = redirect.path;
98
+ if (redirect.props)
99
+ path += `?${queryString.stringify(redirect.props)}`;
100
+
101
+ if (this.props.navigate)
102
+ this.props.navigate(path);
103
+
104
+ return;
105
+ }
106
+
107
+ if (button.clearFormOnClick)
108
+ await this.onLoad();
109
+ }
110
+
111
+ render() {
112
+ return (
113
+ <Container sx={{ padding: 0 }} maxWidth={this.getContainerMaxWidth()}>
114
+ <form autoComplete='off'>
115
+ <Grid container>
116
+ {this.Title()}
117
+ {this.Documentation()}
118
+ {this.Status()}
119
+ {this.Fields()}
120
+ {this.Progress()}
121
+ {this.Buttons()}
122
+ </Grid>
123
+ </form>
124
+ </Container>
125
+ );
126
+ }
127
+
128
+ private getContainerMaxWidth(): 'xs'|'sm' {
129
+ if (this.props.fieldLayout) { // TODO validate that fields are not hidden
130
+ if (this.props.fieldLayout.length < 2)
131
+ return 'xs';
132
+
133
+ for (const row of this.props.fieldLayout) {
134
+ if ((row as string[]).length > 1)
135
+ return 'sm';
136
+ }
137
+ } else {
138
+ const rows: boolean[] = [];
139
+ for (const fieldPropertyName in this.state.fields) {
140
+ const field = this.state.fields[fieldPropertyName].field;
141
+ if (field.accessibility?.hidden)
142
+ continue;
143
+
144
+ if (!field.layout)
145
+ continue;
146
+
147
+ if (rows[field.layout.row])
148
+ return 'sm';
149
+
150
+ rows[field.layout.row] = true;
151
+ }
152
+ }
153
+
154
+ return 'xs';
155
+ }
156
+
157
+ private Title() {
158
+ if (!this.props.name)
159
+ return null;
160
+
161
+ return (
162
+ <Grid
163
+ container
164
+ xs={12}
165
+ justifyContent='flex-start'
166
+ alignItems='flex-start'
167
+ sx={(theme) => ({
168
+ marginTop: theme.spacing(1),
169
+ marginBottom: theme.spacing(3),
170
+ })}
171
+ >
172
+ <Typography
173
+ variant='h5'
174
+ >
175
+ {this.props.name}
176
+ </Typography>
177
+ </Grid>
178
+ );
179
+ }
180
+
181
+ private Documentation() {
182
+ if (!this.props.documentation)
183
+ return null;
184
+
185
+ return (
186
+ <Grid
187
+ container
188
+ xs={12}
189
+ justifyContent='flex-start'
190
+ alignItems='flex-start'
191
+ >
192
+ <Container>
193
+ <this.props.documentation />
194
+ </Container>
195
+ </Grid>
196
+ );
197
+ }
198
+
199
+ private Status() {
200
+ if (!this.state.status || !this.state.status.message)
201
+ return null;
202
+
203
+ return (
204
+ <Grid
205
+ container
206
+ xs={12}
207
+ >
208
+ <Container
209
+ sx={(theme) => ({
210
+ marginBottom: theme.spacing(1),
211
+ })}
212
+ >
213
+ <Typography
214
+ variant='subtitle1'
215
+ color={this.state.status.isError ? 'error' : 'primary'}
216
+ >
217
+ {this.state.status.message}
218
+ </Typography>
219
+ </Container>
220
+ </Grid>
221
+ );
222
+ }
223
+
224
+ private Fields() {
225
+ return (
226
+ <Grid
227
+ container
228
+ direction='column'
229
+ xs={12}
230
+ >
231
+ {
232
+ this.getFieldRows().map((fieldComponents, index) => {
233
+ if (!this.isFieldRowVisible(fieldComponents))
234
+ return null;
235
+
236
+ return (
237
+ <Grid
238
+ container
239
+ spacing={3}
240
+ alignItems='center'
241
+ sx={(theme) => ({
242
+ flexGrow: 1,
243
+ marginBottom: theme.spacing(3),
244
+ })}
245
+ key={index}
246
+ >
247
+ {
248
+ fieldComponents.filter((fieldComponent) => {
249
+ if (fieldComponent.field.accessibility?.hidden)
250
+ return false;
251
+
252
+ return true;
253
+ }).map((fieldComponent) => (
254
+ <Grid item xs key={fieldComponent.field.name}>
255
+ <fieldComponent.component
256
+ field={fieldComponent.field}
257
+ onChange={this.onChange.bind(this)}
258
+ />
259
+ </Grid>
260
+ ))
261
+ }
262
+ </Grid>
263
+ );
264
+ })
265
+ }
266
+ </Grid>
267
+ );
268
+ }
269
+
270
+ private getFieldRows(): FieldComponent<any, any>[][] {
271
+ const rows: FieldComponent<any, any>[][] = [];
272
+ if (!this.state.fields)
273
+ return rows;
274
+
275
+ if (this.props.fieldLayout) {
276
+ if (typeof this.props.fieldLayout[0] === 'string') {
277
+ for (let i = 0; i < this.props.fieldLayout.length; i++) {
278
+ const fieldPropertyName = this.props.fieldLayout[i] as string;
279
+ const fieldComponent = this.state.fields[fieldPropertyName];
280
+ fieldComponent.field.layout = { row: i, width: 12 };
281
+ rows.push([fieldComponent]);
282
+ }
283
+ } else {
284
+ for (let i = 0; i < this.props.fieldLayout.length; i++) {
285
+ const row = this.props.fieldLayout[i] as string[];
286
+ const columns = row.length;
287
+ if (columns > 6)
288
+ 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`);
289
+
290
+ const currentRow: FieldComponent<any, any>[] = [];
291
+ for (const fieldPropertyName of row ) {
292
+ const fieldComponent = this.state.fields[fieldPropertyName];
293
+ fieldComponent.field.layout = { row: i, width: columns == 5 ? 2 : 12/columns as 1 };
294
+ currentRow.push(fieldComponent);
295
+ }
296
+ rows.push(currentRow);
297
+ }
298
+ }
299
+ } else {
300
+ for (const fieldPropertyName in this.state.fields) {
301
+ const fieldComponent: FieldComponent<any, any> = this.state.fields[fieldPropertyName];
302
+ const field = fieldComponent.field;
303
+ if (!field.layout)
304
+ throw new Error(`Unless using FormProps.fieldLayout, Field.layout must be provided; layout not provided for field: ${field.name}`);
305
+
306
+ if (typeof rows[field.layout.row] === 'undefined')
307
+ rows[field.layout.row] = [];
308
+
309
+ if (field.layout.column && rows[field.layout.row].length >= field.layout.column) {
310
+ rows[field.layout.row].splice(field.layout.column - 1, 0, fieldComponent);
311
+ } else
312
+ rows[field.layout.row].push(fieldComponent);
313
+
314
+ const rowWidth = rows[field.layout.row].map((fieldComponent) => fieldComponent.field.layout?.width ? fieldComponent.field.layout.width as number : 0).reduce((accumulator, currentWidth) => accumulator + currentWidth);
315
+ if (rowWidth > 12)
316
+ throw new Error(`Width of row exceeds maximum width (12), row width: ${rowWidth}, row index: ${field.layout.row}`);
317
+ }
318
+ }
319
+
320
+ return rows;
321
+ }
322
+
323
+ private isFieldRowVisible(fieldComponents: FieldComponent<any, any>[]): boolean {
324
+ for (const fieldComponent of fieldComponents) {
325
+ if (!fieldComponent.field.accessibility || !fieldComponent.field.accessibility?.hidden)
326
+ return true;
327
+ }
328
+
329
+ return false;
330
+ }
331
+
332
+ private Progress() {
333
+ if (!this.state.progress || !this.state.progress.visible)
334
+ return null;
335
+
336
+ return (
337
+ <Grid
338
+ container
339
+ xs={12}
340
+ justifyContent='center'
341
+ alignItems='center'
342
+ spacing={2}
343
+ >
344
+ <Container>
345
+ <LinearProgress variant='indeterminate' color='primary' />
346
+ </Container>
347
+ </Grid>
348
+ );
349
+ }
350
+
351
+ private Buttons() {
352
+ return (
353
+ <Grid
354
+ container
355
+ direction="row"
356
+ justifyContent="flex-end"
357
+ alignItems="center"
358
+ xs={12}
359
+ sx={(theme) => ({
360
+ marginTop: theme.spacing(2),
361
+ marginBottom: theme.spacing(1),
362
+ })}
363
+ >
364
+ {
365
+ Object.keys(this.props.buttons).map((buttonPropertyName) => this.props.buttons[buttonPropertyName]).filter((button) => {
366
+ if (button.accessibility?.hidden)
367
+ return false;
368
+
369
+ return true;
370
+ }).map((button, key) => {
371
+ return (
372
+ <Grid
373
+ key={key}
374
+ sx={(theme) => ({
375
+ marginLeft: theme.spacing(1),
376
+ })}
377
+ >
378
+ { button.style.icon ?
379
+ <IconButton
380
+ disabled={button.accessibility?.disabled}
381
+ onClick={(event: any) => this.onClick(button)}
382
+ >
383
+ <button.style.icon />
384
+ </IconButton>
385
+ :
386
+ <Button
387
+ color={button.style.color}
388
+ variant={button.style.variant ? button.style.variant : 'contained'}
389
+ disabled={button.accessibility?.disabled}
390
+ onClick={event => this.onClick(button)}
391
+ >
392
+ {button.name}
393
+ </Button>
394
+ }
395
+ </Grid>
396
+ )
397
+ })
398
+ }
399
+ </Grid>
400
+ );
401
+ }
402
+ }
403
+
404
+ type FormType = <F extends Fields, B extends FormButtons<F>>(props: Omit<FormProps<F, B>, 'classes'>) => JSX.Element;
405
+ export const Form = withRouter(FormComponent) as FormType;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { Fields } from './Field';
3
+
4
+ export type FormButton<F extends Fields> = {
5
+ name: string,
6
+ accessibility?: {
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
+
21
+ export abstract class FormButtons<F extends Fields> { [name: string]: FormButton<F> }
22
+
23
+ export const clearButton: FormButton<any> = {
24
+ name: 'Clear',
25
+ style: {
26
+ color: 'primary',
27
+ variant: 'text',
28
+ },
29
+ clearFormOnClick: true
30
+ };