mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 13:17:57 +00:00
Fix optimistic rendering issues on views (#2851)
* Fix optimistic rendering issues on views * Remove virtualizer
This commit is contained in:
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
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 { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@@ -55,7 +55,7 @@ export const useFindManyRecords = <
|
||||
hasNextPageFamilyState(findManyQueryStateIdentifier),
|
||||
);
|
||||
|
||||
const [, setIsFetchingMoreObjects] = useRecoilState(
|
||||
const setIsFetchingMoreObjects = useSetRecoilState(
|
||||
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
|
||||
);
|
||||
|
||||
@@ -142,6 +142,18 @@ export const useFindManyRecords = <
|
||||
...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges,
|
||||
]);
|
||||
}
|
||||
|
||||
if (data?.[objectMetadataItem.namePlural]) {
|
||||
setLastCursor(
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
|
||||
.endCursor,
|
||||
);
|
||||
setHasNextPage(
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo
|
||||
.hasNextPage,
|
||||
);
|
||||
}
|
||||
|
||||
onCompleted?.({
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
@@ -151,15 +163,6 @@ export const useFindManyRecords = <
|
||||
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
|
||||
});
|
||||
|
||||
if (data?.[objectMetadataItem.namePlural]) {
|
||||
setLastCursor(
|
||||
data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor,
|
||||
);
|
||||
setHasNextPage(
|
||||
data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.assign({}, prev, {
|
||||
[objectMetadataItem.namePlural]: {
|
||||
__typename: `${capitalize(
|
||||
@@ -218,5 +221,6 @@ export const useFindManyRecords = <
|
||||
loading,
|
||||
error,
|
||||
fetchMoreRecords,
|
||||
queryStateIdentifier: findManyQueryStateIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
@@ -21,11 +21,12 @@ export const useObjectRecordTable = () => {
|
||||
objectNameSingular,
|
||||
},
|
||||
);
|
||||
|
||||
const { tableFiltersState, tableSortsState } = useRecordTableScopedStates();
|
||||
const { tableFiltersState, tableSortsState, tableLastRowVisibleState } =
|
||||
useRecordTableScopedStates();
|
||||
|
||||
const tableFilters = useRecoilValue(tableFiltersState);
|
||||
const tableSorts = useRecoilValue(tableSortsState);
|
||||
const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState);
|
||||
|
||||
const filter = turnFiltersIntoWhereClause(
|
||||
tableFilters,
|
||||
@@ -36,16 +37,21 @@ export const useObjectRecordTable = () => {
|
||||
foundObjectMetadataItem?.fields ?? [],
|
||||
);
|
||||
|
||||
const { records, loading, fetchMoreRecords } = useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
});
|
||||
const { records, loading, fetchMoreRecords, queryStateIdentifier } =
|
||||
useFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
onCompleted: () => {
|
||||
setLastRowVisible(false);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
records,
|
||||
loading,
|
||||
fetchMoreRecords,
|
||||
queryStateIdentifier,
|
||||
setRecordTableData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,21 +7,16 @@ import { capitalize } from '~/utils/string/capitalize';
|
||||
export const useUpdateOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const {
|
||||
objectMetadataItem,
|
||||
updateOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
findManyRecordsQuery,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const updateOneRecord = async ({
|
||||
idToUpdate,
|
||||
input,
|
||||
forceRefetch,
|
||||
}: {
|
||||
idToUpdate: string;
|
||||
input: Record<string, any>;
|
||||
|
||||
@@ -1,92 +1,33 @@
|
||||
import { useContext } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
||||
import {
|
||||
RecordTableRow,
|
||||
StyledRow,
|
||||
} from '@/ui/object/record-table/components/RecordTableRow';
|
||||
import { RecordTableBodyFetchMoreLoader } from '@/ui/object/record-table/components/RecordTableBodyFetchMoreLoader';
|
||||
import { RecordTableRow } from '@/ui/object/record-table/components/RecordTableRow';
|
||||
import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext';
|
||||
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 { 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 = () => {
|
||||
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 [isFetchingMoreObjects] = useRecoilState(
|
||||
isFetchingMoreRecordsFamilyState(scopeId),
|
||||
);
|
||||
|
||||
const isFetchingRecordTableData = useRecoilValue(
|
||||
isFetchingRecordTableDataState,
|
||||
);
|
||||
|
||||
const lastRowId = tableRowIds[tableRowIds.length - 1];
|
||||
|
||||
const scrollWrapperRef = useContext(ScrollWrapperContext);
|
||||
|
||||
if (isFetchingRecordTableData) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{tableRowIds.map((rowId, rowIndex) => (
|
||||
{tableRowIds.slice().map((rowId, rowIndex) => (
|
||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||
<RowIndexContext.Provider value={rowIndex}>
|
||||
<RenderIfVisible
|
||||
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>
|
||||
<RecordTableRow key={rowId} rowId={rowId} />
|
||||
</RowIndexContext.Provider>
|
||||
</RowIdContext.Provider>
|
||||
))}
|
||||
<tbody>
|
||||
{isFetchingMoreObjects && (
|
||||
<StyledRow selected={false}>
|
||||
<td style={{ height: 50 }} colSpan={1000}>
|
||||
Loading more...
|
||||
</td>
|
||||
</StyledRow>
|
||||
)}
|
||||
</tbody>
|
||||
<RecordTableBodyFetchMoreLoader />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const RecordTableBodyEffect = () => {
|
||||
const {
|
||||
fetchMoreRecords: fetchMoreObjects,
|
||||
records,
|
||||
setRecordTableData,
|
||||
queryStateIdentifier,
|
||||
} = useObjectRecordTable();
|
||||
const { tableLastRowVisibleState } = useRecordTableScopedStates();
|
||||
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
|
||||
const [tableLastRowVisible, setTableLastRowVisible] = useRecoilState(
|
||||
tableLastRowVisibleState,
|
||||
);
|
||||
|
||||
const isFetchingMoreObjects = useRecoilValue(
|
||||
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordTableData(records);
|
||||
}, [records, setRecordTableData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableLastRowVisible && isDefined(fetchMoreObjects)) {
|
||||
if (tableLastRowVisible && !isFetchingMoreObjects) {
|
||||
fetchMoreObjects();
|
||||
}
|
||||
}, [fetchMoreObjects, tableLastRowVisible]);
|
||||
}, [
|
||||
fetchMoreObjects,
|
||||
isFetchingMoreObjects,
|
||||
setTableLastRowVisible,
|
||||
tableLastRowVisible,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { ColumnContext } from '../contexts/ColumnContext';
|
||||
import { useRecordTableScopedStates } from '../hooks/internal/useRecordTableScopedStates';
|
||||
import { useCurrentRowSelected } from '../record-table-row/hooks/useCurrentRowSelected';
|
||||
@@ -18,36 +21,53 @@ type RecordTableRowProps = {
|
||||
rowId: string;
|
||||
};
|
||||
|
||||
export const RecordTableRow = forwardRef<
|
||||
HTMLTableRowElement,
|
||||
RecordTableRowProps
|
||||
>(({ rowId }, ref) => {
|
||||
const StyledPlaceholder = styled.td`
|
||||
height: 30px;
|
||||
`;
|
||||
|
||||
export const RecordTableRow = ({ rowId }: RecordTableRowProps) => {
|
||||
const { visibleTableColumnsSelector } = useRecordTableScopedStates();
|
||||
|
||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector);
|
||||
|
||||
const { currentRowSelected } = useCurrentRowSelected();
|
||||
|
||||
const scrollWrapperRef = useContext(ScrollWrapperContext);
|
||||
|
||||
const { ref: elementRef, inView } = useInView({
|
||||
root: scrollWrapperRef.current,
|
||||
rootMargin: '1000px',
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
ref={ref}
|
||||
ref={elementRef}
|
||||
data-testid={`row-id-${rowId}`}
|
||||
selected={currentRowSelected}
|
||||
data-selectable-id={rowId}
|
||||
>
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{[...visibleTableColumns]
|
||||
.sort((columnA, columnB) => columnA.position - columnB.position)
|
||||
.map((column, columnIndex) => {
|
||||
return (
|
||||
<ColumnContext.Provider value={column} key={column.fieldMetadataId}>
|
||||
<RecordTableCell cellIndex={columnIndex} />
|
||||
</ColumnContext.Provider>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
{inView ? (
|
||||
<>
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{[...visibleTableColumns]
|
||||
.sort((columnA, columnB) => columnA.position - columnB.position)
|
||||
.map((column, columnIndex) => {
|
||||
return (
|
||||
<ColumnContext.Provider
|
||||
value={column}
|
||||
key={column.fieldMetadataId}
|
||||
>
|
||||
<RecordTableCell cellIndex={columnIndex} />
|
||||
</ColumnContext.Provider>
|
||||
);
|
||||
})}
|
||||
<td></td>
|
||||
</>
|
||||
) : (
|
||||
<StyledPlaceholder />
|
||||
)}
|
||||
</StyledRow>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
|
||||
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 { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
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 { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
@@ -20,7 +22,7 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
export type UpdateViewButtonGroupProps = {
|
||||
hotkeyScope: string;
|
||||
hotkeyScope: HotkeyScope;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
@@ -28,7 +30,6 @@ export const UpdateViewButtonGroup = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: UpdateViewButtonGroupProps) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { updateCurrentView, setViewEditMode } = useViewBar();
|
||||
const { canPersistFiltersSelector, canPersistSortsSelector } =
|
||||
useViewScopedStates();
|
||||
@@ -38,53 +39,43 @@ export const UpdateViewButtonGroup = ({
|
||||
|
||||
const canPersistView = canPersistFilters || canPersistSorts;
|
||||
|
||||
const handleArrowDownButtonClick = useCallback(() => {
|
||||
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
|
||||
}, []);
|
||||
|
||||
const handleCreateViewButtonClick = useCallback(() => {
|
||||
setViewEditMode('create');
|
||||
onViewEditModeChange?.();
|
||||
setIsDropdownOpen(false);
|
||||
}, [setViewEditMode, onViewEditModeChange]);
|
||||
|
||||
const handleDropdownClose = useCallback(() => {
|
||||
setIsDropdownOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleViewSubmit = async () => {
|
||||
await updateCurrentView?.();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter, Key.Escape],
|
||||
handleDropdownClose,
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
if (!canPersistView) return null;
|
||||
if (!canPersistView) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ButtonGroup size="small" accent="blue">
|
||||
<Button title="Update view" onClick={handleViewSubmit} />
|
||||
<Button
|
||||
size="small"
|
||||
Icon={IconChevronDown}
|
||||
onClick={handleArrowDownButtonClick}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
<DropdownScope dropdownScopeId={UpdateViewDropdownId}>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
clickableComponent={
|
||||
<StyledContainer>
|
||||
<ButtonGroup size="small" accent="blue">
|
||||
<Button title="Update view" onClick={handleViewSubmit} />
|
||||
<Button size="small" Icon={IconChevronDown} />
|
||||
</ButtonGroup>
|
||||
</StyledContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ export const ViewBar = ({
|
||||
rightComponent={
|
||||
<UpdateViewButtonGroup
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
|
||||
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
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 { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
|
||||
import { getViewScopedStatesFromSnapshot } from '../utils/getViewScopedStatesFromSnapshot';
|
||||
|
||||
export const ViewBarEffect = () => {
|
||||
const {
|
||||
scopeId: viewScopeId,
|
||||
loadView,
|
||||
changeViewInUrl,
|
||||
loadViewFields,
|
||||
@@ -25,58 +21,62 @@ export const ViewBarEffect = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
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 viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState);
|
||||
const setCurrentViewId = useSetRecoilState(currentViewIdState);
|
||||
|
||||
useFindManyRecords({
|
||||
const { records: newViews } = useFindManyRecords<GraphQLView>({
|
||||
skip: !viewObjectMetadataId,
|
||||
objectNameSingular: 'view',
|
||||
filter: {
|
||||
type: { eq: viewType },
|
||||
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);
|
||||
|
||||
if (!isDeeplyEqual(views, nextViews)) {
|
||||
set(viewsState, nextViews);
|
||||
}
|
||||
|
||||
const currentView =
|
||||
data.edges
|
||||
.map((view) => view.node)
|
||||
.find((view) => view.id === currentViewIdFromUrl) ??
|
||||
data.edges[0]?.node ??
|
||||
null;
|
||||
|
||||
if (!currentView) return;
|
||||
|
||||
set(currentViewIdState, currentView.id);
|
||||
|
||||
if (currentView?.viewFields) {
|
||||
loadViewFields(currentView.viewFields, currentView.id);
|
||||
loadViewFilters(currentView.viewFilters, currentView.id);
|
||||
loadViewSorts(currentView.viewSorts, currentView.id);
|
||||
}
|
||||
|
||||
if (!nextViews.length) return;
|
||||
if (!currentViewIdFromUrl) return changeViewInUrl(nextViews[0].id);
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!newViews.length) return;
|
||||
|
||||
if (!isDeeplyEqual(views, newViews)) {
|
||||
setViews(newViews);
|
||||
}
|
||||
|
||||
const currentView =
|
||||
newViews.find((view) => view.id === currentViewIdFromUrl) ??
|
||||
newViews[0] ??
|
||||
null;
|
||||
|
||||
if (!currentView) return;
|
||||
|
||||
setCurrentViewId(currentView.id);
|
||||
|
||||
if (currentView?.viewFields) {
|
||||
loadViewFields(currentView.viewFields, currentView.id);
|
||||
loadViewFilters(currentView.viewFilters, currentView.id);
|
||||
loadViewSorts(currentView.viewSorts, currentView.id);
|
||||
}
|
||||
|
||||
if (!currentViewIdFromUrl) return changeViewInUrl(currentView.id);
|
||||
}, [
|
||||
changeViewInUrl,
|
||||
currentViewIdFromUrl,
|
||||
loadViewFields,
|
||||
loadViewFilters,
|
||||
loadViewSorts,
|
||||
newViews,
|
||||
setCurrentViewId,
|
||||
setViews,
|
||||
views,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentViewIdFromUrl) return;
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const UpdateViewDropdownId = 'update-view';
|
||||
Reference in New Issue
Block a user