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,
|
EmptyValue,
|
||||||
SocialDetails,
|
SocialDetails,
|
||||||
EditButton,
|
EditButton,
|
||||||
AboutSection,
|
|
||||||
AboutSectionText,
|
|
||||||
GroupsSection,
|
GroupsSection,
|
||||||
|
AboutSection,
|
||||||
} from '../shared/SidebarStyledComponents';
|
} from '../shared/SidebarStyledComponents';
|
||||||
import GroupMembersSideBarSection from './GroupMembersSideBarSection';
|
import GroupMembersSideBarSection from './GroupMembersSideBarSection';
|
||||||
import { useUserContext } from '../../context/useUserContext';
|
import { useUserContext } from '../../context/useUserContext';
|
||||||
|
import StripMarkdownText, { removeMarkdown } from '../shared/components/styled/StripMarkdownText';
|
||||||
const { Paragraph } = Typography;
|
import { Editor } from '../shared/tabs/Documentation/components/editor/Editor';
|
||||||
|
import EditGroupDescriptionModal from './EditGroupDescriptionModal';
|
||||||
|
import { REDESIGN_COLORS } from '../shared/constants';
|
||||||
|
|
||||||
type SideBarData = {
|
type SideBarData = {
|
||||||
photoUrl: string | undefined;
|
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.
|
* Responsible for reading & writing users.
|
||||||
*/
|
*/
|
||||||
@ -106,7 +162,17 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
|||||||
const me = useUserContext();
|
const me = useUserContext();
|
||||||
const canEditGroup = me?.platformPrivileges?.manageIdentities;
|
const canEditGroup = me?.platformPrivileges?.manageIdentities;
|
||||||
const [groupTitle, setGroupTitle] = useState(name);
|
const [groupTitle, setGroupTitle] = useState(name);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [isUpdatingDescription, SetIsUpdatingDescription] = useState(false);
|
||||||
|
const [stagedDescription, setStagedDescription] = useState(aboutText);
|
||||||
|
|
||||||
const [updateName] = useUpdateNameMutation();
|
const [updateName] = useUpdateNameMutation();
|
||||||
|
const overLimit = removeMarkdown(aboutText || '').length > 80;
|
||||||
|
const ABBREVIATED_LIMIT = 80;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStagedDescription(aboutText);
|
||||||
|
}, [aboutText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGroupTitle(groupTitle);
|
setGroupTitle(groupTitle);
|
||||||
@ -136,12 +202,12 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// About Text save
|
// About Text save
|
||||||
const onSaveAboutMe = (inputString) => {
|
const onSaveAboutMe = () => {
|
||||||
updateCorpGroupPropertiesMutation({
|
updateCorpGroupPropertiesMutation({
|
||||||
variables: {
|
variables: {
|
||||||
urn: urn || '',
|
urn: urn || '',
|
||||||
input: {
|
input: {
|
||||||
description: inputString,
|
description: stagedDescription,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -201,16 +267,65 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
|||||||
</SocialDetails>
|
</SocialDetails>
|
||||||
<Divider className="divider-aboutSection" />
|
<Divider className="divider-aboutSection" />
|
||||||
<AboutSection>
|
<AboutSection>
|
||||||
{TITLES.about}
|
<Row>
|
||||||
<AboutSectionText>
|
<Col span={22}>{TITLES.about}</Col>
|
||||||
<Paragraph
|
<Col span={2}>
|
||||||
editable={canEditGroup ? { onChange: onSaveAboutMe } : false}
|
<EditIcon onClick={() => SetIsUpdatingDescription(true)} data-testid="edit-icon" />
|
||||||
ellipsis={{ rows: 2, expandable: true, symbol: 'Read more' }}
|
</Col>
|
||||||
>
|
</Row>
|
||||||
{aboutText || <EmptyValue />}
|
|
||||||
</Paragraph>
|
|
||||||
</AboutSectionText>
|
|
||||||
</AboutSection>
|
</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" />
|
<Divider className="divider-groupsSection" />
|
||||||
<GroupsSection>
|
<GroupsSection>
|
||||||
<GroupOwnerSideBarSection ownership={ownership} urn={urn || ''} refetch={refetch} />
|
<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 { message, Button, Input, Modal, Typography, Form, Collapse } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { useCreateGroupMutation } from '../../../graphql/group.generated';
|
import { useCreateGroupMutation } from '../../../graphql/group.generated';
|
||||||
import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
|
import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
|
||||||
import { validateCustomUrnId } from '../../shared/textUtil';
|
import { validateCustomUrnId } from '../../shared/textUtil';
|
||||||
import analytics, { EventType } from '../../analytics';
|
import analytics, { EventType } from '../../analytics';
|
||||||
import { CorpGroup, EntityType } from '../../../types.generated';
|
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 = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCreate: (group: CorpGroup) => void;
|
onCreate: (group: CorpGroup) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledEditor = styled(MarkdownEditor)`
|
||||||
|
border: 1px solid ${ANTD_GRAY[4]};
|
||||||
|
`;
|
||||||
|
|
||||||
export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
||||||
const [stagedName, setStagedName] = useState('');
|
const [stagedName, setStagedName] = useState('');
|
||||||
const [stagedDescription, setStagedDescription] = useState('');
|
const [stagedDescription, setStagedDescription] = useState('');
|
||||||
@ -19,45 +26,54 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
|||||||
const [createButtonEnabled, setCreateButtonEnabled] = useState(true);
|
const [createButtonEnabled, setCreateButtonEnabled] = useState(true);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// Reference to the styled editor for handling focus
|
||||||
|
const styledEditorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const onCreateGroup = () => {
|
const onCreateGroup = () => {
|
||||||
createGroupMutation({
|
// Check if the Enter key was pressed inside the styled editor to prevent unintended form submission
|
||||||
variables: {
|
const isEditorNewlineKeypress =
|
||||||
input: {
|
document.activeElement !== styledEditorRef.current &&
|
||||||
id: stagedId,
|
!styledEditorRef.current?.contains(document.activeElement);
|
||||||
name: stagedName,
|
if (isEditorNewlineKeypress) {
|
||||||
description: stagedDescription,
|
createGroupMutation({
|
||||||
},
|
variables: {
|
||||||
},
|
input: {
|
||||||
})
|
id: stagedId,
|
||||||
.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,
|
name: stagedName,
|
||||||
info: {
|
description: stagedDescription,
|
||||||
description: stagedDescription,
|
},
|
||||||
},
|
},
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.then(({ data, errors }) => {
|
||||||
message.destroy();
|
if (!errors) {
|
||||||
message.error({ content: `Failed to create group!: \n ${e.message || ''}`, duration: 3 });
|
analytics.event({
|
||||||
})
|
type: EventType.CreateGroupEvent,
|
||||||
.finally(() => {
|
});
|
||||||
setStagedName('');
|
message.success({
|
||||||
setStagedDescription('');
|
content: `Created group!`,
|
||||||
});
|
duration: 3,
|
||||||
onClose();
|
});
|
||||||
|
// 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
|
// Handle the Enter press
|
||||||
@ -65,8 +81,13 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
|||||||
querySelectorToExecuteClick: '#createGroupButton',
|
querySelectorToExecuteClick: '#createGroupButton',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateDescription(description: string) {
|
||||||
|
setStagedDescription(description);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
width={700}
|
||||||
title="Create new group"
|
title="Create new group"
|
||||||
visible
|
visible
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
@ -112,12 +133,11 @@ export default function CreateGroupModal({ onClose, onCreate }: Props) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={<Typography.Text strong>Description</Typography.Text>}>
|
<Form.Item label={<Typography.Text strong>Description</Typography.Text>}>
|
||||||
<Typography.Paragraph>An optional description for your new group.</Typography.Paragraph>
|
<Typography.Paragraph>An optional description for your new group.</Typography.Paragraph>
|
||||||
<Form.Item name="description" rules={[{ whitespace: true }, { min: 1, max: 500 }]} hasFeedback>
|
<Form.Item name="description" rules={[{ whitespace: true }]} hasFeedback>
|
||||||
<Input
|
{/* Styled editor for the group description */}
|
||||||
placeholder="A description for your group"
|
<div ref={styledEditorRef}>
|
||||||
value={stagedDescription}
|
<StyledEditor doNotFocus content={stagedDescription} onChange={updateDescription} />
|
||||||
onChange={(event) => setStagedDescription(event.target.value)}
|
</div>
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Collapse ghost>
|
<Collapse ghost>
|
||||||
|
@ -72,8 +72,10 @@ describe("create and manage group", () => {
|
|||||||
cy.focused().clear().type(`Test group EDITED ${test_id}{enter}`);
|
cy.focused().clear().type(`Test group EDITED ${test_id}{enter}`);
|
||||||
cy.waitTextVisible("Name Updated");
|
cy.waitTextVisible("Name Updated");
|
||||||
cy.contains(`Test group EDITED ${test_id}`).should("be.visible");
|
cy.contains(`Test group EDITED ${test_id}`).should("be.visible");
|
||||||
cy.contains("Test group description").find('[aria-label="edit"]').click();
|
cy.get('[data-testid="edit-icon"]').click();
|
||||||
cy.focused().type(" EDITED{enter}");
|
cy.waitTextVisible("Edit Description");
|
||||||
|
cy.get("#description").should("be.visible").type(" EDITED");
|
||||||
|
cy.get("#updateGroupButton").click();
|
||||||
cy.waitTextVisible("Changes saved.");
|
cy.waitTextVisible("Changes saved.");
|
||||||
cy.contains("Test group description EDITED").should("be.visible");
|
cy.contains("Test group description EDITED").should("be.visible");
|
||||||
cy.clickOptionWithText("Add Owners");
|
cy.clickOptionWithText("Add Owners");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user