@judo/components 0.1.1-alpha.0

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.
@@ -0,0 +1,156 @@
1
+ import { Delete, LinkOff, NoteAdd, Visibility } from '@mui/icons-material';
2
+ import { ButtonBase, Grid, IconButton, InputAdornment, TextField } from '@mui/material';
3
+ import { useState } from 'react';
4
+ import type { ReactNode } from 'react';
5
+ import { exists } from '@judo/utilities';
6
+
7
+ interface AggregationInputProps {
8
+ name?: string;
9
+ id?: string;
10
+ label?: string;
11
+ value: any | undefined | null;
12
+ error?: boolean | undefined;
13
+ helperText?: string | undefined;
14
+ disabled?: boolean | undefined;
15
+ readonly?: boolean | undefined;
16
+ labelList: string[];
17
+ icon?: ReactNode;
18
+ onSet?: () => Promise<void> | undefined;
19
+ onView?: () => void | undefined;
20
+ onCreate?: () => void | undefined;
21
+ onDelete?: () => Promise<void> | undefined;
22
+ onRemove?: () => void | undefined;
23
+ }
24
+
25
+ interface ViewIconsProps {
26
+ value: any | undefined | null;
27
+ disabled: boolean;
28
+ onView?: () => void | undefined;
29
+ onCreate?: () => void | undefined;
30
+ onDelete?: () => Promise<void> | undefined;
31
+ }
32
+
33
+ interface EditIconsProps {
34
+ value: any | undefined | null;
35
+ disabled: boolean;
36
+ onRemove?: () => void | undefined;
37
+ }
38
+
39
+ export const AggregationInput = ({
40
+ name,
41
+ id,
42
+ label,
43
+ value,
44
+ error = false,
45
+ helperText,
46
+ disabled = false,
47
+ readonly = true,
48
+ labelList,
49
+ icon,
50
+ onSet,
51
+ onView,
52
+ onCreate,
53
+ onRemove,
54
+ onDelete,
55
+ }: AggregationInputProps) => {
56
+ const [focused, setFocused] = useState(false);
57
+
58
+ let icons: ReactNode;
59
+
60
+ if (readonly) {
61
+ icons = <ViewIcons value={value} disabled={disabled} onCreate={onCreate} onView={onView} onDelete={onDelete} />;
62
+ } else {
63
+ icons = <EditIcons value={value} disabled={disabled} onRemove={onRemove} />;
64
+ }
65
+
66
+ return (
67
+ <Grid container item direction="row" justifyContent="stretch" alignContent="stretch">
68
+ <ButtonBase
69
+ sx={{ padding: 0 }}
70
+ disabled={disabled || readonly}
71
+ onFocusCapture={() => setFocused(true)}
72
+ onBlur={() => setFocused(false)}
73
+ onClick={onSet}
74
+ >
75
+ <TextField
76
+ disabled={!onSet || disabled}
77
+ name={name}
78
+ id={id}
79
+ label={label}
80
+ error={error}
81
+ helperText={helperText}
82
+ focused={focused}
83
+ fullWidth
84
+ value={labelList.join(' - ')}
85
+ className={!readonly ? undefined : 'Mui-readOnly'}
86
+ sx={{
87
+ ':hover': {
88
+ cursor: 'pointer',
89
+ },
90
+ '.MuiFilledInput-input:hover': {
91
+ cursor: 'pointer',
92
+ },
93
+ }}
94
+ InputProps={{
95
+ readOnly: true,
96
+ startAdornment: <InputAdornment position="start">{icon}</InputAdornment>,
97
+ }}
98
+ />
99
+ </ButtonBase>
100
+ {icons}
101
+ </Grid>
102
+ );
103
+ };
104
+
105
+ const ViewIcons = ({ value, disabled, onCreate, onDelete, onView }: ViewIconsProps) => {
106
+ let icons: ReactNode;
107
+
108
+ if (exists(value)) {
109
+ icons = (
110
+ <>
111
+ {onView && (
112
+ <IconButton disabled={disabled} onClick={onView}>
113
+ <Visibility />
114
+ </IconButton>
115
+ )}
116
+ {onDelete && (
117
+ <IconButton disabled={disabled} onClick={onDelete}>
118
+ <Delete />
119
+ </IconButton>
120
+ )}
121
+ </>
122
+ );
123
+ } else {
124
+ icons = (
125
+ <>
126
+ {onCreate && (
127
+ <IconButton disabled={disabled} onClick={onCreate}>
128
+ <NoteAdd />
129
+ </IconButton>
130
+ )}
131
+ </>
132
+ );
133
+ }
134
+
135
+ return <>{icons}</>;
136
+ };
137
+
138
+ const EditIcons = ({ value, disabled, onRemove }: EditIconsProps) => {
139
+ let icons: ReactNode;
140
+
141
+ if (exists(value)) {
142
+ icons = (
143
+ <>
144
+ {onRemove && (
145
+ <IconButton disabled={disabled} onClick={onRemove}>
146
+ <LinkOff />
147
+ </IconButton>
148
+ )}
149
+ </>
150
+ );
151
+ } else {
152
+ icons = <></>;
153
+ }
154
+
155
+ return <>{icons}</>;
156
+ };
@@ -0,0 +1,144 @@
1
+ import { Home } from '@mui/icons-material';
2
+ import { Breadcrumbs, Typography } from '@mui/material';
3
+ import { useState, useContext, createContext, useMemo, useEffect } from 'react';
4
+ import type { ReactNode } from 'react';
5
+ import { useLocation, useNavigate } from 'react-router-dom';
6
+ import type { To } from 'react-router-dom';
7
+
8
+ interface BreadcrumbProviderProps {
9
+ children: ReactNode;
10
+ }
11
+
12
+ export type JudoNavigationSetTitle = (pageTitle: string) => void;
13
+
14
+ interface JudoNavigationProviderContext {
15
+ clearNavigate: (to: To) => void;
16
+ navigate: (to: To) => void;
17
+ back: () => void;
18
+ isBackDisabled: boolean;
19
+ setTitle: JudoNavigationSetTitle;
20
+ }
21
+
22
+ interface BreadcrumbItem {
23
+ key: string;
24
+ path: To;
25
+ label: string | null;
26
+ }
27
+
28
+ // @ts-ignore
29
+ const JudoNavigationContextState = createContext<JudoNavigationProviderContext>();
30
+
31
+ const BreadcrumbContextState = createContext<BreadcrumbItem[]>([]);
32
+
33
+ export const useJudoNavigation = () => {
34
+ const context = useContext(JudoNavigationContextState);
35
+
36
+ if (context === undefined) {
37
+ throw new Error('useJudoNavigation was used outside of its Provider');
38
+ }
39
+
40
+ return context;
41
+ };
42
+
43
+ export const BreadcrumbProvider = ({ children }: BreadcrumbProviderProps) => {
44
+ const navigate = useNavigate();
45
+ const location = useLocation();
46
+
47
+ const [breadcrumbItems, setBreadcrumbItems] = useState<BreadcrumbItem[]>([]);
48
+ const [nextBreadcrumbItem, setNextBreadcrumbItem] = useState<BreadcrumbItem>({} as any);
49
+
50
+ useEffect(() => {
51
+ setNextBreadcrumbItem((prevNextBreadcrumbItem) => {
52
+ return {
53
+ ...prevNextBreadcrumbItem,
54
+ key: '0.' + location.pathname,
55
+ path: location.pathname,
56
+ };
57
+ });
58
+ }, []);
59
+
60
+ const isBackDisabled = useMemo(() => {
61
+ return breadcrumbItems.length === 0;
62
+ }, [breadcrumbItems]);
63
+
64
+ const judoNavigationContext = {
65
+ clearNavigate: (to: To) => {
66
+ setBreadcrumbItems([]);
67
+ setNextBreadcrumbItem({
68
+ key: '0.' + to.toString(),
69
+ label: null,
70
+ path: to,
71
+ });
72
+
73
+ navigate(to);
74
+ },
75
+ navigate: (to: To) => {
76
+ if (nextBreadcrumbItem.label === null) {
77
+ throw Error('Page title has not been set!');
78
+ }
79
+
80
+ setBreadcrumbItems((prevBreadCrumbItems) => {
81
+ return [...prevBreadCrumbItems, nextBreadcrumbItem];
82
+ });
83
+
84
+ setNextBreadcrumbItem({
85
+ key: breadcrumbItems.length + '.' + to.toString(),
86
+ label: null,
87
+ path: to,
88
+ });
89
+
90
+ navigate(to);
91
+ },
92
+ back: () => {
93
+ if (breadcrumbItems.length !== 0) {
94
+ const lastItem = breadcrumbItems[breadcrumbItems.length - 1];
95
+ setNextBreadcrumbItem(lastItem);
96
+ setBreadcrumbItems((prevBreadcrumbItems) => {
97
+ return [...prevBreadcrumbItems.slice(0, prevBreadcrumbItems.length - 1)];
98
+ });
99
+ navigate(lastItem.path);
100
+ return;
101
+ }
102
+
103
+ setBreadcrumbItems([]);
104
+ setNextBreadcrumbItem({
105
+ key: '0.' + '/'.toString(),
106
+ label: null,
107
+ path: '/',
108
+ });
109
+
110
+ navigate('/');
111
+ },
112
+ isBackDisabled: isBackDisabled,
113
+ setTitle: (pageTitle: string) => {
114
+ setNextBreadcrumbItem((prevNextBreadcrumbItem) => {
115
+ return { ...prevNextBreadcrumbItem, label: pageTitle };
116
+ });
117
+ },
118
+ };
119
+
120
+ return (
121
+ <BreadcrumbContextState.Provider value={breadcrumbItems}>
122
+ <JudoNavigationContextState.Provider value={judoNavigationContext}>
123
+ {children}
124
+ </JudoNavigationContextState.Provider>
125
+ </BreadcrumbContextState.Provider>
126
+ );
127
+ };
128
+
129
+ export const CustomBreadcrumb = () => {
130
+ const breadcrumbItems = useContext(BreadcrumbContextState);
131
+
132
+ return (
133
+ <Breadcrumbs maxItems={2} separator=">">
134
+ <Home />
135
+ {breadcrumbItems.map(({ label, key }, index) => {
136
+ return (
137
+ <Typography color="text.primary" key={key}>
138
+ {label}
139
+ </Typography>
140
+ );
141
+ })}
142
+ </Breadcrumbs>
143
+ );
144
+ };
@@ -0,0 +1,35 @@
1
+ import { useMatch, useResolvedPath } from 'react-router-dom';
2
+ import type { LinkProps } from 'react-router-dom';
3
+ import { theme } from '@judo/theme';
4
+ import { ListItem, ListItemButton } from '@mui/material';
5
+ import { useJudoNavigation } from './CustomBreadcrumb';
6
+
7
+ const customLinkItemStyle = {
8
+ py: '2px',
9
+ px: 3,
10
+ color: theme.palette.text.secondary,
11
+ '&:hover, &:focus': {
12
+ color: theme.palette.secondary.main,
13
+ },
14
+ '&.Mui-selected': {
15
+ color: theme.palette.secondary.main,
16
+ },
17
+ };
18
+
19
+ export function CustomLink({ children, to, ...props }: LinkProps) {
20
+ const resolved = useResolvedPath(to);
21
+ const match = useMatch({ path: resolved.pathname, end: true });
22
+ const { clearNavigate } = useJudoNavigation();
23
+
24
+ const onClickHandler = () => {
25
+ clearNavigate(to);
26
+ };
27
+
28
+ return (
29
+ <ListItem disablePadding style={{ textDecoration: 'none' }}>
30
+ <ListItemButton selected={!!match} sx={customLinkItemStyle} onClick={onClickHandler}>
31
+ {children}
32
+ </ListItemButton>
33
+ </ListItem>
34
+ );
35
+ }
@@ -0,0 +1,41 @@
1
+ import { TablePagination } from '@mui/material';
2
+ import type { MouseEvent, Dispatch, SetStateAction } from 'react';
3
+
4
+ export interface CustomTablePaginationProps {
5
+ pageChange: (isNext: boolean) => Promise<void>;
6
+ isNextButtonEnabled: boolean;
7
+ page: number;
8
+ rowPerPage: number;
9
+ setPage: Dispatch<SetStateAction<number>>;
10
+ }
11
+
12
+ export const CustomTablePagination = (props: CustomTablePaginationProps) => {
13
+ const handleChangePage = async (event: MouseEvent<HTMLButtonElement> | null, newPage: number) => {
14
+ let isNext = true;
15
+ if (newPage < props.page) {
16
+ isNext = false;
17
+ }
18
+
19
+ props.setPage(newPage);
20
+
21
+ await props.pageChange(isNext);
22
+ };
23
+
24
+ return (
25
+ <TablePagination
26
+ component="div"
27
+ count={-1}
28
+ page={props.page}
29
+ onPageChange={handleChangePage}
30
+ rowsPerPage={props.rowPerPage}
31
+ rowsPerPageOptions={[props.rowPerPage]}
32
+ labelDisplayedRows={({ from, to }) => `${from}–${to}`}
33
+ nextIconButtonProps={{
34
+ disabled: !props.isNextButtonEnabled,
35
+ }}
36
+ backIconButtonProps={{
37
+ disabled: props.page === 0,
38
+ }}
39
+ />
40
+ );
41
+ };
@@ -0,0 +1,124 @@
1
+ import { Button, ClickAwayListener, Grow, MenuItem, MenuList, Paper, Popper } from '@mui/material';
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import type { ReactNode, KeyboardEvent, SyntheticEvent } from 'react';
4
+ import { KeyboardArrowDown } from '@mui/icons-material';
5
+
6
+ interface DropdownMenuItem {
7
+ disabled?: boolean;
8
+ visible?: boolean;
9
+ label?: string;
10
+ onClick: () => void;
11
+ startIcon?: ReactNode;
12
+ endIcon?: ReactNode;
13
+ }
14
+
15
+ interface DropdownButtonProps {
16
+ children?: ReactNode;
17
+ id?: string | undefined;
18
+ menuItems: DropdownMenuItem[];
19
+ disabled?: boolean;
20
+ showDropdownIcon?: boolean;
21
+ fullWidth?: boolean;
22
+ variant?: 'text' | 'outlined' | 'contained' | undefined;
23
+ }
24
+
25
+ export function DropdownButton({
26
+ children,
27
+ id,
28
+ menuItems,
29
+ disabled = false,
30
+ showDropdownIcon = true,
31
+ fullWidth = false,
32
+ variant = 'contained',
33
+ }: DropdownButtonProps) {
34
+ const [open, setOpen] = useState(false);
35
+ const anchorRef = useRef<HTMLButtonElement>(null);
36
+
37
+ const handleToggle = () => {
38
+ setOpen((prevOpen) => !prevOpen);
39
+ };
40
+
41
+ const handleClose = (event: Event | SyntheticEvent) => {
42
+ if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
43
+ return;
44
+ }
45
+
46
+ setOpen(false);
47
+ };
48
+
49
+ function handleListKeyDown(event: KeyboardEvent) {
50
+ if (event.key === 'Tab') {
51
+ event.preventDefault();
52
+ setOpen(false);
53
+ } else if (event.key === 'Escape') {
54
+ setOpen(false);
55
+ }
56
+ }
57
+
58
+ // return focus to the button when we transitioned from !open -> open
59
+ const prevOpen = useRef(open);
60
+ useEffect(() => {
61
+ if (prevOpen.current && !open) {
62
+ anchorRef.current!.focus();
63
+ }
64
+
65
+ prevOpen.current = open;
66
+ }, [open]);
67
+
68
+ return (
69
+ <>
70
+ <Button
71
+ ref={anchorRef}
72
+ id={id}
73
+ onClick={handleToggle}
74
+ endIcon={showDropdownIcon && <KeyboardArrowDown />}
75
+ disabled={disabled}
76
+ fullWidth={fullWidth}
77
+ variant={variant}
78
+ >
79
+ {children}
80
+ </Button>
81
+ <Popper
82
+ open={open}
83
+ anchorEl={anchorRef.current}
84
+ placement="bottom"
85
+ transition
86
+ style={{ zIndex: 1400, minWidth: anchorRef.current?.scrollWidth }}
87
+ >
88
+ {({ TransitionProps, placement }) => (
89
+ <Grow
90
+ {...TransitionProps}
91
+ style={{
92
+ transformOrigin: placement === 'bottom-start' ? 'left top' : 'left bottom',
93
+ }}
94
+ >
95
+ <Paper>
96
+ <ClickAwayListener onClickAway={handleClose}>
97
+ <MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}>
98
+ {menuItems
99
+ .filter((menuItem) => menuItem.visible ?? true)
100
+ .map((menuItem, index) => {
101
+ return (
102
+ <MenuItem
103
+ id={menuItem.label ?? '' + index}
104
+ disabled={menuItem.disabled ?? false}
105
+ onClick={(event) => {
106
+ handleClose(event);
107
+ menuItem.onClick();
108
+ }}
109
+ >
110
+ {menuItem.startIcon}
111
+ {menuItem.label}
112
+ {menuItem.endIcon}
113
+ </MenuItem>
114
+ );
115
+ })}
116
+ </MenuList>
117
+ </ClickAwayListener>
118
+ </Paper>
119
+ </Grow>
120
+ )}
121
+ </Popper>
122
+ </>
123
+ );
124
+ }
@@ -0,0 +1,35 @@
1
+ import { AppBar, Toolbar, Grid, Typography, Divider } from '@mui/material';
2
+ import { useEffect } from 'react';
3
+ import type { ReactNode } from 'react';
4
+ import { useJudoNavigation } from './CustomBreadcrumb';
5
+
6
+ interface PageHeaderProps {
7
+ title: string;
8
+ children: ReactNode;
9
+ }
10
+
11
+ export const PageHeader = ({ title, children }: PageHeaderProps) => {
12
+ const { setTitle } = useJudoNavigation();
13
+
14
+ useEffect(() => {
15
+ setTitle(title);
16
+ }, [title]);
17
+
18
+ return (
19
+ <>
20
+ <AppBar component="div" position="static" elevation={0} sx={{ zIndex: 0 }}>
21
+ <Toolbar>
22
+ <Grid container alignItems="center" spacing={1}>
23
+ <Grid item xs>
24
+ <Typography component="span" color="text.primary" variant="h5">
25
+ {title}
26
+ </Typography>
27
+ </Grid>
28
+ {children}
29
+ </Grid>
30
+ </Toolbar>
31
+ </AppBar>
32
+ <Divider />
33
+ </>
34
+ );
35
+ };
@@ -0,0 +1,64 @@
1
+ import { CheckBoxOutlined } from '@mui/icons-material';
2
+ import { TextField, InputAdornment, MenuItem } from '@mui/material';
3
+ import type { ChangeEvent } from 'react';
4
+ import { TRINARY_LOGIC } from '@judo/components-api';
5
+
6
+ interface TrinaryLogicProps {
7
+ readOnly?: boolean;
8
+ value?: boolean | null;
9
+ id?: string;
10
+ label: string;
11
+ name?: string;
12
+ error?: boolean | undefined;
13
+ helperText?: string | undefined;
14
+ onChange?: (value: boolean | null) => void;
15
+ }
16
+
17
+ const TrinaryLogicCombobox = ({
18
+ readOnly = false,
19
+ value = null,
20
+ id,
21
+ label,
22
+ name,
23
+ error,
24
+ helperText,
25
+ onChange,
26
+ }: TrinaryLogicProps) => {
27
+ const onChangeHandler = onChange
28
+ ? (event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
29
+ const index = Array.from(TRINARY_LOGIC.values()).indexOf(event.target.value);
30
+ const keysArray = Array.from(TRINARY_LOGIC.keys());
31
+ onChange(keysArray[index]);
32
+ }
33
+ : undefined;
34
+
35
+ return (
36
+ <TextField
37
+ name={name}
38
+ id={id}
39
+ label={label}
40
+ select
41
+ value={TRINARY_LOGIC.get(value)}
42
+ onChange={onChangeHandler}
43
+ className={readOnly ? 'Mui-readOnly' : undefined}
44
+ error={error}
45
+ helperText={helperText}
46
+ InputProps={{
47
+ readOnly: readOnly,
48
+ startAdornment: (
49
+ <InputAdornment position="start">
50
+ <CheckBoxOutlined />
51
+ </InputAdornment>
52
+ ),
53
+ }}
54
+ >
55
+ {Array.from(TRINARY_LOGIC.keys()).map((key) => (
56
+ <MenuItem key={TRINARY_LOGIC.get(key)} value={TRINARY_LOGIC.get(key)}>
57
+ {TRINARY_LOGIC.get(key)}
58
+ </MenuItem>
59
+ ))}
60
+ </TextField>
61
+ );
62
+ };
63
+
64
+ export default TrinaryLogicCombobox;
@@ -0,0 +1,50 @@
1
+ import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';
2
+ import { useEffect, useRef } from 'react';
3
+ import type { ConfirmationDialogProps } from '@judo/components-api';
4
+
5
+ export const ConfirmationDialog = ({
6
+ confirmationMessage,
7
+ title,
8
+ resolve,
9
+ open,
10
+ handleClose,
11
+ }: ConfirmationDialogProps) => {
12
+ const descriptionElementRef = useRef<HTMLElement>(null);
13
+ useEffect(() => {
14
+ if (open) {
15
+ const { current: descriptionElement } = descriptionElementRef;
16
+ if (descriptionElement !== null) {
17
+ descriptionElement.focus();
18
+ }
19
+ }
20
+ }, [open]);
21
+
22
+ const cancel = () => {
23
+ handleClose();
24
+ resolve(false);
25
+ };
26
+
27
+ const ok = () => {
28
+ handleClose();
29
+ resolve(true);
30
+ };
31
+
32
+ return (
33
+ <Dialog open={open} onClose={handleClose} scroll="paper" fullWidth={true} maxWidth={'xs'}>
34
+ {title && <DialogTitle id="scroll-dialog-title">{title}</DialogTitle>}
35
+ <DialogContent dividers={!!title}>
36
+ <DialogContentText id="scroll-dialog-description" ref={descriptionElementRef} tabIndex={-1}>
37
+ {confirmationMessage}
38
+ </DialogContentText>
39
+ </DialogContent>
40
+ <DialogActions>
41
+ <Button variant="text" onClick={cancel}>
42
+ No
43
+ </Button>
44
+ <Button variant="text" onClick={ok}>
45
+ Yes
46
+ </Button>
47
+ </DialogActions>
48
+ </Dialog>
49
+ );
50
+ };