mirror of
https://github.com/logseq/logseq.git
synced 2026-05-28 06:34:34 +00:00
wip: property UX
This commit is contained in:
@@ -2269,7 +2269,8 @@
|
||||
|
||||
(rum/defc block-content < rum/reactive
|
||||
[config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide? selected?]
|
||||
(let [content (property/remove-built-in-properties format content)
|
||||
(let [repo (state/get-current-repo)
|
||||
content (property/remove-built-in-properties format content)
|
||||
{:block/keys [title body] :as block} (if (:block/title block) block
|
||||
(merge block (block/parse-title-and-body uuid format pre-block? content)))
|
||||
collapsed? (util/collapsed? block)
|
||||
@@ -2335,15 +2336,17 @@
|
||||
(when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
|
||||
(timestamp-cp block "SCHEDULED" scheduled-ast)))
|
||||
|
||||
(when-let [invalid-properties (:block/invalid-properties block)]
|
||||
(invalid-properties-cp invalid-properties))
|
||||
(when-not (config/db-based-graph? repo)
|
||||
(when-let [invalid-properties (:block/invalid-properties block)]
|
||||
(invalid-properties-cp invalid-properties)))
|
||||
|
||||
(when (and (seq properties)
|
||||
(let [hidden? (property-edit/properties-hidden? properties)]
|
||||
(not hidden?))
|
||||
(not (and block-ref? (or (seq title) (seq body))))
|
||||
(not (:slide? config))
|
||||
(not= block-type :whiteboard-shape))
|
||||
(not= block-type :whiteboard-shape)
|
||||
(not (config/db-based-graph? repo)))
|
||||
(properties-cp config block))
|
||||
|
||||
(block-content-inner config block body plugin-slotted? collapsed? block-ref-with-title?)
|
||||
@@ -2843,20 +2846,21 @@
|
||||
(if whiteboard-block?
|
||||
(block-reference {} (str uuid) nil)
|
||||
;; Not embed self
|
||||
(let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content))
|
||||
hide-block-refs-count? (and (:embed? config)
|
||||
(= (:block/uuid block) (:embed-id config)))]
|
||||
(block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count? selected?)))
|
||||
[:div.flex.flex-col.w-full
|
||||
(let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content))
|
||||
hide-block-refs-count? (and (:embed? config)
|
||||
(= (:block/uuid block) (:embed-id config)))]
|
||||
(block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count? selected?))
|
||||
(when (config/db-based-graph? repo)
|
||||
(property-component/properties-area block
|
||||
(:block/properties block)
|
||||
(:block/properties-text-values block)
|
||||
edit-input-id {:inline-text inline-text
|
||||
:editor-box (get config :editor-box)}))])
|
||||
|
||||
(when @*show-right-menu?
|
||||
(block-right-menu config block edit?))]
|
||||
|
||||
(when (config/db-based-graph? repo)
|
||||
(property-component/properties-area block
|
||||
(:block/properties block)
|
||||
(:block/properties-text-values block)
|
||||
edit-input-id))
|
||||
|
||||
(when-not (:hide-children? config)
|
||||
(let [children (db/sort-by-left (:block/_parent block) block)]
|
||||
(block-children config block children collapsed?)))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[frontend.components.block :as block]
|
||||
[frontend.components.datetime :as datetime-comp]
|
||||
[frontend.components.search :as search]
|
||||
[frontend.components.search.highlight :as highlight]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
@@ -157,7 +158,7 @@
|
||||
(when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
|
||||
[:div.flex.space-x-1
|
||||
[:div (when-not (db/page-exists? page-name) (t :new-page))]
|
||||
(search/highlight-exact-query page-name q)]]
|
||||
(highlight/highlight-exact-query page-name q)]]
|
||||
:open? chosen?
|
||||
:manual? true
|
||||
:fixed-position? true
|
||||
|
||||
@@ -5,23 +5,29 @@
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.db :as db]
|
||||
[frontend.config :as config]
|
||||
[rum.core :as rum]
|
||||
[frontend.state :as state]
|
||||
[frontend.mixins :as mixins]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.string :as string]))
|
||||
[clojure.string :as string]
|
||||
[goog.dom :as gdom]
|
||||
[frontend.search :as search]
|
||||
[frontend.components.search.highlight :as highlight]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.modules.shortcut.core :as shortcut]
|
||||
[medley.core :as medley]))
|
||||
|
||||
(rum/defcs property-config <
|
||||
rum/static
|
||||
(rum/local nil ::property-name)
|
||||
(rum/local nil ::property-schema)
|
||||
{:will-mount (fn [state]
|
||||
(let [[repo property-uuid] (:rum/args state)
|
||||
property (db/pull repo '[*] [:block/uuid property-uuid])]
|
||||
(let [[repo property] (:rum/args state)]
|
||||
(reset! (::property-name state) (:block/name property))
|
||||
(reset! (::property-schema state) (:block/schema property))
|
||||
state))}
|
||||
[state repo property-uuid]
|
||||
[state repo property]
|
||||
(let [*property-name (::property-name state)
|
||||
*property-schema (::property-schema state)]
|
||||
[:div.property-configure
|
||||
@@ -29,40 +35,117 @@
|
||||
|
||||
[:div.grid.gap-2.p-1
|
||||
[:div.grid.grid-cols-4.gap-1.items-center.leading-8
|
||||
[:label.cols-1 "Name:"]
|
||||
[:input.form-input
|
||||
{:on-change #(reset! *property-name (util/evalue %))
|
||||
:value @*property-name}]]
|
||||
[:label.cols-1 "Name:"]
|
||||
[:input.form-input
|
||||
{:on-change #(reset! *property-name (util/evalue %))
|
||||
:value @*property-name}]]
|
||||
|
||||
[:div.grid.grid-cols-4.gap-1.leading-8
|
||||
[:label.cols-1 "Schema type:"]
|
||||
(let [schema-types (->> (keys property-handler/builtin-schema-types)
|
||||
(map (comp string/capitalize name))
|
||||
(map (fn [type]
|
||||
{:label type
|
||||
:value type
|
||||
:selected (= (keyword (string/lower-case type))
|
||||
(:type @*property-schema))})))]
|
||||
(ui/select schema-types
|
||||
(fn [_e v]
|
||||
(let [type (keyword (string/lower-case v))]
|
||||
(swap! *property-schema assoc :type type)))))]
|
||||
[:div.grid.grid-cols-4.gap-1.leading-8
|
||||
[:label.cols-1 "Schema type:"]
|
||||
(let [schema-types (->> (keys property-handler/builtin-schema-types)
|
||||
(map (comp string/capitalize name))
|
||||
(map (fn [type]
|
||||
{:label type
|
||||
:value type
|
||||
:selected (= (keyword (string/lower-case type))
|
||||
(:type @*property-schema))})))]
|
||||
(ui/select schema-types
|
||||
(fn [_e v]
|
||||
(let [type (keyword (string/lower-case v))]
|
||||
(swap! *property-schema assoc :type type)))))]
|
||||
|
||||
[:div.grid.grid-cols-4.gap-1.items-center.leading-8
|
||||
[:label.cols-1 "Multiple values:"]
|
||||
(ui/checkbox {:checked (= :many (:cardinality @*property-schema))
|
||||
:on-change (fn [v]
|
||||
(swap! *property-schema assoc :cardinality (if (= "on" (util/evalue v)) :many :one)))})]
|
||||
[:div.grid.grid-cols-4.gap-1.items-center.leading-8
|
||||
[:label.cols-1 "Multiple values:"]
|
||||
(let [many? (boolean (= :many (:cardinality @*property-schema)))]
|
||||
(ui/checkbox {:checked many?
|
||||
:on-change (fn []
|
||||
(swap! *property-schema assoc :cardinality (if many? :one :many)))}))]
|
||||
|
||||
[:div
|
||||
(ui/button
|
||||
"Save"
|
||||
:on-click (fn []
|
||||
(property-handler/update-property!
|
||||
repo property-uuid
|
||||
{:property-name @*property-name
|
||||
:property-schema @*property-schema})
|
||||
(state/close-modal!)))]]]))
|
||||
[:div
|
||||
(ui/button
|
||||
"Save"
|
||||
:on-click (fn []
|
||||
(property-handler/update-property!
|
||||
repo (:block/uuid property)
|
||||
{:property-name @*property-name
|
||||
:property-schema @*property-schema})
|
||||
(state/close-modal!)))]
|
||||
|
||||
(when config/dev?
|
||||
[:div {:style {:max-width 900}}
|
||||
[:hr]
|
||||
[:p "Debug data:"]
|
||||
[:code
|
||||
(str property)]])]]))
|
||||
|
||||
(rum/defc search-item-render
|
||||
[search-q content]
|
||||
[:div.font-medium
|
||||
(highlight/highlight-exact-query content search-q)])
|
||||
|
||||
(defn- exit-edit-property
|
||||
[*property-key *property-value]
|
||||
(reset! *property-key nil)
|
||||
(reset! *property-value nil)
|
||||
(property-handler/set-editing-new-property! nil))
|
||||
|
||||
(defn- add-property!
|
||||
[block *property-key *property-value]
|
||||
(let [repo (state/get-current-repo)]
|
||||
(when (and @*property-key @*property-value)
|
||||
(property-handler/add-property! repo block @*property-key @*property-value))
|
||||
(exit-edit-property *property-key *property-value)))
|
||||
|
||||
(rum/defcs property-key-input < rum/reactive
|
||||
(rum/local true ::search?)
|
||||
shortcut/disable-all-shortcuts
|
||||
[state entity *property-key *property-value]
|
||||
(let [*search? (::search? state)
|
||||
result (when-not (string/blank? @*property-key)
|
||||
(search/property-search @*property-key))]
|
||||
[:div
|
||||
[:div.ls-property-add.grid.grid-cols-4.gap-1.flex.flex-row.items-center
|
||||
[:input#add-property.form-input.simple-input.block.col-span-1.focus:outline-none
|
||||
{:placeholder "Property key"
|
||||
:value (rum/react *property-key)
|
||||
:auto-focus true
|
||||
:on-change (fn [e]
|
||||
(reset! *property-key (util/evalue e))
|
||||
(reset! *search? true))
|
||||
:on-key-down (fn [e]
|
||||
(case (util/ekey e)
|
||||
"Escape"
|
||||
(exit-edit-property *property-key *property-value)
|
||||
|
||||
"Enter"
|
||||
(do
|
||||
(reset! *search? false)
|
||||
(.focus (js/document.getElementById "add-property-value")))
|
||||
|
||||
nil))}]
|
||||
|
||||
[:input#add-property-value.block-properties
|
||||
{:on-change #(reset! *property-value (util/evalue %))
|
||||
:on-key-down (fn [e]
|
||||
(case (util/ekey e)
|
||||
"Enter"
|
||||
(do
|
||||
(add-property! entity *property-key *property-value)
|
||||
(reset! *search? false))
|
||||
|
||||
nil))}]
|
||||
|
||||
[:a.close {:on-mouse-down #(exit-edit-property *property-key *property-value)}
|
||||
svg/close]]
|
||||
(when @*search?
|
||||
(ui/auto-complete
|
||||
result
|
||||
{:class "search-results"
|
||||
:on-chosen (fn [chosen]
|
||||
(reset! *property-key chosen)
|
||||
(reset! *search? false)
|
||||
(.focus (js/document.getElementById "add-property-value")))
|
||||
:item-render #(search-item-render @*property-key %)}))]))
|
||||
|
||||
(rum/defcs new-property < rum/reactive
|
||||
(rum/local nil ::property-key)
|
||||
@@ -74,22 +157,13 @@
|
||||
:on-hide (fn []
|
||||
(property-handler/set-editing-new-property! nil))
|
||||
:node (js/document.getElementById "edit-new-property"))))
|
||||
[state repo block edit-input-id properties]
|
||||
(let [new-property? (= edit-input-id (state/sub :ui/new-property-input-id))
|
||||
*property-key (::property-key state)
|
||||
[state repo block edit-input-id properties new-property?]
|
||||
(let [*property-key (::property-key state)
|
||||
*property-value (::property-value state)]
|
||||
(cond
|
||||
new-property?
|
||||
[:div#edit-new-property
|
||||
[:input.block-properties {:on-change #(reset! *property-key (util/evalue %))}]
|
||||
[:input.block-properties {:on-change #(reset! *property-value (util/evalue %))}]
|
||||
[:a {:on-click (fn []
|
||||
(when (and @*property-key @*property-value)
|
||||
(property-handler/add-property! repo block @*property-key @*property-value))
|
||||
(reset! *property-key nil)
|
||||
(reset! *property-value nil)
|
||||
(property-handler/set-editing-new-property! nil))}
|
||||
"Save"]]
|
||||
(property-key-input block *property-key *property-value)]
|
||||
|
||||
(seq properties)
|
||||
[:a {:title "Add another value"
|
||||
@@ -99,26 +173,150 @@
|
||||
(reset! *property-value nil))}
|
||||
(ui/icon "circle-plus")])))
|
||||
|
||||
(rum/defc properties-area < rum/static
|
||||
[block properties properties-text-values edit-input-id]
|
||||
(let [repo (state/get-current-repo)]
|
||||
[:div.ls-properties-area.pl-6
|
||||
(when (seq properties)
|
||||
[:div
|
||||
(for [[prop-uuid-or-built-in-prop v] properties]
|
||||
(if (uuid? prop-uuid-or-built-in-prop)
|
||||
(when-let [property (db/pull [:block/uuid prop-uuid-or-built-in-prop])]
|
||||
[:div
|
||||
[:a.mr-2
|
||||
{:on-click (fn [] (state/set-modal! #(property-config repo prop-uuid-or-built-in-prop)))}
|
||||
(:block/name property)]
|
||||
[:span (or (get properties-text-values prop-uuid-or-built-in-prop) (str v))]
|
||||
[:a.ml-8 {:on-click
|
||||
(fn []
|
||||
(property-handler/remove-property! repo block prop-uuid-or-built-in-prop))}
|
||||
"DEL"]])
|
||||
;; builtin
|
||||
[:div
|
||||
[:a.mr-2 (str prop-uuid-or-built-in-prop)]
|
||||
[:span v]]))])
|
||||
(new-property repo block edit-input-id properties)]))
|
||||
(rum/defcs property-key < (rum/local false ::show-close?)
|
||||
[state block property]
|
||||
(let [repo (state/get-current-repo)
|
||||
*show-close? (::show-close? state)]
|
||||
[:div.relative
|
||||
{:on-mouse-over (fn [_] (reset! *show-close? true))
|
||||
:on-mouse-out (fn [_] (reset! *show-close? false))}
|
||||
[:a.mr-2
|
||||
{:on-click (fn [] (state/set-modal! #(property-config repo property)))}
|
||||
(:block/name property)]
|
||||
(when @*show-close?
|
||||
[:div.absolute.top-0.right-0
|
||||
[:a.fade-link.fade-in.py-2.px-1
|
||||
{:title "Remove this property"
|
||||
:on-click (fn [_e]
|
||||
(property-handler/remove-property! repo block (:block/uuid property)))}
|
||||
(ui/icon "x")]])]))
|
||||
|
||||
(rum/defcs multiple-value-item < (rum/local false ::show-close?)
|
||||
[state entity property item dom-id' editor-id' {:keys [edit-fn page-cp inline-text]}]
|
||||
(let [*show-close? (::show-close? state)
|
||||
object? (= :object (:type (:block/schema property)))
|
||||
block (when object? (db/pull [:block/uuid item]))]
|
||||
[:div.flex.flex-1.flex-row {:on-mouse-over #(reset! *show-close? true)
|
||||
:on-mouse-out #(reset! *show-close? false)}
|
||||
[:div.flex.flex-1.property-value-content
|
||||
{:id dom-id'
|
||||
:on-click (fn []
|
||||
;; (edit-fn editor-id' dom-id' item)
|
||||
)}
|
||||
(if block
|
||||
;; TODO: page/block
|
||||
(str block)
|
||||
(inline-text {} :markdown (str item)))]
|
||||
(when @*show-close?
|
||||
[:a.close.fade-in
|
||||
{:title "Delete this value"
|
||||
:on-mouse-down
|
||||
(fn []
|
||||
(property-handler/delete-property-value! (state/get-current-repo)
|
||||
entity
|
||||
(:block/uuid property)
|
||||
item))}
|
||||
svg/close])]))
|
||||
|
||||
;; (add-property! block *property-key *property-value)
|
||||
(rum/defcs property-value < rum/reactive
|
||||
[state block property {:keys [inline-text editor-box page-cp]}]
|
||||
(let [k (:block/uuid property)
|
||||
v (get (:block/properties-text-values block)
|
||||
k
|
||||
(get (:block/properties block) k))
|
||||
dom-id (str "ls-property-" k)
|
||||
editor-id (str "property-" (:db/id block) "-" k)
|
||||
editing? (state/sub [:editor/editing? editor-id])
|
||||
schema (:block/schema property)
|
||||
edit-fn (fn [editor-id id v]
|
||||
(let [v (str v)
|
||||
cursor-range (util/caret-range (gdom/getElement (or id dom-id)))]
|
||||
(state/set-editing! editor-id v block cursor-range)
|
||||
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(state/set-editor-action-data! {:block block
|
||||
:property property
|
||||
:pos 0})
|
||||
(state/set-editor-action! :property-value-search)
|
||||
(state/set-state! :ui/editing-property property))
|
||||
50)))
|
||||
multiple-values? (= :many (:cardinality schema))
|
||||
type (:type schema)]
|
||||
(cond
|
||||
multiple-values?
|
||||
(let [v' (if (coll? v) v (when v [v]))
|
||||
v' (if (seq v') v' [""])
|
||||
editor-id' (str editor-id (count v'))
|
||||
new-editing? (state/sub [:editor/editing? editor-id'])]
|
||||
[:div.flex.flex-1.flex-col
|
||||
[:div.flex.flex-1.flex-col
|
||||
(for [[idx item] (medley/indexed v')]
|
||||
(let [dom-id' (str dom-id "-" idx)
|
||||
editor-id' (str editor-id idx)
|
||||
editing? (state/sub [:editor/editing? editor-id'])]
|
||||
(if editing?
|
||||
(editor-box {:format :markdown
|
||||
:block block} editor-id' {})
|
||||
(multiple-value-item block property item dom-id' editor-id' {:page-cp page-cp
|
||||
:edit-fn edit-fn
|
||||
:inline-text inline-text}))))
|
||||
|
||||
(let [fv (first v')]
|
||||
(when (and (not new-editing?)
|
||||
fv
|
||||
(or (and (string? fv) (not (string/blank? fv)))
|
||||
(and (not (string? fv)) (some? fv))))
|
||||
[:div.rounded-sm.ml-1
|
||||
{:on-click (fn []
|
||||
(edit-fn (str editor-id (count v')) nil ""))}
|
||||
[:div.flex.flex-row
|
||||
[:div.block {:style {:height 20
|
||||
:width 20}}
|
||||
[:a.add-button-link.block {:title "Add another value"
|
||||
:style {:margin-left -4}}
|
||||
(ui/icon "circle-plus")]]]]))]
|
||||
(when new-editing?
|
||||
(editor-box {:format :markdown
|
||||
:block block} editor-id' {}))])
|
||||
|
||||
editing?
|
||||
(editor-box {:format :markdown
|
||||
:block block} editor-id {})
|
||||
|
||||
:else
|
||||
[:div.flex.flex-1.property-value-content
|
||||
{:id dom-id
|
||||
:on-click (fn []
|
||||
(edit-fn editor-id nil v))}
|
||||
(cond
|
||||
(and (= type :date) (string/blank? v))
|
||||
[:div "TBD (date icon)"]
|
||||
|
||||
:else
|
||||
(when-not (string/blank? (str v))
|
||||
(inline-text {} :markdown (str v))))])))
|
||||
|
||||
(rum/defc properties-area < rum/reactive
|
||||
[block properties properties-text-values edit-input-id block-components-m]
|
||||
(let [repo (state/get-current-repo)
|
||||
new-property? (= edit-input-id (state/sub :ui/new-property-input-id))]
|
||||
(when (or (seq properties) new-property?)
|
||||
[:div.ls-properties-area
|
||||
(when (seq properties)
|
||||
[:div
|
||||
(for [[prop-uuid-or-built-in-prop v] properties]
|
||||
(if (uuid? prop-uuid-or-built-in-prop)
|
||||
(when-let [property (db/pull [:block/uuid prop-uuid-or-built-in-prop])]
|
||||
[:div.grid.grid-cols-4.gap-1
|
||||
[:div.property-key.col-span-1
|
||||
(property-key block property)]
|
||||
[:div.property-value.col-span-3
|
||||
(property-value block property block-components-m)]])
|
||||
;; TODO: built in properties should have UUID and corresponding schema
|
||||
;; builtin
|
||||
[:div
|
||||
[:a.mr-2 (str prop-uuid-or-built-in-prop)]
|
||||
[:span v]]))])
|
||||
(new-property repo block edit-input-id properties new-property?)])))
|
||||
|
||||
34
src/main/frontend/components/property.css
Normal file
34
src/main/frontend/components/property.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.property-value-content {
|
||||
@apply px-1 rounded-sm;
|
||||
cursor: text;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.property-value-content:hover {
|
||||
background: var(--ls-secondary-background-color);
|
||||
}
|
||||
|
||||
.ls-properties-area {
|
||||
.add-button-link {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr:nth-child(even), tr:nth-child(odd) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.editor-inner {
|
||||
@apply px-1;
|
||||
}
|
||||
}
|
||||
|
||||
input.simple-input {
|
||||
@apply px-1;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
input.simple-input:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
[frontend.util :as util]
|
||||
[frontend.components.block :as block]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.components.search.highlight :as highlight]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.page :as page-handler]
|
||||
@@ -25,44 +26,6 @@
|
||||
[frontend.modules.shortcut.core :as shortcut]
|
||||
[frontend.util.text :as text-util]))
|
||||
|
||||
(defn highlight-exact-query
|
||||
[content q]
|
||||
(if (or (string/blank? content) (string/blank? q))
|
||||
content
|
||||
(when (and content q)
|
||||
(let [q-words (string/split q #" ")
|
||||
lc-content (util/search-normalize content (state/enable-search-remove-accents?))
|
||||
lc-q (util/search-normalize q (state/enable-search-remove-accents?))]
|
||||
(if (and (string/includes? lc-content lc-q)
|
||||
(not (util/safe-re-find #" " q)))
|
||||
(let [i (string/index-of lc-content lc-q)
|
||||
[before after] [(subs content 0 i) (subs content (+ i (count q)))]]
|
||||
[:div
|
||||
(when-not (string/blank? before)
|
||||
[:span before])
|
||||
[:mark.p-0.rounded-none (subs content i (+ i (count q)))]
|
||||
(when-not (string/blank? after)
|
||||
[:span after])])
|
||||
(let [elements (loop [words q-words
|
||||
content content
|
||||
result []]
|
||||
(if (and (seq words) content)
|
||||
(let [word (first words)
|
||||
lc-word (util/search-normalize word (state/enable-search-remove-accents?))
|
||||
lc-content (util/search-normalize content (state/enable-search-remove-accents?))]
|
||||
(if-let [i (string/index-of lc-content lc-word)]
|
||||
(recur (rest words)
|
||||
(subs content (+ i (count word)))
|
||||
(vec
|
||||
(concat result
|
||||
[[:span (subs content 0 i)]
|
||||
[:mark.p-0.rounded-none (subs content i (+ i (count word)))]])))
|
||||
(recur nil
|
||||
content
|
||||
result)))
|
||||
(conj result [:span content])))]
|
||||
[:p {:class "m-0"} elements]))))))
|
||||
|
||||
(defn highlight-page-content-query
|
||||
"Return hiccup of highlighted page content FTS result"
|
||||
[content q]
|
||||
@@ -113,7 +76,7 @@
|
||||
(clojure.core/uuid uuid)
|
||||
{:indent? false})])
|
||||
[:div {:class "font-medium" :key "content"}
|
||||
(highlight-exact-query content q)]]))
|
||||
(highlight/highlight-exact-query content q)]]))
|
||||
|
||||
(defonce search-timeout (atom nil))
|
||||
|
||||
@@ -282,12 +245,12 @@
|
||||
(search-result-item {:name (if (model/whiteboard-page? data) "whiteboard" "page")
|
||||
:extension? true
|
||||
:title (t (if (model/whiteboard-page? data) :search-item/whiteboard :search-item/page))}
|
||||
(highlight-exact-query data search-q))]
|
||||
(highlight/highlight-exact-query data search-q))]
|
||||
|
||||
:file
|
||||
(search-result-item {:name "file"
|
||||
:title (t :search-item/file)}
|
||||
(highlight-exact-query data search-q))
|
||||
(highlight/highlight-exact-query data search-q))
|
||||
|
||||
:block
|
||||
(let [{:block/keys [page uuid content]} data ;; content here is normalized
|
||||
|
||||
43
src/main/frontend/components/search/highlight.cljs
Normal file
43
src/main/frontend/components/search/highlight.cljs
Normal file
@@ -0,0 +1,43 @@
|
||||
(ns frontend.components.search.highlight
|
||||
"Search highlight component"
|
||||
(:require [frontend.util :as util]
|
||||
[frontend.state :as state]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(defn highlight-exact-query
|
||||
[content q]
|
||||
(if (or (string/blank? content) (string/blank? q))
|
||||
content
|
||||
(when (and content q)
|
||||
(let [q-words (string/split q #" ")
|
||||
lc-content (util/search-normalize content (state/enable-search-remove-accents?))
|
||||
lc-q (util/search-normalize q (state/enable-search-remove-accents?))]
|
||||
(if (and (string/includes? lc-content lc-q)
|
||||
(not (util/safe-re-find #" " q)))
|
||||
(let [i (string/index-of lc-content lc-q)
|
||||
[before after] [(subs content 0 i) (subs content (+ i (count q)))]]
|
||||
[:div
|
||||
(when-not (string/blank? before)
|
||||
[:span before])
|
||||
[:mark.p-0.rounded-none (subs content i (+ i (count q)))]
|
||||
(when-not (string/blank? after)
|
||||
[:span after])])
|
||||
(let [elements (loop [words q-words
|
||||
content content
|
||||
result []]
|
||||
(if (and (seq words) content)
|
||||
(let [word (first words)
|
||||
lc-word (util/search-normalize word (state/enable-search-remove-accents?))
|
||||
lc-content (util/search-normalize content (state/enable-search-remove-accents?))]
|
||||
(if-let [i (string/index-of lc-content lc-word)]
|
||||
(recur (rest words)
|
||||
(subs content (+ i (count word)))
|
||||
(vec
|
||||
(concat result
|
||||
[[:span (subs content 0 i)]
|
||||
[:mark.p-0.rounded-none (subs content i (+ i (count word)))]])))
|
||||
(recur nil
|
||||
content
|
||||
result)))
|
||||
(conj result [:span content])))]
|
||||
[:p {:class "m-0"} elements]))))))
|
||||
@@ -1213,6 +1213,20 @@ independent of format as format specific heading characters are stripped"
|
||||
(distinct)
|
||||
(sort))))
|
||||
|
||||
(defn get-block-property-values
|
||||
"Get blocks which have this property."
|
||||
[property-uuid]
|
||||
(->>
|
||||
(d/q
|
||||
'[:find ?b ?v
|
||||
:in $ ?property-uuid
|
||||
:where
|
||||
[?b :block/properties ?p]
|
||||
[(get ?p ?property-uuid) ?v]
|
||||
[(some? ?v)]]
|
||||
(conn/get-db)
|
||||
property-uuid)))
|
||||
|
||||
(defn get-template-by-name
|
||||
[name]
|
||||
(when (string? name)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.db-based.editor :as db-editor-handler]
|
||||
[frontend.handler.file-based.editor :as file-editor-handler]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.modules.outliner.core :as outliner-core]
|
||||
[frontend.modules.outliner.transaction :as outliner-tx]
|
||||
@@ -1161,10 +1162,17 @@
|
||||
|
||||
(defn save-block-aux!
|
||||
[block value opts]
|
||||
(let [value (string/trim value)]
|
||||
;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever
|
||||
;; maybe we shouldn't save the block/file in "will-unmount" event?
|
||||
(save-block-if-changed! block value opts)))
|
||||
(let [entity (db/entity [:block/uuid (:block/uuid block)])
|
||||
editing-property (:ui/editing-property @state/state)]
|
||||
(when (:db/id entity)
|
||||
(let [value (string/trim value)]
|
||||
(if editing-property
|
||||
(property-handler/add-property! (state/get-current-repo) entity
|
||||
(:block/name editing-property)
|
||||
value)
|
||||
;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever
|
||||
;; maybe we shouldn't save the block/file in "will-unmount" event?
|
||||
(save-block-if-changed! block value opts))))))
|
||||
|
||||
(defn save-block!
|
||||
([repo block-or-uuid content]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[clojure.string :as string]
|
||||
[clojure.set :as set]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.format.block :as block]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.modules.outliner.core :as outliner-core]
|
||||
@@ -46,7 +47,10 @@
|
||||
refs' (->> refs
|
||||
(remove string/blank?)
|
||||
distinct)]
|
||||
refs'))
|
||||
(-> (map #(if (util/uuid-string? %)
|
||||
{:block/uuid (uuid %)}
|
||||
(block/page-name->map % true)) refs')
|
||||
set)))
|
||||
|
||||
(defn- infer-schema-from-input-string
|
||||
[v-str]
|
||||
@@ -84,41 +88,48 @@
|
||||
[repo block k-name v]
|
||||
(let [property (db/pull repo '[*] [:block/name k-name])
|
||||
property-uuid (or (:block/uuid property) (random-uuid))
|
||||
existing-schema (:block/schema property)
|
||||
property-type (:type existing-schema)
|
||||
{:keys [type cardinality]} (:block/schema property)
|
||||
multiple-values? (= cardinality :many)
|
||||
infer-schema (infer-schema-from-input-string v)
|
||||
property-type (or property-type infer-schema :default)
|
||||
schema (get builtin-schema-types property-type)]
|
||||
property-type (or type infer-schema :default)
|
||||
schema (get builtin-schema-types property-type)
|
||||
properties (:block/properties block)
|
||||
value (get properties property-uuid)]
|
||||
(when-let [v* (try
|
||||
(convert-property-input-string property-type v)
|
||||
(catch :default e
|
||||
(notification/show! (str e) :error false)
|
||||
nil))]
|
||||
(if-let [msg (me/humanize (mu/explain-data schema v*))]
|
||||
(notification/show! msg :error false)
|
||||
(do (when (nil? property) ;if property not exists yet
|
||||
(when-not (contains? (if (set? value) value #{value}) v*)
|
||||
(if-let [msg (me/humanize (mu/explain-data schema v*))]
|
||||
(notification/show! msg :error false)
|
||||
(do
|
||||
(when (nil? property) ;if property not exists yet
|
||||
(db/transact! repo [(outliner-core/block-with-timestamps
|
||||
{:block/schema {:type property-type}
|
||||
:block/original-name k-name
|
||||
:block/name (util/page-name-sanity-lc k-name)
|
||||
:block/uuid property-uuid
|
||||
:block/type "property"})]))
|
||||
(let [block-properties (assoc (:block/properties block)
|
||||
property-uuid
|
||||
(if (= property-type :default)
|
||||
(let [refs (extract-page-refs-from-prop-str-value v*)]
|
||||
(if (seq refs) (set refs) v*))
|
||||
v*))
|
||||
(let [refs (when (= property-type :default) (extract-page-refs-from-prop-str-value v*))
|
||||
refs' (when (seq refs)
|
||||
(concat (:block/refs (db/pull [:block/uuid (:block/uuid block)]))
|
||||
refs))
|
||||
v' (if (= property-type :default)
|
||||
(if (seq refs) refs v*)
|
||||
v*)
|
||||
new-value (if multiple-values? (vec (distinct (conj value v'))) v')
|
||||
block-properties (assoc properties property-uuid new-value)
|
||||
block-properties-text-values
|
||||
(if (= property-type :default)
|
||||
(if (and (not multiple-values?) (= property-type :default))
|
||||
(assoc (:block/properties-text-values block) property-uuid v*)
|
||||
(dissoc (:block/properties-text-values block) property-uuid))]
|
||||
(outliner-tx/transact!
|
||||
{:outliner-op :save-block}
|
||||
(outliner-core/save-block!
|
||||
{:block/uuid (:block/uuid block)
|
||||
;; TODO: fix block/properties-order
|
||||
(db/transact! repo
|
||||
[{:block/uuid (:block/uuid block)
|
||||
:block/properties block-properties
|
||||
:block/properties-text-values block-properties-text-values}))))))))
|
||||
:block/properties-text-values block-properties-text-values
|
||||
:block/refs refs'}]))))))))
|
||||
|
||||
(defn remove-property!
|
||||
[repo block k-uuid-or-builtin-k-name]
|
||||
@@ -131,65 +142,50 @@
|
||||
:block/properties (dissoc origin-properties k-uuid-or-builtin-k-name)
|
||||
:block/properties-text-values (dissoc (:block/properties-text-values block) k-uuid-or-builtin-k-name)}])))
|
||||
|
||||
(defn- fix-cardinality-many-values!
|
||||
[property-uuid]
|
||||
(let [ev (->> (model/get-block-property-values property-uuid)
|
||||
(remove (fn [[_ v]] (coll? v))))
|
||||
tx-data (map (fn [[e v]]
|
||||
(let [entity (db/entity e)
|
||||
properties (:block/properties entity)]
|
||||
{:db/id e
|
||||
:block/properties (assoc properties property-uuid [v])})) ev)]
|
||||
(when (seq tx-data)
|
||||
(db/transact! tx-data))))
|
||||
|
||||
(defn update-property!
|
||||
[repo property-uuid {:keys [property-name property-schema]}]
|
||||
{:pre [(uuid? property-uuid)]}
|
||||
(let [tx-data (cond-> {:block/uuid property-uuid}
|
||||
property-name (assoc :block/name property-name)
|
||||
property-schema (assoc :block/schema property-schema)
|
||||
true outliner-core/block-with-updated-at)]
|
||||
(db/transact! repo [tx-data])))
|
||||
(when-let [property (db/entity [:block/uuid property-uuid])]
|
||||
(when (and (= :many (:cardinality property-schema))
|
||||
(not= :many (:cardinality (:block/schema property))))
|
||||
;; cardinality changed from :one to :many
|
||||
(fix-cardinality-many-values! property-uuid))
|
||||
(let [tx-data (cond-> {:block/uuid property-uuid}
|
||||
property-name (assoc :block/name property-name)
|
||||
property-schema (assoc :block/schema property-schema)
|
||||
true outliner-core/block-with-updated-at)]
|
||||
(db/transact! repo [tx-data]))))
|
||||
|
||||
(defn- extract-refs
|
||||
[entity properties]
|
||||
(let [property-values (->>
|
||||
properties
|
||||
(map (fn [[k v]]
|
||||
(let [schema (:block/schema (db/pull [:block/uuid k]))
|
||||
object? (= (:type schema) :object)
|
||||
f (if object? page-ref/->page-ref identity)]
|
||||
(->> (if (coll? v)
|
||||
v
|
||||
[v])
|
||||
(map f)))))
|
||||
(apply concat)
|
||||
(filter string?))
|
||||
block-text (string/join " "
|
||||
(cons
|
||||
(:block/content entity)
|
||||
property-values))
|
||||
ast-refs (gp-mldoc/get-references block-text (gp-mldoc/default-config :markdown))
|
||||
refs (map #(or (gp-block/get-page-reference % #{})
|
||||
(gp-block/get-block-reference %)) ast-refs)
|
||||
refs' (->> refs
|
||||
(remove string/blank?)
|
||||
distinct)]
|
||||
(map #(if (util/uuid-string? %)
|
||||
[:block/uuid (uuid %)]
|
||||
(block/page-name->map % true)) refs')))
|
||||
|
||||
(comment
|
||||
(defn delete-property-value!
|
||||
"Delete value if a property has multiple values"
|
||||
[entity property-id property-value]
|
||||
(when (and entity (uuid? property-id))
|
||||
(when (not= property-id (:block/uuid entity))
|
||||
(when-let [property (db/pull [:block/uuid property-id])]
|
||||
(let [schema (:block/schema property)
|
||||
[success? property-value-or-error] (validate schema property-value)
|
||||
multiple-values? (:multiple-values? schema)]
|
||||
(when (and multiple-values? success?)
|
||||
(let [properties (:block/properties entity)
|
||||
properties' (update properties property-id disj property-value-or-error)
|
||||
refs (extract-refs entity properties')]
|
||||
(outliner-tx/transact!
|
||||
{:outliner-op :save-block}
|
||||
(outliner-core/save-block!
|
||||
{:block/uuid (:block/uuid entity)
|
||||
:block/properties properties'
|
||||
:block/refs refs}))))
|
||||
(state/clear-editor-action!)
|
||||
(state/clear-edit!)))))))
|
||||
(defn delete-property-value!
|
||||
"Delete value if a property has multiple values"
|
||||
[repo block property-id property-value]
|
||||
(when (and block (uuid? property-id))
|
||||
(when (not= property-id (:block/uuid block))
|
||||
(when-let [property (db/pull [:block/uuid property-id])]
|
||||
(let [schema (:block/schema property)]
|
||||
(when (= :many (:cardinality schema))
|
||||
(let [properties (:block/properties block)
|
||||
properties' (update properties property-id
|
||||
(fn [col]
|
||||
(vec (remove #{property-value} col))))]
|
||||
(outliner-tx/transact!
|
||||
{:outliner-op :save-block}
|
||||
(outliner-core/save-block!
|
||||
{:block/uuid (:block/uuid block)
|
||||
:block/properties properties'}))))
|
||||
(state/clear-editor-action!))))))
|
||||
|
||||
(defn set-editing-new-property!
|
||||
[value]
|
||||
|
||||
@@ -1872,7 +1872,8 @@ Similar to re-frame subscriptions"
|
||||
(assoc
|
||||
:editor/editing? {edit-input-id true}
|
||||
:editor/set-timestamp-block nil
|
||||
:cursor-range cursor-range))))
|
||||
:cursor-range cursor-range
|
||||
:ui/editing-property nil))))
|
||||
(set-state! :editor/block block)
|
||||
(set-state! :editor/content content :path-in-sub-atom edit-input-id)
|
||||
(set-state! :editor/last-key-code nil)
|
||||
|
||||
Reference in New Issue
Block a user