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:
Aditya Pimpalkar
2023-09-19 23:31:21 +01:00
committed by GitHub
parent 321488ad3c
commit cb05b1fbc9
25 changed files with 979 additions and 33 deletions

View File

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

View File

@ -42,6 +42,7 @@ export {
IconFileImport,
IconFileUpload,
IconForbid,
IconGripVertical,
IconHeart,
IconHelpCircle,
IconInbox,

View File

@ -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} />

View File

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

View File

@ -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>

View File

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

View File

@ -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>

View File

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

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isDraggingAndSelectingState = atom<boolean>({
key: 'isDraggingAndSelectingState',
default: true,
});

View File

@ -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?.();

View File

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

View File

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