From 49cad81c83c7236fcf14666eb102981877a1f7c2 Mon Sep 17 00:00:00 2001 From: Matthew Huebert Date: Tue, 9 Jan 2024 17:41:25 +0100 Subject: [PATCH] manage tags (board-admin) --- src/sb/app.cljc | 2 + src/sb/app/board/admin_ui.cljc | 4 ++ src/sb/app/board/data.cljc | 6 +- src/sb/app/entity/data.cljc | 23 +++--- src/sb/app/field/admin_ui.cljc | 127 ++++++++++++++++++++++++++++++--- src/sb/app/field/ui.cljc | 26 +++---- src/sb/app/member/ui.cljc | 10 +-- src/sb/app/views/radix.cljc | 2 +- src/sb/i18n.cljc | 6 ++ src/sb/migration/one_time.clj | 4 +- 10 files changed, 166 insertions(+), 44 deletions(-) diff --git a/src/sb/app.cljc b/src/sb/app.cljc index ff270f59..4dbcbb70 100644 --- a/src/sb/app.cljc +++ b/src/sb/app.cljc @@ -57,6 +57,8 @@ :entity/video {:view field.ui/video-field} :entity/fields {:view field.admin-ui/fields-editor :make-field field.admin-ui/make-field:fields} + :entity/member-tags {:view field.admin-ui/tags-editor + :make-field field.admin-ui/make-field:tags} :entity/field-entries {:view field.ui/entries-field :make-field field.ui/make-field:entries} :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 6dcf3137..6fb74d62 100644 --- a/src/sb/app/board/admin_ui.cljc +++ b/src/sb/app/board/admin_ui.cljc @@ -24,6 +24,9 @@ [:<> (header/entity board nil) + #_[:div {:class "max-w-[600px] mx-auto my-6 flex-v gap-6"} + (use-persisted :entity/member-tags)] + [radix/accordion {:class "max-w-[600px] mx-auto my-6 flex-v gap-6" :multiple true} @@ -38,6 +41,7 @@ [:div.field-label (t :tr/projects-and-members)] [:div.flex-v.gap-4 + (use-persisted :entity/member-tags) (use-persisted :entity/member-fields) (use-persisted :entity/project-fields)] diff --git a/src/sb/app/board/data.cljc b/src/sb/app/board/data.cljc index e7827fbb..0b9ebd5e 100644 --- a/src/sb/app/board/data.cljc +++ b/src/sb/app/board/data.cljc @@ -30,7 +30,6 @@ :board/max-projects-per-member {s- :int} :board/sticky-color {:doc "Deprecate - sticky notes can pick their own colors" s- :html/color} - :board/member-tags {s- [:sequential :tag/as-map]} :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" @@ -80,7 +79,7 @@ (? :board/max-members-per-project) (? :board/max-projects-per-member) (? :entity/member-fields) - (? :board/member-tags) + (? :entity/member-tags) (? :board/new-projects-require-approval?) (? :entity/project-fields) (? :board/project-sharing-buttons) @@ -127,7 +126,7 @@ :account/display-name]} {:member/tags [:entity/id :tag/label - :tag/background-color]} + :tag/color]} :entity/field-entries {:member/entity [:entity/id]} {:member/custom-tags [:tag/label]} @@ -187,6 +186,7 @@ [{:keys [board-id member/roles]}] (some-> (q/pull `[~@entity.data/entity-keys + :entity/member-tags {:entity/member-fields ~field.data/field-keys} {:entity/project-fields ~field.data/field-keys}] board-id) (merge {:member/roles roles}))) diff --git a/src/sb/app/entity/data.cljc b/src/sb/app/entity/data.cljc index fafd116f..d72f439a 100644 --- a/src/sb/app/entity/data.cljc +++ b/src/sb/app/entity/data.cljc @@ -9,17 +9,17 @@ (sch/register! (merge - {:tag/id unique-uuid - :tag/background-color {s- :html/color}, - :tag/label {s- :string},, - :tag/restricted? {:doc "Tag may only be modified by an admin of the owner of this tag" - s- :boolean} - :tag/as-map {:doc "Description of a tag which may be applied to an entity." - s- [:map {:closed true} - :tag/id - :tag/label - (? :tag/background-color) - (? :tag/restricted?)]}} + {:tag/id unique-uuid + :tag/color {s- :html/color}, + :tag/label {s- :string},, + :tag/restricted? {:doc "Tag may only be modified by an admin of the owner of this tag" + s- :boolean} + :tag/as-map {:doc "Description of a tag which may be applied to an entity." + s- [:map {:closed true} + :tag/id + :tag/label + (? :tag/color) + (? :tag/restricted?)]}} {:entity/id unique-uuid :entity/title {:doc "Title of entity, for card/header display." @@ -48,6 +48,7 @@ :chat.message]} :entity/project-fields {s- :entity/fields} :entity/member-fields {s- :entity/fields} + :entity/member-tags {s- [:sequential :tag/as-map]} :entity/draft? {:doc "Entity is not yet published - visible only to creator/team" s- :boolean} :entity/description {:doc "Description of an entity (for card/header display)" diff --git a/src/sb/app/field/admin_ui.cljc b/src/sb/app/field/admin_ui.cljc index 80871868..7d6cc853 100644 --- a/src/sb/app/field/admin_ui.cljc +++ b/src/sb/app/field/admin_ui.cljc @@ -1,13 +1,12 @@ (ns sb.app.field.admin-ui - (:require [applied-science.js-interop :as j] + (:require #?(:cljs ["@radix-ui/react-popover" :as Pop]) [clojure.string :as str] [inside-out.forms :as io] [promesa.core :as p] - [re-db.reactive :as r] - [sb.app.entity.ui :as entity.ui :refer [view-field]] [sb.app.entity.data :as entity.data] - [sb.app.field.ui :as field.ui] + [sb.app.entity.ui :refer [view-field]] [sb.app.field.data :as data] + [sb.app.field.ui :as field.ui] [sb.app.form.ui :as form.ui] [sb.app.views.radix :as radix] [sb.app.views.ui :as ui] @@ -68,7 +67,7 @@ (p/let [result (entity.data/maybe-save-field ?options)] (reset! ?new (:init ?new)) result)))} - [field.ui/text-field ?new {:placeholder "Option label" + [field.ui/text-field ?new {:placeholder "Option label" :field/classes {:wrapper "flex-auto"}}] [:div.btn.bg-white.px-3.py-1.shadow "Add Option"]]) #_[ui/pprinted @?options]])) @@ -200,13 +199,121 @@ (reset! !new-field nil) (entity.data/maybe-save-field ?fields)))} [:div.h-10.flex.items-center [(:icon (data/field-types @?type)) "icon-lg text-gray-700 mx-2"]] - [field.ui/text-field ?label {:field/label false - :ref !autofocus-ref - :placeholder (:field/label ?label) - :field/classes {:wrapper "flex-auto"}}] + [field.ui/text-field ?label {:field/label false + :ref !autofocus-ref + :placeholder (:field/label ?label) + :field/classes {:wrapper "flex-auto"}}] [:button.btn.btn-white.h-10 {:type "submit"} (t :tr/add)] [:div.flex.items-center.justify-center.icon-light-gray.h-10.w-7 {:on-mouse-down #(reset! !new-field nil)} [icons/close "w-4 h-4 "]]] - [:div.pl-12.py-2 (form.ui/show-field-messages ?new-field)]])])) \ No newline at end of file + [:div.pl-12.py-2 (form.ui/show-field-messages ?new-field)]])])) + +(defn make-field:tags [init props] + (io/field :many (u/prune {:tag/id ?id + :tag/label ?label + :tag/color ?color + :tag/restricted? ?restricted?}) + :init init)) + +(ui/defview tag-form [{:keys [?state + init + on-submit + close!]}] + (let [!ref (h/use-ref) + {:as ?tag :syms [?label ?color ?restricted?]} (h/use-memo #(io/form (u/prune {:tag/id ?id + :tag/label ?label + :tag/color (?color :init "#dddddd") + :tag/restricted? (?restricted? :field/label (t :tr/restricted))}) + :required [?label] + :init init) + (h/use-deps init)) + submit! #(on-submit ?tag close!)] + [:el.relative Pop/Root {:open true :on-open-change #(close!)} + [:el Pop/Anchor] + [:el Pop/Content {:as-child true} + [:div.p-2.z-10 {:class radix/float-small} + [:form.outline-none.flex-v.gap-2.items-stretch + {:ref !ref + :on-submit (fn [e] + (.preventDefault e) + (submit!))} + [:div.flex.gap-2 + [field.ui/text-field ?label {:placeholder (t :tr/label) + :field/keybindings {:Enter submit!} + :field/multi-line? false + :field/can-edit? true + :field/label false}] + + [:div.relative.w-10.h-10.overflow-hidden.rounded.outline.outline-black.outline-1 [field.ui/color-field ?color {:field/can-edit? true}]] + [:button.flex.items-center {:type "submit"} [icons/checkmark "w-5 h-5 icon-gray"]]] + [field.ui/checkbox-field ?restricted? {:field/can-edit? true + :field/classes {:wrapper "pl-3"}}]] + (form.ui/show-field-messages ?state)]]])) + +(ui/defview show-tag + {:key (fn [_ ?tag] #?(:cljs (goog/getUid ?tag)))} + [{:keys [?tags + !editing + use-order]} ?tag] + (let [{:syms [?label ?color]} ?tag + bg (or (u/some-str @?color) "#dddddd") + color (color/contrasting-text-color bg) + tag (v/x [:div.rounded.bg-badge.text-badge-txt.py-1.px-2.text-sm.inline-flex + {:key @?label + :style {:background-color bg :color color}} @?label]) + {:keys [drag-handle-props + drag-subject-props + dragging + dropping]} (use-order ?tag)] + [radix/context-menu {:trigger [:div.transition-all + (v/merge-props drag-handle-props + drag-subject-props + {:class (cond (= dropping :before) "pl-2" + dragging "opacity-20")}) + tag + (when (= ?tag @!editing) + [tag-form {:close! #(reset! !editing nil) + :?state ?tags + :init @?tag + :on-submit (fn [?new-tag close!] + (reset! ?tag @?new-tag) + (p/let [res (entity.data/maybe-save-field ?tag)] + (when-not (:error res) + (reset! ?tag @?new-tag) + (close!))))}])] + :items [[radix/context-menu-item + {:on-select (fn [] + (io/remove-many! ?tag) + (entity.data/maybe-save-field ?tags))} + (t :tr/remove)] + [radix/context-menu-item + {:on-select (fn [] (p/do (p/delay 0) (reset! !editing ?tag)))} + (t :tr/edit)]]}])) + +(ui/defview tags-editor [?tags _] + (let [!editing (h/use-state nil) + use-order (ui/use-orderable-parent ?tags {:axis :x})] + [:div.field-wrapper + (form.ui/show-label ?tags) + [:div.flex.gap-1 + (map (partial show-tag {:?tags ?tags + :!editing !editing + :use-order use-order}) ?tags) + (let [!creating-new (h/use-state false)] + [:div.inline-flex.text-sm.gap-1.items-center.rounded.hover:bg-gray-100.p-1 + {:on-click #(reset! !creating-new true)} + [:span.cursor-default (t :tr/add-tag)] + [icons/plus "w-4 h-4 icon-gray"] + (when @!creating-new + [tag-form {:?state ?tags + :on-submit (fn [?tag close!] + (io/add-many! ?tags (assoc @?tag :tag/id (random-uuid))) + (p/let [res (entity.data/maybe-save-field ?tags)] + (when-not (:error res) + (io/clear! ?tag) + (close!)) + res)) + :init {:badge/color "#dddddd"} + :close! #(reset! !creating-new false)}])])]])) \ No newline at end of file diff --git a/src/sb/app/field/ui.cljc b/src/sb/app/field/ui.cljc index 24dd6f4d..0a98bd7d 100644 --- a/src/sb/app/field/ui.cljc +++ b/src/sb/app/field/ui.cljc @@ -66,6 +66,7 @@ [?field props] (let [messages (forms/visible-messages ?field) loading? (:loading? ?field) + {:keys [field/classes]} props props (-> (v/merge-props (form.ui/?field-props ?field (merge {:field/event->value (comp boolean (j/get-in [:target :checked]))} props)) @@ -83,6 +84,7 @@ (update :checked boolean)) ] [:div.flex.flex-col.gap-1.relative + [:label.relative.flex.items-center (when loading? [:div.h-5.w-5.inline-flex.items-center.justify-center.absolute @@ -296,11 +298,10 @@ (update :value #(or % "#ffffff")) (form.ui/pass-props))]) -(ui/defview badge-form [{:keys [init +(ui/defview badge-form [{:keys [?state + init on-submit close!]}] - ;; TODO - ;; - color picker should show colors already used in the board? (let [!ref (h/use-ref) {:as ?badge :syms [?label ?color]} (h/use-memo #(io/form {:badge/label ?label :badge/color (?color :init "#dddddd")} @@ -308,7 +309,7 @@ :init init) (h/use-deps init)) submit! #(on-submit ?badge close!)] - [:el.relative Pop/Root {:open true :on-open-change #(do (prn :on-open-change %) (close!))} + [:el.relative Pop/Root {:open true :on-open-change #(close!)} [:el Pop/Anchor] [:el Pop/Content {:as-child true} [:div.p-2.z-10 {:class radix/float-small} @@ -324,7 +325,7 @@ :field/label false}] [:div.relative.w-10.h-10.overflow-hidden.rounded.outline.outline-black.outline-1 [color-field ?color {:field/can-edit? true}]] [:button.flex.items-center {:type "submit"} [icons/checkmark "w-5 h-5 icon-gray"]]] - (form.ui/show-field-messages (or (io/parent ?badge) ?badge))]]])) + (form.ui/show-field-messages ?state)]]])) (ui/defview badges-field* [?badges {:keys [member/roles]}] (let [board-admin? (:role/board-admin roles) @@ -341,8 +342,8 @@ [radix/context-menu {:trigger [:div badge (when (= ?badge @!editing) - [badge-form {:?badge ?badge - :close! #(reset! !editing nil) + [badge-form {:close! #(reset! !editing nil) + :?state ?badges :init @?badge :on-submit (fn [?new-badge close!] (reset! ?badge @?new-badge) @@ -367,10 +368,11 @@ (when @!creating-new [badge-form {:on-submit (fn [?badge close!] (io/add-many! ?badges @?badge) - (io/clear! ?badge) (p/let [res (entity.data/maybe-save-field ?badges)] (when-not (:error res) + (io/clear! ?badge) (close!)))) + :?state ?badges :init {:badge/color "#dddddd"} :close! #(reset! !creating-new false)}])]))])) @@ -556,17 +558,17 @@ (defn show-select:card [{:keys [field/options]} {:keys [select/value]}] (let [{:keys [field-option/label field-option/color] - :or {color "#dddddd"}} (u/find-first options #(= value (:field-option/value %)))] + :or {color "#dddddd"}} (u/find-first options #(= value (:field-option/value %)))] [:div {:class radix/select-trigger-classes :style {:background-color color - :color (color/contrasting-text-color color)}} + :color (color/contrasting-text-color color)}} label])) (defn show-image-list:card [{:keys [image-list/images]}] (when-let [{:keys [entity/id]} (first images)] [:img.max-h-80 {:src (asset.ui/asset-src id :card)}])) -(defn show-prose:card [field {:as m :prose/keys [format string]}] +(ui/defview show-prose:card [field {:as m :prose/keys [format string]}] (when-not (str/blank? string) (let [string (u/truncate-string string 140)] (case format @@ -591,7 +593,7 @@ :field.type/video [show-video-url (:video/url entry)] :field.type/select [show-select:card field entry] :field.type/link-list [show-link-list:card field entry] - :field.type/image-list [show-image-list:card field entry ] + :field.type/image-list [show-image-list:card field entry] :field.type/prose [show-prose:card field entry] (str "no match" field))) diff --git a/src/sb/app/member/ui.cljc b/src/sb/app/member/ui.cljc index 743a7f76..6354f634 100644 --- a/src/sb/app/member/ui.cljc +++ b/src/sb/app/member/ui.cljc @@ -25,16 +25,16 @@ (when-let [tags (seq (concat tags custom-tags))] [:section [:h3 (t :tr/tags)] (into [:ul] - (map (fn [{:tag/keys [label background-color]}] - [:li {:style (when background-color {:background-color background-color})} label])) + (map (fn [{:tag/keys [label color]}] + [:li {:style (when color {:background-color color})} label])) tags)]) (when avatar [:img {:src (asset.ui/asset-src avatar :card)}])])) -(defn show-tag [{:keys [tag/label tag/background-color] :or {background-color "#dddddd"}}] +(defn show-tag [{:keys [tag/label tag/color] :or {color "#dddddd"}}] [:div.tag-sm {:key label - :style {:background-color background-color - :color (color/contrasting-text-color background-color)}} + :style {:background-color color + :color (color/contrasting-text-color color)}} label]) (ui/defview card diff --git a/src/sb/app/views/radix.cljc b/src/sb/app/views/radix.cljc index b7e40807..e70d9a73 100644 --- a/src/sb/app/views/radix.cljc +++ b/src/sb/app/views/radix.cljc @@ -230,7 +230,7 @@ (v/defview context-menu [{:keys [trigger items]}] [:el ContextMenu/Root - [:el.cursor-context-menu.select-none ContextMenu/Trigger {:as-child true} (v/x trigger)] + [:el.cursor-context-menu ContextMenu/Trigger {:as-child true} (v/x trigger)] [:el ContextMenu/Content {:style {:z-index 20} :class [float-small "p-1 min-w-32"]} diff --git a/src/sb/i18n.cljc b/src/sb/i18n.cljc index 999d2525..d78992d3 100644 --- a/src/sb/i18n.cljc +++ b/src/sb/i18n.cljc @@ -48,6 +48,12 @@ See https://iso639-3.sil.org/code_tables/639/data/all for list of codes" :tr/drafts {:en "Drafts" :fr "Brouillons" :es "Borradores"} + :tr/add-tag {:en "Add tag" + :fr "Ajouter un mot-clé" + :es "Añadir etiqueta"} + :tr/restricted {:en "Restricted" + :fr "Restreint" + :es "Restringido"} :tr/further-instructions {:en "Further instructions" :fr "Instructions supplémentaires" :es "Instrucciones adicionales"} diff --git a/src/sb/migration/one_time.clj b/src/sb/migration/one_time.clj index 192e5f70..34d1e8b9 100644 --- a/src/sb/migration/one_time.clj +++ b/src/sb/migration/one_time.clj @@ -660,14 +660,14 @@ (to-uuid :tag %))) (map #(-> % (dissoc "order") - (rename-keys {"color" :tag/background-color + (rename-keys {"color" :tag/color "name" :tag/label "label" :tag/label "restrict" :tag/restricted?}) (u/update-some {:tag/restricted? (constantly true)}))) (filter :tag/label) vec)))) - (rename :board/member-tags)) + (rename :entity/member-tags)) "social" (& (xf (fn [m] (into {} (mapcat {"facebook" [[:social.sharing-button/facebook true]] "twitter" [[:social.sharing-button/twitter true]] "qrCode" [[:social.sharing-button/qr-code true]]