mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-29 10:57:52 +00:00
feat(ui) Update About section in summary tab with new design (#14678)
This commit is contained in:
parent
c1694a3db1
commit
4296069475
@ -36,6 +36,8 @@ import { FloatingToolbar } from '@components/components/Editor/toolbar/FloatingT
|
||||
import { TableCellMenu } from '@components/components/Editor/toolbar/TableCellMenu';
|
||||
import { Toolbar } from '@components/components/Editor/toolbar/Toolbar';
|
||||
|
||||
import { notEmpty } from '@app/entityV2/shared/utils';
|
||||
|
||||
type EditorProps = {
|
||||
readOnly?: boolean;
|
||||
content?: string;
|
||||
@ -86,7 +88,7 @@ export const Editor = forwardRef((props: EditorProps, ref) => {
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
if (readOnly && content) {
|
||||
if (readOnly && notEmpty(content)) {
|
||||
manager.store.commands.setContent(content);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useEntityData } from '@app/entity/shared/EntityContext';
|
||||
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 { StyledDivider } from '@app/entityV2/summary/styledComponents';
|
||||
import { PageTemplateProvider } from '@app/homeV3/context/PageTemplateContext';
|
||||
@ -22,17 +22,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function SummaryTab({ properties }: { properties?: Props }) {
|
||||
const hideLinksButton = properties?.hideLinksButton;
|
||||
|
||||
const { urn } = useEntityData();
|
||||
return (
|
||||
<PageTemplateProvider templateType={PageTemplateSurfaceType.AssetSummary}>
|
||||
<SummaryWrapper>
|
||||
<PropertiesHeader />
|
||||
{/* div prevents 16px gap inside of about and links sections */}
|
||||
<div>
|
||||
<AboutSection />
|
||||
{!hideLinksButton && <Links />}
|
||||
</div>
|
||||
<AboutSection hideLinksButton={!!properties?.hideLinksButton} key={urn} />
|
||||
<StyledDivider />
|
||||
<Template />
|
||||
</SummaryWrapper>
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
import { Editor } from '@components';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DescriptionActionsBar from '@app/entityV2/summary/documentation/DescriptionActionsBar';
|
||||
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;
|
||||
margin-top: 4px;
|
||||
&&& {
|
||||
.remirror-editor {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
${({ $isEditing }) =>
|
||||
$isEditing &&
|
||||
`
|
||||
&&& {
|
||||
.remirror-editor-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const DescriptionContainer = styled.div`
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
const toolbarStyles = {
|
||||
marginLeft: '-8px',
|
||||
};
|
||||
|
||||
export default function AboutContent() {
|
||||
const {
|
||||
isEditing,
|
||||
setIsEditing,
|
||||
initialDescription,
|
||||
updatedDescription,
|
||||
setUpdatedDescription,
|
||||
handleDescriptionUpdate,
|
||||
handleCancel,
|
||||
emptyDescriptionText,
|
||||
} = useDescriptionUtils();
|
||||
const canEditDescription = useDocumentationPermission();
|
||||
|
||||
let content;
|
||||
|
||||
if (!updatedDescription && !isEditing) {
|
||||
content = <EmptyDescription />;
|
||||
} else if (isEditing) {
|
||||
content = (
|
||||
<StyledEditor
|
||||
content={updatedDescription}
|
||||
placeholder={emptyDescriptionText}
|
||||
hideHighlightToolbar
|
||||
onChange={(description) => setUpdatedDescription(description)}
|
||||
$isEditing={isEditing}
|
||||
toolbarStyles={toolbarStyles}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<DescriptionViewer>
|
||||
<StyledEditor content={updatedDescription} readOnly />
|
||||
</DescriptionViewer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<DescriptionContainer
|
||||
onClick={() => {
|
||||
if (canEditDescription) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</DescriptionContainer>
|
||||
{isEditing && (
|
||||
<DescriptionActionsBar
|
||||
onCancel={handleCancel}
|
||||
onUpdate={handleDescriptionUpdate}
|
||||
areActionsDisabled={updatedDescription === initialDescription}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,15 +1,112 @@
|
||||
import { Text } from '@components';
|
||||
import React from 'react';
|
||||
import { Button, Editor, Text, Tooltip } from '@components';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import AboutContent from '@app/entityV2/summary/documentation/AboutContent';
|
||||
import DescriptionViewer from '@app/entityV2/summary/documentation/DescriptionViewer';
|
||||
import EditDescriptionModal from '@app/entityV2/summary/documentation/EditDescriptionModal';
|
||||
import { useDescriptionUtils } from '@app/entityV2/summary/documentation/useDescriptionUtils';
|
||||
import { useDocumentationPermission } from '@app/entityV2/summary/documentation/useDocumentationPermission';
|
||||
import AddLinkModal from '@app/entityV2/summary/links/AddLinkModal';
|
||||
import Links from '@app/entityV2/summary/links/Links';
|
||||
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
|
||||
|
||||
const StyledEditor = styled(Editor)<{ $isEditing?: boolean }>`
|
||||
border: none;
|
||||
margin-top: 4px;
|
||||
&&& {
|
||||
.remirror-editor {
|
||||
padding: 0;
|
||||
}
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const ButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const DescriptionContainer = styled.div`
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
hideLinksButton: boolean;
|
||||
}
|
||||
|
||||
export default function AboutSection({ hideLinksButton }: Props) {
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false);
|
||||
const [showAddDescriptionModal, setShowDescriptionModal] = useState(false);
|
||||
|
||||
const hasLinkPermissions = useLinkPermission();
|
||||
const canEditDescription = useDocumentationPermission();
|
||||
const {
|
||||
displayedDescription,
|
||||
updatedDescription,
|
||||
setUpdatedDescription,
|
||||
handleDescriptionUpdate,
|
||||
emptyDescriptionText,
|
||||
} = useDescriptionUtils();
|
||||
|
||||
export default function AboutSection() {
|
||||
return (
|
||||
<>
|
||||
<Text weight="bold" color="gray" colorLevel={600} size="sm">
|
||||
About
|
||||
</Text>
|
||||
<AboutContent />
|
||||
</>
|
||||
<div>
|
||||
<SectionHeaderWrapper>
|
||||
<Text weight="bold" color="gray" colorLevel={600} size="sm">
|
||||
About
|
||||
</Text>
|
||||
<ButtonsWrapper>
|
||||
{hasLinkPermissions && (
|
||||
<Tooltip title="Add link">
|
||||
<Button
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="xs"
|
||||
icon={{ icon: 'LinkSimple', source: 'phosphor', size: 'lg' }}
|
||||
style={{ padding: '0 2px' }}
|
||||
onClick={() => setShowAddLinkModal(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEditDescription && (
|
||||
<Tooltip title="Edit description">
|
||||
<Button
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="xs"
|
||||
icon={{ icon: 'PencilSimpleLine', source: 'phosphor', size: 'lg' }}
|
||||
style={{ padding: '0 2px' }}
|
||||
onClick={() => setShowDescriptionModal(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ButtonsWrapper>
|
||||
</SectionHeaderWrapper>
|
||||
<DescriptionContainer>
|
||||
<DescriptionViewer>
|
||||
<StyledEditor content={displayedDescription} placeholder={emptyDescriptionText} readOnly />
|
||||
</DescriptionViewer>
|
||||
</DescriptionContainer>
|
||||
{!hideLinksButton && <Links />}
|
||||
{showAddLinkModal && <AddLinkModal setShowAddLinkModal={setShowAddLinkModal} />}
|
||||
{showAddDescriptionModal && (
|
||||
<EditDescriptionModal
|
||||
updatedDescription={updatedDescription}
|
||||
setUpdatedDescription={setUpdatedDescription}
|
||||
handleDescriptionUpdate={handleDescriptionUpdate}
|
||||
emptyDescriptionText={emptyDescriptionText}
|
||||
closeModal={() => {
|
||||
setShowDescriptionModal(false);
|
||||
setUpdatedDescription(displayedDescription);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,10 +11,11 @@ const Wrapper = styled.div<{ expanded: boolean }>`
|
||||
transition: max-height 0.3s ease;
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div<{ expanded: boolean }>`
|
||||
overflow: ${({ expanded }) => (expanded ? 'visible' : 'hidden')};
|
||||
${({ expanded }) =>
|
||||
!expanded &&
|
||||
const ContentContainer = styled.div<{ $expanded: boolean; $hasMore: boolean }>`
|
||||
overflow: ${({ $expanded }) => ($expanded ? 'visible' : 'hidden')};
|
||||
${({ $expanded, $hasMore }) =>
|
||||
!$expanded &&
|
||||
$hasMore &&
|
||||
`
|
||||
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 60%, rgba(0,0,0,0.5) 90%, rgba(0,0,0,0) 100%);
|
||||
mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 60%, rgba(0,0,0,0.5) 90%, rgba(0,0,0,0) 100%);
|
||||
@ -23,6 +24,7 @@ const ContentContainer = styled.div<{ expanded: boolean }>`
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
align-self: center;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@ -43,7 +45,7 @@ export default function DescriptionViewer({ children }: Props) {
|
||||
|
||||
return (
|
||||
<Wrapper expanded={isExpanded}>
|
||||
<ContentContainer ref={contentRef} expanded={isExpanded}>
|
||||
<ContentContainer ref={contentRef} $expanded={isExpanded} $hasMore={hasMore}>
|
||||
{children}
|
||||
</ContentContainer>
|
||||
{hasMore && (
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import { Editor, Modal } from '@components';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
border: none;
|
||||
&&& {
|
||||
.remirror-editor {
|
||||
padding: 0;
|
||||
min-height: 350px;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const toolbarStyles = {
|
||||
marginLeft: '-8px',
|
||||
marginBottom: '16px',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
marginTop: '-24px',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
updatedDescription: string;
|
||||
setUpdatedDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
handleDescriptionUpdate: () => Promise<void>;
|
||||
emptyDescriptionText: string;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export default function EditDescriptionModal({
|
||||
updatedDescription,
|
||||
setUpdatedDescription,
|
||||
handleDescriptionUpdate,
|
||||
emptyDescriptionText,
|
||||
closeModal,
|
||||
}: Props) {
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Description"
|
||||
onCancel={closeModal}
|
||||
width="80vw"
|
||||
style={{ maxWidth: '1200px' }}
|
||||
buttons={[
|
||||
{
|
||||
text: 'Cancel',
|
||||
variant: 'text',
|
||||
onClick: () => closeModal(),
|
||||
},
|
||||
{
|
||||
text: 'Publish',
|
||||
onClick: () => {
|
||||
handleDescriptionUpdate();
|
||||
closeModal();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<StyledEditor
|
||||
content={updatedDescription}
|
||||
placeholder={emptyDescriptionText}
|
||||
hideHighlightToolbar
|
||||
onChange={(description) => setUpdatedDescription(description)}
|
||||
toolbarStyles={toolbarStyles}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -61,9 +61,7 @@ describe('useDescriptionUtils', () => {
|
||||
it('should initialize hook state correctly', () => {
|
||||
const { result } = renderHook(() => useDescriptionUtils());
|
||||
|
||||
expect(result.current.initialDescription).toBe(description);
|
||||
expect(result.current.updatedDescription).toBe(description);
|
||||
expect(result.current.isEditing).toBe(false);
|
||||
expect(result.current.emptyDescriptionText).toBe(`Write a description for this ${entityName.toLowerCase()}`);
|
||||
});
|
||||
|
||||
@ -77,45 +75,9 @@ describe('useDescriptionUtils', () => {
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.initialDescription).toBe(newDescription);
|
||||
expect(result.current.updatedDescription).toBe(newDescription);
|
||||
});
|
||||
|
||||
it('should reset isEditing to false when urn changes', () => {
|
||||
const { result, rerender } = renderHook(() => useDescriptionUtils());
|
||||
|
||||
act(() => {
|
||||
result.current.setIsEditing(true);
|
||||
});
|
||||
expect(result.current.isEditing).toBe(true);
|
||||
|
||||
(useEntityData as Mock).mockReturnValue({
|
||||
entityData: { description },
|
||||
urn: 'urn:li:entity:456',
|
||||
entityType,
|
||||
});
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.isEditing).toBe(false);
|
||||
});
|
||||
|
||||
it('should reset updatedDescription and disable editing on handleCancel', () => {
|
||||
const { result } = renderHook(() => useDescriptionUtils());
|
||||
|
||||
act(() => {
|
||||
result.current.setUpdatedDescription('Some edited text');
|
||||
result.current.setIsEditing(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleCancel();
|
||||
});
|
||||
|
||||
expect(result.current.updatedDescription).toBe(result.current.initialDescription);
|
||||
expect(result.current.isEditing).toBe(false);
|
||||
});
|
||||
|
||||
it('should call legacy update method when updateEntity exists on handleDescriptionUpdate', async () => {
|
||||
const { result } = renderHook(() => useDescriptionUtils());
|
||||
|
||||
@ -154,7 +116,6 @@ describe('useDescriptionUtils', () => {
|
||||
});
|
||||
|
||||
expect(refetchMockDelayed).toHaveBeenCalled();
|
||||
expect(result.current.isEditing).toBe(false);
|
||||
});
|
||||
|
||||
it('should return correct emptyDescriptionText placeholder', () => {
|
||||
|
||||
@ -8,7 +8,7 @@ import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||
import { useUpdateDescriptionMutation } from '@graphql/mutations.generated';
|
||||
|
||||
export function useDescriptionUtils() {
|
||||
const { entityData, urn, entityType } = useEntityData();
|
||||
const { entityData, entityType } = useEntityData();
|
||||
const entityRegistry = useEntityRegistryV2();
|
||||
const mutationUrn = useMutationUrn();
|
||||
const refetch = useRefetch();
|
||||
@ -19,21 +19,13 @@ export function useDescriptionUtils() {
|
||||
entityProperties: entityData,
|
||||
});
|
||||
|
||||
const [initialDescription, setInitialDescription] = useState<string>(displayedDescription);
|
||||
const [updatedDescription, setUpdatedDescription] = useState<string>(displayedDescription);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const updateEntity = useEntityUpdate<GenericEntityUpdate>();
|
||||
|
||||
useEffect(() => {
|
||||
setInitialDescription(displayedDescription);
|
||||
setUpdatedDescription(displayedDescription);
|
||||
}, [displayedDescription]);
|
||||
|
||||
// Reset isEditing when asset changes
|
||||
useEffect(() => {
|
||||
setIsEditing(false);
|
||||
}, [urn]);
|
||||
|
||||
const updateDescriptionLegacy = () => {
|
||||
return updateEntity?.({
|
||||
variables: { urn: mutationUrn, input: { editableProperties: { description: updatedDescription } } },
|
||||
@ -41,7 +33,7 @@ export function useDescriptionUtils() {
|
||||
};
|
||||
|
||||
const updateDescription = () => {
|
||||
updateDescriptionMutation({
|
||||
return updateDescriptionMutation({
|
||||
variables: {
|
||||
input: {
|
||||
description: updatedDescription,
|
||||
@ -59,27 +51,16 @@ export function useDescriptionUtils() {
|
||||
// Use the new update description path.
|
||||
await updateDescription();
|
||||
}
|
||||
setTimeout(() => {
|
||||
refetch();
|
||||
}, 2000);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
setUpdatedDescription(initialDescription);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const emptyDescriptionText = `Write a description for this ${entityRegistry.getEntityName(entityType)?.toLowerCase()}`;
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
setIsEditing,
|
||||
initialDescription,
|
||||
displayedDescription,
|
||||
updatedDescription,
|
||||
setUpdatedDescription,
|
||||
handleDescriptionUpdate,
|
||||
handleCancel,
|
||||
emptyDescriptionText,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
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';
|
||||
import { useLinkPermission } from '@app/entityV2/summary/links/useLinkPermission';
|
||||
|
||||
const LinksSection = styled.div`
|
||||
display: flex;
|
||||
@ -12,12 +10,9 @@ const LinksSection = styled.div`
|
||||
`;
|
||||
|
||||
export default function Links() {
|
||||
const hasLinkPermissions = useLinkPermission();
|
||||
|
||||
return (
|
||||
<LinksSection>
|
||||
<LinksList />
|
||||
{hasLinkPermissions && <AddLinkButton />}
|
||||
</LinksSection>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ const ListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
export default function LinksList() {
|
||||
@ -64,6 +65,8 @@ export default function LinksList() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!links.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListContainer>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user