diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 16258aa7..87c02c73 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,9 +1,9 @@ class PostsController < ApplicationController - before_action :authenticate_user!, only: [:create] + before_action :authenticate_user!, only: [:create, :update] def index posts = Post - .select(:title, :description, :post_status_id) + .select(:id, :title, :description, :post_status_id) .where(filter_params) .search_by_name_or_description(params[:search]) .page(params[:page]) @@ -23,6 +23,37 @@ class PostsController < ApplicationController end end + def show + @post = Post.find(params[:id]) + @post_statuses = PostStatus + .find_roadmap + .select(:id, :name, :color) + + respond_to do |format| + format.html + + format.json { render json: @post } + end + end + + def update + post = Post.find(params[:id]) + + if current_user.role == :user && current_user.id != post.user_id + render json: I18n.t('errors.unauthorized'), status: :unauthorized + end + + post.post_status_id = params[:post][:post_status_id] + + if post.save + render json: post, status: :no_content + else + render json: { + error: I18n.t('errors.post.update', message: post.errors.full_messages) + }, status: :unprocessable_entity + end + end + private def filter_params diff --git a/app/javascript/actions/changePostStatus.ts b/app/javascript/actions/changePostStatus.ts new file mode 100644 index 00000000..fdeb095c --- /dev/null +++ b/app/javascript/actions/changePostStatus.ts @@ -0,0 +1,42 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; +import { State } from '../reducers/rootReducer'; + +export const CHANGE_POST_STATUS_SUCCESS = 'CHANGE_POST_STATUS_SUCCESS'; +interface ChangePostStatusSuccessAction { + type: typeof CHANGE_POST_STATUS_SUCCESS; + newPostStatusId; +} + +const changePostStatusSuccess = (newPostStatusId: number): ChangePostStatusSuccessAction => ({ + type: CHANGE_POST_STATUS_SUCCESS, + newPostStatusId, +}); + +export const changePostStatus = ( + postId: number, + newPostStatusId: number, + authenticityToken: string, +): ThunkAction> => async (dispatch) => { + try { + const response = await fetch(`/posts/${postId}`, { + method: 'PATCH', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-Token': authenticityToken, + }, + body: JSON.stringify({ + post: { + post_status_id: newPostStatusId, + }, + }) + }); + + if (response.status === 204) { + dispatch(changePostStatusSuccess(newPostStatusId)); + } + } catch (e) { + console.log(e); + } +} \ No newline at end of file diff --git a/app/javascript/actions/requestPost.ts b/app/javascript/actions/requestPost.ts new file mode 100644 index 00000000..1c00a5de --- /dev/null +++ b/app/javascript/actions/requestPost.ts @@ -0,0 +1,57 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import IPostJSON from '../interfaces/json/IPost'; + +import { State } from '../reducers/rootReducer'; + +export const POST_REQUEST_START = 'POST_REQUEST_START'; +interface PostRequestStartAction { + type: typeof POST_REQUEST_START; +} + +export const POST_REQUEST_SUCCESS = 'POST_REQUEST_SUCCESS'; +interface PostRequestSuccessAction { + type: typeof POST_REQUEST_SUCCESS; + post: IPostJSON; +} + +export const POST_REQUEST_FAILURE = 'POST_REQUEST_FAILURE'; +interface PostRequestFailureAction { + type: typeof POST_REQUEST_FAILURE; + error: string; +} + +export type PostRequestActionTypes = + PostRequestStartAction | + PostRequestSuccessAction | + PostRequestFailureAction; + + +const postRequestStart = (): PostRequestActionTypes => ({ + type: POST_REQUEST_START, +}); + +export const postRequestSuccess = (post: IPostJSON): PostRequestActionTypes => ({ + type: POST_REQUEST_SUCCESS, + post, +}); + +const postRequestFailure = (error: string): PostRequestActionTypes => ({ + type: POST_REQUEST_FAILURE, + error, +}); + +export const requestPost = ( + postId: number, +): ThunkAction> => async (dispatch) => { + dispatch(postRequestStart()); + + try { + const response = await fetch(`/posts/${postId}.json`); + const json = await response.json(); + dispatch(postRequestSuccess(json)); + } catch (e) { + dispatch(postRequestFailure(e)); + } +} \ No newline at end of file diff --git a/app/javascript/components/Board/PostList.tsx b/app/javascript/components/Board/PostList.tsx index d07b0d0f..694c9f73 100644 --- a/app/javascript/components/Board/PostList.tsx +++ b/app/javascript/components/Board/PostList.tsx @@ -42,6 +42,7 @@ const PostList = ({ posts.length > 0 ? posts.map((post, i) => ( postStatus.id === post.postStatusId)} diff --git a/app/javascript/components/Board/PostListItem.tsx b/app/javascript/components/Board/PostListItem.tsx index 282b5c82..5abc6103 100644 --- a/app/javascript/components/Board/PostListItem.tsx +++ b/app/javascript/components/Board/PostListItem.tsx @@ -3,13 +3,14 @@ import * as React from 'react'; import IPostStatus from '../../interfaces/IPostStatus'; interface Props { + id: number; title: string; description?: string; postStatus: IPostStatus; } -const PostListItem = ({ title, description, postStatus}: Props) => ( - +const PostListItem = ({ id, title, description, postStatus}: Props) => ( +
{title}
diff --git a/app/javascript/components/Post/PostP.tsx b/app/javascript/components/Post/PostP.tsx new file mode 100644 index 00000000..3df02d54 --- /dev/null +++ b/app/javascript/components/Post/PostP.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; + +import IPost from '../../interfaces/IPost'; +import IPostStatus from '../../interfaces/IPostStatus'; + +import PostStatusSelect from './PostStatusSelect'; + +interface Props { + postId: number; + post: IPost; + postStatuses: Array; + isLoggedIn: boolean; + isPowerUser: boolean; + authenticityToken: string; + + requestPost(postId: number): void; + changePostStatus( + postId: number, + newPostStatusId: number, + authenticityToken: string, + ): void; +} + +class PostP extends React.Component { + componentDidMount() { + this.props.requestPost(this.props.postId); + } + + render() { + const { + post, + postStatuses, + + isPowerUser, + authenticityToken, + + changePostStatus, + } = this.props; + + return ( +
+

{post.title}

+ { + isPowerUser ? + changePostStatus(post.id, newPostStatusId, authenticityToken) + } + /> + : + LLL + } + +

{post.description}

+
+ ); + } +} + +export default PostP; \ No newline at end of file diff --git a/app/javascript/components/Post/PostStatusSelect.tsx b/app/javascript/components/Post/PostStatusSelect.tsx new file mode 100644 index 00000000..9270cf50 --- /dev/null +++ b/app/javascript/components/Post/PostStatusSelect.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { FormEvent } from 'react'; + +import IPostStatus from '../../interfaces/IPostStatus'; + +interface Props { + postStatuses: Array; + selectedPostStatusId: number; + + handleChange( + newPostStatusId: number, + ): void; +} + +const PostStatusSelect = ({ + postStatuses, + selectedPostStatusId, + handleChange, +}: Props) => ( + +); + +export default PostStatusSelect; \ No newline at end of file diff --git a/app/javascript/components/Post/index.tsx b/app/javascript/components/Post/index.tsx new file mode 100644 index 00000000..a8c45c4e --- /dev/null +++ b/app/javascript/components/Post/index.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Provider } from 'react-redux'; + +import store from '../../stores'; + +import Post from '../../containers/Post'; + +import '../../stylesheets/components/Post.scss'; + +const PostRoot = ({ postId, postStatuses, isLoggedIn, isPowerUser, authenticityToken }) => ( + + + +); + +export default PostRoot; \ No newline at end of file diff --git a/app/javascript/components/Roadmap/PostList.tsx b/app/javascript/components/Roadmap/PostList.tsx index a7a8ac50..752a4a7b 100644 --- a/app/javascript/components/Roadmap/PostList.tsx +++ b/app/javascript/components/Roadmap/PostList.tsx @@ -16,6 +16,7 @@ const PostList = ({ posts, boards }: Props) => ( posts.length > 0 ? posts.map((post, i) => ( board.id === post.board_id).name} diff --git a/app/javascript/components/Roadmap/PostListItem.tsx b/app/javascript/components/Roadmap/PostListItem.tsx index fd2a6de8..228ea50e 100644 --- a/app/javascript/components/Roadmap/PostListItem.tsx +++ b/app/javascript/components/Roadmap/PostListItem.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; interface Props { + id: number; title: string; boardName: string; } -const PostListItem = ({title, boardName}: Props) => ( -
+const PostListItem = ({id, title, boardName}: Props) => ( +
{title}
{boardName}
diff --git a/app/javascript/containers/Post.tsx b/app/javascript/containers/Post.tsx new file mode 100644 index 00000000..3c1e6028 --- /dev/null +++ b/app/javascript/containers/Post.tsx @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; + +import { requestPost } from '../actions/requestPost'; +import { changePostStatus } from '../actions/changePostStatus'; + +import { State } from '../reducers/rootReducer'; + +import PostP from '../components/Post/PostP'; + +const mapStateToProps = (state: State) => ({ + post: state.currentPost, +}); + +const mapDispatchToProps = (dispatch) => ({ + requestPost(postId: number) { + dispatch(requestPost(postId)); + }, + + changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) { + dispatch(changePostStatus(postId, newPostStatusId, authenticityToken)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(PostP); \ No newline at end of file diff --git a/app/javascript/reducers/postReducer.ts b/app/javascript/reducers/postReducer.ts index f7c80cc6..bf8b2198 100644 --- a/app/javascript/reducers/postReducer.ts +++ b/app/javascript/reducers/postReducer.ts @@ -1,3 +1,13 @@ +import { + POST_REQUEST_START, + POST_REQUEST_SUCCESS, + POST_REQUEST_FAILURE, +} from '../actions/requestPost'; + +import { + CHANGE_POST_STATUS_SUCCESS, +} from '../actions/changePostStatus'; + import IPost from '../interfaces/IPost'; const initialState: IPost = { @@ -15,7 +25,7 @@ const postReducer = ( action, ): IPost => { switch (action.type) { - case 'CONVERT': + case POST_REQUEST_SUCCESS: return { id: action.post.id, title: action.post.title, @@ -26,6 +36,14 @@ const postReducer = ( createdAt: action.post.created_at, }; + case CHANGE_POST_STATUS_SUCCESS: + return { + ...state, + postStatusId: action.newPostStatusId, + }; + + case POST_REQUEST_START: + case POST_REQUEST_FAILURE: default: return state; } diff --git a/app/javascript/reducers/postsReducer.ts b/app/javascript/reducers/postsReducer.ts index 9d1b1182..193733d9 100644 --- a/app/javascript/reducers/postsReducer.ts +++ b/app/javascript/reducers/postsReducer.ts @@ -12,6 +12,8 @@ import { POSTS_REQUEST_FAILURE, } from '../actions/requestPosts'; +import { postRequestSuccess } from '../actions/requestPost'; + import { ChangeFiltersActionTypes, SET_SEARCH_FILTER, @@ -54,9 +56,9 @@ const postsReducer = ( return { ...state, items: action.page === 1 ? - action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post})) //improve + action.posts.map(post => postReducer(undefined, postRequestSuccess(post))) : - [...state.items, ...action.posts.map(post => postReducer(undefined, {type: 'CONVERT', post}))], + [...state.items, ...action.posts.map(post => postReducer(undefined, postRequestSuccess(post)))], page: action.page, haveMore: action.posts.length === 15, areLoading: false, diff --git a/app/javascript/reducers/rootReducer.ts b/app/javascript/reducers/rootReducer.ts index fb615a2e..066f40c9 100644 --- a/app/javascript/reducers/rootReducer.ts +++ b/app/javascript/reducers/rootReducer.ts @@ -2,10 +2,12 @@ import { combineReducers } from 'redux'; import postsReducer from './postsReducer'; import postStatusesReducer from './postStatusesReducer'; +import postReducer from './postReducer'; const rootReducer = combineReducers({ posts: postsReducer, postStatuses: postStatusesReducer, + currentPost: postReducer, }); export type State = ReturnType diff --git a/app/javascript/stylesheets/components/Post.scss b/app/javascript/stylesheets/components/Post.scss new file mode 100644 index 00000000..e69de29b diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb new file mode 100644 index 00000000..6c52f6ad --- /dev/null +++ b/app/views/posts/show.html.erb @@ -0,0 +1,12 @@ +<%= + react_component( + 'Post', + { + postId: @post.id, + postStatuses: @post_statuses, + isLoggedIn: user_signed_in?, + isPowerUser: user_signed_in? ? (current_user.role == 'admin' || current_user.role == 'moderator') : false, + authenticityToken: form_authenticity_token, + } + ) +%> \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 975165c7..12a1b3e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,4 +1,6 @@ en: errors: + unauthorized: 'You are not authorized' post: - create: 'Post create error: %{message}' \ No newline at end of file + create: 'Post create error: %{message}' + update: 'Post update error: %{message}' \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index b584e64a..d32ea5c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,6 @@ Rails.application.routes.draw do devise_for :users resources :boards, only: [:show] - resources :posts, only: [:index, :create] + resources :posts, only: [:index, :create, :show, :update] resources :post_statuses, only: [:index] end