diff --git a/resources/migrations/20180314124717-store-pr-title.down.sql b/resources/migrations/20180314124717-store-pr-title.down.sql new file mode 100644 index 0000000..d8d5a59 --- /dev/null +++ b/resources/migrations/20180314124717-store-pr-title.down.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."pull_requests" DROP COLUMN "title"; diff --git a/resources/migrations/20180314124717-store-pr-title.up.sql b/resources/migrations/20180314124717-store-pr-title.up.sql new file mode 100644 index 0000000..1a3e001 --- /dev/null +++ b/resources/migrations/20180314124717-store-pr-title.up.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."pull_requests" ADD COLUMN "title" character varying(256); diff --git a/resources/migrations/20180330143004-pr-data-activity-feed.down.sql b/resources/migrations/20180330143004-pr-data-activity-feed.down.sql new file mode 100644 index 0000000..2fb71ed --- /dev/null +++ b/resources/migrations/20180330143004-pr-data-activity-feed.down.sql @@ -0,0 +1,78 @@ +-- restore the previous version of the view +CREATE VIEW "public"."claims_view" AS SELECT i.title AS issue_title, + i.issue_number, + r.repo AS repo_name, + r.owner AS repo_owner, + COALESCE(u.name, u.login) AS user_name, + u.avatar_url AS user_avatar_url, + i.payout_receipt, + p.updated, + i.updated AS issue_updated, + i.balance_eth, + i.tokens, + i.value_usd, + p.state AS pr_state, + i.is_open AS issue_open, + (case when u.address IS NULL THEN false ELSE true END) AS user_has_address + FROM issues i, + users u, + repositories r, + pull_requests p + WHERE r.repo_id = i.repo_id + AND p.issue_id = i.issue_id + AND p.user_id = u.id + AND i.contract_address IS NOT NULL + AND i.comment_id IS NOT NULL + ORDER BY p.updated; + + +CREATE OR REPLACE VIEW "public"."activity_feed_view" AS + SELECT 'open-claim'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.updated + FROM claims_view + WHERE claims_view.pr_state = 0 + AND claims_view.payout_receipt IS NULL + AND claims_view.issue_open IS TRUE +UNION + SELECT 'claim-pending'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NULL +UNION + SELECT 'claim-payout'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NOT NULL + ORDER BY 12 DESC; diff --git a/resources/migrations/20180330143004-pr-data-activity-feed.up.sql b/resources/migrations/20180330143004-pr-data-activity-feed.up.sql new file mode 100644 index 0000000..38a0cf8 --- /dev/null +++ b/resources/migrations/20180330143004-pr-data-activity-feed.up.sql @@ -0,0 +1,91 @@ +DROP VIEW "public"."claims_view" CASCADE; + +CREATE VIEW "public"."claims_view" AS SELECT i.title AS issue_title, + i.issue_number, + r.repo AS repo_name, + r.owner AS repo_owner, + p.title AS pr_title, -- added + p.pr_number AS pr_number, -- added + p.pr_id AS pr_id, -- added + COALESCE(u.name, u.login) AS user_name, + u.avatar_url AS user_avatar_url, + i.payout_receipt, + p.updated, + i.updated AS issue_updated, + i.balance_eth, + i.tokens, + i.value_usd, + p.state AS pr_state, + i.is_open AS issue_open, + (case when u.address IS NULL THEN false ELSE true END) AS user_has_address + FROM issues i, + users u, + repositories r, + pull_requests p + WHERE r.repo_id = i.repo_id + AND p.issue_id = i.issue_id + AND p.user_id = u.id + AND i.contract_address IS NOT NULL + AND i.comment_id IS NOT NULL + ORDER BY p.updated; + + +CREATE VIEW "public"."activity_feed_view" AS -- altered + SELECT 'open-claim'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.updated + FROM claims_view + WHERE claims_view.pr_state = 0 + AND claims_view.payout_receipt IS NULL + AND claims_view.issue_open IS TRUE +UNION + SELECT 'claim-pending'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NULL +UNION + SELECT 'claim-payout'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NOT NULL + ORDER BY 12 DESC; diff --git a/resources/public/blue-arrow-down.png b/resources/public/blue-arrow-down.png new file mode 100644 index 0000000..962ed79 Binary files /dev/null and b/resources/public/blue-arrow-down.png differ diff --git a/resources/public/blue-arrow-up.png b/resources/public/blue-arrow-up.png new file mode 100644 index 0000000..16774b7 Binary files /dev/null and b/resources/public/blue-arrow-up.png differ diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 4c061f8..ef40b5f 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -207,6 +207,7 @@ AND i.transaction_hash IS NOT NULL; INSERT INTO pull_requests (pr_id, repo_id, pr_number, + title, issue_number, issue_id, commit_sha, @@ -215,6 +216,7 @@ INSERT INTO pull_requests (pr_id, VALUES(:pr_id, :repo_id, :pr_number, + :title, :issue_number, :issue_id, :commit_sha, @@ -225,6 +227,7 @@ SET state = :state, issue_number = :issue_number, issue_id = :issue_id, + title = :title, updated = timezone('utc'::text, now()), commit_sha = :commit_sha; @@ -592,6 +595,9 @@ SELECT issue_title, repo_name, repo_owner, + pr_number, + pr_title, + pr_id, issue_number, user_name, user_avatar_url, diff --git a/src/clj/commiteth/routes/services.clj b/src/clj/commiteth/routes/services.clj index ce5d426..656c804 100644 --- a/src/clj/commiteth/routes/services.clj +++ b/src/clj/commiteth/routes/services.clj @@ -109,10 +109,14 @@ renames {:user_name :display-name :user_avatar_url :avatar-url :issue_title :issue-title + :pr_title :pr-title + :pr_number :pr-number + :pr_id :pr-id :type :item-type :repo_name :repo-name :repo_owner :repo-owner :issue_number :issue-number + :issue_id :issue-id :value_usd :value-usd :claim_count :claim-count :balance_eth :balance-eth diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index b7fc05d..88b374d 100644 --- a/src/clj/commiteth/routes/webhooks.clj +++ b/src/clj/commiteth/routes/webhooks.clj @@ -115,18 +115,18 @@ (defn handle-claim - [issue user-id login name avatar_url owner repo repo-id pr-id pr-number head-sha merged? event-type] + [issue user-id login name avatar_url owner repo repo-id pr-id pr-number pr-title head-sha merged? event-type] (users/create-user user-id login name nil avatar_url) (let [open-or-edit? (contains? #{:opened :edited} event-type) close? (= :closed event-type) pr-data {:repo_id repo-id :pr_id pr-id :pr_number pr-number + :title pr-title :user_id user-id :issue_number (:issue_number issue) :issue_id (:issue_id issue) :state event-type}] - ;; TODO: in the opened case if the submitting user has no ;; Ethereum address stored, we could post a comment to the ;; Github PR explaining that payout is not possible if the PR is @@ -187,6 +187,7 @@ repo-id pr-id pr-number + pr-title head-sha merged? event-type)) diff --git a/src/cljs/commiteth/bounties.cljs b/src/cljs/commiteth/bounties.cljs index f1fa460..0e30bd8 100644 --- a/src/cljs/commiteth/bounties.cljs +++ b/src/cljs/commiteth/bounties.cljs @@ -4,51 +4,95 @@ [commiteth.common :refer [human-time display-data-page items-per-page - issue-url]] + issue-url + pull-request-url]] [commiteth.handlers :as handlers] [commiteth.db :as db] [commiteth.ui-model :as ui-model] [commiteth.subscriptions :as subs] [commiteth.util :as util])) +(defn display-bounty-claims [claims] + [:div.bounty-claims-container.pv3 + (for [claim claims] + (let [{:keys [avatar-url + pr-title + pr-id + updated + repo-owner + repo-name + pr-number]} claim] + ^{:key pr-id} + [:div.bounty-claims-row.open-bounty-item-content.flex + [:div.bounty-claims-icon.pl2 + [:div.ui.tiny.circular.image + [:img {:src avatar-url}]]] + [:span.bounty-claims-text.pt2.pl2 + [:a.fw5 + {:href (pull-request-url repo-owner repo-name pr-number)} + (if pr-title + pr-title + (str "#" pr-number " by " repo-owner " in " repo-name))] + [:span.time.pl2 (human-time updated)]]]))]) + +(defn blue-arrow-box [image-src] + "generates the appropriate container for a blue arrow" + [:span.blue-arrow-box.pa1 + [:img.blue-arrow-image.v-mid {:src image-src}]]) (defn bounty-item [bounty] - (let [{avatar-url :repo_owner_avatar_url - owner :repo-owner - repo-name :repo-name - issue-title :issue-title - issue-number :issue-number - updated :updated - tokens :tokens - balance-eth :balance-eth - value-usd :value-usd - claim-count :claim-count} bounty - full-repo (str owner "/" repo-name) - repo-url (str "https://github.com/" full-repo) - repo-link [:a {:href repo-url} full-repo] - issue-link [:a - {:href (issue-url owner repo-name issue-number)} - issue-title]] - [:div.open-bounty-item - [:div.open-bounty-item-content - [:div.header issue-link] - [:div.bounty-item-row - [:div.time (human-time updated)] - [:span.bounty-repo-label repo-link]] - - [:div.footer-row - [:div.balance-badge "ETH " balance-eth] - (for [[tla balance] tokens] - ^{:key (random-uuid)} - [:div.balance-badge.token - (str (subs (str tla) 1) " " balance)]) - [:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)] - (when (> claim-count 0) - [:span.open-claims-label (str claim-count " open claim" - (when (> claim-count 1) "s"))])]] - [:div.open-bounty-item-icon - [:div.ui.tiny.circular.image - [:img {:src avatar-url}]]]])) + (let [open-bounty-claims (rf/subscribe [::subs/open-bounty-claims])] + (fn [bounty] + (let [{avatar-url :repo_owner_avatar_url + owner :repo-owner + repo-name :repo-name + issue-title :issue-title + issue-number :issue-number + issue-id :issue-id + updated :updated + tokens :tokens + balance-eth :balance-eth + value-usd :value-usd + claim-count :claim-count + claims :claims} bounty + full-repo (str owner "/" repo-name) + repo-url (str "https://github.com/" full-repo) + repo-link [:a {:href repo-url} full-repo] + issue-link [:a + {:href (issue-url owner repo-name issue-number)} + issue-title] + open-claims-click #(rf/dispatch [::handlers/open-bounty-claim issue-id]) + close-claims-click #(rf/dispatch [::handlers/close-bounty-claim issue-id]) + matches-current-issue? (some #{issue-id} @open-bounty-claims)] + [:div + [:div.open-bounty-item + [:div.open-bounty-item-content + [:div.header issue-link] + [:div.bounty-item-row + [:div.time (human-time updated)] + [:span.bounty-repo-label repo-link]] + [:div.footer-row + [:div.balance-badge "ETH " balance-eth] + (for [[tla balance] tokens] + ^{:key (random-uuid)} + [:div.balance-badge.token + (str (subs (str tla) 1) " " balance)]) + [:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)] + (when (> claim-count 0) + [:span.open-claims-label + {:on-click (if matches-current-issue? + #(close-claims-click) + #(open-claims-click))} + (str claim-count " open claim" + (when (> claim-count 1) "s")) + (if matches-current-issue? + [blue-arrow-box "blue-arrow-up.png"] + [blue-arrow-box "blue-arrow-down.png"])])]] + [:div.open-bounty-item-icon + [:div.ui.tiny.circular.image + [:img {:src avatar-url}]]]] + (when matches-current-issue? + [display-bounty-claims claims])])))) (defn bounties-filter-tooltip-value-input-view [label tooltip-open? opts] [:div.open-bounties-filter-element-tooltip-value-input-container diff --git a/src/cljs/commiteth/common.cljs b/src/cljs/commiteth/common.cljs index 7b87894..4eec7c9 100644 --- a/src/cljs/commiteth/common.cljs +++ b/src/cljs/commiteth/common.cljs @@ -48,6 +48,9 @@ (defn issue-url [owner repo number] (str "https://github.com/" owner "/" repo "/issues/" number)) +(defn pull-request-url [owner repo number] + (str "https://github.com/" owner "/" repo "/pull/" number)) + (def items-per-page 15) (defn draw-page-numbers [page-number page-count container-element] diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs index c92b2a1..295969f 100644 --- a/src/cljs/commiteth/db.cljs +++ b/src/cljs/commiteth/db.cljs @@ -4,7 +4,7 @@ (def default-db {:page :bounties :user nil - :user-profile-loaded? false + :user-profile-loaded? false :repos-loading? false :repos {} :activity-feed-loading? false @@ -18,6 +18,7 @@ ::ui-model/bounty-filter-type|currency nil ::ui-model/bounty-filter-type|date nil ::ui-model/bounty-filter-type|owner nil} + ::open-bounty-claims #{} :owner-bounties {} :top-hunters [] :activity-feed []}) diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index 61447c1..e5579fa 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -484,6 +484,16 @@ (.removeEventListener js/window "click" close-dropdown) (assoc db :user-dropdown-open? false))) +(reg-event-db + ::open-bounty-claim + (fn [db [_ opening-issue-id]] + (update db ::db/open-bounty-claims #(conj % opening-issue-id)))) + +(reg-event-db + ::close-bounty-claim + (fn [db [_ closing-issue-id]] + (update db ::db/open-bounty-claims #(disj % closing-issue-id)))) + (reg-event-db ::set-open-bounties-sorting-type (fn [db [_ sorting-type]] diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs index 3ecd6e2..24ffbb7 100644 --- a/src/cljs/commiteth/subscriptions.cljs +++ b/src/cljs/commiteth/subscriptions.cljs @@ -53,17 +53,24 @@ :open-bounties-page :<- [::filtered-and-sorted-open-bounties] :<- [:page-number] - (fn [[open-bounties page-number] _] + :<- [:activity-feed] + (fn [[open-bounties page-number activity-feed] _] (let [total-count (count open-bounties) - start (* (dec page-number) items-per-page) - end (min total-count (+ items-per-page start)) - items (subvec open-bounties start end)] - {:items items - :item-count (count items) + start (* (dec page-number) items-per-page) + end (min total-count (+ items-per-page start)) + items (->> (subvec open-bounties start end) + (map (fn [bounty] + (let [matching-claims (filter + (fn [claim] + (= (:issue-number claim) + (:issue-number bounty))) + activity-feed)] + (assoc bounty :claims matching-claims)))))] + {:items items + :item-count (count items) :total-count total-count :page-number page-number - :page-count (Math/ceil (/ total-count items-per-page))}))) - + :page-count (Math/ceil (/ total-count items-per-page))}))) (reg-sub :owner-bounties @@ -146,6 +153,11 @@ (fn [db _] (:user-dropdown-open? db))) +(reg-sub + ::open-bounty-claims + (fn [db _] + (::db/open-bounty-claims db))) + (reg-sub ::open-bounties-sorting-type (fn [db _] diff --git a/src/less/style.less b/src/less/style.less index 0c3d7c7..714fb7a 100644 --- a/src/less/style.less +++ b/src/less/style.less @@ -714,6 +714,17 @@ label[for="input-hidden"] { padding: 4px; text-align: center; } + + .blue-arrow-box { + width: 24px; + height: 24px; + } + + .blue-arrow-image { + width: 13.5px; + height: 6px; + } + } .open-bounty-item:first-child { @@ -748,8 +759,6 @@ label[for="input-hidden"] { font-size: 15px; } - border-bottom: #eaecee 1px solid !important; - .open-bounty-item-content { width: 80%; padding: .7em 0 1em; @@ -780,8 +789,28 @@ label[for="input-hidden"] { flex-direction: row !important; justify-content: space-between; width: 80%; - } + +.bounty-claims-row { + padding-top: 6px; + padding-bottom: 6px; +} + +.bounty-claims-container { + background-color: #FBFBFB; +} + +.bounty-claims-icon { + width: 40px; + } + +.bounty-claims-text { + a { + text-decoration: none; + color: #42505c; + } +} + .bounty-repo-label a { margin: auto; font-family: "PostGrotesk-Book"; @@ -804,6 +833,7 @@ label[for="input-hidden"] { padding-left: 15px; font-size: 15px; color: #57a7ed; + cursor: pointer; } .activity-item-container {