[FE] handle restricted objects 2 (#12437)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-06-05 15:49:22 +02:00
committed by GitHub
parent ad804ebecd
commit 3f30964523
109 changed files with 904 additions and 306 deletions

View File

@ -67,7 +67,10 @@ export const ActivityRichTextEditor = ({
objectNameSingular: activityObjectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({ recordId: activityId });
const isRecordReadOnly = useIsRecordReadOnly({
recordId: activityId,
objectMetadataId: objectMetadataItemActivity.id,
});
const isReadOnly = isFieldValueReadOnly({
objectNameSingular: activityObjectNameSingular,

View File

@ -7,8 +7,11 @@ import { DropZone } from '@/activities/files/components/DropZone';
import { useAttachments } from '@/activities/files/hooks/useAttachments';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@ -17,8 +20,6 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
const StyledAttachmentsContainer = styled.div`
display: flex;
@ -47,8 +48,6 @@ export const Attachments = ({
const [isDraggingFile, setIsDraggingFile] = useState(false);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]);
};
@ -63,6 +62,16 @@ export const Attachments = ({
const isAttachmentsEmpty = !attachments || attachments.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (loading && isAttachmentsEmpty) {
return <SkeletonLoader />;
}
@ -94,7 +103,7 @@ export const Attachments = ({
onChange={handleFileChange}
type="file"
/>
{!hasObjectReadOnlyPermission && (
{!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
title="Add file"
@ -120,7 +129,7 @@ export const Attachments = ({
title="All"
attachments={attachments ?? []}
button={
!hasObjectReadOnlyPermission && (
!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
size="small"

View File

@ -12,9 +12,10 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { isDefined } from 'twenty-shared/utils';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({
activityObjectNameSingular,
@ -32,6 +33,7 @@ export const usePrepareFindManyActivitiesQuery = ({
const cache = useApolloClient().cache;
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
useUpsertFindManyRecordsQueryInCache({
@ -64,6 +66,7 @@ export const usePrepareFindManyActivitiesQuery = ({
objectMetadataItem: targetableObjectMetadataItem,
objectMetadataItems,
cache,
objectPermissionsByObjectMetadataId,
});
const activityTargets: (TaskTarget | NoteTarget)[] =

View File

@ -3,9 +3,12 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { NoteList } from '@/activities/notes/components/NoteList';
import { useNotes } from '@/activities/notes/hooks/useNotes';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@ -14,8 +17,6 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
const StyledNotesContainer = styled.div`
display: flex;
@ -32,14 +33,22 @@ export const Notes = ({
}) => {
const { notes, loading } = useNotes(targetableObject);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note,
});
const isNotesEmpty = !notes || notes.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (loading && isNotesEmpty) {
return <SkeletonLoader />;
}
@ -59,7 +68,7 @@ export const Notes = ({
There are no associated notes with this record.
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{!hasObjectReadOnlyPermission && (
{!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
title="New note"
@ -81,7 +90,7 @@ export const Notes = ({
title="All"
notes={notes}
button={
!hasObjectReadOnlyPermission && (
!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
size="small"

View File

@ -1,27 +1,31 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { Button } from 'twenty-ui/input';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
export const AddTaskButton = ({
activityTargetableObjects,
activityTargetableObject,
}: {
activityTargetableObjects?: ActivityTargetableObject[];
activityTargetableObject: ActivityTargetableObject;
}) => {
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: activityTargetableObject.targetObjectNameSingular,
});
if (
!isNonEmptyArray(activityTargetableObjects) ||
hasObjectReadOnlyPermission
) {
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return null;
}
@ -33,7 +37,7 @@ export const AddTaskButton = ({
title="Add task"
onClick={() =>
openCreateActivity({
targetableObjects: activityTargetableObjects,
targetableObjects: [activityTargetableObject],
})
}
/>

View File

@ -22,7 +22,7 @@ export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }}
>
<TaskGroups targetableObjects={[targetableObject]} />
<TaskGroups targetableObject={targetableObject} />
</ObjectFilterDropdownComponentInstanceContext.Provider>
</StyledContainer>
);

View File

@ -1,19 +0,0 @@
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
export const PageAddTaskButton = () => {
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
// TODO: fetch workspace member from filter here
const handleClick = () => {
openCreateActivity({
targetableObjects: [],
});
};
return <PageAddButton onClick={handleClick} />;
};

View File

@ -5,13 +5,14 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Task } from '@/activities/types/Task';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@ -20,8 +21,8 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconPlus } from 'twenty-ui/display';
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
const StyledContainer = styled.div`
display: flex;
@ -31,15 +32,23 @@ const StyledContainer = styled.div`
type TaskGroupsProps = {
filterDropdownId?: string;
targetableObjects?: ActivityTargetableObject[];
targetableObject: ActivityTargetableObject;
};
export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
const { tasks, tasksLoading } = useTasks({
targetableObjects: targetableObjects ?? [],
targetableObjects: [targetableObject],
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
@ -74,14 +83,14 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
All tasks addressed. Maintain the momentum.
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{!hasObjectReadOnlyPermission && (
{!hasObjectUpdatePermissions && (
<Button
Icon={IconPlus}
title="New task"
variant={'secondary'}
onClick={() =>
openCreateActivity({
targetableObjects: targetableObjects ?? [],
targetableObjects: [targetableObject],
})
}
/>
@ -107,7 +116,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
tasks={tasksByStatus}
button={
(status === 'TODO' || !hasTodoStatus) && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
<AddTaskButton activityTargetableObject={targetableObject} />
)
}
/>

View File

@ -42,12 +42,10 @@ export const Empty: Story = {};
export const WithTasks: Story = {
args: {
targetableObjects: [
{
id: mockedTasks[0].taskTargets?.[0].personId,
targetObjectNameSingular: 'person',
},
] as ActivityTargetableObject[],
targetableObject: {
id: mockedTasks[0].taskTargets?.[0].personId,
targetObjectNameSingular: 'person',
} as ActivityTargetableObject,
},
parameters: {
msw: graphqlMocks,