[WIP] Whole FE migrated (#2517)

* Wip

* WIP

* Removed concole log

* Add relations to workspace init (#2511)

* Add relations to workspace init

* remove logs

* update prefill

* add missing isSystem

* comment relation fields

* Migrate v2 core models to graphql schema (#2509)

* migrate v2 core models to graphql schema

* Migrate to new workspace member schema

* Continue work

* migrated-main

* Finished accountOwner nested field integration on companies

* Introduce bug

* Fix

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Charles Bochet
2023-11-15 15:46:06 +01:00
committed by GitHub
parent 1f49ed2acf
commit 6129444c5c
129 changed files with 3468 additions and 1497 deletions

View File

@ -3,6 +3,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies';
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
@ -23,6 +24,7 @@ export const useOpenCreateActivityDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const [createActivityMutation] = useCreateActivityMutation();
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setHotkeyScope = useSetHotkeyScope();
const [, setActivityTargetableEntityArray] = useRecoilState(
@ -49,11 +51,11 @@ export const useOpenCreateActivityDrawer = () => {
updatedAt: now,
author: { connect: { id: currentUser?.id ?? '' } },
workspaceMemberAuthor: {
connect: { id: currentUser?.workspaceMember?.id ?? '' },
connect: { id: currentWorkspaceMember?.id ?? '' },
},
assignee: { connect: { id: assigneeId ?? currentUser?.id ?? '' } },
workspaceMemberAssignee: {
connect: { id: currentUser?.workspaceMember?.id ?? '' },
connect: { id: currentWorkspaceMember?.id ?? '' },
},
type: type,
activityTargets: {

View File

@ -1,7 +1,8 @@
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { turnFilterIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFilterIntoWhereClause';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
@ -9,6 +10,7 @@ import { parseDate } from '~/utils/date-utils';
export const useCurrentUserTaskCount = () => {
const [currentUser] = useRecoilState(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { data } = useGetActivitiesQuery({
variables: {
@ -20,8 +22,11 @@ export const useCurrentUserTaskCount = () => {
fieldMetadataId: 'assigneeId',
value: currentUser.id,
operand: ViewFilterOperand.Is,
displayValue: currentUser.displayName,
displayAvatarUrl: currentUser.avatarUrl ?? undefined,
displayValue:
currentWorkspaceMember?.firstName +
' ' +
currentWorkspaceMember?.lastName,
displayAvatarUrl: currentWorkspaceMember?.avatarUrl ?? undefined,
definition: {
type: 'ENTITY',
},

View File

@ -9,7 +9,7 @@ import { useRecoilCallback } from 'recoil';
import { GET_COMPANIES } from '@/companies/graphql/queries/getCompanies';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { generateFindManyCustomObjectsQuery } from '@/object-record/utils/generateFindManyCustomObjectsQuery';
import { useGenerateFindManyCustomObjectsQuery } from '@/object-record/utils/useGenerateFindManyCustomObjectsQuery';
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
import { GET_API_KEYS } from '@/settings/developers/graphql/queries/getApiKeys';
import {
@ -54,7 +54,7 @@ export const useOptimisticEffect = () => {
objectMetadataItem?: ObjectMetadataItem;
}) => {
if (isUsingFlexibleBackend && objectMetadataItem) {
const generatedQuery = generateFindManyCustomObjectsQuery({
const generatedQuery = useGenerateFindManyCustomObjectsQuery({
objectMetadataItem,
});

View File

@ -9,49 +9,5 @@ export const USER_QUERY_FRAGMENT = gql`
lastName
canImpersonate
supportUserHash
avatarUrl
workspaceMember {
id
allowImpersonation
workspace {
id
domainName
displayName
logo
inviteHash
}
assignedActivities {
id
title
}
authoredActivities {
id
title
}
authoredAttachments {
id
name
type
}
settings {
id
colorScheme
locale
}
companies {
id
name
domainName
}
comments {
id
body
}
}
settings {
id
colorScheme
locale
}
}
`;

View File

@ -4,12 +4,19 @@ import {
snapshot_UNSTABLE,
useGotoRecoilSnapshot,
useRecoilState,
useSetRecoilState,
} from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { CREATE_ONE_WORKSPACE_MEMBER_V2 } from '@/object-record/graphql/mutation/createOneWorkspaceMember';
import { FIND_ONE_WORKSPACE_MEMBER_V2 } from '@/object-record/graphql/queries/findOneWorkspaceMember';
import { REACT_APP_SERVER_AUTH_URL } from '~/config';
import {
useChallengeMutation,
useCheckUserExistsLazyQuery,
useGetCurrentWorkspaceLazyQuery,
useSignUpMutation,
useVerifyMutation,
} from '~/generated/graphql';
@ -19,13 +26,20 @@ import { tokenPairState } from '../states/tokenPairState';
export const useAuth = () => {
const [, setTokenPair] = useRecoilState(tokenPairState);
const [, setCurrentUser] = useRecoilState(currentUserState);
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
const [verify] = useVerifyMutation();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
const [getCurrentWorkspaceQuery, { data: getCurrentWorkspaceData }] =
useGetCurrentWorkspaceLazyQuery();
const client = useApolloClient();
@ -67,36 +81,56 @@ export const useAuth = () => {
throw new Error('No verify result');
}
if (!verifyResult.data?.verify.user.workspaceMember) {
throw new Error('No workspace member');
}
if (!verifyResult.data?.verify.user.workspaceMember.settings) {
throw new Error('No settings');
}
setCurrentUser({
...verifyResult.data?.verify.user,
workspaceMember: {
...verifyResult.data?.verify.user.workspaceMember,
settings: verifyResult.data?.verify.user.workspaceMember.settings,
setTokenPair(verifyResult.data?.verify.tokens);
const workspaceMember = await client.query({
query: FIND_ONE_WORKSPACE_MEMBER_V2,
variables: {
filter: {
userId: { eq: verifyResult.data?.verify.user.id },
},
},
});
setTokenPair(verifyResult.data?.verify.tokens);
const currentWorkspace = await getCurrentWorkspaceQuery();
return verifyResult.data?.verify;
setCurrentUser(verifyResult.data?.verify.user);
setCurrentWorkspaceMember(workspaceMember.data?.findMany);
setCurrentWorkspace(currentWorkspace.data?.currentWorkspace ?? null);
return {
user: verifyResult.data?.verify.user,
workspaceMember: workspaceMember.data?.findMany,
workspace: currentWorkspace.data?.currentWorkspace,
tokens: verifyResult.data?.verify.tokens,
};
},
[setTokenPair, verify, setCurrentUser],
[
verify,
setTokenPair,
client,
getCurrentWorkspaceQuery,
setCurrentUser,
setCurrentWorkspaceMember,
setCurrentWorkspace,
],
);
const handleCrendentialsSignIn = useCallback(
async (email: string, password: string) => {
const { loginToken } = await handleChallenge(email, password);
setIsVerifyPendingState(true);
const { user } = await handleVerify(loginToken.token);
return user;
const { user, workspaceMember, workspace } = await handleVerify(
loginToken.token,
);
setIsVerifyPendingState(false);
return {
user,
workspaceMember,
workspace,
};
},
[handleChallenge, handleVerify],
[handleChallenge, handleVerify, setIsVerifyPendingState],
);
const handleSignOut = useCallback(() => {
@ -110,6 +144,8 @@ export const useAuth = () => {
const handleCredentialsSignUp = useCallback(
async (email: string, password: string, workspaceInviteHash?: string) => {
setIsVerifyPendingState(true);
const signUpResult = await signUp({
variables: {
email,
@ -126,13 +162,30 @@ export const useAuth = () => {
throw new Error('No login token');
}
const { user } = await handleVerify(
const { user, workspace } = await handleVerify(
signUpResult.data?.signUp.loginToken.token,
);
return user;
const workspaceMember = await client.mutate({
mutation: CREATE_ONE_WORKSPACE_MEMBER_V2,
variables: {
input: {
firstName: user.firstName ?? '',
lastName: user.lastName ?? '',
colorScheme: 'Light',
userId: user.id,
allowImpersonation: true,
locale: 'en',
},
},
});
setCurrentWorkspaceMember(workspaceMember.data?.createWorkspaceMemberV2);
setIsVerifyPendingState(false);
return { user, workspaceMember, workspace };
},
[signUp, handleVerify],
[setIsVerifyPendingState, signUp, handleVerify, client],
);
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {

View File

@ -1,9 +1,12 @@
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { tokenPairState } from '../states/tokenPairState';
export const useIsLogged = (): boolean => {
const [tokenPair] = useRecoilState(tokenPairState);
const isVerifyPending = useRecoilValue(isVerifyPendingState);
return !!tokenPair;
return !!tokenPair && !isVerifyPending;
};

View File

@ -1,15 +1,26 @@
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsLogged } from '../hooks/useIsLogged';
import { currentUserState } from '../states/currentUserState';
import {
getOnboardingStatus,
OnboardingStatus,
} from '../utils/getOnboardingStatus';
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
const [currentUser] = useRecoilState(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isLoggedIn = useIsLogged();
return getOnboardingStatus(isLoggedIn, currentUser);
console.log(
getOnboardingStatus(isLoggedIn, currentWorkspaceMember, currentWorkspace),
);
return getOnboardingStatus(
isLoggedIn,
currentWorkspaceMember,
currentWorkspace,
);
};

View File

@ -62,7 +62,7 @@ export const useSignInUp = () => {
});
const [showErrors, setShowErrors] = useState(false);
const { data: workspace } = useGetWorkspaceFromInviteHashQuery({
const { data: workspaceFromInviteHash } = useGetWorkspaceFromInviteHashQuery({
variables: { inviteHash: workspaceInviteHash || '' },
});
@ -119,20 +119,23 @@ export const useSignInUp = () => {
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
let user;
let currentWorkspace;
if (signInUpMode === SignInUpMode.SignIn) {
user = await signInWithCredentials(
const { workspace } = await signInWithCredentials(
data.email.toLowerCase(),
data.password,
);
currentWorkspace = workspace;
} else {
user = await signUpWithCredentials(
const { workspace } = await signUpWithCredentials(
data.email.toLowerCase(),
data.password,
workspaceInviteHash,
);
currentWorkspace = workspace;
}
if (user?.workspaceMember?.workspace?.displayName) {
if (currentWorkspace?.displayName) {
navigate('/');
} else {
navigate('/create/workspace');
@ -144,12 +147,12 @@ export const useSignInUp = () => {
}
},
[
navigate,
signInUpMode,
signInWithCredentials,
signUpWithCredentials,
workspaceInviteHash,
navigate,
enqueueSnackBar,
signInUpMode,
],
);
@ -188,6 +191,6 @@ export const useSignInUp = () => {
goBackToEmailStep,
submitCredentials,
form,
workspace: workspace?.findWorkspaceFromInviteHash,
workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
};
};

View File

@ -1,32 +1,11 @@
import { atom } from 'recoil';
import {
User,
UserSettings,
Workspace,
WorkspaceMember,
} from '~/generated/graphql';
import { User } from '~/generated/graphql';
export type CurrentUser = Pick<
User,
| 'id'
| 'email'
| 'displayName'
| 'firstName'
| 'lastName'
| 'avatarUrl'
| 'canImpersonate'
| 'supportUserHash'
> & {
workspaceMember: Pick<WorkspaceMember, 'id' | 'allowImpersonation'> & {
workspace: Pick<
Workspace,
'id' | 'displayName' | 'domainName' | 'inviteHash' | 'logo'
>;
settings: Pick<UserSettings, 'id' | 'colorScheme' | 'locale'>;
};
settings: Pick<UserSettings, 'id' | 'colorScheme' | 'locale'>;
};
'id' | 'email' | 'supportUserHash' | 'canImpersonate'
>;
export const currentUserState = atom<CurrentUser | null>({
key: 'currentUserState',

View File

@ -0,0 +1,18 @@
import { atom } from 'recoil';
import { ColorScheme } from '~/generated-metadata/graphql';
export type CurrentWorkspaceMember = {
id: string;
locale: string;
colorScheme: ColorScheme;
allowImpersonation: boolean;
firstName: string;
lastName: string;
avatarUrl: string;
};
export const currentWorkspaceMemberState = atom<CurrentWorkspaceMember | null>({
key: 'currentWorkspaceMemberState',
default: null,
});

View File

@ -0,0 +1,13 @@
import { atom } from 'recoil';
import { Workspace } from '~/generated-metadata/graphql';
export type CurrentWorkspace = Pick<
Workspace,
'id' | 'inviteHash' | 'logo' | 'displayName'
>;
export const currentWorkspaceState = atom<CurrentWorkspace | null>({
key: 'currentWorkspaceState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isVerifyPendingState = atom<boolean>({
key: 'isVerifyPendingState',
default: false,
});

View File

@ -1,4 +1,5 @@
import { CurrentUser } from '../states/currentUserState';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
export enum OnboardingStatus {
OngoingUserCreation = 'ongoing_user_creation',
@ -9,21 +10,22 @@ export enum OnboardingStatus {
export const getOnboardingStatus = (
isLoggedIn: boolean,
currentUser: CurrentUser | null,
currentWorkspaceMember: CurrentWorkspaceMember | null,
currentWorkspace: CurrentWorkspace | null,
) => {
if (!isLoggedIn) {
return OnboardingStatus.OngoingUserCreation;
}
// if the user has not been fetched yet, we can't know the onboarding status
if (!currentUser) {
if (!currentWorkspaceMember) {
return undefined;
}
if (!currentUser.workspaceMember?.workspace.displayName) {
if (!currentWorkspace?.displayName) {
return OnboardingStatus.OngoingWorkspaceCreation;
}
if (!currentUser.firstName || !currentUser.lastName) {
if (!currentWorkspaceMember.firstName || !currentWorkspaceMember.lastName) {
return OnboardingStatus.OngoingProfileCreation;
}

View File

@ -27,7 +27,6 @@ export const FIND_MANY_METADATA_OBJECTS = gql`
label
description
icon
placeholder
isCustom
isActive
isNullable
@ -36,10 +35,34 @@ export const FIND_MANY_METADATA_OBJECTS = gql`
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
}
toRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
}
}
}
}

View File

@ -10,7 +10,7 @@ import {
import { logError } from '~/utils/logError';
import { FIND_MANY_METADATA_OBJECTS } from '../graphql/queries';
import { formatPagedObjectMetadataItemsToObjectMetadataItems } from '../utils/formatPagedObjectMetadataItemsToObjectMetadataItems';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '../utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { useApolloMetadataClient } from './useApolloMetadataClient';
@ -59,7 +59,7 @@ export const useFindManyObjectMetadataItems = ({
});
const objectMetadataItems = useMemo(() => {
return formatPagedObjectMetadataItemsToObjectMetadataItems({
return mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
});
}, [data]);

View File

@ -0,0 +1,74 @@
import { useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import {
ObjectFilter,
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables,
} from '~/generated-metadata/graphql';
import { logError } from '~/utils/logError';
import { FIND_MANY_METADATA_OBJECTS } from '../graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '../utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { useApolloMetadataClient } from './useApolloMetadataClient';
// TODO: test fetchMore
export const useFindManyObjectMetadataItems = ({
skip,
filter,
}: { skip?: boolean; filter?: ObjectFilter } = {}) => {
const apolloMetadataClient = useApolloMetadataClient();
const { enqueueSnackBar } = useSnackBar();
const {
data,
fetchMore: fetchMoreInternal,
loading,
error,
} = useQuery<ObjectMetadataItemsQuery, ObjectMetadataItemsQueryVariables>(
FIND_MANY_METADATA_OBJECTS,
{
variables: {
filter,
},
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient,
onError: (error) => {
logError('useFindManyObjectMetadataItems error : ' + error);
enqueueSnackBar(
`Error during useFindManyObjectMetadataItems, ${error.message}`,
{
variant: 'error',
},
);
},
onCompleted: () => {},
},
);
const hasMore = data?.objects?.pageInfo?.hasNextPage;
const fetchMore = () =>
fetchMoreInternal({
variables: {
afterCursor: data?.objects?.pageInfo?.endCursor,
},
});
const objectMetadataItems = useMemo(() => {
return mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
});
}, [data]);
return {
objectMetadataItems,
hasMore,
fetchMore,
loading,
error,
};
};

View File

@ -1,10 +1,10 @@
import { gql } from '@apollo/client';
import { generateCreateOneObjectMutation } from '@/object-record/utils/generateCreateOneObjectMutation';
import { generateDeleteOneObjectMutation } from '@/object-record/utils/generateDeleteOneObjectMutation';
import { generateFindManyCustomObjectsQuery } from '@/object-record/utils/generateFindManyCustomObjectsQuery';
import { generateFindOneCustomObjectQuery } from '@/object-record/utils/generateFindOneCustomObjectQuery';
import { generateUpdateOneObjectMutation } from '@/object-record/utils/generateUpdateOneObjectMutation';
import { useGenerateCreateOneObjectMutation } from '@/object-record/utils/generateCreateOneObjectMutation';
import { useGenerateDeleteOneObjectMutation } from '@/object-record/utils/useGenerateDeleteOneObjectMutation';
import { useGenerateFindManyCustomObjectsQuery } from '@/object-record/utils/useGenerateFindManyCustomObjectsQuery';
import { useGenerateFindOneCustomObjectQuery } from '@/object-record/utils/useGenerateFindOneCustomObjectQuery';
import { useGenerateUpdateOneObjectMutation } from '@/object-record/utils/useGenerateUpdateOneObjectMutation';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { FieldMetadata } from '@/ui/object/field/types/FieldMetadata';
import { ColumnDefinition } from '@/ui/object/record-table/types/ColumnDefinition';
@ -16,13 +16,13 @@ import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldM
import { useFindManyObjectMetadataItems } from './useFindManyObjectMetadataItems';
const EMPTY_QUERY = gql`
export const EMPTY_QUERY = gql`
query EmptyQuery {
empty
}
`;
const EMPTY_MUTATION = gql`
export const EMPTY_MUTATION = gql`
mutation EmptyMutation {
empty
}
@ -74,35 +74,25 @@ export const useFindOneObjectMetadataItem = ({
icons,
});
const findManyQuery = foundObjectMetadataItem
? generateFindManyCustomObjectsQuery({
objectMetadataItem: foundObjectMetadataItem,
})
: EMPTY_QUERY;
const findManyQuery = useGenerateFindManyCustomObjectsQuery({
objectMetadataItem: foundObjectMetadataItem,
});
const findOneQuery = foundObjectMetadataItem
? generateFindOneCustomObjectQuery({
objectMetadataItem: foundObjectMetadataItem,
})
: EMPTY_QUERY;
const findOneQuery = useGenerateFindOneCustomObjectQuery({
objectMetadataItem: foundObjectMetadataItem,
});
const createOneMutation = foundObjectMetadataItem
? generateCreateOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
})
: EMPTY_MUTATION;
const createOneMutation = useGenerateCreateOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
});
const updateOneMutation = foundObjectMetadataItem
? generateUpdateOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
})
: EMPTY_MUTATION;
const updateOneMutation = useGenerateUpdateOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
});
const deleteOneMutation = foundObjectMetadataItem
? generateDeleteOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
})
: EMPTY_MUTATION;
const deleteOneMutation = useGenerateDeleteOneObjectMutation({
objectMetadataItem: foundObjectMetadataItem,
});
return {
foundObjectMetadataItem,

View File

@ -0,0 +1,86 @@
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import { FieldType } from '@/ui/object/field/types/FieldType';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const useMapFieldMetadataToGraphQLQuery = () => {
const { objectMetadataItems } = useFindManyObjectMetadataItems();
const mapFieldMetadataToGraphQLQuery = (field: FieldMetadataItem): any => {
// TODO: parse
const fieldType = field.type as FieldType;
const fieldIsSimpleValue = (
[
'TEXT',
'PHONE',
'DATE',
'EMAIL',
'NUMBER',
'BOOLEAN',
'DATE',
] as FieldType[]
).includes(fieldType);
const fieldIsURL = fieldType === 'URL' || fieldType === 'URL_V2';
const fieldIsMoneyAmount =
fieldType === 'MONEY' || fieldType === 'MONEY_AMOUNT_V2';
if (fieldIsSimpleValue) {
return field.name;
} else if (
fieldType === 'RELATION' &&
field.toRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
console.log({ objectMetadataItems, field });
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
);
console.log({ relationMetadataItem });
return `${field.name}
{
id
${relationMetadataItem?.fields
.filter((field) => field.type !== 'RELATION')
.map((field) => field.name)
.join('\n')}
}`;
} else if (
fieldType === 'RELATION' &&
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
return `${field.name}
{
edges {
node {
id
}
}
}`;
} else if (fieldIsURL) {
return `
${field.name}
{
text
link
}
`;
} else if (fieldIsMoneyAmount) {
return `
${field.name}
{
amount
currency
}
`;
}
};
return mapFieldMetadataToGraphQLQuery;
};

View File

@ -1,65 +0,0 @@
import { FieldType } from '@/ui/object/field/types/FieldType';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const mapFieldMetadataToGraphQLQuery = (field: FieldMetadataItem) => {
// TODO: parse
const fieldType = field.type as FieldType;
const fieldIsSimpleValue = (
[
'TEXT',
'PHONE',
'DATE',
'EMAIL',
'NUMBER',
'BOOLEAN',
'DATE',
] as FieldType[]
).includes(fieldType);
const fieldIsURL = fieldType === 'URL' || fieldType === 'URL_V2';
const fieldIsMoneyAmount =
fieldType === 'MONEY' || fieldType === 'MONEY_AMOUNT_V2';
if (fieldIsSimpleValue) {
return field.name;
} else if (
fieldType === 'RELATION' &&
field.toRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
return `${field.name}
{
id
}`;
} else if (
fieldType === 'RELATION' &&
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY'
) {
return `${field.name}
{
edges {
node {
id
}
}
}`;
} else if (fieldIsURL) {
return `
${field.name}
{
text
link
}
`;
} else if (fieldIsMoneyAmount) {
return `
${field.name}
{
amount
currency
}
`;
}
};

View File

@ -2,8 +2,8 @@ import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatPagedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems: pagedObjectMetadataItems,
export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems,
}: {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
}) => {

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const CREATE_ONE_WORKSPACE_MEMBER_V2 = gql`
mutation CreateOneWorkspaceMemberV2($input: WorkspaceMemberV2CreateInput!) {
createWorkspaceMemberV2(data: $input) {
id
firstName
lastName
}
}
`;

View File

@ -0,0 +1,15 @@
import { gql } from '@apollo/client';
export const FIND_ONE_WORKSPACE_MEMBER_V2 = gql`
query FindManyWorkspaceMembersV2($filter: WorkspaceMemberV2FilterInput) {
workspaceMembersV2(filter: $filter) {
edges {
node {
id
firstName
lastName
}
}
}
}
`;

View File

@ -20,12 +20,10 @@ export const useDeleteOneObjectRecord = ({
const [mutate] = useMutation(deleteOneMutation);
const deleteOneObject = foundObjectMetadataItem
? (input: Record<string, any>) => {
? (idToDelete: string) => {
return mutate({
variables: {
input: {
...input,
},
idToDelete,
},
refetchQueries: [getOperationName(findManyQuery) ?? ''],
});

View File

@ -18,7 +18,7 @@ import {
PaginatedObjectTypeEdge,
PaginatedObjectTypeResults,
} from '../types/PaginatedObjectTypeResults';
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
import { mapPaginatedObjectsToObjects } from '../utils/mapPaginatedObjectsToObjects';
// TODO: test with a wrong name
// TODO: add zod to validate that we have at least id on each object
@ -163,7 +163,7 @@ export const useFindManyObjectRecords = <
const objects = useMemo(
() =>
objectNamePlural
? formatPagedObjectsToObjects({
? mapPaginatedObjectsToObjects({
pagedObjects: data,
objectNamePlural,
})

View File

@ -16,6 +16,8 @@ export const useUpdateOneObjectRecord = ({
objectNameSingular,
});
console.log('update one object');
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(updateOneMutation);

View File

@ -1,14 +1,21 @@
import { gql } from '@apollo/client';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const generateCreateOneObjectMutation = ({
export const useGenerateCreateOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItem: ObjectMetadataItem | undefined | null;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
return gql`

View File

@ -1,4 +1,4 @@
export const formatPagedObjectsToObjects = <
export const mapPaginatedObjectsToObjects = <
ObjectType extends { id: string } & Record<string, any>,
ObjectTypeQuery extends {
[objectNamePlural: string]: {

View File

@ -1,14 +1,19 @@
import { gql } from '@apollo/client';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const generateDeleteOneObjectMutation = ({
export const useGenerateDeleteOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItem: ObjectMetadataItem | undefined | null;
}) => {
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const capitalizedObjectName = capitalize(objectMetadataItem?.nameSingular);
return gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {

View File

@ -1,14 +1,21 @@
import { gql } from '@apollo/client';
import { EMPTY_QUERY } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const generateFindManyCustomObjectsQuery = ({
export const useGenerateFindManyCustomObjectsQuery = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItem: ObjectMetadataItem | undefined | null;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_QUERY;
}
return gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,

View File

@ -1,13 +1,20 @@
import { gql } from '@apollo/client';
import { EMPTY_QUERY } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
export const generateFindOneCustomObjectQuery = ({
export const useGenerateFindOneCustomObjectQuery = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItem: ObjectMetadataItem | null | undefined;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_QUERY;
}
return gql`
query FindOne${objectMetadataItem.nameSingular}($objectMetadataId: UUID!) {
${objectMetadataItem.nameSingular}(filter: {

View File

@ -1,7 +1,8 @@
import { gql } from '@apollo/client';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { capitalize } from '~/utils/string/capitalize';
export const getUpdateOneObjectMutationGraphQLField = ({
@ -12,11 +13,17 @@ export const getUpdateOneObjectMutationGraphQLField = ({
return `update${capitalize(objectNameSingular)}`;
};
export const generateUpdateOneObjectMutation = ({
export const useGenerateUpdateOneObjectMutation = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItem: ObjectMetadataItem | undefined | null;
}) => {
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const graphQLFieldForUpdateOneObjectMutation =

View File

@ -0,0 +1,149 @@
import { QueryHookOptions, QueryResult } from '@apollo/client';
import { mapPaginatedObjectsToObjects } from '@/object-record/utils/mapPaginatedObjectsToObjects';
import { EntitiesForMultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { QueryMode, SortOrder } from '~/generated/graphql';
type SelectStringKeys<T> = NonNullable<
{
[K in keyof T]: K extends '__typename'
? never
: T[K] extends string | undefined | null
? K
: never;
}[keyof T]
>;
type ExtractEntityTypeFromQueryResponse<T> = T extends {
searchResults: Array<infer U>;
}
? U
: never;
type SearchFilter = { fieldNames: string[]; filter: string | number };
const DEFAULT_SEARCH_REQUEST_LIMIT = 10;
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
// Filtered entities to select are
export const useFilteredSearchEntityQueryV2 = ({
queryHook,
orderByField,
filters,
sortOrder = SortOrder.Asc,
selectedIds,
mappingFunction,
limit,
excludeEntityIds = [],
objectNamePlural,
}: {
queryHook: (
queryOptions?: QueryHookOptions<any, any>,
) => QueryResult<any, any>;
orderByField: string;
filters: SearchFilter[];
sortOrder?: SortOrder;
selectedIds: string[];
mappingFunction: (entity: any) => EntityForSelect;
limit?: number;
excludeEntityIds?: string[];
objectNamePlural: string;
}): EntitiesForMultipleEntitySelect<EntityForSelect> => {
const { loading: selectedEntitiesLoading, data: selectedEntitiesData } =
queryHook({
variables: {
where: {
id: {
in: selectedIds,
},
},
orderBy: {
[orderByField]: sortOrder,
},
} as any,
});
const searchFilter = filters.map(({ fieldNames, filter }) => {
return {
OR: fieldNames.map((fieldName) => ({
[fieldName]: {
startsWith: `%${filter}%`,
mode: QueryMode.Insensitive,
},
})),
};
});
const {
loading: filteredSelectedEntitiesLoading,
data: filteredSelectedEntitiesData,
} = queryHook({
variables: {
where: {
AND: [
{
AND: searchFilter,
},
{
id: {
in: selectedIds,
},
},
],
},
orderBy: {
[orderByField]: sortOrder,
},
} as any,
});
const { loading: entitiesToSelectLoading, data: entitiesToSelectData } =
queryHook({
variables: {
where: {
AND: [
{
AND: searchFilter,
},
{
id: {
notIn: [...selectedIds, ...excludeEntityIds],
},
},
],
},
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: {
[orderByField]: sortOrder,
},
} as any,
});
console.log({
selectedEntitiesData,
test: mapPaginatedObjectsToObjects({
objectNamePlural: objectNamePlural,
pagedObjects: selectedEntitiesData,
}),
});
return {
selectedEntities: mapPaginatedObjectsToObjects({
objectNamePlural: objectNamePlural,
pagedObjects: selectedEntitiesData,
}).map(mappingFunction),
filteredSelectedEntities: mapPaginatedObjectsToObjects({
objectNamePlural: objectNamePlural,
pagedObjects: filteredSelectedEntitiesData,
}).map(mappingFunction),
entitiesToSelect: mapPaginatedObjectsToObjects({
objectNamePlural: objectNamePlural,
pagedObjects: entitiesToSelectData,
}).map(mappingFunction),
loading:
entitiesToSelectLoading ||
filteredSelectedEntitiesLoading ||
selectedEntitiesLoading,
};
};

View File

@ -5,6 +5,7 @@ import debounce from 'lodash.debounce';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { TextInput } from '@/ui/input/components/TextInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useUpdateUserMutation } from '~/generated/graphql';
@ -30,9 +31,14 @@ export const NameFields = ({
onLastNameUpdate,
}: NameFieldsProps) => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [firstName, setFirstName] = useState(currentUser?.firstName ?? '');
const [lastName, setLastName] = useState(currentUser?.lastName ?? '');
const [firstName, setFirstName] = useState(
currentWorkspaceMember?.firstName ?? '',
);
const [lastName, setLastName] = useState(
currentWorkspaceMember?.lastName ?? '',
);
const [updateUser] = useUpdateUserMutation();
@ -69,13 +75,13 @@ export const NameFields = ({
}, 500);
useEffect(() => {
if (!currentUser) {
if (!currentWorkspaceMember) {
return;
}
if (
currentUser.firstName !== firstName ||
currentUser.lastName !== lastName
currentWorkspaceMember.firstName !== firstName ||
currentWorkspaceMember.lastName !== lastName
) {
debouncedUpdate();
}
@ -83,7 +89,14 @@ export const NameFields = ({
return () => {
debouncedUpdate.cancel();
};
}, [firstName, lastName, currentUser, debouncedUpdate, autoSave]);
}, [
firstName,
lastName,
currentUser,
debouncedUpdate,
autoSave,
currentWorkspaceMember,
]);
return (
<StyledComboInputContainer>

View File

@ -1,8 +1,9 @@
import { useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
@ -16,6 +17,8 @@ export const ProfilePictureUploader = () => {
useUploadProfilePictureMutation();
const [removePicture] = useRemoveProfilePictureMutation();
const [currentUser] = useRecoilState(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [uploadController, setUploadController] =
useState<AbortController | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@ -69,7 +72,7 @@ export const ProfilePictureUploader = () => {
return (
<ImageInput
picture={getImageAbsoluteURIOrBase64(currentUser?.avatarUrl)}
picture={getImageAbsoluteURIOrBase64(currentWorkspaceMember?.avatarUrl)}
onUpload={handleUpload}
onRemove={handleRemove}
onAbort={handleAbort}

View File

@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { Toggle } from '@/ui/input/components/Toggle';
import { useUpdateAllowImpersonationMutation } from '~/generated/graphql';
@ -8,7 +8,7 @@ import { useUpdateAllowImpersonationMutation } from '~/generated/graphql';
export const ToggleField = () => {
const { enqueueSnackBar } = useSnackBar();
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [updateAllowImpersonation] = useUpdateAllowImpersonationMutation();
@ -32,7 +32,7 @@ export const ToggleField = () => {
return (
<Toggle
value={currentUser?.workspaceMember?.allowImpersonation}
value={currentWorkspaceMember?.allowImpersonation}
onChange={handleChange}
/>
);

View File

@ -2,9 +2,9 @@ import { useCallback, useEffect, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import debounce from 'lodash.debounce';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { TextInput } from '@/ui/input/components/TextInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
@ -27,10 +27,11 @@ export const NameField = ({
autoSave = true,
onNameUpdate,
}: NameFieldProps) => {
const [currentUser] = useRecoilState(currentUserState);
const workspace = currentUser?.workspaceMember?.workspace;
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [displayName, setDisplayName] = useState(workspace?.displayName ?? '');
const [displayName, setDisplayName] = useState(
currentWorkspace?.displayName ?? '',
);
const [updateWorkspace] = useUpdateWorkspaceMutation();

View File

@ -1,7 +1,7 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { ImageInput } from '@/ui/input/components/ImageInput';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
@ -13,7 +13,8 @@ import {
export const WorkspaceLogoUploader = () => {
const [uploadLogo] = useUploadWorkspaceLogoMutation();
const [removeLogo] = useRemoveWorkspaceLogoMutation();
const [currentUser] = useRecoilState(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const onUpload = async (file: File) => {
if (!file) {
return;
@ -34,9 +35,7 @@ export const WorkspaceLogoUploader = () => {
return (
<ImageInput
picture={getImageAbsoluteURIOrBase64(
currentUser?.workspaceMember?.workspace.logo,
)}
picture={getImageAbsoluteURIOrBase64(currentWorkspace?.logo)}
onUpload={onUpload}
onRemove={onRemove}
/>

View File

@ -8,7 +8,7 @@ import { Avatar, AvatarType } from '@/users/components/Avatar';
import { Chip, ChipVariant } from './Chip';
type EntityChipProps = {
export type EntityChipProps = {
linkToEntity?: string;
entityId: string;
name: string;

View File

@ -62,9 +62,13 @@ export const SingleEntitySelectBase = <
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
(entity): entity is CustomEntityForSelect =>
assertNotNull(entity) && isNonEmptyString(entity.name.trim()),
assertNotNull(entity) && isNonEmptyString(entity.name),
);
console.log({
entitiesInDropdown,
});
const { preselectedOptionId, resetScroll } = useEntitySelectScroll({
selectableOptionIds: [
EmptyButtonId,

View File

@ -5,6 +5,7 @@ export enum Entity {
Company = 'Company',
Person = 'Person',
User = 'User',
WorkspaceMember = 'WorkspaceMember',
}
export type EntityTypeForSelect =

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
import NavCollapseButton from './NavCollapseButton';
@ -53,8 +54,8 @@ const NavWorkspaceButton = ({
showCollapseButton,
}: NavWorkspaceButtonProps) => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspace = currentUser?.workspaceMember?.workspace;
const DEFAULT_LOGO =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';

View File

@ -3,6 +3,10 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import {
CurrentWorkspaceMember,
currentWorkspaceMemberState,
} from '@/auth/states/currentWorkspaceMemberState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { IconHelpCircle } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
@ -30,13 +34,18 @@ const insertScript = ({
const SupportChat = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const supportChat = useRecoilValue(supportChatState);
const [isFrontChatLoaded, setIsFrontChatLoaded] = useState(false);
const configureFront = useCallback(
(
chatId: string,
currentUser: Pick<User, 'email' | 'displayName' | 'supportUserHash'>,
currentUser: Pick<User, 'email' | 'supportUserHash'>,
currentWorkspaceMember: Pick<
CurrentWorkspaceMember,
'firstName' | 'lastName'
>,
) => {
const url = 'https://chat-assets.frontapp.com/v1/chat.bundle.js';
const script = document.querySelector(`script[src="${url}"]`);
@ -49,7 +58,10 @@ const SupportChat = () => {
chatId,
useDefaultLauncher: false,
email: currentUser.email,
name: currentUser.displayName,
name:
currentWorkspaceMember.firstName +
' ' +
currentWorkspaceMember.lastName,
userHash: currentUser?.supportUserHash,
});
setIsFrontChatLoaded(true);
@ -65,9 +77,14 @@ const SupportChat = () => {
supportChat?.supportDriver === 'front' &&
supportChat.supportFrontChatId &&
currentUser?.email &&
currentWorkspaceMember &&
!isFrontChatLoaded
) {
configureFront(supportChat.supportFrontChatId, currentUser);
configureFront(
supportChat.supportFrontChatId,
currentUser,
currentWorkspaceMember,
);
}
}, [
configureFront,
@ -75,6 +92,7 @@ const SupportChat = () => {
isFrontChatLoaded,
supportChat?.supportDriver,
supportChat.supportFrontChatId,
currentWorkspaceMember,
]);
return isFrontChatLoaded ? (

View File

@ -105,13 +105,16 @@ export const usePersistField = () => {
valueToPersist,
);
console.log({
fieldName,
valueToPersist,
});
updateEntity?.({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist
? { connect: { id: valueToPersist.id } }
: { disconnect: true },
[fieldName]: valueToPersist?.id,
},
},
});

View File

@ -1,24 +1,27 @@
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { getEntityChipFromFieldMetadata } from '@/ui/object/field/meta-types/display/utils/getEntityChipFromFieldMetadata';
import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationField();
const { entityChipDisplayMapper } = fieldDefinition;
if (!entityChipDisplayMapper) {
throw new Error(
"Missing entityChipDisplayMapper in FieldContext. Please provide it in the FieldContextProvider's value prop.",
);
}
const { name, pictureUrl, avatarType } =
entityChipDisplayMapper?.(fieldValue);
console.log({
fieldDefinition,
fieldValue,
});
const entityChipProps = getEntityChipFromFieldMetadata(
fieldDefinition,
fieldValue,
);
return (
<EntityChip
entityId={fieldValue?.id}
name={name}
pictureUrl={pictureUrl}
avatarType={avatarType}
entityId={entityChipProps.entityId}
name={entityChipProps.name}
pictureUrl={entityChipProps.pictureUrl}
avatarType={entityChipProps.avatarType}
/>
);
};

View File

@ -0,0 +1,32 @@
import { EntityChipProps } from '@/ui/display/chip/components/EntityChip';
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/ui/object/field/types/FieldMetadata';
export const getEntityChipFromFieldMetadata = (
fieldDefinition: FieldDefinition<FieldRelationMetadata>,
fieldValue: any,
) => {
const { fieldName } = fieldDefinition.metadata;
const chipValue: Pick<
EntityChipProps,
'name' | 'pictureUrl' | 'avatarType' | 'entityId'
> = {
name: '',
pictureUrl: '',
avatarType: 'rounded',
entityId: fieldValue?.id,
};
console.log({
fieldName,
fieldValue,
});
// TODO: use every
if (fieldName === 'accountOwner' && fieldValue) {
chipValue.name = fieldValue.firstName + ' ' + fieldValue.lastName;
}
return chipValue;
};

View File

@ -22,6 +22,11 @@ export const useRelationField = () => {
}),
);
console.log({
fieldDefinition,
fieldValue,
});
const fieldInitialValue = useFieldInitialValue();
const initialSearchValue = fieldInitialValue?.isEmpty

View File

@ -40,7 +40,7 @@ export const RelationFieldInput = ({
return (
<StyledRelationPickerContainer>
{fieldDefinition.metadata.relationType === Entity.Person ? (
{fieldDefinition.metadata.fieldName === 'person' ? (
<PeoplePicker
personId={initialValue?.id ?? ''}
companyId={initialValue?.companyId ?? ''}
@ -48,7 +48,7 @@ export const RelationFieldInput = ({
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
) : fieldDefinition.metadata.relationType === Entity.User ? (
) : fieldDefinition.metadata.fieldName === 'accountOwner' ? (
<UserPicker
userId={initialValue?.id ?? ''}
onSubmit={handleSubmit}

View File

@ -82,6 +82,8 @@ type RecordTableProps = {
export const RecordTable = ({ updateEntityMutation }: RecordTableProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);
console.log('record table');
const {
leaveTableFocus,
setRowSelectedState,

View File

@ -1,105 +1,32 @@
import { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import {
ColorScheme,
useUpdateOneWorkspaceMemberMutation,
useUpdateUserMutation,
} from '~/generated/graphql';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { ColorScheme } from '~/generated/graphql';
export const useColorScheme = () => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
const [currentWorkspaceMember] = useRecoilState(currentWorkspaceMemberState);
const [updateUser] = useUpdateUserMutation();
const [updateWorkspaceMember] = useUpdateOneWorkspaceMemberMutation();
const colorScheme =
!currentUser?.workspaceMember.settings?.colorScheme &&
!currentUser?.settings?.colorScheme
? ColorScheme.System
: currentUser.workspaceMember.settings?.colorScheme ??
currentUser.settings.colorScheme;
const { updateOneObject: updateOneWorkspaceMember } =
useUpdateOneObjectRecord({
objectNamePlural: 'workspaceMembersV2',
});
const colorScheme = currentWorkspaceMember?.colorScheme ?? ColorScheme.System;
const setColorScheme = useCallback(
async (value: ColorScheme) => {
try {
// connect settings to workspace member if not already connected
await updateWorkspaceMember({
variables: {
where: { id: currentUser?.workspaceMember.id },
data: { settings: { connect: { id: currentUser?.settings.id } } },
},
});
const result = await updateUser({
variables: {
where: {
id: currentUser?.id,
},
data: {
settings: {
update: {
colorScheme: value,
},
},
},
},
optimisticResponse: currentUser
? {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
...currentUser,
workspaceMember: {
...currentUser.workspaceMember,
settings: {
__typename: 'UserSettings',
id: currentUser.settings.id,
colorScheme: value,
locale: currentUser.settings.locale,
},
},
settings: {
__typename: 'UserSettings',
id: currentUser.settings.id,
colorScheme: value,
locale: currentUser.settings.locale,
},
},
}
: undefined,
update: (_cache, { data }) => {
if (
data?.updateUser.workspaceMember?.settings?.colorScheme &&
currentUser
) {
setCurrentUser({
...currentUser,
workspaceMember: {
...currentUser.workspaceMember,
settings: {
...currentUser.workspaceMember.settings,
colorScheme:
data.updateUser.workspaceMember.settings.colorScheme,
},
},
settings: {
...currentUser.settings,
colorScheme:
data.updateUser.workspaceMember.settings.colorScheme,
},
});
}
},
});
if (!result.data || result.errors) {
throw result.errors;
}
} catch (err) {}
if (!currentWorkspaceMember) {
return;
}
await updateOneWorkspaceMember?.({
idToUpdate: currentWorkspaceMember?.id,
input: {
colorScheme: value,
},
});
},
[updateWorkspaceMember, currentUser, updateUser, setCurrentUser],
[currentWorkspaceMember, updateOneWorkspaceMember],
);
return {

View File

@ -1,13 +1,14 @@
import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { useFilteredSearchEntityQueryV2 } from '@/search/hooks/useFilteredSearchEntityQueryV2';
import { IconUserCircle } from '@/ui/display/icon';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useSearchUserQuery } from '~/generated/graphql';
export type UserPickerProps = {
userId: string;
@ -18,7 +19,7 @@ export type UserPickerProps = {
};
type UserForSelect = EntityForSelect & {
entityType: Entity.User;
entityType: Entity.WorkspaceMember;
};
export const UserPicker = ({
@ -35,29 +36,39 @@ export const UserPicker = ({
setRelationPickerSearchFilter(initialSearchFilter ?? '');
}, [initialSearchFilter, setRelationPickerSearchFilter]);
const users = useFilteredSearchEntityQuery({
queryHook: useSearchUserQuery,
const { findManyQuery } = useFindOneObjectMetadataItem({
objectNamePlural: 'workspaceMembersV2',
});
const useFindManyWorkspaceMembers = () => useQuery(findManyQuery, {});
// TODO: put workspace member
const users = useFilteredSearchEntityQueryV2({
queryHook: useFindManyWorkspaceMembers,
filters: [
{
fieldNames: ['firstName', 'lastName'],
filter: relationPickerSearchFilter,
},
],
orderByField: 'firstName',
mappingFunction: (user) => ({
entityType: Entity.User,
id: user.id,
name: user.displayName,
orderByField: '',
mappingFunction: (workspaceMember) => ({
entityType: Entity.WorkspaceMember,
id: workspaceMember.id,
name: workspaceMember.firstName,
avatarType: 'rounded',
avatarUrl: user.avatarUrl ?? '',
originalEntity: user,
avatarUrl: '',
originalEntity: workspaceMember,
}),
selectedIds: userId ? [userId] : [],
objectNamePlural: 'workspaceMembersV2',
});
const handleEntitySelected = async (
selectedUser: UserForSelect | null | undefined,
) => {
console.log({
users,
});
const handleEntitySelected = async (selectedUser: any | null | undefined) => {
onSubmit(selectedUser ?? null);
};

View File

@ -1,29 +1,71 @@
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { useApolloClient } from '@apollo/client';
import { useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { useGetCurrentUserQuery } from '~/generated/graphql';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { FIND_ONE_WORKSPACE_MEMBER_V2 } from '@/object-record/graphql/queries/findOneWorkspaceMember';
import {
useGetCurrentUserQuery,
useGetCurrentWorkspaceQuery,
} from '~/generated/graphql';
export const UserProvider = ({ children }: React.PropsWithChildren) => {
const [, setCurrentUser] = useRecoilState(currentUserState);
const [isLoading, setIsLoading] = useState(true);
const [isWorkspaceMemberLoading, setIsWorkspaceMemberLoading] =
useState(true);
const apolloClient = useApolloClient();
const { data, loading } = useGetCurrentUserQuery();
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
useEffect(() => {
if (!loading) {
setIsLoading(false);
}
if (data?.currentUser?.workspaceMember?.settings) {
setCurrentUser({
...data.currentUser,
workspaceMember: {
...data.currentUser.workspaceMember,
settings: data.currentUser.workspaceMember.settings,
const { data: userData, loading: userLoading } = useGetCurrentUserQuery({
onCompleted: async (data) => {
const workspaceMember = await apolloClient.query({
query: FIND_ONE_WORKSPACE_MEMBER_V2,
variables: {
filter: {
userId: { eq: data.currentUser.id },
},
},
});
setCurrentWorkspaceMember(
workspaceMember.data.workspaceMembersV2.edges[0].node,
);
setIsWorkspaceMemberLoading(false);
},
onError: () => {
setIsWorkspaceMemberLoading(false);
},
});
const { data: workspaceData, loading: workspaceLoading } =
useGetCurrentWorkspaceQuery();
useEffect(() => {
if (!userLoading && !workspaceLoading && !isWorkspaceMemberLoading) {
setIsLoading(false);
}
}, [setCurrentUser, data, isLoading, loading]);
if (userData?.currentUser) {
setCurrentUser(userData.currentUser);
}
if (workspaceData?.currentWorkspace) {
setCurrentWorkspace(workspaceData.currentWorkspace);
}
}, [
setCurrentUser,
isLoading,
userLoading,
workspaceLoading,
userData?.currentUser,
workspaceData?.currentWorkspace,
setCurrentWorkspace,
isWorkspaceMemberLoading,
]);
return isLoading ? <></> : <>{children}</>;
};

View File

@ -5,52 +5,6 @@ export const UPDATE_USER = gql`
updateUser(data: $data, where: $where) {
id
email
displayName
firstName
lastName
avatarUrl
workspaceMember {
id
workspace {
id
domainName
displayName
logo
inviteHash
}
assignedActivities {
id
title
}
authoredActivities {
id
title
}
authoredAttachments {
id
name
type
}
settings {
id
colorScheme
locale
}
companies {
id
name
domainName
}
comments {
id
body
}
}
settings {
id
locale
colorScheme
}
}
}
`;

View File

@ -4,16 +4,7 @@ export const GET_CURRENT_USER = gql`
query GetCurrentUser {
currentUser {
...userFieldsFragment
avatarUrl
canImpersonate
workspaceMember {
...workspaceMemberFieldsFragment
}
settings {
id
locale
colorScheme
}
supportUserHash
}
}

View File

@ -0,0 +1,6 @@
export type WorkspaceMember = {
id: string;
firstName: string;
lastName: string;
avatarUrl: string;
};

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { Avatar } from '@/users/components/Avatar';
import { User } from '~/generated/graphql';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
@ -29,12 +29,7 @@ const StyledEmailText = styled.span`
`;
type WorkspaceMemberCardProps = {
workspaceMember: {
user: Pick<
User,
'id' | 'firstName' | 'lastName' | 'displayName' | 'avatarUrl' | 'email'
>;
};
workspaceMember: WorkspaceMember;
accessory?: React.ReactNode;
};
@ -44,15 +39,19 @@ export const WorkspaceMemberCard = ({
}: WorkspaceMemberCardProps) => (
<StyledContainer>
<Avatar
avatarUrl={workspaceMember.user.avatarUrl}
colorId={workspaceMember.user.id}
placeholder={workspaceMember.user.firstName || ''}
avatarUrl={workspaceMember.avatarUrl}
colorId={workspaceMember.id}
placeholder={workspaceMember.firstName || ''}
type="squared"
size="xl"
/>
<StyledContent>
<OverflowingTextWithTooltip text={workspaceMember.user.displayName} />
<StyledEmailText>{workspaceMember.user.email}</StyledEmailText>
<OverflowingTextWithTooltip
text={workspaceMember.firstName + ' ' + workspaceMember.lastName}
/>
<StyledEmailText>
{workspaceMember.firstName + ' ' + workspaceMember.lastName}
</StyledEmailText>
</StyledContent>
{accessory}

View File

@ -1,42 +0,0 @@
import { gql } from '@apollo/client';
export const WORKSPACE_MEMBER_FIELDS_FRAGMENT = gql`
fragment workspaceMemberFieldsFragment on WorkspaceMember {
id
allowImpersonation
workspace {
id
domainName
displayName
logo
inviteHash
}
assignedActivities {
id
title
}
authoredActivities {
id
title
}
authoredAttachments {
id
name
type
}
settings {
id
colorScheme
locale
}
companies {
id
name
domainName
}
comments {
id
body
}
}
`;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const REMOVE_WORKSPACE_MEMBER = gql`
mutation RemoveWorkspaceMember($where: WorkspaceMemberWhereUniqueInput!) {
deleteWorkspaceMember(where: $where) {
id
}
}
`;

View File

@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_WORKSPACE_MEMBER = gql`
mutation UpdateOneWorkspaceMember(
$data: WorkspaceMemberUpdateInput!
$where: WorkspaceMemberWhereUniqueInput!
) {
UpdateOneWorkspaceMember(data: $data, where: $where) {
...workspaceMemberFieldsFragment
}
}
`;

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const GET_CURRENT_WORKSPACE = gql`
query getCurrentWorkspace {
currentWorkspace {
id
displayName
logo
}
}
`;

View File

@ -4,10 +4,6 @@ export const GET_WORKSPACE_MEMBERS = gql`
query GetWorkspaceMembers($where: WorkspaceMemberWhereInput) {
workspaceMembers: findManyWorkspaceMember(where: $where) {
id
user {
...userFieldsFragment
avatarUrl
}
}
}
`;