Untitled records for CreatedBy (#11914)
# Display "Soft-Deleted Workspace Members" in Actor Field Display Reminder of the issue : <img width="154" alt="Screenshot 2025-05-07 at 12 11 59" src="https://github.com/user-attachments/assets/168f8743-2684-4d9a-b1a4-e86bb335f7a4" /> - `ActorFieldDisplay` component : display soft-deleted members - `UserService` includes soft-deleted records when fetching workspace members. This is the tricky part : do we want that for all workspace members or maybe i could create another property dedicated to workspace members and softdeleted ones. To be discussed Result looks like this (we loose the source and the context in this impleentation) <img width="114" alt="Screenshot 2025-05-07 at 12 05 28" src="https://github.com/user-attachments/assets/3cdddd91-454f-4e96-8d6d-6fe671658945" /> Fixes https://github.com/twentyhq/twenty/issues/11870 Another way we could also get into : We could also, when a workspace user is softDeleted, change the current implementation : we could avoid to delete the ActorMetadata like CreatedByName (and context and source) in the "Person" table. It would look more like this <img width="111" alt="Screenshot 2025-05-07 at 12 06 16" src="https://github.com/user-attachments/assets/daa4ece2-200a-41f0-ba24-177375c72983" /> However, this implementation is requires more work, and IMO harder to maintain since is decouples completely the record from the workspace member. This could be an issue in case we want tohard delete a user, or decide another logic to display the Actor name. Since the usecase should be pretty rare, I chose the first one but willing to discuss it --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
@ -2,13 +2,22 @@ import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { AuthContext } from '@/auth/contexts/AuthContext';
|
||||
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersStates';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
|
||||
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
|
||||
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
|
||||
const currentWorkspaceDeletedMembers = useRecoilValue(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ currentWorkspaceMembers }}>
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
currentWorkspaceMembers,
|
||||
currentWorkspaceDeletedMembers,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { createContext } from 'react';
|
||||
import { DeletedWorkspaceMember } from '~/generated-metadata/graphql';
|
||||
|
||||
export type AuthContextType = {
|
||||
currentWorkspaceMembers: CurrentWorkspaceMember[];
|
||||
currentWorkspaceDeletedMembers: DeletedWorkspaceMember[];
|
||||
};
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>(
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { DeletedWorkspaceMember } from '~/generated-metadata/graphql';
|
||||
|
||||
export const currentWorkspaceDeletedMembersState = createState<
|
||||
DeletedWorkspaceMember[]
|
||||
>({
|
||||
key: 'currentWorkspaceDeletedMembersState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,27 +1,22 @@
|
||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||
import { useActorFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useActorFieldDisplay';
|
||||
import { ActorDisplay } from '@/ui/field/display/components/ActorDisplay';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ActorFieldDisplay = () => {
|
||||
const { fieldValue } = useActorFieldDisplay();
|
||||
|
||||
const name = !fieldValue.workspaceMemberId
|
||||
? fieldValue.name
|
||||
: [
|
||||
fieldValue.workspaceMember?.name.firstName,
|
||||
fieldValue.workspaceMember?.name.lastName,
|
||||
]
|
||||
.filter(isNonEmptyString)
|
||||
.join(' ');
|
||||
const actorFieldDisplay = useActorFieldDisplay();
|
||||
|
||||
const displayActorField = !useIsFieldEmpty();
|
||||
if (!isDefined(actorFieldDisplay)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { fieldValue, name, avatarUrl } = actorFieldDisplay;
|
||||
return displayActorField ? (
|
||||
<ActorDisplay
|
||||
name={name}
|
||||
source={fieldValue.source}
|
||||
avatarUrl={fieldValue.workspaceMember?.avatarUrl}
|
||||
avatarUrl={avatarUrl}
|
||||
workspaceMemberId={fieldValue.workspaceMemberId}
|
||||
context={fieldValue.context}
|
||||
/>
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { AuthContextDecorator } from '~/testing/decorators/AuthContextDecorator';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/ActorFieldDisplay',
|
||||
decorators: [
|
||||
AuthContextDecorator,
|
||||
MemoryRouterDecorator,
|
||||
ChipGeneratorsDecorator,
|
||||
getFieldDecorator('company', 'createdBy', {
|
||||
|
||||
@ -4,12 +4,20 @@ import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadat
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
|
||||
import { AuthContext } from '@/auth/contexts/AuthContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const useActorFieldDisplay = () => {
|
||||
export type ActorFieldDisplayValue = {
|
||||
fieldValue: FieldActorValue;
|
||||
name: string;
|
||||
} & Pick<WorkspaceMember, 'avatarUrl'>;
|
||||
|
||||
export const useActorFieldDisplay = (): ActorFieldDisplayValue | undefined => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { currentWorkspaceMembers } = useContext(AuthContext);
|
||||
const { currentWorkspaceDeletedMembers, currentWorkspaceMembers } =
|
||||
useContext(AuthContext);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
@ -17,14 +25,27 @@ export const useActorFieldDisplay = () => {
|
||||
recordId,
|
||||
fieldName,
|
||||
);
|
||||
if (!isDefined(fieldValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relatedWorkspaceMember = [
|
||||
...currentWorkspaceDeletedMembers,
|
||||
...currentWorkspaceMembers,
|
||||
].find(
|
||||
(workspaceMember) => workspaceMember.id === fieldValue.workspaceMemberId,
|
||||
);
|
||||
if (!isDefined(relatedWorkspaceMember)) {
|
||||
return {
|
||||
fieldValue,
|
||||
name: fieldValue.name,
|
||||
};
|
||||
}
|
||||
|
||||
const { name, avatarUrl } = relatedWorkspaceMember;
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: {
|
||||
...fieldValue,
|
||||
workspaceMember: currentWorkspaceMembers?.find(
|
||||
(member) => member.id === fieldValue?.workspaceMemberId,
|
||||
),
|
||||
},
|
||||
fieldValue,
|
||||
name: `${name.firstName} ${name.lastName}`,
|
||||
avatarUrl: avatarUrl,
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersStates';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@ -45,6 +46,9 @@ export const UserProviderEffect = () => {
|
||||
const setCurrentWorkspaceMembers = useSetRecoilState(
|
||||
currentWorkspaceMembersState,
|
||||
);
|
||||
const setCurrentWorkspaceMembersWithDeleted = useSetRecoilState(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
|
||||
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
||||
skip:
|
||||
@ -77,6 +81,7 @@ export const UserProviderEffect = () => {
|
||||
const {
|
||||
workspaceMember,
|
||||
workspaceMembers,
|
||||
deletedWorkspaceMembers,
|
||||
workspaces: userWorkspaces,
|
||||
} = queryData.currentUser;
|
||||
|
||||
@ -122,6 +127,10 @@ export const UserProviderEffect = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(deletedWorkspaceMembers)) {
|
||||
setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaces)) {
|
||||
const workspaces = userWorkspaces
|
||||
.map(({ workspace }) => workspace)
|
||||
@ -141,6 +150,7 @@ export const UserProviderEffect = () => {
|
||||
queryData?.currentUser,
|
||||
setIsCurrentUserLoaded,
|
||||
setDateTimeFormat,
|
||||
setCurrentWorkspaceMembersWithDeleted,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
@ -19,6 +20,9 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
workspaceMembers {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
deletedWorkspaceMembers {
|
||||
...DeletedWorkspaceMemberQueryFragment
|
||||
}
|
||||
currentUserWorkspace {
|
||||
settingsPermissions
|
||||
objectRecordsPermissions
|
||||
@ -91,4 +95,5 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
}
|
||||
|
||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
|
||||
fragment DeletedWorkspaceMemberQueryFragment on DeletedWorkspaceMember {
|
||||
id
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
avatarUrl
|
||||
userEmail
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user