From db8a772fdeb2c0be5e783a766df9a685d79a64b3 Mon Sep 17 00:00:00 2001 From: sracca Date: Tue, 30 Jul 2019 14:39:43 -0400 Subject: [PATCH 1/4] chore: add user's name to comment --- server/app/graphql/types/comment_type.rb | 2 +- server/app/models/story.rb | 10 ++-- server/app/models/user.rb | 2 +- server/schema.json | 73 +++++++++++++++++++++++- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/server/app/graphql/types/comment_type.rb b/server/app/graphql/types/comment_type.rb index 6f18f1b..fc93ef0 100644 --- a/server/app/graphql/types/comment_type.rb +++ b/server/app/graphql/types/comment_type.rb @@ -6,7 +6,7 @@ class CommentType < Types::BaseObject field 'id', ID, null: false field 'text', String, null: false - field 'person_id', Integer, null: false + field 'person_name', String, null: false field 'created_at', UnixDateTimeType, null: false end end diff --git a/server/app/models/story.rb b/server/app/models/story.rb index ae75674..81c3c59 100644 --- a/server/app/models/story.rb +++ b/server/app/models/story.rb @@ -3,16 +3,18 @@ class Story < ApplicationRecord belongs_to :project - def self.filter_stories_to_review(pt_project:, db_project:, filter:) + def self.filter_stories_to_review(pt_project:, pt_members:, db_project:, filter:) + users = pt_members.map{ |membership| membership.person} pt_project.stories(filter: filter).map do |s| db_story = db_project.stories.find_or_create_by(id: s.id) - db_story.build_story(pt_story: s) + db_story.build_story(pt_story: s, project_users: users) end .compact end - def build_story(pt_story:) + def build_story(pt_story:, project_users:) comms = Story.sort_comments(pt_coms: pt_story.comments).map do |c| - { id: c.id, text: c.text, person_id: c.person_id, + c_name = project_users.select{|u| u.id == c.person_id}.first.name + { id: c.id, text: c.text, person_name: c_name, created_at: c.created_at } end .compact diff --git a/server/app/models/user.rb b/server/app/models/user.rb index 0c56d85..535be65 100644 --- a/server/app/models/user.rb +++ b/server/app/models/user.rb @@ -22,7 +22,7 @@ def list_user_project_stories(projects:, filter:) pr.name = p.name pr.save! self.projects << pr - st = Story.filter_stories_to_review(pt_project: p, db_project: pr, + st = Story.filter_stories_to_review(pt_project: p, pt_members: p.memberships, db_project: pr, filter: filter) { id: pr[:id], name: pr[:name], stories: st } unless st.empty? end .compact diff --git a/server/schema.json b/server/schema.json index a475762..f5db73c 100644 --- a/server/schema.json +++ b/server/schema.json @@ -362,6 +362,28 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "labels", + "description": null, + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "StoryLabel", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": null, @@ -541,7 +563,7 @@ "deprecationReason": null }, { - "name": "personId", + "name": "personName", "description": null, "args": [ @@ -614,6 +636,55 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "StoryLabel", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Error", From c6da07f45efc57fe33749c807b915dc2be0600a7 Mon Sep 17 00:00:00 2001 From: sracca Date: Fri, 2 Aug 2019 13:41:10 -0400 Subject: [PATCH 2/4] feat: load comments from database --- client/components/projects/comment.tsx | 2 +- client/components/projects/newComment.tsx | 2 +- client/components/projects/projects.tsx | 2 +- client/components/projects/types.ts | 2 +- client/components/story/story-comment.tsx | 137 ++++++++++++++++++++++ client/components/story/story.tsx | 18 +-- client/lib/datetime.ts | 3 + package.json | 1 + yarn.lock | 4 +- 9 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 client/components/story/story-comment.tsx create mode 100644 client/lib/datetime.ts diff --git a/client/components/projects/comment.tsx b/client/components/projects/comment.tsx index fad7d60..5da2eb7 100644 --- a/client/components/projects/comment.tsx +++ b/client/components/projects/comment.tsx @@ -11,7 +11,7 @@ const Comment = ({ data }: Props) => { <>
{data.text}

{data.createdAt}

-

person id: {data.personId}

+

person id: {data.personName}

); }; diff --git a/client/components/projects/newComment.tsx b/client/components/projects/newComment.tsx index 67eef06..1e1bcf0 100644 --- a/client/components/projects/newComment.tsx +++ b/client/components/projects/newComment.tsx @@ -8,7 +8,7 @@ const addComment = `mutation AddComment($storyId: String!, $text: String!) { comments { createdAt id - personId + personName text } } diff --git a/client/components/projects/projects.tsx b/client/components/projects/projects.tsx index ab72477..14eaf13 100644 --- a/client/components/projects/projects.tsx +++ b/client/components/projects/projects.tsx @@ -29,7 +29,7 @@ const fetchProjects = `query FetchProjects($filter: String!) { comments { id createdAt - personId + personName text } tasks { diff --git a/client/components/projects/types.ts b/client/components/projects/types.ts index 95164ce..c50ef09 100644 --- a/client/components/projects/types.ts +++ b/client/components/projects/types.ts @@ -18,7 +18,7 @@ export interface Story { export interface Comment { id: string; text: string; - personId: number; + personName: string; createdAt: Date; } diff --git a/client/components/story/story-comment.tsx b/client/components/story/story-comment.tsx new file mode 100644 index 0000000..ac8e2b0 --- /dev/null +++ b/client/components/story/story-comment.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Comment as CommentType } from '~components/projects/types'; +import { parseTimeAndDate } from '~lib/datetime'; +import { colors, fontSizes, spacing } from '~lib/theme'; + +const CommentTitle = styled.div` + color: #363333; + font-size: ${fontSizes.medium}; + margin-bottom: 5px; + margin-left: ${spacing.xxl}; + float: left; +`; + +const CommentBar = styled.rect` + fill: ${props => (props.commentsShown ? '#ffca41' : '#e1e1e1')}; + transition: fill 0.2s ease; +`; + +const ShowComments = styled.div` + display: flex; + flex-direction: column; + &:hover ${CommentBar} { + fill: #ffca41; + } +`; + +const CommentWrapper = styled.div` + width: 560px; + height: auto; + display: flex; + flex-direction: column; + margin-left: ${spacing.xxl}; +`; + +const ProfileWrapper = styled.div` + width: 100%; + height: auto; + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: ${spacing.l}; +`; + +const UserName = styled.div` + color: ${colors.warmGrey}; + opacity: 0.5; + font-size: ${fontSizes.medium}; +`; + +const UserProfile = styled.circle` + fill: #a1a4ad; + margin: 0 auto; +`; + +const CreatedAt = styled.div` + opacity: 0.5; + font-size: ${fontSizes.small}; + font-weight: normal; + line-height: 1.29; + color: ${colors.warmGrey}; + margin-left: auto; +`; + +const CommentText = styled.div` + font-size: ${fontSizes.medium}; + font-weight: normal; + font-style: normal; + line-height: 1.31; + color: #453d3f; + margin-bottom: 32px; +`; + +interface CommentDividerProps { + commentsShown: boolean; +} + +const CommentDivider = ({ commentsShown }: CommentDividerProps) => ( + + + +); + +const Divider = () => ( + + + +); + +const ProfileIcon = () => ( + + + +); + +interface CommentProp { + comment: CommentType; +} + +const Comment = ({ comment }: CommentProp) => { + const { id, text, personName, createdAt } = comment; + return ( + + + + {personName} + {parseTimeAndDate(createdAt)} + + {text} + + + ); +}; + +interface CommentsProps { + comments: CommentType[]; +} + +const Comments = ({ comments }: CommentsProps) => { + const [showComments, setShowComments] = useState(false); + + return ( + <> + setShowComments(!showComments)}> + {`Comments (${comments.length})`} + + + {showComments && comments.map(comment => )} + + ); +}; + +export default Comments; diff --git a/client/components/story/story.tsx b/client/components/story/story.tsx index 7125b2c..a3396c2 100644 --- a/client/components/story/story.tsx +++ b/client/components/story/story.tsx @@ -1,29 +1,16 @@ import React from 'react'; import styled from 'styled-components'; import { Story as StoryType } from '~components/projects/types'; +import Comments from '~components/story/story-comment'; import Description from '~components/story/story-description'; import Details from '~components/story/story-details'; import Tasks from '~components/story/story-tasks'; -import { colors, fontSizes, spacing } from '~lib/theme'; const StoryWrapper = styled.div` padding-top: 138px; padding-left: 138px; `; -const CommentTitle = styled.div` - color: #363333; - font-size: ${fontSizes.medium}; - margin-bottom: 5px; - margin-left: ${spacing.xxl}; -`; - -export const CommentDivider = () => ( - - - -); - interface StoryProps { data: StoryType; } @@ -37,8 +24,7 @@ const Story = ({
- {`Comments (${comments.length})`} - + ); diff --git a/client/lib/datetime.ts b/client/lib/datetime.ts new file mode 100644 index 0000000..955a567 --- /dev/null +++ b/client/lib/datetime.ts @@ -0,0 +1,3 @@ +import { format } from 'date-fns'; + +export const parseTimeAndDate = datetime => format(new Date(datetime), 'MMMM D, h:mm a'); diff --git a/package.json b/package.json index 4619287..7f9c6d2 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/react-redux": "^7.1.1", "@types/react-router-dom": "^4.3.4", "@types/styled-components": "^4.1.18", + "date-fns": "^1.30.1", "graphql": "^14.3.1", "jwt-decode": "^2.2.0", "react": "16.8.6", diff --git a/yarn.lock b/yarn.lock index c7fd33f..c33fa81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4716,7 +4716,7 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@^1.23.0: +date-fns@^1.23.0, date-fns@^1.30.1: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== @@ -9970,7 +9970,7 @@ react-transform-hmr@^1.0.4: global "^4.3.0" react-proxy "^1.1.7" -react@16.8.6, react@^16.8.3: +"react@>= 16.3.0", react@^16.8.3: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== From f3a5674d69f75ecdc53877d9a118d86d8bdcb13a Mon Sep 17 00:00:00 2001 From: sracca Date: Fri, 2 Aug 2019 13:44:04 -0400 Subject: [PATCH 3/4] feat: add a comment to a story --- client/components/projects/newComment.tsx | 13 +- client/components/projects/project.tsx | 12 +- client/components/projects/projects.tsx | 6 +- client/components/projects/types.ts | 2 +- client/components/story-panel.tsx | 5 +- client/components/story/new-comment.tsx | 113 ++++++++++++++++++ client/components/story/story-comment.tsx | 14 ++- client/components/story/story.tsx | 4 +- .../graphql/mutations/create_story_comment.rb | 5 +- server/app/models/story.rb | 14 ++- server/schema.json | 26 ++-- 11 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 client/components/story/new-comment.tsx diff --git a/client/components/projects/newComment.tsx b/client/components/projects/newComment.tsx index 1e1bcf0..df55335 100644 --- a/client/components/projects/newComment.tsx +++ b/client/components/projects/newComment.tsx @@ -2,8 +2,8 @@ import React, { useRef } from 'react'; import { useStore } from 'react-redux'; import { useMutation } from 'urql'; -const addComment = `mutation AddComment($storyId: String!, $text: String!) { - createComment(storyId: $storyId, text: $text) { +const addComment = `mutation AddComment($storyId: String!, $projectId: String! $text: String!) { + createComment(storyId: $storyId, projectId: $projectId, text: $text) { ... on Story { comments { createdAt @@ -22,10 +22,11 @@ const addComment = `mutation AddComment($storyId: String!, $text: String!) { }`; interface Props { + projectId: string; storyId: string; } -const NewComment = ({ storyId }: Props) => { +const NewComment = ({ storyId, projectId }: Props) => { const state = useStore().getState(); const commentRef = useRef(null); @@ -34,7 +35,11 @@ const NewComment = ({ storyId }: Props) => { const onSubmit = event => { event.preventDefault(); - executeMutation({ storyId, text: commentRef.current.value }); + executeMutation({ + storyId, + projectId, + text: commentRef.current.value, + }); commentRef.current.value = ''; }; diff --git a/client/components/projects/project.tsx b/client/components/projects/project.tsx index 22d102d..e0ae364 100644 --- a/client/components/projects/project.tsx +++ b/client/components/projects/project.tsx @@ -4,10 +4,18 @@ import { ReduxState } from '~redux/reducers'; import StoryPanel from '~components/story-panel'; -const Project = () => { +interface ProjectProps { + projectId: string; +} + +const Project = ({ projectId }: ProjectProps) => { const currentStory = useSelector((state: ReduxState) => state.story); - return currentStory.storyPosition != null ? : <>; + return currentStory.storyPosition != null ? ( + + ) : ( + <> + ); }; export default Project; diff --git a/client/components/projects/projects.tsx b/client/components/projects/projects.tsx index 14eaf13..6289f57 100644 --- a/client/components/projects/projects.tsx +++ b/client/components/projects/projects.tsx @@ -69,7 +69,11 @@ const Projects = ({ history }: Props) => { <> - {res.data.projects.all.length > 0 ? : } + {res.data.projects.all.length > 0 ? ( + + ) : ( + + )} ); }; diff --git a/client/components/projects/types.ts b/client/components/projects/types.ts index c50ef09..94b7d9e 100644 --- a/client/components/projects/types.ts +++ b/client/components/projects/types.ts @@ -1,5 +1,5 @@ export interface Project { - id: number; + id: string; name: string; stories: Story[]; } diff --git a/client/components/story-panel.tsx b/client/components/story-panel.tsx index ff7fae7..f08974d 100644 --- a/client/components/story-panel.tsx +++ b/client/components/story-panel.tsx @@ -15,12 +15,13 @@ const PanelWrapper = styled.div` `; interface PanelProps { + projectId: string; data: StoryType; } -const StoryPanel = ({ data }: PanelProps) => ( +const StoryPanel = ({ projectId, data }: PanelProps) => ( - + ); diff --git a/client/components/story/new-comment.tsx b/client/components/story/new-comment.tsx new file mode 100644 index 0000000..b2c1bca --- /dev/null +++ b/client/components/story/new-comment.tsx @@ -0,0 +1,113 @@ +import React, { useRef } from 'react'; +import { useStore } from 'react-redux'; +import styled from 'styled-components'; +import { useMutation } from 'urql'; +import RightArrow from '~assets/images/right-arrow-full.svg'; +import { colors, fontSizes, spacing } from '~lib/theme'; + +const addComment = `mutation AddComment($storyId: String!, $projectId: String! $text: String!) { + createComment(storyId: $storyId, projectId: $projectId, text: $text) { + ... on Story { + comments { + createdAt + id + personName + text + } + } + ... on Error { + code + error + kind + possibleFix + } + } +}`; + +const CommentForm = styled.form` + margin-left: ${spacing.xxl}; + margin-bottom: 38px; + width: 560px; +`; + +const FormSubmit = styled.button` + outline: none; + width: 60px; + height: 60px; + border: solid 1px #d3d2d2; + display: block; + :hover { + background-color: #ffca41; + } +`; + +const InputWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; +`; + +const FormText = styled.input` + outline: none; + font-size: ${fontSizes.medium} + opacity: 0.5; + font-weight: normal; + line-height: 1.31; + color: ${colors.warmGrey}; + width: 500px; + height: 60px; + border: solid 1px #d3d2d2; + padding-left: ${spacing.m} + &:focus { + opacity: 1; + &::-webkit-input-placeholder { + opacity: 0; + } + } + &:focus ~ ${FormSubmit} { + background-color: #ffca41; + } +`; + +const SubmitIcon = styled(RightArrow)` + height: 18px; + width: 18px; +`; + +interface Props { + projectId: string; + storyId: string; +} + +const NewComment = ({ storyId, projectId }: Props) => { + const state = useStore().getState(); + + const commentRef = useRef(null); + + const [, executeMutation] = useMutation(addComment); + + const onSubmit = event => { + event.preventDefault(); + executeMutation({ + storyId, + projectId, + text: commentRef.current.value, + }); + commentRef.current.value = ''; + }; + + return ( + + + + + + + ); +}; + +export default NewComment; diff --git a/client/components/story/story-comment.tsx b/client/components/story/story-comment.tsx index ac8e2b0..5639a44 100644 --- a/client/components/story/story-comment.tsx +++ b/client/components/story/story-comment.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { Comment as CommentType } from '~components/projects/types'; +import NewComment from '~components/story/new-comment'; import { parseTimeAndDate } from '~lib/datetime'; import { colors, fontSizes, spacing } from '~lib/theme'; @@ -117,10 +118,12 @@ const Comment = ({ comment }: CommentProp) => { }; interface CommentsProps { + storyId: string; + projectId: string; comments: CommentType[]; } -const Comments = ({ comments }: CommentsProps) => { +const Comments = ({ storyId, projectId, comments }: CommentsProps) => { const [showComments, setShowComments] = useState(false); return ( @@ -129,7 +132,14 @@ const Comments = ({ comments }: CommentsProps) => { {`Comments (${comments.length})`} - {showComments && comments.map(comment => )} + {showComments && ( + <> + + {comments.map(comment => ( + + ))} + + )} ); }; diff --git a/client/components/story/story.tsx b/client/components/story/story.tsx index a3396c2..3632742 100644 --- a/client/components/story/story.tsx +++ b/client/components/story/story.tsx @@ -12,10 +12,12 @@ const StoryWrapper = styled.div` `; interface StoryProps { + projectId: string; data: StoryType; } const Story = ({ + projectId, data: { id, name, description, storyType, comments, tasks, labels }, }: StoryProps) => { return ( @@ -24,7 +26,7 @@ const Story = ({
- + ); diff --git a/server/app/graphql/mutations/create_story_comment.rb b/server/app/graphql/mutations/create_story_comment.rb index dd80422..87acb64 100644 --- a/server/app/graphql/mutations/create_story_comment.rb +++ b/server/app/graphql/mutations/create_story_comment.rb @@ -3,13 +3,14 @@ module Mutations class CreateStoryComment < BaseMutation argument :story_id, String, required: true + argument :project_id, String, required: true argument :text, String, required: true type Types::StoryResponse - def resolve(story_id:, text:) + def resolve(story_id:, project_id:, text:) token = User.find_by(email: context[:current_user][:email])[:api_token] - Story.add_comment(token: token, story_id: story_id, text: text) + Story.add_comment(token: token, story_id: story_id, text: text, project_id: project_id) rescue TrackerApi::Errors::ClientError, TrackerApi::Errors::ServerError => e e.response.with_indifferent_access[:body] end diff --git a/server/app/models/story.rb b/server/app/models/story.rb index 81c3c59..a971502 100644 --- a/server/app/models/story.rb +++ b/server/app/models/story.rb @@ -35,14 +35,18 @@ def self.sort_comments(pt_coms:) pt_coms.sort_by { |c| c.created_at } .reverse end - def self.build_comment(com:) - { id: com.id, text: com.text, person_id: com.person_id, + def self.build_comment(com:, project_users:) + c_name = project_users.select{|u| u.id == com.person_id}.first.name + { id: com.id, text: com.text, person_name: c_name, created_at: com.created_at } end - def self.add_comment(token:, story_id:, text:) + def self.add_comment(token:, story_id:, text:, project_id:) client = TrackerApi::Client.new(token: token) - build_comment(com: client.story(story_id).create_comment(text: text)) - client.story(story_id) + curr_proj = client.projects.select{ |p| p.id.to_s == project_id}.first + users = curr_proj.memberships.map{ |membership| membership.person} + build_comment(com: client.story(story_id).create_comment(text: text), project_users: users) + curr_story = Story.find(story_id) + curr_story.build_story(pt_story: client.story(story_id), project_users: users) end end diff --git a/server/schema.json b/server/schema.json index f5db73c..336705a 100644 --- a/server/schema.json +++ b/server/schema.json @@ -573,7 +573,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null } }, @@ -606,16 +606,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "Int", - "description": "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "SCALAR", "name": "UnixDateTime", @@ -793,6 +783,20 @@ }, "defaultValue": null }, + { + "name": "projectId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, { "name": "text", "description": null, From b56e8f5ab20025c817afaa1d3bf7676b7d672c67 Mon Sep 17 00:00:00 2001 From: Simon Barber Date: Mon, 12 Aug 2019 14:04:20 -0400 Subject: [PATCH 4/4] delete unused comment file --- client/components/projects/comment.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 client/components/projects/comment.tsx diff --git a/client/components/projects/comment.tsx b/client/components/projects/comment.tsx deleted file mode 100644 index 5da2eb7..0000000 --- a/client/components/projects/comment.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import { Comment as CommentType } from './types'; - -interface Props { - data: CommentType; -} - -const Comment = ({ data }: Props) => { - return ( - <> -
{data.text}
-

{data.createdAt}

-

person id: {data.personName}

- - ); -}; - -export default Comment;