Skip to content

Commit

Permalink
restructure memberships to mediate all of a user's interactions on a …
Browse files Browse the repository at this point in the history
…board

- simplifies how we can traverse to a member's profile within a board
- reflects the way users should be able to customize their own identity per-board
  • Loading branch information
mhuebert committed Jan 13, 2024
1 parent 3e9a001 commit 6e67e59
Show file tree
Hide file tree
Showing 25 changed files with 392 additions and 301 deletions.
2 changes: 1 addition & 1 deletion src/sb/app.cljc
Expand Up @@ -14,7 +14,7 @@
[sb.app.field.admin-ui :as field.admin-ui]
[sb.app.field.ui :as field.ui]
[sb.app.form.ui :as form.ui]
[sb.app.member.ui]
[sb.app.membership.ui]
[sb.app.notification.ui]
[sb.app.org.admin-ui]
[sb.app.org.ui]
Expand Down
40 changes: 34 additions & 6 deletions src/sb/app/account/data.cljc
Expand Up @@ -6,6 +6,7 @@
[sb.authorize :as az]
[sb.query :as q]
[sb.schema :as sch :refer [?]]
[sb.server.datalevin :as dl]
[sb.util :as u]))

(sch/register!
Expand Down Expand Up @@ -39,24 +40,51 @@
(comp (map :membership/entity)
(filter (comp #{:org} :entity/kind))
(map (q/pull entity.data/entity-keys)))
(db/where [[:membership/account account-id]])))
(db/where [[:membership/member account-id]])))

(comment
(let [account-id [:entity/id #uuid "b03a4669-a7ef-3e8e-bddc-8413e004c338"]]
(u/timed `all
(->> (q/pull `[{:membership/_member
[:membership/roles
:entity/id
:entity/kind
{:membership/entity [:entity/id
:entity/kind
:entity/title
:entity/created-at
{:image/avatar [:entity/id]}
{:image/background [:entity/id]}]}
{:membership/_member :...}]}] account-id)
:membership/_member
(mapcat #(cons % (:membership/_member %)))
(map #(assoc (:membership/entity %) :membership/roles (:membership/roles %)))))))
(q/defquery all
{:endpoint {:query true}
:prepare az/with-account-id!}
[{:keys [account-id]}]
(prn account-id)
(u/timed `all
(->> (q/pull '[{:membership/_account
(->> (q/pull `[{:membership/_member
[:membership/roles
:entity/id
:entity/kind
{:membership/member [:entity/id
:entity/kind
:account/display-name
{:image/avatar [:entity/id]}]}
{:membership/entity [:entity/id
:entity/kind
:entity/title
:entity/created-at
{:entity/parent [:entity/id]}
{:image/avatar [:entity/id]}
{:image/background [:entity/id]}]}]}]
account-id)
:membership/_account
(map #(u/lift-key % :membership/entity)))))
{:image/background [:entity/id]}]}
{:membership/_member :...}]}] account-id)
:membership/_member
#_(mapcat #(cons % (:membership/_member %)))
#_(map #(assoc-in % [:membership/entity :membership/roles] (:membership/roles %)))
#_clojure.pprint/pprint)))

#?(:clj
(defn login!
Expand Down
125 changes: 71 additions & 54 deletions src/sb/app/account/ui.cljc
Expand Up @@ -6,18 +6,16 @@
[sb.app.account.data :as data]
[sb.app.entity.ui :as entity.ui]
[sb.app.field.ui :as field.ui]
[sb.app.form.ui :as form.ui]
[sb.app.membership.data :as member.data]
[sb.app.views.header :as header]
[sb.app.views.radix :as radix]
[sb.app.views.ui :as ui]
[sb.i18n :refer [t]]
[sb.icons :as icons]
[sb.routing :as routes]
[sb.routing :as routing]
[sb.util :as u]
[yawn.hooks :as h]
[yawn.view :as v]))


(defn account:sign-in-with-google []
(v/x
[:a.btn.btn-white
Expand All @@ -39,32 +37,32 @@
(ui/with-form [!account {:account/email (?email :init "")
:account/password (?password :init "")}
:required [?email ?password]]
(let [!step (h/use-state :email)]
[:form.flex-grow.m-auto.gap-6.flex-v.max-w-sm.px-4
{:on-submit (fn [^js e]
(.preventDefault e)
(case @!step
:email (do (reset! !step :password)
(js/setTimeout #(.focus (js/document.getElementById "account-password")) 100))
:password (p/let [res (routes/POST 'sb.server.account/login! @!account)]
(js/console.log "res" res)
(prn :res res))))}
(let [!step (h/use-state :email)]
[:form.flex-grow.m-auto.gap-6.flex-v.max-w-sm.px-4
{:on-submit (fn [^js e]
(.preventDefault e)
(case @!step
:email (do (reset! !step :password)
(js/setTimeout #(.focus (js/document.getElementById "account-password")) 100))
:password (p/let [res (routing/POST 'sb.server.account/login! @!account)]
(js/console.log "res" res)
(prn :res res))))}


[:div.flex-v.gap-2
[field.ui/text-field ?email nil]
(when (= :password @!step)
[field.ui/text-field ?password {:id "account-password"}])
(str (forms/visible-messages !account))
[:button.btn.btn-primary.w-full.h-10.text-sm.p-3
(t :tr/continue-with-email)]]
[:div.flex-v.gap-2
[field.ui/text-field ?email nil]
(when (= :password @!step)
[field.ui/text-field ?password {:id "account-password"}])
(str (forms/visible-messages !account))
[:button.btn.btn-primary.w-full.h-10.text-sm.p-3
(t :tr/continue-with-email)]]

[:div.relative
[:div.absolute.inset-0.flex.items-center [:span.w-full.border-t]]
[:div.relative.flex.justify-center.text-xs.uppercase
[:span.bg-secondary.px-2.text-muted-txt (t :tr/or)]]]
[account:sign-in-with-google]
[account:sign-in-terms]])))
[:div.relative
[:div.absolute.inset-0.flex.items-center [:span.w-full.border-t]]
[:div.relative.flex.justify-center.text-xs.uppercase
[:span.bg-secondary.px-2.text-muted-txt (t :tr/or)]]]
[account:sign-in-with-google]
[account:sign-in-terms]])))

(ui/defview sign-in
{:route "/login"}
Expand All @@ -82,44 +80,63 @@
(ui/defview show
{:route "/"
:endpoint/public? true}
[params]
(if (db/get :env/config :account-id)
(let [?filter (h/use-callback (forms/field))
all (data/all {})
account (db/get :env/config :account)
title (v/from-element :div.font-medium.text-xl.px-2)
section (v/from-element :div.flex-v.gap-2)
entities (-> (group-by :entity/kind all)
(update-vals #(->> (sequence (ui/filtered @?filter) %)
(sort-by :entity/created-at u/compare:desc)))
(u/guard seq))]
[{:keys [account-id]} params]
(if account-id
(let [?filter (h/use-callback (forms/field))
all (data/all {})
account (db/get :env/config :account)
{:as entities :keys [board org]} (-> (->> all (map :membership/entity) (group-by :entity/kind))
(update-vals #(sort-by :entity/created-at u/compare:desc %))
(u/guard seq))
boards-by-org (group-by :entity/parent board)
match-text @?filter]
[:div.divide-y
[header/entity (data/account-as-entity account) nil]

(when-let [{:keys [org board project]} entities]
(when (seq entities)
[:div.p-body.flex-v.gap-8
(when (> (count all) 6)
[field.ui/filter-field ?filter nil])
(let [limit (partial ui/truncate-items {:limit 10})]
[:div.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-2.md:gap-8.-mx-2
(when (seq project)
[section
[title (t :tr/projects)]
(limit (map entity.ui/row project))])
(when (seq board)
[section
[title (t :tr/boards)]
(limit (map entity.ui/row board))])
(when (seq org)
[section
[title (t :tr/orgs)]
(limit (map entity.ui/row org))])])])
(when (seq org)
[:div.flex-v.gap-4
(u/for! [org org
:let [projects-by-board (into {}
(keep (fn [board]
(when-let [projects (->> (member.data/membership account-id board)
:membership/_member
(into []
(comp (map :membership/entity)
(ui/filtered @?filter)))
seq)]
[board projects])))
(get boards-by-org org))
boards (into []
(filter (fn [board]
(or (contains? projects-by-board board)
(ui/match-entity match-text board))))
(get boards-by-org org))]
:when (or (seq boards)
(seq projects-by-board)
(ui/match-entity match-text org))]
[:div.contents {:key (:entity/id org)}
[:div.text-lg.font-semibold (:entity/title org)]
(limit
(u/for! [board boards
:let [projects (get projects-by-board board)]]
[:div.flex-v
[entity.ui/row board]
[:div.pl-14.ml-1.flex.flex-wrap.gap-2.mt-2
(u/for! [project projects]
[:a.bg-gray-100.hover:bg-gray-200.rounded.h-12.inline-flex.items-center.px-3.cursor-default
{:href (routing/entity-path project 'ui/show)}
(:entity/title project)])]]))])]))])
[:div.p-body
(when (and (empty? @?filter) (empty? (:board entities)))
(when (and (empty? match-text) (empty? (:board entities)))
[ui/hero
(ui/show-markdown
(t :tr/start-board-new))
[:a.btn.btn-primary.btn-base {:class "mt-6"
:href (routes/path-for ['sb.app.board-data/new])}
:href (routing/path-for ['sb.app.board-data/new])}
(t :tr/create-first-board)]])]])
(ui/redirect `sign-in)))
45 changes: 24 additions & 21 deletions src/sb/app/board/data.cljc
Expand Up @@ -2,7 +2,7 @@
(:require [re-db.api :as db]
[sb.app.field.data :as field.data]
[sb.app.entity.data :as entity.data]
[sb.app.member.data :as member.data]
[sb.app.membership.data :as member.data]
[sb.authorize :as az]
[sb.query :as q]
[sb.schema :as sch :refer [? s-]]
Expand Down Expand Up @@ -102,7 +102,8 @@
)

(q/defquery show
{:prepare [(az/with-roles :board-id)]}
{:prepare [(az/with-roles :board-id)
(member.data/assert-can-view :board-id)]}
[{:keys [board-id membership/roles]}]
(u/timed `show
(if-let [board (db/pull `[~@entity.data/entity-keys
Expand All @@ -116,10 +117,10 @@
(throw (ex-info "Board not found!" {:status 400})))))

(def project-fields `[~@entity.data/entity-keys])
(def member-fields [{:membership/account [:entity/id
:entity/kind
{:image/avatar [:entity/id]}
:account/display-name]}
(def member-fields [{:membership/member [:entity/id
:entity/kind
{:image/avatar [:entity/id]}
:account/display-name]}
{:entity/tags [:entity/id
:tag/label
:tag/color]}
Expand All @@ -129,16 +130,18 @@
:membership/roles])

(q/defquery members
{:prepare [(az/with-roles :board-id)]}
{:prepare [(az/with-roles :board-id)
(member.data/assert-can-view :board-id)]}
[{:keys [board-id membership/roles]}]
(u/timed `members
(->> (db/where [[:membership/entity board-id]])
(remove (some-fn :entity/deleted-at :entity/archived?))
(mapv (db/pull `[~@entity.data/entity-keys
~@member-fields])))))
(u/timed `members (->> (db/entity board-id)
:membership/_entity
(remove (some-fn :entity/deleted-at :entity/archived?))
(mapv (db/pull `[~@entity.data/entity-keys
~@member-fields])))))

(q/defquery projects
{:prepare [(az/with-roles :board-id)]}
{:prepare [(az/with-roles :board-id)
(member.data/assert-can-view :board-id)]}
[{:keys [board-id membership/roles]}]
(u/timed `projects
(->> (db/where [[:entity/parent board-id]])
Expand All @@ -147,11 +150,11 @@

(q/defquery drafts
{:prepare az/with-account-id}
[{:keys [account-id]}]
(into []
(comp (filter (comp (every-pred :entity/draft? #(= :project (:entity/kind %))) :membership/entity))
(map #(db/pull project-fields (:membership/entity %))))
(db/where [[:membership/account (dl/resolve-id account-id)]])))
[{:keys [account-id board-id]}]
(->> (member.data/membership account-id board-id)
:membership/_member
(filter :entity/draft?)
(map #(db/pull project-fields (:membership/entity %)))))

(defn authorize-edit! [board account-id]
(when-not (or (validate/can-edit? account-id board)
Expand All @@ -168,9 +171,9 @@
(let [board (-> (dl/new-entity board :board :by account-id)
(validate/conform :board/as-map))
_ (authorize-create! board account-id)
member (-> {:membership/entity board
:membership/account account-id
:membership/roles #{:role/admin}}
member (-> {:membership/entity board
:membership/member account-id
:membership/roles #{:role/admin}}
(dl/new-entity :membership))]
(db/transact! [member])
board))
Expand Down
4 changes: 2 additions & 2 deletions src/sb/app/board/ui.cljc
Expand Up @@ -8,7 +8,7 @@
[sb.app.board.data :as data]
[sb.app.domain-name.ui :as domain.ui]
[sb.app.entity.ui :as entity.ui]
[sb.app.member.ui :as member.ui]
[sb.app.membership.ui :as member.ui]
[sb.app.field.ui :as field.ui]
[sb.app.form.ui :as form.ui]
[sb.app.project.data :as project.data]
Expand Down Expand Up @@ -134,7 +134,7 @@


[radix/tab-content {:value (t :tr/projects)}
(some->> (seq (data/drafts nil))
(some->> (seq (data/drafts {:board-id board-id}))
(into [:div.grid.border-b-2.border-gray-300.border-dashed.py-3.mb-3]
(map entity.ui/row)))
(into [:div.grid]
Expand Down
4 changes: 4 additions & 0 deletions src/sb/app/chat/data.cljc
Expand Up @@ -7,6 +7,10 @@
[sb.server.datalevin :as dl]
[sb.util :as u]))

;; TODO
;; fix these queries to reflect the change in schema,
;; chat participants are "board memberships" now rather than accounts

(def entity-fields [:entity/id :entity/created-at :entity/updated-at])

(sch/register!
Expand Down

0 comments on commit 6e67e59

Please sign in to comment.