Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -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;
|
||||
@ -0,0 +1,81 @@
|
||||
import { useCallback } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { UpdateViewDropdownId } from '@/views/constants/UpdateViewDropdownId';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
|
||||
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export type UpdateViewButtonGroupProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
export const UpdateViewButtonGroup = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: UpdateViewButtonGroupProps) => {
|
||||
const { updateCurrentView, setViewEditMode } = useViewBar();
|
||||
const { canPersistFiltersSelector, canPersistSortsSelector } =
|
||||
useViewScopedStates();
|
||||
|
||||
const canPersistFilters = useRecoilValue(canPersistFiltersSelector);
|
||||
const canPersistSorts = useRecoilValue(canPersistSortsSelector);
|
||||
|
||||
const canPersistView = canPersistFilters || canPersistSorts;
|
||||
|
||||
const handleCreateViewButtonClick = useCallback(() => {
|
||||
setViewEditMode('create');
|
||||
onViewEditModeChange?.();
|
||||
}, [setViewEditMode, onViewEditModeChange]);
|
||||
|
||||
const handleViewSubmit = async () => {
|
||||
await updateCurrentView?.();
|
||||
};
|
||||
|
||||
if (!canPersistView) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={UpdateViewDropdownId}>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
clickableComponent={
|
||||
<StyledContainer>
|
||||
<ButtonGroup size="small" accent="blue">
|
||||
<Button title="Update view" onClick={handleViewSubmit} />
|
||||
<Button size="small" Icon={IconChevronDown} />
|
||||
</ButtonGroup>
|
||||
</StyledContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
111
packages/twenty-front/src/modules/views/components/ViewBar.tsx
Normal file
111
packages/twenty-front/src/modules/views/components/ViewBar.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { TopBar } from '@/ui/layout/top-bar/TopBar';
|
||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { ViewScope } from '@/views/scopes/ViewScope';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
|
||||
|
||||
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
|
||||
import { ViewBarDetails } from './ViewBarDetails';
|
||||
import { ViewBarEffect } from './ViewBarEffect';
|
||||
import { ViewsDropdownButton } from './ViewsDropdownButton';
|
||||
|
||||
export type ViewBarProps = {
|
||||
viewBarId: string;
|
||||
className?: string;
|
||||
optionsDropdownButton: ReactNode;
|
||||
optionsDropdownScopeId: string;
|
||||
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
|
||||
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
|
||||
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export const ViewBar = ({
|
||||
viewBarId,
|
||||
className,
|
||||
optionsDropdownButton,
|
||||
optionsDropdownScopeId,
|
||||
onViewFieldsChange,
|
||||
onViewFiltersChange,
|
||||
onViewSortsChange,
|
||||
}: ViewBarProps) => {
|
||||
const { openDropdown: openOptionsDropdownButton } = useDropdown({
|
||||
dropdownScopeId: optionsDropdownScopeId,
|
||||
});
|
||||
const { upsertViewSort, upsertViewFilter } = useViewBar({
|
||||
viewBarId: viewBarId,
|
||||
});
|
||||
|
||||
const filterDropdownId = 'view-filter';
|
||||
const sortDropdownId = 'view-sort';
|
||||
|
||||
return (
|
||||
<ViewScope
|
||||
viewScopeId={viewBarId}
|
||||
onViewFieldsChange={onViewFieldsChange}
|
||||
onViewFiltersChange={onViewFiltersChange}
|
||||
onViewSortsChange={onViewSortsChange}
|
||||
>
|
||||
<ViewBarEffect />
|
||||
<ViewBarFilterEffect
|
||||
filterDropdownId={filterDropdownId}
|
||||
onFilterSelect={upsertViewFilter}
|
||||
/>
|
||||
<ViewBarSortEffect
|
||||
sortDropdownId={sortDropdownId}
|
||||
onSortSelect={upsertViewSort}
|
||||
/>
|
||||
|
||||
<TopBar
|
||||
className={className}
|
||||
leftComponent={
|
||||
<ViewsDropdownButton
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
|
||||
optionsDropdownScopeId={optionsDropdownScopeId}
|
||||
/>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
rightComponent={
|
||||
<>
|
||||
<ObjectFilterDropdownButton
|
||||
filterDropdownId={filterDropdownId}
|
||||
hotkeyScope={{
|
||||
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
|
||||
}}
|
||||
/>
|
||||
<ObjectSortDropdownButton
|
||||
sortDropdownId={sortDropdownId}
|
||||
hotkeyScope={{
|
||||
scope: FiltersHotkeyScope.ObjectSortDropdownButton,
|
||||
}}
|
||||
/>
|
||||
{optionsDropdownButton}
|
||||
</>
|
||||
}
|
||||
bottomComponent={
|
||||
<ViewBarDetails
|
||||
filterDropdownId={filterDropdownId}
|
||||
hasFilterButton
|
||||
rightComponent={
|
||||
<UpdateViewButtonGroup
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ViewScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,183 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
||||
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
|
||||
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
|
||||
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
|
||||
import SortOrFilterChip from './SortOrFilterChip';
|
||||
|
||||
export type ViewBarDetailsProps = {
|
||||
hasFilterButton?: boolean;
|
||||
rightComponent?: ReactNode;
|
||||
filterDropdownId?: string;
|
||||
};
|
||||
|
||||
const StyledBar = styled.div`
|
||||
align-items: center;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
z-index: 4;
|
||||
`;
|
||||
|
||||
const StyledChipcontainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
const StyledCancelButton = styled.button`
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: auto;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${(props) => {
|
||||
const horiz = props.theme.spacing(2);
|
||||
const vert = props.theme.spacing(1);
|
||||
return `${vert} ${horiz} ${vert} ${horiz}`;
|
||||
}};
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFilterContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledSeperatorContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSeperator = styled.div`
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.quaternary};
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledAddFilterContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
z-index: 5;
|
||||
`;
|
||||
|
||||
export const ViewBarDetails = ({
|
||||
hasFilterButton = false,
|
||||
rightComponent,
|
||||
filterDropdownId,
|
||||
}: ViewBarDetailsProps) => {
|
||||
const {
|
||||
currentViewSortsState,
|
||||
currentViewFiltersState,
|
||||
canPersistFiltersSelector,
|
||||
canPersistSortsSelector,
|
||||
isViewBarExpandedState,
|
||||
} = useViewScopedStates();
|
||||
const { icons } = useLazyLoadIcons();
|
||||
|
||||
const currentViewSorts = useRecoilValue(currentViewSortsState);
|
||||
const currentViewFilters = useRecoilValue(currentViewFiltersState);
|
||||
const canPersistFilters = useRecoilValue(canPersistFiltersSelector);
|
||||
const canPersistSorts = useRecoilValue(canPersistSortsSelector);
|
||||
const isViewBarExpanded = useRecoilValue(isViewBarExpandedState);
|
||||
|
||||
const { resetViewBar, removeViewSort, removeViewFilter } = useViewBar();
|
||||
|
||||
const canPersistView = canPersistFilters || canPersistSorts;
|
||||
|
||||
const handleCancelClick = () => {
|
||||
resetViewBar();
|
||||
};
|
||||
|
||||
const shouldExpandViewBar =
|
||||
canPersistView ||
|
||||
((currentViewSorts?.length || currentViewFilters?.length) &&
|
||||
isViewBarExpanded);
|
||||
|
||||
if (!shouldExpandViewBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledBar>
|
||||
<StyledFilterContainer>
|
||||
<StyledChipcontainer>
|
||||
{currentViewSorts?.map((sort) => {
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={sort.fieldMetadataId}
|
||||
testId={sort.fieldMetadataId}
|
||||
labelValue={sort.definition.label}
|
||||
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
|
||||
isSort
|
||||
onRemove={() => removeViewSort(sort.fieldMetadataId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!!currentViewSorts?.length && !!currentViewFilters?.length && (
|
||||
<StyledSeperatorContainer>
|
||||
<StyledSeperator />
|
||||
</StyledSeperatorContainer>
|
||||
)}
|
||||
{currentViewFilters?.map((filter) => {
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={filter.fieldMetadataId}
|
||||
testId={filter.fieldMetadataId}
|
||||
labelKey={filter.definition.label}
|
||||
labelValue={`${getOperandLabelShort(filter.operand)} ${
|
||||
filter.displayValue
|
||||
}`}
|
||||
Icon={icons[filter.definition.iconName]}
|
||||
onRemove={() => {
|
||||
removeViewFilter(filter.fieldMetadataId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledChipcontainer>
|
||||
{hasFilterButton && (
|
||||
<StyledAddFilterContainer>
|
||||
<AddObjectFilterFromDetailsButton
|
||||
filterDropdownId={filterDropdownId}
|
||||
/>
|
||||
</StyledAddFilterContainer>
|
||||
)}
|
||||
</StyledFilterContainer>
|
||||
{canPersistView && (
|
||||
<StyledCancelButton
|
||||
data-testid="cancel-button"
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
Reset
|
||||
</StyledCancelButton>
|
||||
)}
|
||||
{rightComponent}
|
||||
</StyledBar>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,87 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
|
||||
export const ViewBarEffect = () => {
|
||||
const {
|
||||
loadView,
|
||||
changeViewInUrl,
|
||||
loadViewFields,
|
||||
loadViewFilters,
|
||||
loadViewSorts,
|
||||
} = useViewBar();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const currentViewIdFromUrl = searchParams.get('view');
|
||||
|
||||
const {
|
||||
viewTypeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewsState,
|
||||
currentViewIdState,
|
||||
} = useViewScopedStates();
|
||||
|
||||
const [views, setViews] = useRecoilState(viewsState);
|
||||
const viewType = useRecoilValue(viewTypeState);
|
||||
const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState);
|
||||
const setCurrentViewId = useSetRecoilState(currentViewIdState);
|
||||
|
||||
const { records: newViews } = useFindManyRecords<GraphQLView>({
|
||||
skip: !viewObjectMetadataId,
|
||||
objectNameSingular: 'view',
|
||||
filter: {
|
||||
type: { eq: viewType },
|
||||
objectMetadataId: { eq: viewObjectMetadataId },
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!newViews.length) return;
|
||||
|
||||
if (!isDeeplyEqual(views, newViews)) {
|
||||
setViews(newViews);
|
||||
}
|
||||
|
||||
const currentView =
|
||||
newViews.find((view) => view.id === currentViewIdFromUrl) ??
|
||||
newViews[0] ??
|
||||
null;
|
||||
|
||||
if (!currentView) return;
|
||||
|
||||
setCurrentViewId(currentView.id);
|
||||
|
||||
if (currentView?.viewFields) {
|
||||
loadViewFields(currentView.viewFields, currentView.id);
|
||||
loadViewFilters(currentView.viewFilters, currentView.id);
|
||||
loadViewSorts(currentView.viewSorts, currentView.id);
|
||||
}
|
||||
|
||||
if (!currentViewIdFromUrl) return changeViewInUrl(currentView.id);
|
||||
}, [
|
||||
changeViewInUrl,
|
||||
currentViewIdFromUrl,
|
||||
loadViewFields,
|
||||
loadViewFilters,
|
||||
loadViewSorts,
|
||||
newViews,
|
||||
setCurrentViewId,
|
||||
setViews,
|
||||
views,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentViewIdFromUrl) return;
|
||||
|
||||
loadView(currentViewIdFromUrl);
|
||||
}, [currentViewIdFromUrl, loadView]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
|
||||
type ViewBarFilterEffectProps = {
|
||||
filterDropdownId: string;
|
||||
onFilterSelect?: ((filter: Filter) => void) | undefined;
|
||||
};
|
||||
|
||||
export const ViewBarFilterEffect = ({
|
||||
filterDropdownId,
|
||||
onFilterSelect,
|
||||
}: ViewBarFilterEffectProps) => {
|
||||
const { availableFilterDefinitionsState, currentViewFiltersState } =
|
||||
useViewScopedStates();
|
||||
|
||||
const availableFilterDefinitions = useRecoilValue(
|
||||
availableFilterDefinitionsState,
|
||||
);
|
||||
const {
|
||||
setAvailableFilterDefinitions,
|
||||
setOnFilterSelect,
|
||||
filterDefinitionUsedInDropdown,
|
||||
setObjectFilterDropdownSelectedRecordIds,
|
||||
isObjectFilterDropdownUnfolded,
|
||||
} = useFilterDropdown({ filterDropdownId: filterDropdownId });
|
||||
|
||||
useEffect(() => {
|
||||
if (availableFilterDefinitions) {
|
||||
setAvailableFilterDefinitions(availableFilterDefinitions);
|
||||
}
|
||||
|
||||
if (onFilterSelect) {
|
||||
setOnFilterSelect(() => onFilterSelect);
|
||||
}
|
||||
}, [
|
||||
availableFilterDefinitions,
|
||||
onFilterSelect,
|
||||
setAvailableFilterDefinitions,
|
||||
setOnFilterSelect,
|
||||
]);
|
||||
|
||||
const currentViewFilters = useRecoilValue(currentViewFiltersState);
|
||||
|
||||
useEffect(() => {
|
||||
if (filterDefinitionUsedInDropdown?.type === 'RELATION') {
|
||||
const viewFilterUsedInDropdown = currentViewFilters.find(
|
||||
(filter) =>
|
||||
filter.fieldMetadataId ===
|
||||
filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
);
|
||||
|
||||
const viewFilterSelectedRecordIds = JSON.parse(
|
||||
viewFilterUsedInDropdown?.value ?? '[]',
|
||||
);
|
||||
|
||||
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds);
|
||||
}
|
||||
}, [
|
||||
filterDefinitionUsedInDropdown,
|
||||
currentViewFilters,
|
||||
setObjectFilterDropdownSelectedRecordIds,
|
||||
isObjectFilterDropdownUnfolded,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown';
|
||||
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
|
||||
type ViewBarSortEffectProps = {
|
||||
sortDropdownId: string;
|
||||
onSortSelect?: ((sort: Sort) => void) | undefined;
|
||||
};
|
||||
|
||||
export const ViewBarSortEffect = ({
|
||||
sortDropdownId,
|
||||
onSortSelect,
|
||||
}: ViewBarSortEffectProps) => {
|
||||
const { availableSortDefinitionsState } = useViewScopedStates();
|
||||
|
||||
const availableSortDefinitions = useRecoilValue(
|
||||
availableSortDefinitionsState,
|
||||
);
|
||||
|
||||
const { setAvailableSortDefinitions, setOnSortSelect } = useSortDropdown({
|
||||
sortDropdownId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (availableSortDefinitions) {
|
||||
setAvailableSortDefinitions(availableSortDefinitions);
|
||||
}
|
||||
if (onSortSelect) {
|
||||
setOnSortSelect(() => onSortSelect);
|
||||
}
|
||||
}, [
|
||||
availableSortDefinitions,
|
||||
onSortSelect,
|
||||
setAvailableSortDefinitions,
|
||||
setOnSortSelect,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,152 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { IconMinus, IconPlus } from '@/ui/display/icon';
|
||||
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
|
||||
import { IconInfoCircle } from '@/ui/input/constants/icons';
|
||||
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type ViewFieldsVisibilityDropdownSectionProps = {
|
||||
fields: Omit<ColumnDefinition<FieldMetadata>, 'size'>[];
|
||||
onVisibilityChange: (
|
||||
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => void;
|
||||
title: string;
|
||||
isVisible: boolean;
|
||||
isDraggable: boolean;
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
};
|
||||
|
||||
export const ViewFieldsVisibilityDropdownSection = ({
|
||||
fields,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
isVisible,
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
}: ViewFieldsVisibilityDropdownSectionProps) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
|
||||
const [openToolTipIndex, setOpenToolTipIndex] = useState<number>();
|
||||
|
||||
const handleInfoButtonClick = (index: number) => {
|
||||
if (index === openToolTipIndex) setOpenToolTipIndex(undefined);
|
||||
else setOpenToolTipIndex(index);
|
||||
};
|
||||
|
||||
const { icons } = useLazyLoadIcons();
|
||||
|
||||
const getIconButtons = (
|
||||
index: number,
|
||||
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
|
||||
) => {
|
||||
if (field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: IconInfoCircle,
|
||||
onClick: () => handleInfoButtonClick(index),
|
||||
isActive: openToolTipIndex === index,
|
||||
},
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [ref],
|
||||
callback: () => {
|
||||
setOpenToolTipIndex(undefined);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{isDraggable ? (
|
||||
<DraggableList
|
||||
onDragEnd={handleOnDrag}
|
||||
draggableItems={
|
||||
<>
|
||||
{[...fields]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((field, index) => (
|
||||
<DraggableItem
|
||||
key={field.fieldMetadataId}
|
||||
draggableId={field.fieldMetadataId}
|
||||
index={index + 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
key={field.fieldMetadataId}
|
||||
LeftIcon={icons[field.iconName]}
|
||||
iconButtons={getIconButtons(index + 1, field)}
|
||||
isTooltipOpen={openToolTipIndex === index + 1}
|
||||
text={field.label}
|
||||
className={`${title}-draggable-item-tooltip-anchor-${
|
||||
index + 1
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
fields.map((field, index) => (
|
||||
<MenuItem
|
||||
key={field.fieldMetadataId}
|
||||
LeftIcon={icons[field.iconName]}
|
||||
iconButtons={getIconButtons(index, field)}
|
||||
isTooltipOpen={openToolTipIndex === index}
|
||||
text={field.label}
|
||||
className={`${title}-fixed-item-tooltip-anchor-${index}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(openToolTipIndex) &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`.${title}-${
|
||||
isDraggable ? 'draggable' : 'fixed'
|
||||
}-item-tooltip-anchor-${openToolTipIndex}`}
|
||||
place="left"
|
||||
content={fields[openToolTipIndex].infoTooltipContent}
|
||||
isOpen={true}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,187 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconList,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from '@/ui/display/icon';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { ViewsDropdownId } from '../constants/ViewsDropdownId';
|
||||
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
|
||||
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledDropdownLabelAdornments = styled.span`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.grayScale.gray35};
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewIcon = styled(IconList)`
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewName = styled.span`
|
||||
display: inline-block;
|
||||
max-width: 130px;
|
||||
@media (max-width: 375px) {
|
||||
max-width: 90px;
|
||||
}
|
||||
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
max-width: 110px;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export type ViewsDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
onViewEditModeChange?: () => void;
|
||||
optionsDropdownScopeId: string;
|
||||
};
|
||||
|
||||
export const ViewsDropdownButton = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
optionsDropdownScopeId,
|
||||
}: ViewsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const { removeView, changeViewInUrl } = useViewBar();
|
||||
|
||||
const { viewsState, currentViewSelector, entityCountInCurrentViewState } =
|
||||
useViewScopedStates();
|
||||
|
||||
const views = useRecoilValue(viewsState);
|
||||
const currentView = useRecoilValue(currentViewSelector);
|
||||
const entityCountInCurrentView = useRecoilValue(
|
||||
entityCountInCurrentViewState,
|
||||
);
|
||||
|
||||
const { setViewEditMode, setCurrentViewId, loadView } = useViewBar();
|
||||
|
||||
const {
|
||||
isDropdownOpen: isViewsDropdownOpen,
|
||||
closeDropdown: closeViewsDropdown,
|
||||
} = useDropdown({
|
||||
dropdownScopeId: ViewsDropdownId,
|
||||
});
|
||||
|
||||
const { openDropdown: openOptionsDropdown } = useDropdown({
|
||||
dropdownScopeId: optionsDropdownScopeId,
|
||||
});
|
||||
|
||||
const handleViewSelect = useRecoilCallback(
|
||||
() => async (viewId: string) => {
|
||||
changeViewInUrl(viewId);
|
||||
loadView(viewId);
|
||||
closeViewsDropdown();
|
||||
},
|
||||
[changeViewInUrl, closeViewsDropdown, loadView],
|
||||
);
|
||||
|
||||
const handleAddViewButtonClick = () => {
|
||||
setViewEditMode('create');
|
||||
onViewEditModeChange?.();
|
||||
closeViewsDropdown();
|
||||
openOptionsDropdown();
|
||||
};
|
||||
|
||||
const handleEditViewButtonClick = (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
changeViewInUrl(viewId);
|
||||
setCurrentViewId(viewId);
|
||||
setViewEditMode('edit');
|
||||
onViewEditModeChange?.();
|
||||
closeViewsDropdown();
|
||||
openOptionsDropdown();
|
||||
};
|
||||
|
||||
const handleDeleteViewButtonClick = async (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
|
||||
await removeView(viewId);
|
||||
closeViewsDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={ViewsDropdownId}>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isViewsDropdownOpen}>
|
||||
<StyledViewIcon size={theme.icon.size.md} />
|
||||
<StyledViewName>{currentView?.name ?? 'All'}</StyledViewName>
|
||||
<StyledDropdownLabelAdornments>
|
||||
· {entityCountInCurrentView}{' '}
|
||||
<IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledDropdownLabelAdornments>
|
||||
</StyledDropdownButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{views.map((view) => (
|
||||
<MenuItem
|
||||
key={view.id}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPencil,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleEditViewButtonClick(event, view.id),
|
||||
},
|
||||
views.length > 1
|
||||
? {
|
||||
Icon: IconTrash,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleDeleteViewButtonClick(event, view.id),
|
||||
}
|
||||
: null,
|
||||
].filter(assertNotNull)}
|
||||
onClick={() => handleViewSelect(view.id)}
|
||||
LeftIcon={IconList}
|
||||
text={view.name}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<StyledBoldDropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleAddViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add view"
|
||||
/>
|
||||
</StyledBoldDropdownMenuItemsContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const UpdateViewDropdownId = 'update-view';
|
||||
@ -0,0 +1 @@
|
||||
export const ViewsDropdownId = 'views';
|
||||
@ -0,0 +1,2 @@
|
||||
// TODO: find a better pattern than using '' as a fallback
|
||||
export const UNDEFINED_FAMILY_ITEM_ID = '';
|
||||
@ -0,0 +1,155 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { getViewScopedStatesFromSnapshot } from '@/views/utils/getViewScopedStatesFromSnapshot';
|
||||
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
|
||||
|
||||
export const useViewFields = (viewScopeId: string) => {
|
||||
const { updateOneRecordMutation, createOneRecordMutation } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: 'viewField',
|
||||
});
|
||||
|
||||
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: 'view',
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const persistViewFields = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (viewFieldsToPersist: ViewField[], viewId?: string) => {
|
||||
const {
|
||||
viewObjectMetadataId,
|
||||
currentViewId,
|
||||
savedViewFieldsByKey,
|
||||
onViewFieldsChange,
|
||||
views,
|
||||
} = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
});
|
||||
|
||||
const {
|
||||
isPersistingViewState,
|
||||
currentViewFieldsState,
|
||||
savedViewFieldsState,
|
||||
} = getViewScopedStatesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
});
|
||||
|
||||
const viewIdToPersist = viewId ?? currentViewId;
|
||||
|
||||
if (!currentViewId || !savedViewFieldsByKey || !viewObjectMetadataId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _createViewFields = (viewFieldsToCreate: ViewField[]) => {
|
||||
if (!viewFieldsToCreate.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
viewFieldsToCreate.map((viewField) =>
|
||||
apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
viewId: viewIdToPersist,
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const _updateViewFields = (viewFieldsToUpdate: ViewField[]) => {
|
||||
if (!viewFieldsToUpdate.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
viewFieldsToUpdate.map((viewField) =>
|
||||
apolloClient.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate: viewField.id,
|
||||
input: {
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const viewFieldsToCreate = viewFieldsToPersist.filter(
|
||||
(viewField) => !savedViewFieldsByKey[viewField.fieldMetadataId],
|
||||
);
|
||||
|
||||
const viewFieldsToUpdate = viewFieldsToPersist.filter(
|
||||
(viewFieldToPersit) =>
|
||||
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId] &&
|
||||
(savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId].size !==
|
||||
viewFieldToPersit.size ||
|
||||
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId]
|
||||
.position !== viewFieldToPersit.position ||
|
||||
savedViewFieldsByKey[viewFieldToPersit.fieldMetadataId]
|
||||
.isVisible !== viewFieldToPersit.isVisible),
|
||||
);
|
||||
|
||||
set(isPersistingViewState, true);
|
||||
|
||||
await _createViewFields(viewFieldsToCreate);
|
||||
|
||||
await _updateViewFields(viewFieldsToUpdate);
|
||||
|
||||
set(isPersistingViewState, false);
|
||||
set(currentViewFieldsState, viewFieldsToPersist);
|
||||
set(savedViewFieldsState, viewFieldsToPersist);
|
||||
|
||||
const existingView = views.find((view) => view.id === viewIdToPersist);
|
||||
|
||||
if (!existingView) {
|
||||
return;
|
||||
}
|
||||
|
||||
modifyRecordFromCache(viewIdToPersist ?? '', {
|
||||
viewFields: () => ({
|
||||
edges: viewFieldsToPersist.map((viewField) => ({
|
||||
node: viewField,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
onViewFieldsChange?.(viewFieldsToPersist);
|
||||
},
|
||||
[
|
||||
viewScopeId,
|
||||
modifyRecordFromCache,
|
||||
apolloClient,
|
||||
createOneRecordMutation,
|
||||
updateOneRecordMutation,
|
||||
],
|
||||
);
|
||||
|
||||
return { persistViewFields };
|
||||
};
|
||||
@ -0,0 +1,252 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { savedViewFiltersScopedFamilyState } from '@/views/states/savedViewFiltersScopedFamilyState';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
|
||||
|
||||
import { useViewScopedStates } from './useViewScopedStates';
|
||||
|
||||
export const useViewFilters = (viewScopeId: string) => {
|
||||
const {
|
||||
updateOneRecordMutation,
|
||||
createOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: 'viewFilter',
|
||||
});
|
||||
|
||||
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: 'view',
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { currentViewFiltersState } = useViewScopedStates({
|
||||
viewScopeId: viewScopeId,
|
||||
});
|
||||
|
||||
const persistViewFilters = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (viewId?: string) => {
|
||||
const {
|
||||
currentViewId,
|
||||
currentViewFilters,
|
||||
savedViewFiltersByKey,
|
||||
views,
|
||||
} = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
if (!currentViewFilters) {
|
||||
return;
|
||||
}
|
||||
if (!savedViewFiltersByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createViewFilters = (viewFiltersToCreate: ViewFilter[]) => {
|
||||
if (!viewFiltersToCreate.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewFiltersToCreate.map((viewFilter) =>
|
||||
apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: viewId ?? currentViewId,
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const updateViewFilters = (viewFiltersToUpdate: ViewFilter[]) => {
|
||||
if (!viewFiltersToUpdate.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewFiltersToUpdate.map((viewFilter) =>
|
||||
apolloClient.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate: viewFilter.id,
|
||||
input: {
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteViewFilters = (viewFilterIdsToDelete: string[]) => {
|
||||
if (!viewFilterIdsToDelete.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewFilterIdsToDelete.map((viewFilterId) =>
|
||||
apolloClient.mutate({
|
||||
mutation: deleteOneRecordMutation,
|
||||
variables: {
|
||||
idToDelete: viewFilterId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const filtersToCreate = currentViewFilters.filter(
|
||||
(filter) => !savedViewFiltersByKey[filter.fieldMetadataId],
|
||||
);
|
||||
await createViewFilters(filtersToCreate);
|
||||
|
||||
const filtersToUpdate = currentViewFilters.filter(
|
||||
(filter) =>
|
||||
savedViewFiltersByKey[filter.fieldMetadataId] &&
|
||||
(savedViewFiltersByKey[filter.fieldMetadataId].operand !==
|
||||
filter.operand ||
|
||||
savedViewFiltersByKey[filter.fieldMetadataId].value !==
|
||||
filter.value),
|
||||
);
|
||||
await updateViewFilters(filtersToUpdate);
|
||||
|
||||
const filterKeys = currentViewFilters.map(
|
||||
(filter) => filter.fieldMetadataId,
|
||||
);
|
||||
const filterKeysToDelete = Object.keys(savedViewFiltersByKey).filter(
|
||||
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
|
||||
);
|
||||
const filterIdsToDelete = filterKeysToDelete.map(
|
||||
(filterKeyToDelete) =>
|
||||
savedViewFiltersByKey[filterKeyToDelete].id ?? '',
|
||||
);
|
||||
await deleteViewFilters(filterIdsToDelete);
|
||||
set(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId ?? currentViewId,
|
||||
}),
|
||||
currentViewFilters,
|
||||
);
|
||||
|
||||
const existingViewId = viewId ?? currentViewId;
|
||||
const existingView = views.find((view) => view.id === existingViewId);
|
||||
|
||||
if (!existingView) {
|
||||
return;
|
||||
}
|
||||
|
||||
modifyRecordFromCache(existingViewId, {
|
||||
viewFilters: () => ({
|
||||
edges: currentViewFilters.map((viewFilter) => ({
|
||||
node: viewFilter,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
createOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
modifyRecordFromCache,
|
||||
updateOneRecordMutation,
|
||||
viewScopeId,
|
||||
],
|
||||
);
|
||||
|
||||
const upsertViewFilter = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(filterToUpsert: Filter) => {
|
||||
const { currentViewId, savedViewFiltersByKey, onViewFiltersChange } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!savedViewFiltersByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingSavedFilterId =
|
||||
savedViewFiltersByKey[filterToUpsert.fieldMetadataId]?.id;
|
||||
|
||||
set(currentViewFiltersState, (filters) => {
|
||||
const newViewFilters = produce(filters, (filtersDraft) => {
|
||||
const existingFilterIndex = filtersDraft.findIndex(
|
||||
(filter) =>
|
||||
filter.fieldMetadataId === filterToUpsert.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (existingFilterIndex === -1 && filterToUpsert.value !== '') {
|
||||
filtersDraft.push({
|
||||
...filterToUpsert,
|
||||
id: existingSavedFilterId,
|
||||
});
|
||||
return filtersDraft;
|
||||
}
|
||||
|
||||
if (filterToUpsert.value === '') {
|
||||
filtersDraft.splice(existingFilterIndex, 1);
|
||||
return filtersDraft;
|
||||
}
|
||||
|
||||
filtersDraft[existingFilterIndex] = {
|
||||
...filterToUpsert,
|
||||
id: existingSavedFilterId,
|
||||
};
|
||||
});
|
||||
onViewFiltersChange?.(newViewFilters);
|
||||
return newViewFilters;
|
||||
});
|
||||
},
|
||||
[currentViewFiltersState, viewScopeId],
|
||||
);
|
||||
|
||||
const removeViewFilter = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(fieldMetadataId: string) => {
|
||||
const { currentViewId, currentViewFilters, onViewFiltersChange } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newViewFilters = currentViewFilters.filter((filter) => {
|
||||
return filter.fieldMetadataId !== fieldMetadataId;
|
||||
});
|
||||
set(currentViewFiltersState, newViewFilters);
|
||||
onViewFiltersChange?.(newViewFilters);
|
||||
},
|
||||
[currentViewFiltersState, viewScopeId],
|
||||
);
|
||||
|
||||
return { persistViewFilters, removeViewFilter, upsertViewFilter };
|
||||
};
|
||||
@ -0,0 +1,86 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
|
||||
import { UNDEFINED_FAMILY_ITEM_ID } from '../../constants';
|
||||
import { ViewScopeInternalContext } from '../../scopes/scope-internal-context/ViewScopeInternalContext';
|
||||
import { currentViewIdScopedState } from '../../states/currentViewIdScopedState';
|
||||
import { getViewScopedStates } from '../../utils/internal/getViewScopedStates';
|
||||
|
||||
export const useViewScopedStates = (args?: { viewScopeId?: string }) => {
|
||||
const { viewScopeId } = args ?? {};
|
||||
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
ViewScopeInternalContext,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
// View
|
||||
const [currentViewId] = useRecoilState(
|
||||
getScopedState(currentViewIdScopedState, scopeId),
|
||||
);
|
||||
|
||||
const viewId = currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
|
||||
|
||||
const {
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersSelector,
|
||||
canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
currentViewSortsState,
|
||||
entityCountInCurrentViewState,
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
viewsState,
|
||||
} = getViewScopedStates({
|
||||
viewScopeId: scopeId,
|
||||
viewId,
|
||||
});
|
||||
|
||||
return {
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersSelector,
|
||||
canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
currentViewSortsState,
|
||||
entityCountInCurrentViewState,
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
viewsState,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,230 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
|
||||
import { savedViewSortsScopedFamilyState } from '@/views/states/savedViewSortsScopedFamilyState';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
|
||||
|
||||
import { useViewScopedStates } from './useViewScopedStates';
|
||||
|
||||
export const useViewSorts = (viewScopeId: string) => {
|
||||
const {
|
||||
updateOneRecordMutation,
|
||||
createOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: 'viewSort',
|
||||
});
|
||||
|
||||
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: 'view',
|
||||
});
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { currentViewSortsState } = useViewScopedStates({
|
||||
viewScopeId: viewScopeId,
|
||||
});
|
||||
|
||||
const persistViewSorts = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (viewId?: string) => {
|
||||
const { currentViewId, currentViewSorts, savedViewSortsByKey, views } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentViewSorts) {
|
||||
return;
|
||||
}
|
||||
if (!savedViewSortsByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createViewSorts = (viewSortsToCreate: ViewSort[]) => {
|
||||
if (!viewSortsToCreate.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewSortsToCreate.map((viewSort) =>
|
||||
apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: viewId ?? currentViewId,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const updateViewSorts = (viewSortsToUpdate: ViewSort[]) => {
|
||||
if (!viewSortsToUpdate.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewSortsToUpdate.map((viewSort) =>
|
||||
apolloClient.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate: viewSort.id,
|
||||
input: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteViewSorts = (viewSortIdsToDelete: string[]) => {
|
||||
if (!viewSortIdsToDelete.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewSortIdsToDelete.map((viewSortId) =>
|
||||
apolloClient.mutate({
|
||||
mutation: deleteOneRecordMutation,
|
||||
variables: {
|
||||
idToDelete: viewSortId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const sortsToCreate = currentViewSorts.filter(
|
||||
(sort) => !savedViewSortsByKey[sort.fieldMetadataId],
|
||||
);
|
||||
|
||||
await createViewSorts(sortsToCreate);
|
||||
|
||||
const sortsToUpdate = currentViewSorts.filter(
|
||||
(sort) =>
|
||||
savedViewSortsByKey[sort.fieldMetadataId] &&
|
||||
savedViewSortsByKey[sort.fieldMetadataId].direction !==
|
||||
sort.direction,
|
||||
);
|
||||
await updateViewSorts(sortsToUpdate);
|
||||
|
||||
const sortKeys = currentViewSorts.map((sort) => sort.fieldMetadataId);
|
||||
const sortKeysToDelete = Object.keys(savedViewSortsByKey).filter(
|
||||
(previousSortKey) => !sortKeys.includes(previousSortKey),
|
||||
);
|
||||
const sortIdsToDelete = sortKeysToDelete.map(
|
||||
(sortKeyToDelete) => savedViewSortsByKey[sortKeyToDelete].id ?? '',
|
||||
);
|
||||
await deleteViewSorts(sortIdsToDelete);
|
||||
set(
|
||||
savedViewSortsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId ?? currentViewId,
|
||||
}),
|
||||
currentViewSorts,
|
||||
);
|
||||
const existingViewId = viewId ?? currentViewId;
|
||||
const existingView = views.find((view) => view.id === existingViewId);
|
||||
|
||||
if (!existingView) {
|
||||
return;
|
||||
}
|
||||
|
||||
modifyRecordFromCache(existingViewId, {
|
||||
viewSorts: () => ({
|
||||
edges: currentViewSorts.map((viewSort) => ({
|
||||
node: viewSort,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
createOneRecordMutation,
|
||||
deleteOneRecordMutation,
|
||||
modifyRecordFromCache,
|
||||
updateOneRecordMutation,
|
||||
viewScopeId,
|
||||
],
|
||||
);
|
||||
|
||||
const upsertViewSort = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(sortToUpsert: Sort) => {
|
||||
const { currentViewId, onViewSortsChange, savedViewSortsByKey } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!savedViewSortsByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingSavedSortId =
|
||||
savedViewSortsByKey[sortToUpsert.fieldMetadataId]?.id;
|
||||
|
||||
set(currentViewSortsState, (sorts) => {
|
||||
const newViewSorts = produce(sorts, (sortsDraft) => {
|
||||
const existingSortIndex = sortsDraft.findIndex(
|
||||
(sort) => sort.fieldMetadataId === sortToUpsert.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (existingSortIndex === -1) {
|
||||
sortsDraft.push({ ...sortToUpsert, id: existingSavedSortId });
|
||||
return sortsDraft;
|
||||
}
|
||||
|
||||
sortsDraft[existingSortIndex] = {
|
||||
...sortToUpsert,
|
||||
id: existingSavedSortId,
|
||||
};
|
||||
});
|
||||
onViewSortsChange?.(newViewSorts);
|
||||
return newViewSorts;
|
||||
});
|
||||
},
|
||||
[currentViewSortsState, viewScopeId],
|
||||
);
|
||||
|
||||
const removeViewSort = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(fieldMetadataId: string) => {
|
||||
const { currentViewId, onViewSortsChange, currentViewSorts } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
});
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newViewSorts = currentViewSorts.filter((filter) => {
|
||||
return filter.fieldMetadataId !== fieldMetadataId;
|
||||
});
|
||||
set(currentViewSortsState, newViewSorts);
|
||||
onViewSortsChange?.(newViewSorts);
|
||||
},
|
||||
[currentViewSortsState, viewScopeId],
|
||||
);
|
||||
|
||||
return { persistViewSorts, upsertViewSort, removeViewSort };
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { getViewScopedStateValuesFromSnapshot } from '@/views/utils/getViewScopedStateValuesFromSnapshot';
|
||||
|
||||
export const useViews = (scopeId: string) => {
|
||||
const {
|
||||
updateOneRecordMutation: updateOneMutation,
|
||||
createOneRecordMutation: createOneMutation,
|
||||
deleteOneRecordMutation: deleteOneMutation,
|
||||
findManyRecordsQuery: findManyQuery,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: 'view',
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const createView = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (view: Pick<GraphQLView, 'id' | 'name'>) => {
|
||||
const { viewObjectMetadataId, viewType } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
if (!viewObjectMetadataId || !viewType) {
|
||||
return;
|
||||
}
|
||||
await apolloClient.mutate({
|
||||
mutation: createOneMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
objectMetadataId: viewObjectMetadataId,
|
||||
type: viewType,
|
||||
},
|
||||
},
|
||||
refetchQueries: [findManyQuery],
|
||||
});
|
||||
},
|
||||
[scopeId, apolloClient, createOneMutation, findManyQuery],
|
||||
);
|
||||
|
||||
const updateView = async (view: GraphQLView) => {
|
||||
await apolloClient.mutate({
|
||||
mutation: updateOneMutation,
|
||||
variables: {
|
||||
idToUpdate: view.id,
|
||||
input: {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
},
|
||||
},
|
||||
refetchQueries: [findManyQuery],
|
||||
});
|
||||
};
|
||||
|
||||
const deleteView = async (viewId: string) => {
|
||||
await apolloClient.mutate({
|
||||
mutation: deleteOneMutation,
|
||||
variables: {
|
||||
idToDelete: viewId,
|
||||
},
|
||||
refetchQueries: [findManyQuery],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createView,
|
||||
deleteView,
|
||||
isFetchingViews: false,
|
||||
updateView,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
export const useMoveViewColumns = () => {
|
||||
const handleColumnMove = <T extends { position: number }>(
|
||||
direction: 'left' | 'right',
|
||||
currentArrayindex: number,
|
||||
targetArray: T[],
|
||||
) => {
|
||||
const targetArrayIndex =
|
||||
direction === 'left' ? currentArrayindex - 1 : currentArrayindex + 1;
|
||||
const targetArraySize = targetArray.length - 1;
|
||||
if (
|
||||
currentArrayindex >= 0 &&
|
||||
targetArrayIndex >= 0 &&
|
||||
currentArrayindex <= targetArraySize &&
|
||||
targetArrayIndex <= targetArraySize
|
||||
) {
|
||||
const currentEntity = targetArray[currentArrayindex];
|
||||
const targetEntity = targetArray[targetArrayIndex];
|
||||
const newArray = [...targetArray];
|
||||
|
||||
newArray[currentArrayindex] = {
|
||||
...targetEntity,
|
||||
index: currentEntity.position,
|
||||
};
|
||||
newArray[targetArrayIndex] = {
|
||||
...currentEntity,
|
||||
index: targetEntity.position,
|
||||
};
|
||||
|
||||
return newArray.map((column, index) => ({
|
||||
...column,
|
||||
position: index,
|
||||
}));
|
||||
}
|
||||
|
||||
return targetArray;
|
||||
};
|
||||
|
||||
return { handleColumnMove };
|
||||
};
|
||||
447
packages/twenty-front/src/modules/views/hooks/useViewBar.ts
Normal file
447
packages/twenty-front/src/modules/views/hooks/useViewBar.ts
Normal file
@ -0,0 +1,447 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
|
||||
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
|
||||
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
|
||||
import { currentViewSortsScopedFamilyState } from '../states/currentViewSortsScopedFamilyState';
|
||||
import { getViewScopedStatesFromSnapshot } from '../utils/getViewScopedStatesFromSnapshot';
|
||||
import { getViewScopedStateValuesFromSnapshot } from '../utils/getViewScopedStateValuesFromSnapshot';
|
||||
|
||||
import { useViewFields } from './internal/useViewFields';
|
||||
import { useViewFilters } from './internal/useViewFilters';
|
||||
import { useViews } from './internal/useViews';
|
||||
import { useViewScopedStates } from './internal/useViewScopedStates';
|
||||
import { useViewSorts } from './internal/useViewSorts';
|
||||
|
||||
type UseViewProps = {
|
||||
viewBarId?: string;
|
||||
};
|
||||
|
||||
export const useViewBar = (props?: UseViewProps) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
ViewScopeInternalContext,
|
||||
props?.viewBarId,
|
||||
);
|
||||
|
||||
const {
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSortsState,
|
||||
viewEditModeState,
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
entityCountInCurrentViewState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
} = useViewScopedStates({
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
const { persistViewSorts, upsertViewSort, removeViewSort } =
|
||||
useViewSorts(scopeId);
|
||||
const { persistViewFilters, upsertViewFilter, removeViewFilter } =
|
||||
useViewFilters(scopeId);
|
||||
const { persistViewFields } = useViewFields(scopeId);
|
||||
const {
|
||||
createView: internalCreateView,
|
||||
updateView: internalUpdateView,
|
||||
deleteView: internalDeleteView,
|
||||
} = useViews(scopeId);
|
||||
|
||||
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
|
||||
|
||||
const setAvailableFieldDefinitions = useSetRecoilState(
|
||||
availableFieldDefinitionsState,
|
||||
);
|
||||
|
||||
const setAvailableSortDefinitions = useSetRecoilState(
|
||||
availableSortDefinitionsState,
|
||||
);
|
||||
|
||||
const setAvailableFilterDefinitions = useSetRecoilState(
|
||||
availableFilterDefinitionsState,
|
||||
);
|
||||
|
||||
const setEntityCountInCurrentView = useSetRecoilState(
|
||||
entityCountInCurrentViewState,
|
||||
);
|
||||
|
||||
const setViewEditMode = useSetRecoilState(viewEditModeState);
|
||||
const setViewObjectMetadataId = useSetRecoilState(viewObjectMetadataIdState);
|
||||
const setViewType = useSetRecoilState(viewTypeState);
|
||||
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const changeViewInUrl = useCallback(
|
||||
(viewId: string) => {
|
||||
setSearchParams({ view: viewId });
|
||||
},
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const loadViewFields = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (
|
||||
data: PaginatedRecordTypeResults<ViewField>,
|
||||
currentViewId: string,
|
||||
) => {
|
||||
const {
|
||||
availableFieldDefinitions,
|
||||
onViewFieldsChange,
|
||||
savedViewFields,
|
||||
isPersistingView,
|
||||
} = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
const { savedViewFieldsState, currentViewFieldsState } =
|
||||
getViewScopedStatesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
if (!availableFieldDefinitions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queriedViewFields = data.edges
|
||||
.map((viewField) => viewField.node)
|
||||
.filter(assertNotNull);
|
||||
|
||||
if (isPersistingView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
||||
set(currentViewFieldsState, queriedViewFields);
|
||||
set(savedViewFieldsState, queriedViewFields);
|
||||
onViewFieldsChange?.(queriedViewFields);
|
||||
}
|
||||
},
|
||||
[scopeId],
|
||||
);
|
||||
|
||||
const loadViewFilters = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (
|
||||
data: PaginatedRecordTypeResults<Required<ViewFilter>>,
|
||||
currentViewId: string,
|
||||
) => {
|
||||
const {
|
||||
availableFilterDefinitions,
|
||||
savedViewFilters,
|
||||
onViewFiltersChange,
|
||||
} = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
const { savedViewFiltersState, currentViewFiltersState } =
|
||||
getViewScopedStatesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
if (!availableFilterDefinitions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queriedViewFilters = data.edges
|
||||
.map(({ node }) => {
|
||||
const availableFilterDefinition = availableFilterDefinitions.find(
|
||||
(filterDefinition) =>
|
||||
filterDefinition.fieldMetadataId === node.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!availableFilterDefinition) return null;
|
||||
|
||||
return {
|
||||
...node,
|
||||
displayValue: node.displayValue ?? node.value,
|
||||
definition: availableFilterDefinition,
|
||||
};
|
||||
})
|
||||
.filter(assertNotNull);
|
||||
|
||||
if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) {
|
||||
set(savedViewFiltersState, queriedViewFilters);
|
||||
set(currentViewFiltersState, queriedViewFilters);
|
||||
}
|
||||
onViewFiltersChange?.(queriedViewFilters);
|
||||
},
|
||||
[scopeId],
|
||||
);
|
||||
|
||||
const loadViewSorts = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (
|
||||
data: PaginatedRecordTypeResults<Required<ViewSort>>,
|
||||
currentViewId: string,
|
||||
) => {
|
||||
const { availableSortDefinitions, savedViewSorts, onViewSortsChange } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
const { savedViewSortsState, currentViewSortsState } =
|
||||
getViewScopedStatesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId: currentViewId,
|
||||
});
|
||||
|
||||
if (!availableSortDefinitions || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queriedViewSorts = data.edges
|
||||
.map(({ node }) => {
|
||||
const availableSortDefinition = availableSortDefinitions.find(
|
||||
(sort) => sort.fieldMetadataId === node.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!availableSortDefinition) return null;
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
fieldMetadataId: node.fieldMetadataId,
|
||||
direction: node.direction,
|
||||
definition: availableSortDefinition,
|
||||
};
|
||||
})
|
||||
.filter(assertNotNull);
|
||||
|
||||
if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) {
|
||||
set(savedViewSortsState, queriedViewSorts);
|
||||
set(currentViewSortsState, queriedViewSorts);
|
||||
}
|
||||
onViewSortsChange?.(queriedViewSorts);
|
||||
},
|
||||
[scopeId],
|
||||
);
|
||||
|
||||
const loadView = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(viewId: string) => {
|
||||
setCurrentViewId?.(viewId);
|
||||
|
||||
const { currentView } = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
viewId,
|
||||
});
|
||||
|
||||
if (!currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadViewFields(currentView.viewFields, viewId);
|
||||
loadViewFilters(currentView.viewFilters, viewId);
|
||||
loadViewSorts(currentView.viewSorts, viewId);
|
||||
},
|
||||
[setCurrentViewId, scopeId, loadViewFields, loadViewFilters, loadViewSorts],
|
||||
);
|
||||
|
||||
const resetViewBar = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const {
|
||||
savedViewFilters,
|
||||
savedViewSorts,
|
||||
onViewFiltersChange,
|
||||
onViewSortsChange,
|
||||
} = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
if (savedViewFilters) {
|
||||
set(currentViewFiltersState, savedViewFilters);
|
||||
onViewFiltersChange?.(savedViewFilters);
|
||||
}
|
||||
if (savedViewSorts) {
|
||||
set(currentViewSortsState, savedViewSorts);
|
||||
onViewSortsChange?.(savedViewSorts);
|
||||
}
|
||||
|
||||
set(viewEditModeState, 'none');
|
||||
},
|
||||
[
|
||||
currentViewFiltersState,
|
||||
currentViewSortsState,
|
||||
scopeId,
|
||||
viewEditModeState,
|
||||
],
|
||||
);
|
||||
|
||||
const createView = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (name: string) => {
|
||||
const newViewId = v4();
|
||||
await internalCreateView({ id: newViewId, name });
|
||||
|
||||
const { currentViewFields, currentViewFilters, currentViewSorts } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
set(
|
||||
currentViewFieldsScopedFamilyState({ scopeId, familyKey: newViewId }),
|
||||
currentViewFields,
|
||||
);
|
||||
|
||||
set(
|
||||
currentViewFiltersScopedFamilyState({
|
||||
scopeId,
|
||||
familyKey: newViewId,
|
||||
}),
|
||||
currentViewFilters,
|
||||
);
|
||||
|
||||
set(
|
||||
currentViewSortsScopedFamilyState({
|
||||
scopeId,
|
||||
familyKey: newViewId,
|
||||
}),
|
||||
currentViewSorts,
|
||||
);
|
||||
|
||||
await persistViewFields(currentViewFields, newViewId);
|
||||
await persistViewFilters(newViewId);
|
||||
await persistViewSorts(newViewId);
|
||||
|
||||
changeViewInUrl(newViewId);
|
||||
},
|
||||
[
|
||||
changeViewInUrl,
|
||||
internalCreateView,
|
||||
persistViewFields,
|
||||
persistViewFilters,
|
||||
persistViewSorts,
|
||||
scopeId,
|
||||
],
|
||||
);
|
||||
|
||||
const updateCurrentView = async () => {
|
||||
await persistViewFilters();
|
||||
await persistViewSorts();
|
||||
};
|
||||
|
||||
const removeView = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (viewIdToDelete: string) => {
|
||||
const { currentViewId } = getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
const { currentViewIdState, viewsState } =
|
||||
getViewScopedStatesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
if (currentViewId === viewIdToDelete) {
|
||||
set(currentViewIdState, undefined);
|
||||
}
|
||||
|
||||
set(viewsState, (previousViews) =>
|
||||
previousViews.filter((view) => view.id !== viewIdToDelete),
|
||||
);
|
||||
|
||||
internalDeleteView(viewIdToDelete);
|
||||
|
||||
if (currentViewId === viewIdToDelete) {
|
||||
setSearchParams();
|
||||
}
|
||||
},
|
||||
[internalDeleteView, scopeId, setSearchParams],
|
||||
);
|
||||
|
||||
const handleViewNameSubmit = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (name?: string) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewEditMode, currentView } =
|
||||
getViewScopedStateValuesFromSnapshot({
|
||||
snapshot,
|
||||
viewScopeId: scopeId,
|
||||
});
|
||||
|
||||
if (!currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewEditMode === 'create' && name) {
|
||||
await createView(name);
|
||||
|
||||
// Temporary to force refetch
|
||||
await internalUpdateView({
|
||||
...currentView,
|
||||
});
|
||||
} else {
|
||||
await internalUpdateView({
|
||||
...currentView,
|
||||
name,
|
||||
});
|
||||
}
|
||||
},
|
||||
[createView, internalUpdateView, scopeId],
|
||||
);
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
currentViewId,
|
||||
|
||||
setCurrentViewId,
|
||||
updateCurrentView,
|
||||
createView,
|
||||
removeView,
|
||||
resetViewBar,
|
||||
handleViewNameSubmit,
|
||||
|
||||
setViewEditMode,
|
||||
setViewObjectMetadataId,
|
||||
setViewType,
|
||||
setEntityCountInCurrentView,
|
||||
setAvailableFieldDefinitions,
|
||||
|
||||
setAvailableSortDefinitions,
|
||||
upsertViewSort,
|
||||
removeViewSort,
|
||||
|
||||
setAvailableFilterDefinitions,
|
||||
upsertViewFilter,
|
||||
removeViewFilter,
|
||||
|
||||
persistViewFields,
|
||||
changeViewInUrl,
|
||||
loadView,
|
||||
loadViewFields,
|
||||
loadViewFilters,
|
||||
loadViewSorts,
|
||||
};
|
||||
};
|
||||
41
packages/twenty-front/src/modules/views/scopes/ViewScope.tsx
Normal file
41
packages/twenty-front/src/modules/views/scopes/ViewScope.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
import { ViewScopeInitEffect } from './init-effect/ViewScopeInitEffect';
|
||||
import { ViewScopeInternalContext } from './scope-internal-context/ViewScopeInternalContext';
|
||||
|
||||
type ViewScopeProps = {
|
||||
children: ReactNode;
|
||||
viewScopeId: string;
|
||||
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
|
||||
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
|
||||
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export const ViewScope = ({
|
||||
children,
|
||||
viewScopeId,
|
||||
onViewSortsChange,
|
||||
onViewFiltersChange,
|
||||
onViewFieldsChange,
|
||||
}: ViewScopeProps) => {
|
||||
return (
|
||||
<ViewScopeInternalContext.Provider
|
||||
value={{
|
||||
scopeId: viewScopeId,
|
||||
}}
|
||||
>
|
||||
<ViewScopeInitEffect
|
||||
viewScopeId={viewScopeId}
|
||||
onViewSortsChange={onViewSortsChange}
|
||||
onViewFiltersChange={onViewFiltersChange}
|
||||
onViewFieldsChange={onViewFieldsChange}
|
||||
/>
|
||||
{children}
|
||||
</ViewScopeInternalContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
type ViewScopeInitEffectProps = {
|
||||
viewScopeId: string;
|
||||
onViewSortsChange?: (sorts: ViewSort[]) => void | Promise<void>;
|
||||
onViewFiltersChange?: (filters: ViewFilter[]) => void | Promise<void>;
|
||||
onViewFieldsChange?: (fields: ViewField[]) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export const ViewScopeInitEffect = ({
|
||||
onViewSortsChange,
|
||||
onViewFiltersChange,
|
||||
onViewFieldsChange,
|
||||
}: ViewScopeInitEffectProps) => {
|
||||
const {
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
} = useViewScopedStates();
|
||||
|
||||
const setOnViewSortsChange = useSetRecoilState(onViewSortsChangeState);
|
||||
const setOnViewFiltersChange = useSetRecoilState(onViewFiltersChangeState);
|
||||
const setOnViewFieldsChange = useSetRecoilState(onViewFieldsChangeState);
|
||||
|
||||
useEffect(() => {
|
||||
setOnViewSortsChange(() => onViewSortsChange);
|
||||
setOnViewFiltersChange(() => onViewFiltersChange);
|
||||
setOnViewFieldsChange(() => onViewFieldsChange);
|
||||
}, [
|
||||
onViewFieldsChange,
|
||||
onViewFiltersChange,
|
||||
onViewSortsChange,
|
||||
setOnViewFieldsChange,
|
||||
setOnViewFiltersChange,
|
||||
setOnViewSortsChange,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -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>();
|
||||
@ -0,0 +1,10 @@
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableFieldDefinitionsScopedState = createScopedState<
|
||||
ColumnDefinition<FieldMetadata>[]
|
||||
>({
|
||||
key: 'availableFieldDefinitionsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableFilterDefinitionsScopedState = createScopedState<
|
||||
FilterDefinition[]
|
||||
>({
|
||||
key: 'availableFilterDefinitionsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableSortDefinitionsScopedState = createScopedState<
|
||||
SortDefinition[]
|
||||
>({
|
||||
key: 'availableSortDefinitionsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const currentViewFieldsScopedFamilyState = createScopedFamilyState<
|
||||
ViewField[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewFieldsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
|
||||
export const currentViewFiltersScopedFamilyState = createScopedFamilyState<
|
||||
ViewFilter[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewFiltersScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const currentViewIdScopedState = createScopedState<string | undefined>({
|
||||
key: 'currentViewIdScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewSort } from '../types/ViewSort';
|
||||
|
||||
export const currentViewSortsScopedFamilyState = createScopedFamilyState<
|
||||
ViewSort[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewSortsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const entityCountInCurrentViewScopedState = createScopedState<number>({
|
||||
key: 'entityCountInCurrentViewScopedState',
|
||||
defaultValue: 0,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const isPersistingViewScopedState = createScopedState<boolean>({
|
||||
key: 'isPersistingViewScopedState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const isViewBarExpandedScopedState = createScopedState<boolean>({
|
||||
key: 'isViewBarExpandedScopedState',
|
||||
defaultValue: true,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const noneScopedFamilyState = createScopedFamilyState<any, string>({
|
||||
key: 'noneScopedFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const onViewFieldsChangeScopedState = createScopedState<
|
||||
((fields: ViewField[]) => void | Promise<void>) | undefined
|
||||
>({
|
||||
key: 'onViewFieldsChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
|
||||
export const onViewFiltersChangeScopedState = createScopedState<
|
||||
((filters: ViewFilter[]) => void | Promise<void>) | undefined
|
||||
>({
|
||||
key: 'onViewFiltersChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
export const onViewSortsChangeScopedState = createScopedState<
|
||||
((sorts: ViewSort[]) => void | Promise<void>) | undefined
|
||||
>({
|
||||
key: 'onViewSortsChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const savedViewFieldsScopedFamilyState = createScopedFamilyState<
|
||||
ViewField[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewFieldsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
|
||||
export const savedViewFiltersScopedFamilyState = createScopedFamilyState<
|
||||
ViewFilter[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewFiltersScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
import { ViewSort } from '../types/ViewSort';
|
||||
|
||||
export const savedViewSortsScopedFamilyState = createScopedFamilyState<
|
||||
ViewSort[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewSortsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { currentViewFiltersScopedFamilyState } from '../currentViewFiltersScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
|
||||
|
||||
export const canPersistViewFiltersScopedFamilySelector = selectorFamily({
|
||||
key: 'canPersistFiltersScopedFamilySelector',
|
||||
get:
|
||||
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
|
||||
({ get }) => {
|
||||
if (!viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return !isDeeplyEqual(
|
||||
get(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
),
|
||||
get(
|
||||
currentViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { createScopedSelector } from '@/ui/utilities/recoil-scope/utils/createScopedSelector';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
|
||||
import { currentViewIdScopedState } from '../currentViewIdScopedState';
|
||||
|
||||
import { viewsByIdScopedSelector } from './viewsByIdScopedSelector';
|
||||
|
||||
export const currentViewScopedSelector = createScopedSelector<
|
||||
GraphQLView | undefined
|
||||
>({
|
||||
key: 'currentViewScopedSelector',
|
||||
get:
|
||||
({ scopeId }: { scopeId: string }) =>
|
||||
({ get }) => {
|
||||
const currentViewId = get(currentViewIdScopedState({ scopeId: scopeId }));
|
||||
|
||||
return currentViewId
|
||||
? get(viewsByIdScopedSelector(scopeId))[currentViewId]
|
||||
: undefined;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
|
||||
import { savedViewFieldsScopedFamilyState } from '../savedViewFieldsScopedFamilyState';
|
||||
|
||||
export const savedViewFieldByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewFieldByKeyScopedFamilySelector',
|
||||
get:
|
||||
({
|
||||
viewScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
viewScopeId: string;
|
||||
viewId: string | undefined;
|
||||
}) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return get(
|
||||
savedViewFieldsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, ViewField>>(
|
||||
(result, column) => ({ ...result, [column.fieldMetadataId]: column }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
|
||||
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
|
||||
|
||||
export const savedViewFiltersByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewFiltersByKeyScopedFamilySelector',
|
||||
get:
|
||||
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return get(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, ViewFilter>>(
|
||||
(result, filter) => ({ ...result, [filter.fieldMetadataId]: filter }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
import { savedViewSortsScopedFamilyState } from '../savedViewSortsScopedFamilyState';
|
||||
|
||||
export const savedViewSortsByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewSortsByKeyScopedFamilySelector',
|
||||
get:
|
||||
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return get(
|
||||
savedViewSortsScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, ViewSort>>(
|
||||
(result, sort) => ({ ...result, [sort.fieldMetadataId]: sort }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
}),
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
|
||||
import { viewsScopedState } from '../viewsScopedState';
|
||||
|
||||
export const viewsByIdScopedSelector = selectorFamily<
|
||||
Record<string, GraphQLView>,
|
||||
string
|
||||
>({
|
||||
key: 'viewsByIdScopedSelector',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) =>
|
||||
get(viewsScopedState({ scopeId: scopeId })).reduce<
|
||||
Record<string, GraphQLView>
|
||||
>((result, view) => ({ ...result, [view.id]: view }), {}),
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const viewEditModeScopedState = createScopedState<
|
||||
'none' | 'edit' | 'create'
|
||||
>({
|
||||
key: 'viewEditModeScopedState',
|
||||
defaultValue: 'none',
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const viewObjectMetadataIdScopeState = createScopedState<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'viewObjectMetadataIdScopeState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
import { ViewType } from '../types/ViewType';
|
||||
|
||||
export const viewTypeScopedState = createScopedState<ViewType>({
|
||||
key: 'viewTypeScopedState',
|
||||
defaultValue: ViewType.Table,
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
|
||||
export const viewsScopedState = createScopedState<GraphQLView[]>({
|
||||
key: 'viewsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
13
packages/twenty-front/src/modules/views/types/GraphQLView.ts
Normal file
13
packages/twenty-front/src/modules/views/types/GraphQLView.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
|
||||
export type GraphQLView = {
|
||||
id: string;
|
||||
name: string;
|
||||
objectMetadataId: string;
|
||||
viewFields: PaginatedRecordTypeResults<ViewField>;
|
||||
viewFilters: PaginatedRecordTypeResults<ViewFilter>;
|
||||
viewSorts: PaginatedRecordTypeResults<ViewSort>;
|
||||
};
|
||||
5
packages/twenty-front/src/modules/views/types/View.ts
Normal file
5
packages/twenty-front/src/modules/views/types/View.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type View = {
|
||||
id: string;
|
||||
name: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
14
packages/twenty-front/src/modules/views/types/ViewField.ts
Normal file
14
packages/twenty-front/src/modules/views/types/ViewField.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { BoardFieldDefinition } from '@/object-record/record-board/types/BoardFieldDefinition';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
|
||||
export type ViewField = {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
size: number;
|
||||
definition:
|
||||
| ColumnDefinition<FieldMetadata>
|
||||
| BoardFieldDefinition<FieldMetadata>;
|
||||
};
|
||||
12
packages/twenty-front/src/modules/views/types/ViewFilter.ts
Normal file
12
packages/twenty-front/src/modules/views/types/ViewFilter.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
|
||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
export type ViewFilter = {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
operand: ViewFilterOperand;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
definition: FilterDefinition;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
export enum ViewFilterOperand {
|
||||
Is = 'is',
|
||||
IsNotNull = 'isNotNull',
|
||||
IsNot = 'isNot',
|
||||
LessThan = 'lessThan',
|
||||
GreaterThan = 'greaterThan',
|
||||
Contains = 'contains',
|
||||
DoesNotContain = 'doesNotContain',
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { SortDirection } from '@/object-record/object-sort-dropdown/types/SortDirection';
|
||||
|
||||
export type ViewSort = {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
direction: SortDirection;
|
||||
definition: SortDefinition;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export enum ViewSortDirection {
|
||||
Asc = 'asc',
|
||||
Desc = 'desc',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum ViewType {
|
||||
Table = 'table',
|
||||
Kanban = 'kanban',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum ViewsHotkeyScope {
|
||||
ListDropdown = 'views-list-dropdown',
|
||||
CreateDropdown = 'views-create-dropdown',
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import { Snapshot } from 'recoil';
|
||||
|
||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
|
||||
import { UNDEFINED_FAMILY_ITEM_ID } from '../constants';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
|
||||
import { getViewScopedStates } from './internal/getViewScopedStates';
|
||||
|
||||
export const getViewScopedStateValuesFromSnapshot = ({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
snapshot: Snapshot;
|
||||
viewScopeId: string;
|
||||
viewId?: string;
|
||||
}) => {
|
||||
const currentViewId = getSnapshotValue(
|
||||
snapshot,
|
||||
getScopedState(currentViewIdScopedState, viewScopeId),
|
||||
);
|
||||
|
||||
const familyItemId = viewId ?? currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
|
||||
|
||||
const {
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersSelector,
|
||||
canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
currentViewSortsState,
|
||||
entityCountInCurrentViewState,
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
viewsState,
|
||||
} = getViewScopedStates({
|
||||
viewScopeId,
|
||||
viewId: familyItemId,
|
||||
});
|
||||
|
||||
return {
|
||||
availableFieldDefinitions: getSnapshotValue(
|
||||
snapshot,
|
||||
availableFieldDefinitionsState,
|
||||
),
|
||||
availableFilterDefinitions: getSnapshotValue(
|
||||
snapshot,
|
||||
availableFilterDefinitionsState,
|
||||
),
|
||||
availableSortDefinitions: getSnapshotValue(
|
||||
snapshot,
|
||||
availableSortDefinitionsState,
|
||||
),
|
||||
canPersistFilters: getSnapshotValue(snapshot, canPersistFiltersSelector),
|
||||
canPersistSorts: getSnapshotValue(snapshot, canPersistSortsSelector),
|
||||
currentViewFields: getSnapshotValue(snapshot, currentViewFieldsState),
|
||||
currentViewFilters: getSnapshotValue(snapshot, currentViewFiltersState),
|
||||
currentViewId: getSnapshotValue(snapshot, currentViewIdState),
|
||||
currentView: getSnapshotValue(snapshot, currentViewSelector),
|
||||
currentViewSorts: getSnapshotValue(snapshot, currentViewSortsState),
|
||||
entityCountInCurrentView: getSnapshotValue(
|
||||
snapshot,
|
||||
entityCountInCurrentViewState,
|
||||
),
|
||||
isViewBarExpanded: getSnapshotValue(snapshot, isViewBarExpandedState),
|
||||
isPersistingView: getSnapshotValue(snapshot, isPersistingViewState),
|
||||
onViewFieldsChange: getSnapshotValue(snapshot, onViewFieldsChangeState),
|
||||
onViewFiltersChange: getSnapshotValue(snapshot, onViewFiltersChangeState),
|
||||
onViewSortsChange: getSnapshotValue(snapshot, onViewSortsChangeState),
|
||||
savedViewFieldsByKey: getSnapshotValue(
|
||||
snapshot,
|
||||
savedViewFieldsByKeySelector,
|
||||
),
|
||||
savedViewFields: getSnapshotValue(snapshot, savedViewFieldsState),
|
||||
savedViewFiltersByKey: getSnapshotValue(
|
||||
snapshot,
|
||||
savedViewFiltersByKeySelector,
|
||||
),
|
||||
savedViewFilters: getSnapshotValue(snapshot, savedViewFiltersState),
|
||||
savedViewSortsByKey: getSnapshotValue(
|
||||
snapshot,
|
||||
savedViewSortsByKeySelector,
|
||||
),
|
||||
savedViewSorts: getSnapshotValue(snapshot, savedViewSortsState),
|
||||
viewEditMode: getSnapshotValue(snapshot, viewEditModeState),
|
||||
viewObjectMetadataId: getSnapshotValue(snapshot, viewObjectMetadataIdState),
|
||||
viewType: getSnapshotValue(snapshot, viewTypeState),
|
||||
views: getSnapshotValue(snapshot, viewsState),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,87 @@
|
||||
import { Snapshot } from 'recoil';
|
||||
|
||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
|
||||
import { UNDEFINED_FAMILY_ITEM_ID } from '../constants';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
|
||||
import { getViewScopedStates } from './internal/getViewScopedStates';
|
||||
|
||||
export const getViewScopedStatesFromSnapshot = ({
|
||||
snapshot,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
snapshot: Snapshot;
|
||||
viewScopeId: string;
|
||||
viewId?: string;
|
||||
}) => {
|
||||
const currentViewId = getSnapshotValue(
|
||||
snapshot,
|
||||
getScopedState(currentViewIdScopedState, viewScopeId),
|
||||
);
|
||||
|
||||
const usedViewId = viewId ?? currentViewId ?? UNDEFINED_FAMILY_ITEM_ID;
|
||||
|
||||
const {
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersSelector: canPersistFiltersSelector,
|
||||
canPersistSortsSelector: canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
currentViewSortsState,
|
||||
entityCountInCurrentViewState,
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeySelector: savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeySelector: savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeySelector: savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
viewsState,
|
||||
} = getViewScopedStates({
|
||||
viewScopeId,
|
||||
viewId: usedViewId,
|
||||
});
|
||||
|
||||
return {
|
||||
availableFieldDefinitionsState,
|
||||
availableFilterDefinitionsState,
|
||||
availableSortDefinitionsState,
|
||||
canPersistFiltersReadOnlyState: canPersistFiltersSelector,
|
||||
canPersistSortsReadOnlyState: canPersistSortsSelector,
|
||||
currentViewFieldsState,
|
||||
currentViewFiltersState,
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
currentViewSortsState,
|
||||
entityCountInCurrentViewState,
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
onViewFieldsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewSortsChangeState,
|
||||
savedViewFieldsByKeyReadOnlyState: savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
savedViewFiltersByKeyReadOnlyState: savedViewFiltersByKeySelector,
|
||||
savedViewFiltersState,
|
||||
savedViewSortsByKeyReadOnlyState: savedViewSortsByKeySelector,
|
||||
savedViewSortsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
viewsState,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,208 @@
|
||||
import { getScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/getScopedFamilyState';
|
||||
import { getScopedSelector } from '@/ui/utilities/recoil-scope/utils/getScopedSelector';
|
||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
|
||||
import { isPersistingViewScopedState } from '@/views/states/isPersistingViewScopedState';
|
||||
import { currentViewScopedSelector } from '@/views/states/selectors/currentViewScopedSelector';
|
||||
|
||||
import { availableFieldDefinitionsScopedState } from '../../states/availableFieldDefinitionsScopedState';
|
||||
import { availableFilterDefinitionsScopedState } from '../../states/availableFilterDefinitionsScopedState';
|
||||
import { availableSortDefinitionsScopedState } from '../../states/availableSortDefinitionsScopedState';
|
||||
import { currentViewFieldsScopedFamilyState } from '../../states/currentViewFieldsScopedFamilyState';
|
||||
import { currentViewFiltersScopedFamilyState } from '../../states/currentViewFiltersScopedFamilyState';
|
||||
import { currentViewSortsScopedFamilyState } from '../../states/currentViewSortsScopedFamilyState';
|
||||
import { entityCountInCurrentViewScopedState } from '../../states/entityCountInCurrentViewScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../../states/isViewBarExpandedScopedState';
|
||||
import { onViewFieldsChangeScopedState } from '../../states/onViewFieldsChangeScopedState';
|
||||
import { onViewFiltersChangeScopedState } from '../../states/onViewFiltersChangeScopedState';
|
||||
import { onViewSortsChangeScopedState } from '../../states/onViewSortsChangeScopedState';
|
||||
import { savedViewFieldsScopedFamilyState } from '../../states/savedViewFieldsScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../../states/savedViewSortsScopedFamilyState';
|
||||
import { canPersistViewFiltersScopedFamilySelector } from '../../states/selectors/canPersistViewFiltersScopedFamilySelector';
|
||||
import { canPersistViewSortsScopedFamilySelector } from '../../states/selectors/canPersistViewSortsScopedFamilySelector';
|
||||
import { savedViewFieldByKeyScopedFamilySelector } from '../../states/selectors/savedViewFieldByKeyScopedFamilySelector';
|
||||
import { savedViewFiltersByKeyScopedFamilySelector } from '../../states/selectors/savedViewFiltersByKeyScopedFamilySelector';
|
||||
import { savedViewSortsByKeyScopedFamilySelector } from '../../states/selectors/savedViewSortsByKeyScopedFamilySelector';
|
||||
import { viewEditModeScopedState } from '../../states/viewEditModeScopedState';
|
||||
import { viewObjectMetadataIdScopeState } from '../../states/viewObjectMetadataIdScopeState';
|
||||
import { viewsScopedState } from '../../states/viewsScopedState';
|
||||
import { viewTypeScopedState } from '../../states/viewTypeScopedState';
|
||||
|
||||
export const getViewScopedStates = ({
|
||||
viewScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
viewScopeId: string;
|
||||
viewId: string;
|
||||
}) => {
|
||||
const viewEditModeState = getScopedState(
|
||||
viewEditModeScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const viewsState = getScopedState(viewsScopedState, viewScopeId);
|
||||
|
||||
const viewObjectMetadataIdState = getScopedState(
|
||||
viewObjectMetadataIdScopeState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const viewTypeState = getScopedState(viewTypeScopedState, viewScopeId);
|
||||
|
||||
const entityCountInCurrentViewState = getScopedState(
|
||||
entityCountInCurrentViewScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const isViewBarExpandedState = getScopedState(
|
||||
isViewBarExpandedScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const isPersistingViewState = getScopedState(
|
||||
isPersistingViewScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
// ViewSorts
|
||||
const currentViewSortsState = getScopedFamilyState(
|
||||
currentViewSortsScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewSortsState = getScopedFamilyState(
|
||||
savedViewSortsScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewSortsByKeySelector = savedViewSortsByKeyScopedFamilySelector({
|
||||
scopeId: viewScopeId,
|
||||
viewId: viewId,
|
||||
});
|
||||
|
||||
const availableSortDefinitionsState = getScopedState(
|
||||
availableSortDefinitionsScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const canPersistSortsSelector = canPersistViewSortsScopedFamilySelector({
|
||||
viewScopeId: viewScopeId,
|
||||
viewId: viewId,
|
||||
});
|
||||
|
||||
// ViewFilters
|
||||
const currentViewFiltersState = getScopedFamilyState(
|
||||
currentViewFiltersScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewFiltersState = getScopedFamilyState(
|
||||
savedViewFiltersScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewFiltersByKeySelector =
|
||||
savedViewFiltersByKeyScopedFamilySelector({
|
||||
scopeId: viewScopeId,
|
||||
viewId: viewId,
|
||||
});
|
||||
|
||||
const availableFilterDefinitionsState = getScopedState(
|
||||
availableFilterDefinitionsScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const canPersistFiltersSelector = canPersistViewFiltersScopedFamilySelector({
|
||||
viewScopeId: viewScopeId,
|
||||
viewId: viewId,
|
||||
});
|
||||
|
||||
// ViewFields
|
||||
const availableFieldDefinitionsState = getScopedState(
|
||||
availableFieldDefinitionsScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const currentViewFieldsState = getScopedFamilyState(
|
||||
currentViewFieldsScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewFieldsState = getScopedFamilyState(
|
||||
savedViewFieldsScopedFamilyState,
|
||||
viewScopeId,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const savedViewFieldsByKeySelector = savedViewFieldByKeyScopedFamilySelector({
|
||||
viewScopeId,
|
||||
viewId,
|
||||
});
|
||||
|
||||
// ViewChangeHandlers
|
||||
const onViewSortsChangeState = getScopedState(
|
||||
onViewSortsChangeScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const onViewFiltersChangeState = getScopedState(
|
||||
onViewFiltersChangeScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const onViewFieldsChangeState = getScopedState(
|
||||
onViewFieldsChangeScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const currentViewIdState = getScopedState(
|
||||
currentViewIdScopedState,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
const currentViewSelector = getScopedSelector(
|
||||
currentViewScopedSelector,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
currentViewIdState,
|
||||
currentViewSelector,
|
||||
|
||||
isViewBarExpandedState,
|
||||
isPersistingViewState,
|
||||
|
||||
viewsState,
|
||||
viewEditModeState,
|
||||
viewObjectMetadataIdState,
|
||||
viewTypeState,
|
||||
entityCountInCurrentViewState,
|
||||
|
||||
availableSortDefinitionsState,
|
||||
currentViewSortsState,
|
||||
savedViewSortsState,
|
||||
savedViewSortsByKeySelector,
|
||||
canPersistSortsSelector,
|
||||
|
||||
availableFilterDefinitionsState,
|
||||
currentViewFiltersState,
|
||||
savedViewFiltersState,
|
||||
savedViewFiltersByKeySelector,
|
||||
canPersistFiltersSelector,
|
||||
|
||||
availableFieldDefinitionsState,
|
||||
currentViewFieldsState,
|
||||
savedViewFieldsByKeySelector,
|
||||
savedViewFieldsState,
|
||||
|
||||
onViewSortsChangeState,
|
||||
onViewFiltersChangeState,
|
||||
onViewFieldsChangeState,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const mapColumnDefinitionsToViewFields = (
|
||||
columnDefinitions: ColumnDefinition<FieldMetadata>[],
|
||||
): ViewField[] => {
|
||||
return columnDefinitions.map((columnDefinition) => ({
|
||||
id: columnDefinition.viewFieldId || '',
|
||||
fieldMetadataId: columnDefinition.fieldMetadataId,
|
||||
position: columnDefinition.position,
|
||||
size: columnDefinition.size,
|
||||
isVisible: columnDefinition.isVisible ?? true,
|
||||
definition: columnDefinition,
|
||||
}));
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { BoardFieldDefinition } from '@/object-record/record-board/types/BoardFieldDefinition';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const mapViewFieldsToBoardFieldDefinitions = (
|
||||
viewFields: ViewField[],
|
||||
fieldsMetadata: BoardFieldDefinition<FieldMetadata>[],
|
||||
): BoardFieldDefinition<FieldMetadata>[] => {
|
||||
return viewFields
|
||||
.map((viewField) => {
|
||||
const correspondingFieldMetadata = fieldsMetadata.find(
|
||||
({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
return correspondingFieldMetadata
|
||||
? {
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
label: correspondingFieldMetadata.label,
|
||||
metadata: correspondingFieldMetadata.metadata,
|
||||
infoTooltipContent: correspondingFieldMetadata.infoTooltipContent,
|
||||
iconName: correspondingFieldMetadata.iconName,
|
||||
type: correspondingFieldMetadata.type,
|
||||
position: viewField.position,
|
||||
isVisible: viewField.isVisible,
|
||||
viewFieldId: viewField.id,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(assertNotNull);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const mapViewFieldsToColumnDefinitions = (
|
||||
viewFields: ViewField[],
|
||||
fieldsMetadata: ColumnDefinition<FieldMetadata>[],
|
||||
): ColumnDefinition<FieldMetadata>[] => {
|
||||
return viewFields
|
||||
.map((viewField) => {
|
||||
const correspondingFieldMetadata = fieldsMetadata.find(
|
||||
({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
);
|
||||
|
||||
return correspondingFieldMetadata
|
||||
? {
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
label: correspondingFieldMetadata.label,
|
||||
metadata: correspondingFieldMetadata.metadata,
|
||||
infoTooltipContent: correspondingFieldMetadata.infoTooltipContent,
|
||||
iconName: correspondingFieldMetadata.iconName,
|
||||
type: correspondingFieldMetadata.type,
|
||||
position: viewField.position,
|
||||
size: viewField.size ?? correspondingFieldMetadata.size,
|
||||
isVisible: viewField.isVisible,
|
||||
viewFieldId: viewField.id,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(assertNotNull);
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
|
||||
export const mapViewFiltersToFilters = (
|
||||
viewFilters: ViewFilter[],
|
||||
): Filter[] => {
|
||||
return viewFilters.map((viewFilter) => {
|
||||
return {
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
definition: viewFilter.definition,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
|
||||
|
||||
import { ViewSort } from '../types/ViewSort';
|
||||
|
||||
export const mapViewSortsToSorts = (viewSorts: ViewSort[]): Sort[] => {
|
||||
return viewSorts.map((viewSort) => {
|
||||
return {
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
direction: viewSort.direction,
|
||||
definition: viewSort.definition,
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user