New view picker (#4610)

* Implement new view picker

* Complete feature

* Fixes according to review
This commit is contained in:
Charles Bochet
2024-03-22 15:04:17 +01:00
committed by GitHub
parent d876b40056
commit 4a493b6ecf
61 changed files with 1216 additions and 422 deletions

View File

@ -1,33 +1,24 @@
import { useEffect } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useSetRecoilState } from 'recoil';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
export const FilterQueryParamsEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams, viewIdQueryParam } =
export const QueryParamsFiltersEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams } =
useViewFromQueryParams();
const { unsavedToUpsertViewFiltersState, currentViewIdState } =
useViewStates();
const { unsavedToUpsertViewFiltersState } = useViewStates();
const setUnsavedViewFilter = useSetRecoilState(
unsavedToUpsertViewFiltersState,
);
const setCurrentViewId = useSetRecoilState(currentViewIdState);
const { resetCurrentView } = useResetCurrentView();
useEffect(() => {
if (isUndefined(viewIdQueryParam) || !viewIdQueryParam) {
if (!hasFiltersQueryParams) {
return;
}
setCurrentViewId(viewIdQueryParam);
}, [getFiltersFromQueryParams, setCurrentViewId, viewIdQueryParam]);
useEffect(() => {
if (!hasFiltersQueryParams) return;
getFiltersFromQueryParams().then((filtersFromParams) => {
if (Array.isArray(filtersFromParams)) {
setUnsavedViewFilter(filtersFromParams);
@ -44,5 +35,5 @@ export const FilterQueryParamsEffect = () => {
setUnsavedViewFilter,
]);
return null;
return <></>;
};

View File

@ -0,0 +1,38 @@
import { useEffect } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useRecoilState } from 'recoil';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined';
export const QueryParamsViewIdEffect = () => {
const { getFiltersFromQueryParams, viewIdQueryParam } =
useViewFromQueryParams();
const { currentViewIdState } = useViewStates();
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
const { viewsOnCurrentObject } = useGetCurrentView();
useEffect(() => {
const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX');
if (isUndefined(viewIdQueryParam) && isDefined(indexView)) {
setCurrentViewId(indexView.id);
return;
}
if (isDefined(viewIdQueryParam)) {
setCurrentViewId(viewIdQueryParam);
}
}, [
currentViewId,
getFiltersFromQueryParams,
setCurrentViewId,
viewIdQueryParam,
viewsOnCurrentObject,
]);
return <></>;
};

View File

@ -7,14 +7,18 @@ import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { UPDATE_VIEW_DROPDOWN_ID } from '@/views/constants/UpdateViewDropdownId';
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.color.blue};
border-radius: ${({ theme }) => theme.border.radius.md};
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
@ -23,25 +27,50 @@ const StyledContainer = styled.div`
export type UpdateViewButtonGroupProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
};
export const UpdateViewButtonGroup = ({
hotkeyScope,
onViewEditModeChange,
}: UpdateViewButtonGroupProps) => {
const { canPersistViewSelector, viewEditModeState } = useViewStates();
const { canPersistViewSelector, currentViewIdState } = useViewStates();
const { saveCurrentViewFilterAndSorts } = useSaveCurrentViewFiltersAndSorts();
const setViewEditMode = useSetRecoilState(viewEditModeState);
const { setViewPickerMode } = useViewPickerMode();
const { viewPickerReferenceViewIdState } = useViewPickerStates();
const canPersistView = useRecoilValue(canPersistViewSelector());
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode('create');
onViewEditModeChange?.();
}, [setViewEditMode, onViewEditModeChange]);
const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown(
UPDATE_VIEW_BUTTON_DROPDOWN_ID,
);
const { openDropdown: openViewPickerDropdown } = useDropdown(
VIEW_PICKER_DROPDOWN_ID,
);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const handleViewSubmit = async () => {
const currentViewId = useRecoilValue(currentViewIdState);
const setViewPickerReferenceViewId = useSetRecoilState(
viewPickerReferenceViewIdState,
);
const handleViewCreate = useCallback(() => {
if (!currentViewId) {
return;
}
openViewPickerDropdown();
setViewPickerReferenceViewId(currentViewId);
setViewPickerMode('create');
closeUpdateViewButtonDropdown();
}, [
closeUpdateViewButtonDropdown,
currentViewId,
openViewPickerDropdown,
setViewPickerMode,
setViewPickerReferenceViewId,
]);
const handleViewUpdate = async () => {
await saveCurrentViewFilterAndSorts();
};
@ -51,27 +80,42 @@ export const UpdateViewButtonGroup = ({
return (
<StyledContainer>
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewSubmit} />
<Dropdown
dropdownId={UPDATE_VIEW_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<Button size="small" accent="blue" Icon={IconChevronDown} />
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleCreateViewButtonClick}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
</>
}
{currentViewWithCombinedFiltersAndSorts?.key !== 'INDEX' ? (
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewUpdate} />
<Dropdown
dropdownId={UPDATE_VIEW_BUTTON_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<Button
size="small"
accent="blue"
Icon={IconChevronDown}
position="right"
/>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleViewCreate}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
</>
}
/>
</ButtonGroup>
) : (
<Button
title="Save as new view"
onClick={handleViewCreate}
accent="blue"
size="small"
variant="secondary"
/>
</ButtonGroup>
)}
</StyledContainer>
);
};

View File

@ -4,26 +4,25 @@ import { useParams } from 'react-router-dom';
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
import { QueryParamsViewIdEffect } from '@/views/components/QueryParamsViewIdEffect';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { ViewScope } from '@/views/scopes/ViewScope';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDropdown';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
optionsDropdownScopeId: string;
onCurrentViewChange: (view: GraphQLView | undefined) => void | Promise<void>;
};
@ -31,18 +30,17 @@ export const ViewBar = ({
viewBarId,
className,
optionsDropdownButton,
optionsDropdownScopeId,
onCurrentViewChange,
}: ViewBarProps) => {
const { openDropdown: openOptionsDropdownButton } = useDropdown(
optionsDropdownScopeId,
);
const { objectNamePlural } = useParams();
const filterDropdownId = 'view-filter';
const sortDropdownId = 'view-sort';
if (!objectNamePlural) {
return;
}
return (
<ViewScope
viewScopeId={viewBarId}
@ -51,16 +49,15 @@ export const ViewBar = ({
<ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect sortDropdownId={sortDropdownId} />
{!!objectNamePlural && <FilterQueryParamsEffect />}
<QueryParamsFiltersEffect />
<QueryParamsViewIdEffect />
<TopBar
className={className}
leftComponent={
<ViewsDropdownButton
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
optionsDropdownScopeId={optionsDropdownScopeId}
/>
<>
<ViewPickerDropdown />
</>
}
displayBottomBorder={false}
rightComponent={
@ -87,8 +84,9 @@ export const ViewBar = ({
viewBarId={viewBarId}
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
hotkeyScope={{
scope: ViewsHotkeyScope.UpdateViewButtonDropdown,
}}
/>
}
/>

View File

@ -1,12 +1,11 @@
import { useEffect, useState } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { GraphQLView } from '@/views/types/GraphQLView';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from '~/utils/isDefined';
type ViewBarEffectProps = {
viewBarId: string;
@ -17,7 +16,6 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
useGetCurrentView(viewBarId);
const {
onCurrentViewChangeState,
currentViewIdState,
availableFilterDefinitionsState,
isPersistingViewFieldsState,
} = useViewStates(viewBarId);
@ -30,7 +28,6 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
availableFilterDefinitionsState,
);
const isPersistingViewFields = useRecoilValue(isPersistingViewFieldsState);
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
useEffect(() => {
if (
@ -39,14 +36,14 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
currentViewSnapshot,
)
) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
if (isUndefined(currentViewWithCombinedFiltersAndSorts)) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(undefined);
return;
}
if (!isPersistingViewFields) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(currentViewWithCombinedFiltersAndSorts);
}
}
@ -58,14 +55,5 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
onCurrentViewChange,
]);
useEffect(() => {
if (
isDefined(currentViewWithCombinedFiltersAndSorts) &&
!isDefined(currentViewId)
) {
setCurrentViewId(currentViewWithCombinedFiltersAndSorts.id);
}
}, [currentViewWithCombinedFiltersAndSorts, currentViewId, setCurrentViewId]);
return <></>;
};

View File

@ -1,189 +0,0 @@
import { MouseEvent } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
IconChevronDown,
IconList,
IconPencil,
IconPlus,
IconTrash,
} from '@/ui/display/icon';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { VIEWS_DROPDOWN_ID } from '@/views/constants/ViewsDropdownId';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useHandleViews } from '@/views/hooks/useHandleViews';
import { isDefined } from '~/utils/isDefined';
import { useViewStates } from '../hooks/internal/useViewStates';
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledDropdownLabelAdornments = styled.span`
align-items: center;
color: ${({ theme }) => theme.grayScale.gray35};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewName = styled.span`
margin-left: ${({ theme }) => theme.spacing(1)};
display: inline-block;
max-width: 130px;
@media (max-width: 375px) {
max-width: 90px;
}
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
max-width: 110px;
}
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
`;
export type ViewsDropdownButtonProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
optionsDropdownScopeId: string;
};
export const ViewsDropdownButton = ({
hotkeyScope,
onViewEditModeChange,
optionsDropdownScopeId,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const { removeView, selectView } = useHandleViews();
const { entityCountInCurrentViewState, viewEditModeState } = useViewStates();
const { currentViewWithCombinedFiltersAndSorts, viewsOnCurrentObject } =
useGetCurrentView();
const entityCountInCurrentView = useRecoilValue(
entityCountInCurrentViewState,
);
const setViewEditMode = useSetRecoilState(viewEditModeState);
const {
isDropdownOpen: isViewsDropdownOpen,
closeDropdown: closeViewsDropdown,
} = useDropdown(VIEWS_DROPDOWN_ID);
const { openDropdown: openOptionsDropdown } = useDropdown(
optionsDropdownScopeId,
);
const handleViewSelect = (viewId: string) => {
selectView(viewId);
closeViewsDropdown();
};
const handleAddViewButtonClick = () => {
setViewEditMode('create');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleEditViewButtonClick = (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
selectView(viewId);
setViewEditMode('edit');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
await removeView(viewId);
selectView(viewsOnCurrentObject.filter((view) => view.id !== viewId)[0].id);
closeViewsDropdown();
};
const { getIcon } = useIcons();
const CurrentViewIcon = getIcon(currentViewWithCombinedFiltersAndSorts?.icon);
return (
<Dropdown
dropdownId={VIEWS_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsDropdownOpen}>
{currentViewWithCombinedFiltersAndSorts && CurrentViewIcon ? (
<CurrentViewIcon size={theme.icon.size.md} />
) : (
<IconList size={theme.icon.size.md} />
)}
<StyledViewName>
{currentViewWithCombinedFiltersAndSorts?.name ?? 'All'}
</StyledViewName>
<StyledDropdownLabelAdornments>
· {entityCountInCurrentView}{' '}
<IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments>
</StyledDropdownButtonContainer>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
{viewsOnCurrentObject.map((view) => (
<MenuItem
key={view.id}
iconButtons={[
currentViewWithCombinedFiltersAndSorts?.id === view.id
? {
Icon: IconPencil,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleEditViewButtonClick(event, view.id),
}
: null,
viewsOnCurrentObject.length > 1
? {
Icon: IconTrash,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleDeleteViewButtonClick(event, view.id),
}
: null,
].filter(isDefined)}
onClick={() => handleViewSelect(view.id)}
LeftIcon={getIcon(view.icon)}
text={view.name}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<StyledBoldDropdownMenuItemsContainer>
<MenuItem
onClick={handleAddViewButtonClick}
LeftIcon={IconPlus}
text="Add view"
/>
</StyledBoldDropdownMenuItemsContainer>
</>
}
/>
);
};