@addev-be/ui 0.3.5 → 0.4.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.
Files changed (30) hide show
  1. package/assets/icons/circle-check.svg +1 -0
  2. package/assets/icons/circle-info.svg +1 -0
  3. package/assets/icons/circle-xmark.svg +1 -0
  4. package/assets/icons/triangle-exclamation.svg +1 -0
  5. package/package.json +1 -1
  6. package/src/Icons.tsx +12 -4
  7. package/src/components/auth/LoginForm.tsx +83 -0
  8. package/src/components/auth/LoginPage.tsx +32 -0
  9. package/src/components/auth/PasswordRecoveryForm.tsx +52 -0
  10. package/src/components/auth/PasswordResetForm.tsx +112 -0
  11. package/src/components/auth/index.ts +4 -0
  12. package/src/components/auth/styles.ts +14 -0
  13. package/src/components/data/DataGrid/DataGridRowTemplate.tsx +69 -0
  14. package/src/components/data/DataGrid/VirtualScroller.tsx +7 -6
  15. package/src/components/data/DataGrid/hooks/useDataGrid.tsx +14 -0
  16. package/src/components/data/DataGrid/index.tsx +2 -82
  17. package/src/components/data/DataGrid/types.ts +9 -0
  18. package/src/components/forms/VerticalLabel.tsx +20 -0
  19. package/src/components/forms/index.ts +1 -1
  20. package/src/components/forms/styles.ts +12 -1
  21. package/src/components/index.ts +2 -0
  22. package/src/components/search/QuickSearchBar.tsx +31 -29
  23. package/src/components/search/styles.ts +37 -18
  24. package/src/components/ui/Card/index.tsx +14 -0
  25. package/src/components/ui/Card/styles.ts +35 -0
  26. package/src/components/ui/Message/index.tsx +57 -0
  27. package/src/components/ui/Message/styles.ts +40 -0
  28. package/src/components/ui/index.ts +3 -0
  29. package/src/providers/ThemeProvider/ThemeProvider.ts +8 -0
  30. package/src/services/WebSocketService.ts +1 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336c-13.3 0-24 10.7-24 24s10.7 24 24 24h80c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V248c0-13.3-10.7-24-24-24H216c-13.3 0-24 10.7-24 24s10.7 24 24 24h24v64H216zm40-144a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M248.4 84.3c1.6-2.7 4.5-4.3 7.6-4.3s6 1.6 7.6 4.3L461.9 410c1.4 2.3 2.1 4.9 2.1 7.5c0 8-6.5 14.5-14.5 14.5H62.5c-8 0-14.5-6.5-14.5-14.5c0-2.7 .7-5.3 2.1-7.5L248.4 84.3zm-41-25L9.1 385c-6 9.8-9.1 21-9.1 32.5C0 452 28 480 62.5 480h387c34.5 0 62.5-28 62.5-62.5c0-11.5-3.2-22.7-9.1-32.5L304.6 59.3C294.3 42.4 275.9 32 256 32s-38.3 10.4-48.6 27.3zM288 368a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm-8-184c0-13.3-10.7-24-24-24s-24 10.7-24 24v96c0 13.3 10.7 24 24 24s24-10.7 24-24V184z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addev-be/ui",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "watch": "tsc -b --watch",
package/src/Icons.tsx CHANGED
@@ -10,6 +10,9 @@ import ArrowsRotateIcon from '../assets/icons/arrows-rotate.svg?react';
10
10
  import ArrowsUpDownIcon from '../assets/icons/arrows-up-down.svg?react';
11
11
  import CheckIcon from '../assets/icons/check.svg?react';
12
12
  import ChevronDownIcon from '../assets/icons/chevron-down.svg?react';
13
+ import CircleCheckIcon from '../assets/icons/circle-check.svg?react';
14
+ import CircleInfoIcon from '../assets/icons/circle-info.svg?react';
15
+ import CircleXMarkIcon from '../assets/icons/circle-xmark.svg?react';
13
16
  import CopyIcon from '../assets/icons/copy.svg?react';
14
17
  import DownIcon from '../assets/icons/down.svg?react';
15
18
  import EllipsisIcon from '../assets/icons/ellipsis.svg?react';
@@ -32,6 +35,7 @@ import TableFooterIcon from '../assets/icons/table-footer.svg?react';
32
35
  import TableFooterSlashIcon from '../assets/icons/table-footer-slash.svg?react';
33
36
  import TableIcon from '../assets/icons/table.svg?react';
34
37
  import TallyIcon from '../assets/icons/tally.svg?react';
38
+ import TriangleExclamationIcon from '../assets/icons/triangle-exclamation.svg?react';
35
39
  import UpIcon from '../assets/icons/up.svg?react';
36
40
  import UserTieIcon from '../assets/icons/user-tie.svg?react';
37
41
  import XBarIcon from '../assets/icons/x-bar.svg?react';
@@ -70,16 +74,17 @@ export const LoadingIcon: FC<IconFCProps> = ({ className, ...props }) => (
70
74
  );
71
75
 
72
76
  export {
73
- ArrowDownAZIcon,
74
77
  ArrowDown19Icon,
78
+ ArrowDownAZIcon,
75
79
  ArrowDownBigSmallIcon,
76
- ArrowUpZAIcon,
77
- ArrowUpBigSmallIcon,
78
- ArrowUp91Icon,
79
80
  ArrowsRotateIcon,
80
81
  ArrowsUpDownIcon,
82
+ ArrowUp91Icon,
83
+ ArrowUpBigSmallIcon,
84
+ ArrowUpZAIcon,
81
85
  CheckIcon,
82
86
  ChevronDownIcon,
87
+ CircleXMarkIcon,
83
88
  CopyIcon,
84
89
  DownIcon,
85
90
  EllipsisIcon,
@@ -105,4 +110,7 @@ export {
105
110
  UpIcon,
106
111
  UserTieIcon,
107
112
  XBarIcon,
113
+ CircleCheckIcon,
114
+ CircleInfoIcon,
115
+ TriangleExclamationIcon,
108
116
  };
@@ -0,0 +1,83 @@
1
+ import { Button, Input } from '../forms';
2
+ import { Card, Message } from '../ui';
3
+ import { FC, useCallback, useEffect, useRef, useState } from 'react';
4
+ import { Link, redirect } from 'react-router-dom';
5
+
6
+ import { FormContainer } from './styles';
7
+ import { StackedLabel } from '../forms/VerticalLabel';
8
+ import { useAuthentication } from '../../providers';
9
+
10
+ export const LoginForm: FC = () => {
11
+ const [username, setUsername] = useState(
12
+ localStorage.getItem('username') || ''
13
+ );
14
+ const [password, setPassword] = useState('');
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [error, setError] = useState('');
17
+ const usernameInputRef = useRef<HTMLInputElement>(null);
18
+ const passwordInputRef = useRef<HTMLInputElement>(null);
19
+
20
+ const { login } = useAuthentication();
21
+
22
+ const onLoginClicked = useCallback(() => {
23
+ setError('');
24
+ setIsLoading(true);
25
+ login(username, password).then((success) => {
26
+ setIsLoading(false);
27
+ if (success) {
28
+ redirect('/');
29
+ } else {
30
+ setError('Identifiants invalides');
31
+ }
32
+ });
33
+ }, [login, password, username]);
34
+
35
+ useEffect(() => {
36
+ if (usernameInputRef.current && passwordInputRef.current) {
37
+ const input = !username
38
+ ? usernameInputRef.current
39
+ : passwordInputRef.current;
40
+ input.select();
41
+ input.focus();
42
+ }
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, []);
45
+
46
+ if (isLoading) return <div>Chargement...</div>;
47
+
48
+ return (
49
+ <Card>
50
+ <FormContainer>
51
+ <StackedLabel label="Adresse e-mail / Nom d'utilisateur">
52
+ <Input
53
+ ref={usernameInputRef}
54
+ type="email"
55
+ autoComplete="email"
56
+ required
57
+ value={username}
58
+ onChange={(e) => setUsername(e.target.value)}
59
+ />
60
+ </StackedLabel>
61
+
62
+ <StackedLabel label="Mot de passe">
63
+ <Input
64
+ ref={passwordInputRef}
65
+ type="password"
66
+ autoComplete="current-password"
67
+ required
68
+ value={password}
69
+ onChange={(e) => setPassword(e.target.value)}
70
+ />
71
+ </StackedLabel>
72
+
73
+ {error && <Message type="error">{error}</Message>}
74
+
75
+ <Button color="primary" onClick={onLoginClicked}>
76
+ Se connecter
77
+ </Button>
78
+
79
+ <Link to="/recover">Mot de passe oublié ?</Link>
80
+ </FormContainer>
81
+ </Card>
82
+ );
83
+ };
@@ -0,0 +1,32 @@
1
+ import { FC } from 'react';
2
+ import { Outlet } from 'react-router-dom';
3
+
4
+ export const LoginPage: FC = () => {
5
+ return (
6
+ <div className="flex flex-row h-full w-full">
7
+ <div className="relative hidden w-0 flex-1 lg:block">
8
+ <img className="absolute inset-0 h-full w-full object-cover" alt="" />
9
+ </div>
10
+ <div className="flex flex-1 flex-col justify-space-evenly overflow-y-auto px-4 py-12 sm:px-6 lg:flex-none lg:px-24 xl:px-36">
11
+ <div className="mx-auto w-full max-w-sm lg:max-w-96 lg:w-96 xl:max-w-[32rem] xl:w-[32rem]">
12
+ <div className="text-center">
13
+ <img
14
+ className="h-20 w-auto inline-block"
15
+ src="/logo192.png"
16
+ alt="Burotel"
17
+ />
18
+ <h2 className="mt-8 text-2xl font-bold leading-9 tracking-tight text-gray-900">
19
+ Connectez-vous
20
+ </h2>
21
+ </div>
22
+
23
+ <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[640px]">
24
+ <div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
25
+ <Outlet />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
@@ -0,0 +1,52 @@
1
+ import { Button, Input } from '../forms';
2
+ import { FC, useCallback, useState } from 'react';
3
+
4
+ import { Card } from '../ui';
5
+ import { FormContainer } from './styles';
6
+ import { Link } from 'react-router-dom';
7
+ import { Message } from '../ui/Message';
8
+ import { StackedLabel } from '../forms/VerticalLabel';
9
+ import { useAuthentication } from '../../providers';
10
+
11
+ export const PasswordRecoveryForm: FC = () => {
12
+ const [email, setEmail] = useState('');
13
+ const [isSubmitted, setIsSubmitted] = useState(false);
14
+ const { sendRecoveryKey } = useAuthentication();
15
+
16
+ const onSubmitClicked = useCallback(() => {
17
+ sendRecoveryKey(email).then(() => {
18
+ setIsSubmitted(true);
19
+ });
20
+ }, [email, sendRecoveryKey]);
21
+
22
+ return (
23
+ <Card>
24
+ <FormContainer>
25
+ {isSubmitted ? (
26
+ <>
27
+ <Message type="success">
28
+ Un e-mail de réinitialisation de mot de passe vous a été envoyé.
29
+ </Message>
30
+ <Link to="/">Retour</Link>
31
+ </>
32
+ ) : (
33
+ <>
34
+ <StackedLabel label="Adresse e-mail">
35
+ <Input
36
+ type="email"
37
+ autoComplete="email"
38
+ required
39
+ value={email}
40
+ onChange={(e) => setEmail(e.target.value)}
41
+ />
42
+ </StackedLabel>
43
+
44
+ <Button color="primary" onClick={onSubmitClicked}>
45
+ Envoyer un lien de récupération
46
+ </Button>
47
+ </>
48
+ )}
49
+ </FormContainer>
50
+ </Card>
51
+ );
52
+ };
@@ -0,0 +1,112 @@
1
+ import { Button, Card, Input, useAuthentication } from '@addev-be/ui';
2
+ import { FC, useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { Link, useParams } from 'react-router-dom';
4
+
5
+ import { FormContainer } from './styles';
6
+ import { Message } from '../ui/Message';
7
+ import { StackedLabel } from '../forms/VerticalLabel';
8
+
9
+ export const PasswordResetForm: FC = () => {
10
+ const { key } = useParams<{ key: string }>();
11
+ const [password1, setPassword1] = useState('');
12
+ const [password2, setPassword2] = useState('');
13
+ const [keyStatus, setKeyStatus] = useState(-1);
14
+ const [status, setStatus] = useState(-1);
15
+ const [error, setError] = useState('');
16
+ const { resetPassword, checkRecoveryKey } = useAuthentication();
17
+
18
+ useEffect(() => {
19
+ if (key) {
20
+ checkRecoveryKey(key).then((status) => {
21
+ setKeyStatus(status);
22
+ });
23
+ }
24
+ }, [checkRecoveryKey, key]);
25
+
26
+ const onSubmitClicked = useCallback(() => {
27
+ if (key) {
28
+ if (password1 !== password2) {
29
+ setError('Les mots de passe ne sont pas identiques');
30
+ return;
31
+ }
32
+ resetPassword(key, password1).then((status) => {
33
+ setStatus(status);
34
+ setError(
35
+ status === 4
36
+ ? 'Le mot de passe est trop faible (min. 8 caractères requis)'
37
+ : ''
38
+ );
39
+ });
40
+ }
41
+ }, [key, password1, password2, resetPassword]);
42
+
43
+ const content = useMemo(() => {
44
+ if (keyStatus < 0) {
45
+ return <p>Chargement...</p>;
46
+ }
47
+ if (keyStatus > 0) {
48
+ return (
49
+ <>
50
+ <Message type="error">
51
+ La clé de récupération fournie est invalide ou expirée
52
+ </Message>
53
+ </>
54
+ );
55
+ }
56
+
57
+ switch (status) {
58
+ case 0:
59
+ return (
60
+ <>
61
+ <Message type="success">
62
+ Votre mot de passe a été réinitialisé avec succès. Vous pouvez
63
+ maintenant vous connecter.
64
+ </Message>
65
+ </>
66
+ );
67
+
68
+ default:
69
+ return null;
70
+ }
71
+ }, [keyStatus, status]);
72
+
73
+ return (
74
+ <Card>
75
+ <FormContainer>
76
+ {content ?? (
77
+ <>
78
+ <StackedLabel label="Nouveau mot de passe">
79
+ <Input
80
+ type="password"
81
+ autoComplete="current-password"
82
+ required
83
+ value={password1}
84
+ onChange={(e) => setPassword1(e.target.value)}
85
+ />
86
+ </StackedLabel>
87
+ <StackedLabel label="Confirmation du mot de passe">
88
+ <Input
89
+ type="password"
90
+ autoComplete="current-password"
91
+ required
92
+ value={password2}
93
+ onChange={(e) => setPassword2(e.target.value)}
94
+ />
95
+ </StackedLabel>
96
+
97
+ {error && (
98
+ <Message className="mt-4" type="error">
99
+ {error}
100
+ </Message>
101
+ )}
102
+
103
+ <Button color="primary" onClick={onSubmitClicked}>
104
+ Réinitialiser le mot de passe
105
+ </Button>
106
+ </>
107
+ )}
108
+ <Link to="/">Retour</Link>
109
+ </FormContainer>
110
+ </Card>
111
+ );
112
+ };
@@ -0,0 +1,4 @@
1
+ export * from './LoginForm';
2
+ export * from './LoginPage';
3
+ export * from './PasswordRecoveryForm';
4
+ export * from './PasswordResetForm';
@@ -0,0 +1,14 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const FormContainer = styled.form.attrs({
4
+ className: 'FormContainer',
5
+ onSubmit: (e) => e.preventDefault(),
6
+ })`
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--space-4);
10
+
11
+ & > a {
12
+ text-align: center;
13
+ }
14
+ `;
@@ -0,0 +1,69 @@
1
+ import * as styles from './styles';
2
+
3
+ import { DataGridCell } from './DataGridCell';
4
+ import { DataGridRowTemplateProps } from './types';
5
+ import { useContext } from 'react';
6
+
7
+ export const DataGridRowTemplate = <R,>({
8
+ row,
9
+ rowIndex,
10
+ context,
11
+ }: DataGridRowTemplateProps<R>) => {
12
+ const { visibleColumns, rowKeyGetter, toggleSelection, ...props } =
13
+ useContext(context);
14
+
15
+ if (!row) {
16
+ return (
17
+ <styles.DataGridRow key={`loading-row-${rowIndex}`}>
18
+ {!!props.selectable && (
19
+ <styles.LoadingCell className="animate-pulse">
20
+ <div />
21
+ </styles.LoadingCell>
22
+ )}
23
+ {visibleColumns.map((_, index) => (
24
+ <styles.LoadingCell
25
+ className="animate-pulse"
26
+ key={`loading-${rowIndex}-${index}`}
27
+ >
28
+ <div />
29
+ </styles.LoadingCell>
30
+ ))}
31
+ </styles.DataGridRow>
32
+ );
33
+ }
34
+ const key = rowKeyGetter(row);
35
+ const selected = props.selectedKeys.includes(key);
36
+ const { className, style } = props.rowClassNameGetter?.(row) ?? {
37
+ className: '',
38
+ style: undefined,
39
+ };
40
+ return (
41
+ <styles.DataGridRow key={key}>
42
+ {!!props.selectable && (
43
+ <styles.SelectionCell
44
+ key="__select_checkbox__"
45
+ onClick={() => toggleSelection(key)}
46
+ >
47
+ <input
48
+ type="checkbox"
49
+ value={key as string}
50
+ checked={selected}
51
+ readOnly
52
+ />
53
+ </styles.SelectionCell>
54
+ )}
55
+ {visibleColumns.map(([key, col], index) => (
56
+ <DataGridCell
57
+ key={`loading-${rowIndex}-${index}`}
58
+ {...(index === 0 ? { className, style } : {})}
59
+ row={row}
60
+ rowIndex={rowIndex}
61
+ column={col}
62
+ columnIndex={index}
63
+ context={context}
64
+ columnKey={key}
65
+ />
66
+ ))}
67
+ </styles.DataGridRow>
68
+ );
69
+ };
@@ -1,12 +1,11 @@
1
1
  import * as styles from './styles';
2
2
 
3
- import { ReactNode, useContext } from 'react';
4
-
5
- import { DataGridContext } from './types';
3
+ import { DataGridContext, DataGridRowTemplateProps } from './types';
4
+ import { FC, useContext } from 'react';
6
5
 
7
6
  type VirtualScrollerProps<R> = {
8
7
  showAllRows?: boolean;
9
- rowTemplate: (row: R, index: number) => ReactNode;
8
+ rowTemplate: FC<DataGridRowTemplateProps<R>>;
10
9
  hasFooter?: boolean;
11
10
  context: DataGridContext<R>;
12
11
  onRangeChange?: (startIndex: number, length: number) => void;
@@ -22,7 +21,7 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
22
21
  gridTemplateColumns,
23
22
  } = useContext(props.context);
24
23
  const {
25
- rowTemplate,
24
+ rowTemplate: RowTemplate,
26
25
  // hasFooter, onRangeChange
27
26
  } = props;
28
27
 
@@ -39,7 +38,9 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
39
38
  $topPadding={topPadding}
40
39
  $rowHeight={rowHeight}
41
40
  >
42
- {visibleRows.map(rowTemplate)}
41
+ {visibleRows.map((row, index) => (
42
+ <RowTemplate row={row} rowIndex={index} context={props.context} />
43
+ ))}
43
44
  </styles.VirtualScrollerRowsContainer>
44
45
  </styles.VirtualScrollerContainer>
45
46
  );
@@ -92,6 +92,17 @@ export const useDataGrid = <R,>(
92
92
  onSelectionChange?.(selectedKeys);
93
93
  }, [onSelectionChange, selectedKeys]);
94
94
 
95
+ const toggleSelection = useCallback(
96
+ (key: string) => {
97
+ if (selectedKeys.includes(key)) {
98
+ setSelectedKeys(selectedKeys.filter((p) => p !== key));
99
+ } else {
100
+ setSelectedKeys([...selectedKeys, key]);
101
+ }
102
+ },
103
+ [selectedKeys, setSelectedKeys]
104
+ );
105
+
95
106
  /** ROWS FILTERING **/
96
107
 
97
108
  const [filters, setFilters] = useState<DataGridFilters>({});
@@ -240,6 +251,7 @@ export const useDataGrid = <R,>(
240
251
  footers,
241
252
  setFooters,
242
253
  footerFunctions,
254
+ toggleSelection,
243
255
  ...override,
244
256
  }),
245
257
  [
@@ -265,6 +277,7 @@ export const useDataGrid = <R,>(
265
277
  gridTemplateColumns,
266
278
  footers,
267
279
  footerFunctions,
280
+ toggleSelection,
268
281
  override,
269
282
  ]
270
283
  );
@@ -298,6 +311,7 @@ export const useDataGrid = <R,>(
298
311
  gridTemplateColumns: '',
299
312
  footers: {},
300
313
  setFooters: () => {},
314
+ toggleSelection: () => {},
301
315
  }),
302
316
  []
303
317
  );
@@ -2,11 +2,10 @@ import * as styles from './styles';
2
2
 
3
3
  import { DataGridContextProps, DataGridProps } from './types';
4
4
 
5
- import { DataGridCell } from './DataGridCell';
6
5
  import { DataGridFooter } from './DataGridFooter';
7
6
  import { DataGridHeader } from './DataGridHeader';
7
+ import { DataGridRowTemplate } from './DataGridRowTemplate';
8
8
  import { VirtualScroller } from './VirtualScroller';
9
- import { useCallback } from 'react';
10
9
  import { useDataGrid } from './hooks';
11
10
 
12
11
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -25,95 +24,16 @@ export const DataGrid = <R,>({
25
24
  } = props;
26
25
  const [contextProps, DataGridContext] = useDataGrid(props, contextOverride);
27
26
  const {
28
- selectedKeys,
29
- setSelectedKeys,
30
27
  columns,
31
- visibleColumns,
32
28
  rowHeight = 32,
33
29
  headerRowHeight = 40,
34
30
  scrollableRef,
35
31
  onScroll,
36
- rowKeyGetter,
37
32
  } = contextProps;
38
33
 
39
34
  const hasFooter = Object.values(columns).some((col) => col.footer);
40
35
 
41
- const toggleSelection = useCallback(
42
- (key: string) => {
43
- if (selectedKeys.includes(key)) {
44
- setSelectedKeys(selectedKeys.filter((p) => p !== key));
45
- } else {
46
- setSelectedKeys([...selectedKeys, key]);
47
- }
48
- },
49
- [selectedKeys, setSelectedKeys]
50
- );
51
-
52
- const rowTemplate = useCallback(
53
- (row: R, rowIndex: number) => {
54
- if (!row) {
55
- return (
56
- <styles.DataGridRow key={`loading-row-${rowIndex}`}>
57
- {!!props.selectable && (
58
- <styles.LoadingCell className="animate-pulse">
59
- <div />
60
- </styles.LoadingCell>
61
- )}
62
- {visibleColumns.map((_, index) => (
63
- <styles.LoadingCell
64
- className="animate-pulse"
65
- key={`loading-${rowIndex}-${index}`}
66
- >
67
- <div />
68
- </styles.LoadingCell>
69
- ))}
70
- </styles.DataGridRow>
71
- );
72
- }
73
- const key = rowKeyGetter(row);
74
- const { className, style } = props.rowClassNameGetter?.(row) ?? {
75
- className: '',
76
- style: undefined,
77
- };
78
- return (
79
- <styles.DataGridRow key={key}>
80
- {!!props.selectable && (
81
- <styles.SelectionCell
82
- key="__select_checkbox__"
83
- onClick={() => toggleSelection(key)}
84
- >
85
- <input
86
- type="checkbox"
87
- value={key as string}
88
- checked={selectedKeys.includes(key)}
89
- readOnly
90
- />
91
- </styles.SelectionCell>
92
- )}
93
- {visibleColumns.map(([key, col], index) => (
94
- <DataGridCell
95
- key={`loading-${rowIndex}-${index}`}
96
- {...(index === 0 ? { className, style } : {})}
97
- row={row}
98
- rowIndex={rowIndex}
99
- column={col}
100
- columnIndex={index}
101
- context={DataGridContext}
102
- columnKey={key}
103
- />
104
- ))}
105
- </styles.DataGridRow>
106
- );
107
- },
108
- [
109
- DataGridContext,
110
- props,
111
- rowKeyGetter,
112
- selectedKeys,
113
- toggleSelection,
114
- visibleColumns,
115
- ]
116
- );
36
+ const rowTemplate = DataGridRowTemplate;
117
37
 
118
38
  return (
119
39
  <DataGridContext.Provider value={contextProps}>
@@ -131,6 +131,7 @@ export type DataGridContextProps<R> = DataGridProps<R> & {
131
131
  length: number;
132
132
  rowKeyGetter: (row: R) => string;
133
133
  gridTemplateColumns: string;
134
+ toggleSelection: (key: string) => void;
134
135
  };
135
136
 
136
137
  export type DataGridContext<R> = Context<DataGridContextProps<R>>;
@@ -265,3 +266,11 @@ export type DataGridFilterCheckbox = {
265
266
  values: DataGridFilterValue[];
266
267
  level: number;
267
268
  };
269
+
270
+ export type DataGridRowTemplateProps<R> = {
271
+ row: R | null;
272
+ rowIndex: number;
273
+ selected?: boolean;
274
+ toggleSelection?: () => void;
275
+ context: DataGridContext<R>;
276
+ };
@@ -0,0 +1,20 @@
1
+ import { FC, HTMLAttributes } from 'react';
2
+
3
+ import { StackedLabelContainer } from './styles';
4
+
5
+ type StackedLabelProps = HTMLAttributes<HTMLDivElement> & {
6
+ label: string;
7
+ };
8
+
9
+ export const StackedLabel: FC<StackedLabelProps> = ({
10
+ label,
11
+ children,
12
+ ...props
13
+ }) => {
14
+ return (
15
+ <StackedLabelContainer {...props}>
16
+ <label>{label}</label>
17
+ {children}
18
+ </StackedLabelContainer>
19
+ );
20
+ };
@@ -2,4 +2,4 @@ export * from './Button';
2
2
  export * from './Select';
3
3
  export * from './IconButton';
4
4
  export * from './IndeterminateCheckbox';
5
- export { Input } from './styles';
5
+ export * from './styles';
@@ -15,6 +15,17 @@ export const inputStyle = css`
15
15
  }
16
16
  `;
17
17
 
18
- export const Input = styled.input`
18
+ export const Input = styled.input.attrs({
19
+ className: 'Input',
20
+ })`
19
21
  ${inputStyle}
20
22
  `;
23
+
24
+ export const StackedLabelContainer = styled.div.attrs({
25
+ className: 'StackedLabelContainer',
26
+ })`
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--space-1);
30
+ color: var(--color-gray-900);
31
+ `;
@@ -1,4 +1,6 @@
1
+ export * from './auth';
1
2
  export * from './data';
2
3
  export * from './forms';
3
4
  export * from './layout';
4
5
  export * from './search';
6
+ export * from './ui';
@@ -1,8 +1,10 @@
1
+ import * as styles from './styles';
2
+
1
3
  import { SearchResults, SearchTypeDefinitions } from './types';
2
4
  import { useCallback, useEffect, useRef, useState } from 'react';
3
5
 
4
6
  import { Dropdown } from '../layout';
5
- import { QuickSearchBarInput } from './styles';
7
+ import { Input } from '../forms';
6
8
  import { QuickSearchResults } from './QuickSearchResults';
7
9
  import { useDebounce } from '@uidotdev/usehooks';
8
10
  import { useGlobalSearchRequestHandler } from '../../services';
@@ -18,8 +20,10 @@ export const QuickSearchBar = <R,>({ definitions }: QuickSearchBarProps<R>) => {
18
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
21
  const [results, setResults] = useState<SearchResults<R> | null>(null);
20
22
 
23
+ const fakeInputRef = useRef<HTMLInputElement | null>(null);
21
24
  const inputRef = useRef<HTMLInputElement | null>(null);
22
- const rect = inputRef.current?.getBoundingClientRect() ?? new DOMRect();
25
+ const rect = fakeInputRef.current?.getBoundingClientRect() ?? new DOMRect();
26
+ const destRect = new DOMRect(rect.x, rect.y, rect.width, 0);
23
27
  const globalSearch = useGlobalSearchRequestHandler();
24
28
 
25
29
  useEffect(() => {
@@ -36,41 +40,39 @@ export const QuickSearchBar = <R,>({ definitions }: QuickSearchBarProps<R>) => {
36
40
  }, [globalSearch, debouncedTerm]);
37
41
 
38
42
  const onFocus = useCallback(() => {
39
- if (term) {
40
- setDropdownVisible(true);
41
- }
42
- }, [term]);
43
-
44
- useEffect(() => {
45
- const input = inputRef.current;
46
- input?.addEventListener('focus', onFocus);
47
- return () => {
48
- input?.removeEventListener('focus', onFocus);
49
- };
50
- }, [onFocus]);
43
+ setDropdownVisible(true);
44
+ requestAnimationFrame(() => {
45
+ inputRef.current?.focus();
46
+ });
47
+ }, []);
51
48
 
52
49
  return (
53
50
  <>
54
- <QuickSearchBarInput
55
- type="text"
56
- value={term}
57
- onChange={(e) => setTerm(e.target.value)}
58
- ref={inputRef}
59
- $opened={dropdownVisible}
60
- />
61
- {results && dropdownVisible && rect && (
51
+ <Input type="text" ref={fakeInputRef} value={term} onFocus={onFocus} />
52
+ {dropdownVisible && rect && (
62
53
  <Dropdown
63
- $sourceRect={rect}
54
+ $sourceRect={destRect}
64
55
  onClose={() => setDropdownVisible(false)}
65
56
  $width={rect.width}
66
- $height={[250, 400]}
57
+ $height={[results ? 250 : rect.height, 400]}
67
58
  $autoPositionY={false}
68
59
  >
69
- <QuickSearchResults
70
- results={results}
71
- definitions={definitions}
72
- term={debouncedTerm}
73
- />
60
+ <styles.QuickSearchDropdownContainer>
61
+ <Input
62
+ type="text"
63
+ ref={inputRef}
64
+ value={term}
65
+ onChange={(e) => setTerm(e.target.value)}
66
+ onClick={(e) => e.stopPropagation()}
67
+ />
68
+ {results && (
69
+ <QuickSearchResults
70
+ results={results}
71
+ definitions={definitions}
72
+ term={debouncedTerm}
73
+ />
74
+ )}
75
+ </styles.QuickSearchDropdownContainer>
74
76
  </Dropdown>
75
77
  )}
76
78
  </>
@@ -1,41 +1,64 @@
1
- import { Input } from '../forms';
2
1
  import styled from 'styled-components';
3
2
 
4
- export const QuickSearchResultsContainer = styled.div`
3
+ export const QuickSearchDropdownContainer = styled.div.attrs({
4
+ className: 'QuickSearchDropdownContainer',
5
+ })`
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ `;
10
+
11
+ export const QuickSearchResultsContainer = styled.div.attrs({
12
+ className: 'QuickSearchResultsContainer',
13
+ })`
5
14
  display: flex;
6
15
  flex-direction: row;
7
16
  height: 100%;
17
+ flex: 1;
18
+ padding: var(--space-2) 0;
19
+ overflow: hidden;
8
20
  `;
9
21
 
10
- export const QuickSearchResultsListContainer = styled.div`
22
+ export const QuickSearchResultsListContainer = styled.div.attrs({
23
+ className: 'QuickSearchResultsListContainer',
24
+ })`
11
25
  display: flex;
12
26
  flex-direction: column;
13
27
  padding: var(--space-2);
14
- border-right: 1px solid var(--color-gray-200);
15
- flex: 3;
28
+ border-right: 1px solid var(--color-neutral-200);
29
+ flex: 1;
16
30
  overflow: auto;
17
31
  `;
18
32
 
19
- export const QuickSearchResultsTitle = styled.div`
33
+ export const QuickSearchResultsTitle = styled.div.attrs({
34
+ className: 'QuickSearchResultsTitle',
35
+ })`
20
36
  margin: 0;
21
37
  margin-bottom: var(--space-1);
22
38
  &:not(:first-child) {
23
39
  margin-top: var(--space-2);
24
40
  }
25
41
  font-weight: bold;
26
- font-size: var(--text-lg);
42
+ font-size: var(--text-sm);
43
+ text-transform: uppercase;
44
+ letter-spacing: 120%;
45
+ color: var(--color-neutral-500);
27
46
  `;
28
47
 
29
- export const QuickSearchResultsItem = styled.div`
48
+ export const QuickSearchResultsItem = styled.div.attrs({
49
+ className: 'QuickSearchResultsItem',
50
+ })`
30
51
  padding: var(--space-2) var(--space-3);
31
52
  cursor: pointer;
32
53
  border-radius: 4px;
33
54
  &:hover {
34
- background-color: var(--color-gray-100);
55
+ background-color: var(--color-neutral-100);
35
56
  }
36
57
  `;
37
58
 
38
- export const QuickSearchResultsDetailsContainer = styled.div`
59
+ export const QuickSearchResultsDetailsContainer = styled.div.attrs({
60
+ className: 'QuickSearchResultsDetailsContainer',
61
+ })`
39
62
  display: flex;
40
63
  flex-direction: column;
41
64
  padding: var(--space-2);
@@ -46,15 +69,11 @@ export const QuickSearchResultsDetailsDivider = styled.hr`
46
69
  margin: var(--space-2) 0;
47
70
  height: 1px;
48
71
  border: none;
49
- background-color: var(--color-gray-200);
72
+ background-color: var(--color-neutral-200);
50
73
  `;
51
74
 
52
- export const QuickSearchResultDetailsTitle = styled.div`
75
+ export const QuickSearchResultDetailsTitle = styled.div.attrs({
76
+ className: 'QuickSearchResultDetailsTitle',
77
+ })`
53
78
  margin: 0;
54
79
  `;
55
-
56
- export const QuickSearchBarInput = styled(Input)<{ $opened?: boolean }>`
57
- position: relative;
58
- width: 100%;
59
- z-index: ${({ $opened }) => ($opened ? 10000 : 0)};
60
- `;
@@ -0,0 +1,14 @@
1
+ import { CardContainer, CardFooter, CardHeader } from './styles';
2
+ import { FC, HTMLAttributes } from 'react';
3
+
4
+ type CardFC = FC<HTMLAttributes<HTMLDivElement>> & {
5
+ Header: typeof CardHeader;
6
+ Footer: typeof CardFooter;
7
+ };
8
+
9
+ export const Card: CardFC = ({ children }) => {
10
+ return <CardContainer>{children}</CardContainer>;
11
+ };
12
+
13
+ Card.Header = CardHeader;
14
+ Card.Footer = CardFooter;
@@ -0,0 +1,35 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const CardContainer = styled.div.attrs({
4
+ className: 'CardContainer',
5
+ })`
6
+ display: flex;
7
+ flex-direction: column;
8
+ background-color: var(--color-white);
9
+ border-radius: var(--rounded-md);
10
+ box-shadow: var(--shadow-md);
11
+ padding: var(--space-4);
12
+ border: 1px solid var(--color-neutral-100);
13
+ `;
14
+
15
+ export const CardHeader = styled.div.attrs({
16
+ className: 'CardHeader',
17
+ })`
18
+ display: flex;
19
+ flex-direction: row;
20
+ align-items: center;
21
+ padding-bottom: var(--space-2);
22
+ border-bottom: 1px solid var(--color-neutral-200);
23
+ margin-bottom: var(--space-2);
24
+ `;
25
+
26
+ export const CardFooter = styled.div.attrs({
27
+ className: 'CardFooter',
28
+ })`
29
+ display: flex;
30
+ flex-direction: row;
31
+ align-items: center;
32
+ padding-top: var(--space-2);
33
+ border-top: 1px solid var(--color-neutral-200);
34
+ margin-top: var(--space-2);
35
+ `;
@@ -0,0 +1,57 @@
1
+ import * as styles from './styles';
2
+
3
+ import {
4
+ CircleCheckIcon,
5
+ CircleInfoIcon,
6
+ CircleXMarkIcon,
7
+ TriangleExclamationIcon,
8
+ } from '../../../Icons';
9
+ import { FC, PropsWithChildren } from 'react';
10
+
11
+ import { ThemeColor } from '../../../providers';
12
+
13
+ type MessageType = 'success' | 'info' | 'warning' | 'error';
14
+
15
+ type MessageProps = PropsWithChildren<{
16
+ className?: string;
17
+ title?: string;
18
+ type: MessageType;
19
+ }>;
20
+
21
+ const colorsMap: Record<
22
+ MessageType,
23
+ {
24
+ baseColor: ThemeColor;
25
+ iconComponent: FC<{ className?: string }>;
26
+ }
27
+ > = {
28
+ success: {
29
+ baseColor: 'green',
30
+ iconComponent: CircleCheckIcon,
31
+ },
32
+ info: {
33
+ baseColor: 'sky',
34
+ iconComponent: CircleInfoIcon,
35
+ },
36
+ warning: {
37
+ baseColor: 'amber',
38
+ iconComponent: TriangleExclamationIcon,
39
+ },
40
+ error: {
41
+ baseColor: 'red',
42
+ iconComponent: CircleXMarkIcon,
43
+ },
44
+ };
45
+
46
+ export const Message: FC<MessageProps> = ({ children, title, type }) => {
47
+ const { iconComponent: Icon, baseColor = 'neutral' } = colorsMap[type];
48
+ return (
49
+ <styles.MessageContainer $baseColor={baseColor}>
50
+ <Icon className="MessageIcon" />
51
+ <styles.MessageContent>
52
+ {title && <h3>{title}</h3>}
53
+ <div>{children}</div>
54
+ </styles.MessageContent>
55
+ </styles.MessageContainer>
56
+ );
57
+ };
@@ -0,0 +1,40 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ import { ThemeColor } from '../../../providers';
4
+
5
+ export const MessageContainer = styled.div.attrs({
6
+ className: 'MessageContainer',
7
+ })<{
8
+ $baseColor: ThemeColor;
9
+ }>`
10
+ display: flex;
11
+ flex-direction: row;
12
+ box-shadow: var(--shadow-md);
13
+ border-radius: var(--rounded-md);
14
+ padding: var(--space-4);
15
+ gap: var(--space-4);
16
+
17
+ ${({ $baseColor }) => css`
18
+ border: 1px solid var(--color-${$baseColor}-300);
19
+ background-color: var(--color-${$baseColor}-100);
20
+ color: var(--color-${$baseColor}-600);
21
+
22
+ .MessageIcon {
23
+ fill: var(--color-${$baseColor}-600);
24
+ }
25
+ `}
26
+
27
+ .MessageIcon {
28
+ width: var(--space-5);
29
+ height: var(--space-5);
30
+ flex-shrink: 0;
31
+ }
32
+ `;
33
+
34
+ export const MessageContent = styled.div.attrs({
35
+ className: 'MessageContent',
36
+ })`
37
+ display: flex;
38
+ flex-direction: column;
39
+ flex-grow: 1;
40
+ `;
@@ -0,0 +1,3 @@
1
+ export * from './Card';
2
+ export * from './ContextMenu';
3
+ export * from './Message';
@@ -52,4 +52,12 @@ export const ThemeProvider = styled.div<ThemeProviderProps>`
52
52
  getThemeValuesCss('shadow', $theme.shadows),
53
53
  ].join('');
54
54
  }}
55
+
56
+ a, a:visited {
57
+ color: var(--color-primary-500);
58
+ }
59
+ a:active,
60
+ a:hover {
61
+ color: var(--color-primary-700);
62
+ }
55
63
  `;
@@ -45,6 +45,7 @@ export class WebSocketService {
45
45
  if (this.socket && this.socket.readyState === 1) {
46
46
  clearInterval(interval);
47
47
  this.setStatus(true);
48
+ this.sendQueue();
48
49
  this.onOpen?.();
49
50
  }
50
51
  }, 100);