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
@@ -75,6 +75,6 @@ class PostStatusesController < ApplicationController
|
||||
def post_status_params
|
||||
params
|
||||
.require(:post_status)
|
||||
.permit(:name, :color)
|
||||
.permit(:name, :color, :show_in_roadmap)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,4 +11,7 @@ class SiteSettingsController < ApplicationController
|
||||
|
||||
def post_statuses
|
||||
end
|
||||
|
||||
def roadmap
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Action } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import IPostStatus from '../../interfaces/IPostStatus';
|
||||
import IPostStatusJSON from '../../interfaces/json/IPostStatus';
|
||||
|
||||
import { State } from '../../reducers/rootReducer';
|
||||
|
||||
@@ -13,7 +13,7 @@ interface PostStatusesRequestStartAction {
|
||||
export const POST_STATUSES_REQUEST_SUCCESS = 'POST_STATUSES_REQUEST_SUCCESS';
|
||||
interface PostStatusesRequestSuccessAction {
|
||||
type: typeof POST_STATUSES_REQUEST_SUCCESS;
|
||||
postStatuses: Array<IPostStatus>;
|
||||
postStatuses: Array<IPostStatusJSON>;
|
||||
}
|
||||
|
||||
export const POST_STATUSES_REQUEST_FAILURE = 'POST_STATUSES_REQUEST_FAILURE';
|
||||
@@ -33,7 +33,7 @@ const postStatusesRequestStart = (): PostStatusesRequestActionTypes => ({
|
||||
});
|
||||
|
||||
const postStatusesRequestSuccess = (
|
||||
postStatuses: Array<IPostStatus>
|
||||
postStatuses: Array<IPostStatusJSON>
|
||||
): PostStatusesRequestActionTypes => ({
|
||||
type: POST_STATUSES_REQUEST_SUCCESS,
|
||||
postStatuses,
|
||||
|
||||
@@ -43,24 +43,34 @@ const postStatusUpdateFailure = (error: string): PostStatusUpdateFailureAction =
|
||||
error,
|
||||
});
|
||||
|
||||
export const updatePostStatus = (
|
||||
id: number,
|
||||
name: string,
|
||||
color: string,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
interface UpdatePostStatusParams {
|
||||
id: number;
|
||||
name?: string;
|
||||
color?: string;
|
||||
showInRoadmap?: boolean;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
export const updatePostStatus = ({
|
||||
id,
|
||||
name = null,
|
||||
color = null,
|
||||
showInRoadmap = null,
|
||||
authenticityToken,
|
||||
}: UpdatePostStatusParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(postStatusUpdateStart());
|
||||
|
||||
const post_status = Object.assign({},
|
||||
name !== null ? {name} : null,
|
||||
color !== null ? {color} : null,
|
||||
showInRoadmap !== null ? {show_in_roadmap: showInRoadmap} : null
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/post_statuses/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: buildRequestHeaders(authenticityToken),
|
||||
body: JSON.stringify({
|
||||
post_status: {
|
||||
name,
|
||||
color,
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify({post_status}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
onSuccess: Function,
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
30
app/javascript/containers/RoadmapSiteSettings.tsx
Normal file
30
app/javascript/containers/RoadmapSiteSettings.tsx
Normal 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);
|
||||
@@ -2,6 +2,8 @@ interface IPostStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
order: number;
|
||||
showInRoadmap: boolean;
|
||||
}
|
||||
|
||||
export default IPostStatus;
|
||||
@@ -2,6 +2,8 @@ interface IPostStatusJSON {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
order: number;
|
||||
show_in_roadmap: boolean;
|
||||
}
|
||||
|
||||
export default IPostStatusJSON;
|
||||
48
app/javascript/reducers/SiteSettings/roadmapReducer.ts
Normal file
48
app/javascript/reducers/SiteSettings/roadmapReducer.ts
Normal 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;
|
||||
@@ -62,6 +62,8 @@ const postStatusesReducer = (
|
||||
id: postStatus.id,
|
||||
name: postStatus.name,
|
||||
color: postStatus.color,
|
||||
order: postStatus.order,
|
||||
showInRoadmap: postStatus.show_in_roadmap,
|
||||
})),
|
||||
areLoading: false,
|
||||
error: '',
|
||||
@@ -85,7 +87,12 @@ const postStatusesReducer = (
|
||||
...state,
|
||||
items: state.items.map(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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -63,15 +63,18 @@ import {
|
||||
|
||||
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
||||
|
||||
interface SiteSettingsState {
|
||||
boards: SiteSettingsBoardsState;
|
||||
postStatuses: SiteSettingsPostStatusesState;
|
||||
roadmap: SiteSettingsRoadmapState;
|
||||
}
|
||||
|
||||
const initialState: SiteSettingsState = {
|
||||
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
||||
};
|
||||
|
||||
const siteSettingsReducer = (
|
||||
@@ -107,22 +110,28 @@ const siteSettingsReducer = (
|
||||
...state,
|
||||
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_SUCCESS:
|
||||
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
||||
case POST_STATUS_DELETE_START:
|
||||
case POST_STATUS_DELETE_SUCCESS:
|
||||
case POST_STATUS_DELETE_FAILURE:
|
||||
case POSTSTATUS_SUBMIT_START:
|
||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||
case POSTSTATUS_SUBMIT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
|
||||
};
|
||||
|
||||
case POSTSTATUS_UPDATE_START:
|
||||
case POSTSTATUS_UPDATE_SUCCESS:
|
||||
case POSTSTATUS_UPDATE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
postStatuses: siteSettingsPostStatusesReducer(state.postStatuses, action),
|
||||
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ span.drag-icon::before {
|
||||
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 {
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@import 'components/SiteSettings';
|
||||
@import 'components/SiteSettings/Boards';
|
||||
@import 'components/SiteSettings/PostStatuses';
|
||||
@import 'components/SiteSettings/Roadmap';
|
||||
|
||||
/* Icons */
|
||||
@import 'icons/drag_icon';
|
||||
@@ -5,6 +5,7 @@
|
||||
<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.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>
|
||||
13
app/views/site_settings/roadmap.html.erb
Normal file
13
app/views/site_settings/roadmap.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="twoColumnsContainer">
|
||||
<%= render 'menu' %>
|
||||
<div>
|
||||
<%=
|
||||
react_component(
|
||||
'SiteSettings/Roadmap',
|
||||
{
|
||||
authenticityToken: form_authenticity_token
|
||||
}
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,6 +103,7 @@ en:
|
||||
title: 'Site settings'
|
||||
boards: 'Boards'
|
||||
post_statuses: 'Statuses'
|
||||
roadmap: 'Roadmap'
|
||||
info_box:
|
||||
up_to_date: 'All changes saved'
|
||||
error: 'An error occurred: %{message}'
|
||||
@@ -119,6 +120,11 @@ en:
|
||||
new: 'New'
|
||||
form:
|
||||
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:
|
||||
opening_greeting: 'Hello!'
|
||||
closing_greeting: 'Have a great day!'
|
||||
|
||||
@@ -103,6 +103,7 @@ it:
|
||||
title: 'Impostazioni sito'
|
||||
boards: 'Bacheche'
|
||||
post_statuses: 'Stati'
|
||||
roadmap: 'Roadmap'
|
||||
info_box:
|
||||
up_to_date: 'Tutte le modifiche sono state salvate'
|
||||
error: 'Si è verificato un errore: %{message}'
|
||||
@@ -119,6 +120,11 @@ it:
|
||||
new: 'Nuovo'
|
||||
form:
|
||||
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:
|
||||
opening_greeting: 'Ciao!'
|
||||
closing_greeting: 'Buona giornata!'
|
||||
|
||||
@@ -35,5 +35,6 @@ Rails.application.routes.draw do
|
||||
get 'general'
|
||||
get 'boards'
|
||||
get 'post_statuses'
|
||||
get 'roadmap'
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user