TableSort.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
  14. import CheckIcon from "@mui/icons-material/Check";
  15. import DeleteIcon from "@mui/icons-material/Delete";
  16. import SortByAlpha from "@mui/icons-material/SortByAlpha";
  17. import Badge from "@mui/material/Badge";
  18. import FormControl from "@mui/material/FormControl";
  19. import Grid from "@mui/material/Grid";
  20. import IconButton from "@mui/material/IconButton";
  21. import InputLabel from "@mui/material/InputLabel";
  22. import MenuItem from "@mui/material/MenuItem";
  23. import OutlinedInput from "@mui/material/OutlinedInput";
  24. import Popover, { PopoverOrigin } from "@mui/material/Popover";
  25. import Select, { SelectChangeEvent } from "@mui/material/Select";
  26. import Switch from "@mui/material/Switch";
  27. import Tooltip from "@mui/material/Tooltip";
  28. import Typography from "@mui/material/Typography";
  29. import { ColumnDesc, getsortByIndex, iconInRowSx } from "./tableUtils";
  30. import { getSuffixedClassNames } from "./utils";
  31. export interface SortDesc {
  32. col: string;
  33. order: boolean;
  34. }
  35. interface TableSortProps {
  36. columns: Record<string, ColumnDesc>;
  37. colsOrder?: Array<string>;
  38. onValidate: (data: Array<SortDesc>) => void;
  39. appliedSorts?: Array<SortDesc>;
  40. className?: string;
  41. }
  42. interface SortRowProps {
  43. idx: number;
  44. sort?: SortDesc;
  45. columns: Record<string, ColumnDesc>;
  46. colsOrder: Array<string>;
  47. setSort: (idx: number, fd: SortDesc, remove?: boolean) => void;
  48. appliedSorts?: Array<SortDesc>;
  49. }
  50. const anchorOrigin = {
  51. vertical: "bottom",
  52. horizontal: "right",
  53. } as PopoverOrigin;
  54. const gridSx = { p: "0.5em", minWidth: "36rem" };
  55. const badgeSx = {
  56. "& .MuiBadge-badge": {
  57. height: "10px",
  58. minWidth: "10px",
  59. width: "10px",
  60. borderRadius: "5px",
  61. },
  62. };
  63. const orderCaptionSx = { ml: 1 };
  64. const getSortDesc = (columns: Record<string, ColumnDesc>, colId?: string, asc?: boolean) =>
  65. colId && asc !== undefined
  66. ? ({
  67. col: columns[colId].dfid,
  68. order: !!asc,
  69. } as SortDesc)
  70. : undefined;
  71. const SortRow = (props: SortRowProps) => {
  72. const { idx, setSort, columns, colsOrder, sort, appliedSorts } = props;
  73. const [colId, setColId] = useState("");
  74. const [order, setOrder] = useState(true); // true => asc
  75. const [enableCheck, setEnableCheck] = useState(false);
  76. const [enableDel, setEnableDel] = useState(false);
  77. const cols = useMemo(() => {
  78. if (!Array.isArray(appliedSorts) || appliedSorts.length == 0) {
  79. return colsOrder;
  80. }
  81. return colsOrder.filter((col) => col == sort?.col || !appliedSorts.some((fd) => col === fd.col));
  82. }, [colsOrder, appliedSorts, sort?.col]);
  83. const onColSelect = useCallback(
  84. (e: SelectChangeEvent<string>) => {
  85. setColId(e.target.value);
  86. setEnableCheck(!!getSortDesc(columns, e.target.value, order));
  87. },
  88. [columns, order]
  89. );
  90. const onOrderSwitch = useCallback(
  91. (e: ChangeEvent<HTMLInputElement>) => {
  92. setOrder(e.target.checked);
  93. setEnableCheck(!!getSortDesc(columns, colId, e.target.checked));
  94. },
  95. [columns, colId]
  96. );
  97. const onDeleteClick = useCallback(() => setSort(idx, undefined as unknown as SortDesc, true), [idx, setSort]);
  98. const onCheckClick = useCallback(() => {
  99. const fd = getSortDesc(columns, colId, order);
  100. fd && setSort(idx, fd);
  101. }, [idx, setSort, columns, colId, order]);
  102. useEffect(() => {
  103. if (sort && idx > -1) {
  104. const col = Object.keys(columns).find((col) => columns[col].dfid === sort.col) || "";
  105. setColId(col);
  106. setOrder(sort.order);
  107. setEnableCheck(false);
  108. setEnableDel(!!getSortDesc(columns, col, sort.order));
  109. } else {
  110. setColId("");
  111. setOrder(true);
  112. setEnableCheck(false);
  113. setEnableDel(false);
  114. }
  115. }, [columns, sort, idx]);
  116. return cols.length ? (
  117. <Grid container item xs={12} alignItems="center">
  118. <Grid item xs={6}>
  119. <FormControl margin="dense">
  120. <InputLabel>Column</InputLabel>
  121. <Select value={colId || ""} onChange={onColSelect} input={<OutlinedInput label="Column" />}>
  122. {cols.map((col) => (
  123. <MenuItem key={col} value={col}>
  124. {columns[col].title || columns[col].dfid}
  125. </MenuItem>
  126. ))}
  127. </Select>
  128. </FormControl>
  129. </Grid>
  130. <Grid item xs={4}>
  131. <Switch checked={order} onChange={onOrderSwitch} />
  132. <Typography variant="caption" color="text.secondary" sx={orderCaptionSx}>
  133. {order ? "asc" : "desc"}
  134. </Typography>
  135. </Grid>
  136. <Grid item xs={1}>
  137. <Tooltip title="Validate">
  138. <span>
  139. <IconButton onClick={onCheckClick} disabled={!enableCheck} sx={iconInRowSx}>
  140. <CheckIcon />
  141. </IconButton>
  142. </span>
  143. </Tooltip>
  144. </Grid>
  145. <Grid item xs={1}>
  146. <Tooltip title="Delete">
  147. <span>
  148. <IconButton onClick={onDeleteClick} disabled={!enableDel} sx={iconInRowSx}>
  149. <DeleteIcon />
  150. </IconButton>
  151. </span>
  152. </Tooltip>
  153. </Grid>
  154. </Grid>
  155. ) : null;
  156. };
  157. const TableSort = (props: TableSortProps) => {
  158. const { onValidate, appliedSorts, columns, className = "" } = props;
  159. const [showSort, setShowSort] = useState(false);
  160. const sortRef = useRef<HTMLButtonElement | null>(null);
  161. const [sorts, setSorts] = useState<Array<SortDesc>>([]);
  162. const colsOrder = useMemo(() => {
  163. if (props.colsOrder) {
  164. return props.colsOrder;
  165. }
  166. return Object.keys(columns).sort(getsortByIndex(columns));
  167. }, [props.colsOrder, columns]);
  168. const onShowSortClick = useCallback(() => setShowSort((f) => !f), []);
  169. const updateSort = useCallback(
  170. (idx: number, nsd: SortDesc, remove?: boolean) => {
  171. setSorts((sds) => {
  172. let newSds;
  173. if (idx > -1) {
  174. if (remove) {
  175. sds.splice(idx, 1);
  176. newSds = [...sds];
  177. } else {
  178. newSds = sds.map((fd, index) => (index == idx ? nsd : fd));
  179. }
  180. } else if (remove) {
  181. newSds = sds;
  182. } else {
  183. newSds = [...sds, nsd];
  184. }
  185. onValidate([...newSds]);
  186. return newSds;
  187. });
  188. },
  189. [onValidate]
  190. );
  191. useEffect(() => {
  192. columns &&
  193. appliedSorts &&
  194. setSorts(appliedSorts.filter((fd) => Object.values(columns).some((cd) => cd.dfid === fd.col)));
  195. }, [columns, appliedSorts]);
  196. return (
  197. <>
  198. <Tooltip title={`${sorts.length} sort${sorts.length > 1 ? "s" : ""} applied`}>
  199. <IconButton
  200. onClick={onShowSortClick}
  201. size="small"
  202. ref={sortRef}
  203. sx={iconInRowSx}
  204. className={getSuffixedClassNames(className, "-sort-icon")}
  205. >
  206. <Badge badgeContent={sorts.length} color="primary" sx={badgeSx}>
  207. <SortByAlpha fontSize="inherit" />
  208. </Badge>
  209. </IconButton>
  210. </Tooltip>
  211. <Popover
  212. anchorEl={sortRef.current}
  213. anchorOrigin={anchorOrigin}
  214. open={showSort}
  215. onClose={onShowSortClick}
  216. className={getSuffixedClassNames(className, "-filter")}
  217. >
  218. <Grid container sx={gridSx} gap={0.5}>
  219. {sorts.map((sd, idx) => (
  220. <SortRow
  221. key={"fd" + idx}
  222. idx={idx}
  223. sort={sd}
  224. columns={columns}
  225. colsOrder={colsOrder}
  226. setSort={updateSort}
  227. appliedSorts={sorts}
  228. />
  229. ))}
  230. <SortRow
  231. idx={-(sorts.length + 1)}
  232. columns={columns}
  233. colsOrder={colsOrder}
  234. setSort={updateSort}
  235. appliedSorts={sorts}
  236. />
  237. </Grid>
  238. </Popover>
  239. </>
  240. );
  241. };
  242. export default TableSort;