1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-04 21:15:25 +02:00

Initial commit

This commit is contained in:
Maksim Eltyshev 2019-08-31 04:07:25 +05:00
commit 5ffef61fe7
613 changed files with 91659 additions and 0 deletions

View file

@ -0,0 +1,101 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
Comment, Icon, Loader, Visibility,
} from 'semantic-ui-react';
import { ActionTypes } from '../../../constants/Enums';
import AddComment from './AddComment';
import Item from './Item';
import styles from './Actions.module.css';
const Actions = React.memo(
({
items,
isFetching,
isAllFetched,
isEditable,
onFetch,
onCommentCreate,
onCommentUpdate,
onCommentDelete,
}) => {
const [t] = useTranslation();
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="comment outline" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.addComment')}</div>
<AddComment onCreate={onCommentCreate} />
</div>
</div>
<div className={styles.contentModule}>
<div className={styles.moduleWrapper}>
<Icon name="list ul" className={styles.moduleIcon} />
<div className={styles.moduleHeader}>{t('common.actions')}</div>
<div className={styles.wrapper}>
<Comment.Group>
{items.map((item) => (item.type === ActionTypes.COMMENT_CARD ? (
<Item.Comment
key={item.id}
data={item.data}
createdAt={item.createdAt}
isPersisted={item.isPersisted}
user={item.user}
isEditable={isEditable}
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 ? (
<Loader active inverted inline="centered" size="small" className={styles.loader} />
) : (
!isAllFetched && <Visibility fireOnMount onOnScreen={onFetch} />
)}
</div>
</div>
</>
);
},
);
Actions.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isFetching: PropTypes.bool.isRequired,
isAllFetched: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
onFetch: PropTypes.func.isRequired,
onCommentCreate: PropTypes.func.isRequired,
onCommentUpdate: PropTypes.func.isRequired,
onCommentDelete: PropTypes.func.isRequired,
};
export default Actions;

View file

@ -0,0 +1,38 @@
.contentModule {
margin-bottom: 24px;
}
.loader {
margin-top: 10px !important;
}
.moduleHeader {
color: #17394d;
font-size: 16px;
font-weight: bold;
line-height: 20px;
margin: 0 0 4px;
padding: 8px 0;
}
.moduleIcon {
color: #17394d;
font-size: 17px !important;
height: 32px !important;
left: -40px;
line-height: 32px;
margin-right: 0 !important;
position: absolute;
top: 2px;
width: 32px !important;
}
.moduleWrapper {
margin: 0 0 0 40px;
position: relative;
}
.wrapper {
margin-left: -40px;
margin-top: 12px;
}

View file

@ -0,0 +1,75 @@
import React, { useCallback, useRef } 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 { useDeepCompareCallback, useForm } from '../../../hooks';
import styles from './AddComment.module.css';
const DEFAULT_DATA = {
text: '',
};
const AddComment = React.memo(({ onCreate }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const textField = useRef(null);
const submit = useDeepCompareCallback(() => {
const cleanData = {
...data,
text: data.text.trim(),
};
if (!cleanData.text) {
textField.current.ref.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
}, [onCreate, data, setData]);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === 'Enter') {
submit();
}
},
[submit],
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
return (
<Form onSubmit={handleSubmit}>
<TextArea
ref={textField}
as={TextareaAutosize}
name="text"
value={data.text}
placeholder={t('common.writeComment')}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
/>
<div className={styles.controls}>
<Button positive content={t('action.addComment')} disabled={!data.text} />
</div>
</Form>
);
});
AddComment.propTypes = {
onCreate: PropTypes.func.isRequired,
};
export default AddComment;

View file

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

View file

@ -0,0 +1,122 @@
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, useDeepCompareCallback, useForm } from '../../../hooks';
import styles from './EditComment.module.css';
const EditComment = 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 = useDeepCompareCallback(() => {
setIsOpened(true);
setData({
text: '',
...defaultData,
});
}, [defaultData, setData]);
const close = useCallback(() => {
setIsOpened(false);
setData(null);
}, [setData]);
const submit = useDeepCompareCallback(() => {
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(
isOpened,
close,
);
const handleSubmit = useCallback(() => {
submit();
}, [submit]);
useEffect(() => {
if (isOpened) {
textField.current.ref.current.select();
}
}, [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>
);
});
EditComment.propTypes = {
children: PropTypes.element.isRequired,
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default React.memo(EditComment);

View file

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

View file

@ -0,0 +1,91 @@
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 { ActionTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment';
import User from '../../User';
import styles from './Item.module.css';
const Item = React.memo(({
type, data, createdAt, user,
}) => {
const [t] = useTranslation();
let contentNode;
switch (type) {
case ActionTypes.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 ActionTypes.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} avatar={user.avatar} />
</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,31 @@
.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,87 @@
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 EditComment from './EditComment';
import User from '../../User';
import DeletePopup from '../../DeletePopup';
import styles from './ItemComment.module.css';
const ItemComment = React.memo(
({
data, createdAt, isPersisted, user, isEditable, onUpdate, onDelete,
}) => {
const [t] = useTranslation();
const editComment = useRef(null);
const handleEditClick = useCallback(() => {
editComment.current.open();
}, []);
return (
<Comment>
<span className={styles.user}>
<User name={user.name} avatar={user.avatar} />
</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>
<EditComment ref={editComment} defaultData={data} onUpdate={onUpdate}>
<>
<p className={styles.text}>{data.text}</p>
<Comment.Actions>
{user.isCurrent && (
<Comment.Action
as="button"
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
)}
{(user.isCurrent || isEditable) && (
<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>
</>
</EditComment>
</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
isEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ItemComment;

View file

@ -0,0 +1,49 @@
.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-color: #fff;
border-radius: 0px 8px 8px;
box-shadow: 0 1px 2px -1px rgba(9, 45, 66, 0.25),
0 0 0 1px rgba(9, 45, 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;
text-overflow: ellipsis;
white-space: pre-line;
word-break: break-word;
}
.title {
padding-bottom: 4px;
}
.user {
display: inline-block;
padding: 4px 8px 0 0;
vertical-align: top;
}

View file

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