Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
39
front/src/modules/ui/table/components/CheckboxCell.tsx
Normal file
39
front/src/modules/ui/table/components/CheckboxCell.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||
|
||||
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
|
||||
import { contextMenuPositionState } from '../states/contextMenuPositionState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export function CheckboxCell() {
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
|
||||
const { currentRowSelected, setCurrentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
function onChange(checked: boolean) {
|
||||
handleCheckboxChange(checked);
|
||||
}
|
||||
|
||||
function handleCheckboxChange(newCheckedValue: boolean) {
|
||||
setCurrentRowSelected(newCheckedValue);
|
||||
|
||||
setContextMenuPosition({ x: null, y: null });
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Checkbox checked={currentRowSelected} onChange={onChange} />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
30
front/src/modules/ui/table/components/ColumnHead.tsx
Normal file
30
front/src/modules/ui/table/components/ColumnHead.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
viewName: string;
|
||||
viewIcon?: ReactNode;
|
||||
};
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
|
||||
return (
|
||||
<StyledTitle>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</StyledTitle>
|
||||
);
|
||||
}
|
||||
113
front/src/modules/ui/table/components/EntityTable.tsx
Normal file
113
front/src/modules/ui/table/components/EntityTable.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TableColumn } from '@/people/table/components/peopleColumns';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
|
||||
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { TableHeader } from '../table-header/components/TableHeader';
|
||||
|
||||
import { EntityTableBody } from './EntityTableBody';
|
||||
import { EntityTableHeader } from './EntityTableHeader';
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border-collapse: collapse;
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-spacing: 0;
|
||||
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||
table-layout: fixed;
|
||||
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
||||
|
||||
th {
|
||||
border: 1px solid ${({ theme }) => theme.background.tertiary};
|
||||
border-collapse: collapse;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
|
||||
:last-child {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:first-of-type {
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:last-of-type {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid ${({ theme }) => theme.background.tertiary};
|
||||
border-collapse: collapse;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
padding: 0;
|
||||
|
||||
text-align: left;
|
||||
|
||||
:last-child {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:first-of-type {
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:last-of-type {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableWithHeader = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps<SortField> = {
|
||||
columns: Array<TableColumn>;
|
||||
viewName: string;
|
||||
viewIcon?: React.ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onRowSelectionChange?: (rowSelection: string[]) => void;
|
||||
};
|
||||
|
||||
export function EntityTable<SortField>({
|
||||
columns,
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onSortsUpdate,
|
||||
}: OwnProps<SortField>) {
|
||||
const tableBodyRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const leaveTableFocus = useLeaveTableFocus();
|
||||
|
||||
useListenClickOutsideArrayOfRef([tableBodyRef], () => {
|
||||
leaveTableFocus();
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTableWithHeader>
|
||||
<TableHeader
|
||||
viewName={viewName}
|
||||
viewIcon={viewIcon}
|
||||
availableSorts={availableSorts}
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
/>
|
||||
<div ref={tableBodyRef}>
|
||||
<StyledTable>
|
||||
<EntityTableHeader columns={columns} />
|
||||
<EntityTableBody columns={columns} />
|
||||
</StyledTable>
|
||||
</div>
|
||||
</StyledTableWithHeader>
|
||||
);
|
||||
}
|
||||
33
front/src/modules/ui/table/components/EntityTableBody.tsx
Normal file
33
front/src/modules/ui/table/components/EntityTableBody.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { TableColumn } from '@/people/table/components/peopleColumns';
|
||||
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState';
|
||||
import { RowContext } from '../states/RowContext';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
import { EntityTableRow } from './EntityTableRow';
|
||||
|
||||
export function EntityTableBody({ columns }: { columns: Array<TableColumn> }) {
|
||||
const rowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
|
||||
|
||||
const isFetchingEntityTableData = useRecoilValue(
|
||||
isFetchingEntityTableDataState,
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{!isFetchingEntityTableData && !isNavbarSwitchingSize
|
||||
? rowIds.map((rowId, index) => (
|
||||
<RecoilScope SpecificContext={RowContext} key={rowId}>
|
||||
<EntityTableRow columns={columns} rowId={rowId} index={index} />
|
||||
</RecoilScope>
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
58
front/src/modules/ui/table/components/EntityTableCell.tsx
Normal file
58
front/src/modules/ui/table/components/EntityTableCell.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
|
||||
import { CellContext } from '../states/CellContext';
|
||||
import { contextMenuPositionState } from '../states/contextMenuPositionState';
|
||||
import { currentColumnNumberScopedState } from '../states/currentColumnNumberScopedState';
|
||||
|
||||
export function EntityTableCell({
|
||||
rowId,
|
||||
cellIndex,
|
||||
children,
|
||||
size,
|
||||
}: {
|
||||
size: number;
|
||||
rowId: string;
|
||||
cellIndex: number;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [, setCurrentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentColumnNumber(cellIndex);
|
||||
}, [cellIndex, setCurrentColumnNumber]);
|
||||
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
|
||||
const { setCurrentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
setCurrentRowSelected(true);
|
||||
|
||||
setContextMenuPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
onContextMenu={(event) => handleContextMenu(event)}
|
||||
style={{
|
||||
width: size,
|
||||
minWidth: size,
|
||||
maxWidth: size,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
39
front/src/modules/ui/table/components/EntityTableHeader.tsx
Normal file
39
front/src/modules/ui/table/components/EntityTableHeader.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { TableColumn } from '@/people/table/components/peopleColumns';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
|
||||
export function EntityTableHeader({
|
||||
columns,
|
||||
}: {
|
||||
columns: Array<TableColumn>;
|
||||
}) {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{
|
||||
width: 30,
|
||||
minWidth: 30,
|
||||
maxWidth: 30,
|
||||
}}
|
||||
>
|
||||
<SelectAllCheckbox />
|
||||
</th>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.id.toString()}
|
||||
style={{
|
||||
width: column.size,
|
||||
minWidth: column.size,
|
||||
maxWidth: column.size,
|
||||
}}
|
||||
>
|
||||
<ColumnHead viewName={column.title} viewIcon={column.icon} />
|
||||
</th>
|
||||
))}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
81
front/src/modules/ui/table/components/EntityTableRow.tsx
Normal file
81
front/src/modules/ui/table/components/EntityTableRow.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { TableColumn } from '@/people/table/components/peopleColumns';
|
||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { CellContext } from '../states/CellContext';
|
||||
import { currentRowEntityIdScopedState } from '../states/currentRowEntityIdScopedState';
|
||||
import { currentRowNumberScopedState } from '../states/currentRowNumberScopedState';
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
import { RowContext } from '../states/RowContext';
|
||||
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import { EntityTableCell } from './EntityTableCell';
|
||||
|
||||
const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.selected ? props.theme.background.secondary : 'none'};
|
||||
`;
|
||||
|
||||
export function EntityTableRow({
|
||||
columns,
|
||||
rowId,
|
||||
index,
|
||||
}: {
|
||||
columns: TableColumn[];
|
||||
rowId: string;
|
||||
index: number;
|
||||
}) {
|
||||
const [currentRowEntityId, setCurrentRowEntityId] = useRecoilScopedState(
|
||||
currentRowEntityIdScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
const isCurrentRowSelected = useRecoilValue(isRowSelectedFamilyState(rowId));
|
||||
|
||||
const [, setCurrentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRowEntityId !== rowId) {
|
||||
setCurrentRowEntityId(rowId);
|
||||
}
|
||||
}, [rowId, setCurrentRowEntityId, currentRowEntityId]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentRowNumber(index);
|
||||
}, [index, setCurrentRowNumber]);
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
key={rowId}
|
||||
data-testid={`row-id-${rowId}`}
|
||||
selected={isCurrentRowSelected}
|
||||
>
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{columns.map((column, columnIndex) => {
|
||||
return (
|
||||
<RecoilScope SpecificContext={CellContext} key={column.id.toString()}>
|
||||
<RecoilScope>
|
||||
<EntityTableCell
|
||||
rowId={rowId}
|
||||
size={column.size}
|
||||
cellIndex={columnIndex}
|
||||
>
|
||||
{column.cellComponent}
|
||||
</EntityTableCell>
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
</StyledRow>
|
||||
);
|
||||
}
|
||||
25
front/src/modules/ui/table/components/HooksEntityTable.tsx
Normal file
25
front/src/modules/ui/table/components/HooksEntityTable.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
|
||||
import { useInitializeEntityTable } from '../hooks/useInitializeEntityTable';
|
||||
import { useInitializeEntityTableFilters } from '../hooks/useInitializeEntityTableFilters';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
|
||||
export function HooksEntityTable({
|
||||
numberOfColumns,
|
||||
availableFilters,
|
||||
}: {
|
||||
numberOfColumns: number;
|
||||
availableFilters: FilterDefinition[];
|
||||
}) {
|
||||
useMapKeyboardToSoftFocus();
|
||||
|
||||
useInitializeEntityTable({
|
||||
numberOfColumns,
|
||||
});
|
||||
|
||||
useInitializeEntityTableFilters({
|
||||
availableFilters,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
}
|
||||
35
front/src/modules/ui/table/components/SelectAllCheckbox.tsx
Normal file
35
front/src/modules/ui/table/components/SelectAllCheckbox.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||
|
||||
import { useSelectAllRows } from '../hooks/useSelectAllRows';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const SelectAllCheckbox = () => {
|
||||
const { selectAllRows, allRowsSelectedStatus } = useSelectAllRows();
|
||||
|
||||
const checked = allRowsSelectedStatus === 'all';
|
||||
const indeterminate = allRowsSelectedStatus === 'some';
|
||||
function onChange(value: boolean) {
|
||||
selectAllRows();
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
indeterminate={indeterminate}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user