feat: Column title menus (#1616)
* view field index to float * gql codegen and package.json * list implementation * db call * reposition logic * lint fix * edge case fix * review changes * handleColumnMove refactor * dropdown recoil scope * rename props * Update server/src/database/migrations/20230727124244_add_view_fields_table/migration.sql --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -12,7 +12,7 @@ import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScop
|
||||
import { DropdownCloseEffect } from './DropdownCloseEffect';
|
||||
|
||||
type OwnProps = {
|
||||
buttonComponents: JSX.Element | JSX.Element[];
|
||||
buttonComponents?: JSX.Element | JSX.Element[];
|
||||
dropdownComponents: JSX.Element | JSX.Element[];
|
||||
dropdownId: string;
|
||||
hotkey?: {
|
||||
@ -75,7 +75,9 @@ export const DropdownButton = ({
|
||||
onHotkeyTriggered={handleHotkeyTriggered}
|
||||
/>
|
||||
)}
|
||||
<div ref={refs.setReference}>{buttonComponents}</div>
|
||||
{buttonComponents && (
|
||||
<div ref={refs.setReference}>{buttonComponents}</div>
|
||||
)}
|
||||
{isDropdownButtonOpen && (
|
||||
<div ref={refs.setFloating} style={floatingStyles}>
|
||||
{dropdownComponents}
|
||||
|
||||
@ -5,6 +5,10 @@ export {
|
||||
IconAlertTriangle,
|
||||
IconArchive,
|
||||
IconArrowBack,
|
||||
IconArrowNarrowDown,
|
||||
IconArrowNarrowLeft,
|
||||
IconArrowNarrowRight,
|
||||
IconArrowNarrowUp,
|
||||
IconArrowDown,
|
||||
IconArrowRight,
|
||||
IconArrowUp,
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
|
||||
|
||||
import { ColumnHeaderDropdownId } from '../constants/ColumnHeaderDropdownId';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
import { EntityTableHeaderOptions } from './EntityTableHeaderOptions';
|
||||
|
||||
type OwnProps = {
|
||||
viewName: string;
|
||||
ViewIcon?: IconComponent;
|
||||
column: ColumnDefinition<ViewFieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
};
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
@ -34,14 +41,30 @@ const StyledText = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ColumnHead = ({ viewName, ViewIcon }: OwnProps) => {
|
||||
export const ColumnHead = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
}: OwnProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { openDropdownButton } = useDropdownButton({
|
||||
dropdownId: ColumnHeaderDropdownId,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTitle>
|
||||
<StyledIcon>
|
||||
{ViewIcon && <ViewIcon size={theme.icon.size.md} />}
|
||||
</StyledIcon>
|
||||
<StyledText>{viewName}</StyledText>
|
||||
</StyledTitle>
|
||||
<>
|
||||
<StyledTitle onClick={openDropdownButton}>
|
||||
<StyledIcon>
|
||||
{column.Icon && <column.Icon size={theme.icon.size.md} />}
|
||||
</StyledIcon>
|
||||
<StyledText>{column.name}</StyledText>
|
||||
</StyledTitle>
|
||||
<EntityTableHeaderOptions
|
||||
column={column}
|
||||
isFirstColumn={isFirstColumn}
|
||||
isLastColumn={isLastColumn}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,8 +3,10 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
@ -163,25 +165,31 @@ export const EntityTableHeader = () => {
|
||||
<SelectAllCheckbox />
|
||||
</th>
|
||||
|
||||
{visibleTableColumns.map((column) => (
|
||||
<StyledColumnHeaderCell
|
||||
key={column.key}
|
||||
isResizing={resizedFieldKey === column.key}
|
||||
columnWidth={Math.max(
|
||||
tableColumnsByKey[column.key].size +
|
||||
(resizedFieldKey === column.key ? resizeFieldOffset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
)}
|
||||
>
|
||||
<ColumnHead viewName={column.name} ViewIcon={column.Icon} />
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldKey(column.key);
|
||||
}}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
{visibleTableColumns.map((column, index) => (
|
||||
<RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}>
|
||||
<StyledColumnHeaderCell
|
||||
key={column.key}
|
||||
isResizing={resizedFieldKey === column.key}
|
||||
columnWidth={Math.max(
|
||||
tableColumnsByKey[column.key].size +
|
||||
(resizedFieldKey === column.key ? resizeFieldOffset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
)}
|
||||
>
|
||||
<ColumnHead
|
||||
column={column}
|
||||
isFirstColumn={index === 0}
|
||||
isLastColumn={index === visibleTableColumns.length - 1}
|
||||
/>
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldKey(column.key);
|
||||
}}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
</RecoilScope>
|
||||
))}
|
||||
<th>
|
||||
{hiddenTableColumns.length > 0 && (
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
||||
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
import { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
|
||||
import {
|
||||
IconArrowNarrowLeft,
|
||||
IconArrowNarrowRight,
|
||||
IconEyeOff,
|
||||
} from '@/ui/icon';
|
||||
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
|
||||
|
||||
import { ColumnHeaderDropdownId } from '../constants/ColumnHeaderDropdownId';
|
||||
import { useTableColumns } from '../hooks/useTableColumns';
|
||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
const StyledDropdownContainer = styled.div`
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
z-index: 1;
|
||||
`;
|
||||
type EntityTableHeaderOptionsProps = {
|
||||
column: ColumnDefinition<ViewFieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
};
|
||||
export const EntityTableHeaderOptions = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
}: EntityTableHeaderOptionsProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
handleColumnVisibilityChange,
|
||||
handleColumnLeftMove,
|
||||
handleColumnRightMove,
|
||||
} = useTableColumns();
|
||||
|
||||
const { closeDropdownButton } = useDropdownButton({
|
||||
dropdownId: ColumnHeaderDropdownId,
|
||||
});
|
||||
|
||||
const handleColumnMoveLeft = () => {
|
||||
closeDropdownButton();
|
||||
if (isFirstColumn) return;
|
||||
else handleColumnLeftMove(column);
|
||||
};
|
||||
|
||||
const handleColumnMoveRight = () => {
|
||||
closeDropdownButton();
|
||||
if (isLastColumn) return;
|
||||
else handleColumnRightMove(column);
|
||||
};
|
||||
|
||||
const handleColumnVisibility = () => {
|
||||
handleColumnVisibilityChange(column);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledDropdownContainer>
|
||||
<DropdownButton
|
||||
dropdownId={ColumnHeaderDropdownId}
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenu>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={() => (
|
||||
<IconArrowNarrowLeft size={theme.icon.size.md} />
|
||||
)}
|
||||
onClick={handleColumnMoveLeft}
|
||||
text="Move left"
|
||||
/>
|
||||
<MenuItem
|
||||
LeftIcon={() => (
|
||||
<IconArrowNarrowRight size={theme.icon.size.md} />
|
||||
)}
|
||||
onClick={handleColumnMoveRight}
|
||||
text="Move right"
|
||||
/>
|
||||
<MenuItem
|
||||
LeftIcon={() => <IconEyeOff size={theme.icon.size.md} />}
|
||||
onClick={handleColumnVisibility}
|
||||
text="Hide"
|
||||
/>
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
}
|
||||
/>
|
||||
</StyledDropdownContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const ColumnHeaderDropdownId = 'table-header-options';
|
||||
@ -36,5 +36,53 @@ export const useTableColumns = () => {
|
||||
[tableColumnsByKey, tableColumns, setTableColumns],
|
||||
);
|
||||
|
||||
return { handleColumnVisibilityChange };
|
||||
const handleColumnMove = useCallback(
|
||||
(direction: string, column: ColumnDefinition<ViewFieldMetadata>) => {
|
||||
const tableColumnIndex = tableColumns.findIndex(
|
||||
(tableColumn) => tableColumn.key === column.key,
|
||||
);
|
||||
if (tableColumnIndex >= 0) {
|
||||
const currentColumn = tableColumns[tableColumnIndex];
|
||||
const targetColumn =
|
||||
direction === 'left'
|
||||
? tableColumns[tableColumnIndex - 1]
|
||||
: tableColumns[tableColumnIndex + 1];
|
||||
const updatedColumns = tableColumns
|
||||
.map((tableColumn) => {
|
||||
switch (tableColumn.key) {
|
||||
case targetColumn.key:
|
||||
return { ...tableColumn, index: currentColumn.index };
|
||||
case currentColumn.key:
|
||||
return { ...tableColumn, index: targetColumn.index };
|
||||
default:
|
||||
return tableColumn;
|
||||
}
|
||||
})
|
||||
.sort((columnA, columnB) => columnA.index - columnB.index);
|
||||
|
||||
setTableColumns(updatedColumns);
|
||||
}
|
||||
},
|
||||
[tableColumns, setTableColumns],
|
||||
);
|
||||
|
||||
const handleColumnLeftMove = useCallback(
|
||||
(column: ColumnDefinition<ViewFieldMetadata>) => {
|
||||
handleColumnMove('left', column);
|
||||
},
|
||||
[handleColumnMove],
|
||||
);
|
||||
|
||||
const handleColumnRightMove = useCallback(
|
||||
(column: ColumnDefinition<ViewFieldMetadata>) => {
|
||||
handleColumnMove('right', column);
|
||||
},
|
||||
[handleColumnMove],
|
||||
);
|
||||
|
||||
return {
|
||||
handleColumnVisibilityChange,
|
||||
handleColumnLeftMove,
|
||||
handleColumnRightMove,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user