@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
package/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export * from './src/router/Router';
2
+ export * from './src/router/Page';
3
+
4
+ export * from './src/form/Form';
5
+ export * from './src/form/Field';
6
+ export * from './src/form/FormButton';
7
+
8
+ export * from './src/form/fields/TextField';
9
+
10
+ export * from './src/form/container/FormPaper';
11
+ export * from './src/form/container/FormPage';
12
+
13
+ export * from './src/container/PageContainer';
14
+ export * from './src/container/NavMenu';
15
+
16
+ export * from './src/list/NestedList';
17
+
18
+ export * from './src/table/Table';
19
+ export * from './src/table/TableButton';
20
+ export * from './src/table/TableLoader';
package/jest.config.js ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ "roots": [
3
+ "<rootDir>/test"
4
+ ],
5
+ "transform": {
6
+ "^.+\\.tsx?$": "ts-jest"
7
+ },
8
+ "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
9
+ "moduleFileExtensions": [
10
+ "ts",
11
+ "tsx",
12
+ "js",
13
+ "jsx",
14
+ "json",
15
+ "node"
16
+ ],
17
+ "testEnvironment": "node"
18
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@proteinjs/ui",
3
+ "version": "1.0.1",
4
+ "description": "UI libs",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/proteinjs/ui.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/proteinjs/ui/issues"
14
+ },
15
+ "homepage": "https://github.com/proteinjs/ui#readme",
16
+ "author": "Brent Bahry",
17
+ "license": "MIT",
18
+ "scripts": {
19
+ "clean": "rm -rf dist/ generated/ node_modules/ package-lock.json",
20
+ "build": "reflection-build && tsc",
21
+ "watch": "reflection-watch"
22
+ },
23
+ "dependencies": {
24
+ "@emotion/react": "11.11.1",
25
+ "@emotion/styled": "11.11.0",
26
+ "@mui/icons-material": "5.14.11",
27
+ "@mui/material": "5.14.11",
28
+ "@proteinjs/reflection": "1.0.2",
29
+ "@proteinjs/util": "1.0.18",
30
+ "history": "5.0.0",
31
+ "moment": "2.29.4",
32
+ "query-string": "6.13.2",
33
+ "react": "18.2.0",
34
+ "react-dom": "18.2.0",
35
+ "react-router": "6.16.0",
36
+ "react-router-dom": "6.16.0",
37
+ "string": "3.3.3"
38
+ },
39
+ "devDependencies": {
40
+ "@proteinjs/reflection-build": "1.0.5",
41
+ "@types/node": "14.0.13",
42
+ "@types/react": "18.2.23",
43
+ "@types/react-dom": "18.2.8",
44
+ "@types/react-router": "5.1.20",
45
+ "@types/react-router-dom": "5.3.3",
46
+ "@types/string": "0.0.30",
47
+ "typescript": "5.2.2"
48
+ },
49
+ "main": "./dist/generated/index.js",
50
+ "types": "./dist/generated/index.d.ts",
51
+ "gitHead": "14b3fefaaceda0824e4d0e09ac03db2adb2e1a8a"
52
+ }
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { Button, IconButton, Menu, MenuItem } from '@mui/material';
3
+ import AccountCircle from '@mui/icons-material/AccountCircle';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { LinkOrDialog } from './NavMenu';
6
+ import { Page } from '../router/Page';
7
+
8
+ export type AccountIconButtonProps = {
9
+ loginClicked: boolean,
10
+ setLoginClicked: React.Dispatch<React.SetStateAction<boolean>>,
11
+ auth?: {
12
+ isLoggedIn: boolean,
13
+ canViewPage: (page: Page) => boolean,
14
+ /** Either a dialog component, or a path to be redirected to */
15
+ login: LinkOrDialog,
16
+ logout: () => Promise<string>
17
+ },
18
+ profileMenuItems?: { name: string, action: LinkOrDialog }[],
19
+ }
20
+
21
+ export const AccountIconButton = ({ loginClicked, setLoginClicked, auth, profileMenuItems }: AccountIconButtonProps) => {
22
+ const navigate = useNavigate();
23
+ const [selectedProfileMenuItem, setSelectedProfileMenuItem] = React.useState<number>(-1);
24
+ const [selectedIndex, setSelectedIndex] = React.useState<number>(1);
25
+ const [anchorEl, setAccountMenuAnchorEl] = React.useState<null|HTMLElement>(null);
26
+ const open = Boolean(anchorEl);
27
+
28
+ if (!auth)
29
+ return null;
30
+
31
+ if (auth.isLoggedIn) {
32
+ return (
33
+ <div>
34
+ <IconButton
35
+ sx={{
36
+ '&:hover': {
37
+ color: '#fff',
38
+ backgroundColor: 'transparent',
39
+ },
40
+ backgroundColor: 'transparent',
41
+ }}
42
+ onClick={event => setAccountMenuAnchorEl(event.currentTarget)}
43
+ >
44
+ <AccountCircle />
45
+ </IconButton>
46
+ <Menu
47
+ anchorEl={anchorEl}
48
+ keepMounted
49
+ open={open}
50
+ onClose={() => setAccountMenuAnchorEl(null)}
51
+ >
52
+ {profileMenuItems &&
53
+ profileMenuItems.map((profileMenuItem, index) => (
54
+ <MenuItem
55
+ onClick={event => {
56
+ if (typeof profileMenuItem.action === 'string') {
57
+ navigate(qualifiedPath(profileMenuItem.action));
58
+ return;
59
+ }
60
+ setSelectedIndex(index);
61
+ }}
62
+ selected={selectedIndex === index}
63
+ >
64
+ {profileMenuItem.name}
65
+ </MenuItem>
66
+ ))
67
+ }
68
+ <MenuItem
69
+ onClick={async (event) => {
70
+ const redirectPath = await auth.logout();
71
+ navigate(qualifiedPath(redirectPath));
72
+ }}
73
+ >
74
+ Logout
75
+ </MenuItem>
76
+ </Menu>
77
+ <ProfileMenuItemAction />
78
+ </div>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div>
84
+ <Button
85
+ color='inherit'
86
+ onClick={event => auth && typeof auth.login === 'string' ? navigate(qualifiedPath(auth.login)) : setLoginClicked(!loginClicked)}
87
+ >
88
+ Login
89
+ </Button>
90
+ <Login />
91
+ </div>
92
+ );
93
+
94
+ function ProfileMenuItemAction() {
95
+ if (selectedProfileMenuItem == -1 || !auth || !profileMenuItems)
96
+ return null;
97
+
98
+ const menuItem = profileMenuItems[selectedProfileMenuItem];
99
+ if (typeof menuItem.action === 'string')
100
+ return null;
101
+
102
+ return <menuItem.action onClose={() => setSelectedProfileMenuItem(-1)} />;
103
+ }
104
+
105
+ function Login() {
106
+ if (!loginClicked || !auth || typeof auth.login === 'string')
107
+ return null;
108
+
109
+ return <auth.login onClose={() => setLoginClicked(false)} />;
110
+ }
111
+
112
+ function qualifiedPath(path: string) {
113
+ if (path.startsWith('/'))
114
+ return path;
115
+
116
+ return `/${path}`;
117
+ }
118
+ }
@@ -0,0 +1,91 @@
1
+ import React from 'react'
2
+ import { Drawer, Toolbar, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
3
+ import { Navigate } from 'react-router-dom'
4
+
5
+ export type LinkOrDialog = string | React.ComponentType<{ onClose: () => void }>;
6
+
7
+ export type NavMenuItem = {
8
+ name: string;
9
+ action: LinkOrDialog;
10
+ icon?: React.ComponentType;
11
+ /** Use to make this item a divider */
12
+ // isDivider?: boolean,
13
+ /** Use to make this item a nested menu */
14
+ // children?: NavMenuItems
15
+ };
16
+
17
+ export type NavMenuProps = {
18
+ navMenuItems?: NavMenuItem[],
19
+ navMenuOpen: boolean,
20
+ setNavMenuOpen: (open: boolean) => void,
21
+ };
22
+
23
+ const drawerWidth = 240;
24
+
25
+ export const NavMenu: React.FC<NavMenuProps> = ({ navMenuItems, navMenuOpen, setNavMenuOpen }) => {
26
+ const [selectedNavMenuItem, setSelectedNavMenuItem] = React.useState<number>(-1);
27
+
28
+ const handleNavMenuItemClick = (index: number) => {
29
+ setSelectedNavMenuItem(index);
30
+ setNavMenuOpen(false);
31
+ };
32
+
33
+ const NavMenuItemAction = () => {
34
+ if (selectedNavMenuItem == -1 || !navMenuItems)
35
+ return null;
36
+
37
+ const menuItem = navMenuItems[selectedNavMenuItem];
38
+
39
+ React.useEffect(() => {
40
+ setSelectedNavMenuItem(-1);
41
+ }, [selectedNavMenuItem]);
42
+
43
+ if (typeof menuItem.action === 'string')
44
+ return <Navigate to={qualifiedPath(menuItem.action)} />;
45
+
46
+ return <menuItem.action onClose={() => setSelectedNavMenuItem(-1)} />;
47
+ };
48
+
49
+ const qualifiedPath = (path: string) => {
50
+ if (path.startsWith('/'))
51
+ return path;
52
+
53
+ return `/${path}`;
54
+ };
55
+
56
+ return (
57
+ <div>
58
+ <Drawer
59
+ sx={{
60
+ width: drawerWidth,
61
+ flexShrink: 0,
62
+ }}
63
+ variant='temporary'
64
+ anchor='left'
65
+ open={navMenuOpen}
66
+ onClose={(_, reason) =>
67
+ reason === 'backdropClick' && setNavMenuOpen(false)
68
+ }
69
+ >
70
+ <Toolbar />
71
+ <List>
72
+ {navMenuItems?.map((navMenuItem, index) => (
73
+ <ListItem key={index}>
74
+ <ListItemButton
75
+ onClick={() => handleNavMenuItemClick(index)}
76
+ >
77
+ {navMenuItem.icon &&
78
+ <ListItemIcon>
79
+ <navMenuItem.icon />
80
+ </ListItemIcon>
81
+ }
82
+ <ListItemText primary={navMenuItem.name} />
83
+ </ListItemButton>
84
+ </ListItem>
85
+ ))}
86
+ </List>
87
+ </Drawer>
88
+ <NavMenuItemAction />
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { AppBar, Toolbar, Box, IconButton, Typography } from '@mui/material';
3
+ import MenuIcon from '@mui/icons-material/Menu';
4
+ import { NavigateFunction, useNavigate } from 'react-router-dom';
5
+ import { Page } from '../router/Page';
6
+ import { createUrlParams } from '../router/createUrlParams';
7
+ import { LinkOrDialog, NavMenu, NavMenuItem } from './NavMenu';
8
+ import { AccountIconButton } from './AccountIconButton';
9
+
10
+ export type PageContainerProps = {
11
+ appName: string,
12
+ page: Page,
13
+ toolbarChildren?: React.ReactNode,
14
+ auth?: {
15
+ isLoggedIn: boolean,
16
+ canViewPage: (page: Page) => boolean,
17
+ /** Either a dialog component, or a path to be redirected to */
18
+ login: LinkOrDialog,
19
+ logout: () => Promise<string>
20
+ },
21
+ profileMenuItems?: { name: string, action: LinkOrDialog }[],
22
+ navMenuItems?: NavMenuItem[],
23
+ }
24
+
25
+ const Page = React.memo(({ auth, page, navigate, loginClicked, setLoginClicked }: { auth: PageContainerProps['auth'], page: PageContainerProps['page'], navigate: NavigateFunction, loginClicked: boolean, setLoginClicked: (loginClicked: boolean) => void }) => {
26
+ if (auth?.canViewPage(page))
27
+ return <page.component urlParams={createUrlParams()} navigate={navigate} />;
28
+
29
+ if (!auth?.isLoggedIn) {
30
+ if (!loginClicked)
31
+ setLoginClicked(true);
32
+
33
+ return null;
34
+ }
35
+
36
+ navigate('/');
37
+ return null;
38
+ });
39
+
40
+ export function PageContainer(props: PageContainerProps) {
41
+ const navigate = useNavigate();
42
+ const { appName, page, toolbarChildren, auth, profileMenuItems, navMenuItems } = props;
43
+ const [loginClicked, setLoginClicked] = React.useState(false);
44
+ const [navMenuOpen, setNavMenuOpen] = React.useState(false);
45
+
46
+ React.useEffect(() => {
47
+ if (auth?.canViewPage(page))
48
+ return;
49
+
50
+ if (!auth?.isLoggedIn) {
51
+ console.log(`User not logged in, redirecting to login`);
52
+ if (typeof auth?.login === 'string') {
53
+ const p = qualifiedPath(auth.login);
54
+ navigate(p);
55
+ }
56
+ }
57
+ }, [page]);
58
+
59
+ return (
60
+ <Box sx={(theme) => {
61
+ const defaultStyles = {};
62
+ if (!page.pageContainerSxProps)
63
+ return defaultStyles;
64
+
65
+ let resolvedStyles;
66
+ if (typeof page.pageContainerSxProps === 'function')
67
+ resolvedStyles = Object.assign(defaultStyles, page.pageContainerSxProps(theme));
68
+ else
69
+ resolvedStyles = Object.assign(defaultStyles, page.pageContainerSxProps);
70
+
71
+ return resolvedStyles;
72
+ }}>
73
+ <AppBar position='static'>
74
+ <Toolbar>
75
+ { navMenuItems &&
76
+ <IconButton
77
+ aria-label='menu'
78
+ onClick={event => setNavMenuOpen(!navMenuOpen)}
79
+ sx={(theme) => ({
80
+ marginRight: theme.spacing(2),
81
+ '&:hover': {
82
+ color: '#fff',
83
+ },
84
+ })}
85
+ style={{ backgroundColor: 'transparent' }}
86
+ >
87
+ <MenuIcon />
88
+ </IconButton>
89
+ }
90
+ <Typography variant='h5' sx={{ flexGrow: 1, color: 'common.white', }}>
91
+ {appName}
92
+ </Typography>
93
+ {toolbarChildren}
94
+ <AccountIconButton loginClicked={loginClicked} setLoginClicked={setLoginClicked} auth={auth} profileMenuItems={profileMenuItems} />
95
+ </Toolbar>
96
+ </AppBar>
97
+ <NavMenu navMenuItems={navMenuItems} navMenuOpen={navMenuOpen} setNavMenuOpen={setNavMenuOpen} />
98
+ <Page auth={auth} page={page} navigate={navigate} loginClicked={loginClicked} setLoginClicked={setLoginClicked} />
99
+ </Box>
100
+ );
101
+
102
+ function qualifiedPath(path: string) {
103
+ if (path.startsWith('/'))
104
+ return path;
105
+
106
+ return `/${path}`;
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import S from 'string';
3
+
4
+ export type Field<T, F extends Fields> = {
5
+ name: string,
6
+ label?: string,
7
+ description?: string,
8
+ accessibility?: {
9
+ required?: boolean,
10
+ hidden?: boolean,
11
+ readonly?: boolean
12
+ },
13
+ layout?: {
14
+ row: number,
15
+ column?: number,
16
+ width: 1|2|3|4|5|6|7|8|9|10|11|12
17
+ },
18
+ value?: T,
19
+ displayValue?: (value: T) => string,
20
+ onLoad?: (fields: F) => Promise<void>,
21
+ onChange?: (value: T, fields: F, setFieldStatus: (message: string, isError: boolean) => void) => Promise<void>
22
+ }
23
+
24
+ export type FieldComponentProps<T, F extends Fields> = {
25
+ field: Field<T, F>,
26
+ onChange: (field: Field<T, F>, value: T, setFieldStatus: (message: string, isError: boolean) => void) => Promise<void>,
27
+ // focused: boolean
28
+ }
29
+
30
+ export type FieldComponent<T, F extends Fields> = {
31
+ field: Field<T, F>,
32
+ component: (props: FieldComponentProps<T, F>) => JSX.Element,
33
+ }
34
+
35
+ export abstract class Fields { [name: string]: FieldComponent<any, any> }
36
+
37
+ export function fieldLabel(field: Field<any, any>) {
38
+ return field.label ? field.label : S(field.name).humanize().capitalize().s;
39
+ }
40
+
41
+ export function fieldDisplayValue(field: Field<any, any>) {
42
+ let value = '';
43
+ if (field.value) {
44
+ if (field.displayValue)
45
+ value = field.displayValue(field.value);
46
+ else
47
+ value = field.value;
48
+ }
49
+
50
+ return value;
51
+ }