Improve viewbar api (#2233)
* create scopes * fix import bug * add useView hook * wip * wip * currentViewId is now retrieved via useView * working on sorts with useView * refactor in progress * refactor in progress * refactor in progress * refactor in progress * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix code * fix code * wip * push * Fix issue dependencies * Fix resize --------- Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
82
front/src/modules/views/components/SortOrFilterChip.tsx
Normal file
82
front/src/modules/views/components/SortOrFilterChip.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconX } from '@/ui/display/icon/index';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
type SortOrFilterChipProps = {
|
||||
labelKey?: string;
|
||||
labelValue: string;
|
||||
Icon?: IconComponent;
|
||||
onRemove: () => void;
|
||||
isSort?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type StyledChipProps = {
|
||||
isSort?: boolean;
|
||||
};
|
||||
|
||||
const StyledChip = styled.div<StyledChipProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accent.quaternary};
|
||||
border: 1px solid ${({ theme }) => theme.accent.tertiary};
|
||||
border-radius: 4px;
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')};
|
||||
padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)};
|
||||
`;
|
||||
const StyledIcon = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledDelete = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
margin-top: 1px;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.accent.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLabelKey = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const SortOrFilterChip = ({
|
||||
labelKey,
|
||||
labelValue,
|
||||
Icon,
|
||||
onRemove,
|
||||
isSort,
|
||||
testId,
|
||||
}: SortOrFilterChipProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledChip isSort={isSort}>
|
||||
{Icon && (
|
||||
<StyledIcon>
|
||||
<Icon size={theme.icon.size.sm} />
|
||||
</StyledIcon>
|
||||
)}
|
||||
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
|
||||
{labelValue}
|
||||
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + testId}>
|
||||
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||
</StyledDelete>
|
||||
</StyledChip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortOrFilterChip;
|
||||
84
front/src/modules/views/components/UpdateViewButtonGroup.tsx
Normal file
84
front/src/modules/views/components/UpdateViewButtonGroup.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
import { useViewInternalStates } from '../hooks/useViewInternalStates';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
export type UpdateViewButtonGroupProps = {
|
||||
hotkeyScope: string;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
export const UpdateViewButtonGroup = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: UpdateViewButtonGroupProps) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { updateCurrentView, setViewEditMode } = useView();
|
||||
const { canPersistFilters, canPersistSorts } = useViewInternalStates();
|
||||
|
||||
const canPersistView = canPersistFilters || canPersistSorts;
|
||||
|
||||
const handleArrowDownButtonClick = useCallback(() => {
|
||||
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
|
||||
}, []);
|
||||
|
||||
const handleCreateViewButtonClick = useCallback(() => {
|
||||
setViewEditMode('create');
|
||||
onViewEditModeChange?.();
|
||||
setIsDropdownOpen(false);
|
||||
}, [setViewEditMode, onViewEditModeChange]);
|
||||
|
||||
const handleDropdownClose = useCallback(() => {
|
||||
setIsDropdownOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleViewSubmit = async () => {
|
||||
await updateCurrentView?.();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter, Key.Escape],
|
||||
handleDropdownClose,
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
if (!canPersistView) return null;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ButtonGroup size="small" accent="blue">
|
||||
<Button title="Update view" onClick={handleViewSubmit} />
|
||||
<Button
|
||||
size="small"
|
||||
Icon={IconChevronDown}
|
||||
onClick={handleArrowDownButtonClick}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
82
front/src/modules/views/components/ViewBar.tsx
Normal file
82
front/src/modules/views/components/ViewBar.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { FilterDropdownButton } from '@/ui/data/filter/components/FilterDropdownButton';
|
||||
import { FilterScope } from '@/ui/data/filter/scopes/FilterScope';
|
||||
import { FiltersHotkeyScope } from '@/ui/data/filter/types/FiltersHotkeyScope';
|
||||
import { SortDropdownButton } from '@/ui/data/sort/components/SortDropdownButton';
|
||||
import { SortScope } from '@/ui/data/sort/scopes/SortScope';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { TopBar } from '@/ui/layout/top-bar/TopBar';
|
||||
|
||||
import { useView } from '../hooks/useView';
|
||||
import { useViewInternalStates } from '../hooks/useViewInternalStates';
|
||||
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
|
||||
|
||||
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
|
||||
import { ViewBarDetails } from './ViewBarDetails';
|
||||
import { ViewsDropdownButton } from './ViewsDropdownButton';
|
||||
|
||||
export type ViewBarProps = {
|
||||
className?: string;
|
||||
optionsDropdownButton: ReactNode;
|
||||
optionsDropdownScopeId: string;
|
||||
};
|
||||
|
||||
export const ViewBar = ({
|
||||
className,
|
||||
optionsDropdownButton,
|
||||
optionsDropdownScopeId,
|
||||
}: ViewBarProps) => {
|
||||
const { openDropdown: openOptionsDropdownButton } = useDropdown({
|
||||
dropdownScopeId: optionsDropdownScopeId,
|
||||
});
|
||||
const { upsertViewSort } = useView();
|
||||
const { availableFilters, availableSorts } = useViewInternalStates();
|
||||
|
||||
return (
|
||||
<FilterScope
|
||||
filterScopeId="view-filter"
|
||||
availableFilters={availableFilters}
|
||||
>
|
||||
<SortScope
|
||||
sortScopeId="view-sort"
|
||||
availableSorts={availableSorts}
|
||||
onSortAdd={upsertViewSort}
|
||||
>
|
||||
<TopBar
|
||||
className={className}
|
||||
leftComponent={
|
||||
<ViewsDropdownButton
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
|
||||
/>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
rightComponent={
|
||||
<>
|
||||
<FilterDropdownButton
|
||||
hotkeyScope={{ scope: FiltersHotkeyScope.FilterDropdownButton }}
|
||||
/>
|
||||
<SortDropdownButton
|
||||
hotkeyScope={{ scope: FiltersHotkeyScope.SortDropdownButton }}
|
||||
isPrimaryButton
|
||||
/>
|
||||
{optionsDropdownButton}
|
||||
</>
|
||||
}
|
||||
bottomComponent={
|
||||
<ViewBarDetails
|
||||
hasFilterButton
|
||||
rightComponent={
|
||||
<UpdateViewButtonGroup
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SortScope>
|
||||
</FilterScope>
|
||||
);
|
||||
};
|
||||
191
front/src/modules/views/components/ViewBarDetails.tsx
Normal file
191
front/src/modules/views/components/ViewBarDetails.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { AddFilterFromDropdownButton } from '@/ui/data/filter/components/AddFilterFromDetailsButton';
|
||||
import { getOperandLabelShort } from '@/ui/data/filter/utils/getOperandLabel';
|
||||
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
|
||||
|
||||
import { useRemoveFilter } from '../hooks/useRemoveFilter';
|
||||
import { useView } from '../hooks/useView';
|
||||
import { useViewInternalStates } from '../hooks/useViewInternalStates';
|
||||
|
||||
import SortOrFilterChip from './SortOrFilterChip';
|
||||
|
||||
export type ViewBarDetailsProps = {
|
||||
hasFilterButton?: boolean;
|
||||
rightComponent?: ReactNode;
|
||||
};
|
||||
|
||||
const StyledBar = styled.div`
|
||||
align-items: center;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
z-index: 4;
|
||||
`;
|
||||
|
||||
const StyledChipcontainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
const StyledCancelButton = styled.button`
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: auto;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${(props) => {
|
||||
const horiz = props.theme.spacing(2);
|
||||
const vert = props.theme.spacing(1);
|
||||
return `${vert} ${horiz} ${vert} ${horiz}`;
|
||||
}};
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFilterContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledSeperatorContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSeperator = styled.div`
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.quaternary};
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledAddFilterContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
z-index: 5;
|
||||
`;
|
||||
|
||||
export const ViewBarDetails = ({
|
||||
hasFilterButton = false,
|
||||
rightComponent,
|
||||
}: ViewBarDetailsProps) => {
|
||||
const {
|
||||
currentViewSorts,
|
||||
setCurrentViewSorts,
|
||||
availableFilters,
|
||||
currentViewFilters,
|
||||
canPersistFilters,
|
||||
canPersistSorts,
|
||||
isViewBarExpanded,
|
||||
} = useViewInternalStates();
|
||||
|
||||
const { resetViewBar } = useView();
|
||||
|
||||
const canPersistView = canPersistFilters || canPersistSorts;
|
||||
|
||||
const filtersWithDefinition = currentViewFilters?.map((filter) => {
|
||||
const filterDefinition = availableFilters.find((availableFilter) => {
|
||||
return availableFilter.key === filter.key;
|
||||
});
|
||||
|
||||
return {
|
||||
...filter,
|
||||
...filterDefinition,
|
||||
};
|
||||
});
|
||||
|
||||
const removeFilter = useRemoveFilter();
|
||||
|
||||
const handleCancelClick = () => {
|
||||
resetViewBar();
|
||||
};
|
||||
|
||||
const handleSortRemove = (sortKey: string) =>
|
||||
setCurrentViewSorts?.((previousSorts) =>
|
||||
previousSorts.filter((sort) => sort.key !== sortKey),
|
||||
);
|
||||
|
||||
const shouldExpandViewBar =
|
||||
canPersistView ||
|
||||
((filtersWithDefinition?.length || currentViewSorts?.length) &&
|
||||
isViewBarExpanded);
|
||||
|
||||
if (!shouldExpandViewBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledBar>
|
||||
<StyledFilterContainer>
|
||||
<StyledChipcontainer>
|
||||
{currentViewSorts?.map((sort) => {
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={sort.key}
|
||||
testId={sort.key}
|
||||
labelValue={sort.definition.label}
|
||||
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
|
||||
isSort
|
||||
onRemove={() => handleSortRemove(sort.key)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!!currentViewSorts?.length && !!filtersWithDefinition?.length && (
|
||||
<StyledSeperatorContainer>
|
||||
<StyledSeperator />
|
||||
</StyledSeperatorContainer>
|
||||
)}
|
||||
{filtersWithDefinition?.map((filter) => {
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={filter.key}
|
||||
testId={filter.key}
|
||||
labelKey={filter.label}
|
||||
labelValue={`${getOperandLabelShort(filter.operand)} ${
|
||||
filter.displayValue
|
||||
}`}
|
||||
Icon={filter.Icon}
|
||||
onRemove={() => {
|
||||
removeFilter(filter.key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledChipcontainer>
|
||||
{hasFilterButton && (
|
||||
<StyledAddFilterContainer>
|
||||
<AddFilterFromDropdownButton />
|
||||
</StyledAddFilterContainer>
|
||||
)}
|
||||
</StyledFilterContainer>
|
||||
{canPersistView && (
|
||||
<StyledCancelButton
|
||||
data-testid="cancel-button"
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
Reset
|
||||
</StyledCancelButton>
|
||||
)}
|
||||
{rightComponent}
|
||||
</StyledBar>
|
||||
);
|
||||
};
|
||||
218
front/src/modules/views/components/ViewBarEffect.tsx
Normal file
218
front/src/modules/views/components/ViewBarEffect.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import {
|
||||
SortOrder,
|
||||
useGetViewFieldsQuery,
|
||||
useGetViewFiltersQuery,
|
||||
useGetViewSortsQuery,
|
||||
useGetViewsQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { useView } from '../hooks/useView';
|
||||
import { useViewInternalStates } from '../hooks/useViewInternalStates';
|
||||
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { availableSortsScopedState } from '../states/availableSortsScopedState';
|
||||
import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
|
||||
export const ViewBarEffect = () => {
|
||||
const {
|
||||
scopeId: viewScopeId,
|
||||
setCurrentViewFields,
|
||||
setSavedViewFields,
|
||||
setCurrentViewSorts,
|
||||
setSavedViewSorts,
|
||||
setCurrentViewFilters,
|
||||
setSavedViewFilters,
|
||||
currentViewId,
|
||||
setViews,
|
||||
setCurrentViewId,
|
||||
} = useView();
|
||||
|
||||
const { viewType, viewObjectId } = useViewInternalStates(viewScopeId);
|
||||
|
||||
useGetViewFieldsQuery({
|
||||
skip: !currentViewId,
|
||||
variables: {
|
||||
orderBy: { index: SortOrder.Asc },
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
|
||||
const availableFields = snapshot
|
||||
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!availableFields || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedViewFields = snapshot
|
||||
.getLoadable(
|
||||
savedViewFieldsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const queriedViewFields = data.viewFields
|
||||
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
|
||||
const columnDefinition = availableFields.find(
|
||||
({ key }) => viewField.key === key,
|
||||
);
|
||||
|
||||
return columnDefinition
|
||||
? {
|
||||
...columnDefinition,
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
index: viewField.index,
|
||||
size: viewField.size ?? columnDefinition.size,
|
||||
isVisible: viewField.isVisible,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
|
||||
|
||||
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
||||
setCurrentViewFields?.(queriedViewFields);
|
||||
setSavedViewFields?.(queriedViewFields);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
useGetViewsQuery({
|
||||
variables: {
|
||||
where: {
|
||||
objectId: { equals: viewObjectId },
|
||||
type: { equals: viewType },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
|
||||
const nextViews = data.views.map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
}));
|
||||
const views = snapshot
|
||||
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
||||
|
||||
if (!nextViews.length) return;
|
||||
|
||||
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
|
||||
}),
|
||||
});
|
||||
|
||||
useGetViewSortsQuery({
|
||||
skip: !currentViewId,
|
||||
variables: {
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
|
||||
const availableSorts = snapshot
|
||||
.getLoadable(availableSortsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!availableSorts || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedViewSorts = snapshot
|
||||
.getLoadable(
|
||||
savedViewSortsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const queriedViewSorts = data.viewSorts
|
||||
.map((viewSort) => {
|
||||
const foundCorrespondingSortDefinition = availableSorts.find(
|
||||
(sort) => sort.key === viewSort.key,
|
||||
);
|
||||
|
||||
if (foundCorrespondingSortDefinition) {
|
||||
return {
|
||||
key: viewSort.key,
|
||||
definition: foundCorrespondingSortDefinition,
|
||||
direction: viewSort.direction.toLowerCase(),
|
||||
} as Sort;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((sort): sort is Sort => !!sort);
|
||||
|
||||
if (!isDeeplyEqual(savedViewSorts, queriedViewSorts)) {
|
||||
setSavedViewSorts?.(queriedViewSorts);
|
||||
setCurrentViewSorts?.(queriedViewSorts);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
useGetViewFiltersQuery({
|
||||
skip: !currentViewId,
|
||||
variables: {
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => (data) => {
|
||||
const availableFilters = snapshot
|
||||
.getLoadable(availableFiltersScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!availableFilters || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedViewFilters = snapshot
|
||||
.getLoadable(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const queriedViewFilters = data.viewFilters
|
||||
.map(({ __typename, name: _name, ...viewFilter }) => {
|
||||
const availableFilter = availableFilters.find(
|
||||
(filter) => filter.key === viewFilter.key,
|
||||
);
|
||||
|
||||
return availableFilter
|
||||
? {
|
||||
...viewFilter,
|
||||
displayValue: viewFilter.displayValue ?? viewFilter.value,
|
||||
type: availableFilter.type,
|
||||
}
|
||||
: undefined;
|
||||
})
|
||||
.filter((filter): filter is Filter => !!filter);
|
||||
|
||||
if (!isDeeplyEqual(savedViewFilters, queriedViewFilters)) {
|
||||
setSavedViewFilters?.(queriedViewFilters);
|
||||
setCurrentViewFilters?.(queriedViewFilters);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,142 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { IconMinus, IconPlus } from '@/ui/display/icon';
|
||||
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
|
||||
import { IconInfoCircle } from '@/ui/input/constants/icons';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ViewFieldForVisibility } from '../types/ViewFieldForVisibility';
|
||||
|
||||
type ViewFieldsVisibilityDropdownSectionProps = {
|
||||
fields: ViewFieldForVisibility[];
|
||||
onVisibilityChange: (field: ViewFieldForVisibility) => void;
|
||||
title: string;
|
||||
isDraggable: boolean;
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
};
|
||||
|
||||
export const ViewFieldsVisibilityDropdownSection = ({
|
||||
fields,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
}: ViewFieldsVisibilityDropdownSectionProps) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
|
||||
const [openToolTipIndex, setOpenToolTipIndex] = useState<number>();
|
||||
|
||||
const handleInfoButtonClick = (index: number) => {
|
||||
if (index === openToolTipIndex) setOpenToolTipIndex(undefined);
|
||||
else setOpenToolTipIndex(index);
|
||||
};
|
||||
|
||||
const getIconButtons = (index: number, field: ViewFieldForVisibility) => {
|
||||
if (field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: IconInfoCircle,
|
||||
onClick: () => handleInfoButtonClick(index),
|
||||
isActive: openToolTipIndex === index,
|
||||
},
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [ref],
|
||||
callback: () => {
|
||||
setOpenToolTipIndex(undefined);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{isDraggable ? (
|
||||
<DraggableList
|
||||
onDragEnd={handleOnDrag}
|
||||
draggableItems={
|
||||
<>
|
||||
{fields
|
||||
.filter(({ index, size }) => index !== 0 || !size)
|
||||
.map((field, index) => (
|
||||
<DraggableItem
|
||||
key={field.key}
|
||||
draggableId={field.key}
|
||||
index={index + 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index + 1, field)}
|
||||
isTooltipOpen={openToolTipIndex === index + 1}
|
||||
text={field.name}
|
||||
className={`${title}-draggable-item-tooltip-anchor-${
|
||||
index + 1
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
fields.map((field, index) => (
|
||||
<MenuItem
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index, field)}
|
||||
isTooltipOpen={openToolTipIndex === index}
|
||||
text={field.name}
|
||||
className={`${title}-fixed-item-tooltip-anchor-${index}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(openToolTipIndex) &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`.${title}-${
|
||||
isDraggable ? 'draggable' : 'fixed'
|
||||
}-item-tooltip-anchor-${openToolTipIndex}`}
|
||||
place="left"
|
||||
content={fields[openToolTipIndex].infoTooltipContent}
|
||||
isOpen={true}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
172
front/src/modules/views/components/ViewsDropdownButton.tsx
Normal file
172
front/src/modules/views/components/ViewsDropdownButton.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconList,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from '@/ui/display/icon';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { ViewsDropdownId } from '../constants/ViewsDropdownId';
|
||||
import { useView } from '../hooks/useView';
|
||||
import { useViewInternalStates } from '../hooks/useViewInternalStates';
|
||||
|
||||
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledDropdownLabelAdornments = styled.span`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.grayScale.gray35};
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewIcon = styled(IconList)`
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewName = styled.span`
|
||||
display: inline-block;
|
||||
max-width: 130px;
|
||||
@media (max-width: 375px) {
|
||||
max-width: 90px;
|
||||
}
|
||||
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
max-width: 110px;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export type ViewsDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
export const ViewsDropdownButton = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: ViewsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const { scopeId, removeView, currentViewId } = useView();
|
||||
|
||||
const {
|
||||
views,
|
||||
currentView,
|
||||
setViewEditMode,
|
||||
setCurrentViewId,
|
||||
entityCountInCurrentView,
|
||||
} = useViewInternalStates(scopeId, currentViewId);
|
||||
|
||||
const { isDropdownOpen, closeDropdown } = useDropdown({
|
||||
dropdownScopeId: ViewsDropdownId,
|
||||
});
|
||||
|
||||
const handleViewSelect = useRecoilCallback(
|
||||
() => async (viewId: string) => {
|
||||
setCurrentViewId(viewId);
|
||||
|
||||
closeDropdown();
|
||||
},
|
||||
[setCurrentViewId, closeDropdown],
|
||||
);
|
||||
|
||||
const handleAddViewButtonClick = () => {
|
||||
setViewEditMode('create');
|
||||
onViewEditModeChange?.();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleEditViewButtonClick = (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setCurrentViewId(viewId);
|
||||
setViewEditMode('edit');
|
||||
onViewEditModeChange?.();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleDeleteViewButtonClick = async (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
|
||||
await removeView(viewId);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={ViewsDropdownId}>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||
<StyledViewIcon size={theme.icon.size.md} />
|
||||
<StyledViewName>{currentView?.name}</StyledViewName>
|
||||
<StyledDropdownLabelAdornments>
|
||||
· {entityCountInCurrentView}{' '}
|
||||
<IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledDropdownLabelAdornments>
|
||||
</StyledDropdownButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{views.map((view) => (
|
||||
<MenuItem
|
||||
key={view.id}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPencil,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleEditViewButtonClick(event, view.id),
|
||||
},
|
||||
views.length > 1
|
||||
? {
|
||||
Icon: IconTrash,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleDeleteViewButtonClick(event, view.id),
|
||||
}
|
||||
: null,
|
||||
].filter(assertNotNull)}
|
||||
onClick={() => handleViewSelect(view.id)}
|
||||
LeftIcon={IconList}
|
||||
text={view.name}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<StyledBoldDropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleAddViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add view"
|
||||
/>
|
||||
</StyledBoldDropdownMenuItemsContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
1
front/src/modules/views/constants/ViewsDropdownId.ts
Normal file
1
front/src/modules/views/constants/ViewsDropdownId.ts
Normal file
@ -0,0 +1 @@
|
||||
export const ViewsDropdownId = 'views';
|
||||
121
front/src/modules/views/hooks/internal/useViewFields.ts
Normal file
121
front/src/modules/views/hooks/internal/useViewFields.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/* eslint-disable no-console */
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
|
||||
import { savedViewFieldByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewFieldByKeyScopedFamilySelector';
|
||||
import { viewObjectIdScopeState } from '@/views/states/viewObjectIdScopeState';
|
||||
import {
|
||||
useCreateViewFieldsMutation,
|
||||
useUpdateViewFieldMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { GET_VIEW_FIELDS } from '../../graphql/queries/getViewFields';
|
||||
|
||||
export const toViewFieldInput = (
|
||||
objectId: string,
|
||||
fieldDefinition: ColumnDefinition<FieldMetadata>,
|
||||
) => ({
|
||||
key: fieldDefinition.key,
|
||||
name: fieldDefinition.name,
|
||||
index: fieldDefinition.index,
|
||||
isVisible: fieldDefinition.isVisible ?? true,
|
||||
objectId,
|
||||
size: fieldDefinition.size,
|
||||
});
|
||||
|
||||
export const useViewFields = (viewScopeId: string) => {
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const persistViewFields = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (viewFieldsToPersist: ColumnDefinition<FieldMetadata>[]) => {
|
||||
const currentViewId = snapshot
|
||||
.getLoadable(currentViewIdScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
const viewObjectId = snapshot
|
||||
.getLoadable(viewObjectIdScopeState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
const savedViewFieldsByKey = snapshot
|
||||
.getLoadable(
|
||||
savedViewFieldByKeyScopedFamilySelector({
|
||||
viewScopeId: viewScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!currentViewId || !savedViewFieldsByKey || !viewObjectId) {
|
||||
return;
|
||||
}
|
||||
const _createViewFields = (
|
||||
viewFieldsToCreate: ColumnDefinition<FieldMetadata>[],
|
||||
objectId: string,
|
||||
) => {
|
||||
if (!currentViewId || !viewFieldsToCreate.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return createViewFieldsMutation({
|
||||
variables: {
|
||||
data: viewFieldsToCreate.map((viewField) => ({
|
||||
...toViewFieldInput(objectId, viewField),
|
||||
viewId: currentViewId,
|
||||
})),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
const _updateViewFields = (
|
||||
viewFieldsToUpdate: ColumnDefinition<FieldMetadata>[],
|
||||
) => {
|
||||
if (!currentViewId || !viewFieldsToUpdate.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
viewFieldsToUpdate.map((viewField) =>
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: {
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
index: viewField.index,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: viewField.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const viewFieldsToCreate = viewFieldsToPersist.filter(
|
||||
(viewField) => !savedViewFieldsByKey[viewField.key],
|
||||
);
|
||||
await _createViewFields(viewFieldsToCreate, viewObjectId);
|
||||
|
||||
const viewFieldsToUpdate = viewFieldsToPersist.filter(
|
||||
(viewFieldToPersit) =>
|
||||
savedViewFieldsByKey[viewFieldToPersit.key] &&
|
||||
(savedViewFieldsByKey[viewFieldToPersit.key].size !==
|
||||
viewFieldToPersit.size ||
|
||||
savedViewFieldsByKey[viewFieldToPersit.key].index !==
|
||||
viewFieldToPersit.index ||
|
||||
savedViewFieldsByKey[viewFieldToPersit.key].isVisible !==
|
||||
viewFieldToPersit.isVisible),
|
||||
);
|
||||
|
||||
await _updateViewFields(viewFieldsToUpdate);
|
||||
},
|
||||
);
|
||||
|
||||
return { persistViewFields };
|
||||
};
|
||||
165
front/src/modules/views/hooks/internal/useViewFilters.ts
Normal file
165
front/src/modules/views/hooks/internal/useViewFilters.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { FilterDefinition } from '@/ui/data/filter/types/FilterDefinition';
|
||||
import { availableFiltersScopedState } from '@/views/states/availableFiltersScopedState';
|
||||
import { currentViewFiltersScopedFamilyState } from '@/views/states/currentViewFiltersScopedFamilyState';
|
||||
import { savedViewFiltersByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewFiltersByKeyScopedFamilySelector';
|
||||
import {
|
||||
useCreateViewFiltersMutation,
|
||||
useDeleteViewFiltersMutation,
|
||||
useUpdateViewFilterMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { useViewStates } from '../useViewStates';
|
||||
|
||||
export const useViewFilters = (viewScopeId: string) => {
|
||||
const { currentViewId } = useViewStates(viewScopeId);
|
||||
|
||||
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
|
||||
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
|
||||
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
|
||||
|
||||
const _createViewFilters = useCallback(
|
||||
(
|
||||
filters: Filter[],
|
||||
availableFilters: FilterDefinition[] = [],
|
||||
viewId = currentViewId,
|
||||
) => {
|
||||
if (!viewId || !filters.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!availableFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
return createViewFiltersMutation({
|
||||
variables: {
|
||||
data: filters.map((filter) => ({
|
||||
displayValue: filter.displayValue ?? filter.value,
|
||||
key: filter.key,
|
||||
name:
|
||||
availableFilters.find(({ key }) => key === filter.key)?.label ??
|
||||
'',
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
},
|
||||
[createViewFiltersMutation, currentViewId],
|
||||
);
|
||||
|
||||
const _updateViewFilters = useCallback(
|
||||
(filters: Filter[], viewId = currentViewId) => {
|
||||
if (!viewId || !filters.length) return;
|
||||
|
||||
return Promise.all(
|
||||
filters.map((filter) =>
|
||||
updateViewFilterMutation({
|
||||
variables: {
|
||||
data: {
|
||||
displayValue: filter.displayValue ?? filter.value,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: filter.key, viewId: viewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[currentViewId, updateViewFilterMutation],
|
||||
);
|
||||
|
||||
const _deleteViewFilters = useCallback(
|
||||
(filterKeys: string[], viewId = currentViewId) => {
|
||||
if (!viewId || !filterKeys.length) return;
|
||||
|
||||
return deleteViewFiltersMutation({
|
||||
variables: {
|
||||
where: {
|
||||
key: { in: filterKeys },
|
||||
viewId: { equals: viewId },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[currentViewId, deleteViewFiltersMutation],
|
||||
);
|
||||
|
||||
const persistViewFilters = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewFilters = snapshot
|
||||
.getLoadable(
|
||||
currentViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const savedViewFiltersByKey = snapshot
|
||||
.getLoadable(
|
||||
savedViewFiltersByKeyScopedFamilySelector({
|
||||
scopeId: viewScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!currentViewFilters) {
|
||||
return;
|
||||
}
|
||||
if (!savedViewFiltersByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableFilters = snapshot
|
||||
.getLoadable(
|
||||
availableFiltersScopedState({
|
||||
scopeId: viewScopeId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const filtersToCreate = currentViewFilters.filter(
|
||||
(filter) => !savedViewFiltersByKey[filter.key],
|
||||
);
|
||||
await _createViewFilters(filtersToCreate, availableFilters);
|
||||
|
||||
const filtersToUpdate = currentViewFilters.filter(
|
||||
(filter) =>
|
||||
savedViewFiltersByKey[filter.key] &&
|
||||
(savedViewFiltersByKey[filter.key].operand !== filter.operand ||
|
||||
savedViewFiltersByKey[filter.key].value !== filter.value),
|
||||
);
|
||||
await _updateViewFilters(filtersToUpdate);
|
||||
|
||||
const filterKeys = currentViewFilters.map((filter) => filter.key);
|
||||
const filterKeysToDelete = Object.keys(savedViewFiltersByKey).filter(
|
||||
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
|
||||
);
|
||||
await _deleteViewFilters(filterKeysToDelete);
|
||||
},
|
||||
[
|
||||
currentViewId,
|
||||
viewScopeId,
|
||||
_createViewFilters,
|
||||
_updateViewFilters,
|
||||
_deleteViewFilters,
|
||||
],
|
||||
);
|
||||
|
||||
return { persistViewFilters };
|
||||
};
|
||||
157
front/src/modules/views/hooks/internal/useViewSorts.ts
Normal file
157
front/src/modules/views/hooks/internal/useViewSorts.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-console */
|
||||
import { useCallback } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import { currentViewSortsScopedFamilyState } from '@/views/states/currentViewSortsScopedFamilyState';
|
||||
import { savedViewSortsByKeyScopedFamilySelector } from '@/views/states/selectors/savedViewSortsByKeyScopedFamilySelector';
|
||||
import {
|
||||
useCreateViewSortsMutation,
|
||||
useDeleteViewSortsMutation,
|
||||
useUpdateViewSortMutation,
|
||||
ViewSortDirection,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { useViewStates } from '../useViewStates';
|
||||
|
||||
export const useViewSorts = (viewScopeId: string) => {
|
||||
const { currentViewId, setCurrentViewSorts } = useViewStates(viewScopeId);
|
||||
|
||||
const [createViewSortsMutation] = useCreateViewSortsMutation();
|
||||
const [updateViewSortMutation] = useUpdateViewSortMutation();
|
||||
const [deleteViewSortsMutation] = useDeleteViewSortsMutation();
|
||||
|
||||
const _createViewSorts = useCallback(
|
||||
(sorts: Sort[], viewId = currentViewId) => {
|
||||
if (!viewId || !sorts.length) return;
|
||||
|
||||
return createViewSortsMutation({
|
||||
variables: {
|
||||
data: sorts.map((sort) => ({
|
||||
key: sort.key,
|
||||
direction: sort.direction as ViewSortDirection,
|
||||
name: sort.definition.label,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
},
|
||||
[createViewSortsMutation, currentViewId],
|
||||
);
|
||||
|
||||
const _updateViewSorts = useCallback(
|
||||
(sorts: Sort[]) => {
|
||||
if (!currentViewId || !sorts.length) return;
|
||||
|
||||
return Promise.all(
|
||||
sorts.map((sort) =>
|
||||
updateViewSortMutation({
|
||||
variables: {
|
||||
data: {
|
||||
direction: sort.direction as ViewSortDirection,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: sort.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[currentViewId, updateViewSortMutation],
|
||||
);
|
||||
|
||||
const _deleteViewSorts = useCallback(
|
||||
(sortKeys: string[]) => {
|
||||
if (!currentViewId || !sortKeys.length) return;
|
||||
|
||||
return deleteViewSortsMutation({
|
||||
variables: {
|
||||
where: {
|
||||
key: { in: sortKeys },
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[currentViewId, deleteViewSortsMutation],
|
||||
);
|
||||
|
||||
const persistViewSorts = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewSorts = snapshot
|
||||
.getLoadable(
|
||||
currentViewSortsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const savedViewSortsByKey = snapshot
|
||||
.getLoadable(
|
||||
savedViewSortsByKeyScopedFamilySelector({
|
||||
scopeId: viewScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!currentViewSorts) {
|
||||
return;
|
||||
}
|
||||
if (!savedViewSortsByKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortsToCreate = currentViewSorts.filter(
|
||||
(sort) => !savedViewSortsByKey[sort.key],
|
||||
);
|
||||
await _createViewSorts(sortsToCreate);
|
||||
|
||||
const sortsToUpdate = currentViewSorts.filter(
|
||||
(sort) =>
|
||||
savedViewSortsByKey[sort.key] &&
|
||||
savedViewSortsByKey[sort.key].direction !== sort.direction,
|
||||
);
|
||||
await _updateViewSorts(sortsToUpdate);
|
||||
|
||||
const sortKeys = currentViewSorts.map((sort) => sort.key);
|
||||
const sortKeysToDelete = Object.keys(savedViewSortsByKey).filter(
|
||||
(previousSortKey) => !sortKeys.includes(previousSortKey),
|
||||
);
|
||||
await _deleteViewSorts(sortKeysToDelete);
|
||||
},
|
||||
[
|
||||
currentViewId,
|
||||
viewScopeId,
|
||||
_createViewSorts,
|
||||
_updateViewSorts,
|
||||
_deleteViewSorts,
|
||||
],
|
||||
);
|
||||
|
||||
const upsertViewSort = (sortToUpsert: Sort) => {
|
||||
setCurrentViewSorts?.((sorts) => {
|
||||
return produce(sorts, (sortsDraft) => {
|
||||
const index = sortsDraft.findIndex(
|
||||
(sort) => sort.key === sortToUpsert.key,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
sortsDraft.push(sortToUpsert);
|
||||
} else {
|
||||
sortsDraft[index] = sortToUpsert;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { persistViewSorts, upsertViewSort };
|
||||
};
|
||||
67
front/src/modules/views/hooks/internal/useViews.ts
Normal file
67
front/src/modules/views/hooks/internal/useViews.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { viewObjectIdScopeState } from '@/views/states/viewObjectIdScopeState';
|
||||
import { viewTypeScopedState } from '@/views/states/viewTypeScopedState';
|
||||
import { View } from '@/views/types/View';
|
||||
import {
|
||||
useCreateViewMutation,
|
||||
useDeleteViewMutation,
|
||||
useUpdateViewMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { GET_VIEWS } from '../../graphql/queries/getViews';
|
||||
|
||||
export const useViews = (scopeId: string) => {
|
||||
const [createViewMutation] = useCreateViewMutation();
|
||||
const [updateViewMutation] = useUpdateViewMutation();
|
||||
const [deleteViewMutation] = useDeleteViewMutation();
|
||||
|
||||
const createView = useRecoilCallback(({ snapshot }) => async (view: View) => {
|
||||
const viewObjectId = await snapshot
|
||||
.getLoadable(viewObjectIdScopeState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
const viewType = await snapshot
|
||||
.getLoadable(viewTypeScopedState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!viewObjectId || !viewType) {
|
||||
return;
|
||||
}
|
||||
await createViewMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...view,
|
||||
objectId: viewObjectId,
|
||||
type: viewType,
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
});
|
||||
|
||||
const updateView = async (view: View) => {
|
||||
await updateViewMutation({
|
||||
variables: {
|
||||
data: { name: view.name },
|
||||
where: { id: view.id },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
const deleteView = async (viewId: string) => {
|
||||
await deleteViewMutation({
|
||||
variables: { where: { id: viewId } },
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createView,
|
||||
deleteView,
|
||||
isFetchingViews: false,
|
||||
updateView,
|
||||
};
|
||||
};
|
||||
@ -1,167 +0,0 @@
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { availableBoardCardFieldsScopedState } from '@/ui/layout/board/states/availableBoardCardFieldsScopedState';
|
||||
import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState';
|
||||
import { savedBoardCardFieldsFamilyState } from '@/ui/layout/board/states/savedBoardCardFieldsFamilyState';
|
||||
import { savedBoardCardFieldsByKeyFamilySelector } from '@/ui/layout/board/states/selectors/savedBoardCardFieldsByKeyFamilySelector';
|
||||
import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
SortOrder,
|
||||
useCreateViewFieldsMutation,
|
||||
useGetViewFieldsQuery,
|
||||
useUpdateViewFieldMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
const toViewFieldInput = (
|
||||
objectId: 'company' | 'person',
|
||||
columDefinition: BoardFieldDefinition<FieldMetadata>,
|
||||
) => ({
|
||||
key: columDefinition.key,
|
||||
name: columDefinition.name,
|
||||
index: columDefinition.index,
|
||||
isVisible: columDefinition.isVisible ?? true,
|
||||
objectId,
|
||||
});
|
||||
|
||||
export const useBoardViewFields = ({
|
||||
objectId,
|
||||
viewFieldDefinition,
|
||||
skipFetch,
|
||||
RecoilScopeContext,
|
||||
}: {
|
||||
objectId: 'company' | 'person';
|
||||
viewFieldDefinition: BoardFieldDefinition<FieldMetadata>[];
|
||||
skipFetch?: boolean;
|
||||
RecoilScopeContext: RecoilScopeContext;
|
||||
}) => {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [availableBoardCardFields, setAvailableBoardCardFields] =
|
||||
useRecoilScopedState(
|
||||
availableBoardCardFieldsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
|
||||
boardCardFieldsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const setSavedBoardCardFields = useSetRecoilState(
|
||||
savedBoardCardFieldsFamilyState(currentViewId),
|
||||
);
|
||||
const savedBoardCardFieldsByKey = useRecoilValue(
|
||||
savedBoardCardFieldsByKeyFamilySelector(currentViewId),
|
||||
);
|
||||
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const createViewFields = (
|
||||
viewFieldDefinitions: BoardFieldDefinition<FieldMetadata>[],
|
||||
viewId = currentViewId,
|
||||
) => {
|
||||
if (!viewId || !viewFieldDefinitions.length) return;
|
||||
|
||||
return createViewFieldsMutation({
|
||||
variables: {
|
||||
data: viewFieldDefinitions.map((field) => ({
|
||||
...toViewFieldInput(objectId, field),
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateViewFields = (
|
||||
viewFieldDefinitions: BoardFieldDefinition<FieldMetadata>[],
|
||||
) => {
|
||||
if (!currentViewId || !viewFieldDefinitions.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewFieldDefinitions.map((field) =>
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: {
|
||||
isVisible: field.isVisible,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: field.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const { refetch } = useGetViewFieldsQuery({
|
||||
skip: !currentViewId || skipFetch,
|
||||
variables: {
|
||||
orderBy: { index: SortOrder.Asc },
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
if (!data.viewFields.length) {
|
||||
// Populate if empty
|
||||
await createViewFields(viewFieldDefinition);
|
||||
return refetch();
|
||||
}
|
||||
|
||||
const nextFields = data.viewFields
|
||||
.map<BoardFieldDefinition<FieldMetadata> | null>((viewField) => {
|
||||
const fieldDefinition = viewFieldDefinition.find(
|
||||
({ key }) => viewField.key === key,
|
||||
);
|
||||
|
||||
return fieldDefinition
|
||||
? {
|
||||
...fieldDefinition,
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
index: viewField.index,
|
||||
isVisible: viewField.isVisible,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter<BoardFieldDefinition<FieldMetadata>>(assertNotNull);
|
||||
|
||||
if (!isDeeplyEqual(boardCardFields, nextFields)) {
|
||||
setSavedBoardCardFields(nextFields);
|
||||
setBoardCardFields(nextFields);
|
||||
}
|
||||
|
||||
if (!availableBoardCardFields.length) {
|
||||
setAvailableBoardCardFields(viewFieldDefinition);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const persistCardFields = async () => {
|
||||
if (!currentViewId) return;
|
||||
|
||||
const viewFieldsToCreate = boardCardFields.filter(
|
||||
(field) => !savedBoardCardFieldsByKey[field.key],
|
||||
);
|
||||
await createViewFields(viewFieldsToCreate);
|
||||
|
||||
const viewFieldsToUpdate = boardCardFields.filter(
|
||||
(field) =>
|
||||
savedBoardCardFieldsByKey[field.key] &&
|
||||
savedBoardCardFieldsByKey[field.key].isVisible !== field.isVisible,
|
||||
);
|
||||
await updateViewFields(viewFieldsToUpdate);
|
||||
|
||||
return refetch();
|
||||
};
|
||||
|
||||
return { createViewFields, persistCardFields };
|
||||
};
|
||||
@ -1,77 +0,0 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
|
||||
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
|
||||
import { useBoardColumns } from '@/ui/layout/board/hooks/useBoardColumns';
|
||||
import { boardCardFieldsScopedState } from '@/ui/layout/board/states/boardCardFieldsScopedState';
|
||||
import { BoardFieldDefinition } from '@/ui/layout/board/types/BoardFieldDefinition';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { ViewType } from '~/generated/graphql';
|
||||
|
||||
import { useBoardViewFields } from './useBoardViewFields';
|
||||
import { useViewFilters } from './useViewFilters';
|
||||
import { useViews } from './useViews';
|
||||
import { useViewSorts } from './useViewSorts';
|
||||
|
||||
export const useBoardViews = ({
|
||||
fieldDefinitions,
|
||||
objectId,
|
||||
RecoilScopeContext,
|
||||
}: {
|
||||
fieldDefinitions: BoardFieldDefinition<FieldMetadata>[];
|
||||
objectId: 'company';
|
||||
RecoilScopeContext: RecoilScopeContext;
|
||||
}) => {
|
||||
const boardCardFields = useRecoilScopedValue(
|
||||
boardCardFieldsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const filters = useRecoilScopedValue(filtersScopedState, RecoilScopeContext);
|
||||
const sorts = useRecoilScopedValue(sortsScopedState, RecoilScopeContext);
|
||||
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const handleViewCreate = async (viewId: string) => {
|
||||
await createViewFields(boardCardFields, viewId);
|
||||
await createViewFilters(filters, viewId);
|
||||
await createViewSorts(sorts, viewId);
|
||||
setSearchParams({ view: viewId });
|
||||
};
|
||||
|
||||
const { createView, deleteView, isFetchingViews, updateView } = useViews({
|
||||
objectId,
|
||||
onViewCreate: handleViewCreate,
|
||||
type: ViewType.Pipeline,
|
||||
RecoilScopeContext,
|
||||
});
|
||||
|
||||
const { createViewFields, persistCardFields } = useBoardViewFields({
|
||||
objectId,
|
||||
viewFieldDefinition: fieldDefinitions,
|
||||
skipFetch: isFetchingViews,
|
||||
RecoilScopeContext,
|
||||
});
|
||||
|
||||
const { persistBoardColumns } = useBoardColumns();
|
||||
|
||||
const { createViewFilters, persistFilters } = useViewFilters({
|
||||
skipFetch: isFetchingViews,
|
||||
RecoilScopeContext,
|
||||
});
|
||||
|
||||
const { createViewSorts, persistSorts } = useViewSorts({
|
||||
skipFetch: isFetchingViews,
|
||||
RecoilScopeContext,
|
||||
});
|
||||
|
||||
const submitCurrentView = async () => {
|
||||
await persistCardFields();
|
||||
await persistBoardColumns();
|
||||
await persistFilters();
|
||||
await persistSorts();
|
||||
};
|
||||
|
||||
return { createView, deleteView, submitCurrentView, updateView };
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
export const useMoveViewColumns = () => {
|
||||
const handleColumnMove = <T extends { index: number }>(
|
||||
direction: 'left' | 'right',
|
||||
currentArrayindex: number,
|
||||
targetArray: T[],
|
||||
) => {
|
||||
const targetArrayIndex =
|
||||
direction === 'left' ? currentArrayindex - 1 : currentArrayindex + 1;
|
||||
const targetArraySize = targetArray.length - 1;
|
||||
if (
|
||||
currentArrayindex >= 0 &&
|
||||
targetArrayIndex >= 0 &&
|
||||
currentArrayindex <= targetArraySize &&
|
||||
targetArrayIndex <= targetArraySize
|
||||
) {
|
||||
const currentEntity = targetArray[currentArrayindex];
|
||||
const targetEntity = targetArray[targetArrayIndex];
|
||||
const newArray = [...targetArray];
|
||||
|
||||
newArray[currentArrayindex] = {
|
||||
...targetEntity,
|
||||
index: currentEntity.index,
|
||||
};
|
||||
newArray[targetArrayIndex] = {
|
||||
...currentEntity,
|
||||
index: targetEntity.index,
|
||||
};
|
||||
return newArray;
|
||||
}
|
||||
|
||||
return targetArray;
|
||||
};
|
||||
|
||||
return { handleColumnMove };
|
||||
};
|
||||
15
front/src/modules/views/hooks/useRemoveFilter.ts
Normal file
15
front/src/modules/views/hooks/useRemoveFilter.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
export const useRemoveFilter = () => {
|
||||
const { setCurrentViewFilters } = useView();
|
||||
|
||||
const removeFilter = (filterKey: string) => {
|
||||
setCurrentViewFilters?.((filters) => {
|
||||
return filters.filter((filter) => {
|
||||
return filter.key !== filterKey;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return removeFilter;
|
||||
};
|
||||
@ -1,183 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { availableTableColumnsScopedState } from '@/ui/data/data-table/states/availableTableColumnsScopedState';
|
||||
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { savedTableColumnsFamilyState } from '@/ui/data/data-table/states/savedTableColumnsFamilyState';
|
||||
import { savedTableColumnsByKeyFamilySelector } from '@/ui/data/data-table/states/selectors/savedTableColumnsByKeyFamilySelector';
|
||||
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
SortOrder,
|
||||
useCreateViewFieldsMutation,
|
||||
useGetViewFieldsQuery,
|
||||
useUpdateViewFieldMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { GET_VIEW_FIELDS } from '../graphql/queries/getViewFields';
|
||||
|
||||
export const toViewFieldInput = (
|
||||
objectId: string,
|
||||
fieldDefinition: ColumnDefinition<FieldMetadata>,
|
||||
) => ({
|
||||
key: fieldDefinition.key,
|
||||
name: fieldDefinition.name,
|
||||
index: fieldDefinition.index,
|
||||
isVisible: fieldDefinition.isVisible ?? true,
|
||||
objectId,
|
||||
size: fieldDefinition.size,
|
||||
});
|
||||
|
||||
export const useTableViewFields = ({
|
||||
objectId,
|
||||
columnDefinitions,
|
||||
skipFetch,
|
||||
}: {
|
||||
objectId: string;
|
||||
columnDefinitions: ColumnDefinition<FieldMetadata>[];
|
||||
skipFetch?: boolean;
|
||||
}) => {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const [previousViewId, setPreviousViewId] = useState<string | undefined>();
|
||||
const [availableTableColumns, setAvailableTableColumns] =
|
||||
useRecoilScopedState(
|
||||
availableTableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const [tableColumns, setTableColumns] = useRecoilScopedState(
|
||||
tableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const setSavedTableColumns = useSetRecoilState(
|
||||
savedTableColumnsFamilyState(currentViewId),
|
||||
);
|
||||
const savedTableColumnsByKey = useRecoilValue(
|
||||
savedTableColumnsByKeyFamilySelector(currentViewId),
|
||||
);
|
||||
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const createViewFields = useCallback(
|
||||
(columns: ColumnDefinition<FieldMetadata>[], viewId = currentViewId) => {
|
||||
if (!viewId || !columns.length) return;
|
||||
|
||||
return createViewFieldsMutation({
|
||||
variables: {
|
||||
data: columns.map((column) => ({
|
||||
...toViewFieldInput(objectId, column),
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
},
|
||||
[createViewFieldsMutation, currentViewId, objectId],
|
||||
);
|
||||
|
||||
const updateViewFields = useCallback(
|
||||
(columns: ColumnDefinition<FieldMetadata>[]) => {
|
||||
if (!currentViewId || !columns.length) return;
|
||||
|
||||
return Promise.all(
|
||||
columns.map((column) =>
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: {
|
||||
isVisible: column.isVisible,
|
||||
size: column.size,
|
||||
index: column.index,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: column.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[currentViewId, updateViewFieldMutation],
|
||||
);
|
||||
|
||||
useGetViewFieldsQuery({
|
||||
skip: !currentViewId || skipFetch || columnDefinitions.length === 0,
|
||||
variables: {
|
||||
orderBy: { index: SortOrder.Asc },
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
if (!data.viewFields.length) {
|
||||
// Populate if empty
|
||||
return createViewFields(columnDefinitions);
|
||||
}
|
||||
|
||||
const nextColumns = data.viewFields
|
||||
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
|
||||
const columnDefinition = columnDefinitions.find(
|
||||
({ key }) => viewField.key === key,
|
||||
);
|
||||
|
||||
return columnDefinition
|
||||
? {
|
||||
...columnDefinition,
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
index: viewField.index,
|
||||
size: viewField.size ?? columnDefinition.size,
|
||||
isVisible: viewField.isVisible,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
|
||||
|
||||
setSavedTableColumns(nextColumns);
|
||||
|
||||
if (
|
||||
previousViewId !== currentViewId &&
|
||||
!isDeeplyEqual(tableColumns, nextColumns)
|
||||
) {
|
||||
setTableColumns(nextColumns);
|
||||
setPreviousViewId(currentViewId);
|
||||
}
|
||||
|
||||
if (!availableTableColumns.length) {
|
||||
setAvailableTableColumns(columnDefinitions);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const persistColumns = useCallback(
|
||||
async (nextColumns: ColumnDefinition<FieldMetadata>[]) => {
|
||||
if (!currentViewId) return;
|
||||
|
||||
const viewFieldsToCreate = nextColumns.filter(
|
||||
(column) => !savedTableColumnsByKey[column.key],
|
||||
);
|
||||
await createViewFields(viewFieldsToCreate);
|
||||
|
||||
const viewFieldsToUpdate = nextColumns.filter(
|
||||
(column) =>
|
||||
savedTableColumnsByKey[column.key] &&
|
||||
(savedTableColumnsByKey[column.key].size !== column.size ||
|
||||
savedTableColumnsByKey[column.key].index !== column.index ||
|
||||
savedTableColumnsByKey[column.key].isVisible !== column.isVisible),
|
||||
);
|
||||
await updateViewFields(viewFieldsToUpdate);
|
||||
},
|
||||
[createViewFields, currentViewId, savedTableColumnsByKey, updateViewFields],
|
||||
);
|
||||
|
||||
return { createViewFields, persistColumns };
|
||||
};
|
||||
@ -1,83 +0,0 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
|
||||
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { ViewType } from '~/generated/graphql';
|
||||
|
||||
import { useTableViewFields } from './useTableViewFields';
|
||||
import { useViewFilters } from './useViewFilters';
|
||||
import { useViews } from './useViews';
|
||||
import { useViewSorts } from './useViewSorts';
|
||||
|
||||
export const useTableViews = ({
|
||||
objectId,
|
||||
columnDefinitions,
|
||||
}: {
|
||||
objectId: string;
|
||||
columnDefinitions: ColumnDefinition<FieldMetadata>[];
|
||||
}) => {
|
||||
const tableColumns = useRecoilScopedValue(
|
||||
tableColumnsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext);
|
||||
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const handleViewCreate = async (viewId: string) => {
|
||||
await createViewFields(tableColumns, viewId);
|
||||
await createViewFilters(filters, viewId);
|
||||
await createViewSorts(sorts, viewId);
|
||||
setSearchParams({ view: viewId });
|
||||
};
|
||||
|
||||
const { createView, deleteView, isFetchingViews, updateView } = useViews({
|
||||
objectId,
|
||||
onViewCreate: handleViewCreate,
|
||||
type: ViewType.Table,
|
||||
RecoilScopeContext: TableRecoilScopeContext,
|
||||
});
|
||||
const { createViewFields, persistColumns } = useTableViewFields({
|
||||
objectId,
|
||||
columnDefinitions,
|
||||
skipFetch: isFetchingViews,
|
||||
});
|
||||
|
||||
const createDefaultViewFields = async () => {
|
||||
await createViewFields(tableColumns);
|
||||
};
|
||||
|
||||
const { createViewFilters, persistFilters } = useViewFilters({
|
||||
RecoilScopeContext: TableRecoilScopeContext,
|
||||
skipFetch: isFetchingViews,
|
||||
});
|
||||
|
||||
const { createViewSorts, persistSorts } = useViewSorts({
|
||||
RecoilScopeContext: TableRecoilScopeContext,
|
||||
skipFetch: isFetchingViews,
|
||||
});
|
||||
|
||||
const submitCurrentView = async () => {
|
||||
await persistFilters();
|
||||
await persistSorts();
|
||||
};
|
||||
|
||||
return {
|
||||
createView,
|
||||
deleteView,
|
||||
persistColumns,
|
||||
submitCurrentView,
|
||||
updateView,
|
||||
createDefaultViewFields,
|
||||
isFetchingViews,
|
||||
};
|
||||
};
|
||||
26
front/src/modules/views/hooks/useUpsertFilter.ts
Normal file
26
front/src/modules/views/hooks/useUpsertFilter.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
|
||||
export const useUpsertFilter = () => {
|
||||
const { setCurrentViewFilters } = useView();
|
||||
|
||||
const upsertFilter = (filterToUpsert: Filter) => {
|
||||
setCurrentViewFilters?.((filters) => {
|
||||
return produce(filters, (filtersDraft) => {
|
||||
const index = filtersDraft.findIndex(
|
||||
(filter) => filter.key === filterToUpsert.key,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
filtersDraft.push(filterToUpsert);
|
||||
} else {
|
||||
filtersDraft[index] = filterToUpsert;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return upsertFilter;
|
||||
};
|
||||
189
front/src/modules/views/hooks/useView.ts
Normal file
189
front/src/modules/views/hooks/useView.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { savedTableColumnsFamilyState } from '@/ui/data/data-table/states/savedTableColumnsFamilyState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
|
||||
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
|
||||
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
|
||||
import { useViewFields } from './internal/useViewFields';
|
||||
import { useViewFilters } from './internal/useViewFilters';
|
||||
import { useViews } from './internal/useViews';
|
||||
import { useViewSorts } from './internal/useViewSorts';
|
||||
import { useViewStates } from './useViewStates';
|
||||
|
||||
type UseViewProps = {
|
||||
viewScopeId?: string;
|
||||
};
|
||||
|
||||
export const useView = (props?: UseViewProps) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
ViewScopeInternalContext,
|
||||
props?.viewScopeId,
|
||||
);
|
||||
|
||||
const {
|
||||
setCurrentViewId,
|
||||
currentViewId,
|
||||
|
||||
setViews,
|
||||
setViewEditMode,
|
||||
setViewObjectId,
|
||||
setViewType,
|
||||
setEntityCountInCurrentView,
|
||||
setIsViewBarExpanded,
|
||||
|
||||
setAvailableSorts,
|
||||
setCurrentViewSorts,
|
||||
setSavedViewSorts,
|
||||
|
||||
setAvailableFilters,
|
||||
setCurrentViewFilters,
|
||||
setSavedViewFilters,
|
||||
|
||||
setAvailableFields,
|
||||
setCurrentViewFields,
|
||||
setSavedViewFields,
|
||||
} = useViewStates(scopeId);
|
||||
|
||||
const { persistViewSorts, upsertViewSort } = useViewSorts(scopeId);
|
||||
const { persistViewFilters } = useViewFilters(scopeId);
|
||||
const { persistViewFields } = useViewFields(scopeId);
|
||||
const { createView: internalCreateView, deleteView: internalDeleteView } =
|
||||
useViews(scopeId);
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const resetViewBar = useRecoilCallback(({ snapshot }) => () => {
|
||||
const savedViewFilters = snapshot
|
||||
.getLoadable(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId,
|
||||
familyKey: currentViewId || '',
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const savedViewSorts = snapshot
|
||||
.getLoadable(
|
||||
savedViewSortsScopedFamilyState({
|
||||
scopeId,
|
||||
familyKey: currentViewId || '',
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (savedViewFilters) {
|
||||
setCurrentViewFilters?.(savedViewFilters);
|
||||
}
|
||||
if (savedViewSorts) {
|
||||
setCurrentViewSorts?.(savedViewSorts);
|
||||
}
|
||||
setViewEditMode?.('none');
|
||||
setIsViewBarExpanded?.(false);
|
||||
});
|
||||
|
||||
const createView = useCallback(
|
||||
async (name: string) => {
|
||||
const newViewId = v4();
|
||||
await internalCreateView({ id: v4(), name });
|
||||
|
||||
// await persistViewFields();
|
||||
await persistViewFilters();
|
||||
await persistViewSorts();
|
||||
//setCurrentViewId(newViewId);
|
||||
|
||||
setSearchParams({ view: newViewId });
|
||||
},
|
||||
[internalCreateView, persistViewFilters, persistViewSorts, setSearchParams],
|
||||
);
|
||||
|
||||
const updateCurrentView = async () => {
|
||||
await persistViewFilters();
|
||||
await persistViewSorts();
|
||||
};
|
||||
|
||||
const removeView = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (viewId: string) => {
|
||||
const currentViewId = await snapshot.getPromise(
|
||||
currentViewIdScopedState({ scopeId }),
|
||||
);
|
||||
|
||||
if (currentViewId === viewId)
|
||||
set(currentViewIdScopedState({ scopeId }), undefined);
|
||||
|
||||
set(viewsScopedState({ scopeId }), (previousViews) =>
|
||||
previousViews.filter((view) => view.id !== viewId),
|
||||
);
|
||||
internalDeleteView(viewId);
|
||||
},
|
||||
[internalDeleteView, scopeId],
|
||||
);
|
||||
|
||||
const handleViewNameSubmit = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (name?: string) => {
|
||||
const viewEditMode = snapshot
|
||||
.getLoadable(viewEditModeScopedState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
const currentViewFields = snapshot
|
||||
.getLoadable(
|
||||
currentViewFieldsScopedFamilyState({
|
||||
scopeId,
|
||||
familyKey: currentViewId || '',
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const isCreateMode = viewEditMode === 'create';
|
||||
|
||||
if (isCreateMode && name && currentViewFields) {
|
||||
await createView(name);
|
||||
set(savedTableColumnsFamilyState(currentViewId), currentViewFields);
|
||||
}
|
||||
},
|
||||
[createView, currentViewId, scopeId],
|
||||
);
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
currentViewId,
|
||||
setCurrentViewId,
|
||||
updateCurrentView,
|
||||
createView,
|
||||
removeView,
|
||||
setIsViewBarExpanded,
|
||||
resetViewBar,
|
||||
handleViewNameSubmit,
|
||||
|
||||
setViews,
|
||||
setViewEditMode,
|
||||
setViewObjectId,
|
||||
setViewType,
|
||||
setEntityCountInCurrentView,
|
||||
|
||||
setAvailableSorts,
|
||||
setCurrentViewSorts,
|
||||
setSavedViewSorts,
|
||||
upsertViewSort,
|
||||
|
||||
setAvailableFilters,
|
||||
setCurrentViewFilters,
|
||||
setSavedViewFilters,
|
||||
|
||||
setAvailableFields,
|
||||
setCurrentViewFields,
|
||||
setSavedViewFields,
|
||||
|
||||
persistViewFields,
|
||||
};
|
||||
};
|
||||
@ -1,178 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
import { availableFiltersScopedState } from '@/ui/data/view-bar/states/availableFiltersScopedState';
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
|
||||
import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState';
|
||||
import { savedFiltersByKeyFamilySelector } from '@/ui/data/view-bar/states/selectors/savedFiltersByKeyFamilySelector';
|
||||
import { Filter } from '@/ui/data/view-bar/types/Filter';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
useCreateViewFiltersMutation,
|
||||
useDeleteViewFiltersMutation,
|
||||
useGetViewFiltersQuery,
|
||||
useUpdateViewFilterMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useViewFilters = ({
|
||||
RecoilScopeContext,
|
||||
skipFetch,
|
||||
}: {
|
||||
RecoilScopeContext: RecoilScopeContext;
|
||||
skipFetch?: boolean;
|
||||
}) => {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [filters, setFilters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [, setSavedFilters] = useRecoilState(
|
||||
savedFiltersFamilyState(currentViewId),
|
||||
);
|
||||
const savedFiltersByKey = useRecoilValue(
|
||||
savedFiltersByKeyFamilySelector(currentViewId),
|
||||
);
|
||||
|
||||
const { refetch } = useGetViewFiltersQuery({
|
||||
skip: !currentViewId || skipFetch,
|
||||
variables: {
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const nextFilters = data.viewFilters
|
||||
.map(({ __typename, name: _name, ...viewFilter }) => {
|
||||
const availableFilter = availableFilters.find(
|
||||
(filter) => filter.key === viewFilter.key,
|
||||
);
|
||||
|
||||
return availableFilter
|
||||
? {
|
||||
...viewFilter,
|
||||
displayValue: viewFilter.displayValue ?? viewFilter.value,
|
||||
type: availableFilter.type,
|
||||
}
|
||||
: undefined;
|
||||
})
|
||||
.filter((filter): filter is Filter => !!filter);
|
||||
|
||||
if (!isDeeplyEqual(filters, nextFilters)) {
|
||||
setSavedFilters(nextFilters);
|
||||
setFilters(nextFilters);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
|
||||
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
|
||||
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
|
||||
|
||||
const createViewFilters = useCallback(
|
||||
(filters: Filter[], viewId = currentViewId) => {
|
||||
if (!viewId || !filters.length) return;
|
||||
|
||||
return createViewFiltersMutation({
|
||||
variables: {
|
||||
data: filters.map((filter) => ({
|
||||
displayValue: filter.displayValue ?? filter.value,
|
||||
key: filter.key,
|
||||
name:
|
||||
availableFilters.find(({ key }) => key === filter.key)?.label ??
|
||||
'',
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
},
|
||||
[availableFilters, createViewFiltersMutation, currentViewId],
|
||||
);
|
||||
|
||||
const updateViewFilters = useCallback(
|
||||
(filters: Filter[]) => {
|
||||
if (!currentViewId || !filters.length) return;
|
||||
|
||||
return Promise.all(
|
||||
filters.map((filter) =>
|
||||
updateViewFilterMutation({
|
||||
variables: {
|
||||
data: {
|
||||
displayValue: filter.displayValue ?? filter.value,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: filter.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[currentViewId, updateViewFilterMutation],
|
||||
);
|
||||
|
||||
const deleteViewFilters = useCallback(
|
||||
(filterKeys: string[]) => {
|
||||
if (!currentViewId || !filterKeys.length) return;
|
||||
|
||||
return deleteViewFiltersMutation({
|
||||
variables: {
|
||||
where: {
|
||||
key: { in: filterKeys },
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[currentViewId, deleteViewFiltersMutation],
|
||||
);
|
||||
|
||||
const persistFilters = useCallback(async () => {
|
||||
if (!currentViewId) return;
|
||||
|
||||
const filtersToCreate = filters.filter(
|
||||
(filter) => !savedFiltersByKey[filter.key],
|
||||
);
|
||||
await createViewFilters(filtersToCreate);
|
||||
|
||||
const filtersToUpdate = filters.filter(
|
||||
(filter) =>
|
||||
savedFiltersByKey[filter.key] &&
|
||||
(savedFiltersByKey[filter.key].operand !== filter.operand ||
|
||||
savedFiltersByKey[filter.key].value !== filter.value),
|
||||
);
|
||||
await updateViewFilters(filtersToUpdate);
|
||||
|
||||
const filterKeys = filters.map((filter) => filter.key);
|
||||
const filterKeysToDelete = Object.keys(savedFiltersByKey).filter(
|
||||
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
|
||||
);
|
||||
await deleteViewFilters(filterKeysToDelete);
|
||||
|
||||
return refetch();
|
||||
}, [
|
||||
currentViewId,
|
||||
filters,
|
||||
createViewFilters,
|
||||
updateViewFilters,
|
||||
savedFiltersByKey,
|
||||
deleteViewFilters,
|
||||
refetch,
|
||||
]);
|
||||
|
||||
return { createViewFilters, persistFilters };
|
||||
};
|
||||
244
front/src/modules/views/hooks/useViewInternalStates.ts
Normal file
244
front/src/modules/views/hooks/useViewInternalStates.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
|
||||
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
|
||||
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { availableSortsScopedState } from '../states/availableSortsScopedState';
|
||||
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
|
||||
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { currentViewSortsScopedFamilyState } from '../states/currentViewSortsScopedFamilyState';
|
||||
import { entityCountInCurrentViewScopedState } from '../states/entityCountInCurrentViewScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
|
||||
import { onViewFieldsChangeScopedState } from '../states/onViewFieldsChangeScopedState';
|
||||
import { onViewFiltersChangeScopedState } from '../states/onViewFiltersChangeScopedState';
|
||||
import { onViewSortsChangeScopedState } from '../states/onViewSortsChangeScopedState';
|
||||
import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
|
||||
import { canPersistViewFiltersScopedFamilySelector } from '../states/selectors/canPersistViewFiltersScopedFamilySelector';
|
||||
import { canPersistViewSortsScopedFamilySelector } from '../states/selectors/canPersistViewSortsScopedFamilySelector';
|
||||
import { currentViewScopedSelector } from '../states/selectors/currentViewScopedSelector';
|
||||
import { currentViewSortsOrderByScopedFamilySelector } from '../states/selectors/currentViewSortsOrderByScopedFamilySelector';
|
||||
import { savedViewFieldByKeyScopedFamilySelector } from '../states/selectors/savedViewFieldByKeyScopedFamilySelector';
|
||||
import { savedViewFiltersByKeyScopedFamilySelector } from '../states/selectors/savedViewFiltersByKeyScopedFamilySelector';
|
||||
import { savedViewSortsByKeyScopedFamilySelector } from '../states/selectors/savedViewSortsByKeyScopedFamilySelector';
|
||||
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
|
||||
import { viewObjectIdScopeState } from '../states/viewObjectIdScopeState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
import { viewTypeScopedState } from '../states/viewTypeScopedState';
|
||||
|
||||
export const useViewInternalStates = (
|
||||
viewScopeId?: string,
|
||||
viewId?: string,
|
||||
) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
ViewScopeInternalContext,
|
||||
viewScopeId,
|
||||
);
|
||||
|
||||
// View
|
||||
const [currentViewId, setCurrentViewId] = useRecoilScopedStateV2(
|
||||
currentViewIdScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const familyItemId = viewId ? viewId : currentViewId;
|
||||
|
||||
const currentView = useRecoilValue(currentViewScopedSelector(scopeId));
|
||||
|
||||
const [viewEditMode, setViewEditMode] = useRecoilScopedStateV2(
|
||||
viewEditModeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [views, setViews] = useRecoilScopedStateV2(viewsScopedState, scopeId);
|
||||
|
||||
const [viewObjectId, setViewObjectId] = useRecoilScopedStateV2(
|
||||
viewObjectIdScopeState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [viewType, setViewType] = useRecoilScopedStateV2(
|
||||
viewTypeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [entityCountInCurrentView, setEntityCountInCurrentView] =
|
||||
useRecoilScopedStateV2(entityCountInCurrentViewScopedState, scopeId);
|
||||
|
||||
const [isViewBarExpanded, setIsViewBarExpanded] = useRecoilScopedStateV2(
|
||||
isViewBarExpandedScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
// ViewSorts
|
||||
const [currentViewSorts, setCurrentViewSorts] = useRecoilScopedFamilyState(
|
||||
currentViewSortsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const [savedViewSorts, setSavedViewSorts] = useRecoilScopedFamilyState(
|
||||
savedViewSortsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const savedViewSortsByKey = useRecoilValue(
|
||||
savedViewSortsByKeyScopedFamilySelector({
|
||||
scopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [availableSorts, setAvailableSorts] = useRecoilScopedStateV2(
|
||||
availableSortsScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const canPersistSorts = useRecoilValue(
|
||||
canPersistViewSortsScopedFamilySelector({
|
||||
viewScopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentViewSortsOrderBy = useRecoilValue(
|
||||
currentViewSortsOrderByScopedFamilySelector({
|
||||
viewScopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
// ViewFilters
|
||||
const [currentViewFilters, setCurrentViewFilters] =
|
||||
useRecoilScopedFamilyState(
|
||||
currentViewFiltersScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const [savedViewFilters, setSavedViewFilters] = useRecoilScopedFamilyState(
|
||||
savedViewFiltersScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const savedViewFiltersByKey = useRecoilValue(
|
||||
savedViewFiltersByKeyScopedFamilySelector({
|
||||
scopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [availableFilters, setAvailableFilters] = useRecoilScopedStateV2(
|
||||
availableFiltersScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const canPersistFilters = useRecoilValue(
|
||||
canPersistViewFiltersScopedFamilySelector({
|
||||
viewScopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
// ViewFields
|
||||
const [availableFields, setAvailableFields] = useRecoilScopedStateV2(
|
||||
availableFieldsScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [currentViewFields, setCurrentViewFields] = useRecoilScopedFamilyState(
|
||||
currentViewFieldsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const [savedViewFields, setSavedViewFields] = useRecoilScopedFamilyState(
|
||||
savedViewFieldsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const savedViewFieldsByKey = useRecoilValue(
|
||||
savedViewFieldByKeyScopedFamilySelector({
|
||||
viewScopeId: scopeId,
|
||||
viewId: familyItemId,
|
||||
}),
|
||||
);
|
||||
|
||||
// ViewChangeHandlers
|
||||
const [onViewSortsChange, setOnViewSortsChange] = useRecoilScopedStateV2(
|
||||
onViewSortsChangeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [onViewFiltersChange, setOnViewFiltersChange] = useRecoilScopedStateV2(
|
||||
onViewFiltersChangeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const [onViewFieldsChange, setOnViewFieldsChange] = useRecoilScopedStateV2(
|
||||
onViewFieldsChangeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
currentViewId,
|
||||
currentView,
|
||||
setCurrentViewId,
|
||||
isViewBarExpanded,
|
||||
setIsViewBarExpanded,
|
||||
|
||||
views,
|
||||
setViews,
|
||||
viewEditMode,
|
||||
setViewEditMode,
|
||||
viewObjectId,
|
||||
setViewObjectId,
|
||||
viewType,
|
||||
setViewType,
|
||||
entityCountInCurrentView,
|
||||
setEntityCountInCurrentView,
|
||||
|
||||
availableSorts,
|
||||
setAvailableSorts,
|
||||
currentViewSorts,
|
||||
setCurrentViewSorts,
|
||||
savedViewSorts,
|
||||
savedViewSortsByKey,
|
||||
setSavedViewSorts,
|
||||
canPersistSorts,
|
||||
currentViewSortsOrderBy,
|
||||
|
||||
availableFilters,
|
||||
setAvailableFilters,
|
||||
currentViewFilters,
|
||||
setCurrentViewFilters,
|
||||
savedViewFilters,
|
||||
savedViewFiltersByKey,
|
||||
setSavedViewFilters,
|
||||
canPersistFilters,
|
||||
|
||||
availableFields,
|
||||
setAvailableFields,
|
||||
currentViewFields,
|
||||
savedViewFieldsByKey,
|
||||
setCurrentViewFields,
|
||||
savedViewFields,
|
||||
setSavedViewFields,
|
||||
|
||||
onViewSortsChange,
|
||||
setOnViewSortsChange,
|
||||
onViewFiltersChange,
|
||||
setOnViewFiltersChange,
|
||||
onViewFieldsChange,
|
||||
setOnViewFieldsChange,
|
||||
};
|
||||
};
|
||||
@ -1,172 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
import { availableSortsScopedState } from '@/ui/data/view-bar/states/availableSortsScopedState';
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState';
|
||||
import { savedSortsByKeyFamilySelector } from '@/ui/data/view-bar/states/selectors/savedSortsByKeyFamilySelector';
|
||||
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
|
||||
import { Sort } from '@/ui/data/view-bar/types/Sort';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
useCreateViewSortsMutation,
|
||||
useDeleteViewSortsMutation,
|
||||
useGetViewSortsQuery,
|
||||
useUpdateViewSortMutation,
|
||||
ViewSortDirection,
|
||||
} from '~/generated/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useViewSorts = ({
|
||||
RecoilScopeContext,
|
||||
skipFetch,
|
||||
}: {
|
||||
RecoilScopeContext: RecoilScopeContext;
|
||||
skipFetch?: boolean;
|
||||
}) => {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [sorts, setSorts] = useRecoilScopedState(
|
||||
sortsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [availableSorts] = useRecoilScopedState(
|
||||
availableSortsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [, setSavedSorts] = useRecoilState(
|
||||
savedSortsFamilyState(currentViewId),
|
||||
);
|
||||
const savedSortsByKey = useRecoilValue(
|
||||
savedSortsByKeyFamilySelector(currentViewId),
|
||||
);
|
||||
|
||||
const { refetch } = useGetViewSortsQuery({
|
||||
skip: !currentViewId || skipFetch,
|
||||
variables: {
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const nextSorts = data.viewSorts
|
||||
.map((viewSort) => {
|
||||
const foundCorrespondingSortDefinition = availableSorts.find(
|
||||
(sort) => sort.key === viewSort.key,
|
||||
);
|
||||
|
||||
if (foundCorrespondingSortDefinition) {
|
||||
return {
|
||||
key: viewSort.key,
|
||||
definition: foundCorrespondingSortDefinition,
|
||||
direction: viewSort.direction.toLowerCase(),
|
||||
} as Sort;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((sort): sort is Sort => !!sort);
|
||||
|
||||
if (!isDeeplyEqual(sorts, nextSorts)) {
|
||||
setSavedSorts(nextSorts);
|
||||
setSorts(nextSorts);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [createViewSortsMutation] = useCreateViewSortsMutation();
|
||||
const [updateViewSortMutation] = useUpdateViewSortMutation();
|
||||
const [deleteViewSortsMutation] = useDeleteViewSortsMutation();
|
||||
|
||||
const createViewSorts = useCallback(
|
||||
(sorts: Sort[], viewId = currentViewId) => {
|
||||
if (!viewId || !sorts.length) return;
|
||||
|
||||
return createViewSortsMutation({
|
||||
variables: {
|
||||
data: sorts.map((sort) => ({
|
||||
key: sort.key,
|
||||
direction: sort.direction as ViewSortDirection,
|
||||
name: sort.definition.label,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
},
|
||||
[createViewSortsMutation, currentViewId],
|
||||
);
|
||||
|
||||
const updateViewSorts = useCallback(
|
||||
(sorts: Sort[]) => {
|
||||
if (!currentViewId || !sorts.length) return;
|
||||
|
||||
return Promise.all(
|
||||
sorts.map((sort) =>
|
||||
updateViewSortMutation({
|
||||
variables: {
|
||||
data: {
|
||||
direction: sort.direction as ViewSortDirection,
|
||||
},
|
||||
where: {
|
||||
viewId_key: { key: sort.key, viewId: currentViewId },
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[currentViewId, updateViewSortMutation],
|
||||
);
|
||||
|
||||
const deleteViewSorts = useCallback(
|
||||
(sortKeys: string[]) => {
|
||||
if (!currentViewId || !sortKeys.length) return;
|
||||
|
||||
return deleteViewSortsMutation({
|
||||
variables: {
|
||||
where: {
|
||||
key: { in: sortKeys },
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[currentViewId, deleteViewSortsMutation],
|
||||
);
|
||||
|
||||
const persistSorts = useCallback(async () => {
|
||||
if (!currentViewId) return;
|
||||
|
||||
const sortsToCreate = sorts.filter((sort) => !savedSortsByKey[sort.key]);
|
||||
await createViewSorts(sortsToCreate);
|
||||
|
||||
const sortsToUpdate = sorts.filter(
|
||||
(sort) =>
|
||||
savedSortsByKey[sort.key] &&
|
||||
savedSortsByKey[sort.key].direction !== sort.direction,
|
||||
);
|
||||
await updateViewSorts(sortsToUpdate);
|
||||
|
||||
const sortKeys = sorts.map((sort) => sort.key);
|
||||
const sortKeysToDelete = Object.keys(savedSortsByKey).filter(
|
||||
(previousSortKey) => !sortKeys.includes(previousSortKey),
|
||||
);
|
||||
await deleteViewSorts(sortKeysToDelete);
|
||||
|
||||
return refetch();
|
||||
}, [
|
||||
currentViewId,
|
||||
sorts,
|
||||
createViewSorts,
|
||||
updateViewSorts,
|
||||
savedSortsByKey,
|
||||
deleteViewSorts,
|
||||
refetch,
|
||||
]);
|
||||
|
||||
return { createViewSorts, persistSorts };
|
||||
};
|
||||
138
front/src/modules/views/hooks/useViewStates.ts
Normal file
138
front/src/modules/views/hooks/useViewStates.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
|
||||
import { useSetRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedFamilyState';
|
||||
import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
import { ViewScopeInternalContext } from '../scopes/scope-internal-context/ViewScopeInternalContext';
|
||||
import { availableFieldsScopedState } from '../states/availableFieldsScopedState';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { availableSortsScopedState } from '../states/availableSortsScopedState';
|
||||
import { currentViewFieldsScopedFamilyState } from '../states/currentViewFieldsScopedFamilyState';
|
||||
import { currentViewFiltersScopedFamilyState } from '../states/currentViewFiltersScopedFamilyState';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { currentViewSortsScopedFamilyState } from '../states/currentViewSortsScopedFamilyState';
|
||||
import { entityCountInCurrentViewScopedState } from '../states/entityCountInCurrentViewScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
|
||||
import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
|
||||
import { viewEditModeScopedState } from '../states/viewEditModeScopedState';
|
||||
import { viewObjectIdScopeState } from '../states/viewObjectIdScopeState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
import { viewTypeScopedState } from '../states/viewTypeScopedState';
|
||||
|
||||
export const useViewStates = (viewScopeId?: string, viewId?: string) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
ViewScopeInternalContext,
|
||||
viewScopeId,
|
||||
);
|
||||
// View
|
||||
const [currentViewId, setCurrentViewId] = useRecoilScopedStateV2(
|
||||
currentViewIdScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const familyItemId = viewId ? viewId : currentViewId;
|
||||
|
||||
const setViewEditMode = useSetRecoilScopedStateV2(
|
||||
viewEditModeScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const setViews = useSetRecoilScopedStateV2(viewsScopedState, scopeId);
|
||||
|
||||
const setViewObjectId = useSetRecoilScopedStateV2(
|
||||
viewObjectIdScopeState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const setViewType = useSetRecoilScopedStateV2(viewTypeScopedState, scopeId);
|
||||
|
||||
const setEntityCountInCurrentView = useSetRecoilScopedStateV2(
|
||||
entityCountInCurrentViewScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const setIsViewBarExpanded = useSetRecoilScopedStateV2(
|
||||
isViewBarExpandedScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
// ViewSorts
|
||||
const setCurrentViewSorts = useSetRecoilScopedFamilyState(
|
||||
currentViewSortsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const setSavedViewSorts = useSetRecoilScopedFamilyState(
|
||||
savedViewSortsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const setAvailableSorts = useSetRecoilScopedStateV2(
|
||||
availableSortsScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
// ViewFilters
|
||||
const setCurrentViewFilters = useSetRecoilScopedFamilyState(
|
||||
currentViewFiltersScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const setSavedViewFilters = useSetRecoilScopedFamilyState(
|
||||
savedViewFiltersScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const setAvailableFilters = useSetRecoilScopedStateV2(
|
||||
availableFiltersScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
// ViewFields
|
||||
const setAvailableFields = useSetRecoilScopedStateV2(
|
||||
availableFieldsScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const setCurrentViewFields = useSetRecoilScopedFamilyState(
|
||||
currentViewFieldsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
const setSavedViewFields = useSetRecoilScopedFamilyState(
|
||||
savedViewFieldsScopedFamilyState,
|
||||
scopeId,
|
||||
familyItemId,
|
||||
);
|
||||
|
||||
return {
|
||||
currentViewId,
|
||||
setCurrentViewId,
|
||||
setIsViewBarExpanded,
|
||||
|
||||
setViews,
|
||||
setViewEditMode,
|
||||
setViewObjectId,
|
||||
setViewType,
|
||||
setEntityCountInCurrentView,
|
||||
|
||||
setAvailableSorts,
|
||||
setCurrentViewSorts,
|
||||
setSavedViewSorts,
|
||||
|
||||
setAvailableFilters,
|
||||
setCurrentViewFilters,
|
||||
setSavedViewFilters,
|
||||
|
||||
setAvailableFields,
|
||||
setCurrentViewFields,
|
||||
setSavedViewFields,
|
||||
};
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { viewsScopedState } from '@/ui/data/view-bar/states/viewsScopedState';
|
||||
import { View } from '@/ui/data/view-bar/types/View';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import {
|
||||
useCreateViewMutation,
|
||||
useDeleteViewMutation,
|
||||
useGetViewsQuery,
|
||||
useUpdateViewMutation,
|
||||
ViewType,
|
||||
} from '~/generated/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { GET_VIEWS } from '../graphql/queries/getViews';
|
||||
|
||||
export const useViews = ({
|
||||
objectId,
|
||||
onViewCreate,
|
||||
RecoilScopeContext,
|
||||
type,
|
||||
}: {
|
||||
objectId: string;
|
||||
onViewCreate?: (viewId: string) => Promise<void>;
|
||||
RecoilScopeContext: RecoilScopeContext;
|
||||
type: ViewType;
|
||||
}) => {
|
||||
const [currentViewId, setCurrentViewId] = useRecoilScopedState(
|
||||
currentViewIdScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
const [views, setViews] = useRecoilScopedState(
|
||||
viewsScopedState,
|
||||
RecoilScopeContext,
|
||||
);
|
||||
|
||||
const [createViewMutation] = useCreateViewMutation();
|
||||
const [updateViewMutation] = useUpdateViewMutation();
|
||||
const [deleteViewMutation] = useDeleteViewMutation();
|
||||
|
||||
const createView = async (view: View) => {
|
||||
const { data } = await createViewMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...view,
|
||||
objectId,
|
||||
type,
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
|
||||
if (data?.view) await onViewCreate?.(data.view.id);
|
||||
};
|
||||
|
||||
const updateView = async (view: View) => {
|
||||
await updateViewMutation({
|
||||
variables: {
|
||||
data: { name: view.name },
|
||||
where: { id: view.id },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
const deleteView = async (viewId: string) => {
|
||||
await deleteViewMutation({
|
||||
variables: { where: { id: viewId } },
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
const { loading } = useGetViewsQuery({
|
||||
variables: {
|
||||
where: {
|
||||
objectId: { equals: objectId },
|
||||
type: { equals: type },
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const nextViews = data.views.map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
}));
|
||||
|
||||
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
||||
|
||||
if (!nextViews.length) return;
|
||||
|
||||
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createView,
|
||||
deleteView,
|
||||
isFetchingViews: loading,
|
||||
updateView,
|
||||
};
|
||||
};
|
||||
43
front/src/modules/views/scopes/ViewScope.tsx
Normal file
43
front/src/modules/views/scopes/ViewScope.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
|
||||
import { ViewScopeInitEffect } from './init-effect/ViewScopeInitEffect';
|
||||
import { ViewScopeInternalContext } from './scope-internal-context/ViewScopeInternalContext';
|
||||
|
||||
type ViewScopeProps = {
|
||||
children: ReactNode;
|
||||
viewScopeId: string;
|
||||
onViewSortsChange?: (sorts: Sort[]) => void | Promise<void>;
|
||||
onViewFiltersChange?: (filters: Filter[]) => void | Promise<void>;
|
||||
onViewFieldsChange?: (
|
||||
fields: ColumnDefinition<FieldMetadata>[],
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export const ViewScope = ({
|
||||
children,
|
||||
viewScopeId,
|
||||
onViewSortsChange,
|
||||
onViewFiltersChange,
|
||||
onViewFieldsChange,
|
||||
}: ViewScopeProps) => {
|
||||
return (
|
||||
<ViewScopeInternalContext.Provider
|
||||
value={{
|
||||
scopeId: viewScopeId,
|
||||
}}
|
||||
>
|
||||
<ViewScopeInitEffect
|
||||
viewScopeId={viewScopeId}
|
||||
onViewSortsChange={onViewSortsChange}
|
||||
onViewFiltersChange={onViewFiltersChange}
|
||||
onViewFieldsChange={onViewFieldsChange}
|
||||
/>
|
||||
{children}
|
||||
</ViewScopeInternalContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
import { useViewInternalStates } from '@/views/hooks/useViewInternalStates';
|
||||
|
||||
type ViewScopeInitEffectProps = {
|
||||
viewScopeId: string;
|
||||
onViewSortsChange?: (sorts: Sort[]) => void | Promise<void>;
|
||||
onViewFiltersChange?: (filters: Filter[]) => void | Promise<void>;
|
||||
onViewFieldsChange?: (
|
||||
fields: ColumnDefinition<FieldMetadata>[],
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export const ViewScopeInitEffect = ({
|
||||
viewScopeId,
|
||||
onViewSortsChange,
|
||||
onViewFiltersChange,
|
||||
onViewFieldsChange,
|
||||
}: ViewScopeInitEffectProps) => {
|
||||
const { currentViewId } = useView();
|
||||
const {
|
||||
setOnViewSortsChange,
|
||||
setOnViewFieldsChange,
|
||||
setOnViewFiltersChange,
|
||||
} = useViewInternalStates(viewScopeId, currentViewId);
|
||||
|
||||
useEffect(() => {
|
||||
setOnViewSortsChange(onViewSortsChange);
|
||||
setOnViewFiltersChange(onViewFiltersChange);
|
||||
setOnViewFieldsChange(onViewFieldsChange);
|
||||
}, [
|
||||
onViewFieldsChange,
|
||||
onViewFiltersChange,
|
||||
onViewSortsChange,
|
||||
setOnViewFieldsChange,
|
||||
setOnViewFiltersChange,
|
||||
setOnViewSortsChange,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -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>();
|
||||
10
front/src/modules/views/states/availableFieldsScopedState.ts
Normal file
10
front/src/modules/views/states/availableFieldsScopedState.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableFieldsScopedState = createScopedState<
|
||||
ColumnDefinition<FieldMetadata>[]
|
||||
>({
|
||||
key: 'availableFieldsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { FilterDefinition } from '@/ui/data/filter/types/FilterDefinition';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableFiltersScopedState = createScopedState<
|
||||
FilterDefinition[]
|
||||
>({
|
||||
key: 'availableFiltersScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { SortDefinition } from '@/ui/data/sort/types/SortDefinition';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const availableSortsScopedState = createScopedState<SortDefinition[]>({
|
||||
key: 'availableSortsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const currentViewFieldsScopedFamilyState = createScopedFamilyState<
|
||||
ColumnDefinition<FieldMetadata>[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewFieldsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const currentViewFiltersScopedFamilyState = createScopedFamilyState<
|
||||
Filter[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewFiltersScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -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,10 @@
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const currentViewSortsScopedFamilyState = createScopedFamilyState<
|
||||
Sort[],
|
||||
string
|
||||
>({
|
||||
key: 'currentViewSortsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -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 isViewBarExpandedScopedState = createScopedState<boolean>({
|
||||
key: 'isViewBarExpandedScopedState',
|
||||
defaultValue: true,
|
||||
});
|
||||
6
front/src/modules/views/states/noneScopedFamilyState.ts
Normal file
6
front/src/modules/views/states/noneScopedFamilyState.ts
Normal file
@ -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,11 @@
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const onViewFieldsChangeScopedState = createScopedState<
|
||||
| ((fields: ColumnDefinition<FieldMetadata>[]) => void | Promise<void>)
|
||||
| undefined
|
||||
>({
|
||||
key: 'onViewFieldsChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const onViewFiltersChangeScopedState = createScopedState<
|
||||
((filters: Filter[]) => void | Promise<void>) | undefined
|
||||
>({
|
||||
key: 'onViewFiltersChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const onViewSortsChangeScopedState = createScopedState<
|
||||
((sorts: Sort[]) => void | Promise<void>) | undefined
|
||||
>({
|
||||
key: 'onViewSortsChangeScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const savedViewFieldsScopedFamilyState = createScopedFamilyState<
|
||||
ColumnDefinition<FieldMetadata>[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewFieldsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const savedViewFiltersScopedFamilyState = createScopedFamilyState<
|
||||
Filter[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewFiltersScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const savedViewSortsScopedFamilyState = createScopedFamilyState<
|
||||
Sort[],
|
||||
string
|
||||
>({
|
||||
key: 'savedViewSortsScopedFamilyState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { currentViewFiltersScopedFamilyState } from '../currentViewFiltersScopedFamilyState';
|
||||
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
|
||||
|
||||
export const canPersistViewFiltersScopedFamilySelector = selectorFamily({
|
||||
key: 'canPersistFiltersScopedFamilySelector',
|
||||
get:
|
||||
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
|
||||
({ get }) => {
|
||||
if (!viewId) {
|
||||
return;
|
||||
}
|
||||
return !isDeeplyEqual(
|
||||
get(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
),
|
||||
get(
|
||||
currentViewFiltersScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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,22 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { View } from '@/views/types/View';
|
||||
|
||||
import { currentViewIdScopedState } from '../currentViewIdScopedState';
|
||||
|
||||
import { viewsByIdScopedSelector } from './viewsByIdScopedSelector';
|
||||
|
||||
export const currentViewScopedSelector = selectorFamily<
|
||||
View | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'currentViewScopedSelector',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) => {
|
||||
const currentViewId = get(currentViewIdScopedState({ scopeId: scopeId }));
|
||||
return currentViewId
|
||||
? get(viewsByIdScopedSelector(scopeId))[currentViewId]
|
||||
: undefined;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { reduceSortsToOrderBy } from '@/ui/data/sort/utils/helpers';
|
||||
import { SortOrder } from '~/generated/graphql';
|
||||
|
||||
import { currentViewSortsScopedFamilyState } from '../currentViewSortsScopedFamilyState';
|
||||
|
||||
export const currentViewSortsOrderByScopedFamilySelector = selectorFamily({
|
||||
key: 'currentViewSortsOrderByScopedFamilySelector',
|
||||
get:
|
||||
({ viewScopeId, viewId }: { viewScopeId: string; viewId?: string }) =>
|
||||
({ get }) => {
|
||||
if (!viewId) {
|
||||
return;
|
||||
}
|
||||
const orderBy = reduceSortsToOrderBy(
|
||||
get(
|
||||
currentViewSortsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
return orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }];
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
|
||||
import { savedViewFieldsScopedFamilyState } from '../savedViewFieldsScopedFamilyState';
|
||||
|
||||
export const savedViewFieldByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewFieldByKeyScopedFamilySelector',
|
||||
get:
|
||||
({
|
||||
viewScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
viewScopeId: string;
|
||||
viewId: string | undefined;
|
||||
}) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return get(
|
||||
savedViewFieldsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, ColumnDefinition<FieldMetadata>>>(
|
||||
(result, column) => ({ ...result, [column.key]: column }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
|
||||
import { savedViewFiltersScopedFamilyState } from '../savedViewFiltersScopedFamilyState';
|
||||
|
||||
export const savedViewFiltersByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewFiltersByKeyScopedFamilySelector',
|
||||
get:
|
||||
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return get(
|
||||
savedViewFiltersScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, Filter>>(
|
||||
(result, filter) => ({ ...result, [filter.key]: filter }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
|
||||
import { savedViewSortsScopedFamilyState } from '../savedViewSortsScopedFamilyState';
|
||||
|
||||
export const savedViewSortsByKeyScopedFamilySelector = selectorFamily({
|
||||
key: 'savedViewSortsByKeyScopedFamilySelector',
|
||||
get:
|
||||
({ scopeId, viewId }: { scopeId: string; viewId: string | undefined }) =>
|
||||
({ get }) => {
|
||||
if (viewId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return get(
|
||||
savedViewSortsScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: viewId,
|
||||
}),
|
||||
).reduce<Record<string, Sort>>(
|
||||
(result, sort) => ({ ...result, [sort.key]: sort }),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -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,19 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { View } from '@/views/types/View';
|
||||
|
||||
import { viewsScopedState } from '../viewsScopedState';
|
||||
|
||||
export const viewsByIdScopedSelector = selectorFamily<
|
||||
Record<string, View>,
|
||||
string
|
||||
>({
|
||||
key: 'viewsByIdScopedSelector',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) =>
|
||||
get(viewsScopedState({ scopeId: scopeId })).reduce<Record<string, View>>(
|
||||
(result, view) => ({ ...result, [view.id]: view }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const viewEditModeScopedState = createScopedState<
|
||||
'none' | 'edit' | 'create'
|
||||
>({
|
||||
key: 'viewEditModeScopedState',
|
||||
defaultValue: 'none',
|
||||
});
|
||||
6
front/src/modules/views/states/viewObjectIdScopeState.ts
Normal file
6
front/src/modules/views/states/viewObjectIdScopeState.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const viewObjectIdScopeState = createScopedState<string | undefined>({
|
||||
key: 'viewObjectIdScopeState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
7
front/src/modules/views/states/viewTypeScopedState.ts
Normal file
7
front/src/modules/views/states/viewTypeScopedState.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
import { ViewType } from '~/generated/graphql';
|
||||
|
||||
export const viewTypeScopedState = createScopedState<ViewType>({
|
||||
key: 'viewTypeScopedState',
|
||||
defaultValue: ViewType.Table,
|
||||
});
|
||||
8
front/src/modules/views/states/viewsScopedState.ts
Normal file
8
front/src/modules/views/states/viewsScopedState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
import { View } from '../types/View';
|
||||
|
||||
export const viewsScopedState = createScopedState<View[]>({
|
||||
key: 'viewsScopedState',
|
||||
defaultValue: [],
|
||||
});
|
||||
1
front/src/modules/views/types/View.ts
Normal file
1
front/src/modules/views/types/View.ts
Normal file
@ -0,0 +1 @@
|
||||
export type View = { id: string; name: string };
|
||||
11
front/src/modules/views/types/ViewFieldForVisibility.ts
Normal file
11
front/src/modules/views/types/ViewFieldForVisibility.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
|
||||
export type ViewFieldForVisibility = Pick<
|
||||
FieldDefinition<FieldMetadata>,
|
||||
'key' | 'name' | 'Icon' | 'infoTooltipContent'
|
||||
> & {
|
||||
isVisible?: boolean;
|
||||
index: number;
|
||||
size?: number | undefined;
|
||||
};
|
||||
4
front/src/modules/views/types/ViewsHotkeyScope.ts
Normal file
4
front/src/modules/views/types/ViewsHotkeyScope.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum ViewsHotkeyScope {
|
||||
ListDropdown = 'views-list-dropdown',
|
||||
CreateDropdown = 'views-create-dropdown',
|
||||
}
|
||||
Reference in New Issue
Block a user