Improve viewbar api (#2233)

* create scopes

* fix import bug

* add useView hook

* wip

* wip

* currentViewId is now retrieved via useView

* working on sorts with useView

* refactor in progress

* refactor in progress

* refactor in progress

* refactor in progress

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix code

* fix code

* wip

* push

* Fix issue dependencies

* Fix resize

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Charles Bochet
2023-10-27 10:52:26 +02:00
committed by GitHub
parent 6a72c14af3
commit 5ba68e997d
205 changed files with 3092 additions and 3249 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,84 @@
import { useCallback, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useView } from '@/views/hooks/useView';
import { useViewInternalStates } from '../hooks/useViewInternalStates';
const StyledContainer = styled.div`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
export type UpdateViewButtonGroupProps = {
hotkeyScope: string;
onViewEditModeChange?: () => void;
};
export const UpdateViewButtonGroup = ({
hotkeyScope,
onViewEditModeChange,
}: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { updateCurrentView, setViewEditMode } = useView();
const { canPersistFilters, canPersistSorts } = useViewInternalStates();
const canPersistView = canPersistFilters || canPersistSorts;
const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []);
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode('create');
onViewEditModeChange?.();
setIsDropdownOpen(false);
}, [setViewEditMode, onViewEditModeChange]);
const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const handleViewSubmit = async () => {
await updateCurrentView?.();
};
useScopedHotkeys(
[Key.Enter, Key.Escape],
handleDropdownClose,
hotkeyScope,
[],
);
if (!canPersistView) return null;
return (
<StyledContainer>
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewSubmit} />
<Button
size="small"
Icon={IconChevronDown}
onClick={handleArrowDownButtonClick}
/>
</ButtonGroup>
{isDropdownOpen && (
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleCreateViewButtonClick}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
)}
</StyledContainer>
);
};

View File

@ -0,0 +1,82 @@
import { ReactNode } from 'react';
import { FilterDropdownButton } from '@/ui/data/filter/components/FilterDropdownButton';
import { FilterScope } from '@/ui/data/filter/scopes/FilterScope';
import { FiltersHotkeyScope } from '@/ui/data/filter/types/FiltersHotkeyScope';
import { SortDropdownButton } from '@/ui/data/sort/components/SortDropdownButton';
import { SortScope } from '@/ui/data/sort/scopes/SortScope';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { useView } from '../hooks/useView';
import { useViewInternalStates } from '../hooks/useViewInternalStates';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {
className?: string;
optionsDropdownButton: ReactNode;
optionsDropdownScopeId: string;
};
export const ViewBar = ({
className,
optionsDropdownButton,
optionsDropdownScopeId,
}: ViewBarProps) => {
const { openDropdown: openOptionsDropdownButton } = useDropdown({
dropdownScopeId: optionsDropdownScopeId,
});
const { upsertViewSort } = useView();
const { availableFilters, availableSorts } = useViewInternalStates();
return (
<FilterScope
filterScopeId="view-filter"
availableFilters={availableFilters}
>
<SortScope
sortScopeId="view-sort"
availableSorts={availableSorts}
onSortAdd={upsertViewSort}
>
<TopBar
className={className}
leftComponent={
<ViewsDropdownButton
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
/>
}
displayBottomBorder={false}
rightComponent={
<>
<FilterDropdownButton
hotkeyScope={{ scope: FiltersHotkeyScope.FilterDropdownButton }}
/>
<SortDropdownButton
hotkeyScope={{ scope: FiltersHotkeyScope.SortDropdownButton }}
isPrimaryButton
/>
{optionsDropdownButton}
</>
}
bottomComponent={
<ViewBarDetails
hasFilterButton
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
/>
}
/>
}
/>
</SortScope>
</FilterScope>
);
};

View File

@ -0,0 +1,191 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { AddFilterFromDropdownButton } from '@/ui/data/filter/components/AddFilterFromDetailsButton';
import { getOperandLabelShort } from '@/ui/data/filter/utils/getOperandLabel';
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { useView } from '../hooks/useView';
import { useViewInternalStates } from '../hooks/useViewInternalStates';
import SortOrFilterChip from './SortOrFilterChip';
export type ViewBarDetailsProps = {
hasFilterButton?: boolean;
rightComponent?: ReactNode;
};
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,
}: ViewBarDetailsProps) => {
const {
currentViewSorts,
setCurrentViewSorts,
availableFilters,
currentViewFilters,
canPersistFilters,
canPersistSorts,
isViewBarExpanded,
} = useViewInternalStates();
const { resetViewBar } = useView();
const canPersistView = canPersistFilters || canPersistSorts;
const filtersWithDefinition = currentViewFilters?.map((filter) => {
const filterDefinition = availableFilters.find((availableFilter) => {
return availableFilter.key === filter.key;
});
return {
...filter,
...filterDefinition,
};
});
const removeFilter = useRemoveFilter();
const handleCancelClick = () => {
resetViewBar();
};
const handleSortRemove = (sortKey: string) =>
setCurrentViewSorts?.((previousSorts) =>
previousSorts.filter((sort) => sort.key !== sortKey),
);
const shouldExpandViewBar =
canPersistView ||
((filtersWithDefinition?.length || currentViewSorts?.length) &&
isViewBarExpanded);
if (!shouldExpandViewBar) {
return null;
}
return (
<StyledBar>
<StyledFilterContainer>
<StyledChipcontainer>
{currentViewSorts?.map((sort) => {
return (
<SortOrFilterChip
key={sort.key}
testId={sort.key}
labelValue={sort.definition.label}
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
isSort
onRemove={() => handleSortRemove(sort.key)}
/>
);
})}
{!!currentViewSorts?.length && !!filtersWithDefinition?.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{filtersWithDefinition?.map((filter) => {
return (
<SortOrFilterChip
key={filter.key}
testId={filter.key}
labelKey={filter.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
Icon={filter.Icon}
onRemove={() => {
removeFilter(filter.key);
}}
/>
);
})}
</StyledChipcontainer>
{hasFilterButton && (
<StyledAddFilterContainer>
<AddFilterFromDropdownButton />
</StyledAddFilterContainer>
)}
</StyledFilterContainer>
{canPersistView && (
<StyledCancelButton
data-testid="cancel-button"
onClick={handleCancelClick}
>
Reset
</StyledCancelButton>
)}
{rightComponent}
</StyledBar>
);
};

View File

@ -0,0 +1,218 @@
import { useRecoilCallback } from 'recoil';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { Filter } from '@/ui/data/filter/types/Filter';
import { Sort } from '@/ui/data/sort/types/Sort';
import {
SortOrder,
useGetViewFieldsQuery,
useGetViewFiltersQuery,
useGetViewSortsQuery,
useGetViewsQuery,
} from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useView } from '../hooks/useView';
import { useViewInternalStates } from '../hooks/useViewInternalStates';
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { availableSortsScopedState } from '../states/availableSortsScopedState';
import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScopedFamilyState';
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
import { viewsScopedState } from '../states/viewsScopedState';
export const ViewBarEffect = () => {
const {
scopeId: viewScopeId,
setCurrentViewFields,
setSavedViewFields,
setCurrentViewSorts,
setSavedViewSorts,
setCurrentViewFilters,
setSavedViewFilters,
currentViewId,
setViews,
setCurrentViewId,
} = useView();
const { viewType, viewObjectId } = useViewInternalStates(viewScopeId);
useGetViewFieldsQuery({
skip: !currentViewId,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
const availableFields = snapshot
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
.getValue();
if (!availableFields || !currentViewId) {
return;
}
const savedViewFields = snapshot
.getLoadable(
savedViewFieldsScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const queriedViewFields = data.viewFields
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
const columnDefinition = availableFields.find(
({ key }) => viewField.key === key,
);
return columnDefinition
? {
...columnDefinition,
key: viewField.key,
name: viewField.name,
index: viewField.index,
size: viewField.size ?? columnDefinition.size,
isVisible: viewField.isVisible,
}
: null;
})
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
setCurrentViewFields?.(queriedViewFields);
setSavedViewFields?.(queriedViewFields);
}
}),
});
useGetViewsQuery({
variables: {
where: {
objectId: { equals: viewObjectId },
type: { equals: viewType },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
const nextViews = data.views.map((view) => ({
id: view.id,
name: view.name,
}));
const views = snapshot
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
.getValue();
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
if (!nextViews.length) return;
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
}),
});
useGetViewSortsQuery({
skip: !currentViewId,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
const availableSorts = snapshot
.getLoadable(availableSortsScopedState({ scopeId: viewScopeId }))
.getValue();
if (!availableSorts || !currentViewId) {
return;
}
const savedViewSorts = snapshot
.getLoadable(
savedViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const queriedViewSorts = data.viewSorts
.map((viewSort) => {
const foundCorrespondingSortDefinition = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
if (foundCorrespondingSortDefinition) {
return {
key: viewSort.key,
definition: foundCorrespondingSortDefinition,
direction: viewSort.direction.toLowerCase(),
} as Sort;
} else {
return undefined;
}
})
.filter((sort): sort is Sort => !!sort);
if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) {
setSavedViewSorts?.(queriedViewSorts);
setCurrentViewSorts?.(queriedViewSorts);
}
}),
});
useGetViewFiltersQuery({
skip: !currentViewId,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => (data) => {
const availableFilters = snapshot
.getLoadable(availableFiltersScopedState({ scopeId: viewScopeId }))
.getValue();
if (!availableFilters || !currentViewId) {
return;
}
const savedViewFilters = snapshot
.getLoadable(
savedViewFiltersScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const queriedViewFilters = data.viewFilters
.map(({ __typename, name: _name, ...viewFilter }) => {
const availableFilter = availableFilters.find(
(filter) => filter.key === viewFilter.key,
);
return availableFilter
? {
...viewFilter,
displayValue: viewFilter.displayValue ?? viewFilter.value,
type: availableFilter.type,
}
: undefined;
})
.filter((filter): filter is Filter => !!filter);
if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) {
setSavedViewFilters?.(queriedViewFilters);
setCurrentViewFilters?.(queriedViewFilters);
}
}),
});
return <></>;
};

View File

@ -0,0 +1,142 @@
import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
DropResult,
OnDragEndResponder,
ResponderProvided,
} from '@hello-pangea/dnd';
import { IconMinus, IconPlus } from '@/ui/display/icon';
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
import { IconInfoCircle } from '@/ui/input/constants/icons';
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';
import { ViewFieldForVisibility } from '../types/ViewFieldForVisibility';
type ViewFieldsVisibilityDropdownSectionProps = {
fields: ViewFieldForVisibility[];
onVisibilityChange: (field: ViewFieldForVisibility) => void;
title: string;
isDraggable: boolean;
onDragEnd?: OnDragEndResponder;
};
export const ViewFieldsVisibilityDropdownSection = ({
fields,
onVisibilityChange,
title,
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 getIconButtons = (index: number, field: ViewFieldForVisibility) => {
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: field.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
.filter(({ index, size }) => index !== 0 || !size)
.map((field, index) => (
<DraggableItem
key={field.key}
draggableId={field.key}
index={index + 1}
itemComponent={
<MenuItemDraggable
key={field.key}
LeftIcon={field.Icon}
iconButtons={getIconButtons(index + 1, field)}
isTooltipOpen={openToolTipIndex === index + 1}
text={field.name}
className={`${title}-draggable-item-tooltip-anchor-${
index + 1
}`}
/>
}
/>
))}
</>
}
/>
) : (
fields.map((field, index) => (
<MenuItem
key={field.key}
LeftIcon={field.Icon}
iconButtons={getIconButtons(index, field)}
isTooltipOpen={openToolTipIndex === index}
text={field.name}
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,172 @@
import { MouseEvent } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback } 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 { assertNotNull } from '~/utils/assert';
import { ViewsDropdownId } from '../constants/ViewsDropdownId';
import { useView } from '../hooks/useView';
import { useViewInternalStates } from '../hooks/useViewInternalStates';
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;
};
export const ViewsDropdownButton = ({
hotkeyScope,
onViewEditModeChange,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const { scopeId, removeView, currentViewId } = useView();
const {
views,
currentView,
setViewEditMode,
setCurrentViewId,
entityCountInCurrentView,
} = useViewInternalStates(scopeId, currentViewId);
const { isDropdownOpen, closeDropdown } = useDropdown({
dropdownScopeId: ViewsDropdownId,
});
const handleViewSelect = useRecoilCallback(
() => async (viewId: string) => {
setCurrentViewId(viewId);
closeDropdown();
},
[setCurrentViewId, closeDropdown],
);
const handleAddViewButtonClick = () => {
setViewEditMode('create');
onViewEditModeChange?.();
closeDropdown();
};
const handleEditViewButtonClick = (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
setCurrentViewId(viewId);
setViewEditMode('edit');
onViewEditModeChange?.();
closeDropdown();
};
const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
await removeView(viewId);
closeDropdown();
};
return (
<DropdownScope dropdownScopeId={ViewsDropdownId}>
<Dropdown
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
<StyledViewIcon size={theme.icon.size.md} />
<StyledViewName>{currentView?.name}</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 ViewsDropdownId = 'views';

View File

@ -0,0 +1,121 @@
/* eslint-disable no-console */
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilCallback } from 'recoil';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
import { savedViewFieldByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewFieldByKeyScopedFamilySelector';
import { viewObjectIdScopeState } from '@/views/states/viewObjectIdScopeState';
import {
useCreateViewFieldsMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '../../graphql/queries/getViewFields';
export const toViewFieldInput = (
objectId: string,
fieldDefinition: ColumnDefinition<FieldMetadata>,
) => ({
key: fieldDefinition.key,
name: fieldDefinition.name,
index: fieldDefinition.index,
isVisible: fieldDefinition.isVisible ?? true,
objectId,
size: fieldDefinition.size,
});
export const useViewFields = (viewScopeId: string) => {
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const persistViewFields = useRecoilCallback(
({ snapshot }) =>
async (viewFieldsToPersist: ColumnDefinition<FieldMetadata>[]) => {
const currentViewId = snapshot
.getLoadable(currentViewIdScopedState({ scopeId: viewScopeId }))
.getValue();
const viewObjectId = snapshot
.getLoadable(viewObjectIdScopeState({ scopeId: viewScopeId }))
.getValue();
const savedViewFieldsByKey = snapshot
.getLoadable(
savedViewFieldByKeyScopedFamilySelector({
viewScopeId: viewScopeId,
viewId: currentViewId,
}),
)
.getValue();
if (!currentViewId || !savedViewFieldsByKey || !viewObjectId) {
return;
}
const _createViewFields = (
viewFieldsToCreate: ColumnDefinition<FieldMetadata>[],
objectId: string,
) => {
if (!currentViewId || !viewFieldsToCreate.length) {
return;
}
return createViewFieldsMutation({
variables: {
data: viewFieldsToCreate.map((viewField) => ({
...toViewFieldInput(objectId, viewField),
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
};
const _updateViewFields = (
viewFieldsToUpdate: ColumnDefinition<FieldMetadata>[],
) => {
if (!currentViewId || !viewFieldsToUpdate.length) {
return;
}
return Promise.all(
viewFieldsToUpdate.map((viewField) =>
updateViewFieldMutation({
variables: {
data: {
isVisible: viewField.isVisible,
size: viewField.size,
index: viewField.index,
},
where: {
viewId_key: { key: viewField.key, viewId: currentViewId },
},
},
}),
),
);
};
const viewFieldsToCreate = viewFieldsToPersist.filter(
(viewField) => !savedViewFieldsByKey[viewField.key],
);
await _createViewFields(viewFieldsToCreate, viewObjectId);
const viewFieldsToUpdate = viewFieldsToPersist.filter(
(viewFieldToPersit) =>
savedViewFieldsByKey[viewFieldToPersit.key] &&
(savedViewFieldsByKey[viewFieldToPersit.key].size !==
viewFieldToPersit.size ||
savedViewFieldsByKey[viewFieldToPersit.key].index !==
viewFieldToPersit.index ||
savedViewFieldsByKey[viewFieldToPersit.key].isVisible !==
viewFieldToPersit.isVisible),
);
await _updateViewFields(viewFieldsToUpdate);
},
);
return { persistViewFields };
};

View File

@ -0,0 +1,165 @@
import { useCallback } from 'react';
import { useRecoilCallback } from 'recoil';
import { Filter } from '@/ui/data/filter/types/Filter';
import { FilterDefinition } from '@/ui/data/filter/types/FilterDefinition';
import { availableFiltersScopedState } from '@/views/states/availableFiltersScopedState';
import { currentViewFiltersScopedFamilyState } from '@/views/states/currentViewFiltersScopedFamilyState';
import { savedViewFiltersByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewFiltersByKeyScopedFamilySelector';
import {
useCreateViewFiltersMutation,
useDeleteViewFiltersMutation,
useUpdateViewFilterMutation,
} from '~/generated/graphql';
import { useViewStates } from '../useViewStates';
export const useViewFilters = (viewScopeId: string) => {
const { currentViewId } = useViewStates(viewScopeId);
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
const _createViewFilters = useCallback(
(
filters: Filter[],
availableFilters: FilterDefinition[] = [],
viewId = currentViewId,
) => {
if (!viewId || !filters.length) {
return;
}
if (!availableFilters) {
return;
}
return createViewFiltersMutation({
variables: {
data: filters.map((filter) => ({
displayValue: filter.displayValue ?? filter.value,
key: filter.key,
name:
availableFilters.find(({ key }) => key === filter.key)?.label ??
'',
operand: filter.operand,
value: filter.value,
viewId,
})),
},
});
},
[createViewFiltersMutation, currentViewId],
);
const _updateViewFilters = useCallback(
(filters: Filter[], viewId = currentViewId) => {
if (!viewId || !filters.length) return;
return Promise.all(
filters.map((filter) =>
updateViewFilterMutation({
variables: {
data: {
displayValue: filter.displayValue ?? filter.value,
operand: filter.operand,
value: filter.value,
},
where: {
viewId_key: { key: filter.key, viewId: viewId },
},
},
}),
),
);
},
[currentViewId, updateViewFilterMutation],
);
const _deleteViewFilters = useCallback(
(filterKeys: string[], viewId = currentViewId) => {
if (!viewId || !filterKeys.length) return;
return deleteViewFiltersMutation({
variables: {
where: {
key: { in: filterKeys },
viewId: { equals: viewId },
},
},
});
},
[currentViewId, deleteViewFiltersMutation],
);
const persistViewFilters = useRecoilCallback(
({ snapshot }) =>
async () => {
if (!currentViewId) {
return;
}
const currentViewFilters = snapshot
.getLoadable(
currentViewFiltersScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const savedViewFiltersByKey = snapshot
.getLoadable(
savedViewFiltersByKeyScopedFamilySelector({
scopeId: viewScopeId,
viewId: currentViewId,
}),
)
.getValue();
if (!currentViewFilters) {
return;
}
if (!savedViewFiltersByKey) {
return;
}
const availableFilters = snapshot
.getLoadable(
availableFiltersScopedState({
scopeId: viewScopeId,
}),
)
.getValue();
const filtersToCreate = currentViewFilters.filter(
(filter) => !savedViewFiltersByKey[filter.key],
);
await _createViewFilters(filtersToCreate, availableFilters);
const filtersToUpdate = currentViewFilters.filter(
(filter) =>
savedViewFiltersByKey[filter.key] &&
(savedViewFiltersByKey[filter.key].operand !== filter.operand ||
savedViewFiltersByKey[filter.key].value !== filter.value),
);
await _updateViewFilters(filtersToUpdate);
const filterKeys = currentViewFilters.map((filter) => filter.key);
const filterKeysToDelete = Object.keys(savedViewFiltersByKey).filter(
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
);
await _deleteViewFilters(filterKeysToDelete);
},
[
currentViewId,
viewScopeId,
_createViewFilters,
_updateViewFilters,
_deleteViewFilters,
],
);
return { persistViewFilters };
};

View File

@ -0,0 +1,157 @@
/* eslint-disable no-console */
import { useCallback } from 'react';
import { produce } from 'immer';
import { useRecoilCallback } from 'recoil';
import { Sort } from '@/ui/data/sort/types/Sort';
import { currentViewSortsScopedFamilyState } from '@/views/states/currentViewSortsScopedFamilyState';
import { savedViewSortsByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewSortsByKeyScopedFamilySelector';
import {
useCreateViewSortsMutation,
useDeleteViewSortsMutation,
useUpdateViewSortMutation,
ViewSortDirection,
} from '~/generated/graphql';
import { useViewStates } from '../useViewStates';
export const useViewSorts = (viewScopeId: string) => {
const { currentViewId, setCurrentViewSorts } = useViewStates(viewScopeId);
const [createViewSortsMutation] = useCreateViewSortsMutation();
const [updateViewSortMutation] = useUpdateViewSortMutation();
const [deleteViewSortsMutation] = useDeleteViewSortsMutation();
const _createViewSorts = useCallback(
(sorts: Sort[], viewId = currentViewId) => {
if (!viewId || !sorts.length) return;
return createViewSortsMutation({
variables: {
data: sorts.map((sort) => ({
key: sort.key,
direction: sort.direction as ViewSortDirection,
name: sort.definition.label,
viewId,
})),
},
});
},
[createViewSortsMutation, currentViewId],
);
const _updateViewSorts = useCallback(
(sorts: Sort[]) => {
if (!currentViewId || !sorts.length) return;
return Promise.all(
sorts.map((sort) =>
updateViewSortMutation({
variables: {
data: {
direction: sort.direction as ViewSortDirection,
},
where: {
viewId_key: { key: sort.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewSortMutation],
);
const _deleteViewSorts = useCallback(
(sortKeys: string[]) => {
if (!currentViewId || !sortKeys.length) return;
return deleteViewSortsMutation({
variables: {
where: {
key: { in: sortKeys },
viewId: { equals: currentViewId },
},
},
});
},
[currentViewId, deleteViewSortsMutation],
);
const persistViewSorts = useRecoilCallback(
({ snapshot }) =>
async () => {
if (!currentViewId) {
return;
}
const currentViewSorts = snapshot
.getLoadable(
currentViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const savedViewSortsByKey = snapshot
.getLoadable(
savedViewSortsByKeyScopedFamilySelector({
scopeId: viewScopeId,
viewId: currentViewId,
}),
)
.getValue();
if (!currentViewSorts) {
return;
}
if (!savedViewSortsByKey) {
return;
}
const sortsToCreate = currentViewSorts.filter(
(sort) => !savedViewSortsByKey[sort.key],
);
await _createViewSorts(sortsToCreate);
const sortsToUpdate = currentViewSorts.filter(
(sort) =>
savedViewSortsByKey[sort.key] &&
savedViewSortsByKey[sort.key].direction !== sort.direction,
);
await _updateViewSorts(sortsToUpdate);
const sortKeys = currentViewSorts.map((sort) => sort.key);
const sortKeysToDelete = Object.keys(savedViewSortsByKey).filter(
(previousSortKey) => !sortKeys.includes(previousSortKey),
);
await _deleteViewSorts(sortKeysToDelete);
},
[
currentViewId,
viewScopeId,
_createViewSorts,
_updateViewSorts,
_deleteViewSorts,
],
);
const upsertViewSort = (sortToUpsert: Sort) => {
setCurrentViewSorts?.((sorts) => {
return produce(sorts, (sortsDraft) => {
const index = sortsDraft.findIndex(
(sort) => sort.key === sortToUpsert.key,
);
if (index === -1) {
sortsDraft.push(sortToUpsert);
} else {
sortsDraft[index] = sortToUpsert;
}
});
});
};
return { persistViewSorts, upsertViewSort };
};

View File

@ -0,0 +1,67 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilCallback } from 'recoil';
import { viewObjectIdScopeState } from '@/views/states/viewObjectIdScopeState';
import { viewTypeScopedState } from '@/views/states/viewTypeScopedState';
import { View } from '@/views/types/View';
import {
useCreateViewMutation,
useDeleteViewMutation,
useUpdateViewMutation,
} from '~/generated/graphql';
import { GET_VIEWS } from '../../graphql/queries/getViews';
export const useViews = (scopeId: string) => {
const [createViewMutation] = useCreateViewMutation();
const [updateViewMutation] = useUpdateViewMutation();
const [deleteViewMutation] = useDeleteViewMutation();
const createView = useRecoilCallback(({ snapshot }) => async (view: View) => {
const viewObjectId = await snapshot
.getLoadable(viewObjectIdScopeState({ scopeId }))
.getValue();
const viewType = await snapshot
.getLoadable(viewTypeScopedState({ scopeId }))
.getValue();
if (!viewObjectId || !viewType) {
return;
}
await createViewMutation({
variables: {
data: {
...view,
objectId: viewObjectId,
type: viewType,
},
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
});
const updateView = async (view: View) => {
await updateViewMutation({
variables: {
data: { name: view.name },
where: { id: view.id },
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
};
const deleteView = async (viewId: string) => {
await deleteViewMutation({
variables: { where: { id: viewId } },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
};
return {
createView,
deleteView,
isFetchingViews: false,
updateView,
};
};

View File

@ -1,167 +0,0 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { availableBoardCardFieldsScopedState } from '@/ui/layout/board/states/availableBoardCardFieldsScopedState';
import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState';
import { savedBoardCardFieldsFamilyState } from '@/ui/layout/board/states/savedBoardCardFieldsFamilyState';
import { savedBoardCardFieldsByKeyFamilySelector } from '@/ui/layout/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector';
import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const toViewFieldInput = (
objectId: 'company' | 'person',
columDefinition: BoardFieldDefinition<FieldMetadata>,
) => ({
key: columDefinition.key,
name: columDefinition.name,
index: columDefinition.index,
isVisible: columDefinition.isVisible ?? true,
objectId,
});
export const useBoardViewFields = ({
objectId,
viewFieldDefinition,
skipFetch,
RecoilScopeContext,
}: {
objectId: 'company' | 'person';
viewFieldDefinition: BoardFieldDefinition<FieldMetadata>[];
skipFetch?: boolean;
RecoilScopeContext: RecoilScopeContext;
}) => {
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
RecoilScopeContext,
);
const [availableBoardCardFields, setAvailableBoardCardFields] =
useRecoilScopedState(
availableBoardCardFieldsScopedState,
RecoilScopeContext,
);
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
RecoilScopeContext,
);
const setSavedBoardCardFields = useSetRecoilState(
savedBoardCardFieldsFamilyState(currentViewId),
);
const savedBoardCardFieldsByKey = useRecoilValue(
savedBoardCardFieldsByKeyFamilySelector(currentViewId),
);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const createViewFields = (
viewFieldDefinitions: BoardFieldDefinition<FieldMetadata>[],
viewId = currentViewId,
) => {
if (!viewId || !viewFieldDefinitions.length) return;
return createViewFieldsMutation({
variables: {
data: viewFieldDefinitions.map((field) => ({
...toViewFieldInput(objectId, field),
viewId,
})),
},
});
};
const updateViewFields = (
viewFieldDefinitions: BoardFieldDefinition<FieldMetadata>[],
) => {
if (!currentViewId || !viewFieldDefinitions.length) return;
return Promise.all(
viewFieldDefinitions.map((field) =>
updateViewFieldMutation({
variables: {
data: {
isVisible: field.isVisible,
},
where: {
viewId_key: { key: field.key, viewId: currentViewId },
},
},
}),
),
);
};
const { refetch } = useGetViewFieldsQuery({
skip: !currentViewId || skipFetch,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: async (data) => {
if (!data.viewFields.length) {
// Populate if empty
await createViewFields(viewFieldDefinition);
return refetch();
}
const nextFields = data.viewFields
.map<BoardFieldDefinition<FieldMetadata> | null>((viewField) => {
const fieldDefinition = viewFieldDefinition.find(
({ key }) => viewField.key === key,
);
return fieldDefinition
? {
...fieldDefinition,
key: viewField.key,
name: viewField.name,
index: viewField.index,
isVisible: viewField.isVisible,
}
: null;
})
.filter<BoardFieldDefinition<FieldMetadata>>(assertNotNull);
if (!isDeeplyEqual(boardCardFields, nextFields)) {
setSavedBoardCardFields(nextFields);
setBoardCardFields(nextFields);
}
if (!availableBoardCardFields.length) {
setAvailableBoardCardFields(viewFieldDefinition);
}
},
});
const persistCardFields = async () => {
if (!currentViewId) return;
const viewFieldsToCreate = boardCardFields.filter(
(field) => !savedBoardCardFieldsByKey[field.key],
);
await createViewFields(viewFieldsToCreate);
const viewFieldsToUpdate = boardCardFields.filter(
(field) =>
savedBoardCardFieldsByKey[field.key] &&
savedBoardCardFieldsByKey[field.key].isVisible !== field.isVisible,
);
await updateViewFields(viewFieldsToUpdate);
return refetch();
};
return { createViewFields, persistCardFields };
};

View File

@ -1,77 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
import { useBoardColumns } from '@/ui/layout/board/hooks/useBoardColumns';
import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState';
import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewType } from '~/generated/graphql';
import { useBoardViewFields } from './useBoardViewFields';
import { useViewFilters } from './useViewFilters';
import { useViews } from './useViews';
import { useViewSorts } from './useViewSorts';
export const useBoardViews = ({
fieldDefinitions,
objectId,
RecoilScopeContext,
}: {
fieldDefinitions: BoardFieldDefinition<FieldMetadata>[];
objectId: 'company';
RecoilScopeContext: RecoilScopeContext;
}) => {
const boardCardFields = useRecoilScopedValue(
boardCardFieldsScopedState,
RecoilScopeContext,
);
const filters = useRecoilScopedValue(filtersScopedState, RecoilScopeContext);
const sorts = useRecoilScopedValue(sortsScopedState, RecoilScopeContext);
const [_, setSearchParams] = useSearchParams();
const handleViewCreate = async (viewId: string) => {
await createViewFields(boardCardFields, viewId);
await createViewFilters(filters, viewId);
await createViewSorts(sorts, viewId);
setSearchParams({ view: viewId });
};
const { createView, deleteView, isFetchingViews, updateView } = useViews({
objectId,
onViewCreate: handleViewCreate,
type: ViewType.Pipeline,
RecoilScopeContext,
});
const { createViewFields, persistCardFields } = useBoardViewFields({
objectId,
viewFieldDefinition: fieldDefinitions,
skipFetch: isFetchingViews,
RecoilScopeContext,
});
const { persistBoardColumns } = useBoardColumns();
const { createViewFilters, persistFilters } = useViewFilters({
skipFetch: isFetchingViews,
RecoilScopeContext,
});
const { createViewSorts, persistSorts } = useViewSorts({
skipFetch: isFetchingViews,
RecoilScopeContext,
});
const submitCurrentView = async () => {
await persistCardFields();
await persistBoardColumns();
await persistFilters();
await persistSorts();
};
return { createView, deleteView, submitCurrentView, updateView };
};

View File

@ -1,35 +0,0 @@
export const useMoveViewColumns = () => {
const handleColumnMove = <T extends { index: 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.index,
};
newArray[targetArrayIndex] = {
...currentEntity,
index: targetEntity.index,
};
return newArray;
}
return targetArray;
};
return { handleColumnMove };
};

View File

@ -0,0 +1,15 @@
import { useView } from '@/views/hooks/useView';
export const useRemoveFilter = () => {
const { setCurrentViewFilters } = useView();
const removeFilter = (filterKey: string) => {
setCurrentViewFilters?.((filters) => {
return filters.filter((filter) => {
return filter.key !== filterKey;
});
});
};
return removeFilter;
};

View File

@ -1,183 +0,0 @@
import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { availableTableColumnsScopedState } from '@/ui/data/data-table/states/availableTableColumnsScopedState';
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '@/ui/data/data-table/states/savedTableColumnsFamilyState';
import { savedTableColumnsByKeyFamilySelector } from '@/ui/data/data-table/states/selectors/savedTableColumnsByKeyFamilySelector';
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { GET_VIEW_FIELDS } from '../graphql/queries/getViewFields';
export const toViewFieldInput = (
objectId: string,
fieldDefinition: ColumnDefinition<FieldMetadata>,
) => ({
key: fieldDefinition.key,
name: fieldDefinition.name,
index: fieldDefinition.index,
isVisible: fieldDefinition.isVisible ?? true,
objectId,
size: fieldDefinition.size,
});
export const useTableViewFields = ({
objectId,
columnDefinitions,
skipFetch,
}: {
objectId: string;
columnDefinitions: ColumnDefinition<FieldMetadata>[];
skipFetch?: boolean;
}) => {
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
TableRecoilScopeContext,
);
const [previousViewId, setPreviousViewId] = useState<string | undefined>();
const [availableTableColumns, setAvailableTableColumns] =
useRecoilScopedState(
availableTableColumnsScopedState,
TableRecoilScopeContext,
);
const [tableColumns, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedTableColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const savedTableColumnsByKey = useRecoilValue(
savedTableColumnsByKeyFamilySelector(currentViewId),
);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const createViewFields = useCallback(
(columns: ColumnDefinition<FieldMetadata>[], viewId = currentViewId) => {
if (!viewId || !columns.length) return;
return createViewFieldsMutation({
variables: {
data: columns.map((column) => ({
...toViewFieldInput(objectId, column),
viewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[createViewFieldsMutation, currentViewId, objectId],
);
const updateViewFields = useCallback(
(columns: ColumnDefinition<FieldMetadata>[]) => {
if (!currentViewId || !columns.length) return;
return Promise.all(
columns.map((column) =>
updateViewFieldMutation({
variables: {
data: {
isVisible: column.isVisible,
size: column.size,
index: column.index,
},
where: {
viewId_key: { key: column.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewFieldMutation],
);
useGetViewFieldsQuery({
skip: !currentViewId || skipFetch || columnDefinitions.length === 0,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: async (data) => {
if (!data.viewFields.length) {
// Populate if empty
return createViewFields(columnDefinitions);
}
const nextColumns = data.viewFields
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
const columnDefinition = columnDefinitions.find(
({ key }) => viewField.key === key,
);
return columnDefinition
? {
...columnDefinition,
key: viewField.key,
name: viewField.name,
index: viewField.index,
size: viewField.size ?? columnDefinition.size,
isVisible: viewField.isVisible,
}
: null;
})
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
setSavedTableColumns(nextColumns);
if (
previousViewId !== currentViewId &&
!isDeeplyEqual(tableColumns, nextColumns)
) {
setTableColumns(nextColumns);
setPreviousViewId(currentViewId);
}
if (!availableTableColumns.length) {
setAvailableTableColumns(columnDefinitions);
}
},
});
const persistColumns = useCallback(
async (nextColumns: ColumnDefinition<FieldMetadata>[]) => {
if (!currentViewId) return;
const viewFieldsToCreate = nextColumns.filter(
(column) => !savedTableColumnsByKey[column.key],
);
await createViewFields(viewFieldsToCreate);
const viewFieldsToUpdate = nextColumns.filter(
(column) =>
savedTableColumnsByKey[column.key] &&
(savedTableColumnsByKey[column.key].size !== column.size ||
savedTableColumnsByKey[column.key].index !== column.index ||
savedTableColumnsByKey[column.key].isVisible !== column.isVisible),
);
await updateViewFields(viewFieldsToUpdate);
},
[createViewFields, currentViewId, savedTableColumnsByKey, updateViewFields],
);
return { createViewFields, persistColumns };
};

View File

@ -1,83 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewType } from '~/generated/graphql';
import { useTableViewFields } from './useTableViewFields';
import { useViewFilters } from './useViewFilters';
import { useViews } from './useViews';
import { useViewSorts } from './useViewSorts';
export const useTableViews = ({
objectId,
columnDefinitions,
}: {
objectId: string;
columnDefinitions: ColumnDefinition<FieldMetadata>[];
}) => {
const tableColumns = useRecoilScopedValue(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const filters = useRecoilScopedValue(
filtersScopedState,
TableRecoilScopeContext,
);
const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext);
const [_, setSearchParams] = useSearchParams();
const handleViewCreate = async (viewId: string) => {
await createViewFields(tableColumns, viewId);
await createViewFilters(filters, viewId);
await createViewSorts(sorts, viewId);
setSearchParams({ view: viewId });
};
const { createView, deleteView, isFetchingViews, updateView } = useViews({
objectId,
onViewCreate: handleViewCreate,
type: ViewType.Table,
RecoilScopeContext: TableRecoilScopeContext,
});
const { createViewFields, persistColumns } = useTableViewFields({
objectId,
columnDefinitions,
skipFetch: isFetchingViews,
});
const createDefaultViewFields = async () => {
await createViewFields(tableColumns);
};
const { createViewFilters, persistFilters } = useViewFilters({
RecoilScopeContext: TableRecoilScopeContext,
skipFetch: isFetchingViews,
});
const { createViewSorts, persistSorts } = useViewSorts({
RecoilScopeContext: TableRecoilScopeContext,
skipFetch: isFetchingViews,
});
const submitCurrentView = async () => {
await persistFilters();
await persistSorts();
};
return {
createView,
deleteView,
persistColumns,
submitCurrentView,
updateView,
createDefaultViewFields,
isFetchingViews,
};
};

View File

@ -0,0 +1,26 @@
import { produce } from 'immer';
import { Filter } from '@/ui/data/filter/types/Filter';
import { useView } from '@/views/hooks/useView';
export const useUpsertFilter = () => {
const { setCurrentViewFilters } = useView();
const upsertFilter = (filterToUpsert: Filter) => {
setCurrentViewFilters?.((filters) => {
return produce(filters, (filtersDraft) => {
const index = filtersDraft.findIndex(
(filter) => filter.key === filterToUpsert.key,
);
if (index === -1) {
filtersDraft.push(filterToUpsert);
} else {
filtersDraft[index] = filterToUpsert;
}
});
});
};
return upsertFilter;
};

View File

@ -0,0 +1,189 @@
import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { savedTableColumnsFamilyState } from '@/ui/data/data-table/states/savedTableColumnsFamilyState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
import { viewsScopedState } from '../states/viewsScopedState';
import { useViewFields } from './internal/useViewFields';
import { useViewFilters } from './internal/useViewFilters';
import { useViews } from './internal/useViews';
import { useViewSorts } from './internal/useViewSorts';
import { useViewStates } from './useViewStates';
type UseViewProps = {
viewScopeId?: string;
};
export const useView = (props?: UseViewProps) => {
const scopeId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
props?.viewScopeId,
);
const {
setCurrentViewId,
currentViewId,
setViews,
setViewEditMode,
setViewObjectId,
setViewType,
setEntityCountInCurrentView,
setIsViewBarExpanded,
setAvailableSorts,
setCurrentViewSorts,
setSavedViewSorts,
setAvailableFilters,
setCurrentViewFilters,
setSavedViewFilters,
setAvailableFields,
setCurrentViewFields,
setSavedViewFields,
} = useViewStates(scopeId);
const { persistViewSorts, upsertViewSort } = useViewSorts(scopeId);
const { persistViewFilters } = useViewFilters(scopeId);
const { persistViewFields } = useViewFields(scopeId);
const { createView: internalCreateView, deleteView: internalDeleteView } =
useViews(scopeId);
const [_, setSearchParams] = useSearchParams();
const resetViewBar = useRecoilCallback(({ snapshot }) => () => {
const savedViewFilters = snapshot
.getLoadable(
savedViewFiltersScopedFamilyState({
scopeId,
familyKey: currentViewId || '',
}),
)
.getValue();
const savedViewSorts = snapshot
.getLoadable(
savedViewSortsScopedFamilyState({
scopeId,
familyKey: currentViewId || '',
}),
)
.getValue();
if (savedViewFilters) {
setCurrentViewFilters?.(savedViewFilters);
}
if (savedViewSorts) {
setCurrentViewSorts?.(savedViewSorts);
}
setViewEditMode?.('none');
setIsViewBarExpanded?.(false);
});
const createView = useCallback(
async (name: string) => {
const newViewId = v4();
await internalCreateView({ id: v4(), name });
// await persistViewFields();
await persistViewFilters();
await persistViewSorts();
//setCurrentViewId(newViewId);
setSearchParams({ view: newViewId });
},
[internalCreateView, persistViewFilters, persistViewSorts, setSearchParams],
);
const updateCurrentView = async () => {
await persistViewFilters();
await persistViewSorts();
};
const removeView = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const currentViewId = await snapshot.getPromise(
currentViewIdScopedState({ scopeId }),
);
if (currentViewId === viewId)
set(currentViewIdScopedState({ scopeId }), undefined);
set(viewsScopedState({ scopeId }), (previousViews) =>
previousViews.filter((view) => view.id !== viewId),
);
internalDeleteView(viewId);
},
[internalDeleteView, scopeId],
);
const handleViewNameSubmit = useRecoilCallback(
({ snapshot, set }) =>
async (name?: string) => {
const viewEditMode = snapshot
.getLoadable(viewEditModeScopedState({ scopeId }))
.getValue();
const currentViewFields = snapshot
.getLoadable(
currentViewFieldsScopedFamilyState({
scopeId,
familyKey: currentViewId || '',
}),
)
.getValue();
const isCreateMode = viewEditMode === 'create';
if (isCreateMode && name && currentViewFields) {
await createView(name);
set(savedTableColumnsFamilyState(currentViewId), currentViewFields);
}
},
[createView, currentViewId, scopeId],
);
return {
scopeId,
currentViewId,
setCurrentViewId,
updateCurrentView,
createView,
removeView,
setIsViewBarExpanded,
resetViewBar,
handleViewNameSubmit,
setViews,
setViewEditMode,
setViewObjectId,
setViewType,
setEntityCountInCurrentView,
setAvailableSorts,
setCurrentViewSorts,
setSavedViewSorts,
upsertViewSort,
setAvailableFilters,
setCurrentViewFilters,
setSavedViewFilters,
setAvailableFields,
setCurrentViewFields,
setSavedViewFields,
persistViewFields,
};
};

View File

@ -1,178 +0,0 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState';
import { savedFiltersByKeyFamilySelector } from '@/ui/data/view-bar/states/selectors/savedFiltersByKeyFamilySelector';
import { Filter } from '@/ui/data/view-bar/types/Filter';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewFiltersMutation,
useDeleteViewFiltersMutation,
useGetViewFiltersQuery,
useUpdateViewFilterMutation,
} from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewFilters = ({
RecoilScopeContext,
skipFetch,
}: {
RecoilScopeContext: RecoilScopeContext;
skipFetch?: boolean;
}) => {
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
RecoilScopeContext,
);
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
RecoilScopeContext,
);
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
RecoilScopeContext,
);
const [, setSavedFilters] = useRecoilState(
savedFiltersFamilyState(currentViewId),
);
const savedFiltersByKey = useRecoilValue(
savedFiltersByKeyFamilySelector(currentViewId),
);
const { refetch } = useGetViewFiltersQuery({
skip: !currentViewId || skipFetch,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: (data) => {
const nextFilters = data.viewFilters
.map(({ __typename, name: _name, ...viewFilter }) => {
const availableFilter = availableFilters.find(
(filter) => filter.key === viewFilter.key,
);
return availableFilter
? {
...viewFilter,
displayValue: viewFilter.displayValue ?? viewFilter.value,
type: availableFilter.type,
}
: undefined;
})
.filter((filter): filter is Filter => !!filter);
if (!isDeeplyEqual(filters, nextFilters)) {
setSavedFilters(nextFilters);
setFilters(nextFilters);
}
},
});
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
const createViewFilters = useCallback(
(filters: Filter[], viewId = currentViewId) => {
if (!viewId || !filters.length) return;
return createViewFiltersMutation({
variables: {
data: filters.map((filter) => ({
displayValue: filter.displayValue ?? filter.value,
key: filter.key,
name:
availableFilters.find(({ key }) => key === filter.key)?.label ??
'',
operand: filter.operand,
value: filter.value,
viewId,
})),
},
});
},
[availableFilters, createViewFiltersMutation, currentViewId],
);
const updateViewFilters = useCallback(
(filters: Filter[]) => {
if (!currentViewId || !filters.length) return;
return Promise.all(
filters.map((filter) =>
updateViewFilterMutation({
variables: {
data: {
displayValue: filter.displayValue ?? filter.value,
operand: filter.operand,
value: filter.value,
},
where: {
viewId_key: { key: filter.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewFilterMutation],
);
const deleteViewFilters = useCallback(
(filterKeys: string[]) => {
if (!currentViewId || !filterKeys.length) return;
return deleteViewFiltersMutation({
variables: {
where: {
key: { in: filterKeys },
viewId: { equals: currentViewId },
},
},
});
},
[currentViewId, deleteViewFiltersMutation],
);
const persistFilters = useCallback(async () => {
if (!currentViewId) return;
const filtersToCreate = filters.filter(
(filter) => !savedFiltersByKey[filter.key],
);
await createViewFilters(filtersToCreate);
const filtersToUpdate = filters.filter(
(filter) =>
savedFiltersByKey[filter.key] &&
(savedFiltersByKey[filter.key].operand !== filter.operand ||
savedFiltersByKey[filter.key].value !== filter.value),
);
await updateViewFilters(filtersToUpdate);
const filterKeys = filters.map((filter) => filter.key);
const filterKeysToDelete = Object.keys(savedFiltersByKey).filter(
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
);
await deleteViewFilters(filterKeysToDelete);
return refetch();
}, [
currentViewId,
filters,
createViewFilters,
updateViewFilters,
savedFiltersByKey,
deleteViewFilters,
refetch,
]);
return { createViewFilters, persistFilters };
};

View File

@ -0,0 +1,244 @@
import { useRecoilValue } from 'recoil';
import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { availableSortsScopedState } from '../states/availableSortsScopedState';
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
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 { currentViewScopedSelector } from '../states/selectors/currentViewScopedSelector';
import { currentViewSortsOrderByScopedFamilySelector } from '../states/selectors/currentViewSortsOrderByScopedFamilySelector';
import { savedViewFieldByKeyScopedFamilySelector } from '../states/selectors/savedViewFieldByKeyScopedFamilySelector';
import { savedViewFiltersByKeyScopedFamilySelector } from '../states/selectors/savedViewFiltersByKeyScopedFamilySelector';
import { savedViewSortsByKeyScopedFamilySelector } from '../states/selectors/savedViewSortsByKeyScopedFamilySelector';
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
import { viewObjectIdScopeState } from '../states/viewObjectIdScopeState';
import { viewsScopedState } from '../states/viewsScopedState';
import { viewTypeScopedState } from '../states/viewTypeScopedState';
export const useViewInternalStates = (
viewScopeId?: string,
viewId?: string,
) => {
const scopeId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
viewScopeId,
);
// View
const [currentViewId, setCurrentViewId] = useRecoilScopedStateV2(
currentViewIdScopedState,
scopeId,
);
const familyItemId = viewId ? viewId : currentViewId;
const currentView = useRecoilValue(currentViewScopedSelector(scopeId));
const [viewEditMode, setViewEditMode] = useRecoilScopedStateV2(
viewEditModeScopedState,
scopeId,
);
const [views, setViews] = useRecoilScopedStateV2(viewsScopedState, scopeId);
const [viewObjectId, setViewObjectId] = useRecoilScopedStateV2(
viewObjectIdScopeState,
scopeId,
);
const [viewType, setViewType] = useRecoilScopedStateV2(
viewTypeScopedState,
scopeId,
);
const [entityCountInCurrentView, setEntityCountInCurrentView] =
useRecoilScopedStateV2(entityCountInCurrentViewScopedState, scopeId);
const [isViewBarExpanded, setIsViewBarExpanded] = useRecoilScopedStateV2(
isViewBarExpandedScopedState,
scopeId,
);
// ViewSorts
const [currentViewSorts, setCurrentViewSorts] = useRecoilScopedFamilyState(
currentViewSortsScopedFamilyState,
scopeId,
familyItemId,
);
const [savedViewSorts, setSavedViewSorts] = useRecoilScopedFamilyState(
savedViewSortsScopedFamilyState,
scopeId,
familyItemId,
);
const savedViewSortsByKey = useRecoilValue(
savedViewSortsByKeyScopedFamilySelector({
scopeId: scopeId,
viewId: familyItemId,
}),
);
const [availableSorts, setAvailableSorts] = useRecoilScopedStateV2(
availableSortsScopedState,
scopeId,
);
const canPersistSorts = useRecoilValue(
canPersistViewSortsScopedFamilySelector({
viewScopeId: scopeId,
viewId: familyItemId,
}),
);
const currentViewSortsOrderBy = useRecoilValue(
currentViewSortsOrderByScopedFamilySelector({
viewScopeId: scopeId,
viewId: familyItemId,
}),
);
// ViewFilters
const [currentViewFilters, setCurrentViewFilters] =
useRecoilScopedFamilyState(
currentViewFiltersScopedFamilyState,
scopeId,
familyItemId,
);
const [savedViewFilters, setSavedViewFilters] = useRecoilScopedFamilyState(
savedViewFiltersScopedFamilyState,
scopeId,
familyItemId,
);
const savedViewFiltersByKey = useRecoilValue(
savedViewFiltersByKeyScopedFamilySelector({
scopeId: scopeId,
viewId: familyItemId,
}),
);
const [availableFilters, setAvailableFilters] = useRecoilScopedStateV2(
availableFiltersScopedState,
scopeId,
);
const canPersistFilters = useRecoilValue(
canPersistViewFiltersScopedFamilySelector({
viewScopeId: scopeId,
viewId: familyItemId,
}),
);
// ViewFields
const [availableFields, setAvailableFields] = useRecoilScopedStateV2(
availableFieldsScopedState,
scopeId,
);
const [currentViewFields, setCurrentViewFields] = useRecoilScopedFamilyState(
currentViewFieldsScopedFamilyState,
scopeId,
familyItemId,
);
const [savedViewFields, setSavedViewFields] = useRecoilScopedFamilyState(
savedViewFieldsScopedFamilyState,
scopeId,
familyItemId,
);
const savedViewFieldsByKey = useRecoilValue(
savedViewFieldByKeyScopedFamilySelector({
viewScopeId: scopeId,
viewId: familyItemId,
}),
);
// ViewChangeHandlers
const [onViewSortsChange, setOnViewSortsChange] = useRecoilScopedStateV2(
onViewSortsChangeScopedState,
scopeId,
);
const [onViewFiltersChange, setOnViewFiltersChange] = useRecoilScopedStateV2(
onViewFiltersChangeScopedState,
scopeId,
);
const [onViewFieldsChange, setOnViewFieldsChange] = useRecoilScopedStateV2(
onViewFieldsChangeScopedState,
scopeId,
);
return {
currentViewId,
currentView,
setCurrentViewId,
isViewBarExpanded,
setIsViewBarExpanded,
views,
setViews,
viewEditMode,
setViewEditMode,
viewObjectId,
setViewObjectId,
viewType,
setViewType,
entityCountInCurrentView,
setEntityCountInCurrentView,
availableSorts,
setAvailableSorts,
currentViewSorts,
setCurrentViewSorts,
savedViewSorts,
savedViewSortsByKey,
setSavedViewSorts,
canPersistSorts,
currentViewSortsOrderBy,
availableFilters,
setAvailableFilters,
currentViewFilters,
setCurrentViewFilters,
savedViewFilters,
savedViewFiltersByKey,
setSavedViewFilters,
canPersistFilters,
availableFields,
setAvailableFields,
currentViewFields,
savedViewFieldsByKey,
setCurrentViewFields,
savedViewFields,
setSavedViewFields,
onViewSortsChange,
setOnViewSortsChange,
onViewFiltersChange,
setOnViewFiltersChange,
onViewFieldsChange,
setOnViewFieldsChange,
};
};

View File

@ -1,172 +0,0 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { availableSortsScopedState } from '@/ui/data/view-bar/states/availableSortsScopedState';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState';
import { savedSortsByKeyFamilySelector } from '@/ui/data/view-bar/states/selectors/savedSortsByKeyFamilySelector';
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
import { Sort } from '@/ui/data/view-bar/types/Sort';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewSortsMutation,
useDeleteViewSortsMutation,
useGetViewSortsQuery,
useUpdateViewSortMutation,
ViewSortDirection,
} from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewSorts = ({
RecoilScopeContext,
skipFetch,
}: {
RecoilScopeContext: RecoilScopeContext;
skipFetch?: boolean;
}) => {
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
RecoilScopeContext,
);
const [sorts, setSorts] = useRecoilScopedState(
sortsScopedState,
RecoilScopeContext,
);
const [availableSorts] = useRecoilScopedState(
availableSortsScopedState,
RecoilScopeContext,
);
const [, setSavedSorts] = useRecoilState(
savedSortsFamilyState(currentViewId),
);
const savedSortsByKey = useRecoilValue(
savedSortsByKeyFamilySelector(currentViewId),
);
const { refetch } = useGetViewSortsQuery({
skip: !currentViewId || skipFetch,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: (data) => {
const nextSorts = data.viewSorts
.map((viewSort) => {
const foundCorrespondingSortDefinition = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
if (foundCorrespondingSortDefinition) {
return {
key: viewSort.key,
definition: foundCorrespondingSortDefinition,
direction: viewSort.direction.toLowerCase(),
} as Sort;
} else {
return undefined;
}
})
.filter((sort): sort is Sort => !!sort);
if (!isDeeplyEqual(sorts, nextSorts)) {
setSavedSorts(nextSorts);
setSorts(nextSorts);
}
},
});
const [createViewSortsMutation] = useCreateViewSortsMutation();
const [updateViewSortMutation] = useUpdateViewSortMutation();
const [deleteViewSortsMutation] = useDeleteViewSortsMutation();
const createViewSorts = useCallback(
(sorts: Sort[], viewId = currentViewId) => {
if (!viewId || !sorts.length) return;
return createViewSortsMutation({
variables: {
data: sorts.map((sort) => ({
key: sort.key,
direction: sort.direction as ViewSortDirection,
name: sort.definition.label,
viewId,
})),
},
});
},
[createViewSortsMutation, currentViewId],
);
const updateViewSorts = useCallback(
(sorts: Sort[]) => {
if (!currentViewId || !sorts.length) return;
return Promise.all(
sorts.map((sort) =>
updateViewSortMutation({
variables: {
data: {
direction: sort.direction as ViewSortDirection,
},
where: {
viewId_key: { key: sort.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewSortMutation],
);
const deleteViewSorts = useCallback(
(sortKeys: string[]) => {
if (!currentViewId || !sortKeys.length) return;
return deleteViewSortsMutation({
variables: {
where: {
key: { in: sortKeys },
viewId: { equals: currentViewId },
},
},
});
},
[currentViewId, deleteViewSortsMutation],
);
const persistSorts = useCallback(async () => {
if (!currentViewId) return;
const sortsToCreate = sorts.filter((sort) => !savedSortsByKey[sort.key]);
await createViewSorts(sortsToCreate);
const sortsToUpdate = sorts.filter(
(sort) =>
savedSortsByKey[sort.key] &&
savedSortsByKey[sort.key].direction !== sort.direction,
);
await updateViewSorts(sortsToUpdate);
const sortKeys = sorts.map((sort) => sort.key);
const sortKeysToDelete = Object.keys(savedSortsByKey).filter(
(previousSortKey) => !sortKeys.includes(previousSortKey),
);
await deleteViewSorts(sortKeysToDelete);
return refetch();
}, [
currentViewId,
sorts,
createViewSorts,
updateViewSorts,
savedSortsByKey,
deleteViewSorts,
refetch,
]);
return { createViewSorts, persistSorts };
};

View File

@ -0,0 +1,138 @@
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
import { useSetRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedFamilyState';
import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { availableSortsScopedState } from '../states/availableSortsScopedState';
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { currentViewSortsScopedFamilyState } from '../states/currentViewSortsScopedFamilyState';
import { entityCountInCurrentViewScopedState } from '../states/entityCountInCurrentViewScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScopedFamilyState';
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
import { viewObjectIdScopeState } from '../states/viewObjectIdScopeState';
import { viewsScopedState } from '../states/viewsScopedState';
import { viewTypeScopedState } from '../states/viewTypeScopedState';
export const useViewStates = (viewScopeId?: string, viewId?: string) => {
const scopeId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
viewScopeId,
);
// View
const [currentViewId, setCurrentViewId] = useRecoilScopedStateV2(
currentViewIdScopedState,
scopeId,
);
const familyItemId = viewId ? viewId : currentViewId;
const setViewEditMode = useSetRecoilScopedStateV2(
viewEditModeScopedState,
scopeId,
);
const setViews = useSetRecoilScopedStateV2(viewsScopedState, scopeId);
const setViewObjectId = useSetRecoilScopedStateV2(
viewObjectIdScopeState,
scopeId,
);
const setViewType = useSetRecoilScopedStateV2(viewTypeScopedState, scopeId);
const setEntityCountInCurrentView = useSetRecoilScopedStateV2(
entityCountInCurrentViewScopedState,
scopeId,
);
const setIsViewBarExpanded = useSetRecoilScopedStateV2(
isViewBarExpandedScopedState,
scopeId,
);
// ViewSorts
const setCurrentViewSorts = useSetRecoilScopedFamilyState(
currentViewSortsScopedFamilyState,
scopeId,
familyItemId,
);
const setSavedViewSorts = useSetRecoilScopedFamilyState(
savedViewSortsScopedFamilyState,
scopeId,
familyItemId,
);
const setAvailableSorts = useSetRecoilScopedStateV2(
availableSortsScopedState,
scopeId,
);
// ViewFilters
const setCurrentViewFilters = useSetRecoilScopedFamilyState(
currentViewFiltersScopedFamilyState,
scopeId,
familyItemId,
);
const setSavedViewFilters = useSetRecoilScopedFamilyState(
savedViewFiltersScopedFamilyState,
scopeId,
familyItemId,
);
const setAvailableFilters = useSetRecoilScopedStateV2(
availableFiltersScopedState,
scopeId,
);
// ViewFields
const setAvailableFields = useSetRecoilScopedStateV2(
availableFieldsScopedState,
scopeId,
);
const setCurrentViewFields = useSetRecoilScopedFamilyState(
currentViewFieldsScopedFamilyState,
scopeId,
familyItemId,
);
const setSavedViewFields = useSetRecoilScopedFamilyState(
savedViewFieldsScopedFamilyState,
scopeId,
familyItemId,
);
return {
currentViewId,
setCurrentViewId,
setIsViewBarExpanded,
setViews,
setViewEditMode,
setViewObjectId,
setViewType,
setEntityCountInCurrentView,
setAvailableSorts,
setCurrentViewSorts,
setSavedViewSorts,
setAvailableFilters,
setCurrentViewFilters,
setSavedViewFilters,
setAvailableFields,
setCurrentViewFields,
setSavedViewFields,
};
};

View File

@ -1,102 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
import { viewsScopedState } from '@/ui/data/view-bar/states/viewsScopedState';
import { View } from '@/ui/data/view-bar/types/View';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import {
useCreateViewMutation,
useDeleteViewMutation,
useGetViewsQuery,
useUpdateViewMutation,
ViewType,
} from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { GET_VIEWS } from '../graphql/queries/getViews';
export const useViews = ({
objectId,
onViewCreate,
RecoilScopeContext,
type,
}: {
objectId: string;
onViewCreate?: (viewId: string) => Promise<void>;
RecoilScopeContext: RecoilScopeContext;
type: ViewType;
}) => {
const [currentViewId, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState,
RecoilScopeContext,
);
const [views, setViews] = useRecoilScopedState(
viewsScopedState,
RecoilScopeContext,
);
const [createViewMutation] = useCreateViewMutation();
const [updateViewMutation] = useUpdateViewMutation();
const [deleteViewMutation] = useDeleteViewMutation();
const createView = async (view: View) => {
const { data } = await createViewMutation({
variables: {
data: {
...view,
objectId,
type,
},
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
if (data?.view) await onViewCreate?.(data.view.id);
};
const updateView = async (view: View) => {
await updateViewMutation({
variables: {
data: { name: view.name },
where: { id: view.id },
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
};
const deleteView = async (viewId: string) => {
await deleteViewMutation({
variables: { where: { id: viewId } },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
};
const { loading } = useGetViewsQuery({
variables: {
where: {
objectId: { equals: objectId },
type: { equals: type },
},
},
onCompleted: (data) => {
const nextViews = data.views.map((view) => ({
id: view.id,
name: view.name,
}));
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
if (!nextViews.length) return;
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
},
});
return {
createView,
deleteView,
isFetchingViews: loading,
updateView,
};
};

View File

@ -0,0 +1,43 @@
import { ReactNode } from 'react';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { Filter } from '@/ui/data/filter/types/Filter';
import { Sort } from '@/ui/data/sort/types/Sort';
import { ViewScopeInitEffect } from './init-effect/ViewScopeInitEffect';
import { ViewScopeInternalContext } from './scope-internal-context/ViewScopeInternalContext';
type ViewScopeProps = {
children: ReactNode;
viewScopeId: string;
onViewSortsChange?: (sorts: Sort[]) => void | Promise<void>;
onViewFiltersChange?: (filters: Filter[]) => void | Promise<void>;
onViewFieldsChange?: (
fields: ColumnDefinition<FieldMetadata>[],
) => 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,46 @@
import { useEffect } from 'react';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { Filter } from '@/ui/data/filter/types/Filter';
import { Sort } from '@/ui/data/sort/types/Sort';
import { useView } from '@/views/hooks/useView';
import { useViewInternalStates } from '@/views/hooks/useViewInternalStates';
type ViewScopeInitEffectProps = {
viewScopeId: string;
onViewSortsChange?: (sorts: Sort[]) => void | Promise<void>;
onViewFiltersChange?: (filters: Filter[]) => void | Promise<void>;
onViewFieldsChange?: (
fields: ColumnDefinition<FieldMetadata>[],
) => void | Promise<void>;
};
export const ViewScopeInitEffect = ({
viewScopeId,
onViewSortsChange,
onViewFiltersChange,
onViewFieldsChange,
}: ViewScopeInitEffectProps) => {
const { currentViewId } = useView();
const {
setOnViewSortsChange,
setOnViewFieldsChange,
setOnViewFiltersChange,
} = useViewInternalStates(viewScopeId, currentViewId);
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 { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const availableFieldsScopedState = createScopedState<
ColumnDefinition<FieldMetadata>[]
>({
key: 'availableFieldsScopedState',
defaultValue: [],
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import { Filter } from '@/ui/data/filter/types/Filter';
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
export const currentViewFiltersScopedFamilyState = createScopedFamilyState<
Filter[],
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,10 @@
import { Sort } from '@/ui/data/sort/types/Sort';
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
export const currentViewSortsScopedFamilyState = createScopedFamilyState<
Sort[],
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 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,11 @@
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const onViewFieldsChangeScopedState = createScopedState<
| ((fields: ColumnDefinition<FieldMetadata>[]) => void | Promise<void>)
| undefined
>({
key: 'onViewFieldsChangeScopedState',
defaultValue: undefined,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
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,22 @@
import { selectorFamily } from 'recoil';
import { View } from '@/views/types/View';
import { currentViewIdScopedState } from '../currentViewIdScopedState';
import { viewsByIdScopedSelector } from './viewsByIdScopedSelector';
export const currentViewScopedSelector = selectorFamily<
View | undefined,
string
>({
key: 'currentViewScopedSelector',
get:
(scopeId) =>
({ get }) => {
const currentViewId = get(currentViewIdScopedState({ scopeId: scopeId }));
return currentViewId
? get(viewsByIdScopedSelector(scopeId))[currentViewId]
: undefined;
},
});

View File

@ -0,0 +1,26 @@
import { selectorFamily } from 'recoil';
import { reduceSortsToOrderBy } from '@/ui/data/sort/utils/helpers';
import { SortOrder } from '~/generated/graphql';
import { currentViewSortsScopedFamilyState } from '../currentViewSortsScopedFamilyState';
export const currentViewSortsOrderByScopedFamilySelector = selectorFamily({
key: 'currentViewSortsOrderByScopedFamilySelector',
get:
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
({ get }) => {
if (!viewId) {
return;
}
const orderBy = reduceSortsToOrderBy(
get(
currentViewSortsScopedFamilyState({
scopeId: viewScopeId,
familyKey: viewId,
}),
),
);
return orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }];
},
});

View File

@ -0,0 +1,32 @@
import { selectorFamily } from 'recoil';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
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, ColumnDefinition<FieldMetadata>>>(
(result, column) => ({ ...result, [column.key]: column }),
{},
);
},
});

View File

@ -0,0 +1,25 @@
import { selectorFamily } from 'recoil';
import { Filter } from '@/ui/data/filter/types/Filter';
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, Filter>>(
(result, filter) => ({ ...result, [filter.key]: filter }),
{},
);
},
});

View File

@ -0,0 +1,25 @@
import { selectorFamily } from 'recoil';
import { Sort } from '@/ui/data/sort/types/Sort';
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, Sort>>(
(result, sort) => ({ ...result, [sort.key]: 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,19 @@
import { selectorFamily } from 'recoil';
import { View } from '@/views/types/View';
import { viewsScopedState } from '../viewsScopedState';
export const viewsByIdScopedSelector = selectorFamily<
Record<string, View>,
string
>({
key: 'viewsByIdScopedSelector',
get:
(scopeId) =>
({ get }) =>
get(viewsScopedState({ scopeId: scopeId })).reduce<Record<string, View>>(
(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,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const viewObjectIdScopeState = createScopedState<string | undefined>({
key: 'viewObjectIdScopeState',
defaultValue: undefined,
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
export type ViewFieldForVisibility = Pick<
FieldDefinition<FieldMetadata>,
'key' | 'name' | 'Icon' | 'infoTooltipContent'
> & {
isVisible?: boolean;
index: number;
size?: number | undefined;
};

View File

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