├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── api │ ├── index.js │ └── index.test.js ├── components │ ├── FakeIssue.js │ ├── FakeIssueList.js │ ├── Issue.css │ ├── Issue.js │ ├── Issue.test.js │ ├── IssueComment.js │ ├── IssueComment.test.js │ ├── IssueComments.js │ ├── IssueComments.test.js │ ├── IssueLabels.js │ ├── IssueList.css │ ├── IssueList.js │ ├── IssueList.test.js │ ├── UserWithAvatar.css │ ├── UserWithAvatar.js │ └── __snapshots__ │ │ ├── Issue.test.js.snap │ │ ├── IssueComment.test.js.snap │ │ ├── IssueComments.test.js.snap │ │ └── IssueList.test.js.snap ├── containers │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── IssueDetailPage.css │ ├── IssueDetailPage.js │ ├── IssueDetailPage.test.js │ ├── IssueListPage.css │ ├── IssueListPage.js │ ├── IssueListPage.test.js │ └── __snapshots__ │ │ ├── IssueDetailPage.test.js.snap │ │ └── IssueListPage.test.js.snap ├── fixtures │ ├── comments.json │ └── issues │ │ ├── page1.json │ │ ├── page2.json │ │ ├── page3.json │ │ └── page49.json ├── images │ └── no-avatar.png ├── index.css ├── index.js ├── redux │ ├── actions.js │ ├── reducers.js │ └── reducers.test.js └── utils │ ├── stringUtils.js │ ├── stringUtils.test.js │ └── testUtils.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Issues 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). Read on for how to run and test the app. 4 | 5 | ## Run The App 6 | 7 | ``` 8 | yarn # or 'npm install' 9 | yarn start 10 | ``` 11 | 12 | It will open up a browser to [http://localhost:3000](http://localhost:3000). 13 | 14 | ## Run The Tests 15 | 16 | ``` 17 | CI=true yarn test 18 | ``` 19 | 20 | The `CI=true` is not required, but it will enter watch mode unless that flag is present. 21 | 22 | ## Bonus Features 23 | 24 | #### View Any repo 25 | 26 | While the app defaults to viewing the issues for `rails/rails`, it will work with any org/repo combo. With the app running, try these: 27 | 28 | * [http://localhost:3000/facebookincubator/create-react-app/issues](http://localhost:3000/facebookincubator/create-react-app/issues) 29 | * [http://localhost:3000/facebook/react/issues](http://localhost:3000/facebook/react/issues) 30 | * [http://localhost:3000/twbs/bootstrap/issues](http://localhost:3000/twbs/bootstrap/issues) 31 | 32 | #### Markdown Summaries and Comments 33 | 34 | The full issue summaries and comments are passed through the `react-markdown` component to make them nicer to read. 35 | 36 | Try this one: [http://localhost:3000/rails/rails/issues/27224](http://localhost:3000/rails/rails/issues/27224) 37 | 38 | #### Loading Placeholders 39 | 40 | While the issue list is loading, placeholder issues are displayed for a smoother UX. 41 | 42 | Try any repo's issues page: 43 | 44 | * [http://localhost:3000/facebookincubator/create-react-app/issues](http://localhost:3000/facebookincubator/create-react-app/issues) 45 | * [http://localhost:3000/facebook/react/issues](http://localhost:3000/facebook/react/issues) 46 | * [http://localhost:3000/twbs/bootstrap/issues](http://localhost:3000/twbs/bootstrap/issues) 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "issues-viewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.7.0", 7 | "enzyme-to-json": "^1.4.5", 8 | "react-addons-test-utils": "^15.4.2", 9 | "react-scripts": "0.8.4" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.15.3", 13 | "axios-mock-adapter": "^1.7.1", 14 | "parse-link-header": "^0.4.1", 15 | "react": "^15.4.2", 16 | "react-addons-create-fragment": "^15.4.2", 17 | "react-dom": "^15.4.2", 18 | "react-markdown": "^2.4.2", 19 | "react-paginate": "^4.1.0", 20 | "react-redux": "^5.0.1", 21 | "react-router": "^3.0.0", 22 | "react-router-scroll": "^0.4.1", 23 | "redux": "^3.6.0", 24 | "redux-thunk": "^2.1.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test --env=jsdom", 30 | "eject": "react-scripts eject" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dceddia/github-issues-viewer/78d5e4c03dbeb32882a144abe916e5838ce0b0b2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | Github Issues Viewer 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import parseLink from 'parse-link-header'; 3 | import MockAxios from 'axios-mock-adapter'; 4 | import page1 from '../fixtures/issues/page1'; 5 | import page2 from '../fixtures/issues/page2'; 6 | import page3 from '../fixtures/issues/page3'; 7 | import page49 from '../fixtures/issues/page49'; 8 | import comments from '../fixtures/comments'; 9 | 10 | const USE_STATIC_DATA = false; 11 | 12 | if(USE_STATIC_DATA) { 13 | let mock = new MockAxios(axios); 14 | 15 | mock.onGet('https://api.github.com/repos/rails/rails/issues?per_page=25&page=1') 16 | .reply(200, page1.data, {link: page1.link}); 17 | mock.onGet('https://api.github.com/repos/rails/rails/issues?per_page=25&page=2') 18 | .reply(200, page2.data, {link: page2.link}); 19 | mock.onGet('https://api.github.com/repos/rails/rails/issues?per_page=25&page=3') 20 | .reply(200, page3.data, {link: page3.link}); 21 | mock.onGet('https://api.github.com/repos/rails/rails/issues?per_page=25&page=49') 22 | .reply(200, page49.data, {link: page49.link}); 23 | mock.onGet(/issues\/404$/).reply(404); 24 | mock.onGet(/issues\/slow$/).reply(function(config) { 25 | return new Promise((resolve, reject) => { 26 | resolve([200, page1.data, {link: page1.link}]); 27 | }); 28 | }); 29 | mock.onGet(/issues\/99$/) 30 | .reply(config => { 31 | const number = config.url.match(/issues\/(\d+)/)[1]; 32 | const issue = { 33 | ...page1.data[0], 34 | number, 35 | comments: 0 36 | }; 37 | return [200, issue]; 38 | }); 39 | mock.onGet(/issues\/\d+$/) 40 | .reply(config => { 41 | const number = config.url.match(/issues\/(\d+)/)[1]; 42 | const issue = { 43 | ...page1.data[0], 44 | number 45 | }; 46 | return [200, issue]; 47 | }); 48 | mock.onGet(/issues\/\d+\/comments$/) 49 | .reply(200, comments); 50 | } 51 | 52 | const isLastPage = (pageLinks) => { 53 | return Object.keys(pageLinks).length === 2 && 54 | pageLinks.first && pageLinks.prev; 55 | } 56 | 57 | const getPageCount = (pageLinks) => { 58 | if(!pageLinks) { 59 | return 0; 60 | } 61 | if(isLastPage(pageLinks)) { 62 | return parseInt(pageLinks.prev.page, 10) + 1; 63 | } else if(pageLinks.last) { 64 | return parseInt(pageLinks.last.page, 10) 65 | } else { 66 | return 0; 67 | } 68 | } 69 | 70 | export function getIssues(org, repo, page = 1) { 71 | const url = `https://api.github.com/repos/${org}/${repo}/issues?per_page=25&page=${page}`; 72 | return axios.get(url) 73 | .then(res => { 74 | const pageLinks = parseLink(res.headers.link); 75 | const pageCount = getPageCount(pageLinks); 76 | return { 77 | pageLinks, 78 | pageCount, 79 | data: res.data 80 | }; 81 | }) 82 | .catch(err => Promise.reject(err)); 83 | } 84 | 85 | export function getRepoDetails(org, repo) { 86 | const url = `https://api.github.com/repos/${org}/${repo}`; 87 | return axios.get(url) 88 | .then(res => res.data) 89 | .catch(err => Promise.reject(-1)); 90 | } 91 | 92 | export function getIssue(org, repo, number) { 93 | const url = `https://api.github.com/repos/${org}/${repo}/issues/${number}`; 94 | return axios.get(url) 95 | .then(res => res.data) 96 | .catch(err => Promise.reject(err)); 97 | } 98 | 99 | export function getComments(url) { 100 | return axios.get(url) 101 | .then(res => res.data) 102 | .catch(err => Promise.reject(err)); 103 | } 104 | -------------------------------------------------------------------------------- /src/api/index.test.js: -------------------------------------------------------------------------------- 1 | import { getIssues, getIssue, getRepoDetails } from './index'; 2 | import axios from 'axios'; 3 | 4 | describe('getIssues', () => { 5 | it('builds the url', (done) => { 6 | axios.get = url => { 7 | expect(url).toEqual('https://api.github.com/repos/foo/bar/issues?per_page=25&page=123'); 8 | done(); 9 | return Promise.resolve({data: {}, headers: {}}); 10 | } 11 | getIssues('foo', 'bar', 123); 12 | }); 13 | 14 | it('builds the url with a default page', (done) => { 15 | axios.get = url => { 16 | expect(url).toEqual('https://api.github.com/repos/foo/bar/issues?per_page=25&page=1'); 17 | done(); 18 | return Promise.resolve({data: {}, headers: {}}); 19 | } 20 | getIssues('foo', 'bar'); 21 | }); 22 | 23 | it('parses successful response', (done) => { 24 | axios.get = url => { 25 | return Promise.resolve({ 26 | data: 'the data', 27 | headers: { 28 | link: "; rel=\"next\", ; rel=\"last\"" 29 | } 30 | }); 31 | }; 32 | 33 | getIssues('rails', 'rails').then(issues => { 34 | expect(issues.data).toEqual('the data'); 35 | expect(issues.pageCount).toEqual(49); 36 | expect(issues.pageLinks).toEqual({ 37 | "last": { 38 | "page": "49", 39 | "per_page": "25", 40 | "rel": "last", 41 | "url": "https://api.github.com/repositories/8514/issues?per_page=25&page=49", 42 | }, 43 | "next": { 44 | "page": "2", 45 | "per_page": "25", 46 | "rel": "next", 47 | "url": "https://api.github.com/repositories/8514/issues?per_page=25&page=2", 48 | } 49 | }); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('returns correct pageCount on last page', (done) => { 55 | axios.get = url => { 56 | return Promise.resolve({ 57 | data: 'the data', 58 | headers: { 59 | link: '; rel="first", ; rel="prev"' 60 | } 61 | }); 62 | }; 63 | 64 | getIssues('rails', 'rails').then(issues => { 65 | try { 66 | expect(issues.data).toEqual('the data'); 67 | expect(issues.pageCount).toEqual(49); 68 | expect(issues.pageLinks).toEqual({ 69 | "first": { 70 | "page": "1", 71 | "per_page": "25", 72 | "rel": "first", 73 | "url": "https://api.github.com/repositories/8514/issues?per_page=25&page=1", 74 | }, 75 | "prev": { 76 | "page": "48", 77 | "per_page": "25", 78 | "rel": "prev", 79 | "url": "https://api.github.com/repositories/8514/issues?per_page=25&page=48", 80 | } 81 | }); 82 | done(); 83 | } catch(e) { 84 | fail(e); 85 | done(); 86 | } 87 | }); 88 | }); 89 | 90 | it('parses error responses', (done) => { 91 | const error = new Error('something bad'); 92 | axios.get = url => { 93 | return Promise.reject(error); 94 | }; 95 | 96 | getIssues('fail', 'fail') 97 | .then(fail) 98 | .catch((e) => { 99 | expect(e).toEqual(error); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('getRepoDetails', () => { 106 | it('builds the url', (done) => { 107 | axios.get = url => { 108 | expect(url).toEqual('https://api.github.com/repos/foo/bar'); 109 | done(); 110 | return Promise.resolve({}); 111 | } 112 | getRepoDetails('foo', 'bar'); 113 | }); 114 | 115 | it('handles successful responses', (done) => { 116 | axios.get = url => { 117 | return Promise.resolve({ 118 | data: { 119 | open_issues_count: 42 120 | } 121 | }); 122 | }; 123 | 124 | getRepoDetails('rails', 'rails').then(count => { 125 | expect(count).toEqual({open_issues_count: 42}); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('handles error responses', (done) => { 131 | axios.get = url => { 132 | return Promise.reject('terrible error'); 133 | }; 134 | 135 | getRepoDetails('rails', 'rails') 136 | .then(fail) 137 | .catch(count => { 138 | expect(count).toEqual(-1); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | 144 | describe('getIssue', () => { 145 | const issue = {number: 1234, body: 'something'}; 146 | 147 | it('builds the url', (done) => { 148 | axios.get = url => { 149 | expect(url).toEqual('https://api.github.com/repos/foo/bar/issues/123'); 150 | done(); 151 | return Promise.resolve({}); 152 | } 153 | getIssue('foo', 'bar', 123); 154 | }); 155 | 156 | it('handles successful responses', (done) => { 157 | axios.get = url => { 158 | return Promise.resolve({ 159 | data: issue 160 | }); 161 | }; 162 | 163 | getIssue('rails', 'rails', 1234).then(data => { 164 | expect(data).toEqual(issue); 165 | done(); 166 | }).catch(fail); 167 | }); 168 | 169 | it('handles error responses', (done) => { 170 | const error = new Error('terrible error'); 171 | axios.get = url => { 172 | return Promise.reject(error); 173 | }; 174 | 175 | getIssue('rails', 'rails', 1234) 176 | .then(fail) 177 | .catch(err => { 178 | expect(err).toEqual(error); 179 | done(); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /src/components/FakeIssue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserWithAvatar from './UserWithAvatar'; 3 | import IssueLabels from './IssueLabels'; 4 | import noAvatar from '../images/no-avatar.png'; 5 | import './Issue.css'; 6 | 7 | export default function FakeIssue() { 8 | const fakeLabels = [ 9 | {id: 1, name: 'loading...', color: 'ccc'}, 10 | {id: 2, name: 'please wait', color: 'ccc'}, 11 | {id: 3, name: 'coming soon', color: 'ccc'}, 12 | {id: 4, name: 'thanks', color: 'ccc'}, 13 | ]; 14 | 15 | const upTo4 = Math.floor(Math.random() * 5); 16 | 17 | return ( 18 |
19 | 20 |
21 | 22 |   23 |   24 | 25 |

 

26 | 27 |
28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /src/components/FakeIssueList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import FakeIssue from './FakeIssue'; 3 | import './IssueList.css'; 4 | 5 | export default function FakeIssueList({ number = 25 }) { 6 | return ( 7 |
    8 | {Array.from(Array(number)).map((issue, i) => 9 |
  • 10 | 11 |
  • 12 | )} 13 |
14 | ); 15 | } 16 | 17 | FakeIssueList.propTypes = { 18 | number: PropTypes.number 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Issue.css: -------------------------------------------------------------------------------- 1 | .issue { 2 | border-bottom: 1px solid #ddd; 3 | display: flex; 4 | padding: 1rem 0.5rem; 5 | } 6 | 7 | .issue__number { 8 | font-size: 14px; 9 | color: #999; 10 | margin-right: 0.5rem; 11 | } 12 | 13 | .issue__title { 14 | font-weight: bold; 15 | } 16 | 17 | .issue__label { 18 | display: inline-block; 19 | font-size: 0.75rem; 20 | padding: 1px 5px; 21 | margin: 0 0.5rem 0.5rem 0; 22 | border-radius: 5px; 23 | border: 1px solid #ccc; 24 | background-color: #fff; 25 | } 26 | 27 | .issue .issue__user { 28 | margin-right: 0.5rem; 29 | } 30 | 31 | .issue__summary { 32 | padding-left: 0.5rem; 33 | } 34 | 35 | .issue__labels { 36 | padding-left: 0.5rem; 37 | } 38 | 39 | .issue__body { 40 | word-wrap: break-word; 41 | overflow: hidden; 42 | } 43 | 44 | .issue--loading .issue__body { 45 | width: 100%; 46 | } 47 | .issue--loading .issue__number, 48 | .issue--loading .issue__title { 49 | background-color: #e5e5e5; 50 | display: inline-block; 51 | margin-right: 0.5rem; 52 | } 53 | .issue--loading .issue__number { 54 | width: 20%; 55 | max-width: 4rem; 56 | } 57 | .issue--loading .issue__title { 58 | width: 70%; 59 | } 60 | .issue--loading .issue__summary { 61 | background-color: #e5e5e5; 62 | width: 95%; 63 | } -------------------------------------------------------------------------------- /src/components/Issue.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import UserWithAvatar from './UserWithAvatar'; 4 | import IssueLabels from './IssueLabels'; 5 | import { shorten } from '../utils/stringUtils'; 6 | import './Issue.css'; 7 | 8 | export default function Issue({ number, title, labels, user, summary }, { router }) { 9 | const {org, repo} = router.params; 10 | return ( 11 |
12 | 13 |
14 | 15 | #{number} 16 | {title} 17 | 18 |

{shorten(summary)}

19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | Issue.propTypes = { 26 | number: PropTypes.number.isRequired, 27 | title: PropTypes.string.isRequired, 28 | labels: PropTypes.arrayOf(PropTypes.shape({ 29 | id: PropTypes.number, 30 | name: PropTypes.string, 31 | color: PropTypes.string 32 | })).isRequired, 33 | user: PropTypes.shape({ 34 | login: PropTypes.string.isRequired, 35 | avatar_url: PropTypes.string 36 | }).isRequired, 37 | summary: PropTypes.string.isRequired 38 | }; 39 | 40 | Issue.contextTypes = { 41 | router: PropTypes.object.isRequired 42 | }; -------------------------------------------------------------------------------- /src/components/Issue.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { checkSnapshot } from '../utils/testUtils'; 4 | import Issue, { shorten } from './Issue'; 5 | 6 | it('renders', () => { 7 | const context = { router: {params: {org: 'rails', repo: 'rails'}} }; 8 | const tree = shallow( 9 | , { context } 21 | ); 22 | 23 | checkSnapshot(tree); 24 | }); -------------------------------------------------------------------------------- /src/components/IssueComment.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import UserWithAvatar from './UserWithAvatar'; 4 | import { insertMentionLinks } from '../utils/stringUtils'; 5 | 6 | export default function IssueComment({ comment }) { 7 | return ( 8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | IssueComment.propTypes = { 19 | comment: PropTypes.shape({ 20 | user: PropTypes.shape({ 21 | login: PropTypes.string, 22 | avatar_url: PropTypes.string 23 | }).isRequired, 24 | body: PropTypes.string 25 | }).isRequired 26 | }; -------------------------------------------------------------------------------- /src/components/IssueComment.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { checkSnapshot } from '../utils/testUtils'; 4 | import IssueComment from './IssueComment'; 5 | 6 | it('renders a comment', () => { 7 | const comment = { 8 | user: { 9 | login: "somebody", 10 | avatar_url: "http://foo" 11 | }, 12 | body: "a comment" 13 | }; 14 | const tree = shallow( 15 | 16 | ); 17 | checkSnapshot(tree); 18 | }); -------------------------------------------------------------------------------- /src/components/IssueComments.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IssueComment from './IssueComment'; 3 | 4 | export default function IssueComments({ comments = [] }) { 5 | return ( 6 |
    7 | {comments.map(comment => 8 |
  • 9 | 10 |
  • 11 | )} 12 |
13 | ); 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/IssueComments.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { checkSnapshot } from '../utils/testUtils'; 4 | import IssueComments from './IssueComments'; 5 | 6 | it('renders with no comments', () => { 7 | const tree = shallow(); 8 | checkSnapshot(tree); 9 | }); 10 | 11 | it('renders comments', () => { 12 | const comment = { 13 | user: { 14 | login: "somebody", 15 | avatar_url: "http://foo" 16 | }, 17 | body: "a comment" 18 | }; 19 | const comments = [{...comment, id: 1}, {...comment, id: 2}]; 20 | const tree = shallow( 21 | 22 | ); 23 | checkSnapshot(tree); 24 | }); -------------------------------------------------------------------------------- /src/components/IssueLabels.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const IssueLabels = ({ labels }) => ( 4 |
5 | {labels.map(label => 6 | 13 | {label.name} 14 | 15 | )} 16 |
17 | ); 18 | 19 | IssueLabels.propTypes = { 20 | labels: PropTypes.arrayOf(PropTypes.shape({ 21 | id: PropTypes.number, 22 | name: PropTypes.string, 23 | color: PropTypes.string 24 | })).isRequired 25 | }; 26 | 27 | export default IssueLabels; 28 | -------------------------------------------------------------------------------- /src/components/IssueList.css: -------------------------------------------------------------------------------- 1 | .issues { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | .issues > li { 6 | list-style: none; 7 | } -------------------------------------------------------------------------------- /src/components/IssueList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Issue from './Issue'; 3 | import './IssueList.css'; 4 | 5 | export default function IssueList({ issues }) { 6 | return ( 7 |
    8 | {issues.map(issue => 9 |
  • 10 | 16 |
  • 17 | )} 18 |
19 | ); 20 | } 21 | 22 | IssueList.propTypes = { 23 | issues: PropTypes.arrayOf(PropTypes.shape({ 24 | number: PropTypes.number.isRequired, 25 | user: PropTypes.shape({ 26 | login: PropTypes.string, 27 | avatar_url: PropTypes.string, 28 | gravatar_id: PropTypes.string 29 | }).isRequired, 30 | title: PropTypes.string, 31 | body: PropTypes.string, 32 | labels: PropTypes.arrayOf(PropTypes.shape({ 33 | id: PropTypes.number, 34 | name: PropTypes.string, 35 | color: PropTypes.string 36 | })) 37 | })) 38 | }; -------------------------------------------------------------------------------- /src/components/IssueList.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { checkSnapshot } from '../utils/testUtils'; 4 | import IssueList from './IssueList'; 5 | 6 | let issues = [ 7 | { 8 | "url": "https://api.github.com/repos/rails/rails/issues/27599", 9 | "repository_url": "https://api.github.com/repos/rails/rails", 10 | "labels_url": "https://api.github.com/repos/rails/rails/issues/27599/labels{/name}", 11 | "comments_url": "https://api.github.com/repos/rails/rails/issues/27599/comments", 12 | "events_url": "https://api.github.com/repos/rails/rails/issues/27599/events", 13 | "html_url": "https://github.com/rails/rails/pull/27599", 14 | "id": 199328180, 15 | "number": 27599, 16 | "title": "Fix bug with symbolized keys in .where with nested join (alternative to #27598)", 17 | "user": { 18 | "login": "NickLaMuro", 19 | "id": 314014, 20 | "avatar_url": "https://avatars.githubusercontent.com/u/314014?v=3", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/NickLaMuro", 23 | "html_url": "https://github.com/NickLaMuro", 24 | "followers_url": "https://api.github.com/users/NickLaMuro/followers", 25 | "following_url": "https://api.github.com/users/NickLaMuro/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/NickLaMuro/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/NickLaMuro/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/NickLaMuro/subscriptions", 29 | "organizations_url": "https://api.github.com/users/NickLaMuro/orgs", 30 | "repos_url": "https://api.github.com/users/NickLaMuro/repos", 31 | "events_url": "https://api.github.com/users/NickLaMuro/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/NickLaMuro/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "labels": [ 37 | { 38 | "id": 107191, 39 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 40 | "name": "activerecord", 41 | "color": "0b02e1", 42 | "default": false 43 | }, 44 | { 45 | "id": 128692, 46 | "url": "https://api.github.com/repos/rails/rails/labels/needs%20feedback", 47 | "name": "needs feedback", 48 | "color": "ededed", 49 | "default": false 50 | } 51 | ], 52 | "state": "open", 53 | "locked": false, 54 | "assignee": { 55 | "login": "sgrif", 56 | "id": 1529387, 57 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 58 | "gravatar_id": "", 59 | "url": "https://api.github.com/users/sgrif", 60 | "html_url": "https://github.com/sgrif", 61 | "followers_url": "https://api.github.com/users/sgrif/followers", 62 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 63 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 64 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 65 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 66 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 67 | "repos_url": "https://api.github.com/users/sgrif/repos", 68 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 69 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 70 | "type": "User", 71 | "site_admin": false 72 | }, 73 | "assignees": [ 74 | { 75 | "login": "sgrif", 76 | "id": 1529387, 77 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 78 | "gravatar_id": "", 79 | "url": "https://api.github.com/users/sgrif", 80 | "html_url": "https://github.com/sgrif", 81 | "followers_url": "https://api.github.com/users/sgrif/followers", 82 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 83 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 84 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 85 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 86 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 87 | "repos_url": "https://api.github.com/users/sgrif/repos", 88 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 89 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 90 | "type": "User", 91 | "site_admin": false 92 | } 93 | ], 94 | "milestone": null, 95 | "comments": 2, 96 | "created_at": "2017-01-07T01:05:51Z", 97 | "updated_at": "2017-01-07T01:15:38Z", 98 | "closed_at": null, 99 | "pull_request": { 100 | "url": "https://api.github.com/repos/rails/rails/pulls/27599", 101 | "html_url": "https://github.com/rails/rails/pull/27599", 102 | "diff_url": "https://github.com/rails/rails/pull/27599.diff", 103 | "patch_url": "https://github.com/rails/rails/pull/27599.patch" 104 | }, 105 | "body": "Summary\n-------\nIn https://github.com/rails/rails/pull/25146, code was added to fix making where clauses against tables with an `enum` column with a `join` present as part of the query. As part of this fix, it called `singularize` on the `table_name` variable that was passed into the `associated_table` method.\n\n`table_name`, in some circumstances, can also be a symbol if more than one level of joins exists in the Relation (i.e `joins(:book => :subscription)`). This fixes that by adding chaning the `.stringify_keys!` (found in `ActiveRecord::Relation::WhereClauseFactory`) to be a `.deep_stringify_keys!` to stringfy keys at all levels.\n\n\nOther Information\n-----------------\nThis bug only surfaces when a join is made more than 1 level deep since the `where_clause_builder` calls `stringify_keys!` on the top level of the `.where` hash:\n\nhttps://github.com/rails/rails/blob/21e5fd4/activerecord/lib/active_record/relation/where_clause_factory.rb#L16\n\nSo this hides this edge case from showing up in the test suite with the current coverage and the test that was in PR #25146.\n\nThis is the alternative to https://github.com/rails/rails/pull/27598 in which the change from PR #25146 was fixed in isolation. Instead, here we fix the false assumption that all `table_name` values being passed into `.associated_table` are a string. This might have wider effects because of that, so that should be considered when reviewing." 106 | }, 107 | { 108 | "url": "https://api.github.com/repos/rails/rails/issues/27598", 109 | "repository_url": "https://api.github.com/repos/rails/rails", 110 | "labels_url": "https://api.github.com/repos/rails/rails/issues/27598/labels{/name}", 111 | "comments_url": "https://api.github.com/repos/rails/rails/issues/27598/comments", 112 | "events_url": "https://api.github.com/repos/rails/rails/issues/27598/events", 113 | "html_url": "https://github.com/rails/rails/pull/27598", 114 | "id": 199327680, 115 | "number": 27598, 116 | "title": "Fix bug with symbolized keys in .where with nested join", 117 | "user": { 118 | "login": "NickLaMuro", 119 | "id": 314014, 120 | "avatar_url": "https://avatars.githubusercontent.com/u/314014?v=3", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/NickLaMuro", 123 | "html_url": "https://github.com/NickLaMuro", 124 | "followers_url": "https://api.github.com/users/NickLaMuro/followers", 125 | "following_url": "https://api.github.com/users/NickLaMuro/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/NickLaMuro/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/NickLaMuro/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/NickLaMuro/subscriptions", 129 | "organizations_url": "https://api.github.com/users/NickLaMuro/orgs", 130 | "repos_url": "https://api.github.com/users/NickLaMuro/repos", 131 | "events_url": "https://api.github.com/users/NickLaMuro/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/NickLaMuro/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | }, 136 | "labels": [ 137 | { 138 | "id": 107191, 139 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 140 | "name": "activerecord", 141 | "color": "0b02e1", 142 | "default": false 143 | }, 144 | { 145 | "id": 128692, 146 | "url": "https://api.github.com/repos/rails/rails/labels/needs%20feedback", 147 | "name": "needs feedback", 148 | "color": "ededed", 149 | "default": false 150 | } 151 | ], 152 | "state": "open", 153 | "locked": false, 154 | "assignee": { 155 | "login": "sgrif", 156 | "id": 1529387, 157 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 158 | "gravatar_id": "", 159 | "url": "https://api.github.com/users/sgrif", 160 | "html_url": "https://github.com/sgrif", 161 | "followers_url": "https://api.github.com/users/sgrif/followers", 162 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 163 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 164 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 165 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 166 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 167 | "repos_url": "https://api.github.com/users/sgrif/repos", 168 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 169 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 170 | "type": "User", 171 | "site_admin": false 172 | }, 173 | "assignees": [ 174 | { 175 | "login": "sgrif", 176 | "id": 1529387, 177 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 178 | "gravatar_id": "", 179 | "url": "https://api.github.com/users/sgrif", 180 | "html_url": "https://github.com/sgrif", 181 | "followers_url": "https://api.github.com/users/sgrif/followers", 182 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 183 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 184 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 185 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 186 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 187 | "repos_url": "https://api.github.com/users/sgrif/repos", 188 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 189 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 190 | "type": "User", 191 | "site_admin": false 192 | } 193 | ], 194 | "milestone": null, 195 | "comments": 3, 196 | "created_at": "2017-01-07T01:00:48Z", 197 | "updated_at": "2017-01-07T01:15:33Z", 198 | "closed_at": null, 199 | "pull_request": { 200 | "url": "https://api.github.com/repos/rails/rails/pulls/27598", 201 | "html_url": "https://github.com/rails/rails/pull/27598", 202 | "diff_url": "https://github.com/rails/rails/pull/27598.diff", 203 | "patch_url": "https://github.com/rails/rails/pull/27598.patch" 204 | }, 205 | "body": "Summary\n-------\nIn https://github.com/rails/rails/pull/25146, code was added to fix making where clauses against tables with an `enum` column with a `join` present as part of the query. As part of this fix, it called `singularize` on the `table_name` variable that was passed into the `associated_table` method.\n\n`table_name`, in some circumstances, can also be a symbol if more than one level of joins exists in the Relation (i.e `joins(:book => :subscription)`). This fixes that by adding `.to_s` before calling `.singularize` on the `table_name` variable.\n\n\nOther Information\n-----------------\nThis bug only surfaces when a join is made more than 1 level deep since the `where_clause_builder` calls `stringify_keys!` on the top level of the `.where` hash:\n\nhttps://github.com/rails/rails/blob/21e5fd4/activerecord/lib/active_record/relation/where_clause_factory.rb#L16\n\nSo this hides this edge case from showing up in the test suite with the current coverage and the test that was in PR #25146.\n\nThe other solution to this problem is to deeply stringify the keys in the `where_clause_builder` and all method calls following that can safely assume strings are being passed in as keys. This is a heavier hammer, but the assumption is already being made on the top level to do this, so it seems like a better place to put it instead of scattered throughout the codebase and having it handle both strings/symbols in multiple places. This alternative will be proposed in a separate PR.\n\n\nAlso of note, the this probably isn't the best place for a test like this, but it was the simplest way I could get a test in place without familiarizing myself with the entire ActiveRecord test suite (copying what was done in the previous PR). Suggestions welcome for a better place for this test to live, but it is worth noting that this same test will also be used to confirm the same working functionality in the alternative form for this PR." 206 | }, 207 | { 208 | "url": "https://api.github.com/repos/rails/rails/issues/27597", 209 | "repository_url": "https://api.github.com/repos/rails/rails", 210 | "labels_url": "https://api.github.com/repos/rails/rails/issues/27597/labels{/name}", 211 | "comments_url": "https://api.github.com/repos/rails/rails/issues/27597/comments", 212 | "events_url": "https://api.github.com/repos/rails/rails/issues/27597/events", 213 | "html_url": "https://github.com/rails/rails/pull/27597", 214 | "id": 199307639, 215 | "number": 27597, 216 | "title": "Consistency between first() and last() with limit", 217 | "user": { 218 | "login": "brchristian", 219 | "id": 2460418, 220 | "avatar_url": "https://avatars.githubusercontent.com/u/2460418?v=3", 221 | "gravatar_id": "", 222 | "url": "https://api.github.com/users/brchristian", 223 | "html_url": "https://github.com/brchristian", 224 | "followers_url": "https://api.github.com/users/brchristian/followers", 225 | "following_url": "https://api.github.com/users/brchristian/following{/other_user}", 226 | "gists_url": "https://api.github.com/users/brchristian/gists{/gist_id}", 227 | "starred_url": "https://api.github.com/users/brchristian/starred{/owner}{/repo}", 228 | "subscriptions_url": "https://api.github.com/users/brchristian/subscriptions", 229 | "organizations_url": "https://api.github.com/users/brchristian/orgs", 230 | "repos_url": "https://api.github.com/users/brchristian/repos", 231 | "events_url": "https://api.github.com/users/brchristian/events{/privacy}", 232 | "received_events_url": "https://api.github.com/users/brchristian/received_events", 233 | "type": "User", 234 | "site_admin": false 235 | }, 236 | "labels": [ 237 | { 238 | "id": 107191, 239 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 240 | "name": "activerecord", 241 | "color": "0b02e1", 242 | "default": false 243 | } 244 | ], 245 | "state": "open", 246 | "locked": false, 247 | "assignee": { 248 | "login": "pixeltrix", 249 | "id": 6321, 250 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 251 | "gravatar_id": "", 252 | "url": "https://api.github.com/users/pixeltrix", 253 | "html_url": "https://github.com/pixeltrix", 254 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 255 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 256 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 257 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 258 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 259 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 260 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 261 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 262 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 263 | "type": "User", 264 | "site_admin": false 265 | }, 266 | "assignees": [ 267 | { 268 | "login": "pixeltrix", 269 | "id": 6321, 270 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 271 | "gravatar_id": "", 272 | "url": "https://api.github.com/users/pixeltrix", 273 | "html_url": "https://github.com/pixeltrix", 274 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 275 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 276 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 277 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 278 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 279 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 280 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 281 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 282 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 283 | "type": "User", 284 | "site_admin": false 285 | } 286 | ], 287 | "milestone": null, 288 | "comments": 1, 289 | "created_at": "2017-01-06T22:34:28Z", 290 | "updated_at": "2017-01-06T22:46:24Z", 291 | "closed_at": null, 292 | "pull_request": { 293 | "url": "https://api.github.com/repos/rails/rails/pulls/27597", 294 | "html_url": "https://github.com/rails/rails/pull/27597", 295 | "diff_url": "https://github.com/rails/rails/pull/27597.diff", 296 | "patch_url": "https://github.com/rails/rails/pull/27597.patch" 297 | }, 298 | "body": "Fixes #23979.\r\n\r\nAs discussed in #23979, there was an inconsistency between the way that `first()` and `last()` would interact with `limit`. Specifically:\r\n\r\n```Ruby\r\n> Topic.limit(1).first(2).size\r\n=> 2\r\n> Topic.limit(1).last(2).size\r\n=> 1\r\n```\r\n\r\nThis PR is a refactor and rebase of #24124, with a simpler test suite and simpler implementation.\r\n\r\nDiscussion with Rails community members as well as DHH in https://github.com/rails/rails/pull/23598#issuecomment-189675440 showed that the behavior or `first` should be brought into line with `last` (rather than vice-versa).\r\n\r\nThis PR resolves the inconsistency between `first` and `last` when used in conjunction with `limit`.\r\n" 299 | } 300 | ]; 301 | 302 | // Jest doesn't handle \r\n in snapshots very well. 303 | // https://github.com/facebook/jest/pull/1879#issuecomment-261019033 304 | // Replace the body with some plain text 305 | issues = issues.map(issue => ({ 306 | ...issue, 307 | body: "something with no line breaks" 308 | })); 309 | 310 | it('renders', () => { 311 | const tree = shallow( 312 | 313 | ); 314 | 315 | checkSnapshot(tree); 316 | }); -------------------------------------------------------------------------------- /src/components/UserWithAvatar.css: -------------------------------------------------------------------------------- 1 | .issue__user { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .issue__user.vertical { 7 | text-align: center; 8 | margin-right: 0.5rem; 9 | flex-direction: column; 10 | justify-content: flex-start; 11 | min-width: 80px; 12 | max-width: 80px; 13 | } 14 | 15 | .issue__user__avatar { 16 | width: 40px; 17 | height: 40px; 18 | border-radius: 50%; 19 | box-shadow: 0 0 2px rgba(0,0,0,0.5); 20 | } 21 | 22 | .issue__user__name { 23 | color: #555; 24 | font-size: 0.8rem; 25 | word-wrap: break-word; 26 | max-width: 100%; 27 | } 28 | 29 | .issue__user.horizontal .issue__user__name { 30 | display: inline; 31 | margin-left: 0.5rem; 32 | } -------------------------------------------------------------------------------- /src/components/UserWithAvatar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import './UserWithAvatar.css'; 3 | 4 | const UserWithAvatar = ({ user, orientation = 'vertical', link = true }) => { 5 | const Wrapper = link ? 'a' : 'span'; 6 | return ( 7 | 8 | 9 |
{user.login}
10 |
11 | ); 12 | }; 13 | 14 | UserWithAvatar.propTypes = { 15 | user: PropTypes.shape({ 16 | login: PropTypes.string.isRequired, 17 | avatar_url: PropTypes.string 18 | }).isRequired, 19 | orientation: PropTypes.oneOf(['horizontal', 'vertical']) 20 | }; 21 | 22 | export default UserWithAvatar; -------------------------------------------------------------------------------- /src/components/__snapshots__/Issue.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test renders 1`] = ` 2 |
4 | 11 |
13 | 17 | 19 | # 20 | 27593 21 | 22 | 24 | Yield in partial view does not call block given by calling view 25 | 26 | 27 |

29 | Fixes #23979. 30 | 31 | As discussed in #23979, there was an inconsistency between the way that \`first()\` and \`last()\` would interact with \`limit\`. ... 32 |

33 | 43 |
44 |
45 | `; 46 | -------------------------------------------------------------------------------- /src/components/__snapshots__/IssueComment.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test renders a comment 1`] = ` 2 |
4 | 12 |
14 | 18 |
19 |
20 | `; 21 | -------------------------------------------------------------------------------- /src/components/__snapshots__/IssueComments.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test renders comments 1`] = ` 2 |
    4 |
  • 5 | 16 |
  • 17 |
  • 18 | 29 |
  • 30 |
31 | `; 32 | 33 | exports[`test renders with no comments 1`] = ` 34 |
    36 | `; 37 | -------------------------------------------------------------------------------- /src/components/__snapshots__/IssueList.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test renders 1`] = ` 2 |
      4 |
    • 6 | 49 |
    • 50 |
    • 52 | 95 |
    • 96 |
    • 98 | 134 |
    • 135 |
    136 | `; 137 | -------------------------------------------------------------------------------- /src/containers/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | max-width: 768px; 3 | margin: 0 auto; 4 | padding: 1rem; 5 | } -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 |
    8 | {this.props.children} 9 |
    10 | ); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/containers/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/containers/IssueDetailPage.css: -------------------------------------------------------------------------------- 1 | .issue-detail__meta { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | font-size: 1.2rem; 6 | margin-bottom: 1rem; 7 | flex-wrap: wrap; 8 | } 9 | 10 | .issue-detail__number { 11 | color: #999; 12 | } 13 | 14 | .issue-detail__state { 15 | border: 2px solid; 16 | border-radius: 3px; 17 | padding: 0.2rem 2rem; 18 | } 19 | 20 | .issue-detail__summary { 21 | margin-bottom: 2rem; 22 | padding: 0 1rem; 23 | font-size: 1rem; 24 | line-height: 1.4rem; 25 | } 26 | 27 | .issue-detail--loading, 28 | .issue-detail--error { 29 | text-align: center; 30 | } 31 | 32 | .markdown img { 33 | max-width: 100%; 34 | } 35 | .markdown pre { 36 | border: 1px solid #ccc; 37 | padding: 0.5rem; 38 | border-radius: 2px; 39 | background-color: #f9f9f9; 40 | overflow-x: auto; 41 | } 42 | .markdown p > code { 43 | padding: 3px 5px; 44 | border-radius: 2px; 45 | background-color: #f7f7f7; 46 | } 47 | 48 | .issue-detail__state--open { 49 | border-color: #7CB342; 50 | background-color: #8bc34a; 51 | color: #fff; 52 | font-weight: 600; 53 | } 54 | 55 | .divider--short { 56 | max-width: 80%; 57 | border: none; 58 | border-bottom: 1px solid #ccc; 59 | margin-bottom: 2rem; 60 | } 61 | 62 | .issue-detail .issue__labels { 63 | margin-bottom: 2rem; 64 | } 65 | 66 | .issue-detail--no-comments, 67 | .issue-detail--comments-loading, 68 | .issue-detail--comments-error { 69 | text-align: center; 70 | color: #999; 71 | } 72 | 73 | .issue-detail__comments { 74 | padding: 0 1rem; 75 | } 76 | .issue-detail__comments > li { 77 | list-style: none; 78 | } 79 | 80 | .issue-detail__comment { 81 | margin-bottom: 1rem; 82 | border-bottom: 1px solid #ccc; 83 | } 84 | 85 | .issue-detail__comment__body { 86 | padding-left: 1rem; 87 | margin-bottom: 1rem; 88 | } 89 | 90 | .issue-detail__comment__avatar { 91 | width: 40px; 92 | height: 40px; 93 | border-radius: 50%; 94 | margin-right: 0.5rem; 95 | min-width: 40px; 96 | box-shadow: 0 0 2px rgba(0,0,0,0.5); 97 | } 98 | 99 | .issue-detail__comment__username { 100 | font-weight: bold; 101 | } -------------------------------------------------------------------------------- /src/containers/IssueDetailPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { getIssue, getComments } from '../redux/actions'; 3 | import { connect } from 'react-redux'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import { insertMentionLinks } from '../utils/stringUtils'; 6 | import UserWithAvatar from '../components/UserWithAvatar'; 7 | import IssueLabels from '../components/IssueLabels'; 8 | import IssueComments from '../components/IssueComments'; 9 | import './IssueDetailPage.css'; 10 | 11 | const IssueState = ({ issue: {state} }) => ( 12 | 13 | {state} 14 | 15 | ); 16 | 17 | const IssueNumber = ({ issue }) => ( 18 | 19 | #{issue.number} 20 | 21 | ); 22 | 23 | export class IssueDetailPage extends Component { 24 | componentDidMount() { 25 | // Fetch the issue if we weren't given one 26 | if(!this.props.issue) { 27 | this.props.getIssue(); 28 | } else { 29 | // If we have the issue already, get its comments 30 | this.props.getComments(this.props.issue); 31 | } 32 | } 33 | 34 | componentWillReceiveProps(newProps) { 35 | if(newProps.issue !== this.props.issue) { 36 | this.props.getComments(newProps.issue); 37 | } 38 | } 39 | 40 | renderComments() { 41 | const {issue, comments, commentsError} = this.props; 42 | 43 | // Return early if there's an error 44 | if(commentsError) { 45 | return ( 46 |
    47 | There was a problem fetching the comments. 48 |
    49 | ); 50 | } 51 | 52 | // The issue has no comments 53 | if(issue.comments === 0) { 54 | return
    No comments
    ; 55 | } 56 | 57 | // The issue has comments, but they're not loaded yet 58 | if(!comments || comments.length === 0) { 59 | return
    Comments loading...
    60 | } 61 | 62 | // Comments are loaded 63 | return ; 64 | } 65 | 66 | renderContent() { 67 | const {issue} = this.props; 68 | 69 | return ( 70 |
    71 |

    {issue.title}

    72 |
    73 | 74 | 75 | 76 |
    77 | 78 |
    79 |
    80 | 81 |
    82 |
    83 | {this.renderComments()} 84 |
    85 | ); 86 | } 87 | 88 | renderLoading() { 89 | return ( 90 |
    91 |

    Loading issue #{this.props.params.issueId}...

    92 |
    93 | ); 94 | } 95 | 96 | render() { 97 | const {issue, issueError, params} = this.props; 98 | 99 | if(issueError) { 100 | return ( 101 |
    102 |

    There was a problem loading issue #{params.issueId}

    103 |

    {issueError.toString()}

    104 |
    105 | ); 106 | } 107 | 108 | return ( 109 |
    110 | {issue && this.renderContent()} 111 | {!issue && this.renderLoading()} 112 |
    113 | ); 114 | } 115 | } 116 | 117 | IssueDetailPage.propTypes = { 118 | params: PropTypes.shape({ 119 | issueId: PropTypes.string.isRequired 120 | }).isRequired, 121 | issue: PropTypes.object, 122 | issueError: PropTypes.object, 123 | comments: PropTypes.array, 124 | commentsError: PropTypes.object, 125 | getIssue: PropTypes.func.isRequired, 126 | getComments: PropTypes.func.isRequired 127 | }; 128 | 129 | const mapState = ({ issues, commentsByIssue }, ownProps) => { 130 | const issueNum = ownProps.params.issueId; 131 | return { 132 | issue: issues.issuesByNumber[issueNum], 133 | issueError: issues.error, 134 | comments: commentsByIssue[issueNum], 135 | commentsError: commentsByIssue['error'] 136 | }; 137 | }; 138 | 139 | const mapDispatch = (dispatch, ownProps) => { 140 | const {org, repo, issueId} = ownProps.params; 141 | return { 142 | getIssue: () => dispatch(getIssue(org, repo, issueId)), 143 | getComments: (issue) => { 144 | return dispatch(getComments(issue)); 145 | } 146 | }; 147 | }; 148 | 149 | export default connect(mapState, mapDispatch)(IssueDetailPage); 150 | -------------------------------------------------------------------------------- /src/containers/IssueDetailPage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import { IssueDetailPage } from './IssueDetailPage' 4 | import { checkSnapshot } from '../utils/testUtils'; 5 | import page1 from '../fixtures/issues/page1'; 6 | 7 | const params = {issueId: "1"}; 8 | const issue = page1.data[0]; 9 | 10 | it('renders loading', () => { 11 | const tree = shallow( 12 | 16 | ); 17 | checkSnapshot(tree); 18 | }); 19 | 20 | it('renders an error when issue does not load', () => { 21 | const tree = shallow( 22 | 27 | ); 28 | checkSnapshot(tree); 29 | }); 30 | 31 | it('renders an issue that has no comments', () => { 32 | const tree = shallow( 33 | 38 | ); 39 | checkSnapshot(tree); 40 | }); 41 | 42 | it('renders an issue that has comments that are being loaded', () => { 43 | const tree = shallow( 44 | 49 | ); 50 | checkSnapshot(tree); 51 | }); 52 | 53 | it('renders an issue with its loaded comments', () => { 54 | let issueWithComments = {...issue, comments: 2}; 55 | let comments = [{id: 1}, {id: 2}]; 56 | 57 | const tree = shallow( 58 | 64 | ); 65 | checkSnapshot(tree); 66 | }); 67 | 68 | it('renders an issue with a comment-loading error', () => { 69 | const error = new Error('something went wrong'); 70 | const tree = shallow( 71 | 77 | ); 78 | checkSnapshot(tree); 79 | }); 80 | 81 | describe('fetching data', () => { 82 | let getIssue, getComments; 83 | 84 | beforeEach(() => { 85 | getIssue = jest.fn(); 86 | getComments = jest.fn(); 87 | }); 88 | 89 | it('fetches the issue, but not comments yet', () => { 90 | const tree = mount( 91 | 96 | ); 97 | expect(getIssue).toBeCalled(); 98 | expect(getComments).not.toBeCalled(); 99 | }); 100 | 101 | it('fetches comments if issue is passed in', () => { 102 | const tree = mount( 103 | 108 | ); 109 | expect(getIssue).not.toBeCalled(); 110 | expect(getComments).toBeCalled(); 111 | }); 112 | 113 | it('fetches comments after the issue loads', () => { 114 | const tree = mount( 115 | 120 | ); 121 | 122 | expect(getIssue).toBeCalled(); 123 | expect(getComments).not.toBeCalled(); 124 | 125 | tree.setProps({ issue }); 126 | expect(getComments).toBeCalled(); 127 | }); 128 | }); -------------------------------------------------------------------------------- /src/containers/IssueListPage.css: -------------------------------------------------------------------------------- 1 | .issues__pagination { 2 | padding-top: 1rem; 3 | text-align: center; 4 | } 5 | 6 | .issues__pagination > ul { 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | .issues__pagination li { 12 | display: inline-block; 13 | } 14 | .issues__pagination li > a { 15 | padding: 0.5rem; 16 | margin: 1px; 17 | display: inline-block; 18 | cursor: pointer; 19 | background-color: #fff; 20 | border: 1px solid #d8eef5; 21 | border-radius: 2px; 22 | min-width: 1rem; 23 | } 24 | 25 | .issues__pagination li > a:focus { 26 | outline: none; 27 | } 28 | 29 | .issues__pagination li > a:hover { 30 | background-color: #d8eef5; 31 | } 32 | 33 | .issues__pagination .selected a { 34 | box-shadow: 0 0 2px #1b7d9e; 35 | background-color: #f3fcff; 36 | } 37 | 38 | .issues__pagination .disabled > a { 39 | color: #ccc; 40 | background-color: #f8f8f8; 41 | border-color: #eee; 42 | cursor: default; 43 | } 44 | .issues__pagination .disabled > a:hover { 45 | background-color: #f8f8f8; 46 | } 47 | .issues__pagination .break { 48 | margin: 0 8px; 49 | } 50 | .issues__pagination .previous { 51 | margin-right: 1rem; 52 | } 53 | .issues__pagination .next { 54 | margin-left: 1rem; 55 | } -------------------------------------------------------------------------------- /src/containers/IssueListPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import IssueList from '../components/IssueList'; 4 | import { getIssues, getRepoDetails } from '../redux/actions'; 5 | import Paginate from 'react-paginate'; 6 | import FakeIssueList from '../components/FakeIssueList'; 7 | import './IssueListPage.css'; 8 | 9 | function Header({ openIssuesCount, org, repo }) { 10 | if(openIssuesCount === -1) { 11 | return ( 12 |

    13 | Open issues for 14 |

    15 | ); 16 | } else { 17 | const pluralizedIssue = openIssuesCount === 1 ? 'issue' : 'issues'; 18 | return ( 19 |

    20 | {openIssuesCount} open {pluralizedIssue} for 21 |

    22 | ); 23 | } 24 | } 25 | 26 | function OrgRepo({ org, repo }) { 27 | return ( 28 | 29 | {org} 30 | {' / '} 31 | {repo} 32 | 33 | ) 34 | } 35 | 36 | export class IssueListPage extends Component { 37 | static contextTypes = { 38 | router: PropTypes.object.isRequired 39 | }; 40 | 41 | componentDidMount() { 42 | const {getIssues, getRepoDetails, org, repo} = this.props; 43 | 44 | const currentPage = Math.max(1, 45 | (parseInt(this.props.location.query.page, 10) || 1) 46 | ); 47 | 48 | getRepoDetails(org, repo); 49 | getIssues(org, repo, currentPage); 50 | } 51 | 52 | handlePageChange = ({ selected }) => { 53 | const newPage = selected + 1; 54 | 55 | this.context.router.push({ 56 | pathname: this.props.location.pathname, 57 | query: { page: newPage } 58 | }); 59 | } 60 | 61 | componentWillReceiveProps(newProps) { 62 | const {getIssues, org, repo, location} = newProps; 63 | 64 | // Fetch new issues whenever the page changes 65 | if(location.query.page !== this.props.location.query.page) { 66 | getIssues(org, repo, location.query.page); 67 | } 68 | } 69 | 70 | render() { 71 | const { 72 | org, repo, isLoading, issues, 73 | pageCount, openIssuesCount, issuesError, 74 | location 75 | } = this.props; 76 | 77 | if(issuesError) { 78 | return ( 79 |
    80 |

    Something went wrong...

    81 |
    {issuesError.toString()}
    82 |
    83 | ); 84 | } 85 | 86 | const currentPage = Math.min( 87 | pageCount, 88 | Math.max(1, parseInt(location.query.page, 10) || 1) 89 | ) - 1; 90 | 91 | return ( 92 |
    93 |
    94 | {isLoading 95 | ? 96 | : 97 | } 98 |
    99 | 107 |
    108 |
    109 | ); 110 | } 111 | } 112 | 113 | IssueListPage.propTypes = { 114 | org: PropTypes.string.isRequired, 115 | repo: PropTypes.string.isRequired, 116 | issues: PropTypes.array.isRequired, 117 | openIssuesCount: PropTypes.number.isRequired, 118 | isLoading: PropTypes.bool.isRequired, 119 | pageCount: PropTypes.number.isRequired 120 | }; 121 | 122 | const selectIssues = issues => 123 | issues.currentPageIssues.map(number => issues.issuesByNumber[number]); 124 | 125 | const mapStateToProps = ({ issues, repo }, ownProps) => ({ 126 | issues: selectIssues(issues), 127 | issuesError: issues.error, 128 | openIssuesCount: repo.openIssuesCount, 129 | isLoading: issues.isLoading, 130 | pageCount: issues.pageCount, 131 | org: ownProps.params.org, 132 | repo: ownProps.params.repo 133 | }); 134 | 135 | const mapDispatch = { getIssues, getRepoDetails }; 136 | 137 | export default connect(mapStateToProps, mapDispatch)(IssueListPage); 138 | -------------------------------------------------------------------------------- /src/containers/IssueListPage.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../api'); 2 | 3 | import React from 'react'; 4 | import { shallow, mount } from 'enzyme'; 5 | import { checkSnapshot, afterPromises } from '../utils/testUtils'; 6 | import * as API from '../api'; 7 | import { IssueListPage } from './IssueListPage'; 8 | import page1 from '../fixtures/issues/page1.json'; 9 | 10 | // Jest doesn't handle \r\n in snapshots very well. 11 | // https://github.com/facebook/jest/pull/1879#issuecomment-261019033 12 | // Replace the body with some plain text 13 | page1.data = page1.data.map(issue => ({ 14 | ...issue, 15 | body: 'something without newlines' 16 | })); 17 | 18 | const location = { 19 | query: {} 20 | }; 21 | 22 | const context = { 23 | router: {} 24 | }; 25 | 26 | it('renders while loading', () => { 27 | const tree = shallow( 28 | , 37 | { context } 38 | ); 39 | checkSnapshot(tree); 40 | }); 41 | 42 | it('renders with issues', () => { 43 | const tree = shallow( 44 | , 53 | { context } 54 | ); 55 | checkSnapshot(tree); 56 | }); 57 | 58 | it('calls getIssues and getRepoDetails', () => { 59 | let mockGetIssues = jest.fn(); 60 | let mockGetRepoDetails = jest.fn(); 61 | 62 | const tree = mount( 63 | , 73 | { context } 74 | ); 75 | 76 | expect(mockGetIssues).toBeCalled(); 77 | expect(mockGetRepoDetails).toBeCalled(); 78 | }); 79 | 80 | describe('fetching data', () => { 81 | let getIssues, getRepoDetails; 82 | 83 | beforeEach(() => { 84 | getIssues = jest.fn(); 85 | getRepoDetails = jest.fn(); 86 | }); 87 | 88 | const render = ({...props}) => { 89 | return mount( 90 | , 101 | { context } 102 | ); 103 | }; 104 | 105 | it('fetches data on mount w/ no page set', () => { 106 | render({ 107 | location: {query: {}} 108 | }); 109 | expect(getIssues).toBeCalledWith("rails", "rails", 1); 110 | }); 111 | 112 | it('fetches data based on page from url', () => { 113 | render({ 114 | location: {query: {page: 7}} 115 | }); 116 | expect(getIssues).toBeCalledWith("rails", "rails", 7); 117 | }); 118 | 119 | it('fetches data when page changes', () => { 120 | const wrapper = render({ 121 | location: {query: {page: 7}} 122 | }); 123 | expect(getIssues).toBeCalledWith("rails", "rails", 7); 124 | wrapper.setProps({location: {query: {page: 8}}}); 125 | expect(getIssues).toBeCalledWith("rails", "rails", 8); 126 | }) 127 | }); 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/fixtures/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/repos/rails/rails/issues/comments/270990369", 4 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-270990369", 5 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 6 | "id": 270990369, 7 | "user": { 8 | "login": "utilum", 9 | "id": 6261109, 10 | "avatar_url": "https://avatars.githubusercontent.com/u/6261109?v=3", 11 | "gravatar_id": "", 12 | "url": "https://api.github.com/users/utilum", 13 | "html_url": "https://github.com/utilum", 14 | "followers_url": "https://api.github.com/users/utilum/followers", 15 | "following_url": "https://api.github.com/users/utilum/following{/other_user}", 16 | "gists_url": "https://api.github.com/users/utilum/gists{/gist_id}", 17 | "starred_url": "https://api.github.com/users/utilum/starred{/owner}{/repo}", 18 | "subscriptions_url": "https://api.github.com/users/utilum/subscriptions", 19 | "organizations_url": "https://api.github.com/users/utilum/orgs", 20 | "repos_url": "https://api.github.com/users/utilum/repos", 21 | "events_url": "https://api.github.com/users/utilum/events{/privacy}", 22 | "received_events_url": "https://api.github.com/users/utilum/received_events", 23 | "type": "User", 24 | "site_admin": false 25 | }, 26 | "created_at": "2017-01-06T19:54:10Z", 27 | "updated_at": "2017-01-06T19:54:10Z", 28 | "body": "Can reproduce." 29 | }, 30 | { 31 | "url": "https://api.github.com/repos/rails/rails/issues/comments/270990414", 32 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-270990414", 33 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 34 | "id": 270990414, 35 | "user": { 36 | "login": "jtrost", 37 | "id": 1156272, 38 | "avatar_url": "https://avatars.githubusercontent.com/u/1156272?v=3", 39 | "gravatar_id": "", 40 | "url": "https://api.github.com/users/jtrost", 41 | "html_url": "https://github.com/jtrost", 42 | "followers_url": "https://api.github.com/users/jtrost/followers", 43 | "following_url": "https://api.github.com/users/jtrost/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/jtrost/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/jtrost/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/jtrost/subscriptions", 47 | "organizations_url": "https://api.github.com/users/jtrost/orgs", 48 | "repos_url": "https://api.github.com/users/jtrost/repos", 49 | "events_url": "https://api.github.com/users/jtrost/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/jtrost/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | }, 54 | "created_at": "2017-01-06T19:54:20Z", 55 | "updated_at": "2017-01-06T19:54:20Z", 56 | "body": "One thing that stands out about your code is that the Invite class does not define the `<=>` operator. The [Comparable documentation](https://ruby-doc.org/core-2.3.3/Comparable.html) says:\r\n>The class must define the <=> operator, which compares the receiver against another object, returning -1, 0, or +1 depending on whether the receiver is less than, equal to, or greater than the other object.\r\n\r\nIt's not clear what the Invite class is comparing, however if you add this method to the class then the test will pass.\r\n````ruby\r\ndef <=>(other)\r\n 1\r\nend\r\n````" 57 | }, 58 | { 59 | "url": "https://api.github.com/repos/rails/rails/issues/comments/270992272", 60 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-270992272", 61 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 62 | "id": 270992272, 63 | "user": { 64 | "login": "jnimety", 65 | "id": 70484, 66 | "avatar_url": "https://avatars.githubusercontent.com/u/70484?v=3", 67 | "gravatar_id": "", 68 | "url": "https://api.github.com/users/jnimety", 69 | "html_url": "https://github.com/jnimety", 70 | "followers_url": "https://api.github.com/users/jnimety/followers", 71 | "following_url": "https://api.github.com/users/jnimety/following{/other_user}", 72 | "gists_url": "https://api.github.com/users/jnimety/gists{/gist_id}", 73 | "starred_url": "https://api.github.com/users/jnimety/starred{/owner}{/repo}", 74 | "subscriptions_url": "https://api.github.com/users/jnimety/subscriptions", 75 | "organizations_url": "https://api.github.com/users/jnimety/orgs", 76 | "repos_url": "https://api.github.com/users/jnimety/repos", 77 | "events_url": "https://api.github.com/users/jnimety/events{/privacy}", 78 | "received_events_url": "https://api.github.com/users/jnimety/received_events", 79 | "type": "User", 80 | "site_admin": false 81 | }, 82 | "created_at": "2017-01-06T20:02:07Z", 83 | "updated_at": "2017-01-06T20:02:07Z", 84 | "body": "It still fails if I add a `score` column and use\r\n\r\n```\r\ndef <=>(other)\r\n other.score <=> self.score\r\nend\r\n```" 85 | }, 86 | { 87 | "url": "https://api.github.com/repos/rails/rails/issues/comments/270997315", 88 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-270997315", 89 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 90 | "id": 270997315, 91 | "user": { 92 | "login": "alexcameron89", 93 | "id": 5634381, 94 | "avatar_url": "https://avatars.githubusercontent.com/u/5634381?v=3", 95 | "gravatar_id": "", 96 | "url": "https://api.github.com/users/alexcameron89", 97 | "html_url": "https://github.com/alexcameron89", 98 | "followers_url": "https://api.github.com/users/alexcameron89/followers", 99 | "following_url": "https://api.github.com/users/alexcameron89/following{/other_user}", 100 | "gists_url": "https://api.github.com/users/alexcameron89/gists{/gist_id}", 101 | "starred_url": "https://api.github.com/users/alexcameron89/starred{/owner}{/repo}", 102 | "subscriptions_url": "https://api.github.com/users/alexcameron89/subscriptions", 103 | "organizations_url": "https://api.github.com/users/alexcameron89/orgs", 104 | "repos_url": "https://api.github.com/users/alexcameron89/repos", 105 | "events_url": "https://api.github.com/users/alexcameron89/events{/privacy}", 106 | "received_events_url": "https://api.github.com/users/alexcameron89/received_events", 107 | "type": "User", 108 | "site_admin": false 109 | }, 110 | "created_at": "2017-01-06T20:25:29Z", 111 | "updated_at": "2017-01-06T20:25:29Z", 112 | "body": "This looks to be fixed on master by @sgrif with 15ddd5179c746a2201b9805edf0764f515da8d7a." 113 | }, 114 | { 115 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271002157", 116 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271002157", 117 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 118 | "id": 271002157, 119 | "user": { 120 | "login": "jnimety", 121 | "id": 70484, 122 | "avatar_url": "https://avatars.githubusercontent.com/u/70484?v=3", 123 | "gravatar_id": "", 124 | "url": "https://api.github.com/users/jnimety", 125 | "html_url": "https://github.com/jnimety", 126 | "followers_url": "https://api.github.com/users/jnimety/followers", 127 | "following_url": "https://api.github.com/users/jnimety/following{/other_user}", 128 | "gists_url": "https://api.github.com/users/jnimety/gists{/gist_id}", 129 | "starred_url": "https://api.github.com/users/jnimety/starred{/owner}{/repo}", 130 | "subscriptions_url": "https://api.github.com/users/jnimety/subscriptions", 131 | "organizations_url": "https://api.github.com/users/jnimety/orgs", 132 | "repos_url": "https://api.github.com/users/jnimety/repos", 133 | "events_url": "https://api.github.com/users/jnimety/events{/privacy}", 134 | "received_events_url": "https://api.github.com/users/jnimety/received_events", 135 | "type": "User", 136 | "site_admin": false 137 | }, 138 | "created_at": "2017-01-06T20:48:38Z", 139 | "updated_at": "2017-01-06T20:48:38Z", 140 | "body": "fantastic, thanks for looking into this." 141 | }, 142 | { 143 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271003232", 144 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271003232", 145 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 146 | "id": 271003232, 147 | "user": { 148 | "login": "sgrif", 149 | "id": 1529387, 150 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 151 | "gravatar_id": "", 152 | "url": "https://api.github.com/users/sgrif", 153 | "html_url": "https://github.com/sgrif", 154 | "followers_url": "https://api.github.com/users/sgrif/followers", 155 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 156 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 157 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 158 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 159 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 160 | "repos_url": "https://api.github.com/users/sgrif/repos", 161 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 162 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 163 | "type": "User", 164 | "site_admin": false 165 | }, 166 | "created_at": "2017-01-06T20:53:26Z", 167 | "updated_at": "2017-01-06T20:53:26Z", 168 | "body": "Hm... That was committed to 5-0-stable several months before 5.0.1 was released though. I'm curious why it wasn't included." 169 | }, 170 | { 171 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271003624", 172 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271003624", 173 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 174 | "id": 271003624, 175 | "user": { 176 | "login": "sgrif", 177 | "id": 1529387, 178 | "avatar_url": "https://avatars.githubusercontent.com/u/1529387?v=3", 179 | "gravatar_id": "", 180 | "url": "https://api.github.com/users/sgrif", 181 | "html_url": "https://github.com/sgrif", 182 | "followers_url": "https://api.github.com/users/sgrif/followers", 183 | "following_url": "https://api.github.com/users/sgrif/following{/other_user}", 184 | "gists_url": "https://api.github.com/users/sgrif/gists{/gist_id}", 185 | "starred_url": "https://api.github.com/users/sgrif/starred{/owner}{/repo}", 186 | "subscriptions_url": "https://api.github.com/users/sgrif/subscriptions", 187 | "organizations_url": "https://api.github.com/users/sgrif/orgs", 188 | "repos_url": "https://api.github.com/users/sgrif/repos", 189 | "events_url": "https://api.github.com/users/sgrif/events{/privacy}", 190 | "received_events_url": "https://api.github.com/users/sgrif/received_events", 191 | "type": "User", 192 | "site_admin": false 193 | }, 194 | "created_at": "2017-01-06T20:55:26Z", 195 | "updated_at": "2017-01-06T20:55:26Z", 196 | "body": "I'm being thick. That commit was in 5.0.1. I don't think that commit fixed this issue." 197 | }, 198 | { 199 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271006988", 200 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271006988", 201 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 202 | "id": 271006988, 203 | "user": { 204 | "login": "jnimety", 205 | "id": 70484, 206 | "avatar_url": "https://avatars.githubusercontent.com/u/70484?v=3", 207 | "gravatar_id": "", 208 | "url": "https://api.github.com/users/jnimety", 209 | "html_url": "https://github.com/jnimety", 210 | "followers_url": "https://api.github.com/users/jnimety/followers", 211 | "following_url": "https://api.github.com/users/jnimety/following{/other_user}", 212 | "gists_url": "https://api.github.com/users/jnimety/gists{/gist_id}", 213 | "starred_url": "https://api.github.com/users/jnimety/starred{/owner}{/repo}", 214 | "subscriptions_url": "https://api.github.com/users/jnimety/subscriptions", 215 | "organizations_url": "https://api.github.com/users/jnimety/orgs", 216 | "repos_url": "https://api.github.com/users/jnimety/repos", 217 | "events_url": "https://api.github.com/users/jnimety/events{/privacy}", 218 | "received_events_url": "https://api.github.com/users/jnimety/received_events", 219 | "type": "User", 220 | "site_admin": false 221 | }, 222 | "created_at": "2017-01-06T21:09:19Z", 223 | "updated_at": "2017-01-06T21:09:19Z", 224 | "body": "here's an updated test, it fails as long as the compare method returns equality (0).\r\n\r\n```\r\nbegin\r\n require \"bundler/inline\"\r\nrescue LoadError => e\r\n $stderr.puts \"Bundler version 1.10 or later is required. Please update your Bundler\"\r\n raise e\r\nend\r\n\r\ngemfile(true) do\r\n source \"https://rubygems.org\"\r\n gem \"activerecord\", \"5.0.1\"\r\n #gem \"activerecord\", \"5.0.0.1\"\r\n gem \"pg\"\r\nend\r\n\r\nrequire \"active_record\"\r\nrequire \"minitest/autorun\"\r\nrequire \"logger\"\r\n\r\n# This connection will do for database-independent bug reports.\r\nActiveRecord::Base.establish_connection adapter: \"postgresql\"\r\nActiveRecord::Base.connection.drop_database \"test_has_many_through_with_comparable\"\r\nActiveRecord::Base.connection.create_database \"test_has_many_through_with_comparable\"\r\nActiveRecord::Base.establish_connection adapter: \"postgresql\", database: \"test_has_many_through_with_comparable\"\r\nActiveRecord::Base.logger = Logger.new(STDOUT)\r\n\r\nActiveRecord::Schema.define do\r\n create_table :events, force: true do |t|\r\n end\r\n\r\n create_table :invites, force: true do |t|\r\n t.integer :event_id\r\n t.integer :user_id\r\n end\r\n\r\n create_table :users, force: true do |t|\r\n end\r\nend\r\n\r\nclass Event < ActiveRecord::Base\r\n has_many :invites\r\n\r\n has_many :users, :through => :invites\r\nend\r\n\r\nclass Invite < ActiveRecord::Base\r\n include Comparable\r\n\r\n belongs_to :event\r\n belongs_to :user\r\n\r\n def <=>(other)\r\n 0\r\n end\r\nend\r\n\r\nclass User < ActiveRecord::Base\r\nend\r\n\r\nclass BugTest < Minitest::Test\r\n def test_has_many_through_with_comparable\r\n user1 = User.create\r\n user2 = User.create\r\n\r\n event = Event.create(user_ids: [user1.id, user2.id]) # Invite#event_id does not get set for user2\r\n\r\n assert_equal event.invites.count, 2\r\n end\r\nend\r\n```" 225 | }, 226 | { 227 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271032552", 228 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271032552", 229 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 230 | "id": 271032552, 231 | "user": { 232 | "login": "maclover7", 233 | "id": 3020626, 234 | "avatar_url": "https://avatars.githubusercontent.com/u/3020626?v=3", 235 | "gravatar_id": "", 236 | "url": "https://api.github.com/users/maclover7", 237 | "html_url": "https://github.com/maclover7", 238 | "followers_url": "https://api.github.com/users/maclover7/followers", 239 | "following_url": "https://api.github.com/users/maclover7/following{/other_user}", 240 | "gists_url": "https://api.github.com/users/maclover7/gists{/gist_id}", 241 | "starred_url": "https://api.github.com/users/maclover7/starred{/owner}{/repo}", 242 | "subscriptions_url": "https://api.github.com/users/maclover7/subscriptions", 243 | "organizations_url": "https://api.github.com/users/maclover7/orgs", 244 | "repos_url": "https://api.github.com/users/maclover7/repos", 245 | "events_url": "https://api.github.com/users/maclover7/events{/privacy}", 246 | "received_events_url": "https://api.github.com/users/maclover7/received_events", 247 | "type": "User", 248 | "site_admin": false 249 | }, 250 | "created_at": "2017-01-06T22:54:24Z", 251 | "updated_at": "2017-01-06T22:59:05Z", 252 | "body": "Marking as a regression since the tests pass on `4-2-stable`, but are failing on v5.0.1. Investigating...\r\n\r\nEDIT: Per git bisect, it looks like 15ddd5179c746a2201b9805edf0764f515da8d7a is the commit that introduces the change in behavior in Active Record." 253 | }, 254 | { 255 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271080684", 256 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271080684", 257 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 258 | "id": 271080684, 259 | "user": { 260 | "login": "kamipo", 261 | "id": 12642, 262 | "avatar_url": "https://avatars.githubusercontent.com/u/12642?v=3", 263 | "gravatar_id": "", 264 | "url": "https://api.github.com/users/kamipo", 265 | "html_url": "https://github.com/kamipo", 266 | "followers_url": "https://api.github.com/users/kamipo/followers", 267 | "following_url": "https://api.github.com/users/kamipo/following{/other_user}", 268 | "gists_url": "https://api.github.com/users/kamipo/gists{/gist_id}", 269 | "starred_url": "https://api.github.com/users/kamipo/starred{/owner}{/repo}", 270 | "subscriptions_url": "https://api.github.com/users/kamipo/subscriptions", 271 | "organizations_url": "https://api.github.com/users/kamipo/orgs", 272 | "repos_url": "https://api.github.com/users/kamipo/repos", 273 | "events_url": "https://api.github.com/users/kamipo/events{/privacy}", 274 | "received_events_url": "https://api.github.com/users/kamipo/received_events", 275 | "type": "User", 276 | "site_admin": false 277 | }, 278 | "created_at": "2017-01-07T12:25:35Z", 279 | "updated_at": "2017-01-07T12:25:35Z", 280 | "body": "Fixed in #27442 on master." 281 | }, 282 | { 283 | "url": "https://api.github.com/repos/rails/rails/issues/comments/271083985", 284 | "html_url": "https://github.com/rails/rails/issues/27595#issuecomment-271083985", 285 | "issue_url": "https://api.github.com/repos/rails/rails/issues/27595", 286 | "id": 271083985, 287 | "user": { 288 | "login": "maclover7", 289 | "id": 3020626, 290 | "avatar_url": "https://avatars.githubusercontent.com/u/3020626?v=3", 291 | "gravatar_id": "", 292 | "url": "https://api.github.com/users/maclover7", 293 | "html_url": "https://github.com/maclover7", 294 | "followers_url": "https://api.github.com/users/maclover7/followers", 295 | "following_url": "https://api.github.com/users/maclover7/following{/other_user}", 296 | "gists_url": "https://api.github.com/users/maclover7/gists{/gist_id}", 297 | "starred_url": "https://api.github.com/users/maclover7/starred{/owner}{/repo}", 298 | "subscriptions_url": "https://api.github.com/users/maclover7/subscriptions", 299 | "organizations_url": "https://api.github.com/users/maclover7/orgs", 300 | "repos_url": "https://api.github.com/users/maclover7/repos", 301 | "events_url": "https://api.github.com/users/maclover7/events{/privacy}", 302 | "received_events_url": "https://api.github.com/users/maclover7/received_events", 303 | "type": "User", 304 | "site_admin": false 305 | }, 306 | "created_at": "2017-01-07T13:31:26Z", 307 | "updated_at": "2017-01-07T13:31:26Z", 308 | "body": "👍 Added `needs backport` label on that PR, so hopefully will be backported to `5-0-stable` soon by the core team." 309 | } 310 | ] -------------------------------------------------------------------------------- /src/fixtures/issues/page49.json: -------------------------------------------------------------------------------- 1 | { 2 | "link": "; rel=\"first\", ; rel=\"prev\"", 3 | "data": 4 | [ 5 | { 6 | "url": "https://api.github.com/repos/rails/rails/issues/8679", 7 | "repository_url": "https://api.github.com/repos/rails/rails", 8 | "labels_url": "https://api.github.com/repos/rails/rails/issues/8679/labels{/name}", 9 | "comments_url": "https://api.github.com/repos/rails/rails/issues/8679/comments", 10 | "events_url": "https://api.github.com/repos/rails/rails/issues/8679/events", 11 | "html_url": "https://github.com/rails/rails/issues/8679", 12 | "id": 9606495, 13 | "number": 8679, 14 | "title": "assert_recognizes don't aware of constraints", 15 | "user": { 16 | "login": "mmontossi", 17 | "id": 1836781, 18 | "avatar_url": "https://avatars.githubusercontent.com/u/1836781?v=3", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/mmontossi", 21 | "html_url": "https://github.com/mmontossi", 22 | "followers_url": "https://api.github.com/users/mmontossi/followers", 23 | "following_url": "https://api.github.com/users/mmontossi/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/mmontossi/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/mmontossi/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/mmontossi/subscriptions", 27 | "organizations_url": "https://api.github.com/users/mmontossi/orgs", 28 | "repos_url": "https://api.github.com/users/mmontossi/repos", 29 | "events_url": "https://api.github.com/users/mmontossi/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/mmontossi/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "labels": [ 35 | { 36 | "id": 107189, 37 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 38 | "name": "actionpack", 39 | "color": "FFF700", 40 | "default": false 41 | }, 42 | { 43 | "id": 41328116, 44 | "url": "https://api.github.com/repos/rails/rails/labels/attached%20PR", 45 | "name": "attached PR", 46 | "color": "006b75", 47 | "default": false 48 | }, 49 | { 50 | "id": 149514554, 51 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 52 | "name": "pinned", 53 | "color": "f7c6c7", 54 | "default": false 55 | } 56 | ], 57 | "state": "open", 58 | "locked": false, 59 | "assignee": { 60 | "login": "pixeltrix", 61 | "id": 6321, 62 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 63 | "gravatar_id": "", 64 | "url": "https://api.github.com/users/pixeltrix", 65 | "html_url": "https://github.com/pixeltrix", 66 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 67 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 68 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 69 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 70 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 71 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 72 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 73 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 74 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 75 | "type": "User", 76 | "site_admin": false 77 | }, 78 | "assignees": [ 79 | { 80 | "login": "pixeltrix", 81 | "id": 6321, 82 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 83 | "gravatar_id": "", 84 | "url": "https://api.github.com/users/pixeltrix", 85 | "html_url": "https://github.com/pixeltrix", 86 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 87 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 88 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 89 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 90 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 91 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 92 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 93 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 94 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 95 | "type": "User", 96 | "site_admin": false 97 | } 98 | ], 99 | "milestone": null, 100 | "comments": 12, 101 | "created_at": "2013-01-01T22:48:56Z", 102 | "updated_at": "2016-04-20T21:02:10Z", 103 | "closed_at": null, 104 | "body": "I notice in the method recognized_request_for used in assert_recognizes that it's not aware of the constraints of a route, so will always trigger an RoutingError exception.\n\nhttps://github.com/rails/rails/blob/v3.2.9.rc3/actionpack/lib/action_dispatch/testing/assertions/routing.rb#L210\n" 105 | }, 106 | { 107 | "url": "https://api.github.com/repos/rails/rails/issues/8343", 108 | "repository_url": "https://api.github.com/repos/rails/rails", 109 | "labels_url": "https://api.github.com/repos/rails/rails/issues/8343/labels{/name}", 110 | "comments_url": "https://api.github.com/repos/rails/rails/issues/8343/comments", 111 | "events_url": "https://api.github.com/repos/rails/rails/issues/8343/events", 112 | "html_url": "https://github.com/rails/rails/pull/8343", 113 | "id": 8742501, 114 | "number": 8343, 115 | "title": "Add Touch method to ActiveRelation", 116 | "user": { 117 | "login": "duggiefresh", 118 | "id": 1206678, 119 | "avatar_url": "https://avatars.githubusercontent.com/u/1206678?v=3", 120 | "gravatar_id": "", 121 | "url": "https://api.github.com/users/duggiefresh", 122 | "html_url": "https://github.com/duggiefresh", 123 | "followers_url": "https://api.github.com/users/duggiefresh/followers", 124 | "following_url": "https://api.github.com/users/duggiefresh/following{/other_user}", 125 | "gists_url": "https://api.github.com/users/duggiefresh/gists{/gist_id}", 126 | "starred_url": "https://api.github.com/users/duggiefresh/starred{/owner}{/repo}", 127 | "subscriptions_url": "https://api.github.com/users/duggiefresh/subscriptions", 128 | "organizations_url": "https://api.github.com/users/duggiefresh/orgs", 129 | "repos_url": "https://api.github.com/users/duggiefresh/repos", 130 | "events_url": "https://api.github.com/users/duggiefresh/events{/privacy}", 131 | "received_events_url": "https://api.github.com/users/duggiefresh/received_events", 132 | "type": "User", 133 | "site_admin": false 134 | }, 135 | "labels": [ 136 | { 137 | "id": 107191, 138 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 139 | "name": "activerecord", 140 | "color": "0b02e1", 141 | "default": false 142 | } 143 | ], 144 | "state": "open", 145 | "locked": false, 146 | "assignee": { 147 | "login": "rafaelfranca", 148 | "id": 47848, 149 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 150 | "gravatar_id": "", 151 | "url": "https://api.github.com/users/rafaelfranca", 152 | "html_url": "https://github.com/rafaelfranca", 153 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 154 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 155 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 156 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 157 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 158 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 159 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 160 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 161 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 162 | "type": "User", 163 | "site_admin": false 164 | }, 165 | "assignees": [ 166 | { 167 | "login": "rafaelfranca", 168 | "id": 47848, 169 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 170 | "gravatar_id": "", 171 | "url": "https://api.github.com/users/rafaelfranca", 172 | "html_url": "https://github.com/rafaelfranca", 173 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 174 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 175 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 176 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 177 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 178 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 179 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 180 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 181 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 182 | "type": "User", 183 | "site_admin": false 184 | } 185 | ], 186 | "milestone": null, 187 | "comments": 20, 188 | "created_at": "2012-11-27T23:29:13Z", 189 | "updated_at": "2016-06-16T18:58:52Z", 190 | "closed_at": null, 191 | "pull_request": { 192 | "url": "https://api.github.com/repos/rails/rails/pulls/8343", 193 | "html_url": "https://github.com/rails/rails/pull/8343", 194 | "diff_url": "https://github.com/rails/rails/pull/8343.diff", 195 | "patch_url": "https://github.com/rails/rails/pull/8343.patch" 196 | }, 197 | "body": "This allows for more efficient touching of many records\n" 198 | }, 199 | { 200 | "url": "https://api.github.com/repos/rails/rails/issues/8294", 201 | "repository_url": "https://api.github.com/repos/rails/rails", 202 | "labels_url": "https://api.github.com/repos/rails/rails/issues/8294/labels{/name}", 203 | "comments_url": "https://api.github.com/repos/rails/rails/issues/8294/comments", 204 | "events_url": "https://api.github.com/repos/rails/rails/issues/8294/events", 205 | "html_url": "https://github.com/rails/rails/issues/8294", 206 | "id": 8561022, 207 | "number": 8294, 208 | "title": "assert_routing does not work for mounts", 209 | "user": { 210 | "login": "jmazzi", 211 | "id": 2273, 212 | "avatar_url": "https://avatars.githubusercontent.com/u/2273?v=3", 213 | "gravatar_id": "", 214 | "url": "https://api.github.com/users/jmazzi", 215 | "html_url": "https://github.com/jmazzi", 216 | "followers_url": "https://api.github.com/users/jmazzi/followers", 217 | "following_url": "https://api.github.com/users/jmazzi/following{/other_user}", 218 | "gists_url": "https://api.github.com/users/jmazzi/gists{/gist_id}", 219 | "starred_url": "https://api.github.com/users/jmazzi/starred{/owner}{/repo}", 220 | "subscriptions_url": "https://api.github.com/users/jmazzi/subscriptions", 221 | "organizations_url": "https://api.github.com/users/jmazzi/orgs", 222 | "repos_url": "https://api.github.com/users/jmazzi/repos", 223 | "events_url": "https://api.github.com/users/jmazzi/events{/privacy}", 224 | "received_events_url": "https://api.github.com/users/jmazzi/received_events", 225 | "type": "User", 226 | "site_admin": false 227 | }, 228 | "labels": [ 229 | { 230 | "id": 107189, 231 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 232 | "name": "actionpack", 233 | "color": "FFF700", 234 | "default": false 235 | }, 236 | { 237 | "id": 41328116, 238 | "url": "https://api.github.com/repos/rails/rails/labels/attached%20PR", 239 | "name": "attached PR", 240 | "color": "006b75", 241 | "default": false 242 | }, 243 | { 244 | "id": 149514554, 245 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 246 | "name": "pinned", 247 | "color": "f7c6c7", 248 | "default": false 249 | } 250 | ], 251 | "state": "open", 252 | "locked": false, 253 | "assignee": { 254 | "login": "pixeltrix", 255 | "id": 6321, 256 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 257 | "gravatar_id": "", 258 | "url": "https://api.github.com/users/pixeltrix", 259 | "html_url": "https://github.com/pixeltrix", 260 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 261 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 262 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 263 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 264 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 265 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 266 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 267 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 268 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 269 | "type": "User", 270 | "site_admin": false 271 | }, 272 | "assignees": [ 273 | { 274 | "login": "pixeltrix", 275 | "id": 6321, 276 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 277 | "gravatar_id": "", 278 | "url": "https://api.github.com/users/pixeltrix", 279 | "html_url": "https://github.com/pixeltrix", 280 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 281 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 282 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 283 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 284 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 285 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 286 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 287 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 288 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 289 | "type": "User", 290 | "site_admin": false 291 | } 292 | ], 293 | "milestone": null, 294 | "comments": 13, 295 | "created_at": "2012-11-21T22:34:37Z", 296 | "updated_at": "2016-02-17T05:41:13Z", 297 | "closed_at": null, 298 | "body": "The following code causes the error \"No route matches /test\" on Rails 3.2.9. I don't think this is a 3.2.9 specific issue, tho.\n\nRoutes\n\n``` ruby\nRouteIssue::Application.routes.draw do\n class RackApp\n def call(env)\n [200, {\"Content-Type\" => \"text/html\"}, [\"I'm Old Gregg\"]]\n end\n end\n\n mount RackApp.new, at: \"/test\"\nend\n```\n\nTest\n\n``` ruby\nassert_routing \"/test\", { :controller => \"test\" }\n```\n\nIs there another way I should be testing mounts?\n" 299 | }, 300 | { 301 | "url": "https://api.github.com/repos/rails/rails/issues/8189", 302 | "repository_url": "https://api.github.com/repos/rails/rails", 303 | "labels_url": "https://api.github.com/repos/rails/rails/issues/8189/labels{/name}", 304 | "comments_url": "https://api.github.com/repos/rails/rails/issues/8189/comments", 305 | "events_url": "https://api.github.com/repos/rails/rails/issues/8189/events", 306 | "html_url": "https://github.com/rails/rails/pull/8189", 307 | "id": 8296100, 308 | "number": 8189, 309 | "title": "Move multi-parameter attributes from ActiveRecord to ActiveModel", 310 | "user": { 311 | "login": "georgebrock", 312 | "id": 35861, 313 | "avatar_url": "https://avatars.githubusercontent.com/u/35861?v=3", 314 | "gravatar_id": "", 315 | "url": "https://api.github.com/users/georgebrock", 316 | "html_url": "https://github.com/georgebrock", 317 | "followers_url": "https://api.github.com/users/georgebrock/followers", 318 | "following_url": "https://api.github.com/users/georgebrock/following{/other_user}", 319 | "gists_url": "https://api.github.com/users/georgebrock/gists{/gist_id}", 320 | "starred_url": "https://api.github.com/users/georgebrock/starred{/owner}{/repo}", 321 | "subscriptions_url": "https://api.github.com/users/georgebrock/subscriptions", 322 | "organizations_url": "https://api.github.com/users/georgebrock/orgs", 323 | "repos_url": "https://api.github.com/users/georgebrock/repos", 324 | "events_url": "https://api.github.com/users/georgebrock/events{/privacy}", 325 | "received_events_url": "https://api.github.com/users/georgebrock/received_events", 326 | "type": "User", 327 | "site_admin": false 328 | }, 329 | "labels": [ 330 | { 331 | "id": 107189, 332 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 333 | "name": "actionpack", 334 | "color": "FFF700", 335 | "default": false 336 | }, 337 | { 338 | "id": 107190, 339 | "url": "https://api.github.com/repos/rails/rails/labels/activemodel", 340 | "name": "activemodel", 341 | "color": "00E5FF", 342 | "default": false 343 | }, 344 | { 345 | "id": 107191, 346 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 347 | "name": "activerecord", 348 | "color": "0b02e1", 349 | "default": false 350 | } 351 | ], 352 | "state": "open", 353 | "locked": true, 354 | "assignee": { 355 | "login": "rafaelfranca", 356 | "id": 47848, 357 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 358 | "gravatar_id": "", 359 | "url": "https://api.github.com/users/rafaelfranca", 360 | "html_url": "https://github.com/rafaelfranca", 361 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 362 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 363 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 364 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 365 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 366 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 367 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 368 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 369 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 370 | "type": "User", 371 | "site_admin": false 372 | }, 373 | "assignees": [ 374 | { 375 | "login": "rafaelfranca", 376 | "id": 47848, 377 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 378 | "gravatar_id": "", 379 | "url": "https://api.github.com/users/rafaelfranca", 380 | "html_url": "https://github.com/rafaelfranca", 381 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 382 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 383 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 384 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 385 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 386 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 387 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 388 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 389 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 390 | "type": "User", 391 | "site_admin": false 392 | } 393 | ], 394 | "milestone": null, 395 | "comments": 31, 396 | "created_at": "2012-11-12T17:19:01Z", 397 | "updated_at": "2014-09-03T02:57:52Z", 398 | "closed_at": null, 399 | "pull_request": { 400 | "url": "https://api.github.com/repos/rails/rails/pulls/8189", 401 | "html_url": "https://github.com/rails/rails/pull/8189", 402 | "diff_url": "https://github.com/rails/rails/pull/8189.diff", 403 | "patch_url": "https://github.com/rails/rails/pull/8189.patch" 404 | }, 405 | "body": "Multi-parameter attributes (used to build a complex type like a `Date` from several simple values that can be easily POSTed) are currently implemented in `ActiveRecord::AttributeAssignment`. This is pretty frustrating if you're trying to use an `ActiveModel::Model` with `Date` or `DateTime` fields.\n\nI've fixed this by moving multi-parameter conversion to the `ActionController::Parameters` class, so it happens before the value hits the model. IMO the model layer shouldn't need to be aware of the format that's used to pass the data from the client to the server.\n\nTo support this the type that the values should be re-assembled into is included in the form (e.g. ``) instead of being determined from the model schema.\n\nThe main aim was to get multi-parameter attributes working with `ActiveModel`, but a nice side effect of this implementation is that it becomes very easy to register custom types. For example, you could easily submit an amount of money as a currency and an amount and have it converted to your own custom `Money` class:\n\n```\n # In an initializer somewhere:\n ActionController::MultiParameterConverter.register_type('Money') { |currency, amount|\n Money.new(currency, amount)\n }\n\n # View:\n \n \n \n```\n\nThis isn't a finished implementation yet. It needs documentation, and the `ActiveRecord` implementation needs to be deprecated or removed. I've added multi-parameter support to `ActionController` and updated the `date_select`, `datetime_select` and `time_select` helpers to use the new implementation. I figured this was far enough to get some feedback.\n\n---\n\n**Edit:** Renamed from _Move multi-parameter attributes from ActiveRecord to ActionController_ to _Move multi-parameter attributes from ActiveRecord to ActiveModel_ to reflect the current state of the change.\n" 406 | }, 407 | { 408 | "url": "https://api.github.com/repos/rails/rails/issues/8103", 409 | "repository_url": "https://api.github.com/repos/rails/rails", 410 | "labels_url": "https://api.github.com/repos/rails/rails/issues/8103/labels{/name}", 411 | "comments_url": "https://api.github.com/repos/rails/rails/issues/8103/comments", 412 | "events_url": "https://api.github.com/repos/rails/rails/issues/8103/events", 413 | "html_url": "https://github.com/rails/rails/issues/8103", 414 | "id": 8063875, 415 | "number": 8103, 416 | "title": "Limit replaces select clause when used with having", 417 | "user": { 418 | "login": "wb-lifebooker", 419 | "id": 798786, 420 | "avatar_url": "https://avatars.githubusercontent.com/u/798786?v=3", 421 | "gravatar_id": "", 422 | "url": "https://api.github.com/users/wb-lifebooker", 423 | "html_url": "https://github.com/wb-lifebooker", 424 | "followers_url": "https://api.github.com/users/wb-lifebooker/followers", 425 | "following_url": "https://api.github.com/users/wb-lifebooker/following{/other_user}", 426 | "gists_url": "https://api.github.com/users/wb-lifebooker/gists{/gist_id}", 427 | "starred_url": "https://api.github.com/users/wb-lifebooker/starred{/owner}{/repo}", 428 | "subscriptions_url": "https://api.github.com/users/wb-lifebooker/subscriptions", 429 | "organizations_url": "https://api.github.com/users/wb-lifebooker/orgs", 430 | "repos_url": "https://api.github.com/users/wb-lifebooker/repos", 431 | "events_url": "https://api.github.com/users/wb-lifebooker/events{/privacy}", 432 | "received_events_url": "https://api.github.com/users/wb-lifebooker/received_events", 433 | "type": "User", 434 | "site_admin": false 435 | }, 436 | "labels": [ 437 | { 438 | "id": 107191, 439 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 440 | "name": "activerecord", 441 | "color": "0b02e1", 442 | "default": false 443 | }, 444 | { 445 | "id": 41328116, 446 | "url": "https://api.github.com/repos/rails/rails/labels/attached%20PR", 447 | "name": "attached PR", 448 | "color": "006b75", 449 | "default": false 450 | } 451 | ], 452 | "state": "open", 453 | "locked": false, 454 | "assignee": null, 455 | "assignees": [ 456 | 457 | ], 458 | "milestone": null, 459 | "comments": 14, 460 | "created_at": "2012-11-02T16:38:07Z", 461 | "updated_at": "2016-07-30T21:35:22Z", 462 | "closed_at": null, 463 | "body": "Practical Problem:\n`Thing.group(\"things.id, other_things.id\").having(\"other_things.id = 1\").\n includes(:other_things).count`\nvs\n`Thing.group(\"things.id, other_things.id\").having(\"other_things.id = 1\").\n includes(:other_things).limit(1)`\n\nEasy way to reproduce it:\n\n`Thing.select(\"b\").group(\"things.id\").having(\"other_things.id = 1\").includes(:other_things).limit(1)`\n\n`SELECT DISTINCT `things`.id FROM `things` LEFT OUTER JOIN `other_things` ON `other_things`.`thing_id` = `things`.`id` ...`\n\nthe select clause is completely replaced\n\n`Thing.select(\"b\").group(\"things.id\").having(\"other_things.id = 1\").includes(:other_things).count`\n\n`SELECT COUNT(DISTINCT b) AS count_b, b, things.id AS things_id FROM `things` \nLEFT OUTER JOIN `other_things` ON `other_things`.`thing_id` = `things`.`id` GROUP BY things.id \nHAVING other_things.id = 1`\n\n---\n\n.count has the correct behavior, and leaves the extra columns in the select clause. .limit() replaces the select clause and breaks the query.\n" 464 | }, 465 | { 466 | "url": "https://api.github.com/repos/rails/rails/issues/7584", 467 | "repository_url": "https://api.github.com/repos/rails/rails", 468 | "labels_url": "https://api.github.com/repos/rails/rails/issues/7584/labels{/name}", 469 | "comments_url": "https://api.github.com/repos/rails/rails/issues/7584/comments", 470 | "events_url": "https://api.github.com/repos/rails/rails/issues/7584/events", 471 | "html_url": "https://github.com/rails/rails/pull/7584", 472 | "id": 6752275, 473 | "number": 7584, 474 | "title": "partitioning updates working", 475 | "user": { 476 | "login": "keithgabryelski", 477 | "id": 884378, 478 | "avatar_url": "https://avatars.githubusercontent.com/u/884378?v=3", 479 | "gravatar_id": "", 480 | "url": "https://api.github.com/users/keithgabryelski", 481 | "html_url": "https://github.com/keithgabryelski", 482 | "followers_url": "https://api.github.com/users/keithgabryelski/followers", 483 | "following_url": "https://api.github.com/users/keithgabryelski/following{/other_user}", 484 | "gists_url": "https://api.github.com/users/keithgabryelski/gists{/gist_id}", 485 | "starred_url": "https://api.github.com/users/keithgabryelski/starred{/owner}{/repo}", 486 | "subscriptions_url": "https://api.github.com/users/keithgabryelski/subscriptions", 487 | "organizations_url": "https://api.github.com/users/keithgabryelski/orgs", 488 | "repos_url": "https://api.github.com/users/keithgabryelski/repos", 489 | "events_url": "https://api.github.com/users/keithgabryelski/events{/privacy}", 490 | "received_events_url": "https://api.github.com/users/keithgabryelski/received_events", 491 | "type": "User", 492 | "site_admin": false 493 | }, 494 | "labels": [ 495 | { 496 | "id": 107191, 497 | "url": "https://api.github.com/repos/rails/rails/labels/activerecord", 498 | "name": "activerecord", 499 | "color": "0b02e1", 500 | "default": false 501 | }, 502 | { 503 | "id": 70310659, 504 | "url": "https://api.github.com/repos/rails/rails/labels/PostgreSQL", 505 | "name": "PostgreSQL", 506 | "color": "fbca04", 507 | "default": false 508 | } 509 | ], 510 | "state": "open", 511 | "locked": false, 512 | "assignee": { 513 | "login": "rafaelfranca", 514 | "id": 47848, 515 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 516 | "gravatar_id": "", 517 | "url": "https://api.github.com/users/rafaelfranca", 518 | "html_url": "https://github.com/rafaelfranca", 519 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 520 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 521 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 522 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 523 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 524 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 525 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 526 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 527 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 528 | "type": "User", 529 | "site_admin": false 530 | }, 531 | "assignees": [ 532 | { 533 | "login": "rafaelfranca", 534 | "id": 47848, 535 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 536 | "gravatar_id": "", 537 | "url": "https://api.github.com/users/rafaelfranca", 538 | "html_url": "https://github.com/rafaelfranca", 539 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 540 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 541 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 542 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 543 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 544 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 545 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 546 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 547 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 548 | "type": "User", 549 | "site_admin": false 550 | } 551 | ], 552 | "milestone": null, 553 | "comments": 14, 554 | "created_at": "2012-09-10T02:29:33Z", 555 | "updated_at": "2015-10-20T21:07:29Z", 556 | "closed_at": null, 557 | "pull_request": { 558 | "url": "https://api.github.com/repos/rails/rails/pulls/7584", 559 | "html_url": "https://github.com/rails/rails/pull/7584", 560 | "diff_url": "https://github.com/rails/rails/pull/7584.diff", 561 | "patch_url": "https://github.com/rails/rails/pull/7584.patch" 562 | }, 563 | "body": "basic work for supporting partitioned tables in postgresql\n\nthese changes are associated with this pull request: https://github.com/rails/rails/pull/7573\nwhich were changes associated with rails 3.2.8.\n\nIf I were to sum up the work, it would be:\n- provide an instance method arel_table used for any operation that has access to the instance should use the instance method to acquire an arel_table associated with the current models attributes.\n- alter class method arel_table to handle parameters, with no parameters do original work -- with parameters associate the table with the specific partitioned table determined by key attributes\n- provide methods to manage key attributes and values (these are the fields the db table is partitioned on)\n- provide an instance method table_name which calls the altered class method table_name which now takes attribute values that should determine the specific partitioned table to name\n- bunch of helper methods in postgresql connection area associated with schema management. this is for reasons 1) create_schema seems like a useful method, 2) adding foreign key is needed because in postgres child partitions need to manage the foreign key references, 3) some sequence method changes to support tables in non-public (well non search path) schema: this is probably generally useful work as rails seems broken about non-public schemas\n- change any self.class.arel_table to self.arel_table\n- create is a little weird because it needs to acquire the primary key if it isn't supplied (for instance ID where your need to fetch from the sequence -- for this work to be complete we need to supply the model instance method \"prefetch_primary_key?\" instead of it being on connection since prefetching isn't needed for any tables that aren't partitioned by a primary key)\n- some helper methods for finds (from_partition(*x)) which we've found useful in our day to day coding. this method just sets the table name (this is useful because find from the parent table even when partition keys are provided can take an inordinate time if the number of child tables is large -- so specifying the specific child table is useful).\n\nthe rest of the code to support partitioning is here: https://github.com/fiksu/partitioned/tree/rails-3-2-8-patching -- you'll need to pull from that branch (which doesn't try to patch rails -- so use it with this pull request). the master branch patches rails 3.2.8 correctly -- you can use it on your own. The current rubygem of partitioned patches rails in a different (and more conservative way) -- I don't think you should look at that code.\n\nYou could probably remove a bunch of stuff to make this code faster for the common non-partitioned case.\n- instance arel_table could just call class method arel_table\n- self.class.arel_table could just do the old work\n- instance table_name could just call class method table_name which did just the old work\n\nthen one might provide fixups for those methods for models where partitioning is desired.\n\nI think the ugliest part of this code is update -- although I haven't walked down this path, it would seem the best way to manage this would be to add a hook to attribute modifications and fix up the all arel_tables that the attributes point to if the partitioned key values changes\n\nI'm willing to help in any way that makes sense to support partitioning in a future rails version.\n" 564 | }, 565 | { 566 | "url": "https://api.github.com/repos/rails/rails/issues/7245", 567 | "repository_url": "https://api.github.com/repos/rails/rails", 568 | "labels_url": "https://api.github.com/repos/rails/rails/issues/7245/labels{/name}", 569 | "comments_url": "https://api.github.com/repos/rails/rails/issues/7245/comments", 570 | "events_url": "https://api.github.com/repos/rails/rails/issues/7245/events", 571 | "html_url": "https://github.com/rails/rails/issues/7245", 572 | "id": 6006282, 573 | "number": 7245, 574 | "title": "Inconsistent output from ActiveSupport::TimeZone.all", 575 | "user": { 576 | "login": "wonnage", 577 | "id": 125177, 578 | "avatar_url": "https://avatars.githubusercontent.com/u/125177?v=3", 579 | "gravatar_id": "", 580 | "url": "https://api.github.com/users/wonnage", 581 | "html_url": "https://github.com/wonnage", 582 | "followers_url": "https://api.github.com/users/wonnage/followers", 583 | "following_url": "https://api.github.com/users/wonnage/following{/other_user}", 584 | "gists_url": "https://api.github.com/users/wonnage/gists{/gist_id}", 585 | "starred_url": "https://api.github.com/users/wonnage/starred{/owner}{/repo}", 586 | "subscriptions_url": "https://api.github.com/users/wonnage/subscriptions", 587 | "organizations_url": "https://api.github.com/users/wonnage/orgs", 588 | "repos_url": "https://api.github.com/users/wonnage/repos", 589 | "events_url": "https://api.github.com/users/wonnage/events{/privacy}", 590 | "received_events_url": "https://api.github.com/users/wonnage/received_events", 591 | "type": "User", 592 | "site_admin": false 593 | }, 594 | "labels": [ 595 | { 596 | "id": 107194, 597 | "url": "https://api.github.com/repos/rails/rails/labels/activesupport", 598 | "name": "activesupport", 599 | "color": "FC9300", 600 | "default": false 601 | }, 602 | { 603 | "id": 149514554, 604 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 605 | "name": "pinned", 606 | "color": "f7c6c7", 607 | "default": false 608 | } 609 | ], 610 | "state": "open", 611 | "locked": false, 612 | "assignee": { 613 | "login": "pixeltrix", 614 | "id": 6321, 615 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 616 | "gravatar_id": "", 617 | "url": "https://api.github.com/users/pixeltrix", 618 | "html_url": "https://github.com/pixeltrix", 619 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 620 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 621 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 622 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 623 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 624 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 625 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 626 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 627 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 628 | "type": "User", 629 | "site_admin": false 630 | }, 631 | "assignees": [ 632 | { 633 | "login": "pixeltrix", 634 | "id": 6321, 635 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 636 | "gravatar_id": "", 637 | "url": "https://api.github.com/users/pixeltrix", 638 | "html_url": "https://github.com/pixeltrix", 639 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 640 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 641 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 642 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 643 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 644 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 645 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 646 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 647 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 648 | "type": "User", 649 | "site_admin": false 650 | } 651 | ], 652 | "milestone": null, 653 | "comments": 17, 654 | "created_at": "2012-08-03T01:20:57Z", 655 | "updated_at": "2015-03-04T16:25:50Z", 656 | "closed_at": null, 657 | "body": "Due to caching, `ActiveSupport::TimeZone.all` returns different results if a non-ActiveSupport-supported zone was looked up first. \n\n```\nActiveSupport::TimeZone.all\n# not in ActiveSupport::TimeZone::MAPPING, but still a valid zone\nchicago = ActiveSupport::TimeZone['America/Chicago']\nActiveSupport::TimeZone.all.include?(chicago)\n=> false\n```\n\n```\nchicago = ActiveSupport::TimeZone['America/Chicago']\nActiveSupport::TimeZone.all.include?(chicago)\n=> true\n```\n\nThis affects `time_zone_options_for_select`, in that the `selected` arg of that function is a string matched to the names of zones in `ActiveSupport::TimeZone.all`. If your app stores timezones in TZInfo format, the helper may not generate an option tag for a recognized zone.\n\nI see two ways around this:\n1. Change the helper to recognize TZInfo identifiers\n2. Update the zones cache when lazy-loading time zones.\n\nChanging the helper might unintentionally change your data (\"America/Chicago\" would get converted to \"Central Time\"). It seems like Rails is opinionated about what zones it wants to use, so that might not be a big deal. The second approach avoids that problem, but still requires you to look up the alternate zone before it shows up in the list.\n\nThoughts?\n" 658 | }, 659 | { 660 | "url": "https://api.github.com/repos/rails/rails/issues/7218", 661 | "repository_url": "https://api.github.com/repos/rails/rails", 662 | "labels_url": "https://api.github.com/repos/rails/rails/issues/7218/labels{/name}", 663 | "comments_url": "https://api.github.com/repos/rails/rails/issues/7218/comments", 664 | "events_url": "https://api.github.com/repos/rails/rails/issues/7218/events", 665 | "html_url": "https://github.com/rails/rails/issues/7218", 666 | "id": 5954214, 667 | "number": 7218, 668 | "title": "image_tag in ActionView::TestCase not respecting asset pipeline", 669 | "user": { 670 | "login": "Mandaryn", 671 | "id": 71698, 672 | "avatar_url": "https://avatars.githubusercontent.com/u/71698?v=3", 673 | "gravatar_id": "", 674 | "url": "https://api.github.com/users/Mandaryn", 675 | "html_url": "https://github.com/Mandaryn", 676 | "followers_url": "https://api.github.com/users/Mandaryn/followers", 677 | "following_url": "https://api.github.com/users/Mandaryn/following{/other_user}", 678 | "gists_url": "https://api.github.com/users/Mandaryn/gists{/gist_id}", 679 | "starred_url": "https://api.github.com/users/Mandaryn/starred{/owner}{/repo}", 680 | "subscriptions_url": "https://api.github.com/users/Mandaryn/subscriptions", 681 | "organizations_url": "https://api.github.com/users/Mandaryn/orgs", 682 | "repos_url": "https://api.github.com/users/Mandaryn/repos", 683 | "events_url": "https://api.github.com/users/Mandaryn/events{/privacy}", 684 | "received_events_url": "https://api.github.com/users/Mandaryn/received_events", 685 | "type": "User", 686 | "site_admin": false 687 | }, 688 | "labels": [ 689 | { 690 | "id": 3666649, 691 | "url": "https://api.github.com/repos/rails/rails/labels/actionview", 692 | "name": "actionview", 693 | "color": "d7e102", 694 | "default": false 695 | }, 696 | { 697 | "id": 41328116, 698 | "url": "https://api.github.com/repos/rails/rails/labels/attached%20PR", 699 | "name": "attached PR", 700 | "color": "006b75", 701 | "default": false 702 | }, 703 | { 704 | "id": 149514554, 705 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 706 | "name": "pinned", 707 | "color": "f7c6c7", 708 | "default": false 709 | } 710 | ], 711 | "state": "open", 712 | "locked": false, 713 | "assignee": { 714 | "login": "guilleiguaran", 715 | "id": 160941, 716 | "avatar_url": "https://avatars.githubusercontent.com/u/160941?v=3", 717 | "gravatar_id": "", 718 | "url": "https://api.github.com/users/guilleiguaran", 719 | "html_url": "https://github.com/guilleiguaran", 720 | "followers_url": "https://api.github.com/users/guilleiguaran/followers", 721 | "following_url": "https://api.github.com/users/guilleiguaran/following{/other_user}", 722 | "gists_url": "https://api.github.com/users/guilleiguaran/gists{/gist_id}", 723 | "starred_url": "https://api.github.com/users/guilleiguaran/starred{/owner}{/repo}", 724 | "subscriptions_url": "https://api.github.com/users/guilleiguaran/subscriptions", 725 | "organizations_url": "https://api.github.com/users/guilleiguaran/orgs", 726 | "repos_url": "https://api.github.com/users/guilleiguaran/repos", 727 | "events_url": "https://api.github.com/users/guilleiguaran/events{/privacy}", 728 | "received_events_url": "https://api.github.com/users/guilleiguaran/received_events", 729 | "type": "User", 730 | "site_admin": false 731 | }, 732 | "assignees": [ 733 | { 734 | "login": "guilleiguaran", 735 | "id": 160941, 736 | "avatar_url": "https://avatars.githubusercontent.com/u/160941?v=3", 737 | "gravatar_id": "", 738 | "url": "https://api.github.com/users/guilleiguaran", 739 | "html_url": "https://github.com/guilleiguaran", 740 | "followers_url": "https://api.github.com/users/guilleiguaran/followers", 741 | "following_url": "https://api.github.com/users/guilleiguaran/following{/other_user}", 742 | "gists_url": "https://api.github.com/users/guilleiguaran/gists{/gist_id}", 743 | "starred_url": "https://api.github.com/users/guilleiguaran/starred{/owner}{/repo}", 744 | "subscriptions_url": "https://api.github.com/users/guilleiguaran/subscriptions", 745 | "organizations_url": "https://api.github.com/users/guilleiguaran/orgs", 746 | "repos_url": "https://api.github.com/users/guilleiguaran/repos", 747 | "events_url": "https://api.github.com/users/guilleiguaran/events{/privacy}", 748 | "received_events_url": "https://api.github.com/users/guilleiguaran/received_events", 749 | "type": "User", 750 | "site_admin": false 751 | } 752 | ], 753 | "milestone": null, 754 | "comments": 19, 755 | "created_at": "2012-07-31T21:30:49Z", 756 | "updated_at": "2016-02-16T02:31:34Z", 757 | "closed_at": null, 758 | "body": "While testing helper methods that use image_tag\n\n``` ruby\nmodule SomeHelper\n def using_image_tag\n image_tag(\"rails.png\")\n end\nend\n```\n\nthe helper returns the image path starting with '/images' even though the asset pipeline is turned on\n\n``` ruby\nrequire 'test_helper'\n\nclass SomeHelperTest < ActionView::TestCase\n test \"should resolve src to /assets/rails.png\" do\n assert_equal \"\\\"Rails\\\"\", using_image_tag\n end\nend\n```\n\ngiving\n\n``` output\n 1) Failure:\ntest_should_resolve_src_to_/assets/rails.png(SomeHelperTest) [/Users/manda/projects/demo/test/unit/helpers/some_helper_test.rb:5]:\n<\"\\\"Rails\\\"\"> expected but was\n<\"\\\"Rails\\\"\">.\n```\n\na repo with new app showing the problem, just run rake test: git://github.com/Mandaryn/action_view_test_case_problem_with_image_tag_and_asset_pipeline.git\n" 759 | }, 760 | { 761 | "url": "https://api.github.com/repos/rails/rails/issues/7047", 762 | "repository_url": "https://api.github.com/repos/rails/rails", 763 | "labels_url": "https://api.github.com/repos/rails/rails/issues/7047/labels{/name}", 764 | "comments_url": "https://api.github.com/repos/rails/rails/issues/7047/comments", 765 | "events_url": "https://api.github.com/repos/rails/rails/issues/7047/events", 766 | "html_url": "https://github.com/rails/rails/issues/7047", 767 | "id": 5610876, 768 | "number": 7047, 769 | "title": "Optional parameters in routes", 770 | "user": { 771 | "login": "woto", 772 | "id": 146704, 773 | "avatar_url": "https://avatars.githubusercontent.com/u/146704?v=3", 774 | "gravatar_id": "", 775 | "url": "https://api.github.com/users/woto", 776 | "html_url": "https://github.com/woto", 777 | "followers_url": "https://api.github.com/users/woto/followers", 778 | "following_url": "https://api.github.com/users/woto/following{/other_user}", 779 | "gists_url": "https://api.github.com/users/woto/gists{/gist_id}", 780 | "starred_url": "https://api.github.com/users/woto/starred{/owner}{/repo}", 781 | "subscriptions_url": "https://api.github.com/users/woto/subscriptions", 782 | "organizations_url": "https://api.github.com/users/woto/orgs", 783 | "repos_url": "https://api.github.com/users/woto/repos", 784 | "events_url": "https://api.github.com/users/woto/events{/privacy}", 785 | "received_events_url": "https://api.github.com/users/woto/received_events", 786 | "type": "User", 787 | "site_admin": false 788 | }, 789 | "labels": [ 790 | { 791 | "id": 107189, 792 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 793 | "name": "actionpack", 794 | "color": "FFF700", 795 | "default": false 796 | }, 797 | { 798 | "id": 149514554, 799 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 800 | "name": "pinned", 801 | "color": "f7c6c7", 802 | "default": false 803 | } 804 | ], 805 | "state": "open", 806 | "locked": false, 807 | "assignee": { 808 | "login": "pixeltrix", 809 | "id": 6321, 810 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 811 | "gravatar_id": "", 812 | "url": "https://api.github.com/users/pixeltrix", 813 | "html_url": "https://github.com/pixeltrix", 814 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 815 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 816 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 817 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 818 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 819 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 820 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 821 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 822 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 823 | "type": "User", 824 | "site_admin": false 825 | }, 826 | "assignees": [ 827 | { 828 | "login": "pixeltrix", 829 | "id": 6321, 830 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 831 | "gravatar_id": "", 832 | "url": "https://api.github.com/users/pixeltrix", 833 | "html_url": "https://github.com/pixeltrix", 834 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 835 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 836 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 837 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 838 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 839 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 840 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 841 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 842 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 843 | "type": "User", 844 | "site_admin": false 845 | } 846 | ], 847 | "milestone": null, 848 | "comments": 23, 849 | "created_at": "2012-07-13T18:30:14Z", 850 | "updated_at": "2015-12-18T03:35:23Z", 851 | "closed_at": null, 852 | "body": "Hello, sorry for Bad English.\nToday i updated from 3.1.3 to 3.2.6 and found that my routes doesn't work correct. I'm absolutely sure that it's because of update. But i'm not sure that this is documented feature.\n\nmy route looks like\n\n```\n resources :searches do \n match '(/:catalog_number(/:manufacturer(/:replacements)))' => \"searches#index\", :on => :collection, :as => :search, :via => :get\n end\n```\n\nand I call it so\n\n```\n<%= link_to 'Посмотреть аналоги всех найденных номеров', search_searches_path(params[:catalog_number], nil, \"1\"), :remote => true, :class => 'ajax-search' %>\n```\n\nto get url's that's i need\n.../searches/catalog_number/manufacturer/1\n.../searches/catalog_number/manufacturer/\n.../searches/catalog_number?replacements=1\n\nfor now it's always looks like if i add nil to :manufacturer parameter\n.../searches/catalog_number\n\nI can write more cleanly examples but especially copy-paste to reproduce error.\n" 853 | }, 854 | { 855 | "url": "https://api.github.com/repos/rails/rails/issues/6922", 856 | "repository_url": "https://api.github.com/repos/rails/rails", 857 | "labels_url": "https://api.github.com/repos/rails/rails/issues/6922/labels{/name}", 858 | "comments_url": "https://api.github.com/repos/rails/rails/issues/6922/comments", 859 | "events_url": "https://api.github.com/repos/rails/rails/issues/6922/events", 860 | "html_url": "https://github.com/rails/rails/pull/6922", 861 | "id": 5375927, 862 | "number": 6922, 863 | "title": "Allow assert_recognizes and recognize_path to support url redirects from...", 864 | "user": { 865 | "login": "andrewferk", 866 | "id": 113192, 867 | "avatar_url": "https://avatars.githubusercontent.com/u/113192?v=3", 868 | "gravatar_id": "", 869 | "url": "https://api.github.com/users/andrewferk", 870 | "html_url": "https://github.com/andrewferk", 871 | "followers_url": "https://api.github.com/users/andrewferk/followers", 872 | "following_url": "https://api.github.com/users/andrewferk/following{/other_user}", 873 | "gists_url": "https://api.github.com/users/andrewferk/gists{/gist_id}", 874 | "starred_url": "https://api.github.com/users/andrewferk/starred{/owner}{/repo}", 875 | "subscriptions_url": "https://api.github.com/users/andrewferk/subscriptions", 876 | "organizations_url": "https://api.github.com/users/andrewferk/orgs", 877 | "repos_url": "https://api.github.com/users/andrewferk/repos", 878 | "events_url": "https://api.github.com/users/andrewferk/events{/privacy}", 879 | "received_events_url": "https://api.github.com/users/andrewferk/received_events", 880 | "type": "User", 881 | "site_admin": false 882 | }, 883 | "labels": [ 884 | { 885 | "id": 107189, 886 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 887 | "name": "actionpack", 888 | "color": "FFF700", 889 | "default": false 890 | } 891 | ], 892 | "state": "open", 893 | "locked": false, 894 | "assignee": { 895 | "login": "pixeltrix", 896 | "id": 6321, 897 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 898 | "gravatar_id": "", 899 | "url": "https://api.github.com/users/pixeltrix", 900 | "html_url": "https://github.com/pixeltrix", 901 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 902 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 903 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 904 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 905 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 906 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 907 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 908 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 909 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 910 | "type": "User", 911 | "site_admin": false 912 | }, 913 | "assignees": [ 914 | { 915 | "login": "pixeltrix", 916 | "id": 6321, 917 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 918 | "gravatar_id": "", 919 | "url": "https://api.github.com/users/pixeltrix", 920 | "html_url": "https://github.com/pixeltrix", 921 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 922 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 923 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 924 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 925 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 926 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 927 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 928 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 929 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 930 | "type": "User", 931 | "site_admin": false 932 | } 933 | ], 934 | "milestone": null, 935 | "comments": 26, 936 | "created_at": "2012-07-01T18:32:21Z", 937 | "updated_at": "2016-04-20T23:12:38Z", 938 | "closed_at": null, 939 | "pull_request": { 940 | "url": "https://api.github.com/repos/rails/rails/pulls/6922", 941 | "html_url": "https://github.com/rails/rails/pull/6922", 942 | "diff_url": "https://github.com/rails/rails/pull/6922.diff", 943 | "patch_url": "https://github.com/rails/rails/pull/6922.patch" 944 | }, 945 | "body": "Currently, there is no way to unit test the Redirection module in ActionDispatch::Routing. Also, redirects are not recognized as paths using `recognize_path`. These problems lead to a potential false positive. If you have this routing:\n\n``` ruby\nget 'help' => redirect('http://help.example.org/')\nget 'help' => 'help#index'\n```\n\nand this test:\n\n``` ruby\ntest \"should recognize /help as help#index\" do\n assert_recognizes({:controller => 'help', :action => 'index'}, '/help')\nend\n```\n\nthe test passes, but actually, it should recognize /help as a redirect.\n\nWith this commit, it fixes the false negative and allows for url redirection:\n\n``` ruby\nget 'help' => redirect(\"http://example.org/\")\nassert_recognizes(\"http://example.org/\", \"/help\")\n```\n\nThis is just a start to fix this issue, as the Redirection module offers much more than just external url redirection. Would definitely be willing to discuss other ways to unit test redirection; however, i think it is a bug that assert_recognizes does not recognize redirection routes.\n\nMore referenced here.\n" 946 | }, 947 | { 948 | "url": "https://api.github.com/repos/rails/rails/issues/5223", 949 | "repository_url": "https://api.github.com/repos/rails/rails", 950 | "labels_url": "https://api.github.com/repos/rails/rails/issues/5223/labels{/name}", 951 | "comments_url": "https://api.github.com/repos/rails/rails/issues/5223/comments", 952 | "events_url": "https://api.github.com/repos/rails/rails/issues/5223/events", 953 | "html_url": "https://github.com/rails/rails/issues/5223", 954 | "id": 3443840, 955 | "number": 5223, 956 | "title": "RemoteIp middleware trusted proxies config does not affect Rack::Request::trusted_proxy?", 957 | "user": { 958 | "login": "courtland", 959 | "id": 12778, 960 | "avatar_url": "https://avatars.githubusercontent.com/u/12778?v=3", 961 | "gravatar_id": "", 962 | "url": "https://api.github.com/users/courtland", 963 | "html_url": "https://github.com/courtland", 964 | "followers_url": "https://api.github.com/users/courtland/followers", 965 | "following_url": "https://api.github.com/users/courtland/following{/other_user}", 966 | "gists_url": "https://api.github.com/users/courtland/gists{/gist_id}", 967 | "starred_url": "https://api.github.com/users/courtland/starred{/owner}{/repo}", 968 | "subscriptions_url": "https://api.github.com/users/courtland/subscriptions", 969 | "organizations_url": "https://api.github.com/users/courtland/orgs", 970 | "repos_url": "https://api.github.com/users/courtland/repos", 971 | "events_url": "https://api.github.com/users/courtland/events{/privacy}", 972 | "received_events_url": "https://api.github.com/users/courtland/received_events", 973 | "type": "User", 974 | "site_admin": false 975 | }, 976 | "labels": [ 977 | { 978 | "id": 107189, 979 | "url": "https://api.github.com/repos/rails/rails/labels/actionpack", 980 | "name": "actionpack", 981 | "color": "FFF700", 982 | "default": false 983 | }, 984 | { 985 | "id": 126910270, 986 | "url": "https://api.github.com/repos/rails/rails/labels/With%20reproduction%20steps", 987 | "name": "With reproduction steps", 988 | "color": "009800", 989 | "default": false 990 | } 991 | ], 992 | "state": "open", 993 | "locked": false, 994 | "assignee": null, 995 | "assignees": [ 996 | 997 | ], 998 | "milestone": null, 999 | "comments": 26, 1000 | "created_at": "2012-02-29T20:43:11Z", 1001 | "updated_at": "2016-11-30T22:44:03Z", 1002 | "closed_at": null, 1003 | "body": "I have clients that connect to my rails app from a private IP address, where the clients real IP is presented to rails via the X-Forwarded-For header (Thin cluster behind a pound load balancer). By default, rails assumes these private addresses are \"trusted proxies\", which causes Request#remote_ip to return '127.0.0.1'.\n\nThis was somewhat addressed by pull request #2632 by making TRUSTED_PROXIES configurable in the RemoteIp Class.\n\nHowever, Rails::Rack::Logger methods still call Rack::Request#ip which causes the IP address displayed in my logs to be 127.0.0.1.\n\nBased on this change in rack: https://github.com/rack/rack/pull/192\nIt seems that ActionDispatch::Request should override Rack::Request#trusted_proxy? with the same \"trusted proxies\" that are configured for RemoteIp.\n\nAt the moment I have the following in an initializer to fix the problem for me, but it is obviously a really bad hack.\n\n```\nmodule Rack\n class Request\n def trusted_proxy?(ip)\n ip =~ /^127\\.0\\.0\\.1$/\n end\n end\nend\n```\n\nDoes anyone have comments or suggestions otherwise? I can attempt a patch if my logic seems sound. Thanks.\n" 1004 | }, 1005 | { 1006 | "url": "https://api.github.com/repos/rails/rails/issues/2686", 1007 | "repository_url": "https://api.github.com/repos/rails/rails", 1008 | "labels_url": "https://api.github.com/repos/rails/rails/issues/2686/labels{/name}", 1009 | "comments_url": "https://api.github.com/repos/rails/rails/issues/2686/comments", 1010 | "events_url": "https://api.github.com/repos/rails/rails/issues/2686/events", 1011 | "html_url": "https://github.com/rails/rails/issues/2686", 1012 | "id": 1480676, 1013 | "number": 2686, 1014 | "title": "Attachments not visible in mail clients when additional inline attachments present", 1015 | "user": { 1016 | "login": "icanhasserver", 1017 | "id": 1003332, 1018 | "avatar_url": "https://avatars.githubusercontent.com/u/1003332?v=3", 1019 | "gravatar_id": "", 1020 | "url": "https://api.github.com/users/icanhasserver", 1021 | "html_url": "https://github.com/icanhasserver", 1022 | "followers_url": "https://api.github.com/users/icanhasserver/followers", 1023 | "following_url": "https://api.github.com/users/icanhasserver/following{/other_user}", 1024 | "gists_url": "https://api.github.com/users/icanhasserver/gists{/gist_id}", 1025 | "starred_url": "https://api.github.com/users/icanhasserver/starred{/owner}{/repo}", 1026 | "subscriptions_url": "https://api.github.com/users/icanhasserver/subscriptions", 1027 | "organizations_url": "https://api.github.com/users/icanhasserver/orgs", 1028 | "repos_url": "https://api.github.com/users/icanhasserver/repos", 1029 | "events_url": "https://api.github.com/users/icanhasserver/events{/privacy}", 1030 | "received_events_url": "https://api.github.com/users/icanhasserver/received_events", 1031 | "type": "User", 1032 | "site_admin": false 1033 | }, 1034 | "labels": [ 1035 | { 1036 | "id": 107188, 1037 | "url": "https://api.github.com/repos/rails/rails/labels/actionmailer", 1038 | "name": "actionmailer", 1039 | "color": "8B00FC", 1040 | "default": false 1041 | }, 1042 | { 1043 | "id": 41328116, 1044 | "url": "https://api.github.com/repos/rails/rails/labels/attached%20PR", 1045 | "name": "attached PR", 1046 | "color": "006b75", 1047 | "default": false 1048 | }, 1049 | { 1050 | "id": 149514554, 1051 | "url": "https://api.github.com/repos/rails/rails/labels/pinned", 1052 | "name": "pinned", 1053 | "color": "f7c6c7", 1054 | "default": false 1055 | } 1056 | ], 1057 | "state": "open", 1058 | "locked": false, 1059 | "assignee": { 1060 | "login": "pixeltrix", 1061 | "id": 6321, 1062 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 1063 | "gravatar_id": "", 1064 | "url": "https://api.github.com/users/pixeltrix", 1065 | "html_url": "https://github.com/pixeltrix", 1066 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 1067 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 1068 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 1069 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 1070 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 1071 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 1072 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 1073 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 1074 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 1075 | "type": "User", 1076 | "site_admin": false 1077 | }, 1078 | "assignees": [ 1079 | { 1080 | "login": "pixeltrix", 1081 | "id": 6321, 1082 | "avatar_url": "https://avatars.githubusercontent.com/u/6321?v=3", 1083 | "gravatar_id": "", 1084 | "url": "https://api.github.com/users/pixeltrix", 1085 | "html_url": "https://github.com/pixeltrix", 1086 | "followers_url": "https://api.github.com/users/pixeltrix/followers", 1087 | "following_url": "https://api.github.com/users/pixeltrix/following{/other_user}", 1088 | "gists_url": "https://api.github.com/users/pixeltrix/gists{/gist_id}", 1089 | "starred_url": "https://api.github.com/users/pixeltrix/starred{/owner}{/repo}", 1090 | "subscriptions_url": "https://api.github.com/users/pixeltrix/subscriptions", 1091 | "organizations_url": "https://api.github.com/users/pixeltrix/orgs", 1092 | "repos_url": "https://api.github.com/users/pixeltrix/repos", 1093 | "events_url": "https://api.github.com/users/pixeltrix/events{/privacy}", 1094 | "received_events_url": "https://api.github.com/users/pixeltrix/received_events", 1095 | "type": "User", 1096 | "site_admin": false 1097 | } 1098 | ], 1099 | "milestone": null, 1100 | "comments": 50, 1101 | "created_at": "2011-08-25T06:48:13Z", 1102 | "updated_at": "2016-09-10T18:40:41Z", 1103 | "closed_at": null, 1104 | "body": "When assembling an email with mixed inline / normal attachments, only the inline attachments (i.e. images) are shown. Some mail clients don't detect the attached files, the most prominent being Thunderbird and Outlook.\n\nCode used with ActionMailer 3.0.10:\n\n``` ruby\nclass MultipartTest < ActionMailer::Base\n def test_email(recipient)\n attachments['file1.pdf'] = File.read('/somewhere/file1.pdf')\n attachments['file2.pdf'] = File.read('/somewhere/file2.pdf')\n attachments.inline['image1.gif'] = File.read('/somewhere/image1.gif')\n mail(\n :to => recipient,\n :subject => 'Multipart test'\n )\n end\nend\n```\n\nThe generated email comes as follows:\n\n```\nmultipart/related\n multipart/alternative\n text/plain\n text/html\n attachment (disposition: inline, image1)\n attachment (disposition: attachment, file1)\n attachment (disposition: attachment, file2)\n```\n\nWhile ActionMailer::Base::set_content_type() chooses multipart/related as soon as it detects at least one inline attachment, mail clients always wrap the attached files (disposition: attachment) in an additional multipart/mixed layer, conforming to RFC 2046, Section 5.1.3.\n\nWhen leaving out the inline attachement, the MIME type generated by ActionMailer is correct (multipart/mixed) and the attachments are visible.\n\nHere are some MIME layouts, as generated by mail clients:\n\n```\nmultipart/mixed\n multipart/alternative\n text/plain\n multipart/related\n text/html\n attachment (disposition: inline, image1)\n attachment (disposition: attachment, file1)\n attachment (disposition: attachment, file2)\n```\n\nor:\n\n```\nmultipart/mixed\n multipart/related\n multipart/alternative\n text/plain\n text/html\n attachment (disposition: inline, image1)\n attachment (disposition: attachment, file1)\n attachment (disposition: attachment, file2)\n```\n" 1105 | }, 1106 | { 1107 | "url": "https://api.github.com/repos/rails/rails/issues/2045", 1108 | "repository_url": "https://api.github.com/repos/rails/rails", 1109 | "labels_url": "https://api.github.com/repos/rails/rails/issues/2045/labels{/name}", 1110 | "comments_url": "https://api.github.com/repos/rails/rails/issues/2045/comments", 1111 | "events_url": "https://api.github.com/repos/rails/rails/issues/2045/events", 1112 | "html_url": "https://github.com/rails/rails/pull/2045", 1113 | "id": 1211180, 1114 | "number": 2045, 1115 | "title": "Add possibility to render partial from subfolder with inheritance", 1116 | "user": { 1117 | "login": "aratak", 1118 | "id": 30642, 1119 | "avatar_url": "https://avatars.githubusercontent.com/u/30642?v=3", 1120 | "gravatar_id": "", 1121 | "url": "https://api.github.com/users/aratak", 1122 | "html_url": "https://github.com/aratak", 1123 | "followers_url": "https://api.github.com/users/aratak/followers", 1124 | "following_url": "https://api.github.com/users/aratak/following{/other_user}", 1125 | "gists_url": "https://api.github.com/users/aratak/gists{/gist_id}", 1126 | "starred_url": "https://api.github.com/users/aratak/starred{/owner}{/repo}", 1127 | "subscriptions_url": "https://api.github.com/users/aratak/subscriptions", 1128 | "organizations_url": "https://api.github.com/users/aratak/orgs", 1129 | "repos_url": "https://api.github.com/users/aratak/repos", 1130 | "events_url": "https://api.github.com/users/aratak/events{/privacy}", 1131 | "received_events_url": "https://api.github.com/users/aratak/received_events", 1132 | "type": "User", 1133 | "site_admin": false 1134 | }, 1135 | "labels": [ 1136 | { 1137 | "id": 3666649, 1138 | "url": "https://api.github.com/repos/rails/rails/labels/actionview", 1139 | "name": "actionview", 1140 | "color": "d7e102", 1141 | "default": false 1142 | } 1143 | ], 1144 | "state": "open", 1145 | "locked": true, 1146 | "assignee": { 1147 | "login": "rafaelfranca", 1148 | "id": 47848, 1149 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 1150 | "gravatar_id": "", 1151 | "url": "https://api.github.com/users/rafaelfranca", 1152 | "html_url": "https://github.com/rafaelfranca", 1153 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 1154 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 1155 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 1156 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 1157 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 1158 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 1159 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 1160 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 1161 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 1162 | "type": "User", 1163 | "site_admin": false 1164 | }, 1165 | "assignees": [ 1166 | { 1167 | "login": "rafaelfranca", 1168 | "id": 47848, 1169 | "avatar_url": "https://avatars.githubusercontent.com/u/47848?v=3", 1170 | "gravatar_id": "", 1171 | "url": "https://api.github.com/users/rafaelfranca", 1172 | "html_url": "https://github.com/rafaelfranca", 1173 | "followers_url": "https://api.github.com/users/rafaelfranca/followers", 1174 | "following_url": "https://api.github.com/users/rafaelfranca/following{/other_user}", 1175 | "gists_url": "https://api.github.com/users/rafaelfranca/gists{/gist_id}", 1176 | "starred_url": "https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}", 1177 | "subscriptions_url": "https://api.github.com/users/rafaelfranca/subscriptions", 1178 | "organizations_url": "https://api.github.com/users/rafaelfranca/orgs", 1179 | "repos_url": "https://api.github.com/users/rafaelfranca/repos", 1180 | "events_url": "https://api.github.com/users/rafaelfranca/events{/privacy}", 1181 | "received_events_url": "https://api.github.com/users/rafaelfranca/received_events", 1182 | "type": "User", 1183 | "site_admin": false 1184 | } 1185 | ], 1186 | "milestone": null, 1187 | "comments": 38, 1188 | "created_at": "2011-07-12T20:20:44Z", 1189 | "updated_at": "2014-06-12T10:53:32Z", 1190 | "closed_at": null, 1191 | "pull_request": { 1192 | "url": "https://api.github.com/repos/rails/rails/pulls/2045", 1193 | "html_url": "https://github.com/rails/rails/pull/2045", 1194 | "diff_url": "https://github.com/rails/rails/pull/2045.diff", 1195 | "patch_url": "https://github.com/rails/rails/pull/2045.patch" 1196 | }, 1197 | "body": "The new feature named \"template inheritance\" don't allow to render partial inside subfolders. Partials with slash in path name can be found only from views root folder. \n\nMy pull request extends behavior of template inheritance, and allow to render partial inside subfolders with inheritance feature. I suggest to use `./` at the start, and this partial will be found with relative path (from current directory). So, \n\n```\n render :partial => \"./head/menu\"\n```\n\ncan be found in several folders, as template inheritance means:\n\n```\n - views\n - application\n - head\n - menu.html.erb\n - controller_name\n - head\n - menu.html.erb\n```\n" 1198 | } 1199 | ]} 1200 | -------------------------------------------------------------------------------- /src/images/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dceddia/github-issues-viewer/78d5e4c03dbeb32882a144abe916e5838ce0b0b2/src/images/no-avatar.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | font-size: 14px; 6 | color: #444; 7 | font-family: "Open Sans", Helvetica, sans-serif; 8 | } 9 | 10 | /*ul { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | li { 16 | list-style: none; 17 | }*/ 18 | 19 | a { 20 | text-decoration: none; 21 | color: #444; 22 | } 23 | 24 | h1 { 25 | font-weight: 300; 26 | } 27 | 28 | h2, h3, h4, h5, h6 { 29 | font-weight: 400; 30 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import { Provider } from 'react-redux'; 6 | import { Router, Route, IndexRoute, IndexRedirect, browserHistory, applyRouterMiddleware } from 'react-router'; 7 | import useScroll from 'react-router-scroll/lib/useScroll'; 8 | import rootReducer from './redux/reducers'; 9 | import App from './containers/App'; 10 | import IssueListPage from './containers/IssueListPage'; 11 | import IssueDetailPage from './containers/IssueDetailPage'; 12 | import './index.css'; 13 | 14 | let store = createStore(rootReducer, applyMiddleware(thunk)); 15 | 16 | const routes = ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | ReactDOM.render( 29 | 30 | {routes} 31 | , 32 | document.getElementById('root') 33 | ); 34 | -------------------------------------------------------------------------------- /src/redux/actions.js: -------------------------------------------------------------------------------- 1 | import * as API from '../api'; 2 | 3 | export const GET_ISSUE_BEGIN = 'GET_ISSUE_BEGIN'; 4 | export const GET_ISSUE_SUCCESS = 'GET_ISSUE_SUCCESS'; 5 | export const GET_ISSUE_FAILURE = 'GET_ISSUE_FAILURE'; 6 | export const GET_ISSUES_BEGIN = 'GET_ISSUES_BEGIN'; 7 | export const GET_ISSUES_SUCCESS = 'GET_ISSUES_SUCCESS'; 8 | export const GET_ISSUES_FAILURE = 'GET_ISSUES_FAILURE'; 9 | export const GET_COMMENTS_BEGIN = 'GET_COMMENTS_BEGIN'; 10 | export const GET_COMMENTS_SUCCESS = 'GET_COMMENTS_SUCCESS'; 11 | export const GET_COMMENTS_FAILURE = 'GET_COMMENTS_FAILURE'; 12 | export const GET_REPO_DETAILS_BEGIN = 'GET_REPO_DETAILS_BEGIN'; 13 | export const GET_REPO_DETAILS_SUCCESS = 'GET_REPO_DETAILS_SUCCESS'; 14 | export const GET_REPO_DETAILS_FAILURE = 'GET_REPO_DETAILS_FAILURE'; 15 | 16 | export function getIssuesSuccess(issueResponse) { 17 | return { 18 | type: GET_ISSUES_SUCCESS, 19 | payload: { 20 | pageCount: issueResponse.pageCount, 21 | pageLinks: issueResponse.pageLinks, 22 | issues: issueResponse.data, 23 | loading: false 24 | } 25 | }; 26 | } 27 | 28 | export function getIssuesFailure(error) { 29 | return { 30 | type: GET_ISSUES_FAILURE, 31 | error 32 | }; 33 | } 34 | 35 | export function getIssues(org, repo, page) { 36 | return dispatch => { 37 | dispatch({type: GET_ISSUES_BEGIN}); 38 | API.getIssues(org, repo, page) 39 | .then(res => dispatch(getIssuesSuccess(res))) 40 | .catch(error => dispatch(getIssuesFailure(error))); 41 | }; 42 | } 43 | 44 | export function getRepoDetailsSuccess(details) { 45 | return { 46 | type: GET_REPO_DETAILS_SUCCESS, 47 | payload: details 48 | } 49 | } 50 | 51 | export function getRepoDetailsFailure(error) { 52 | return { 53 | type: GET_REPO_DETAILS_FAILURE, 54 | error 55 | } 56 | } 57 | 58 | export function getRepoDetails(org, repo) { 59 | return dispatch => { 60 | dispatch({type: GET_REPO_DETAILS_BEGIN }); 61 | API.getRepoDetails(org, repo) 62 | .then(details => dispatch(getRepoDetailsSuccess(details))) 63 | .catch(error => dispatch(getRepoDetailsFailure(error))); 64 | } 65 | } 66 | 67 | 68 | export function getIssueSuccess(issue) { 69 | return { 70 | type: GET_ISSUE_SUCCESS, 71 | payload: issue 72 | }; 73 | } 74 | 75 | export function getIssueFailure(error) { 76 | return { 77 | type: GET_ISSUE_FAILURE, 78 | error 79 | }; 80 | } 81 | 82 | export function getIssue(org, repo, number) { 83 | return dispatch => { 84 | dispatch({type: GET_ISSUE_BEGIN}); 85 | API.getIssue(org, repo, number) 86 | .then(res => dispatch(getIssueSuccess(res))) 87 | .catch(error => dispatch(getIssueFailure(error))); 88 | }; 89 | } 90 | 91 | export function getCommentsSuccess(issueNumber, comments) { 92 | return { 93 | type: GET_COMMENTS_SUCCESS, 94 | payload: { 95 | comments, 96 | issueNumber 97 | } 98 | }; 99 | } 100 | 101 | export function getCommentsFailure(error) { 102 | return { 103 | type: GET_COMMENTS_FAILURE, 104 | error 105 | }; 106 | } 107 | 108 | export function getComments(issue) { 109 | return dispatch => { 110 | if(!issue || !issue.comments) { 111 | return; 112 | } 113 | 114 | dispatch({type: GET_COMMENTS_BEGIN}); 115 | API.getComments(issue.comments_url) 116 | .then(comments => dispatch(getCommentsSuccess(issue.number, comments))) 117 | .catch(error => dispatch(getCommentsFailure(error))); 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { 3 | GET_ISSUE_BEGIN, GET_ISSUE_SUCCESS, GET_ISSUE_FAILURE, 4 | GET_ISSUES_BEGIN, GET_ISSUES_SUCCESS, GET_ISSUES_FAILURE, 5 | GET_COMMENTS_BEGIN, GET_COMMENTS_SUCCESS, GET_COMMENTS_FAILURE, 6 | GET_REPO_DETAILS_BEGIN, GET_REPO_DETAILS_SUCCESS, GET_REPO_DETAILS_FAILURE, 7 | } from './actions'; 8 | 9 | 10 | const initialIssuesState = { 11 | issuesByNumber: {}, 12 | currentPageIssues: [], 13 | pageCount: 0, 14 | pageLinks: {}, 15 | isLoading: false, 16 | error: null 17 | }; 18 | 19 | export function issuesReducer(state = initialIssuesState, action) { 20 | switch(action.type) { 21 | case GET_ISSUE_BEGIN: 22 | case GET_ISSUES_BEGIN: 23 | return { 24 | ...state, 25 | isLoading: true 26 | }; 27 | case GET_ISSUE_SUCCESS: 28 | return { 29 | ...state, 30 | issuesByNumber: { 31 | ...state.issuesByNumber, 32 | [action.payload.number]: action.payload 33 | }, 34 | isLoading: false, 35 | error: null 36 | }; 37 | case GET_ISSUES_SUCCESS: 38 | return { 39 | ...state, 40 | pageCount: action.payload.pageCount, 41 | pageLinks: action.payload.pageLinks, 42 | issuesByNumber: action.payload.issues.reduce((result, issue) => { 43 | result[issue.number] = issue; 44 | return result; 45 | }, {}), 46 | currentPageIssues: action.payload.issues.map(issue => issue.number), 47 | isLoading: false, 48 | error: null 49 | }; 50 | case GET_ISSUE_FAILURE: 51 | case GET_ISSUES_FAILURE: 52 | return { 53 | ...state, 54 | isLoading: false, 55 | error: action.error 56 | }; 57 | default: 58 | return state; 59 | } 60 | } 61 | 62 | const initialRepoState = { 63 | openIssuesCount: -1, 64 | error: null 65 | }; 66 | 67 | export function repoReducer(state = initialRepoState, action) { 68 | switch(action.type) { 69 | case GET_REPO_DETAILS_BEGIN: 70 | return state; 71 | case GET_REPO_DETAILS_SUCCESS: 72 | return { 73 | ...state, 74 | openIssuesCount: action.payload.open_issues_count, 75 | error: null 76 | }; 77 | case GET_REPO_DETAILS_FAILURE: 78 | return { 79 | ...state, 80 | openIssuesCount: -1, 81 | error: action.error 82 | }; 83 | default: 84 | return state; 85 | } 86 | } 87 | 88 | const initialCommentsState = { 89 | error: null 90 | }; 91 | 92 | export function commentsReducer(state = initialCommentsState, action) { 93 | switch(action.type) { 94 | case GET_COMMENTS_BEGIN: 95 | return state; 96 | case GET_COMMENTS_SUCCESS: 97 | return { 98 | ...state, 99 | [action.payload.issueNumber]: action.payload.comments, 100 | error: null 101 | }; 102 | case GET_COMMENTS_FAILURE: 103 | return { 104 | ...state, 105 | error: action.error 106 | }; 107 | default: 108 | return state; 109 | } 110 | } 111 | 112 | export default combineReducers({ 113 | issues: issuesReducer, 114 | commentsByIssue: commentsReducer, 115 | repo: repoReducer 116 | }); -------------------------------------------------------------------------------- /src/redux/reducers.test.js: -------------------------------------------------------------------------------- 1 | import { issuesReducer, repoReducer, commentsReducer } from './reducers'; 2 | import { 3 | GET_ISSUE_BEGIN, GET_ISSUE_SUCCESS, GET_ISSUE_FAILURE, 4 | GET_ISSUES_BEGIN, GET_ISSUES_SUCCESS, GET_ISSUES_FAILURE, 5 | GET_COMMENTS_BEGIN, GET_COMMENTS_SUCCESS, GET_COMMENTS_FAILURE, 6 | GET_REPO_DETAILS_BEGIN, GET_REPO_DETAILS_SUCCESS, GET_REPO_DETAILS_FAILURE, 7 | } from './actions'; 8 | 9 | describe('issuesReducer', () => { 10 | describe('getting multiple issues', () => { 11 | it('has initial state', () => { 12 | expect(issuesReducer(undefined, {})).toEqual({ 13 | issuesByNumber: {}, 14 | currentPageIssues: [], 15 | pageCount: 0, 16 | pageLinks: {}, 17 | isLoading: false, 18 | error: null 19 | }); 20 | }); 21 | 22 | it('handles BEGIN', () => { 23 | expect(issuesReducer({}, {type: GET_ISSUES_BEGIN })).toEqual({ 24 | isLoading: true 25 | }); 26 | }); 27 | 28 | it('handles FAILURE', () => { 29 | expect(issuesReducer({}, {type: GET_ISSUES_FAILURE, error: 'foo'})).toEqual({ 30 | isLoading: false, 31 | error: 'foo' 32 | }); 33 | }); 34 | 35 | it('handles SUCCESS with issues', () => { 36 | const payload = { 37 | pageCount: 42, 38 | pageLinks: {next: {}}, 39 | issues: [{ 40 | number: 1 41 | }, { 42 | number: 2 43 | }] 44 | }; 45 | 46 | expect(issuesReducer({}, {type: GET_ISSUES_SUCCESS, payload })).toEqual({ 47 | issuesByNumber: { 48 | 1: {number: 1}, 49 | 2: {number: 2}, 50 | }, 51 | currentPageIssues: [1, 2], 52 | pageCount: 42, 53 | pageLinks: payload.pageLinks, 54 | isLoading: false, 55 | error: null 56 | }); 57 | }); 58 | 59 | it('handles SUCCESS with empty issues', () => { 60 | const payload = { 61 | pageCount: 42, 62 | pageLinks: {next: {}}, 63 | issues: [] 64 | }; 65 | 66 | expect(issuesReducer({}, {type: GET_ISSUES_SUCCESS, payload})).toEqual({ 67 | issuesByNumber: {}, 68 | currentPageIssues: [], 69 | pageCount: 42, 70 | pageLinks: payload.pageLinks, 71 | isLoading: false, 72 | error: null 73 | }); 74 | }); 75 | }); 76 | 77 | describe('getting a single issue', () => { 78 | it('handles BEGIN', () => { 79 | expect(issuesReducer({}, {type: GET_ISSUE_BEGIN})).toEqual({ 80 | isLoading: true 81 | }); 82 | }); 83 | 84 | it('handles SUCCESS when no issues are present', () => { 85 | const payload = { number: 1 }; 86 | expect(issuesReducer({}, {type: GET_ISSUE_SUCCESS, payload})).toEqual({ 87 | issuesByNumber: { 88 | 1: {number: 1} 89 | }, 90 | isLoading: false, 91 | error: null 92 | }); 93 | }); 94 | 95 | it('handles SUCCESS when other issues already exist', () => { 96 | const payload = { number: 3 }; 97 | expect(issuesReducer({ 98 | issuesByNumber: { 99 | 1: {number: 1}, 100 | 2: {number: 2} 101 | } 102 | }, {type: GET_ISSUE_SUCCESS, payload})).toEqual({ 103 | issuesByNumber: { 104 | 1: {number: 1}, 105 | 2: {number: 2}, 106 | 3: {number: 3} 107 | }, 108 | isLoading: false, 109 | error: null 110 | }); 111 | }); 112 | 113 | it('updates existing issue', () => { 114 | const payload = { number: 1, updated: true }; 115 | expect(issuesReducer({ 116 | issuesByNumber: { 117 | 1: {number: 1}, 118 | 2: {number: 2} 119 | } 120 | }, {type: GET_ISSUE_SUCCESS, payload})).toEqual({ 121 | issuesByNumber: { 122 | 1: {number: 1, updated: true}, 123 | 2: {number: 2}, 124 | }, 125 | isLoading: false, 126 | error: null 127 | }); 128 | }); 129 | 130 | it('handles FAILURE', () => { 131 | expect(issuesReducer({}, {type: GET_ISSUE_FAILURE, error: 'foo'})).toEqual({ 132 | isLoading: false, 133 | error: 'foo' 134 | }); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('repoReducer', () => { 140 | it('has initial state', () => { 141 | expect(repoReducer(undefined, {})).toEqual({ 142 | openIssuesCount: -1, 143 | error: null 144 | }); 145 | }); 146 | 147 | it('handles BEGIN', () => { 148 | const state = { openIssuesCount: 10 }; 149 | expect(repoReducer(state, {type: GET_REPO_DETAILS_BEGIN})).toEqual(state); 150 | }); 151 | 152 | it('handles SUCCESS', () => { 153 | const state = { openIssuesCount: 10 }; 154 | const payload = { open_issues_count: 42 }; 155 | expect(repoReducer(state, {type: GET_REPO_DETAILS_SUCCESS, payload})).toEqual({ 156 | openIssuesCount: 42, 157 | error: null 158 | }); 159 | }); 160 | 161 | it('handles FAILURE', () => { 162 | const state = { openIssuesCount: 10 }; 163 | const error = new Error('something bad'); 164 | expect(repoReducer(state, {type: GET_REPO_DETAILS_FAILURE, error})).toEqual({ 165 | openIssuesCount: -1, 166 | error 167 | }); 168 | }); 169 | }); 170 | 171 | describe('commentsReducer', () => { 172 | it('has initial state', () => { 173 | expect(commentsReducer(undefined, {})).toEqual({ 174 | error: null 175 | }); 176 | }); 177 | 178 | it('handles BEGIN', () => { 179 | expect(commentsReducer({}, {type: GET_COMMENTS_BEGIN})).toEqual({}); 180 | }); 181 | 182 | it('handles SUCCESS when no comments exist', () => { 183 | const state = {}; 184 | const payload = { 185 | comments: [1, 2, 3], 186 | issueNumber: 1 187 | }; 188 | expect(commentsReducer(state, {type: GET_COMMENTS_SUCCESS, payload})).toEqual({ 189 | 1: [1, 2, 3], 190 | error: null 191 | }); 192 | }); 193 | 194 | it('merges in comments for new issues', () => { 195 | const state = { 196 | 1: [1, 2] 197 | }; 198 | const payload = { 199 | comments: [3, 4], 200 | issueNumber: 2 201 | }; 202 | expect(commentsReducer(state, {type: GET_COMMENTS_SUCCESS, payload})).toEqual({ 203 | 1: [1, 2], 204 | 2: [3, 4], 205 | error: null 206 | }); 207 | }); 208 | 209 | it('replaces existing comments', () => { 210 | const state = { 211 | 1: [1, 2] 212 | }; 213 | const payload = { 214 | comments: [3, 4], 215 | issueNumber: 1 216 | }; 217 | expect(commentsReducer(state, {type: GET_COMMENTS_SUCCESS, payload})).toEqual({ 218 | 1: [3, 4], 219 | error: null 220 | }); 221 | }); 222 | 223 | it('handles FAILURE', () => { 224 | const state = { 225 | 1: [1, 2] 226 | }; 227 | const error = new Error('something bad'); 228 | expect(commentsReducer(state, {type: GET_COMMENTS_FAILURE, error})).toEqual({ 229 | 1: [1, 2], 230 | error 231 | }); 232 | }); 233 | }); -------------------------------------------------------------------------------- /src/utils/stringUtils.js: -------------------------------------------------------------------------------- 1 | export function insertMentionLinks(markdown) { 2 | return markdown.replace(/\B(@([a-zA-Z0-9](-?[a-zA-Z0-9_])+))/g, `**[$1](https://github.com/$2)**`); 3 | } 4 | 5 | export function shorten(text = "", maxLength = 140) { 6 | // Normalize newlines 7 | let cleanText = text.replace(/\\r\\n/g, "\n"); 8 | 9 | // Return if short enough already 10 | if(cleanText.length <= maxLength) { 11 | return cleanText; 12 | } 13 | 14 | const ellip = " ..."; 15 | 16 | // Return the 140 chars as-is if they end in a non-word char 17 | const oneTooLarge = cleanText.substr(0, 141); 18 | if(/\W$/.test(oneTooLarge)) { 19 | return oneTooLarge.substr(0, 140) + ellip; 20 | } 21 | 22 | // Walk backwards to the nearest non-word character 23 | let i = oneTooLarge.length; 24 | while(--i) { 25 | if(/\W/.test(oneTooLarge[i])) { 26 | return oneTooLarge.substr(0, i) + ellip; 27 | } 28 | } 29 | 30 | return oneTooLarge.substr(0, 140) + ellip; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/stringUtils.test.js: -------------------------------------------------------------------------------- 1 | import { shorten, insertMentionLinks } from './stringUtils'; 2 | 3 | describe('shorten', () => { 4 | const ellip = " ..."; 5 | it('leaves text alone if short enough', () => { 6 | const justFine = 'a'.repeat(139); 7 | expect(shorten(justFine)).toEqual(justFine); 8 | }); 9 | 10 | it('shortens when there are no breaks', () => { 11 | const tooLong = 'a'.repeat(141); 12 | const expected = 'a'.repeat(140) + ellip; 13 | expect(shorten(tooLong)).toEqual(expected); 14 | }); 15 | 16 | it('shortens to 140 when 141 is a non-word', () => { 17 | const tooLong = 'a'.repeat(140) + " "; 18 | const expected = 'a'.repeat(140) + ellip; 19 | expect(shorten(tooLong)).toEqual(expected); 20 | }); 21 | 22 | describe('shortening more than necessary', () => { 23 | const testWithChar = (char) => { 24 | const tooLong = 'a'.repeat(135) + char + 'b'.repeat(50); 25 | const expected = 'a'.repeat(135) + ellip; 26 | expect(shorten(tooLong)).toEqual(expected); 27 | }; 28 | 29 | it('shortens to the nearest whitespace', () => { 30 | testWithChar(' '); 31 | testWithChar('\n'); 32 | testWithChar('\t'); 33 | }); 34 | 35 | it('shortens to the nearest punctuation', () => { 36 | testWithChar('.'); 37 | testWithChar(';'); 38 | testWithChar('!'); 39 | }); 40 | }) 41 | }); 42 | 43 | describe('insertMentionLinks', () => { 44 | it('replaces mention at beginning of line', () => { 45 | expect(insertMentionLinks("@foo bar baz")) 46 | .toEqual("**[@foo](https://github.com/foo)** bar baz"); 47 | }); 48 | 49 | it('replaces all mentions', () => { 50 | expect(insertMentionLinks("@foo @bar @baz")) 51 | .toEqual("**[@foo](https://github.com/foo)** **[@bar](https://github.com/bar)** **[@baz](https://github.com/baz)**"); 52 | }); 53 | 54 | it('does not replace emails', () => { 55 | expect(insertMentionLinks("the@person.com")) 56 | .toEqual("the@person.com"); 57 | }); 58 | 59 | it('only includes word characters', () => { 60 | expect(insertMentionLinks("@the_person")) 61 | .toEqual("**[@the_person](https://github.com/the_person)**"); 62 | 63 | expect(insertMentionLinks("@the_person.")) 64 | .toEqual("**[@the_person](https://github.com/the_person)**."); 65 | }); 66 | 67 | it('allows dashes', () => { 68 | expect(insertMentionLinks("@the-person")) 69 | .toEqual("**[@the-person](https://github.com/the-person)**"); 70 | }); 71 | 72 | it('only allows single dashes', () => { 73 | expect(insertMentionLinks("@the--person")) 74 | .toEqual("**[@the](https://github.com/the)**--person"); 75 | }); 76 | 77 | it('cannot start with dash or underscore', () => { 78 | expect(insertMentionLinks("@-foo")).toEqual("@-foo"); 79 | expect(insertMentionLinks("@_foo")).toEqual("@_foo"); 80 | }); 81 | }); -------------------------------------------------------------------------------- /src/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | import toJson from 'enzyme-to-json'; 2 | 3 | export function afterPromises(done, fn) { 4 | setTimeout(() => { 5 | try { 6 | fn(); 7 | done(); 8 | } catch (e) { 9 | fail(); 10 | } 11 | }, 1); 12 | } 13 | 14 | export function checkSnapshot(tree) { 15 | expect(toJson(tree)).toMatchSnapshot(); 16 | } --------------------------------------------------------------------------------