mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-03 12:16:10 +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 DescriptionViewer from '@app/entityV2/summary/documentation/DescriptionViewer';
|
||||||
import EmptyDescription from '@app/entityV2/summary/documentation/EmptyDescription';
|
import EmptyDescription from '@app/entityV2/summary/documentation/EmptyDescription';
|
||||||
import { useDescriptionUtils } from '@app/entityV2/summary/documentation/useDescriptionUtils';
|
import { useDescriptionUtils } from '@app/entityV2/summary/documentation/useDescriptionUtils';
|
||||||
|
import { useDocumentationPermission } from '@app/entityV2/summary/documentation/useDocumentationPermission';
|
||||||
|
|
||||||
const StyledEditor = styled(Editor)<{ $isEditing?: boolean }>`
|
const StyledEditor = styled(Editor)<{ $isEditing?: boolean }>`
|
||||||
border: none;
|
border: none;
|
||||||
@ -46,6 +47,7 @@ export default function AboutContent() {
|
|||||||
handleCancel,
|
handleCancel,
|
||||||
emptyDescriptionText,
|
emptyDescriptionText,
|
||||||
} = useDescriptionUtils();
|
} = useDescriptionUtils();
|
||||||
|
const canEditDescription = useDocumentationPermission();
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
@ -71,7 +73,15 @@ export default function AboutContent() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DescriptionContainer onClick={() => setIsEditing(true)}>{content}</DescriptionContainer>
|
<DescriptionContainer
|
||||||
|
onClick={() => {
|
||||||
|
if (canEditDescription) {
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DescriptionContainer>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<DescriptionActionsBar
|
<DescriptionActionsBar
|
||||||
onCancel={handleCancel}
|
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 AvatarPillWithLinkAndHover from '@components/components/Avatar/AvatarPillWithLinkAndHover';
|
||||||
|
|
||||||
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
||||||
|
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
|
||||||
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
import { InstitutionalMemoryMetadata } from '@types';
|
import { InstitutionalMemoryMetadata } from '@types';
|
||||||
@ -45,6 +46,8 @@ type Props = {
|
|||||||
|
|
||||||
export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete, setShowEditLinkModal }: Props) {
|
export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete, setShowEditLinkModal }: Props) {
|
||||||
const entityRegistry = useEntityRegistryV2();
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
const hasLinkPermissions = useLinkPermission();
|
||||||
|
|
||||||
const createdBy = link.actor;
|
const createdBy = link.actor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,28 +64,32 @@ export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete,
|
|||||||
Added {formatDateString(link.created.time)} by{' '}
|
Added {formatDateString(link.created.time)} by{' '}
|
||||||
</Text>
|
</Text>
|
||||||
<AvatarPillWithLinkAndHover user={createdBy} size="sm" entityRegistry={entityRegistry} />
|
<AvatarPillWithLinkAndHover user={createdBy} size="sm" entityRegistry={entityRegistry} />
|
||||||
<StyledIcon
|
{hasLinkPermissions && (
|
||||||
icon="PencilSimpleLine"
|
<>
|
||||||
source="phosphor"
|
<StyledIcon
|
||||||
color="gray"
|
icon="PencilSimpleLine"
|
||||||
size="md"
|
source="phosphor"
|
||||||
onClick={(e) => {
|
color="gray"
|
||||||
e.preventDefault();
|
size="md"
|
||||||
setSelectedLink(link);
|
onClick={(e) => {
|
||||||
setShowEditLinkModal(true);
|
e.preventDefault();
|
||||||
}}
|
setSelectedLink(link);
|
||||||
/>
|
setShowEditLinkModal(true);
|
||||||
<StyledIcon
|
}}
|
||||||
icon="Trash"
|
/>
|
||||||
source="phosphor"
|
<StyledIcon
|
||||||
color="red"
|
icon="Trash"
|
||||||
size="md"
|
source="phosphor"
|
||||||
onClick={(e) => {
|
color="red"
|
||||||
e.preventDefault();
|
size="md"
|
||||||
setSelectedLink(link);
|
onClick={(e) => {
|
||||||
setShowConfirmDelete(true);
|
e.preventDefault();
|
||||||
}}
|
setSelectedLink(link);
|
||||||
/>
|
setShowConfirmDelete(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</RightSection>
|
</RightSection>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import AddLinkButton from '@app/entityV2/summary/links/AddLinkButton';
|
import AddLinkButton from '@app/entityV2/summary/links/AddLinkButton';
|
||||||
import LinksList from '@app/entityV2/summary/links/LinksList';
|
import LinksList from '@app/entityV2/summary/links/LinksList';
|
||||||
|
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
|
||||||
|
|
||||||
const LinksSection = styled.div`
|
const LinksSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -11,10 +12,12 @@ const LinksSection = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Links() {
|
export default function Links() {
|
||||||
|
const hasLinkPermissions = useLinkPermission();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinksSection>
|
<LinksSection>
|
||||||
<LinksList />
|
<LinksList />
|
||||||
<AddLinkButton />
|
{hasLinkPermissions && <AddLinkButton />}
|
||||||
</LinksSection>
|
</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