Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionBar } from '@/ui/action-bar/components/ActionBar';
|
||||
|
||||
import { selectedRowIdsSelector } from '../../states/selectedRowIdsSelector';
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export function EntityTableActionBar({ children }: OwnProps) {
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
|
||||
|
||||
return <ActionBar selectedIds={selectedRowIds}>{children}</ActionBar>;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
type?: 'standard' | 'warning';
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type StyledButtonProps = {
|
||||
type: 'standard' | 'warning';
|
||||
};
|
||||
|
||||
const StyledButton = styled.div<StyledButtonProps>`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${(props) =>
|
||||
props.type === 'warning'
|
||||
? props.theme.color.red
|
||||
: props.theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonLabel = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export function EntityTableActionBarButton({
|
||||
label,
|
||||
icon,
|
||||
type = 'standard',
|
||||
onClick,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledButton type={type} onClick={onClick}>
|
||||
{icon}
|
||||
<StyledButtonLabel>{label}</StyledButtonLabel>
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { IconNotes } from '@/ui/icon/index';
|
||||
|
||||
import { EntityTableActionBarButton } from './EntityTableActionBarButton';
|
||||
|
||||
type OwnProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export function TableActionBarButtonToggleComments({ onClick }: OwnProps) {
|
||||
return (
|
||||
<EntityTableActionBarButton
|
||||
label="Note"
|
||||
icon={<IconNotes size={16} />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
export function CellSkeleton() {
|
||||
return (
|
||||
<div style={{ width: '100%', alignItems: 'center' }}>
|
||||
<Skeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from '../hooks/useCurrentCellEditMode';
|
||||
import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell';
|
||||
|
||||
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
|
||||
|
||||
export const CellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCell({
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editHotkeyScope,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
|
||||
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
|
||||
|
||||
return (
|
||||
<CellBaseContainer>
|
||||
{isCurrentCellInEditMode ? (
|
||||
<EditableCellEditMode
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<EditableCellSoftFocusMode editHotkeyScope={editHotkeyScope}>
|
||||
{nonEditModeContent}
|
||||
</EditableCellSoftFocusMode>
|
||||
) : (
|
||||
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useSetSoftFocusOnCurrentCell } from '../hooks/useSetSoftFocusOnCurrentCell';
|
||||
|
||||
type Props = {
|
||||
softFocus?: boolean;
|
||||
};
|
||||
|
||||
export const EditableCellNormalModeOuterContainer = styled.div<Props>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
||||
${(props) =>
|
||||
props.softFocus
|
||||
? `background: ${props.theme.background.transparent.secondary};
|
||||
border-radius: ${props.theme.border.radius.md};
|
||||
box-shadow: inset 0 0 0 1px ${props.theme.font.color.extraLight};`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export const EditableCellNormalModeInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function EditableCellDisplayMode({
|
||||
children,
|
||||
}: React.PropsWithChildren<unknown>) {
|
||||
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
|
||||
|
||||
function handleOnClick() {
|
||||
setSoftFocusOnCurrentCell();
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellNormalModeOuterContainer onClick={handleOnClick}>
|
||||
<EditableCellNormalModeInnerContainer>
|
||||
{children}
|
||||
</EditableCellNormalModeInnerContainer>
|
||||
</EditableCellNormalModeOuterContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { ReactElement, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '@/ui/themes/effects';
|
||||
|
||||
import { useRegisterCloseCellHandlers } from '../hooks/useRegisterCloseCellHandlers';
|
||||
|
||||
export const EditableCellEditModeContainer = styled.div<OwnProps>`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
left: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
right: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||
|
||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
${overlayBackground}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
children: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
onOutsideClick?: () => void;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
children,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useRegisterCloseCellHandlers(wrapperRef, onSubmit, onCancel);
|
||||
|
||||
return (
|
||||
<EditableCellEditModeContainer
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{children}
|
||||
</EditableCellEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
import { isNonTextWritingKey } from '@/ui/hotkey/utils/isNonTextWritingKey';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
import {
|
||||
EditableCellNormalModeInnerContainer,
|
||||
EditableCellNormalModeOuterContainer,
|
||||
} from './EditableCellDisplayMode';
|
||||
|
||||
export function EditableCellSoftFocusMode({
|
||||
children,
|
||||
editHotkeyScope,
|
||||
}: React.PropsWithChildren<{ editHotkeyScope?: HotkeyScope }>) {
|
||||
const { openEditableCell } = useEditableCell();
|
||||
|
||||
function openEditMode() {
|
||||
openEditableCell(
|
||||
editHotkeyScope ?? {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
openEditMode();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'*',
|
||||
(keyboardEvent) => {
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
openEditMode();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openEditMode],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
openEditMode();
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellNormalModeOuterContainer
|
||||
onClick={handleClick}
|
||||
softFocus={true}
|
||||
>
|
||||
<EditableCellNormalModeInnerContainer>
|
||||
{children}
|
||||
</EditableCellNormalModeInnerContainer>
|
||||
</EditableCellNormalModeOuterContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const HoverableMenuItem = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import {
|
||||
CellPositionDecorator,
|
||||
ComponentDecorator,
|
||||
} from '~/testing/decorators';
|
||||
|
||||
import { EditableCellText } from '../../types/EditableCellText';
|
||||
|
||||
const meta: Meta<typeof EditableCellText> = {
|
||||
title: 'UI/EditableCell/EditableCellText',
|
||||
component: EditableCellText,
|
||||
decorators: [ComponentDecorator, CellPositionDecorator],
|
||||
args: {
|
||||
value: 'Content',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EditableCellText>;
|
||||
|
||||
export const DisplayMode: Story = {
|
||||
render: EditableCellText,
|
||||
};
|
||||
|
||||
export const SoftFocusMode: Story = {
|
||||
...DisplayMode,
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await step('Click once', () =>
|
||||
userEvent.click(canvas.getByText('Content')),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const EditMode: Story = {
|
||||
...DisplayMode,
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const click = async () => userEvent.click(canvas.getByText('Content'));
|
||||
|
||||
await step('Click once', click);
|
||||
await step('Click twice', click);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useMoveEditModeToCellPosition } from '../../hooks/useMoveEditModeToCellPosition';
|
||||
import { isCellInEditModeFamilyState } from '../../states/isCellInEditModeFamilyState';
|
||||
|
||||
import { useCurrentCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export function useCurrentCellEditMode() {
|
||||
const moveEditModeToCellPosition = useMoveEditModeToCellPosition();
|
||||
|
||||
const currentCellPosition = useCurrentCellPosition();
|
||||
|
||||
const [isCurrentCellInEditMode] = useRecoilState(
|
||||
isCellInEditModeFamilyState(currentCellPosition),
|
||||
);
|
||||
|
||||
const setCurrentCellInEditMode = useCallback(() => {
|
||||
moveEditModeToCellPosition(currentCellPosition);
|
||||
}, [currentCellPosition, moveEditModeToCellPosition]);
|
||||
|
||||
return { isCurrentCellInEditMode, setCurrentCellInEditMode };
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
|
||||
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
|
||||
import { RowContext } from '../../states/RowContext';
|
||||
import { CellPosition } from '../../types/CellPosition';
|
||||
|
||||
export function useCurrentCellPosition() {
|
||||
const [currentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
const [currentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
const currentCellPosition: CellPosition = useMemo(
|
||||
() => ({
|
||||
column: currentColumnNumber,
|
||||
row: currentRowNumber,
|
||||
}),
|
||||
[currentColumnNumber, currentRowNumber],
|
||||
);
|
||||
|
||||
return currentCellPosition;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode';
|
||||
import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||
|
||||
export function useEditableCell() {
|
||||
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
|
||||
|
||||
function closeEditableCell() {
|
||||
closeCurrentCellInEditMode();
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
}
|
||||
|
||||
const openEditableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(HotkeyScope: HotkeyScope) => {
|
||||
const isSomeInputInEditMode = snapshot
|
||||
.getLoadable(isSomeInputInEditModeState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!isSomeInputInEditMode) {
|
||||
set(isSomeInputInEditModeState, true);
|
||||
|
||||
setCurrentCellInEditMode();
|
||||
|
||||
setHotkeyScope(HotkeyScope.scope);
|
||||
}
|
||||
},
|
||||
[setCurrentCellInEditMode, setHotkeyScope],
|
||||
);
|
||||
|
||||
return {
|
||||
closeEditableCell,
|
||||
openEditableCell,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isSoftFocusOnCellFamilyState } from '../../states/isSoftFocusOnCellFamilyState';
|
||||
|
||||
import { useCurrentCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export function useIsSoftFocusOnCurrentCell() {
|
||||
const currentCellPosition = useCurrentCellPosition();
|
||||
|
||||
const isSoftFocusOnCell = useRecoilValue(
|
||||
isSoftFocusOnCellFamilyState(currentCellPosition),
|
||||
);
|
||||
|
||||
return isSoftFocusOnCell;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||
import { useEditableCell } from './useEditableCell';
|
||||
|
||||
export function useRegisterCloseCellHandlers(
|
||||
wrapperRef: React.RefObject<HTMLDivElement>,
|
||||
onSubmit?: () => void,
|
||||
onCancel?: () => void,
|
||||
) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
}
|
||||
});
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveDown();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveDown],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
closeEditableCell();
|
||||
onCancel?.();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onCancel],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'tab',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveRight();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveRight],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'shift+tab',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveLeft();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveRight],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useSetSoftFocusPosition } from '../../hooks/useSetSoftFocusPosition';
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
|
||||
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
|
||||
import { isSoftFocusActiveState } from '../../states/isSoftFocusActiveState';
|
||||
import { RowContext } from '../../states/RowContext';
|
||||
import { CellPosition } from '../../types/CellPosition';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
export function useSetSoftFocusOnCurrentCell() {
|
||||
const setSoftFocusPosition = useSetSoftFocusPosition();
|
||||
|
||||
const [currentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
const [currentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
const currentTablePosition: CellPosition = useMemo(
|
||||
() => ({
|
||||
column: currentColumnNumber,
|
||||
row: currentRowNumber,
|
||||
}),
|
||||
[currentColumnNumber, currentRowNumber],
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
setSoftFocusPosition(currentTablePosition);
|
||||
|
||||
set(isSoftFocusActiveState, true);
|
||||
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
},
|
||||
[setHotkeyScope, currentTablePosition, setSoftFocusPosition],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCreateModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isCreateModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
import { EditableCellDateEditMode } from './EditableCellDateEditMode';
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function EditableCellDate({
|
||||
value,
|
||||
onChange,
|
||||
editModeHorizontalAlign,
|
||||
}: EditableDateProps) {
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<EditableCellDateEditMode onChange={onChange} value={value} />
|
||||
}
|
||||
nonEditModeContent={<InplaceInputDateDisplayMode value={value} />}
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDateEditMode }}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
const EditableCellDateEditModeContainer = styled.div`
|
||||
margin-top: -1px;
|
||||
width: inherit;
|
||||
`;
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
};
|
||||
|
||||
export function EditableCellDateEditMode({
|
||||
value,
|
||||
onChange,
|
||||
}: EditableDateProps) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
|
||||
function handleDateChange(newDate: Date) {
|
||||
onChange(newDate);
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
closeEditableCell();
|
||||
},
|
||||
TableHotkeyScope.CellDateEditMode,
|
||||
[closeEditableCell],
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCellDateEditModeContainer>
|
||||
<InplaceInputDate onChange={handleDateChange} value={value} />
|
||||
</EditableCellDateEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
import { EditableCellDoubleTextEditMode } from './EditableCellDoubleTextEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
nonEditModeContent: ReactElement;
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function EditableCellDoubleText({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
nonEditModeContent,
|
||||
loading,
|
||||
}: OwnProps) {
|
||||
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
|
||||
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstInternalValue(firstValue);
|
||||
setSecondInternalValue(secondValue);
|
||||
}, [firstValue, secondValue]);
|
||||
|
||||
function handleOnChange(firstValue: string, secondValue: string): void {
|
||||
setFirstInternalValue(firstValue);
|
||||
setSecondInternalValue(secondValue);
|
||||
onChange(firstValue, secondValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }}
|
||||
editModeContent={
|
||||
<EditableCellDoubleTextEditMode
|
||||
firstValue={firstInternalValue}
|
||||
secondValue={secondInternalValue}
|
||||
firstValuePlaceholder={firstValuePlaceholder}
|
||||
secondValuePlaceholder={secondValuePlaceholder}
|
||||
onChange={handleOnChange}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={loading ? <CellSkeleton /> : nonEditModeContent}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > input:last-child {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
`;
|
||||
|
||||
export function EditableCellDoubleTextEditMode({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
|
||||
|
||||
const firstValueInputRef = useRef<HTMLInputElement>(null);
|
||||
const secondValueInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
function closeCell() {
|
||||
setFocusPosition('left');
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
closeCell();
|
||||
moveDown();
|
||||
onSubmit?.();
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
onCancel?.();
|
||||
closeCell();
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'tab',
|
||||
() => {
|
||||
if (focusPosition === 'left') {
|
||||
setFocusPosition('right');
|
||||
secondValueInputRef.current?.focus();
|
||||
} else {
|
||||
onSubmit?.();
|
||||
closeCell();
|
||||
moveRight();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell, moveRight, focusPosition],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'shift+tab',
|
||||
() => {
|
||||
if (focusPosition === 'right') {
|
||||
setFocusPosition('left');
|
||||
firstValueInputRef.current?.focus();
|
||||
} else {
|
||||
onSubmit?.();
|
||||
closeCell();
|
||||
moveLeft();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell, moveRight, focusPosition],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<InplaceInputTextEditMode
|
||||
autoFocus
|
||||
placeholder={firstValuePlaceholder}
|
||||
ref={firstValueInputRef}
|
||||
value={firstValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.value, secondValue);
|
||||
}}
|
||||
/>
|
||||
<InplaceInputTextEditMode
|
||||
placeholder={secondValuePlaceholder}
|
||||
ref={secondValueInputRef}
|
||||
value={secondValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(firstValue, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (updated: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellPhone({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
autoFocus
|
||||
placeholder={placeholder || ''}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<InplaceInputPhoneDisplayMode value={inputValue} />}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const EditableCellRelationCreateButton = styled.button`
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: 'Inter';
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: 31px;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
@ -0,0 +1,61 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
|
||||
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
loading?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellText({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
editModeHorizontalAlign,
|
||||
loading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
value={internalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
nonEditModeContent={
|
||||
loading ? (
|
||||
<CellSkeleton />
|
||||
) : (
|
||||
<InplaceInputTextDisplayMode>
|
||||
{internalValue}
|
||||
</InplaceInputTextDisplayMode>
|
||||
)
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
112
front/src/modules/ui/table/editable-cell/types/EditableChip.tsx
Normal file
112
front/src/modules/ui/table/editable-cell/types/EditableChip.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChangeEvent,
|
||||
ComponentType,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/themes/effects';
|
||||
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
export type EditableChipProps = {
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
picture: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<{
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
isOverlapped?: boolean;
|
||||
}>;
|
||||
commentThreadCount?: number;
|
||||
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
rightEndContents?: ReactNode[];
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const StyledInplaceInput = styled.input`
|
||||
width: 100%;
|
||||
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
const NoEditModeContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const RightContainer = styled.div`
|
||||
margin-left: ${(props) => props.theme.spacing(1)};
|
||||
`;
|
||||
|
||||
// TODO: move right end content in EditableCell
|
||||
export function EditableCellChip({
|
||||
id,
|
||||
value,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
picture,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
rightEndContents,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: EditableChipProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleRightEndContentClick = (
|
||||
event: React.MouseEvent<HTMLDivElement>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
nonEditModeContent={
|
||||
<NoEditModeContainer>
|
||||
<ChipComponent id={id} name={inputValue} picture={picture} />
|
||||
<RightContainer>
|
||||
{rightEndContents &&
|
||||
rightEndContents.length > 0 &&
|
||||
rightEndContents.map((content, index) => (
|
||||
<div key={index} onClick={handleRightEndContentClick}>
|
||||
{content}
|
||||
</div>
|
||||
))}
|
||||
</RightContainer>
|
||||
</NoEditModeContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
22
front/src/modules/ui/table/hooks/useClearCellInEditMode.ts
Normal file
22
front/src/modules/ui/table/hooks/useClearCellInEditMode.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
|
||||
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
|
||||
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
|
||||
|
||||
export function useCloseCurrentCellInEditMode() {
|
||||
return useRecoilCallback(({ set, snapshot }) => {
|
||||
return async () => {
|
||||
const currentCellInEditModePosition = await snapshot.getPromise(
|
||||
currentCellInEditModePositionState,
|
||||
);
|
||||
|
||||
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
|
||||
|
||||
// TODO: find a way to remove this
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
set(isSomeInputInEditModeState, false);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
18
front/src/modules/ui/table/hooks/useCurrentEntityId.ts
Normal file
18
front/src/modules/ui/table/hooks/useCurrentEntityId.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { currentRowEntityIdScopedState } from '../states/currentRowEntityIdScopedState';
|
||||
import { RowContext } from '../states/RowContext';
|
||||
|
||||
export type TableDimensions = {
|
||||
numberOfColumns: number;
|
||||
numberOfRows: number;
|
||||
};
|
||||
|
||||
export function useCurrentRowEntityId() {
|
||||
const currentRowEntityIdScoped = useRecoilScopedValue(
|
||||
currentRowEntityIdScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
return currentRowEntityIdScoped;
|
||||
}
|
||||
36
front/src/modules/ui/table/hooks/useCurrentRowSelected.ts
Normal file
36
front/src/modules/ui/table/hooks/useCurrentRowSelected.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
|
||||
import { useCurrentRowEntityId } from './useCurrentEntityId';
|
||||
|
||||
export function useCurrentRowSelected() {
|
||||
const currentRowId = useCurrentRowEntityId();
|
||||
|
||||
const [isRowSelected] = useRecoilState(
|
||||
isRowSelectedFamilyState(currentRowId ?? ''),
|
||||
);
|
||||
|
||||
const setCurrentRowSelected = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(newSelectedState: boolean) => {
|
||||
if (!currentRowId) return;
|
||||
|
||||
const isRowSelected = snapshot
|
||||
.getLoadable(isRowSelectedFamilyState(currentRowId))
|
||||
.valueOrThrow();
|
||||
|
||||
if (newSelectedState && !isRowSelected) {
|
||||
set(isRowSelectedFamilyState(currentRowId), true);
|
||||
} else if (!newSelectedState && isRowSelected) {
|
||||
set(isRowSelectedFamilyState(currentRowId), false);
|
||||
}
|
||||
},
|
||||
[currentRowId],
|
||||
);
|
||||
|
||||
return {
|
||||
currentRowSelected: isRowSelected,
|
||||
setCurrentRowSelected,
|
||||
};
|
||||
}
|
||||
19
front/src/modules/ui/table/hooks/useDisableSoftFocus.ts
Normal file
19
front/src/modules/ui/table/hooks/useDisableSoftFocus.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
|
||||
export function useDisableSoftFocus() {
|
||||
return useRecoilCallback(({ set, snapshot }) => {
|
||||
return () => {
|
||||
const currentPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(isSoftFocusActiveState, false);
|
||||
|
||||
set(isSoftFocusOnCellFamilyState(currentPosition), false);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
35
front/src/modules/ui/table/hooks/useInitializeEntityTable.ts
Normal file
35
front/src/modules/ui/table/hooks/useInitializeEntityTable.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
import { useResetTableRowSelection } from './useResetTableRowSelection';
|
||||
|
||||
export type TableDimensions = {
|
||||
numberOfColumns: number;
|
||||
numberOfRows: number;
|
||||
};
|
||||
|
||||
export function useInitializeEntityTable({
|
||||
numberOfColumns,
|
||||
}: {
|
||||
numberOfColumns: number;
|
||||
}) {
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
useEffect(() => {
|
||||
resetTableRowSelection();
|
||||
}, [resetTableRowSelection]);
|
||||
|
||||
const [, setTableDimensions] = useRecoilState(entityTableDimensionsState);
|
||||
|
||||
useEffect(() => {
|
||||
setTableDimensions({
|
||||
numberOfColumns,
|
||||
numberOfRows: tableRowIds?.length,
|
||||
});
|
||||
}, [tableRowIds, numberOfColumns, setTableDimensions]);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { TableContext } from '../states/TableContext';
|
||||
|
||||
export function useInitializeEntityTableFilters({
|
||||
availableFilters,
|
||||
}: {
|
||||
availableFilters: FilterDefinition[];
|
||||
}) {
|
||||
const [, setAvailableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
TableContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableFilters(availableFilters);
|
||||
}, [setAvailableFilters, availableFilters]);
|
||||
}
|
||||
53
front/src/modules/ui/table/hooks/useLeaveTableFocus.ts
Normal file
53
front/src/modules/ui/table/hooks/useLeaveTableFocus.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { useCloseCurrentCellInEditMode } from './useClearCellInEditMode';
|
||||
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||
|
||||
export function useLeaveTableFocus() {
|
||||
const disableSoftFocus = useDisableSoftFocus();
|
||||
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const isSoftFocusActive = snapshot
|
||||
.getLoadable(isSoftFocusActiveState)
|
||||
.valueOrThrow();
|
||||
|
||||
const isSomeInputInEditMode = snapshot
|
||||
.getLoadable(isSomeInputInEditModeState)
|
||||
.valueOrThrow();
|
||||
|
||||
const currentHotkeyScope = snapshot
|
||||
.getLoadable(currentHotkeyScopeState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (isSomeInputInEditMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSoftFocusActive && !isSomeInputInEditMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentHotkeyScope?.scope === TableHotkeyScope.Table) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeCurrentCellInEditMode();
|
||||
disableSoftFocus();
|
||||
|
||||
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
||||
},
|
||||
[setHotkeyScope, closeCurrentCellInEditMode, disableSoftFocus],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
|
||||
import { isSomeInputInEditModeState } from '../states/isSomeInputInEditModeState';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||
import { useMoveSoftFocus } from './useMoveSoftFocus';
|
||||
|
||||
export function useMapKeyboardToSoftFocus() {
|
||||
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
|
||||
|
||||
const disableSoftFocus = useDisableSoftFocus();
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const [isSomeInputInEditMode] = useRecoilState(isSomeInputInEditModeState);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
|
||||
() => {
|
||||
if (!isSomeInputInEditMode) {
|
||||
moveUp();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveUp, isSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowDown,
|
||||
() => {
|
||||
if (!isSomeInputInEditMode) {
|
||||
moveDown();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveDown, isSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
|
||||
() => {
|
||||
if (!isSomeInputInEditMode) {
|
||||
moveLeft();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveLeft, isSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowRight, Key.Tab],
|
||||
() => {
|
||||
if (!isSomeInputInEditMode) {
|
||||
moveRight();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveRight, isSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
||||
disableSoftFocus();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[disableSoftFocus],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentCellInEditModePositionState } from '../states/currentCellInEditModePositionState';
|
||||
import { isCellInEditModeFamilyState } from '../states/isCellInEditModeFamilyState';
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export function useMoveEditModeToCellPosition() {
|
||||
return useRecoilCallback(({ set, snapshot }) => {
|
||||
return (newPosition: CellPosition) => {
|
||||
const currentCellInEditModePosition = snapshot
|
||||
.getLoadable(currentCellInEditModePositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(isCellInEditModeFamilyState(currentCellInEditModePosition), false);
|
||||
|
||||
set(currentCellInEditModePositionState, newPosition);
|
||||
|
||||
set(isCellInEditModeFamilyState(newPosition), true);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
154
front/src/modules/ui/table/hooks/useMoveSoftFocus.ts
Normal file
154
front/src/modules/ui/table/hooks/useMoveSoftFocus.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState';
|
||||
import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
|
||||
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
|
||||
|
||||
// TODO: stories
|
||||
export function useMoveSoftFocus() {
|
||||
const setSoftFocusPosition = useSetSoftFocusPosition();
|
||||
|
||||
const moveUp = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const softFocusPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
let newRowNumber = softFocusPosition.row - 1;
|
||||
|
||||
if (newRowNumber < 0) {
|
||||
newRowNumber = 0;
|
||||
}
|
||||
|
||||
setSoftFocusPosition({
|
||||
...softFocusPosition,
|
||||
row: newRowNumber,
|
||||
});
|
||||
},
|
||||
[setSoftFocusPosition],
|
||||
);
|
||||
|
||||
const moveDown = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const softFocusPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableRows = snapshot
|
||||
.getLoadable(numberOfTableRowsSelectorState)
|
||||
.valueOrThrow();
|
||||
|
||||
let newRowNumber = softFocusPosition.row + 1;
|
||||
|
||||
if (newRowNumber >= numberOfTableRows) {
|
||||
newRowNumber = numberOfTableRows - 1;
|
||||
}
|
||||
|
||||
setSoftFocusPosition({
|
||||
...softFocusPosition,
|
||||
row: newRowNumber,
|
||||
});
|
||||
},
|
||||
[setSoftFocusPosition],
|
||||
);
|
||||
|
||||
const moveRight = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const softFocusPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableColumns = snapshot
|
||||
.getLoadable(numberOfTableColumnsSelectorState)
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableRows = snapshot
|
||||
.getLoadable(numberOfTableRowsSelectorState)
|
||||
.valueOrThrow();
|
||||
|
||||
const currentColumnNumber = softFocusPosition.column;
|
||||
const currentRowNumber = softFocusPosition.row;
|
||||
|
||||
const isLastRowAndLastColumn =
|
||||
currentColumnNumber === numberOfTableColumns - 1 &&
|
||||
currentRowNumber === numberOfTableRows - 1;
|
||||
|
||||
const isLastColumnButNotLastRow =
|
||||
currentColumnNumber === numberOfTableColumns - 1 &&
|
||||
currentRowNumber !== numberOfTableRows - 1;
|
||||
|
||||
const isNotLastColumn =
|
||||
currentColumnNumber !== numberOfTableColumns - 1;
|
||||
|
||||
if (isLastRowAndLastColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNotLastColumn) {
|
||||
setSoftFocusPosition({
|
||||
row: currentRowNumber,
|
||||
column: currentColumnNumber + 1,
|
||||
});
|
||||
} else if (isLastColumnButNotLastRow) {
|
||||
setSoftFocusPosition({
|
||||
row: currentRowNumber + 1,
|
||||
column: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setSoftFocusPosition],
|
||||
);
|
||||
|
||||
const moveLeft = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const softFocusPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableColumns = snapshot
|
||||
.getLoadable(numberOfTableColumnsSelectorState)
|
||||
.valueOrThrow();
|
||||
|
||||
const currentColumnNumber = softFocusPosition.column;
|
||||
const currentRowNumber = softFocusPosition.row;
|
||||
|
||||
const isFirstRowAndFirstColumn =
|
||||
currentColumnNumber === 0 && currentRowNumber === 0;
|
||||
|
||||
const isFirstColumnButNotFirstRow =
|
||||
currentColumnNumber === 0 && currentRowNumber > 0;
|
||||
|
||||
const isNotFirstColumn = currentColumnNumber > 0;
|
||||
|
||||
if (isFirstRowAndFirstColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNotFirstColumn) {
|
||||
setSoftFocusPosition({
|
||||
row: currentRowNumber,
|
||||
column: currentColumnNumber - 1,
|
||||
});
|
||||
} else if (isFirstColumnButNotFirstRow) {
|
||||
setSoftFocusPosition({
|
||||
row: currentRowNumber - 1,
|
||||
column: numberOfTableColumns - 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setSoftFocusPosition],
|
||||
);
|
||||
|
||||
return {
|
||||
moveDown,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export function useResetTableRowSelection() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const tableRowIds = snapshot
|
||||
.getLoadable(tableRowIdsState)
|
||||
.valueOrThrow();
|
||||
|
||||
for (const rowId of tableRowIds) {
|
||||
set(isRowSelectedFamilyState(rowId), false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
41
front/src/modules/ui/table/hooks/useSelectAllRows.ts
Normal file
41
front/src/modules/ui/table/hooks/useSelectAllRows.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { allRowsSelectedStatusSelector } from '../states/allRowsSelectedStatusSelector';
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export function useSelectAllRows() {
|
||||
const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector);
|
||||
|
||||
const selectAllRows = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const allRowsSelectedStatus = snapshot
|
||||
.getLoadable(allRowsSelectedStatusSelector)
|
||||
.valueOrThrow();
|
||||
|
||||
const tableRowIds = snapshot
|
||||
.getLoadable(tableRowIdsState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (
|
||||
allRowsSelectedStatus === 'none' ||
|
||||
allRowsSelectedStatus === 'some'
|
||||
) {
|
||||
for (const rowId of tableRowIds) {
|
||||
set(isRowSelectedFamilyState(rowId), true);
|
||||
}
|
||||
} else {
|
||||
for (const rowId of tableRowIds) {
|
||||
set(isRowSelectedFamilyState(rowId), false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
allRowsSelectedStatus,
|
||||
selectAllRows,
|
||||
};
|
||||
}
|
||||
24
front/src/modules/ui/table/hooks/useSetSoftFocusPosition.ts
Normal file
24
front/src/modules/ui/table/hooks/useSetSoftFocusPosition.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { isSoftFocusOnCellFamilyState } from '../states/isSoftFocusOnCellFamilyState';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export function useSetSoftFocusPosition() {
|
||||
return useRecoilCallback(({ set, snapshot }) => {
|
||||
return (newPosition: CellPosition) => {
|
||||
const currentPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(isSoftFocusActiveState, true);
|
||||
|
||||
set(isSoftFocusOnCellFamilyState(currentPosition), false);
|
||||
|
||||
set(softFocusPositionState, newPosition);
|
||||
|
||||
set(isSoftFocusOnCellFamilyState(newPosition), true);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
3
front/src/modules/ui/table/states/CellContext.ts
Normal file
3
front/src/modules/ui/table/states/CellContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const CellContext = createContext<string | null>(null);
|
||||
3
front/src/modules/ui/table/states/RowContext.ts
Normal file
3
front/src/modules/ui/table/states/RowContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RowContext = createContext<string | null>(null);
|
||||
3
front/src/modules/ui/table/states/TableContext.ts
Normal file
3
front/src/modules/ui/table/states/TableContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const TableContext = createContext<string | null>(null);
|
||||
@ -0,0 +1,26 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { AllRowsSelectedStatus } from '../types/AllRowSelectedStatus';
|
||||
|
||||
import { numberOfTableRowsSelectorState } from './numberOfTableRowsSelectorState';
|
||||
import { selectedRowIdsSelector } from './selectedRowIdsSelector';
|
||||
|
||||
export const allRowsSelectedStatusSelector = selector<AllRowsSelectedStatus>({
|
||||
key: 'allRowsSelectedStatusSelector',
|
||||
get: ({ get }) => {
|
||||
const numberOfRows = get(numberOfTableRowsSelectorState);
|
||||
|
||||
const selectedRowIds = get(selectedRowIdsSelector);
|
||||
|
||||
const numberOfSelectedRows = selectedRowIds.length;
|
||||
|
||||
const allRowsSelectedStatus =
|
||||
numberOfSelectedRows === 0
|
||||
? 'none'
|
||||
: numberOfRows === numberOfSelectedRows
|
||||
? 'all'
|
||||
: 'some';
|
||||
|
||||
return allRowsSelectedStatus;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { PositionType } from '@/ui/action-bar/types/PositionType';
|
||||
|
||||
export const contextMenuPositionState = atom<PositionType>({
|
||||
key: 'contextMenuPositionState',
|
||||
default: {
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export const currentCellInEditModePositionState = atom<CellPosition>({
|
||||
key: 'currentCellInEditModePositionState',
|
||||
default: {
|
||||
row: 0,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentColumnNumberScopedState = atomFamily<number, string>({
|
||||
key: 'currentColumnNumberScopedState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentRowEntityIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'currentRowEntityIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentRowNumberScopedState = atomFamily<number, string>({
|
||||
key: 'currentRowNumberScopedState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { TableDimensions } from '../hooks/useInitializeEntityTable';
|
||||
|
||||
export const entityTableDimensionsState = atom<TableDimensions>({
|
||||
key: 'entityTableDimensionsState',
|
||||
default: {
|
||||
numberOfRows: 0,
|
||||
numberOfColumns: 0,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export const isCellInEditModeFamilyState = atomFamily<boolean, CellPosition>({
|
||||
key: 'isCellInEditModeFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isFetchingEntityTableDataState = atom<boolean>({
|
||||
key: 'isFetchingEntityTableDataState',
|
||||
default: true,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isRowSelectedFamilyState = atomFamily<boolean, string>({
|
||||
key: 'isRowSelectedFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isSoftFocusActiveState = atom<boolean>({
|
||||
key: 'isSoftFocusActiveState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export const isSoftFocusOnCellFamilyState = atomFamily<boolean, CellPosition>({
|
||||
key: 'isSoftFocusOnCellFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isSomeInputInEditModeState = atom<boolean>({
|
||||
key: 'isSomeInputInEditModeState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { entityTableDimensionsState } from './entityTableDimensionsState';
|
||||
|
||||
export const numberOfTableColumnsSelectorState = selector<number>({
|
||||
key: 'numberOfTableColumnsState',
|
||||
get: ({ get }) => {
|
||||
const { numberOfColumns } = get(entityTableDimensionsState);
|
||||
|
||||
return numberOfColumns;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { entityTableDimensionsState } from './entityTableDimensionsState';
|
||||
|
||||
export const numberOfTableRowsSelectorState = selector<number>({
|
||||
key: 'numberOfTableRowsState',
|
||||
get: ({ get }) => {
|
||||
const { numberOfRows } = get(entityTableDimensionsState);
|
||||
|
||||
return numberOfRows;
|
||||
},
|
||||
});
|
||||
15
front/src/modules/ui/table/states/selectedRowIdsSelector.ts
Normal file
15
front/src/modules/ui/table/states/selectedRowIdsSelector.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from './isRowSelectedFamilyState';
|
||||
import { tableRowIdsState } from './tableRowIdsState';
|
||||
|
||||
export const selectedRowIdsSelector = selector<string[]>({
|
||||
key: 'selectedRowIdsSelector',
|
||||
get: ({ get }) => {
|
||||
const rowIds = get(tableRowIdsState);
|
||||
|
||||
return rowIds.filter(
|
||||
(rowId) => get(isRowSelectedFamilyState(rowId)) === true,
|
||||
);
|
||||
},
|
||||
});
|
||||
11
front/src/modules/ui/table/states/softFocusPositionState.ts
Normal file
11
front/src/modules/ui/table/states/softFocusPositionState.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { CellPosition } from '../types/CellPosition';
|
||||
|
||||
export const softFocusPositionState = atom<CellPosition>({
|
||||
key: 'softFocusPositionState',
|
||||
default: {
|
||||
row: 0,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
6
front/src/modules/ui/table/states/tableRowIdsState.ts
Normal file
6
front/src/modules/ui/table/states/tableRowIdsState.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const tableRowIdsState = atom<string[]>({
|
||||
key: 'tableRowIdsState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,131 @@
|
||||
import { ReactNode, useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
|
||||
import { TableContext } from '../../states/TableContext';
|
||||
|
||||
type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
viewIcon?: ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTableHeader = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
& > svg {
|
||||
font-size: ${({ theme }) => theme.icon.size.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledViewSection = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledFilters = styled.div`
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
export function TableHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onSortsUpdate,
|
||||
}: OwnProps<SortField>) {
|
||||
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
|
||||
[],
|
||||
);
|
||||
|
||||
const sortSelect = useCallback(
|
||||
(newSort: SelectedSortType<SortField>) => {
|
||||
const newSorts = updateSortOrFilterByKey(sorts, newSort);
|
||||
innerSetSorts(newSorts);
|
||||
onSortsUpdate && onSortsUpdate(newSorts);
|
||||
},
|
||||
[onSortsUpdate, sorts],
|
||||
);
|
||||
|
||||
const sortUnselect = useCallback(
|
||||
(sortKey: string) => {
|
||||
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
|
||||
innerSetSorts(newSorts);
|
||||
onSortsUpdate && onSortsUpdate(newSorts);
|
||||
},
|
||||
[onSortsUpdate, sorts],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTableHeader>
|
||||
<StyledViewSection>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</StyledViewSection>
|
||||
<StyledFilters>
|
||||
<FilterDropdownButton
|
||||
context={TableContext}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
</StyledFilters>
|
||||
</StyledTableHeader>
|
||||
<SortAndFilterBar
|
||||
context={TableContext}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function updateSortOrFilterByKey<SortOrFilter extends { key: string }>(
|
||||
sorts: Readonly<SortOrFilter[]>,
|
||||
newSort: SortOrFilter,
|
||||
): SortOrFilter[] {
|
||||
const newSorts = [...sorts];
|
||||
const existingSortIndex = sorts.findIndex((sort) => sort.key === newSort.key);
|
||||
|
||||
if (existingSortIndex !== -1) {
|
||||
newSorts[existingSortIndex] = newSort;
|
||||
} else {
|
||||
newSorts.push(newSort);
|
||||
}
|
||||
|
||||
return newSorts;
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { IconList } from '@/ui/icon/index';
|
||||
import { availableSorts } from '~/pages/companies/companies-sorts';
|
||||
import { getRenderWrapperForEntityTableComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { TableHeader } from '../TableHeader';
|
||||
|
||||
const meta: Meta<typeof TableHeader> = {
|
||||
title: 'UI/Table/TableHeader',
|
||||
component: TableHeader,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TableHeader>;
|
||||
|
||||
export const Empty: Story = {
|
||||
render: getRenderWrapperForEntityTableComponent(
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
|
||||
export const WithSortsAndFilters: Story = {
|
||||
render: getRenderWrapperForEntityTableComponent(
|
||||
<TableHeader
|
||||
viewName="ViewName"
|
||||
viewIcon={<IconList />}
|
||||
availableSorts={availableSorts}
|
||||
/>,
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const outsideClick = await canvas.findByText('ViewName');
|
||||
|
||||
userEvent.click(await canvas.findByText('Filter'));
|
||||
userEvent.click(await canvas.findByText('Name'));
|
||||
const nameInput = await canvas.findByPlaceholderText('Name');
|
||||
userEvent.type(nameInput, 'My name');
|
||||
userEvent.click(outsideClick);
|
||||
|
||||
userEvent.click(await canvas.findByText('Sort'));
|
||||
userEvent.click(await canvas.findByText('Name'));
|
||||
|
||||
userEvent.click(await canvas.findByText('Sort'));
|
||||
userEvent.click(await canvas.findByText('Creation'));
|
||||
|
||||
userEvent.click(await canvas.findByText('Sort'));
|
||||
userEvent.click(await canvas.findByText('Address'));
|
||||
|
||||
userEvent.click(await canvas.findByText('Filter'));
|
||||
userEvent.click(await canvas.findByText('Employees'));
|
||||
const employeesInput = await canvas.findByPlaceholderText('Employees');
|
||||
userEvent.type(employeesInput, '12');
|
||||
|
||||
userEvent.click(await canvas.findByText('Sort'));
|
||||
userEvent.click(await canvas.findByText('Url'));
|
||||
|
||||
userEvent.click(await canvas.findByText('Filter'));
|
||||
userEvent.click(await canvas.findByText('Created at'));
|
||||
userEvent.click(await canvas.findByText('6'));
|
||||
userEvent.click(outsideClick);
|
||||
},
|
||||
};
|
||||
1
front/src/modules/ui/table/types/AllRowSelectedStatus.ts
Normal file
1
front/src/modules/ui/table/types/AllRowSelectedStatus.ts
Normal file
@ -0,0 +1 @@
|
||||
export type AllRowsSelectedStatus = 'none' | 'some' | 'all';
|
||||
4
front/src/modules/ui/table/types/CellPosition.ts
Normal file
4
front/src/modules/ui/table/types/CellPosition.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type CellPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
4
front/src/modules/ui/table/types/TableDimensions.ts
Normal file
4
front/src/modules/ui/table/types/TableDimensions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type TablePosition = {
|
||||
numberOfRows: number;
|
||||
numberOfColumns: number;
|
||||
};
|
||||
7
front/src/modules/ui/table/types/TableHotkeyScope.ts
Normal file
7
front/src/modules/ui/table/types/TableHotkeyScope.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum TableHotkeyScope {
|
||||
CellDoubleTextInput = 'cell-double-text-input',
|
||||
CellEditMode = 'cell-edit-mode',
|
||||
CellDateEditMode = 'cell-date-edit-mode',
|
||||
TableSoftFocus = 'table-soft-focus',
|
||||
Table = 'table',
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { CellPosition } from '../CellPosition';
|
||||
|
||||
export function isTablePosition(value: any): value is CellPosition {
|
||||
return (
|
||||
value && typeof value.row === 'number' && typeof value.column === 'number'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user