mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-01 11:19:05 +00:00
feat(ui/summary-tab): use permissions for documentation and links in the summary tab (#14629)
This commit is contained in:
parent
94c56decdc
commit
510b2a4082
@ -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}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user