Skip to content

Commit

Permalink
Feat collaboration (#2859)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sachin-chaurasiya committed Feb 28, 2022
1 parent 22393c2 commit a68667c
Show file tree
Hide file tree
Showing 50 changed files with 3,033 additions and 266 deletions.
5 changes: 4 additions & 1 deletion openmetadata-ui/src/main/resources/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"postcss": "^8.3.2",
"process": "^0.11.10",
"prop-types": "^15.7.2",
"quill-markdown-shortcuts": "^0.0.10",
"react": "^16.14.0",
"react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.0.4",
Expand All @@ -53,6 +54,7 @@
"react-js-pagination": "^3.0.3",
"react-markdown": "^6.0.3",
"react-oidc": "^1.0.3",
"react-quill": "^1.3.5",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.1",
"react-slick": "^0.28.1",
Expand All @@ -68,7 +70,8 @@
"stream-http": "^3.2.0",
"styled-components": "^5.2.3",
"tailwindcss": "^2.1.4",
"to-arraybuffer": "^1.0.1"
"to-arraybuffer": "^1.0.1",
"turndown": "^7.1.1"
},
"scripts": {
"start": "NODE_ENV=development BABEL_ENV=development webpack serve --config ./webpack.config.dev.js --env development",
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 20 additions & 4 deletions openmetadata-ui/src/main/resources/ui/src/axiosAPIs/feedsAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,27 @@
*/

import { AxiosResponse } from 'axios';
import { Feed, FeedById } from 'Models';
import { Feed, Post } from 'Models';
import APIClient from './index';

export const getAllFeeds: Function = (): Promise<AxiosResponse> => {
return APIClient.get('/feed');
export const getAllFeeds: Function = (
entityLink?: string
): Promise<AxiosResponse> => {
return APIClient.get(`/feed`, {
params: {
entityLink: entityLink,
},
});
};

export const getFeedCount: Function = (
entityLink?: string
): Promise<AxiosResponse> => {
return APIClient.get(`/feed/count`, {
params: {
entityLink: entityLink,
},
});
};

export const postFeed: Function = (data: Feed): Promise<AxiosResponse> => {
Expand All @@ -29,7 +45,7 @@ export const getFeedById: Function = (id: string): Promise<AxiosResponse> => {

export const postFeedById: Function = (
id: string,
data: FeedById
data: Post
): Promise<AxiosResponse> => {
return APIClient.post(`/feed/${id}/posts`, data);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import classNames from 'classnames';
import { isUndefined, toLower } from 'lodash';
import { Post } from 'Models';
import React, { FC, Fragment, HTMLAttributes } from 'react';
import { Link } from 'react-router-dom';
import AppState from '../../../AppState';
import { TabSpecificField } from '../../../enums/entity.enum';
import { getPartialNameFromFQN } from '../../../utils/CommonUtils';
import {
getEntityField,
getEntityFQN,
getEntityType,
getReplyText,
} from '../../../utils/FeedUtils';
import { getEntityLink } from '../../../utils/TableUtils';
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
import Avatar from '../../common/avatar/Avatar';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';

interface ActivityFeedCardProp extends HTMLAttributes<HTMLDivElement> {
feed: Post;
entityLink?: string;
repliedUsers?: Array<string>;
replies?: number;
isEntityFeed?: boolean;
threadId?: string;
lastReplyTimeStamp?: number;
isFooterVisible?: boolean;
onThreadSelect?: (id: string) => void;
}
interface FeedHeaderProp
extends HTMLAttributes<HTMLDivElement>,
Pick<ActivityFeedCardProp, 'isEntityFeed'> {
createdBy: string;
timeStamp: number;
entityType: string;
entityFQN: string;
entityField: string;
}
interface FeedBodyProp extends HTMLAttributes<HTMLDivElement> {
message: string;
}
interface FeedFooterProp
extends HTMLAttributes<HTMLDivElement>,
Pick<
ActivityFeedCardProp,
| 'replies'
| 'repliedUsers'
| 'threadId'
| 'onThreadSelect'
| 'lastReplyTimeStamp'
| 'isFooterVisible'
> {}

const FeedHeader: FC<FeedHeaderProp> = ({
className,
createdBy,
timeStamp,
entityFQN,
entityType,
entityField,
isEntityFeed,
}) => {
return (
<div className={classNames('tw-flex tw-mb-1.5', className)}>
<Avatar name={createdBy} type="square" width="30" />
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
{createdBy}
{entityFQN && entityType && entityFQN ? (
<span className="tw-pl-1 tw-font-normal">
posted on{' '}
{isEntityFeed ? (
<span className="tw-heading">{entityField}</span>
) : (
<Fragment>
{entityType}{' '}
<Link
to={`${getEntityLink(
entityType as string,
entityFQN as string
)}/${TabSpecificField.ACTIVITY_FEED}`}>
<button className="link-text" disabled={AppState.isTourOpen}>
{getPartialNameFromFQN(
entityFQN as string,
entityType === 'table' ? ['table'] : ['database']
)}
</button>
</Link>
</Fragment>
)}
</span>
) : null}
<span className="tw-text-grey-muted tw-pl-2 tw-text-xs">
{getDayTimeByTimeStamp(timeStamp)}
</span>
</h6>
</div>
);
};

const FeedBody: FC<FeedBodyProp> = ({ message, className }) => {
return (
<div className={className}>
<RichTextEditorPreviewer
className="activity-feed-card-text"
enableSeeMoreVariant={false}
markdown={message}
/>
</div>
);
};

export const FeedFooter: FC<FeedFooterProp> = ({
repliedUsers,
replies,
className,
threadId,
onThreadSelect,
lastReplyTimeStamp,
isFooterVisible,
}) => {
const repliesCount = isUndefined(replies) ? 0 : replies;

return (
<div className={className}>
{!isUndefined(repliedUsers) &&
!isUndefined(replies) &&
isFooterVisible ? (
<div className="tw-flex tw-group">
{repliedUsers?.map((u, i) => (
<Avatar
className="tw-mt-0.5 tw-mx-0.5"
key={i}
name={u}
type="square"
width="22"
/>
))}
<p
className="tw-ml-1 link-text tw-text-xs tw-mt-1.5 tw-underline"
onClick={() => onThreadSelect?.(threadId as string)}>
{getReplyText(repliesCount)}
</p>
{lastReplyTimeStamp && repliesCount > 0 ? (
<span className="tw-text-grey-muted tw-pl-2 tw-text-xs tw-font-medium tw-mt-1.5">
Last reply{' '}
{toLower(getDayTimeByTimeStamp(lastReplyTimeStamp as number))}
</span>
) : null}
</div>
) : null}
</div>
);
};

const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
feed,
className,
replies,
repliedUsers,
entityLink,
isEntityFeed,
threadId,
lastReplyTimeStamp,
onThreadSelect,
isFooterVisible = false,
}) => {
const entityType = getEntityType(entityLink as string);
const entityFQN = getEntityFQN(entityLink as string);
const entityField = getEntityField(entityLink as string);

return (
<div className={classNames(className)}>
<FeedHeader
createdBy={feed.from}
entityFQN={entityFQN as string}
entityField={entityField as string}
entityType={entityType as string}
isEntityFeed={isEntityFeed}
timeStamp={feed.postTs}
/>
<FeedBody
className="tw-mx-7 tw-ml-9 tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md tw-break-all"
message={feed.message}
/>
<FeedFooter
className="tw-ml-9 tw-mt-3"
isFooterVisible={isFooterVisible}
lastReplyTimeStamp={lastReplyTimeStamp}
repliedUsers={repliedUsers}
replies={replies}
threadId={threadId}
onThreadSelect={onThreadSelect}
/>
</div>
);
};

export default ActivityFeedCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import classNames from 'classnames';
import React, { FC, HTMLAttributes, useRef, useState } from 'react';
import { HTMLToMarkdown } from '../../../utils/FeedUtils';
import SVGIcons from '../../../utils/SvgUtils';
import { Button } from '../../buttons/Button/Button';
import PopOver from '../../common/popover/PopOver';
import FeedEditor from '../../FeedEditor/FeedEditor';

interface ActivityFeedEditorProp extends HTMLAttributes<HTMLDivElement> {
onSave?: (value: string) => void;
buttonClass?: string;
}
type EditorContentRef = {
getEditorValue: () => string;
clearEditorValue: () => string;
};

const ActivityFeedEditor: FC<ActivityFeedEditorProp> = ({
className,
buttonClass = '',
onSave,
}) => {
const editorRef = useRef<EditorContentRef>();
const [editorValue, setEditorValue] = useState<string>('');

const onChangeHandler = (value: string) => {
setEditorValue(HTMLToMarkdown.turndown(value));
};
const onSaveHandler = () => {
if (editorRef.current) {
if (editorRef.current?.getEditorValue()) {
setEditorValue('');
editorRef.current?.clearEditorValue();
onSave?.(editorRef.current?.getEditorValue());
}
}
};

return (
<div className={classNames('tw-relative', className)}>
<FeedEditor
defaultValue={editorValue}
ref={editorRef}
onChangeHandler={onChangeHandler}
onSave={onSaveHandler}
/>
<div className="tw-absolute tw-right-2 tw-bottom-2 tw-flex tw-flex-row tw-items-center tw-justify-end">
<PopOver
html={
<>
<strong>Send now</strong>
</>
}
position="top"
size="small"
trigger="mouseenter">
<Button
className={classNames(
'tw-bg-gray-400 tw-py-0.5 tw-px-1 tw-rounded',
buttonClass
)}
disabled={editorValue.length === 0}
size="custom"
theme={editorValue.length > 0 ? 'primary' : 'default'}
variant="contained"
onClick={onSaveHandler}>
<SVGIcons alt="paper-plane" icon="icon-paper-plane" width="18px" />
</Button>
</PopOver>
</div>
</div>
);
};

export default ActivityFeedEditor;

0 comments on commit a68667c

Please sign in to comment.