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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { SortOrder } from '~/generated/graphql';
import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
@ -17,13 +16,7 @@ const meta: Meta<typeof EntityBoard> = {
decorators: [
(Story) => (
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard
orderBy={[
{
createdAt: SortOrder.Asc,
},
]}
/>
<HooksCompanyBoard />
<MemoryRouter>
<Story />
</MemoryRouter>

View File

@ -5,7 +5,6 @@ import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
import { BoardCardIdContext } from '@/ui/board/contexts/BoardCardIdContext';
import { BoardColumnRecoilScopeContext } from '@/ui/board/states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { SortOrder } from '~/generated/graphql';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedPipelineProgressData } from '~/testing/mock-data/pipeline-progress';
@ -19,13 +18,7 @@ const meta: Meta<typeof CompanyBoardCard> = {
decorators: [
(Story) => (
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
<HooksCompanyBoard
orderBy={[
{
createdAt: SortOrder.Asc,
},
]}
/>
<HooksCompanyBoard />
<RecoilScope SpecificContext={BoardColumnRecoilScopeContext}>
<BoardCardIdContext.Provider value={mockedPipelineProgressData[1].id}>
<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 { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState';
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 {
Pipeline,
PipelineProgressableType,
PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By,
useGetCompaniesQuery,
useGetPipelineProgressQuery,
useGetPipelinesQuery,
@ -25,13 +25,7 @@ import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
import { CompanyBoardRecoilScopeContext } from '../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
export function HooksCompanyBoard({
orderBy,
}: {
orderBy: PipelineProgresses_Order_By[];
setActionBar?: () => void;
setContextMenu?: () => void;
}) {
export function HooksCompanyBoard() {
const setFieldsDefinitionsState = useSetRecoilState(
viewFieldsDefinitionsState,
);
@ -71,6 +65,10 @@ export function HooksCompanyBoard({
?.map((pipelineStage) => pipelineStage.id)
.flat();
const sortsOrderBy = useRecoilScopedValue(
sortsOrderByScopedSelector,
CompanyBoardRecoilScopeContext,
);
const whereFilters = useMemo(() => {
return {
AND: [
@ -86,7 +84,7 @@ export function HooksCompanyBoard({
useGetPipelineProgressQuery({
variables: {
where: whereFilters,
orderBy,
orderBy: sortsOrderBy,
},
onCompleted: (data) => {
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 { useTableViews } from '@/views/hooks/useTableViews';
import {
SortOrder,
UpdateOneCompanyMutationVariables,
useGetCompaniesQuery,
useUpdateOneCompanyMutation,
@ -55,16 +54,14 @@ export function CompanyTable() {
getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery}
getRequestOptimisticEffect={getCompaniesOptimisticEffect}
orderBy={
sortsOrderBy.length ? sortsOrderBy : [{ createdAt: SortOrder.Desc }]
}
orderBy={sortsOrderBy}
whereFilters={filtersWhere}
filterDefinitionArray={companiesFilters}
setContextMenuEntries={setContextMenuEntries}
setActionBarEntries={setActionBarEntries}
/>
<EntityTable
viewName="All Companies"
defaultViewName="All Companies"
availableSorts={availableSorts}
onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit}

View File

@ -9,7 +9,7 @@ export function CompanyTableMockMode() {
<>
<CompanyTableMockData />
<EntityTable
viewName="All Companies"
defaultViewName="All Companies"
availableSorts={availableSorts}
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 { useTableViews } from '@/views/hooks/useTableViews';
import {
SortOrder,
UpdateOnePersonMutationVariables,
useGetPeopleQuery,
useUpdateOnePersonMutation,
@ -54,16 +53,14 @@ export function PeopleTable() {
getRequestResultKey="people"
useGetRequest={useGetPeopleQuery}
getRequestOptimisticEffect={getPeopleOptimisticEffect}
orderBy={
sortsOrderBy.length ? sortsOrderBy : [{ createdAt: SortOrder.Desc }]
}
orderBy={sortsOrderBy}
whereFilters={filtersWhere}
filterDefinitionArray={peopleFilters}
setContextMenuEntries={setContextMenuEntries}
setActionBarEntries={setActionBarEntries}
/>
<EntityTable
viewName="All People"
defaultViewName="All People"
availableSorts={availableSorts}
onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit}

View File

@ -1,10 +1,4 @@
import {
type ComponentProps,
Context,
type ReactNode,
useCallback,
useState,
} from 'react';
import type { ComponentProps, Context, ReactNode } from 'react';
import styled from '@emotion/styled';
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 ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails';
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 { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
@ -25,7 +19,6 @@ type OwnProps<SortField> = ComponentProps<'div'> & {
viewName: string;
viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
context: Context<string | null>;
};
@ -44,33 +37,10 @@ export function BoardHeader<SortField>({
viewName,
viewIcon,
availableSorts,
onSortsUpdate,
onStageAdd,
context,
...props
}: 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 (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<TopBar
@ -90,9 +60,7 @@ export function BoardHeader<SortField>({
/>
<SortDropdownButton<SortField>
context={context}
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []}
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/>
<BoardOptionsDropdown
@ -101,34 +69,8 @@ export function BoardHeader<SortField>({
/>
</>
}
bottomComponent={
<ViewBarDetails
context={context}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {
innerSetSorts([]);
onSortsUpdate?.([]);
}}
/>
}
bottomComponent={<ViewBarDetails context={context} />}
/>
</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 { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { SelectedSortType } from '@/ui/view-bar/types/interface';
import {
PipelineProgress,
PipelineProgressOrderByWithRelationInput,
PipelineStage,
useUpdateOnePipelineProgressStageMutation,
} from '~/generated/graphql';
@ -51,15 +49,11 @@ export function EntityBoard({
onColumnAdd,
onColumnDelete,
onEditColumnTitle,
updateSorts,
}: {
boardOptions: BoardOptions;
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
onColumnDelete?: (boardColumnId: string) => void;
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
updateSorts: (
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
) => void;
}) {
const [boardColumns] = useRecoilState(boardColumnsState);
const setCardSelected = useSetCardSelected();
@ -140,7 +134,6 @@ export function EntityBoard({
viewName="All opportunities"
viewIcon={<IconList size={theme.icon.size.md} />}
availableSorts={boardOptions.sorts}
onSortsUpdate={updateSorts}
onStageAdd={onColumnAdd}
context={CompanyBoardRecoilScopeContext}
/>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,151 +1,90 @@
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 { TopBar } from '@/ui/top-bar/TopBar';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
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 { FilterDropdownButton } from '@/ui/view-bar/components/FilterDropdownButton';
import { SortDropdownButton } from '@/ui/view-bar/components/SortDropdownButton';
import ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails';
import { ViewBar, type ViewBarProps } from '@/ui/view-bar/components/ViewBar';
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 { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState';
import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector';
import { tableColumnsScopedState } from '../../states/tableColumnsScopedState';
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
type OwnProps<SortField> = {
viewName: string;
availableSorts?: Array<SortType<SortField>>;
onViewsChange?: (views: View[]) => void;
onViewSubmit?: () => void;
export type TableHeaderProps<SortField> = {
onImport?: () => void;
};
} & Pick<
ViewBarProps<SortField>,
'availableSorts' | 'defaultViewName' | 'onViewsChange' | 'onViewSubmit'
>;
export function TableHeader<SortField>({
viewName,
availableSorts,
onImport,
onViewsChange,
onViewSubmit,
onImport,
}: OwnProps<SortField>) {
...props
}: TableHeaderProps<SortField>) {
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
TableRecoilScopeContext,
);
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
TableRecoilScopeContext,
);
const canPersistTableColumns = useRecoilValue(
canPersistTableColumnsScopedFamilySelector([tableScopeId, currentViewId]),
);
const canPersistFilters = useRecoilValue(
canPersistFiltersScopedFamilySelector([tableScopeId, currentViewId]),
const tableColumns = useRecoilScopedValue(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedTableColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const canPersistSorts = useRecoilValue(
canPersistSortsScopedFamilySelector([tableScopeId, currentViewId]),
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const savedTableColumns = await snapshot.getPromise(
savedTableColumnsFamilyState(viewId),
);
set(tableColumnsScopedState(tableScopeId), savedTableColumns);
},
[tableScopeId],
);
const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort);
setSorts(newSorts);
},
[setSorts, sorts],
);
const handleViewSubmit = async () => {
if (canPersistTableColumns) setSavedTableColumns(tableColumns);
const sortUnselect = useCallback(
(sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
setSorts(newSorts);
},
[setSorts, sorts],
await onViewSubmit?.();
};
const OptionsDropdownButton = useCallback(
() => (
<TableOptionsDropdown
onImport={onImport}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
),
[onImport, onViewsChange],
);
return (
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<TopBar
leftComponent={
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
HotkeyScope={ViewsHotkeyScope.ListDropdown}
/>
}
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}
/>
}
/>
}
<ViewBar
{...props}
canPersistViewFields={canPersistTableColumns}
onViewSelect={handleViewSelect}
onViewSubmit={handleViewSubmit}
OptionsDropdownButton={OptionsDropdownButton}
optionsDropdownKey={TableOptionsDropdownKey}
scopeContext={TableRecoilScopeContext}
/>
</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 { IconChevronDown } from '@/ui/icon';
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 { SelectedSortType, SortType } from '../types/interface';
import DropdownButton from './DropdownButton';
type OwnProps<SortField> = {
isSortSelected: boolean;
onSortSelect: (sort: SelectedSortType<SortField>) => void;
export type SortDropdownButtonProps<SortField> = {
availableSorts: SortType<SortField>[];
HotkeyScope: FiltersHotkeyScope;
context: Context<string | null>;
@ -23,21 +23,37 @@ type OwnProps<SortField> = {
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField>({
isSortSelected,
context,
availableSorts,
onSortSelect,
HotkeyScope,
}: OwnProps<SortField>) {
}: SortDropdownButtonProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc');
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const isSortSelected = sorts.length > 0;
const onSortItemSelect = useCallback(
(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(() => {
@ -46,12 +62,8 @@ export function SortDropdownButton<SortField>({
}, []);
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setIsUnfolded(true);
} else {
setIsUnfolded(false);
resetState();
}
setIsUnfolded(newIsUnfolded);
if (!newIsUnfolded) resetState();
}
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 { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
@ -6,7 +6,6 @@ import { Key } from 'ts-key-enum';
import { Button } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconChevronDown, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
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 { 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`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
type TableUpdateViewButtonGroupProps = {
onViewSubmit?: () => void;
export type UpdateViewButtonGroupProps = {
canPersistViewFields?: boolean;
HotkeyScope: string;
onViewEditModeChange?: () => void;
onViewSubmit?: () => void | Promise<void>;
scopeContext: Context<string | null>;
};
export const TableUpdateViewButtonGroup = ({
onViewSubmit,
export const UpdateViewButtonGroup = ({
canPersistViewFields,
HotkeyScope,
}: TableUpdateViewButtonGroupProps) => {
onViewEditModeChange,
onViewSubmit,
scopeContext,
}: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const recoilScopeId = useContextScopeId(scopeContext);
const currentViewId = useRecoilScopedValue(
currentViewIdScopedState,
TableRecoilScopeContext,
scopeContext,
);
const tableColumns = useRecoilScopedValue(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedColumns = useSetRecoilState(
savedTableColumnsFamilyState(currentViewId),
);
const canPersistColumns = useRecoilValue(
canPersistTableColumnsScopedFamilySelector([tableScopeId, currentViewId]),
);
const filters = useRecoilScopedValue(
filtersScopedState,
TableRecoilScopeContext,
);
const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
const setSavedFilters = useSetRecoilState(
savedFiltersFamilyState(currentViewId),
);
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 canPersistSorts = useRecoilValue(
canPersistSortsScopedFamilySelector([tableScopeId, currentViewId]),
canPersistSortsScopedFamilySelector([recoilScopeId, currentViewId]),
);
const setViewEditMode = useSetRecoilState(viewEditModeState);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: 'options',
});
const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []);
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined });
openOptionsDropdownButton();
onViewEditModeChange?.();
setIsDropdownOpen(false);
}, [setViewEditMode, openOptionsDropdownButton]);
}, [setViewEditMode, onViewEditModeChange]);
const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const handleViewSubmit = useCallback(async () => {
if (canPersistColumns) setSavedColumns(tableColumns);
const handleViewSubmit = async () => {
if (canPersistFilters) setSavedFilters(filters);
if (canPersistSorts) setSavedSorts(sorts);
await Promise.resolve(onViewSubmit?.());
}, [
canPersistColumns,
canPersistFilters,
canPersistSorts,
filters,
onViewSubmit,
setSavedColumns,
setSavedFilters,
setSavedSorts,
sorts,
tableColumns,
]);
await onViewSubmit?.();
};
useScopedHotkeys(
[Key.Enter, Key.Escape],
@ -132,7 +102,7 @@ export const TableUpdateViewButtonGroup = ({
title="Update view"
disabled={
!currentViewId ||
(!canPersistColumns && !canPersistFilters && !canPersistSorts)
(!canPersistViewFields && !canPersistFilters && !canPersistSorts)
}
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 { filtersScopedState } from '../states/filtersScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import { sortsScopedState } from '../states/sortsScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType } from '../types/interface';
import { getOperandLabelShort } from '../utils/getOperandLabel';
@ -20,12 +21,9 @@ import { getOperandLabelShort } from '../utils/getOperandLabel';
import { FilterDropdownButton } from './FilterDropdownButton';
import SortOrFilterChip from './SortOrFilterChip';
type OwnProps<SortField> = {
type OwnProps = {
canPersistView?: boolean;
context: Context<string | null>;
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
onCancelClick: () => void;
hasFilterButton?: boolean;
rightComponent?: ReactNode;
};
@ -101,24 +99,25 @@ const StyledAddFilterContainer = styled.div`
function ViewBarDetails<SortField>({
canPersistView,
context,
sorts,
onRemoveSort,
onCancelClick,
hasFilterButton = false,
rightComponent,
}: OwnProps<SortField>) {
}: OwnProps) {
const theme = useTheme();
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
context,
);
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortsScopedState,
context,
);
const [isViewBarExpanded] = useRecoilScopedState(
isViewBarExpandedScopedState,
context,
@ -139,9 +138,14 @@ function ViewBarDetails<SortField>({
function handleCancelClick() {
setFilters([]);
onCancelClick();
setSorts([]);
}
const handleSortRemove = (sortKey: string) =>
setSorts((previousSorts) =>
previousSorts.filter((sort) => sort.key !== sortKey),
);
const shouldExpandViewBar =
canPersistView ||
((filtersWithDefinition.length || sorts.length) && isViewBarExpanded);
@ -166,7 +170,7 @@ function ViewBarDetails<SortField>({
: IconArrowNarrowUp
}
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 styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import {
IconChevronDown,
IconList,
@ -32,10 +37,6 @@ import type { View } from '@/ui/view-bar/types/View';
import { ViewsHotkeyScope } from '@/ui/view-bar/types/ViewsHotkeyScope';
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(
StyledDropdownMenuItemsContainer,
)`
@ -69,37 +70,39 @@ const StyledViewName = styled.span`
white-space: nowrap;
`;
type TableViewsDropdownButtonProps = {
export type ViewsDropdownButtonProps = {
defaultViewName: string;
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,
HotkeyScope,
onViewEditModeChange,
onViewsChange,
}: TableViewsDropdownButtonProps) => {
onViewSelect,
scopeContext,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: 'options',
});
const recoilScopeId = useContextScopeId(scopeContext);
const [, setCurrentViewId] = useRecoilScopedState(
currentViewIdScopedState,
TableRecoilScopeContext,
scopeContext,
);
const currentView = useRecoilScopedValue(
currentViewScopedSelector,
TableRecoilScopeContext,
scopeContext,
);
const [views, setViews] = useRecoilScopedState(
viewsScopedState,
TableRecoilScopeContext,
scopeContext,
);
const setViewEditMode = useSetRecoilState(viewEditModeState);
@ -111,9 +114,7 @@ export const TableViewsDropdownButton = ({
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const savedColumns = await snapshot.getPromise(
savedTableColumnsFamilyState(viewId),
);
await onViewSelect?.(viewId);
const savedFilters = await snapshot.getPromise(
savedFiltersFamilyState(viewId),
);
@ -121,29 +122,28 @@ export const TableViewsDropdownButton = ({
savedSortsFamilyState(viewId),
);
set(tableColumnsScopedState(tableScopeId), savedColumns);
set(filtersScopedState(tableScopeId), savedFilters);
set(sortsScopedState(tableScopeId), savedSorts);
set(currentViewIdScopedState(tableScopeId), viewId);
set(filtersScopedState(recoilScopeId), savedFilters);
set(sortsScopedState(recoilScopeId), savedSorts);
set(currentViewIdScopedState(recoilScopeId), viewId);
setIsUnfolded(false);
},
[tableScopeId],
[onViewSelect, recoilScopeId],
);
const handleAddViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined });
openOptionsDropdownButton();
onViewEditModeChange?.();
setIsUnfolded(false);
}, [setViewEditMode, openOptionsDropdownButton]);
}, [setViewEditMode, onViewEditModeChange]);
const handleEditViewButtonClick = useCallback(
(event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation();
setViewEditMode({ mode: 'edit', viewId });
openOptionsDropdownButton();
onViewEditModeChange?.();
setIsUnfolded(false);
},
[setViewEditMode, openOptionsDropdownButton],
[setViewEditMode, onViewEditModeChange],
);
const handleDeleteViewButtonClick = useCallback(
@ -155,7 +155,7 @@ export const TableViewsDropdownButton = ({
const nextViews = views.filter((view) => view.id !== viewId);
setViews(nextViews);
await Promise.resolve(onViewsChange?.(nextViews));
await onViewsChange?.(nextViews);
setIsUnfolded(false);
},
[currentView?.id, onViewsChange, setCurrentViewId, setViews, views],

View File

@ -1,12 +1,16 @@
import { selectorFamily } from 'recoil';
import { SortOrder } from '~/generated/graphql';
import { reduceSortsToOrderBy } from '../../helpers';
import { sortsScopedState } from '../sortsScopedState';
export const sortsOrderByScopedSelector = selectorFamily({
key: 'sortsOrderByScopedSelector',
get:
(param: string) =>
({ get }) =>
reduceSortsToOrderBy(get(sortsScopedState(param))),
(scopeId: string) =>
({ get }) => {
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]);
if (viewToCreate) {
await createView(viewToCreate);
return refetch();
await refetch();
return;
}
const viewToUpdate = nextViews.find(
@ -122,7 +123,8 @@ export const useViews = ({
);
if (viewToUpdate) {
await updateView(viewToUpdate);
return refetch();
await refetch();
return;
}
const nextViewIds = nextViews.map((nextView) => nextView.id);
@ -131,7 +133,7 @@ export const useViews = ({
);
if (viewIdToDelete) await deleteView(viewIdToDelete);
return refetch();
await refetch();
};
return { handleViewsChange, isFetchingViews: loading };

View File

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