feat(ui/summary-tab): use permissions for documentation and links in the summary tab (#14629)

This commit is contained in:
purnimagarg1 2025-09-04 19:56:47 +05:30 committed by GitHub
parent 94c56decdc
commit 510b2a4082
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 329 additions and 24 deletions

View File

@ -6,6 +6,7 @@ import DescriptionActionsBar from '@app/entityV2/summary/documentation/Descripti
import DescriptionViewer from '@app/entityV2/summary/documentation/DescriptionViewer';
import EmptyDescription from '@app/entityV2/summary/documentation/EmptyDescription';
import { useDescriptionUtils } from '@app/entityV2/summary/documentation/useDescriptionUtils';
import { useDocumentationPermission } from '@app/entityV2/summary/documentation/useDocumentationPermission';
const StyledEditor = styled(Editor)<{ $isEditing?: boolean }>`
border: none;
@ -46,6 +47,7 @@ export default function AboutContent() {
handleCancel,
emptyDescriptionText,
} = useDescriptionUtils();
const canEditDescription = useDocumentationPermission();
let content;
@ -71,7 +73,15 @@ export default function AboutContent() {
}
return (
<>
<DescriptionContainer onClick={() => setIsEditing(true)}>{content}</DescriptionContainer>
<DescriptionContainer
onClick={() => {
if (canEditDescription) {
setIsEditing(true);
}
}}
>
{content}
</DescriptionContainer>
{isEditing && (
<DescriptionActionsBar
onCancel={handleCancel}

View File

@ -0,0 +1,69 @@
import { renderHook } from '@testing-library/react-hooks';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useEntityData } from '@app/entity/shared/EntityContext';
import { useDocumentationPermission } from '@app/entityV2/summary/documentation/useDocumentationPermission';
import { useCanUpdateGlossaryEntity } from '@app/entityV2/summary/shared/useCanUpdateGlossaryEntity';
// Mocks
vi.mock('@app/entity/shared/EntityContext', () => ({
useEntityData: vi.fn(),
}));
vi.mock('@app/entityV2/summary/shared/useCanUpdateGlossaryEntity', () => ({
useCanUpdateGlossaryEntity: vi.fn(),
}));
describe('useDocumentationPermissions', () => {
const setup = (entityDataProps, canUpdateGlossaryEntityMock) => {
(useEntityData as unknown as any).mockReturnValue({
entityData: entityDataProps,
});
(useCanUpdateGlossaryEntity as unknown as any).mockReturnValue(canUpdateGlossaryEntityMock);
return renderHook(() => useDocumentationPermission());
};
afterEach(() => {
vi.resetAllMocks();
});
it('should return true when canEditDescription is true and canUpdateGlossaryEntity is false', () => {
const { result } = setup({ privileges: { canEditDescription: true } }, false);
expect(result.current).toBe(true);
});
it('should return true when canEditDescription is false and canUpdateGlossaryEntity is true', () => {
const { result } = setup({ privileges: { canEditDescription: false } }, true);
expect(result.current).toBe(true);
});
it('should return true when both canEditDescription and canUpdateGlossaryEntity are true', () => {
const { result } = setup({ privileges: { canEditDescription: true } }, true);
expect(result.current).toBe(true);
});
it('should return false when both canEditDescription and canUpdateGlossaryEntity are false', () => {
const { result } = setup({ privileges: { canEditDescription: false } }, false);
expect(result.current).toBe(false);
});
it('should return false when entityData is missing and canUpdateGlossaryEntity is false', () => {
const { result } = setup(undefined, false);
expect(result.current).toBe(false);
});
it('should return true when entityData is missing but canUpdateGlossaryEntity is true', () => {
const { result } = setup(undefined, true);
expect(result.current).toBe(true);
});
it('should return false when entityData.privileges is missing and canUpdateGlossaryEntity is false', () => {
const { result } = setup({}, false);
expect(result.current).toBe(false);
});
it('should return true when entityData.privileges is missing but canUpdateGlossaryEntity is true', () => {
const { result } = setup({}, true);
expect(result.current).toBe(true);
});
});

View File

@ -0,0 +1,14 @@
import { useEntityData } from '@app/entity/shared/EntityContext';
import { useCanUpdateGlossaryEntity } from '@app/entityV2/summary/shared/useCanUpdateGlossaryEntity';
export function useDocumentationPermission() {
const { entityData } = useEntityData();
const canUpdateGlossaryEntity = useCanUpdateGlossaryEntity();
// Edit description permission
const canEditDescription = !!entityData?.privileges?.canEditDescription;
const hasDocumentationPermissions = canEditDescription || canUpdateGlossaryEntity;
return hasDocumentationPermissions;
}

View File

@ -5,6 +5,7 @@ import styled from 'styled-components';
import AvatarPillWithLinkAndHover from '@components/components/Avatar/AvatarPillWithLinkAndHover';
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
import { InstitutionalMemoryMetadata } from '@types';
@ -45,6 +46,8 @@ type Props = {
export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete, setShowEditLinkModal }: Props) {
const entityRegistry = useEntityRegistryV2();
const hasLinkPermissions = useLinkPermission();
const createdBy = link.actor;
return (
@ -61,28 +64,32 @@ export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete,
Added {formatDateString(link.created.time)} by{' '}
</Text>
<AvatarPillWithLinkAndHover user={createdBy} size="sm" entityRegistry={entityRegistry} />
<StyledIcon
icon="PencilSimpleLine"
source="phosphor"
color="gray"
size="md"
onClick={(e) => {
e.preventDefault();
setSelectedLink(link);
setShowEditLinkModal(true);
}}
/>
<StyledIcon
icon="Trash"
source="phosphor"
color="red"
size="md"
onClick={(e) => {
e.preventDefault();
setSelectedLink(link);
setShowConfirmDelete(true);
}}
/>
{hasLinkPermissions && (
<>
<StyledIcon
icon="PencilSimpleLine"
source="phosphor"
color="gray"
size="md"
onClick={(e) => {
e.preventDefault();
setSelectedLink(link);
setShowEditLinkModal(true);
}}
/>
<StyledIcon
icon="Trash"
source="phosphor"
color="red"
size="md"
onClick={(e) => {
e.preventDefault();
setSelectedLink(link);
setShowConfirmDelete(true);
}}
/>
</>
)}
</RightSection>
</LinkContainer>
</a>

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
import AddLinkButton from '@app/entityV2/summary/links/AddLinkButton';
import LinksList from '@app/entityV2/summary/links/LinksList';
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
const LinksSection = styled.div`
display: flex;
@ -11,10 +12,12 @@ const LinksSection = styled.div`
`;
export default function Links() {
const hasLinkPermissions = useLinkPermission();
return (
<LinksSection>
<LinksList />
<AddLinkButton />
{hasLinkPermissions && <AddLinkButton />}
</LinksSection>
);
}

View File

@ -0,0 +1,69 @@
import { renderHook } from '@testing-library/react-hooks';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useEntityData } from '@app/entity/shared/EntityContext';
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
import { useCanUpdateGlossaryEntity } from '@app/entityV2/summary/shared/useCanUpdateGlossaryEntity';
// Mocks
vi.mock('@app/entity/shared/EntityContext', () => ({
useEntityData: vi.fn(),
}));
vi.mock('@app/entityV2/summary/shared/useCanUpdateGlossaryEntity', () => ({
useCanUpdateGlossaryEntity: vi.fn(),
}));
describe('useGetLinkPermissions', () => {
const setup = (entityDataProps, canUpdateGlossaryEntityMock) => {
(useEntityData as unknown as any).mockReturnValue({
entityData: entityDataProps,
});
(useCanUpdateGlossaryEntity as unknown as any).mockReturnValue(canUpdateGlossaryEntityMock);
return renderHook(() => useLinkPermission());
};
afterEach(() => {
vi.resetAllMocks();
});
it('should return true when canEditLinks is true and canUpdateGlossaryEntity is false', () => {
const { result } = setup({ privileges: { canEditLinks: true } }, false);
expect(result.current).toBe(true);
});
it('should return true when canEditLinks is false and canUpdateGlossaryEntity is true', () => {
const { result } = setup({ privileges: { canEditLinks: false } }, true);
expect(result.current).toBe(true);
});
it('should return true when both canEditLinks and canUpdateGlossaryEntity are true', () => {
const { result } = setup({ privileges: { canEditLinks: true } }, true);
expect(result.current).toBe(true);
});
it('should return false when both canEditLinks and canUpdateGlossaryEntity are false', () => {
const { result } = setup({ privileges: { canEditLinks: false } }, false);
expect(result.current).toBe(false);
});
it('should return false when entityData is missing and canUpdateGlossaryEntity is false', () => {
const { result } = setup(undefined, false);
expect(result.current).toBe(false);
});
it('should return true when entityData is missing but canUpdateGlossaryEntity is true', () => {
const { result } = setup(undefined, true);
expect(result.current).toBe(true);
});
it('should return false when entityData.privileges is missing and canUpdateGlossaryEntity is false', () => {
const { result } = setup({}, false);
expect(result.current).toBe(false);
});
it('should return true when entityData.privileges is missing but canUpdateGlossaryEntity is true', () => {
const { result } = setup({}, true);
expect(result.current).toBe(true);
});
});

View File

@ -0,0 +1,14 @@
import { useEntityData } from '@app/entity/shared/EntityContext';
import { useCanUpdateGlossaryEntity } from '@app/entityV2/summary/shared/useCanUpdateGlossaryEntity';
export function useLinkPermission() {
const { entityData } = useEntityData();
const canUpdateGlossaryEntity = useCanUpdateGlossaryEntity();
// Edit links permission
const canEditLinks = !!entityData?.privileges?.canEditLinks;
const hasLinkPermissions = canEditLinks || canUpdateGlossaryEntity;
return hasLinkPermissions;
}

View File

@ -0,0 +1,100 @@
import { renderHook } from '@testing-library/react-hooks';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useUserContext } from '@app/context/useUserContext';
import { useEntityData } from '@app/entity/shared/EntityContext';
import { useCanUpdateGlossaryEntity } from '@app/entityV2/summary/shared/useCanUpdateGlossaryEntity';
import { EntityType } from '@types';
// Mocks
vi.mock('@app/entity/shared/EntityContext', () => ({
useEntityData: vi.fn(),
}));
vi.mock('@app/context/useUserContext', () => ({
useUserContext: vi.fn(),
}));
describe('useCanUpdateGlossaryEntity', () => {
const setup = (entityType, canManageGlossaries, canManageChildren) => {
(useEntityData as unknown as any).mockReturnValue({
entityData: { privileges: { canManageChildren } },
entityType,
});
(useUserContext as unknown as any).mockReturnValue({
platformPrivileges: { manageGlossaries: canManageGlossaries },
});
return renderHook(() => useCanUpdateGlossaryEntity());
};
afterEach(() => {
vi.resetAllMocks();
});
it('should return true when entityType is GlossaryNode, canManageGlossaries is true, canManageChildren is false', () => {
const { result } = setup(EntityType.GlossaryNode, true, false);
expect(result.current).toBe(true);
});
it('should return true when entityType is GlossaryNode, canManageGlossaries is false, canManageChildren is true', () => {
const { result } = setup(EntityType.GlossaryNode, false, true);
expect(result.current).toBe(true);
});
it('should return true when entityType is GlossaryTerm, canManageGlossaries is true, canManageChildren is false', () => {
const { result } = setup(EntityType.GlossaryTerm, true, false);
expect(result.current).toBe(true);
});
it('should return true when entityType is GlossaryTerm, canManageGlossaries is false, canManageChildren is true', () => {
const { result } = setup(EntityType.GlossaryTerm, false, true);
expect(result.current).toBe(true);
});
it('should return true when entityType is GlossaryTerm, both permissions are true', () => {
const { result } = setup(EntityType.GlossaryTerm, true, true);
expect(result.current).toBe(true);
});
it('should return false when entityType is GlossaryNode, both permissions are false', () => {
const { result } = setup(EntityType.GlossaryNode, false, false);
expect(result.current).toBe(false);
});
it('should return false when entityType is GlossaryTerm, both permissions are false', () => {
const { result } = setup(EntityType.GlossaryTerm, false, false);
expect(result.current).toBe(false);
});
it('should return false when entityType is Dataset, both permissions are true', () => {
const { result } = setup(EntityType.Dataset, true, true);
expect(result.current).toBe(false);
});
it('should return false when entityType is missing', () => {
const { result } = setup(undefined, true, true);
expect(result.current).toBe(false);
});
it('should return false when entityData is missing', () => {
(useEntityData as unknown as any).mockReturnValue({
entityData: undefined,
entityType: EntityType.GlossaryNode,
});
(useUserContext as unknown as any).mockReturnValue({
platformPrivileges: { manageGlossaries: true },
});
const { result } = renderHook(() => useCanUpdateGlossaryEntity());
expect(result.current).toBe(true);
});
it('should return false when user context is missing', () => {
(useEntityData as unknown as any).mockReturnValue({
entityData: { privileges: { canManageChildren: true } },
entityType: EntityType.GlossaryNode,
});
(useUserContext as unknown as any).mockReturnValue(undefined);
const { result } = renderHook(() => useCanUpdateGlossaryEntity());
expect(result.current).toBe(true);
});
});

View File

@ -0,0 +1,19 @@
import { useUserContext } from '@app/context/useUserContext';
import { useEntityData } from '@app/entity/shared/EntityContext';
import { EntityType } from '@types';
export function useCanUpdateGlossaryEntity() {
const { entityData, entityType } = useEntityData();
const user = useUserContext();
const canManageGlossaries = !!user?.platformPrivileges?.manageGlossaries;
const canManageChildren = !!entityData?.privileges?.canManageChildren;
// Manage Glossary or manage children permission for Glossary terms and Glossary nodes
const canUpdateGlossaryEntity =
(entityType === EntityType.GlossaryNode || entityType === EntityType.GlossaryTerm) &&
(canManageGlossaries || canManageChildren);
return canUpdateGlossaryEntity;
}