mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add roadmap management to Site settings (#123)
This commit is contained in:
committed by
GitHub
parent
db674eaf6a
commit
e2065b2c5e
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -56,12 +56,12 @@ const PostUpdateList = ({
|
||||
{postUpdate.body}
|
||||
</ReactMarkdown>
|
||||
:
|
||||
<React.Fragment>
|
||||
<>
|
||||
<i>{I18n.t('post.updates_box.status_change')}</i>
|
||||
<PostStatusLabel
|
||||
{...postStatuses.find(postStatus => postStatus.id === postUpdate.postStatusId)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
33
app/javascript/components/SiteSettings/Roadmap/index.tsx
Normal file
33
app/javascript/components/SiteSettings/Roadmap/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user