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

View File

@ -0,0 +1 @@
export const UpdateViewDropdownId = 'update-view';

View File

@ -0,0 +1 @@
export const ViewsDropdownId = 'views';

View File

@ -0,0 +1,2 @@
// TODO: find a better pattern than using '' as a fallback
export const UNDEFINED_FAMILY_ITEM_ID = '';

View File

@ -0,0 +1,155 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ViewField } from '@/views/types/ViewField';
import { getViewScopedStatesFromSnapshot } from '@/views/utils/getViewScopedStatesFromSnapshot';
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
export const useViewFields = (viewScopeId: string) => {
const { updateOneRecordMutation, createOneRecordMutation } =
useObjectMetadataItem({
objectNameSingular: 'viewField',
});
const { modifyRecordFromCache } = useObjectMetadataItem({
objectNameSingular: 'view',
});
const apolloClient = useApolloClient();
const persistViewFields = useRecoilCallback(
({ snapshot, set }) =>
async (viewFieldsToPersist: ViewField[], viewId?: string) => {
const {
viewObjectMetadataId,
currentViewId,
savedViewFieldsByKey,
onViewFieldsChange,
views,
} = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
viewId,
});
const {
isPersistingViewState,
currentViewFieldsState,
savedViewFieldsState,
} = getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId,
viewId,
});
const viewIdToPersist = viewId ?? currentViewId;
if (!currentViewId || !savedViewFieldsByKey || !viewObjectMetadataId) {
return;
}
const _createViewFields = (viewFieldsToCreate: ViewField[]) => {
if (!viewFieldsToCreate.length) {
return;
}
return Promise.all(
viewFieldsToCreate.map((viewField) =>
apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: {
fieldMetadataId: viewField.fieldMetadataId,
viewId: viewIdToPersist,
isVisible: viewField.isVisible,
size: viewField.size,
position: viewField.position,
},
},
}),
),
);
};
const _updateViewFields = (viewFieldsToUpdate: ViewField[]) => {
if (!viewFieldsToUpdate.length) {
return;
}
return Promise.all(
viewFieldsToUpdate.map((viewField) =>
apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate: viewField.id,
input: {
isVisible: viewField.isVisible,
size: viewField.size,
position: viewField.position,
},
},
}),
),
);
};
const viewFieldsToCreate = viewFieldsToPersist.filter(
(viewField) => !savedViewFieldsByKey[viewField.fieldMetadataId],
);
const viewFieldsToUpdate = viewFieldsToPersist.filter(
(viewFieldToPersit) =>
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId] &&
(savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId].size !==
viewFieldToPersit.size ||
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId]
.position !== viewFieldToPersit.position ||
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId]
.isVisible !== viewFieldToPersit.isVisible),
);
set(isPersistingViewState, true);
await _createViewFields(viewFieldsToCreate);
await _updateViewFields(viewFieldsToUpdate);
set(isPersistingViewState, false);
set(currentViewFieldsState, viewFieldsToPersist);
set(savedViewFieldsState, viewFieldsToPersist);
const existingView = views.find((view) => view.id === viewIdToPersist);
if (!existingView) {
return;
}
modifyRecordFromCache(viewIdToPersist ?? '', {
viewFields: () => ({
edges: viewFieldsToPersist.map((viewField) => ({
node: viewField,
cursor: '',
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
}),
});
onViewFieldsChange?.(viewFieldsToPersist);
},
[
viewScopeId,
modifyRecordFromCache,
apolloClient,
createOneRecordMutation,
updateOneRecordMutation,
],
);
return { persistViewFields };
};

View File

@ -0,0 +1,252 @@
import { useApolloClient } from '@apollo/client';
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { savedViewFiltersScopedFamilyState } from '@/views/states/savedViewFiltersScopedFamilyState';
import { ViewFilter } from '@/views/types/ViewFilter';
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
import { useViewScopedStates } from './useViewScopedStates';
export const useViewFilters = (viewScopeId: string) => {
const {
updateOneRecordMutation,
createOneRecordMutation,
deleteOneRecordMutation,
} = useObjectMetadataItem({
objectNameSingular: 'viewFilter',
});
const { modifyRecordFromCache } = useObjectMetadataItem({
objectNameSingular: 'view',
});
const apolloClient = useApolloClient();
const { currentViewFiltersState } = useViewScopedStates({
viewScopeId: viewScopeId,
});
const persistViewFilters = useRecoilCallback(
({ snapshot, set }) =>
async (viewId?: string) => {
const {
currentViewId,
currentViewFilters,
savedViewFiltersByKey,
views,
} = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
if (!currentViewFilters) {
return;
}
if (!savedViewFiltersByKey) {
return;
}
const createViewFilters = (viewFiltersToCreate: ViewFilter[]) => {
if (!viewFiltersToCreate.length) return;
return Promise.all(
viewFiltersToCreate.map((viewFilter) =>
apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: {
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: viewId ?? currentViewId,
value: viewFilter.value,
displayValue: viewFilter.displayValue,
operand: viewFilter.operand,
},
},
}),
),
);
};
const updateViewFilters = (viewFiltersToUpdate: ViewFilter[]) => {
if (!viewFiltersToUpdate.length) return;
return Promise.all(
viewFiltersToUpdate.map((viewFilter) =>
apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate: viewFilter.id,
input: {
value: viewFilter.value,
displayValue: viewFilter.displayValue,
operand: viewFilter.operand,
},
},
}),
),
);
};
const deleteViewFilters = (viewFilterIdsToDelete: string[]) => {
if (!viewFilterIdsToDelete.length) return;
return Promise.all(
viewFilterIdsToDelete.map((viewFilterId) =>
apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete: viewFilterId,
},
}),
),
);
};
const filtersToCreate = currentViewFilters.filter(
(filter) => !savedViewFiltersByKey[filter.fieldMetadataId],
);
await createViewFilters(filtersToCreate);
const filtersToUpdate = currentViewFilters.filter(
(filter) =>
savedViewFiltersByKey[filter.fieldMetadataId] &&
(savedViewFiltersByKey[filter.fieldMetadataId].operand !==
filter.operand ||
savedViewFiltersByKey[filter.fieldMetadataId].value !==
filter.value),
);
await updateViewFilters(filtersToUpdate);
const filterKeys = currentViewFilters.map(
(filter) => filter.fieldMetadataId,
);
const filterKeysToDelete = Object.keys(savedViewFiltersByKey).filter(
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
);
const filterIdsToDelete = filterKeysToDelete.map(
(filterKeyToDelete) =>
savedViewFiltersByKey[filterKeyToDelete].id ?? '',
);
await deleteViewFilters(filterIdsToDelete);
set(
savedViewFiltersScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId ?? currentViewId,
}),
currentViewFilters,
);
const existingViewId = viewId ?? currentViewId;
const existingView = views.find((view) => view.id === existingViewId);
if (!existingView) {
return;
}
modifyRecordFromCache(existingViewId, {
viewFilters: () => ({
edges: currentViewFilters.map((viewFilter) => ({
node: viewFilter,
cursor: '',
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
}),
});
},
[
apolloClient,
createOneRecordMutation,
deleteOneRecordMutation,
modifyRecordFromCache,
updateOneRecordMutation,
viewScopeId,
],
);
const upsertViewFilter = useRecoilCallback(
({ snapshot, set }) =>
(filterToUpsert: Filter) => {
const { currentViewId, savedViewFiltersByKey, onViewFiltersChange } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
if (!savedViewFiltersByKey) {
return;
}
const existingSavedFilterId =
savedViewFiltersByKey[filterToUpsert.fieldMetadataId]?.id;
set(currentViewFiltersState, (filters) => {
const newViewFilters = produce(filters, (filtersDraft) => {
const existingFilterIndex = filtersDraft.findIndex(
(filter) =>
filter.fieldMetadataId === filterToUpsert.fieldMetadataId,
);
if (existingFilterIndex === -1 && filterToUpsert.value !== '') {
filtersDraft.push({
...filterToUpsert,
id: existingSavedFilterId,
});
return filtersDraft;
}
if (filterToUpsert.value === '') {
filtersDraft.splice(existingFilterIndex, 1);
return filtersDraft;
}
filtersDraft[existingFilterIndex] = {
...filterToUpsert,
id: existingSavedFilterId,
};
});
onViewFiltersChange?.(newViewFilters);
return newViewFilters;
});
},
[currentViewFiltersState, viewScopeId],
);
const removeViewFilter = useRecoilCallback(
({ snapshot, set }) =>
(fieldMetadataId: string) => {
const { currentViewId, currentViewFilters, onViewFiltersChange } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
const newViewFilters = currentViewFilters.filter((filter) => {
return filter.fieldMetadataId !== fieldMetadataId;
});
set(currentViewFiltersState, newViewFilters);
onViewFiltersChange?.(newViewFilters);
},
[currentViewFiltersState, viewScopeId],
);
return { persistViewFilters, removeViewFilter, upsertViewFilter };
};

View File

@ -0,0 +1,86 @@
import { useRecoilState } from 'recoil';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
import { UNDEFINED_FAMILY_ITEM_ID } from '../../constants';
import { ViewScopeInternalContext } from '../../scopes/scope-internal-context/ViewScopeInternalContext';
import { currentViewIdScopedState } from '../../states/currentViewIdScopedState';
import { getViewScopedStates } from '../../utils/internal/getViewScopedStates';
export const useViewScopedStates = (args?: { viewScopeId?: string }) => {
const { viewScopeId } = args ?? {};
const scopeId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
viewScopeId,
);
// View
const [currentViewId] = useRecoilState(
getScopedState(currentViewIdScopedState, scopeId),
);
const viewId = currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
const {
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
canPersistFiltersSelector,
canPersistSortsSelector,
currentViewFieldsState,
currentViewFiltersState,
currentViewIdState,
currentViewSelector,
currentViewSortsState,
entityCountInCurrentViewState,
isViewBarExpandedState,
isPersistingViewState,
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
savedViewFieldsByKeySelector,
savedViewFieldsState,
savedViewFiltersByKeySelector,
savedViewFiltersState,
savedViewSortsByKeySelector,
savedViewSortsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
viewsState,
} = getViewScopedStates({
viewScopeId: scopeId,
viewId,
});
return {
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
canPersistFiltersSelector,
canPersistSortsSelector,
currentViewFieldsState,
currentViewFiltersState,
currentViewIdState,
currentViewSelector,
currentViewSortsState,
entityCountInCurrentViewState,
isViewBarExpandedState,
isPersistingViewState,
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
savedViewFieldsByKeySelector,
savedViewFieldsState,
savedViewFiltersByKeySelector,
savedViewFiltersState,
savedViewSortsByKeySelector,
savedViewSortsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
viewsState,
};
};

View File

@ -0,0 +1,230 @@
import { useApolloClient } from '@apollo/client';
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { savedViewSortsScopedFamilyState } from '@/views/states/savedViewSortsScopedFamilyState';
import { ViewSort } from '@/views/types/ViewSort';
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
import { useViewScopedStates } from './useViewScopedStates';
export const useViewSorts = (viewScopeId: string) => {
const {
updateOneRecordMutation,
createOneRecordMutation,
deleteOneRecordMutation,
} = useObjectMetadataItem({
objectNameSingular: 'viewSort',
});
const { modifyRecordFromCache } = useObjectMetadataItem({
objectNameSingular: 'view',
});
const apolloClient = useApolloClient();
const { currentViewSortsState } = useViewScopedStates({
viewScopeId: viewScopeId,
});
const persistViewSorts = useRecoilCallback(
({ snapshot, set }) =>
async (viewId?: string) => {
const { currentViewId, currentViewSorts, savedViewSortsByKey, views } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
if (!currentViewSorts) {
return;
}
if (!savedViewSortsByKey) {
return;
}
const createViewSorts = (viewSortsToCreate: ViewSort[]) => {
if (!viewSortsToCreate.length) return;
return Promise.all(
viewSortsToCreate.map((viewSort) =>
apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: {
fieldMetadataId: viewSort.fieldMetadataId,
viewId: viewId ?? currentViewId,
direction: viewSort.direction,
},
},
}),
),
);
};
const updateViewSorts = (viewSortsToUpdate: ViewSort[]) => {
if (!viewSortsToUpdate.length) return;
return Promise.all(
viewSortsToUpdate.map((viewSort) =>
apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate: viewSort.id,
input: {
direction: viewSort.direction,
},
},
}),
),
);
};
const deleteViewSorts = (viewSortIdsToDelete: string[]) => {
if (!viewSortIdsToDelete.length) return;
return Promise.all(
viewSortIdsToDelete.map((viewSortId) =>
apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete: viewSortId,
},
}),
),
);
};
const sortsToCreate = currentViewSorts.filter(
(sort) => !savedViewSortsByKey[sort.fieldMetadataId],
);
await createViewSorts(sortsToCreate);
const sortsToUpdate = currentViewSorts.filter(
(sort) =>
savedViewSortsByKey[sort.fieldMetadataId] &&
savedViewSortsByKey[sort.fieldMetadataId].direction !==
sort.direction,
);
await updateViewSorts(sortsToUpdate);
const sortKeys = currentViewSorts.map((sort) => sort.fieldMetadataId);
const sortKeysToDelete = Object.keys(savedViewSortsByKey).filter(
(previousSortKey) => !sortKeys.includes(previousSortKey),
);
const sortIdsToDelete = sortKeysToDelete.map(
(sortKeyToDelete) => savedViewSortsByKey[sortKeyToDelete].id ?? '',
);
await deleteViewSorts(sortIdsToDelete);
set(
savedViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId ?? currentViewId,
}),
currentViewSorts,
);
const existingViewId = viewId ?? currentViewId;
const existingView = views.find((view) => view.id === existingViewId);
if (!existingView) {
return;
}
modifyRecordFromCache(existingViewId, {
viewSorts: () => ({
edges: currentViewSorts.map((viewSort) => ({
node: viewSort,
cursor: '',
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
}),
});
},
[
apolloClient,
createOneRecordMutation,
deleteOneRecordMutation,
modifyRecordFromCache,
updateOneRecordMutation,
viewScopeId,
],
);
const upsertViewSort = useRecoilCallback(
({ snapshot, set }) =>
(sortToUpsert: Sort) => {
const { currentViewId, onViewSortsChange, savedViewSortsByKey } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
if (!savedViewSortsByKey) {
return;
}
const existingSavedSortId =
savedViewSortsByKey[sortToUpsert.fieldMetadataId]?.id;
set(currentViewSortsState, (sorts) => {
const newViewSorts = produce(sorts, (sortsDraft) => {
const existingSortIndex = sortsDraft.findIndex(
(sort) => sort.fieldMetadataId === sortToUpsert.fieldMetadataId,
);
if (existingSortIndex === -1) {
sortsDraft.push({ ...sortToUpsert, id: existingSavedSortId });
return sortsDraft;
}
sortsDraft[existingSortIndex] = {
...sortToUpsert,
id: existingSavedSortId,
};
});
onViewSortsChange?.(newViewSorts);
return newViewSorts;
});
},
[currentViewSortsState, viewScopeId],
);
const removeViewSort = useRecoilCallback(
({ snapshot, set }) =>
(fieldMetadataId: string) => {
const { currentViewId, onViewSortsChange, currentViewSorts } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId,
});
if (!currentViewId) {
return;
}
const newViewSorts = currentViewSorts.filter((filter) => {
return filter.fieldMetadataId !== fieldMetadataId;
});
set(currentViewSortsState, newViewSorts);
onViewSortsChange?.(newViewSorts);
},
[currentViewSortsState, viewScopeId],
);
return { persistViewSorts, upsertViewSort, removeViewSort };
};

View File

@ -0,0 +1,78 @@
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { GraphQLView } from '@/views/types/GraphQLView';
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
export const useViews = (scopeId: string) => {
const {
updateOneRecordMutation: updateOneMutation,
createOneRecordMutation: createOneMutation,
deleteOneRecordMutation: deleteOneMutation,
findManyRecordsQuery: findManyQuery,
} = useObjectMetadataItem({
objectNameSingular: 'view',
});
const apolloClient = useApolloClient();
const createView = useRecoilCallback(
({ snapshot }) =>
async (view: Pick<GraphQLView, 'id' | 'name'>) => {
const { viewObjectMetadataId, viewType } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
if (!viewObjectMetadataId || !viewType) {
return;
}
await apolloClient.mutate({
mutation: createOneMutation,
variables: {
input: {
id: view.id,
name: view.name,
objectMetadataId: viewObjectMetadataId,
type: viewType,
},
},
refetchQueries: [findManyQuery],
});
},
[scopeId, apolloClient, createOneMutation, findManyQuery],
);
const updateView = async (view: GraphQLView) => {
await apolloClient.mutate({
mutation: updateOneMutation,
variables: {
idToUpdate: view.id,
input: {
id: view.id,
name: view.name,
},
},
refetchQueries: [findManyQuery],
});
};
const deleteView = async (viewId: string) => {
await apolloClient.mutate({
mutation: deleteOneMutation,
variables: {
idToDelete: viewId,
},
refetchQueries: [findManyQuery],
});
};
return {
createView,
deleteView,
isFetchingViews: false,
updateView,
};
};

View File

@ -0,0 +1,39 @@
export const useMoveViewColumns = () => {
const handleColumnMove = <T extends { position: number }>(
direction: 'left' | 'right',
currentArrayindex: number,
targetArray: T[],
) => {
const targetArrayIndex =
direction === 'left' ? currentArrayindex - 1 : currentArrayindex + 1;
const targetArraySize = targetArray.length - 1;
if (
currentArrayindex >= 0 &&
targetArrayIndex >= 0 &&
currentArrayindex <= targetArraySize &&
targetArrayIndex <= targetArraySize
) {
const currentEntity = targetArray[currentArrayindex];
const targetEntity = targetArray[targetArrayIndex];
const newArray = [...targetArray];
newArray[currentArrayindex] = {
...targetEntity,
index: currentEntity.position,
};
newArray[targetArrayIndex] = {
...currentEntity,
index: targetEntity.position,
};
return newArray.map((column, index) => ({
...column,
position: index,
}));
}
return targetArray;
};
return { handleColumnMove };
};

View File

@ -0,0 +1,447 @@
import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
import { currentViewSortsScopedFamilyState } from '../states/currentViewSortsScopedFamilyState';
import { getViewScopedStatesFromSnapshot } from '../utils/getViewScopedStatesFromSnapshot';
import { getViewScopedStateValuesFromSnapshot } from '../utils/getViewScopedStateValuesFromSnapshot';
import { useViewFields } from './internal/useViewFields';
import { useViewFilters } from './internal/useViewFilters';
import { useViews } from './internal/useViews';
import { useViewScopedStates } from './internal/useViewScopedStates';
import { useViewSorts } from './internal/useViewSorts';
type UseViewProps = {
viewBarId?: string;
};
export const useViewBar = (props?: UseViewProps) => {
const scopeId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
props?.viewBarId,
);
const {
currentViewFiltersState,
currentViewIdState,
currentViewSortsState,
viewEditModeState,
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
entityCountInCurrentViewState,
viewObjectMetadataIdState,
viewTypeState,
} = useViewScopedStates({
viewScopeId: scopeId,
});
const { persistViewSorts, upsertViewSort, removeViewSort } =
useViewSorts(scopeId);
const { persistViewFilters, upsertViewFilter, removeViewFilter } =
useViewFilters(scopeId);
const { persistViewFields } = useViewFields(scopeId);
const {
createView: internalCreateView,
updateView: internalUpdateView,
deleteView: internalDeleteView,
} = useViews(scopeId);
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
const setAvailableFieldDefinitions = useSetRecoilState(
availableFieldDefinitionsState,
);
const setAvailableSortDefinitions = useSetRecoilState(
availableSortDefinitionsState,
);
const setAvailableFilterDefinitions = useSetRecoilState(
availableFilterDefinitionsState,
);
const setEntityCountInCurrentView = useSetRecoilState(
entityCountInCurrentViewState,
);
const setViewEditMode = useSetRecoilState(viewEditModeState);
const setViewObjectMetadataId = useSetRecoilState(viewObjectMetadataIdState);
const setViewType = useSetRecoilState(viewTypeState);
const [_, setSearchParams] = useSearchParams();
const changeViewInUrl = useCallback(
(viewId: string) => {
setSearchParams({ view: viewId });
},
[setSearchParams],
);
const loadViewFields = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<ViewField>,
currentViewId: string,
) => {
const {
availableFieldDefinitions,
onViewFieldsChange,
savedViewFields,
isPersistingView,
} = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
const { savedViewFieldsState, currentViewFieldsState } =
getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
if (!availableFieldDefinitions) {
return;
}
const queriedViewFields = data.edges
.map((viewField) => viewField.node)
.filter(assertNotNull);
if (isPersistingView) {
return;
}
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
set(currentViewFieldsState, queriedViewFields);
set(savedViewFieldsState, queriedViewFields);
onViewFieldsChange?.(queriedViewFields);
}
},
[scopeId],
);
const loadViewFilters = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<Required<ViewFilter>>,
currentViewId: string,
) => {
const {
availableFilterDefinitions,
savedViewFilters,
onViewFiltersChange,
} = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
const { savedViewFiltersState, currentViewFiltersState } =
getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
if (!availableFilterDefinitions) {
return;
}
const queriedViewFilters = data.edges
.map(({ node }) => {
const availableFilterDefinition = availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId === node.fieldMetadataId,
);
if (!availableFilterDefinition) return null;
return {
...node,
displayValue: node.displayValue ?? node.value,
definition: availableFilterDefinition,
};
})
.filter(assertNotNull);
if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) {
set(savedViewFiltersState, queriedViewFilters);
set(currentViewFiltersState, queriedViewFilters);
}
onViewFiltersChange?.(queriedViewFilters);
},
[scopeId],
);
const loadViewSorts = useRecoilCallback(
({ snapshot, set }) =>
async (
data: PaginatedRecordTypeResults<Required<ViewSort>>,
currentViewId: string,
) => {
const { availableSortDefinitions, savedViewSorts, onViewSortsChange } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
const { savedViewSortsState, currentViewSortsState } =
getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId: currentViewId,
});
if (!availableSortDefinitions || !currentViewId) {
return;
}
const queriedViewSorts = data.edges
.map(({ node }) => {
const availableSortDefinition = availableSortDefinitions.find(
(sort) => sort.fieldMetadataId === node.fieldMetadataId,
);
if (!availableSortDefinition) return null;
return {
id: node.id,
fieldMetadataId: node.fieldMetadataId,
direction: node.direction,
definition: availableSortDefinition,
};
})
.filter(assertNotNull);
if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) {
set(savedViewSortsState, queriedViewSorts);
set(currentViewSortsState, queriedViewSorts);
}
onViewSortsChange?.(queriedViewSorts);
},
[scopeId],
);
const loadView = useRecoilCallback(
({ snapshot }) =>
(viewId: string) => {
setCurrentViewId?.(viewId);
const { currentView } = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
viewId,
});
if (!currentView) {
return;
}
loadViewFields(currentView.viewFields, viewId);
loadViewFilters(currentView.viewFilters, viewId);
loadViewSorts(currentView.viewSorts, viewId);
},
[setCurrentViewId, scopeId, loadViewFields, loadViewFilters, loadViewSorts],
);
const resetViewBar = useRecoilCallback(
({ snapshot, set }) =>
() => {
const {
savedViewFilters,
savedViewSorts,
onViewFiltersChange,
onViewSortsChange,
} = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
if (savedViewFilters) {
set(currentViewFiltersState, savedViewFilters);
onViewFiltersChange?.(savedViewFilters);
}
if (savedViewSorts) {
set(currentViewSortsState, savedViewSorts);
onViewSortsChange?.(savedViewSorts);
}
set(viewEditModeState, 'none');
},
[
currentViewFiltersState,
currentViewSortsState,
scopeId,
viewEditModeState,
],
);
const createView = useRecoilCallback(
({ snapshot, set }) =>
async (name: string) => {
const newViewId = v4();
await internalCreateView({ id: newViewId, name });
const { currentViewFields, currentViewFilters, currentViewSorts } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
set(
currentViewFieldsScopedFamilyState({ scopeId, familyKey: newViewId }),
currentViewFields,
);
set(
currentViewFiltersScopedFamilyState({
scopeId,
familyKey: newViewId,
}),
currentViewFilters,
);
set(
currentViewSortsScopedFamilyState({
scopeId,
familyKey: newViewId,
}),
currentViewSorts,
);
await persistViewFields(currentViewFields, newViewId);
await persistViewFilters(newViewId);
await persistViewSorts(newViewId);
changeViewInUrl(newViewId);
},
[
changeViewInUrl,
internalCreateView,
persistViewFields,
persistViewFilters,
persistViewSorts,
scopeId,
],
);
const updateCurrentView = async () => {
await persistViewFilters();
await persistViewSorts();
};
const removeView = useRecoilCallback(
({ set, snapshot }) =>
async (viewIdToDelete: string) => {
const { currentViewId } = getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
const { currentViewIdState, viewsState } =
getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
if (currentViewId === viewIdToDelete) {
set(currentViewIdState, undefined);
}
set(viewsState, (previousViews) =>
previousViews.filter((view) => view.id !== viewIdToDelete),
);
internalDeleteView(viewIdToDelete);
if (currentViewId === viewIdToDelete) {
setSearchParams();
}
},
[internalDeleteView, scopeId, setSearchParams],
);
const handleViewNameSubmit = useRecoilCallback(
({ snapshot }) =>
async (name?: string) => {
if (!name) {
return;
}
const { viewEditMode, currentView } =
getViewScopedStateValuesFromSnapshot({
snapshot,
viewScopeId: scopeId,
});
if (!currentView) {
return;
}
if (viewEditMode === 'create' && name) {
await createView(name);
// Temporary to force refetch
await internalUpdateView({
...currentView,
});
} else {
await internalUpdateView({
...currentView,
name,
});
}
},
[createView, internalUpdateView, scopeId],
);
return {
scopeId,
currentViewId,
setCurrentViewId,
updateCurrentView,
createView,
removeView,
resetViewBar,
handleViewNameSubmit,
setViewEditMode,
setViewObjectMetadataId,
setViewType,
setEntityCountInCurrentView,
setAvailableFieldDefinitions,
setAvailableSortDefinitions,
upsertViewSort,
removeViewSort,
setAvailableFilterDefinitions,
upsertViewFilter,
removeViewFilter,
persistViewFields,
changeViewInUrl,
loadView,
loadViewFields,
loadViewFilters,
loadViewSorts,
};
};

View File

@ -0,0 +1,41 @@
import { ReactNode } from 'react';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
import { ViewField } from '../types/ViewField';
import { ViewScopeInitEffect } from './init-effect/ViewScopeInitEffect';
import { ViewScopeInternalContext } from './scope-internal-context/ViewScopeInternalContext';
type ViewScopeProps = {
children: ReactNode;
viewScopeId: string;
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
};
export const ViewScope = ({
children,
viewScopeId,
onViewSortsChange,
onViewFiltersChange,
onViewFieldsChange,
}: ViewScopeProps) => {
return (
<ViewScopeInternalContext.Provider
value={{
scopeId: viewScopeId,
}}
>
<ViewScopeInitEffect
viewScopeId={viewScopeId}
onViewSortsChange={onViewSortsChange}
onViewFiltersChange={onViewFiltersChange}
onViewFieldsChange={onViewFieldsChange}
/>
{children}
</ViewScopeInternalContext.Provider>
);
};

View File

@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
type ViewScopeInitEffectProps = {
viewScopeId: string;
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
};
export const ViewScopeInitEffect = ({
onViewSortsChange,
onViewFiltersChange,
onViewFieldsChange,
}: ViewScopeInitEffectProps) => {
const {
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
} = useViewScopedStates();
const setOnViewSortsChange = useSetRecoilState(onViewSortsChangeState);
const setOnViewFiltersChange = useSetRecoilState(onViewFiltersChangeState);
const setOnViewFieldsChange = useSetRecoilState(onViewFieldsChangeState);
useEffect(() => {
setOnViewSortsChange(() => onViewSortsChange);
setOnViewFiltersChange(() => onViewFiltersChange);
setOnViewFieldsChange(() => onViewFieldsChange);
}, [
onViewFieldsChange,
onViewFiltersChange,
onViewSortsChange,
setOnViewFieldsChange,
setOnViewFiltersChange,
setOnViewSortsChange,
]);
return <></>;
};

View File

@ -0,0 +1,7 @@
import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey';
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
type ViewScopeInternalContextProps = ScopedStateKey;
export const ViewScopeInternalContext =
createScopeInternalContext<ViewScopeInternalContextProps>();

View File

@ -0,0 +1,10 @@
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const availableFieldDefinitionsScopedState = createScopedState<
ColumnDefinition<FieldMetadata>[]
>({
key: 'availableFieldDefinitionsScopedState',
defaultValue: [],
});

View File

@ -0,0 +1,9 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const availableFilterDefinitionsScopedState = createScopedState<
FilterDefinition[]
>({
key: 'availableFilterDefinitionsScopedState',
defaultValue: [],
});

View File

@ -0,0 +1,9 @@
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const availableSortDefinitionsScopedState = createScopedState<
SortDefinition[]
>({
key: 'availableSortDefinitionsScopedState',
defaultValue: [],
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewField } from '../types/ViewField';
export const currentViewFieldsScopedFamilyState = createScopedFamilyState<
ViewField[],
string
>({
key: 'currentViewFieldsScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewFilter } from '../types/ViewFilter';
export const currentViewFiltersScopedFamilyState = createScopedFamilyState<
ViewFilter[],
string
>({
key: 'currentViewFiltersScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const currentViewIdScopedState = createScopedState<string | undefined>({
key: 'currentViewIdScopedState',
defaultValue: undefined,
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewSort } from '../types/ViewSort';
export const currentViewSortsScopedFamilyState = createScopedFamilyState<
ViewSort[],
string
>({
key: 'currentViewSortsScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const entityCountInCurrentViewScopedState = createScopedState<number>({
key: 'entityCountInCurrentViewScopedState',
defaultValue: 0,
});

View File

@ -0,0 +1,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const isPersistingViewScopedState = createScopedState<boolean>({
key: 'isPersistingViewScopedState',
defaultValue: false,
});

View File

@ -0,0 +1,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const isViewBarExpandedScopedState = createScopedState<boolean>({
key: 'isViewBarExpandedScopedState',
defaultValue: true,
});

View File

@ -0,0 +1,6 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
export const noneScopedFamilyState = createScopedFamilyState<any, string>({
key: 'noneScopedFamilyState',
defaultValue: null,
});

View File

@ -0,0 +1,10 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { ViewField } from '../types/ViewField';
export const onViewFieldsChangeScopedState = createScopedState<
((fields: ViewField[]) => void | Promise<void>) | undefined
>({
key: 'onViewFieldsChangeScopedState',
defaultValue: undefined,
});

View File

@ -0,0 +1,9 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { ViewFilter } from '@/views/types/ViewFilter';
export const onViewFiltersChangeScopedState = createScopedState<
((filters: ViewFilter[]) => void | Promise<void>) | undefined
>({
key: 'onViewFiltersChangeScopedState',
defaultValue: undefined,
});

View File

@ -0,0 +1,9 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { ViewSort } from '@/views/types/ViewSort';
export const onViewSortsChangeScopedState = createScopedState<
((sorts: ViewSort[]) => void | Promise<void>) | undefined
>({
key: 'onViewSortsChangeScopedState',
defaultValue: undefined,
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewField } from '../types/ViewField';
export const savedViewFieldsScopedFamilyState = createScopedFamilyState<
ViewField[],
string
>({
key: 'savedViewFieldsScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewFilter } from '../types/ViewFilter';
export const savedViewFiltersScopedFamilyState = createScopedFamilyState<
ViewFilter[],
string
>({
key: 'savedViewFiltersScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,11 @@
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
import { ViewSort } from '../types/ViewSort';
export const savedViewSortsScopedFamilyState = createScopedFamilyState<
ViewSort[],
string
>({
key: 'savedViewSortsScopedFamilyState',
defaultValue: [],
});

View File

@ -0,0 +1,32 @@
import { selectorFamily } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { currentViewFiltersScopedFamilyState } from '../currentViewFiltersScopedFamilyState';
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
export const canPersistViewFiltersScopedFamilySelector = selectorFamily({
key: 'canPersistFiltersScopedFamilySelector',
get:
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
({ get }) => {
if (!viewId) {
return;
}
return !isDeeplyEqual(
get(
savedViewFiltersScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
),
get(
currentViewFiltersScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
),
);
},
});

View File

@ -0,0 +1,31 @@
import { selectorFamily } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { currentViewSortsScopedFamilyState } from '../currentViewSortsScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../savedViewSortsScopedFamilyState';
export const canPersistViewSortsScopedFamilySelector = selectorFamily({
key: 'canPersistSortsScopedFamilySelector',
get:
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
({ get }) => {
if (!viewId) {
return;
}
return !isDeeplyEqual(
get(
savedViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
),
get(
currentViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
),
);
},
});

View File

@ -0,0 +1,21 @@
import { createScopedSelector } from '@/ui/utilities/recoil-scope/utils/createScopedSelector';
import { GraphQLView } from '@/views/types/GraphQLView';
import { currentViewIdScopedState } from '../currentViewIdScopedState';
import { viewsByIdScopedSelector } from './viewsByIdScopedSelector';
export const currentViewScopedSelector = createScopedSelector<
GraphQLView | undefined
>({
key: 'currentViewScopedSelector',
get:
({ scopeId }: { scopeId: string }) =>
({ get }) => {
const currentViewId = get(currentViewIdScopedState({ scopeId: scopeId }));
return currentViewId
? get(viewsByIdScopedSelector(scopeId))[currentViewId]
: undefined;
},
});

View File

@ -0,0 +1,32 @@
import { selectorFamily } from 'recoil';
import { ViewField } from '@/views/types/ViewField';
import { savedViewFieldsScopedFamilyState } from '../savedViewFieldsScopedFamilyState';
export const savedViewFieldByKeyScopedFamilySelector = selectorFamily({
key: 'savedViewFieldByKeyScopedFamilySelector',
get:
({
viewScopeId,
viewId,
}: {
viewScopeId: string;
viewId: string | undefined;
}) =>
({ get }) => {
if (viewId === undefined) {
return undefined;
}
return get(
savedViewFieldsScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
).reduce<Record<string, ViewField>>(
(result, column) => ({ ...result, [column.fieldMetadataId]: column }),
{},
);
},
});

View File

@ -0,0 +1,25 @@
import { selectorFamily } from 'recoil';
import { ViewFilter } from '@/views/types/ViewFilter';
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
export const savedViewFiltersByKeyScopedFamilySelector = selectorFamily({
key: 'savedViewFiltersByKeyScopedFamilySelector',
get:
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
({ get }) => {
if (viewId === undefined) {
return undefined;
}
return get(
savedViewFiltersScopedFamilyState({
scopeId: scopeId,
familyKey: viewId,
}),
).reduce<Record<string, ViewFilter>>(
(result, filter) => ({ ...result, [filter.fieldMetadataId]: filter }),
{},
);
},
});

View File

@ -0,0 +1,25 @@
import { selectorFamily } from 'recoil';
import { ViewSort } from '@/views/types/ViewSort';
import { savedViewSortsScopedFamilyState } from '../savedViewSortsScopedFamilyState';
export const savedViewSortsByKeyScopedFamilySelector = selectorFamily({
key: 'savedViewSortsByKeyScopedFamilySelector',
get:
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
({ get }) => {
if (viewId === undefined) {
return undefined;
}
return get(
savedViewSortsScopedFamilyState({
scopeId: scopeId,
familyKey: viewId,
}),
).reduce<Record<string, ViewSort>>(
(result, sort) => ({ ...result, [sort.fieldMetadataId]: sort }),
{},
);
},
});

View File

@ -0,0 +1,16 @@
import { selectorFamily } from 'recoil';
import { savedViewSortsScopedFamilyState } from '../savedViewSortsScopedFamilyState';
export const savedViewSortsFamilySelector = selectorFamily({
key: 'savedViewSortsFamilySelector',
get:
({ scopeId, viewId }: { scopeId: string; viewId: string }) =>
({ get }) =>
get(
savedViewSortsScopedFamilyState({
scopeId: scopeId,
familyKey: viewId,
}),
),
});

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import { GraphQLView } from '@/views/types/GraphQLView';
import { viewsScopedState } from '../viewsScopedState';
export const viewsByIdScopedSelector = selectorFamily<
Record<string, GraphQLView>,
string
>({
key: 'viewsByIdScopedSelector',
get:
(scopeId) =>
({ get }) =>
get(viewsScopedState({ scopeId: scopeId })).reduce<
Record<string, GraphQLView>
>((result, view) => ({ ...result, [view.id]: view }), {}),
});

View File

@ -0,0 +1,8 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const viewEditModeScopedState = createScopedState<
'none' | 'edit' | 'create'
>({
key: 'viewEditModeScopedState',
defaultValue: 'none',
});

View File

@ -0,0 +1,8 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const viewObjectMetadataIdScopeState = createScopedState<
string | undefined
>({
key: 'viewObjectMetadataIdScopeState',
defaultValue: undefined,
});

View File

@ -0,0 +1,8 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { ViewType } from '../types/ViewType';
export const viewTypeScopedState = createScopedState<ViewType>({
key: 'viewTypeScopedState',
defaultValue: ViewType.Table,
});

View File

@ -0,0 +1,7 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { GraphQLView } from '@/views/types/GraphQLView';
export const viewsScopedState = createScopedState<GraphQLView[]>({
key: 'viewsScopedState',
defaultValue: [],
});

View File

@ -0,0 +1,13 @@
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewSort } from '@/views/types/ViewSort';
export type GraphQLView = {
id: string;
name: string;
objectMetadataId: string;
viewFields: PaginatedRecordTypeResults<ViewField>;
viewFilters: PaginatedRecordTypeResults<ViewFilter>;
viewSorts: PaginatedRecordTypeResults<ViewSort>;
};

View File

@ -0,0 +1,5 @@
export type View = {
id: string;
name: string;
objectMetadataId: string;
};

View File

@ -0,0 +1,14 @@
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { BoardFieldDefinition } from '@/object-record/record-board/types/BoardFieldDefinition';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
export type ViewField = {
id: string;
fieldMetadataId: string;
position: number;
isVisible: boolean;
size: number;
definition:
| ColumnDefinition<FieldMetadata>
| BoardFieldDefinition<FieldMetadata>;
};

View File

@ -0,0 +1,12 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { ViewFilterOperand } from './ViewFilterOperand';
export type ViewFilter = {
id: string;
fieldMetadataId: string;
operand: ViewFilterOperand;
value: string;
displayValue: string;
definition: FilterDefinition;
};

View File

@ -0,0 +1,9 @@
export enum ViewFilterOperand {
Is = 'is',
IsNotNull = 'isNotNull',
IsNot = 'isNot',
LessThan = 'lessThan',
GreaterThan = 'greaterThan',
Contains = 'contains',
DoesNotContain = 'doesNotContain',
}

View File

@ -0,0 +1,9 @@
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection';
export type ViewSort = {
id: string;
fieldMetadataId: string;
direction: SortDirection;
definition: SortDefinition;
};

View File

@ -0,0 +1,4 @@
export enum ViewSortDirection {
Asc = 'asc',
Desc = 'desc',
}

View File

@ -0,0 +1,4 @@
export enum ViewType {
Table = 'table',
Kanban = 'kanban',
}

View File

@ -0,0 +1,4 @@
export enum ViewsHotkeyScope {
ListDropdown = 'views-list-dropdown',
CreateDropdown = 'views-create-dropdown',
}

View File

@ -0,0 +1,108 @@
import { Snapshot } from 'recoil';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { UNDEFINED_FAMILY_ITEM_ID } from '../constants';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { getViewScopedStates } from './internal/getViewScopedStates';
export const getViewScopedStateValuesFromSnapshot = ({
snapshot,
viewScopeId,
viewId,
}: {
snapshot: Snapshot;
viewScopeId: string;
viewId?: string;
}) => {
const currentViewId = getSnapshotValue(
snapshot,
getScopedState(currentViewIdScopedState, viewScopeId),
);
const familyItemId = viewId ?? currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
const {
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
canPersistFiltersSelector,
canPersistSortsSelector,
currentViewFieldsState,
currentViewFiltersState,
currentViewIdState,
currentViewSelector,
currentViewSortsState,
entityCountInCurrentViewState,
isViewBarExpandedState,
isPersistingViewState,
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
savedViewFieldsByKeySelector,
savedViewFieldsState,
savedViewFiltersByKeySelector,
savedViewFiltersState,
savedViewSortsByKeySelector,
savedViewSortsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
viewsState,
} = getViewScopedStates({
viewScopeId,
viewId: familyItemId,
});
return {
availableFieldDefinitions: getSnapshotValue(
snapshot,
availableFieldDefinitionsState,
),
availableFilterDefinitions: getSnapshotValue(
snapshot,
availableFilterDefinitionsState,
),
availableSortDefinitions: getSnapshotValue(
snapshot,
availableSortDefinitionsState,
),
canPersistFilters: getSnapshotValue(snapshot, canPersistFiltersSelector),
canPersistSorts: getSnapshotValue(snapshot, canPersistSortsSelector),
currentViewFields: getSnapshotValue(snapshot, currentViewFieldsState),
currentViewFilters: getSnapshotValue(snapshot, currentViewFiltersState),
currentViewId: getSnapshotValue(snapshot, currentViewIdState),
currentView: getSnapshotValue(snapshot, currentViewSelector),
currentViewSorts: getSnapshotValue(snapshot, currentViewSortsState),
entityCountInCurrentView: getSnapshotValue(
snapshot,
entityCountInCurrentViewState,
),
isViewBarExpanded: getSnapshotValue(snapshot, isViewBarExpandedState),
isPersistingView: getSnapshotValue(snapshot, isPersistingViewState),
onViewFieldsChange: getSnapshotValue(snapshot, onViewFieldsChangeState),
onViewFiltersChange: getSnapshotValue(snapshot, onViewFiltersChangeState),
onViewSortsChange: getSnapshotValue(snapshot, onViewSortsChangeState),
savedViewFieldsByKey: getSnapshotValue(
snapshot,
savedViewFieldsByKeySelector,
),
savedViewFields: getSnapshotValue(snapshot, savedViewFieldsState),
savedViewFiltersByKey: getSnapshotValue(
snapshot,
savedViewFiltersByKeySelector,
),
savedViewFilters: getSnapshotValue(snapshot, savedViewFiltersState),
savedViewSortsByKey: getSnapshotValue(
snapshot,
savedViewSortsByKeySelector,
),
savedViewSorts: getSnapshotValue(snapshot, savedViewSortsState),
viewEditMode: getSnapshotValue(snapshot, viewEditModeState),
viewObjectMetadataId: getSnapshotValue(snapshot, viewObjectMetadataIdState),
viewType: getSnapshotValue(snapshot, viewTypeState),
views: getSnapshotValue(snapshot, viewsState),
};
};

View File

@ -0,0 +1,87 @@
import { Snapshot } from 'recoil';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { UNDEFINED_FAMILY_ITEM_ID } from '../constants';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { getViewScopedStates } from './internal/getViewScopedStates';
export const getViewScopedStatesFromSnapshot = ({
snapshot,
viewScopeId,
viewId,
}: {
snapshot: Snapshot;
viewScopeId: string;
viewId?: string;
}) => {
const currentViewId = getSnapshotValue(
snapshot,
getScopedState(currentViewIdScopedState, viewScopeId),
);
const usedViewId = viewId ?? currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
const {
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
canPersistFiltersSelector: canPersistFiltersSelector,
canPersistSortsSelector: canPersistSortsSelector,
currentViewFieldsState,
currentViewFiltersState,
currentViewIdState,
currentViewSelector,
currentViewSortsState,
entityCountInCurrentViewState,
isViewBarExpandedState,
isPersistingViewState,
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
savedViewFieldsByKeySelector: savedViewFieldsByKeySelector,
savedViewFieldsState,
savedViewFiltersByKeySelector: savedViewFiltersByKeySelector,
savedViewFiltersState,
savedViewSortsByKeySelector: savedViewSortsByKeySelector,
savedViewSortsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
viewsState,
} = getViewScopedStates({
viewScopeId,
viewId: usedViewId,
});
return {
availableFieldDefinitionsState,
availableFilterDefinitionsState,
availableSortDefinitionsState,
canPersistFiltersReadOnlyState: canPersistFiltersSelector,
canPersistSortsReadOnlyState: canPersistSortsSelector,
currentViewFieldsState,
currentViewFiltersState,
currentViewIdState,
currentViewSelector,
currentViewSortsState,
entityCountInCurrentViewState,
isViewBarExpandedState,
isPersistingViewState,
onViewFieldsChangeState,
onViewFiltersChangeState,
onViewSortsChangeState,
savedViewFieldsByKeyReadOnlyState: savedViewFieldsByKeySelector,
savedViewFieldsState,
savedViewFiltersByKeyReadOnlyState: savedViewFiltersByKeySelector,
savedViewFiltersState,
savedViewSortsByKeyReadOnlyState: savedViewSortsByKeySelector,
savedViewSortsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
viewsState,
};
};

View File

@ -0,0 +1,208 @@
import { getScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/getScopedFamilyState';
import { getScopedSelector } from '@/ui/utilities/recoil-scope/utils/getScopedSelector';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
import { isPersistingViewScopedState } from '@/views/states/isPersistingViewScopedState';
import { currentViewScopedSelector } from '@/views/states/selectors/currentViewScopedSelector';
import { availableFieldDefinitionsScopedState } from '../../states/availableFieldDefinitionsScopedState';
import { availableFilterDefinitionsScopedState } from '../../states/availableFilterDefinitionsScopedState';
import { availableSortDefinitionsScopedState } from '../../states/availableSortDefinitionsScopedState';
import { currentViewFieldsScopedFamilyState } from '../../states/currentViewFieldsScopedFamilyState';
import { currentViewFiltersScopedFamilyState } from '../../states/currentViewFiltersScopedFamilyState';
import { currentViewSortsScopedFamilyState } from '../../states/currentViewSortsScopedFamilyState';
import { entityCountInCurrentViewScopedState } from '../../states/entityCountInCurrentViewScopedState';
import { isViewBarExpandedScopedState } from '../../states/isViewBarExpandedScopedState';
import { onViewFieldsChangeScopedState } from '../../states/onViewFieldsChangeScopedState';
import { onViewFiltersChangeScopedState } from '../../states/onViewFiltersChangeScopedState';
import { onViewSortsChangeScopedState } from '../../states/onViewSortsChangeScopedState';
import { savedViewFieldsScopedFamilyState } from '../../states/savedViewFieldsScopedFamilyState';
import { savedViewFiltersScopedFamilyState } from '../../states/savedViewFiltersScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../../states/savedViewSortsScopedFamilyState';
import { canPersistViewFiltersScopedFamilySelector } from '../../states/selectors/canPersistViewFiltersScopedFamilySelector';
import { canPersistViewSortsScopedFamilySelector } from '../../states/selectors/canPersistViewSortsScopedFamilySelector';
import { savedViewFieldByKeyScopedFamilySelector } from '../../states/selectors/savedViewFieldByKeyScopedFamilySelector';
import { savedViewFiltersByKeyScopedFamilySelector } from '../../states/selectors/savedViewFiltersByKeyScopedFamilySelector';
import { savedViewSortsByKeyScopedFamilySelector } from '../../states/selectors/savedViewSortsByKeyScopedFamilySelector';
import { viewEditModeScopedState } from '../../states/viewEditModeScopedState';
import { viewObjectMetadataIdScopeState } from '../../states/viewObjectMetadataIdScopeState';
import { viewsScopedState } from '../../states/viewsScopedState';
import { viewTypeScopedState } from '../../states/viewTypeScopedState';
export const getViewScopedStates = ({
viewScopeId,
viewId,
}: {
viewScopeId: string;
viewId: string;
}) => {
const viewEditModeState = getScopedState(
viewEditModeScopedState,
viewScopeId,
);
const viewsState = getScopedState(viewsScopedState, viewScopeId);
const viewObjectMetadataIdState = getScopedState(
viewObjectMetadataIdScopeState,
viewScopeId,
);
const viewTypeState = getScopedState(viewTypeScopedState, viewScopeId);
const entityCountInCurrentViewState = getScopedState(
entityCountInCurrentViewScopedState,
viewScopeId,
);
const isViewBarExpandedState = getScopedState(
isViewBarExpandedScopedState,
viewScopeId,
);
const isPersistingViewState = getScopedState(
isPersistingViewScopedState,
viewScopeId,
);
// ViewSorts
const currentViewSortsState = getScopedFamilyState(
currentViewSortsScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewSortsState = getScopedFamilyState(
savedViewSortsScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewSortsByKeySelector = savedViewSortsByKeyScopedFamilySelector({
scopeId: viewScopeId,
viewId: viewId,
});
const availableSortDefinitionsState = getScopedState(
availableSortDefinitionsScopedState,
viewScopeId,
);
const canPersistSortsSelector = canPersistViewSortsScopedFamilySelector({
viewScopeId: viewScopeId,
viewId: viewId,
});
// ViewFilters
const currentViewFiltersState = getScopedFamilyState(
currentViewFiltersScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewFiltersState = getScopedFamilyState(
savedViewFiltersScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewFiltersByKeySelector =
savedViewFiltersByKeyScopedFamilySelector({
scopeId: viewScopeId,
viewId: viewId,
});
const availableFilterDefinitionsState = getScopedState(
availableFilterDefinitionsScopedState,
viewScopeId,
);
const canPersistFiltersSelector = canPersistViewFiltersScopedFamilySelector({
viewScopeId: viewScopeId,
viewId: viewId,
});
// ViewFields
const availableFieldDefinitionsState = getScopedState(
availableFieldDefinitionsScopedState,
viewScopeId,
);
const currentViewFieldsState = getScopedFamilyState(
currentViewFieldsScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewFieldsState = getScopedFamilyState(
savedViewFieldsScopedFamilyState,
viewScopeId,
viewId,
);
const savedViewFieldsByKeySelector = savedViewFieldByKeyScopedFamilySelector({
viewScopeId,
viewId,
});
// ViewChangeHandlers
const onViewSortsChangeState = getScopedState(
onViewSortsChangeScopedState,
viewScopeId,
);
const onViewFiltersChangeState = getScopedState(
onViewFiltersChangeScopedState,
viewScopeId,
);
const onViewFieldsChangeState = getScopedState(
onViewFieldsChangeScopedState,
viewScopeId,
);
const currentViewIdState = getScopedState(
currentViewIdScopedState,
viewScopeId,
);
const currentViewSelector = getScopedSelector(
currentViewScopedSelector,
viewScopeId,
);
return {
currentViewIdState,
currentViewSelector,
isViewBarExpandedState,
isPersistingViewState,
viewsState,
viewEditModeState,
viewObjectMetadataIdState,
viewTypeState,
entityCountInCurrentViewState,
availableSortDefinitionsState,
currentViewSortsState,
savedViewSortsState,
savedViewSortsByKeySelector,
canPersistSortsSelector,
availableFilterDefinitionsState,
currentViewFiltersState,
savedViewFiltersState,
savedViewFiltersByKeySelector,
canPersistFiltersSelector,
availableFieldDefinitionsState,
currentViewFieldsState,
savedViewFieldsByKeySelector,
savedViewFieldsState,
onViewSortsChangeState,
onViewFiltersChangeState,
onViewFieldsChangeState,
};
};

View File

@ -0,0 +1,17 @@
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ViewField } from '../types/ViewField';
export const mapColumnDefinitionsToViewFields = (
columnDefinitions: ColumnDefinition<FieldMetadata>[],
): ViewField[] => {
return columnDefinitions.map((columnDefinition) => ({
id: columnDefinition.viewFieldId || '',
fieldMetadataId: columnDefinition.fieldMetadataId,
position: columnDefinition.position,
size: columnDefinition.size,
isVisible: columnDefinition.isVisible ?? true,
definition: columnDefinition,
}));
};

View File

@ -0,0 +1,32 @@
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { BoardFieldDefinition } from '@/object-record/record-board/types/BoardFieldDefinition';
import { assertNotNull } from '~/utils/assert';
import { ViewField } from '../types/ViewField';
export const mapViewFieldsToBoardFieldDefinitions = (
viewFields: ViewField[],
fieldsMetadata: BoardFieldDefinition<FieldMetadata>[],
): BoardFieldDefinition<FieldMetadata>[] => {
return viewFields
.map((viewField) => {
const correspondingFieldMetadata = fieldsMetadata.find(
({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId,
);
return correspondingFieldMetadata
? {
fieldMetadataId: viewField.fieldMetadataId,
label: correspondingFieldMetadata.label,
metadata: correspondingFieldMetadata.metadata,
infoTooltipContent: correspondingFieldMetadata.infoTooltipContent,
iconName: correspondingFieldMetadata.iconName,
type: correspondingFieldMetadata.type,
position: viewField.position,
isVisible: viewField.isVisible,
viewFieldId: viewField.id,
}
: null;
})
.filter(assertNotNull);
};

View File

@ -0,0 +1,33 @@
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { assertNotNull } from '~/utils/assert';
import { ViewField } from '../types/ViewField';
export const mapViewFieldsToColumnDefinitions = (
viewFields: ViewField[],
fieldsMetadata: ColumnDefinition<FieldMetadata>[],
): ColumnDefinition<FieldMetadata>[] => {
return viewFields
.map((viewField) => {
const correspondingFieldMetadata = fieldsMetadata.find(
({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId,
);
return correspondingFieldMetadata
? {
fieldMetadataId: viewField.fieldMetadataId,
label: correspondingFieldMetadata.label,
metadata: correspondingFieldMetadata.metadata,
infoTooltipContent: correspondingFieldMetadata.infoTooltipContent,
iconName: correspondingFieldMetadata.iconName,
type: correspondingFieldMetadata.type,
position: viewField.position,
size: viewField.size ?? correspondingFieldMetadata.size,
isVisible: viewField.isVisible,
viewFieldId: viewField.id,
}
: null;
})
.filter(assertNotNull);
};

View File

@ -0,0 +1,17 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { ViewFilter } from '../types/ViewFilter';
export const mapViewFiltersToFilters = (
viewFilters: ViewFilter[],
): Filter[] => {
return viewFilters.map((viewFilter) => {
return {
fieldMetadataId: viewFilter.fieldMetadataId,
value: viewFilter.value,
displayValue: viewFilter.displayValue,
operand: viewFilter.operand,
definition: viewFilter.definition,
};
});
};

View File

@ -0,0 +1,13 @@
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
import { ViewSort } from '../types/ViewSort';
export const mapViewSortsToSorts = (viewSorts: ViewSort[]): Sort[] => {
return viewSorts.map((viewSort) => {
return {
fieldMetadataId: viewSort.fieldMetadataId,
direction: viewSort.direction,
definition: viewSort.definition,
};
});
};