kelt-ui-kit-react 2.1.0 → 2.1.2
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/dist/button/buttonImport/ButtonImport.d.ts +13 -0
- package/dist/button/buttonImport/ButtonImport.view.d.ts +1 -0
- package/dist/dataTable/dataTable.interface.d.ts +2 -0
- package/dist/dataTable/dataTable.utils.d.ts +3 -0
- package/dist/dataTable/dataTableActions/DataTableActionMenu.d.ts +9 -0
- package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2622 -2419
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/App.css +953 -161
- package/src/App.menu.tsx +8 -0
- package/src/button/buttonFile/ButtonFile.tsx +4 -1
- package/src/button/buttonImport/ButtonImport.tsx +99 -0
- package/src/button/buttonImport/ButtonImport.view.tsx +17 -0
- package/src/button/buttonImport/buttonImport.css +38 -0
- package/src/dataTable/DataTable.tsx +24 -25
- package/src/dataTable/DataTable.view.tsx +13 -0
- package/src/dataTable/dataTable.css +51 -3
- package/src/dataTable/dataTable.interface.ts +2 -0
- package/src/dataTable/dataTable.utils.ts +15 -2
- package/src/dataTable/dataTableActions/DataTableAction.tsx +2 -1
- package/src/dataTable/dataTableActions/DataTableActionMenu.tsx +95 -0
- package/src/dataTable/dataTableColumn/DataTableColumn.tsx +68 -0
- package/src/dataTable/dataTableRow/DataTableRow.tsx +3 -1
- package/src/docs/catalog.ts +1 -0
- package/src/index.ts +1 -0
- package/src/select/Select.tsx +1 -1
- package/src/select/select.css +28 -22
- package/src/tabs/tabs.css +7 -1
- package/tests/dataTable.test.mjs +9 -0
- package/tests/documentation.test.mjs +1 -1
package/src/App.menu.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { AddressSearchView } from "./addressSearch/AddressSearch.view";
|
|
|
6
6
|
import { BadgeView } from "./badge/Badge.view";
|
|
7
7
|
import { ButtonView } from "./button/Button.view";
|
|
8
8
|
import { ButtonFileView } from "./button/buttonFile/ButtonFile.view";
|
|
9
|
+
import { ButtonImportView } from "./button/buttonImport/ButtonImport.view";
|
|
9
10
|
import { CardView } from "./card/Card.view";
|
|
10
11
|
import { CarouselView } from "./carousel/Carousel.view";
|
|
11
12
|
import { DamierView } from "./damier/Damier.view";
|
|
@@ -251,6 +252,13 @@ export const MenuList: MenuProps[] = [
|
|
|
251
252
|
children: <NavLink to="/address-search">Address search</NavLink>,
|
|
252
253
|
element: <AddressSearchView />,
|
|
253
254
|
},
|
|
255
|
+
{
|
|
256
|
+
id: 31,
|
|
257
|
+
name: "Button Import",
|
|
258
|
+
link: "/button-import",
|
|
259
|
+
children: <NavLink to="/button-import">Button Import</NavLink>,
|
|
260
|
+
element: <ButtonImportView />,
|
|
261
|
+
},
|
|
254
262
|
];
|
|
255
263
|
|
|
256
264
|
export const AppMenu = (): JSX.Element => {
|
|
@@ -31,6 +31,9 @@ export const ButtonFile = ({
|
|
|
31
31
|
|
|
32
32
|
reader.readAsArrayBuffer(file);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// Permet de resélectionner le même fichier (sinon onChange ne se déclenche pas)
|
|
36
|
+
e.target.value = "";
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
const handleButtonClick = () => {
|
|
@@ -43,7 +46,7 @@ export const ButtonFile = ({
|
|
|
43
46
|
type="file"
|
|
44
47
|
ref={fileInputRef}
|
|
45
48
|
onChange={handleFileChange}
|
|
46
|
-
|
|
49
|
+
style={{ display: "none" }}
|
|
47
50
|
accept={accept}
|
|
48
51
|
/>
|
|
49
52
|
<Button
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ChangeEvent, useRef, useState } from "react";
|
|
2
|
+
import { Icon } from "../../icon/Icon";
|
|
3
|
+
import { IconSizeEnum } from "../../icon/iconSize.enum";
|
|
4
|
+
import { Modal } from "../../modal/Modal";
|
|
5
|
+
import { Button } from "../Button";
|
|
6
|
+
import "./buttonImport.css";
|
|
7
|
+
|
|
8
|
+
interface ButtonImportProps {
|
|
9
|
+
onValidate: (file: File) => void | Promise<void>;
|
|
10
|
+
accept?: string;
|
|
11
|
+
buttonText?: string;
|
|
12
|
+
buttonClassName?: string;
|
|
13
|
+
buttonClassIcon?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
classIcon?: string;
|
|
17
|
+
validateText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ButtonImport = ({
|
|
21
|
+
onValidate,
|
|
22
|
+
accept = "*",
|
|
23
|
+
buttonText = "Importer",
|
|
24
|
+
buttonClassName,
|
|
25
|
+
buttonClassIcon = "",
|
|
26
|
+
title = "Importer un fichier",
|
|
27
|
+
description = "Sélectionner un fichier à importer.",
|
|
28
|
+
classIcon = "bi-file-earmark-arrow-up",
|
|
29
|
+
validateText = "Valider",
|
|
30
|
+
}: ButtonImportProps) => {
|
|
31
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
32
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
33
|
+
const [file, setFile] = useState<File | null>(null);
|
|
34
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
35
|
+
|
|
36
|
+
const handleClose = () => {
|
|
37
|
+
if (loading) return;
|
|
38
|
+
setIsOpen(false);
|
|
39
|
+
setFile(null);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
43
|
+
setFile(e.target.files?.[0] ?? null);
|
|
44
|
+
// Permet de resélectionner le même fichier
|
|
45
|
+
e.target.value = "";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleValidate = async () => {
|
|
49
|
+
if (!file) return;
|
|
50
|
+
setLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
await onValidate(file);
|
|
53
|
+
setIsOpen(false);
|
|
54
|
+
setFile(null);
|
|
55
|
+
} catch {
|
|
56
|
+
// L'appelant gère l'erreur, la modal reste ouverte pour réessayer
|
|
57
|
+
} finally {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<Button
|
|
65
|
+
className={buttonClassName}
|
|
66
|
+
classIcon={buttonClassIcon}
|
|
67
|
+
title={buttonText}
|
|
68
|
+
onClick={() => setIsOpen(true)}
|
|
69
|
+
/>
|
|
70
|
+
<Modal isOpen={isOpen} onClose={handleClose} title={title}>
|
|
71
|
+
<input
|
|
72
|
+
type="file"
|
|
73
|
+
ref={fileInputRef}
|
|
74
|
+
onChange={handleFileChange}
|
|
75
|
+
style={{ display: "none" }}
|
|
76
|
+
accept={accept}
|
|
77
|
+
/>
|
|
78
|
+
<div
|
|
79
|
+
className="button-import-zone"
|
|
80
|
+
onClick={() => fileInputRef.current?.click()}
|
|
81
|
+
>
|
|
82
|
+
<div>{description}</div>
|
|
83
|
+
<Icon classIcon={classIcon} size={IconSizeEnum.LARGE} />
|
|
84
|
+
<div className="button-import-file mt-2">
|
|
85
|
+
{file ? file.name : "Aucun fichier sélectionné"}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="d-flex justify-content-end mt-2">
|
|
89
|
+
|
|
90
|
+
<Button
|
|
91
|
+
title={validateText}
|
|
92
|
+
disabled={!file || loading}
|
|
93
|
+
onClick={handleValidate}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</Modal>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ButtonImport } from "./ButtonImport";
|
|
2
|
+
|
|
3
|
+
export const ButtonImportView = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="d-flex">
|
|
6
|
+
<ButtonImport
|
|
7
|
+
accept=".csv,text/csv"
|
|
8
|
+
buttonText="Importer CSV"
|
|
9
|
+
title="Import des stocks"
|
|
10
|
+
description="Sélectionner un fichier CSV afin de mettre à jour les stocks."
|
|
11
|
+
onValidate={(file: File) => {
|
|
12
|
+
console.log("Fichier validé:", file.name, file.size);
|
|
13
|
+
}}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.button-import-zone {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
width: 100%;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
text-align: center;
|
|
8
|
+
padding: 1rem 0;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
|
|
11
|
+
> div {
|
|
12
|
+
margin-bottom: 1rem;
|
|
13
|
+
color: #8f8888;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bi {
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
border: 1px solid #d5d5d5;
|
|
19
|
+
padding: 2rem;
|
|
20
|
+
color: #d5d5d5;
|
|
21
|
+
width: 5rem;
|
|
22
|
+
height: 5rem;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:hover .bi {
|
|
30
|
+
border-color: #8f8888;
|
|
31
|
+
color: #8f8888;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.button-import-file {
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
word-break: break-all;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -12,17 +12,17 @@ import {
|
|
|
12
12
|
dataTableColumnsInterface,
|
|
13
13
|
} from "./dataTable.interface";
|
|
14
14
|
import { DataTableAction } from "./dataTableActions/DataTableAction";
|
|
15
|
+
import { DataTableActionMenu } from "./dataTableActions/DataTableActionMenu";
|
|
15
16
|
import { DataTableColumn } from "./dataTableColumn/DataTableColumn";
|
|
16
17
|
import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
|
|
17
18
|
import { DataTableRow } from "./dataTableRow/DataTableRow";
|
|
18
19
|
import { DataTableFilters } from "./dataTableFilters/DataTableFilters";
|
|
19
20
|
import { useDataTableState } from "./useDataTableState";
|
|
20
|
-
import { DataTableFilterState, DataTableSortState, flattenColumns, getActionsWidth } from "./dataTable.utils";
|
|
21
|
+
import { DataTableFilterState, DataTableSortState, flattenColumns, getActionsWidth, isMenuAction } from "./dataTable.utils";
|
|
21
22
|
export type {
|
|
22
23
|
DataTableDateRangeFilterValue, DataTableFilterState, DataTableSortDirection,
|
|
23
24
|
DataTableSortState, FilterValue,
|
|
24
25
|
} from "./dataTable.utils";
|
|
25
|
-
const DEFAULT_FILTER_COLUMN_WIDTH = 100;
|
|
26
26
|
|
|
27
27
|
export type DataTableProps<T> = {
|
|
28
28
|
id: string;
|
|
@@ -93,9 +93,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
93
93
|
const tableState = useDataTableState(id, groupedColumns, sourceData, onFiltersChange, onSortChange);
|
|
94
94
|
const { flatColumns: columns, sortedData: data } = tableState;
|
|
95
95
|
const actionsColumnWidth = getActionsWidth(actions, explicitActionsWidth);
|
|
96
|
-
const
|
|
96
|
+
const inlineActions = useMemo(() => actions.filter((action) => !isMenuAction(action)), [actions]);
|
|
97
|
+
const menuActions = useMemo(() => actions.filter(isMenuAction), [actions]);
|
|
97
98
|
const groupRowRef = useRef<HTMLTableRowElement>(null);
|
|
98
|
-
const [filterHeight, setFilterHeight] = useState(0);
|
|
99
99
|
const [groupHeight, setGroupHeight] = useState(0);
|
|
100
100
|
const hasGroups = groupedColumns.some((column) => column.children?.length);
|
|
101
101
|
const stickyOffsets = useMemo(() => {
|
|
@@ -112,7 +112,6 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
112
112
|
|
|
113
113
|
useLayoutEffect(() => {
|
|
114
114
|
const measure = () => {
|
|
115
|
-
setFilterHeight(filterRowRef.current?.getBoundingClientRect().height ?? 0);
|
|
116
115
|
setGroupHeight(groupRowRef.current?.getBoundingClientRect().height ?? 0);
|
|
117
116
|
};
|
|
118
117
|
measure();
|
|
@@ -121,10 +120,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
121
120
|
return () => window.removeEventListener("resize", measure);
|
|
122
121
|
}
|
|
123
122
|
const observer = new ResizeObserver(measure);
|
|
124
|
-
if (filterRowRef.current) observer.observe(filterRowRef.current);
|
|
125
123
|
if (groupRowRef.current) observer.observe(groupRowRef.current);
|
|
126
124
|
return () => observer.disconnect();
|
|
127
|
-
}, [
|
|
125
|
+
}, [hasGroups, groupedColumns]);
|
|
128
126
|
|
|
129
127
|
const isMobile = useIsMobile();
|
|
130
128
|
const showMobileCardView = mobileCardView && isMobile;
|
|
@@ -531,8 +529,8 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
531
529
|
isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
|
|
532
530
|
onToggleFilter={tableState.toggleFilter} onToggleSort={tableState.toggleSort}
|
|
533
531
|
sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
|
|
532
|
+
filterControl={tableState.renderFilter(column)}
|
|
534
533
|
/></tr></thead></table>
|
|
535
|
-
{tableState.renderFilter(column)}
|
|
536
534
|
</div>
|
|
537
535
|
))}
|
|
538
536
|
</div>
|
|
@@ -609,7 +607,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
609
607
|
))}
|
|
610
608
|
{actions.length > 0 && (
|
|
611
609
|
<div className="data-table-mobile-card__actions card-actions">
|
|
612
|
-
{
|
|
610
|
+
{inlineActions.map((action, actionIndex) => (
|
|
613
611
|
<DataTableAction
|
|
614
612
|
key={action.id}
|
|
615
613
|
action={action}
|
|
@@ -619,6 +617,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
619
617
|
onFocusNextRow={focusNextRow}
|
|
620
618
|
/>
|
|
621
619
|
))}
|
|
620
|
+
<DataTableActionMenu
|
|
621
|
+
actions={menuActions}
|
|
622
|
+
row={row}
|
|
623
|
+
rowIndex={rowIndex}
|
|
624
|
+
onFocusNextRow={focusNextRow}
|
|
625
|
+
/>
|
|
622
626
|
</div>
|
|
623
627
|
)}
|
|
624
628
|
</div>
|
|
@@ -643,29 +647,17 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
643
647
|
{actions.length > 0 && <col style={{ width: actionsColumnWidth }} />}
|
|
644
648
|
</colgroup>
|
|
645
649
|
<thead>
|
|
646
|
-
{tableState.visibleFilters.size > 0 && <tr className="data-table-filter-row" ref={filterRowRef}>
|
|
647
|
-
{onSelectionChange && <th />}
|
|
648
|
-
{columns.map((column) => <th className="filter-row-cell" key={column.id}
|
|
649
|
-
style={{ position: "sticky", top: 0, zIndex: 14, background: "#f9fbfc",
|
|
650
|
-
width: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
|
|
651
|
-
minWidth: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
|
|
652
|
-
maxWidth: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
|
|
653
|
-
...(column.sticky ? { left: stickyOffsets.get(column.id) } : {}) }}>
|
|
654
|
-
{tableState.renderFilter(column)}
|
|
655
|
-
</th>)}
|
|
656
|
-
{actions.length > 0 && <th />}
|
|
657
|
-
</tr>}
|
|
658
650
|
{hasGroups && <tr ref={groupRowRef} className="data-table-group-row">
|
|
659
651
|
{onSelectionChange && <th />}
|
|
660
652
|
{groupedColumns.map((column) => <th key={column.id} colSpan={flattenColumns([column]).length}
|
|
661
|
-
style={{ top:
|
|
653
|
+
style={{ top: 0 }}>{column.children?.length ? column.label : ""}</th>)}
|
|
662
654
|
{actions.length > 0 && <th />}
|
|
663
655
|
</tr>}
|
|
664
656
|
<tr>
|
|
665
657
|
{onSelectionChange && (
|
|
666
658
|
<th
|
|
667
659
|
className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
|
|
668
|
-
style={{ width: "40px", textAlign: "center", top:
|
|
660
|
+
style={{ width: "40px", textAlign: "center", top: groupHeight,
|
|
669
661
|
...(stickyOffsets.size > 0 ? { left: 0, zIndex: 12 } : {}) }}
|
|
670
662
|
>
|
|
671
663
|
<Input
|
|
@@ -686,11 +678,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
686
678
|
idx={idx}
|
|
687
679
|
onColumnClick={onColumnClick}
|
|
688
680
|
stickyLeft={stickyOffsets.get(column.id)}
|
|
689
|
-
stickyTop={
|
|
681
|
+
stickyTop={groupHeight}
|
|
690
682
|
isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
|
|
691
683
|
onToggleFilter={tableState.toggleFilter}
|
|
692
684
|
onToggleSort={tableState.toggleSort}
|
|
693
685
|
sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
|
|
686
|
+
filterControl={tableState.renderFilter(column)}
|
|
694
687
|
/>
|
|
695
688
|
))}
|
|
696
689
|
{actions.length > 0 && (
|
|
@@ -782,7 +775,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
782
775
|
onClick={(e) => e.stopPropagation()}
|
|
783
776
|
>
|
|
784
777
|
<div className="d-flex align-items-center justify-content-end">
|
|
785
|
-
{
|
|
778
|
+
{inlineActions.map((action, index) => (
|
|
786
779
|
<DataTableAction
|
|
787
780
|
key={index}
|
|
788
781
|
action={action}
|
|
@@ -792,6 +785,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
792
785
|
onFocusNextRow={focusNextRow}
|
|
793
786
|
/>
|
|
794
787
|
))}
|
|
788
|
+
<DataTableActionMenu
|
|
789
|
+
actions={menuActions}
|
|
790
|
+
row={row}
|
|
791
|
+
rowIndex={rowIndex}
|
|
792
|
+
onFocusNextRow={focusNextRow}
|
|
793
|
+
/>
|
|
795
794
|
</div>
|
|
796
795
|
</td>
|
|
797
796
|
)}
|
|
@@ -64,10 +64,23 @@ export const DataTableView = () => {
|
|
|
64
64
|
console.log("Edit action clicked for row:", row);
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
id: "duplicate",
|
|
69
|
+
label: "Dupliquer",
|
|
70
|
+
type: "button",
|
|
71
|
+
classIcon: "bi-copy",
|
|
72
|
+
display: "menu",
|
|
73
|
+
onClick: (row: any) => {
|
|
74
|
+
console.log("Duplicate action clicked for row:", row);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
67
77
|
{
|
|
68
78
|
id: "delete",
|
|
69
79
|
label: "Delete",
|
|
70
80
|
type: "button",
|
|
81
|
+
classIcon: "bi-trash",
|
|
82
|
+
display: "menu",
|
|
83
|
+
shouldDisplay: (row) => row.id !== 1,
|
|
71
84
|
onClick: (row: any) => {
|
|
72
85
|
console.log("Delete action clicked for row:", row);
|
|
73
86
|
},
|
|
@@ -134,7 +134,12 @@
|
|
|
134
134
|
background-color: #fff;
|
|
135
135
|
color: var(--tertiary-color);
|
|
136
136
|
min-width: 100px;
|
|
137
|
-
padding: .5rem;
|
|
137
|
+
padding: .5rem 0;
|
|
138
|
+
|
|
139
|
+
&:first-child,
|
|
140
|
+
&:last-child {
|
|
141
|
+
padding: .5rem;
|
|
142
|
+
}
|
|
138
143
|
|
|
139
144
|
.data-table-column-filter,
|
|
140
145
|
.data-table-column-sort {
|
|
@@ -152,6 +157,7 @@
|
|
|
152
157
|
|
|
153
158
|
&:hover,
|
|
154
159
|
&:focus-within {
|
|
160
|
+
|
|
155
161
|
.data-table-column-filter,
|
|
156
162
|
.data-table-column-sort {
|
|
157
163
|
visibility: visible;
|
|
@@ -302,32 +308,74 @@
|
|
|
302
308
|
}
|
|
303
309
|
}
|
|
304
310
|
}
|
|
311
|
+
|
|
305
312
|
.data-table-action-icon {
|
|
306
313
|
cursor: pointer;
|
|
307
314
|
display: flex;
|
|
308
315
|
align-items: center;
|
|
309
316
|
}
|
|
317
|
+
|
|
318
|
+
.data-table-action-menu-toggle {
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
width: 32px;
|
|
324
|
+
height: 32px;
|
|
325
|
+
border-radius: 4px;
|
|
326
|
+
|
|
327
|
+
&:hover,
|
|
328
|
+
&[aria-expanded="true"] {
|
|
329
|
+
background: rgba(0, 0, 0, 0.06);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.data-table-action-menu {
|
|
334
|
+
min-width: 150px;
|
|
335
|
+
max-width: 300px;
|
|
336
|
+
padding: 0.25rem;
|
|
337
|
+
|
|
338
|
+
button {
|
|
339
|
+
font-size: 0.85rem;
|
|
340
|
+
width: 100%;
|
|
341
|
+
padding: 1rem 0rem !important;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
310
345
|
.data-table-column-heading {
|
|
311
346
|
display: flex;
|
|
312
347
|
align-items: center;
|
|
313
348
|
gap: 4px;
|
|
314
349
|
|
|
315
|
-
|
|
350
|
+
>.flex-1 {
|
|
316
351
|
flex: 1;
|
|
317
352
|
}
|
|
318
353
|
}
|
|
354
|
+
|
|
355
|
+
.data-table-filter-popover {
|
|
356
|
+
z-index: 1200;
|
|
357
|
+
padding: 8px;
|
|
358
|
+
box-sizing: border-box;
|
|
359
|
+
border: 1px solid #dce3e7;
|
|
360
|
+
border-radius: 7px;
|
|
361
|
+
background: #fff;
|
|
362
|
+
box-shadow: 0 6px 18px rgba(20, 40, 50, 0.16);
|
|
363
|
+
}
|
|
364
|
+
|
|
319
365
|
.data-table-group-row th {
|
|
320
366
|
height: 40px;
|
|
321
367
|
box-sizing: border-box;
|
|
322
368
|
}
|
|
369
|
+
|
|
323
370
|
.data-table-action-link {
|
|
324
371
|
min-width: 0;
|
|
325
372
|
background: transparent;
|
|
326
373
|
color: var(--primary-color);
|
|
327
374
|
text-decoration: underline;
|
|
328
375
|
}
|
|
376
|
+
|
|
329
377
|
.data-table-mobile-controls {
|
|
330
378
|
display: grid;
|
|
331
379
|
gap: 12px;
|
|
332
380
|
margin-bottom: 16px;
|
|
333
|
-
}
|
|
381
|
+
}
|
|
@@ -63,4 +63,6 @@ export interface dataTableActionsInterface<T> {
|
|
|
63
63
|
width?: string | number;
|
|
64
64
|
minWidth?: string | number;
|
|
65
65
|
focusNextRow?: boolean;
|
|
66
|
+
/** "inline" (défaut) : affichée dans la ligne ; "menu" : regroupée dans le menu « ⋮ » en fin de ligne */
|
|
67
|
+
display?: "inline" | "menu";
|
|
66
68
|
}
|
|
@@ -19,10 +19,23 @@ export function getFilterKey<T>(column: dataTableColumnsInterface<T>) {
|
|
|
19
19
|
return column.name ?? column.id;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export const ACTION_MENU_WIDTH = 40;
|
|
23
|
+
|
|
24
|
+
export function isMenuAction<T>(action: dataTableActionsInterface<T>) {
|
|
25
|
+
return action.display === "menu";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isActionDisplayed<T>(action: dataTableActionsInterface<T>, row: T) {
|
|
29
|
+
return action.visible !== false && (action.shouldDisplay ? action.shouldDisplay(row) : true);
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
export function getActionsWidth<T>(actions: dataTableActionsInterface<T>[], explicitWidth?: number) {
|
|
23
33
|
if (explicitWidth !== undefined) return explicitWidth;
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
const inlineActions = actions.filter((action) => !isMenuAction(action));
|
|
35
|
+
const menuWidth = inlineActions.length < actions.length ? ACTION_MENU_WIDTH : 0;
|
|
36
|
+
if (inlineActions.length === 0 && menuWidth > 0) return menuWidth;
|
|
37
|
+
if (!inlineActions.some((action) => typeof action.width === "number")) return 200;
|
|
38
|
+
return inlineActions.reduce((total, action) => total + (typeof action.width === "number" ? action.width : 0), menuWidth);
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
export function sortRows<T>(data: T[], columns: dataTableColumnsInterface<T>[], sort: DataTableSortState | null): T[] {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Button } from "../../button/Button";
|
|
2
2
|
import { Icon } from "../../icon/Icon";
|
|
3
3
|
import { dataTableActionsInterface } from "../dataTable.interface";
|
|
4
|
+
import { isActionDisplayed } from "../dataTable.utils";
|
|
4
5
|
type DataTableActionProps = {
|
|
5
6
|
action: dataTableActionsInterface<any>;
|
|
6
7
|
row: any;
|
|
@@ -16,7 +17,7 @@ export const DataTableAction = ({
|
|
|
16
17
|
onFocusNextRow,
|
|
17
18
|
}: DataTableActionProps) => {
|
|
18
19
|
let title = action.label;
|
|
19
|
-
const display =
|
|
20
|
+
const display = isActionDisplayed(action, row);
|
|
20
21
|
if (!display) return null;
|
|
21
22
|
if (action.labelButton) {
|
|
22
23
|
title = action.labelButton ? action.labelButton(row) : action.label;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Button } from "../../button/Button";
|
|
3
|
+
import { Icon } from "../../icon/Icon";
|
|
4
|
+
import { IconSizeEnum } from "../../icon/iconSize.enum";
|
|
5
|
+
import { MenuInterface } from "../../menus/menu/menu.interface";
|
|
6
|
+
import { Menus } from "../../menus/Menus";
|
|
7
|
+
import { OverlayPanel } from "../../overlayPanel/OverlayPanel";
|
|
8
|
+
import { dataTableActionsInterface } from "../dataTable.interface";
|
|
9
|
+
import { isActionDisplayed } from "../dataTable.utils";
|
|
10
|
+
|
|
11
|
+
type DataTableActionMenuProps = {
|
|
12
|
+
actions: dataTableActionsInterface<any>[];
|
|
13
|
+
row: any;
|
|
14
|
+
rowIndex?: number;
|
|
15
|
+
onFocusNextRow?: (rowIndex: number) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DataTableActionMenu = ({
|
|
19
|
+
actions,
|
|
20
|
+
row,
|
|
21
|
+
rowIndex,
|
|
22
|
+
onFocusNextRow,
|
|
23
|
+
}: DataTableActionMenuProps) => {
|
|
24
|
+
const refButton = useRef<HTMLDivElement>(null);
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const visibleActions = actions.filter((action) => isActionDisplayed(action, row));
|
|
27
|
+
|
|
28
|
+
const items: MenuInterface[] = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
visibleActions.map((action, index) => ({
|
|
31
|
+
id: index,
|
|
32
|
+
icon: action.classIcon,
|
|
33
|
+
name: action.label,
|
|
34
|
+
disabled: action.disabled,
|
|
35
|
+
children: (
|
|
36
|
+
<Button
|
|
37
|
+
key={action.id}
|
|
38
|
+
classIcon={action.classIcon}
|
|
39
|
+
sizeIcon={IconSizeEnum.LARGE}
|
|
40
|
+
disabled={action.disabled}
|
|
41
|
+
title={action.labelButton ? action.labelButton(row) : action.label}
|
|
42
|
+
onClick={(e) => {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
if (action.disabled) return;
|
|
45
|
+
setOpen(false);
|
|
46
|
+
action.onClick(row, rowIndex);
|
|
47
|
+
if (action.focusNextRow && rowIndex !== undefined) {
|
|
48
|
+
onFocusNextRow?.(rowIndex);
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
),
|
|
53
|
+
})),
|
|
54
|
+
[visibleActions, row, rowIndex, onFocusNextRow],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (visibleActions.length === 0) return null;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<div
|
|
62
|
+
ref={refButton}
|
|
63
|
+
className="data-table-action-menu-toggle"
|
|
64
|
+
title="Actions"
|
|
65
|
+
role="button"
|
|
66
|
+
tabIndex={0}
|
|
67
|
+
aria-haspopup="menu"
|
|
68
|
+
aria-expanded={open}
|
|
69
|
+
onClick={(e) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
setOpen(!open);
|
|
72
|
+
}}
|
|
73
|
+
onKeyDown={(e) => {
|
|
74
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
setOpen(!open);
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<Icon size={IconSizeEnum.MEDIUM} classIcon="bi-three-dots-vertical" />
|
|
82
|
+
</div>
|
|
83
|
+
{open && refButton.current && (
|
|
84
|
+
<OverlayPanel
|
|
85
|
+
show={open}
|
|
86
|
+
position="bottomRight"
|
|
87
|
+
referenceElement={refButton.current}
|
|
88
|
+
closeOverlay={() => setOpen(false)}
|
|
89
|
+
>
|
|
90
|
+
<Menus className="data-table-action-menu" items={items} />
|
|
91
|
+
</OverlayPanel>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
};
|