mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-19 06:38:04 +00:00
feat : markdown support for group description (#9455)
This commit is contained in:
parent
e58e2bf3be
commit
b4fe451d93
@ -0,0 +1,64 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Modal, Form } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Editor } from '../shared/tabs/Documentation/components/editor/Editor';
|
||||
import { ANTD_GRAY } from '../shared/constants';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSaveAboutMe: () => void;
|
||||
setStagedDescription: (des: string) => void;
|
||||
stagedDescription: string | undefined;
|
||||
};
|
||||
const StyledEditor = styled(Editor)`
|
||||
border: 1px solid ${ANTD_GRAY[4]};
|
||||
`;
|
||||
|
||||
export default function EditGroupDescriptionModal({
|
||||
onClose,
|
||||
onSaveAboutMe,
|
||||
setStagedDescription,
|
||||
stagedDescription,
|
||||
}: Props) {
|
||||
const [form] = Form.useForm();
|
||||
const [aboutText,setAboutText] = useState(stagedDescription)
|
||||
|
||||
function updateDescription(description: string) {
|
||||
setAboutText(aboutText)
|
||||
setStagedDescription(description);
|
||||
|
||||
}
|
||||
|
||||
const saveDescription = () => {
|
||||
onSaveAboutMe();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={700}
|
||||
title="Edit Description"
|
||||
visible
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} type="text">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button id="updateGroupButton" onClick={saveDescription} disabled={!stagedDescription}>
|
||||
Update
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form form={form} initialValues={{}} layout="vertical">
|
||||
<Form.Item name="description" rules={[{ whitespace: true }, { min: 1, max: 500 }]} hasFeedback>
|
||||
<div>
|
||||
<StyledEditor content={aboutText} onChange={updateDescription} />
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -16,14 +16,15 @@ import {
|
||||
EmptyValue,
|
||||
SocialDetails,
|
||||
EditButton,
|
||||
AboutSection,
|
||||
AboutSectionText,
|
||||
GroupsSection,
|
||||
AboutSection,
|
||||
} from '../shared/SidebarStyledComponents';
|
||||
import GroupMembersSideBarSection from './GroupMembersSideBarSection';
|
||||
import { useUserContext } from '../../context/useUserContext';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
import StripMarkdownText, { removeMarkdown } from '../shared/components/styled/StripMarkdownText';
|
||||
import { Editor } from '../shared/tabs/Documentation/components/editor/Editor';
|
||||
import EditGroupDescriptionModal from './EditGroupDescriptionModal';
|
||||
import { REDESIGN_COLORS } from '../shared/constants';
|
||||
|
||||
type SideBarData = {
|
||||
photoUrl: string | undefined;
|
||||
@ -80,6 +81,61 @@ const GroupTitle = styled(Typography.Title)`
|
||||
}
|
||||
`;
|
||||
|
||||
const EditIcon = styled(EditOutlined)`
|
||||
cursor: pointer;
|
||||
color: ${REDESIGN_COLORS.BLUE};
|
||||
`;
|
||||
const AddNewDescription = styled(Button)`
|
||||
display: none;
|
||||
margin: -4px;
|
||||
width: 140px;
|
||||
`;
|
||||
|
||||
const StyledViewer = styled(Editor)`
|
||||
padding-right: 8px;
|
||||
display: block;
|
||||
|
||||
.remirror-editor.ProseMirror {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const DescriptionContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
text-align:left;
|
||||
font-weight: normal;
|
||||
font
|
||||
min-height: 22px;
|
||||
|
||||
&:hover ${AddNewDescription} {
|
||||
display: block;
|
||||
}
|
||||
& ins.diff {
|
||||
background-color: #b7eb8f99;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
background-color: #b7eb8faa;
|
||||
}
|
||||
}
|
||||
& del.diff {
|
||||
background-color: #ffa39e99;
|
||||
text-decoration: line-through;
|
||||
&: hover {
|
||||
background-color: #ffa39eaa;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandedActions = styled.div`
|
||||
height: 10px;
|
||||
`;
|
||||
const ReadLessText = styled(Typography.Link)`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Responsible for reading & writing users.
|
||||
*/
|
||||
@ -106,7 +162,17 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
||||
const me = useUserContext();
|
||||
const canEditGroup = me?.platformPrivileges?.manageIdentities;
|
||||
const [groupTitle, setGroupTitle] = useState(name);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isUpdatingDescription, SetIsUpdatingDescription] = useState(false);
|
||||
const [stagedDescription, setStagedDescription] = useState(aboutText);
|
||||
|
||||
const [updateName] = useUpdateNameMutation();
|
||||
const overLimit = removeMarkdown(aboutText || '').length > 80;
|
||||
const ABBREVIATED_LIMIT = 80;
|
||||
|
||||
useEffect(() => {
|
||||
setStagedDescription(aboutText);
|
||||
}, [aboutText]);
|
||||
|
||||
useEffect(() => {
|
||||
setGroupTitle(groupTitle);
|
||||
@ -136,12 +202,12 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
||||
};
|
||||
|
||||
// About Text save
|
||||
const onSaveAboutMe = (inputString) => {
|
||||
const onSaveAboutMe = () => {
|
||||
updateCorpGroupPropertiesMutation({
|
||||
variables: {
|
||||
urn: urn || '',
|
||||
input: {
|
||||
description: inputString,
|
||||
description: stagedDescription,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -201,16 +267,65 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
||||
</SocialDetails>
|
||||
<Divider className="divider-aboutSection" />
|
||||
<AboutSection>
|
||||
{TITLES.about}
|
||||
<AboutSectionText>
|
||||
<Paragraph
|
||||
editable={canEditGroup ? { onChange: onSaveAboutMe } : false}
|
||||
ellipsis={{ rows: 2, expandable: true, symbol: 'Read more' }}
|
||||
>
|
||||
{aboutText || <EmptyValue />}
|
||||
</Paragraph>
|
||||
</AboutSectionText>
|
||||
<Row>
|
||||
<Col span={22}>{TITLES.about}</Col>
|
||||
<Col span={2}>
|
||||
<EditIcon onClick={() => SetIsUpdatingDescription(true)} data-testid="edit-icon" />
|
||||
</Col>
|
||||
</Row>
|
||||
</AboutSection>
|
||||
<DescriptionContainer>
|
||||
{(aboutText && expanded) || !overLimit ? (
|
||||
<>
|
||||
{/* Read only viewer for displaying group description */}
|
||||
<StyledViewer content={aboutText} readOnly />
|
||||
<ExpandedActions>
|
||||
{overLimit && (
|
||||
<ReadLessText
|
||||
onClick={() => {
|
||||
setExpanded(false);
|
||||
}}
|
||||
>
|
||||
Read Less
|
||||
</ReadLessText>
|
||||
)}
|
||||
</ExpandedActions>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Display abbreviated description with option to read more */}
|
||||
<StripMarkdownText
|
||||
limit={ABBREVIATED_LIMIT}
|
||||
readMore={
|
||||
<>
|
||||
<Typography.Link
|
||||
onClick={() => {
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
Read More
|
||||
</Typography.Link>
|
||||
</>
|
||||
}
|
||||
shouldWrap
|
||||
>
|
||||
{aboutText}
|
||||
</StripMarkdownText>
|
||||
</>
|
||||
)}
|
||||
</DescriptionContainer>
|
||||
{/* Modal for updating group description */}
|
||||
{isUpdatingDescription && (
|
||||
<EditGroupDescriptionModal
|
||||
onClose={() => {
|
||||
SetIsUpdatingDescription(false);
|
||||
setStagedDescription(aboutText);
|
||||
}}
|
||||
onSaveAboutMe={onSaveAboutMe}
|
||||
setStagedDescription={setStagedDescription}
|
||||
stagedDescription={stagedDescription}
|
||||
/>
|
||||
)}
|
||||
<Divider className="divider-groupsSection" />
|
||||
<GroupsSection>
|
||||
<GroupOwnerSideBarSection ownership={ownership} urn={urn || ''} refetch={refetch} />
|
||||
|
@ -1,16 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { message, Button, Input, Modal, Typography, Form, Collapse } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { useCreateGroupMutation } from '../../../graphql/group.generated';
|
||||
import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
|
||||
import { validateCustomUrnId } from '../../shared/textUtil';
|
||||
import analytics, { EventType } from '../../analytics';
|
||||
import { CorpGroup, EntityType } from '../../../types.generated';
|
||||
import { Editor as MarkdownEditor } from '../../entity/shared/tabs/Documentation/components/editor/Editor';
|
||||
import { ANTD_GRAY } from '../../entity/shared/constants';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onCreate: (group: CorpGroup) => void;
|
||||
};
|
||||
|
||||
const StyledEditor = styled(MarkdownEditor)`
|
||||
border: 1px solid ${ANTD_GRAY[4]};
|
||||
`;
|
||||
|
||||
export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
||||
const [stagedName, setStagedName] = useState('');
|
||||
const [stagedDescription, setStagedDescription] = useState('');
|
||||
@ -19,45 +26,54 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
||||
const [createButtonEnabled, setCreateButtonEnabled] = useState(true);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// Reference to the styled editor for handling focus
|
||||
const styledEditorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onCreateGroup = () => {
|
||||
createGroupMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: stagedId,
|
||||
name: stagedName,
|
||||
description: stagedDescription,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data, errors }) => {
|
||||
if (!errors) {
|
||||
analytics.event({
|
||||
type: EventType.CreateGroupEvent,
|
||||
});
|
||||
message.success({
|
||||
content: `Created group!`,
|
||||
duration: 3,
|
||||
});
|
||||
// TODO: Get a full corp group back from create endpoint.
|
||||
onCreate({
|
||||
urn: data?.createGroup || '',
|
||||
type: EntityType.CorpGroup,
|
||||
// Check if the Enter key was pressed inside the styled editor to prevent unintended form submission
|
||||
const isEditorNewlineKeypress =
|
||||
document.activeElement !== styledEditorRef.current &&
|
||||
!styledEditorRef.current?.contains(document.activeElement);
|
||||
if (isEditorNewlineKeypress) {
|
||||
createGroupMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: stagedId,
|
||||
name: stagedName,
|
||||
info: {
|
||||
description: stagedDescription,
|
||||
},
|
||||
});
|
||||
}
|
||||
description: stagedDescription,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to create group!: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
setStagedName('');
|
||||
setStagedDescription('');
|
||||
});
|
||||
onClose();
|
||||
.then(({ data, errors }) => {
|
||||
if (!errors) {
|
||||
analytics.event({
|
||||
type: EventType.CreateGroupEvent,
|
||||
});
|
||||
message.success({
|
||||
content: `Created group!`,
|
||||
duration: 3,
|
||||
});
|
||||
// TODO: Get a full corp group back from create endpoint.
|
||||
onCreate({
|
||||
urn: data?.createGroup || '',
|
||||
type: EntityType.CorpGroup,
|
||||
name: stagedName,
|
||||
info: {
|
||||
description: stagedDescription,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to create group!: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
setStagedName('');
|
||||
setStagedDescription('');
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the Enter press
|
||||
@ -65,8 +81,13 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
||||
querySelectorToExecuteClick: '#createGroupButton',
|
||||
});
|
||||
|
||||
function updateDescription(description: string) {
|
||||
setStagedDescription(description);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={700}
|
||||
title="Create new group"
|
||||
visible
|
||||
onCancel={onClose}
|
||||
@ -112,12 +133,11 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
||||
</Form.Item>
|
||||
<Form.Item label={<Typography.Text strong>Description</Typography.Text>}>
|
||||
<Typography.Paragraph>An optional description for your new group.</Typography.Paragraph>
|
||||
<Form.Item name="description" rules={[{ whitespace: true }, { min: 1, max: 500 }]} hasFeedback>
|
||||
<Input
|
||||
placeholder="A description for your group"
|
||||
value={stagedDescription}
|
||||
onChange={(event) => setStagedDescription(event.target.value)}
|
||||
/>
|
||||
<Form.Item name="description" rules={[{ whitespace: true }]} hasFeedback>
|
||||
{/* Styled editor for the group description */}
|
||||
<div ref={styledEditorRef}>
|
||||
<StyledEditor doNotFocus content={stagedDescription} onChange={updateDescription} />
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Collapse ghost>
|
||||
|
@ -72,8 +72,10 @@ describe("create and manage group", () => {
|
||||
cy.focused().clear().type(`Test group EDITED ${test_id}{enter}`);
|
||||
cy.waitTextVisible("Name Updated");
|
||||
cy.contains(`Test group EDITED ${test_id}`).should("be.visible");
|
||||
cy.contains("Test group description").find('[aria-label="edit"]').click();
|
||||
cy.focused().type(" EDITED{enter}");
|
||||
cy.get('[data-testid="edit-icon"]').click();
|
||||
cy.waitTextVisible("Edit Description");
|
||||
cy.get("#description").should("be.visible").type(" EDITED");
|
||||
cy.get("#updateGroupButton").click();
|
||||
cy.waitTextVisible("Changes saved.");
|
||||
cy.contains("Test group description EDITED").should("be.visible");
|
||||
cy.clickOptionWithText("Add Owners");
|
||||
|
Loading…
x
Reference in New Issue
Block a user