🦣🦣🦣 Table virtualization (#14743)
This big PR implements table virtualization with an offset paging, allowing a way more fluid UX. It is a v1 that should be improved in the future with partial data loading and optimization of the browser display performance of a row. But with this PR we have the solid enough technical foundation, both frontend and backend, to get to a smooth table UX. Fixes and improvements after first successful round of development (needed to have main clean) : - [x] Delete should refresh virtualized portion only and reset all table - [x] Fix add new : top and bottom - [x] Table empty shouldn’t show when first loading - [x] Fix d&d - [x] Fix sorts - [x] Fix drag when scrolling after a full virtual page (it throws an error) - [x] Si update mais qu’on a un sort ou filter, alors il faut trigger le refresh - [x] Reset scroll position between tables - [x] Reset scroll shadows between tables - [x] Setup d&n for virtual list : https://github.com/hello-pangea/dnd/blob/main/docs/patterns/virtual-lists.md - [x] Full table re-render when entering edit mode - [x] Clean code and prepare for merge Fixes https://github.com/twentyhq/core-team-issues/issues/1613 that contains other bugs to be fixed before merge --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
co-authored by
Charles Bochet
parent
720e93a68a
commit
68c86871dd
@@ -61,9 +61,9 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 53,
|
||||
lines: 52,
|
||||
functions: 42,
|
||||
statements: 52,
|
||||
lines: 51,
|
||||
functions: 41,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useHTMLElementByIdWhenAvailable = (id: string) => {
|
||||
|
||||
if (isDefined(elementFoundBeforeObservingMutation)) {
|
||||
setElement(elementFoundBeforeObservingMutation);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -7,11 +7,12 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useActivityTargetObjectRecords = (
|
||||
activityRecordId?: string,
|
||||
activityTargets?: NoteTarget[] | TaskTarget[],
|
||||
activityTargets?: Nullable<NoteTarget[] | TaskTarget[]>,
|
||||
) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
|
||||
+2
-1
@@ -6,12 +6,13 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GetActivityTargetObjectRecordsProps = {
|
||||
activityRecord: Note | Task;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
activityTargets?: NoteTarget[] | TaskTarget[];
|
||||
activityTargets?: Nullable<NoteTarget[] | TaskTarget[]>;
|
||||
};
|
||||
|
||||
export const getActivityTargetObjectRecords = ({
|
||||
|
||||
+4
@@ -41,12 +41,14 @@ const mocks = [
|
||||
$orderBy: [MessageOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
messages(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
offset: $offset
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
@@ -209,12 +211,14 @@ const mocks = [
|
||||
$orderBy: [MessageParticipantOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
messageParticipants(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
offset: $offset
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
+9
-3
@@ -1,13 +1,19 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { print } from 'graphql';
|
||||
|
||||
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||
import { print } from 'graphql';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
const expectedQueryTemplate = `
|
||||
query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) {
|
||||
people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) {
|
||||
query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int, $offset: Int) {
|
||||
people(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
offset: $offset
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
|
||||
|
||||
+2
@@ -74,12 +74,14 @@ const mock: MockedResponse = {
|
||||
$orderBy: [PersonOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
offset: $offset
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
+2
@@ -35,12 +35,14 @@ const mocks: MockedResponse[] = [
|
||||
$orderBy: [PersonOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
offset: $offset
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type useCreateManyRecordsProps,
|
||||
} from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
@@ -26,6 +27,8 @@ export const useBatchCreateManyRecords = <
|
||||
setBatchedRecordsCount?: (count: number) => void;
|
||||
abortController?: AbortController;
|
||||
}) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
|
||||
const { createManyRecords } = useCreateManyRecords({
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
@@ -97,6 +100,8 @@ export const useBatchCreateManyRecords = <
|
||||
|
||||
await refetchAggregateQueries();
|
||||
|
||||
registerObjectOperation(objectNameSingular, { type: 'create-many' });
|
||||
|
||||
return allCreatedRecords;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/
|
||||
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type FieldActorForInputValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
@@ -49,6 +50,8 @@ export const useCreateManyRecords = <
|
||||
shouldMatchRootQueryFilter,
|
||||
shouldRefetchAggregateQueries = true,
|
||||
}: useCreateManyRecordsProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
@@ -228,7 +231,11 @@ export const useCreateManyRecords = <
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (shouldRefetchAggregateQueries) await refetchAggregateQueries();
|
||||
if (shouldRefetchAggregateQueries) {
|
||||
await refetchAggregateQueries();
|
||||
}
|
||||
|
||||
registerObjectOperation(objectNameSingular, { type: 'create-many' });
|
||||
|
||||
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/
|
||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
@@ -38,6 +39,7 @@ export const useCreateOneRecord = <
|
||||
skipPostOptimisticEffect = false,
|
||||
shouldMatchRootQueryFilter,
|
||||
}: useCreateOneRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -174,6 +176,9 @@ export const useCreateOneRecord = <
|
||||
});
|
||||
|
||||
await refetchAggregateQueries();
|
||||
|
||||
registerObjectOperation(objectNameSingular, { type: 'create-one' });
|
||||
|
||||
return createdObject.data?.[mutationResponseField] ?? null;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'
|
||||
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -32,6 +33,7 @@ export type DeleteManyRecordsProps = {
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteManyRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
@@ -215,6 +217,11 @@ export const useDeleteManyRecords = ({
|
||||
}
|
||||
}
|
||||
await refetchAggregateQueries();
|
||||
|
||||
registerObjectOperation(objectNameSingular, {
|
||||
type: 'delete-many',
|
||||
});
|
||||
|
||||
return deletedRecords;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordF
|
||||
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
@@ -24,6 +25,7 @@ type useDeleteOneRecordProps = {
|
||||
export const useDeleteOneRecord = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
@@ -154,6 +156,11 @@ export const useDeleteOneRecord = ({
|
||||
});
|
||||
|
||||
await refetchAggregateQueries();
|
||||
|
||||
registerObjectOperation(objectNameSingular, {
|
||||
type: 'delete-one',
|
||||
});
|
||||
|
||||
return deletedRecord.data?.[mutationResponseField] ?? null;
|
||||
},
|
||||
[
|
||||
@@ -165,6 +172,8 @@ export const useDeleteOneRecord = ({
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
refetchAggregateQueries,
|
||||
objectNameSingular,
|
||||
registerObjectOperation,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMu
|
||||
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -29,6 +30,7 @@ export type DestroyManyRecordsProps = {
|
||||
export const useDestroyManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDestroyManyRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
@@ -137,6 +139,11 @@ export const useDestroyManyRecords = ({
|
||||
}
|
||||
|
||||
await refetchAggregateQueries();
|
||||
|
||||
registerObjectOperation(objectNameSingular, {
|
||||
type: 'destroy-many',
|
||||
});
|
||||
|
||||
return destroyedRecords;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -19,6 +20,8 @@ type useDestroyOneRecordProps = {
|
||||
export const useDestroyOneRecord = ({
|
||||
objectNameSingular,
|
||||
}: useDestroyOneRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
@@ -81,6 +84,10 @@ export const useDestroyOneRecord = ({
|
||||
throw error;
|
||||
});
|
||||
|
||||
registerObjectOperation(objectMetadataItem.nameSingular, {
|
||||
type: 'destroy-one',
|
||||
});
|
||||
|
||||
return deletedRecord.data?.[mutationResponseField] ?? null;
|
||||
},
|
||||
[
|
||||
@@ -92,6 +99,7 @@ export const useDestroyOneRecord = ({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
registerObjectOperation,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ import { type OnFindManyRecordsCompleted } from '@/object-record/types/OnFindMan
|
||||
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
||||
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { cursorFamilyState } from '../states/cursorFamilyState';
|
||||
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
|
||||
@@ -88,7 +89,7 @@ export const useLazyFetchMoreRecordsWithPagination = <
|
||||
// This function is equivalent to merge function + read function in field policy
|
||||
const fetchMoreRecordsLazy = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
async (limit = DEFAULT_SEARCH_REQUEST_LIMIT) => {
|
||||
const hasNextPageLocal = snapshot
|
||||
.getLoadable(hasNextPageFamilyState(queryIdentifier))
|
||||
.getValue();
|
||||
@@ -105,6 +106,7 @@ export const useLazyFetchMoreRecordsWithPagination = <
|
||||
try {
|
||||
const { data: fetchMoreDataResult } = await fetchMore({
|
||||
variables: {
|
||||
limit,
|
||||
filter,
|
||||
orderBy,
|
||||
lastCursor: isNonEmptyString(lastCursorLocal)
|
||||
|
||||
@@ -96,7 +96,7 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
|
||||
return {
|
||||
data: null,
|
||||
records: [],
|
||||
records: null,
|
||||
totalCount: 0,
|
||||
hasNextPage: false,
|
||||
error: undefined,
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||
import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
|
||||
import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields';
|
||||
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
type UseLazyFindManyRecordsWithOffsetParams = Pick<
|
||||
UseFindManyRecordsParams<ObjectRecord>,
|
||||
'objectNameSingular'
|
||||
>;
|
||||
|
||||
export const useLazyFindManyRecordsWithOffset = ({
|
||||
objectNameSingular,
|
||||
}: UseLazyFindManyRecordsWithOffsetParams) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const params = useFindManyRecordIndexTableParams(objectNameSingular);
|
||||
|
||||
const recordGqlFields = useRecordsFieldVisibleGqlFields({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const hasReadPermission = objectPermissions.canReadObjectRecords;
|
||||
|
||||
const [findManyRecords] = useLazyQuery<RecordGqlOperationFindManyResult>(
|
||||
findManyRecordsQuery,
|
||||
{
|
||||
variables: {
|
||||
...params,
|
||||
},
|
||||
onError: handleFindManyRecordsError,
|
||||
client: apolloCoreClient,
|
||||
},
|
||||
);
|
||||
|
||||
const findManyRecordsLazyWithOffset = useRecoilCallback(
|
||||
() => async (limit: number, offset: number) => {
|
||||
if (!hasReadPermission) {
|
||||
return {
|
||||
data: null,
|
||||
records: null,
|
||||
totalCount: 0,
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await findManyRecords({
|
||||
variables: {
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
|
||||
const records = getRecordsFromRecordConnection({
|
||||
recordConnection: {
|
||||
edges: result?.data?.[objectMetadataItem.namePlural]?.edges ?? [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: result?.data,
|
||||
records,
|
||||
error: result?.error,
|
||||
};
|
||||
},
|
||||
[hasReadPermission, findManyRecords, objectMetadataItem.namePlural],
|
||||
);
|
||||
|
||||
return {
|
||||
findManyRecordsLazyWithOffset,
|
||||
};
|
||||
};
|
||||
+3
-2
@@ -55,8 +55,9 @@ export const useLoadSelectedRecordsInContextStore = ({
|
||||
objectRecordIds.length,
|
||||
);
|
||||
|
||||
const records = await findManyRecordsLazy();
|
||||
upsertRecords(records.records);
|
||||
const { records } = await findManyRecordsLazy();
|
||||
|
||||
upsertRecords(records ?? []);
|
||||
};
|
||||
},
|
||||
[objectRecordIds, objectMetadataItemId, findManyRecordsLazy, upsertRecords],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDupli
|
||||
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
|
||||
import { useMergeManyRecordsMutation } from '@/object-record/hooks/useMergeManyRecordsMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
@@ -27,6 +28,7 @@ export const useMergeManyRecords = <
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
}: UseMergeManyRecordsProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -103,6 +105,10 @@ export const useMergeManyRecords = <
|
||||
await refetchAggregateQueries();
|
||||
}
|
||||
|
||||
registerObjectOperation(objectNameSingular, {
|
||||
type: 'merge-records',
|
||||
});
|
||||
|
||||
return mergedObject.data?.[mutationResponseField] ?? null;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
@@ -116,6 +122,8 @@ export const useMergeManyRecords = <
|
||||
mergeManyRecordsMutation,
|
||||
objectMetadataItem.namePlural,
|
||||
refetchAggregateQueries,
|
||||
registerObjectOperation,
|
||||
objectNameSingular,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
type ObjectOperationData,
|
||||
objectOperationsByObjectNameSingularFamilyState,
|
||||
} from '@/object-record/states/objectOperationsByObjectNameSingularFamilyState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useRegisterObjectOperation = () => {
|
||||
const registerObjectOperation = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(objectNameSingular: string, data: ObjectOperationData) => {
|
||||
set(
|
||||
objectOperationsByObjectNameSingularFamilyState({
|
||||
objectNameSingular,
|
||||
}),
|
||||
(currentValue) => {
|
||||
const newValue = currentValue.concat();
|
||||
|
||||
newValue.push({
|
||||
id: v4(),
|
||||
timestamp: +new Date(),
|
||||
data,
|
||||
});
|
||||
|
||||
return newValue;
|
||||
},
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
registerObjectOperation,
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
@@ -30,6 +31,8 @@ type RestoreManyRecordsProps = {
|
||||
export const useRestoreManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useRestoreManyRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
|
||||
const apiConfig = useRecoilValue(apiConfigState);
|
||||
|
||||
const mutationPageSize =
|
||||
@@ -191,6 +194,10 @@ export const useRestoreManyRecords = ({
|
||||
|
||||
restoredRecords.push(...restoredRecordsForThisBatch);
|
||||
|
||||
registerObjectOperation(objectMetadataItem.nameSingular, {
|
||||
type: 'restore-many',
|
||||
});
|
||||
|
||||
if (isDefined(delayInMsBetweenRequests)) {
|
||||
await sleep(delayInMsBetweenRequests);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graph
|
||||
import { generateDepthRecordGqlFieldsFromRecord } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromRecord';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
|
||||
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
@@ -36,6 +37,7 @@ export const useUpdateOneRecord = <
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
}: useUpdateOneRecordProps) => {
|
||||
const { registerObjectOperation } = useRegisterObjectOperation();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
@@ -217,7 +219,15 @@ export const useUpdateOneRecord = <
|
||||
});
|
||||
|
||||
await refetchAggregateQueries();
|
||||
return updatedRecord?.data?.[mutationResponseField] ?? null;
|
||||
|
||||
const udpatedRecord = updatedRecord?.data?.[mutationResponseField] ?? null;
|
||||
|
||||
registerObjectOperation(objectNameSingular, {
|
||||
type: 'update-one',
|
||||
result: { updatedRecord, updateInput: updateOneRecordInput },
|
||||
});
|
||||
|
||||
return udpatedRecord;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+8
-7
@@ -3,7 +3,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { allCardsSelectedStatusComponentSelector } from '@/object-record/record-board/states/selectors/allCardsSelectedStatusComponentSelector';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { allRecordIdsOfAllRecordGroupsComponentSelector } from '@/object-record/record-index/states/selectors/allRecordIdsOfAllRecordGroupsComponentSelector';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
@@ -16,10 +16,11 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
isRecordBoardCardSelectedComponentFamilyState,
|
||||
recordBoardId,
|
||||
);
|
||||
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackState(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
recordBoardId,
|
||||
);
|
||||
const allRecordIdsOfAllRecordGroupsCallbackSelector =
|
||||
useRecoilComponentCallbackState(
|
||||
allRecordIdsOfAllRecordGroupsComponentSelector,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
|
||||
@@ -33,7 +34,7 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
|
||||
const allRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
allRecordIdsOfAllRecordGroupsCallbackSelector,
|
||||
);
|
||||
|
||||
if (allCardsSelectedStatus === 'all') {
|
||||
@@ -50,7 +51,7 @@ export const useSelectAllCards = (recordBoardId?: string) => {
|
||||
},
|
||||
[
|
||||
allCardsSelectedStatusSelector,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
allRecordIdsOfAllRecordGroupsCallbackSelector,
|
||||
resetRecordSelection,
|
||||
isRecordBoardCardSelectedFamilyState,
|
||||
],
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { allRecordIdsOfAllRecordGroupsComponentSelector } from '@/object-record/record-index/states/selectors/allRecordIdsOfAllRecordGroupsComponentSelector';
|
||||
import { type AllRowsSelectedStatus } from '@/object-record/record-table/types/AllRowSelectedStatus';
|
||||
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
|
||||
|
||||
@@ -12,7 +12,7 @@ export const allCardsSelectedStatusComponentSelector =
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const allRecordIds = get(
|
||||
recordIndexAllRecordIdsComponentSelector.selectorFamily({
|
||||
allRecordIdsOfAllRecordGroupsComponentSelector.selectorFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-3
@@ -25,13 +25,12 @@ export const useRecordBoardDragOperations = () => {
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: selectFieldMetadataItem.name,
|
||||
recordIdsByGroupFamilyState: recordIndexRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: ({ recordId, position, groupValue }) => {
|
||||
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
|
||||
updateOneRecord({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: groupValue,
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position,
|
||||
},
|
||||
});
|
||||
|
||||
+8
-9
@@ -4,10 +4,10 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
|
||||
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
import { extractRecordPositions } from '@/object-record/record-drag/shared/utils/extractRecordPositions';
|
||||
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { computeNewPositionOfDraggedRecord } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import {
|
||||
@@ -58,19 +58,18 @@ export const useHandleDragOneCalendarCard = () => {
|
||||
.getLoadable(calendarDayRecordIdsSelector(destinationDate))
|
||||
.getValue() as string[];
|
||||
|
||||
const recordPositionData = extractRecordPositions(
|
||||
const recordsWithPosition = extractRecordPositions(
|
||||
destinationRecordIds,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const positions = calculateDragPositions({
|
||||
recordIds: destinationRecordIds,
|
||||
recordsToMove: [recordId],
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
const targetRecordId = destinationRecordIds[destinationIndex];
|
||||
|
||||
const newPosition = positions[recordId];
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
idOfItemToMove: recordId,
|
||||
idOfTargetItem: targetRecordId,
|
||||
});
|
||||
|
||||
const targetDate = parse(destinationDate, 'yyyy-MM-dd', new Date());
|
||||
const currentFieldValue = record[calendarFieldMetadata.name];
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { type RecordDragUpdate } from '@/object-record/record-drag/shared/types/RecordDragUpdate';
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
|
||||
export type MultiDragResult = {
|
||||
recordUpdates: RecordDragUpdate[];
|
||||
recordUpdates: RecordWithPosition[];
|
||||
};
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export type RecordDragPositionData = {
|
||||
recordId: string;
|
||||
position?: number;
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export type RecordDragUpdate = {
|
||||
recordId: string;
|
||||
position: number;
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
};
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
|
||||
describe('calculateDragPositions', () => {
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
{ recordId: 'record-4', position: 4 },
|
||||
{ recordId: 'record-5', position: 5 },
|
||||
];
|
||||
|
||||
describe('Single record drag', () => {
|
||||
it('should calculate position when moving record forward', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-2'],
|
||||
destinationIndex: 3,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-2']).toBeGreaterThan(3);
|
||||
expect(result['record-2']).toBeLessThan(5);
|
||||
});
|
||||
|
||||
it('should calculate position when moving record backward', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-4'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-4']).toBeGreaterThan(1);
|
||||
expect(result['record-4']).toBeLessThan(2);
|
||||
});
|
||||
|
||||
it('should handle moving to the beginning', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-3']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle moving to the end', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi record drag', () => {
|
||||
it('should calculate positions for multiple records with proper spacing', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-1', 'record-3'],
|
||||
destinationIndex: 3,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(4);
|
||||
expect(result['record-1']).toBeLessThan(5);
|
||||
expect(result['record-3']).toBeGreaterThan(result['record-1']);
|
||||
expect(result['record-3']).toBeLessThan(5);
|
||||
});
|
||||
|
||||
it('should maintain order of dragged records', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-2', 'record-3', 'record-4'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-2']).toBeLessThan(result['record-3']);
|
||||
expect(result['record-3']).toBeLessThan(result['record-4']);
|
||||
expect(result['record-4']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle moving multiple records to the end', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-1', 'record-2'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
expect(result['record-2']).toBeGreaterThan(result['record-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle undefined positions', () => {
|
||||
const dataWithUndefined: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: undefined },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: undefined },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: dataWithUndefined,
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
expect(isNaN(result['record-1'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty record arrays', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: [],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: [],
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
});
|
||||
|
||||
it('should handle negative positions', () => {
|
||||
const negativeData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: -5 },
|
||||
{ recordId: 'record-2', position: -3 },
|
||||
{ recordId: 'record-3', position: -1 },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: negativeData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(-1);
|
||||
});
|
||||
|
||||
it('should handle very small gaps between positions', () => {
|
||||
const tightData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 1.0001 },
|
||||
{ recordId: 'record-3', position: 1.0002 },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: tightData,
|
||||
});
|
||||
|
||||
expect(result['record-3']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle single record in list', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: [{ recordId: 'record-1', position: 1 }],
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
});
|
||||
|
||||
it('should preserve relative order when moving multiple non-consecutive records', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-1', 'record-3', 'record-5'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
const positions = ['record-1', 'record-3', 'record-5'].map(
|
||||
(id) => result[id],
|
||||
);
|
||||
|
||||
expect(positions[0]).toBeLessThan(positions[1]);
|
||||
expect(positions[1]).toBeLessThan(positions[2]);
|
||||
expect(typeof positions[2]).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-18
@@ -36,9 +36,9 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
{ id: 'record-1', position: 1 },
|
||||
{ id: 'record-2', position: 2 },
|
||||
{ id: 'record-3', position: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -55,9 +55,9 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: undefined },
|
||||
{ recordId: 'record-3', position: undefined },
|
||||
{ id: 'record-1', position: 1 },
|
||||
{ id: 'record-2', position: undefined },
|
||||
{ id: 'record-3', position: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -72,9 +72,9 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: undefined },
|
||||
{ recordId: 'non-existent', position: undefined },
|
||||
{ id: 'record-1', position: 1 },
|
||||
{ id: 'record-2', position: undefined },
|
||||
{ id: 'non-existent', position: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -100,9 +100,9 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-3', position: 75 },
|
||||
{ recordId: 'record-1', position: 100 },
|
||||
{ recordId: 'record-2', position: 50 },
|
||||
{ id: 'record-3', position: 75 },
|
||||
{ id: 'record-1', position: 100 },
|
||||
{ id: 'record-2', position: 50 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -118,8 +118,8 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: null },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ id: 'record-1', position: null },
|
||||
{ id: 'record-2', position: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -135,8 +135,8 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: -5 },
|
||||
{ recordId: 'record-2', position: -10 },
|
||||
{ id: 'record-1', position: -5 },
|
||||
{ id: 'record-2', position: -10 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -152,8 +152,8 @@ describe('extractRecordPositions', () => {
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1.5 },
|
||||
{ recordId: 'record-2', position: 2.75 },
|
||||
{ id: 'record-1', position: 1.5 },
|
||||
{ id: 'record-2', position: 2.75 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-drag/shared/utils/getDraggedRecordPosition';
|
||||
|
||||
describe('getDraggedRecordPosition', () => {
|
||||
it('when both records defined and positive, should return the average of the two positions', () => {
|
||||
expect(getDraggedRecordPosition(1, 3)).toBe(2);
|
||||
});
|
||||
|
||||
it('when both records defined and negative, should return the average of the two positions', () => {
|
||||
expect(getDraggedRecordPosition(-3, -1)).toBe(-2);
|
||||
});
|
||||
|
||||
it('when both records defined and one negative, should return the average of the two positions', () => {
|
||||
expect(getDraggedRecordPosition(-1, 3)).toBe(1);
|
||||
});
|
||||
|
||||
it('when only record after, should return the position - 1', () => {
|
||||
expect(getDraggedRecordPosition(undefined, 3)).toBe(2);
|
||||
});
|
||||
|
||||
it('when only record before, should return the position + 1', () => {
|
||||
expect(getDraggedRecordPosition(1, undefined)).toBe(2);
|
||||
});
|
||||
|
||||
it('when both records undefined, should return 1', () => {
|
||||
expect(getDraggedRecordPosition(undefined, undefined)).toBe(1);
|
||||
});
|
||||
});
|
||||
-343
@@ -1,343 +0,0 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { type Snapshot } from 'recoil';
|
||||
|
||||
import { processGroupDragOperation } from '../processGroupDragOperation';
|
||||
|
||||
jest.mock('../getDragOperationType');
|
||||
jest.mock('../extractRecordPositions');
|
||||
jest.mock('../processSingleDrag');
|
||||
jest.mock('../processMultiDrag');
|
||||
jest.mock('@/ui/utilities/state/utils/getSnapshotValue');
|
||||
|
||||
import { getDragOperationType } from '../getDragOperationType';
|
||||
import { extractRecordPositions } from '../extractRecordPositions';
|
||||
import { processSingleDrag } from '../processSingleDrag';
|
||||
import { processMultiDrag } from '../processMultiDrag';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
const mockGetDragOperationType = getDragOperationType as jest.Mock;
|
||||
const mockExtractRecordPositions = extractRecordPositions as jest.Mock;
|
||||
const mockProcessSingleDrag = processSingleDrag as jest.Mock;
|
||||
const mockProcessMultiDrag = processMultiDrag as jest.Mock;
|
||||
const mockGetSnapshotValue = getSnapshotValue as jest.Mock;
|
||||
|
||||
describe('processGroupDragOperation', () => {
|
||||
const mockSnapshot = {} as Snapshot;
|
||||
const mockOnUpdateRecord = jest.fn();
|
||||
const mockRecordIdsByGroupFamilyState = jest.fn();
|
||||
|
||||
const createDropResult = (destination?: {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
}): DropResult => ({
|
||||
draggableId: 'record-1',
|
||||
type: 'DEFAULT',
|
||||
source: {
|
||||
droppableId: 'group-1',
|
||||
index: 0,
|
||||
},
|
||||
destination: destination ?? null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Early returns', () => {
|
||||
it('should return early when destination is not defined', () => {
|
||||
const result = createDropResult();
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetSnapshotValue).not.toHaveBeenCalled();
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when record group is not defined', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
|
||||
mockGetSnapshotValue.mockReturnValueOnce(undefined);
|
||||
|
||||
expect(() => {
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
}).toThrow('Record group is not defined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single drag operation', () => {
|
||||
it('should process single drag operation successfully', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = {
|
||||
value: 'in-progress',
|
||||
fieldMetadataId: 'field-1',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-2', 'record-3'];
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-2', position: 1 },
|
||||
{ recordId: 'record-3', position: 2 },
|
||||
];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue(mockRecordPositionData);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 1.5,
|
||||
groupValue: 'in-progress',
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetDragOperationType).toHaveBeenCalledWith({
|
||||
draggedRecordId: 'record-1',
|
||||
selectedRecordIds: ['record-1'],
|
||||
});
|
||||
|
||||
expect(mockProcessSingleDrag).toHaveBeenCalledWith({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: mockDestinationRecordIds,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledWith({
|
||||
recordId: 'record-1',
|
||||
position: 1.5,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return early when single drag has no position', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = { value: 'done', fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds = ['record-2'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: undefined,
|
||||
groupValue: 'done',
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi drag operation', () => {
|
||||
it('should process multi drag operation successfully', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = {
|
||||
value: 'in-review',
|
||||
fieldMetadataId: 'field-1',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-4', 'record-5'];
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-4', position: 1 },
|
||||
{ recordId: 'record-5', position: 2 },
|
||||
];
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue(mockRecordPositionData);
|
||||
mockGetDragOperationType.mockReturnValue('multi');
|
||||
mockProcessMultiDrag.mockReturnValue({
|
||||
recordUpdates: [
|
||||
{ recordId: 'record-1', position: 1.3 },
|
||||
{ recordId: 'record-2', position: 1.6 },
|
||||
{ recordId: 'record-3', position: 1.9 },
|
||||
],
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: 'priority',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetDragOperationType).toHaveBeenCalledWith({
|
||||
draggedRecordId: 'record-1',
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
expect(mockProcessMultiDrag).toHaveBeenCalledWith({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: mockDestinationRecordIds,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledTimes(3);
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(1, {
|
||||
recordId: 'record-1',
|
||||
position: 1.3,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(2, {
|
||||
recordId: 'record-2',
|
||||
position: 1.6,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(3, {
|
||||
recordId: 'record-3',
|
||||
position: 1.9,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty multi drag result', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 0 });
|
||||
const mockRecordGroup = { value: null, fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds: string[] = [];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('multi');
|
||||
mockProcessMultiDrag.mockReturnValue({
|
||||
recordUpdates: [],
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1', 'record-2'],
|
||||
selectFieldName: 'category',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group value handling', () => {
|
||||
it('should handle null group value correctly', () => {
|
||||
const result = createDropResult({ droppableId: 'no-group', index: 0 });
|
||||
const mockRecordGroup = { value: null, fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds = ['record-1'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 1,
|
||||
groupValue: null,
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledWith({
|
||||
recordId: 'record-1',
|
||||
position: 1,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct group value to processors', () => {
|
||||
const result = createDropResult({
|
||||
droppableId: 'custom-group',
|
||||
index: 2,
|
||||
});
|
||||
const mockRecordGroup = {
|
||||
value: 'custom-value',
|
||||
fieldMetadataId: 'field-custom',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-a', 'record-b', 'record-c'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 2.5,
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'customField',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockProcessSingleDrag).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupValue: 'custom-value',
|
||||
selectFieldName: 'customField',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+47
-189
@@ -1,211 +1,69 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { processMultiDrag } from '@/object-record/record-drag/shared/utils/processMultiDrag';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/record-drag/shared/utils/calculateDragPositions',
|
||||
() => ({
|
||||
calculateDragPositions: jest.fn(({ recordsToMove }) => {
|
||||
const positions: Record<string, number> = {};
|
||||
recordsToMove.forEach((id: string, index: number) => {
|
||||
positions[id] = 1500 + index * 500;
|
||||
});
|
||||
return positions;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
|
||||
describe('processMultiDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1000 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 3000 },
|
||||
{ recordId: 'record-4', position: 4000 },
|
||||
const recordsWithPosition: RecordWithPosition[] = [
|
||||
{ id: 'record-1', position: 1000 },
|
||||
{ id: 'record-2', position: 2000 },
|
||||
{ id: 'record-3', position: 3000 },
|
||||
{ id: 'record-4', position: 4000 },
|
||||
];
|
||||
|
||||
const createDropResult = (
|
||||
draggableId: string,
|
||||
destinationIndex: number | null,
|
||||
): DropResult => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'source',
|
||||
index: 0,
|
||||
},
|
||||
destination:
|
||||
destinationIndex !== null
|
||||
? {
|
||||
droppableId: 'destination',
|
||||
index: destinationIndex,
|
||||
}
|
||||
: null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
it('should process multi drag for table and drop at the end', () => {
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
draggedRecordId: 'record-2',
|
||||
targetRecordId: 'record-4',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ id: 'record-1', position: 4001 },
|
||||
{ id: 'record-2', position: 4002 },
|
||||
{ id: 'record-3', position: 4003 },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Table context (without group fields)', () => {
|
||||
it('should process multi drag for table', () => {
|
||||
const result = createDropResult('record-2', 3);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
it('should throw error when destination is null', () => {
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
expect(() => {
|
||||
processMultiDrag({
|
||||
draggedRecordId: 'record-1',
|
||||
targetRecordId: '',
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsWithPosition,
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 2500 },
|
||||
]);
|
||||
|
||||
dragResult.recordUpdates.forEach((update) => {
|
||||
expect(update).not.toHaveProperty('groupValue');
|
||||
expect(update).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const result = createDropResult('record-1', null);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
expect(() => {
|
||||
processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
});
|
||||
}).toThrow('Cannot find item to move for id : ');
|
||||
});
|
||||
|
||||
describe('Board context (with group fields)', () => {
|
||||
it('should process multi drag for board with group fields', () => {
|
||||
const result = createDropResult('record-2', 3);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
it('should handle single record in selection', () => {
|
||||
const selectedRecordIds = ['record-1'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 1500,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 2000,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-3',
|
||||
position: 2500,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
]);
|
||||
const dragResult = processMultiDrag({
|
||||
draggedRecordId: 'record-1',
|
||||
targetRecordId: 'record-3',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const result = createDropResult('record-1', 1);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 1500,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 2000,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ id: 'record-1', position: 3500 },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle single record in selection', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds = ['record-1'];
|
||||
it('should handle empty selection', () => {
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
]);
|
||||
const dragResult = processMultiDrag({
|
||||
draggedRecordId: 'record-1',
|
||||
targetRecordId: 'record-3',
|
||||
selectedRecordIds,
|
||||
recordsWithPosition,
|
||||
});
|
||||
|
||||
it('should handle empty selection', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not include group fields when only one is provided', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
]);
|
||||
|
||||
dragResult.recordUpdates.forEach((update) => {
|
||||
expect(update).not.toHaveProperty('groupValue');
|
||||
expect(update).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
});
|
||||
expect(dragResult.recordUpdates).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
+65
-136
@@ -1,156 +1,85 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/record-drag/shared/utils/calculateDragPositions',
|
||||
() => ({
|
||||
calculateDragPositions: jest.fn(({ recordsToMove }) => {
|
||||
const positions: Record<string, number> = {};
|
||||
recordsToMove.forEach((id: string, index: number) => {
|
||||
positions[id] = 1000 + index * 1000;
|
||||
});
|
||||
return positions;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
|
||||
describe('processSingleDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1000 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 3000 },
|
||||
const mockRecordPositionData: RecordWithPosition[] = [
|
||||
{ id: 'record-1', position: 1000 },
|
||||
{ id: 'record-2', position: 2000 },
|
||||
{ id: 'record-3', position: 3000 },
|
||||
];
|
||||
|
||||
const createDropResult = (
|
||||
draggableId: string,
|
||||
destinationIndex: number | null,
|
||||
): DropResult => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'source',
|
||||
index: 0,
|
||||
},
|
||||
destination:
|
||||
destinationIndex !== null
|
||||
? {
|
||||
droppableId: 'destination',
|
||||
index: destinationIndex,
|
||||
}
|
||||
: null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
});
|
||||
|
||||
describe('Table context (without group fields)', () => {
|
||||
it('should process single drag for table', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
expect(dragResult).not.toHaveProperty('groupValue');
|
||||
expect(dragResult).not.toHaveProperty('selectFieldName');
|
||||
it('should process single drag at end of list', () => {
|
||||
const dragResult = processSingleDrag({
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-3',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const result = createDropResult('record-1', null);
|
||||
|
||||
expect(() => {
|
||||
processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
expect(dragResult).toEqual({
|
||||
id: 'record-1',
|
||||
position: 3001,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Board context (with group fields)', () => {
|
||||
it('should process single drag for board with group fields', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
groupValue: 'group-1',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
it('should process single drag at beginning', () => {
|
||||
const dragResult = processSingleDrag({
|
||||
sourceRecordId: 'record-3',
|
||||
targetRecordId: 'record-1',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
expect(dragResult).toEqual({
|
||||
id: 'record-3',
|
||||
position: 999,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty record list', () => {
|
||||
const result = createDropResult('record-1', 0);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: [],
|
||||
recordIds: [],
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
it('should process single drag in between from before', () => {
|
||||
const dragResult = processSingleDrag({
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
});
|
||||
|
||||
it('should not include group fields when only one is provided', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
expect(dragResult).not.toHaveProperty('groupValue');
|
||||
expect(dragResult).not.toHaveProperty('selectFieldName');
|
||||
expect(dragResult).toEqual({
|
||||
id: 'record-1',
|
||||
position: 2500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should process single drag in between from after', () => {
|
||||
const dragResult = processSingleDrag({
|
||||
sourceRecordId: 'record-3',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
id: 'record-3',
|
||||
position: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should process single drag from other list', () => {
|
||||
const dragResult = processSingleDrag({
|
||||
sourceRecordId: 'record-4',
|
||||
targetRecordId: 'record-2',
|
||||
recordsWithPosition: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
id: 'record-4',
|
||||
position: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty record list', () => {
|
||||
expect(() =>
|
||||
processSingleDrag({
|
||||
sourceRecordId: 'record-1',
|
||||
targetRecordId: 'record-1',
|
||||
recordsWithPosition: [],
|
||||
}),
|
||||
).toThrowError('Cannot find item to move for id : record-1');
|
||||
});
|
||||
});
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-drag/shared/utils/getDraggedRecordPosition';
|
||||
import { getIndexNeighboursElementsFromArray } from '~/utils/array/getIndexNeighboursElementsFromArray';
|
||||
|
||||
type DragPositionCalculationParams = {
|
||||
recordIds: string[];
|
||||
recordsToMove: string[];
|
||||
destinationIndex: number;
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
};
|
||||
|
||||
export const calculateDragPositions = ({
|
||||
recordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
}: DragPositionCalculationParams): Record<string, number> => {
|
||||
const otherRecordIds = recordIds.filter(
|
||||
(recordId: string) => !recordsToMove.includes(recordId),
|
||||
);
|
||||
|
||||
const filteredRecordIds =
|
||||
recordsToMove.length === 1
|
||||
? otherRecordIds
|
||||
: recordIds.filter((recordId) => recordId !== recordsToMove[0]);
|
||||
|
||||
const { before: recordBeforeId, after: recordAfterId } =
|
||||
getIndexNeighboursElementsFromArray({
|
||||
index: destinationIndex,
|
||||
array: filteredRecordIds,
|
||||
});
|
||||
|
||||
const recordBefore = recordBeforeId
|
||||
? recordPositionData.find((r) => r.recordId === recordBeforeId)
|
||||
: null;
|
||||
|
||||
const recordAfter = recordAfterId
|
||||
? recordPositionData.find((r) => r.recordId === recordAfterId)
|
||||
: null;
|
||||
|
||||
const basePosition = getDraggedRecordPosition(
|
||||
recordBefore?.position,
|
||||
recordAfter?.position,
|
||||
);
|
||||
|
||||
const positions: Record<string, number> = {};
|
||||
|
||||
for (const [index, recordId] of recordsToMove.entries()) {
|
||||
if (recordsToMove.length > 1) {
|
||||
const availableSpace = recordAfter?.position
|
||||
? recordAfter.position - basePosition
|
||||
: 1;
|
||||
|
||||
const increment = availableSpace / (recordsToMove.length + 1);
|
||||
positions[recordId] = basePosition + (index + 1) * increment;
|
||||
} else {
|
||||
positions[recordId] = basePosition;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
};
|
||||
+4
-2
@@ -1,16 +1,18 @@
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { type Snapshot } from 'recoil';
|
||||
|
||||
export const extractRecordPositions = (
|
||||
recordIds: string[],
|
||||
snapshot: Snapshot,
|
||||
) => {
|
||||
): RecordWithPosition[] => {
|
||||
return recordIds.map((recordId) => {
|
||||
const record = snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordId))
|
||||
.getValue();
|
||||
|
||||
return {
|
||||
recordId,
|
||||
id: recordId,
|
||||
position: record?.position,
|
||||
};
|
||||
});
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
export const getDraggedRecordPosition = (
|
||||
recordBeforePosition?: number,
|
||||
recordAfterPosition?: number,
|
||||
): number => {
|
||||
if (isDefined(recordAfterPosition) && isDefined(recordBeforePosition)) {
|
||||
return (recordBeforePosition + recordAfterPosition) / 2;
|
||||
}
|
||||
|
||||
if (isDefined(recordAfterPosition)) {
|
||||
return recordAfterPosition - 1;
|
||||
}
|
||||
|
||||
if (isDefined(recordBeforePosition)) {
|
||||
return recordBeforePosition + 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
+42
-32
@@ -5,36 +5,34 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { extractRecordPositions } from './extractRecordPositions';
|
||||
import { getDragOperationType } from './getDragOperationType';
|
||||
import { processMultiDrag } from './processMultiDrag';
|
||||
import { processSingleDrag } from './processSingleDrag';
|
||||
|
||||
type ProcessGroupDragOperationParams = {
|
||||
result: DropResult;
|
||||
snapshot: Snapshot;
|
||||
selectedRecordIds: string[];
|
||||
selectFieldName: string;
|
||||
recordIdsByGroupFamilyState: any;
|
||||
onUpdateRecord: (update: {
|
||||
recordId: string;
|
||||
position?: number;
|
||||
groupValue?: string | null;
|
||||
selectFieldName: string;
|
||||
}) => void;
|
||||
onUpdateRecord: (
|
||||
update: { recordId: string; position?: number },
|
||||
targetRecordGroupId: RecordGroupDefinition['value'],
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const processGroupDragOperation = ({
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName,
|
||||
recordIdsByGroupFamilyState,
|
||||
onUpdateRecord,
|
||||
}: ProcessGroupDragOperationParams) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationGroupId = result.destination.droppableId;
|
||||
|
||||
const recordGroup = getSnapshotValue(
|
||||
@@ -51,7 +49,7 @@ export const processGroupDragOperation = ({
|
||||
recordIdsByGroupFamilyState(destinationGroupId),
|
||||
) as string[];
|
||||
|
||||
const recordPositionData = extractRecordPositions(
|
||||
const recordsWithPosition = extractRecordPositions(
|
||||
destinationRecordIds,
|
||||
snapshot,
|
||||
);
|
||||
@@ -62,42 +60,54 @@ export const processGroupDragOperation = ({
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
|
||||
const isDroppedAfterList = destinationIndex >= recordsWithPosition.length;
|
||||
|
||||
const targetRecord = isDroppedAfterList
|
||||
? recordsWithPosition.at(-1)
|
||||
: recordsWithPosition.at(result.destination.index);
|
||||
|
||||
if (!isDefined(targetRecord)) {
|
||||
throw new Error(
|
||||
`targetRecord cannot be found in passed recordsWithPosition, this should not happen.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dragOperationType === 'single') {
|
||||
const singleDragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds: destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
sourceRecordId: draggedRecordId,
|
||||
targetRecordId: targetRecord.id,
|
||||
recordsWithPosition: recordsWithPosition,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdateRecord({
|
||||
recordId: singleDragResult.recordId,
|
||||
position: singleDragResult.position,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
onUpdateRecord(
|
||||
{
|
||||
recordId: singleDragResult.id,
|
||||
position: singleDragResult.position,
|
||||
},
|
||||
recordGroup.value,
|
||||
);
|
||||
} else {
|
||||
const multiDragResult = processMultiDrag({
|
||||
result,
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
recordIds: destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
recordsWithPosition,
|
||||
targetRecordId: targetRecord.id,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
onUpdateRecord({
|
||||
recordId: update.recordId,
|
||||
position: update.position,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
onUpdateRecord(
|
||||
{
|
||||
recordId: update.id,
|
||||
position: update.position,
|
||||
},
|
||||
recordGroup.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+14
-48
@@ -1,62 +1,28 @@
|
||||
import { type MultiDragResult } from '@/object-record/record-drag/shared/types/MultiDragResult';
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { computeNewPositionsOfDraggedRecords } from '@/object-record/utils/computeNewPositionsOfDraggedRecords';
|
||||
|
||||
type MultiDragContext = {
|
||||
result: DropResult;
|
||||
draggedRecordId: string;
|
||||
targetRecordId: string;
|
||||
selectedRecordIds: string[];
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
recordIds: string[];
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
recordsWithPosition: RecordWithPosition[];
|
||||
};
|
||||
|
||||
export const processMultiDrag = ({
|
||||
result,
|
||||
draggedRecordId,
|
||||
targetRecordId,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
recordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
recordsWithPosition,
|
||||
}: MultiDragContext): MultiDragResult => {
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
|
||||
const positions = calculateDragPositions({
|
||||
recordIds,
|
||||
recordsToMove: selectedRecordIds,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
|
||||
const recordUpdates = selectedRecordIds.map((recordId) => {
|
||||
const baseUpdate = {
|
||||
recordId,
|
||||
position: positions[recordId],
|
||||
};
|
||||
|
||||
const shouldIncludeGroupFields =
|
||||
isDefined(selectFieldName) &&
|
||||
(isDefined(groupValue) || isNull(groupValue));
|
||||
|
||||
if (shouldIncludeGroupFields) {
|
||||
return {
|
||||
...baseUpdate,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
};
|
||||
}
|
||||
|
||||
return baseUpdate;
|
||||
const newPositionOfDraggedRecords = computeNewPositionsOfDraggedRecords({
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
draggedRecordId,
|
||||
targetRecordId,
|
||||
sourceRecordIds: selectedRecordIds,
|
||||
});
|
||||
|
||||
return {
|
||||
recordUpdates,
|
||||
recordUpdates: newPositionOfDraggedRecords ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
+18
-47
@@ -1,56 +1,27 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { type RecordDragUpdate } from '@/object-record/record-drag/shared/types/RecordDragUpdate';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeNewPositionOfDraggedRecord,
|
||||
type RecordWithPosition,
|
||||
} from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
|
||||
type SingleDragContext = {
|
||||
result: DropResult;
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
recordIds: string[];
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
targetRecordId: string;
|
||||
sourceRecordId: string;
|
||||
recordsWithPosition: RecordWithPosition[];
|
||||
};
|
||||
|
||||
export const processSingleDrag = ({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}: SingleDragContext): RecordDragUpdate => {
|
||||
const draggedRecordId = result.draggableId;
|
||||
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
const recordsToMove = [draggedRecordId];
|
||||
|
||||
const positions = calculateDragPositions({
|
||||
recordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
targetRecordId,
|
||||
sourceRecordId,
|
||||
recordsWithPosition,
|
||||
}: SingleDragContext): RecordWithPosition => {
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: recordsWithPosition,
|
||||
idOfItemToMove: sourceRecordId,
|
||||
idOfTargetItem: targetRecordId,
|
||||
});
|
||||
|
||||
const baseResult = {
|
||||
recordId: draggedRecordId,
|
||||
position: positions[draggedRecordId],
|
||||
return {
|
||||
id: sourceRecordId,
|
||||
position: newPosition,
|
||||
};
|
||||
|
||||
const shouldIncludeGroupFields =
|
||||
isDefined(selectFieldName) && (isDefined(groupValue) || isNull(groupValue));
|
||||
|
||||
if (shouldIncludeGroupFields) {
|
||||
return {
|
||||
...baseResult,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
};
|
||||
}
|
||||
|
||||
return baseResult;
|
||||
};
|
||||
|
||||
-1
@@ -80,7 +80,6 @@ export const useRecordTableGroupDragOperations = () => {
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: fieldMetadata.name,
|
||||
recordIdsByGroupFamilyState: recordIdsByGroupFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }) => {
|
||||
updateOneRow({
|
||||
|
||||
+66
-25
@@ -3,7 +3,6 @@ import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { getDragOperationType } from '@/object-record/record-drag/shared/utils/getDragOperationType';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
@@ -13,22 +12,26 @@ import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { extractRecordPositions } from '@/object-record/record-drag/shared/utils/extractRecordPositions';
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { processMultiDrag } from '@/object-record/record-drag/shared/utils/processMultiDrag';
|
||||
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { allRecordIdsWithoutGroupsComponentSelector } from '@/object-record/record-index/states/selectors/allRecordIdsWithoutGroupsComponentSelector';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { useResetVirtualizationBecauseDataChanged } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged';
|
||||
import { useTriggerFetchPages } from '@/object-record/record-table/virtualization/hooks/useTriggerFetchPages';
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRecordTableDragOperations = () => {
|
||||
export const useRecordTableWithoutGroupDragOperations = () => {
|
||||
const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackState(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
const allRecordIdsWithoutGroupCallbackSelector =
|
||||
useRecoilComponentCallbackState(allRecordIdsWithoutGroupsComponentSelector);
|
||||
|
||||
const selectedRowIdsSelector = useRecoilComponentCallbackState(
|
||||
selectedRowIdsComponentSelector,
|
||||
@@ -42,9 +45,14 @@ export const useRecordTableDragOperations = () => {
|
||||
const { openModal } = useModal();
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const processDragOperation = useRecoilCallback(
|
||||
const { resetVirtualization } =
|
||||
useResetVirtualizationBecauseDataChanged(objectNameSingular);
|
||||
|
||||
const { triggerFetchPagesWithoutDebounce } = useTriggerFetchPages();
|
||||
|
||||
const processDragOperationWithoutGroup = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult) => {
|
||||
async (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
if (currentRecordSorts.length > 0) {
|
||||
@@ -52,9 +60,9 @@ export const useRecordTableDragOperations = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecordIds = getSnapshotValue(
|
||||
const allSparseRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
allRecordIdsWithoutGroupCallbackSelector,
|
||||
);
|
||||
|
||||
const draggedRecordId = result.draggableId;
|
||||
@@ -63,10 +71,17 @@ export const useRecordTableDragOperations = () => {
|
||||
selectedRowIdsSelector,
|
||||
);
|
||||
|
||||
const recordPositionData = extractRecordPositions(
|
||||
allRecordIds,
|
||||
snapshot,
|
||||
);
|
||||
const recordsWithPosition: RecordWithPosition[] = allSparseRecordIds
|
||||
.filter(isDefined)
|
||||
.map((recordId) => ({
|
||||
id: recordId,
|
||||
position:
|
||||
getSnapshotValue(snapshot, recordStoreFamilyState(recordId))
|
||||
?.position ?? null,
|
||||
}));
|
||||
|
||||
const contiguousRecordsWithPosition =
|
||||
recordsWithPosition.filter(isDefined);
|
||||
|
||||
const dragOperationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
@@ -74,10 +89,20 @@ export const useRecordTableDragOperations = () => {
|
||||
});
|
||||
|
||||
if (dragOperationType === 'single') {
|
||||
const targetRecordId = allSparseRecordIds.at(
|
||||
result.destination.index,
|
||||
);
|
||||
|
||||
if (!isDefined(targetRecordId)) {
|
||||
throw new Error(
|
||||
`Target record id cannot be found, this should not happen`,
|
||||
);
|
||||
}
|
||||
|
||||
const singleDragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds: allRecordIds,
|
||||
sourceRecordId: draggedRecordId,
|
||||
targetRecordId: targetRecordId ?? '',
|
||||
recordsWithPosition: contiguousRecordsWithPosition,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
@@ -85,28 +110,42 @@ export const useRecordTableDragOperations = () => {
|
||||
}
|
||||
|
||||
updateOneRow({
|
||||
idToUpdate: singleDragResult.recordId,
|
||||
idToUpdate: singleDragResult.id,
|
||||
updateOneRecordInput: {
|
||||
position: singleDragResult.position,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const targetRecordId = allSparseRecordIds.at(
|
||||
result.destination.index,
|
||||
);
|
||||
|
||||
if (!isDefined(targetRecordId)) {
|
||||
throw new Error(
|
||||
`Target record id cannot be found, this should not happen`,
|
||||
);
|
||||
}
|
||||
|
||||
const multiDragResult = processMultiDrag({
|
||||
result,
|
||||
draggedRecordId,
|
||||
targetRecordId: targetRecordId ?? '',
|
||||
selectedRecordIds: multiDragState.originalSelection,
|
||||
recordPositionData,
|
||||
recordIds: allRecordIds,
|
||||
recordsWithPosition: contiguousRecordsWithPosition,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
updateOneRow({
|
||||
idToUpdate: update.recordId,
|
||||
idToUpdate: update.id,
|
||||
updateOneRecordInput: {
|
||||
position: update.position,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await resetVirtualization();
|
||||
|
||||
await triggerFetchPagesWithoutDebounce();
|
||||
},
|
||||
[
|
||||
selectedRowIdsSelector,
|
||||
@@ -114,9 +153,11 @@ export const useRecordTableDragOperations = () => {
|
||||
openModal,
|
||||
currentRecordSorts,
|
||||
multiDragState.originalSelection,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
allRecordIdsWithoutGroupCallbackSelector,
|
||||
resetVirtualization,
|
||||
triggerFetchPagesWithoutDebounce,
|
||||
],
|
||||
);
|
||||
|
||||
return { processDragOperation };
|
||||
return { processDragOperationWithoutGroup };
|
||||
};
|
||||
+6
-7
@@ -2,7 +2,7 @@ import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpda
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { computeNewPositionOfRecordWithPosition } from '@/object-record/utils/computeNewPositionOfRecordWithPosition';
|
||||
import { computeNewPositionOfDraggedRecord } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
@@ -35,12 +35,11 @@ export const useReorderVisibleRecordFields = (recordTableId: string) => {
|
||||
|
||||
const recordToMove = visibleRecordFields[fromIndex];
|
||||
|
||||
const newPositionOfTargetRecord =
|
||||
computeNewPositionOfRecordWithPosition({
|
||||
arrayOfRecordsWithPosition: currentRecordFields,
|
||||
idOfItemToMove: idOfRecordToMove,
|
||||
idOfTargetItem: idOfTargetRecord,
|
||||
});
|
||||
const newPositionOfTargetRecord = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: currentRecordFields,
|
||||
idOfItemToMove: idOfRecordToMove,
|
||||
idOfTargetItem: idOfTargetRecord,
|
||||
});
|
||||
|
||||
updateRecordField(recordToMove.fieldMetadataItemId, {
|
||||
position: newPositionOfTargetRecord,
|
||||
|
||||
+13
-4
@@ -9,6 +9,7 @@ import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui
|
||||
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import styled from '@emotion/styled';
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -38,7 +39,15 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
fieldValue as NoteTarget[] | TaskTarget[],
|
||||
);
|
||||
|
||||
if (!fieldValue || !relationObjectNameSingular) {
|
||||
if (!isDefined(fieldValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isArray(fieldValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isDefined(!relationObjectNameSingular)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,7 +74,7 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
return isFocused ? (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{fieldValue
|
||||
.map((record) => {
|
||||
?.map((record) => {
|
||||
if (!isDefined(record) || !isDefined(record[relationFieldName])) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -83,7 +92,7 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{fieldValue
|
||||
.map((record) => {
|
||||
?.map((record) => {
|
||||
if (!isDefined(record) || !isDefined(record[relationFieldName])) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -115,7 +124,7 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
} else {
|
||||
return (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{fieldValue.filter(isDefined).map((record) => {
|
||||
{fieldValue?.filter(isDefined).map((record) => {
|
||||
const recordChipData = generateRecordChipData(record);
|
||||
return (
|
||||
<RecordChip
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const lastShowPageRecordIdState = createState<string | null>({
|
||||
key: 'lastShowPageRecordIdState',
|
||||
defaultValue: null,
|
||||
|
||||
+7
-4
@@ -2,6 +2,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||
import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields';
|
||||
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
|
||||
|
||||
export const useRecordIndexTableFetchMore = (objectNameSingular: string) => {
|
||||
const params = useFindManyRecordIndexTableParams(objectNameSingular);
|
||||
|
||||
@@ -13,12 +14,14 @@ export const useRecordIndexTableFetchMore = (objectNameSingular: string) => {
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const { fetchMoreRecordsLazy, queryIdentifier } = useLazyFindManyRecords({
|
||||
...params,
|
||||
recordGqlFields,
|
||||
});
|
||||
const { fetchMoreRecordsLazy, queryIdentifier, findManyRecordsLazy } =
|
||||
useLazyFindManyRecords({
|
||||
...params,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
return {
|
||||
findManyRecordsLazy,
|
||||
fetchMoreRecordsLazy,
|
||||
queryIdentifier,
|
||||
};
|
||||
|
||||
+4
-4
@@ -17,18 +17,18 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const { records, hasNextPage, queryIdentifier, loading } = useFindManyRecords(
|
||||
{
|
||||
const { records, hasNextPage, queryIdentifier, loading, totalCount } =
|
||||
useFindManyRecords({
|
||||
...params,
|
||||
recordGqlFields,
|
||||
skip: showAuthModal,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
records: showAuthModal ? SIGN_IN_BACKGROUND_MOCK_COMPANIES : records,
|
||||
loading: showAuthModal ? false : loading,
|
||||
hasNextPage,
|
||||
queryIdentifier,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const allRecordIdsOfAllRecordGroupsComponentSelector =
|
||||
createComponentSelector<string[]>({
|
||||
key: 'recordIndexAllRecordIdsComponentSelector',
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const recordGroupIds = get(
|
||||
recordGroupIdsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (recordGroupIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return recordGroupIds.reduce<string[]>((acc, recordGroupId) => {
|
||||
const rowIds = get(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
|
||||
instanceId,
|
||||
familyKey: recordGroupId,
|
||||
}),
|
||||
);
|
||||
|
||||
return [...acc, ...rowIds];
|
||||
}, []);
|
||||
},
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { NO_RECORD_GROUP_FAMILY_KEY } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { type SparseArray } from '~/types/SparseArray';
|
||||
|
||||
export const allRecordIdsWithoutGroupsComponentSelector =
|
||||
createComponentSelector<SparseArray<string>>({
|
||||
key: 'allRecordIdsWithoutGroupsComponentSelector',
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
return get(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
|
||||
instanceId,
|
||||
familyKey: NO_RECORD_GROUP_FAMILY_KEY,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
+4
-8
@@ -4,14 +4,10 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
/**
|
||||
* Do not use this key outside of this file.
|
||||
* This is a temporary key to store the record ids for the default record group.
|
||||
*/
|
||||
const defaultFamilyKey = 'record-group-default-id';
|
||||
export const NO_RECORD_GROUP_FAMILY_KEY = 'record-group-default-id';
|
||||
|
||||
export const recordIndexAllRecordIdsComponentSelector = createComponentSelector<
|
||||
ObjectRecord['id'][]
|
||||
string[]
|
||||
>({
|
||||
key: 'recordIndexAllRecordIdsComponentSelector',
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
@@ -28,7 +24,7 @@ export const recordIndexAllRecordIdsComponentSelector = createComponentSelector<
|
||||
return get(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
|
||||
instanceId,
|
||||
familyKey: defaultFamilyKey,
|
||||
familyKey: NO_RECORD_GROUP_FAMILY_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -53,7 +49,7 @@ export const recordIndexAllRecordIdsComponentSelector = createComponentSelector<
|
||||
set(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({
|
||||
instanceId,
|
||||
familyKey: defaultFamilyKey,
|
||||
familyKey: NO_RECORD_GROUP_FAMILY_KEY,
|
||||
}),
|
||||
recordIds,
|
||||
),
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const recordIndexHasRecordsComponentSelector =
|
||||
createComponentSelector<boolean>({
|
||||
key: 'recordIndexHasRecordsComponentSelector',
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const allRecordIds = get(
|
||||
recordIndexAllRecordIdsComponentSelector.selectorFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return allRecordIds.length > 0;
|
||||
},
|
||||
});
|
||||
+4
-4
@@ -3,7 +3,7 @@ import { useRef } from 'react';
|
||||
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector';
|
||||
import { RecordTableBodyEffectsWrapper } from '@/object-record/record-table/components/RecordTableBodyEffectsWrapper';
|
||||
import { RecordTableContent } from '@/object-record/record-table/components/RecordTableContent';
|
||||
import { RecordTableEmpty } from '@/object-record/record-table/components/RecordTableEmpty';
|
||||
@@ -35,8 +35,8 @@ export const RecordTable = () => {
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
const recordTableHasRecords = useRecoilComponentValue(
|
||||
recordIndexHasRecordsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export const RecordTable = () => {
|
||||
const { resetTableRowSelection } = useResetTableRowSelection(recordTableId);
|
||||
|
||||
const recordTableIsEmpty =
|
||||
!isRecordTableInitialLoading && allRecordIds.length === 0;
|
||||
!isRecordTableInitialLoading && !recordTableHasRecords;
|
||||
|
||||
if (!isNonEmptyString(objectNameSingular)) {
|
||||
return <></>;
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
|
||||
import { hasRecordTableFetchedAllRecordsComponentState } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
|
||||
export const RecordTableAddNew = () => {
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
|
||||
const hasRecordTableFetchedAllRecords = useRecoilComponentValue(
|
||||
hasRecordTableFetchedAllRecordsComponentState,
|
||||
);
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
const hasAnySoftDeleteFilterOnView = useRecoilComponentValue(
|
||||
hasAnySoftDeleteFilterOnViewComponentSelector,
|
||||
);
|
||||
|
||||
if (hasAnySoftDeleteFilterOnView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasObjectUpdatePermissions || !hasRecordTableFetchedAllRecords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableActionRow
|
||||
onClick={() => {
|
||||
createNewIndexRecord({
|
||||
position: 'last',
|
||||
});
|
||||
}}
|
||||
LeftIcon={IconPlus}
|
||||
text={t`Add New`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+6
-2
@@ -2,8 +2,9 @@ import { RecordTableDeactivateRecordTableRowEffect } from '@/object-record/recor
|
||||
import { RecordTableBodyEscapeHotkeyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect';
|
||||
import { RecordTableBodyFocusClickOutsideEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect';
|
||||
import { RecordTableBodyFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect';
|
||||
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
|
||||
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
|
||||
import { RecordTableNoRecordGroupScrollToPreviousRecordEffect } from '@/object-record/record-table/virtualization/components/RecordTableNoRecordGroupScrollToPreviousRecordEffect';
|
||||
import { RecordTableVirtualizedInitialDataLoadEffect } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedInitialDataLoadEffect';
|
||||
|
||||
export interface RecordTableBodyEffectsWrapperProps {
|
||||
hasRecordGroups: boolean;
|
||||
@@ -19,7 +20,10 @@ export const RecordTableBodyEffectsWrapper = ({
|
||||
{hasRecordGroups ? (
|
||||
<RecordTableRecordGroupBodyEffects />
|
||||
) : (
|
||||
<RecordTableNoRecordGroupBodyEffect />
|
||||
<>
|
||||
<RecordTableNoRecordGroupScrollToPreviousRecordEffect />
|
||||
<RecordTableVirtualizedInitialDataLoadEffect />
|
||||
</>
|
||||
)}
|
||||
<RecordTableBodyEscapeHotkeyEffect />
|
||||
<RecordTableBodyFocusKeyboardEffect />
|
||||
|
||||
+18
-7
@@ -13,9 +13,10 @@ import { recordTableHoverPositionComponentState } from '@/object-record/record-t
|
||||
import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
@@ -52,6 +53,7 @@ export const RecordTableContent = ({
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setIsDragging(false);
|
||||
|
||||
handleDragSelectionEnd();
|
||||
};
|
||||
|
||||
@@ -76,15 +78,24 @@ export const RecordTableContent = ({
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const isSomeCellInEditMode = useRecoilComponentValue(
|
||||
const isSomeCellInEditModeCallbackState = useRecoilComponentCallbackState(
|
||||
isSomeCellInEditModeComponentSelector,
|
||||
);
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isSomeCellInEditMode) {
|
||||
setRecordTableHoverPosition(null);
|
||||
}
|
||||
};
|
||||
const handleMouseLeave = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const isSomeCellInEditMode = getSnapshotValue(
|
||||
snapshot,
|
||||
isSomeCellInEditModeCallbackState,
|
||||
);
|
||||
|
||||
if (!isSomeCellInEditMode) {
|
||||
setRecordTableHoverPosition(null);
|
||||
}
|
||||
},
|
||||
[isSomeCellInEditModeCallbackState, setRecordTableHoverPosition],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTableContainer ref={containerRef}>
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
|
||||
import { useAssignRecordsToStore } from '@/object-record/record-table/virtualization/hooks/useAssignRecordsToStore';
|
||||
import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows';
|
||||
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
|
||||
export const RecordTableNoRecordGroupAddNew = () => {
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
const hasAnySoftDeleteFilterOnView = useRecoilComponentValue(
|
||||
hasAnySoftDeleteFilterOnViewComponentSelector,
|
||||
);
|
||||
|
||||
const totalNumberOfRecordsToVirtualize = useRecoilComponentValue(
|
||||
totalNumberOfRecordsToVirtualizeComponentState,
|
||||
);
|
||||
|
||||
const { loadRecordsToVirtualRows } = useLoadRecordsToVirtualRows();
|
||||
const { assignRecordsToStore } = useAssignRecordsToStore();
|
||||
|
||||
const handleButtonClick = useRecoilCallback(
|
||||
() => async () => {
|
||||
const createdRecord = await createNewIndexRecord({
|
||||
position: 'last',
|
||||
});
|
||||
|
||||
assignRecordsToStore({ records: [createdRecord] });
|
||||
|
||||
if (isDefined(totalNumberOfRecordsToVirtualize)) {
|
||||
loadRecordsToVirtualRows({
|
||||
records: [createdRecord],
|
||||
startingRealIndex: totalNumberOfRecordsToVirtualize,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
createNewIndexRecord,
|
||||
assignRecordsToStore,
|
||||
loadRecordsToVirtualRows,
|
||||
totalNumberOfRecordsToVirtualize,
|
||||
],
|
||||
);
|
||||
|
||||
if (hasAnySoftDeleteFilterOnView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasObjectUpdatePermissions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableActionRow
|
||||
onClick={handleButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text={t`Add New`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+7
@@ -8,8 +8,10 @@ import {
|
||||
useOpenRecordTableCell,
|
||||
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
|
||||
import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
|
||||
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
|
||||
import { type MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
||||
import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type RecordTableNoRecordGroupBodyContextProviderProps = {
|
||||
@@ -56,6 +58,10 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
|
||||
triggerActionMenuDropdown(event, recordId);
|
||||
};
|
||||
|
||||
const hasUserSelectedAllRows = useRecoilComponentValue(
|
||||
hasUserSelectedAllRowsComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordTableBodyContextProvider
|
||||
value={{
|
||||
@@ -64,6 +70,7 @@ export const RecordTableNoRecordGroupBodyContextProvider = ({
|
||||
onCloseTableCell: handleCloseTableCell,
|
||||
onMoveHoverToCurrentCell: handleMoveHoverToCurrentCell,
|
||||
onActionMenuDropdownOpened: handleActionMenuDropdown,
|
||||
hasUserSelectedAllRows,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+23
-17
@@ -1,31 +1,37 @@
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableAddNew } from '@/object-record/record-table/components/RecordTableAddNew';
|
||||
import { RecordTableBodyDroppablePlaceholder } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppablePlaceholder';
|
||||
import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader';
|
||||
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
|
||||
import { RecordTableNoRecordGroupAddNew } from '@/object-record/record-table/components/RecordTableNoRecordGroupAddNew';
|
||||
import { RecordTableRowVirtualizedContainer } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedContainer';
|
||||
import { RecordTableVirtualizedBodyPlaceholder } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedBodyPlaceholder';
|
||||
import { RecordTableVirtualizedDebugHelper } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedDebugHelper';
|
||||
import { NUMBER_OF_VIRTUALIZED_ROWS } from '@/object-record/record-table/virtualization/constants/NumberOfVirtualizedRows';
|
||||
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getContiguousIncrementalValues } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableNoRecordGroupRows = () => {
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
const totalNumberOfRecordsToVirtualize =
|
||||
useRecoilComponentValue(totalNumberOfRecordsToVirtualizeComponentState) ??
|
||||
0;
|
||||
|
||||
const numberOfRows = Math.min(
|
||||
totalNumberOfRecordsToVirtualize,
|
||||
NUMBER_OF_VIRTUALIZED_ROWS,
|
||||
);
|
||||
|
||||
const virtualRowIndices = getContiguousIncrementalValues(numberOfRows);
|
||||
|
||||
return (
|
||||
<>
|
||||
{allRecordIds.map((recordId, rowIndex) => {
|
||||
<RecordTableVirtualizedBodyPlaceholder />
|
||||
{virtualRowIndices.map((virtualRowIndex) => {
|
||||
return (
|
||||
<RecordTableRow
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
rowIndexForFocus={rowIndex}
|
||||
rowIndexForDrag={rowIndex}
|
||||
isFirstRowOfGroup={rowIndex === 0}
|
||||
<RecordTableRowVirtualizedContainer
|
||||
key={virtualRowIndex}
|
||||
virtualIndex={virtualRowIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<RecordTableBodyFetchMoreLoader />
|
||||
<RecordTableBodyDroppablePlaceholder />
|
||||
<RecordTableAddNew />
|
||||
<RecordTableNoRecordGroupAddNew />
|
||||
<RecordTableVirtualizedDebugHelper />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-2
@@ -20,7 +20,7 @@ import { css, type Theme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
|
||||
const VerticalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
|
||||
export const VerticalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
|
||||
&::before {
|
||||
bottom: -1px;
|
||||
box-shadow:
|
||||
@@ -38,7 +38,11 @@ const VerticalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
|
||||
}
|
||||
`;
|
||||
|
||||
const HorizontalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
|
||||
export const HorizontalScrollBoxShadowCSS = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Theme;
|
||||
}) => css`
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -72,6 +76,8 @@ const StyledTable = styled.div<{
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
position: relative;
|
||||
|
||||
div.header-cell {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
-1
@@ -188,7 +188,6 @@ const meta: Meta = {
|
||||
mockPerformance.entityValue.__typename.toLocaleLowerCase(),
|
||||
}) + mockPerformance.recordId,
|
||||
isSelected: false,
|
||||
inView: true,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowDraggableContextProvider
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const ROW_HEIGHT = 32;
|
||||
+1
@@ -15,6 +15,7 @@ export type RecordTableBodyContextProps = {
|
||||
event: React.MouseEvent,
|
||||
recordId: string,
|
||||
) => void;
|
||||
hasUserSelectedAllRows?: boolean;
|
||||
};
|
||||
|
||||
export const [
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ export type RecordTableRowContextValue = {
|
||||
recordId: string;
|
||||
rowIndex: number;
|
||||
isSelected: boolean;
|
||||
inView: boolean;
|
||||
isRecordReadOnly?: boolean;
|
||||
};
|
||||
|
||||
|
||||
+34
@@ -8,7 +8,9 @@ import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/use
|
||||
import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell';
|
||||
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState';
|
||||
@@ -45,6 +47,18 @@ export const useSetRecordTableData = ({
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const isRecordTableInitialLoadingCallbackState =
|
||||
useRecoilComponentCallbackState(
|
||||
isRecordTableInitialLoadingComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const recordIdByRealIndexCallbackState =
|
||||
useRecoilComponentFamilyCallbackState(
|
||||
recordIdByRealIndexComponentFamilyState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const setRecordTableHoverPosition = useSetRecoilComponentState(
|
||||
recordTableHoverPositionComponentState,
|
||||
recordTableId,
|
||||
@@ -111,6 +125,24 @@ export const useSetRecordTableData = ({
|
||||
} else {
|
||||
set(recordIndexAllRecordIdsSelector, recordIds);
|
||||
}
|
||||
|
||||
const isTableInitialLoading = getSnapshotValue(
|
||||
snapshot,
|
||||
isRecordTableInitialLoadingCallbackState,
|
||||
);
|
||||
|
||||
if (isTableInitialLoading) {
|
||||
for (const [realIndex, recordId] of recordIds.entries()) {
|
||||
const currentRecordIdAtRealIndex = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIdByRealIndexCallbackState({ realIndex }),
|
||||
);
|
||||
|
||||
if (recordId !== currentRecordIdAtRealIndex) {
|
||||
set(recordIdByRealIndexCallbackState({ realIndex }), recordId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -121,6 +153,8 @@ export const useSetRecordTableData = ({
|
||||
unfocusRecordTableRow,
|
||||
setRecordTableHoverPosition,
|
||||
isRowSelectedFamilyState,
|
||||
recordIdByRealIndexCallbackState,
|
||||
isRecordTableInitialLoadingCallbackState,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
+4
-1
@@ -48,11 +48,12 @@ export const useCreateNewIndexRecord = ({
|
||||
.getLoadable(recordIndexOpenRecordInState)
|
||||
.getValue();
|
||||
|
||||
await createOneRecord({
|
||||
const createdRecord = await createOneRecord({
|
||||
id: recordId,
|
||||
...recordInputFromFilters,
|
||||
...recordInput,
|
||||
});
|
||||
|
||||
if (
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL &&
|
||||
canOpenObjectInSidePanel(objectMetadataItem.nameSingular)
|
||||
@@ -83,6 +84,8 @@ export const useCreateNewIndexRecord = ({
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdRecord;
|
||||
},
|
||||
[
|
||||
buildRecordInputFromFilters,
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useScrollTableToPosition = () => {
|
||||
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const scrollTableToPosition = useCallback(
|
||||
({
|
||||
horizontalScrollInPx,
|
||||
verticalScrollInPx,
|
||||
}: {
|
||||
verticalScrollInPx?: number;
|
||||
horizontalScrollInPx?: number;
|
||||
}) => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
|
||||
if (isDefined(verticalScrollInPx)) {
|
||||
scrollWrapperElement?.scrollTo({
|
||||
top: verticalScrollInPx,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(horizontalScrollInPx)) {
|
||||
scrollWrapperElement?.scrollTo({
|
||||
left: horizontalScrollInPx,
|
||||
});
|
||||
}
|
||||
},
|
||||
[getScrollWrapperElement],
|
||||
);
|
||||
|
||||
return { scrollTableToPosition };
|
||||
};
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type RecordTableBodyDroppableProps = {
|
||||
children: ReactNode;
|
||||
recordGroupId?: string;
|
||||
isDropDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecordTableBodyDroppable = ({
|
||||
children,
|
||||
recordGroupId,
|
||||
isDropDisabled,
|
||||
}: RecordTableBodyDroppableProps) => {
|
||||
const [v4Persistable] = useState(v4());
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={recordGroupId ?? v4Persistable}
|
||||
isDropDisabled={isDropDisabled}
|
||||
>
|
||||
{(provided) => (
|
||||
<>
|
||||
<RecordTableBody
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
>
|
||||
{children}
|
||||
</RecordTableBodyDroppableContextProvider>
|
||||
</RecordTableBody>
|
||||
</>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useRecordIndexTableFetchMore } from '@/object-record/record-index/hooks/useRecordIndexTableFetchMore';
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { hasRecordTableFetchedAllRecordsComponentState } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentState';
|
||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { GRAY_SCALE } from 'twenty-ui/theme';
|
||||
|
||||
const StyledText = styled.div`
|
||||
align-items: center;
|
||||
box-shadow: none;
|
||||
color: ${GRAY_SCALE.gray40};
|
||||
display: flex;
|
||||
height: ${RECORD_TABLE_ROW_HEIGHT}px;
|
||||
margin-left: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RecordTableBodyFetchMoreLoader = () => {
|
||||
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
|
||||
|
||||
const { fetchMoreRecordsLazy } =
|
||||
useRecordIndexTableFetchMore(objectNameSingular);
|
||||
|
||||
const [isFetchingMoreRecords, setIsFetchingMoreRecords] = useRecoilState(
|
||||
isFetchingMoreRecordsFamilyState(recordTableId),
|
||||
);
|
||||
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const hasRecordTableFetchedAllRecordsComponents = useRecoilComponentValue(
|
||||
hasRecordTableFetchedAllRecordsComponentState,
|
||||
);
|
||||
|
||||
const showLoadingMoreRow = !hasRecordTableFetchedAllRecordsComponents;
|
||||
|
||||
const { ref: tbodyRef } = useInView({
|
||||
onChange: async (inView) => {
|
||||
if (isFetchingMoreRecords || !inView) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFetchingMoreRecords(true);
|
||||
await fetchMoreRecordsLazy();
|
||||
setIsFetchingMoreRecords(false);
|
||||
},
|
||||
delay: 1000,
|
||||
rootMargin: '1000px',
|
||||
root: scrollWrapperHTMLElement,
|
||||
});
|
||||
|
||||
if (!showLoadingMoreRow) {
|
||||
return <></>;
|
||||
}
|
||||
// TODO: fix here styling
|
||||
|
||||
return (
|
||||
<div ref={tbodyRef}>
|
||||
<div>
|
||||
<StyledText>Loading more...</StyledText>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-2
@@ -14,7 +14,7 @@ export const RecordTableBodyLoading = () => {
|
||||
|
||||
return (
|
||||
<RecordTableBody>
|
||||
{Array.from({ length: 50 }).map((_, rowIndex) => (
|
||||
{Array.from({ length: 80 }).map((_, rowIndex) => (
|
||||
<RecordTableRowContextProvider
|
||||
key={rowIndex}
|
||||
value={{
|
||||
@@ -23,7 +23,6 @@ export const RecordTableBodyLoading = () => {
|
||||
recordId: `${rowIndex}`,
|
||||
rowIndex,
|
||||
isSelected: false,
|
||||
inView: true,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowDraggableContextProvider
|
||||
|
||||
+6
-5
@@ -12,10 +12,10 @@ import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
|
||||
import { useRecordTableDragOperations } from '@/object-record/record-drag/table/hooks/useRecordTableDragOperations';
|
||||
import { useRecordTableWithoutGroupDragOperations } from '@/object-record/record-drag/table/hooks/useRecordTableWithoutGroupDragOperations';
|
||||
import { selectedRowIdsComponentSelector } from '../../states/selectors/selectedRowIdsComponentSelector';
|
||||
|
||||
export const RecordTableBodyDragDropContextProvider = ({
|
||||
export const RecordTableBodyNoRecordGroupDragDropContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
@@ -29,7 +29,8 @@ export const RecordTableBodyDragDropContextProvider = ({
|
||||
|
||||
const { startDrag } = useStartRecordDrag('table', recordTableId);
|
||||
const { endDrag } = useEndRecordDrag('table', recordTableId);
|
||||
const { processDragOperation } = useRecordTableDragOperations();
|
||||
const { processDragOperationWithoutGroup } =
|
||||
useRecordTableWithoutGroupDragOperations();
|
||||
|
||||
const handleDragStart = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
@@ -46,10 +47,10 @@ export const RecordTableBodyDragDropContextProvider = ({
|
||||
|
||||
const handleDragEnd = useRecoilCallback(
|
||||
() => (result: DropResult) => {
|
||||
processDragOperation(result);
|
||||
processDragOperationWithoutGroup(result);
|
||||
endDrag();
|
||||
},
|
||||
[endDrag, processDragOperation],
|
||||
[endDrag, processDragOperationWithoutGroup],
|
||||
);
|
||||
|
||||
return (
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableBodyVirtualizedDraggableClone } from '@/object-record/record-table/record-table-body/components/RecordTableBodyVirtualizedDraggableClone';
|
||||
import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type RecordTableBodyNoRecordGroupDroppableProps = {
|
||||
children: ReactNode;
|
||||
isDropDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecordTableBodyNoRecordGroupDroppable = ({
|
||||
children,
|
||||
isDropDisabled,
|
||||
}: RecordTableBodyNoRecordGroupDroppableProps) => {
|
||||
const [v4Persistable] = useState(v4());
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={v4Persistable}
|
||||
isDropDisabled={isDropDisabled}
|
||||
mode={'virtual'}
|
||||
renderClone={(draggableProvided, draggableSnapshot, rubric) => (
|
||||
<RecordTableBodyVirtualizedDraggableClone
|
||||
draggableProvided={draggableProvided}
|
||||
draggableSnapshot={draggableSnapshot}
|
||||
rubric={rubric}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided) => (
|
||||
<RecordTableBody
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
>
|
||||
{children}
|
||||
</RecordTableBodyDroppableContextProvider>
|
||||
</RecordTableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type RecordTableBodyRecordGroupDroppableProps = {
|
||||
children: ReactNode;
|
||||
recordGroupId: string;
|
||||
isDropDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecordTableBodyRecordGroupDroppable = ({
|
||||
children,
|
||||
recordGroupId,
|
||||
isDropDisabled,
|
||||
}: RecordTableBodyRecordGroupDroppableProps) => {
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={recordGroupId}
|
||||
isDropDisabled={isDropDisabled}
|
||||
mode={'standard'}
|
||||
>
|
||||
{(provided) => (
|
||||
<RecordTableBody
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
>
|
||||
{children}
|
||||
</RecordTableBodyDroppableContextProvider>
|
||||
</RecordTableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { HorizontalScrollBoxShadowCSS } from '@/object-record/record-table/components/RecordTableStyleWrapper';
|
||||
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
|
||||
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
|
||||
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
|
||||
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidthClassName';
|
||||
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
|
||||
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidthClassName';
|
||||
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthClassName';
|
||||
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthVariableName';
|
||||
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthVariableName';
|
||||
import { RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE } from '@/object-record/record-table/constants/RecordTableLabelIdentifierColumnWidthOnMobile';
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { useRecordTableLastColumnWidthToFill } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill';
|
||||
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
|
||||
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
|
||||
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
|
||||
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
|
||||
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
|
||||
import { getRecordTableColumnFieldWidthCSSVariableName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthCSSVariableName';
|
||||
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
type DraggableProvided,
|
||||
type DraggableRubric,
|
||||
type DraggableStateSnapshot,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
|
||||
// TODO: see how we can merge this with RecordTableStyleWrapper,
|
||||
// because we have not decided a strategy for sharing CSS bits yet
|
||||
const StyledRowDraggableCloneCSSBridge = styled.div<{
|
||||
visibleRecordFields: RecordField[];
|
||||
lastColumnWidth: number;
|
||||
}>`
|
||||
div.table-cell:nth-of-type(1) {
|
||||
position: sticky;
|
||||
left: 0px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.withGroups.sticky};
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.withoutGroups.sticky};
|
||||
}
|
||||
|
||||
div.table-cell-0-0 {
|
||||
position: sticky;
|
||||
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
|
||||
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
|
||||
|
||||
${HorizontalScrollBoxShadowCSS}
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(3) {
|
||||
position: sticky;
|
||||
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
|
||||
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.withoutGroups.sticky};
|
||||
|
||||
${HorizontalScrollBoxShadowCSS}
|
||||
}
|
||||
|
||||
div.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
|
||||
width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
|
||||
min-width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
|
||||
max-width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
|
||||
}
|
||||
|
||||
div.${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME} {
|
||||
width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
|
||||
min-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
|
||||
max-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
|
||||
}
|
||||
|
||||
div.${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME} {
|
||||
width: ${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH}px;
|
||||
min-width: ${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH}px;
|
||||
max-width: ${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH}px;
|
||||
}
|
||||
|
||||
${({ visibleRecordFields, lastColumnWidth }) => {
|
||||
let returnedCSS = '';
|
||||
|
||||
for (let i = 0; i < visibleRecordFields.length; i++) {
|
||||
returnedCSS += `--record-table-column-field-${i}: ${visibleRecordFields[i].size}px; \n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < visibleRecordFields.length; i++) {
|
||||
returnedCSS += `div.${getRecordTableColumnFieldWidthClassName(i)} {
|
||||
width: var(${getRecordTableColumnFieldWidthCSSVariableName(i)});
|
||||
min-width: var(${getRecordTableColumnFieldWidthCSSVariableName(i)});
|
||||
max-width: var(${getRecordTableColumnFieldWidthCSSVariableName(i)});
|
||||
} \n`;
|
||||
|
||||
const isLabelIdentifierColumn = i === 0;
|
||||
|
||||
if (isLabelIdentifierColumn) {
|
||||
returnedCSS += `div.${getRecordTableColumnFieldWidthClassName(i)} {
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: ${RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE}px;
|
||||
max-width: ${RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE}px;
|
||||
min-width: ${RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE}px;
|
||||
}
|
||||
} \n`;
|
||||
}
|
||||
}
|
||||
|
||||
returnedCSS += `${RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME}: ${lastColumnWidth}px;`;
|
||||
returnedCSS += `${RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME}: ${lastColumnWidth}px;`;
|
||||
|
||||
return returnedCSS;
|
||||
}};
|
||||
|
||||
div.${RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME} {
|
||||
width: var(${RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME});
|
||||
min-width: var(
|
||||
${RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME}
|
||||
);
|
||||
max-width: var(
|
||||
${RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME}
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTableBodyVirtualizedDraggableClone = ({
|
||||
draggableProvided,
|
||||
draggableSnapshot,
|
||||
rubric,
|
||||
}: {
|
||||
draggableProvided: DraggableProvided;
|
||||
draggableSnapshot: DraggableStateSnapshot;
|
||||
rubric: DraggableRubric;
|
||||
}) => {
|
||||
const realIndex = rubric.source.index;
|
||||
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const recordId = useRecoilComponentFamilyValue(
|
||||
recordIdByRealIndexComponentFamilyState,
|
||||
{ realIndex },
|
||||
);
|
||||
|
||||
const { lastColumnWidth } = useRecordTableLastColumnWidthToFill();
|
||||
|
||||
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSecondaryDragged =
|
||||
multiDragState?.isDragging &&
|
||||
multiDragState.originalSelection.includes(recordId) &&
|
||||
recordId !== multiDragState.primaryDraggedRecordId;
|
||||
|
||||
return (
|
||||
<StyledRowDraggableCloneCSSBridge
|
||||
lastColumnWidth={lastColumnWidth}
|
||||
visibleRecordFields={visibleRecordFields}
|
||||
>
|
||||
<RecordTableTr
|
||||
recordId={recordId}
|
||||
focusIndex={realIndex}
|
||||
ref={draggableProvided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
background: draggableSnapshot.isDragging
|
||||
? theme.background.transparent.light
|
||||
: undefined,
|
||||
borderColor: draggableSnapshot.isDragging
|
||||
? `${theme.border.color.medium}`
|
||||
: 'transparent',
|
||||
opacity: isSecondaryDragged ? 0.3 : undefined,
|
||||
}}
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
data-testid={`row-id-${recordId}`}
|
||||
data-virtualized-id={recordId}
|
||||
data-selectable-id={recordId}
|
||||
onClick={() => {}}
|
||||
isFirstRowOfGroup={false}
|
||||
>
|
||||
<RecordTableRowDraggableContextProvider
|
||||
value={{
|
||||
isDragging: draggableSnapshot.isDragging,
|
||||
dragHandleProps: draggableProvided.dragHandleProps,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellDragAndDrop />
|
||||
<RecordTableCellCheckbox />
|
||||
<RecordTableFieldsCells />
|
||||
<RecordTablePlusButtonCellPlaceholder />
|
||||
<RecordTableLastEmptyCell />
|
||||
<RecordTableRowMultiDragPreview
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
/>
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableTr>
|
||||
</StyledRowDraggableCloneCSSBridge>
|
||||
);
|
||||
};
|
||||
+16
-11
@@ -1,38 +1,43 @@
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector';
|
||||
import { RecordTableNoRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider';
|
||||
import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows';
|
||||
import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider';
|
||||
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
|
||||
|
||||
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
|
||||
import { RecordTableBodyNoRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyNoRecordGroupDragDropContextProvider';
|
||||
import { RecordTableBodyNoRecordGroupDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyNoRecordGroupDroppable';
|
||||
import { RecordTableCellPortals } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortals';
|
||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { RecordTableVirtualizedDataChangedEffect } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedDataChangedEffect';
|
||||
import { RecordTableVirtualizedRowTreadmillEffect } from '@/object-record/record-table/virtualization/components/RecordTableVirtualizedRowTreadmillEffect';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
export const RecordTableNoRecordGroupBody = () => {
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
const recordTableHasRecords = useRecoilComponentValue(
|
||||
recordIndexHasRecordsComponentSelector,
|
||||
);
|
||||
|
||||
const isRecordTableInitialLoading = useRecoilComponentValue(
|
||||
isRecordTableInitialLoadingComponentState,
|
||||
);
|
||||
|
||||
if (isRecordTableInitialLoading && allRecordIds.length === 0) {
|
||||
if (isRecordTableInitialLoading && !recordTableHasRecords) {
|
||||
return <RecordTableBodyLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableNoRecordGroupBodyContextProvider>
|
||||
<RecordTableBodyDragDropContextProvider>
|
||||
<RecordTableBodyDroppable>
|
||||
<RecordTableBodyNoRecordGroupDragDropContextProvider>
|
||||
<RecordTableBodyNoRecordGroupDroppable>
|
||||
<RecordTableNoRecordGroupRows />
|
||||
<RecordTableCellPortals />
|
||||
</RecordTableBodyDroppable>
|
||||
{!isRecordTableInitialLoading && allRecordIds.length > 0 && (
|
||||
</RecordTableBodyNoRecordGroupDroppable>
|
||||
{!isRecordTableInitialLoading && recordTableHasRecords && (
|
||||
<RecordTableAggregateFooter />
|
||||
)}
|
||||
</RecordTableBodyDragDropContextProvider>
|
||||
<RecordTableVirtualizedRowTreadmillEffect />
|
||||
<RecordTableVirtualizedDataChangedEffect />
|
||||
</RecordTableBodyNoRecordGroupDragDropContextProvider>
|
||||
</RecordTableNoRecordGroupBodyContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
|
||||
import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
|
||||
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
|
||||
import { hasRecordTableFetchedAllRecordsComponentState } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentState';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
export const RecordTableNoRecordGroupBodyEffect = () => {
|
||||
const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { records, loading, hasNextPage } =
|
||||
useRecordIndexTableQuery(objectNameSingular);
|
||||
|
||||
const setRecordTableData = useSetRecordTableData({
|
||||
recordTableId,
|
||||
});
|
||||
|
||||
const showAuthModal = useShowAuthModal();
|
||||
|
||||
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
||||
|
||||
const setHasRecordTableFetchedAllRecordsComponents =
|
||||
useSetRecoilComponentState(hasRecordTableFetchedAllRecordsComponentState);
|
||||
const setIsRecordTableInitialLoading = useSetRecoilComponentState(
|
||||
isRecordTableInitialLoadingComponentState,
|
||||
);
|
||||
const isFetchingMoreRecords = useRecoilValue(
|
||||
isFetchingMoreRecordsFamilyState(recordTableId),
|
||||
);
|
||||
|
||||
const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState);
|
||||
|
||||
const { scrollToPosition } = useScrollToPosition();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !isFetchingMoreRecords) {
|
||||
setRecordTableData({
|
||||
records,
|
||||
});
|
||||
setHasRecordTableFetchedAllRecordsComponents(!hasNextPage);
|
||||
setIsRecordTableInitialLoading(false);
|
||||
}
|
||||
}, [
|
||||
hasNextPage,
|
||||
isFetchingMoreRecords,
|
||||
loading,
|
||||
records,
|
||||
setHasRecordTableFetchedAllRecordsComponents,
|
||||
setIsRecordTableInitialLoading,
|
||||
setRecordTableData,
|
||||
showAuthModal,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitializedScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(lastShowPageRecordId)) {
|
||||
const isRecordAlreadyFetched = records.some(
|
||||
(record) => record.id === lastShowPageRecordId,
|
||||
);
|
||||
|
||||
if (isRecordAlreadyFetched) {
|
||||
const recordPosition = records.findIndex(
|
||||
(record) => record.id === lastShowPageRecordId,
|
||||
);
|
||||
|
||||
const positionInPx = recordPosition * ROW_HEIGHT;
|
||||
|
||||
scrollToPosition(positionInPx);
|
||||
|
||||
setHasInitializedScroll(true);
|
||||
}
|
||||
}
|
||||
}, [hasInitializedScroll, lastShowPageRecordId, records, scrollToPosition]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
+3
-2
@@ -5,7 +5,8 @@ import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/state
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
|
||||
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
|
||||
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
|
||||
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
@@ -67,7 +68,7 @@ export const RecordTableRecordGroupBodyEffect = () => {
|
||||
);
|
||||
|
||||
if (recordPosition !== -1) {
|
||||
const positionInPx = recordPosition * ROW_HEIGHT;
|
||||
const positionInPx = recordPosition * RECORD_TABLE_ROW_HEIGHT;
|
||||
|
||||
scrollToPosition(positionInPx);
|
||||
}
|
||||
|
||||
+5
-3
@@ -3,9 +3,9 @@ import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/re
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
|
||||
import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows';
|
||||
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
|
||||
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
|
||||
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
||||
import { RecordTableBodyRecordGroupDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDroppable';
|
||||
import { RecordTableCellPortals } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortals';
|
||||
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
@@ -40,11 +40,13 @@ export const RecordTableRecordGroupsBody = () => {
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||
<RecordTableBodyRecordGroupDroppable
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordTableRecordGroupSection />
|
||||
<RecordTableRecordGroupRows />
|
||||
{index === 0 && <RecordTableCellPortals />}
|
||||
</RecordTableBodyDroppable>
|
||||
</RecordTableBodyRecordGroupDroppable>
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
))}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
|
||||
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidthClassName';
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellStyleWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellStyleWrapper';
|
||||
import { Checkbox } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: ${RECORD_TABLE_ROW_HEIGHT}px;
|
||||
justify-content: center;
|
||||
min-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH};
|
||||
width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
// TODO: refactor
|
||||
const StyledRecordTableTd = styled(RecordTableCellStyleWrapper)`
|
||||
border-left: 1px solid transparent;
|
||||
`;
|
||||
|
||||
export const RecordTableCellCheckboxPlaceholder = () => {
|
||||
const { hasUserSelectedAllRows } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
return (
|
||||
<StyledRecordTableTd
|
||||
isSelected={hasUserSelectedAllRows}
|
||||
hasRightBorder={false}
|
||||
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
|
||||
>
|
||||
<StyledContainer data-select-disable>
|
||||
<Checkbox hoverable checked={hasUserSelectedAllRows === true} />
|
||||
</StyledContainer>
|
||||
</StyledRecordTableTd>
|
||||
);
|
||||
};
|
||||
+15
-2
@@ -1,17 +1,30 @@
|
||||
import { RecordTableCellSkeletonLoader } from '@/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader';
|
||||
import { RecordTableCellStyleWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellStyleWrapper';
|
||||
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
|
||||
import { type Theme, useTheme } from '@emotion/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledStaticCellSkeleton = styled.div<{ theme: Theme }>`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
`;
|
||||
|
||||
export const RecordTableCellLoading = ({
|
||||
recordFieldIndex,
|
||||
isSelected = false,
|
||||
}: {
|
||||
recordFieldIndex: number;
|
||||
isSelected?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<RecordTableCellStyleWrapper
|
||||
widthClassName={getRecordTableColumnFieldWidthClassName(recordFieldIndex)}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<RecordTableCellSkeletonLoader />
|
||||
<StyledStaticCellSkeleton theme={theme} />
|
||||
</RecordTableCellStyleWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -50,7 +50,6 @@ export const RecordTableCellPortalContexts = ({
|
||||
recordId,
|
||||
rowIndex: position.row,
|
||||
isSelected: false,
|
||||
inView: true,
|
||||
pathToShowPage:
|
||||
getBasePathToShowPage({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@ export const StyledCell = styled.div<{
|
||||
hasBottomBorder && !isDragging ? borderColor : 'transparent'};
|
||||
|
||||
color: ${({ fontColor }) => fontColor};
|
||||
border-right: ${({ borderColor, hasRightBorder, isDragging }) =>
|
||||
hasRightBorder && !isDragging ? `1px solid ${borderColor}` : 'none'};
|
||||
border-right: ${({ borderColor, hasRightBorder }) =>
|
||||
hasRightBorder ? `1px solid ${borderColor}` : 'none'};
|
||||
|
||||
padding: 0;
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellStyleWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellStyleWrapper';
|
||||
|
||||
export const RecordTablePlusButtonCellPlaceholder = () => {
|
||||
const { isSelected } = useRecordTableRowContextOrThrow();
|
||||
|
||||
return (
|
||||
<RecordTableCellStyleWrapper
|
||||
isSelected={isSelected}
|
||||
hasRightBorder={false}
|
||||
widthClassName={RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-1
@@ -8,7 +8,6 @@ export const recordTableRowContextValue: RecordTableRowContextValue = {
|
||||
recordId: 'recordId',
|
||||
pathToShowPage: '/',
|
||||
objectNameSingular: 'objectNameSingular',
|
||||
inView: true,
|
||||
};
|
||||
|
||||
export const recordTableRowDraggableContextValue: RecordTableRowDraggableContextValue = {
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableCellsEmpty } from '@/object-record/record-table/record-table-row/components/RecordTableCellsEmpty';
|
||||
import { RecordTableCellsVisible } from '@/object-record/record-table/record-table-row/components/RecordTableCellsVisible';
|
||||
|
||||
export const RecordTableCells = () => {
|
||||
const { inView } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
|
||||
|
||||
const areCellsVisible = inView || isDragging;
|
||||
|
||||
return areCellsVisible ? (
|
||||
<RecordTableCellsVisible />
|
||||
) : (
|
||||
<RecordTableCellsEmpty />
|
||||
);
|
||||
};
|
||||
-2
@@ -7,7 +7,6 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { RecordTableTrEffect } from '@/object-record/record-table/record-table-row/components/RecordTableTrEffect';
|
||||
|
||||
type RecordTableDraggableTrProps = {
|
||||
className?: string;
|
||||
@@ -45,7 +44,6 @@ export const RecordTableDraggableTr = ({
|
||||
>
|
||||
{(draggableProvided, draggableSnapshot) => (
|
||||
<>
|
||||
<RecordTableTrEffect recordId={recordId} />
|
||||
<RecordTableTr
|
||||
recordId={recordId}
|
||||
focusIndex={focusIndex}
|
||||
|
||||
+1
-2
@@ -7,7 +7,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { RecordTableTrEffect } from '@/object-record/record-table/record-table-row/components/RecordTableTrEffect';
|
||||
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
@@ -51,7 +51,6 @@ export const RecordTableDraggableTrFirstRowOfGroup = ({
|
||||
>
|
||||
{(draggableProvided, draggableSnapshot) => (
|
||||
<>
|
||||
<RecordTableTrEffect recordId={recordId} />
|
||||
<RecordTableTr
|
||||
recordId={recordId}
|
||||
focusIndex={focusIndex}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableCellsVisible = () => {
|
||||
export const RecordTableFieldsCells = () => {
|
||||
const { isSelected, rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
|
||||
+4
-3
@@ -3,9 +3,10 @@ import { RecordTableCellCheckbox } from '@/object-record/record-table/record-tab
|
||||
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
|
||||
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
|
||||
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
|
||||
import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells';
|
||||
|
||||
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
|
||||
import { RecordTableDraggableTrFirstRowOfGroup } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTrFirstRowOfGroup';
|
||||
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
|
||||
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
|
||||
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
@@ -54,7 +55,7 @@ export const RecordTableRow = ({
|
||||
)}
|
||||
<RecordTableCellDragAndDrop />
|
||||
<RecordTableCellCheckbox />
|
||||
<RecordTableCells />
|
||||
<RecordTableFieldsCells />
|
||||
<RecordTablePlusButtonCellPlaceholder />
|
||||
<RecordTableLastEmptyCell />
|
||||
<ListenRecordUpdatesEffect
|
||||
@@ -77,7 +78,7 @@ export const RecordTableRow = ({
|
||||
)}
|
||||
<RecordTableCellDragAndDrop />
|
||||
<RecordTableCellCheckbox />
|
||||
<RecordTableCells />
|
||||
<RecordTableFieldsCells />
|
||||
<RecordTablePlusButtonCellPlaceholder />
|
||||
<RecordTableLastEmptyCell />
|
||||
<ListenRecordUpdatesEffect
|
||||
|
||||
+2
-49
@@ -36,26 +36,17 @@ const StyledTr = styled.div<{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&[data-next-row-active-or-focused='true'] {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-focused='true'] {
|
||||
&[data-focused='true'],
|
||||
&[data-active='true'] {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-color: ${({ theme }) => theme.border.color.medium};
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
margin-left: -1px;
|
||||
|
||||
@@ -70,44 +61,6 @@ const StyledTr = styled.div<{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active='true'] {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
background-color: ${({ theme }) => theme.accent.quaternary};
|
||||
|
||||
z-index: var(--z-index-for-normal-cells);
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
border-left: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm} 0 0
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
margin-left: -1px;
|
||||
|
||||
div {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
z-index: var(--z-index-for-sticky-cells);
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
z-index: var(--z-index-for-sticky-cells);
|
||||
}
|
||||
&:nth-of-type(1) {
|
||||
z-index: var(--z-index-for-sticky-cells);
|
||||
}
|
||||
&:last-of-type {
|
||||
border-right: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-radius: 0 ${({ theme }) => theme.border.radius.sm}
|
||||
${({ theme }) => theme.border.radius.sm} 0;
|
||||
z-index: var(--z-index-for-normal-cells);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTableRowDiv = StyledTr;
|
||||
|
||||
+5
-13
@@ -1,16 +1,15 @@
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableFirstRowOfGroup } from '@/object-record/record-table/record-table-row/components/RecordTableFirstRowOfGroup';
|
||||
import { RecordTableRowDiv } from '@/object-record/record-table/record-table-row/components/RecordTableRowDiv';
|
||||
import { isRecordIdFirstOfGroupComponentFamilySelector } from '@/object-record/record-table/record-table-row/states/isRecordIdFirstOfGroupComponentFamilySelector';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { forwardRef, type ReactNode } from 'react';
|
||||
@@ -45,11 +44,6 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
|
||||
recordId,
|
||||
);
|
||||
|
||||
const isRowVisible = useRecoilComponentFamilyValue(
|
||||
isRowVisibleComponentFamilyState,
|
||||
recordId,
|
||||
);
|
||||
|
||||
const isActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
focusIndex,
|
||||
@@ -60,15 +54,14 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
|
||||
focusIndex + 1,
|
||||
);
|
||||
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
const nextRecordId = useRecoilComponentFamilyValue(
|
||||
recordIdByRealIndexComponentFamilyState,
|
||||
{ realIndex: focusIndex + 1 },
|
||||
);
|
||||
|
||||
const nextRecordId = allRecordIds[focusIndex + 1];
|
||||
|
||||
const isNextRecordIdFirstOfGroup = useRecoilComponentFamilyValue(
|
||||
isRecordIdFirstOfGroupComponentFamilySelector,
|
||||
{ recordId: nextRecordId },
|
||||
{ recordId: nextRecordId ?? '' },
|
||||
);
|
||||
|
||||
const isFocused = useRecoilComponentFamilyValue(
|
||||
@@ -105,7 +98,6 @@ export const RecordTableTr = forwardRef<HTMLDivElement, RecordTableTrProps>(
|
||||
}) + recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isSelected: currentRowSelected,
|
||||
inView: isRowVisible,
|
||||
isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
import { isRowVisibleComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowVisibleComponentFamilyState';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useSetRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyState';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type RecordTableTrEffectProps = {
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const RecordTableTrEffect = ({ recordId }: RecordTableTrEffectProps) => {
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const setIsRowVisible = useSetRecoilComponentFamilyState(
|
||||
isRowVisibleComponentFamilyState,
|
||||
recordId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
root: scrollWrapperHTMLElement,
|
||||
rootMargin: '1000px',
|
||||
threshold: 0.1,
|
||||
};
|
||||
|
||||
const callback = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry) => {
|
||||
const isIntersecting = entry.isIntersecting;
|
||||
|
||||
if (isIntersecting) {
|
||||
setIsRowVisible(true);
|
||||
}
|
||||
|
||||
if (!isIntersecting) {
|
||||
setIsRowVisible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(callback, options);
|
||||
|
||||
observer.observe(
|
||||
document.querySelector(
|
||||
`[data-virtualized-id="${recordId}"]`,
|
||||
) as HTMLElement,
|
||||
);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [recordId, scrollWrapperHTMLElement, setIsRowVisible]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export const isRowVisibleComponentFamilyState = createComponentFamilyState<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isRowVisibleComponentFamilyState',
|
||||
defaultValue: true,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { useScrollTableToPosition } from '@/object-record/record-table/hooks/useScrollTableToPosition';
|
||||
import { useProcessTreadmillScrollTop } from '@/object-record/record-table/virtualization/hooks/useProcessTreadmillScrollTop';
|
||||
import { useTriggerFetchPages } from '@/object-record/record-table/virtualization/hooks/useTriggerFetchPages';
|
||||
import { useTriggerInitialRecordTableDataLoad } from '@/object-record/record-table/virtualization/hooks/useTriggerInitialRecordTableDataLoad';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
export const RecordTableNoRecordGroupScrollToPreviousRecordEffect = () => {
|
||||
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState(
|
||||
lastShowPageRecordIdState,
|
||||
);
|
||||
|
||||
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
||||
|
||||
const { scrollTableToPosition } = useScrollTableToPosition();
|
||||
|
||||
const { triggerInitialRecordTableDataLoad } =
|
||||
useTriggerInitialRecordTableDataLoad();
|
||||
|
||||
const { processTreadmillScrollTop } = useProcessTreadmillScrollTop();
|
||||
|
||||
const { triggerFetchPagesWithoutDebounce } = useTriggerFetchPages();
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
setLastShowPageRecordId(null);
|
||||
|
||||
const recordPosition = allRecordIds.findIndex(
|
||||
(recordId) => recordId === lastShowPageRecordId,
|
||||
);
|
||||
|
||||
await triggerInitialRecordTableDataLoad();
|
||||
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
|
||||
const tableScrollWrapperHeight = scrollWrapperElement?.clientHeight ?? 0;
|
||||
|
||||
const numberOfRowsDisplayedInTable = Math.min(
|
||||
Math.floor(tableScrollWrapperHeight / (RECORD_TABLE_ROW_HEIGHT + 1)),
|
||||
30,
|
||||
);
|
||||
|
||||
const halfNumberOfRowsVisible = Math.floor(
|
||||
numberOfRowsDisplayedInTable / 2,
|
||||
);
|
||||
|
||||
const recordPositionInPx = recordPosition * (RECORD_TABLE_ROW_HEIGHT + 1);
|
||||
|
||||
const targetScrollPositionInPx = Math.max(
|
||||
0,
|
||||
recordPositionInPx -
|
||||
halfNumberOfRowsVisible * (RECORD_TABLE_ROW_HEIGHT + 1),
|
||||
);
|
||||
|
||||
scrollTableToPosition({
|
||||
horizontalScrollInPx: 0,
|
||||
verticalScrollInPx: targetScrollPositionInPx,
|
||||
});
|
||||
|
||||
processTreadmillScrollTop(targetScrollPositionInPx);
|
||||
|
||||
setHasInitializedScroll(true);
|
||||
|
||||
await triggerFetchPagesWithoutDebounce();
|
||||
};
|
||||
|
||||
if (isNonEmptyString(lastShowPageRecordId)) {
|
||||
run();
|
||||
}
|
||||
}, [
|
||||
hasInitializedScroll,
|
||||
lastShowPageRecordId,
|
||||
scrollTableToPosition,
|
||||
allRecordIds,
|
||||
setLastShowPageRecordId,
|
||||
triggerInitialRecordTableDataLoad,
|
||||
processTreadmillScrollTop,
|
||||
getScrollWrapperElement,
|
||||
triggerFetchPagesWithoutDebounce,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { RecordTableRowVirtualizedDebugRowHelper } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedDebugRowHelper';
|
||||
import { RecordTableRowVirtualizedRouterLevel1 } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedRouterLevel1';
|
||||
import { TABLE_VIRTUALIZATION_DEBUG_ACTIVATED } from '@/object-record/record-table/virtualization/constants/TableVirtualizationDebugActivated';
|
||||
|
||||
import { realIndexByVirtualIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/realIndexByVirtualIndexComponentFamilyState';
|
||||
import { totalNumberOfRecordsToVirtualizeComponentState } from '@/object-record/record-table/virtualization/states/totalNumberOfRecordsToVirtualizeComponentState';
|
||||
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledVirtualizedRowContainer = styled.div<{
|
||||
pixelsFromTop: number;
|
||||
}>`
|
||||
height: ${RECORD_TABLE_ROW_HEIGHT + 1};
|
||||
position: absolute;
|
||||
top: ${({ pixelsFromTop }) => pixelsFromTop}px;
|
||||
`;
|
||||
|
||||
type RecordTableRowVirtualizedContainerProps = {
|
||||
virtualIndex: number;
|
||||
};
|
||||
|
||||
export const RecordTableRowVirtualizedContainer = ({
|
||||
virtualIndex,
|
||||
}: RecordTableRowVirtualizedContainerProps) => {
|
||||
const realIndex = useRecoilComponentFamilyValue(
|
||||
realIndexByVirtualIndexComponentFamilyState,
|
||||
{ virtualIndex },
|
||||
);
|
||||
|
||||
const totalNumberOfRecordsToVirtualize =
|
||||
useRecoilComponentValue(totalNumberOfRecordsToVirtualizeComponentState) ??
|
||||
0;
|
||||
|
||||
if (!isDefined(realIndex) || realIndex >= totalNumberOfRecordsToVirtualize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pixelsFromTop =
|
||||
realIndex * (RECORD_TABLE_ROW_HEIGHT + 1) + (RECORD_TABLE_ROW_HEIGHT + 1);
|
||||
|
||||
return (
|
||||
<StyledVirtualizedRowContainer
|
||||
id={`row-virtual-index-${virtualIndex}`}
|
||||
pixelsFromTop={pixelsFromTop}
|
||||
>
|
||||
{TABLE_VIRTUALIZATION_DEBUG_ACTIVATED && (
|
||||
<RecordTableRowVirtualizedDebugRowHelper virtualIndex={virtualIndex} />
|
||||
)}
|
||||
<RecordTableRowVirtualizedRouterLevel1 realIndex={realIndex} />
|
||||
</StyledVirtualizedRowContainer>
|
||||
);
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { realIndexByVirtualIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/realIndexByVirtualIndexComponentFamilyState';
|
||||
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
|
||||
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledDebugColumn = styled.div<{ width: number }>`
|
||||
height: 25px;
|
||||
max-height: 25px;
|
||||
min-width: ${({ width }) => width}px;
|
||||
max-width: ${({ width }) => width}px;
|
||||
overflow: scroll;
|
||||
|
||||
display: flex;
|
||||
text-wrap-mode: nowrap;
|
||||
padding-right: 2px;
|
||||
padding-left: 2px;
|
||||
`;
|
||||
|
||||
type RecordTableRowVirtualizedDebugRowHelperProps = {
|
||||
virtualIndex: number;
|
||||
};
|
||||
|
||||
export const RecordTableRowVirtualizedDebugRowHelper = ({
|
||||
virtualIndex,
|
||||
}: RecordTableRowVirtualizedDebugRowHelperProps) => {
|
||||
const realIndex = useRecoilComponentFamilyValue(
|
||||
realIndexByVirtualIndexComponentFamilyState,
|
||||
{ virtualIndex },
|
||||
);
|
||||
|
||||
const recordId = useRecoilComponentFamilyValue(
|
||||
recordIdByRealIndexComponentFamilyState,
|
||||
{ realIndex },
|
||||
);
|
||||
|
||||
const pixelsFromTop =
|
||||
(realIndex ?? 0) * (RECORD_TABLE_ROW_HEIGHT + 1) +
|
||||
(RECORD_TABLE_ROW_HEIGHT + 1);
|
||||
|
||||
const record = useRecoilValue(recordStoreFamilyState(recordId ?? ''));
|
||||
const { objectMetadataItem, objectNameSingular } =
|
||||
useRecordTableContextOrThrow();
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
const labelIdentifier = isDefined(record)
|
||||
? getLabelIdentifierFieldValue(
|
||||
record,
|
||||
labelIdentifierFieldMetadataItem,
|
||||
objectNameSingular,
|
||||
)
|
||||
: '-';
|
||||
|
||||
const position = record?.position;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 250,
|
||||
top: 5,
|
||||
zIndex: 20,
|
||||
color: 'darkblue',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid blue',
|
||||
padding: 2,
|
||||
display: 'flex',
|
||||
maxHeight: 16,
|
||||
overflow: 'clip',
|
||||
}}
|
||||
>
|
||||
<StyledDebugColumn width={70}>virtual :{virtualIndex}</StyledDebugColumn>
|
||||
<StyledDebugColumn width={70}>real :{realIndex}</StyledDebugColumn>
|
||||
<StyledDebugColumn width={100}>pos :{position}</StyledDebugColumn>
|
||||
<StyledDebugColumn width={80}>
|
||||
px:
|
||||
{pixelsFromTop}
|
||||
</StyledDebugColumn>
|
||||
<StyledDebugColumn width={150}>
|
||||
<b>{labelIdentifier}</b>
|
||||
</StyledDebugColumn>
|
||||
<StyledDebugColumn width={300}>
|
||||
id:
|
||||
{recordId}
|
||||
</StyledDebugColumn>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
|
||||
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
|
||||
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
|
||||
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
|
||||
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
|
||||
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
|
||||
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
|
||||
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
|
||||
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
|
||||
import { recordIdByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilyState';
|
||||
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenRecordUpdatesEffect';
|
||||
import { getDefaultRecordFieldsToListen } from '@/subscription/utils/getDefaultRecordFieldsToListen.util';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableRowVirtualizedFullDataProps = {
|
||||
realIndex: number;
|
||||
};
|
||||
|
||||
// TODO: Full Data will take its full meaning when we'll have different levels of data : with relations, only identifiers, etc.
|
||||
export const RecordTableRowVirtualizedFullData = ({
|
||||
realIndex,
|
||||
}: RecordTableRowVirtualizedFullDataProps) => {
|
||||
const { objectNameSingular } = useRecordIndexContextOrThrow();
|
||||
const listenedFields = getDefaultRecordFieldsToListen({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const isFocused = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
realIndex ?? 0,
|
||||
);
|
||||
|
||||
const isRowFocusActive = useRecoilComponentValue(
|
||||
isRecordTableRowFocusActiveComponentState,
|
||||
);
|
||||
|
||||
const recordId = useRecoilComponentFamilyValue(
|
||||
recordIdByRealIndexComponentFamilyState,
|
||||
{ realIndex },
|
||||
);
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
return <RecordTableRowVirtualizedSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableDraggableTr
|
||||
recordId={recordId}
|
||||
draggableIndex={realIndex}
|
||||
focusIndex={realIndex}
|
||||
>
|
||||
{isRowFocusActive && isFocused && (
|
||||
<>
|
||||
<RecordTableRowHotkeyEffect />
|
||||
<RecordTableRowArrowKeysEffect />
|
||||
</>
|
||||
)}
|
||||
<RecordTableCellDragAndDrop />
|
||||
<RecordTableCellCheckbox />
|
||||
<RecordTableFieldsCells />
|
||||
<RecordTablePlusButtonCellPlaceholder />
|
||||
<RecordTableLastEmptyCell />
|
||||
<ListenRecordUpdatesEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordId={recordId}
|
||||
listenedFields={listenedFields}
|
||||
/>
|
||||
</RecordTableDraggableTr>
|
||||
);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { RecordTableRowVirtualizedRouterLevel2 } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedRouterLevel2';
|
||||
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
|
||||
import { lowDetailsActivatedComponentState } from '@/object-record/record-table/virtualization/states/lowDetailsActivatedComponentState';
|
||||
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
type RecordTableRowVirtualizedRouterLevel1Props = {
|
||||
realIndex: number;
|
||||
};
|
||||
|
||||
export const RecordTableRowVirtualizedRouterLevel1 = ({
|
||||
realIndex,
|
||||
}: RecordTableRowVirtualizedRouterLevel1Props) => {
|
||||
const lowDetailsActivated = useRecoilComponentValue(
|
||||
lowDetailsActivatedComponentState,
|
||||
);
|
||||
|
||||
if (lowDetailsActivated) {
|
||||
return <RecordTableRowVirtualizedSkeleton />;
|
||||
}
|
||||
|
||||
return <RecordTableRowVirtualizedRouterLevel2 realIndex={realIndex} />;
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { RecordTableRowVirtualizedFullData } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedFullData';
|
||||
|
||||
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
|
||||
import { dataLoadingStatusByRealIndexComponentFamilyState } from '@/object-record/record-table/virtualization/states/dataLoadingStatusByRealIndexComponentFamilyState';
|
||||
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
|
||||
type RecordTableRowVirtualizedRouterLevel2Props = {
|
||||
realIndex: number;
|
||||
};
|
||||
|
||||
export const RecordTableRowVirtualizedRouterLevel2 = ({
|
||||
realIndex,
|
||||
}: RecordTableRowVirtualizedRouterLevel2Props) => {
|
||||
const dataLoadingStatus = useRecoilComponentFamilyValue(
|
||||
dataLoadingStatusByRealIndexComponentFamilyState,
|
||||
{ realIndex },
|
||||
);
|
||||
|
||||
if (dataLoadingStatus === null) {
|
||||
return <RecordTableRowVirtualizedSkeleton />;
|
||||
}
|
||||
|
||||
return <RecordTableRowVirtualizedFullData realIndex={realIndex} />;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user