refactor: add ViewBar and move view components to ui/view-bar (#1495)

Closes #1494
This commit is contained in:
Thaïs
2023-09-08 11:57:16 +02:00
committed by GitHub
parent ccb57c91a3
commit df17da80fc
22 changed files with 325 additions and 376 deletions

View File

@ -3,7 +3,6 @@ import { Meta, StoryObj } from '@storybook/react';
import { EntityBoard } from '@/ui/board/components/EntityBoard'; import { EntityBoard } from '@/ui/board/components/EntityBoard';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { SortOrder } from '~/generated/graphql';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
@ -17,13 +16,7 @@ const meta: Meta<typeof EntityBoard> = {
decorators: [ decorators: [
(Story) => ( (Story) => (
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}> <RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard <HooksCompanyBoard />
orderBy={[
{
createdAt: SortOrder.Asc,
},
]}
/>
<MemoryRouter> <MemoryRouter>
<Story /> <Story />
</MemoryRouter> </MemoryRouter>

View File

@ -5,7 +5,6 @@ import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext'; import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext'; import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { SortOrder } from '~/generated/graphql';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress'; import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress';
@ -19,13 +18,7 @@ const meta: Meta<typeof CompanyBoardCard> = {
decorators: [ decorators: [
(Story) => ( (Story) => (
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}> <RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard <HooksCompanyBoard />
orderBy={[
{
createdAt: SortOrder.Asc,
},
]}
/>
<RecoilScope SpecificContext={BoardColumnRecoilScopeContext}> <RecoilScope SpecificContext={BoardColumnRecoilScopeContext}>
<BoardCardIdContext.Provider value={mockedPipelineProgressData[1].id}> <BoardCardIdContext.Provider value={mockedPipelineProgressData[1].id}>
<MemoryRouter> <MemoryRouter>

View File

@ -10,11 +10,11 @@ import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoi
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState';
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector';
import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause';
import { import {
Pipeline, Pipeline,
PipelineProgressableType, PipelineProgressableType,
PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By,
useGetCompaniesQuery, useGetCompaniesQuery,
useGetPipelineProgressQuery, useGetPipelineProgressQuery,
useGetPipelinesQuery, useGetPipelinesQuery,
@ -25,13 +25,7 @@ import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns'; import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext'; import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
export function HooksCompanyBoard({ export function HooksCompanyBoard() {
orderBy,
}: {
orderBy: PipelineProgresses_Order_By[];
setActionBar?: () => void;
setContextMenu?: () => void;
}) {
const setFieldsDefinitionsState = useSetRecoilState( const setFieldsDefinitionsState = useSetRecoilState(
viewFieldsDefinitionsState, viewFieldsDefinitionsState,
); );
@ -71,6 +65,10 @@ export function HooksCompanyBoard({
?.map((pipelineStage) => pipelineStage.id) ?.map((pipelineStage) => pipelineStage.id)
.flat(); .flat();
const sortsOrderBy = useRecoilScopedValue(
sortsOrderByScopedSelector,
CompanyBoardRecoilScopeContext,
);
const whereFilters = useMemo(() => { const whereFilters = useMemo(() => {
return { return {
AND: [ AND: [
@ -86,7 +84,7 @@ export function HooksCompanyBoard({
useGetPipelineProgressQuery({ useGetPipelineProgressQuery({
variables: { variables: {
where: whereFilters, where: whereFilters,
orderBy, orderBy: sortsOrderBy,
}, },
onCompleted: (data) => { onCompleted: (data) => {
const pipelineProgresses = data?.findManyPipelineProgress || []; const pipelineProgresses = data?.findManyPipelineProgress || [];

View File

@ -12,7 +12,6 @@ import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filte
import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector';
import { useTableViews } from '@/views/hooks/useTableViews'; import { useTableViews } from '@/views/hooks/useTableViews';
import { import {
SortOrder,
UpdateOneCompanyMutationVariables, UpdateOneCompanyMutationVariables,
useGetCompaniesQuery, useGetCompaniesQuery,
useUpdateOneCompanyMutation, useUpdateOneCompanyMutation,
@ -55,16 +54,14 @@ export function CompanyTable() {
getRequestResultKey="companies" getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery} useGetRequest={useGetCompaniesQuery}
getRequestOptimisticEffect={getCompaniesOptimisticEffect} getRequestOptimisticEffect={getCompaniesOptimisticEffect}
orderBy={ orderBy={sortsOrderBy}
sortsOrderBy.length ? sortsOrderBy : [{ createdAt: SortOrder.Desc }]
}
whereFilters={filtersWhere} whereFilters={filtersWhere}
filterDefinitionArray={companiesFilters} filterDefinitionArray={companiesFilters}
setContextMenuEntries={setContextMenuEntries} setContextMenuEntries={setContextMenuEntries}
setActionBarEntries={setActionBarEntries} setActionBarEntries={setActionBarEntries}
/> />
<EntityTable <EntityTable
viewName="All Companies" defaultViewName="All Companies"
availableSorts={availableSorts} availableSorts={availableSorts}
onViewsChange={handleViewsChange} onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit} onViewSubmit={handleViewSubmit}

View File

@ -9,7 +9,7 @@ export function CompanyTableMockMode() {
<> <>
<CompanyTableMockData /> <CompanyTableMockData />
<EntityTable <EntityTable
viewName="All Companies" defaultViewName="All Companies"
availableSorts={availableSorts} availableSorts={availableSorts}
updateEntityMutation={[useUpdateOneCompanyMutation()]} updateEntityMutation={[useUpdateOneCompanyMutation()]}
/> />

View File

@ -12,7 +12,6 @@ import { filtersWhereScopedSelector } from '@/ui/view-bar/states/selectors/filte
import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector'; import { sortsOrderByScopedSelector } from '@/ui/view-bar/states/selectors/sortsOrderByScopedSelector';
import { useTableViews } from '@/views/hooks/useTableViews'; import { useTableViews } from '@/views/hooks/useTableViews';
import { import {
SortOrder,
UpdateOnePersonMutationVariables, UpdateOnePersonMutationVariables,
useGetPeopleQuery, useGetPeopleQuery,
useUpdateOnePersonMutation, useUpdateOnePersonMutation,
@ -54,16 +53,14 @@ export function PeopleTable() {
getRequestResultKey="people" getRequestResultKey="people"
useGetRequest={useGetPeopleQuery} useGetRequest={useGetPeopleQuery}
getRequestOptimisticEffect={getPeopleOptimisticEffect} getRequestOptimisticEffect={getPeopleOptimisticEffect}
orderBy={ orderBy={sortsOrderBy}
sortsOrderBy.length ? sortsOrderBy : [{ createdAt: SortOrder.Desc }]
}
whereFilters={filtersWhere} whereFilters={filtersWhere}
filterDefinitionArray={peopleFilters} filterDefinitionArray={peopleFilters}
setContextMenuEntries={setContextMenuEntries} setContextMenuEntries={setContextMenuEntries}
setActionBarEntries={setActionBarEntries} setActionBarEntries={setActionBarEntries}
/> />
<EntityTable <EntityTable
viewName="All People" defaultViewName="All People"
availableSorts={availableSorts} availableSorts={availableSorts}
onViewsChange={handleViewsChange} onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit} onViewSubmit={handleViewSubmit}

View File

@ -1,10 +1,4 @@
import { import type { ComponentProps, Context, ReactNode } from 'react';
type ComponentProps,
Context,
type ReactNode,
useCallback,
useState,
} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
@ -14,7 +8,7 @@ import { FilterDropdownButton } from '@/ui/view-bar/components/FilterDropdownBut
import { SortDropdownButton } from '@/ui/view-bar/components/SortDropdownButton'; import { SortDropdownButton } from '@/ui/view-bar/components/SortDropdownButton';
import ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails'; import ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails';
import { FiltersHotkeyScope } from '@/ui/view-bar/types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '@/ui/view-bar/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/view-bar/types/interface'; import { SortType } from '@/ui/view-bar/types/interface';
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition'; import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope'; import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
@ -25,7 +19,6 @@ type OwnProps<SortField> = ComponentProps<'div'> & {
viewName: string; viewName: string;
viewIcon?: ReactNode; viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void; onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
context: Context<string | null>; context: Context<string | null>;
}; };
@ -44,33 +37,10 @@ export function BoardHeader<SortField>({
viewName, viewName,
viewIcon, viewIcon,
availableSorts, availableSorts,
onSortsUpdate,
onStageAdd, onStageAdd,
context, context,
...props ...props
}: OwnProps<SortField>) { }: 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 ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<TopBar <TopBar
@ -90,9 +60,7 @@ export function BoardHeader<SortField>({
/> />
<SortDropdownButton<SortField> <SortDropdownButton<SortField>
context={context} context={context}
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []} availableSorts={availableSorts || []}
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/> />
<BoardOptionsDropdown <BoardOptionsDropdown
@ -101,34 +69,8 @@ export function BoardHeader<SortField>({
/> />
</> </>
} }
bottomComponent={ bottomComponent={<ViewBarDetails context={context} />}
<ViewBarDetails
context={context}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {
innerSetSorts([]);
onSortsUpdate?.([]);
}}
/>
}
/> />
</RecoilScope> </RecoilScope>
); );
} }
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;
}

View File

@ -17,10 +17,8 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { SelectedSortType } from '@/ui/view-bar/types/interface';
import { import {
PipelineProgress, PipelineProgress,
PipelineProgressOrderByWithRelationInput,
PipelineStage, PipelineStage,
useUpdateOnePipelineProgressStageMutation, useUpdateOnePipelineProgressStageMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
@ -51,15 +49,11 @@ export function EntityBoard({
onColumnAdd, onColumnAdd,
onColumnDelete, onColumnDelete,
onEditColumnTitle, onEditColumnTitle,
updateSorts,
}: { }: {
boardOptions: BoardOptions; boardOptions: BoardOptions;
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void; onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
onColumnDelete?: (boardColumnId: string) => void; onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void; onEditColumnTitle: (columnId: string, title: string, color: string) => void;
updateSorts: (
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
) => void;
}) { }) {
const [boardColumns] = useRecoilState(boardColumnsState); const [boardColumns] = useRecoilState(boardColumnsState);
const setCardSelected = useSetCardSelected(); const setCardSelected = useSetCardSelected();
@ -140,7 +134,6 @@ export function EntityBoard({
viewName="All opportunities" viewName="All opportunities"
viewIcon={<IconList size={theme.icon.size.md} />} viewIcon={<IconList size={theme.icon.size.md} />}
availableSorts={boardOptions.sorts} availableSorts={boardOptions.sorts}
onSortsUpdate={updateSorts}
onStageAdd={onColumnAdd} onStageAdd={onColumnAdd}
context={CompanyBoardRecoilScopeContext} context={CompanyBoardRecoilScopeContext}
/> />

View File

@ -8,15 +8,16 @@ import {
useListenClickOutsideByClassName, useListenClickOutsideByClassName,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { SortType } from '@/ui/view-bar/types/interface';
import type { View } from '@/ui/view-bar/types/View';
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext'; import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus'; import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus'; import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection'; import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState'; import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
import { TableHeader } from '../table-header/components/TableHeader'; import {
TableHeader,
type TableHeaderProps,
} from '../table-header/components/TableHeader';
import { TableHotkeyScope } from '../types/TableHotkeyScope'; import { TableHotkeyScope } from '../types/TableHotkeyScope';
import { EntityTableBody } from './EntityTableBody'; import { EntityTableBody } from './EntityTableBody';
@ -85,21 +86,22 @@ const StyledTableContainer = styled.div`
`; `;
type OwnProps<SortField> = { type OwnProps<SortField> = {
viewName: string;
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onViewsChange?: (views: View[]) => void;
onViewSubmit?: () => void;
onImport?: () => void;
updateEntityMutation: any; updateEntityMutation: any;
}; } & Pick<
TableHeaderProps<SortField>,
| 'availableSorts'
| 'defaultViewName'
| 'onImport'
| 'onViewsChange'
| 'onViewSubmit'
>;
export function EntityTable<SortField>({ export function EntityTable<SortField>({
viewName,
availableSorts, availableSorts,
defaultViewName,
onImport,
onViewsChange, onViewsChange,
onViewSubmit, onViewSubmit,
onImport,
updateEntityMutation, updateEntityMutation,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const tableBodyRef = useRef<HTMLDivElement>(null); const tableBodyRef = useRef<HTMLDivElement>(null);
@ -139,11 +141,11 @@ export function EntityTable<SortField>({
<StyledTableWithHeader> <StyledTableWithHeader>
<StyledTableContainer ref={tableBodyRef}> <StyledTableContainer ref={tableBodyRef}>
<TableHeader <TableHeader
viewName={viewName} availableSorts={availableSorts ?? []}
availableSorts={availableSorts} defaultViewName={defaultViewName}
onImport={onImport}
onViewsChange={onViewsChange} onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit} onViewSubmit={onViewSubmit}
onImport={onImport}
/> />
<ScrollWrapper> <ScrollWrapper>
<div> <div>

View File

@ -2,6 +2,8 @@ import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import type { View } from '@/ui/view-bar/types/View'; import type { View } from '@/ui/view-bar/types/View';
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
import { TableOptionsDropdownButton } from './TableOptionsDropdownButton'; import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent'; import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
@ -20,7 +22,7 @@ export function TableOptionsDropdown({
<DropdownButton <DropdownButton
buttonComponents={<TableOptionsDropdownButton />} buttonComponents={<TableOptionsDropdownButton />}
dropdownHotkeyScope={customHotkeyScope} dropdownHotkeyScope={customHotkeyScope}
dropdownKey="options" dropdownKey={TableOptionsDropdownKey}
dropdownComponents={ dropdownComponents={
<TableOptionsDropdownContent <TableOptionsDropdownContent
onImport={onImport} onImport={onImport}

View File

@ -1,9 +1,11 @@
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton'; import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
export function TableOptionsDropdownButton() { export function TableOptionsDropdownButton() {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({ const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
key: 'options', key: TableOptionsDropdownKey,
}); });
return ( return (

View File

@ -29,6 +29,7 @@ import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/Tabl
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState'; import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector'; import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector';
import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector'; import { visibleTableColumnsScopedSelector } from '../../states/selectors/visibleTableColumnsScopedSelector';
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection'; import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection';
@ -48,7 +49,9 @@ export function TableOptionsDropdownContent({
}: TableOptionsDropdownButtonProps) { }: TableOptionsDropdownButtonProps) {
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const { closeDropdownButton } = useDropdownButton({ key: 'options' }); const { closeDropdownButton } = useDropdownButton({
key: TableOptionsDropdownKey,
});
const [selectedOption, setSelectedOption] = useState<Option | undefined>( const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined, undefined,

View File

@ -1,151 +1,90 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { TopBar } from '@/ui/top-bar/TopBar';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { FilterDropdownButton } from '@/ui/view-bar/components/FilterDropdownButton'; import { ViewBar, type ViewBarProps } from '@/ui/view-bar/components/ViewBar';
import { SortDropdownButton } from '@/ui/view-bar/components/SortDropdownButton';
import ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState'; import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { canPersistFiltersScopedFamilySelector } from '@/ui/view-bar/states/selectors/canPersistFiltersScopedFamilySelector';
import { canPersistSortsScopedFamilySelector } from '@/ui/view-bar/states/selectors/canPersistSortsScopedFamilySelector';
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { FiltersHotkeyScope } from '@/ui/view-bar/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/view-bar/types/interface';
import type { View } from '@/ui/view-bar/types/View';
import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope';
import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown'; import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown';
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector'; import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
type OwnProps<SortField> = { export type TableHeaderProps<SortField> = {
viewName: string;
availableSorts?: Array<SortType<SortField>>;
onViewsChange?: (views: View[]) => void;
onViewSubmit?: () => void;
onImport?: () => void; onImport?: () => void;
}; } & Pick<
ViewBarProps<SortField>,
'availableSorts' | 'defaultViewName' | 'onViewsChange' | 'onViewSubmit'
>;
export function TableHeader<SortField>({ export function TableHeader<SortField>({
viewName, onImport,
availableSorts,
onViewsChange, onViewsChange,
onViewSubmit, onViewSubmit,
onImport, ...props
}: OwnProps<SortField>) { }: TableHeaderProps<SortField>) {
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue( const currentViewId = useRecoilScopedValue(
currentViewIdScopedState, currentViewIdScopedState,
TableRecoilScopeContext, TableRecoilScopeContext,
); );
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
TableRecoilScopeContext,
);
const canPersistTableColumns = useRecoilValue( const canPersistTableColumns = useRecoilValue(
canPersistTableColumnsScopedFamilySelector([tableScopeId, currentViewId]), canPersistTableColumnsScopedFamilySelector([tableScopeId, currentViewId]),
); );
const canPersistFilters = useRecoilValue( const tableColumns = useRecoilScopedValue(
canPersistFiltersScopedFamilySelector([tableScopeId, currentViewId]), tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedTableColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
); );
const canPersistSorts = useRecoilValue( const handleViewSelect = useRecoilCallback(
canPersistSortsScopedFamilySelector([tableScopeId, currentViewId]), ({ set, snapshot }) =>
async (viewId: string) => {
const savedTableColumns = await snapshot.getPromise(
savedTableColumnsFamilyState(viewId),
);
set(tableColumnsScopedState(tableScopeId), savedTableColumns);
},
[tableScopeId],
); );
const sortSelect = useCallback( const handleViewSubmit = async () => {
(newSort: SelectedSortType<SortField>) => { if (canPersistTableColumns) setSavedTableColumns(tableColumns);
const newSorts = updateSortOrFilterByKey(sorts, newSort);
setSorts(newSorts);
},
[setSorts, sorts],
);
const sortUnselect = useCallback( await onViewSubmit?.();
(sortKey: string) => { };
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
setSorts(newSorts); const OptionsDropdownButton = useCallback(
}, () => (
[setSorts, sorts], <TableOptionsDropdown
onImport={onImport}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
),
[onImport, onViewsChange],
); );
return ( return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<TopBar <ViewBar
leftComponent={ {...props}
<TableViewsDropdownButton canPersistViewFields={canPersistTableColumns}
defaultViewName={viewName} onViewSelect={handleViewSelect}
onViewsChange={onViewsChange} onViewSubmit={handleViewSubmit}
HotkeyScope={ViewsHotkeyScope.ListDropdown} OptionsDropdownButton={OptionsDropdownButton}
/> optionsDropdownKey={TableOptionsDropdownKey}
} scopeContext={TableRecoilScopeContext}
displayBottomBorder={false}
rightComponent={
<>
<FilterDropdownButton
context={TableRecoilScopeContext}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<SortDropdownButton<SortField>
context={TableRecoilScopeContext}
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []}
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<TableOptionsDropdown
onImport={onImport}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
</>
}
bottomComponent={
<ViewBarDetails
canPersistView={
canPersistTableColumns || canPersistFilters || canPersistSorts
}
context={TableRecoilScopeContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => setSorts([])}
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={ViewsHotkeyScope.CreateDropdown}
/>
}
/>
}
/> />
</RecoilScope> </RecoilScope>
); );
} }
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;
}

View File

@ -0,0 +1 @@
export const TableOptionsDropdownKey = 'table-options';

View File

@ -5,15 +5,15 @@ import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/Style
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown } from '@/ui/icon'; import { IconChevronDown } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { sortsScopedState } from '../states/sortsScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '../types/interface'; import { SelectedSortType, SortType } from '../types/interface';
import DropdownButton from './DropdownButton'; import DropdownButton from './DropdownButton';
type OwnProps<SortField> = { export type SortDropdownButtonProps<SortField> = {
isSortSelected: boolean;
onSortSelect: (sort: SelectedSortType<SortField>) => void;
availableSorts: SortType<SortField>[]; availableSorts: SortType<SortField>[];
HotkeyScope: FiltersHotkeyScope; HotkeyScope: FiltersHotkeyScope;
context: Context<string | null>; context: Context<string | null>;
@ -23,21 +23,37 @@ type OwnProps<SortField> = {
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc']; const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField>({ export function SortDropdownButton<SortField>({
isSortSelected, context,
availableSorts, availableSorts,
onSortSelect,
HotkeyScope, HotkeyScope,
}: OwnProps<SortField>) { }: SortDropdownButtonProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] = const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc'); useState<SelectedSortType<SortField>['order']>('asc');
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const isSortSelected = sorts.length > 0;
const onSortItemSelect = useCallback( const onSortItemSelect = useCallback(
(sort: SortType<SortField>) => { (sort: SortType<SortField>) => {
onSortSelect({ ...sort, order: selectedSortDirection }); const newSort = { ...sort, order: selectedSortDirection };
const sortIndex = sorts.findIndex((sort) => sort.key === newSort.key);
const newSorts = [...sorts];
if (sortIndex !== -1) {
newSorts[sortIndex] = newSort;
} else {
newSorts.push(newSort);
}
setSorts(newSorts);
}, },
[onSortSelect, selectedSortDirection], [selectedSortDirection, setSorts, sorts],
); );
const resetState = useCallback(() => { const resetState = useCallback(() => {
@ -46,12 +62,8 @@ export function SortDropdownButton<SortField>({
}, []); }, []);
function handleIsUnfoldedChange(newIsUnfolded: boolean) { function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) { setIsUnfolded(newIsUnfolded);
setIsUnfolded(true); if (!newIsUnfolded) resetState();
} else {
setIsUnfolded(false);
resetState();
}
} }
function handleAddSort(sort: SortType<SortField>) { function handleAddSort(sort: SortType<SortField>) {

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { type Context, useCallback, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
@ -6,7 +6,6 @@ import { Key } from 'ts-key-enum';
import { Button } from '@/ui/button/components/Button'; import { Button } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup'; import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconChevronDown, IconPlus } from '@/ui/icon'; import { IconChevronDown, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -22,101 +21,72 @@ import { canPersistSortsScopedFamilySelector } from '@/ui/view-bar/states/select
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState'; import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: inline-flex; display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)}; margin-right: ${({ theme }) => theme.spacing(2)};
position: relative; position: relative;
`; `;
type TableUpdateViewButtonGroupProps = { export type UpdateViewButtonGroupProps = {
onViewSubmit?: () => void; canPersistViewFields?: boolean;
HotkeyScope: string; HotkeyScope: string;
onViewEditModeChange?: () => void;
onViewSubmit?: () => void | Promise<void>;
scopeContext: Context<string | null>;
}; };
export const TableUpdateViewButtonGroup = ({ export const UpdateViewButtonGroup = ({
onViewSubmit, canPersistViewFields,
HotkeyScope, HotkeyScope,
}: TableUpdateViewButtonGroupProps) => { onViewEditModeChange,
onViewSubmit,
scopeContext,
}: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const recoilScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue( const currentViewId = useRecoilScopedValue(
currentViewIdScopedState, currentViewIdScopedState,
TableRecoilScopeContext, scopeContext,
); );
const tableColumns = useRecoilScopedValue( const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const canPersistColumns = useRecoilValue(
canPersistTableColumnsScopedFamilySelector([tableScopeId, currentViewId]),
);
const filters = useRecoilScopedValue(
filtersScopedState,
TableRecoilScopeContext,
);
const setSavedFilters = useSetRecoilState( const setSavedFilters = useSetRecoilState(
savedFiltersFamilyState(currentViewId), savedFiltersFamilyState(currentViewId),
); );
const canPersistFilters = useRecoilValue( const canPersistFilters = useRecoilValue(
canPersistFiltersScopedFamilySelector([tableScopeId, currentViewId]), canPersistFiltersScopedFamilySelector([recoilScopeId, currentViewId]),
); );
const sorts = useRecoilScopedValue(sortsScopedState, TableRecoilScopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
const setSavedSorts = useSetRecoilState(savedSortsFamilyState(currentViewId)); const setSavedSorts = useSetRecoilState(savedSortsFamilyState(currentViewId));
const canPersistSorts = useRecoilValue( const canPersistSorts = useRecoilValue(
canPersistSortsScopedFamilySelector([tableScopeId, currentViewId]), canPersistSortsScopedFamilySelector([recoilScopeId, currentViewId]),
); );
const setViewEditMode = useSetRecoilState(viewEditModeState); const setViewEditMode = useSetRecoilState(viewEditModeState);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: 'options',
});
const handleArrowDownButtonClick = useCallback(() => { const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen); setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []); }, []);
const handleCreateViewButtonClick = useCallback(() => { const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined }); setViewEditMode({ mode: 'create', viewId: undefined });
openOptionsDropdownButton(); onViewEditModeChange?.();
setIsDropdownOpen(false); setIsDropdownOpen(false);
}, [setViewEditMode, openOptionsDropdownButton]); }, [setViewEditMode, onViewEditModeChange]);
const handleDropdownClose = useCallback(() => { const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false); setIsDropdownOpen(false);
}, []); }, []);
const handleViewSubmit = useCallback(async () => { const handleViewSubmit = async () => {
if (canPersistColumns) setSavedColumns(tableColumns);
if (canPersistFilters) setSavedFilters(filters); if (canPersistFilters) setSavedFilters(filters);
if (canPersistSorts) setSavedSorts(sorts); if (canPersistSorts) setSavedSorts(sorts);
await Promise.resolve(onViewSubmit?.()); await onViewSubmit?.();
}, [ };
canPersistColumns,
canPersistFilters,
canPersistSorts,
filters,
onViewSubmit,
setSavedColumns,
setSavedFilters,
setSavedSorts,
sorts,
tableColumns,
]);
useScopedHotkeys( useScopedHotkeys(
[Key.Enter, Key.Escape], [Key.Enter, Key.Escape],
@ -132,7 +102,7 @@ export const TableUpdateViewButtonGroup = ({
title="Update view" title="Update view"
disabled={ disabled={
!currentViewId || !currentViewId ||
(!canPersistColumns && !canPersistFilters && !canPersistSorts) (!canPersistViewFields && !canPersistFilters && !canPersistSorts)
} }
onClick={handleViewSubmit} onClick={handleViewSubmit}
/> />

View File

@ -0,0 +1,120 @@
import { ComponentProps, type ComponentType, type Context } from 'react';
import { useRecoilValue } from 'recoil';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
import { canPersistFiltersScopedFamilySelector } from '../states/selectors/canPersistFiltersScopedFamilySelector';
import { canPersistSortsScopedFamilySelector } from '../states/selectors/canPersistSortsScopedFamilySelector';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { FilterDropdownButton } from './FilterDropdownButton';
import {
SortDropdownButton,
SortDropdownButtonProps,
} from './SortDropdownButton';
import {
UpdateViewButtonGroup,
UpdateViewButtonGroupProps,
} from './UpdateViewButtonGroup';
import ViewBarDetails from './ViewBarDetails';
import {
ViewsDropdownButton,
ViewsDropdownButtonProps,
} from './ViewsDropdownButton';
export type ViewBarProps<SortField> = ComponentProps<'div'> & {
canPersistViewFields?: boolean;
OptionsDropdownButton: ComponentType;
optionsDropdownKey: string;
scopeContext: Context<string | null>;
} & Pick<
ViewsDropdownButtonProps,
'defaultViewName' | 'onViewsChange' | 'onViewSelect'
> &
Pick<SortDropdownButtonProps<SortField>, 'availableSorts'> &
Pick<UpdateViewButtonGroupProps, 'onViewSubmit'>;
export const ViewBar = <SortField,>({
availableSorts,
canPersistViewFields,
defaultViewName,
onViewsChange,
onViewSelect,
onViewSubmit,
OptionsDropdownButton,
optionsDropdownKey,
scopeContext,
...props
}: ViewBarProps<SortField>) => {
const recoilScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
scopeContext,
);
const canPersistFilters = useRecoilValue(
canPersistFiltersScopedFamilySelector([recoilScopeId, currentViewId]),
);
const canPersistSorts = useRecoilValue(
canPersistSortsScopedFamilySelector([recoilScopeId, currentViewId]),
);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: optionsDropdownKey,
});
return (
<TopBar
{...props}
leftComponent={
<ViewsDropdownButton
defaultViewName={defaultViewName}
onViewEditModeChange={openOptionsDropdownButton}
onViewsChange={onViewsChange}
onViewSelect={onViewSelect}
HotkeyScope={ViewsHotkeyScope.ListDropdown}
scopeContext={scopeContext}
/>
}
displayBottomBorder={false}
rightComponent={
<>
<FilterDropdownButton
context={scopeContext}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<SortDropdownButton<SortField>
context={scopeContext}
availableSorts={availableSorts}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<OptionsDropdownButton />
</>
}
bottomComponent={
<ViewBarDetails
canPersistView={
canPersistViewFields || canPersistFilters || canPersistSorts
}
context={scopeContext}
hasFilterButton
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}
onViewSubmit={onViewSubmit}
HotkeyScope={ViewsHotkeyScope.CreateDropdown}
scopeContext={scopeContext}
/>
}
/>
}
/>
);
};

View File

@ -13,6 +13,7 @@ import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState'; import { filtersScopedState } from '../states/filtersScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState'; import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import { sortsScopedState } from '../states/sortsScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType } from '../types/interface'; import { SelectedSortType } from '../types/interface';
import { getOperandLabelShort } from '../utils/getOperandLabel'; import { getOperandLabelShort } from '../utils/getOperandLabel';
@ -20,12 +21,9 @@ import { getOperandLabelShort } from '../utils/getOperandLabel';
import { FilterDropdownButton } from './FilterDropdownButton'; import { FilterDropdownButton } from './FilterDropdownButton';
import SortOrFilterChip from './SortOrFilterChip'; import SortOrFilterChip from './SortOrFilterChip';
type OwnProps<SortField> = { type OwnProps = {
canPersistView?: boolean; canPersistView?: boolean;
context: Context<string | null>; context: Context<string | null>;
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
onCancelClick: () => void;
hasFilterButton?: boolean; hasFilterButton?: boolean;
rightComponent?: ReactNode; rightComponent?: ReactNode;
}; };
@ -101,24 +99,25 @@ const StyledAddFilterContainer = styled.div`
function ViewBarDetails<SortField>({ function ViewBarDetails<SortField>({
canPersistView, canPersistView,
context, context,
sorts,
onRemoveSort,
onCancelClick,
hasFilterButton = false, hasFilterButton = false,
rightComponent, rightComponent,
}: OwnProps<SortField>) { }: OwnProps) {
const theme = useTheme(); const theme = useTheme();
const [filters, setFilters] = useRecoilScopedState( const [filters, setFilters] = useRecoilScopedState(
filtersScopedState, filtersScopedState,
context, context,
); );
const [availableFilters] = useRecoilScopedState( const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState, availableFiltersScopedState,
context, context,
); );
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const [isViewBarExpanded] = useRecoilScopedState( const [isViewBarExpanded] = useRecoilScopedState(
isViewBarExpandedScopedState, isViewBarExpandedScopedState,
context, context,
@ -139,9 +138,14 @@ function ViewBarDetails<SortField>({
function handleCancelClick() { function handleCancelClick() {
setFilters([]); setFilters([]);
onCancelClick(); setSorts([]);
} }
const handleSortRemove = (sortKey: string) =>
setSorts((previousSorts) =>
previousSorts.filter((sort) => sort.key !== sortKey),
);
const shouldExpandViewBar = const shouldExpandViewBar =
canPersistView || canPersistView ||
((filtersWithDefinition.length || sorts.length) && isViewBarExpanded); ((filtersWithDefinition.length || sorts.length) && isViewBarExpanded);
@ -166,7 +170,7 @@ function ViewBarDetails<SortField>({
: IconArrowNarrowUp : IconArrowNarrowUp
} }
isSort isSort
onRemove={() => onRemoveSort(sort.key)} onRemove={() => handleSortRemove(sort.key)}
/> />
); );
})} })}

View File

@ -1,11 +1,16 @@
import { type MouseEvent, useCallback, useEffect, useState } from 'react'; import {
type Context,
type MouseEvent,
useCallback,
useEffect,
useState,
} from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { import {
IconChevronDown, IconChevronDown,
IconList, IconList,
@ -32,10 +37,6 @@ import type { View } from '@/ui/view-bar/types/View';
import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope'; import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope';
import { assertNotNull } from '~/utils/assert'; import { assertNotNull } from '~/utils/assert';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
const StyledBoldDropdownMenuItemsContainer = styled( const StyledBoldDropdownMenuItemsContainer = styled(
StyledDropdownMenuItemsContainer, StyledDropdownMenuItemsContainer,
)` )`
@ -69,37 +70,39 @@ const StyledViewName = styled.span`
white-space: nowrap; white-space: nowrap;
`; `;
type TableViewsDropdownButtonProps = { export type ViewsDropdownButtonProps = {
defaultViewName: string; defaultViewName: string;
HotkeyScope: ViewsHotkeyScope; HotkeyScope: ViewsHotkeyScope;
onViewsChange?: (views: View[]) => void; onViewEditModeChange?: () => void;
onViewsChange?: (views: View[]) => void | Promise<void>;
onViewSelect?: (viewId: string) => void | Promise<void>;
scopeContext: Context<string | null>;
}; };
export const TableViewsDropdownButton = ({ export const ViewsDropdownButton = ({
defaultViewName, defaultViewName,
HotkeyScope, HotkeyScope,
onViewEditModeChange,
onViewsChange, onViewsChange,
}: TableViewsDropdownButtonProps) => { onViewSelect,
scopeContext,
}: ViewsDropdownButtonProps) => {
const theme = useTheme(); const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const recoilScopeId = useContextScopeId(scopeContext);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: 'options',
});
const [, setCurrentViewId] = useRecoilScopedState( const [, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState, currentViewIdScopedState,
TableRecoilScopeContext, scopeContext,
); );
const currentView = useRecoilScopedValue( const currentView = useRecoilScopedValue(
currentViewScopedSelector, currentViewScopedSelector,
TableRecoilScopeContext, scopeContext,
); );
const [views, setViews] = useRecoilScopedState( const [views, setViews] = useRecoilScopedState(
viewsScopedState, viewsScopedState,
TableRecoilScopeContext, scopeContext,
); );
const setViewEditMode = useSetRecoilState(viewEditModeState); const setViewEditMode = useSetRecoilState(viewEditModeState);
@ -111,9 +114,7 @@ export const TableViewsDropdownButton = ({
const handleViewSelect = useRecoilCallback( const handleViewSelect = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
async (viewId: string) => { async (viewId: string) => {
const savedColumns = await snapshot.getPromise( await onViewSelect?.(viewId);
savedTableColumnsFamilyState(viewId),
);
const savedFilters = await snapshot.getPromise( const savedFilters = await snapshot.getPromise(
savedFiltersFamilyState(viewId), savedFiltersFamilyState(viewId),
); );
@ -121,29 +122,28 @@ export const TableViewsDropdownButton = ({
savedSortsFamilyState(viewId), savedSortsFamilyState(viewId),
); );
set(tableColumnsScopedState(tableScopeId), savedColumns); set(filtersScopedState(recoilScopeId), savedFilters);
set(filtersScopedState(tableScopeId), savedFilters); set(sortsScopedState(recoilScopeId), savedSorts);
set(sortsScopedState(tableScopeId), savedSorts); set(currentViewIdScopedState(recoilScopeId), viewId);
set(currentViewIdScopedState(tableScopeId), viewId);
setIsUnfolded(false); setIsUnfolded(false);
}, },
[tableScopeId], [onViewSelect, recoilScopeId],
); );
const handleAddViewButtonClick = useCallback(() => { const handleAddViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined }); setViewEditMode({ mode: 'create', viewId: undefined });
openOptionsDropdownButton(); onViewEditModeChange?.();
setIsUnfolded(false); setIsUnfolded(false);
}, [setViewEditMode, openOptionsDropdownButton]); }, [setViewEditMode, onViewEditModeChange]);
const handleEditViewButtonClick = useCallback( const handleEditViewButtonClick = useCallback(
(event: MouseEvent<HTMLButtonElement>, viewId: string) => { (event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation(); event.stopPropagation();
setViewEditMode({ mode: 'edit', viewId }); setViewEditMode({ mode: 'edit', viewId });
openOptionsDropdownButton(); onViewEditModeChange?.();
setIsUnfolded(false); setIsUnfolded(false);
}, },
[setViewEditMode, openOptionsDropdownButton], [setViewEditMode, onViewEditModeChange],
); );
const handleDeleteViewButtonClick = useCallback( const handleDeleteViewButtonClick = useCallback(
@ -155,7 +155,7 @@ export const TableViewsDropdownButton = ({
const nextViews = views.filter((view) => view.id !== viewId); const nextViews = views.filter((view) => view.id !== viewId);
setViews(nextViews); setViews(nextViews);
await Promise.resolve(onViewsChange?.(nextViews)); await onViewsChange?.(nextViews);
setIsUnfolded(false); setIsUnfolded(false);
}, },
[currentView?.id, onViewsChange, setCurrentViewId, setViews, views], [currentView?.id, onViewsChange, setCurrentViewId, setViews, views],

View File

@ -1,12 +1,16 @@
import { selectorFamily } from 'recoil'; import { selectorFamily } from 'recoil';
import { SortOrder } from '~/generated/graphql';
import { reduceSortsToOrderBy } from '../../helpers'; import { reduceSortsToOrderBy } from '../../helpers';
import { sortsScopedState } from '../sortsScopedState'; import { sortsScopedState } from '../sortsScopedState';
export const sortsOrderByScopedSelector = selectorFamily({ export const sortsOrderByScopedSelector = selectorFamily({
key: 'sortsOrderByScopedSelector', key: 'sortsOrderByScopedSelector',
get: get:
(param: string) => (scopeId: string) =>
({ get }) => ({ get }) => {
reduceSortsToOrderBy(get(sortsScopedState(param))), const orderBy = reduceSortsToOrderBy(get(sortsScopedState(scopeId)));
return orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }];
},
}); });

View File

@ -113,7 +113,8 @@ export const useViews = ({
const viewToCreate = nextViews.find((nextView) => !viewsById[nextView.id]); const viewToCreate = nextViews.find((nextView) => !viewsById[nextView.id]);
if (viewToCreate) { if (viewToCreate) {
await createView(viewToCreate); await createView(viewToCreate);
return refetch(); await refetch();
return;
} }
const viewToUpdate = nextViews.find( const viewToUpdate = nextViews.find(
@ -122,7 +123,8 @@ export const useViews = ({
); );
if (viewToUpdate) { if (viewToUpdate) {
await updateView(viewToUpdate); await updateView(viewToUpdate);
return refetch(); await refetch();
return;
} }
const nextViewIds = nextViews.map((nextView) => nextView.id); const nextViewIds = nextViews.map((nextView) => nextView.id);
@ -131,7 +133,7 @@ export const useViews = ({
); );
if (viewIdToDelete) await deleteView(viewIdToDelete); if (viewIdToDelete) await deleteView(viewIdToDelete);
return refetch(); await refetch();
}; };
return { handleViewsChange, isFetchingViews: loading }; return { handleViewsChange, isFetchingViews: loading };

View File

@ -1,4 +1,3 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -16,13 +15,7 @@ import { PageBody } from '@/ui/layout/components/PageBody';
import { PageContainer } from '@/ui/layout/components/PageContainer'; import { PageContainer } from '@/ui/layout/components/PageContainer';
import { PageHeader } from '@/ui/layout/components/PageHeader'; import { PageHeader } from '@/ui/layout/components/PageHeader';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { reduceSortsToOrderBy } from '@/ui/view-bar/helpers'; import { useUpdatePipelineStageMutation } from '~/generated/graphql';
import { SelectedSortType } from '@/ui/view-bar/types/interface';
import {
PipelineProgressOrderByWithRelationInput,
SortOrder,
useUpdatePipelineStageMutation,
} from '~/generated/graphql';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
const StyledPageHeader = styled(PageHeader)` const StyledPageHeader = styled(PageHeader)`
@ -33,23 +26,6 @@ const StyledPageHeader = styled(PageHeader)`
export function Opportunities() { export function Opportunities() {
const theme = useTheme(); const theme = useTheme();
const [orderBy, setOrderBy] = useState<
PipelineProgressOrderByWithRelationInput[]
>([{ createdAt: SortOrder.Asc }]);
const updateSorts = useCallback(
(
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
) => {
setOrderBy(
sorts.length
? reduceSortsToOrderBy(sorts)
: [{ createdAt: SortOrder.Asc }],
);
},
[],
);
const { handlePipelineStageAdd, handlePipelineStageDelete } = const { handlePipelineStageAdd, handlePipelineStageDelete } =
usePipelineStages(); usePipelineStages();
@ -91,10 +67,9 @@ export function Opportunities() {
<PageBody> <PageBody>
<BoardOptionsContext.Provider value={opportunitiesBoardOptions}> <BoardOptionsContext.Provider value={opportunitiesBoardOptions}>
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}> <RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard orderBy={orderBy} /> <HooksCompanyBoard />
<EntityBoard <EntityBoard
boardOptions={opportunitiesBoardOptions} boardOptions={opportunitiesBoardOptions}
updateSorts={updateSorts}
onEditColumnTitle={handleEditColumnTitle} onEditColumnTitle={handleEditColumnTitle}
onColumnAdd={handlePipelineStageAdd} onColumnAdd={handlePipelineStageAdd}
onColumnDelete={handlePipelineStageDelete} onColumnDelete={handlePipelineStageDelete}