feat: reorder columns from table options (#1636)
* draggable prop addition * draggable component addition * state modification * drag select state addition * changed state name * main merged * lint fix --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -234,6 +234,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
title="Visible"
|
||||
fields={visibleBoardCardFields}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={true}
|
||||
/>
|
||||
)}
|
||||
{hasVisibleFields && hasHiddenFields && (
|
||||
@ -244,6 +245,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
title="Hidden"
|
||||
fields={hiddenBoardCardFields}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -42,6 +42,7 @@ export {
|
||||
IconFileImport,
|
||||
IconFileUpload,
|
||||
IconForbid,
|
||||
IconGripVertical,
|
||||
IconHeart,
|
||||
IconHelpCircle,
|
||||
IconInbox,
|
||||
|
||||
@ -14,6 +14,7 @@ export type MenuItemIconButton = {
|
||||
};
|
||||
|
||||
export type MenuItemProps = {
|
||||
isDraggable?: boolean;
|
||||
LeftIcon?: IconComponent | null;
|
||||
accent?: MenuItemAccent;
|
||||
text: string;
|
||||
@ -35,6 +36,7 @@ const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)`
|
||||
`;
|
||||
|
||||
export const MenuItem = ({
|
||||
isDraggable,
|
||||
LeftIcon,
|
||||
accent = 'default',
|
||||
text,
|
||||
@ -52,7 +54,11 @@ export const MenuItem = ({
|
||||
className={className}
|
||||
accent={accent}
|
||||
>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
|
||||
<MenuItemLeftContent
|
||||
isDraggable={isDraggable ? true : false}
|
||||
LeftIcon={LeftIcon ?? undefined}
|
||||
text={text}
|
||||
/>
|
||||
<div className="hoverable-buttons">
|
||||
{showIconButtons && (
|
||||
<FloatingIconButtonGroup iconButtons={iconButtons} />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { IconGripVertical } from '@/ui/icon';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
|
||||
@ -9,15 +10,26 @@ import {
|
||||
} from './StyledMenuItemBase';
|
||||
|
||||
type OwnProps = {
|
||||
isDraggable?: boolean;
|
||||
LeftIcon: IconComponent | null | undefined;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const MenuItemLeftContent = ({ LeftIcon, text }: OwnProps) => {
|
||||
export const MenuItemLeftContent = ({
|
||||
isDraggable,
|
||||
LeftIcon,
|
||||
text,
|
||||
}: OwnProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledMenuItemLeftContent>
|
||||
{isDraggable && (
|
||||
<IconGripVertical
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{LeftIcon && (
|
||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -14,6 +15,7 @@ import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
|
||||
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
|
||||
import { isDraggingAndSelectingState } from '../states/isDraggingAndSelectingState';
|
||||
import { TableHeader } from '../table-header/components/TableHeader';
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
|
||||
@ -88,6 +90,9 @@ type OwnProps = {
|
||||
|
||||
export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
const [isDraggingAndSelecting, setIsDraggingAndSelecting] = useRecoilState(
|
||||
isDraggingAndSelectingState,
|
||||
);
|
||||
|
||||
const setRowSelectedState = useSetRowSelectedState();
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
@ -100,6 +105,7 @@ export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
|
||||
refs: [tableBodyRef],
|
||||
callback: () => {
|
||||
leaveTableFocus();
|
||||
setIsDraggingAndSelecting(true);
|
||||
},
|
||||
});
|
||||
|
||||
@ -132,11 +138,13 @@ export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
|
||||
</StyledTable>
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
<DragSelect
|
||||
dragSelectable={tableBodyRef}
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionChange={setRowSelectedState}
|
||||
/>
|
||||
{isDraggingAndSelecting && (
|
||||
<DragSelect
|
||||
dragSelectable={tableBodyRef}
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionChange={setRowSelectedState}
|
||||
/>
|
||||
)}
|
||||
</StyledTableContainer>
|
||||
</StyledTableWithHeader>
|
||||
</EntityUpdateMutationContext.Provider>
|
||||
|
||||
@ -19,6 +19,19 @@ export const useTableColumns = () => {
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const handleColumnReorder = useCallback(
|
||||
(columns: ColumnDefinition<ViewFieldMetadata>[]) => {
|
||||
const updatedColumnOrder = columns
|
||||
.map((column, index) => {
|
||||
return { ...column, index };
|
||||
})
|
||||
.sort((columnA, columnB) => columnA.index - columnB.index);
|
||||
|
||||
setTableColumns(updatedColumnOrder);
|
||||
},
|
||||
[setTableColumns],
|
||||
);
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(column: ColumnDefinition<ViewFieldMetadata>) => {
|
||||
const nextColumns = tableColumnsByKey[column.key]
|
||||
@ -84,5 +97,6 @@ export const useTableColumns = () => {
|
||||
handleColumnVisibilityChange,
|
||||
handleColumnLeftMove,
|
||||
handleColumnRightMove,
|
||||
handleColumnReorder,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
|
||||
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
|
||||
import { TableOptionsDropdownId } from '@/ui/table/constants/TableOptionsDropdownId';
|
||||
import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState';
|
||||
|
||||
export const TableOptionsDropdownButton = () => {
|
||||
const [, setIsDraggingAndSelecting] = useRecoilState(
|
||||
isDraggingAndSelectingState,
|
||||
);
|
||||
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
|
||||
dropdownId: TableOptionsDropdownId,
|
||||
});
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsDraggingAndSelecting(false);
|
||||
toggleDropdownButton();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownButtonOpen}
|
||||
onClick={toggleDropdownButton}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
Options
|
||||
</StyledHeaderDropdownButton>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useContext, useRef, useState } from 'react';
|
||||
import { useCallback, useContext, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
@ -89,7 +90,8 @@ export const TableOptionsDropdownContent = () => {
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const { handleColumnVisibilityChange } = useTableColumns();
|
||||
const { handleColumnVisibilityChange, handleColumnReorder } =
|
||||
useTableColumns();
|
||||
|
||||
const { upsertView } = useUpsertView();
|
||||
|
||||
@ -115,6 +117,21 @@ export const TableOptionsDropdownContent = () => {
|
||||
setCurrentMenu(option);
|
||||
};
|
||||
|
||||
const handleReorderField: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = Array.from(visibleTableColumns);
|
||||
const [removed] = reorderFields.splice(result.source.index, 1);
|
||||
reorderFields.splice(result.destination.index, 0, removed);
|
||||
|
||||
handleColumnReorder(reorderFields);
|
||||
},
|
||||
[visibleTableColumns, handleColumnReorder],
|
||||
);
|
||||
|
||||
const resetMenu = () => setCurrentMenu(undefined);
|
||||
|
||||
useScopedHotkeys(
|
||||
@ -186,6 +203,8 @@ export const TableOptionsDropdownContent = () => {
|
||||
title="Visible"
|
||||
fields={visibleTableColumns}
|
||||
onVisibilityChange={handleColumnVisibilityChange}
|
||||
isDraggable={true}
|
||||
onDragEnd={handleReorderField}
|
||||
/>
|
||||
{hiddenTableColumns.length > 0 && (
|
||||
<>
|
||||
@ -194,6 +213,7 @@ export const TableOptionsDropdownContent = () => {
|
||||
title="Hidden"
|
||||
fields={hiddenTableColumns}
|
||||
onVisibilityChange={handleColumnVisibilityChange}
|
||||
isDraggable={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isDraggingAndSelectingState = atom<boolean>({
|
||||
key: 'isDraggingAndSelectingState',
|
||||
default: true,
|
||||
});
|
||||
@ -12,6 +12,7 @@ import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScop
|
||||
|
||||
import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId';
|
||||
import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown';
|
||||
import { isDraggingAndSelectingState } from '../../states/isDraggingAndSelectingState';
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
|
||||
import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector';
|
||||
@ -40,6 +41,9 @@ export const TableHeader = () => {
|
||||
const [savedTableColumns, setSavedTableColumns] = useRecoilState(
|
||||
savedTableColumnsFamilyState(currentViewId),
|
||||
);
|
||||
const [, setIsDraggingAndSelecting] = useRecoilState(
|
||||
isDraggingAndSelectingState,
|
||||
);
|
||||
|
||||
const handleViewBarReset = () => setTableColumns(savedTableColumns);
|
||||
|
||||
@ -57,6 +61,7 @@ export const TableHeader = () => {
|
||||
const handleCurrentViewSubmit = async () => {
|
||||
if (canPersistTableColumns) {
|
||||
setSavedTableColumns(tableColumns);
|
||||
setIsDraggingAndSelecting(true);
|
||||
}
|
||||
|
||||
await onCurrentViewSubmit?.();
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconArrowDown, IconArrowUp } from '@/ui/icon/index';
|
||||
import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
@ -115,6 +116,10 @@ export const ViewBarDetails = ({
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setIsDraggingAndSelecting] = useRecoilState(
|
||||
isDraggingAndSelectingState,
|
||||
);
|
||||
|
||||
const savedFilters = useRecoilValue(
|
||||
savedFiltersFamilySelector(currentViewId),
|
||||
);
|
||||
@ -167,6 +172,7 @@ export const ViewBarDetails = ({
|
||||
|
||||
const handleCancelClick = () => {
|
||||
onViewBarReset?.();
|
||||
setIsDraggingAndSelecting(true);
|
||||
setFilters(savedFilters);
|
||||
setSorts(savedSorts);
|
||||
};
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import type {
|
||||
@ -11,31 +21,86 @@ type OwnProps<Field> = {
|
||||
fields: Field[];
|
||||
onVisibilityChange: (field: Field) => void;
|
||||
title: string;
|
||||
isDraggable: boolean;
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
};
|
||||
|
||||
const StyledDropdownMenuItemWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ViewFieldsVisibilityDropdownSection = <
|
||||
Field extends ViewFieldDefinition<ViewFieldMetadata>,
|
||||
>({
|
||||
fields,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
}: OwnProps<Field>) => (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{fields.map((field) => (
|
||||
<MenuItem
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
]}
|
||||
text={field.name}
|
||||
/>
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
}: OwnProps<Field>) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{isDraggable && (
|
||||
<DragDropContext onDragEnd={handleOnDrag}>
|
||||
<StyledDropdownMenuItemWrapper>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{fields.map((field, index) => (
|
||||
<Draggable
|
||||
key={field.key}
|
||||
draggableId={field.key}
|
||||
index={index}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<MenuItem
|
||||
isDraggable={isDraggable}
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
]}
|
||||
text={field.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</StyledDropdownMenuItemWrapper>
|
||||
</DragDropContext>
|
||||
)}
|
||||
{!isDraggable &&
|
||||
fields.map((field) => (
|
||||
<MenuItem
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
]}
|
||||
text={field.name}
|
||||
/>
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user