Feature/filter and sort board (#725)
* Get pipeline progress from stage IDs * Rename hooks file * Addd first amount filter * Add remaining filters * Design fixes * Add filtering on creation date or amount * Fix card updates and creations with the new state management * Keep ordering when dropping a card * Add remainint sorts * Make board header more generic * Move available filters and sorts to board options * Fix decorators for test * Add pipeline stage ids to mock data * Adapt mock data * Linter
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { companyBoardOptions } from '@/companies/components/companyBoardOptions';
|
||||
import { EntityBoard } from '@/pipeline/components/EntityBoard';
|
||||
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
|
||||
import { BoardDecorator } from '~/testing/decorators';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
@ -17,7 +17,12 @@ type Story = StoryObj<typeof EntityBoard>;
|
||||
|
||||
export const OneColumnBoard: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<EntityBoard boardOptions={companyBoardOptions} />,
|
||||
<EntityBoard
|
||||
boardOptions={opportunitiesBoardOptions}
|
||||
updateSorts={() => {
|
||||
return;
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
|
||||
@ -9,10 +9,6 @@ export const pipeline = {
|
||||
name: 'New',
|
||||
index: 0,
|
||||
color: '#B76796',
|
||||
pipelineProgresses: [
|
||||
{ id: 'fe256b39-3ec3-4fe7-8998-b76aa0bfb600' },
|
||||
{ id: '4a886c90-f4f2-4984-8222-882ebbb905d6' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pipeline-stage-2',
|
||||
|
||||
@ -5,7 +5,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import { GET_PIPELINES } from '@/pipeline/queries';
|
||||
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries';
|
||||
import { BoardCardContext } from '@/pipeline/states/BoardCardContext';
|
||||
import { pipelineProgressIdScopedState } from '@/pipeline/states/pipelineProgressIdScopedState';
|
||||
import { selectedBoardCardsState } from '@/pipeline/states/selectedBoardCardsState';
|
||||
@ -108,7 +108,10 @@ export function CompanyBoardCard() {
|
||||
amount: pipelineProgress.amount,
|
||||
closeDate: pipelineProgress.closeDate || null,
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
refetchQueries: [
|
||||
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
|
||||
getOperationName(GET_PIPELINES) ?? '',
|
||||
],
|
||||
});
|
||||
},
|
||||
[updatePipelineProgress],
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
import { Context } from 'react';
|
||||
|
||||
import { FilterDropdownEntitySearchSelect } from '@/ui/filter-n-sort/components/FilterDropdownEntitySearchSelect';
|
||||
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/filter-n-sort/states/filterDropdownSelectedEntityIdScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { TableContext } from '@/ui/table/states/TableContext';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '../queries';
|
||||
|
||||
export function FilterDropdownCompanySearchSelect() {
|
||||
export function FilterDropdownCompanySearchSelect({
|
||||
context,
|
||||
}: {
|
||||
context: Context<string | null>;
|
||||
}) {
|
||||
const filterDropdownSearchInput = useRecoilScopedValue(
|
||||
filterDropdownSearchInputScopedState,
|
||||
TableContext,
|
||||
context,
|
||||
);
|
||||
|
||||
const [filterDropdownSelectedEntityId] = useRecoilScopedState(
|
||||
filterDropdownSelectedEntityIdScopedState,
|
||||
TableContext,
|
||||
context,
|
||||
);
|
||||
|
||||
const usersForSelect = useFilteredSearchCompanyQuery({
|
||||
@ -28,7 +33,7 @@ export function FilterDropdownCompanySearchSelect() {
|
||||
return (
|
||||
<FilterDropdownEntitySearchSelect
|
||||
entitiesForSelect={usersForSelect}
|
||||
context={TableContext}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { useInitializeCompanyBoardFilters } from '@/companies/hooks/useInitializeCompanyBoardFilters';
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import {
|
||||
CompanyForBoard,
|
||||
@ -11,15 +12,30 @@ import { boardState } from '@/pipeline/states/boardState';
|
||||
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
|
||||
import { isBoardLoadedState } from '@/pipeline/states/isBoardLoadedState';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By } from '~/generated/graphql';
|
||||
import {
|
||||
Pipeline,
|
||||
PipelineStage,
|
||||
useGetCompaniesQuery,
|
||||
useGetPipelineProgressQuery,
|
||||
useGetPipelinesQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export function HookCompanyBoard() {
|
||||
import { CompanyBoardContext } from '../states/CompanyBoardContext';
|
||||
|
||||
export function HooksCompanyBoard({
|
||||
availableFilters,
|
||||
orderBy,
|
||||
}: {
|
||||
availableFilters: FilterDefinition[];
|
||||
orderBy: PipelineProgresses_Order_By[];
|
||||
}) {
|
||||
useInitializeCompanyBoardFilters({
|
||||
availableFilters,
|
||||
});
|
||||
const [currentPipeline, setCurrentPipeline] =
|
||||
useRecoilState(currentPipelineState);
|
||||
|
||||
@ -44,29 +60,47 @@ export function HookCompanyBoard() {
|
||||
title: pipelineStage.name,
|
||||
colorCode: pipelineStage.color,
|
||||
index: pipelineStage.index || 0,
|
||||
pipelineProgressIds:
|
||||
pipelineStage.pipelineProgresses?.map(
|
||||
(item) => item.id as string,
|
||||
) || [],
|
||||
pipelineProgressIds: [],
|
||||
})) || [];
|
||||
setBoard(initialBoard);
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
});
|
||||
|
||||
const pipelineProgressIds = currentPipeline?.pipelineStages
|
||||
?.map((pipelineStage: PipelineStage) =>
|
||||
(
|
||||
pipelineStage.pipelineProgresses?.map((item) => item.id as string) || []
|
||||
).flat(),
|
||||
)
|
||||
const pipelineStageIds = currentPipeline?.pipelineStages
|
||||
?.map((pipelineStage) => pipelineStage.id)
|
||||
.flat();
|
||||
|
||||
const filters = useRecoilScopedValue(filtersScopedState, CompanyBoardContext);
|
||||
|
||||
const whereFilters = useMemo(() => {
|
||||
return {
|
||||
AND: [
|
||||
{ pipelineStageId: { in: pipelineStageIds } },
|
||||
...filters.map(turnFilterIntoWhereClause),
|
||||
],
|
||||
};
|
||||
}, [filters, pipelineStageIds]) as any;
|
||||
|
||||
const pipelineProgressesQuery = useGetPipelineProgressQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: { in: pipelineProgressIds },
|
||||
},
|
||||
where: whereFilters,
|
||||
orderBy,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const pipelineProgresses = data?.findManyPipelineProgress || [];
|
||||
setBoard((board) =>
|
||||
board?.map((boardPipelineStage) => ({
|
||||
...boardPipelineStage,
|
||||
pipelineProgressIds: pipelineProgresses
|
||||
.filter(
|
||||
(pipelineProgress) =>
|
||||
pipelineProgress.pipelineStageId ===
|
||||
boardPipelineStage.pipelineStageId,
|
||||
)
|
||||
.map((pipelineProgress) => pipelineProgress.id),
|
||||
})),
|
||||
);
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GET_PIPELINES } from '@/pipeline/queries';
|
||||
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries';
|
||||
import { BoardColumnContext } from '@/pipeline/states/BoardColumnContext';
|
||||
import { boardState } from '@/pipeline/states/boardState';
|
||||
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
|
||||
@ -37,7 +37,10 @@ export function NewCompanyProgressButton() {
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation({
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
refetchQueries: [
|
||||
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
|
||||
getOperationName(GET_PIPELINES) ?? '',
|
||||
],
|
||||
});
|
||||
|
||||
const handleEntitySelect = useCallback(
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
|
||||
import { NewCompanyProgressButton } from '@/companies/components/NewCompanyProgressButton';
|
||||
import { BoardOptions } from '@/pipeline/types/BoardOptions';
|
||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||
|
||||
export const companyBoardOptions: BoardOptions = {
|
||||
newCardComponent: (
|
||||
<RecoilScope>
|
||||
<NewCompanyProgressButton />
|
||||
</RecoilScope>
|
||||
),
|
||||
cardComponent: <CompanyBoardCard />,
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext';
|
||||
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
export function useInitializeCompanyBoardFilters({
|
||||
availableFilters,
|
||||
}: {
|
||||
availableFilters: FilterDefinition[];
|
||||
}) {
|
||||
const [, setAvailableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
CompanyBoardContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableFilters(availableFilters);
|
||||
}, [setAvailableFilters, availableFilters]);
|
||||
}
|
||||
@ -1,10 +1,18 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
import { IconList } from '@tabler/icons-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext';
|
||||
import { BoardHeader } from '@/ui/board/components/BoardHeader';
|
||||
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
PipelineProgress,
|
||||
PipelineProgressOrderByWithRelationInput,
|
||||
PipelineStage,
|
||||
useUpdateOnePipelineProgressStageMutation,
|
||||
} from '~/generated/graphql';
|
||||
@ -13,14 +21,31 @@ import {
|
||||
getOptimisticlyUpdatedBoard,
|
||||
StyledBoard,
|
||||
} from '../../ui/board/components/Board';
|
||||
import { GET_PIPELINE_PROGRESS } from '../queries';
|
||||
import { BoardColumnContext } from '../states/BoardColumnContext';
|
||||
import { boardState } from '../states/boardState';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
import { EntityBoardColumn } from './EntityBoardColumn';
|
||||
|
||||
export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
|
||||
const StyledBoardWithHeader = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function EntityBoard({
|
||||
boardOptions,
|
||||
updateSorts,
|
||||
}: {
|
||||
boardOptions: BoardOptions;
|
||||
updateSorts: (
|
||||
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
|
||||
) => void;
|
||||
}) {
|
||||
const [board, setBoard] = useRecoilState(boardState);
|
||||
const theme = useTheme();
|
||||
const [updatePipelineProgressStage] =
|
||||
useUpdateOnePipelineProgressStageMutation();
|
||||
|
||||
@ -34,6 +59,7 @@ export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
|
||||
id: pipelineProgressId,
|
||||
pipelineStageId,
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_PIPELINE_PROGRESS) ?? ''],
|
||||
});
|
||||
},
|
||||
[updatePipelineProgressStage],
|
||||
@ -69,18 +95,27 @@ export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
|
||||
: [];
|
||||
|
||||
return (board?.length ?? 0) > 0 ? (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{sortedBoard.map((column) => (
|
||||
<RecoilScope
|
||||
SpecificContext={BoardColumnContext}
|
||||
key={column.pipelineStageId}
|
||||
>
|
||||
<EntityBoardColumn boardOptions={boardOptions} column={column} />
|
||||
</RecoilScope>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
<StyledBoardWithHeader>
|
||||
<BoardHeader
|
||||
viewName="All opportunities"
|
||||
viewIcon={<IconList size={theme.icon.size.md} />}
|
||||
availableSorts={boardOptions.sorts}
|
||||
onSortsUpdate={updateSorts}
|
||||
context={CompanyBoardContext}
|
||||
/>
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{sortedBoard.map((column) => (
|
||||
<RecoilScope
|
||||
SpecificContext={BoardColumnContext}
|
||||
key={column.pipelineStageId}
|
||||
>
|
||||
<EntityBoardColumn boardOptions={boardOptions} column={column} />
|
||||
</RecoilScope>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
</StyledBoardWithHeader>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import {
|
||||
PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By,
|
||||
SortOrder as Order_By,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type PipelineProgressesSelectedSortType =
|
||||
SelectedSortType<PipelineProgresses_Order_By>;
|
||||
|
||||
export const GET_PIPELINES = gql`
|
||||
query GetPipelines($where: PipelineWhereInput) {
|
||||
findManyPipeline(where: $where) {
|
||||
@ -11,18 +20,19 @@ export const GET_PIPELINES = gql`
|
||||
name
|
||||
color
|
||||
index
|
||||
pipelineProgresses {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PIPELINE_PROGRESS = gql`
|
||||
query GetPipelineProgress($where: PipelineProgressWhereInput) {
|
||||
findManyPipelineProgress(where: $where, orderBy: { createdAt: asc }) {
|
||||
query GetPipelineProgress(
|
||||
$where: PipelineProgressWhereInput
|
||||
$orderBy: [PipelineProgressOrderByWithRelationInput!]
|
||||
) {
|
||||
findManyPipelineProgress(where: $where, orderBy: $orderBy) {
|
||||
id
|
||||
pipelineStageId
|
||||
progressableType
|
||||
progressableId
|
||||
amount
|
||||
@ -83,3 +93,9 @@ export const ADD_ENTITY_TO_PIPELINE = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const defaultPipelineProgressOrderBy: PipelineProgresses_Order_By[] = [
|
||||
{
|
||||
createdAt: Order_By.Asc,
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { FilterDefinitionByEntity } from '@/ui/filter-n-sort/types/FilterDefinitionByEntity';
|
||||
import { SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { PipelineProgress } from '~/generated/graphql';
|
||||
import { PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By } from '~/generated/graphql';
|
||||
|
||||
export type BoardOptions = {
|
||||
newCardComponent: React.ReactNode;
|
||||
cardComponent: React.ReactNode;
|
||||
filters: FilterDefinitionByEntity<PipelineProgress>[];
|
||||
sorts: Array<SortType<PipelineProgresses_Order_By>>;
|
||||
};
|
||||
|
||||
131
front/src/modules/ui/board/components/BoardHeader.tsx
Normal file
131
front/src/modules/ui/board/components/BoardHeader.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { Context, ReactNode, useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
|
||||
type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
viewIcon?: ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
context: Context<string | null>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTableHeader = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 40px;
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
& > svg {
|
||||
font-size: ${({ theme }) => theme.icon.size.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledViewSection = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledFilters = styled.div`
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
export function BoardHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onSortsUpdate,
|
||||
context,
|
||||
}: OwnProps<SortField>) {
|
||||
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
|
||||
[],
|
||||
);
|
||||
|
||||
const sortSelect = useCallback(
|
||||
(newSort: SelectedSortType<SortField>) => {
|
||||
const newSorts = updateSortOrFilterByKey(sorts, newSort);
|
||||
innerSetSorts(newSorts);
|
||||
onSortsUpdate && onSortsUpdate(newSorts);
|
||||
},
|
||||
[onSortsUpdate, sorts],
|
||||
);
|
||||
|
||||
const sortUnselect = useCallback(
|
||||
(sortKey: string) => {
|
||||
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
|
||||
innerSetSorts(newSorts);
|
||||
onSortsUpdate && onSortsUpdate(newSorts);
|
||||
},
|
||||
[onSortsUpdate, sorts],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTableHeader>
|
||||
<StyledViewSection>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</StyledViewSection>
|
||||
<StyledFilters>
|
||||
<FilterDropdownButton
|
||||
context={context}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
</StyledFilters>
|
||||
</StyledTableHeader>
|
||||
<SortAndFilterBar
|
||||
context={context}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
innerSetSorts([]);
|
||||
onSortsUpdate && onSortsUpdate([]);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function updateSortOrFilterByKey<SortOrFilter extends { key: string }>(
|
||||
sorts: Readonly<SortOrFilter[]>,
|
||||
newSort: SortOrFilter,
|
||||
): SortOrFilter[] {
|
||||
const newSorts = [...sorts];
|
||||
const existingSortIndex = sorts.findIndex((sort) => sort.key === newSort.key);
|
||||
|
||||
if (existingSortIndex !== -1) {
|
||||
newSorts[existingSortIndex] = newSort;
|
||||
} else {
|
||||
newSorts.push(newSort);
|
||||
}
|
||||
|
||||
return newSorts;
|
||||
}
|
||||
Reference in New Issue
Block a user