2282 Rename components to use the new naming convention part 1 (#2293)
renaming in progress
This commit is contained in:
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
|
||||
|
||||
import { selectedRowIdsSelector } from '../../states/selectors/selectedRowIdsSelector';
|
||||
|
||||
export const RecordTableActionBar = () => {
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
|
||||
|
||||
return <ActionBar selectedIds={selectedRowIds} />;
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { useCallback } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
||||
|
||||
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CheckboxCell = () => {
|
||||
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
||||
const { currentRowSelected, setCurrentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setCurrentRowSelected(!currentRowSelected);
|
||||
setActionBarOpenState(true);
|
||||
}, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
|
||||
|
||||
return (
|
||||
<StyledContainer onClick={handleClick}>
|
||||
<Checkbox checked={currentRowSelected} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
type ColumnHeadProps = {
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
};
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
|
||||
& > svg {
|
||||
height: ${({ theme }) => theme.icon.size.md}px;
|
||||
width: ${({ theme }) => theme.icon.size.md}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledText = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ColumnHead = ({ column }: ColumnHeadProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTitle>
|
||||
<StyledIcon>
|
||||
{column.Icon && <column.Icon size={theme.icon.size.md} />}
|
||||
</StyledIcon>
|
||||
<StyledText>{column.label}</StyledText>
|
||||
</StyledTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
|
||||
import { FieldMetadata } from '../../field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu';
|
||||
|
||||
type ColumnHeadWithDropdownProps = {
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
primaryColumnKey: string;
|
||||
};
|
||||
|
||||
export const ColumnHeadWithDropdown = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
primaryColumnKey,
|
||||
}: ColumnHeadWithDropdownProps) => {
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={column.fieldId + '-header'}>
|
||||
<Dropdown
|
||||
clickableComponent={<ColumnHead column={column} />}
|
||||
dropdownComponents={
|
||||
<RecordTableColumnDropdownMenu
|
||||
column={column}
|
||||
isFirstColumn={isFirstColumn}
|
||||
isLastColumn={isLastColumn}
|
||||
primaryColumnKey={primaryColumnKey}
|
||||
/>
|
||||
}
|
||||
dropdownOffset={{ x: -1 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownHotkeyScope={{ scope: column.fieldId + '-header' }}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,143 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import {
|
||||
useListenClickOutside,
|
||||
useListenClickOutsideByClassName,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
|
||||
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
|
||||
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { RecordTableBody } from './RecordTableBody';
|
||||
import { RecordTableHeader } from './RecordTableHeader';
|
||||
|
||||
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.border.color.light};
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableWithHeader = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type RecordTableProps = {
|
||||
updateEntityMutation: (params: any) => void;
|
||||
};
|
||||
|
||||
export const RecordTable = ({ updateEntityMutation }: RecordTableProps) => {
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setRowSelectedState = useSetRowSelectedState();
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
|
||||
useMapKeyboardToSoftFocus();
|
||||
|
||||
const leaveTableFocus = useLeaveTableFocus();
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [tableBodyRef],
|
||||
callback: () => {
|
||||
leaveTableFocus();
|
||||
},
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
'escape',
|
||||
() => {
|
||||
resetTableRowSelection();
|
||||
},
|
||||
TableHotkeyScope.Table,
|
||||
);
|
||||
|
||||
useListenClickOutsideByClassName({
|
||||
classNames: ['entity-table-cell'],
|
||||
excludeClassNames: ['action-bar', 'context-menu'],
|
||||
callback: () => {
|
||||
resetTableRowSelection();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EntityUpdateMutationContext.Provider value={updateEntityMutation}>
|
||||
<StyledTableWithHeader>
|
||||
<StyledTableContainer>
|
||||
<ScrollWrapper>
|
||||
<div ref={tableBodyRef}>
|
||||
<StyledTable className="entity-table-cell">
|
||||
<RecordTableHeader />
|
||||
<RecordTableBody />
|
||||
</StyledTable>
|
||||
<DragSelect
|
||||
dragSelectable={tableBodyRef}
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionChange={setRowSelectedState}
|
||||
/>
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
</StyledTableContainer>
|
||||
</StyledTableWithHeader>
|
||||
</EntityUpdateMutationContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useVirtual } from '@tanstack/react-virtual';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
||||
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
|
||||
|
||||
import { RowIdContext } from '../contexts/RowIdContext';
|
||||
import { RowIndexContext } from '../contexts/RowIndexContext';
|
||||
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
import { RecordTableRow } from './RecordTableRow';
|
||||
|
||||
type SpaceProps = {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
};
|
||||
|
||||
const StyledSpace = styled.td<SpaceProps>`
|
||||
${({ top }) => top && `padding-top: ${top}px;`}
|
||||
${({ bottom }) => bottom && `padding-bottom: ${bottom}px;`}
|
||||
`;
|
||||
|
||||
export const RecordTableBody = () => {
|
||||
const scrollWrapperRef = useScrollWrapperScopedRef();
|
||||
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
|
||||
const isFetchingRecordTableData = useRecoilValue(
|
||||
isFetchingRecordTableDataState,
|
||||
);
|
||||
|
||||
const rowVirtualizer = useVirtual({
|
||||
size: tableRowIds.length,
|
||||
parentRef: scrollWrapperRef,
|
||||
overscan: 50,
|
||||
});
|
||||
|
||||
const items = rowVirtualizer.virtualItems;
|
||||
const paddingTop = items.length > 0 ? items[0].start : 0;
|
||||
const paddingBottom =
|
||||
items.length > 0
|
||||
? rowVirtualizer.totalSize - items[items.length - 1].end
|
||||
: 0;
|
||||
|
||||
if (isFetchingRecordTableData || isNavbarSwitchingSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{paddingTop > 0 && (
|
||||
<tr>
|
||||
<StyledSpace top={paddingTop} />
|
||||
</tr>
|
||||
)}
|
||||
{items.map((virtualItem) => {
|
||||
const rowId = tableRowIds[virtualItem.index];
|
||||
|
||||
return (
|
||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||
<RowIndexContext.Provider value={virtualItem.index}>
|
||||
<RecordTableRow
|
||||
key={virtualItem.index}
|
||||
ref={virtualItem.measureRef}
|
||||
rowId={rowId}
|
||||
/>
|
||||
</RowIndexContext.Provider>
|
||||
</RowIdContext.Provider>
|
||||
);
|
||||
})}
|
||||
{paddingBottom > 0 && (
|
||||
<tr>
|
||||
<StyledSpace bottom={paddingBottom} />
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
||||
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { FieldContext } from '../../field/contexts/FieldContext';
|
||||
import { isFieldRelation } from '../../field/types/guards/isFieldRelation';
|
||||
import { ColumnContext } from '../contexts/ColumnContext';
|
||||
import { ColumnIndexContext } from '../contexts/ColumnIndexContext';
|
||||
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
|
||||
import { RowIdContext } from '../contexts/RowIdContext';
|
||||
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
|
||||
import { TableCell } from '../table-cell/components/TableCell';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
export const RecordTableCell = ({ cellIndex }: { cellIndex: number }) => {
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
||||
const currentRowId = useContext(RowIdContext);
|
||||
|
||||
const { setCurrentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setCurrentRowSelected(true);
|
||||
setContextMenuPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
setContextMenuOpenState(true);
|
||||
};
|
||||
|
||||
const columnDefinition = useContext(ColumnContext);
|
||||
|
||||
const updateEntityMutation = useContext(EntityUpdateMutationContext);
|
||||
|
||||
if (!columnDefinition || !currentRowId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const customHotkeyScope = isFieldRelation(columnDefinition)
|
||||
? RelationPickerHotkeyScope.RelationPicker
|
||||
: TableHotkeyScope.CellEditMode;
|
||||
|
||||
return (
|
||||
<RecoilScope>
|
||||
<ColumnIndexContext.Provider value={cellIndex}>
|
||||
<td onContextMenu={(event) => handleContextMenu(event)}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recoilScopeId: currentRowId + columnDefinition.label,
|
||||
entityId: currentRowId,
|
||||
fieldDefinition: columnDefinition,
|
||||
useUpdateEntityMutation: () => [updateEntityMutation, {}],
|
||||
hotkeyScope: customHotkeyScope,
|
||||
}}
|
||||
>
|
||||
<TableCell customHotkeyScope={{ scope: customHotkeyScope }} />
|
||||
</FieldContext.Provider>
|
||||
</td>
|
||||
</ColumnIndexContext.Provider>
|
||||
</RecoilScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { IconArrowLeft, IconArrowRight, IconEyeOff } from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { useTableColumns } from '../hooks/useTableColumns';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export type RecordTableColumnDropdownMenuProps = {
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
primaryColumnKey: string;
|
||||
};
|
||||
|
||||
export const RecordTableColumnDropdownMenu = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
primaryColumnKey,
|
||||
}: RecordTableColumnDropdownMenuProps) => {
|
||||
const { handleColumnVisibilityChange, handleMoveTableColumn } =
|
||||
useTableColumns();
|
||||
|
||||
const { closeDropdown } = useDropdown();
|
||||
|
||||
const handleColumnMoveLeft = () => {
|
||||
closeDropdown();
|
||||
if (isFirstColumn) {
|
||||
return;
|
||||
}
|
||||
handleMoveTableColumn('left', column);
|
||||
};
|
||||
|
||||
const handleColumnMoveRight = () => {
|
||||
closeDropdown();
|
||||
if (isLastColumn) {
|
||||
return;
|
||||
}
|
||||
handleMoveTableColumn('right', column);
|
||||
};
|
||||
|
||||
const handleColumnVisibility = () => {
|
||||
closeDropdown();
|
||||
handleColumnVisibilityChange(column);
|
||||
};
|
||||
|
||||
return column.fieldId === primaryColumnKey ? (
|
||||
<></>
|
||||
) : (
|
||||
<DropdownMenuItemsContainer>
|
||||
{!isFirstColumn && (
|
||||
<MenuItem
|
||||
LeftIcon={IconArrowLeft}
|
||||
onClick={handleColumnMoveLeft}
|
||||
text="Move left"
|
||||
/>
|
||||
)}
|
||||
{!isLastColumn && (
|
||||
<MenuItem
|
||||
LeftIcon={IconArrowRight}
|
||||
onClick={handleColumnMoveRight}
|
||||
text="Move right"
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
LeftIcon={IconEyeOff}
|
||||
onClick={handleColumnVisibility}
|
||||
text="Hide"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,77 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import defaults from 'lodash/defaults';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
SortOrder,
|
||||
useGetCompaniesQuery,
|
||||
useGetPeopleQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { FilterDefinition } from '../../filter/types/FilterDefinition';
|
||||
import { SortDefinition } from '../../sort/types/SortDefinition';
|
||||
import { useSetRecordTableData } from '../hooks/useSetRecordTableData';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { tablefiltersWhereScopedSelector } from '../states/selectors/tablefiltersWhereScopedSelector';
|
||||
import { tableSortsOrderByScopedSelector } from '../states/selectors/tableSortsOrderByScopedSelector';
|
||||
|
||||
export const RecordTableEffect = ({
|
||||
useGetRequest,
|
||||
getRequestResultKey,
|
||||
getRequestOptimisticEffectDefinition,
|
||||
|
||||
setActionBarEntries,
|
||||
setContextMenuEntries,
|
||||
}: {
|
||||
useGetRequest: typeof useGetCompaniesQuery | typeof useGetPeopleQuery;
|
||||
getRequestResultKey: string;
|
||||
getRequestOptimisticEffectDefinition: OptimisticEffectDefinition<any>;
|
||||
|
||||
filterDefinitionArray: FilterDefinition[];
|
||||
sortDefinitionArray: SortDefinition[];
|
||||
setActionBarEntries?: () => void;
|
||||
setContextMenuEntries?: () => void;
|
||||
}) => {
|
||||
const setRecordTableData = useSetRecordTableData();
|
||||
const { registerOptimisticEffect } = useOptimisticEffect();
|
||||
|
||||
const tableSortsOrderBy = useRecoilScopedValue(
|
||||
tableSortsOrderByScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const sortsOrderBy = defaults(tableSortsOrderBy, [
|
||||
{
|
||||
createdAt: SortOrder.Desc,
|
||||
},
|
||||
]);
|
||||
const tablefiltersWhere = useRecoilScopedValue(
|
||||
tablefiltersWhereScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
useGetRequest({
|
||||
variables: { orderBy: sortsOrderBy, where: tablefiltersWhere },
|
||||
onCompleted: (data: any) => {
|
||||
const entities = data[getRequestResultKey] ?? [];
|
||||
|
||||
setRecordTableData(entities);
|
||||
|
||||
registerOptimisticEffect({
|
||||
variables: { orderBy: sortsOrderBy, where: tablefiltersWhere },
|
||||
definition: getRequestOptimisticEffectDefinition,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
setActionBarEntries?.();
|
||||
setContextMenuEntries?.();
|
||||
}, [searchParams, setActionBarEntries, setContextMenuEntries]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,228 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { useTableColumns } from '../hooks/useTableColumns';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
|
||||
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
|
||||
import { tableColumnsByKeyScopedSelector } from '../states/selectors/tableColumnsByKeyScopedSelector';
|
||||
import { visibleTableColumnsScopedSelector } from '../states/selectors/visibleTableColumnsScopedSelector';
|
||||
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
|
||||
|
||||
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
|
||||
import { RecordTableHeaderPlusButtonContent } from './RecordTableHeaderPlusButtonContent';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 104;
|
||||
|
||||
const StyledColumnHeaderCell = styled.th<{
|
||||
columnWidth: number;
|
||||
isResizing?: boolean;
|
||||
}>`
|
||||
${({ columnWidth }) => `
|
||||
min-width: ${columnWidth}px;
|
||||
width: ${columnWidth}px;
|
||||
`}
|
||||
position: relative;
|
||||
user-select: none;
|
||||
${({ isResizing, theme }) => {
|
||||
if (isResizing) {
|
||||
return `&:after {
|
||||
background-color: ${theme.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}`;
|
||||
}
|
||||
}};
|
||||
`;
|
||||
|
||||
const StyledResizeHandler = styled.div`
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
position: absolute;
|
||||
right: -9px;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledTableHead = styled.thead`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledColumnHeadContainer = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const HIDDEN_TABLE_COLUMN_DROPDOWN_SCOPE_ID =
|
||||
'hidden-table-columns-dropdown-scope-id';
|
||||
|
||||
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
|
||||
'hidden-table-columns-dropdown-hotkey-scope-id';
|
||||
|
||||
export const RecordTableHeader = () => {
|
||||
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
|
||||
resizeFieldOffsetState,
|
||||
);
|
||||
const tableColumns = useRecoilScopedValue(
|
||||
tableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const tableColumnsByKey = useRecoilScopedValue(
|
||||
tableColumnsByKeyScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const hiddenTableColumns = useRecoilScopedValue(
|
||||
hiddenTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const visibleTableColumns = useRecoilScopedValue(
|
||||
visibleTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
|
||||
|
||||
const { handleColumnsChange } = useTableColumns();
|
||||
|
||||
const handleResizeHandlerStart = useCallback((positionX: number) => {
|
||||
setInitialPointerPositionX(positionX);
|
||||
}, []);
|
||||
|
||||
const handleResizeHandlerMove = useCallback(
|
||||
(positionX: number) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
setResizeFieldOffset(positionX - initialPointerPositionX);
|
||||
},
|
||||
[setResizeFieldOffset, initialPointerPositionX],
|
||||
);
|
||||
|
||||
const handleResizeHandlerEnd = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
if (!resizedFieldKey) return;
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(
|
||||
tableColumnsByKey[resizedFieldKey].size +
|
||||
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
);
|
||||
|
||||
set(resizeFieldOffsetState, 0);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldKey(null);
|
||||
|
||||
if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) {
|
||||
const nextColumns = tableColumns.map((column) =>
|
||||
column.fieldId === resizedFieldKey
|
||||
? { ...column, size: nextWidth }
|
||||
: column,
|
||||
);
|
||||
|
||||
await handleColumnsChange(nextColumns);
|
||||
}
|
||||
},
|
||||
[resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
shouldTrackPointer: resizedFieldKey !== null,
|
||||
onMouseDown: handleResizeHandlerStart,
|
||||
onMouseMove: handleResizeHandlerMove,
|
||||
onMouseUp: handleResizeHandlerEnd,
|
||||
});
|
||||
|
||||
const primaryColumn = visibleTableColumns.find(
|
||||
(column) => column.position === 0,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTableHead data-select-disable>
|
||||
<tr>
|
||||
<th
|
||||
style={{
|
||||
width: 30,
|
||||
minWidth: 30,
|
||||
maxWidth: 30,
|
||||
}}
|
||||
>
|
||||
<SelectAllCheckbox />
|
||||
</th>
|
||||
{visibleTableColumns.map((column) => (
|
||||
<StyledColumnHeaderCell
|
||||
key={column.fieldId}
|
||||
isResizing={resizedFieldKey === column.fieldId}
|
||||
columnWidth={Math.max(
|
||||
tableColumnsByKey[column.fieldId].size +
|
||||
(resizedFieldKey === column.fieldId ? resizeFieldOffset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
)}
|
||||
>
|
||||
<StyledColumnHeadContainer>
|
||||
<ColumnHeadWithDropdown
|
||||
column={column}
|
||||
isFirstColumn={column.position === 1}
|
||||
isLastColumn={
|
||||
column.position === visibleTableColumns.length - 1
|
||||
}
|
||||
primaryColumnKey={primaryColumn?.fieldId || ''}
|
||||
/>
|
||||
</StyledColumnHeadContainer>
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldKey(column.fieldId);
|
||||
}}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
))}
|
||||
<th>
|
||||
{hiddenTableColumns.length > 0 && (
|
||||
<StyledColumnHeadContainer>
|
||||
<DropdownScope
|
||||
dropdownScopeId={HIDDEN_TABLE_COLUMN_DROPDOWN_SCOPE_ID}
|
||||
>
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
size="medium"
|
||||
variant="tertiary"
|
||||
Icon={IconPlus}
|
||||
position="middle"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={<RecordTableHeaderPlusButtonContent />}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownHotkeyScope={{
|
||||
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
</StyledColumnHeadContainer>
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
</StyledTableHead>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { FieldMetadata } from '../../field/types/FieldMetadata';
|
||||
import { useTableColumns } from '../hooks/useTableColumns';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const RecordTableHeaderPlusButtonContent = () => {
|
||||
const { closeDropdown } = useDropdown();
|
||||
|
||||
const hiddenTableColumns = useRecoilScopedValue(
|
||||
hiddenTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const { handleColumnVisibilityChange } = useTableColumns();
|
||||
|
||||
const handleAddColumn = useCallback(
|
||||
(column: ColumnDefinition<FieldMetadata>) => {
|
||||
closeDropdown();
|
||||
handleColumnVisibilityChange(column);
|
||||
},
|
||||
[handleColumnVisibilityChange, closeDropdown],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
{hiddenTableColumns.map((column) => (
|
||||
<MenuItem
|
||||
key={column.fieldId}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: () => handleAddColumn(column),
|
||||
},
|
||||
]}
|
||||
LeftIcon={column.Icon}
|
||||
text={column.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { forwardRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { ColumnContext } from '../contexts/ColumnContext';
|
||||
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { visibleTableColumnsScopedSelector } from '../states/selectors/visibleTableColumnsScopedSelector';
|
||||
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import { RecordTableCell } from './RecordTableCell';
|
||||
|
||||
const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.selected ? props.theme.accent.quaternary : 'none'};
|
||||
`;
|
||||
|
||||
type RecordTableRowProps = {
|
||||
rowId: string;
|
||||
};
|
||||
|
||||
export const RecordTableRow = forwardRef<
|
||||
HTMLTableRowElement,
|
||||
RecordTableRowProps
|
||||
>(({ rowId }, ref) => {
|
||||
const visibleTableColumns = useRecoilScopedValue(
|
||||
visibleTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const { currentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
ref={ref}
|
||||
data-testid={`row-id-${rowId}`}
|
||||
selected={currentRowSelected}
|
||||
data-selectable-id={rowId}
|
||||
>
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{[...visibleTableColumns]
|
||||
.sort((columnA, columnB) => columnA.position - columnB.position)
|
||||
.map((column, columnIndex) => {
|
||||
return (
|
||||
<ColumnContext.Provider value={column} key={column.fieldId}>
|
||||
<RecordTableCell cellIndex={columnIndex} />
|
||||
</ColumnContext.Provider>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
</StyledRow>
|
||||
);
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
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';
|
||||
|
||||
const onChange = () => {
|
||||
selectAllRows();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
indeterminate={indeterminate}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const ColumnHeadDropdownId = 'table-head-options';
|
||||
@ -0,0 +1,2 @@
|
||||
// We should either apply the constant all caps case or maybe define a more general enum to store those ids ?
|
||||
export const TableOptionsDropdownId = 'table-options';
|
||||
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
|
||||
|
||||
import { selectedRowIdsSelector } from '../../states/selectors/selectedRowIdsSelector';
|
||||
|
||||
export const RecordTableContextMenu = () => {
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
|
||||
return <ContextMenu selectedIds={selectedRowIds} />;
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
export const CellHotkeyScopeContext = createContext<HotkeyScope | null>(null);
|
||||
@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const ColumnContext =
|
||||
createContext<ColumnDefinition<FieldMetadata> | null>(null);
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ColumnIndexContext = createContext<number>(0);
|
||||
@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const EntityUpdateMutationContext = createContext<(params: any) => void>(
|
||||
{} as any,
|
||||
);
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RowIdContext = createContext<string | null>(null);
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RowIndexContext = createContext<number>(0);
|
||||
@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const TableContext = createContext<{
|
||||
onColumnsChange?: (
|
||||
columns: ColumnDefinition<FieldMetadata>[],
|
||||
) => void | Promise<void>;
|
||||
}>({});
|
||||
@ -0,0 +1,48 @@
|
||||
import { useCurrentTableCellEditMode } from '../table-cell/hooks/useCurrentTableCellEditMode';
|
||||
import { useTableCell } from '../table-cell/hooks/useTableCell';
|
||||
|
||||
import { useMoveSoftFocus } from './useMoveSoftFocus';
|
||||
|
||||
export const useCellInputEventHandlers = <T>({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: {
|
||||
onSubmit?: (newValue: T) => void;
|
||||
onCancel?: () => void;
|
||||
}) => {
|
||||
const { closeTableCell: closeEditableCell } = useTableCell();
|
||||
const { isCurrentTableCellInEditMode: isCurrentCellInEditMode } =
|
||||
useCurrentTableCellEditMode();
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
return {
|
||||
handleClickOutside: (event: MouseEvent | TouchEvent, newValue: T) => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
onSubmit?.(newValue);
|
||||
|
||||
closeEditableCell();
|
||||
}
|
||||
},
|
||||
handleEscape: () => {
|
||||
closeEditableCell();
|
||||
onCancel?.();
|
||||
},
|
||||
handleEnter: (newValue: T) => {
|
||||
onSubmit?.(newValue);
|
||||
closeEditableCell();
|
||||
moveDown();
|
||||
},
|
||||
handleTab: (newValue: T) => {
|
||||
onSubmit?.(newValue);
|
||||
closeEditableCell();
|
||||
moveRight();
|
||||
},
|
||||
handleShiftTab: (newValue: T) => {
|
||||
onSubmit?.(newValue);
|
||||
closeEditableCell();
|
||||
moveLeft();
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
|
||||
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
|
||||
|
||||
export const useCloseCurrentTableCellInEditMode = () =>
|
||||
useRecoilCallback(({ set, snapshot }) => {
|
||||
return async () => {
|
||||
const currentTableCellInEditModePosition = snapshot
|
||||
.getLoadable(currentTableCellInEditModePositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(
|
||||
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||
false,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
@ -0,0 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { RowIdContext } from '../contexts/RowIdContext';
|
||||
|
||||
export const useCurrentRowEntityId = () => {
|
||||
const currentEntityId = useContext(RowIdContext);
|
||||
|
||||
return currentEntityId;
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { RowIdContext } from '../contexts/RowIdContext';
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
|
||||
export const useCurrentRowSelected = () => {
|
||||
const currentRowId = useContext(RowIdContext);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { isSoftFocusOnTableCellFamilyState } from '../states/isSoftFocusOnTableCellFamilyState';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
|
||||
export const useDisableSoftFocus = () =>
|
||||
useRecoilCallback(({ set, snapshot }) => {
|
||||
return () => {
|
||||
const currentPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(isSoftFocusActiveState, false);
|
||||
|
||||
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
|
||||
};
|
||||
}, []);
|
||||
@ -0,0 +1,26 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
|
||||
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
|
||||
|
||||
export const useGetIsSomeCellInEditMode = () => {
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const currentTableCellInEditModePosition = snapshot
|
||||
.getLoadable(currentTableCellInEditModePositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const isSomeCellInEditMode = snapshot
|
||||
.getLoadable(
|
||||
isTableCellInEditModeFamilyState(
|
||||
currentTableCellInEditModePosition,
|
||||
),
|
||||
)
|
||||
.valueOrThrow();
|
||||
|
||||
return isSomeCellInEditMode;
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
|
||||
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||
|
||||
export const useLeaveTableFocus = () => {
|
||||
const disableSoftFocus = useDisableSoftFocus();
|
||||
const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const isSoftFocusActive = snapshot
|
||||
.getLoadable(isSoftFocusActiveState)
|
||||
.valueOrThrow();
|
||||
|
||||
const currentHotkeyScope = snapshot
|
||||
.getLoadable(currentHotkeyScopeState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!isSoftFocusActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentHotkeyScope?.scope === TableHotkeyScope.Table) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeCurrentCellInEditMode();
|
||||
disableSoftFocus();
|
||||
},
|
||||
[closeCurrentCellInEditMode, disableSoftFocus],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||
import { useMoveSoftFocus } from './useMoveSoftFocus';
|
||||
|
||||
export const useMapKeyboardToSoftFocus = () => {
|
||||
const { moveDown, moveLeft, moveRight, moveUp } = useMoveSoftFocus();
|
||||
|
||||
const disableSoftFocus = useDisableSoftFocus();
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, `${Key.Shift}+${Key.Enter}`],
|
||||
() => {
|
||||
moveUp();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveUp],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowDown,
|
||||
() => {
|
||||
moveDown();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveDown],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowLeft, `${Key.Shift}+${Key.Tab}`],
|
||||
() => {
|
||||
moveLeft();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveLeft],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowRight, Key.Tab],
|
||||
() => {
|
||||
moveRight();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[moveRight],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
|
||||
disableSoftFocus();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[disableSoftFocus],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
|
||||
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const useMoveEditModeToTableCellPosition = () =>
|
||||
useRecoilCallback(({ set, snapshot }) => {
|
||||
return (newPosition: TableCellPosition) => {
|
||||
const currentTableCellInEditModePosition = snapshot
|
||||
.getLoadable(currentTableCellInEditModePositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(
|
||||
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||
false,
|
||||
);
|
||||
|
||||
set(currentTableCellInEditModePositionState, newPosition);
|
||||
|
||||
set(isTableCellInEditModeFamilyState(newPosition), true);
|
||||
};
|
||||
}, []);
|
||||
@ -0,0 +1,159 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
|
||||
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { numberOfTableColumnsScopedSelector } from '../states/selectors/numberOfTableColumnsScopedSelector';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
|
||||
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
|
||||
|
||||
// TODO: stories
|
||||
|
||||
export const useMoveSoftFocus = () => {
|
||||
const tableScopeId = useRecoilScopeId(TableRecoilScopeContext);
|
||||
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(numberOfTableRowsState)
|
||||
.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(numberOfTableColumnsScopedSelector(tableScopeId))
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableRows = snapshot
|
||||
.getLoadable(numberOfTableRowsState)
|
||||
.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, tableScopeId],
|
||||
);
|
||||
|
||||
const moveLeft = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const softFocusPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const numberOfTableColumns = snapshot
|
||||
.getLoadable(numberOfTableColumnsScopedSelector(tableScopeId))
|
||||
.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, tableScopeId],
|
||||
);
|
||||
|
||||
return {
|
||||
moveDown,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
|
||||
import { currentTableCellInEditModePositionState } from '../states/currentTableCellInEditModePositionState';
|
||||
import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditModeFamilyState';
|
||||
import { useSetSoftFocusOnCurrentTableCell } from '../table-cell/hooks/useSetSoftFocusOnCurrentTableCell';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
export const useMoveSoftFocusToCurrentCellOnHover = () => {
|
||||
const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const currentTableCellInEditModePosition = snapshot
|
||||
.getLoadable(currentTableCellInEditModePositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
const isSomeCellInEditMode = snapshot.getLoadable(
|
||||
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||
);
|
||||
|
||||
const currentHotkeyScope = snapshot
|
||||
.getLoadable(currentHotkeyScopeState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (
|
||||
currentHotkeyScope.scope !== TableHotkeyScope.TableSoftFocus &&
|
||||
currentHotkeyScope.scope !== TableHotkeyScope.CellEditMode
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSomeCellInEditMode.contents) {
|
||||
setSoftFocusOnCurrentTableCell();
|
||||
}
|
||||
},
|
||||
[setSoftFocusOnCurrentTableCell],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
export const useMoveViewColumns = () => {
|
||||
const handleColumnMove = <T extends { position: number }>(
|
||||
direction: 'left' | 'right',
|
||||
currentArrayindex: number,
|
||||
targetArray: T[],
|
||||
) => {
|
||||
const targetArrayIndex =
|
||||
direction === 'left' ? currentArrayindex - 1 : currentArrayindex + 1;
|
||||
const targetArraySize = targetArray.length - 1;
|
||||
if (
|
||||
currentArrayindex >= 0 &&
|
||||
targetArrayIndex >= 0 &&
|
||||
currentArrayindex <= targetArraySize &&
|
||||
targetArrayIndex <= targetArraySize
|
||||
) {
|
||||
const currentEntity = targetArray[currentArrayindex];
|
||||
const targetEntity = targetArray[targetArrayIndex];
|
||||
const newArray = [...targetArray];
|
||||
|
||||
newArray[currentArrayindex] = {
|
||||
...targetEntity,
|
||||
index: currentEntity.position,
|
||||
};
|
||||
newArray[targetArrayIndex] = {
|
||||
...currentEntity,
|
||||
index: targetEntity.position,
|
||||
};
|
||||
|
||||
return newArray.map((column, index) => ({
|
||||
...column,
|
||||
position: index,
|
||||
}));
|
||||
}
|
||||
|
||||
return targetArray;
|
||||
};
|
||||
|
||||
return { handleColumnMove };
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export const useResetTableRowSelection = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const tableRowIds = snapshot
|
||||
.getLoadable(tableRowIdsState)
|
||||
.valueOrThrow();
|
||||
|
||||
for (const rowId of tableRowIds) {
|
||||
set(isRowSelectedFamilyState(rowId), false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,41 @@
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
import { allRowsSelectedStatusSelector } from '../states/selectors/allRowsSelectedStatusSelector';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export const 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,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { isFetchingRecordTableDataState } from '../states/isFetchingRecordTableDataState';
|
||||
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
import { useResetTableRowSelection } from './useResetTableRowSelection';
|
||||
|
||||
export const useSetRecordTableData = () => {
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
const { setEntityCountInCurrentView } = useView();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(newEntityArray: T[]) => {
|
||||
for (const entity of newEntityArray) {
|
||||
const currentEntity = snapshot
|
||||
.getLoadable(entityFieldsFamilyState(entity.id))
|
||||
.valueOrThrow();
|
||||
|
||||
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
|
||||
set(entityFieldsFamilyState(entity.id), entity);
|
||||
}
|
||||
}
|
||||
|
||||
const entityIds = newEntityArray.map((entity) => entity.id);
|
||||
|
||||
set(tableRowIdsState, (currentRowIds) => {
|
||||
if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) {
|
||||
return entityIds;
|
||||
}
|
||||
|
||||
return currentRowIds;
|
||||
});
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
set(numberOfTableRowsState, entityIds.length);
|
||||
setEntityCountInCurrentView(entityIds.length);
|
||||
set(isFetchingRecordTableDataState, false);
|
||||
},
|
||||
[resetTableRowSelection, setEntityCountInCurrentView],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
|
||||
|
||||
export const useSetRowSelectedState = () =>
|
||||
useRecoilCallback(({ set }) => (rowId: string, selected: boolean) => {
|
||||
set(isRowSelectedFamilyState(rowId), selected);
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isSoftFocusActiveState } from '../states/isSoftFocusActiveState';
|
||||
import { isSoftFocusOnTableCellFamilyState } from '../states/isSoftFocusOnTableCellFamilyState';
|
||||
import { softFocusPositionState } from '../states/softFocusPositionState';
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const useSetSoftFocusPosition = () =>
|
||||
useRecoilCallback(({ set, snapshot }) => {
|
||||
return (newPosition: TableCellPosition) => {
|
||||
const currentPosition = snapshot
|
||||
.getLoadable(softFocusPositionState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(isSoftFocusActiveState, true);
|
||||
|
||||
set(isSoftFocusOnTableCellFamilyState(currentPosition), false);
|
||||
|
||||
set(softFocusPositionState, newPosition);
|
||||
|
||||
set(isSoftFocusOnTableCellFamilyState(newPosition), true);
|
||||
};
|
||||
}, []);
|
||||
@ -0,0 +1,124 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
import { useMoveViewColumns } from '@/ui/object/record-table/hooks/useMoveViewColumns';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { TableContext } from '../contexts/TableContext';
|
||||
import { availableTableColumnsScopedState } from '../states/availableTableColumnsScopedState';
|
||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { savedTableColumnsFamilyState } from '../states/savedTableColumnsFamilyState';
|
||||
import { visibleTableColumnsScopedSelector } from '../states/selectors/visibleTableColumnsScopedSelector';
|
||||
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const useTableColumns = () => {
|
||||
const { onColumnsChange } = useContext(TableContext);
|
||||
|
||||
const [availableTableColumns] = useRecoilScopedState(
|
||||
availableTableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const { currentViewId } = useView();
|
||||
|
||||
const setSavedTableColumns = useSetRecoilState(
|
||||
savedTableColumnsFamilyState(currentViewId),
|
||||
);
|
||||
const [tableColumns, setTableColumns] = useRecoilScopedState(
|
||||
tableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const visibleTableColumns = useRecoilScopedValue(
|
||||
visibleTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const { handleColumnMove } = useMoveViewColumns();
|
||||
|
||||
const handleColumnsChange = useCallback(
|
||||
async (columns: ColumnDefinition<FieldMetadata>[]) => {
|
||||
setSavedTableColumns(columns);
|
||||
setTableColumns(columns);
|
||||
|
||||
await onColumnsChange?.(columns);
|
||||
},
|
||||
[onColumnsChange, setSavedTableColumns, setTableColumns],
|
||||
);
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
async (
|
||||
viewField: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => {
|
||||
const isNewColumn = !tableColumns.some(
|
||||
(tableColumns) => tableColumns.fieldId === viewField.fieldId,
|
||||
);
|
||||
|
||||
if (isNewColumn) {
|
||||
const newColumn = availableTableColumns.find(
|
||||
(availableTableColumn) =>
|
||||
availableTableColumn.fieldId === viewField.fieldId,
|
||||
);
|
||||
if (!newColumn) return;
|
||||
|
||||
const nextColumns = [
|
||||
...tableColumns,
|
||||
{ ...newColumn, isVisible: true },
|
||||
];
|
||||
|
||||
await handleColumnsChange(nextColumns);
|
||||
} else {
|
||||
const nextColumns = tableColumns.map((previousColumn) =>
|
||||
previousColumn.fieldId === viewField.fieldId
|
||||
? { ...previousColumn, isVisible: !viewField.isVisible }
|
||||
: previousColumn,
|
||||
);
|
||||
|
||||
await handleColumnsChange(nextColumns);
|
||||
}
|
||||
},
|
||||
[tableColumns, availableTableColumns, handleColumnsChange],
|
||||
);
|
||||
|
||||
const handleMoveTableColumn = useCallback(
|
||||
async (
|
||||
direction: 'left' | 'right',
|
||||
column: ColumnDefinition<FieldMetadata>,
|
||||
) => {
|
||||
const currentColumnArrayIndex = visibleTableColumns.findIndex(
|
||||
(visibleColumn) => visibleColumn.fieldId === column.fieldId,
|
||||
);
|
||||
|
||||
const columns = handleColumnMove(
|
||||
direction,
|
||||
currentColumnArrayIndex,
|
||||
visibleTableColumns,
|
||||
);
|
||||
|
||||
await handleColumnsChange(columns);
|
||||
},
|
||||
[visibleTableColumns, handleColumnMove, handleColumnsChange],
|
||||
);
|
||||
|
||||
const handleColumnReorder = useCallback(
|
||||
async (columns: ColumnDefinition<FieldMetadata>[]) => {
|
||||
const updatedColumns = columns.map((column, index) => ({
|
||||
...column,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
await handleColumnsChange(updatedColumns);
|
||||
},
|
||||
[handleColumnsChange],
|
||||
);
|
||||
|
||||
return {
|
||||
handleColumnVisibilityChange,
|
||||
handleMoveTableColumn,
|
||||
handleColumnReorder,
|
||||
handleColumnsChange,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
|
||||
|
||||
export const useUpsertRecordTableItem = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(entity: T) => {
|
||||
const currentEntity = snapshot
|
||||
.getLoadable(entityFieldsFamilyState(entity.id))
|
||||
.valueOrThrow();
|
||||
|
||||
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
|
||||
set(entityFieldsFamilyState(entity.id), entity);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,33 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
|
||||
|
||||
export const useUpsertRecordTableItems = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(entities: T[]) => {
|
||||
// Create a map of new entities for quick lookup.
|
||||
const newEntityMap = new Map(
|
||||
entities.map((entity) => [entity.id, entity]),
|
||||
);
|
||||
|
||||
// Filter out entities that are already the same in the state.
|
||||
const entitiesToUpdate = entities.filter((entity) => {
|
||||
const currentEntity = snapshot
|
||||
.getLoadable(entityFieldsFamilyState(entity.id))
|
||||
.valueMaybe();
|
||||
|
||||
return (
|
||||
!currentEntity ||
|
||||
JSON.stringify(currentEntity) !==
|
||||
JSON.stringify(newEntityMap.get(entity.id))
|
||||
);
|
||||
});
|
||||
|
||||
// Batch set state for the filtered entities.
|
||||
for (const entity of entitiesToUpdate) {
|
||||
set(entityFieldsFamilyState(entity.id), entity);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,16 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export const useUpsertTableRowId = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(rowId: string) => {
|
||||
const currentRowIds = snapshot
|
||||
.getLoadable(tableRowIdsState)
|
||||
.valueOrThrow();
|
||||
|
||||
set(tableRowIdsState, Array.from(new Set([rowId, ...currentRowIds])));
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,17 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
|
||||
export const useUpsertTableRowIds = () =>
|
||||
useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(rowIds: string[]) => {
|
||||
const currentRowIds = snapshot
|
||||
.getLoadable(tableRowIdsState)
|
||||
.valueOrThrow();
|
||||
|
||||
const uniqueRowIds = Array.from(new Set([...rowIds, ...currentRowIds]));
|
||||
set(tableRowIdsState, uniqueRowIds);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -0,0 +1,29 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
|
||||
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
|
||||
|
||||
import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
|
||||
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
|
||||
|
||||
export const TableOptionsDropdown = ({
|
||||
onImport,
|
||||
}: {
|
||||
onImport?: () => void;
|
||||
}) => {
|
||||
const { setViewEditMode } = useView();
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={TableOptionsDropdownId}>
|
||||
<Dropdown
|
||||
clickableComponent={<TableOptionsDropdownButton />}
|
||||
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
|
||||
dropdownOffset={{ y: 8 }}
|
||||
dropdownComponents={<TableOptionsDropdownContent onImport={onImport} />}
|
||||
onClickOutside={() => setViewEditMode('none')}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
||||
|
||||
export const TableOptionsDropdownButton = () => {
|
||||
const { isDropdownOpen, toggleDropdown } = useDropdown({
|
||||
dropdownScopeId: TableOptionsDropdownId,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownOpen}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
Options
|
||||
</StyledHeaderDropdownButton>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,162 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/display/icon';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
import { useViewGetStates } from '@/views/hooks/useViewGetStates';
|
||||
|
||||
import { useTableColumns } from '../../hooks/useTableColumns';
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector';
|
||||
import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector';
|
||||
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
|
||||
|
||||
type TableOptionsMenu = 'fields';
|
||||
|
||||
export const TableOptionsDropdownContent = ({
|
||||
onImport,
|
||||
}: {
|
||||
onImport?: () => void;
|
||||
}) => {
|
||||
const { setViewEditMode, handleViewNameSubmit } = useView();
|
||||
const { viewEditMode, currentView } = useViewGetStates();
|
||||
|
||||
const { closeDropdown } = useDropdown();
|
||||
|
||||
const [currentMenu, setCurrentMenu] = useState<TableOptionsMenu | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const viewEditInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const visibleTableColumns = useRecoilScopedValue(
|
||||
visibleTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const hiddenTableColumns = useRecoilScopedValue(
|
||||
hiddenTableColumnsScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const { handleColumnVisibilityChange, handleColumnReorder } =
|
||||
useTableColumns();
|
||||
|
||||
const handleSelectMenu = (option: TableOptionsMenu) => {
|
||||
const name = viewEditInputRef.current?.value;
|
||||
handleViewNameSubmit(name);
|
||||
setCurrentMenu(option);
|
||||
};
|
||||
|
||||
const handleReorderField: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (
|
||||
!result.destination ||
|
||||
result.destination.index === 1 ||
|
||||
result.source.index === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = [...visibleTableColumns];
|
||||
const [removed] = reorderFields.splice(result.source.index - 1, 1);
|
||||
reorderFields.splice(result.destination.index - 1, 0, removed);
|
||||
|
||||
handleColumnReorder(reorderFields);
|
||||
},
|
||||
[visibleTableColumns, handleColumnReorder],
|
||||
);
|
||||
|
||||
const resetMenu = () => setCurrentMenu(undefined);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
closeDropdown();
|
||||
},
|
||||
TableOptionsHotkeyScope.Dropdown,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
const name = viewEditInputRef.current?.value;
|
||||
handleViewNameSubmit(name);
|
||||
resetMenu();
|
||||
setViewEditMode('none');
|
||||
closeDropdown();
|
||||
},
|
||||
TableOptionsHotkeyScope.Dropdown,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!currentMenu && (
|
||||
<>
|
||||
<DropdownMenuInput
|
||||
ref={viewEditInputRef}
|
||||
autoFocus={viewEditMode !== 'none'}
|
||||
placeholder={
|
||||
viewEditMode === 'create'
|
||||
? 'New view'
|
||||
: viewEditMode === 'edit'
|
||||
? 'View name'
|
||||
: ''
|
||||
}
|
||||
defaultValue={viewEditMode === 'create' ? '' : currentView?.name}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => handleSelectMenu('fields')}
|
||||
LeftIcon={IconTag}
|
||||
text="Fields"
|
||||
/>
|
||||
{onImport && (
|
||||
<MenuItem
|
||||
onClick={onImport}
|
||||
LeftIcon={IconFileImport}
|
||||
text="Import"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
{currentMenu === 'fields' && (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||
Fields
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSeparator />
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Visible"
|
||||
fields={visibleTableColumns}
|
||||
onVisibilityChange={handleColumnVisibilityChange}
|
||||
isDraggable={true}
|
||||
onDragEnd={handleReorderField}
|
||||
/>
|
||||
{hiddenTableColumns.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Hidden"
|
||||
fields={hiddenTableColumns}
|
||||
onVisibilityChange={handleColumnVisibilityChange}
|
||||
isDraggable={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const availableTableColumnsScopedState = atomFamily<
|
||||
ColumnDefinition<FieldMetadata>[],
|
||||
string
|
||||
>({
|
||||
key: 'availableTableColumnsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const currentTableCellInEditModePositionState = atom<TableCellPosition>({
|
||||
key: 'currentTableCellInEditModePositionState',
|
||||
default: {
|
||||
row: 0,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isFetchingRecordTableDataState = atom<boolean>({
|
||||
key: 'isFetchingRecordTableDataState',
|
||||
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,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const isSoftFocusOnTableCellFamilyState = atomFamily<
|
||||
boolean,
|
||||
TableCellPosition
|
||||
>({
|
||||
key: 'isSoftFocusOnTableCellFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const isTableCellInEditModeFamilyState = atomFamily<
|
||||
boolean,
|
||||
TableCellPosition
|
||||
>({
|
||||
key: 'isTableCellInEditModeFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const numberOfTableRowsState = atom<number>({
|
||||
key: 'numberOfTableRowsState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const TableRecoilScopeContext = createContext<string | null>(null);
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const resizeFieldOffsetState = atom<number>({
|
||||
key: 'resizeFieldOffsetState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const savedTableColumnsFamilyState = atomFamily<
|
||||
ColumnDefinition<FieldMetadata>[],
|
||||
string | undefined
|
||||
>({
|
||||
key: 'savedTableColumnsFamilyState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
import { selector } from 'recoil';
|
||||
|
||||
import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus';
|
||||
import { numberOfTableRowsState } from '../numberOfTableRowsState';
|
||||
|
||||
import { selectedRowIdsSelector } from './selectedRowIdsSelector';
|
||||
|
||||
export const allRowsSelectedStatusSelector = selector<AllRowsSelectedStatus>({
|
||||
key: 'allRowsSelectedStatusSelector',
|
||||
get: ({ get }) => {
|
||||
const numberOfRows = get(numberOfTableRowsState);
|
||||
|
||||
const selectedRowIds = get(selectedRowIdsSelector);
|
||||
|
||||
const numberOfSelectedRows = selectedRowIds.length;
|
||||
|
||||
const allRowsSelectedStatus =
|
||||
numberOfSelectedRows === 0
|
||||
? 'none'
|
||||
: numberOfRows === numberOfSelectedRows
|
||||
? 'all'
|
||||
: 'some';
|
||||
|
||||
return allRowsSelectedStatus;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { availableTableColumnsScopedState } from '../availableTableColumnsScopedState';
|
||||
import { tableColumnsScopedState } from '../tableColumnsScopedState';
|
||||
|
||||
export const hiddenTableColumnsScopedSelector = selectorFamily({
|
||||
key: 'hiddenTableColumnsScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) => {
|
||||
const columns = get(tableColumnsScopedState(scopeId));
|
||||
const columnKeys = columns.map(({ fieldId }) => fieldId);
|
||||
const otherAvailableColumns = get(
|
||||
availableTableColumnsScopedState(scopeId),
|
||||
).filter(({ fieldId }) => !columnKeys.includes(fieldId));
|
||||
|
||||
return [
|
||||
...columns.filter((column) => !column.isVisible),
|
||||
...otherAvailableColumns,
|
||||
];
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { tableColumnsScopedState } from '../tableColumnsScopedState';
|
||||
|
||||
export const numberOfTableColumnsScopedSelector = selectorFamily({
|
||||
key: 'numberOfTableColumnsScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) =>
|
||||
get(tableColumnsScopedState(scopeId)).length,
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||
import { savedTableColumnsFamilyState } from '../savedTableColumnsFamilyState';
|
||||
|
||||
export const savedTableColumnsByKeyFamilySelector = selectorFamily({
|
||||
key: 'savedTableColumnsByKeyFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedTableColumnsFamilyState(viewId)).reduce<
|
||||
Record<string, ColumnDefinition<FieldMetadata>>
|
||||
>((result, column) => ({ ...result, [column.fieldId]: column }), {}),
|
||||
});
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||
import { tableColumnsScopedState } from '../tableColumnsScopedState';
|
||||
|
||||
export const tableColumnsByKeyScopedSelector = selectorFamily({
|
||||
key: 'tableColumnsByKeyScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) =>
|
||||
get(tableColumnsScopedState(scopeId)).reduce<
|
||||
Record<string, ColumnDefinition<FieldMetadata>>
|
||||
>((result, column) => ({ ...result, [column.fieldId]: column }), {}),
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { reduceSortsToOrderBy } from '@/ui/object/sort/utils/helpers';
|
||||
import { SortOrder } from '~/generated/graphql';
|
||||
|
||||
import { tableSortsScopedState } from '../tableSortsScopedState';
|
||||
|
||||
export const tableSortsOrderByScopedSelector = selectorFamily({
|
||||
key: 'tableSortsOrderByScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) => {
|
||||
const orderBy = reduceSortsToOrderBy(get(tableSortsScopedState(scopeId)));
|
||||
return orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }];
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { turnFilterIntoWhereClause } from '../../../filter/utils/turnFilterIntoWhereClause';
|
||||
import { tableFiltersScopedState } from '../tableFiltersScopedState';
|
||||
|
||||
export const tablefiltersWhereScopedSelector = selectorFamily({
|
||||
key: 'tablefiltersWhereScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) => ({
|
||||
AND: get(tableFiltersScopedState(scopeId)).map(turnFilterIntoWhereClause),
|
||||
}),
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { tableColumnsScopedState } from '../tableColumnsScopedState';
|
||||
|
||||
export const visibleTableColumnsScopedSelector = selectorFamily({
|
||||
key: 'visibleTableColumnsScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) =>
|
||||
[
|
||||
...get(tableColumnsScopedState(scopeId)).filter(
|
||||
(column) => column.isVisible,
|
||||
),
|
||||
].sort((a, b) => a.position - b.position),
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { TableCellPosition } from '../types/TableCellPosition';
|
||||
|
||||
export const softFocusPositionState = atom<TableCellPosition>({
|
||||
key: 'softFocusPositionState',
|
||||
default: {
|
||||
row: 0,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export const tableColumnsScopedState = atomFamily<
|
||||
ColumnDefinition<FieldMetadata>[],
|
||||
string
|
||||
>({
|
||||
key: 'tableColumnsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '../../filter/types/Filter';
|
||||
|
||||
export const tableFiltersScopedState = atomFamily<Filter[], string>({
|
||||
key: 'tableFiltersScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const tableRowIdsState = atom<string[]>({
|
||||
key: 'tableRowIdsState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Sort } from '../../sort/types/Sort';
|
||||
|
||||
export const tableSortsScopedState = atomFamily<Sort[], string>({
|
||||
key: 'tableSortsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { IconArrowUpRight } from '@/ui/display/icon';
|
||||
import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay';
|
||||
import { FieldInput } from '@/ui/object/field/components/FieldInput';
|
||||
import { useGetButtonIcon } from '@/ui/object/field/hooks/useGetButtonIcon';
|
||||
import { FieldInputEvent } from '@/ui/object/field/types/FieldInputEvent';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { ColumnIndexContext } from '../../contexts/ColumnIndexContext';
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellContainer } from './TableCellContainer';
|
||||
|
||||
export const TableCell = ({
|
||||
customHotkeyScope,
|
||||
}: {
|
||||
customHotkeyScope: HotkeyScope;
|
||||
}) => {
|
||||
const isFirstColumn = useContext(ColumnIndexContext) === 0;
|
||||
|
||||
const buttonIcon = useGetButtonIcon();
|
||||
|
||||
const { closeTableCell } = useTableCell();
|
||||
|
||||
const { moveLeft, moveRight, moveDown } = useMoveSoftFocus();
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveDown();
|
||||
};
|
||||
|
||||
const handleSubmit: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveRight();
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveLeft();
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellContainer
|
||||
editHotkeyScope={customHotkeyScope}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
onCancel={handleCancel}
|
||||
onClickOutside={handleCancel}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<FieldDisplay />}
|
||||
buttonIcon={isFirstColumn ? IconArrowUpRight : buttonIcon}
|
||||
></TableCellContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
`;
|
||||
|
||||
type TableCellButtonProps = {
|
||||
onClick?: () => void;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
);
|
||||
@ -0,0 +1,142 @@
|
||||
import { ReactElement, useContext, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { useIsFieldEmpty } from '@/ui/object/field/hooks/useIsFieldEmpty';
|
||||
import { useIsFieldInputOnly } from '@/ui/object/field/hooks/useIsFieldInputOnly';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
||||
import { ColumnIndexContext } from '../../contexts/ColumnIndexContext';
|
||||
import { useGetIsSomeCellInEditMode } from '../../hooks/useGetIsSomeCellInEditMode';
|
||||
import { useMoveSoftFocusToCurrentCellOnHover } from '../../hooks/useMoveSoftFocusToCurrentCellOnHover';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useCurrentTableCellEditMode } from '../hooks/useCurrentTableCellEditMode';
|
||||
import { useIsSoftFocusOnCurrentTableCell } from '../hooks/useIsSoftFocusOnCurrentTableCell';
|
||||
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellButton } from './TableCellButton';
|
||||
import { TableCellDisplayMode } from './TableCellDisplayMode';
|
||||
import { TableCellEditMode } from './TableCellEditMode';
|
||||
import { TableCellSoftFocusMode } from './TableCellSoftFocusMode';
|
||||
|
||||
const StyledCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export type TableCellContainerProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
transparent?: boolean;
|
||||
maxContentWidth?: number;
|
||||
buttonIcon?: IconComponent;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
};
|
||||
|
||||
export const TableCellContainer = ({
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editHotkeyScope,
|
||||
buttonIcon,
|
||||
}: TableCellContainerProps) => {
|
||||
const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
|
||||
|
||||
const getIsSomeCellInEditMode = useGetIsSomeCellInEditMode();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const moveSoftFocusToCurrentCellOnHover =
|
||||
useMoveSoftFocusToCurrentCellOnHover();
|
||||
|
||||
const hasSoftFocus = useIsSoftFocusOnCurrentTableCell();
|
||||
|
||||
const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell();
|
||||
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
setSoftFocusOnCurrentTableCell();
|
||||
openTableCell();
|
||||
};
|
||||
|
||||
const handleContainerMouseEnter = () => {
|
||||
const isSomeCellInEditMode = getIsSomeCellInEditMode();
|
||||
|
||||
if (!isHovered && !isSomeCellInEditMode) {
|
||||
setIsHovered(true);
|
||||
moveSoftFocusToCurrentCellOnHover();
|
||||
}
|
||||
};
|
||||
|
||||
const handleContainerMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const editModeContentOnly = useIsFieldInputOnly();
|
||||
|
||||
const isFirstColumnCell = useContext(ColumnIndexContext) === 0;
|
||||
|
||||
const isEmpty = useIsFieldEmpty();
|
||||
|
||||
const showButton =
|
||||
!!buttonIcon &&
|
||||
hasSoftFocus &&
|
||||
!isCurrentTableCellInEditMode &&
|
||||
!editModeContentOnly &&
|
||||
(!isFirstColumnCell || !isEmpty);
|
||||
|
||||
return (
|
||||
<CellHotkeyScopeContext.Provider
|
||||
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
||||
>
|
||||
<StyledCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
>
|
||||
{isCurrentTableCellInEditMode ? (
|
||||
<TableCellEditMode
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</TableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<>
|
||||
{showButton && (
|
||||
<TableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||
)}
|
||||
<TableCellSoftFocusMode>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</TableCellSoftFocusMode>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showButton && (
|
||||
<TableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||
)}
|
||||
<TableCellDisplayMode>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</TableCellDisplayMode>
|
||||
</>
|
||||
)}
|
||||
</StyledCellBaseContainer>
|
||||
</CellHotkeyScopeContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { Ref } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type EditableCellDisplayContainerProps = {
|
||||
softFocus?: boolean;
|
||||
onClick?: () => void;
|
||||
scrollRef?: Ref<HTMLDivElement>;
|
||||
isHovered?: boolean;
|
||||
};
|
||||
|
||||
const StyledEditableCellDisplayModeOuterContainer = styled.div<
|
||||
Pick<EditableCellDisplayContainerProps, 'softFocus' | 'isHovered'>
|
||||
>`
|
||||
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.sm};
|
||||
outline: 1px solid ${props.theme.font.color.extraLight};`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
const StyledEditableCellDisplayModeInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const TableCellDisplayContainer = ({
|
||||
children,
|
||||
softFocus,
|
||||
onClick,
|
||||
scrollRef,
|
||||
}: React.PropsWithChildren<EditableCellDisplayContainerProps>) => (
|
||||
<StyledEditableCellDisplayModeOuterContainer
|
||||
data-testid={
|
||||
softFocus ? 'editable-cell-soft-focus-mode' : 'editable-cell-display-mode'
|
||||
}
|
||||
onClick={onClick}
|
||||
softFocus={softFocus}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<StyledEditableCellDisplayModeInnerContainer>
|
||||
{children}
|
||||
</StyledEditableCellDisplayModeInnerContainer>
|
||||
</StyledEditableCellDisplayModeOuterContainer>
|
||||
);
|
||||
@ -0,0 +1,30 @@
|
||||
import { useIsFieldInputOnly } from '@/ui/object/field/hooks/useIsFieldInputOnly';
|
||||
|
||||
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellDisplayContainer } from './TableCellDisplayContainer';
|
||||
|
||||
export const TableCellDisplayMode = ({
|
||||
children,
|
||||
}: React.PropsWithChildren<unknown>) => {
|
||||
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentTableCell();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const handleClick = () => {
|
||||
setSoftFocusOnCurrentCell();
|
||||
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellDisplayContainer onClick={handleClick}>
|
||||
{children}
|
||||
</TableCellDisplayContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
`;
|
||||
|
||||
type TableCellButtonProps = {
|
||||
onClick?: () => void;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
);
|
||||
@ -0,0 +1,34 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledEditableCellEditModeContainer = styled.div<TableCellEditModeProps>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: -1px;
|
||||
min-width: 200px;
|
||||
width: calc(100% + 2px);
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export type TableCellEditModeProps = {
|
||||
children: ReactElement;
|
||||
transparent?: boolean;
|
||||
maxContentWidth?: number;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
initialValue?: string;
|
||||
};
|
||||
|
||||
export const TableCellEditMode = ({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
children,
|
||||
}: TableCellEditModeProps) => (
|
||||
<StyledEditableCellEditModeContainer
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{children}
|
||||
</StyledEditableCellEditModeContainer>
|
||||
);
|
||||
@ -0,0 +1,79 @@
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
|
||||
import { useIsFieldInputOnly } from '@/ui/object/field/hooks/useIsFieldInputOnly';
|
||||
import { useToggleEditOnlyInput } from '@/ui/object/field/hooks/useToggleEditOnlyInput';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellDisplayContainer } from './TableCellDisplayContainer';
|
||||
|
||||
type TableCellSoftFocusModeProps = PropsWithChildren<unknown>;
|
||||
|
||||
export const TableCellSoftFocusMode = ({
|
||||
children,
|
||||
}: TableCellSoftFocusModeProps) => {
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
const toggleEditOnlyInput = useToggleEditOnlyInput();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
}, []);
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
} else {
|
||||
toggleEditOnlyInput();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openTableCell],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'*',
|
||||
(keyboardEvent) => {
|
||||
if (!isFieldInputOnly) {
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
openTableCell();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openTableCell],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellDisplayContainer
|
||||
onClick={handleClick}
|
||||
softFocus
|
||||
scrollRef={scrollRef}
|
||||
>
|
||||
{children}
|
||||
</TableCellDisplayContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
import { ColumnIndexContext } from '../../contexts/ColumnIndexContext';
|
||||
import { RowIndexContext } from '../../contexts/RowIndexContext';
|
||||
import { TableCellPosition } from '../../types/TableCellPosition';
|
||||
|
||||
export const useCurrentTableCellPosition = () => {
|
||||
const currentRowNumber = useContext(RowIndexContext);
|
||||
const currentColumnNumber = useContext(ColumnIndexContext);
|
||||
|
||||
const currentTableCellPosition: TableCellPosition = useMemo(
|
||||
() => ({
|
||||
column: currentColumnNumber,
|
||||
row: currentRowNumber,
|
||||
}),
|
||||
[currentColumnNumber, currentRowNumber],
|
||||
);
|
||||
|
||||
return currentTableCellPosition;
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useMoveEditModeToTableCellPosition } from '../../hooks/useMoveEditModeToCellPosition';
|
||||
import { isTableCellInEditModeFamilyState } from '../../states/isTableCellInEditModeFamilyState';
|
||||
|
||||
import { useCurrentTableCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export const useCurrentTableCellEditMode = () => {
|
||||
const moveEditModeToTableCellPosition = useMoveEditModeToTableCellPosition();
|
||||
|
||||
const currentTableCellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const [isCurrentTableCellInEditMode] = useRecoilState(
|
||||
isTableCellInEditModeFamilyState(currentTableCellPosition),
|
||||
);
|
||||
|
||||
const setCurrentTableCellInEditMode = useCallback(() => {
|
||||
moveEditModeToTableCellPosition(currentTableCellPosition);
|
||||
}, [currentTableCellPosition, moveEditModeToTableCellPosition]);
|
||||
|
||||
return {
|
||||
isCurrentTableCellInEditMode,
|
||||
setCurrentTableCellInEditMode,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isSoftFocusOnTableCellFamilyState } from '../../states/isSoftFocusOnTableCellFamilyState';
|
||||
|
||||
import { useCurrentTableCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export const useIsSoftFocusOnCurrentTableCell = () => {
|
||||
const currentTableCellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const isSoftFocusOnTableCell = useRecoilValue(
|
||||
isSoftFocusOnTableCellFamilyState(currentTableCellPosition),
|
||||
);
|
||||
|
||||
return isSoftFocusOnTableCell;
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
|
||||
import { useSetSoftFocusPosition } from '../../hooks/useSetSoftFocusPosition';
|
||||
import { isSoftFocusActiveState } from '../../states/isSoftFocusActiveState';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentTableCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export const useSetSoftFocusOnCurrentTableCell = () => {
|
||||
const setSoftFocusPosition = useSetSoftFocusPosition();
|
||||
|
||||
const currentTableCellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
setSoftFocusPosition(currentTableCellPosition);
|
||||
|
||||
set(isSoftFocusActiveState, true);
|
||||
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
},
|
||||
[setHotkeyScope, currentTableCellPosition, setSoftFocusPosition],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,68 @@
|
||||
import { useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
|
||||
import { useIsFieldEmpty } from '@/ui/object/field/hooks/useIsFieldEmpty';
|
||||
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
||||
import { ColumnIndexContext } from '../../contexts/ColumnIndexContext';
|
||||
import { useCloseCurrentTableCellInEditMode } from '../../hooks/useCloseCurrentTableCellInEditMode';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentTableCellEditMode } from './useCurrentTableCellEditMode';
|
||||
|
||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
};
|
||||
|
||||
export const useTableCell = () => {
|
||||
const { setCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||
|
||||
const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode();
|
||||
|
||||
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
|
||||
|
||||
const closeTableCell = () => {
|
||||
setDragSelectionStartEnabled(true);
|
||||
closeCurrentTableCellInEditMode();
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isFirstColumnCell = useContext(ColumnIndexContext) === 0;
|
||||
|
||||
const isEmpty = useIsFieldEmpty();
|
||||
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const openTableCell = () => {
|
||||
if (isFirstColumnCell && !isEmpty && fieldDefinition.basePathToShowPage) {
|
||||
navigate(`${fieldDefinition.basePathToShowPage}${entityId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setDragSelectionStartEnabled(false);
|
||||
setCurrentTableCellInEditMode();
|
||||
|
||||
if (customCellHotkeyScope) {
|
||||
setHotkeyScope(
|
||||
customCellHotkeyScope.scope,
|
||||
customCellHotkeyScope.customScopes,
|
||||
);
|
||||
} else {
|
||||
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
closeTableCell,
|
||||
openTableCell,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCreateModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isCreateModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export type AllRowsSelectedStatus = 'none' | 'some' | 'all';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
|
||||
|
||||
export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & {
|
||||
size: number;
|
||||
position: number;
|
||||
isVisible?: boolean;
|
||||
viewFieldId?: string;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export type TableCellPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export type TablePosition = {
|
||||
numberOfRows: number;
|
||||
numberOfColumns: number;
|
||||
};
|
||||
@ -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,3 @@
|
||||
export enum TableOptionsHotkeyScope {
|
||||
Dropdown = 'table-options-dropdown',
|
||||
}
|
||||
Reference in New Issue
Block a user