diff --git a/src/cljs/ulysses/components/basic.cljs b/src/cljs/ulysses/components/basic.cljs index 31b3364..5d1e140 100644 --- a/src/cljs/ulysses/components/basic.cljs +++ b/src/cljs/ulysses/components/basic.cljs @@ -59,6 +59,22 @@ [:div.tooltip-arrow] [:div.tooltip-inner text]]) +(defn tab + [name on-click is-active] + [:li.nav-item + [hink + on-click + (classes-attr :nav-link {:active is-active}) + name]]) + +(defn tabs + [ts] + [:ul.nav.nav-tabs + (map-subels + (fn [{:keys [name on-click is-active]}] + [tab name on-click is-active]) + ts)]) + ;; ---------------------------------------------------------------------------- ;; css transition group ;; ---------------------------------------------------------------------------- diff --git a/src/cljs/ulysses/db.cljs b/src/cljs/ulysses/db.cljs index aadebb2..f6cfab0 100644 --- a/src/cljs/ulysses/db.cljs +++ b/src/cljs/ulysses/db.cljs @@ -36,10 +36,15 @@ ; grant ops (bare-bones, for listings) :grant-ops [] - ; faculties (extended, with keywords) + ; workspaces (bare-bones) + :workspaces [{:id 1 :name "a" :grant-op-id 1} + {:id 2 :name "b" :grant-op-id 1} + {:id 3 :name "c" :grant-op-id 1}] + + ; faculties (extended, with keywords) [builder] :faculties {} - ; faculty titles + ; faculty titles [builder] :faculty-titles [] ;; builder ---- @@ -50,8 +55,15 @@ ; builder: current faculty pool results given filters (with metrics) :builder-faculties-pool [] - ; builder: current faculty workspace (list of ids) - :builder-faculties-workspace [] + ; builder: current workspace (bare-bones) + :builder-workspace nil + + ; builder: current workspace faculties (list of ids) + :builder-workspace-faculties [] + + ; builder: until back-end persistence, just store faculty-workspace + ; relationships as "join rows", ex. {:faculty-id 3 :workspace-id 4} + :TMP-builder-faculty-workspaces [] ;; ------------------------------------------------------------------------- ;; user data @@ -76,4 +88,9 @@ :faculty-years-uconn 0} ; user workspace-faculty hover (id) - :builder-faculties-workspace-hover nil}) + :builder-workspace-faculty-hover nil}) + +(defn refresh-keys + "given a db, set the given keys to their original state" + [db keys] + (merge db (select-keys default-db keys))) diff --git a/src/cljs/ulysses/handlers.cljs b/src/cljs/ulysses/handlers.cljs index 4f45f46..9a1871e 100644 --- a/src/cljs/ulysses/handlers.cljs +++ b/src/cljs/ulysses/handlers.cljs @@ -11,6 +11,10 @@ parse-int-id str->int to-id-map + find-by-id + remove-by-id + merge-by-id + new-id scroll-to request faculties-pool-params @@ -101,7 +105,7 @@ (request db [:funding grant-op-id] :handler #(dispatch [:receive-builder-grant-op %]) - :error-handler #(dispatch [:nil-builder-grant-op]))) + :error-handler #(dispatch [:nil-builder]))) db)) (register-handler @@ -112,11 +116,6 @@ :nih_rfa clean-incoming-grant-op)))) -(register-handler - :nil-builder-grant-op - (fn [db _] - (assoc db :builder-grant-op nil))) - ; faculties pool ---- (register-handler @@ -139,11 +138,6 @@ (assoc db :builder-faculties-pool response)))) -(register-handler - :nil-faculties-pool - (fn [db _] - (assoc db :builder-faculties-pool []))) - ; grant ops ---- (register-handler @@ -196,30 +190,127 @@ ;; user data ;; ---------------------------------------------------------------------------- -;; working faculties ---- +;; workspace management ---- + +;; NOTE most of the code in this section interfaces with +;; db :workspaces -- eventually, all five handlers +;; should really just be plain http requests so that +;; the back-end handles this stuff + +(register-handler + :workspace-new-blank + (fn [db _] + (if-let [op (:builder-grant-op db)] + (let [id (-> db :workspaces new-id)] + (dispatch [:workspace-switch id]) + (-> db + (update :workspaces + (fn [ws] + (conj ws + {:id id + :name (str id) + :user-id 1 ; temporary, of course + :grant-op-id (:id op)}))))) + db))) + +(register-handler + :workspace-new-default + (fn [db _] + (dispatch [:workspace-new-blank]) + ; TODO + db)) + +; rename current workspace +(register-handler + :workspace-rename + (fn [db [_ name]] + (if-let [current (:builder-workspace db)] + (-> db + (assoc-in [:builder-workspace :name] name) + (update :workspaces + (fn [ws] + (if-let [f (find-by-id (:id current) ws)] + (->> name + (assoc f :name) + (vector) + (merge-by-id ws) + (vec)) + ws)))) + db))) + +; delete current workspace +(register-handler + :workspace-delete + (fn [db _] + (dispatch [:nil-workspace]) + (dispatch [:workspace-switch nil]) + (if-let [current (:builder-workspace db)] + (update db :workspaces + (fn [ws] + (vec (remove-by-id (:id current) ws)))) + db))) + +(register-handler + :workspace-switch + (fn [db [_ workspace-id-maybe]] + (if workspace-id-maybe + (if-let [target (find-by-id workspace-id-maybe (:workspaces db))] + (-> db + (assoc :builder-workspace target) + (assoc :builder-workspace-faculties + ; TMP workspace persistence + (->> db + :TMP-builder-faculty-workspaces + (filter #(= workspace-id-maybe (:workspace-id %))) + (map :faculty-id)))) + db) + db))) + +;; workspace faculties management ---- (register-handler :add-working-faculty (fn [db [_ faculty-id]] (dispatch [:request-faculty faculty-id]) - (update db :builder-faculties-workspace - (fn [current] - (-> faculty-id - (cons current) - (distinct)))))) + (if-let [current (:builder-workspace db)] + (-> db + (update :builder-workspace-faculties + (fn [fids] + (->> faculty-id + (conj fids) + (distinct)))) + (update :TMP-builder-faculty-workspaces + (fn [fws] + (conj fws + {:faculty-id faculty-id + :workspace-id (:id current)})))) + ; TODO create new workspace from default, + ; then add this new working faculty + db))) (register-handler :remove-working-faculty (fn [db [_ faculty-id]] ;; since the remove button is in the hover area... - (dispatch [:nil-builder-faculties-workspace-hover]) - (update db :builder-faculties-workspace - (partial remove #{faculty-id})))) - -(register-handler - :nil-faculties-workspace - (fn [db _] - (assoc db :builder-faculties-workspace []))) + (dispatch [:nil-builder-workspace-faculty-hover]) + (if-let [current (:builder-workspace db)] + (-> db + (update :builder-workspace-faculties + (fn [fws] + (vec (remove #{faculty-id} fws)))) + (update :TMP-builder-faculty-workspaces + (let [current-id (:id current)] + (fn [fws] + (vec + (remove + (fn [fw] + (and + (= faculty-id (:faculty-id %)) + (= current-id (:workspace-id %)))) + fws)))))) + ; TODO create new workspace from default, + ; then remove this new working faculty + db))) ;; home filters ---- @@ -265,13 +356,34 @@ ;; builder faculty workspace hover ---- (register-handler - :builder-faculties-workspace-hover + :builder-workspace-faculty-hover (fn [db [_ faculty-id]] - (assoc db :builder-faculties-workspace-hover + (assoc db :builder-workspace-faculty-hover faculty-id))) (register-handler - :nil-builder-faculties-workspace-hover + :nil-builder-workspace-faculty-hover (fn [db _] - (assoc db :builder-faculties-workspace-hover + (assoc db :builder-workspace-faculty-hover nil))) + +;; builder nil + +(register-handler + :nil-workspace + (fn [db _] + (db/refresh-keys db + [:builder-workspace + :builder-workspace-faculties + :builder-workspace-faculty-hover]))) + +(register-handler + :nil-builder + (fn [db _] + (db/refresh-keys db + [:builder-grant-op + :builder-faculties-pool + :builder-workspace + :builder-workspace-faculties + :builder-filters + :builder-workspace-faculty-hover]))) diff --git a/src/cljs/ulysses/pages/builder.cljs b/src/cljs/ulysses/pages/builder.cljs index 79e358b..21f6473 100644 --- a/src/cljs/ulysses/pages/builder.cljs +++ b/src/cljs/ulysses/pages/builder.cljs @@ -1,9 +1,9 @@ (ns ulysses.pages.builder (:require [re-frame.core :as re-frame :refer [subscribe dispatch]] - [ulysses.components.basic :refer [hink link loading-or-no-results fa]] + [ulysses.components.basic :refer [hink link loading-or-no-results fa tabs]] [ulysses.components.misc :refer [grant-op-meta]] [ulysses.components.word-cloud :refer [word-cloud]] - [ulysses.utils :refer [map-subels map-lookup str->int]] + [ulysses.utils :refer [map-subels map-lookup str->int classes-attr]] [reagent.core :as r] [clojure.string :as string] [cljs.pprint :as pprint])) @@ -56,7 +56,7 @@ (into [:div] body)]) (defn sidebar - [faculty-titles builder-filters] + [faculty-titles filters] [:div.builder-panel-sidebar [:h3 "Filters"] @@ -64,18 +64,18 @@ "Titles" [faculty-title-multiselect faculty-titles - builder-filters]] + filters]] [sidebar-section "Years at UConn" [faculty-years-uconn - builder-filters]]]) + filters]]]) (defn workspace-row [faculty] (let [{:keys [id name title year_hired]} faculty] [:tr.faculty-row - {:on-mouse-over #(dispatch [:builder-faculties-workspace-hover id]) - :on-mouse-leave #(dispatch [:nil-builder-faculties-workspace-hover])} + {:on-mouse-over #(dispatch [:builder-workspace-faculty-hover id]) + :on-mouse-leave #(dispatch [:nil-builder-workspace-faculty-hover])} [:td.first-col [hink (partial dispatch [:remove-working-faculty id]) @@ -97,6 +97,34 @@ [:tbody (map-subels workspace-row faculties)]]])) +(defn workspace-header-tabs [workspaces workspace-current] + [tabs + (map + (fn [{:keys [id name]}] + ; make tab-map + {:name (if-not (string/blank? name) name (str "untitled #" id)) + :on-click #(dispatch [:workspace-switch id]) + :is-active (= id (:id workspace-current))}) + workspaces)]) + +(defn workspace-header [workspaces workspace-current] + (let [btn-attrs (classes-attr :btn :btn-sm :btn-primary)] + [:div + [:div.workspace-header-first + [:h3 "Working Team"] + [:div.workspace-actions + [hink #(dispatch [:workspace-new-blank]) btn-attrs "New Blank"] + [hink #(dispatch [:workspace-new-default]) btn-attrs "New from Default"]]] + [workspace-header-tabs workspaces workspace-current]])) + +(defn workspace-footer [workspace-current] + [:div.workspace-footer + (when workspace-current + [hink + #(dispatch [:workspace-delete]) + (classes-attr :btn :btn-sm :btn-danger) + "Delete Current Workspace"])]) + (defn pool-row [fam] (let [{:keys [faculty fundingCoverageScore]} fam {:keys [id name title year_hired]} faculty] @@ -125,21 +153,22 @@ (map-subels pool-row faculties-and-metrics)]]])) (defn builder-panel - [op faculties faculty-titles - faculties-pool faculties-workspace - faculties-workspace-hover - builder-filters] + [op + faculties faculty-titles faculties-pool + workspaces workspace-current workspace-faculties workspace-faculty-hover + filters] [:div.builder-panel - [sidebar faculty-titles builder-filters] + [sidebar faculty-titles filters] [:div.builder-panel-main [:div.builder-panel-half-section [:div.inner - [:h3 "Working Team"] - [workspace (map-lookup faculties faculties-workspace)]] + [workspace-header workspaces workspace-current] + [workspace (map-lookup faculties workspace-faculties)] + [workspace-footer workspace-current]] [:div.inner [:h3 "Word Cloud"] - (when faculties-workspace-hover - [word-cloud 20 (:keywords faculties-workspace-hover)])]] + (when workspace-faculty-hover + [word-cloud 20 (:keywords workspace-faculty-hover)])]] [:div.builder-panel-half-section [:div.inner [:h3 "Results"] @@ -164,19 +193,27 @@ (defn main [] (let [page-with-args (subscribe [:page-with-args]) - builder-filters (subscribe [:builder-filters]) + grant-op (subscribe [:builder-grant-op]) + faculties (subscribe [:faculties]) faculty-titles (subscribe [:faculty-titles]) faculties-pool (subscribe [:builder-faculties-pool]) - faculties-workspace (subscribe [:builder-faculties-workspace]) - faculties-workspace-hover (subscribe [:builder-faculties-workspace-hover]) + + workspaces (subscribe [:workspaces-current]) + workspace (subscribe [:builder-workspace]) + workspace-faculties (subscribe [:builder-workspace-faculties]) + workspace-faculty-hover (subscribe [:builder-workspace-faculty-hover]) + + filters (subscribe [:builder-filters]) + last-id (atom nil)] (r/create-class {:component-will-mount (fn [] (let [id (get-grant-op-id @page-with-args)] (reset! last-id id) + (dispatch [:nil-builder]) (dispatch [:request-faculty-titles]) ; common with will-update: (dispatch [:request-builder-grant-op id]) @@ -186,27 +223,33 @@ (let [pwa @page-with-args id (get-grant-op-id pwa)] (when (and (not= id @last-id) (= :builder (first pwa))) + (dispatch [:nil-builder]) (dispatch [:request-builder-grant-op id]) (dispatch [:request-faculties-pool])) (reset! last-id id))) :component-will-unmount (fn [] - (dispatch [:nil-faculties-pool]) - (dispatch [:nil-faculties-workspace])) + (dispatch [:nil-builder])) :reagent-render (fn [] (let [op @grant-op - fa @faculties - ft @faculty-titles - fp @faculties-pool - fw @faculties-workspace - fh @faculties-workspace-hover - bf @builder-filters _ @page-with-args] (if op [:div [builder-header op] - [builder-panel op fa ft fp fw fh bf]] + [builder-panel + op + + @faculties + @faculty-titles + @faculties-pool + + @workspaces + @workspace + @workspace-faculties + @workspace-faculty-hover + + @filters]] [loading-or-no-results :not-found-message "The requested grant opportunity was not found."])))}))) diff --git a/src/cljs/ulysses/subs.cljs b/src/cljs/ulysses/subs.cljs index fd96e2b..11cfce5 100644 --- a/src/cljs/ulysses/subs.cljs +++ b/src/cljs/ulysses/subs.cljs @@ -59,6 +59,11 @@ (fn [db _] (reaction (:grant-ops @db)))) +(register-sub + :builder-grant-op + (fn [db _] + (reaction (:builder-grant-op @db)))) + (register-sub :faculties (fn [db _] @@ -70,19 +75,24 @@ (reaction (:faculty-titles @db)))) (register-sub - :builder-grant-op + :builder-faculties-pool (fn [db _] - (reaction (:builder-grant-op @db)))) + (reaction (:builder-faculties-pool @db)))) (register-sub - :builder-faculties-pool + :workspaces (fn [db _] - (reaction (:builder-faculties-pool @db)))) + (reaction (:workspaces @db)))) + +(register-sub + :builder-workspace + (fn [db _] + (reaction (:builder-workspace @db)))) (register-sub - :builder-faculties-workspace + :builder-workspace-faculties (fn [db _] - (reaction (:builder-faculties-workspace @db)))) + (reaction (:builder-workspace-faculties @db)))) ;; ---------------------------------------------------------------------------- ;; user data @@ -103,9 +113,21 @@ ;; ---------------------------------------------------------------------------- (register-sub - :builder-faculties-workspace-hover + :workspaces-current + (fn [db _] + (reaction + (let [{:keys [workspaces builder-grant-op]} @db] + (if builder-grant-op + (let [{:keys [id]} builder-grant-op] + (filter + (fn [w] (= id (:grant-op-id w))) + workspaces)) + (vector)))))) + +(register-sub + :builder-workspace-faculty-hover (fn [db _] (reaction - (let [{:keys [faculties builder-faculties-workspace-hover db]} @db] - (when builder-faculties-workspace-hover - (get faculties builder-faculties-workspace-hover)))))) + (let [{:keys [faculties builder-workspace-faculty-hover]} @db] + (when builder-workspace-faculty-hover + (get faculties builder-workspace-faculty-hover)))))) diff --git a/src/cljs/ulysses/utils.cljs b/src/cljs/ulysses/utils.cljs index 107bf7a..3355104 100644 --- a/src/cljs/ulysses/utils.cljs +++ b/src/cljs/ulysses/utils.cljs @@ -50,10 +50,11 @@ coll)) (defn str->int - "string to integer; nil if not possible" + "string or integer to integer; nil if not possible" [s] - (when (and (string? s) (re-find #"^\d+$" s)) - (js/parseInt s))) + (if (integer? s) s + (when (and (string? s) (re-find #"^\d+$" s)) + (js/parseInt s)))) (defn url-encode-component "urlencode" @@ -106,6 +107,12 @@ (hash-map) seqable)) +(defn new-id + "generate a new integer id, given a list of maps + containing existing integer ids" + [existing] + (->> existing (map :id) (apply max) (inc))) + (defn map-lookup "map ids to elements retrieved from map, removing if not found" diff --git a/src/sass/components/_builder-panel.scss b/src/sass/components/_builder-panel.scss index 0dd73d6..f1bedcc 100644 --- a/src/sass/components/_builder-panel.scss +++ b/src/sass/components/_builder-panel.scss @@ -30,3 +30,33 @@ padding: $box-padding-standard; } } + +.workspace-header-first { + @include clearfix; + + h3 { + display: block; + float: left; + } + + .workspace-actions { + float: right; + text-align: right; + + .btn { + margin-right: 3px; + + &:last-of-type { + margin-right: 0; + } + } + } +} + +.workspace-footer { + text-align: right; + + .btn { + margin-top: 15px; + } +} diff --git a/src/sass/components/_grant-op-pagination.scss b/src/sass/components/_grant-op-pagination.scss index 7a3a94f..b107428 100644 --- a/src/sass/components/_grant-op-pagination.scss +++ b/src/sass/components/_grant-op-pagination.scss @@ -7,9 +7,11 @@ } .pagination-top { + @include fluid-container-margin; + padding: 0 9px; position: absolute; - right: .5%; + right: 0; top: -27px; .arrow-link-wrapper {