diff --git a/src/sb/app.cljc b/src/sb/app.cljc index bbfd810d..00dbf70e 100644 --- a/src/sb/app.cljc +++ b/src/sb/app.cljc @@ -29,24 +29,31 @@ (def client-endpoints (t/read (shadow.resource/inline "public/js/sparkboard.views.transit.json")))) (def global-field-meta - {:string {:view field.ui/text-field} - :http/url {:view field.ui/text-field} - :boolean {:view field.ui/checkbox-field} - :project/badges {:view field.ui/badges-field} - :prose/as-map {:view field.ui/prose-field} - :entity/tags {:view field.ui/tags-field} - :account/email {:props {:type "email" - :placeholder (t :tr/email)} - :validators [form.ui/email-validator]} - :account/password {:view field.ui/text-field - :props {:type "password" - :placeholder (t :tr/password)} - :validators [(io/min-length 8)]} - :entity/title {:validators [(io/min-length 3)]} - :field/options {:view field.admin-ui/options-editor} - :entity/domain-name {:view domain.ui/domain-field} - :entity/video {:view field.ui/video-field} - :entity/fields {:view field.admin-ui/fields-editor} - :entity/member-tags {:view field.admin-ui/tags-editor} - :entity/field-entries {:view field.ui/entries-field} - :asset/as-map {:view field.ui/image-field}}) \ No newline at end of file + {:string {:view field.ui/text-field} + :http/url {:view field.ui/text-field} + :boolean {:view field.ui/checkbox-field} + :project/badges {:view field.ui/badges-field} + :prose/as-map {:view field.ui/prose-field} + :entity/tags {:view field.ui/tags-field} + :account/email {:props {:type "email" + :placeholder (t :tr/email)} + :validators [form.ui/email-validator]} + :account/password {:view field.ui/text-field + :props {:type "password" + :placeholder (t :tr/password)} + :validators [(io/min-length 8)]} + :entity/title {:validators [(io/min-length 3)]} + :field/options {:view field.admin-ui/options-editor} + :entity/domain-name {:view domain.ui/domain-field} + :entity/video {:view field.ui/video-field} + :entity/fields {:view field.admin-ui/fields-editor} + :entity/member-tags {:view field.admin-ui/tags-editor} + :entity/field-entries {:view field.ui/entries-field} + :entity/admission-policy {:view field.ui/select-field + :props {:field/wrap keyword + :field/unwrap #(subs (str %) 1) + :field/options [{:field-option/value :admission-policy/open + :field-option/label (t :tr/anyone-may-join)} + {:field-option/value :admission-policy/invite-only + :field-option/label (t :tr/invite-only)}]}} + :asset/as-map {:view field.ui/image-field}}) \ No newline at end of file diff --git a/src/sb/app/board/admin_ui.cljc b/src/sb/app/board/admin_ui.cljc index fadd0743..0199b125 100644 --- a/src/sb/app/board/admin_ui.cljc +++ b/src/sb/app/board/admin_ui.cljc @@ -1,7 +1,7 @@ (ns sb.app.board.admin-ui (:require [inside-out.forms :as io] [sb.app.board.data :as data] - [sb.app.entity.ui :as entity.ui :refer [use-persisted-attr]] + [sb.app.entity.ui :as entity.ui :refer [persisted-attr]] [sb.app.views.header :as header] [sb.app.views.radix :as radix] [sb.app.views.ui :as ui] @@ -24,8 +24,8 @@ (apply concat) (into #{})) use-persisted (fn [attr & [props]] - (use-persisted-attr board attr (merge {:field/can-edit? true - :field/color-list colors} props)))] + (persisted-attr board attr (merge {:field/can-edit? true + :field/color-list colors} props)))] [:<> (header/entity board nil) @@ -54,7 +54,7 @@ [:div.field-label (t :tr/registration)] [:div.flex-v.gap-4 - (use-persisted :board/registration-open?) + (use-persisted :entity/admission-policy) (use-persisted :board/registration-url-override) (use-persisted :board/registration-page-message) (use-persisted :board/invite-email-text)] @@ -69,7 +69,6 @@ ;; Registration ;; - :board/registration-invitation-email-text ;; - :board/registration-newsletter-field? - ;; - :board/registration-open? ;; - :board/registration-message ;; - :board/registration-url-override ;; - :board/registration-codes diff --git a/src/sb/app/board/data.cljc b/src/sb/app/board/data.cljc index f7e73225..d9097484 100644 --- a/src/sb/app/board/data.cljc +++ b/src/sb/app/board/data.cljc @@ -33,9 +33,7 @@ :board/invite-email-text {:hint "Text of email sent when inviting a user to a board." s- :string}, :board/registration-newsletter-field? {:hint "During registration, request permission to send the user an email newsletter" - s- :boolean}, - :board/registration-open? {:hint "Allows new registrations via the registration page. Does not affect invitations." - s- :boolean}, + s- :boolean},, :board/registration-page-message {:hint "Content displayed on registration screen (before user chooses provider / enters email)" s- :prose/as-map}, :board/registration-url-override {:hint "URL to redirect user for registration (replaces the Sparkboard registration page, admins are expected to invite users)", @@ -52,7 +50,7 @@ :entity/kind :entity/parent - :board/registration-open? + :entity/admission-policy (? :image/avatar) (? :image/logo-large) @@ -111,7 +109,7 @@ :entity/member-tags :entity/member-fields :entity/project-fields - :board/registration-open? + :entity/admission-policy {:entity/parent [~@entity.data/listing-fields :org/show-org-tab?]}] board-id)] (merge board {:membership/roles roles}) @@ -195,6 +193,7 @@ (u/timed `settings (some-> (q/pull `[~@entity.data/listing-fields + :entity/admission-policy {:image/background [:entity/id]} {:entity/domain-name [:domain-name/name]} :entity/member-tags diff --git a/src/sb/app/entity/ui.cljc b/src/sb/app/entity/ui.cljc index 82bdc126..3a00e6c4 100644 --- a/src/sb/app/entity/ui.cljc +++ b/src/sb/app/entity/ui.cljc @@ -42,20 +42,22 @@ (when-some [init (:init m)] (reset! ?field init)) ?field) -(defn use-persisted-attr [e a & {:as props}] - #?(:cljs - (let [persisted-value (get e a) - view (or (:view props) (-> a io/global-meta :view)) - make-field (or (:make-?field (meta view)) - (fn [init _props] (io/field :init init))) - ?field (h/use-memo #(doto (make-field persisted-value props) - (add-meta! {:attribute a - :db/id (sch/wrap-id e) - :field/label (:field/label props) - :field/persisted? true})) - ;; create a new field when the persisted value changes - (h/use-deps persisted-value))] - (view-field ?field (assoc props :view view))))) +(ui/defview persisted-attr [e a props] + (let [persisted-value (get e a) + view (or (:view props) (-> a io/global-meta :view)) + make-field (or (:make-?field (meta view)) + (fn [init _props] (io/field :init init))) + ?field (h/use-memo #(doto (make-field persisted-value props) + (add-meta! {:attribute a + :db/id (sch/wrap-id e) + :field/label (:field/label props) + :field/persisted? true})) + ;; create a new field when the persisted value changes + ;; TODO - instead of creating a new field, reset :init if possible. + ;; this will break one place where we currently rely on recognizing a new field + ;; (image-field) + (h/use-deps persisted-value))] + (view-field ?field (assoc props :view view)))) #?(:cljs (defn href [{:as e :entity/keys [kind id]} key] diff --git a/src/sb/app/field/ui.cljc b/src/sb/app/field/ui.cljc index 8689ef94..e450c221 100644 --- a/src/sb/app/field/ui.cljc +++ b/src/sb/app/field/ui.cljc @@ -289,25 +289,31 @@ {:style (color/color-pair color)} label])) -(ui/defview select-field [?field {:as props :field/keys [label +(ui/defview select-field [?field {:as props :field/keys [wrap + unwrap + label options - can-edit?]}] + can-edit?] + :or {unwrap identity + wrap identity}}] [:div.field-wrapper (form.ui/show-label ?field label) (if can-edit? - [radix/select-menu (-> (form.ui/?field-props ?field (merge {:field/event->value identity} - props)) - (set/rename-keys {:on-change :on-value-change}) - (assoc :on-value-change (fn [v] - (reset! ?field v) - (entity.data/maybe-save-field ?field)) - :field/can-edit? (:field/can-edit? props) - :field/options (->> options - (map (fn [{:field-option/keys [label value color]}] - {:text label - :value value})) - doall)))] + (with-messages-popover ?field + [radix/select-menu (-> (form.ui/?field-props ?field (merge {:field/event->value identity} + props)) + (set/rename-keys {:on-change :on-value-change}) + (assoc :on-value-change (fn [v] + (reset! ?field (wrap v)) + (entity.data/maybe-save-field ?field)) + :field/can-edit? (:field/can-edit? props) + :field/options (->> options + (map (fn [{:as opt :field-option/keys [label value color]}] + {:text label + :value (unwrap value)})) + doall)))]) [show-select-value props @?field]) + (when (:loading? ?field) [:div.loading-bar.absolute.bottom-0.left-0.right-0 {:class "h-[3px]"}])]) @@ -586,9 +592,10 @@ img))) (ui/defview images-field [?images {:as props :field/keys [label can-edit?]}] - (let [!?current (h/use-state-with-deps (first ?images) [?images]) + (let [hook-deps (h/use-deps (:init ?images)) + !?current (h/use-state-with-deps (first ?images) hook-deps) use-order (ui/use-orderable-parent ?images {:axis :x}) - [selected-url loading?] (ui/use-last-loaded (some-> @!?current ('?id) deref (asset.ui/asset-src :card)) [?images])] + [selected-url loading?] (ui/use-last-loaded (some-> @!?current ('?id) deref (asset.ui/asset-src :card)) hook-deps)] [:div.field-wrapper (form.ui/show-label ?images label) (when selected-url diff --git a/src/sb/app/membership/ui.cljc b/src/sb/app/membership/ui.cljc index 31e3c5f4..ddae6588 100644 --- a/src/sb/app/membership/ui.cljc +++ b/src/sb/app/membership/ui.cljc @@ -32,7 +32,7 @@ (when (:image/avatar account) [ui/avatar {:size 20} account]) [:div.flex-v.gap-2 [:h1.font-medium.text-2xl.flex-auto.flex.items-center.mt-2 (-> membership :membership/member :account/display-name)] - (entity.ui/use-persisted-attr membership :entity/tags field-params)] + [entity.ui/persisted-attr membership :entity/tags field-params]] [:div.flex.px-1.rounded-bl-lg.border-b.border-l.absolute.top-0.right-0 dev-panel @@ -52,11 +52,11 @@ [radix/dialog-close [:div.modal-title-icon [icons/close]]]]]] [:div.px-body.flex-v.gap-6 - (entity.ui/use-persisted-attr membership - :entity/field-entries - {:entity/fields (->> membership :membership/entity :entity/member-fields) - :membership/roles roles - :field/can-edit? can-edit?})]])) + [entity.ui/persisted-attr membership + :entity/field-entries + {:entity/fields (->> membership :membership/entity :entity/member-fields) + :membership/roles roles + :field/can-edit? can-edit?}]]])) (defn show-tag [{:keys [tag/label tag/color] :or {color "#dddddd"}}] [:div.tag-md diff --git a/src/sb/app/org/admin_ui.cljc b/src/sb/app/org/admin_ui.cljc index 4b363b56..e4761490 100644 --- a/src/sb/app/org/admin_ui.cljc +++ b/src/sb/app/org/admin_ui.cljc @@ -11,7 +11,7 @@ [{:as params :keys [org-id]}] (let [org (data/settings params) use-attr (fn [attr & [props]] - (entity.ui/use-persisted-attr org attr (merge {:field/can-edit? true} props)))] + (entity.ui/persisted-attr org attr (merge {:field/can-edit? true} props)))] [:<> (header/entity org nil) [:div {:class form.ui/form-classes} diff --git a/src/sb/app/project/data.cljc b/src/sb/app/project/data.cljc index 106c0c77..4da7a532 100644 --- a/src/sb/app/project/data.cljc +++ b/src/sb/app/project/data.cljc @@ -24,8 +24,6 @@ {:project/open-requests {:doc "Currently active requests for help" s- [:sequential :request/map]}, - :project/team-complete? {:doc "Project team marked sufficient" - s- :boolean} :project/community-actions {s- [:sequential :community-action/as-map]} :project/approved? {:doc "Set by an admin when :board/new-projects-require-approval? is enabled. Unapproved projects are hidden." s- :boolean} @@ -34,6 +32,10 @@ :project/number {:doc "Number assigned to a project by its board (stored as text because may contain annotations)", :todo "This could be stored in the board entity, a map of {project, number}, so that projects may participate in multiple boards" s- :string} + :entity/admission-policy {:doc "A policy for who may join a team." + s- [:enum + :admission-policy/open + :admission-policy/invite-only]} :project/admin-description {:doc "A description field only writable by an admin" s- :prose/as-map} :entity/archived? {:doc "Marks a project inactive, hidden." @@ -46,6 +48,7 @@ :entity/parent :entity/title :entity/created-at + :entity/admission-policy (? :entity/uploads) (? :entity/updated-at) (? :entity/draft?) @@ -65,8 +68,7 @@ (? :project/admin-description) (? :project/sticky?) (? :project/open-requests) - (? :entity/description) - (? :project/team-complete?)] + (? :entity/description)] } :community-action/as-map {s- [:map-of :community-action/label @@ -88,6 +90,7 @@ :entity/kind :entity/title :entity/description + :entity/admission-policy {:entity/video [:video/url]} {:project/badges [:badge/label :badge/color]} diff --git a/src/sb/app/project/ui.cljc b/src/sb/app/project/ui.cljc index 04a14414..d7df6e51 100644 --- a/src/sb/app/project/ui.cljc +++ b/src/sb/app/project/ui.cljc @@ -121,7 +121,9 @@ [:div.flex.flex-wrap.gap-2 (for [{:as tag :tag/keys [id label color]} (resolved-tags board-membership)] [:div.tag-sm {:style (color/color-pair color)} - label])]]])]] + label])]]])] + (when ((some-fn :role/admin :role/board-admin) (:membership/roles props)) + [entity.ui/persisted-attr project :entity/admission-policy props])] ) (ui/defview show @@ -147,11 +149,11 @@ (t :tr/publish)]]) [:div.flex [:h1.font-medium.text-2xl.flex-auto.px-body.flex.items-center.pt-6 - (entity.ui/use-persisted-attr project :entity/title (merge field-params - {:field/label false - :field/multi-line? false - :field/unstyled? (some-> (:entity/title project) - (not= "Untitled"))}))] + [entity.ui/persisted-attr project :entity/title (merge field-params + {:field/label false + :field/multi-line? false + :field/unstyled? (some-> (:entity/title project) + (not= "Untitled"))})]] [:div.flex.px-1.rounded-bl-lg.border-b.border-l.absolute.top-0.right-0 @@ -174,21 +176,20 @@ [:div.modal-title-icon [icons/close]]]]]] [:div.px-body.flex-v.gap-6 - (entity.ui/use-persisted-attr project :project/badges field-params) - (entity.ui/use-persisted-attr project :entity/description (merge field-params - {:field/label false - :placeholder "Description"})) - (entity.ui/use-persisted-attr project :entity/video field-params) - - (entity.ui/use-persisted-attr project - :entity/field-entries - {:entity/fields (->> project :entity/parent :entity/project-fields) - :membership/roles roles - :field/can-edit? can-edit?}) + [entity.ui/persisted-attr project :project/badges field-params] + [entity.ui/persisted-attr project :entity/description (merge field-params + {:field/label false + :placeholder "Description"})] + [entity.ui/persisted-attr project :entity/video field-params] + + [entity.ui/persisted-attr project + :entity/field-entries + {:entity/fields (->> project :entity/parent :entity/project-fields) + :membership/roles roles + :field/can-edit? can-edit?}] [project-members project field-params] - [:section.flex-v.gap-2.items-start [manage-community-actions project (:project/community-actions project)]]]])) diff --git a/src/sb/app/views/radix.cljc b/src/sb/app/views/radix.cljc index bf8f9ae2..6c22fb39 100644 --- a/src/sb/app/views/radix.cljc +++ b/src/sb/app/views/radix.cljc @@ -23,7 +23,8 @@ (def menu-content-classes (v/classes ["rounded-sm bg-popover text-popover-txt " "shadow-md ring-1 ring-txt/10" "focus:outline-none z-50" - "gap-1 py-1 px-0"])) + "gap-1 py-1 px-0" + "overflow-hidden"])) (def menu-content (v/from-element :el dm/Content {:sideOffset 4 :collision-padding 16 :align "start" @@ -138,12 +139,11 @@ [:div.flex-grow] (when can-edit? [:el.group-disabled:text-gray-400 sel/Icon (icons/chevron-down)])] - [:el sel/Portal {:container (yawn.util/find-or-create-element id)} - [:el sel/Content {:class (:content classes)} - [:el.p-1 sel/ScrollUpButton (icons/chevron-up "mx-auto")] - (into [:el sel/Viewport {}] (map select-item) options) - [:el.p-1 sel/ScrollDownButton (icons/chevron-down "mx-auto")] - [:el sel/Arrow]]]]))) + [:el sel/Content {:class (:content classes)} + [:el.p-1 sel/ScrollUpButton (icons/chevron-up "mx-auto")] + (into [:el.p-1 sel/Viewport {}] (map select-item) options) + [:el.p-1 sel/ScrollDownButton (icons/chevron-down "mx-auto")] + [:el sel/Arrow]]]))) (def select-separator (v/from-element :el sel/Separator)) (def select-label (v/from-element :el sel/Label {:class "text-txt/70"})) diff --git a/src/sb/i18n.cljc b/src/sb/i18n.cljc index 37622f84..b7aef0bf 100644 --- a/src/sb/i18n.cljc +++ b/src/sb/i18n.cljc @@ -417,9 +417,12 @@ See https://iso639-3.sil.org/code_tables/639/data/all for list of codes" :tr/registration-newsletter-field? {:en "Registration newsletter field" :fr "Champ d'inscription à la newsletter" :es "Campo de registro de boletín"} - :tr/registration-open? {:en "Registration is open" - :fr "L'inscription est ouverte" - :es "El registro está abierto"} + :tr/anyone-may-join {:en "Anyone may join" + :fr "Tout le monde peut rejoindre" + :es "Cualquiera puede unirse"} + :tr/invite-only {:en "Invite only" + :fr "Invitation seulement" + :es "Solo por invitación"} :tr/registration-page-message {:en "Registration page message" :fr "Message de la page d'inscription" :es "Mensaje de la página de registro"} diff --git a/src/sb/migrate/one_time.clj b/src/sb/migrate/one_time.clj index 442d3313..36d8c142 100644 --- a/src/sb/migrate/one_time.clj +++ b/src/sb/migrate/one_time.clj @@ -483,7 +483,7 @@ })))))] (-> (dissoc m k) (cond-> (seq image-list-assets) - (update :entity/uploads (fnil into []) image-list-assets)) + (update :entity/uploads (fnil into []) image-list-assets)) (cond-> entry-value (assoc-in [to-k field-id] entry-value))))) m @@ -547,7 +547,7 @@ (def changes {:board/as-map [::prepare (partial flat-map :entity/id (partial to-uuid :board)) ::always lookup-domain - ::defaults {:board/registration-open? true + ::defaults {:entity/admission-policy :open :entity/public? true :entity/parent (uuid-ref :org "base")} "createdAt" (& (xf #(Date. %)) (rename :entity/created-at)) @@ -698,7 +698,11 @@ "slack" (& (xf (fn [{:strs [team-id]}] [:slack.team/id team-id])) (rename :board/slack.team)) - "registrationOpen" (rename :board/registration-open?) + "registrationOpen" (& (xf (fn [open?] + (if open? + :admission-policy/open + :admission-policy/invite-only))) + (rename :entity/admission-policy)) "registrationCode" (& (xf (fn [code] (when-not (str/blank? code) {code {:registration-code/active? true}}))) @@ -973,7 +977,8 @@ (rename :discussion/posts)) :boardId rm ::always (remove-when (comp empty? :discussion/posts))] - :project/as-map [::defaults {:entity/archived? false} + :project/as-map [::defaults {:entity/archived? false + :entity/admission-policy :open} ::always (remove-when :entity/deleted-at) ::always (remove-when #(contains? #{"example" nil} (:boardId %))) @@ -996,7 +1001,10 @@ (rename :project/badges)) ;; should be ref :active (& (xf not) (rename :entity/archived?)) :approved (rename :project/approved?) - :ready (rename :project/team-complete?) + :ready (& (xf (fn [ready?] + (if ready? :admission-policy/invite-only + :admission-policy/open))) + (rename :entity/admission-policy)) :members (& (fn [project a v] (let [project-id (:entity/id project)]