mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-29 11:35:56 +00:00
feat(ui/summary): build add, view and delete link functionalities in the new summary tab (#14540)
This commit is contained in:
parent
3fe39bf149
commit
4f98d15d9a
@ -0,0 +1,42 @@
|
|||||||
|
import { Avatar } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AvatarType } from '@components/components/AvatarStack/types';
|
||||||
|
import { AvatarSizeOptions } from '@components/theme/config';
|
||||||
|
|
||||||
|
import EntityRegistry from '@app/entityV2/EntityRegistry';
|
||||||
|
import { HoverEntityTooltip } from '@app/recommendations/renderer/component/HoverEntityTooltip';
|
||||||
|
|
||||||
|
import { CorpGroup, CorpUser, EntityType } from '@types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user?: CorpUser | CorpGroup;
|
||||||
|
size?: AvatarSizeOptions;
|
||||||
|
entityRegistry: EntityRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AvatarPillWithLinkAndHover({ user, size, entityRegistry }: Props) {
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const avatarUrl = user.editableProperties?.pictureLink;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverEntityTooltip entity={user} showArrow={false}>
|
||||||
|
<Link
|
||||||
|
to={`${entityRegistry.getEntityUrl(user.type, user.urn)}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
name={entityRegistry.getDisplayName(user.type, user)}
|
||||||
|
imageUrl={avatarUrl}
|
||||||
|
size={size}
|
||||||
|
type={user.type === EntityType.CorpUser ? AvatarType.user : AvatarType.group}
|
||||||
|
showInPill
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</HoverEntityTooltip>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import AboutSection from '@app/entityV2/summary/documentation/AboutSection';
|
import AboutSection from '@app/entityV2/summary/documentation/AboutSection';
|
||||||
|
import Links from '@app/entityV2/summary/links/Links';
|
||||||
import PropertiesHeader from '@app/entityV2/summary/properties/PropertiesHeader';
|
import PropertiesHeader from '@app/entityV2/summary/properties/PropertiesHeader';
|
||||||
|
|
||||||
const SummaryWrapper = styled.div`
|
const SummaryWrapper = styled.div`
|
||||||
@ -18,12 +19,19 @@ const StyledDivider = styled(Divider)`
|
|||||||
color: ${colors.gray[100]};
|
color: ${colors.gray[100]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function SummaryTab() {
|
interface Props {
|
||||||
|
hideLinksButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SummaryTab({ properties }: { properties?: Props }) {
|
||||||
|
const hideLinksButton = properties?.hideLinksButton;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SummaryWrapper>
|
<SummaryWrapper>
|
||||||
<PropertiesHeader />
|
<PropertiesHeader />
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
|
{!hideLinksButton && <Links />}
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
</SummaryWrapper>
|
</SummaryWrapper>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Button } from '@components';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import AddLinkModal from '@app/entityV2/summary/links/AddLinkModal';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
width: fit-content;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function AddLinkButton() {
|
||||||
|
const [showAddLinkModal, setShowAddLinkModal] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
setShowAddLinkModal(true);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledButton variant="text" icon={{ icon: 'Plus', source: 'phosphor' }} onClick={handleButtonClick}>
|
||||||
|
Add Link
|
||||||
|
</StyledButton>
|
||||||
|
{showAddLinkModal && <AddLinkModal setShowAddLinkModal={setShowAddLinkModal} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import { Input, Modal } from '@components';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import { useForm } from 'antd/lib/form/Form';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ModalButton } from '@components/components/Modal/Modal';
|
||||||
|
|
||||||
|
import { useLinkUtils } from '@app/entityV2/summary/links/useLinkUtils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setShowAddLinkModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AddLinkModal({ setShowAddLinkModal }: Props) {
|
||||||
|
const [form] = useForm();
|
||||||
|
const { handleAddLink } = useLinkUtils();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setShowAddLinkModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
form.validateFields()
|
||||||
|
.then((values) => handleAddLink(values))
|
||||||
|
.then(() => handleClose());
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons: ModalButton[] = [
|
||||||
|
{ text: 'Cancel', variant: 'outline', color: 'gray', onClick: handleClose },
|
||||||
|
{ text: 'Add', variant: 'filled', onClick: handleAdd },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Modal title="Add Link" onCancel={handleClose} buttons={buttons}>
|
||||||
|
<Form form={form} autoComplete="off">
|
||||||
|
<Form.Item
|
||||||
|
name="url"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'A URL is required.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'url',
|
||||||
|
message: 'This field must be a valid url.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input label="URL" placeholder="https://" isRequired />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="label"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'A label is required.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input label="Label" placeholder="A short label for this link" isRequired />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
import { Icon, Text, colors } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import AvatarPillWithLinkAndHover from '@components/components/Avatar/AvatarPillWithLinkAndHover';
|
||||||
|
|
||||||
|
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
||||||
|
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
|
import { InstitutionalMemoryMetadata } from '@types';
|
||||||
|
|
||||||
|
const LinkContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: ${colors.gray[1500]};
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LeftSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RightSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled(Icon)`
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: InstitutionalMemoryMetadata;
|
||||||
|
setSelectedLink: (link: InstitutionalMemoryMetadata | null) => void;
|
||||||
|
setShowConfirmDelete: (show: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LinkItem({ link, setSelectedLink, setShowConfirmDelete }: Props) {
|
||||||
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
const createdBy = link.actor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={link.url} target="_blank" rel="noreferrer">
|
||||||
|
<LinkContainer>
|
||||||
|
<LeftSection>
|
||||||
|
<Icon icon="LinkSimple" source="phosphor" color="primary" size="lg" />
|
||||||
|
<Text color="primary" lineHeight="normal">
|
||||||
|
{link.description || link.label}
|
||||||
|
</Text>
|
||||||
|
</LeftSection>
|
||||||
|
<RightSection>
|
||||||
|
<Text color="gray" size="sm">
|
||||||
|
Added {formatDateString(link.created.time)} by{' '}
|
||||||
|
</Text>
|
||||||
|
<AvatarPillWithLinkAndHover user={createdBy} size="sm" entityRegistry={entityRegistry} />
|
||||||
|
<StyledIcon icon="PencilSimpleLine" source="phosphor" color="gray" size="md" />
|
||||||
|
<StyledIcon
|
||||||
|
icon="Trash"
|
||||||
|
source="phosphor"
|
||||||
|
color="red"
|
||||||
|
size="md"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedLink(link);
|
||||||
|
setShowConfirmDelete(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RightSection>
|
||||||
|
</LinkContainer>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
20
datahub-web-react/src/app/entityV2/summary/links/Links.tsx
Normal file
20
datahub-web-react/src/app/entityV2/summary/links/Links.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import AddLinkButton from '@app/entityV2/summary/links/AddLinkButton';
|
||||||
|
import LinksList from '@app/entityV2/summary/links/LinksList';
|
||||||
|
|
||||||
|
const LinksSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function Links() {
|
||||||
|
return (
|
||||||
|
<LinksSection>
|
||||||
|
<LinksList />
|
||||||
|
<AddLinkButton />
|
||||||
|
</LinksSection>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { useEntityData } from '@app/entity/shared/EntityContext';
|
||||||
|
import LinkItem from '@app/entityV2/summary/links/LinkItem';
|
||||||
|
import { useLinkUtils } from '@app/entityV2/summary/links/useLinkUtils';
|
||||||
|
import { ConfirmationModal } from '@app/sharedV2/modals/ConfirmationModal';
|
||||||
|
|
||||||
|
import { InstitutionalMemoryMetadata } from '@types';
|
||||||
|
|
||||||
|
const ListContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function LinksList() {
|
||||||
|
const { entityData } = useEntityData();
|
||||||
|
const links = entityData?.institutionalMemory?.elements || [];
|
||||||
|
const [showConfirmDelete, setShowConfirmDelete] = useState<boolean>(false);
|
||||||
|
const [selectedLink, setSelectedLink] = useState<InstitutionalMemoryMetadata | null>(null);
|
||||||
|
const { handleDeleteLink } = useLinkUtils();
|
||||||
|
|
||||||
|
if (links.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedLink) {
|
||||||
|
handleDeleteLink(selectedLink).then(() => {
|
||||||
|
setSelectedLink(null);
|
||||||
|
setShowConfirmDelete(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowConfirmDelete(false);
|
||||||
|
setSelectedLink(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListContainer>
|
||||||
|
{links.map((link) => {
|
||||||
|
return (
|
||||||
|
<LinkItem
|
||||||
|
link={link}
|
||||||
|
setSelectedLink={setSelectedLink}
|
||||||
|
setShowConfirmDelete={setShowConfirmDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ListContainer>
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showConfirmDelete}
|
||||||
|
handleClose={handleCloseModal}
|
||||||
|
handleConfirm={handleDelete}
|
||||||
|
modalTitle="Confirm Delete"
|
||||||
|
modalText="Are you sure you want to delete this link?"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import analytics, { EntityActionType, EventType } from '@app/analytics';
|
||||||
|
// Helpers
|
||||||
|
import { useUserContext } from '@app/context/useUserContext';
|
||||||
|
import { useEntityData, useMutationUrn, useRefetch } from '@app/entity/shared/EntityContext';
|
||||||
|
import { useLinkUtils } from '@app/entityV2/summary/links/useLinkUtils';
|
||||||
|
|
||||||
|
import { EntityType } from '@types';
|
||||||
|
|
||||||
|
// Mock external modules
|
||||||
|
vi.mock('@app/context/useUserContext', () => ({
|
||||||
|
useUserContext: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/entity/shared/EntityContext', () => ({
|
||||||
|
useEntityData: vi.fn(),
|
||||||
|
useMutationUrn: vi.fn(),
|
||||||
|
useRefetch: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const removeLinkMutationMock = vi.fn();
|
||||||
|
const addLinkMutationMock = vi.fn();
|
||||||
|
vi.mock('@graphql/mutations.generated', () => ({
|
||||||
|
useRemoveLinkMutation: () => [removeLinkMutationMock],
|
||||||
|
useAddLinkMutation: () => [addLinkMutationMock],
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('antd', () => ({
|
||||||
|
message: {
|
||||||
|
success: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/analytics', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: { event: vi.fn() },
|
||||||
|
EventType: { EntityActionEvent: 'EntityActionEvent' },
|
||||||
|
EntityActionType: { UpdateLinks: 'UpdateLinks' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockUserContext = (user: any) => {
|
||||||
|
(useUserContext as Mock).mockReturnValue(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEntityContext = ({
|
||||||
|
urn = 'test-urn',
|
||||||
|
entityType = 'dataset',
|
||||||
|
refetch = vi.fn(),
|
||||||
|
mutationUrn = 'mutation-urn',
|
||||||
|
} = {}) => {
|
||||||
|
(useEntityData as Mock).mockReturnValue({ urn, entityType });
|
||||||
|
(useRefetch as Mock).mockReturnValue(refetch);
|
||||||
|
(useMutationUrn as Mock).mockReturnValue(mutationUrn);
|
||||||
|
return { urn, entityType, refetch, mutationUrn };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock link
|
||||||
|
const mockDeleteLinkInput = {
|
||||||
|
url: 'http://test.com',
|
||||||
|
associatedUrn: 'urn:foo',
|
||||||
|
actor: { urn: 'urn:actor', type: EntityType.CorpUser, username: 'actor' },
|
||||||
|
author: { urn: 'urn:author', type: EntityType.CorpUser, username: 'author' },
|
||||||
|
created: { time: Date.now() },
|
||||||
|
description: 'desc',
|
||||||
|
label: 'label',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useLinkUtils', () => {
|
||||||
|
describe('handleDeleteLink', () => {
|
||||||
|
it('should remove a link and show success message when mutation succeeds', async () => {
|
||||||
|
const { refetch } = mockEntityContext();
|
||||||
|
mockUserContext({ urn: 'user-1' });
|
||||||
|
|
||||||
|
removeLinkMutationMock.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLinkUtils());
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleDeleteLink(mockDeleteLinkInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(removeLinkMutationMock).toHaveBeenCalledWith({
|
||||||
|
variables: { input: { linkUrl: 'http://test.com', resourceUrn: 'urn:foo' } },
|
||||||
|
});
|
||||||
|
expect(message.success).toHaveBeenCalledWith({ content: 'Link Removed', duration: 2 });
|
||||||
|
expect(refetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error message when link removal fails', async () => {
|
||||||
|
const { refetch } = mockEntityContext();
|
||||||
|
mockUserContext({ urn: 'user-1' });
|
||||||
|
|
||||||
|
removeLinkMutationMock.mockRejectedValueOnce(new Error('Network issue'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLinkUtils());
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleDeleteLink(mockDeleteLinkInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.destroy).toHaveBeenCalled();
|
||||||
|
expect(message.error).toHaveBeenCalledWith({
|
||||||
|
content: expect.stringContaining('Error removing link:'),
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
expect(refetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleAddLink', () => {
|
||||||
|
it('should add a link, show success message, trigger analytics event, and refetch when mutation succeeds', async () => {
|
||||||
|
const { refetch, mutationUrn, entityType } = mockEntityContext();
|
||||||
|
mockUserContext({ urn: 'user-1' });
|
||||||
|
|
||||||
|
addLinkMutationMock.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLinkUtils());
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleAddLink({ url: 'http://test.com.com', label: 'Test Link' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(addLinkMutationMock).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
input: { linkUrl: 'http://test.com.com', label: 'Test Link', resourceUrn: mutationUrn },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.success).toHaveBeenCalledWith({ content: 'Link Added', duration: 2 });
|
||||||
|
|
||||||
|
expect(analytics.event).toHaveBeenCalledWith({
|
||||||
|
type: EventType.EntityActionEvent,
|
||||||
|
entityType,
|
||||||
|
entityUrn: mutationUrn,
|
||||||
|
actionType: EntityActionType.UpdateLinks,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error message when link add mutation fails', async () => {
|
||||||
|
mockEntityContext();
|
||||||
|
mockUserContext({ urn: 'user-1' });
|
||||||
|
|
||||||
|
addLinkMutationMock.mockRejectedValueOnce(new Error('Add failed'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLinkUtils());
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleAddLink({ url: 'bad-url', label: 'Bad' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.destroy).toHaveBeenCalled();
|
||||||
|
expect(message.error).toHaveBeenCalledWith({
|
||||||
|
content: expect.stringContaining('Failed to add link:'),
|
||||||
|
duration: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error message when adding link without a user context', async () => {
|
||||||
|
mockEntityContext();
|
||||||
|
mockUserContext(undefined);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLinkUtils());
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleAddLink({ url: 'test', label: 'no-user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.error).toHaveBeenCalledWith({
|
||||||
|
content: 'Error adding link: no user',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
expect(addLinkMutationMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
import analytics, { EntityActionType, EventType } from '@app/analytics';
|
||||||
|
import { useUserContext } from '@app/context/useUserContext';
|
||||||
|
import { useEntityData, useMutationUrn, useRefetch } from '@app/entity/shared/EntityContext';
|
||||||
|
|
||||||
|
import { useAddLinkMutation, useRemoveLinkMutation } from '@graphql/mutations.generated';
|
||||||
|
import { InstitutionalMemoryMetadata } from '@types';
|
||||||
|
|
||||||
|
export function useLinkUtils() {
|
||||||
|
const { urn: entityUrn, entityType } = useEntityData();
|
||||||
|
const refetch = useRefetch();
|
||||||
|
const mutationUrn = useMutationUrn();
|
||||||
|
const user = useUserContext();
|
||||||
|
|
||||||
|
const [removeLinkMutation] = useRemoveLinkMutation();
|
||||||
|
const [addLinkMutation] = useAddLinkMutation();
|
||||||
|
|
||||||
|
const handleDeleteLink = async (link: InstitutionalMemoryMetadata) => {
|
||||||
|
try {
|
||||||
|
await removeLinkMutation({
|
||||||
|
variables: { input: { linkUrl: link.url, resourceUrn: link.associatedUrn || entityUrn } },
|
||||||
|
});
|
||||||
|
message.success({ content: 'Link Removed', duration: 2 });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.destroy();
|
||||||
|
if (e instanceof Error) {
|
||||||
|
message.error({ content: `Error removing link: \n ${e.message || ''}`, duration: 2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refetch?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddLink = async (formValues) => {
|
||||||
|
if (user?.urn) {
|
||||||
|
try {
|
||||||
|
await addLinkMutation({
|
||||||
|
variables: {
|
||||||
|
input: { linkUrl: formValues.url, label: formValues.label, resourceUrn: mutationUrn },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success({ content: 'Link Added', duration: 2 });
|
||||||
|
analytics.event({
|
||||||
|
type: EventType.EntityActionEvent,
|
||||||
|
entityType,
|
||||||
|
entityUrn: mutationUrn,
|
||||||
|
actionType: EntityActionType.UpdateLinks,
|
||||||
|
});
|
||||||
|
refetch?.();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.destroy();
|
||||||
|
if (e instanceof Error) {
|
||||||
|
message.error({ content: `Failed to add link: \n ${e.message || ''}`, duration: 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error({ content: `Error adding link: no user`, duration: 2 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleDeleteLink, handleAddLink };
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user