Fix optimistic rendering (#2882)

* Release 0.2.1

* Optimistic rendering fixes

* Fix optimistic rendering

* Fix issues on Tasks

* Fix Opportunity picker and relation picker
This commit is contained in:
Charles Bochet
2023-12-09 10:38:37 +01:00
committed by GitHub
parent e7bdb17128
commit 9d4ed323a7
26 changed files with 267 additions and 157 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "twenty", "name": "twenty",
"version": "0.2.0", "version": "0.2.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@air/react-drag-to-select": "^5.0.8", "@air/react-drag-to-select": "^5.0.8",

View File

@ -25,6 +25,7 @@ export const ActivityBodyEditor = ({
const [body, setBody] = useState<string | null>(null); const [body, setBody] = useState<string | null>(null);
const { updateOneRecord } = useUpdateOneRecord({ const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: 'activity', objectNameSingular: 'activity',
refetchFindManyQuery: true,
}); });
useEffect(() => { useEffect(() => {

View File

@ -82,6 +82,7 @@ export const ActivityEditor = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({ const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
objectNameSingular: 'activity', objectNameSingular: 'activity',
refetchFindManyQuery: true,
}); });
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({ const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({

View File

@ -24,6 +24,7 @@ export const useOpenCreateActivityDrawer = () => {
}); });
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({ const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
objectNameSingular: 'activity', objectNameSingular: 'activity',
refetchFindManyQuery: true,
}); });
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();

View File

@ -13,6 +13,7 @@ export const ActivityActionBar = ({ activityId }: ActivityActionBarProps) => {
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({ const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({
objectNameSingular: 'activity', objectNameSingular: 'activity',
refetchFindManyQuery: true,
}); });
const deleteActivity = () => { const deleteActivity = () => {

View File

@ -8,6 +8,7 @@ type Task = Pick<Activity, 'id' | 'completedAt'>;
export const useCompleteTask = (task: Task) => { export const useCompleteTask = (task: Task) => {
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({ const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({
objectNameSingular: 'activity', objectNameSingular: 'activity',
refetchFindManyQuery: true,
}); });
const completeTask = useCallback( const completeTask = useCallback(

View File

@ -146,6 +146,7 @@ export const useOptimisticEffect = ({
} }
} }
}, },
[apolloClient.cache],
); );
return { return {

View File

@ -7,12 +7,10 @@ import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardC
import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity'; import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
export const NewOpportunityButton = () => { export const NewOpportunityButton = () => {
const [isCreatingCard, setIsCreatingCard] = useState(false); const [isCreatingCard, setIsCreatingCard] = useState(false);
@ -55,9 +53,7 @@ export const NewOpportunityButton = () => {
setIsCreatingCard(false); setIsCreatingCard(false);
}; };
const [relationPickerSearchFilter] = useRecoilScopedState( const { relationPickerSearchFilter } = useRelationPicker();
relationPickerSearchFilterScopedState,
);
// TODO: refactor useFilteredSearchEntityQuery // TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery } = useObjectMetadataItem({ const { findManyRecordsQuery } = useObjectMetadataItem({

View File

@ -68,6 +68,13 @@ export const RecordShowPage = () => {
objectNameSingular, objectNameSingular,
}); });
const objectMetadataType =
objectMetadataItem?.nameSingular === 'company'
? 'Company'
: objectMetadataItem?.nameSingular === 'person'
? 'Person'
: 'Custom';
const useUpdateOneObjectRecordMutation: () => [ const useUpdateOneObjectRecordMutation: () => [
(params: any) => any, (params: any) => any,
any, any,
@ -171,22 +178,21 @@ export const RecordShowPage = () => {
hasBackButton hasBackButton
Icon={IconBuildingSkyscraper} Icon={IconBuildingSkyscraper}
> >
<PageFavoriteButton {objectMetadataType !== 'Custom' && (
isFavorite={isFavorite} <>
onClick={handleFavoriteButtonClick} <PageFavoriteButton
/> isFavorite={isFavorite}
<ShowPageAddButton onClick={handleFavoriteButtonClick}
key="add" />
entity={{ <ShowPageAddButton
id: record.id, key="add"
type: entity={{
objectMetadataItem?.nameSingular === 'company' id: record.id,
? 'Company' type: objectMetadataType,
: objectMetadataItem?.nameSingular === 'person' }}
? 'Person' />
: 'Custom', </>
}} )}
/>
</PageHeader> </PageHeader>
<PageBody> <PageBody>
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}> <RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>

View File

@ -1,24 +1,29 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord'; import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type useCreateOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useCreateOneRecord = <T>({ export const useCreateOneRecord = <T>({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { refetchFindManyQuery = false,
}: useCreateOneRecordProps) => {
const { triggerOptimisticEffects } = useOptimisticEffect({ const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular, objectNameSingular,
}); });
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem( const { objectMetadataItem, createOneRecordMutation, findManyRecordsQuery } =
{ useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}, });
);
// TODO: type this with a minimal type at least with Record<string, any> // TODO: type this with a minimal type at least with Record<string, any>
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -30,20 +35,30 @@ export const useCreateOneRecord = <T>({
const createOneRecord = async (input: Record<string, any>) => { const createOneRecord = async (input: Record<string, any>) => {
const recordId = v4(); const recordId = v4();
triggerOptimisticEffects( const generatedEmptyRecord = generateEmptyRecord({
`${capitalize(objectMetadataItem.nameSingular)}Edge`, id: recordId,
generateEmptyRecord(recordId), ...input,
); });
if (generatedEmptyRecord) {
triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
generatedEmptyRecord,
);
}
const createdObject = await apolloClient.mutate({ const createdObject = await apolloClient.mutate({
mutation: createOneRecordMutation, mutation: createOneRecordMutation,
variables: { variables: {
input: { ...input, id: recordId }, input: { id: recordId, ...input },
}, },
optimisticResponse: { optimisticResponse: {
[`create${capitalize(objectMetadataItem.nameSingular)}`]: [`create${capitalize(objectMetadataItem.nameSingular)}`]:
generateEmptyRecord(recordId), generateEmptyRecord({ id: recordId, ...input }),
}, },
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
}); });
if (!createdObject.data) { if (!createdObject.data) {

View File

@ -1,25 +1,30 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict'; import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type useDeleteOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useDeleteOneRecord = <T>({ export const useDeleteOneRecord = <T>({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { refetchFindManyQuery = false,
}: useDeleteOneRecordProps) => {
const { performOptimisticEvict } = useOptimisticEvict(); const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({ const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular, objectNameSingular,
}); });
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem( const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
{ useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}, });
);
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -42,6 +47,9 @@ export const useDeleteOneRecord = <T>({
variables: { variables: {
idToDelete, idToDelete,
}, },
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
}); });
return deletedRecord.data[ return deletedRecord.data[
@ -54,6 +62,8 @@ export const useDeleteOneRecord = <T>({
performOptimisticEvict, performOptimisticEvict,
apolloClient, apolloClient,
deleteOneRecordMutation, deleteOneRecordMutation,
refetchFindManyQuery,
findManyRecordsQuery,
], ],
); );

View File

@ -5,13 +5,17 @@ export const useGenerateEmptyRecord = ({
}: { }: {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const generateEmptyRecord = (id: string) => { // Todo fix typing once we generate the return base on Metadata
const generateEmptyRecord = <T>(input: Partial<T> & { id: string }) => {
// Todo replace this by runtime typing
const validatedInput = input as { id: string } & { [key: string]: any };
if (objectMetadataItem.nameSingular === 'company') { if (objectMetadataItem.nameSingular === 'company') {
return { return {
id, id: validatedInput.id,
domainName: '', domainName: '',
accountOwnerId: null, accountOwnerId: null,
createdAt: '2023-12-05T16:04:42.261Z', createdAt: new Date().toISOString(),
address: '', address: '',
people: [ people: [
{ {
@ -38,7 +42,7 @@ export const useGenerateEmptyRecord = ({
currencyCode: null, currencyCode: null,
__typename: 'Currency', __typename: 'Currency',
}, },
updatedAt: '2023-12-05T16:04:42.261Z', updatedAt: new Date().toISOString(),
employees: null, employees: null,
accountOwner: null, accountOwner: null,
name: '', name: '',
@ -56,12 +60,12 @@ export const useGenerateEmptyRecord = ({
__typename: 'OpportunityConnection', __typename: 'OpportunityConnection',
}, },
__typename: 'Company', __typename: 'Company',
}; } as T;
} }
if (objectMetadataItem.nameSingular === 'person') { if (objectMetadataItem.nameSingular === 'person') {
return { return {
id, id: validatedInput.id,
activityTargets: { activityTargets: {
edges: [], edges: [],
__typename: 'ActivityTargetConnection', __typename: 'ActivityTargetConnection',
@ -98,8 +102,8 @@ export const useGenerateEmptyRecord = ({
__typename: 'FullName', __typename: 'FullName',
}, },
avatarUrl: '', avatarUrl: '',
updatedAt: '2023-12-05T16:45:11.840Z', updatedAt: new Date().toISOString(),
createdAt: '2023-12-05T16:45:11.840Z', createdAt: new Date().toISOString(),
city: '', city: '',
linkedinLink: { linkedinLink: {
label: '', label: '',
@ -107,25 +111,16 @@ export const useGenerateEmptyRecord = ({
__typename: 'Link', __typename: 'Link',
}, },
__typename: 'Person', __typename: 'Person',
}; } as T;
} }
if (objectMetadataItem.nameSingular === 'opportunity') { if (objectMetadataItem.nameSingular === 'opportunity') {
return { return {
id, id: validatedInput.id,
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', pipelineStepId: validatedInput.pipelineStepId,
closeDate: null, closeDate: null,
companyId: '04b2e9f5-0713-40a5-8216-82802401d33e', updatedAt: new Date().toISOString(),
updatedAt: '2023-12-05T16:46:27.621Z', pipelineStep: null,
pipelineStep: {
id: '30b14887-d592-427d-bd97-6e670158db02',
position: 2,
name: 'Meeting',
updatedAt: '2023-12-05T11:29:21.485Z',
createdAt: '2023-12-05T11:29:21.485Z',
color: 'sky',
__typename: 'PipelineStep',
},
probability: '0', probability: '0',
pointOfContactId: null, pointOfContactId: null,
personId: null, personId: null,
@ -134,41 +129,38 @@ export const useGenerateEmptyRecord = ({
currencyCode: null, currencyCode: null,
__typename: 'Currency', __typename: 'Currency',
}, },
createdAt: '2023-12-05T16:46:27.621Z', createdAt: new Date().toISOString(),
pointOfContact: null, pointOfContact: null,
person: null, person: null,
company: { company: null,
id: '04b2e9f5-0713-40a5-8216-82802401d33e', companyId: validatedInput.companyId,
domainName: 'qonto.com',
accountOwnerId: null,
createdAt: '2023-12-05T11:29:21.484Z',
address: '',
xLink: {
label: '',
url: '',
__typename: 'Link',
},
idealCustomerProfile: null,
annualRecurringRevenue: {
amountMicros: null,
currencyCode: null,
__typename: 'Currency',
},
updatedAt: '2023-12-05T11:29:21.484Z',
employees: null,
name: 'Qonto',
linkedinLink: {
label: '',
url: '',
__typename: 'Link',
},
__typename: 'Company',
},
__typename: 'Opportunity', __typename: 'Opportunity',
}; } as T;
} }
return {}; if (objectMetadataItem.nameSingular === 'opportunity') {
return {
id: validatedInput.id,
pipelineStepId: validatedInput.pipelineStepId,
closeDate: null,
updatedAt: new Date().toISOString(),
pipelineStep: null,
probability: '0',
pointOfContactId: null,
personId: null,
amount: {
amountMicros: null,
currencyCode: null,
__typename: 'Currency',
},
createdAt: new Date().toISOString(),
pointOfContact: null,
person: null,
company: null,
companyId: validatedInput.companyId,
__typename: 'Opportunity',
} as T;
}
}; };
return { return {

View File

@ -1,16 +1,26 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type useUpdateOneRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
export const useUpdateOneRecord = <T>({ export const useUpdateOneRecord = <T>({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { refetchFindManyQuery = false,
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } = }: useUpdateOneRecordProps) => {
useObjectMetadataItem({ const {
objectNameSingular, objectMetadataItem,
}); updateOneRecordMutation,
getRecordFromCache,
findManyRecordsQuery,
} = useObjectMetadataItem({
objectNameSingular,
});
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -38,6 +48,9 @@ export const useUpdateOneRecord = <T>({
...input, ...input,
}, },
}, },
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
}); });
if (!updatedRecord?.data) { if (!updatedRecord?.data) {

View File

@ -7,11 +7,9 @@ import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid } from '@/ui/display/icon'; import { IconForbid } from '@/ui/display/icon';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
export type RelationPickerProps = { export type RelationPickerProps = {
recordId?: string; recordId?: string;
@ -32,8 +30,8 @@ export const RelationPicker = ({
initialSearchFilter, initialSearchFilter,
fieldDefinition, fieldDefinition,
}: RelationPickerProps) => { }: RelationPickerProps) => {
const [relationPickerSearchFilter, setRelationPickerSearchFilter] = const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
useRecoilScopedState(relationPickerSearchFilterScopedState); useRelationPicker();
useEffect(() => { useEffect(() => {
setRelationPickerSearchFilter(initialSearchFilter ?? ''); setRelationPickerSearchFilter(initialSearchFilter ?? '');

View File

@ -2,14 +2,13 @@ import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { IconUserCircle } from '@/ui/display/icon'; import { IconUserCircle } from '@/ui/display/icon';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { mockedPeopleData } from '~/testing/mock-data/people'; import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep'; import { sleep } from '~/testing/sleep';
import { relationPickerSearchFilterScopedState } from '../../states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '../../types/EntityForSelect'; import { EntityForSelect } from '../../types/EntityForSelect';
import { SingleEntitySelect } from '../SingleEntitySelect'; import { SingleEntitySelect } from '../SingleEntitySelect';
@ -44,9 +43,7 @@ const meta: Meta<typeof SingleEntitySelect> = {
width, width,
}) => { }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const relationPickerSearchFilter = useRecoilScopedValue( const { relationPickerSearchFilter } = useRelationPicker();
relationPickerSearchFilterScopedState,
);
return ( return (
<SingleEntitySelect <SingleEntitySelect

View File

@ -12,17 +12,20 @@ export const useRelationPickerScopedStates = (args?: {
relationPickerScopedId, relationPickerScopedId,
); );
const { identifiersMapperState } = getRelationPickerScopedStates({ const {
relationPickerScopeId: scopeId, identifiersMapperState,
}); relationPickerSearchFilterState,
relationPickerPreselectedIdState,
const { searchQueryState } = getRelationPickerScopedStates({ searchQueryState,
} = getRelationPickerScopedStates({
relationPickerScopeId: scopeId, relationPickerScopeId: scopeId,
}); });
return { return {
scopeId, scopeId,
identifiersMapperState, identifiersMapperState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState, searchQueryState,
}; };
}; };

View File

@ -1,12 +1,10 @@
import scrollIntoView from 'scroll-into-view'; import scrollIntoView from 'scroll-into-view';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { CreateButtonId } from '../constants'; import { CreateButtonId } from '../constants';
import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex'; import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
@ -17,15 +15,12 @@ export const useEntitySelectScroll = ({
selectableOptionIds: string[]; selectableOptionIds: string[];
containerRef: React.RefObject<HTMLDivElement>; containerRef: React.RefObject<HTMLDivElement>;
}) => { }) => {
const [relationPickerPreselectedId, setRelationPickerPreselectedId] = const { relationPickerPreselectedId, setRelationPickerPreselectedId } =
useRecoilScopedState( useRelationPicker();
relationPickerPreselectedIdScopedState,
RelationPickerRecoilScopeContext,
);
const preselectedIdIndex = getPreselectedIdIndex( const preselectedIdIndex = getPreselectedIdIndex(
selectableOptionIds, selectableOptionIds,
relationPickerPreselectedId, relationPickerPreselectedId ?? '',
); );
const resetScroll = () => { const resetScroll = () => {

View File

@ -1,16 +1,13 @@
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { relationPickerPreselectedIdScopedState } from '@/object-record/relation-picker/states/relationPickerPreselectedIdScopedState'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
export const useEntitySelectSearch = () => { export const useEntitySelectSearch = () => {
const [, setRelationPickerPreselectedId] = useRecoilScopedState( const {
relationPickerPreselectedIdScopedState, setRelationPickerPreselectedId,
); relationPickerSearchFilter,
setRelationPickerSearchFilter,
const [relationPickerSearchFilter, setRelationPickerSearchFilter] = } = useRelationPicker();
useRecoilScopedState(relationPickerSearchFilterScopedState);
const debouncedSetSearchFilter = debounce( const debouncedSetSearchFilter = debounce(
setRelationPickerSearchFilter, setRelationPickerSearchFilter,

View File

@ -14,10 +14,14 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
props?.relationPickerScopeId, props?.relationPickerScopeId,
); );
const { identifiersMapperState, searchQueryState } = const {
useRelationPickerScopedStates({ identifiersMapperState,
relationPickerScopedId: scopeId, searchQueryState,
}); relationPickerSearchFilterState,
relationPickerPreselectedIdState,
} = useRelationPickerScopedStates({
relationPickerScopedId: scopeId,
});
const [identifiersMapper, setIdentifiersMapper] = useRecoilState( const [identifiersMapper, setIdentifiersMapper] = useRecoilState(
identifiersMapperState, identifiersMapperState,
@ -25,11 +29,21 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
const [searchQuery, setSearchQuery] = useRecoilState(searchQueryState); const [searchQuery, setSearchQuery] = useRecoilState(searchQueryState);
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
useRecoilState(relationPickerSearchFilterState);
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
useRecoilState(relationPickerPreselectedIdState);
return { return {
scopeId, scopeId,
identifiersMapper, identifiersMapper,
setIdentifiersMapper, setIdentifiersMapper,
searchQuery, searchQuery,
setSearchQuery, setSearchQuery,
relationPickerSearchFilter,
setRelationPickerSearchFilter,
relationPickerPreselectedId,
setRelationPickerPreselectedId,
}; };
}; };

View File

@ -1,9 +1,8 @@
import { atomFamily } from 'recoil'; import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const relationPickerPreselectedIdScopedState = atomFamily< export const relationPickerPreselectedIdScopedState = createScopedState<
string, string | undefined
string
>({ >({
key: 'relationPickerPreselectedIdScopedState', key: 'relationPickerPreselectedIdScopedState',
default: (param) => param, defaultValue: undefined,
}); });

View File

@ -1,8 +1,6 @@
import { atomFamily } from 'recoil'; import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const relationPickerSearchFilterScopedState = atomFamily<string, string>( export const relationPickerSearchFilterScopedState = createScopedState<string>({
{ key: 'relationPickerSearchFilterScopedState',
key: 'relationPickerSearchFilterScopedState', defaultValue: '',
default: '', });
},
);

View File

@ -1,4 +1,6 @@
import { identifiersMapperScopedState } from '@/object-record/relation-picker/states/identifiersMapperScopedState'; import { identifiersMapperScopedState } from '@/object-record/relation-picker/states/identifiersMapperScopedState';
import { relationPickerPreselectedIdScopedState } from '@/object-record/relation-picker/states/relationPickerPreselectedIdScopedState';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { searchQueryScopedState } from '@/object-record/relation-picker/states/searchQueryScopedState'; import { searchQueryScopedState } from '@/object-record/relation-picker/states/searchQueryScopedState';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState'; import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
@ -17,8 +19,20 @@ export const getRelationPickerScopedStates = ({
relationPickerScopeId, relationPickerScopeId,
); );
const relationPickerPreselectedIdState = getScopedState(
relationPickerPreselectedIdScopedState,
relationPickerScopeId,
);
const relationPickerSearchFilterState = getScopedState(
relationPickerSearchFilterScopedState,
relationPickerScopeId,
);
return { return {
identifiersMapperState, identifiersMapperState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState, searchQueryState,
}; };
}; };

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
const StyledSection = styled.div` const StyledSection = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.betweenSiblingsGap};
`; `;
export { StyledSection as NavigationDrawerSection }; export { StyledSection as NavigationDrawerSection };

View File

@ -3,8 +3,8 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem'; import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults'; import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
@ -74,8 +74,8 @@ export const SettingsObjectNewFieldStep2 = () => {
const [objectViews, setObjectViews] = useState<View[]>([]); const [objectViews, setObjectViews] = useState<View[]>([]);
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]); const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
const { createOneRecord: createOneViewField } = useCreateOneRecord({ const { modifyRecordFromCache: modifyViewFromCache } = useObjectMetadataItem({
objectNameSingular: 'viewField', objectNameSingular: 'view',
}); });
useFindManyRecords({ useFindManyRecords({
@ -141,7 +141,7 @@ export const SettingsObjectNewFieldStep2 = () => {
); );
objectViews.forEach(async (view) => { objectViews.forEach(async (view) => {
await createOneViewField?.({ const viewFieldToCreate = {
viewId: view.id, viewId: view.id,
fieldMetadataId: fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE' validatedFormValues.relation.type === 'MANY_TO_ONE'
@ -150,10 +150,25 @@ export const SettingsObjectNewFieldStep2 = () => {
position: activeObjectMetadataItem.fields.length, position: activeObjectMetadataItem.fields.length,
isVisible: true, isVisible: true,
size: 100, size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
}); });
}); });
relationObjectViews.forEach(async (view) => { relationObjectViews.forEach(async (view) => {
await createOneViewField?.({ const viewFieldToCreate = {
viewId: view.id, viewId: view.id,
fieldMetadataId: fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE' validatedFormValues.relation.type === 'MANY_TO_ONE'
@ -162,10 +177,24 @@ export const SettingsObjectNewFieldStep2 = () => {
position: relationObjectMetadataItem?.fields.length, position: relationObjectMetadataItem?.fields.length,
isVisible: true, isVisible: true,
size: 100, size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
}); });
}); });
} else { } else {
await createMetadataField({ const createdMetadataField = await createMetadataField({
description: validatedFormValues.description, description: validatedFormValues.description,
icon: validatedFormValues.icon, icon: validatedFormValues.icon,
label: validatedFormValues.label ?? '', label: validatedFormValues.label ?? '',
@ -176,6 +205,31 @@ export const SettingsObjectNewFieldStep2 = () => {
? validatedFormValues.select ? validatedFormValues.select
: undefined, : undefined,
}); });
objectViews.forEach(async (view) => {
const viewFieldToCreate = {
viewId: view.id,
fieldMetadataId: createdMetadataField.data?.createOneField.id,
position: activeObjectMetadataItem.fields.length,
isVisible: true,
size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
});
});
} }
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectSlug}`);

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.2.0", "version": "0.2.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@ -131,6 +131,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true, isNullable: true,
isActive: true, isActive: true,
isCustom: false, isCustom: false,
isSystem: true,
workspaceId: record.workspaceId, workspaceId: record.workspaceId,
defaultValue: { type: 'now' }, defaultValue: { type: 'now' },
}, },
@ -168,7 +169,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: record.nameSingular, name: record.nameSingular,
label: record.labelSingular, label: record.labelSingular,
targetColumnMap: {}, targetColumnMap: {
value: `${createdObjectMetadata.targetTableName}Id`,
},
description: `ActivityTarget ${record.labelSingular}`, description: `ActivityTarget ${record.labelSingular}`,
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
isNullable: true, isNullable: true,