Add roadmap management to Site settings (#123)

This commit is contained in:
Riccardo Graziosi
2022-06-12 15:22:06 +02:00
committed by GitHub
parent db674eaf6a
commit e2065b2c5e
31 changed files with 495 additions and 60 deletions

View File

@@ -81,7 +81,7 @@ const Comment = ({
</a>
{
isPowerUser ?
<React.Fragment>
<>
<Separator />
<a
onClick={() => handleToggleIsCommentUpdate(id, isPostUpdate)}
@@ -103,7 +103,7 @@ const Comment = ({
{I18n.t('common.buttons.delete')}
</a>
</React.Fragment>
</>
:
null
}

View File

@@ -36,7 +36,7 @@ const CommentList = ({
isPowerUser,
userEmail,
}: Props) => (
<React.Fragment>
<>
{comments.map((comment, i) => {
if (comment.parentId === parentId) {
return (
@@ -77,7 +77,7 @@ const CommentList = ({
);
} else return null;
})}
</React.Fragment>
</>
);
export default CommentList;

View File

@@ -41,11 +41,11 @@ const NewComment = ({
isPowerUser,
userEmail,
}: Props) => (
<React.Fragment>
<>
<div className="newCommentForm">
{
isLoggedIn ?
<React.Fragment>
<>
<div className="commentBodyForm">
<Gravatar email={userEmail} size={48} className="currentUserAvatar" />
<textarea
@@ -69,7 +69,7 @@ const NewComment = ({
:
null
}
</React.Fragment>
</>
:
<a href="/users/sign_in" className="loginInfo">
{I18n.t('post.new_comment.not_logged_in')}
@@ -78,7 +78,7 @@ const NewComment = ({
</div>
{ error ? <DangerText>{error}</DangerText> : null }
</React.Fragment>
</>
);
export default NewComment;

View File

@@ -56,12 +56,12 @@ const PostUpdateList = ({
{postUpdate.body}
</ReactMarkdown>
:
<React.Fragment>
<>
<i>{I18n.t('post.updates_box.status_change')}</i>&nbsp;
<PostStatusLabel
{...postStatuses.find(postStatus => postStatus.id === postUpdate.postStatusId)}
/>
</React.Fragment>
</>
}
</div>

View File

@@ -72,7 +72,7 @@ class BoardsEditable extends React.Component<Props, State> {
<DragZone dndProvided={provided} isDragDisabled={settingsAreUpdating} />
{ editMode === false ?
<React.Fragment>
<>
<div className="boardInfo">
<div className="boardName">
<PostBoardLabel name={name} />
@@ -94,9 +94,9 @@ class BoardsEditable extends React.Component<Props, State> {
{I18n.t('common.buttons.delete')}
</a>
</div>
</React.Fragment>
</>
:
<React.Fragment>
<>
<BoardForm
mode='update'
id={id}
@@ -110,7 +110,7 @@ class BoardsEditable extends React.Component<Props, State> {
onClick={this.toggleEditMode}>
{I18n.t('common.buttons.cancel')}
</a>
</React.Fragment>
</>
}
</li>
)}

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import I18n from 'i18n-js';
import BoardEditable from './BoardEditable';
import BoardForm from './BoardForm';
@@ -97,8 +97,8 @@ class BoardsSiteSettingsP extends React.Component<Props> {
{
boards.items.length > 0 ?
<DragDropContext onDragEnd={this.handleDragEnd}>
<Droppable droppableId="boards">
{provided => (
<Droppable droppableId="boards">
{provided => (
<ul ref={provided.innerRef} {...provided.droppableProps} className="boardsList">
{boards.items.map((board, i) => (
<BoardEditable
@@ -117,7 +117,7 @@ class BoardsSiteSettingsP extends React.Component<Props> {
{provided.placeholder}
</ul>
)}
</Droppable>
</Droppable>
</DragDropContext>
:
boards.areLoading ?

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { Draggable } from 'react-beautiful-dnd';
import I18n from 'i18n-js';
import PostStatusLabel from "../../common/PostStatusLabel";
import DragZone from '../../common/DragZone';
@@ -71,7 +70,7 @@ class PostStatusEditable extends React.Component<Props, State> {
<DragZone dndProvided={provided} isDragDisabled={settingsAreUpdating} />
{ editMode === false ?
<React.Fragment>
<>
<PostStatusLabel name={name} color={color} />
<div className="postStatusEditableActions">
@@ -86,9 +85,9 @@ class PostStatusEditable extends React.Component<Props, State> {
{I18n.t('common.buttons.delete')}
</a>
</div>
</React.Fragment>
</>
:
<React.Fragment>
<>
<PostStatusForm
mode='update'
id={id}
@@ -102,7 +101,7 @@ class PostStatusEditable extends React.Component<Props, State> {
onClick={this.toggleEditMode}>
{I18n.t('common.buttons.cancel')}
</a>
</React.Fragment>
</>
}
</li>
)}

View File

@@ -1,10 +1,7 @@
import * as React from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import I18n from 'i18n-js';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import IPostStatus from '../../../interfaces/IPostStatus';
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
import { CenteredMutedText } from '../../common/CustomTexts';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import PostStatusForm from './PostStatusForm';
@@ -12,6 +9,9 @@ import PostStatusEditable from './PostStatusEditable';
import Spinner from '../../common/Spinner';
import Box from '../../common/Box';
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
import IPostStatus from '../../../interfaces/IPostStatus';
interface Props {
authenticityToken: string;
postStatuses: PostStatusesState;
@@ -92,8 +92,8 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
{
postStatuses.items.length > 0 ?
<DragDropContext onDragEnd={this.handleDragEnd}>
<Droppable droppableId="postStatuses">
{provided => (
<Droppable droppableId="postStatuses">
{provided => (
<ul ref={provided.innerRef} {...provided.droppableProps} className="postStatusesList">
{postStatuses.items.map((postStatus, i) => (
<PostStatusEditable
@@ -112,13 +112,13 @@ class PostStatusesSiteSettingsP extends React.Component<Props> {
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
:
postStatuses.areLoading ?
<Spinner />
</Droppable>
</DragDropContext>
:
<CenteredMutedText>{I18n.t('site_settings.post_statuses.empty')}</CenteredMutedText>
postStatuses.areLoading ?
<Spinner />
:
<CenteredMutedText>{I18n.t('site_settings.post_statuses.empty')}</CenteredMutedText>
}
</Box>

View File

@@ -0,0 +1,40 @@
import * as React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import DragZone from '../../common/DragZone';
import { TitleText } from '../../common/CustomTexts';
interface Props {
id: number;
name: string;
color: string;
index: number;
settingsAreUpdating: boolean;
headerOnly?: boolean;
}
const RoadmapPostStatus = ({
id,
name,
color,
index,
settingsAreUpdating,
headerOnly,
}: Props) => (
<Draggable key={id} draggableId={id.toString()} index={index} isDragDisabled={settingsAreUpdating}>
{(provided, snapshot) => (
<div
className={`roadmapPostStatus${snapshot.isDragging ? '' : ' notDragging'}${headerOnly ? ' headerOnly' : ''}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className="roadmapPostStatusHeader" style={{backgroundColor: color}}>
<DragZone color='white' dndProvided={provided} isDragDisabled={settingsAreUpdating} />
<TitleText>{name}</TitleText>
</div>
</div>
)}
</Draggable>
);
export default RoadmapPostStatus;

View File

@@ -0,0 +1,167 @@
import * as React from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import I18n from 'i18n-js';
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
import Box from '../../common/Box';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import RoadmapPostStatus from './RoadmapPostStatus';
import IPostStatus from '../../../interfaces/IPostStatus';
import { MutedText } from '../../common/CustomTexts';
interface Props {
authenticityToken: string,
postStatuses: PostStatusesState,
settingsAreUpdating: boolean,
settingsError: string,
requestPostStatuses(): void;
updatePostStatus(
id: number,
showInRoadmap: boolean,
onComplete: Function,
authenticityToken: string,
): void;
}
interface State {
isDragging: number;
}
class RoadmapSiteSettingsP extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isDragging: null,
};
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
}
componentDidMount() {
this.props.requestPostStatuses();
}
handleDragStart(result) {
this.setState({ isDragging: parseInt(result.draggableId) });
}
handleDragEnd(result) {
if (result.destination == null || result.source.droppableId === result.destination.droppableId) {
this.setState({ isDragging: null });
return;
}
this.props.updatePostStatus(
result.draggableId,
result.destination.droppableId === 'statusesInRoadmap',
() => this.setState({ isDragging: null }),
this.props.authenticityToken,
);
}
// Workaround needed because after dropping a post status, the state is not yet updated
// with the new showInRoadmap value (we need to wait POSTSTATUS_UPDATE_SUCCESS dispatch)
// and the UI would flicker, moving the poststatus back in its original spot
placeDraggingStatusDuringUpdate(statusesInRoadmap: IPostStatus[], statusesNotInRoadmap: IPostStatus[]) {
const { postStatuses } = this.props;
const movedPostStatus = postStatuses.items.find(postStatus => postStatus.id === this.state.isDragging);
if (movedPostStatus.showInRoadmap) {
statusesInRoadmap = statusesInRoadmap.filter(postStatus => postStatus.id !== this.state.isDragging);
statusesNotInRoadmap.push(movedPostStatus);
statusesNotInRoadmap.sort((ps1, ps2) => ps1.order - ps2.order);
} else {
statusesNotInRoadmap = statusesNotInRoadmap.filter(postStatus => postStatus.id !== this.state.isDragging);
statusesInRoadmap.push(movedPostStatus);
statusesInRoadmap.sort((ps1, ps2) => ps1.order - ps2.order);
}
return [statusesInRoadmap, statusesNotInRoadmap];
}
render() {
const { postStatuses, settingsAreUpdating, settingsError } = this.props;
const { isDragging } = this.state;
let statusesInRoadmap = postStatuses.items.filter(postStatus => postStatus.showInRoadmap);
let statusesNotInRoadmap = postStatuses.items.filter(postStatus => !postStatus.showInRoadmap);
if (settingsAreUpdating && this.state.isDragging) {
[statusesInRoadmap, statusesNotInRoadmap] = this.placeDraggingStatusDuringUpdate(statusesInRoadmap, statusesNotInRoadmap);
}
return (
<DragDropContext onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd}>
<Box>
<h2>{I18n.t('site_settings.roadmap.title')}</h2>
<Droppable droppableId="statusesInRoadmap" direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={`roadmapPostStatuses${isDragging ? ' isDraggingSomething' : ''}${snapshot.isDraggingOver ? ' isDraggingOver' : ''}`}
{...provided.droppableProps}
>
{statusesInRoadmap.map((postStatus, i) => (
<RoadmapPostStatus
id={postStatus.id}
name={postStatus.name}
color={postStatus.color}
index={i}
settingsAreUpdating={settingsAreUpdating}
key={postStatus.id}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
{
statusesInRoadmap.length > 0 ?
null
:
<MutedText>{I18n.t('site_settings.roadmap.empty')}</MutedText>
}
<MutedText>{I18n.t('site_settings.roadmap.help')}</MutedText>
</Box>
<Box>
<h2>{I18n.t('site_settings.roadmap.title2')}</h2>
<Droppable droppableId="statusesNotInRoadmap" direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={`roadmapPostStatuses${isDragging ? ' isDraggingSomething' : ''}${snapshot.isDraggingOver ? ' isDraggingOver' : ''}`}
{...provided.droppableProps}
>
{statusesNotInRoadmap.map((postStatus, i) => (
<RoadmapPostStatus
id={postStatus.id}
name={postStatus.name}
color={postStatus.color}
index={i}
settingsAreUpdating={settingsAreUpdating}
headerOnly
key={postStatus.id}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</Box>
<SiteSettingsInfoBox areUpdating={settingsAreUpdating || postStatuses.areLoading} error={settingsError} />
</DragDropContext>
);
}
}
export default RoadmapSiteSettingsP;

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import RoadmapSiteSettings from '../../../containers/RoadmapSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { State } from '../../../reducers/rootReducer';
interface Props {
authenticityToken: string;
}
class RoadmapSiteSettingsRoot extends React.Component<Props> {
store: Store<State, any>;
constructor(props: Props) {
super(props);
this.store = createStoreHelper();
}
render() {
return (
<Provider store={this.store}>
<RoadmapSiteSettings
authenticityToken={this.props.authenticityToken}
/>
</Provider>
);
}
}
export default RoadmapSiteSettingsRoot;

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
const DragZone = ({dndProvided, isDragDisabled}) => (
const DragZone = ({dndProvided, isDragDisabled, color = 'black'}) => (
<span
className={`drag-zone${isDragDisabled ? ' drag-zone-disabled' : ''}`}
{...dndProvided.dragHandleProps}
>
<span className="drag-icon"></span>
<span className={`drag-icon${color === 'white' ? ' drag-icon-white' : ''}`}></span>
</span>
);