diff --git a/src/components/FeedPortal/components/GithubActivity.jsx b/src/components/FeedPortal/components/GithubActivity.jsx index 380fac3c..bd81a841 100644 --- a/src/components/FeedPortal/components/GithubActivity.jsx +++ b/src/components/FeedPortal/components/GithubActivity.jsx @@ -19,7 +19,8 @@ const IssueIcon = () => { export const GithubActivityPullRequest = ( data ) => { return ( - +
{data.title}
@@ -30,7 +31,8 @@ export const GithubActivityPullRequest = ( data ) => { } export const GithubActivityIssue = ( data ) => { return ( -
+
{data.title}
diff --git a/src/components/FeedPortal/components/NewsFeed.jsx b/src/components/FeedPortal/components/NewsFeed.jsx index 6ae0494a..22ed6682 100644 --- a/src/components/FeedPortal/components/NewsFeed.jsx +++ b/src/components/FeedPortal/components/NewsFeed.jsx @@ -14,28 +14,28 @@ const NewsFeed = ({ type, loading, data }) => { ${project ? `${project.team_name.toUpperCase()}` : "ALL"} NEWS `; - const renderNewsfeedItems = items => items.map( item => { return FeedItemContainer({ component: NewsfeedItems[item.type], item, key: item.id, - }) + }); } ); - const renderStandups = standups => standups.map( - standup => ( - standup.submitted_at - ? FeedItemContainer({ - item: { standup, type: "NewsfeedStandup", user: standup.member, timestamp: standup.submitted_at }, - key: standup.id, - component: NewsfeedItems.NewsfeedStandup, - }) - : null - ), - ); + const renderStandups = standups => { + return ( + FeedItemContainer({ + item: { + standups, type: "NewsfeedStandup", + user: standups[standups.length-1].member, + timestamp: standups[standups.length-1].submitted_at }, + key: standups[standups.length-1].id, + component: NewsfeedItems.NewsfeedStandup, + }) + ); + }; const renderFeed = ({ user, project }) => { let dataToRender = ( @@ -91,6 +91,8 @@ const getNewsfeed = (project_id) => { worked_on working_on blocked_on + is_expired + expiration member { id username diff --git a/src/components/FeedPortal/components/NewsfeedStandup.jsx b/src/components/FeedPortal/components/NewsfeedStandup.jsx index 799ff645..e3b3d3ea 100644 --- a/src/components/FeedPortal/components/NewsfeedStandup.jsx +++ b/src/components/FeedPortal/components/NewsfeedStandup.jsx @@ -1,57 +1,61 @@ -import * as React from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; + import "./NewsfeedStandup.css"; +import FeedItemContainer from "./FeedItem"; +import StandupDetail from "./StandupDetail"; +import StandupSummary from "./StandupSummary"; -const sentimentMap = { - red: 'Trouble Ahead!', - yellow: 'Nervous', - green: 'Great!', -}; - -const responseLabelMap = { - progress_sentiment: 'Health Status', - worked_on: 'Worked on', - working_on: 'Working on', - blocked_on: 'Blocked on', -} +class NewsfeedStandup extends React.Component { + + static propTypes = { + standups: PropTypes.array.isRequired, + timestamp: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + user: PropTypes.object.isRequired, + }; -const classNameSelector = (item, data) => { - let className = "team-standup-answer"; - if (item === "progress_sentiment") { - className += ` team-standup-status--${data}`; + constructor(props) { + super(props); + + const { standups } = props; + this.state = { + standups: standups, + selected_standup: {}, + }; + + this.updateSelectedStandup = this.updateSelectedStandup.bind(this); } - return className; -}; -const renderResponses = standupFields => Object.keys(standupFields).map( - standupField => { - const fieldValue = standupFields[standupField]; - const className = classNameSelector(standupField, fieldValue); + updateSelectedStandup= (standup) => { + this.setState({ selected_standup: { ...standup } }); + } + + renderResponses = () => { return ( -
- -
- { - standupField === "progress_sentiment" - ? sentimentMap[fieldValue] - : fieldValue - } + +
+
+ +
+
+ +
-
+ ); - }, -); - -const NewsfeedStandup = ({ - standup: { - progress_sentiment, - worked_on, - working_on, - blocked_on, - }, -}) => ( -
- {renderResponses({ progress_sentiment, worked_on, working_on, blocked_on })} -
- ); + }; + + render = () => { + return ( + + { this.renderResponses() } + + ); + } + +} export default NewsfeedStandup; diff --git a/src/components/FeedPortal/components/NewsfeedStandup.scss b/src/components/FeedPortal/components/NewsfeedStandup.scss index 103c55c6..95b9c9f9 100644 --- a/src/components/FeedPortal/components/NewsfeedStandup.scss +++ b/src/components/FeedPortal/components/NewsfeedStandup.scss @@ -1,14 +1,30 @@ @import '../../../styles/abstracts/_variables.scss'; - /* ========================== CONTAINER ========================== */ -.team-standup-card-container { - background-color: $palest-grey; - padding: 40px; +.team-standup-container { + display: grid; + grid-template-columns: 25% 72%; + grid-gap: 1rem; +} + +.team-standup-summary { + background-color: white; + display: table-cell; // Force height of all children to that of largest child + grid-column: 1; + grid-row: 1; + padding: .25rem; +} + +.team-standup-detail { + background-color: white; + display: table-cell; // Force height of all children to that of largest child + grid-column: 2; + grid-row: 1; + padding: .25rem; } /* ========================== @@ -24,12 +40,21 @@ data object .team-standup-label { grid-area: l; - border-right: 1px solid $grey; display: block; - padding-bottom: 30px; font-size: 14px; - color: $med-grey; - text-transform: uppercase; + color: #9e9e9e; +} + +.team-standup-label--bordered { + border-right: 1px solid #9e9e9e; +} + +.team-standup-label--padtop { + padding-top: 1rem; +} + +.team-standup-label--padded { + padding-bottom: 30px; } .team-standup-answer { @@ -39,6 +64,13 @@ data object color: $dark-grey; padding-bottom: 30px; } + +.team-standup-id { + display: block; + font-size: 12px; + color: $health-green; + padding-left: 0; +} /* ========================== COLORED STATUS diff --git a/src/components/FeedPortal/components/StandupCompleted.jsx b/src/components/FeedPortal/components/StandupCompleted.jsx new file mode 100644 index 00000000..8d0515b4 --- /dev/null +++ b/src/components/FeedPortal/components/StandupCompleted.jsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; + +import "./NewsfeedStandup.css"; + +const INITIAL_LIST_LIMIT = 3; + +class StandupCompleted extends React.Component { + + static propTypes = { + sortedStandups: PropTypes.array.isRequired, + newStandupSelected: PropTypes.func.isRequired, + updateSelectedStandup: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.defaultDisplayListCount = INITIAL_LIST_LIMIT; + this.state = { + listDisplayLimit: this.defaultDisplayListCount, + scrollText: "More..." + }; + } + + filterStandups = (standup, standupIndex, displayCount) => { + return standupIndex !== 0 && standup.submitted_at && displayCount <= this.state.listDisplayLimit; + } + + renderMore = (sortedStandups) => { + const completedStandupCount = sortedStandups.reduce((count, standup, standupIndex) => { + if (this.filterStandups(standup, standupIndex, 0)) { + count += 1; + } + return count; + }, 0); + + return ( +
+ { completedStandupCount > INITIAL_LIST_LIMIT + ? { + if (this.state.scrollText === 'More...') { + this.setState({ + listDisplayLimit: completedStandupCount, + scrollText: 'Less...' + }); + } else { + this.setState({ + listDisplayLimit: this.defaultDisplayListCount, + scrollText: 'More...' + }); + } + } }> + { this.state.scrollText } + + : null + } +
+ ); + } + + render = () => { + const { sortedStandups, newStandupSelected, updateSelectedStandup } = this.props; + let displayCount = 1; + const incrementDisplayCount = () => { + displayCount += 1; + }; + + return ( + + + { sortedStandups.length > 0 + ? sortedStandups.map( (standup, standupIndex) => ( + this.filterStandups(standup, standupIndex, displayCount) + ? { + newStandupSelected(e, standup, updateSelectedStandup); + } }> + { new Date(standup.submitted_at).toLocaleDateString()} - { standup.member.username } + { incrementDisplayCount() } + + : null + )) + :
No standups
+ } + { this.renderMore(sortedStandups) } +
+ ); + }; + +} + +export default StandupCompleted; diff --git a/src/components/FeedPortal/components/StandupDetail.jsx b/src/components/FeedPortal/components/StandupDetail.jsx new file mode 100644 index 00000000..7a88aa39 --- /dev/null +++ b/src/components/FeedPortal/components/StandupDetail.jsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import "./NewsfeedStandup.css"; + +const sentimentMap = { + red: 'Trouble Ahead!', + yellow: 'Nervous', + green: 'Great!', +}; + +const responseLabelMap = { + progress_sentiment: 'Health Status', + worked_on: 'Worked on', + working_on: 'Working on', + blocked_on: 'Blocked on', +}; + +const classNameSelector = (item, data) => { + let className = "team-standup-answer"; + if (item === "progress_sentiment") { + className += ` team-standup-status--${data}`; + } + return className; +}; + +const renderResponses = standupFields => Object.keys(standupFields).map( + standupField => { + const fieldValue = standupFields[standupField]; + const className = classNameSelector(standupField, fieldValue); + return ( + +
+ +
+ { + standupField === "progress_sentiment" + ? sentimentMap[fieldValue] + : fieldValue + } +
+
+
+ ); + }); + +const StandupDetail = ({ + standup: { + progress_sentiment, + worked_on, + working_on, + blocked_on, + }, +}) => ( +
+ {renderResponses({ progress_sentiment, worked_on, working_on, blocked_on })} +
+ ); + +export default StandupDetail; diff --git a/src/components/FeedPortal/components/StandupPending.jsx b/src/components/FeedPortal/components/StandupPending.jsx new file mode 100644 index 00000000..577fe0cc --- /dev/null +++ b/src/components/FeedPortal/components/StandupPending.jsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { Link } from "react-router-dom" + +import "./NewsfeedStandup.css"; + +const StandupPending = ({ sortedStandups }) => { + // Extract the most recent uncompleted standup regardless of its expiration + const pendingStandup = sortedStandups.reduce( (standups, currentStandup) => { + if (!currentStandup.submitted_at) { + standups.push(currentStandup) + } + return standups; + }, [])[0]; + const standupId = `${ new Date(pendingStandup.expiration).toLocaleDateString() } - ${ pendingStandup.member.username }`; + return ( + + + { !pendingStandup.submitted_at && pendingStandup.expiration > Date.now() + ?
+ + { pendingStandup + ? standupId + : "No Standup Available" } + +
+ :
No pending standup
+ } +
+ ); + +} + +export default StandupPending; diff --git a/src/components/FeedPortal/components/StandupRecent.jsx b/src/components/FeedPortal/components/StandupRecent.jsx new file mode 100644 index 00000000..003c9d27 --- /dev/null +++ b/src/components/FeedPortal/components/StandupRecent.jsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import "./NewsfeedStandup.css"; + +const StandupRecent = ({ sortedStandups, newStandupSelected, updateSelectedStandup }) => { + const mostRecentStandup = sortedStandups[0]; + return ( + + + { mostRecentStandup.submitted_at + ? + :
No completed standups
+ } +
+ ); +} + +export default StandupRecent; diff --git a/src/components/FeedPortal/components/StandupSummary.jsx b/src/components/FeedPortal/components/StandupSummary.jsx new file mode 100644 index 00000000..006b9961 --- /dev/null +++ b/src/components/FeedPortal/components/StandupSummary.jsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; + +import "./NewsfeedStandup.css"; +import StandupCompleted from './StandupCompleted'; +import StandupPending from './StandupPending'; +import StandupRecent from './StandupRecent'; + +class StandupSummary extends React.Component { + + static propTypes = { + standups: PropTypes.array.isRequired, + updateSelectedStandup: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.state = { + displayCount: 0, + }; + + // Force the most recently completed standup to be displayed in the detail panel + this.sortedStandups = props.standups + .slice() // Copy the array of standups so we don't modify props + .sort( (a, b) => (b.submitted_at - a.submitted_at) ); + props.updateSelectedStandup(this.sortedStandups[0]); + } + + // Update the selected standup in the containers state to force its display + // in the detail component + newStandupSelected = (e, mostRecentStandup, updateSelectedStandup) => { + updateSelectedStandup(mostRecentStandup); + } + + renderResponses = (props) => { + return ( + + + + + + ); + }; + + render = () => { + return ( +
+ { this.renderResponses(this.props) } +
+ ); + }; + +} + +export default StandupSummary; diff --git a/src/components/ProjectShowcase/components/ExternalLinks.jsx b/src/components/ProjectShowcase/components/ExternalLinks.jsx index 71e9225c..0e45bbab 100644 --- a/src/components/ProjectShowcase/components/ExternalLinks.jsx +++ b/src/components/ProjectShowcase/components/ExternalLinks.jsx @@ -125,6 +125,7 @@ class ExternalLinks extends React.Component { Live Link diff --git a/src/components/Ticketbox/components/Success.jsx b/src/components/Ticketbox/components/Success.jsx index 69f23016..be7f0676 100644 --- a/src/components/Ticketbox/components/Success.jsx +++ b/src/components/Ticketbox/components/Success.jsx @@ -5,7 +5,7 @@ const Success = ({ category, url }) => ( Success!
{url - ?
View {category} Issue on Github + ? View {category} Issue on Github :
Please wait a few days for us to review and get back to you.
diff --git a/src/components/UserProfile/UserSideBar.jsx b/src/components/UserProfile/UserSideBar.jsx index 6a93d079..212cb1b0 100644 --- a/src/components/UserProfile/UserSideBar.jsx +++ b/src/components/UserProfile/UserSideBar.jsx @@ -8,7 +8,7 @@ const Links = ({ user: { username } }) => (

links

  • - +
  • diff --git a/src/fragmentTypes.json b/src/fragmentTypes.json index c278b1e3..083531ff 100644 --- a/src/fragmentTypes.json +++ b/src/fragmentTypes.json @@ -1 +1 @@ -{"__schema":{"types":[{"kind":"INTERFACE","name":"ProjectBase","possibleTypes":[{"name":"CohortProject"},{"name":"Project"}]},{"kind":"INTERFACE","name":"SkillInterface","possibleTypes":[{"name":"Skill"},{"name":"AcquiredSkill"},{"name":"DesiredSkill"}]},{"kind":"INTERFACE","name":"UserBase","possibleTypes":[{"name":"User"},{"name":"CohortMember"},{"name":"InactiveUser"},{"name":"ProjectMember"}]},{"kind":"INTERFACE","name":"NewsfeedItem","possibleTypes":[{"name":"GithubActivityIssue"},{"name":"GithubActivityPullRequest"},{"name":"NewsfeedVoyage"},{"name":"NewsfeedStandup"},{"name":"NewsfeedAvailableStandup"}]},{"kind":"INTERFACE","name":"HelpRequest","possibleTypes":[{"name":"TeamHelpRequest"}]},{"kind":"INTERFACE","name":"GithubActivityItem","possibleTypes":[{"name":"GithubActivityIssue"},{"name":"GithubActivityPullRequest"}]}]}} \ No newline at end of file +{"__schema":{"types":[{"kind":"INTERFACE","name":"ProjectBase","possibleTypes":[{"name":"CohortProject"},{"name":"Project"}]},{"kind":"INTERFACE","name":"SkillInterface","possibleTypes":[{"name":"Skill"},{"name":"AcquiredSkill"},{"name":"DesiredSkill"}]},{"kind":"INTERFACE","name":"UserBase","possibleTypes":[{"name":"User"},{"name":"CohortMember"},{"name":"ProjectMember"}]},{"kind":"INTERFACE","name":"HelpRequestBase","possibleTypes":[{"name":"HelpRequest"},{"name":"InactiveMember"},{"name":"ChangeProject"}]},{"kind":"INTERFACE","name":"NewsfeedItem","possibleTypes":[{"name":"GithubActivityIssue"},{"name":"GithubActivityPullRequest"},{"name":"NewsfeedVoyage"},{"name":"NewsfeedStandup"},{"name":"NewsfeedAvailableStandup"}]},{"kind":"INTERFACE","name":"GithubActivityItem","possibleTypes":[{"name":"GithubActivityIssue"},{"name":"GithubActivityPullRequest"}]}]}} \ No newline at end of file