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

@@ -75,6 +75,6 @@ class PostStatusesController < ApplicationController
def post_status_params def post_status_params
params params
.require(:post_status) .require(:post_status)
.permit(:name, :color) .permit(:name, :color, :show_in_roadmap)
end end
end end

View File

@@ -11,4 +11,7 @@ class SiteSettingsController < ApplicationController
def post_statuses def post_statuses
end end
def roadmap
end
end end

View File

@@ -1,7 +1,7 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import IPostStatus from '../../interfaces/IPostStatus'; import IPostStatusJSON from '../../interfaces/json/IPostStatus';
import { State } from '../../reducers/rootReducer'; import { State } from '../../reducers/rootReducer';
@@ -13,7 +13,7 @@ interface PostStatusesRequestStartAction {
export const POST_STATUSES_REQUEST_SUCCESS = 'POST_STATUSES_REQUEST_SUCCESS'; export const POST_STATUSES_REQUEST_SUCCESS = 'POST_STATUSES_REQUEST_SUCCESS';
interface PostStatusesRequestSuccessAction { interface PostStatusesRequestSuccessAction {
type: typeof POST_STATUSES_REQUEST_SUCCESS; type: typeof POST_STATUSES_REQUEST_SUCCESS;
postStatuses: Array<IPostStatus>; postStatuses: Array<IPostStatusJSON>;
} }
export const POST_STATUSES_REQUEST_FAILURE = 'POST_STATUSES_REQUEST_FAILURE'; export const POST_STATUSES_REQUEST_FAILURE = 'POST_STATUSES_REQUEST_FAILURE';
@@ -33,7 +33,7 @@ const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({
}); });
const postStatusesRequestSuccess = ( const postStatusesRequestSuccess = (
postStatuses: Array<IPostStatus> postStatuses: Array<IPostStatusJSON>
): PostStatusesRequestActionTypes => ({ ): PostStatusesRequestActionTypes => ({
type: POST_STATUSES_REQUEST_SUCCESS, type: POST_STATUSES_REQUEST_SUCCESS,
postStatuses, postStatuses,

View File

@@ -43,24 +43,34 @@ const postStatusUpdateFailure = (error: string): PostStatusUpdateFailureAction =
error, error,
}); });
export const updatePostStatus = ( interface UpdatePostStatusParams {
id: number, id: number;
name: string, name?: string;
color: string, color?: string;
authenticityToken: string, showInRoadmap?: boolean;
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => { authenticityToken: string;
}
export const updatePostStatus = ({
id,
name = null,
color = null,
showInRoadmap = null,
authenticityToken,
}: UpdatePostStatusParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(postStatusUpdateStart()); dispatch(postStatusUpdateStart());
const post_status = Object.assign({},
name !== null ? {name} : null,
color !== null ? {color} : null,
showInRoadmap !== null ? {show_in_roadmap: showInRoadmap} : null
);
try { try {
const res = await fetch(`/post_statuses/${id}`, { const res = await fetch(`/post_statuses/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: buildRequestHeaders(authenticityToken), headers: buildRequestHeaders(authenticityToken),
body: JSON.stringify({ body: JSON.stringify({post_status}),
post_status: {
name,
color,
},
}),
}); });
const json = await res.json(); const json = await res.json();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ const mapDispatchToProps = (dispatch: any) => ({
onSuccess: Function, onSuccess: Function,
authenticityToken: string, authenticityToken: string,
) { ) {
dispatch(updatePostStatus(id, name, color, authenticityToken)).then(res => { dispatch(updatePostStatus({id, name, color, authenticityToken})).then(res => {
if (res && res.status === HttpStatus.OK) onSuccess(); if (res && res.status === HttpStatus.OK) onSuccess();
}); });
}, },

View File

@@ -0,0 +1,30 @@
import { connect } from "react-redux";
import RoadmapSiteSettingsP from "../components/SiteSettings/Roadmap/RoadmapSiteSettingsP";
import { requestPostStatuses } from "../actions/PostStatus/requestPostStatuses";
import { State } from "../reducers/rootReducer";
import { updatePostStatus } from "../actions/PostStatus/updatePostStatus";
const mapStateToProps = (state: State) => ({
postStatuses: state.postStatuses,
settingsAreUpdating: state.siteSettings.roadmap.areUpdating,
settingsError: state.siteSettings.roadmap.error,
});
const mapDispatchToProps = (dispatch: any) => ({
requestPostStatuses() {
dispatch(requestPostStatuses());
},
updatePostStatus(id: number, showInRoadmap: boolean, onComplete: Function, authenticityToken: string) {
dispatch(updatePostStatus({id, showInRoadmap, authenticityToken})).then(() => {
onComplete();
});
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(RoadmapSiteSettingsP);

View File

@@ -2,6 +2,8 @@ interface IPostStatus {
id: number; id: number;
name: string; name: string;
color: string; color: string;
order: number;
showInRoadmap: boolean;
} }
export default IPostStatus; export default IPostStatus;

View File

@@ -2,6 +2,8 @@ interface IPostStatusJSON {
id: number; id: number;
name: string; name: string;
color: string; color: string;
order: number;
show_in_roadmap: boolean;
} }
export default IPostStatusJSON; export default IPostStatusJSON;

View File

@@ -0,0 +1,48 @@
import {
PostStatusUpdateActionTypes,
POSTSTATUS_UPDATE_START,
POSTSTATUS_UPDATE_SUCCESS,
POSTSTATUS_UPDATE_FAILURE,
} from '../../actions/PostStatus/updatePostStatus';
export interface SiteSettingsRoadmapState {
areUpdating: boolean;
error: string;
}
const initialState = {
areUpdating: false,
error: '',
};
const siteSettingsRoadmapReducer = (
state = initialState,
action: PostStatusUpdateActionTypes,
): SiteSettingsRoadmapState => {
switch (action.type) {
case POSTSTATUS_UPDATE_START:
return {
...state,
areUpdating: true,
};
case POSTSTATUS_UPDATE_SUCCESS:
return {
...state,
areUpdating: false,
error: '',
};
case POSTSTATUS_UPDATE_FAILURE:
return {
...state,
areUpdating: false,
error: action.error,
};
default:
return state;
}
}
export default siteSettingsRoadmapReducer;

View File

@@ -62,6 +62,8 @@ const postStatusesReducer = (
id: postStatus.id, id: postStatus.id,
name: postStatus.name, name: postStatus.name,
color: postStatus.color, color: postStatus.color,
order: postStatus.order,
showInRoadmap: postStatus.show_in_roadmap,
})), })),
areLoading: false, areLoading: false,
error: '', error: '',
@@ -85,7 +87,12 @@ const postStatusesReducer = (
...state, ...state,
items: state.items.map(postStatus => { items: state.items.map(postStatus => {
if (postStatus.id !== action.postStatus.id) return postStatus; if (postStatus.id !== action.postStatus.id) return postStatus;
return {...postStatus, name: action.postStatus.name, color: action.postStatus.color}; return {
...postStatus,
name: action.postStatus.name,
color: action.postStatus.color,
showInRoadmap: action.postStatus.show_in_roadmap,
};
}), }),
}; };

View File

@@ -63,15 +63,18 @@ import {
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer'; import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer'; import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
interface SiteSettingsState { interface SiteSettingsState {
boards: SiteSettingsBoardsState; boards: SiteSettingsBoardsState;
postStatuses: SiteSettingsPostStatusesState; postStatuses: SiteSettingsPostStatusesState;
roadmap: SiteSettingsRoadmapState;
} }
const initialState: SiteSettingsState = { const initialState: SiteSettingsState = {
boards: siteSettingsBoardsReducer(undefined, {} as any), boards: siteSettingsBoardsReducer(undefined, {} as any),
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any), postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
}; };
const siteSettingsReducer = ( const siteSettingsReducer = (
@@ -107,22 +110,28 @@ const siteSettingsReducer = (
...state, ...state,
boards: siteSettingsBoardsReducer(state.boards, action), boards: siteSettingsBoardsReducer(state.boards, action),
}; };
case POSTSTATUS_SUBMIT_START:
case POSTSTATUS_SUBMIT_SUCCESS:
case POSTSTATUS_SUBMIT_FAILURE:
case POSTSTATUS_ORDER_UPDATE_START: case POSTSTATUS_ORDER_UPDATE_START:
case POSTSTATUS_ORDER_UPDATE_SUCCESS: case POSTSTATUS_ORDER_UPDATE_SUCCESS:
case POSTSTATUS_ORDER_UPDATE_FAILURE: case POSTSTATUS_ORDER_UPDATE_FAILURE:
case POST_STATUS_DELETE_START: case POST_STATUS_DELETE_START:
case POST_STATUS_DELETE_SUCCESS: case POST_STATUS_DELETE_SUCCESS:
case POST_STATUS_DELETE_FAILURE: case POST_STATUS_DELETE_FAILURE:
case POSTSTATUS_SUBMIT_START: return {
case POSTSTATUS_SUBMIT_SUCCESS: ...state,
case POSTSTATUS_SUBMIT_FAILURE: postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
};
case POSTSTATUS_UPDATE_START: case POSTSTATUS_UPDATE_START:
case POSTSTATUS_UPDATE_SUCCESS: case POSTSTATUS_UPDATE_SUCCESS:
case POSTSTATUS_UPDATE_FAILURE: case POSTSTATUS_UPDATE_FAILURE:
return { return {
...state, ...state,
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action), postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
}; };
default: default:

View File

@@ -0,0 +1,52 @@
.roadmapPostStatuses {
@extend
.d-flex,
.flex-row,
.flex-wrap;
min-height: 150px;
box-sizing: border-box;
border: 2px dotted transparent;
border-radius: 0.5rem;
&.isDraggingSomething {
border-color: black;
}
&.isDraggingOver {
background-color: rgba(255, 255, 0, 0.2);
}
.roadmapPostStatus {
@extend
.card,
.m-2,
.p-0;
@include media-breakpoint-down(sm) {
flex: 0 0 100%;
}
box-sizing: border-box;
flex: 0 0 28%;
background-color: $astuto-light-grey;
overflow: hidden;
height: 150px;
&.headerOnly { height: fit-content; }
&.notDragging { transform: none !important; }
.roadmapPostStatusHeader {
@extend
.d-flex,
.flex-row,
.card-header;
color: white;
padding: 8px 4px;
.titleText { @extend .align-self-center; }
}
}
}

View File

@@ -15,6 +15,11 @@ span.drag-icon::before {
background-repeat: repeat-x; background-repeat: repeat-x;
} }
span.drag-icon.drag-icon-white,
span.drag-icon.drag-icon-white::before {
background-image: radial-gradient(white 40%, transparent 40%);
}
span.drag-icon::before { span.drag-icon::before {
content: ''; content: '';
display: block; display: block;

View File

@@ -19,6 +19,7 @@
@import 'components/SiteSettings'; @import 'components/SiteSettings';
@import 'components/SiteSettings/Boards'; @import 'components/SiteSettings/Boards';
@import 'components/SiteSettings/PostStatuses'; @import 'components/SiteSettings/PostStatuses';
@import 'components/SiteSettings/Roadmap';
/* Icons */ /* Icons */
@import 'icons/drag_icon'; @import 'icons/drag_icon';

View File

@@ -5,6 +5,7 @@
<div class="verticalNavigation" role="tablist" aria-orientation="vertical"> <div class="verticalNavigation" role="tablist" aria-orientation="vertical">
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %> <%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %> <%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,13 @@
<div class="twoColumnsContainer">
<%= render 'menu' %>
<div>
<%=
react_component(
'SiteSettings/Roadmap',
{
authenticityToken: form_authenticity_token
}
)
%>
</div>
</div>

View File

@@ -103,6 +103,7 @@ en:
title: 'Site settings' title: 'Site settings'
boards: 'Boards' boards: 'Boards'
post_statuses: 'Statuses' post_statuses: 'Statuses'
roadmap: 'Roadmap'
info_box: info_box:
up_to_date: 'All changes saved' up_to_date: 'All changes saved'
error: 'An error occurred: %{message}' error: 'An error occurred: %{message}'
@@ -119,6 +120,11 @@ en:
new: 'New' new: 'New'
form: form:
name: 'Status name' name: 'Status name'
roadmap:
title: 'Roadmap'
title2: 'Not in roadmap'
empty: 'The roadmap is empty.'
help: 'You can add new statuses to the roadmap by dragging them from the section below. If you want to add a new status or change their order, go to Site settings -> Statuses.'
user_mailer: user_mailer:
opening_greeting: 'Hello!' opening_greeting: 'Hello!'
closing_greeting: 'Have a great day!' closing_greeting: 'Have a great day!'

View File

@@ -103,6 +103,7 @@ it:
title: 'Impostazioni sito' title: 'Impostazioni sito'
boards: 'Bacheche' boards: 'Bacheche'
post_statuses: 'Stati' post_statuses: 'Stati'
roadmap: 'Roadmap'
info_box: info_box:
up_to_date: 'Tutte le modifiche sono state salvate' up_to_date: 'Tutte le modifiche sono state salvate'
error: 'Si è verificato un errore: %{message}' error: 'Si è verificato un errore: %{message}'
@@ -119,6 +120,11 @@ it:
new: 'Nuovo' new: 'Nuovo'
form: form:
name: 'Nome stato' name: 'Nome stato'
roadmap:
title: 'Roadmap'
title2: 'Non mostrati in roadmap'
empty: 'La roadmap è vuota.'
help: "Puoi aggiungere nuovi stati alla roadmap trascinandoli dalla sezione sottostante. Se vuoi aggiungere un nuovo stato o cambiarne l'ordine, vai in Impostazioni sito -> Stati."
user_mailer: user_mailer:
opening_greeting: 'Ciao!' opening_greeting: 'Ciao!'
closing_greeting: 'Buona giornata!' closing_greeting: 'Buona giornata!'

View File

@@ -35,5 +35,6 @@ Rails.application.routes.draw do
get 'general' get 'general'
get 'boards' get 'boards'
get 'post_statuses' get 'post_statuses'
get 'roadmap'
end end
end end