Steps to test

1. Run metadata migrations
2. Run sync-metadata on your workspace
3. Enable the following feature flags: 
IS_SEARCH_ENABLED
IS_QUERY_RUNNER_TWENTY_ORM_ENABLED
IS_WORKSPACE_MIGRATED_FOR_SEARCH
4. Type Cmd + K and search anything
This commit is contained in:
Marie
2024-10-03 17:18:49 +02:00
committed by GitHub
parent 4c250dd811
commit 5f9435c718
71 changed files with 1517 additions and 209 deletions

View File

@ -16,7 +16,9 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
import { Opportunity } from '@/opportunities/Opportunity';
import { Person } from '@/people/types/Person';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
@ -165,8 +167,21 @@ export const CommandMenu = () => {
[closeCommandMenu],
);
const { records: people } = useFindManyRecords<Person>({
skip: !isCommandMenuOpened,
const isTwentyOrmEnabled = useIsFeatureEnabled(
'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
);
const isWorkspaceMigratedForSearch = useIsFeatureEnabled(
'IS_WORKSPACE_MIGRATED_FOR_SEARCH',
);
const isSearchEnabled =
useIsFeatureEnabled('IS_SEARCH_ENABLED') &&
isTwentyOrmEnabled &&
isWorkspaceMigratedForSearch;
const { records: peopleFromFindMany } = useFindManyRecords<Person>({
skip: !isCommandMenuOpened || isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Person,
filter: commandMenuSearch
? makeOrFilterVariables([
@ -183,9 +198,24 @@ export const CommandMenu = () => {
: undefined,
limit: 3,
});
const { records: peopleFromSearch } = useSearchRecords<Person>({
skip: !isCommandMenuOpened || !isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Person,
limit: 3,
searchInput: commandMenuSearch ?? undefined,
});
const { records: companies } = useFindManyRecords<Company>({
skip: !isCommandMenuOpened,
const people = isSearchEnabled ? peopleFromSearch : peopleFromFindMany;
const { records: companiesFromSearch } = useSearchRecords<Company>({
skip: !isCommandMenuOpened || !isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Company,
limit: 3,
searchInput: commandMenuSearch ?? undefined,
});
const { records: companiesFromFindMany } = useFindManyRecords<Company>({
skip: !isCommandMenuOpened || isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Company,
filter: commandMenuSearch
? {
@ -195,6 +225,10 @@ export const CommandMenu = () => {
limit: 3,
});
const companies = isSearchEnabled
? companiesFromSearch
: companiesFromFindMany;
const { records: notes } = useFindManyRecords<Note>({
skip: !isCommandMenuOpened,
objectNameSingular: CoreObjectNameSingular.Note,
@ -207,8 +241,8 @@ export const CommandMenu = () => {
limit: 3,
});
const { records: opportunities } = useFindManyRecords({
skip: !isCommandMenuOpened,
const { records: opportunitiesFromFindMany } = useFindManyRecords({
skip: !isCommandMenuOpened || isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Opportunity,
filter: commandMenuSearch
? {
@ -218,6 +252,17 @@ export const CommandMenu = () => {
limit: 3,
});
const { records: opportunitiesFromSearch } = useSearchRecords<Opportunity>({
skip: !isCommandMenuOpened || !isSearchEnabled,
objectNameSingular: CoreObjectNameSingular.Opportunity,
limit: 3,
searchInput: commandMenuSearch ?? undefined,
});
const opportunities = isSearchEnabled
? opportunitiesFromSearch
: opportunitiesFromFindMany;
const peopleCommands = useMemo(
() =>
people.map(({ id, name: { firstName, lastName } }) => ({

View File

@ -1,16 +1,29 @@
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { WorkspaceActivationStatus } from '~/generated/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const filterTsVectorFields = (
objectMetadataItems: ObjectMetadataItem[],
): ObjectMetadataItem[] => {
return objectMetadataItems.map((item) => ({
...item,
fields: item.fields.filter(
(field) => field.type !== FieldMetadataType.TsVector,
),
}));
};
export const ObjectMetadataItemsLoadEffect = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
@ -21,26 +34,32 @@ export const ObjectMetadataItemsLoadEffect = () => {
skip: !isLoggedIn,
});
const [objectMetadataItems, setObjectMetadataItems] = useRecoilState(
objectMetadataItemsState,
const updateObjectMetadataItems = useRecoilCallback(
({ set, snapshot }) =>
() => {
const filteredFields = filterTsVectorFields(newObjectMetadataItems);
const toSetObjectMetadataItems =
isUndefinedOrNull(currentUser) ||
currentWorkspace?.activationStatus !==
WorkspaceActivationStatus.Active
? generatedMockObjectMetadataItems
: filteredFields;
if (
!isDeeplyEqual(
snapshot.getLoadable(objectMetadataItemsState).getValue(),
toSetObjectMetadataItems,
)
) {
set(objectMetadataItemsState, toSetObjectMetadataItems);
}
},
[currentUser, currentWorkspace?.activationStatus, newObjectMetadataItems],
);
useEffect(() => {
const toSetObjectMetadataItems =
isUndefinedOrNull(currentUser) ||
currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active
? generatedMockObjectMetadataItems
: newObjectMetadataItems;
if (!isDeeplyEqual(objectMetadataItems, toSetObjectMetadataItems)) {
setObjectMetadataItems(toSetObjectMetadataItems);
}
}, [
currentUser,
currentWorkspace?.activationStatus,
newObjectMetadataItems,
objectMetadataItems,
setObjectMetadataItems,
]);
updateObjectMetadataItems();
}, [updateObjectMetadataItems]);
return <></>;
};

View File

@ -0,0 +1,5 @@
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
export type RecordGqlOperationSearchResult = {
[objectNamePlural: string]: RecordGqlConnection;
};

View File

@ -0,0 +1,94 @@
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { RecordGqlOperationSearchResult } from '@/object-record/graphql/types/RecordGqlOperationSearchResult';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useSearchRecordsQuery } from '@/object-record/hooks/useSearchRecordsQuery';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useMemo } from 'react';
import { logError } from '~/utils/logError';
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
RecordGqlOperationVariables & {
onError?: (error?: Error) => void;
skip?: boolean;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
fetchPolicy?: WatchQueryFetchPolicy;
searchInput?: string;
};
export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
searchInput,
limit,
skip,
recordGqlFields,
fetchPolicy,
}: UseSearchRecordsParams) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { searchRecordsQuery } = useSearchRecordsQuery({
objectNameSingular,
recordGqlFields,
});
const { enqueueSnackBar } = useSnackBar();
const { data, loading, error } = useQuery<RecordGqlOperationSearchResult>(
searchRecordsQuery,
{
skip:
skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput,
variables: {
search: searchInput,
limit: limit,
},
fetchPolicy: fetchPolicy,
onError: (error) => {
logError(
`useSearchRecords for "${objectMetadataItem.namePlural}" error : ` +
error,
);
enqueueSnackBar(
`Error during useSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
{
variant: SnackBarVariant.Error,
},
);
},
},
);
const queryResponseField = getSearchRecordsQueryResponseField(
objectMetadataItem.namePlural,
);
const result = data?.[queryResponseField];
const records = useMemo(
() =>
result
? (getRecordsFromRecordConnection({
recordConnection: result,
}) as T[])
: [],
[result],
);
return {
objectMetadataItem,
records: records,
loading,
error,
};
};

View File

@ -0,0 +1,33 @@
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateSearchRecordsQuery } from '@/object-record/utils/generateSearchRecordsQuery';
export const useSearchRecordsQuery = ({
objectNameSingular,
recordGqlFields,
computeReferences,
}: {
objectNameSingular: string;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
computeReferences?: boolean;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const searchRecordsQuery = generateSearchRecordsQuery({
objectMetadataItem,
objectMetadataItems,
recordGqlFields,
computeReferences,
});
return {
searchRecordsQuery,
};
};

View File

@ -0,0 +1,40 @@
import gql from 'graphql-tag';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
import { capitalize } from '~/utils/string/capitalize';
export type QueryCursorDirection = 'before' | 'after';
export const generateSearchRecordsQuery = ({
objectMetadataItem,
objectMetadataItems,
recordGqlFields,
computeReferences,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[]; // TODO - what is this used for?
recordGqlFields?: RecordGqlOperationGqlRecordFields;
computeReferences?: boolean;
}) => gql`
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) {
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem,
recordGqlFields,
computeReferences,
})}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;

View File

@ -0,0 +1,4 @@
import { capitalize } from '~/utils/string/capitalize';
export const getSearchRecordsQueryResponseField = (objectNamePlural: string) =>
`search${capitalize(objectNamePlural)}`;

View File

@ -0,0 +1,8 @@
export type Opportunity = {
__typename: 'Opportunity';
id: string;
createdAt: string;
updatedAt?: string;
deletedAt?: string | null;
name: string | null;
};

View File

@ -2,5 +2,5 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
export type SettingsSupportedFieldType = Exclude<
FieldMetadataType,
FieldMetadataType.Position
FieldMetadataType.Position | FieldMetadataType.TsVector
>;

View File

@ -9,4 +9,7 @@ export type FeatureFlagKey =
| 'IS_FREE_ACCESS_ENABLED'
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
| 'IS_WORKFLOW_ENABLED'
| 'IS_WORKSPACE_FAVORITE_ENABLED';
| 'IS_WORKSPACE_FAVORITE_ENABLED'
| 'IS_SEARCH_ENABLED'
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
| 'IS_WORKSPACE_MIGRATED_FOR_SEARCH';