Fix fetch more notes (#16442)

fixes : https://github.com/twentyhq/twenty/issues/16320

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2025-12-12 14:43:21 +01:00
committed by GitHub
parent 8dbcd506ed
commit 6acdde72ef
13 changed files with 607 additions and 114 deletions
@@ -6,6 +6,10 @@ type CustomResolverFetchMoreLoaderProps = {
onLastRowVisible: (...args: any[]) => any;
};
const StyledContainer = styled.div`
min-height: 1px;
`;
const StyledText = styled.div`
align-items: center;
box-shadow: none;
@@ -21,12 +25,16 @@ export const CustomResolverFetchMoreLoader = ({
onLastRowVisible,
}: CustomResolverFetchMoreLoaderProps) => {
const { ref: tbodyRef } = useInView({
onChange: onLastRowVisible,
onChange: (inView) => {
if (inView) {
onLastRowVisible();
}
},
});
return (
<div ref={tbodyRef}>
<StyledContainer ref={tbodyRef}>
{loading && <StyledText>Loading more...</StyledText>}
</div>
</StyledContainer>
);
};
@@ -1,11 +1,12 @@
import { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { type RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
import { isDefined } from 'twenty-shared/utils';
type FindActivityTargetsOperationSignatureFactory = {
objectNameSingular: CoreObjectNameSingular;
objectNameSingular: CoreObjectNameSingular.Note | CoreObjectNameSingular.Task;
objectMetadataItems: ObjectMetadataItem[];
};
@@ -14,14 +15,50 @@ export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSig
> = ({
objectNameSingular,
objectMetadataItems,
}: FindActivityTargetsOperationSignatureFactory) => ({
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
variables: {},
fields: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
});
}: FindActivityTargetsOperationSignatureFactory) => {
const targetObjectNameSingular =
getJoinObjectNameSingular(objectNameSingular);
const targetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === targetObjectNameSingular,
);
const activityObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (
!isDefined(targetObjectMetadataItem) ||
!isDefined(activityObjectMetadataItem)
) {
throw new Error(`Cannot find target or targetable object metadata item`);
}
const activityFieldKeys = generateDepthRecordGqlFieldsFromObject({
objectMetadataItems,
objectMetadataItem: activityObjectMetadataItem,
depth: 0,
});
return {
objectNameSingular: targetObjectNameSingular,
variables: {},
fields: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
[objectNameSingular]: {
...activityFieldKeys,
[targetObjectMetadataItem.namePlural]:
generateDepthRecordGqlFieldsFromObject({
objectMetadataItems,
objectMetadataItem: targetObjectMetadataItem,
depth: 1,
}),
},
},
};
};
@@ -15,7 +15,7 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
}));
const mockActivityTarget = {
__typename: 'ActivityTarget',
__typename: 'TaskTarget',
updatedAt: '2021-08-03T19:20:06.000Z',
createdAt: '2021-08-03T19:20:06.000Z',
personId: '1',
@@ -55,18 +55,11 @@ describe('useActivities', () => {
);
useActivityTargetsForTargetableObjectsMock.useActivityTargetsForTargetableObjects.mockReturnValue(
{
activityTargets: [mockActivityTarget],
activityTargets: [{ ...mockActivityTarget, task: mockActivity }],
loadingActivityTargets: false,
},
);
const useFindManyRecordsMock = jest.requireMock(
'@/object-record/hooks/useFindManyRecords',
);
useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
records: [mockActivity],
});
const { result } = renderHook(
() => {
const activities = useActivities({
@@ -74,9 +67,9 @@ describe('useActivities', () => {
targetableObjects: [
{ targetObjectNameSingular: 'company', id: '123' },
],
activitiesFilters: {},
activitiesOrderByVariables: [{}],
skip: false,
limit: 10,
activityTargetsOrderByVariables: [{}],
});
return activities;
},
@@ -1,92 +1,84 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { type Note } from '@/activities/types/Note';
import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type Task } from '@/activities/types/Task';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useActivities = <T extends Task | Note>({
objectNameSingular,
targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
activityTargetsOrderByVariables,
skip,
limit,
}: {
objectNameSingular: CoreObjectNameSingular;
objectNameSingular: CoreObjectNameSingular.Note | CoreObjectNameSingular.Task;
targetableObjects: ActivityTargetableObject[];
activitiesFilters: RecordGqlOperationFilter;
activitiesOrderByVariables: RecordGqlOperationOrderBy;
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
skip?: boolean;
limit: number;
}) => {
const { objectMetadataItems } = useObjectMetadataItems();
const updateActivitiesInStore = useRecoilCallback(
({ set }) =>
(activityTargets: (TaskTarget | NoteTarget)[]) => {
for (const activityTarget of activityTargets) {
const activity = activityTarget[objectNameSingular];
set(recordStoreFamilyState(activity.id), activity);
}
},
[objectNameSingular],
);
const { activityTargets, loadingActivityTargets } =
useActivityTargetsForTargetableObjects({
objectNameSingular,
targetableObjects,
skip: skip,
const {
activityTargets,
loadingActivityTargets,
totalCountActivityTargets,
fetchMoreActivityTargets,
hasNextPage,
} = useActivityTargetsForTargetableObjects({
objectNameSingular,
targetableObjects,
skip: skip,
activityTargetsOrderByVariables,
onCompleted: updateActivitiesInStore,
limit,
});
const activities = activityTargets.map((activityTarget) => {
return activityTarget[objectNameSingular];
}) as T[];
const fetchMoreActivities = async () => {
const result = await fetchMoreActivityTargets();
if (!isDefined(result?.data)) {
return [];
}
const activityTargets = getRecordsFromRecordConnection<
TaskTarget | NoteTarget
>({
recordConnection: result.data,
});
const activityIds = [
...new Set(
activityTargets
? [
...activityTargets
.map(
(activityTarget) =>
activityTarget.taskId ?? activityTarget.noteId,
)
.filter(isNonEmptyString),
].sort(sortByAscString)
: [],
),
];
updateActivitiesInStore(activityTargets);
const skipBecauseNoActivityTargetFound = activityIds.length === 0;
const filter: RecordGqlOperationFilter = {
id: {
in: activityIds,
},
...activitiesFilters,
return activityTargets.map((activityTarget) => {
return activityTarget[objectNameSingular];
}) as T[];
};
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
findActivitiesOperationSignatureFactory({
objectMetadataItems,
objectNameSingular,
});
const { records: activities, loading: loadingActivities } =
useFindManyRecords<Task | Note>({
skip: skip || loadingActivityTargets || skipBecauseNoActivityTargetFound,
objectNameSingular:
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
recordGqlFields: FIND_ACTIVITIES_OPERATION_SIGNATURE.fields,
filter,
orderBy: activitiesOrderByVariables,
onCompleted: useRecoilCallback(
({ set }) =>
(activities) => {
for (const activity of activities) {
set(recordStoreFamilyState(activity.id), activity);
}
},
[],
),
});
return {
activities: activities as T[],
loading: loadingActivities || loadingActivityTargets,
loading: loadingActivityTargets,
totalCountActivities: totalCountActivityTargets,
fetchMoreActivities,
hasNextPage,
};
};
@@ -7,6 +7,7 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargetsForTargetableObjects = ({
@@ -14,14 +15,18 @@ export const useActivityTargetsForTargetableObjects = ({
targetableObjects,
skip,
onCompleted,
activityTargetsOrderByVariables,
limit,
}: {
objectNameSingular: CoreObjectNameSingular;
objectNameSingular: CoreObjectNameSingular.Note | CoreObjectNameSingular.Task;
targetableObjects: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>[];
skip?: boolean;
onCompleted?: (activityTargets: (TaskTarget | NoteTarget)[]) => void;
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
limit: number;
}) => {
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
@@ -38,18 +43,28 @@ export const useActivityTargetsForTargetableObjects = ({
// TODO: We want to optimistically remove from this request
// If we are on a show page and we remove the current show page object corresponding activity target
// See also if we need to update useTimelineActivities
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords<TaskTarget | NoteTarget>({
skip,
objectNameSingular:
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,
filter: activityTargetsFilter,
recordGqlFields: FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.fields,
onCompleted,
});
const {
records: activityTargets,
loading: loadingActivityTargets,
totalCount: totalCountActivityTargets,
fetchMoreRecords: fetchMoreActivityTargets,
hasNextPage,
} = useFindManyRecords<TaskTarget | NoteTarget>({
skip,
objectNameSingular:
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,
filter: activityTargetsFilter,
recordGqlFields: FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.fields,
onCompleted,
orderBy: activityTargetsOrderByVariables,
limit,
});
return {
activityTargets,
loadingActivityTargets,
totalCountActivityTargets: totalCountActivityTargets ?? 0,
fetchMoreActivityTargets,
hasNextPage,
};
};
@@ -40,6 +40,7 @@ export const useUpdateActivityTargetFromCell = ({
joinObjectNameSingular === ''
? activityObjectNameSingular
: joinObjectNameSingular,
shouldMatchRootQueryFilter: true,
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
@@ -1,5 +1,5 @@
import { type ReactElement } from 'react';
import styled from '@emotion/styled';
import { type ReactElement } from 'react';
import { type Note } from '@/activities/types/Note';
@@ -9,6 +9,7 @@ type NoteListProps = {
title: string;
notes: Note[];
button?: ReactElement | false | null;
totalCount: number;
};
const StyledContainer = styled.div`
@@ -47,13 +48,18 @@ const StyledNoteContainer = styled.div`
width: 100%;
`;
export const NoteList = ({ title, notes, button }: NoteListProps) => (
export const NoteList = ({
title,
notes,
totalCount,
button,
}: NoteListProps) => (
<>
{notes && notes.length > 0 && (
<StyledContainer>
<StyledTitleBar>
<StyledTitle>
{title} <StyledCount>{notes.length}</StyledCount>
{title} <StyledCount>{totalCount}</StyledCount>
</StyledTitle>
{button}
</StyledTitleBar>
@@ -1,3 +1,4 @@
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { NoteList } from '@/activities/notes/components/NoteList';
@@ -29,7 +30,14 @@ const StyledNotesContainer = styled.div`
export const NotesCard = () => {
const targetRecord = useTargetRecord();
const { notes, loading } = useNotes(targetRecord);
const { notes, loading, totalCountNotes, fetchMoreNotes, hasNextPage } =
useNotes(targetRecord);
const handleLastRowVisible = async () => {
if (hasNextPage) {
await fetchMoreNotes();
}
};
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note,
@@ -87,6 +95,7 @@ export const NotesCard = () => {
<NoteList
title={t`All`}
notes={notes}
totalCount={totalCountNotes}
button={
hasObjectUpdatePermissions && (
<Button
@@ -103,6 +112,10 @@ export const NotesCard = () => {
)
}
/>
<CustomResolverFetchMoreLoader
loading={loading}
onLastRowVisible={handleLastRowVisible}
/>
</StyledNotesContainer>
);
};
@@ -15,17 +15,22 @@ export const useNotes = (targetableObject: ActivityTargetableObject) => {
const notesQueryVariables = useMemo(
() =>
({
filter: {},
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
}) as RecordGqlOperationVariables,
[],
);
const { activities, loading } = useActivities<Note>({
const {
activities,
loading,
totalCountActivities,
fetchMoreActivities,
hasNextPage,
} = useActivities<Note>({
objectNameSingular: CoreObjectNameSingular.Note,
activitiesFilters: notesQueryVariables.filter ?? {},
activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}],
activityTargetsOrderByVariables: notesQueryVariables.orderBy ?? [{}],
targetableObjects: [targetableObject],
limit: 10,
});
const [currentNotesQueryVariables, setCurrentNotesQueryVariables] =
@@ -45,5 +50,8 @@ export const useNotes = (targetableObject: ActivityTargetableObject) => {
return {
notes: activities as Note[],
loading,
totalCountNotes: totalCountActivities,
fetchMoreNotes: fetchMoreActivities,
hasNextPage,
};
};
@@ -9,15 +9,24 @@ type UseTasksProps = {
};
export const useTasks = ({ targetableObjects }: UseTasksProps) => {
const { activities: tasks, loading: tasksLoading } = useActivities<Task>({
const {
activities: tasks,
loading: tasksLoading,
fetchMoreActivities: fetchMoreTasks,
hasNextPage,
totalCountActivities,
} = useActivities<Task>({
objectNameSingular: CoreObjectNameSingular.Task,
targetableObjects,
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
activityTargetsOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
limit: 200,
});
return {
tasks: (tasks ?? []) as Task[],
tasksLoading,
fetchMoreTasks,
hasNextPage,
totalCountTasks: totalCountActivities,
};
};
@@ -0,0 +1,274 @@
import { AppPath } from 'twenty-shared/types';
import {
type AvailableWorkspace,
type AvailableWorkspaces,
} from '~/generated/graphql';
import {
countAvailableWorkspaces,
getAvailableWorkspacePathAndSearchParams,
getFirstAvailableWorkspaces,
} from '../availableWorkspacesUtils';
const createMockAvailableWorkspace = (
overrides: Partial<AvailableWorkspace> = {},
): AvailableWorkspace => ({
__typename: 'AvailableWorkspace',
id: 'workspace-1',
displayName: 'Test Workspace',
logo: null,
sso: [],
workspaceUrls: {
customUrl: null,
subdomainUrl: 'https://test.twenty.com',
},
...overrides,
});
const createMockAvailableWorkspaces = (
signInWorkspaces: AvailableWorkspace[] = [],
signUpWorkspaces: AvailableWorkspace[] = [],
): AvailableWorkspaces => ({
__typename: 'AvailableWorkspaces',
availableWorkspacesForSignIn: signInWorkspaces,
availableWorkspacesForSignUp: signUpWorkspaces,
});
describe('availableWorkspacesUtils', () => {
describe('countAvailableWorkspaces', () => {
it('should return 0 when both arrays are empty', () => {
const availableWorkspaces = createMockAvailableWorkspaces([], []);
expect(countAvailableWorkspaces(availableWorkspaces)).toBe(0);
});
it('should count only sign-in workspaces when sign-up is empty', () => {
const signInWorkspaces = [
createMockAvailableWorkspace({ id: 'ws-1' }),
createMockAvailableWorkspace({ id: 'ws-2' }),
];
const availableWorkspaces = createMockAvailableWorkspaces(
signInWorkspaces,
[],
);
expect(countAvailableWorkspaces(availableWorkspaces)).toBe(2);
});
it('should count only sign-up workspaces when sign-in is empty', () => {
const signUpWorkspaces = [
createMockAvailableWorkspace({ id: 'ws-1' }),
createMockAvailableWorkspace({ id: 'ws-2' }),
createMockAvailableWorkspace({ id: 'ws-3' }),
];
const availableWorkspaces = createMockAvailableWorkspaces(
[],
signUpWorkspaces,
);
expect(countAvailableWorkspaces(availableWorkspaces)).toBe(3);
});
it('should count workspaces from both arrays', () => {
const signInWorkspaces = [
createMockAvailableWorkspace({ id: 'ws-1' }),
createMockAvailableWorkspace({ id: 'ws-2' }),
];
const signUpWorkspaces = [
createMockAvailableWorkspace({ id: 'ws-3' }),
createMockAvailableWorkspace({ id: 'ws-4' }),
createMockAvailableWorkspace({ id: 'ws-5' }),
];
const availableWorkspaces = createMockAvailableWorkspaces(
signInWorkspaces,
signUpWorkspaces,
);
expect(countAvailableWorkspaces(availableWorkspaces)).toBe(5);
});
});
describe('getFirstAvailableWorkspaces', () => {
it('should return first sign-in workspace when available', () => {
const signInWorkspace = createMockAvailableWorkspace({
id: 'sign-in-ws',
displayName: 'Sign In Workspace',
});
const signUpWorkspace = createMockAvailableWorkspace({
id: 'sign-up-ws',
displayName: 'Sign Up Workspace',
});
const availableWorkspaces = createMockAvailableWorkspaces(
[signInWorkspace],
[signUpWorkspace],
);
expect(getFirstAvailableWorkspaces(availableWorkspaces)).toEqual(
signInWorkspace,
);
});
it('should return first sign-up workspace when sign-in is empty', () => {
const signUpWorkspace = createMockAvailableWorkspace({
id: 'sign-up-ws',
displayName: 'Sign Up Workspace',
});
const availableWorkspaces = createMockAvailableWorkspaces(
[],
[signUpWorkspace],
);
expect(getFirstAvailableWorkspaces(availableWorkspaces)).toEqual(
signUpWorkspace,
);
});
it('should return undefined when both arrays are empty', () => {
const availableWorkspaces = createMockAvailableWorkspaces([], []);
expect(getFirstAvailableWorkspaces(availableWorkspaces)).toBeUndefined();
});
it('should prioritize sign-in over sign-up workspace', () => {
const firstSignInWorkspace = createMockAvailableWorkspace({
id: 'first-sign-in',
});
const secondSignInWorkspace = createMockAvailableWorkspace({
id: 'second-sign-in',
});
const signUpWorkspace = createMockAvailableWorkspace({
id: 'sign-up-ws',
});
const availableWorkspaces = createMockAvailableWorkspaces(
[firstSignInWorkspace, secondSignInWorkspace],
[signUpWorkspace],
);
expect(getFirstAvailableWorkspaces(availableWorkspaces)).toEqual(
firstSignInWorkspace,
);
});
});
describe('getAvailableWorkspacePathAndSearchParams', () => {
describe('when workspace has loginToken', () => {
it('should return Verify path with loginToken in search params', () => {
const workspace = createMockAvailableWorkspace({
loginToken: 'test-login-token',
});
const result = getAvailableWorkspacePathAndSearchParams(workspace);
expect(result).toEqual({
pathname: AppPath.Verify,
searchParams: { loginToken: 'test-login-token' },
});
});
it('should include default search params with loginToken', () => {
const workspace = createMockAvailableWorkspace({
loginToken: 'test-login-token',
});
const result = getAvailableWorkspacePathAndSearchParams(workspace, {
foo: 'bar',
});
expect(result).toEqual({
pathname: AppPath.Verify,
searchParams: { foo: 'bar', loginToken: 'test-login-token' },
});
});
});
describe('when workspace has personalInviteToken and inviteHash', () => {
it('should return Invite path with inviteToken in search params', () => {
const workspace = createMockAvailableWorkspace({
personalInviteToken: 'test-invite-token',
inviteHash: 'test-invite-hash',
});
const result = getAvailableWorkspacePathAndSearchParams(workspace);
expect(result).toEqual({
pathname: '/invite/test-invite-hash',
searchParams: { inviteToken: 'test-invite-token' },
});
});
it('should include default search params with inviteToken', () => {
const workspace = createMockAvailableWorkspace({
personalInviteToken: 'test-invite-token',
inviteHash: 'test-invite-hash',
});
const result = getAvailableWorkspacePathAndSearchParams(workspace, {
source: 'email',
});
expect(result).toEqual({
pathname: '/invite/test-invite-hash',
searchParams: { source: 'email', inviteToken: 'test-invite-token' },
});
});
});
describe('when workspace has only personalInviteToken without inviteHash', () => {
it('should return SignInUp path with inviteToken in search params', () => {
const workspace = createMockAvailableWorkspace({
personalInviteToken: 'test-invite-token',
inviteHash: undefined,
});
const result = getAvailableWorkspacePathAndSearchParams(workspace);
expect(result).toEqual({
pathname: AppPath.SignInUp,
searchParams: { inviteToken: 'test-invite-token' },
});
});
});
describe('when workspace has neither loginToken nor invite tokens', () => {
it('should return SignInUp path with empty search params', () => {
const workspace = createMockAvailableWorkspace();
const result = getAvailableWorkspacePathAndSearchParams(workspace);
expect(result).toEqual({
pathname: AppPath.SignInUp,
searchParams: {},
});
});
it('should preserve default search params', () => {
const workspace = createMockAvailableWorkspace();
const result = getAvailableWorkspacePathAndSearchParams(workspace, {
redirect: '/dashboard',
});
expect(result).toEqual({
pathname: AppPath.SignInUp,
searchParams: { redirect: '/dashboard' },
});
});
});
describe('priority: loginToken over invite tokens', () => {
it('should prioritize loginToken when both loginToken and invite tokens exist', () => {
const workspace = createMockAvailableWorkspace({
loginToken: 'test-login-token',
personalInviteToken: 'test-invite-token',
inviteHash: 'test-invite-hash',
});
const result = getAvailableWorkspacePathAndSearchParams(workspace);
expect(result).toEqual({
pathname: AppPath.Verify,
searchParams: { loginToken: 'test-login-token' },
});
});
});
});
});
@@ -0,0 +1,92 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generateDepthRecordGqlFieldsFromRecord should generate depth one record gql fields from empty record 1`] = `
{
"accountOwner": false,
"accountOwnerId": false,
"address": false,
"annualRecurringRevenue": false,
"attachments": false,
"createdAt": false,
"createdBy": false,
"deletedAt": false,
"domainName": false,
"employees": false,
"favorites": false,
"id": false,
"idealCustomerProfile": false,
"introVideo": false,
"linkedinLink": false,
"name": false,
"noteTargets": false,
"opportunities": false,
"people": false,
"position": false,
"searchVector": false,
"tagline": false,
"taskTargets": false,
"timelineActivities": false,
"updatedAt": false,
"visaSponsorship": false,
"workPolicy": false,
"xLink": false,
}
`;
exports[`generateDepthRecordGqlFieldsFromRecord should generate depth one record gql fields from record with some fields 1`] = `
{
"accountOwner": false,
"accountOwnerId": false,
"address": false,
"annualRecurringRevenue": false,
"attachments": false,
"createdAt": false,
"createdBy": false,
"deletedAt": false,
"domainName": true,
"employees": false,
"favorites": false,
"id": true,
"idealCustomerProfile": false,
"introVideo": false,
"linkedinLink": false,
"name": true,
"noteTargets": false,
"opportunities": false,
"people": false,
"position": false,
"searchVector": false,
"tagline": false,
"taskTargets": false,
"timelineActivities": false,
"updatedAt": false,
"visaSponsorship": false,
"workPolicy": false,
"xLink": false,
}
`;
exports[`generateDepthRecordGqlFieldsFromRecord should generate depth zero record gql fields from record with some fields 1`] = `
{
"accountOwnerId": false,
"address": false,
"annualRecurringRevenue": false,
"createdAt": false,
"createdBy": false,
"deletedAt": false,
"domainName": false,
"employees": false,
"id": true,
"idealCustomerProfile": false,
"introVideo": false,
"linkedinLink": false,
"name": true,
"position": false,
"searchVector": false,
"tagline": false,
"updatedAt": false,
"visaSponsorship": false,
"workPolicy": false,
"xLink": false,
}
`;
@@ -0,0 +1,45 @@
import { generateDepthRecordGqlFieldsFromRecord } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromRecord';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
describe('generateDepthRecordGqlFieldsFromRecord', () => {
it('should generate depth one record gql fields from record with some fields', () => {
const result = generateDepthRecordGqlFieldsFromRecord({
objectMetadataItem: getMockObjectMetadataItemOrThrow('company'),
objectMetadataItems: generatedMockObjectMetadataItems,
depth: 1,
record: {
id: '123',
name: 'Test Company',
domainName: 'test.com',
},
});
expect(result).toMatchSnapshot();
});
it('should generate depth zero record gql fields from record with some fields', () => {
const result = generateDepthRecordGqlFieldsFromRecord({
objectMetadataItem: getMockObjectMetadataItemOrThrow('company'),
objectMetadataItems: generatedMockObjectMetadataItems,
depth: 0,
record: {
id: '123',
name: 'Test Company',
},
});
expect(result).toMatchSnapshot();
});
it('should generate depth one record gql fields from empty record', () => {
const result = generateDepthRecordGqlFieldsFromRecord({
objectMetadataItem: getMockObjectMetadataItemOrThrow('company'),
objectMetadataItems: generatedMockObjectMetadataItems,
depth: 1,
record: {},
});
expect(result).toMatchSnapshot();
});
});