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:
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@air/react-drag-to-select": "^5.0.8",
|
||||
@ -179,4 +179,4 @@
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ export const ActivityBodyEditor = ({
|
||||
const [body, setBody] = useState<string | null>(null);
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -82,6 +82,7 @@ export const ActivityEditor = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
|
||||
|
||||
@ -24,6 +24,7 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
});
|
||||
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
@ -13,6 +13,7 @@ export const ActivityActionBar = ({ activityId }: ActivityActionBarProps) => {
|
||||
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
|
||||
const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
const deleteActivity = () => {
|
||||
|
||||
@ -8,6 +8,7 @@ type Task = Pick<Activity, 'id' | 'completedAt'>;
|
||||
export const useCompleteTask = (task: Task) => {
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
const completeTask = useCallback(
|
||||
|
||||
@ -146,6 +146,7 @@ export const useOptimisticEffect = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[apolloClient.cache],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -7,12 +7,10 @@ import { BoardColumnContext } from '@/object-record/record-board/contexts/BoardC
|
||||
import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity';
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
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 { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
export const NewOpportunityButton = () => {
|
||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
@ -55,9 +53,7 @@ export const NewOpportunityButton = () => {
|
||||
setIsCreatingCard(false);
|
||||
};
|
||||
|
||||
const [relationPickerSearchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
const { relationPickerSearchFilter } = useRelationPicker();
|
||||
|
||||
// TODO: refactor useFilteredSearchEntityQuery
|
||||
const { findManyRecordsQuery } = useObjectMetadataItem({
|
||||
|
||||
@ -68,6 +68,13 @@ export const RecordShowPage = () => {
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectMetadataType =
|
||||
objectMetadataItem?.nameSingular === 'company'
|
||||
? 'Company'
|
||||
: objectMetadataItem?.nameSingular === 'person'
|
||||
? 'Person'
|
||||
: 'Custom';
|
||||
|
||||
const useUpdateOneObjectRecordMutation: () => [
|
||||
(params: any) => any,
|
||||
any,
|
||||
@ -171,22 +178,21 @@ export const RecordShowPage = () => {
|
||||
hasBackButton
|
||||
Icon={IconBuildingSkyscraper}
|
||||
>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: record.id,
|
||||
type:
|
||||
objectMetadataItem?.nameSingular === 'company'
|
||||
? 'Company'
|
||||
: objectMetadataItem?.nameSingular === 'person'
|
||||
? 'Person'
|
||||
: 'Custom',
|
||||
}}
|
||||
/>
|
||||
{objectMetadataType !== 'Custom' && (
|
||||
<>
|
||||
<PageFavoriteButton
|
||||
isFavorite={isFavorite}
|
||||
onClick={handleFavoriteButtonClick}
|
||||
/>
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: record.id,
|
||||
type: objectMetadataType,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||
|
||||
@ -1,24 +1,29 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useCreateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
export const useCreateOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
refetchFindManyQuery = false,
|
||||
}: useCreateOneRecordProps) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||
{
|
||||
const { objectMetadataItem, createOneRecordMutation, findManyRecordsQuery } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: type this with a minimal type at least with Record<string, any>
|
||||
const apolloClient = useApolloClient();
|
||||
@ -30,20 +35,30 @@ export const useCreateOneRecord = <T>({
|
||||
const createOneRecord = async (input: Record<string, any>) => {
|
||||
const recordId = v4();
|
||||
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
generateEmptyRecord(recordId),
|
||||
);
|
||||
const generatedEmptyRecord = generateEmptyRecord({
|
||||
id: recordId,
|
||||
...input,
|
||||
});
|
||||
|
||||
if (generatedEmptyRecord) {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
generatedEmptyRecord,
|
||||
);
|
||||
}
|
||||
|
||||
const createdObject = await apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: { ...input, id: recordId },
|
||||
input: { id: recordId, ...input },
|
||||
},
|
||||
optimisticResponse: {
|
||||
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
||||
generateEmptyRecord(recordId),
|
||||
generateEmptyRecord({ id: recordId, ...input }),
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
if (!createdObject.data) {
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
export const useDeleteOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
refetchFindManyQuery = false,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { performOptimisticEvict } = useOptimisticEvict();
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
|
||||
{
|
||||
const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
@ -42,6 +47,9 @@ export const useDeleteOneRecord = <T>({
|
||||
variables: {
|
||||
idToDelete,
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
return deletedRecord.data[
|
||||
@ -54,6 +62,8 @@ export const useDeleteOneRecord = <T>({
|
||||
performOptimisticEvict,
|
||||
apolloClient,
|
||||
deleteOneRecordMutation,
|
||||
refetchFindManyQuery,
|
||||
findManyRecordsQuery,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -5,13 +5,17 @@ export const useGenerateEmptyRecord = ({
|
||||
}: {
|
||||
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') {
|
||||
return {
|
||||
id,
|
||||
id: validatedInput.id,
|
||||
domainName: '',
|
||||
accountOwnerId: null,
|
||||
createdAt: '2023-12-05T16:04:42.261Z',
|
||||
createdAt: new Date().toISOString(),
|
||||
address: '',
|
||||
people: [
|
||||
{
|
||||
@ -38,7 +42,7 @@ export const useGenerateEmptyRecord = ({
|
||||
currencyCode: null,
|
||||
__typename: 'Currency',
|
||||
},
|
||||
updatedAt: '2023-12-05T16:04:42.261Z',
|
||||
updatedAt: new Date().toISOString(),
|
||||
employees: null,
|
||||
accountOwner: null,
|
||||
name: '',
|
||||
@ -56,12 +60,12 @@ export const useGenerateEmptyRecord = ({
|
||||
__typename: 'OpportunityConnection',
|
||||
},
|
||||
__typename: 'Company',
|
||||
};
|
||||
} as T;
|
||||
}
|
||||
|
||||
if (objectMetadataItem.nameSingular === 'person') {
|
||||
return {
|
||||
id,
|
||||
id: validatedInput.id,
|
||||
activityTargets: {
|
||||
edges: [],
|
||||
__typename: 'ActivityTargetConnection',
|
||||
@ -98,8 +102,8 @@ export const useGenerateEmptyRecord = ({
|
||||
__typename: 'FullName',
|
||||
},
|
||||
avatarUrl: '',
|
||||
updatedAt: '2023-12-05T16:45:11.840Z',
|
||||
createdAt: '2023-12-05T16:45:11.840Z',
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
city: '',
|
||||
linkedinLink: {
|
||||
label: '',
|
||||
@ -107,25 +111,16 @@ export const useGenerateEmptyRecord = ({
|
||||
__typename: 'Link',
|
||||
},
|
||||
__typename: 'Person',
|
||||
};
|
||||
} as T;
|
||||
}
|
||||
|
||||
if (objectMetadataItem.nameSingular === 'opportunity') {
|
||||
return {
|
||||
id,
|
||||
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
||||
id: validatedInput.id,
|
||||
pipelineStepId: validatedInput.pipelineStepId,
|
||||
closeDate: null,
|
||||
companyId: '04b2e9f5-0713-40a5-8216-82802401d33e',
|
||||
updatedAt: '2023-12-05T16:46:27.621Z',
|
||||
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',
|
||||
},
|
||||
updatedAt: new Date().toISOString(),
|
||||
pipelineStep: null,
|
||||
probability: '0',
|
||||
pointOfContactId: null,
|
||||
personId: null,
|
||||
@ -134,41 +129,38 @@ export const useGenerateEmptyRecord = ({
|
||||
currencyCode: null,
|
||||
__typename: 'Currency',
|
||||
},
|
||||
createdAt: '2023-12-05T16:46:27.621Z',
|
||||
createdAt: new Date().toISOString(),
|
||||
pointOfContact: null,
|
||||
person: null,
|
||||
company: {
|
||||
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
|
||||
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',
|
||||
},
|
||||
company: null,
|
||||
companyId: validatedInput.companyId,
|
||||
__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 {
|
||||
|
||||
@ -1,16 +1,26 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useUpdateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
export const useUpdateOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
refetchFindManyQuery = false,
|
||||
}: useUpdateOneRecordProps) => {
|
||||
const {
|
||||
objectMetadataItem,
|
||||
updateOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
findManyRecordsQuery,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
@ -38,6 +48,9 @@ export const useUpdateOneRecord = <T>({
|
||||
...input,
|
||||
},
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
if (!updatedRecord?.data) {
|
||||
|
||||
@ -7,11 +7,9 @@ import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
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 { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { IconForbid } from '@/ui/display/icon';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
recordId?: string;
|
||||
@ -32,8 +30,8 @@ export const RelationPicker = ({
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
|
||||
useRecoilScopedState(relationPickerSearchFilterScopedState);
|
||||
const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
|
||||
useRelationPicker();
|
||||
|
||||
useEffect(() => {
|
||||
setRelationPickerSearchFilter(initialSearchFilter ?? '');
|
||||
|
||||
@ -2,14 +2,13 @@ import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { IconUserCircle } from '@/ui/display/icon';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
import { relationPickerSearchFilterScopedState } from '../../states/relationPickerSearchFilterScopedState';
|
||||
import { EntityForSelect } from '../../types/EntityForSelect';
|
||||
import { SingleEntitySelect } from '../SingleEntitySelect';
|
||||
|
||||
@ -44,9 +43,7 @@ const meta: Meta<typeof SingleEntitySelect> = {
|
||||
width,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const relationPickerSearchFilter = useRecoilScopedValue(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
const { relationPickerSearchFilter } = useRelationPicker();
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
|
||||
@ -12,17 +12,20 @@ export const useRelationPickerScopedStates = (args?: {
|
||||
relationPickerScopedId,
|
||||
);
|
||||
|
||||
const { identifiersMapperState } = getRelationPickerScopedStates({
|
||||
relationPickerScopeId: scopeId,
|
||||
});
|
||||
|
||||
const { searchQueryState } = getRelationPickerScopedStates({
|
||||
const {
|
||||
identifiersMapperState,
|
||||
relationPickerSearchFilterState,
|
||||
relationPickerPreselectedIdState,
|
||||
searchQueryState,
|
||||
} = getRelationPickerScopedStates({
|
||||
relationPickerScopeId: scopeId,
|
||||
});
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
identifiersMapperState,
|
||||
relationPickerSearchFilterState,
|
||||
relationPickerPreselectedIdState,
|
||||
searchQueryState,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import scrollIntoView from 'scroll-into-view';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { CreateButtonId } from '../constants';
|
||||
import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
|
||||
import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
|
||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
|
||||
|
||||
@ -17,15 +15,12 @@ export const useEntitySelectScroll = ({
|
||||
selectableOptionIds: string[];
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
|
||||
useRecoilScopedState(
|
||||
relationPickerPreselectedIdScopedState,
|
||||
RelationPickerRecoilScopeContext,
|
||||
);
|
||||
const { relationPickerPreselectedId, setRelationPickerPreselectedId } =
|
||||
useRelationPicker();
|
||||
|
||||
const preselectedIdIndex = getPreselectedIdIndex(
|
||||
selectableOptionIds,
|
||||
relationPickerPreselectedId,
|
||||
relationPickerPreselectedId ?? '',
|
||||
);
|
||||
|
||||
const resetScroll = () => {
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { relationPickerPreselectedIdScopedState } from '@/object-record/relation-picker/states/relationPickerPreselectedIdScopedState';
|
||||
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
|
||||
export const useEntitySelectSearch = () => {
|
||||
const [, setRelationPickerPreselectedId] = useRecoilScopedState(
|
||||
relationPickerPreselectedIdScopedState,
|
||||
);
|
||||
|
||||
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
|
||||
useRecoilScopedState(relationPickerSearchFilterScopedState);
|
||||
const {
|
||||
setRelationPickerPreselectedId,
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
} = useRelationPicker();
|
||||
|
||||
const debouncedSetSearchFilter = debounce(
|
||||
setRelationPickerSearchFilter,
|
||||
|
||||
@ -14,10 +14,14 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
|
||||
props?.relationPickerScopeId,
|
||||
);
|
||||
|
||||
const { identifiersMapperState, searchQueryState } =
|
||||
useRelationPickerScopedStates({
|
||||
relationPickerScopedId: scopeId,
|
||||
});
|
||||
const {
|
||||
identifiersMapperState,
|
||||
searchQueryState,
|
||||
relationPickerSearchFilterState,
|
||||
relationPickerPreselectedIdState,
|
||||
} = useRelationPickerScopedStates({
|
||||
relationPickerScopedId: scopeId,
|
||||
});
|
||||
|
||||
const [identifiersMapper, setIdentifiersMapper] = useRecoilState(
|
||||
identifiersMapperState,
|
||||
@ -25,11 +29,21 @@ export const useRelationPicker = (props?: useRelationPickeProps) => {
|
||||
|
||||
const [searchQuery, setSearchQuery] = useRecoilState(searchQueryState);
|
||||
|
||||
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
|
||||
useRecoilState(relationPickerSearchFilterState);
|
||||
|
||||
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
|
||||
useRecoilState(relationPickerPreselectedIdState);
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
identifiersMapper,
|
||||
setIdentifiersMapper,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
relationPickerSearchFilter,
|
||||
setRelationPickerSearchFilter,
|
||||
relationPickerPreselectedId,
|
||||
setRelationPickerPreselectedId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const relationPickerPreselectedIdScopedState = atomFamily<
|
||||
string,
|
||||
string
|
||||
export const relationPickerPreselectedIdScopedState = createScopedState<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'relationPickerPreselectedIdScopedState',
|
||||
default: (param) => param,
|
||||
defaultValue: undefined,
|
||||
});
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const relationPickerSearchFilterScopedState = atomFamily<string, string>(
|
||||
{
|
||||
key: 'relationPickerSearchFilterScopedState',
|
||||
default: '',
|
||||
},
|
||||
);
|
||||
export const relationPickerSearchFilterScopedState = createScopedState<string>({
|
||||
key: 'relationPickerSearchFilterScopedState',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
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 { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
|
||||
@ -17,8 +19,20 @@ export const getRelationPickerScopedStates = ({
|
||||
relationPickerScopeId,
|
||||
);
|
||||
|
||||
const relationPickerPreselectedIdState = getScopedState(
|
||||
relationPickerPreselectedIdScopedState,
|
||||
relationPickerScopeId,
|
||||
);
|
||||
|
||||
const relationPickerSearchFilterState = getScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
relationPickerScopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
identifiersMapperState,
|
||||
relationPickerSearchFilterState,
|
||||
relationPickerPreselectedIdState,
|
||||
searchQueryState,
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
const StyledSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${({ theme }) => theme.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
export { StyledSection as NavigationDrawerSection };
|
||||
|
||||
@ -3,8 +3,8 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
@ -74,8 +74,8 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
const [objectViews, setObjectViews] = useState<View[]>([]);
|
||||
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
|
||||
|
||||
const { createOneRecord: createOneViewField } = useCreateOneRecord({
|
||||
objectNameSingular: 'viewField',
|
||||
const { modifyRecordFromCache: modifyViewFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: 'view',
|
||||
});
|
||||
|
||||
useFindManyRecords({
|
||||
@ -141,7 +141,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
);
|
||||
|
||||
objectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
const viewFieldToCreate = {
|
||||
viewId: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
@ -150,10 +150,25 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
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: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
relationObjectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
const viewFieldToCreate = {
|
||||
viewId: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
@ -162,10 +177,24 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
position: relationObjectMetadataItem?.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: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await createMetadataField({
|
||||
const createdMetadataField = await createMetadataField({
|
||||
description: validatedFormValues.description,
|
||||
icon: validatedFormValues.icon,
|
||||
label: validatedFormValues.label ?? '',
|
||||
@ -176,6 +205,31 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
? validatedFormValues.select
|
||||
: 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}`);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@ -148,4 +148,4 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
workspaceId: record.workspaceId,
|
||||
defaultValue: { type: 'now' },
|
||||
},
|
||||
@ -168,7 +169,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: record.nameSingular,
|
||||
label: record.labelSingular,
|
||||
targetColumnMap: {},
|
||||
targetColumnMap: {
|
||||
value: `${createdObjectMetadata.targetTableName}Id`,
|
||||
},
|
||||
description: `ActivityTarget ${record.labelSingular}`,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
|
||||
Reference in New Issue
Block a user