feat(ui) Update About section in summary tab with new design (#14678)

This commit is contained in:
Chris Collins 2025-09-05 10:31:02 -04:00 committed by GitHub
parent c1694a3db1
commit 4296069475
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 197 additions and 209 deletions

View File

@ -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

View File

@ -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>

View File

@ -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}
/>
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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 && (

View File

@ -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>
);
}

View File

@ -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', () => {

View File

@ -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,
};
}

View File

@ -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} />}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>