feat: show/hide table columns (#1078)

Closes #813
This commit is contained in:
Thaïs
2023-08-04 19:44:46 +02:00
committed by GitHub
parent 417ca3d131
commit c6bec40c90
20 changed files with 434 additions and 147 deletions

View File

@ -5,7 +5,6 @@ import { IconPencil } from '@tabler/icons-react';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { icon } from '@/ui/theme/constants/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -46,9 +45,7 @@ export function BoardColumnMenu({
{openMenu === 'actions' && (
<DropdownMenuItemsContainer>
<DropdownMenuSelectableItem onClick={() => setOpenMenu('title')}>
<DropdownButton.StyledIcon>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
</DropdownButton.StyledIcon>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
Rename
</DropdownMenuSelectableItem>
</DropdownMenuItemsContainer>

View File

@ -12,7 +12,7 @@ const StyledIconButtonGroupContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0.5)};
`;
type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
export type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
variant: IconButtonVariant;
size: IconButtonSize;
children: React.ReactElement | React.ReactElement[];

View File

@ -0,0 +1,58 @@
import { ComponentProps, ReactElement } from 'react';
import styled from '@emotion/styled';
const StyledHeader = styled.li`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: calc(${({ theme }) => theme.spacing(2)})
calc(${({ theme }) => theme.spacing(2)});
user-select: none;
${({ onClick, theme }) => {
if (onClick) {
return `
cursor: pointer;
&:hover {
background: ${theme.background.transparent.light};
}
`;
}
}}
`;
const StyledStartIconWrapper = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledEndIconWrapper = styled(StyledStartIconWrapper)`
color: ${({ theme }) => theme.font.color.tertiary};
display: inline-flex;
margin-left: auto;
margin-right: 0;
`;
type DropdownMenuHeaderProps = ComponentProps<'li'> & {
startIcon?: ReactElement;
endIcon?: ReactElement;
};
export const DropdownMenuHeader = ({
children,
startIcon,
endIcon,
...props
}: DropdownMenuHeaderProps) => (
<StyledHeader {...props}>
{startIcon && <StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>}
{children}
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>}
</StyledHeader>
);

View File

@ -1,8 +1,15 @@
import { ComponentProps } from 'react';
import styled from '@emotion/styled';
import {
IconButtonGroup,
type IconButtonGroupProps,
} from '@/ui/button/components/IconButtonGroup';
import { hoverBackground } from '@/ui/theme/constants/effects';
export const DropdownMenuItem = styled.li`
const styledIconButtonGroupClassName = 'dropdown-menu-item-actions';
const StyledItem = styled.li`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
@ -25,6 +32,43 @@ export const DropdownMenuItem = styled.li`
${hoverBackground};
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
&:hover {
.${styledIconButtonGroupClassName} {
display: flex;
}
}
`;
const StyledActions = styled(IconButtonGroup)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
export type DropdownMenuItemProps = ComponentProps<'li'> & {
actions?: IconButtonGroupProps['children'];
};
export const DropdownMenuItem = ({
actions,
children,
...props
}: DropdownMenuItemProps) => (
<StyledItem {...props}>
{children}
{actions && (
<StyledActions
className={styledIconButtonGroupClassName}
variant="transparent"
size="small"
>
{actions}
</StyledActions>
)}
</StyledItem>
);

View File

@ -0,0 +1,11 @@
import styled from '@emotion/styled';
export const DropdownMenuSubheader = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xxs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
text-transform: uppercase;
width: 100%;
`;

View File

@ -2,18 +2,21 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react';
import { IconPlus } from '@/ui/icon/index';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPlus, IconUser } from '@/ui/icon';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { Avatar } from '@/users/components/Avatar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenu } from '../DropdownMenu';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
import { DropdownMenuSearch } from '../DropdownMenuSearch';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
import { DropdownMenuSubheader } from '../DropdownMenuSubheader';
const meta: Meta<typeof DropdownMenu> = {
title: 'UI/Dropdown/DropdownMenu',
@ -59,47 +62,36 @@ const MenuAbsolutePositionWrapper = styled.div`
width: fit-content;
`;
const FakeMenuItemList = () => (
<>
<DropdownMenuItem>Company A</DropdownMenuItem>
<DropdownMenuItem>Company B</DropdownMenuItem>
<DropdownMenuItem>Company C</DropdownMenuItem>
<DropdownMenuItem>Person 2</DropdownMenuItem>
<DropdownMenuItem>Company D</DropdownMenuItem>
<DropdownMenuItem>Person 1</DropdownMenuItem>
</>
);
const mockSelectArray = [
{
id: '1',
name: 'Company A',
avatarUrl: avatarUrl,
avatarUrl,
},
{
id: '2',
name: 'Company B',
avatarUrl: avatarUrl,
avatarUrl,
},
{
id: '3',
name: 'Company C',
avatarUrl: avatarUrl,
avatarUrl,
},
{
id: '4',
name: 'Person 2',
avatarUrl: avatarUrl,
avatarUrl,
},
{
id: '5',
name: 'Company D',
avatarUrl: avatarUrl,
avatarUrl,
},
{
id: '6',
name: 'Person 1',
avatarUrl: avatarUrl,
avatarUrl,
},
];
@ -189,12 +181,77 @@ export const SimpleMenuItem: Story = {
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<FakeMenuItemList />
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
),
};
export const WithHeaders: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuHeader>Header</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 1</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
{mockSelectArray.slice(0, 3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 2</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
{mockSelectArray.slice(3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
),
};
export const WithIcons: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>
<IconUser size={16} />
{name}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
),
};
export const WithActions: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }, index) => (
<DropdownMenuItem
className={index === 0 ? 'hover' : undefined}
actions={[
<IconButton icon={<IconUser />} />,
<IconButton icon={<IconPlus />} />,
]}
>
{name}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
),
parameters: {
pseudo: { hover: ['.hover'] },
},
};
export const LoadingMenu: Story = {
...WithContentBelow,
render: () => (
@ -215,25 +272,9 @@ export const Search: Story = {
<DropdownMenuSearch />
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<FakeMenuItemList />
</DropdownMenuItemsContainer>
</DropdownMenu>
),
};
export const Button: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuItem>
<IconPlus size={16} />
<div>Create new</div>
</DropdownMenuItem>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<FakeSelectableMenuItemList />
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
),

View File

@ -2,7 +2,6 @@ import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { IconChevronDown } from '@/ui/icon/index';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
@ -50,29 +49,6 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
}
`;
const StyledDropdownTopOption = styled.li`
color: ${({ theme }) => theme.font.color.primary};
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: space-between;
padding: calc(${({ theme }) => theme.spacing(2)})
calc(${({ theme }) => theme.spacing(2)});
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
user-select: none;
`;
const StyledIcon = styled.div`
display: flex;
justify-content: center;
margin-right: ${({ theme }) => theme.spacing(1)};
min-width: ${({ theme }) => theme.spacing(4)};
`;
function DropdownButton({
label,
isActive,
@ -117,23 +93,4 @@ function DropdownButton({
);
}
const StyleAngleDownContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 100%;
justify-content: center;
margin-left: auto;
`;
function DropdownTopOptionAngleDown() {
return (
<StyleAngleDownContainer>
<IconChevronDown size={16} />
</StyleAngleDownContainer>
);
}
DropdownButton.StyledDropdownTopOption = StyledDropdownTopOption;
DropdownButton.StyledDropdownTopOptionAngleDown = DropdownTopOptionAngleDown;
DropdownButton.StyledIcon = StyledIcon;
export default DropdownButton;

View File

@ -13,8 +13,6 @@ import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSe
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import DropdownButton from './DropdownButton';
export function FilterDropdownFilterSelect({
context,
}: {
@ -61,9 +59,7 @@ export function FilterDropdownFilterSelect({
setFilterDropdownSearchInput('');
}}
>
<DropdownButton.StyledIcon>
{availableFilter.icon}
</DropdownButton.StyledIcon>
{availableFilter.icon}
{availableFilter.label}
</DropdownMenuSelectableItem>
))}

View File

@ -1,18 +1,21 @@
import { Context } from 'react';
import { useTheme } from '@emotion/react';
import { IconChevronDown } from '@tabler/icons-react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { getOperandLabel } from '../utils/getOperandLabel';
import DropdownButton from './DropdownButton';
export function FilterDropdownOperandButton({
context,
}: {
context: Context<string | null>;
}) {
const theme = useTheme();
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
@ -29,12 +32,12 @@ export function FilterDropdownOperandButton({
}
return (
<DropdownButton.StyledDropdownTopOption
<DropdownMenuHeader
key={'selected-filter-operand'}
endIcon={<IconChevronDown size={theme.icon.size.md} />}
onClick={() => setIsOperandSelectionUnfolded(true)}
>
{getOperandLabel(selectedOperandInDropdown)}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
</DropdownMenuHeader>
);
}

View File

@ -1,5 +1,8 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import { IconChevronDown } from '@tabler/icons-react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
@ -24,10 +27,10 @@ export function SortDropdownButton<SortField>({
onSortSelect,
HotkeyScope,
}: OwnProps<SortField>) {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc');
@ -76,13 +79,12 @@ export function SortDropdownButton<SortField>({
</DropdownMenuItemsContainer>
) : (
<>
<DropdownButton.StyledDropdownTopOption
<DropdownMenuHeader
endIcon={<IconChevronDown size={theme.icon.size.md} />}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
@ -94,9 +96,7 @@ export function SortDropdownButton<SortField>({
onSortItemSelect(sort);
}}
>
<DropdownButton.StyledIcon>
{sort.icon}
</DropdownButton.StyledIcon>
{sort.icon}
{sort.label}
</DropdownMenuSelectableItem>
))}

View File

@ -51,3 +51,4 @@ export { IconUserCircle } from '@tabler/icons-react';
export { IconCalendar } from '@tabler/icons-react';
export { IconPencil } from '@tabler/icons-react';
export { IconCircleDot } from '@tabler/icons-react';
export { IconTag } from '@tabler/icons-react';

View File

@ -1,9 +1,8 @@
import React, { ComponentProps, useRef } from 'react';
import { cloneElement, ComponentProps, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconButtonGroup } from '@/ui/button/components/IconButtonGroup';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
@ -19,23 +18,6 @@ const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledIconButtonGroup = styled(IconButtonGroup)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
const styledIconButtonGroupClassName = 'column-menu-item-icon-button-group';
const StyledColumnMenuItem = styled(DropdownMenuItem)`
position: relative;
&:hover {
.${styledIconButtonGroupClassName} {
display: flex;
}
}
`;
type EntityTableColumnMenuProps = {
onAddViewField: (
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
@ -62,23 +44,21 @@ export const EntityTableColumnMenu = ({
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
{viewFieldDefinitions.map((viewFieldDefinition) => (
<StyledColumnMenuItem key={viewFieldDefinition.id}>
{viewFieldDefinition.columnIcon &&
React.cloneElement(viewFieldDefinition.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
<StyledIconButtonGroup
className={styledIconButtonGroupClassName}
variant="transparent"
size="small"
>
<DropdownMenuItem
key={viewFieldDefinition.id}
actions={
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddViewField(viewFieldDefinition)}
/>
</StyledIconButtonGroup>
</StyledColumnMenuItem>
}
>
{viewFieldDefinition.columnIcon &&
cloneElement(viewFieldDefinition.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</StyledColumnMenu>

View File

@ -19,6 +19,7 @@ import {
addableViewFieldDefinitionsState,
columnWidthByViewFieldIdState,
viewFieldsState,
visibleViewFieldsState,
} from '../states/viewFieldsState';
import type {
ViewFieldDefinition,
@ -87,8 +88,8 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
export function EntityTableHeader() {
const theme = useTheme();
const [{ objectName, viewFields }, setViewFieldsState] =
useRecoilState(viewFieldsState);
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
const viewFields = useRecoilValue(visibleViewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
addableViewFieldDefinitionsState,

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { ViewFieldContext } from '../states/ViewFieldContext';
import { viewFieldsState } from '../states/viewFieldsState';
import { visibleViewFieldsState } from '../states/viewFieldsState';
import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCell';
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const { viewFields } = useRecoilValue(viewFieldsState);
const viewFields = useRecoilValue(visibleViewFieldsState);
return (
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
@ -22,10 +22,7 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
</td>
{viewFields.map((viewField, columnIndex) => {
return (
<ViewFieldContext.Provider
value={viewField}
key={viewField.columnOrder}
>
<ViewFieldContext.Provider value={viewField} key={viewField.id}>
<EntityTableCell cellIndex={columnIndex} />
</ViewFieldContext.Provider>
);

View File

@ -28,7 +28,7 @@ export const toViewFieldInput = (
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: true,
isVisible: viewFieldDefinition.isVisible ?? true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
@ -64,6 +64,7 @@ export const useLoadView = ({
columnLabel: viewField.fieldName,
columnOrder: viewField.index,
columnSize: viewField.sizeInPx,
isVisible: viewField.isVisible,
}));
setViewFieldsState({ objectName, viewFields });

View File

@ -47,3 +47,15 @@ export const addableViewFieldDefinitionsState = selector({
);
},
});
export const visibleViewFieldsState = selector({
key: 'visibleViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => viewField.isVisible),
});
export const hiddenViewFieldsState = selector({
key: 'hiddenViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => !viewField.isVisible),
});

View File

@ -7,6 +7,7 @@ import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownBu
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TopBar } from '@/ui/top-bar/TopBar';
import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton';
import { TableContext } from '../../states/TableContext';
@ -76,6 +77,9 @@ export function TableHeader<SortField>({
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
<OptionsDropdownButton
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
</>
}
bottomComponent={

View File

@ -91,6 +91,7 @@ export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
columnOrder: number;
columnIcon?: JSX.Element;
filterIcon?: JSX.Element;
isVisible?: boolean;
metadata: T;
};

View File

@ -0,0 +1,137 @@
import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
import {
hiddenViewFieldsState,
visibleViewFieldsState,
} from '@/ui/table/states/viewFieldsState';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
import { useUpdateViewFieldMutation } from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '../queries/select';
import { OptionsDropdownSection } from './OptionsDropdownSection';
type OptionsDropdownButtonProps = {
HotkeyScope: FiltersHotkeyScope;
};
enum Option {
Properties = 'Properties',
}
export const OptionsDropdownButton = ({
HotkeyScope,
}: OptionsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined,
);
const visibleFields = useRecoilValue(visibleViewFieldsState);
const hiddenFields = useRecoilValue(hiddenViewFieldsState);
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const handleViewFieldVisibilityChange = useCallback(
(viewFieldId: string, nextIsVisible: boolean) => {
updateViewFieldMutation({
variables: {
data: { isVisible: nextIsVisible },
where: { id: viewFieldId },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[updateViewFieldMutation],
);
const renderFieldActions = useCallback(
(viewField: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!viewField.isVisible || visibleFields.length > 1 ? (
<IconButton
icon={
viewField.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleViewFieldVisibilityChange(viewField.id, !viewField.isVisible)
}
/>
) : undefined,
[handleViewFieldVisibilityChange, theme.icon.size.sm, visibleFields.length],
);
const resetSelectedOption = useCallback(() => {
setSelectedOption(undefined);
}, []);
return (
<DropdownButton
label="Options"
isActive={false}
isUnfolded={isUnfolded}
onIsUnfoldedChange={setIsUnfolded}
HotkeyScope={HotkeyScope}
>
{!selectedOption && (
<>
<DropdownMenuHeader>View settings</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<DropdownMenuItem
onClick={() => setSelectedOption(Option.Properties)}
>
<IconTag size={theme.icon.size.md} />
Properties
</DropdownMenuItem>
</DropdownMenuItemsContainer>
</>
)}
{selectedOption === Option.Properties && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={resetSelectedOption}
>
Properties
</DropdownMenuHeader>
<DropdownMenuSeparator />
<OptionsDropdownSection
renderActions={renderFieldActions}
title="Visible"
viewFields={visibleFields}
/>
{hiddenFields.length > 0 && (
<>
<DropdownMenuSeparator />
<OptionsDropdownSection
renderActions={renderFieldActions}
title="Hidden"
viewFields={hiddenFields}
/>
</>
)}
</>
)}
</DropdownButton>
);
};

View File

@ -0,0 +1,46 @@
import { cloneElement } from 'react';
import { useTheme } from '@emotion/react';
import {
DropdownMenuItem,
DropdownMenuItemProps,
} from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSubheader } from '@/ui/dropdown/components/DropdownMenuSubheader';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
type OptionsDropdownSectionProps = {
renderActions: (
viewField: ViewFieldDefinition<ViewFieldMetadata>,
) => DropdownMenuItemProps['actions'];
title: string;
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
};
export const OptionsDropdownSection = ({
renderActions,
title,
viewFields,
}: OptionsDropdownSectionProps) => {
const theme = useTheme();
return (
<>
<DropdownMenuSubheader>{title}</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
{viewFields.map((viewField) => (
<DropdownMenuItem actions={renderActions(viewField)}>
{viewField.columnIcon &&
cloneElement(viewField.columnIcon, {
size: theme.icon.size.md,
})}
{viewField.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</>
);
};