diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss index 072371418..4cec9202b 100644 --- a/app/assets/stylesheets/settings.scss +++ b/app/assets/stylesheets/settings.scss @@ -358,6 +358,14 @@ overflow-y: auto; background: white; box-shadow: $shadow; + &.loading-repos { + height: 400px; + background: white url(image_path('loading-ellipsis.svg')) no-repeat center center; + background-size: 50px; + } + &.github-repos-errored { + height: 400px; + } .github-repo-row { background:white; padding: 12px; @@ -367,8 +375,30 @@ } .github-repo-row-name { font-weight:bold; + button { + cursor:pointer; + font-size:1.2em; + font-weight: bold; + width: 130px; + background: white; + border:0px solid; + border-radius:3px; + padding: 3px 0px; + margin-top:-5px; + float: right; + min-height: 28.5px; + &:hover, &:focus { + background: $green; + } + &:disabled { + cursor: wait; + background: white url(image_path('loading-ellipsis.svg')) no-repeat center center; + background-size: 75px; + } + } } .github-repo-fork { + margin-left: 5px; font-weight:400; background: $purple; color:$bold-blue; @@ -376,22 +406,6 @@ padding: 1px 6px; font-size:0.9em; } - form { - display:inline-block; - float:right; - button { - cursor:pointer; - font-size:13px; - background: white; - border:1px solid $medium-gray; - border-radius:3px; - padding: 2px 8px; - margin-top:-5px; - &:hover { - opacity:0.9; - } - } - } .github-repo-featured-indicator { display:inline-block; float:right; diff --git a/app/controllers/api/v0/github_repos_controller.rb b/app/controllers/api/v0/github_repos_controller.rb new file mode 100644 index 000000000..1ffb24514 --- /dev/null +++ b/app/controllers/api/v0/github_repos_controller.rb @@ -0,0 +1,54 @@ +module Api + module V0 + class GithubReposController < ApplicationController + def index + client = create_octokit_client + existing_user_repos = GithubRepo. + where(user_id: current_user.id, featured: true).pluck(:github_id_code) #=> [1,2,3] + existing_user_repos = Set.new(existing_user_repos) + @repos = client.repositories.map do |repo| + repo.selected = existing_user_repos.include?(repo.id) + repo + end + end + + def update_or_create + @client = create_octokit_client + @repo = GithubRepo.find_or_create(fetched_repo_params) + if @repo.valid? + render json: { featured: @repo.featured } + else + render json: "error: #{@repo.errors.full_messages}" + end + end + + private + + def create_octokit_client + current_user_token = Identity.find_by(provider: "github", user_id: current_user.id).token + Octokit::Client.new(access_token: current_user_token) + end + + def fetched_repo_params + params[:github_repo] = JSON.parse(params[:github_repo]) + fetched_repo = @client.repositories.select do |repo| + repo.id == permitted_attributes(GithubRepo)[:github_id_code].to_i + end.first + { + github_id_code: fetched_repo.id, + user_id: current_user.id, + name: fetched_repo.name, + description: fetched_repo.description, + language: fetched_repo.language, + fork: fetched_repo.fork, + url: fetched_repo.html_url, + bytes_size: fetched_repo.size, + watchers_count: fetched_repo.watchers, + stargazers_count: fetched_repo.stargazers_count, + featured: permitted_attributes(GithubRepo)[:featured], + info_hash: fetched_repo.to_hash + } + end + end + end +end diff --git a/app/javascript/githubRepos/__tests__/__snapshots__/githubRepos.test.jsx.snap b/app/javascript/githubRepos/__tests__/__snapshots__/githubRepos.test.jsx.snap new file mode 100644 index 000000000..7da5c19dd --- /dev/null +++ b/app/javascript/githubRepos/__tests__/__snapshots__/githubRepos.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when there are no repos loaded yet should render and match the snapshot 1`] = ` +
+`; + +exports[` when there was an error in the response renders and matches the snapshot 1`] = ` +RenderContext { + "0": VNode { + "attributes": undefined, + "children": Array [], + "key": undefined, + "nodeName": [Function], + }, + "length": 1, +} +`; diff --git a/app/javascript/githubRepos/__tests__/__snapshots__/singleRepo.test.jsx.snap b/app/javascript/githubRepos/__tests__/__snapshots__/singleRepo.test.jsx.snap new file mode 100644 index 000000000..31e6ca280 --- /dev/null +++ b/app/javascript/githubRepos/__tests__/__snapshots__/singleRepo.test.jsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when it is a fork should render and match the snapshot 1`] = ` +
+
+ + dev.to + + fork + +
+
+`; + +exports[` when it is not already selected should render and match the snapshot 1`] = ` +
+
+ + dev.to +
+
+`; + +exports[` when it is selected should render and match the snapshot 1`] = ` +
+
+ + dev.to +
+
+`; diff --git a/app/javascript/githubRepos/__tests__/githubRepos.test.jsx b/app/javascript/githubRepos/__tests__/githubRepos.test.jsx new file mode 100644 index 000000000..6ad93ad0d --- /dev/null +++ b/app/javascript/githubRepos/__tests__/githubRepos.test.jsx @@ -0,0 +1,34 @@ +import { h } from 'preact'; +import { shallow } from 'preact-render-spy'; +import render from 'preact-render-to-json'; +import fetch from 'jest-fetch-mock'; +import { GithubRepos } from '../githubRepos'; + +global.fetch = fetch; + +describe('', () => { + describe('when there are no repos loaded yet', () => { + it('should render and match the snapshot', () => { + const tree = render(); + expect(tree).toMatchSnapshot(); + }); + + it('should have the loading div', () => { + const context = shallow(); + expect(context.find('.loading-repos')[0].attributes.className).toEqual( + 'github-repos loading-repos', + ); + }); + }); + + describe('when there was an error in the response', () => { + it('renders and matches the snapshot', () => { + fetch.mockReject('some error'); + const context = shallow(); + context.setState({ erroredOut: true }); + context.rerender(); + const tree = render(context); + expect(tree).toMatchSnapshot(); + }); + }); +}); diff --git a/app/javascript/githubRepos/__tests__/singleRepo.test.jsx b/app/javascript/githubRepos/__tests__/singleRepo.test.jsx new file mode 100644 index 000000000..c8445ccfb --- /dev/null +++ b/app/javascript/githubRepos/__tests__/singleRepo.test.jsx @@ -0,0 +1,56 @@ +import { h } from 'preact'; +import { shallow } from 'preact-render-spy'; +import render from 'preact-render-to-json'; +import fetch from 'jest-fetch-mock'; +import { SingleRepo } from '../singleRepo'; + +global.fetch = fetch; + +describe('', () => { + describe('when it is not already selected', () => { + const subject = ( + + ); + + it('should render and match the snapshot', () => { + const tree = render(subject); + expect(tree).toMatchSnapshot(); + }); + + it('should have a state of { selected: false }', () => { + const context = shallow(subject); + expect(context.state()).toEqual({ selected: false }); + }); + }); + + describe('when it is selected', () => { + const subject = ( + + ); + it('should render and match the snapshot', () => { + const tree = render(subject); + expect(tree).toMatchSnapshot(); + }); + + it('should have a state of { selected: true }', () => { + const context = shallow(subject); + expect(context.state()).toEqual({ selected: true }); + }); + }); + + describe('when it is a fork', () => { + const subject = ( + + ); + + it('should render and match the snapshot', () => { + const tree = render(subject); + expect(tree).toMatchSnapshot(); + }); + }); +}); diff --git a/app/javascript/githubRepos/githubRepos.jsx b/app/javascript/githubRepos/githubRepos.jsx new file mode 100644 index 000000000..7238f63a5 --- /dev/null +++ b/app/javascript/githubRepos/githubRepos.jsx @@ -0,0 +1,59 @@ +import { h, Component } from 'preact'; +import { SingleRepo } from './singleRepo'; + +export class GithubRepos extends Component { + state = { + repos: [], + erroredOut: false, + }; + + componentDidMount() { + this.getGithubRepos(); + } + + getGithubRepos = () => { + fetch(`/api/github_repos`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(json => { + this.setState({ repos: json }); + }) + .catch(error => { + console.log(error); + this.setState({ erroredOut: true }); + }); + }; + + render() { + const { repos, erroredOut } = this.state; + const allRepos = repos.map(repo => ( + + )); + + if (erroredOut) { + return ( +
+ An error occurred. Please check your browser console and email + yo@dev.to + for more help. +
+ ); + } + if (repos.length > 0) { + return
{allRepos}
; + } + return
; + } +} + +GithubRepos.displayName = 'GitHub Repos Wrapper'; diff --git a/app/javascript/githubRepos/singleRepo.jsx b/app/javascript/githubRepos/singleRepo.jsx new file mode 100644 index 000000000..3ff9f5297 --- /dev/null +++ b/app/javascript/githubRepos/singleRepo.jsx @@ -0,0 +1,89 @@ +import { h, Component } from 'preact'; +import PropTypes from 'prop-types'; + +export class SingleRepo extends Component { + constructor(props) { + super(props); + const { selected } = this.props; + this.state = { selected }; + } + + forkLabel = () => { + const { fork } = this.props; + if (fork) { + return fork; + } + return null; + }; + + submitRepo = () => { + const { selected } = this.state; + const { githubIdCode } = this.props; + + const submitButton = document.getElementById( + `github-repo-button-${githubIdCode}`, + ); + submitButton.textContent = ''; + submitButton.disabled = true; + + const csrfToken = document.querySelector("meta[name='csrf-token']").content; + const formData = new FormData(); + const formAttributes = { + github_id_code: githubIdCode, + featured: !selected, + }; + formData.append('github_repo', JSON.stringify(formAttributes)); + + fetch('/api/github_repos/update_or_create', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': csrfToken, + }, + body: formData, + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(json => { + submitButton.disabled = false; + this.setState({ selected: json.featured }); + }); + }; + + githubRepoClassName = () => { + const { selected } = this.state; + if (selected) { + return 'github-repo-row github-repo-row-selected'; + } + return 'github-repo-row'; + }; + + render() { + const { selected } = this.state; + const { name, githubIdCode } = this.props; + return ( +
+
+ + {name} + {this.forkLabel()} +
+
+ ); + } +} + +SingleRepo.displayName = 'Single GitHub Repo'; + +SingleRepo.propTypes = { + name: PropTypes.string.isRequired, + githubIdCode: PropTypes.number.isRequired, + fork: PropTypes.bool.isRequired, + selected: PropTypes.bool.isRequired, +}; diff --git a/app/javascript/packs/githubRepos.jsx b/app/javascript/packs/githubRepos.jsx new file mode 100644 index 000000000..511a95e13 --- /dev/null +++ b/app/javascript/packs/githubRepos.jsx @@ -0,0 +1,8 @@ +import { h, render } from 'preact'; +import { GithubRepos } from '../githubRepos/githubRepos'; + +document.addEventListener('DOMContentLoaded', () => { + const root = document.getElementById('github-repos-container'); + + render(, root, root.firstElementChild); +}); diff --git a/app/policies/github_repo_policy.rb b/app/policies/github_repo_policy.rb index 52a105f74..6c081f4fc 100644 --- a/app/policies/github_repo_policy.rb +++ b/app/policies/github_repo_policy.rb @@ -8,7 +8,7 @@ class GithubRepoPolicy < ApplicationPolicy end def permitted_attributes - %i[github_id_code] + %i[github_id_code featured] end private diff --git a/app/views/api/v0/github_repos/index.json.jbuilder b/app/views/api/v0/github_repos/index.json.jbuilder new file mode 100644 index 000000000..5e662a26f --- /dev/null +++ b/app/views/api/v0/github_repos/index.json.jbuilder @@ -0,0 +1,6 @@ +json.array! @repos.each do |repo| + json.github_id_code repo.id + json.name repo.name + json.fork repo.fork + json.selected repo.selected +end diff --git a/app/views/users/_integrations.html.erb b/app/views/users/_integrations.html.erb index 978e07d64..d817e710e 100644 --- a/app/views/users/_integrations.html.erb +++ b/app/views/users/_integrations.html.erb @@ -1,33 +1,9 @@

Pin a few of your GitHub repos to your profile

<% if @client %> -
- <% @client.repositories.each do |repo| %> - <% if (user_repo = GithubRepo.find_by(github_id_code: repo.id)) && user_repo.featured %> -
-
- <%= repo.name %> <%= "fork".html_safe if repo.fork %> - <%= form_for(user_repo) do |f| %> - - <% end %> -
-
- <% else %> -
-
- <%= repo.name %> <%= "fork".html_safe if repo.fork %> - <%= form_for(GithubRepo.new) do |f| %> - <%= f.hidden_field :github_id_code, value: repo.id %> - - <% end %> -
-
- <% end %> - <% end %> -
-<% else %> -
- - CONNECT GITHUB ACCOUNT - +
+
+
+ <%= javascript_pack_tag "githubRepos", defer: true %> + <% end %> diff --git a/config/routes.rb b/config/routes.rb index 0d25ac310..f02c16b34 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,11 @@ Rails.application.routes.draw do end end resources :follows, only: [:create] + resources :github_repos, only: [:index] do + collection do + post "/update_or_create", to: "github_repos#update_or_create" + end + end end end