Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,82 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconX } from '@/ui/display/icon/index';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
type SortOrFilterChipProps = {
labelKey?: string;
labelValue: string;
Icon?: IconComponent;
onRemove: () => void;
isSort?: boolean;
testId?: string;
};
type StyledChipProps = {
isSort?: boolean;
};
const StyledChip = styled.div<StyledChipProps>`
align-items: center;
background-color: ${({ theme }) => theme.accent.quaternary};
border: 1px solid ${({ theme }) => theme.accent.tertiary};
border-radius: 4px;
color: ${({ theme }) => theme.color.blue};
display: flex;
flex-direction: row;
flex-shrink: 0;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')};
padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)};
`;
const StyledIcon = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledDelete = styled.div`
align-items: center;
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
margin-left: ${({ theme }) => theme.spacing(2)};
margin-top: 1px;
user-select: none;
&:hover {
background-color: ${({ theme }) => theme.accent.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;
const StyledLabelKey = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const SortOrFilterChip = ({
labelKey,
labelValue,
Icon,
onRemove,
isSort,
testId,
}: SortOrFilterChipProps) => {
const theme = useTheme();
return (
<StyledChip isSort={isSort}>
{Icon && (
<StyledIcon>
<Icon size={theme.icon.size.sm} />
</StyledIcon>
)}
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{labelValue}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + testId}>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
</StyledChip>
);
};
export default SortOrFilterChip;

View File

@ -0,0 +1,81 @@
import { useCallback } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { UpdateViewDropdownId } from '@/views/constants/UpdateViewDropdownId';
import { useViewBar } from '@/views/hooks/useViewBar';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
const StyledContainer = styled.div`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
export type UpdateViewButtonGroupProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
};
export const UpdateViewButtonGroup = ({
hotkeyScope,
onViewEditModeChange,
}: UpdateViewButtonGroupProps) => {
const { updateCurrentView, setViewEditMode } = useViewBar();
const { canPersistFiltersSelector, canPersistSortsSelector } =
useViewScopedStates();
const canPersistFilters = useRecoilValue(canPersistFiltersSelector);
const canPersistSorts = useRecoilValue(canPersistSortsSelector);
const canPersistView = canPersistFilters || canPersistSorts;
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode('create');
onViewEditModeChange?.();
}, [setViewEditMode, onViewEditModeChange]);
const handleViewSubmit = async () => {
await updateCurrentView?.();
};
if (!canPersistView) {
return <></>;
}
return (
<DropdownScope dropdownScopeId={UpdateViewDropdownId}>
<Dropdown
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledContainer>
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewSubmit} />
<Button size="small" Icon={IconChevronDown} />
</ButtonGroup>
</StyledContainer>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleCreateViewButtonClick}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
</>
}
/>
</DropdownScope>
);
};

View File

@ -0,0 +1,111 @@
import { ReactNode } from 'react';
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewScope } from '@/views/scopes/ViewScope';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewBarEffect } from './ViewBarEffect';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
optionsDropdownScopeId: string;
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
};
export const ViewBar = ({
viewBarId,
className,
optionsDropdownButton,
optionsDropdownScopeId,
onViewFieldsChange,
onViewFiltersChange,
onViewSortsChange,
}: ViewBarProps) => {
const { openDropdown: openOptionsDropdownButton } = useDropdown({
dropdownScopeId: optionsDropdownScopeId,
});
const { upsertViewSort, upsertViewFilter } = useViewBar({
viewBarId: viewBarId,
});
const filterDropdownId = 'view-filter';
const sortDropdownId = 'view-sort';
return (
<ViewScope
viewScopeId={viewBarId}
onViewFieldsChange={onViewFieldsChange}
onViewFiltersChange={onViewFiltersChange}
onViewSortsChange={onViewSortsChange}
>
<ViewBarEffect />
<ViewBarFilterEffect
filterDropdownId={filterDropdownId}
onFilterSelect={upsertViewFilter}
/>
<ViewBarSortEffect
sortDropdownId={sortDropdownId}
onSortSelect={upsertViewSort}
/>
<TopBar
className={className}
leftComponent={
<ViewsDropdownButton
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
optionsDropdownScopeId={optionsDropdownScopeId}
/>
}
displayBottomBorder={false}
rightComponent={
<>
<ObjectFilterDropdownButton
filterDropdownId={filterDropdownId}
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
}}
/>
<ObjectSortDropdownButton
sortDropdownId={sortDropdownId}
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectSortDropdownButton,
}}
/>
{optionsDropdownButton}
</>
}
bottomComponent={
<ViewBarDetails
filterDropdownId={filterDropdownId}
hasFilterButton
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
/>
}
/>
}
/>
</ViewScope>
);
};

View File

@ -0,0 +1,183 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { useViewBar } from '@/views/hooks/useViewBar';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
import SortOrFilterChip from './SortOrFilterChip';
export type ViewBarDetailsProps = {
hasFilterButton?: boolean;
rightComponent?: ReactNode;
filterDropdownId?: string;
};
const StyledBar = styled.div`
align-items: center;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
flex-direction: row;
height: 40px;
justify-content: space-between;
z-index: 4;
`;
const StyledChipcontainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
height: 40px;
justify-content: space-between;
margin-left: ${({ theme }) => theme.spacing(2)};
overflow-x: auto;
`;
const StyledCancelButton = styled.button`
background-color: inherit;
border: none;
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: auto;
margin-right: ${({ theme }) => theme.spacing(2)};
padding: ${(props) => {
const horiz = props.theme.spacing(2);
const vert = props.theme.spacing(1);
return `${vert} ${horiz} ${vert} ${horiz}`;
}};
user-select: none;
&:hover {
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)};
}
`;
const StyledFilterContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledSeperatorContainer = styled.div`
align-items: flex-start;
align-self: stretch;
display: flex;
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledSeperator = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.background.quaternary};
width: 1px;
`;
const StyledAddFilterContainer = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
z-index: 5;
`;
export const ViewBarDetails = ({
hasFilterButton = false,
rightComponent,
filterDropdownId,
}: ViewBarDetailsProps) => {
const {
currentViewSortsState,
currentViewFiltersState,
canPersistFiltersSelector,
canPersistSortsSelector,
isViewBarExpandedState,
} = useViewScopedStates();
const { icons } = useLazyLoadIcons();
const currentViewSorts = useRecoilValue(currentViewSortsState);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
const canPersistFilters = useRecoilValue(canPersistFiltersSelector);
const canPersistSorts = useRecoilValue(canPersistSortsSelector);
const isViewBarExpanded = useRecoilValue(isViewBarExpandedState);
const { resetViewBar, removeViewSort, removeViewFilter } = useViewBar();
const canPersistView = canPersistFilters || canPersistSorts;
const handleCancelClick = () => {
resetViewBar();
};
const shouldExpandViewBar =
canPersistView ||
((currentViewSorts?.length || currentViewFilters?.length) &&
isViewBarExpanded);
if (!shouldExpandViewBar) {
return null;
}
return (
<StyledBar>
<StyledFilterContainer>
<StyledChipcontainer>
{currentViewSorts?.map((sort) => {
return (
<SortOrFilterChip
key={sort.fieldMetadataId}
testId={sort.fieldMetadataId}
labelValue={sort.definition.label}
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
isSort
onRemove={() => removeViewSort(sort.fieldMetadataId)}
/>
);
})}
{!!currentViewSorts?.length && !!currentViewFilters?.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{currentViewFilters?.map((filter) => {
return (
<SortOrFilterChip
key={filter.fieldMetadataId}
testId={filter.fieldMetadataId}
labelKey={filter.definition.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
Icon={icons[filter.definition.iconName]}
onRemove={() => {
removeViewFilter(filter.fieldMetadataId);
}}
/>
);
})}
</StyledChipcontainer>
{hasFilterButton && (
<StyledAddFilterContainer>
<AddObjectFilterFromDetailsButton
filterDropdownId={filterDropdownId}
/>
</StyledAddFilterContainer>
)}
</StyledFilterContainer>
{canPersistView && (
<StyledCancelButton
data-testid="cancel-button"
onClick={handleCancelClick}
>
Reset
</StyledCancelButton>
)}
{rightComponent}
</StyledBar>
);
};

View File

@ -0,0 +1,87 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useViewBar } from '@/views/hooks/useViewBar';
import { GraphQLView } from '@/views/types/GraphQLView';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
export const ViewBarEffect = () => {
const {
loadView,
changeViewInUrl,
loadViewFields,
loadViewFilters,
loadViewSorts,
} = useViewBar();
const [searchParams] = useSearchParams();
const currentViewIdFromUrl = searchParams.get('view');
const {
viewTypeState,
viewObjectMetadataIdState,
viewsState,
currentViewIdState,
} = useViewScopedStates();
const [views, setViews] = useRecoilState(viewsState);
const viewType = useRecoilValue(viewTypeState);
const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState);
const setCurrentViewId = useSetRecoilState(currentViewIdState);
const { records: newViews } = useFindManyRecords<GraphQLView>({
skip: !viewObjectMetadataId,
objectNameSingular: 'view',
filter: {
type: { eq: viewType },
objectMetadataId: { eq: viewObjectMetadataId },
},
});
useEffect(() => {
if (!newViews.length) return;
if (!isDeeplyEqual(views, newViews)) {
setViews(newViews);
}
const currentView =
newViews.find((view) => view.id === currentViewIdFromUrl) ??
newViews[0] ??
null;
if (!currentView) return;
setCurrentViewId(currentView.id);
if (currentView?.viewFields) {
loadViewFields(currentView.viewFields, currentView.id);
loadViewFilters(currentView.viewFilters, currentView.id);
loadViewSorts(currentView.viewSorts, currentView.id);
}
if (!currentViewIdFromUrl) return changeViewInUrl(currentView.id);
}, [
changeViewInUrl,
currentViewIdFromUrl,
loadViewFields,
loadViewFilters,
loadViewSorts,
newViews,
setCurrentViewId,
setViews,
views,
]);
useEffect(() => {
if (!currentViewIdFromUrl) return;
loadView(currentViewIdFromUrl);
}, [currentViewIdFromUrl, loadView]);
return <></>;
};

View File

@ -0,0 +1,70 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
type ViewBarFilterEffectProps = {
filterDropdownId: string;
onFilterSelect?: ((filter: Filter) => void) | undefined;
};
export const ViewBarFilterEffect = ({
filterDropdownId,
onFilterSelect,
}: ViewBarFilterEffectProps) => {
const { availableFilterDefinitionsState, currentViewFiltersState } =
useViewScopedStates();
const availableFilterDefinitions = useRecoilValue(
availableFilterDefinitionsState,
);
const {
setAvailableFilterDefinitions,
setOnFilterSelect,
filterDefinitionUsedInDropdown,
setObjectFilterDropdownSelectedRecordIds,
isObjectFilterDropdownUnfolded,
} = useFilterDropdown({ filterDropdownId: filterDropdownId });
useEffect(() => {
if (availableFilterDefinitions) {
setAvailableFilterDefinitions(availableFilterDefinitions);
}
if (onFilterSelect) {
setOnFilterSelect(() => onFilterSelect);
}
}, [
availableFilterDefinitions,
onFilterSelect,
setAvailableFilterDefinitions,
setOnFilterSelect,
]);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
useEffect(() => {
if (filterDefinitionUsedInDropdown?.type === 'RELATION') {
const viewFilterUsedInDropdown = currentViewFilters.find(
(filter) =>
filter.fieldMetadataId ===
filterDefinitionUsedInDropdown.fieldMetadataId,
);
const viewFilterSelectedRecordIds = JSON.parse(
viewFilterUsedInDropdown?.value ?? '[]',
);
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds);
}
}, [
filterDefinitionUsedInDropdown,
currentViewFilters,
setObjectFilterDropdownSelectedRecordIds,
isObjectFilterDropdownUnfolded,
]);
return <></>;
};

View File

@ -0,0 +1,42 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
type ViewBarSortEffectProps = {
sortDropdownId: string;
onSortSelect?: ((sort: Sort) => void) | undefined;
};
export const ViewBarSortEffect = ({
sortDropdownId,
onSortSelect,
}: ViewBarSortEffectProps) => {
const { availableSortDefinitionsState } = useViewScopedStates();
const availableSortDefinitions = useRecoilValue(
availableSortDefinitionsState,
);
const { setAvailableSortDefinitions, setOnSortSelect } = useSortDropdown({
sortDropdownId,
});
useEffect(() => {
if (availableSortDefinitions) {
setAvailableSortDefinitions(availableSortDefinitions);
}
if (onSortSelect) {
setOnSortSelect(() => onSortSelect);
}
}, [
availableSortDefinitions,
onSortSelect,
setAvailableSortDefinitions,
setOnSortSelect,
]);
return <></>;
};

View File

@ -0,0 +1,152 @@
import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
DropResult,
OnDragEndResponder,
ResponderProvided,
} from '@hello-pangea/dnd';
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { IconMinus, IconPlus } from '@/ui/display/icon';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
import { IconInfoCircle } from '@/ui/input/constants/icons';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
type ViewFieldsVisibilityDropdownSectionProps = {
fields: Omit<ColumnDefinition<FieldMetadata>, 'size'>[];
onVisibilityChange: (
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
) => void;
title: string;
isVisible: boolean;
isDraggable: boolean;
onDragEnd?: OnDragEndResponder;
};
export const ViewFieldsVisibilityDropdownSection = ({
fields,
onVisibilityChange,
title,
isVisible,
isDraggable,
onDragEnd,
}: ViewFieldsVisibilityDropdownSectionProps) => {
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided);
};
const [openToolTipIndex, setOpenToolTipIndex] = useState<number>();
const handleInfoButtonClick = (index: number) => {
if (index === openToolTipIndex) setOpenToolTipIndex(undefined);
else setOpenToolTipIndex(index);
};
const { icons } = useLazyLoadIcons();
const getIconButtons = (
index: number,
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
) => {
if (field.infoTooltipContent) {
return [
{
Icon: IconInfoCircle,
onClick: () => handleInfoButtonClick(index),
isActive: openToolTipIndex === index,
},
{
Icon: field.isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
];
}
if (!field.infoTooltipContent) {
return [
{
Icon: isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
];
}
};
const ref = useRef<HTMLDivElement>(null);
useListenClickOutside({
refs: [ref],
callback: () => {
setOpenToolTipIndex(undefined);
},
});
return (
<div ref={ref}>
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer>
{isDraggable ? (
<DraggableList
onDragEnd={handleOnDrag}
draggableItems={
<>
{[...fields]
.sort((a, b) => a.position - b.position)
.map((field, index) => (
<DraggableItem
key={field.fieldMetadataId}
draggableId={field.fieldMetadataId}
index={index + 1}
itemComponent={
<MenuItemDraggable
key={field.fieldMetadataId}
LeftIcon={icons[field.iconName]}
iconButtons={getIconButtons(index + 1, field)}
isTooltipOpen={openToolTipIndex === index + 1}
text={field.label}
className={`${title}-draggable-item-tooltip-anchor-${
index + 1
}`}
/>
}
/>
))}
</>
}
/>
) : (
fields.map((field, index) => (
<MenuItem
key={field.fieldMetadataId}
LeftIcon={icons[field.iconName]}
iconButtons={getIconButtons(index, field)}
isTooltipOpen={openToolTipIndex === index}
text={field.label}
className={`${title}-fixed-item-tooltip-anchor-${index}`}
/>
))
)}
</DropdownMenuItemsContainer>
{isDefined(openToolTipIndex) &&
createPortal(
<AppTooltip
anchorSelect={`.${title}-${
isDraggable ? 'draggable' : 'fixed'
}-item-tooltip-anchor-${openToolTipIndex}`}
place="left"
content={fields[openToolTipIndex].infoTooltipContent}
isOpen={true}
/>,
document.body,
)}
</div>
);
};

View File

@ -0,0 +1,187 @@
import { MouseEvent } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import {
IconChevronDown,
IconList,
IconPencil,
IconPlus,
IconTrash,
} from '@/ui/display/icon';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useViewBar } from '@/views/hooks/useViewBar';
import { assertNotNull } from '~/utils/assert';
import { ViewsDropdownId } from '../constants/ViewsDropdownId';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledDropdownLabelAdornments = styled.span`
align-items: center;
color: ${({ theme }) => theme.grayScale.gray35};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewIcon = styled(IconList)`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewName = styled.span`
display: inline-block;
max-width: 130px;
@media (max-width: 375px) {
max-width: 90px;
}
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
max-width: 110px;
}
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
`;
export type ViewsDropdownButtonProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
optionsDropdownScopeId: string;
};
export const ViewsDropdownButton = ({
hotkeyScope,
onViewEditModeChange,
optionsDropdownScopeId,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const { removeView, changeViewInUrl } = useViewBar();
const { viewsState, currentViewSelector, entityCountInCurrentViewState } =
useViewScopedStates();
const views = useRecoilValue(viewsState);
const currentView = useRecoilValue(currentViewSelector);
const entityCountInCurrentView = useRecoilValue(
entityCountInCurrentViewState,
);
const { setViewEditMode, setCurrentViewId, loadView } = useViewBar();
const {
isDropdownOpen: isViewsDropdownOpen,
closeDropdown: closeViewsDropdown,
} = useDropdown({
dropdownScopeId: ViewsDropdownId,
});
const { openDropdown: openOptionsDropdown } = useDropdown({
dropdownScopeId: optionsDropdownScopeId,
});
const handleViewSelect = useRecoilCallback(
() => async (viewId: string) => {
changeViewInUrl(viewId);
loadView(viewId);
closeViewsDropdown();
},
[changeViewInUrl, closeViewsDropdown, loadView],
);
const handleAddViewButtonClick = () => {
setViewEditMode('create');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleEditViewButtonClick = (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
changeViewInUrl(viewId);
setCurrentViewId(viewId);
setViewEditMode('edit');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
await removeView(viewId);
closeViewsDropdown();
};
return (
<DropdownScope dropdownScopeId={ViewsDropdownId}>
<Dropdown
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsDropdownOpen}>
<StyledViewIcon size={theme.icon.size.md} />
<StyledViewName>{currentView?.name ?? 'All'}</StyledViewName>
<StyledDropdownLabelAdornments>
· {entityCountInCurrentView}{' '}
<IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments>
</StyledDropdownButtonContainer>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
{views.map((view) => (
<MenuItem
key={view.id}
iconButtons={[
{
Icon: IconPencil,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleEditViewButtonClick(event, view.id),
},
views.length > 1
? {
Icon: IconTrash,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleDeleteViewButtonClick(event, view.id),
}
: null,
].filter(assertNotNull)}
onClick={() => handleViewSelect(view.id)}
LeftIcon={IconList}
text={view.name}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<StyledBoldDropdownMenuItemsContainer>
<MenuItem
onClick={handleAddViewButtonClick}
LeftIcon={IconPlus}
text="Add view"
/>
</StyledBoldDropdownMenuItemsContainer>
</>
}
/>
</DropdownScope>
);
};