@takaro/lib-components 0.0.11 → 0.0.14
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/Dockerfile.dev +1 -1
- package/package.json +47 -47
- package/src/components/actions/IconButton/index.tsx +2 -1
- package/src/components/actions/IconButton/style.ts +0 -19
- package/src/components/actions/ToggleButton/ToggleButton.stories.tsx +1 -0
- package/src/components/actions/ToggleButton/ToggleButton.tsx +3 -1
- package/src/components/actions/ToggleButton/ToggleButtonGroup.tsx +16 -2
- package/src/components/data/Drawer/Drawer.stories.tsx +19 -10
- package/src/components/data/Drawer/DrawerContent.tsx +56 -1
- package/src/components/data/Drawer/useDrawer.tsx +16 -2
- package/src/components/data/Table/index.tsx +94 -33
- package/src/components/data/Table/style.ts +21 -0
- package/src/components/data/Table/subcomponents/ColumnHeader/index.tsx +1 -1
- package/src/components/data/Table/subcomponents/Pagination/PageSizeSelect.tsx +1 -0
- package/src/components/feedback/Badge/Badge.stories.tsx +23 -0
- package/src/components/feedback/Badge/index.tsx +47 -0
- package/src/components/feedback/ErrorPage/index.tsx +1 -1
- package/src/components/feedback/index.ts +3 -0
- package/src/components/inputs/Date/DatePicker/Controlled.tsx +2 -0
- package/src/components/inputs/Date/DatePicker/DatePicker.stories.tsx +1 -1
- package/src/components/inputs/Date/DatePicker/Generic.tsx +20 -4
- package/src/components/inputs/Date/DatePicker/style.ts +3 -4
- package/src/components/inputs/DurationField/Duration.stories.tsx +4 -1
- package/src/components/inputs/index.ts +7 -0
- package/src/components/inputs/selects/SelectQueryField/Controlled.tsx +10 -0
- package/src/components/inputs/selects/SelectQueryField/Generic/index.tsx +20 -2
- package/src/components/inputs/selects/SelectQueryField/SelectQueryField.stories.tsx +20 -0
- package/src/components/inputs/selects/SubComponents/OptionGroup.tsx +4 -2
- package/src/components/inputs/selects/index.tsx +0 -1
- package/src/components/other/CollapseList/index.tsx +26 -31
- package/src/helpers/getInitials.ts +1 -0
- package/src/hooks/useLocalStorage.tsx +0 -3
package/Dockerfile.dev
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takaro/lib-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Takaro UI is a simple and customizable component library to build React apps faster within the Takaro eco system",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -15,58 +15,58 @@
|
|
|
15
15
|
"test:update": "jest --updateSnapshot"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@hookform/devtools": "
|
|
19
|
-
"@mdx-js/react": "
|
|
20
|
-
"@types/jest": "
|
|
21
|
-
"@types/react-window": "
|
|
22
|
-
"@types/styled-components": "
|
|
23
|
-
"@types/topojson-client": "
|
|
24
|
-
"react": "
|
|
25
|
-
"react-dom": "
|
|
18
|
+
"@hookform/devtools": "4.3.1",
|
|
19
|
+
"@mdx-js/react": "3.0.1",
|
|
20
|
+
"@types/jest": "29.5.13",
|
|
21
|
+
"@types/react-window": "1.8.8",
|
|
22
|
+
"@types/styled-components": "5.1.34",
|
|
23
|
+
"@types/topojson-client": "3.1.5",
|
|
24
|
+
"react": "18.3.1",
|
|
25
|
+
"react-dom": "18.3.1",
|
|
26
26
|
"storybook": "7.6.20"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@floating-ui/react": "
|
|
30
|
-
"@monaco-editor/react": "
|
|
31
|
-
"@rjsf/core": "
|
|
32
|
-
"@rjsf/utils": "
|
|
33
|
-
"@rjsf/validator-ajv8": "
|
|
34
|
-
"@tanstack/react-table": "
|
|
35
|
-
"@tanstack/react-router": "1.
|
|
29
|
+
"@floating-ui/react": "0.26.24",
|
|
30
|
+
"@monaco-editor/react": "4.6.0",
|
|
31
|
+
"@rjsf/core": "5.20.0",
|
|
32
|
+
"@rjsf/utils": "5.20.0",
|
|
33
|
+
"@rjsf/validator-ajv8": "5.20.0",
|
|
34
|
+
"@tanstack/react-table": "8.20.5",
|
|
35
|
+
"@tanstack/react-router": "1.58.15",
|
|
36
36
|
"@types/luxon": "3.4.2",
|
|
37
|
-
"framer-motion": "
|
|
37
|
+
"framer-motion": "11.9.0",
|
|
38
38
|
"luxon": "3.5.0",
|
|
39
|
-
"notistack": "
|
|
40
|
-
"polished": "
|
|
41
|
-
"react": "
|
|
42
|
-
"react-dnd": "
|
|
43
|
-
"@sentry/react": "
|
|
44
|
-
"react-dnd-html5-backend": "
|
|
45
|
-
"react-dom": "
|
|
46
|
-
"react-hook-form": "
|
|
47
|
-
"react-icons": "
|
|
48
|
-
"react-intersection-observer": "
|
|
49
|
-
"react-virtualized-auto-sizer": "
|
|
50
|
-
"react-window": "
|
|
51
|
-
"simplebar-react": "
|
|
52
|
-
"styled-components": "
|
|
53
|
-
"web-vitals": "
|
|
54
|
-
"@visx/scale": "
|
|
55
|
-
"@visx/responsive": "
|
|
56
|
-
"@visx/group": "
|
|
57
|
-
"@visx/heatmap": "
|
|
58
|
-
"@visx/tooltip": "
|
|
59
|
-
"@visx/event": "
|
|
60
|
-
"@visx/shape": "
|
|
61
|
-
"@visx/grid": "
|
|
62
|
-
"@visx/vendor": "
|
|
63
|
-
"@visx/brush": "
|
|
64
|
-
"@visx/axis": "
|
|
39
|
+
"notistack": "3.0.1",
|
|
40
|
+
"polished": "4.3.1",
|
|
41
|
+
"react": "18.3.1",
|
|
42
|
+
"react-dnd": "16.0.1",
|
|
43
|
+
"@sentry/react": "8.32.0",
|
|
44
|
+
"react-dnd-html5-backend": "16.0.1",
|
|
45
|
+
"react-dom": "18.3.1",
|
|
46
|
+
"react-hook-form": "7.53.0",
|
|
47
|
+
"react-icons": "5.3.0",
|
|
48
|
+
"react-intersection-observer": "9.13.1",
|
|
49
|
+
"react-virtualized-auto-sizer": "1.0.24",
|
|
50
|
+
"react-window": "1.8.10",
|
|
51
|
+
"simplebar-react": "3.2.6",
|
|
52
|
+
"styled-components": "5.3.11",
|
|
53
|
+
"web-vitals": "4.2.3",
|
|
54
|
+
"@visx/scale": "3.5.0",
|
|
55
|
+
"@visx/responsive": "3.10.2",
|
|
56
|
+
"@visx/group": "3.3.0",
|
|
57
|
+
"@visx/heatmap": "3.3.0",
|
|
58
|
+
"@visx/tooltip": "3.3.0",
|
|
59
|
+
"@visx/event": "3.3.0",
|
|
60
|
+
"@visx/shape": "3.5.0",
|
|
61
|
+
"@visx/grid": "3.5.0",
|
|
62
|
+
"@visx/vendor": "3.5.0",
|
|
63
|
+
"@visx/brush": "3.10.4",
|
|
64
|
+
"@visx/axis": "3.10.1",
|
|
65
65
|
"@visx/pattern": "3.3.0",
|
|
66
|
-
"@visx/curve": "
|
|
67
|
-
"@visx/gradient": "
|
|
68
|
-
"@visx/mock-data": "
|
|
66
|
+
"@visx/curve": "3.3.0",
|
|
67
|
+
"@visx/gradient": "3.3.0",
|
|
68
|
+
"@visx/mock-data": "3.3.0",
|
|
69
69
|
"@visx/geo": "3.5.0",
|
|
70
|
-
"topojson-client": "
|
|
70
|
+
"topojson-client": "3.1.0"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneElement, forwardRef, ReactElement } from 'react';
|
|
2
2
|
import { Color, Size } from '../../../styled/types';
|
|
3
|
+
import { Badge } from '../../../components';
|
|
3
4
|
import { Default } from './style';
|
|
4
5
|
|
|
5
6
|
export interface IconButtonProps {
|
|
@@ -35,7 +36,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
|
|
35
36
|
return (
|
|
36
37
|
<Default type="button" color={color} onClick={onClick} ref={ref} disabled={disabled} aria-label={ariaLabel}>
|
|
37
38
|
{cloneElement(icon, { size: getSize(size) })}
|
|
38
|
-
{badge && <
|
|
39
|
+
{badge && <Badge>{badge}</Badge>}
|
|
39
40
|
</Default>
|
|
40
41
|
);
|
|
41
42
|
});
|
|
@@ -51,25 +51,6 @@ export const Default = styled.button<{ color: Color }>`
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
div {
|
|
55
|
-
background-color: ${({ theme }) => theme.colors.background};
|
|
56
|
-
color: ${({ theme }) => theme.colors.text};
|
|
57
|
-
font-size: ${({ theme }) => theme.fontSize.tiny};
|
|
58
|
-
font-weight: 600;
|
|
59
|
-
border-radius: ${({ theme }) => theme.borderRadius.small};
|
|
60
|
-
width: fit-content;
|
|
61
|
-
height: 1.5rem;
|
|
62
|
-
line-height: 1.1rem;
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
justify-content: center;
|
|
66
|
-
position: absolute;
|
|
67
|
-
top: -${({ theme }) => theme.spacing['0_75']};
|
|
68
|
-
right: -${({ theme }) => theme.spacing['0_75']};
|
|
69
|
-
padding: ${({ theme }) => theme.spacing['0_25']};
|
|
70
|
-
border: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
54
|
svg {
|
|
74
55
|
cursor: pointer;
|
|
75
56
|
fill: ${({ theme }) => theme.colors.text};
|
|
@@ -17,7 +17,9 @@ export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(fun
|
|
|
17
17
|
{ selected = false, disabled = false, onClick = undefined, value, parentClickEvent = () => {}, children, tooltip },
|
|
18
18
|
ref,
|
|
19
19
|
) {
|
|
20
|
-
const handleOnClick = () => {
|
|
20
|
+
const handleOnClick = (m: MouseEvent) => {
|
|
21
|
+
m.preventDefault();
|
|
22
|
+
m.stopPropagation();
|
|
21
23
|
if (disabled) return;
|
|
22
24
|
parentClickEvent(value);
|
|
23
25
|
if (onClick) {
|
|
@@ -47,7 +47,12 @@ export const ToggleButtonGroup: FC<ToggleButtonGroupProps> & SubComponents = ({
|
|
|
47
47
|
const m = new Map<string, boolean>();
|
|
48
48
|
Children.forEach(children, (child) => {
|
|
49
49
|
if (isValidElement(child)) {
|
|
50
|
-
|
|
50
|
+
console.log(child.props.value, defaultValue);
|
|
51
|
+
if (child.props.value === defaultValue) {
|
|
52
|
+
m.set(child.props.value, true);
|
|
53
|
+
} else {
|
|
54
|
+
m.set(child.props.value, false);
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
});
|
|
53
58
|
return m;
|
|
@@ -65,7 +70,16 @@ export const ToggleButtonGroup: FC<ToggleButtonGroupProps> & SubComponents = ({
|
|
|
65
70
|
}
|
|
66
71
|
return setSelected(value);
|
|
67
72
|
}
|
|
68
|
-
|
|
73
|
+
|
|
74
|
+
// In case there always has to be one value selected, the clicked value is true and there is currently only one value selected then we don't do anything
|
|
75
|
+
if (
|
|
76
|
+
!canSelectNone &&
|
|
77
|
+
(selected as Map<string, boolean>).get(value) === true &&
|
|
78
|
+
Array.from((selected as Map<string, boolean>).values()).filter(Boolean).length === 1
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
setSelected(new Map((selected as Map<string, boolean>).set(value, !(selected as Map<string, boolean>).get(value))));
|
|
70
84
|
};
|
|
71
85
|
|
|
@@ -5,6 +5,7 @@ import { styled } from '../../../styled';
|
|
|
5
5
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
8
|
+
import { DrawerOptions } from './useDrawer';
|
|
8
9
|
|
|
9
10
|
const ButtonContainer = styled.div`
|
|
10
11
|
display: flex;
|
|
@@ -18,7 +19,10 @@ const Status = styled.div`
|
|
|
18
19
|
export default {
|
|
19
20
|
title: 'data/Drawer',
|
|
20
21
|
component: Drawer,
|
|
21
|
-
|
|
22
|
+
args: {
|
|
23
|
+
promptCloseConfirmation: false,
|
|
24
|
+
},
|
|
25
|
+
} as Meta<DrawerOptions>;
|
|
22
26
|
|
|
23
27
|
export const Loading: StoryFn = () => {
|
|
24
28
|
return <DrawerSkeleton />;
|
|
@@ -33,7 +37,7 @@ interface FormFields {
|
|
|
33
37
|
onStoreFront: boolean;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
export const Default: StoryFn = () => {
|
|
40
|
+
export const Default: StoryFn<DrawerOptions> = ({ promptCloseConfirmation }) => {
|
|
37
41
|
const [open, setOpen] = useState<boolean>(false);
|
|
38
42
|
|
|
39
43
|
const validationSchema = useMemo(
|
|
@@ -57,7 +61,7 @@ export const Default: StoryFn = () => {
|
|
|
57
61
|
return (
|
|
58
62
|
<>
|
|
59
63
|
<Button onClick={() => setOpen(true)} text="Open drawer" />
|
|
60
|
-
<Drawer open={open} onOpenChange={setOpen}>
|
|
64
|
+
<Drawer open={open} onOpenChange={setOpen} promptCloseConfirmation={promptCloseConfirmation}>
|
|
61
65
|
<Drawer.Content>
|
|
62
66
|
<Drawer.Heading>Product Details</Drawer.Heading>
|
|
63
67
|
<Drawer.Body>
|
|
@@ -85,11 +89,8 @@ export const Default: StoryFn = () => {
|
|
|
85
89
|
/>
|
|
86
90
|
</CollapseList.Item>
|
|
87
91
|
<CollapseList.Item title="Pricing">
|
|
88
|
-
<RadioGroup
|
|
89
|
-
|
|
90
|
-
label="Selection a price type"
|
|
91
|
-
name="priceType"
|
|
92
|
-
options={[
|
|
92
|
+
<RadioGroup control={control} label="Selection a price type" name="priceType">
|
|
93
|
+
{[
|
|
93
94
|
{
|
|
94
95
|
label: 'Fixed',
|
|
95
96
|
labelPosition: 'left',
|
|
@@ -100,8 +101,16 @@ export const Default: StoryFn = () => {
|
|
|
100
101
|
labelPosition: 'left',
|
|
101
102
|
value: 'variable',
|
|
102
103
|
},
|
|
103
|
-
]}
|
|
104
|
-
|
|
104
|
+
].map(({ value, label }) => (
|
|
105
|
+
<div
|
|
106
|
+
key={`gender-${value}`}
|
|
107
|
+
style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}
|
|
108
|
+
>
|
|
109
|
+
<RadioGroup.Item value={value} id={value} />
|
|
110
|
+
<label htmlFor={value}>{label}</label>
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</RadioGroup>
|
|
105
114
|
</CollapseList.Item>
|
|
106
115
|
</CollapseList>
|
|
107
116
|
<CollapseList.Item title="Settings">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { forwardRef, HTMLProps, useState } from 'react';
|
|
2
2
|
import { styled } from '../../../styled';
|
|
3
|
+
import { Button } from '../../../components';
|
|
3
4
|
import { AnimatePresence, motion, PanInfo } from 'framer-motion';
|
|
4
5
|
import SimpleBar from 'simplebar-react';
|
|
5
6
|
|
|
@@ -15,6 +16,39 @@ const Container = styled(motion.div)`
|
|
|
15
16
|
border-left: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
16
17
|
`;
|
|
17
18
|
|
|
19
|
+
const ButtonContainer = styled.div`
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: flex-start;
|
|
23
|
+
gap: ${({ theme }) => theme.spacing[1]};
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const CloseConfirmationWrapper = styled.div`
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 0;
|
|
29
|
+
left: 0;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
background: rgba(0, 0, 0, 0.8);
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const CloseConfirmationContainer = styled.div`
|
|
39
|
+
height: 150px;
|
|
40
|
+
padding: ${({ theme }) => theme.spacing[2]};
|
|
41
|
+
width: calc(100% - 200px);
|
|
42
|
+
background-color: ${({ theme }) => theme.colors.background};
|
|
43
|
+
border: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
44
|
+
border-radius: ${({ theme }) => theme.borderRadius.medium};
|
|
45
|
+
|
|
46
|
+
h2,
|
|
47
|
+
p {
|
|
48
|
+
margin-bottom: ${({ theme }) => theme.spacing[1]};
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
18
52
|
export const HandleContainer = styled.div`
|
|
19
53
|
height: 80vh;
|
|
20
54
|
top: 10vh;
|
|
@@ -35,7 +69,16 @@ export const HandleContainer = styled.div`
|
|
|
35
69
|
`;
|
|
36
70
|
|
|
37
71
|
export const DrawerContent = forwardRef<HTMLElement, HTMLProps<HTMLDivElement>>(function DrawerContent(props, propRef) {
|
|
38
|
-
const {
|
|
72
|
+
const {
|
|
73
|
+
context,
|
|
74
|
+
labelId,
|
|
75
|
+
descriptionId,
|
|
76
|
+
getFloatingProps,
|
|
77
|
+
setOpen,
|
|
78
|
+
canDrag,
|
|
79
|
+
showConfirmDialog,
|
|
80
|
+
setShowConfirmDialog,
|
|
81
|
+
} = useDrawerContext();
|
|
39
82
|
|
|
40
83
|
const ref = useMergeRefs([context.refs.setFloating, propRef]);
|
|
41
84
|
const [dragPosition, setDragPosition] = useState<number>(0);
|
|
@@ -100,6 +143,18 @@ export const DrawerContent = forwardRef<HTMLElement, HTMLProps<HTMLDivElement>>(
|
|
|
100
143
|
</HandleContainer>
|
|
101
144
|
)}
|
|
102
145
|
<SimpleBar style={{ maxHeight: '92vh' }}>{props.children}</SimpleBar>
|
|
146
|
+
{showConfirmDialog && (
|
|
147
|
+
<CloseConfirmationWrapper>
|
|
148
|
+
<CloseConfirmationContainer>
|
|
149
|
+
<h2>Your progress will be lost</h2>
|
|
150
|
+
<p>Are you sure you want to exit? Your progress will not be saved.</p>
|
|
151
|
+
<ButtonContainer>
|
|
152
|
+
<Button text="Cancel" color="secondary" onClick={() => setShowConfirmDialog(false)} />
|
|
153
|
+
<Button text="Discard changes" onClick={() => setOpen(false)} color="error" />
|
|
154
|
+
</ButtonContainer>
|
|
155
|
+
</CloseConfirmationContainer>
|
|
156
|
+
</CloseConfirmationWrapper>
|
|
157
|
+
)}
|
|
103
158
|
</Container>
|
|
104
159
|
</FloatingFocusManager>
|
|
105
160
|
</FloatingOverlay>
|
|
@@ -6,17 +6,20 @@ export interface DrawerOptions {
|
|
|
6
6
|
initialOpen?: boolean;
|
|
7
7
|
onOpenChange?: (open: boolean) => void;
|
|
8
8
|
canDrag?: boolean;
|
|
9
|
+
promptCloseConfirmation?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function useDrawer({
|
|
12
13
|
initialOpen = false,
|
|
13
14
|
canDrag = false,
|
|
14
15
|
open: controlledOpen,
|
|
16
|
+
promptCloseConfirmation = false,
|
|
15
17
|
onOpenChange: setControlledOpen,
|
|
16
18
|
}: DrawerOptions) {
|
|
17
19
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
|
|
18
20
|
const [labelId, setLabelId] = useState<string | undefined>();
|
|
19
21
|
const [descriptionId, setDescriptionId] = useState<string | undefined>();
|
|
22
|
+
const [showConfirmDialog, setShowConfirmDialog] = useState<boolean>(false);
|
|
20
23
|
|
|
21
24
|
const open = controlledOpen ?? uncontrolledOpen;
|
|
22
25
|
const setOpen = setControlledOpen ?? setUncontrolledOpen;
|
|
@@ -32,13 +35,24 @@ export function useDrawer({
|
|
|
32
35
|
useClick(context, {
|
|
33
36
|
enabled: controlledOpen == null,
|
|
34
37
|
}),
|
|
35
|
-
useDismiss(context, {
|
|
38
|
+
useDismiss(context, {
|
|
39
|
+
outsidePressEvent: 'mousedown',
|
|
40
|
+
outsidePress: (_mouseEvent) => {
|
|
41
|
+
if (promptCloseConfirmation === true) {
|
|
42
|
+
setShowConfirmDialog(true);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
36
48
|
]);
|
|
37
49
|
|
|
38
50
|
return useMemo(
|
|
39
51
|
() => ({
|
|
40
52
|
open,
|
|
41
53
|
setOpen,
|
|
54
|
+
showConfirmDialog,
|
|
55
|
+
setShowConfirmDialog,
|
|
42
56
|
...interactions,
|
|
43
57
|
...data,
|
|
44
58
|
labelId,
|
|
@@ -47,6 +61,6 @@ export function useDrawer({
|
|
|
47
61
|
setDescriptionId,
|
|
48
62
|
canDrag,
|
|
49
63
|
}),
|
|
50
|
-
[open, setOpen, interactions, data, labelId, descriptionId],
|
|
64
|
+
[open, setOpen, interactions, data, labelId, descriptionId, showConfirmDialog],
|
|
51
65
|
);
|
|
52
66
|
}
|
|
@@ -17,11 +17,18 @@ import {
|
|
|
17
17
|
VisibilityState,
|
|
18
18
|
ColumnPinningState,
|
|
19
19
|
RowSelectionState,
|
|
20
|
+
ExpandedState,
|
|
21
|
+
getExpandedRowModel,
|
|
22
|
+
Row,
|
|
20
23
|
} from '@tanstack/react-table';
|
|
21
24
|
import { Wrapper, StyledTable, Toolbar, Flex, TableWrapper } from './style';
|
|
22
|
-
import { Button, Empty, Spinner, ToggleButtonGroup } from '../../../components';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
+
import { Button, Empty, IconButton, Spinner, ToggleButtonGroup } from '../../../components';
|
|
26
|
+
import {
|
|
27
|
+
AiOutlinePicCenter as RelaxedDensityIcon,
|
|
28
|
+
AiOutlinePicRight as TightDensityIcon,
|
|
29
|
+
AiOutlineRight as ExpandIcon,
|
|
30
|
+
AiOutlineUp as CollapseIcon,
|
|
31
|
+
} from 'react-icons/ai';
|
|
25
32
|
import { ColumnHeader } from './subcomponents/ColumnHeader';
|
|
26
33
|
import { ColumnVisibility } from './subcomponents/ColumnVisibility';
|
|
27
34
|
import { Filter } from './subcomponents/Filter';
|
|
@@ -37,8 +44,12 @@ export interface TableProps<DataType extends object> {
|
|
|
37
44
|
data: DataType[];
|
|
38
45
|
isLoading?: boolean;
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
/// Condition for row to be expandable
|
|
48
|
+
canExpand?: (row: Row<DataType>) => boolean;
|
|
49
|
+
/// What to render when row can be expanded
|
|
50
|
+
renderDetailPanel?: (row: Row<DataType>) => JSX.Element;
|
|
51
|
+
|
|
52
|
+
/// currently not possible to type this properly: https://github.com/TanStack/table/issues/4241
|
|
42
53
|
columns: ColumnDef<DataType, any>[];
|
|
43
54
|
|
|
44
55
|
/// Renders actions that are always visible
|
|
@@ -83,7 +94,9 @@ export function Table<DataType extends object>({
|
|
|
83
94
|
title,
|
|
84
95
|
rowSelection,
|
|
85
96
|
columnSearch,
|
|
97
|
+
renderDetailPanel,
|
|
86
98
|
renderToolbar,
|
|
99
|
+
canExpand = () => false,
|
|
87
100
|
renderRowSelectionActions,
|
|
88
101
|
isLoading = false,
|
|
89
102
|
}: TableProps<DataType>) {
|
|
@@ -111,9 +124,13 @@ export function Table<DataType extends object>({
|
|
|
111
124
|
{} as Record<string, boolean>,
|
|
112
125
|
);
|
|
113
126
|
});
|
|
127
|
+
|
|
114
128
|
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({});
|
|
115
129
|
const { storedValue: density, setValue: setDensity } = useLocalStorage<Density>(`table-density-${id}`, 'tight');
|
|
116
130
|
|
|
131
|
+
// Might because potentially none fullfil the canExpand condtion.
|
|
132
|
+
const rowsMightExpand = renderDetailPanel ? true : false;
|
|
133
|
+
const [expanded, setExpanded] = useState<ExpandedState>({});
|
|
117
134
|
const [openColumnVisibilityTooltip, setOpenColumnVisibilityTooltip] = useState<boolean>(false);
|
|
118
135
|
const [hasShownColumnVisibilityTooltip, setHasShownColumnVisibilityTooltip] = useState<boolean>(false);
|
|
119
136
|
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>(
|
|
@@ -126,6 +143,7 @@ export function Table<DataType extends object>({
|
|
|
126
143
|
);
|
|
127
144
|
|
|
128
145
|
const ROW_SELECTION_COL_SPAN = rowSelection ? 1 : 0;
|
|
146
|
+
const EXPAND_ROW_COL_SPAN = rowsMightExpand ? 1 : 0;
|
|
129
147
|
const MINIMUM_ROW_COUNT_FOR_PAGINATION = 5;
|
|
130
148
|
|
|
131
149
|
// handles the column visibility tooltip (shows tooltip when the first column is hidden)
|
|
@@ -154,6 +172,7 @@ export function Table<DataType extends object>({
|
|
|
154
172
|
data,
|
|
155
173
|
columns,
|
|
156
174
|
getCoreRowModel: getCoreRowModel(),
|
|
175
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
157
176
|
pageCount: pagination?.pageOptions.pageCount ?? -1,
|
|
158
177
|
manualPagination: true,
|
|
159
178
|
paginateExpandedRows: true, // Expanded rows will be paginated this means that rows that take up more space will be shown on next page.
|
|
@@ -169,6 +188,7 @@ export function Table<DataType extends object>({
|
|
|
169
188
|
enablePinning: true,
|
|
170
189
|
enableHiding: !!columnVisibility,
|
|
171
190
|
enableRowSelection: !!rowSelection,
|
|
191
|
+
getRowCanExpand: canExpand,
|
|
172
192
|
autoResetPageIndex: false,
|
|
173
193
|
|
|
174
194
|
columnResizeMode: 'onChange',
|
|
@@ -180,6 +200,7 @@ export function Table<DataType extends object>({
|
|
|
180
200
|
onColumnOrderChange: setColumnOrder,
|
|
181
201
|
onColumnPinningChange: setColumnPinning,
|
|
182
202
|
onRowSelectionChange: rowSelection ? rowSelection?.setRowSelectionState : undefined,
|
|
203
|
+
onExpandedChange: setExpanded,
|
|
183
204
|
|
|
184
205
|
initialState: {
|
|
185
206
|
columnVisibility,
|
|
@@ -194,6 +215,7 @@ export function Table<DataType extends object>({
|
|
|
194
215
|
state: {
|
|
195
216
|
columnVisibility,
|
|
196
217
|
columnOrder,
|
|
218
|
+
expanded,
|
|
197
219
|
sorting: sorting.sortingState,
|
|
198
220
|
columnFilters: columnFiltering.columnFiltersState,
|
|
199
221
|
globalFilter: columnSearch.columnSearchState,
|
|
@@ -204,6 +226,7 @@ export function Table<DataType extends object>({
|
|
|
204
226
|
});
|
|
205
227
|
|
|
206
228
|
const tableHasNoData = isLoading === false && table.getRowModel().rows.length === 0;
|
|
229
|
+
const tableHasData = isLoading === false && table.getRowModel().rows.length !== 0;
|
|
207
230
|
|
|
208
231
|
// rowSelection.rowSelectionState has the following shape: { [rowId: string]: boolean }
|
|
209
232
|
const hasRowSelection = useMemo(() => {
|
|
@@ -254,7 +277,7 @@ export function Table<DataType extends object>({
|
|
|
254
277
|
<thead>
|
|
255
278
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
256
279
|
<tr key={headerGroup.id}>
|
|
257
|
-
{rowSelection &&
|
|
280
|
+
{rowSelection && tableHasData && (
|
|
258
281
|
<Th
|
|
259
282
|
isActive={false}
|
|
260
283
|
isRight={false}
|
|
@@ -276,6 +299,17 @@ export function Table<DataType extends object>({
|
|
|
276
299
|
</div>
|
|
277
300
|
</Th>
|
|
278
301
|
)}
|
|
302
|
+
{rowsMightExpand && tableHasData && (
|
|
303
|
+
<Th
|
|
304
|
+
isActive={false}
|
|
305
|
+
isRight={false}
|
|
306
|
+
isDragging={false}
|
|
307
|
+
canDrag={false}
|
|
308
|
+
isRowSelection={true}
|
|
309
|
+
width={15}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
312
|
+
|
|
279
313
|
{headerGroup.headers.map((header) => (
|
|
280
314
|
<ColumnHeader
|
|
281
315
|
header={header}
|
|
@@ -291,7 +325,7 @@ export function Table<DataType extends object>({
|
|
|
291
325
|
{/* loading state */}
|
|
292
326
|
{isLoading && (
|
|
293
327
|
<tr>
|
|
294
|
-
<td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN}>
|
|
328
|
+
<td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN + EXPAND_ROW_COL_SPAN}>
|
|
295
329
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '15px' }}>
|
|
296
330
|
<Spinner size="small" />
|
|
297
331
|
</div>
|
|
@@ -302,7 +336,7 @@ export function Table<DataType extends object>({
|
|
|
302
336
|
{/* empty state */}
|
|
303
337
|
{tableHasNoData && (
|
|
304
338
|
<tr>
|
|
305
|
-
<td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN}>
|
|
339
|
+
<td colSpan={table.getAllColumns().length + ROW_SELECTION_COL_SPAN + EXPAND_ROW_COL_SPAN}>
|
|
306
340
|
<div style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
307
341
|
<Empty
|
|
308
342
|
header=""
|
|
@@ -323,30 +357,51 @@ export function Table<DataType extends object>({
|
|
|
323
357
|
|
|
324
358
|
{!isLoading &&
|
|
325
359
|
table.getRowModel().rows.map((row) => (
|
|
326
|
-
|
|
327
|
-
{row.
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
360
|
+
<>
|
|
361
|
+
<tr key={row.id}>
|
|
362
|
+
{row.getCanExpand() ? (
|
|
363
|
+
<td style={{ paddingRight: '10px', width: '15px' }}>
|
|
364
|
+
{row.getIsExpanded() ? (
|
|
365
|
+
<IconButton
|
|
366
|
+
size="tiny"
|
|
367
|
+
icon={<CollapseIcon />}
|
|
368
|
+
ariaLabel="Collapse expanded row"
|
|
369
|
+
onClick={() => row.toggleExpanded(false)}
|
|
370
|
+
/>
|
|
371
|
+
) : (
|
|
372
|
+
<IconButton
|
|
373
|
+
size="tiny"
|
|
374
|
+
icon={<ExpandIcon />}
|
|
375
|
+
ariaLabel="expand row"
|
|
376
|
+
onClick={() => row.toggleExpanded(true)}
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
</td>
|
|
380
|
+
) : rowsMightExpand ? (
|
|
381
|
+
<td />
|
|
382
|
+
) : (
|
|
383
|
+
<></>
|
|
384
|
+
)}
|
|
385
|
+
{row.getCanSelect() && (
|
|
386
|
+
<td style={{ paddingRight: '10px', width: '15px' }}>
|
|
387
|
+
<CheckBox
|
|
388
|
+
value={row.getIsSelected()}
|
|
389
|
+
id={row.id}
|
|
390
|
+
name={row.id}
|
|
391
|
+
hasError={false}
|
|
392
|
+
disabled={!row.getCanSelect()}
|
|
393
|
+
onChange={() => row.toggleSelected()}
|
|
394
|
+
hasDescription={false}
|
|
395
|
+
size="small"
|
|
396
|
+
/>
|
|
397
|
+
</td>
|
|
398
|
+
)}
|
|
399
|
+
{row.getVisibleCells().map(({ column, id, getContext }) => (
|
|
400
|
+
<td key={id}>{flexRender(column.columnDef.cell, getContext())}</td>
|
|
401
|
+
))}
|
|
402
|
+
</tr>
|
|
403
|
+
{row.getIsExpanded() && renderDetailPanel!(row)}
|
|
404
|
+
</>
|
|
350
405
|
))}
|
|
351
406
|
</tbody>
|
|
352
407
|
|
|
@@ -355,9 +410,15 @@ export function Table<DataType extends object>({
|
|
|
355
410
|
<tr>
|
|
356
411
|
{/* This is the row selection */}
|
|
357
412
|
{ROW_SELECTION_COL_SPAN ? <td colSpan={1} /> : null}
|
|
413
|
+
{/* This is for the row expansion icon */}
|
|
414
|
+
{EXPAND_ROW_COL_SPAN ? <td colSpan={1} /> : null}
|
|
358
415
|
{pagination && (
|
|
359
416
|
<>
|
|
360
|
-
<td
|
|
417
|
+
<td
|
|
418
|
+
colSpan={
|
|
419
|
+
table.getVisibleLeafColumns().length - 3 - ROW_SELECTION_COL_SPAN - EXPAND_ROW_COL_SPAN
|
|
420
|
+
}
|
|
421
|
+
/>
|
|
361
422
|
<td colSpan={1}>
|
|
362
423
|
<span>
|
|
363
424
|
showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}-
|
|
@@ -46,6 +46,27 @@ export const StyledTable = styled.table<{ density: Density }>`
|
|
|
46
46
|
border-bottom: none;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
tr.subrow {
|
|
50
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
51
|
+
|
|
52
|
+
&:nth-child(even) {
|
|
53
|
+
background-color: ${({ theme }) => theme.colors.background};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:nth-child(odd) {
|
|
57
|
+
background-color: ${({ theme }) => theme.colors.backgroundAlt};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
td {
|
|
61
|
+
&:first-child {
|
|
62
|
+
padding-left: 0;
|
|
63
|
+
padding-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
padding: ${({ theme, density }) =>
|
|
66
|
+
density === 'tight' ? `${theme.spacing['0_5']} 0` : `${theme.spacing['0_5']} 0`};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
td {
|
|
50
71
|
border-right: none;
|
|
51
72
|
border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
@@ -72,7 +72,7 @@ export function ColumnHeader<DataType extends object>({ header, table, isLoading
|
|
|
72
72
|
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
|
|
73
73
|
|
|
74
74
|
// Determine mouse position
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
const clientOffset = monitor.getClientOffset()!;
|
|
77
77
|
|
|
78
78
|
// Get pixels to the top
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Meta, StoryFn } from '@storybook/react';
|
|
3
|
+
import { Badge, BadgeProps } from '.';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Feedback/Badge',
|
|
7
|
+
component: Badge,
|
|
8
|
+
args: {
|
|
9
|
+
variant: 'warning',
|
|
10
|
+
animate: false,
|
|
11
|
+
},
|
|
12
|
+
} as Meta<BadgeProps>;
|
|
13
|
+
|
|
14
|
+
export const Default: StoryFn<BadgeProps> = (args) => (
|
|
15
|
+
<div>
|
|
16
|
+
<h2 style={{ backgroundColor: 'orange', position: 'relative', width: 'fit-content' }}>
|
|
17
|
+
this is the title{' '}
|
|
18
|
+
<Badge variant={args.variant} animate={args.animate}>
|
|
19
|
+
here
|
|
20
|
+
</Badge>
|
|
21
|
+
</h2>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { forwardRef, PropsWithChildren } from 'react';
|
|
2
|
+
import { pulseAnimation, styled } from '../../../styled';
|
|
3
|
+
import { AlertVariants, Color } from '../../../styled';
|
|
4
|
+
import { shade } from 'polished';
|
|
5
|
+
|
|
6
|
+
type ColorVariant = AlertVariants | Color | 'default';
|
|
7
|
+
|
|
8
|
+
const Container = styled.div<{ variant: ColorVariant; animate: boolean }>`
|
|
9
|
+
background-color: ${({ theme, variant }) =>
|
|
10
|
+
variant === 'default' ? theme.colors.background : shade('0.8', theme.colors[variant])};
|
|
11
|
+
color: ${({ theme, variant }) => (variant === 'default' ? theme.colors.text : theme.colors[variant])};
|
|
12
|
+
font-size: ${({ theme }) => theme.fontSize.tiny};
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
border-radius: ${({ theme }) => theme.borderRadius.small};
|
|
15
|
+
width: fit-content;
|
|
16
|
+
height: 1.5rem;
|
|
17
|
+
line-height: 1.1rem;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: -${({ theme }) => theme.spacing['1']};
|
|
23
|
+
right: -${({ theme }) => theme.spacing['0_75']};
|
|
24
|
+
padding: ${({ theme }) => theme.spacing['0_25']};
|
|
25
|
+
border: 1px solid
|
|
26
|
+
${({ theme, variant }) => (variant === 'default' ? theme.colors.backgroundAccent : theme.colors[variant])};
|
|
27
|
+
|
|
28
|
+
animation: ${({ animate, variant, theme }) =>
|
|
29
|
+
animate ? pulseAnimation(variant === 'default' ? theme.colors.backgroundAccent : theme.colors[variant]) : 'none'}
|
|
30
|
+
5s infinite ease-in-out;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export interface BadgeProps {
|
|
34
|
+
variant?: ColorVariant;
|
|
35
|
+
animate?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Badge = forwardRef<HTMLDivElement, PropsWithChildren<BadgeProps>>(function Badge(
|
|
39
|
+
{ variant = 'default', children, animate = false },
|
|
40
|
+
ref,
|
|
41
|
+
) {
|
|
42
|
+
return (
|
|
43
|
+
<Container ref={ref} variant={variant} animate={animate}>
|
|
44
|
+
{children}
|
|
45
|
+
</Container>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -128,7 +128,7 @@ const defaultListItems = [
|
|
|
128
128
|
icon: <AiOutlineMenu />,
|
|
129
129
|
title: 'Api reference',
|
|
130
130
|
description: 'A complete API reference for our libraries',
|
|
131
|
-
to: 'https://api.
|
|
131
|
+
to: 'https://api.takaro.io/api.html',
|
|
132
132
|
},
|
|
133
133
|
/*
|
|
134
134
|
{
|
|
@@ -29,6 +29,7 @@ export const ControlledDatePicker: FC<ControlledDatePickerProps> = (props) => {
|
|
|
29
29
|
description,
|
|
30
30
|
allowFutureDates = true,
|
|
31
31
|
allowPastDates = true,
|
|
32
|
+
canClear,
|
|
32
33
|
customDateFilter,
|
|
33
34
|
} = defaultsApplier(props);
|
|
34
35
|
|
|
@@ -91,6 +92,7 @@ export const ControlledDatePicker: FC<ControlledDatePickerProps> = (props) => {
|
|
|
91
92
|
format={format}
|
|
92
93
|
placeholder={placeholder}
|
|
93
94
|
mode={mode}
|
|
95
|
+
canClear={canClear}
|
|
94
96
|
/>
|
|
95
97
|
)}
|
|
96
98
|
{showError && error?.message && <ErrorMessage message={error.message} />}
|
|
@@ -253,7 +253,7 @@ export const AbsoluteSubmit = () => {
|
|
|
253
253
|
name="date"
|
|
254
254
|
required={false}
|
|
255
255
|
loading={false}
|
|
256
|
-
description={'The role will be automatically removed after this date'}
|
|
256
|
+
description={'The role will be automatically removed after this date.'}
|
|
257
257
|
popOverPlacement={'bottom'}
|
|
258
258
|
timePickerOptions={{ interval: 30 }}
|
|
259
259
|
allowPastDates={false}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FC, useLayoutEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { Button, Popover } from '../../../../components';
|
|
1
|
+
import { FC, MouseEvent, useLayoutEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Button, IconButton, Popover } from '../../../../components';
|
|
3
3
|
import { DateTime, DateTimeFormatOptions, Settings } from 'luxon';
|
|
4
4
|
import { dateFormats, timeFormats } from './formats';
|
|
5
5
|
import { GenericInputProps } from '../../InputProps';
|
|
@@ -8,6 +8,7 @@ import { TimePicker } from '../subcomponents/TimePicker';
|
|
|
8
8
|
import { Calendar } from '../subcomponents/Calendar';
|
|
9
9
|
import { RelativePicker, timeDirection } from '../subcomponents/RelativePicker';
|
|
10
10
|
import { Placement } from '@floating-ui/react';
|
|
11
|
+
import { AiOutlineClose as ClearIcon } from 'react-icons/ai';
|
|
11
12
|
|
|
12
13
|
interface TimePickerOptions {
|
|
13
14
|
/// Determines the interval between time options
|
|
@@ -51,6 +52,9 @@ export interface DatePickerProps {
|
|
|
51
52
|
|
|
52
53
|
/// Placeholder text for the input
|
|
53
54
|
placeholder?: string;
|
|
55
|
+
|
|
56
|
+
/// Can set field back to undefined
|
|
57
|
+
canClear?: boolean;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export type GenericDatePickerProps = GenericInputProps<string, HTMLInputElement> & DatePickerProps;
|
|
@@ -71,6 +75,7 @@ export const GenericDatePicker: FC<GenericDatePickerProps> = ({
|
|
|
71
75
|
format = DateTime.DATE_SHORT,
|
|
72
76
|
allowPastDates = true,
|
|
73
77
|
allowFutureDates = true,
|
|
78
|
+
canClear = false,
|
|
74
79
|
customDateFilter,
|
|
75
80
|
mode,
|
|
76
81
|
}) => {
|
|
@@ -172,11 +177,22 @@ export const GenericDatePicker: FC<GenericDatePickerProps> = ({
|
|
|
172
177
|
return renderPlaceholder();
|
|
173
178
|
};
|
|
174
179
|
|
|
180
|
+
const handleClear = (e: MouseEvent) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
if (onChange) {
|
|
184
|
+
onChange(null as any);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
175
188
|
return (
|
|
176
189
|
<Popover placement={popOverPlacement} open={open} onOpenChange={setOpen}>
|
|
177
|
-
<Popover.Trigger asChild>
|
|
190
|
+
<Popover.Trigger asChild readOnly={readOnly}>
|
|
178
191
|
<ResultContainer readOnly={readOnly} hasError={hasError} onClick={() => setOpen(!open)}>
|
|
179
|
-
{renderResult()}
|
|
192
|
+
<span>{renderResult()}</span>
|
|
193
|
+
{!readOnly && canClear && value && !open && (
|
|
194
|
+
<IconButton size="tiny" icon={<ClearIcon />} ariaLabel="clear" onClick={handleClear} />
|
|
195
|
+
)}
|
|
180
196
|
</ResultContainer>
|
|
181
197
|
</Popover.Trigger>
|
|
182
198
|
<Popover.Content>
|
|
@@ -18,10 +18,9 @@ export const ResultContainer = styled.div<{ readOnly: boolean; hasError: boolean
|
|
|
18
18
|
z-index: ${({ theme }) => theme.zIndex.dropdown};
|
|
19
19
|
cursor: ${({ readOnly }) => (readOnly ? 'not-allowed' : 'pointer')};
|
|
20
20
|
user-select: none;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: space-between;
|
|
25
24
|
`;
|
|
26
25
|
|
|
27
26
|
export const ContentContainer = styled.div`
|
|
@@ -3,6 +3,7 @@ import { DurationField, DurationFieldProps } from '../../../components';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { StoryFn, Meta } from '@storybook/react';
|
|
5
5
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
|
6
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
title: 'Inputs/DurationField',
|
|
@@ -26,7 +27,9 @@ export const Default: StoryFn<DurationFieldProps> = (args) => {
|
|
|
26
27
|
duration: z.number().positive(),
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
const { control, handleSubmit } = useForm<z.infer<typeof validationSchema>>(
|
|
30
|
+
const { control, handleSubmit } = useForm<z.infer<typeof validationSchema>>({
|
|
31
|
+
resolver: zodResolver(validationSchema),
|
|
32
|
+
});
|
|
30
33
|
const onSubmit: SubmitHandler<z.infer<typeof validationSchema>> = ({ duration }) => {
|
|
31
34
|
setResult(duration);
|
|
32
35
|
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export interface PaginationProps {
|
|
2
|
+
isFetching: boolean;
|
|
3
|
+
hasNextPage: boolean;
|
|
4
|
+
isFetchingNextPage: boolean;
|
|
5
|
+
fetchNextPage: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export { ControlledCheckBox as CheckBox } from './CheckBox';
|
|
2
9
|
export type { ControlledCheckBoxProps as CheckBoxProps } from './CheckBox';
|
|
3
10
|
export { GenericCheckBox as UnControlledCheckBox } from './CheckBox/Generic';
|
|
@@ -29,6 +29,11 @@ export const ControlledSelectQueryField: FC<ControlledSelectQueryFieldProps> & S
|
|
|
29
29
|
inPortal,
|
|
30
30
|
debounce,
|
|
31
31
|
isLoadingData,
|
|
32
|
+
hasNextPage,
|
|
33
|
+
fetchNextPage,
|
|
34
|
+
isFetching,
|
|
35
|
+
isFetchingNextPage,
|
|
36
|
+
optionCount,
|
|
32
37
|
handleInputValueChange,
|
|
33
38
|
} = defaultsApplier(props);
|
|
34
39
|
|
|
@@ -85,7 +90,12 @@ export const ControlledSelectQueryField: FC<ControlledSelectQueryFieldProps> & S
|
|
|
85
90
|
onFocus={handleOnFocus}
|
|
86
91
|
value={field.value}
|
|
87
92
|
debounce={debounce}
|
|
93
|
+
optionCount={optionCount}
|
|
88
94
|
handleInputValueChange={handleInputValueChange}
|
|
95
|
+
isFetching={isFetching}
|
|
96
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
97
|
+
hasNextPage={hasNextPage}
|
|
98
|
+
fetchNextPage={fetchNextPage}
|
|
89
99
|
>
|
|
90
100
|
{children}
|
|
91
101
|
</GenericSelectQueryField>
|
|
@@ -28,14 +28,15 @@ import { useDebounce } from '../../../../../hooks';
|
|
|
28
28
|
import { setAriaDescribedBy } from '../../../layout';
|
|
29
29
|
import { FeedBackContainer } from '../style';
|
|
30
30
|
import { SelectItem, SelectContext, getLabelFromChildren } from '../../';
|
|
31
|
+
import { PaginationProps } from '../../../';
|
|
31
32
|
|
|
32
33
|
/* The SearchField depends on a few things of <Select/> */
|
|
33
34
|
import { GroupLabel } from '../../SelectField/style';
|
|
34
35
|
import { SelectContainer, SelectButton, StyledArrowIcon, StyledFloatingOverlay } from '../../sharedStyle';
|
|
35
|
-
import { IconButton, Spinner } from '../../../../../components';
|
|
36
|
+
import { IconButton, InfiniteScroll, Spinner } from '../../../../../components';
|
|
36
37
|
import { GenericTextField } from '../../../TextField/Generic';
|
|
37
38
|
|
|
38
|
-
interface SharedSelectQueryFieldProps {
|
|
39
|
+
interface SharedSelectQueryFieldProps extends PaginationProps {
|
|
39
40
|
// Enables loading data feedback for user
|
|
40
41
|
isLoadingData?: boolean;
|
|
41
42
|
/// The placeholder text to show when the input is empty
|
|
@@ -53,6 +54,9 @@ interface SharedSelectQueryFieldProps {
|
|
|
53
54
|
|
|
54
55
|
/// The selected items shown in the select field
|
|
55
56
|
render?: (selectedItems: SelectItem[]) => React.ReactNode;
|
|
57
|
+
|
|
58
|
+
/// The total options that will be visible when fully loaded
|
|
59
|
+
optionCount?: number;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
interface SingleSelectQueryFieldProps extends SharedSelectQueryFieldProps {
|
|
@@ -100,12 +104,17 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
|
|
|
100
104
|
hasError,
|
|
101
105
|
children,
|
|
102
106
|
readOnly,
|
|
107
|
+
isFetchingNextPage,
|
|
108
|
+
isFetching,
|
|
109
|
+
fetchNextPage,
|
|
110
|
+
hasNextPage,
|
|
103
111
|
render,
|
|
104
112
|
multiple = false,
|
|
105
113
|
canClear = false,
|
|
106
114
|
debounce = 250,
|
|
107
115
|
isLoadingData: isLoading = false,
|
|
108
116
|
handleInputValueChange,
|
|
117
|
+
optionCount,
|
|
109
118
|
} = defaultsApplier(props);
|
|
110
119
|
|
|
111
120
|
const [open, setOpen] = useState<boolean>(false);
|
|
@@ -201,6 +210,7 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
|
|
|
201
210
|
name={`${name}-input`}
|
|
202
211
|
hasDescription={false}
|
|
203
212
|
icon={<SearchIcon />}
|
|
213
|
+
suffix={isLoading ? 'Loading' : optionCount !== undefined ? `Result: ${optionCount}` : undefined}
|
|
204
214
|
hasError={hasError}
|
|
205
215
|
value={inputValue.value}
|
|
206
216
|
onChange={onInputChange}
|
|
@@ -215,6 +225,14 @@ export const GenericSelectQueryField = forwardRef<HTMLInputElement, GenericSelec
|
|
|
215
225
|
</FeedBackContainer>
|
|
216
226
|
)}
|
|
217
227
|
{hasOptions && options}
|
|
228
|
+
{hasOptions && !isLoading && (
|
|
229
|
+
<InfiniteScroll
|
|
230
|
+
isFetching={isFetching}
|
|
231
|
+
hasNextPage={hasNextPage}
|
|
232
|
+
fetchNextPage={fetchNextPage}
|
|
233
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
218
236
|
{/* Basically first interaction */}
|
|
219
237
|
{!hasOptions && inputValue.value === '' && <FeedBackContainer>Start typing to search</FeedBackContainer>}
|
|
220
238
|
{/* When there is no result */}
|
|
@@ -69,6 +69,11 @@ export const ServerSideSubmit: StoryFn<SelectQueryFieldProps> = (args) => {
|
|
|
69
69
|
handleInputValueChange={mockAPICall}
|
|
70
70
|
isLoadingData={loading}
|
|
71
71
|
required={false}
|
|
72
|
+
hasNextPage={false}
|
|
73
|
+
optionCount={10}
|
|
74
|
+
isFetching={false}
|
|
75
|
+
isFetchingNextPage={false}
|
|
76
|
+
fetchNextPage={() => {}}
|
|
72
77
|
name="film"
|
|
73
78
|
>
|
|
74
79
|
{/* In this case the label is the same as the value but ofcourse that can differ*/}
|
|
@@ -124,6 +129,11 @@ export const ClientSideSubmit: StoryFn<SelectQueryFieldProps> = (args) => {
|
|
|
124
129
|
handleInputValueChange={handleInputChange}
|
|
125
130
|
required={false}
|
|
126
131
|
debounce={0}
|
|
132
|
+
hasNextPage={false}
|
|
133
|
+
optionCount={10}
|
|
134
|
+
isFetching={false}
|
|
135
|
+
isFetchingNextPage={false}
|
|
136
|
+
fetchNextPage={() => {}}
|
|
127
137
|
name="film"
|
|
128
138
|
>
|
|
129
139
|
{/* In this case the label is the same as the value but ofcourse that can differ*/}
|
|
@@ -186,6 +196,11 @@ export const ClientSideMultiSelectSubmit: StoryFn<SelectQueryFieldProps> = (args
|
|
|
186
196
|
required={false}
|
|
187
197
|
canClear={args.canClear}
|
|
188
198
|
debounce={0}
|
|
199
|
+
hasNextPage={false}
|
|
200
|
+
optionCount={10}
|
|
201
|
+
isFetching={false}
|
|
202
|
+
isFetchingNextPage={false}
|
|
203
|
+
fetchNextPage={() => {}}
|
|
189
204
|
multiple
|
|
190
205
|
name="films"
|
|
191
206
|
>
|
|
@@ -240,6 +255,11 @@ export const Generic: StoryFn<SelectQueryFieldProps> = () => {
|
|
|
240
255
|
hasDescription={false}
|
|
241
256
|
value={result}
|
|
242
257
|
name="film"
|
|
258
|
+
hasNextPage={false}
|
|
259
|
+
optionCount={10}
|
|
260
|
+
isFetching={false}
|
|
261
|
+
isFetchingNextPage={false}
|
|
262
|
+
fetchNextPage={() => {}}
|
|
243
263
|
>
|
|
244
264
|
{/* In this case the label is the same as the value but ofcourse that can differ*/}
|
|
245
265
|
<SelectQueryField.OptionGroup>
|
|
@@ -6,10 +6,12 @@ export interface OptionGroupProps extends PropsWithChildren {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export const OptionGroup: FC<OptionGroupProps> = ({ children, label }) => {
|
|
9
|
-
|
|
9
|
+
{
|
|
10
|
+
/* This is actually never rendered, the optiongroup is build in the select fields themself*/
|
|
11
|
+
}
|
|
10
12
|
return (
|
|
11
13
|
<li>
|
|
12
|
-
{label
|
|
14
|
+
{label && <div>{label}</div>}
|
|
13
15
|
<ul>{children}</ul>
|
|
14
16
|
</li>
|
|
15
17
|
);
|
|
@@ -42,7 +42,6 @@ export const getLabelFromChildren = (children: ReactNode, value: string) => {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// eslint-disable-next-line no-console
|
|
46
45
|
console.error(
|
|
47
46
|
`No label found for value ${value}. This occurs when a value is passed through the defaultValue prop of useForm, but the value is not present in the options.`,
|
|
48
47
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FC, PropsWithChildren, ReactElement, useState } from 'react';
|
|
2
2
|
import { styled } from '../../../styled';
|
|
3
3
|
import { IoMdArrowDropup as ArrowUp } from 'react-icons/io';
|
|
4
|
-
import {
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
5
|
import { useTheme } from '../../../hooks';
|
|
6
6
|
|
|
7
7
|
const StyledList = styled.div`
|
|
@@ -78,36 +78,31 @@ const Item: FC<PropsWithChildren<ItemProps>> = ({ collapsed = false, title, chil
|
|
|
78
78
|
<ArrowUp size={18} />
|
|
79
79
|
</Header>
|
|
80
80
|
{description && <p>{description}</p>}
|
|
81
|
-
<
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{children}
|
|
107
|
-
</motion.div>
|
|
108
|
-
</motion.div>
|
|
109
|
-
)}
|
|
110
|
-
</AnimatePresence>
|
|
81
|
+
<motion.div
|
|
82
|
+
style={{ maxHeight: '100%', overflowY: 'hidden' }}
|
|
83
|
+
key={`collapse-item-${title}`}
|
|
84
|
+
variants={{
|
|
85
|
+
open: { opacity: 1, height: 'auto', flexGrow: 1, minHeight: 0, overflowY: 'auto', visibility: 'visible' },
|
|
86
|
+
collapsed: { opacity: 0, height: 0, visibility: 'hidden' },
|
|
87
|
+
}}
|
|
88
|
+
initial="collapsed"
|
|
89
|
+
animate={isCollapsed ? 'collapsed' : 'open'}
|
|
90
|
+
transition={{ duration: 0.125, ease: 'linear' }}
|
|
91
|
+
>
|
|
92
|
+
<motion.div
|
|
93
|
+
variants={{
|
|
94
|
+
open: { y: 0 },
|
|
95
|
+
collapsed: { y: -6 },
|
|
96
|
+
}}
|
|
97
|
+
transition={{ duration: 0.125, ease: 'linear' }}
|
|
98
|
+
style={{
|
|
99
|
+
transformOrigin: 'top center',
|
|
100
|
+
padding: `${theme.spacing['0_75']} ${theme.spacing['0_5']} ${theme.spacing['1_5']} ${theme.spacing['1']}`,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</motion.div>
|
|
105
|
+
</motion.div>
|
|
111
106
|
</div>
|
|
112
107
|
);
|
|
113
108
|
};
|
|
@@ -8,7 +8,6 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
8
8
|
const item = window.localStorage.getItem(key);
|
|
9
9
|
return item ? JSON.parse(item) : initialValue;
|
|
10
10
|
} catch (e) {
|
|
11
|
-
// eslint-disable-next-line no-console
|
|
12
11
|
console.error('Error reading the local storage value', e);
|
|
13
12
|
return initialValue;
|
|
14
13
|
}
|
|
@@ -24,11 +23,9 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
24
23
|
} catch (e) {
|
|
25
24
|
// DOMException code 22 for QuotaExceededError
|
|
26
25
|
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
|
|
27
|
-
// eslint-disable-next-line no-console
|
|
28
26
|
console.error('LocalStorage quota exceeded', e);
|
|
29
27
|
setError(e);
|
|
30
28
|
} else {
|
|
31
|
-
// eslint-disable-next-line no-console
|
|
32
29
|
console.error('Error setting the local storage value', e);
|
|
33
30
|
}
|
|
34
31
|
}
|