Fix optimistic rendering issues on views (#2851)

* Fix optimistic rendering issues on views

* Remove virtualizer
This commit is contained in:
Charles Bochet
2023-12-06 16:55:09 +01:00
committed by GitHub
parent 93decaceab
commit 076a67b0e2
12 changed files with 229 additions and 318 deletions

View File

@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { isNonEmptyArray } from '@apollo/client/utilities'; import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
@@ -55,7 +55,7 @@ export const useFindManyRecords = <
hasNextPageFamilyState(findManyQueryStateIdentifier), hasNextPageFamilyState(findManyQueryStateIdentifier),
); );
const [, setIsFetchingMoreObjects] = useRecoilState( const setIsFetchingMoreObjects = useSetRecoilState(
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier), isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
); );
@@ -142,6 +142,18 @@ export const useFindManyRecords = <
...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges, ...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges,
]); ]);
} }
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.endCursor,
);
setHasNextPage(
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
.hasNextPage,
);
}
onCompleted?.({ onCompleted?.({
__typename: `${capitalize( __typename: `${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
@@ -151,15 +163,6 @@ export const useFindManyRecords = <
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo, fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
}); });
if (data?.[objectMetadataItem.namePlural]) {
setLastCursor(
data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor,
);
setHasNextPage(
data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage,
);
}
return Object.assign({}, prev, { return Object.assign({}, prev, {
[objectMetadataItem.namePlural]: { [objectMetadataItem.namePlural]: {
__typename: `${capitalize( __typename: `${capitalize(
@@ -218,5 +221,6 @@ export const useFindManyRecords = <
loading, loading,
error, error,
fetchMoreRecords, fetchMoreRecords,
queryStateIdentifier: findManyQueryStateIdentifier,
}; };
}; };

View File

@@ -1,4 +1,4 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
@@ -21,11 +21,12 @@ export const useObjectRecordTable = () => {
objectNameSingular, objectNameSingular,
}, },
); );
const { tableFiltersState, tableSortsState, tableLastRowVisibleState } =
const { tableFiltersState, tableSortsState } = useRecordTableScopedStates(); useRecordTableScopedStates();
const tableFilters = useRecoilValue(tableFiltersState); const tableFilters = useRecoilValue(tableFiltersState);
const tableSorts = useRecoilValue(tableSortsState); const tableSorts = useRecoilValue(tableSortsState);
const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState);
const filter = turnFiltersIntoWhereClause( const filter = turnFiltersIntoWhereClause(
tableFilters, tableFilters,
@@ -36,16 +37,21 @@ export const useObjectRecordTable = () => {
foundObjectMetadataItem?.fields ?? [], foundObjectMetadataItem?.fields ?? [],
); );
const { records, loading, fetchMoreRecords } = useFindManyRecords({ const { records, loading, fetchMoreRecords, queryStateIdentifier } =
useFindManyRecords({
objectNameSingular, objectNameSingular,
filter, filter,
orderBy, orderBy,
onCompleted: () => {
setLastRowVisible(false);
},
}); });
return { return {
records, records,
loading, loading,
fetchMoreRecords, fetchMoreRecords,
queryStateIdentifier,
setRecordTableData, setRecordTableData,
}; };
}; };

View File

@@ -7,12 +7,8 @@ import { capitalize } from '~/utils/string/capitalize';
export const useUpdateOneRecord = <T>({ export const useUpdateOneRecord = <T>({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { }: ObjectMetadataItemIdentifier) => {
const { const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
objectMetadataItem, useObjectMetadataItem({
updateOneRecordMutation,
getRecordFromCache,
findManyRecordsQuery,
} = useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}); });
@@ -21,7 +17,6 @@ export const useUpdateOneRecord = <T>({
const updateOneRecord = async ({ const updateOneRecord = async ({
idToUpdate, idToUpdate,
input, input,
forceRefetch,
}: { }: {
idToUpdate: string; idToUpdate: string;
input: Record<string, any>; input: Record<string, any>;

View File

@@ -1,92 +1,33 @@
import { useContext } from 'react'; import { useRecoilValue } from 'recoil';
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { RecordTableBodyFetchMoreLoader } from '@/ui/object/record-table/components/RecordTableBodyFetchMoreLoader';
import { import { RecordTableRow } from '@/ui/object/record-table/components/RecordTableRow';
RecordTableRow,
StyledRow,
} from '@/ui/object/record-table/components/RecordTableRow';
import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext'; import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext';
import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext'; import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState'; import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import RenderIfVisible from '@/ui/utilities/virtualizer/RenderIfVisible';
export const RecordTableBody = () => { export const RecordTableBody = () => {
const { scopeId } = useRecordTable();
const onLastRowVisible = useRecoilCallback(
({ set }) =>
async (inView: boolean) => {
const { tableLastRowVisibleState } = getRecordTableScopedStates({
recordTableScopeId: scopeId,
});
set(tableLastRowVisibleState, inView);
},
[scopeId],
);
const { ref: lastTableRowRef } = useInView({
onChange: onLastRowVisible,
});
const tableRowIds = useRecoilValue(tableRowIdsState); const tableRowIds = useRecoilValue(tableRowIdsState);
const [isFetchingMoreObjects] = useRecoilState(
isFetchingMoreRecordsFamilyState(scopeId),
);
const isFetchingRecordTableData = useRecoilValue( const isFetchingRecordTableData = useRecoilValue(
isFetchingRecordTableDataState, isFetchingRecordTableDataState,
); );
const lastRowId = tableRowIds[tableRowIds.length - 1];
const scrollWrapperRef = useContext(ScrollWrapperContext);
if (isFetchingRecordTableData) { if (isFetchingRecordTableData) {
return <></>; return <></>;
} }
return ( return (
<> <>
{tableRowIds.map((rowId, rowIndex) => ( {tableRowIds.slice().map((rowId, rowIndex) => (
<RowIdContext.Provider value={rowId} key={rowId}> <RowIdContext.Provider value={rowId} key={rowId}>
<RowIndexContext.Provider value={rowIndex}> <RowIndexContext.Provider value={rowIndex}>
<RenderIfVisible <RecordTableRow key={rowId} rowId={rowId} />
rootElement="tbody"
placeholderElement="tr"
defaultHeight={32}
initialVisible={rowIndex < 30}
root={scrollWrapperRef.current}
>
<RecordTableRow
key={rowId}
ref={
rowId === lastRowId && rowIndex > 30
? lastTableRowRef
: undefined
}
rowId={rowId}
/>
</RenderIfVisible>
</RowIndexContext.Provider> </RowIndexContext.Provider>
</RowIdContext.Provider> </RowIdContext.Provider>
))} ))}
<tbody> <RecordTableBodyFetchMoreLoader />
{isFetchingMoreObjects && (
<StyledRow selected={false}>
<td style={{ height: 50 }} colSpan={1000}>
Loading more...
</td>
</StyledRow>
)}
</tbody>
</> </>
); );
}; };

View File

@@ -1,28 +1,40 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable'; import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates'; import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates';
import { isDefined } from '~/utils/isDefined';
export const RecordTableBodyEffect = () => { export const RecordTableBodyEffect = () => {
const { const {
fetchMoreRecords: fetchMoreObjects, fetchMoreRecords: fetchMoreObjects,
records, records,
setRecordTableData, setRecordTableData,
queryStateIdentifier,
} = useObjectRecordTable(); } = useObjectRecordTable();
const { tableLastRowVisibleState } = useRecordTableScopedStates(); const { tableLastRowVisibleState } = useRecordTableScopedStates();
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState); const [tableLastRowVisible, setTableLastRowVisible] = useRecoilState(
tableLastRowVisibleState,
);
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
);
useEffect(() => { useEffect(() => {
setRecordTableData(records); setRecordTableData(records);
}, [records, setRecordTableData]); }, [records, setRecordTableData]);
useEffect(() => { useEffect(() => {
if (tableLastRowVisible && isDefined(fetchMoreObjects)) { if (tableLastRowVisible && !isFetchingMoreObjects) {
fetchMoreObjects(); fetchMoreObjects();
} }
}, [fetchMoreObjects, tableLastRowVisible]); }, [
fetchMoreObjects,
isFetchingMoreObjects,
setTableLastRowVisible,
tableLastRowVisible,
]);
return <></>; return <></>;
}; };

View File

@@ -0,0 +1,53 @@
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useObjectRecordTable } from '@/object-record/hooks/useObjectRecordTable';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { StyledRow } from '@/ui/object/record-table/components/RecordTableRow';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
export const RecordTableBodyFetchMoreLoader = () => {
const { queryStateIdentifier } = useObjectRecordTable();
const { scopeId } = useRecordTable();
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
);
const isFetchingRecordTableData = useRecoilValue(
isFetchingRecordTableDataState,
);
const onLastRowVisible = useRecoilCallback(
({ set }) =>
async (inView: boolean) => {
const { tableLastRowVisibleState } = getRecordTableScopedStates({
recordTableScopeId: scopeId,
});
set(tableLastRowVisibleState, inView);
},
[scopeId],
);
const { ref: tbodyRef } = useInView({
onChange: onLastRowVisible,
});
if (isFetchingRecordTableData) {
return <></>;
}
return (
<tbody ref={tbodyRef}>
{isFetchingMoreObjects && (
<StyledRow selected={false}>
<td style={{ height: 50 }} colSpan={1000}>
Loading more...
</td>
</StyledRow>
)}
</tbody>
);
};

View File

@@ -1,7 +1,10 @@
import { forwardRef } from 'react'; import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { ColumnContext } from '../contexts/ColumnContext'; import { ColumnContext } from '../contexts/ColumnContext';
import { useRecordTableScopedStates } from '../hooks/internal/useRecordTableScopedStates'; import { useRecordTableScopedStates } from '../hooks/internal/useRecordTableScopedStates';
import { useCurrentRowSelected } from '../record-table-row/hooks/useCurrentRowSelected'; import { useCurrentRowSelected } from '../record-table-row/hooks/useCurrentRowSelected';
@@ -18,23 +21,33 @@ type RecordTableRowProps = {
rowId: string; rowId: string;
}; };
export const RecordTableRow = forwardRef< const StyledPlaceholder = styled.td`
HTMLTableRowElement, height: 30px;
RecordTableRowProps `;
>(({ rowId }, ref) => {
export const RecordTableRow = ({ rowId }: RecordTableRowProps) => {
const { visibleTableColumnsSelector } = useRecordTableScopedStates(); const { visibleTableColumnsSelector } = useRecordTableScopedStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector); const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector);
const { currentRowSelected } = useCurrentRowSelected(); const { currentRowSelected } = useCurrentRowSelected();
const scrollWrapperRef = useContext(ScrollWrapperContext);
const { ref: elementRef, inView } = useInView({
root: scrollWrapperRef.current,
rootMargin: '1000px',
});
return ( return (
<StyledRow <StyledRow
ref={ref} ref={elementRef}
data-testid={`row-id-${rowId}`} data-testid={`row-id-${rowId}`}
selected={currentRowSelected} selected={currentRowSelected}
data-selectable-id={rowId} data-selectable-id={rowId}
> >
{inView ? (
<>
<td> <td>
<CheckboxCell /> <CheckboxCell />
</td> </td>
@@ -42,12 +55,19 @@ export const RecordTableRow = forwardRef<
.sort((columnA, columnB) => columnA.position - columnB.position) .sort((columnA, columnB) => columnA.position - columnB.position)
.map((column, columnIndex) => { .map((column, columnIndex) => {
return ( return (
<ColumnContext.Provider value={column} key={column.fieldMetadataId}> <ColumnContext.Provider
value={column}
key={column.fieldMetadataId}
>
<RecordTableCell cellIndex={columnIndex} /> <RecordTableCell cellIndex={columnIndex} />
</ColumnContext.Provider> </ColumnContext.Provider>
); );
})} })}
<td></td> <td></td>
</>
) : (
<StyledPlaceholder />
)}
</StyledRow> </StyledRow>
); );
}); };

View File

@@ -1,112 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
type RenderIfVisibleProps = {
/**
* Whether the element should be visible initially or not.
* Useful e.g. for always setting the first N items to visible.
* Default: false
*/
initialVisible?: boolean;
/** An estimate of the element's height */
defaultHeight?: number;
/** How far outside the viewport in pixels should elements be considered visible? */
visibleOffset?: number;
/** Should the element stay rendered after it becomes visible? */
stayRendered?: boolean;
root?: HTMLElement | null;
/** E.g. 'span', 'tbody'. Default = 'div' */
rootElement?: string;
rootElementClass?: string;
/** E.g. 'span', 'tr'. Default = 'div' */
placeholderElement?: string;
placeholderElementClass?: string;
children: React.ReactNode;
};
const RenderIfVisible = ({
initialVisible = false,
defaultHeight = 300,
visibleOffset = 1000,
stayRendered = false,
root = null,
rootElement = 'div',
rootElementClass = '',
placeholderElement = 'div',
placeholderElementClass = '',
children,
}: RenderIfVisibleProps) => {
const [isVisible, setIsVisible] = useState<boolean>(initialVisible);
// eslint-disable-next-line twenty/no-state-useref
const wasVisible = useRef<boolean>(initialVisible);
// eslint-disable-next-line twenty/no-state-useref
const placeholderHeight = useRef<number>(defaultHeight);
const intersectionRef = useRef<HTMLDivElement>(null);
// Set visibility with intersection observer
useEffect(() => {
if (intersectionRef.current) {
const localRef = intersectionRef.current;
const observer = new IntersectionObserver(
(entries) => {
// Before switching off `isVisible`, set the height of the placeholder
if (!entries[0].isIntersecting) {
placeholderHeight.current = localRef!.offsetHeight;
}
if (typeof window !== undefined && window.requestIdleCallback) {
window.requestIdleCallback(
() => setIsVisible(entries[0].isIntersecting),
{
timeout: 600,
},
);
} else {
setIsVisible(entries[0].isIntersecting);
}
},
{ root, rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px` },
);
observer.observe(localRef);
return () => {
if (localRef) {
observer.unobserve(localRef);
}
};
}
return () => {};
}, [root, visibleOffset]);
useEffect(() => {
if (isVisible) {
wasVisible.current = true;
}
}, [isVisible]);
const placeholderStyle = { height: placeholderHeight.current };
const rootClasses = useMemo(
() => `renderIfVisible ${rootElementClass}`,
[rootElementClass],
);
const placeholderClasses = useMemo(
() => `renderIfVisible-placeholder ${placeholderElementClass}`,
[placeholderElementClass],
);
// eslint-disable-next-line react/no-children-prop
return React.createElement(rootElement, {
children:
isVisible || (stayRendered && wasVisible.current) ? (
<>{children}</>
) : (
React.createElement(placeholderElement, {
className: placeholderClasses,
style: placeholderStyle,
})
),
ref: intersectionRef,
className: rootClasses,
});
};
export default RenderIfVisible;

View File

@@ -1,14 +1,16 @@
import { useCallback, useState } from 'react'; import { useCallback } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconChevronDown, IconPlus } from '@/ui/display/icon'; import { IconChevronDown, IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { UpdateViewDropdownId } from '@/views/constants/UpdateViewDropdownId';
import { useViewBar } from '@/views/hooks/useViewBar'; import { useViewBar } from '@/views/hooks/useViewBar';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates'; import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
@@ -20,7 +22,7 @@ const StyledContainer = styled.div`
`; `;
export type UpdateViewButtonGroupProps = { export type UpdateViewButtonGroupProps = {
hotkeyScope: string; hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void; onViewEditModeChange?: () => void;
}; };
@@ -28,7 +30,6 @@ export const UpdateViewButtonGroup = ({
hotkeyScope, hotkeyScope,
onViewEditModeChange, onViewEditModeChange,
}: UpdateViewButtonGroupProps) => { }: UpdateViewButtonGroupProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { updateCurrentView, setViewEditMode } = useViewBar(); const { updateCurrentView, setViewEditMode } = useViewBar();
const { canPersistFiltersSelector, canPersistSortsSelector } = const { canPersistFiltersSelector, canPersistSortsSelector } =
useViewScopedStates(); useViewScopedStates();
@@ -38,45 +39,33 @@ export const UpdateViewButtonGroup = ({
const canPersistView = canPersistFilters || canPersistSorts; const canPersistView = canPersistFilters || canPersistSorts;
const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []);
const handleCreateViewButtonClick = useCallback(() => { const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode('create'); setViewEditMode('create');
onViewEditModeChange?.(); onViewEditModeChange?.();
setIsDropdownOpen(false);
}, [setViewEditMode, onViewEditModeChange]); }, [setViewEditMode, onViewEditModeChange]);
const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const handleViewSubmit = async () => { const handleViewSubmit = async () => {
await updateCurrentView?.(); await updateCurrentView?.();
}; };
useScopedHotkeys( if (!canPersistView) {
[Key.Enter, Key.Escape], return <></>;
handleDropdownClose, }
hotkeyScope,
[],
);
if (!canPersistView) return null;
return ( return (
<DropdownScope dropdownScopeId={UpdateViewDropdownId}>
<Dropdown
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledContainer> <StyledContainer>
<ButtonGroup size="small" accent="blue"> <ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewSubmit} /> <Button title="Update view" onClick={handleViewSubmit} />
<Button <Button size="small" Icon={IconChevronDown} />
size="small"
Icon={IconChevronDown}
onClick={handleArrowDownButtonClick}
/>
</ButtonGroup> </ButtonGroup>
</StyledContainer>
{isDropdownOpen && ( }
dropdownComponents={
<>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItem <MenuItem
onClick={handleCreateViewButtonClick} onClick={handleCreateViewButtonClick}
@@ -84,7 +73,9 @@ export const UpdateViewButtonGroup = ({
text="Create view" text="Create view"
/> />
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
)} </>
</StyledContainer> }
/>
</DropdownScope>
); );
}; };

View File

@@ -100,7 +100,7 @@ export const ViewBar = ({
rightComponent={ rightComponent={
<UpdateViewButtonGroup <UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton} onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={ViewsHotkeyScope.CreateDropdown} hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
/> />
} }
/> />

View File

@@ -1,20 +1,16 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useViewBar } from '@/views/hooks/useViewBar'; import { useViewBar } from '@/views/hooks/useViewBar';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates'; import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
import { getViewScopedStatesFromSnapshot } from '../utils/getViewScopedStatesFromSnapshot';
export const ViewBarEffect = () => { export const ViewBarEffect = () => {
const { const {
scopeId: viewScopeId,
loadView, loadView,
changeViewInUrl, changeViewInUrl,
loadViewFields, loadViewFields,
@@ -25,45 +21,42 @@ export const ViewBarEffect = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const currentViewIdFromUrl = searchParams.get('view'); const currentViewIdFromUrl = searchParams.get('view');
const { viewTypeState, viewObjectMetadataIdState } = useViewScopedStates(); const {
viewTypeState,
viewObjectMetadataIdState,
viewsState,
currentViewIdState,
} = useViewScopedStates();
const [views, setViews] = useRecoilState(viewsState);
const viewType = useRecoilValue(viewTypeState); const viewType = useRecoilValue(viewTypeState);
const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState); const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState);
const setCurrentViewId = useSetRecoilState(currentViewIdState);
useFindManyRecords({ const { records: newViews } = useFindManyRecords<GraphQLView>({
skip: !viewObjectMetadataId, skip: !viewObjectMetadataId,
objectNameSingular: 'view', objectNameSingular: 'view',
filter: { filter: {
type: { eq: viewType }, type: { eq: viewType },
objectMetadataId: { eq: viewObjectMetadataId }, objectMetadataId: { eq: viewObjectMetadataId },
}, },
onCompleted: useRecoilCallback(
({ snapshot, set }) =>
async (data: PaginatedRecordTypeResults<GraphQLView>) => {
const nextViews = data.edges.map(({ node }) => node);
const { viewsState, currentViewIdState } =
getViewScopedStatesFromSnapshot({
snapshot,
viewScopeId,
}); });
const views = getSnapshotValue(snapshot, viewsState); useEffect(() => {
if (!newViews.length) return;
if (!isDeeplyEqual(views, nextViews)) { if (!isDeeplyEqual(views, newViews)) {
set(viewsState, nextViews); setViews(newViews);
} }
const currentView = const currentView =
data.edges newViews.find((view) => view.id === currentViewIdFromUrl) ??
.map((view) => view.node) newViews[0] ??
.find((view) => view.id === currentViewIdFromUrl) ??
data.edges[0]?.node ??
null; null;
if (!currentView) return; if (!currentView) return;
set(currentViewIdState, currentView.id); setCurrentViewId(currentView.id);
if (currentView?.viewFields) { if (currentView?.viewFields) {
loadViewFields(currentView.viewFields, currentView.id); loadViewFields(currentView.viewFields, currentView.id);
@@ -71,11 +64,18 @@ export const ViewBarEffect = () => {
loadViewSorts(currentView.viewSorts, currentView.id); loadViewSorts(currentView.viewSorts, currentView.id);
} }
if (!nextViews.length) return; if (!currentViewIdFromUrl) return changeViewInUrl(currentView.id);
if (!currentViewIdFromUrl) return changeViewInUrl(nextViews[0].id); }, [
}, changeViewInUrl,
), currentViewIdFromUrl,
}); loadViewFields,
loadViewFilters,
loadViewSorts,
newViews,
setCurrentViewId,
setViews,
views,
]);
useEffect(() => { useEffect(() => {
if (!currentViewIdFromUrl) return; if (!currentViewIdFromUrl) return;

View File

@@ -0,0 +1 @@
export const UpdateViewDropdownId = 'update-view';