1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-02 03:55:26 +02:00

ref: Refactoring

This commit is contained in:
Maksim Eltyshev 2022-08-04 13:31:14 +02:00
parent aa4723d7fe
commit 3f8216dca8
189 changed files with 3781 additions and 3486 deletions

View file

@ -0,0 +1,112 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Comment, Icon, Loader, Visibility } from 'semantic-ui-react';
import { ActivityTypes } from '../../../constants/Enums';
import CommentAdd from './CommentAdd';
import Item from './Item';
import styles from './Activities.module.scss';
const Activities = React.memo(
({
items,
isFetching,
isAllFetched,
isDetailsVisible,
isDetailsFetching,
canEdit,
canEditAllComments,
onFetch,
onDetailsToggle,
onCommentCreate,
onCommentUpdate,
onCommentDelete,
}) => {
const [t] = useTranslation();
const handleToggleDetailsClick = useCallback(() => {
onDetailsToggle(!isDetailsVisible);
}, [isDetailsVisible, onDetailsToggle]);
const handleCommentUpdate = useCallback(
(id, data) => {
onCommentUpdate(id, data);
},
[onCommentUpdate],
);
const handleCommentDelete = useCallback(
(id) => {
onCommentDelete(id);
},
[onCommentDelete],
);
return (
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="list ul" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>
{t('common.actions')}
<Button
content={isDetailsVisible ? t('action.hideDetails') : t('action.showDetails')}
className={styles.toggleButton}
onClick={handleToggleDetailsClick}
/>
</div>
{canEdit && <CommentAdd onCreate={onCommentCreate} />}
<div className={styles.wrapper}>
<Comment.Group>
{items.map((item) =>
item.type === ActivityTypes.COMMENT_CARD ? (
<Item.Comment
key={item.id}
data={item.data}
createdAt={item.createdAt}
isPersisted={item.isPersisted}
user={item.user}
canEdit={(item.user.isCurrent && canEdit) || canEditAllComments}
onUpdate={(data) => handleCommentUpdate(item.id, data)}
onDelete={() => handleCommentDelete(item.id)}
/>
) : (
<Item
key={item.id}
type={item.type}
data={item.data}
createdAt={item.createdAt}
user={item.user}
/>
),
)}
</Comment.Group>
</div>
{isFetching || isDetailsFetching ? (
<Loader active inverted inline="centered" size="small" className={styles.loader} />
) : (
!isAllFetched && <Visibility fireOnMount onOnScreen={onFetch} />
)}
</div>
</div>
);
},
);
Activities.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isFetching: PropTypes.bool.isRequired,
isAllFetched: PropTypes.bool.isRequired,
isDetailsVisible: PropTypes.bool.isRequired,
isDetailsFetching: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
canEditAllComments: PropTypes.bool.isRequired,
onFetch: PropTypes.func.isRequired,
onDetailsToggle: PropTypes.func.isRequired,
onCommentCreate: PropTypes.func.isRequired,
onCommentUpdate: PropTypes.func.isRequired,
onCommentDelete: PropTypes.func.isRequired,
};
export default Activities;

View file

@ -0,0 +1,60 @@
:global(#app) {
.contentModule {
margin-bottom: 24px;
}
.loader {
margin-top: 10px;
}
.moduleHeader {
align-items: center;
color: #17394d;
display: flex;
font-size: 16px;
font-weight: bold;
height: 36px;
justify-content: space-between;
margin: 0 0 4px;
}
.moduleIcon {
color: #17394d;
font-size: 17px;
height: 32px;
left: -40px;
line-height: 32px;
margin-right: 0;
position: absolute;
top: 2px;
width: 32px;
}
.moduleWrapper {
margin: 0 0 0 40px;
position: relative;
}
.toggleButton {
background: transparent;
box-shadow: none;
color: #6b808c;
float: right;
font-weight: normal;
margin-right: 0;
padding: 6px 11px;
text-align: left;
text-decoration: underline;
transition: none;
&:hover {
background: rgba(9, 30, 66, 0.08);
color: #092d42;
}
}
.wrapper {
margin-left: -40px;
margin-top: 12px;
}
}

View file

@ -0,0 +1,102 @@
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useDidUpdate, useToggle } from '../../../lib/hooks';
import { useClosableForm, useForm } from '../../../hooks';
import styles from './CommentAdd.module.scss';
const DEFAULT_DATA = {
text: '',
};
const CommentAdd = React.memo(({ onCreate }) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectTextFieldState, selectTextField] = useToggle();
const textField = useRef(null);
const close = useCallback(() => {
setIsOpened(false);
}, []);
const submit = useCallback(() => {
const cleanData = {
...data,
text: data.text.trim(),
};
if (!cleanData.text) {
textField.current.ref.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
selectTextField();
}, [onCreate, data, setData, selectTextField]);
const handleFieldFocus = useCallback(() => {
setIsOpened(true);
}, []);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === 'Enter') {
submit();
}
},
[submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(close);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useDidUpdate(() => {
textField.current.ref.current.focus();
}, [selectTextFieldState]);
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={textField}
as={TextareaAutosize}
name="text"
value={data.text}
placeholder={t('common.writeComment')}
minRows={isOpened ? 3 : 1}
spellCheck={false}
className={styles.field}
onFocus={handleFieldFocus}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
{isOpened && (
<div className={styles.controls}>
<Button
positive
content={t('action.addComment')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
)}
</Form>
);
});
CommentAdd.propTypes = {
onCreate: PropTypes.func.isRequired,
};
export default CommentAdd;

View file

@ -0,0 +1,25 @@
:global(#app) {
.controls {
clear: both;
margin-top: 6px;
}
.field {
background: #fff;
border: 0;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.5;
font-size: 14px;
margin-bottom: 6px;
overflow: hidden;
padding: 8px 12px;
resize: none;
width: 100%;
&:focus {
outline: none;
}
}
}

View file

@ -0,0 +1,120 @@
import { dequal } from 'dequal';
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useForm } from '../../../hooks';
import styles from './CommentEdit.module.scss';
const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(null);
const textField = useRef(null);
const open = useCallback(() => {
setIsOpened(true);
setData({
text: '',
...defaultData,
});
}, [defaultData, setData]);
const close = useCallback(() => {
setIsOpened(false);
setData(null);
}, [setData]);
const submit = useCallback(() => {
const cleanData = {
...data,
text: data.text.trim(),
};
if (!cleanData.text) {
textField.current.ref.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
close();
}, [defaultData, onUpdate, data, close]);
useImperativeHandle(
ref,
() => ({
open,
close,
}),
[open, close],
);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === 'Enter') {
submit();
}
},
[submit],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(
close,
isOpened,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
textField.current.ref.current.focus();
}
}, [isOpened]);
if (!isOpened) {
return children;
}
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={textField}
as={TextareaAutosize}
name="text"
value={data.text}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.save')}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
CommentEdit.propTypes = {
children: PropTypes.element.isRequired,
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(CommentEdit);

View file

@ -0,0 +1,26 @@
:global(#app) {
.controls {
clear: both;
margin-top: 6px;
}
.field {
background: #fff;
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.4;
font-size: 14px;
margin-bottom: 4px;
overflow: hidden;
padding: 8px 12px;
resize: none;
width: 100%;
&:focus {
outline: none;
}
}
}

View file

@ -0,0 +1,89 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import { ActivityTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment';
import User from '../../User';
import styles from './Item.module.scss';
const Item = React.memo(({ type, data, createdAt, user }) => {
const [t] = useTranslation();
let contentNode;
switch (type) {
case ActivityTypes.CREATE_CARD:
contentNode = (
<Trans
i18nKey="common.userAddedThisCardToList"
values={{
user: user.name,
list: data.list.name,
}}
>
<span className={styles.author}>{user.name}</span>
<span className={styles.text}>
{' added this card to '}
{data.list.name}
</span>
</Trans>
);
break;
case ActivityTypes.MOVE_CARD:
contentNode = (
<Trans
i18nKey="common.userMovedThisCardFromListToList"
values={{
user: user.name,
fromList: data.fromList.name,
toList: data.toList.name,
}}
>
<span className={styles.author}>{user.name}</span>
<span className={styles.text}>
{' moved this card from '}
{data.fromList.name}
{' to '}
{data.toList.name}
</span>
</Trans>
);
break;
default:
contentNode = null;
}
return (
<Comment>
<span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} />
</span>
<div className={classNames(styles.content)}>
<div>{contentNode}</div>
<span className={styles.date}>
{t('format:longDateTime', {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</div>
</Comment>
);
});
Item.Comment = ItemComment;
Item.propTypes = {
type: PropTypes.string.isRequired,
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
createdAt: PropTypes.instanceOf(Date).isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Item;

View file

@ -0,0 +1,33 @@
:global(#app) {
.author {
color: #17394d;
display: inline-block;
font-weight: bold;
line-height: 20px;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
}
.date {
color: #6b808c;
display: inline-block;
font-size: 12px;
line-height: 20px;
}
.text {
line-height: 20px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}
}

View file

@ -0,0 +1,86 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import { Markdown } from '../../../lib/custom-ui';
import CommentEdit from './CommentEdit';
import User from '../../User';
import DeletePopup from '../../DeletePopup';
import styles from './ItemComment.module.scss';
const ItemComment = React.memo(
({ data, createdAt, isPersisted, user, canEdit, onUpdate, onDelete }) => {
const [t] = useTranslation();
const commentEdit = useRef(null);
const handleEditClick = useCallback(() => {
commentEdit.current.open();
}, []);
return (
<Comment>
<span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} />
</span>
<div className={classNames(styles.content)}>
<div className={styles.title}>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t('format:longDateTime', {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</div>
<CommentEdit ref={commentEdit} defaultData={data} onUpdate={onUpdate}>
<>
<div className={styles.text}>
<Markdown linkTarget="_blank">{data.text}</Markdown>
</div>
{canEdit && (
<Comment.Actions>
<Comment.Action
as="button"
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
<DeletePopup
title={t('common.deleteComment', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisComment')}
buttonContent={t('action.deleteComment')}
onConfirm={onDelete}
>
<Comment.Action
as="button"
content={t('action.delete')}
disabled={!isPersisted}
/>
</DeletePopup>
</Comment.Actions>
)}
</>
</CommentEdit>
</div>
</Comment>
);
},
);
ItemComment.propTypes = {
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
createdAt: PropTypes.instanceOf(Date).isRequired,
isPersisted: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ItemComment;

View file

@ -0,0 +1,48 @@
:global(#app) {
.author {
color: #17394d;
display: inline-block;
font-weight: bold;
line-height: 20px;
margin-right: 8px;
}
.content {
border-bottom: 1px solid #092d4221;
display: inline-block;
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
}
.date {
color: #6b808c;
display: inline-block;
font-size: 12px;
line-height: 20px;
}
.text {
background: #fff;
border-radius: 0px 8px 8px;
box-shadow: 0 1px 2px -1px rgba(9, 30, 66, 0.25),
0 0 0 1px rgba(9, 30, 66, 0.08);
box-sizing: border-box;
color: #17394d;
display: inline-block;
margin: 1px 2px 4px 1px;
max-width: 100%;
overflow: hidden;
padding: 8px 12px;
}
.title {
padding-bottom: 4px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}
}

View file

@ -0,0 +1,3 @@
import Activities from './Activities';
export default Activities;