Refactor UI folder (#2016)

* Added Overview page

* Revised Getting Started page

* Minor revision

* Edited readme, minor modifications to docs

* Removed sweep.yaml, .devcontainer, .ergomake

* Moved security.md to .github, added contributing.md

* changes as per code review

* updated contributing.md

* fixed broken links & added missing links in doc, improved structure

* fixed link in wsl setup

* fixed server link, added https cloning in yarn-setup

* removed package-lock.json

* added doc card, admonitions

* removed underline from nav buttons

* refactoring modules/ui

* refactoring modules/ui

* Change folder case

* Fix theme location

* Fix case 2

* Fix storybook

---------

Co-authored-by: Nimra Ahmed <nimra1408@gmail.com>
Co-authored-by: Nimra Ahmed <50912134+nimraahmed@users.noreply.github.com>
This commit is contained in:
Charles Bochet
2023-10-14 00:04:29 +02:00
committed by GitHub
parent a35ea5e8f9
commit 258685467b
732 changed files with 1106 additions and 1010 deletions

View File

@ -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>
);
};

View File

@ -0,0 +1,51 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { FieldMetadata } from '@/ui/data/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.name}</StyledText>
</StyledTitle>
</>
);
};

View File

@ -0,0 +1,40 @@
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { ColumnDefinition } from '../types/ColumnDefinition';
import { ColumnHead } from './ColumnHead';
import { DataTableColumnDropdownMenu } from './DataTableColumnDropdownMenu';
type ColumnHeadWithDropdownProps = {
column: ColumnDefinition<FieldMetadata>;
isFirstColumn: boolean;
isLastColumn: boolean;
primaryColumnKey: string;
};
export const ColumnHeadWithDropdown = ({
column,
isFirstColumn,
isLastColumn,
primaryColumnKey,
}: ColumnHeadWithDropdownProps) => {
return (
<DropdownScope dropdownScopeId={column.key + '-header'}>
<DropdownMenu
clickableComponent={<ColumnHead column={column} />}
dropdownComponents={
<DataTableColumnDropdownMenu
column={column}
isFirstColumn={isFirstColumn}
isLastColumn={isLastColumn}
primaryColumnKey={primaryColumnKey}
/>
}
dropdownHotkeyScope={{ scope: column.key + '-header' }}
dropdownOffset={{ x: 0, y: -8 }}
/>
</DropdownScope>
);
};

View File

@ -0,0 +1,144 @@
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 { TableHeader } from '../table-header/components/TableHeader';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { DataTableBody } from './DataTableBody';
import { DataTableHeader } from './DataTableHeader';
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;
`;
type DataTableProps = {
updateEntityMutation: (params: any) => void;
};
export const DataTable = ({ updateEntityMutation }: DataTableProps) => {
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 ref={tableBodyRef}>
<TableHeader />
<ScrollWrapper>
<div>
<StyledTable className="entity-table-cell">
<DataTableHeader />
<DataTableBody />
</StyledTable>
</div>
</ScrollWrapper>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={resetTableRowSelection}
onDragSelectionChange={setRowSelectedState}
/>
</StyledTableContainer>
</StyledTableWithHeader>
</EntityUpdateMutationContext.Provider>
);
};

View File

@ -0,0 +1,82 @@
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 { isFetchingDataTableDataState } from '../states/isFetchingDataTableDataState';
import { tableRowIdsState } from '../states/tableRowIdsState';
import { DataTableRow } from './DataTableRow';
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 DataTableBody = () => {
const scrollWrapperRef = useScrollWrapperScopedRef();
const tableRowIds = useRecoilValue(tableRowIdsState);
// eslint-disable-next-line no-console
console.log({ tableRowIds });
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
const isFetchingDataTableData = useRecoilValue(isFetchingDataTableDataState);
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 (isFetchingDataTableData || 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}>
<DataTableRow
key={virtualItem.index}
ref={virtualItem.measureRef}
rowId={rowId}
/>
</RowIndexContext.Provider>
</RowIdContext.Provider>
);
})}
{paddingBottom > 0 && (
<tr>
<StyledSpace bottom={paddingBottom} />
</tr>
)}
</tbody>
);
};

View File

@ -0,0 +1,70 @@
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { FieldContext } from '@/ui/data/field/contexts/FieldContext';
import { isFieldRelation } from '@/ui/data/field/types/guards/isFieldRelation';
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 { 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 DataTableCell = ({ 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);
// eslint-disable-next-line no-console
console.log({ columnDefinition, currentRowId });
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.name,
entityId: currentRowId,
fieldDefinition: columnDefinition,
useUpdateEntityMutation: () => [updateEntityMutation, {}],
hotkeyScope: customHotkeyScope,
}}
>
<TableCell customHotkeyScope={{ scope: customHotkeyScope }} />
</FieldContext.Provider>
</td>
</ColumnIndexContext.Provider>
</RecoilScope>
);
};

View File

@ -0,0 +1,79 @@
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { IconArrowLeft, IconArrowRight, IconEyeOff } from '@/ui/display/icon';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ColumnHeadDropdownId } from '../constants/ColumnHeadDropdownId';
import { useTableColumns } from '../hooks/useTableColumns';
import { ColumnDefinition } from '../types/ColumnDefinition';
export type DataTableColumnDropdownMenuProps = {
column: ColumnDefinition<FieldMetadata>;
isFirstColumn: boolean;
isLastColumn: boolean;
primaryColumnKey: string;
};
export const DataTableColumnDropdownMenu = ({
column,
isFirstColumn,
isLastColumn,
primaryColumnKey,
}: DataTableColumnDropdownMenuProps) => {
const { handleColumnVisibilityChange, handleMoveTableColumn } =
useTableColumns();
const { closeDropdown } = useDropdown({
dropdownScopeId: ColumnHeadDropdownId,
});
const handleColumnMoveLeft = () => {
closeDropdown();
if (isFirstColumn) {
return;
}
handleMoveTableColumn('left', column);
};
const handleColumnMoveRight = () => {
closeDropdown();
if (isLastColumn) {
return;
}
handleMoveTableColumn('right', column);
};
const handleColumnVisibility = () => {
handleColumnVisibilityChange(column);
};
return column.key === primaryColumnKey ? (
<></>
) : (
<StyledDropdownMenu>
<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>
</StyledDropdownMenu>
);
};

View File

@ -0,0 +1,106 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState';
import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState';
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
import { FilterDefinition } from '@/ui/data/view-bar/types/FilterDefinition';
import { SortDefinition } from '@/ui/data/view-bar/types/SortDefinition';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { SortOrder } from '~/generated/graphql';
import { useSetDataTableData } from '../hooks/useSetDataTableData';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
export const DataTableEffect = ({
useGetRequest,
getRequestResultKey,
getRequestOptimisticEffectDefinition,
orderBy = [
{
createdAt: SortOrder.Desc,
},
],
whereFilters,
filterDefinitionArray,
setActionBarEntries,
setContextMenuEntries,
sortDefinitionArray,
}: {
// TODO: type this
useGetRequest: any;
getRequestResultKey: string;
getRequestOptimisticEffectDefinition: OptimisticEffectDefinition<any>;
// TODO: type this and replace with defaultSorts reduce should be applied to defaultSorts in this component not before
orderBy?: any;
// TODO: type this and replace with defaultFilters reduce should be applied to defaultFilters in this component not before
whereFilters?: any;
filterDefinitionArray: FilterDefinition[];
sortDefinitionArray: SortDefinition[];
setActionBarEntries?: () => void;
setContextMenuEntries?: () => void;
}) => {
const setDataTableData = useSetDataTableData();
const { registerOptimisticEffect } = useOptimisticEffect();
useGetRequest({
variables: { orderBy, where: whereFilters },
onCompleted: (data: any) => {
const entities = data[getRequestResultKey] ?? [];
setDataTableData(entities, filterDefinitionArray, sortDefinitionArray);
registerOptimisticEffect({
variables: { orderBy, where: whereFilters },
definition: getRequestOptimisticEffectDefinition,
});
},
});
const [searchParams] = useSearchParams();
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const currentView = await snapshot.getPromise(
currentViewIdScopedState(tableRecoilScopeId),
);
if (currentView === viewId) {
return;
}
const savedFilters = await snapshot.getPromise(
savedFiltersFamilyState(viewId),
);
const savedSorts = await snapshot.getPromise(
savedSortsFamilyState(viewId),
);
set(filtersScopedState(tableRecoilScopeId), savedFilters);
set(sortsScopedState(tableRecoilScopeId), savedSorts);
set(currentViewIdScopedState(tableRecoilScopeId), viewId);
},
[tableRecoilScopeId],
);
useEffect(() => {
const viewId = searchParams.get('view');
if (viewId) {
handleViewSelect(viewId);
}
setActionBarEntries?.();
setContextMenuEntries?.();
}, [
handleViewSelect,
searchParams,
setActionBarEntries,
setContextMenuEntries,
]);
return <></>;
};

View File

@ -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 { 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 { DataTableHeaderPlusButton } from './DataTableHeaderPlusButton';
import { SelectAllCheckbox } from './SelectAllCheckbox';
const COLUMN_MIN_WIDTH = 75;
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 StyledAddIconButtonWrapper = styled.div`
display: inline-flex;
position: relative;
`;
const StyledDataTableColumnMenu = styled(DataTableHeaderPlusButton)`
position: absolute;
right: 0;
top: 100%;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
const StyledTableHead = styled.thead`
cursor: pointer;
`;
const StyledColumnHeadContainer = styled.div`
position: relative;
z-index: 1;
`;
export const DataTableHeader = () => {
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 [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
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.key === resizedFieldKey
? { ...column, size: nextWidth }
: column,
);
await handleColumnsChange(nextColumns);
}
},
[resizedFieldKey, tableColumnsByKey, tableColumns, handleColumnsChange],
);
useTrackPointer({
shouldTrackPointer: resizedFieldKey !== null,
onMouseDown: handleResizeHandlerStart,
onMouseMove: handleResizeHandlerMove,
onMouseUp: handleResizeHandlerEnd,
});
const toggleColumnMenu = useCallback(() => {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const primaryColumn = visibleTableColumns[0];
return (
<StyledTableHead data-select-disable>
<tr>
<th
style={{
width: 30,
minWidth: 30,
maxWidth: 30,
}}
>
<SelectAllCheckbox />
</th>
{visibleTableColumns.map((column, index) => (
<StyledColumnHeaderCell
key={column.key}
isResizing={resizedFieldKey === column.key}
columnWidth={Math.max(
tableColumnsByKey[column.key].size +
(resizedFieldKey === column.key ? resizeFieldOffset : 0),
COLUMN_MIN_WIDTH,
)}
>
<StyledColumnHeadContainer>
<ColumnHeadWithDropdown
column={column}
isFirstColumn={index === 1}
isLastColumn={index === visibleTableColumns.length - 1}
primaryColumnKey={primaryColumn.key}
/>
</StyledColumnHeadContainer>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldKey(column.key);
}}
/>
</StyledColumnHeaderCell>
))}
<th>
{hiddenTableColumns.length > 0 && (
<StyledAddIconButtonWrapper>
<IconButton
size="medium"
variant="tertiary"
Icon={IconPlus}
onClick={toggleColumnMenu}
position="middle"
/>
{isColumnMenuOpen && (
<StyledDataTableColumnMenu
onAddColumn={toggleColumnMenu}
onClickOutside={toggleColumnMenu}
/>
)}
</StyledAddIconButtonWrapper>
)}
</th>
</tr>
</StyledTableHead>
);
};

View File

@ -0,0 +1,71 @@
import { ComponentProps, useCallback, useRef } from 'react';
import styled from '@emotion/styled';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { IconPlus } from '@/ui/display/icon';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableColumns } from '../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
import { ColumnDefinition } from '../types/ColumnDefinition';
const StyledHeaderPlusButton = styled(StyledDropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
type DataTableHeaderPlusButtonProps = {
onAddColumn?: () => void;
onClickOutside?: () => void;
} & ComponentProps<'div'>;
export const DataTableHeaderPlusButton = ({
onAddColumn,
onClickOutside = () => undefined,
}: DataTableHeaderPlusButtonProps) => {
const ref = useRef<HTMLDivElement>(null);
const hiddenTableColumns = useRecoilScopedValue(
hiddenTableColumnsScopedSelector,
TableRecoilScopeContext,
);
useListenClickOutside({
refs: [ref],
callback: onClickOutside,
});
const { handleColumnVisibilityChange } = useTableColumns();
const handleAddColumn = useCallback(
(column: ColumnDefinition<FieldMetadata>) => {
onAddColumn?.();
handleColumnVisibilityChange(column);
},
[handleColumnVisibilityChange, onAddColumn],
);
return (
<StyledHeaderPlusButton ref={ref}>
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<MenuItem
key={column.key}
iconButtons={[
{
Icon: IconPlus,
onClick: () => handleAddColumn(column),
},
]}
LeftIcon={column.Icon}
text={column.name}
/>
))}
</DropdownMenuItemsContainer>
</StyledHeaderPlusButton>
);
};

View File

@ -0,0 +1,53 @@
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 { DataTableCell } from './DataTableCell';
const StyledRow = styled.tr<{ selected: boolean }>`
background: ${(props) =>
props.selected ? props.theme.accent.quaternary : 'none'};
`;
type DataTableRowProps = {
rowId: string;
};
export const DataTableRow = forwardRef<HTMLTableRowElement, DataTableRowProps>(
({ rowId }, ref) => {
const visibleTableColumns = useRecoilScopedValue(
visibleTableColumnsScopedSelector,
TableRecoilScopeContext,
);
const { currentRowSelected } = useCurrentRowSelected();
// eslint-disable-next-line no-console
console.log({ visibleTableColumns });
return (
<StyledRow
ref={ref}
data-testid={`row-id-${rowId}`}
selected={currentRowSelected}
data-selectable-id={rowId}
>
<td>
<CheckboxCell />
</td>
{visibleTableColumns.map((column, columnIndex) => {
return (
<ColumnContext.Provider value={column} key={column.key}>
<DataTableCell cellIndex={columnIndex} />
</ColumnContext.Provider>
);
})}
<td></td>
</StyledRow>
);
},
);

View File

@ -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>
);
};