mirror of
https://github.com/lingble/twenty.git
synced 2025-11-20 07:54:52 +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 { 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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 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>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -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 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const ViewBar = ({
|
|||||||
rightComponent={
|
rightComponent={
|
||||||
<UpdateViewButtonGroup
|
<UpdateViewButtonGroup
|
||||||
onViewEditModeChange={openOptionsDropdownButton}
|
onViewEditModeChange={openOptionsDropdownButton}
|
||||||
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
|
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const UpdateViewDropdownId = 'update-view';
|
||||||
Reference in New Issue
Block a user