diff --git a/resources/css/common.css b/resources/css/common.css index a71b4f7294..7633fe7d33 100644 --- a/resources/css/common.css +++ b/resources/css/common.css @@ -142,6 +142,7 @@ body { line-height: 1.5; background-color: transparent; min-height: 100%; + word-break: break-word; /* compatible for overflow-wrap: anywhere */ } a svg { @@ -716,6 +717,6 @@ svg.tip { hr { - margin: 2rem 0; - border-color: var(--ls-border-color, #ccc); + margin: 2rem 0; + border-color: var(--ls-border-color, #ccc); } diff --git a/src/main/api.cljs b/src/main/api.cljs index 88b18711fe..9159b52cb6 100644 --- a/src/main/api.cljs +++ b/src/main/api.cljs @@ -2,9 +2,17 @@ (:require [frontend.db :as db] [frontend.state :as state] [datascript.core :as d] - [cljs.reader])) + [cljs.reader] + [frontend.db.query-dsl :as query-dsl])) -(defn ^:export query +(defn ^:export q + [query-string] + (when-let [repo (state/get-current-repo)] + (when-let [conn (db/get-conn repo)] + (when-let [result (query-dsl/query repo query-string)] + @result)))) + +(defn ^:export datascript_query [query & inputs] (when-let [repo (state/get-current-repo)] (when-let [conn (db/get-conn repo)] diff --git a/src/main/frontend/commands.cljs b/src/main/frontend/commands.cljs index 524a4e2a90..f62246cad0 100644 --- a/src/main/frontend/commands.cljs +++ b/src/main/frontend/commands.cljs @@ -49,14 +49,14 @@ (defn embed-page [] (conj - [[:editor/input "{{{embed [[]]}}}" {:last-pattern slash - :backward-pos 5}]] + [[:editor/input "{{embed [[]]}}" {:last-pattern slash + :backward-pos 4}]] [:editor/search-page :embed])) (defn embed-block [] - [[:editor/input "{{{embed (())}}}" {:last-pattern slash - :backward-pos 5}] + [[:editor/input "{{embed (())}}" {:last-pattern slash + :backward-pos 4}] [:editor/search-block :embed]]) ;; Stop now!! @@ -100,6 +100,7 @@ [:editor/show-date-picker]]] ["Scheduled" [[:editor/clear-current-slash] [:editor/show-date-picker]]] + ["Query" [[:editor/input "{{query }}" {:backward-pos 2}]]] ["Draw" [[:editor/input "/draw "] [:editor/show-input [{:command :draw :id :title @@ -124,8 +125,8 @@ ["Image Link" link-steps] (when (state/logged?) ["Upload an image" [[:editor/click-hidden-file-input :id]]]) - ["Embed Youtube Video" [[:editor/input "{{{youtube }}}" {:last-pattern slash - :backward-pos 3}]]] + ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash + :backward-pos 2}]]] ["Html Inline " (->inline "html")] ;; TODO: diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 3aeadf0b79..7ea015a0b4 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -8,6 +8,7 @@ [frontend.state :as state] [frontend.db :as db] [frontend.db.model :as model] + [frontend.db.query-dsl :as query-dsl] [dommy.core :as d] [datascript.core :as dc] [goog.dom :as gdom] @@ -402,6 +403,13 @@ (let [inline-list (mldoc/inline->edn v (mldoc/default-config format))] [:div.inline.mr-1 (map-inline {} inline-list)]))) +(defn selection-range-in-block? [] + (and (= "Range" (. (js/window.getSelection) -type)) + (-> (js/window.getSelection) + (.-anchorNode) + (.-parentNode) + (.closest ".block-content")))) + (defn- render-macro [config name arguments macro-content format] (if macro-content @@ -436,6 +444,8 @@ (not (= (:id config) "contents"))) [:span.text-gray-500 "]]"])])) +(declare custom-query) + (defn inline [{:keys [html-export?] :as config} item] (match item @@ -647,6 +657,13 @@ [title]) arguments)] (cond + (= name "query") + [:div.dsl-query + (let [query (string/join "," arguments)] + (custom-query (assoc config :dsl-query? true) + {:title [:code.p-1 (str "Query: " query)] + :query query}))] + (= name "youtube") (let [url (first arguments)] (when-let [youtube-id (cond @@ -659,14 +676,17 @@ :else nil)] (when-not (string/blank? youtube-id) - [:iframe - {:allow-full-screen "allowfullscreen" - :allow - "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - :frame-border "0" - :src (str "https://www.youtube.com/embed/" youtube-id) - :height "315" - :width "560"}]))) + (let [width (min (- (util/get-width) 96) + 560) + height (int (* width (/ 315 560)))] + [:iframe + {:allow-full-screen "allowfullscreen" + :allow + "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" + :frame-border "0" + :src (str "https://www.youtube.com/embed/" youtube-id) + :height height + :width width}])))) (= name "embed") (let [a (first arguments)] @@ -1040,10 +1060,11 @@ (when (seq properties) [:div.blocks-properties.text-sm.opacity-80.my-1.p-2 (for [[k v] properties] + ^{:key (str (:block/uuid block) "-" k)} [:div.my-1 [:b k] [:span.mr-1 ":"] - (inline-text (:block/format block) v)])]))) + (inline-text (:block/format block) (str v))])]))) (rum/defcs timestamp-cp < rum/reactive (rum/local false ::show?) @@ -1082,48 +1103,49 @@ (rum/defc block-content < rum/reactive [config {:block/keys [uuid title level body meta content marker dummy? page format repo children pre-block? properties collapsed? idx block-refs-count scheduled scheduled-ast deadline deadline-ast repeated?] :as block} edit-input-id block-id slide?] (let [dragging? (rum/react *dragging?) - attrs {:blockid (str uuid) + attrs {:blockid (str uuid) ;; FIXME: Click to copy a selection instead of click first and then copy ;; It seems that `util/caret-range` can't get the correct range - :on-click (fn [e] - (let [target (gobj/get e "target")] - (when-not (or (util/link? target) - (util/input? target) - (util/details-or-summary? target) - (and (util/sup? target) - (d/has-class? target "fn"))) - (editor-handler/clear-selection! nil) - (editor-handler/unhighlight-block!) - (let [cursor-range (util/caret-range (gdom/getElement block-id)) - properties-hidden? (text/properties-hidden? properties) - content (text/remove-level-spaces content format) - content (if properties-hidden? (text/remove-properties! content) content)] - (state/set-editing! - edit-input-id - content - block - cursor-range)) - (util/stop e)))) - :on-drag-over (fn [event] - (util/stop event) - (when-not (dnd-same-block? uuid) - (show-dnd-separator (str uuid "-nested")))) + :on-click (fn [e] + (when-not (selection-range-in-block?) + (let [target (gobj/get e "target")] + (when-not (or (util/link? target) + (util/input? target) + (util/details-or-summary? target) + (and (util/sup? target) + (d/has-class? target "fn"))) + (editor-handler/clear-selection! nil) + (editor-handler/unhighlight-block!) + (let [cursor-range (util/caret-range (gdom/getElement block-id)) + properties-hidden? (text/properties-hidden? properties) + content (text/remove-level-spaces content format) + content (if properties-hidden? (text/remove-properties! content) content)] + (state/set-editing! + edit-input-id + content + block + cursor-range)) + (util/stop e))))) + :on-drag-over (fn [event] + (util/stop event) + (when-not (dnd-same-block? uuid) + (show-dnd-separator (str uuid "-nested")))) :on-drag-leave (fn [event] (hide-dnd-separator (str uuid)) (hide-dnd-separator (str uuid "-nested")) (hide-dnd-separator (str uuid "-top"))) - :on-drop (fn [event] - (util/stop event) - (when-not (dnd-same-block? uuid) - (let [from-dom-id (get-data-transfer-attr event "block-dom-id")] - (dnd/move-block @*dragging-block - block - from-dom-id - false - true))) - (reset! *dragging? false) - (reset! *dragging-block nil) - (editor-handler/unhighlight-block!))}] + :on-drop (fn [event] + (util/stop event) + (when-not (dnd-same-block? uuid) + (let [from-dom-id (get-data-transfer-attr event "block-dom-id")] + (dnd/move-block @*dragging-block + block + from-dom-id + false + true))) + (reset! *dragging? false) + (reset! *dragging-block nil) + (editor-handler/unhighlight-block!))}] [:div.flex.relative [:div.flex-1.flex-col.relative.block-content (cond-> {:id (str "block-content-" uuid)} @@ -1529,33 +1551,47 @@ (defn built-in-custom-query? [title] - (contains? #{"🔨 NOW" "📅 NEXT"} - title)) + (let [repo (state/get-current-repo)] + (let [queries (state/sub [:config repo :default-queries :journals])] + (when (seq queries) + (boolean (some #(= % title) (map :title queries))))))) + +(defn- trigger-custom-query! + [state] + (let [[config query] (:rum/args state) + query-atom (if (:dsl-query? config) + (query-dsl/query (state/get-current-repo) (:query query)) + (db/custom-query query))] + (assoc state :query-atom query-atom))) (rum/defcs custom-query < rum/reactive - {:will-mount (fn [state] - (let [[config query] (:rum/args state) - query-atom (db/custom-query query)] - (assoc state :query-atom query-atom))) + {:will-mount trigger-custom-query! :did-mount (fn [state] (when-let [query (last (:rum/args state))] (state/add-custom-query-component! query (:rum/react-component state))) state) + :did-remount (fn [_old_state state] + (trigger-custom-query! state)) :will-unmount (fn [state] (when-let [query (last (:rum/args state))] (state/remove-custom-query-component! query) (db/remove-custom-query! (state/get-current-repo) query)) state)} [state config {:keys [title query inputs view collapsed? children?] :as q}] - (let [query-atom (:query-atom state)] + (let [dsl-query? (:dsl-query? config) + query-atom (:query-atom state)] (let [current-block-uuid (or (:block/uuid (:block config)) (:block/uuid config)) ;; exclude the current one, otherwise it'll loop forever remove-blocks (if current-block-uuid [current-block-uuid] nil) query-result (and query-atom (rum/react query-atom)) + + result (if (and query-result dsl-query?) + query-result + (db/custom-query-result-transform query-result remove-blocks q)) result (if query-result (db/custom-query-result-transform query-result remove-blocks q)) - view-f (sci/eval-string (pr-str view)) + view-f (and view (sci/eval-string (pr-str view))) only-blocks? (:block/uuid (first result)) blocks-grouped-by-page? (and (seq result) (coll? (first result)) @@ -1582,6 +1618,7 @@ (and (seq result) (or only-blocks? blocks-grouped-by-page?)) (->hiccup result (cond-> (assoc config + ;; :editor-box editor/box :custom-query? true :group-by-page? blocks-grouped-by-page?) children? @@ -1592,9 +1629,9 @@ ;; page list (and (seq result) (:page/name (first result))) - [:ol + [:ul#query-pages.mt-1 (for [{:page/keys [name original-name] :as page-entity} result] - [:li + [:li.mt-1 [:a {:href (rfe/href :page {:name name}) :on-click (fn [e] (util/stop e) diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index 9025bf7baf..68246e9702 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -87,7 +87,15 @@ .ls-block { min-height: 24px; padding: 2px 0; - margin-top: 1px; + border-bottom: 1px solid transparent; + + &.selected { + border-bottom-color: var(--ls-primary-background-color); + + &:last-child { + border-bottom-color: transparent; + } + } img { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); diff --git a/src/main/frontend/components/content.cljs b/src/main/frontend/components/content.cljs index 88c3c3056f..a80522e3ce 100644 --- a/src/main/frontend/components/content.cljs +++ b/src/main/frontend/components/content.cljs @@ -47,7 +47,7 @@ (rum/defc custom-context-menu-content [] - [:div#custom-context-menu.w-48.rounded-md.shadow-lg.transition.ease-out.duration-100.transform.opacity-100.scale-100.enter-done.absolute {:style {:z-index 4}} + [:div#custom-context-menu [:div.py-1.rounded-md.bg-base-3.shadow-xs (ui/menu-link {:key "cut" @@ -111,7 +111,7 @@ (let [properties (:block/properties block) heading (get properties "heading") heading? (= heading "true")] - [:div#custom-context-menu.w-64.rounded-md.shadow-lg.transition.ease-out.duration-100.transform.opacity-100.scale-100.enter-done.absolute {:style {:z-index 4}} + [:div#custom-context-menu [:div.py-1.rounded-md.bg-base-3.shadow-xs [:div.flex-row.flex.justify-between.py-4.pl-2 [:div.flex-row.flex.justify-between @@ -177,7 +177,7 @@ (ui/menu-link {:key "Copy block ref" :on-click (fn [_e] - (editor-handler/copy-block-ref! block-id))} + (editor-handler/copy-block-ref! block-id #(str "((" % "))")))} "Copy block ref") (block-template block-id) diff --git a/src/main/frontend/components/content.css b/src/main/frontend/components/content.css index 797f96fed2..4d60fbc8f8 100644 --- a/src/main/frontend/components/content.css +++ b/src/main/frontend/components/content.css @@ -9,4 +9,11 @@ .content a.initial-color:hover { color: initial; text-decoration: none; +} + +#custom-context-menu { + @apply rounded-md shadow-lg transition ease-out duration-100 transform + opacity-100 scale-100 absolute; + + z-index: calc(var(--ls-z-index-level-1) + 1); } \ No newline at end of file diff --git a/src/main/frontend/components/diff.cljs b/src/main/frontend/components/diff.cljs index 47332e5bc0..64946a5ad7 100644 --- a/src/main/frontend/components/diff.cljs +++ b/src/main/frontend/components/diff.cljs @@ -59,7 +59,8 @@ (if collapse? (svg/arrow-right) (svg/arrow-down))] - [:span.cp__diff-file-header-content path] + [:span.cp__diff-file-header-content {:style {:word-break "break-word"}} + path] (when resolved? [:span.text-green-600 {:dangerouslySetInnerHTML diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index 08049159b6..da3d0a8779 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -643,7 +643,7 @@ (when-let [element (gdom/getElement id)] (.focus element))) state) - :did-remount (fn [state] + :did-remount (fn [_old-state state] (keyboards-handler/esc-save! state) state) :will-unmount (fn [state] diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index 8425a40069..726a3a2e06 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -370,7 +370,8 @@ [:div.text-sm.ml-1.mb-4.flex-1.inline-flex {:key "page-file"} [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)] - [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4} + [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4 + :word-break "break-word"} :href (str "/file/" (util/url-encode file-path))} file-path] diff --git a/src/main/frontend/components/right_sidebar.css b/src/main/frontend/components/right_sidebar.css index c5a802ef18..5f070dad46 100644 --- a/src/main/frontend/components/right_sidebar.css +++ b/src/main/frontend/components/right_sidebar.css @@ -1,3 +1,12 @@ -.white-theme .cp__right-sidebar-settings a { +html[data-theme=light] { + .cp__right-sidebar-settings a { color: var(--ls-primary-text-color); + } + + .cp__right-sidebar { + .block-content { + --ls-page-inline-code-bg-color: var(--ls-quaternary-background-color); + --ls-page-blockquote-bg-color: var(--ls-quaternary-background-color); + } + } } diff --git a/src/main/frontend/components/search.cljs b/src/main/frontend/components/search.cljs index c0192f461f..ad91b2c617 100644 --- a/src/main/frontend/components/search.cljs +++ b/src/main/frontend/components/search.cljs @@ -13,6 +13,7 @@ [frontend.search :as search] [clojure.string :as string] [goog.dom :as gdom] + [medley.core :as medley] [frontend.context.i18n :as i18n])) (rum/defc dropdown-content-wrapper [state content] @@ -24,6 +25,46 @@ "exited" "transition ease-in duration-75 transform opacity-0 scale-95")} content]) +(defn- partition-between + "Split `coll` at positions where `pred?` is true." + [pred? coll] + (let [switch (reductions not= true (map pred? coll (rest coll)))] + (map (partial map first) (partition-by second (map list coll switch))))) + +(rum/defc highlight-fuzzy + [content indexes] + (let [n (count content) + max-hightlighted-len 64 + max-surrounding-len 32 + + first-index (first indexes) + last-index (nth indexes (dec (count indexes))) + last-index (min (+ first-index max-hightlighted-len -1) last-index) + last-index* (+ last-index max-surrounding-len) + indexes (take-while #(<= % last-index*) indexes) + content-begin (max 0 (- first-index max-surrounding-len)) + content-end (min n (+ last-index 1 max-surrounding-len)) ; exclusive + + ; finds inconsecutive sections + sections (partition-between #(> (- %2 %) 1) indexes) + hl-ranges (for [sec sections + :let [begin (first sec) + end (-> sec last inc)]] + [begin end]) ; `end` is exclusive + hl-ranges* (concat [[content-begin content-begin]] + hl-ranges + [[content-end content-end]]) + normal-ranges (for [[[_ begin] [end _]] (partition 2 1 hl-ranges*)] [begin end]) + normal-hl-pairs (partition-all 2 (medley/interleave-all normal-ranges hl-ranges))] + [:p + (mapcat + (fn [[normal highlighted]] + [(when-some [[begin end] normal] + [:span (subs content begin end)]) + (when-some [[begin end] highlighted] + [:mark (subs content begin end)])]) + normal-hl-pairs)])) + (rum/defc highlight [content q] (let [q-pattern (->> q @@ -135,11 +176,11 @@ data] :block - (let [{:block/keys [page content]} data] + (let [{:block/keys [page content indexes]} data] (let [page (:page/original-name page)] [:div.flex-1 [:div.text-sm.font-medium page] - (highlight content search-q)])) + (highlight-fuzzy content indexes)])) nil))})]))) diff --git a/src/main/frontend/components/settings.cljs b/src/main/frontend/components/settings.cljs index e1e971bd7d..1fe85ea694 100644 --- a/src/main/frontend/components/settings.cljs +++ b/src/main/frontend/components/settings.cljs @@ -68,6 +68,7 @@ preferred-workflow (keyword (state/sub [:me :preferred_workflow])) preferred-language (state/sub [:preferred-language]) enable-timetracking? (state/enable-timetracking?) + enable-block-time? (state/enable-block-time?) show-brackets? (state/show-brackets?) github-token (state/sub [:me :access-token]) cors-proxy (state/sub [:me :cors_proxy]) @@ -128,101 +129,112 @@ [:a {:href (str "/file/" (util/url-encode (str config/app-name "/" config/config-file)))} (t :settings-page/edit-config-edn)]) - (when logged? [:hr]) + [:hr] - (when logged? - [:div.mt-6.sm:mt-5 - [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 - [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 - {:for "preferred_format"} - (t :settings-page/preferred-file-format)] - [:div.mt-1.sm:mt-0.sm:col-span-2 - [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs - [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5 - {:on-change (fn [e] - (let [format (-> (util/evalue e) + [:div.mt-6.sm:mt-5 + [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 + {:for "preferred_format"} + (t :settings-page/preferred-file-format)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs + [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5 + {:on-change (fn [e] + (let [format (-> (util/evalue e) + (string/lower-case) + keyword)] + (user-handler/set-preferred-format! format)))} + (for [format [:org :markdown]] + [:option (cond-> + {:key (name format)} + (= format preferred-format) + (assoc :selected "selected")) + (string/capitalize (name format))])]]]] + [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 + {:for "preferred_workflow"} + (t :settings-page/preferred-workflow)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs + [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5 + {:on-change (fn [e] + (let [workflow (-> (util/evalue e) (string/lower-case) - keyword)] - (user-handler/set-preferred-format! format)))} - (for [format [:org :markdown]] - [:option (cond-> - {:key (name format)} - (= format preferred-format) - (assoc :selected "selected")) - (string/capitalize (name format))])]]]] - [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 - [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 - {:for "preferred_workflow"} - (t :settings-page/preferred-workflow)] - [:div.mt-1.sm:mt-0.sm:col-span-2 - [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs - [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5 - {:on-change (fn [e] - (let [workflow (-> (util/evalue e) - (string/lower-case) - keyword) - workflow (if (= workflow :now/later) - :now - :todo)] - (user-handler/set-preferred-workflow! workflow)))} - (for [workflow [:now :todo]] - [:option (cond-> - {:key (name workflow)} - (= workflow preferred-workflow) - (assoc :selected "selected")) - (if (= workflow :now) - "NOW/LATER" - "TODO/DOING")])]]]] + keyword) + workflow (if (= workflow :now/later) + :now + :todo)] + (user-handler/set-preferred-workflow! workflow)))} + (for [workflow [:now :todo]] + [:option (cond-> + {:key (name workflow)} + (= workflow preferred-workflow) + (assoc :selected "selected")) + (if (= workflow :now) + "NOW/LATER" + "TODO/DOING")])]]]] - [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 - [:label.block.text-sm.font-medium.leading-5.opacity-70 - {:for "enable_timetracking"} - (t :settings-page/enable-timetracking)] - [:div.mt-1.sm:mt-0.sm:col-span-2 - [:div.max-w-lg.rounded-md.sm:max-w-xs - (ui/toggle enable-timetracking? - (fn [] - (let [value (not enable-timetracking?)] - (config-handler/set-config! :feature/enable-timetracking? value))))]]] + [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.opacity-70 + {:for "enable_timetracking"} + (t :settings-page/enable-timetracking)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.sm:max-w-xs + (ui/toggle enable-timetracking? + (fn [] + (let [value (not enable-timetracking?)] + (config-handler/set-config! :feature/enable-timetracking? value))))]]] - [:hr] + [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.opacity-70 + {:for "enable_block_time"} + (t :settings-page/enable-block-time)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.sm:max-w-xs + (ui/toggle enable-block-time? + (fn [] + (let [value (not enable-block-time?)] + (config-handler/set-config! :feature/enable-block-time? value))))]]] - (ui/admonition - :important - [:p (t :settings-page/dont-use-other-peoples-proxy-servers) - [:a {:href "https://github.com/isomorphic-git/cors-proxy" - :target "_blank"} - "https://github.com/isomorphic-git/cors-proxy"]]) + [:hr] - [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 - [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 - {:for "cors"} - (t :settings-page/custom-cors-proxy-server)] - [:div.mt-1.sm:mt-0.sm:col-span-2 - [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs - [:input#pat.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5 - {:default-value cors-proxy - :on-blur (fn [event] - (when-let [server (util/evalue event)] - (user-handler/set-cors! server) - (notification/show! "Custom CORS proxy updated successfully!" :success))) - :on-key-press (fn [event] - (let [k (gobj/get event "key")] - (if (= "Enter" k) - (when-let [server (util/evalue event)] - (user-handler/set-cors! server) - (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]] + (when logged? + [:div + (ui/admonition + :important + [:p (t :settings-page/dont-use-other-peoples-proxy-servers) + [:a {:href "https://github.com/isomorphic-git/cors-proxy" + :target "_blank"} + "https://github.com/isomorphic-git/cors-proxy"]]) + [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 + {:for "cors"} + (t :settings-page/custom-cors-proxy-server)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs + [:input#pat.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5 + {:default-value cors-proxy + :on-blur (fn [event] + (when-let [server (util/evalue event)] + (user-handler/set-cors! server) + (notification/show! "Custom CORS proxy updated successfully!" :success))) + :on-key-press (fn [event] + (let [k (gobj/get event "key")] + (if (= "Enter" k) + (when-let [server (util/evalue event)] + (user-handler/set-cors! server) + (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]] - [:hr] + [:hr]]) - [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 - [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 - {:for "developer_mode"} - (t :settings-page/developer-mode)] - [:div.mt-1.sm:mt-0.sm:col-span-2 - [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs - (ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode)) - :on-click #(state/set-developer-mode! (not developer-mode?)))]]] + [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5 + [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70 + {:for "developer_mode"} + (t :settings-page/developer-mode)] + [:div.mt-1.sm:mt-0.sm:col-span-2 + [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs + (ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode)) + :on-click #(state/set-developer-mode! (not developer-mode?)))]]] - [:br] - (t :settings-page/developer-mode-desc)])]]))) + [:br] + (t :settings-page/developer-mode-desc)]]]))) diff --git a/src/main/frontend/components/sidebar.css b/src/main/frontend/components/sidebar.css index 697c7c7af6..b37e85c3d5 100644 --- a/src/main/frontend/components/sidebar.css +++ b/src/main/frontend/components/sidebar.css @@ -115,7 +115,7 @@ height: calc(100vh - 3rem); overflow-x: hidden; overflow-y: auto; - z-index: 9; + z-index: var(--ls-z-index-level-1); transition: width 0.3s, opacity 0.2s; border-radius: var(--ls-border-radius-low) 0 0 0; diff --git a/src/main/frontend/components/theme.css b/src/main/frontend/components/theme.css index 0a1d0256c4..b342b0cd3d 100644 --- a/src/main/frontend/components/theme.css +++ b/src/main/frontend/components/theme.css @@ -1,6 +1,13 @@ :root { scrollbar-width: thin; scrollbar-color: var(--ls-scrollbar-foreground-color) var(--ls-scrollbar-background-color); + + --ls-z-index-level-0: 0; + --ls-z-index-level-1: 9; + --ls-z-index-level-2: 99; + --ls-z-index-level-3: 999; + --ls-z-index-level-4: 9999; + --ls-z-index-level-5: 99999; } html:not(.is-mac) { diff --git a/src/main/frontend/db/query_custom.cljs b/src/main/frontend/db/query_custom.cljs index 52db36d66e..9e4dffda69 100644 --- a/src/main/frontend/db/query_custom.cljs +++ b/src/main/frontend/db/query_custom.cljs @@ -37,33 +37,6 @@ :else input)) -(defn- custom-query-aux - [{:keys [query inputs result-transform] :as query'} query-opts] - (try - (let [inputs (map resolve-input inputs) - repo (state/get-current-repo) - k [:custom query']] - (apply react/q repo k query-opts query inputs)) - (catch js/Error e - (println "Custom query failed: ") - (js/console.dir e)))) - -(defn custom-query - ([query] - (custom-query query {})) - ([query query-opts] - (when-let [query' (cond - (and (string? query) - (not (string/blank? query))) - (reader/read-string query) - - (map? query) - query - - :else - nil)] - (custom-query-aux query' query-opts)))) - (defn custom-query-result-transform [query-result remove-blocks q] (let [repo (state/get-current-repo) @@ -92,3 +65,30 @@ (if block? (db-utils/group-by-page result) result))))) + +(defn react-query + [repo {:keys [query inputs] :as query'} query-opts] + (try + (let [inputs (map resolve-input inputs) + repo (or repo (state/get-current-repo)) + k [:custom query']] + (apply react/q repo k query-opts query inputs)) + (catch js/Error e + (println "Custom query failed: ") + (js/console.dir e)))) + +(defn custom-query + ([query] + (custom-query query {})) + ([query query-opts] + (when-let [query' (cond + (and (string? query) + (not (string/blank? query))) + (reader/read-string query) + + (map? query) + query + + :else + nil)] + (react-query (state/get-current-repo) query' query-opts)))) diff --git a/src/main/frontend/db/query_dsl.cljs b/src/main/frontend/db/query_dsl.cljs new file mode 100644 index 0000000000..310d82ee20 --- /dev/null +++ b/src/main/frontend/db/query_dsl.cljs @@ -0,0 +1,390 @@ +(ns frontend.db.query-dsl + (:require [cljs.reader :as reader] + [frontend.db.utils :as db-utils] + [datascript.core :as d] + [lambdaisland.glogi :as log] + [clojure.string :as string] + [frontend.db :as db] + [frontend.text :as text] + [frontend.db.query-custom :as query-custom] + [frontend.date :as date] + [cljs-time.core :as t] + [cljs-time.coerce :as tc] + [frontend.util :as util] + [medley.core :as medley] + [clojure.walk :as walk] + [clojure.core])) + +;; Query fields: + +;; and +;; or +;; not +;; between +;; Example: (between -7d +7d) +;; (between created-at -1d today) +;; (between last-modified-at -1d today) +;; [[page-ref]] +;; property (block) +;; todo (block) +;; priority (block) +;; page-property (page) +;; page-tags (page) +;; all-page-tags +;; project (block, TBD) + +;; Sort by (field, asc/desc): + +;; created_at +;; last_modified_at + +;; (sort-by last_modified_at asc) + +(defonce remove-nil? (partial remove nil?)) + +(defn query-wrapper + [where blocks?] + (let [q (if blocks? ; FIXME: it doesn't need to be either blocks or pages + '[:find (pull ?b [*]) + :where] + '[:find (pull ?p [*]) + :where]) + result (if (coll? (first where)) + (apply conj q where) + (conj q where))] + (prn "Datascript query: " result) + result)) + +;; (between -7d +7d) +(defn- ->journal-day-int [input] + (let [input (string/lower-case (name input))] + (cond + (= "today" input) + (db-utils/date->int (t/today)) + + (= "yesterday" input) + (db-utils/date->int (t/yesterday)) + + (= "tomorrow" input) + (db-utils/date->int (t/plus (t/today) (t/days 1))) + + (text/page-ref? input) + (let [input (-> (text/page-ref-un-brackets! input) + (string/replace ":" "") + (string/capitalize))] + (when (date/valid-journal-title? input) + (date/journal-title->int input))) + + :else + (let [duration (util/parse-int (subs input 0 (dec (count input)))) + kind (last input) + tf (case kind + "y" t/years + "m" t/months + "w" t/weeks + t/days)] + (db-utils/date->int (t/plus (t/today) (tf duration))))))) + +(defn- ->timestamp [input] + (let [input (string/lower-case (name input))] + (cond + (= "now" input) + (util/time-ms) + + (= "today" input) + (tc/to-long (t/today)) + + (= "yesterday" input) + (tc/to-long (t/yesterday)) + + (= "tomorrow" input) + (tc/to-long (t/plus (t/today) (t/days 1))) + + (text/page-ref? input) + (let [input (-> (text/page-ref-un-brackets! input) + (string/replace ":" "") + (string/capitalize))] + (when (date/valid-journal-title? input) + (date/journal-title->long input))) + + :else + (let [duration (util/parse-int (subs input 0 (dec (count input)))) + kind (last input) + tf (case kind + "y" t/years + "m" t/months + "w" t/weeks + "h" t/hours + "n" t/minutes ; min + t/days)] + (tc/to-long (t/plus (t/today) (tf duration))))))) + +#_(defn uniq-symbol + [counter prefix] + (let [result (symbol (str prefix (when-not (zero? @counter) + @counter)))] + (swap! counter inc) + result)) + +(defn build-query + ([repo e env] + (build-query repo e env 0)) + ([repo e {:keys [sort-by blocks? counter current-filter] :as env} level] + ;; TODO: replace with multi-methods for extensibility. + (let [fe (first e) + page-ref? (text/page-ref? e)] + (when (or (and page-ref? + (not (contains? #{'page-property 'page-tags} (:current-filter env)))) + (contains? #{'between 'property 'todo 'priority 'sort-by} fe)) + (reset! blocks? true)) + (cond + (nil? e) + nil + + page-ref? + (let [page-name (text/page-ref-un-brackets! e)] + (when (and (not (string/blank? page-name)) + (some? (db-utils/entity repo [:page/name page-name]))) + [['?b :block/ref-pages [:page/name page-name]]])) + + (contains? #{'and 'or 'not} fe) + (let [clauses (->> (map (fn [form] + (build-query repo form (assoc env :current-filter fe) (inc level))) + (rest e)) + remove-nil? + (distinct))] + (when (seq clauses) + (let [result (cond + (= fe 'not) + (let [clauses (if (coll? (first clauses)) + (apply concat clauses) + clauses) + clauses (if (and (= 1 (count clauses)) + (= 'and (ffirst clauses))) + ;; unflatten + (rest (first clauses)) + clauses)] + (cons fe (seq clauses))) + + (coll? (first clauses)) + (if (= current-filter 'not) + (->> (apply concat clauses) + (apply list fe)) + (->> (map #(cons 'and (seq %)) clauses) + (apply list fe))) + + :else + (apply list fe clauses))] + (cond + (and (zero? level) (= 'and fe)) + (distinct (apply concat clauses)) + + (and (zero? level) (= 'or fe)) + result + + :else + [result])))) + + (and (= 'between fe) + (= 3 (count e))) + (let [start (->journal-day-int (nth e 1)) + end (->journal-day-int (nth e 2)) + [start end] (sort [start end])] + [['?b :block/page '?p] + ['?p :page/journal? true] + ['?p :page/journal-day '?d] + [(list '>= '?d start)] + [(list '<= '?d end)]]) + + ;; (between created_at -1d today) + (and (= 'between fe) + (= 4 (count e))) + (let [k (-> (second e) + (name) + (string/lower-case) + (string/replace "-" "_"))] + (when (contains? #{"created_at" "last_modified_at"} k) + (let [start (->timestamp (nth e 2)) + end (->timestamp (nth e 3))] + (when (and start end) + (let [[start end] (sort [start end]) + sym '?v] + [['?b :block/properties '?p] + [(list 'get '?p k) sym] + [(list '>= sym start)] + [(list '< sym end)]]))))) + + (and (= 'property fe) + (= 3 (count e))) + (let [v (some-> (name (nth e 2)) + (text/page-ref-un-brackets!)) + sym '?v] + [['?b :block/properties '?p] + [(list 'get '?p (name (nth e 1))) sym] + (list + 'or + [(list '= sym v)] + [(list 'contains? sym v)])]) + + (and (= 'property fe) + (= 2 (count e))) + [['?b :block/properties '?p] + [(list 'get '?p (name (nth e 1)))]] + + (= 'todo fe) + (let [markers (if (coll? (first (rest e))) + (first (rest e)) + (rest e))] + (when (seq markers) + (let [markers (set (map (comp string/upper-case name) markers))] + [['?b :block/marker '?marker] + [(list 'contains? markers '?marker)]]))) + + (= 'priority fe) + (let [priorities (if (coll? (first (rest e))) + (first (rest e)) + (rest e))] + (when (seq priorities) + (let [priorities (set (map (comp string/upper-case name) priorities))] + [['?b :block/priority '?priority] + [(list 'contains? priorities '?priority)]]))) + + (= 'sort-by fe) + (let [[k order] (rest e) + order (if (and order (contains? #{:asc :desc} + (keyword (string/lower-case (name order))))) + (keyword (string/lower-case (name order))) + :desc) + k (-> (string/lower-case (name k)) + (string/replace "-" "_"))] + (when (contains? #{"created_at" "last_modified_at"} k) + (let [comp (if (= order :desc) >= <=)] + (reset! sort-by + (fn [result] + (->> result + flatten + (clojure.core/sort-by #(get-in % [:block/properties k]) + comp)))) + nil))) + + (= 'page-property fe) + (let [[k v] (rest e)] + (if v + (let [v (some->> (name (nth e 2)) + (text/page-ref-un-brackets!)) + sym '?v] + [['?p :page/properties '?prop] + [(list 'get '?prop (keyword (nth e 1))) sym] + (list + 'or + [(list '= sym v)] + [(list 'contains? sym v)])]) + [['?p :page/properties '?prop] + [(list 'get '?prop (keyword (nth e 1)))]])) + + (= 'page-tags fe) + (let [tags (if (coll? (first (rest e))) + (first (rest e)) + (rest e))] + (when (seq tags) + (let [tags (set (map (comp text/page-ref-un-brackets! name) tags))] + [['?p :page/tags '?t] + ['?t :tag/name '?tag] + [(list 'contains? tags '?tag)]]))) + + (= 'all-page-tags fe) + [['?t :tag/name '?tag] + ['?p :page/name '?tag]] + + :else + nil)))) + +(defn- pre-transform + [s] + (some-> s + (string/replace text/page-ref-re "\"[[$1]]\"") + (string/replace text/between-re (fn [[_ x]] + (->> (string/split x #" ") + (remove string/blank?) + (map (fn [x] + (if (or (contains? #{"+" "-"} (first x)) + (and (re-find #"\d" (first x)) + (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"]))) + (keyword (name x)) + x))) + (string/join " ") + (util/format "(between %s)")))))) + +(defn parse + [repo s] + (when (and (string? s) + (not (string/blank? s))) + (let [counter (atom 0)] + (try + (let [form (some-> s + (pre-transform) + (reader/read-string)) + sort-by (atom nil) + blocks? (atom nil) + result (when form (build-query repo form {:sort-by sort-by + :blocks? blocks? + :counter counter})) + result (when (seq result) + (let [key (if (coll? (first result)) + (keyword (ffirst result)) + (keyword (first result)))] + (case key + :and + (rest result) + + :not + (cons ['?b :block/uuid] result) + + :or + result + + result)))] + {:query result + :sort-by @sort-by + :blocks? (boolean @blocks?)}) + (catch js/Error e + (log/error :query-dsl/parse-error e)))))) + +(defn query + [repo query-string] + (when query-string + (let [{:keys [query sort-by blocks?]} (parse repo query-string)] + (when query + (let [query (query-wrapper query blocks?)] + (query-custom/react-query repo + {:query query} + (if sort-by + {:transform-fn sort-by}))))))) + +(comment + ;; {{query (and (page-property foo bar) [[hello]])}} + + (query "(and [[foo]] [[bar]])") + + (query "(or [[foo]] [[bar]])") + + (query "(not (or [[foo]] [[bar]]))") + + (query "(between -7d +7d)") + + (query "(between -7d today)") + + (query "(between created_at yesterday today)") + + (query "(and [[some page]] (property foo bar))") + + (query "(and [[some page]] (todo now later))") + + (query "(and [[some page]] (priority A))") + + ;; nested query + (query "(and [[baz]] (or [[foo]] [[bar]]))") + + (query "(and [[some page]] (sort-by created-at))") + + (query "(and (page-property foo bar) [[hello]])")) diff --git a/src/main/frontend/db/react.cljs b/src/main/frontend/db/react.cljs index 7474a227ae..c6ff505927 100644 --- a/src/main/frontend/db/react.cljs +++ b/src/main/frontend/db/react.cljs @@ -51,7 +51,7 @@ (let [state @query-state state (->> (filter (fn [[[_repo k] v]] (contains? #{:blocks :block/block :custom} k)) state) - (into {}))] + (into {}))] (reset! query-state state))) ;; TODO: Add components which subscribed to a specific query @@ -72,21 +72,21 @@ (defn add-query-component! [key component] (swap! query-components update key - (fn [components] - (distinct (conj components component))))) + (fn [components] + (distinct (conj components component))))) (defn remove-query-component! [component] (reset! - query-components - (->> (for [[k components] @query-components - :let [new-components (remove #(= component %) components)]] - (if (empty? new-components) ; no subscribed components - (do (remove-q! k) - nil) - [k new-components])) - (keep identity) - (into {})))) + query-components + (->> (for [[k components] @query-components + :let [new-components (remove #(= component %) components)]] + (if (empty? new-components) ; no subscribed components + (do (remove-q! k) + nil) + [k new-components])) + (keep identity) + (into {})))) (defn get-page-blocks-cache-atom [repo page-id] @@ -160,6 +160,7 @@ ;; TODO: Extract several parts to handlers + (defn get-current-page [] (let [match (:route-match @state/state) @@ -179,8 +180,8 @@ (when page (let [page-name (util/url-decode (string/lower-case page))] (db-utils/entity (if tag? - [:tag/name page-name] - [:page/name page-name])))))) + [:tag/name page-name] + [:page/name page-name])))))) (defn get-current-priority [] diff --git a/src/main/frontend/db_schema.cljs b/src/main/frontend/db_schema.cljs index 813f814485..bdfc1cfcfe 100644 --- a/src/main/frontend/db_schema.cljs +++ b/src/main/frontend/db_schema.cljs @@ -94,17 +94,5 @@ :block/deadline-ast {} :block/repeated? {} - ;; TODO: To make this really working, every block needs a persisting `CUSTOM-ID`, which I'd like to avoid for now. - ;; Any suggestions? - :block/created-at {} - :block/last-modified-at {} - ;; For pages - :tag/name {:db/unique :db.unique/identity} - ;; ;; Definitions, useful for tags and future anki cards - ;; :definition/block {:db/valueType :db.type/ref} - ;; ;; Why not make :definition/key unique? - ;; ;; Multiple definitions with the same key in either one page or multiple pages - ;; :definition/key {} - ;; :definition/value {} -}) + :tag/name {:db/unique :db.unique/identity}}) diff --git a/src/main/frontend/dicts.cljs b/src/main/frontend/dicts.cljs index 1573043809..0441c6a2c9 100644 --- a/src/main/frontend/dicts.cljs +++ b/src/main/frontend/dicts.cljs @@ -289,6 +289,7 @@ title: How to take dummy notes? :settings-page/preferred-file-format "Preferred file format" :settings-page/preferred-workflow "Preferred workflow" :settings-page/enable-timetracking "Enable timetracking" + :settings-page/enable-block-time "Enable block timestamps" :settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check " :settings-page/custom-cors-proxy-server "Custom CORS proxy server" :settings-page/developer-mode "Developer mode" @@ -768,6 +769,7 @@ title: How to take dummy notes? :settings-page/preferred-file-format "首选文件格式" :settings-page/preferred-workflow "首选工作流" :settings-page/enable-timetracking "开启 timetracking" + :settings-page/enable-block-time "记录 block 创建/修改时间" :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器,Logseq 将不会对此损失负责。您可以自己部署它,请查阅 " :settings-page/custom-cors-proxy-server "自定义 CORS 代理服务器" :settings-page/developer-mode "开发者模式" diff --git a/src/main/frontend/extensions/code.cljs b/src/main/frontend/extensions/code.cljs index 3f29e83f1f..d5f9feace3 100644 --- a/src/main/frontend/extensions/code.cljs +++ b/src/main/frontend/extensions/code.cljs @@ -107,42 +107,42 @@ (if @editor-atom @editor-atom (let [[config id attr code pos_meta] (:rum/args state) - original-mode (get attr :data-lang) - mode (or original-mode "javascript") - clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode) - mode (if clojure? "clojure" (text->cm-mode mode)) - lisp? (or clojure? - (contains? #{"scheme" "racket" "lisp"} mode)) - textarea (gdom/getElement id) - editor (or - @(:editor-atom state) - (when textarea - (from-textarea textarea - #js {:mode mode - :matchBrackets lisp? - :autoCloseBrackets true - :lineNumbers true - :extraKeys #js {"Esc" (fn [cm] - (let [save! #(save-file-or-block-when-blur-or-esc! cm textarea config state)] - (if-let [block-id (:block/uuid config)] - (let [block (db/pull [:block/uuid block-id]) - value (.getValue cm) - textarea-value (gobj/get textarea "value") - changed? (not= value textarea-value)] - (if changed? - (save!) - (editor-handler/edit-block! block :max (:block/format block) block-id))) - (save!))))}})))] - (when editor - (let [element (.getWrapperElement editor)] - (.on editor "blur" (fn [] - (save-file-or-block-when-blur-or-esc! editor textarea config state))) - (.addEventListener element "click" - (fn [e] - (util/stop e))) - (.save editor) - (.refresh editor))) - editor)))) + original-mode (get attr :data-lang) + mode (or original-mode "javascript") + clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode) + mode (if clojure? "clojure" (text->cm-mode mode)) + lisp? (or clojure? + (contains? #{"scheme" "racket" "lisp"} mode)) + textarea (gdom/getElement id) + editor (or + @(:editor-atom state) + (when textarea + (from-textarea textarea + #js {:mode mode + :matchBrackets lisp? + :autoCloseBrackets true + :lineNumbers true + :extraKeys #js {"Esc" (fn [cm] + (let [save! #(save-file-or-block-when-blur-or-esc! cm textarea config state)] + (if-let [block-id (:block/uuid config)] + (let [block (db/pull [:block/uuid block-id]) + value (.getValue cm) + textarea-value (gobj/get textarea "value") + changed? (not= value textarea-value)] + (if changed? + (save!) + (editor-handler/edit-block! block :max (:block/format block) block-id))) + (save!))))}})))] + (when editor + (let [element (.getWrapperElement editor)] + (.on editor "blur" (fn [] + (save-file-or-block-when-blur-or-esc! editor textarea config state))) + (.addEventListener element "click" + (fn [e] + (util/stop e))) + (.save editor) + (.refresh editor))) + editor)))) (defn- load-and-render! [state] @@ -158,7 +158,7 @@ :did-mount (fn [state] (load-and-render! state) state) - :did-remount (fn [state] + :did-remount (fn [old_state state] (load-and-render! state) state)} [state config id attr code pos_meta] diff --git a/src/main/frontend/external/roam.cljs b/src/main/frontend/external/roam.cljs index 5284c5c29b..16a8f2a1d2 100644 --- a/src/main/frontend/external/roam.cljs +++ b/src/main/frontend/external/roam.cljs @@ -39,7 +39,7 @@ (let [name (case name "[[embed]]" "embed" name)] - (util/format "{{{%s %s}}}" name arg)) + (util/format "{{%s %s}}" name arg)) original))))) (defn load-all-refed-uids! diff --git a/src/main/frontend/format/block.cljs b/src/main/frontend/format/block.cljs index 6ea94721db..b4d48a759d 100644 --- a/src/main/frontend/format/block.cljs +++ b/src/main/frontend/format/block.cljs @@ -8,6 +8,7 @@ [frontend.config :as config] [datascript.core :as d] [frontend.date :as date] + [frontend.text :as text] [medley.core :as medley])) (defn heading-block? @@ -134,6 +135,12 @@ (= "List" (first block)) (:name (first (second block))))) +(defn- ->schema-properties + [properties] + (-> properties + (update "created_at" util/safe-parse-int) + (update "last_modified_at" util/safe-parse-int))) + (defn extract-properties [[_ properties] start-pos end-pos] (let [properties (->> (into {} properties) @@ -144,8 +151,9 @@ (contains? config/markers k') (util/safe-parse-int v')) (util/safe-parse-int v') - v')] - [k' v']))))] + (text/split-page-refs-without-brackets v'))] + [k' v']))) + (->schema-properties))] {:properties properties :start-pos start-pos :end-pos end-pos})) @@ -379,7 +387,7 @@ block {:block/meta meta :block/marker (get block :block/marker "nil") - :block/properties (get block :block/properties []) + :block/properties (get block :block/properties {}) :block/file file :block/format format :block/page page diff --git a/src/main/frontend/format/mldoc.cljs b/src/main/frontend/format/mldoc.cljs index 7172e8ee88..ac6ee7484a 100644 --- a/src/main/frontend/format/mldoc.cljs +++ b/src/main/frontend/format/mldoc.cljs @@ -6,7 +6,9 @@ [cljs.core.match :refer-macros [match]] [lambdaisland.glogi :as log] [goog.object :as gobj] - ["mldoc" :as mldoc :refer [Mldoc]])) + [frontend.text :as text] + ["mldoc" :as mldoc :refer [Mldoc]] + [medley.core :as medley])) (defonce parseJson (gobj/get Mldoc "parseJson")) (defonce parseInlineJson (gobj/get Mldoc "parseInlineJson")) @@ -31,35 +33,6 @@ [text config] (parseInlineJson text (or config default-config))) -;; E.g "Foo Bar \"Bar Baz\"" -(defn- sep-by-quote-or-space-or-comma - [s] - (when s - (let [comma? (re-find #"[,|,]+" s)] - (some->> - (string/split s #"[\"|\,|,]{1}") - (remove string/blank?) - (map (fn [s] - (if (and (not comma?) - (or (contains? #{" " "#"} (first s)) - (= " " (last s)))) - ;; space/hashtag separated tags - (string/split (string/trim s) #" ") - s))) - flatten - distinct - (remove string/blank?) - (map string/lower-case) - (map string/trim))))) - -(defn- remove-page-ref-brackets - [s] - (if (and (string? s) - (string/starts-with? s "[[") - (string/ends-with? s "]]")) - (subs s 2 (- (count s) 2)) - s)) - ;; Org-roam (defn get-tags-from-definition [ast] @@ -101,8 +74,11 @@ directive? (fn [item] (= "directive" (string/lower-case (first item)))) properties (->> (take-while directive? ast) (map (fn [[_ k v]] - [(keyword (string/lower-case k)) - v])) + (let [k (keyword (string/lower-case k)) + v (if (contains? #{:title :description} k) + v + (text/split-page-refs-without-brackets v))] + [k v]))) (into {})) macro-properties (filter (fn [x] (= :macro (first x))) properties) macros (if (seq macro-properties) @@ -124,17 +100,7 @@ properties (if (seq properties) (cond-> properties (:roam_key properties) - (assoc :key (:roam_key properties)) - (:alias properties) - (update :alias - (fn [s] - (->> s - (sep-by-quote-or-space-or-comma) - (map remove-page-ref-brackets)))) - (:tags properties) - (update :tags sep-by-quote-or-space-or-comma) - (:roam_tags properties) - (update :roam_tags sep-by-quote-or-space-or-comma)) + (assoc :key (:roam_key properties))) properties) definition-tags (get-tags-from-definition ast) properties (if definition-tags diff --git a/src/main/frontend/graph.cljs b/src/main/frontend/graph.cljs index 7173332abd..6c8ce90b17 100644 --- a/src/main/frontend/graph.cljs +++ b/src/main/frontend/graph.cljs @@ -54,20 +54,6 @@ (.toString 16) (.padStart 6 "0")))) -(defn- text-mode - [node ctx global-scale dark?] - (let [label (gobj/get node "id") - x (gobj/get node "x") - y (gobj/get node "y") - color (gobj/get node "color") - font-size (/ 14 global-scale) - text-width (gobj/get (.measureText ctx label) "width")] - (set! (.-font ctx) (str font-size "px Inter")) - (set! (.-filltextAlign ctx) "center") - (set! (.-textBaseLine ctx) "middle") - (set! (.-fillStyle ctx) color) - (.fillText ctx label (- x (/ text-width 2)) y))) - (defn- dot-mode [node ctx global-scale dark?] (let [label (gobj/get node "id") @@ -148,12 +134,13 @@ (let [link {:source (gobj/get link "source") :target (gobj/get link "target")}] (if (contains? @highlight-links link) 5 1))) - :linkDirectionalParticles 4 - :linkDirectionalParticleWidth (fn [link] (let [link {:source (-> (gobj/get link "source") - (gobj/get "id")) - :target (-> (gobj/get link "target") - (gobj/get "id"))}] - (if (contains? @highlight-links link) 4 0))) + :linkDirectionalParticles 2 + :linkDirectionalParticleWidth (fn [link] + (let [link {:source (-> (gobj/get link "source") + (gobj/get "id")) + :target (-> (gobj/get link "target") + (gobj/get "id"))}] + (if (contains? @highlight-links link) 2 0))) :onNodeHover on-node-hover :onLinkHover on-link-hover :nodeLabel "id" @@ -188,7 +175,5 @@ (case @graph-mode :dot-text (dot-text-mode node ctx global-scale dark?) - :dot - (dot-mode node ctx global-scale dark?) - (text-mode node ctx global-scale dark?)))} + (dot-mode node ctx global-scale dark?)))} option))) diff --git a/src/main/frontend/handler.cljs b/src/main/frontend/handler.cljs index 6fd4ab71c5..3cf6d9b74b 100644 --- a/src/main/frontend/handler.cljs +++ b/src/main/frontend/handler.cljs @@ -87,23 +87,11 @@ :else (state/set-db-restoring! false)) - (if (schema-changed?) - (do - (notification/show! - [:p "Database schema changed, your notes will be exported as zip files, your repos will be re-indexed then."] - :warning - false) - (let [export-repos (for [repo repos] - (when-let [url (:url repo)] - (println "Export repo: " url) - (export-handler/export-repo-as-zip! url)))] - (-> (p/all export-repos) - (p/then (fn [] - (store-schema!) - (js/setTimeout clear-stores-and-refresh! 5000))) - (p/catch (fn [error] - (log/error :export/zip {:error error - :repos repos})))))) + (if false ; FIXME: incompatible changes + (notification/show! + [:p "Database schema changed, please export your notes as a zip file, and re-index your repos."] + :warning + false) (store-schema!)) (nfs/ask-permission-if-local?) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index a7323b8f74..b46d47bcd1 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -380,22 +380,44 @@ "ls-block" "edit-block")))) +(defn- with-time-properties + [block properties] + (if (and (state/enable-block-time?) + (not (:block/pre-block? block))) + (let [time (util/time-ms) + props (into {} (:block/properties block))] + (merge properties + (if-let [created-at (get props "created_at")] + {"created_at" created-at + "last_modified_at" time} + {"created_at" time + "last_modified_at" time}))) + properties)) + +(defn- block-text-with-time + [block format value] + (let [value (text/remove-level-spaces value (keyword format)) + properties (with-time-properties block {})] + (text/re-construct-block-properties value properties))) + (defn save-block-if-changed! ([block value] (save-block-if-changed! block value nil)) ([{:block/keys [uuid content meta file page dummy? format repo pre-block? content ref-pages ref-blocks] :as block} value - {:keys [indent-left? custom-properties remove-property? rebuild-content?] + {:keys [indent-left? custom-properties remove-properties rebuild-content?] :or {rebuild-content? true custom-properties nil - remove-property? false}}] + remove-properties nil}}] (let [value value repo (or repo (state/get-current-repo)) e (db/entity repo [:block/uuid uuid]) block (assoc (with-block-meta repo block) - :block/properties (:block/properties e)) + ;; (into {} ...) to fix the old data + :block/properties (into {} (:block/properties e))) format (or format (state/get-preferred-format)) page (db/entity repo (:db/id page)) + ;; page properties [old-properties new-properties] (when pre-block? [(:page/properties (db/entity (:db/id page))) (mldoc/parse-properties value format)]) @@ -415,17 +437,14 @@ new-properties (if permalink-changed? (assoc new-properties :old_permalink (:permalink old-properties)) new-properties) - value (cond - (or (seq custom-properties) - remove-property?) - (text/re-construct-block-properties block value custom-properties) - - (and (seq (:block/properties block)) - (text/properties-hidden? (:block/properties block))) - (text/re-construct-block-properties block value (:block/properties block)) - - :else - value)] + text-properties (text/extract-properties value) + properties (->> custom-properties + (with-time-properties block) + (merge text-properties)) + properties (if (and (seq properties) (seq remove-properties)) + (medley/remove-keys (fn [k] (contains? (set remove-properties) k)) properties) + properties) + value (text/re-construct-block-properties value properties)] (cond (not= (string/trim content) (string/trim value)) ; block content changed (let [file (db/entity repo (:db/id file))] @@ -455,10 +474,11 @@ (util/format "File %s already exists!" file-path)] :error) ;; create the file - (let [content (str (util/default-content-with-title format + (let [value (block-text-with-time nil format value) + content (str (util/default-content-with-title format (or (:page/original-name page) (:page/name page))) - (text/remove-level-spaces value (keyword format)))] + value)] (p/let [_ (fs/create-if-not-exists repo dir file-path content) _ (git-handler/git-add repo path)] (file-handler/reset-file! repo path content) @@ -607,7 +627,9 @@ (let [value (if create-new-block? (str fst-block-text "\n" snd-block-text) value) - value (text/re-construct-block-properties block value properties) + text-properties (text/extract-properties fst-block-text) + properties (with-time-properties block text-properties) + value (text/re-construct-block-properties value properties) value (rebuild-block-content value format) [new-content value] (new-file-content block file-content value) parse-result (block/parse-block (assoc block :block/content value) format) @@ -708,11 +730,12 @@ (:page/name page)))] (p/let [_ (fs/create-if-not-exists repo dir file-path content) _ (git-handler/git-add repo path)] - (file-handler/reset-file! repo path - (str content - (text/remove-level-spaces value (keyword format)) - "\n" - snd-block-text)) + (let [value (block-text-with-time nil format value)] + (file-handler/reset-file! repo path + (str content + value + "\n" + snd-block-text))) (ui-handler/re-render-root!) ;; Continue to edit the last block @@ -761,14 +784,13 @@ (defn- with-timetracking-properties [block value] (let [new-marker (first (re-find format/bare-marker-pattern (or value ""))) - new-marker (if new-marker (string/lower-case (string/trim new-marker))) - properties (into {} (:block/properties block))] + new-marker (if new-marker (string/lower-case (string/trim new-marker)))] (if (and new-marker (not= new-marker (string/lower-case (or (:block/marker block) ""))) (state/enable-timetracking?)) - (assoc properties new-marker (util/time-ms)) - properties))) + {new-marker (util/time-ms)} + {}))) (defn insert-new-block! [state] @@ -844,13 +866,10 @@ (defn- with-marker-time [block marker] - (let [properties (:block/properties block) - properties (into {} properties)] - (if (state/enable-timetracking?) - (assoc properties - (string/lower-case marker) - (util/time-ms)) - properties))) + (if (state/enable-timetracking?) + (let [marker (string/lower-case marker)] + {marker (util/time-ms)}) + {})) (defn check [{:block/keys [uuid marker content meta file dummy? repeated?] :as block}] @@ -1050,8 +1069,7 @@ (let [{:block/keys [content properties]} block] (when (get properties key) (save-block-if-changed! block content - {:custom-properties (dissoc properties key) - :remove-property? true})))))) + {:remove-properties [key]})))))) (defn set-block-property! [block-id key value] @@ -1067,13 +1085,9 @@ nil :else - (let [properties (:block/properties block) - properties' (if (seq properties) - (assoc properties key value) - {key value})] - (save-block-if-changed! block content - {:custom-properties properties' - :rebuild-content? false})))))))) + (save-block-if-changed! block content + {:custom-properties {key value} + :rebuild-content? false}))))))) (defn set-block-timestamp! [block-id key value] @@ -1120,11 +1134,12 @@ (save-block-if-changed! block new-content))))))) (defn copy-block-ref! - [block-id] - (let [block (db/entity [:block/uuid block-id])] - (when-not (:block/pre-block? block) - (set-block-property! block-id "id" (str block-id)))) - (util/copy-to-clipboard! (str block-id))) + ([block-id] (copy-block-ref! block-id #(str %))) + ([block-id tap-clipboard] + (let [block (db/entity [:block/uuid block-id])] + (when-not (:block/pre-block? block) + (set-block-property! block-id "id" (str block-id)))) + (util/copy-to-clipboard! (tap-clipboard block-id)))) (defn clear-selection! [_e] diff --git a/src/main/frontend/handler/graph.cljs b/src/main/frontend/handler/graph.cljs index 0dc4a8f5e8..1acae0beef 100644 --- a/src/main/frontend/handler/graph.cljs +++ b/src/main/frontend/handler/graph.cljs @@ -21,21 +21,26 @@ edges))) (defn- build-nodes - [dark? current-page edges nodes] - (mapv (fn [p] - (let [current-page? (= p current-page) - color (case [dark? current-page?] - [false false] "#222222" - [false true] "#045591" - [true false] "#8abbbb" - [true true] "#ffffff")] ; FIXME: Put it into CSS - {:id p - :name p - :val (get-connections p edges) - :autoColorBy "group" - :group (js/Math.ceil (* (js/Math.random) 12)) - :color color})) - (set (flatten nodes)))) + [dark? current-page edges tags nodes] + (let [pages (->> (set (flatten nodes)) + (remove nil?))] + (mapv (fn [p] + (let [current-page? (= p current-page) + color (case [dark? current-page?] ; FIXME: Put it into CSS + [false false] "#222222" + [false true] "#045591" + [true false] "#8abbbb" + [true true] "#ffffff") + color (if (contains? tags (string/lower-case p)) + (if dark? "orange" "green") + color)] + {:id p + :name p + :val (get-connections p edges) + :autoColorBy "group" + :group (js/Math.ceil (* (js/Math.random) 12)) + :color color})) + pages))) (defn- normalize-page-name [{:keys [nodes links] :as g}] @@ -62,6 +67,7 @@ (when-let [repo (state/get-current-repo)] (let [relation (db/get-pages-relation repo show-journal?) tagged-pages (db/get-all-tagged-pages repo) + tags (set (map second tagged-pages)) linked-pages (-> (concat relation tagged-pages) @@ -86,7 +92,7 @@ (fn [[_ to]] (nil? to)) nodes)) - nodes (build-nodes dark? current-page edges nodes)] + nodes (build-nodes dark? current-page edges tags nodes)] (normalize-page-name {:nodes nodes :links edges}))))) @@ -138,7 +144,7 @@ tags) (remove nil?) (distinct) - (build-nodes dark? page edges))] + (build-nodes dark? page edges (set tags)))] (normalize-page-name {:nodes nodes :links edges}))))) @@ -172,6 +178,7 @@ (map first ref-blocks)) (remove nil?) (distinct) - (build-nodes dark? block edges))] + ;; FIXME: get block tags + (build-nodes dark? block edges #{}))] {:nodes nodes :links edges})))) diff --git a/src/main/frontend/handler/user.cljs b/src/main/frontend/handler/user.cljs index fe23bc0e14..b3ff4df9d5 100644 --- a/src/main/frontend/handler/user.cljs +++ b/src/main/frontend/handler/user.cljs @@ -7,7 +7,8 @@ [frontend.storage :as storage] [promesa.core :as p] [goog.object :as gobj] - [frontend.handler.notification :as notification]) + [frontend.handler.notification :as notification] + [frontend.handler.config :as config-handler]) (:import [goog.format EmailAddress])) (defn email? [v] @@ -40,13 +41,15 @@ (defn set-preferred-format! [format] (when format + (config-handler/set-config! :preferred_format format) (state/set-preferred-format! format) (when (:name (:me @state/state)) - (util/post (str config/api "set_preferred_format") - {:preferred_format (name format)} - (fn [_result] - (notification/show! "Format set successfully!" :success)) - (fn [_e]))))) + (when (state/logged?) + (util/post (str config/api "set_preferred_format") + {:preferred_format (name format)} + (fn [_result] + (notification/show! "Format set successfully!" :success)) + (fn [_e])))))) (defn set-preferred-workflow! [workflow] @@ -63,9 +66,9 @@ [_e] (when (js/confirm "Your local notes will be completely removed after signing out. Continue?") (-> - (idb/clear-local-storage-and-idb!) - (p/catch (fn [e] + (idb/clear-local-storage-and-idb!) + (p/catch (fn [e] (println "sign out error: ") (js/console.dir e))) - (p/finally (fn [] + (p/finally (fn [] (set! (.-href js/window.location) "/logout")))))) diff --git a/src/main/frontend/search.cljs b/src/main/frontend/search.cljs index d3e4dc2e6c..6fd66e7bf5 100644 --- a/src/main/frontend/search.cljs +++ b/src/main/frontend/search.cljs @@ -154,9 +154,10 @@ (bean/->clj))] (->> (map - (fn [{:keys [target uuid]}] + (fn [{:keys [target uuid indexes]}] {:block/uuid uuid - :block/content target}) + :block/content target + :block/indexes indexes}) ; For result highlight result) (remove nil?))))))))) @@ -228,8 +229,9 @@ (fn [pages] (let [pages (or pages (array)) pages (.filter pages (fn [page] - (not (contains? pages-to-remove-set - (string/lower-case (gobj/get page "name"))))))] + (when-let [page-name (gobj/get page "name")] + (not (contains? pages-to-remove-set + (string/lower-case page-name))))))] (.concat pages (bean/->js pages-to-add))))))) (when (seq blocks) (let [blocks-result (db/pull-many '[:db/id :block/uuid :block/format :block/content] (set (map :e blocks))) diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 1525670b71..38b407539f 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -177,6 +177,11 @@ (not (false? (:feature/enable-timetracking? (get (sub-config) (get-current-repo)))))) +(defn enable-block-time? + [] + (true? (:feature/enable-block-time? + (get (sub-config) (get-current-repo))))) + ;; Enable by default (defn show-brackets? [] diff --git a/src/main/frontend/text.cljs b/src/main/frontend/text.cljs index a23cc28a32..64347854d6 100644 --- a/src/main/frontend/text.cljs +++ b/src/main/frontend/text.cljs @@ -2,8 +2,47 @@ (:require [frontend.config :as config] [frontend.util :as util] [clojure.string :as string] - [clojure.set :as set] - [frontend.db :as db])) + [clojure.set :as set])) + +(defn page-ref? + [s] + (and + (string? s) + (string/starts-with? s "[[") + (string/ends-with? s "]]"))) + +(defonce page-ref-re #"\[\[(.*?)\]\]") + +(defonce between-re #"\(between ([^\)]+)\)") + +(defn page-ref-un-brackets! + [s] + (when (string? s) + (let [s (if (page-ref? s) + (subs s 2 (- (count s) 2)) + s)] + (string/lower-case s)))) + +;; E.g "Foo Bar \"Bar Baz\"" +(defn- sep-by-comma-or-quote + [s] + (when s + (some->> + (string/split s #"[\"|\,|,]{1}") + (remove string/blank?) + (map string/lower-case) + (map string/trim)))) + +(defn split-page-refs-without-brackets + [s] + (if (and (string? s) + (or (re-find #"[\"|\,|,]+" s) + (re-find page-ref-re s))) + (->> s + (sep-by-comma-or-quote) + (map page-ref-un-brackets!) + (set)) + s)) (defn remove-level-spaces ([text format] @@ -33,7 +72,8 @@ (def hidden-properties (set/union - #{"id" "custom_id" "heading" "background_color"} + #{"id" "custom_id" "heading" "background_color" + "created_at" "last_modified_at"} config/markers)) (defn properties-hidden? @@ -112,23 +152,24 @@ (when (and start end) (subs text start (+ end 5))))) +(defn extract-properties + [text] + (when-let [properties-text (get-properties-text text)] + (->> (string/split-lines properties-text) + (map (fn [line] + (when (= ":" (first line)) + (let [[k v] (util/split-first ":" (subs line 1))] + (when (and k v) + (let [k (string/trim (string/lower-case k)) + v (string/trim (string/lower-case v))] + (when-not (contains? #{"properties" "end"} k) + [k v]))))))) + (into {})))) + (defn re-construct-block-properties - [block content properties] - (let [content' (-> (remove-level-spaces content (:block/format block)) - (string/trim) - (string/lower-case)) - properties-text (get-properties-text content)] - (if (or - (and - properties-text - (string/starts-with? content' (string/lower-case properties-text))) - (and (contains-properties? content) - ;; not changed - (= (seq (:block/properties (db/entity [:block/uuid (:block/uuid block)]))) - (seq properties)))) - content - (-> (remove-properties! content) - (rejoin-properties properties))))) + [content properties] + (-> (remove-properties! content) + (rejoin-properties properties))) (defn insert-property [content key value] diff --git a/src/test/frontend/db/config.cljs b/src/test/frontend/db/config.cljs new file mode 100644 index 0000000000..7227e2e868 --- /dev/null +++ b/src/test/frontend/db/config.cljs @@ -0,0 +1,12 @@ +(ns frontend.db.config + (:require [frontend.db.conn :as conn])) + +(defonce test-db "test-db") + +(defn start-test-db! + [] + (conn/start! nil test-db)) + +(defn destroy-test-db! + [] + (conn/destroy-all!)) diff --git a/src/test/frontend/db/model_test.cljs b/src/test/frontend/db/model_test.cljs index d5afdedd98..fb55540d43 100644 --- a/src/test/frontend/db/model_test.cljs +++ b/src/test/frontend/db/model_test.cljs @@ -1,84 +1,74 @@ (ns frontend.db.model-test (:require [frontend.db.model :as model] - [frontend.db.conn :as conn] + [frontend.db.config :refer [test-db] :as config] [datascript.core :as d] [frontend.db-schema :as schema] [frontend.handler.repo :as repo-handler] - [cljs.test :refer [deftest is are testing]])) - -(defonce test-db "test-db") - -(defn- run-db! - [f] - (conn/start! nil test-db) - (f) - (conn/destroy-all!)) + [cljs.test :refer [deftest is are testing use-fixtures]])) (deftest test-page-alias-with-multiple-alias [] - (run-db! - (fn [] - (let [files [{:file/path "a.md" - :file/content "---\ntitle: a\nalias: b, c\n---"} - {:file/path "b.md" - :file/content "---\ntitle: b\nalias: a, d\n---"} - {:file/path "e.md" - :file/content "---\ntitle: e\n---\n## ref to [[b]]"}] - _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) - a-aliases (model/page-alias-set test-db "a") - b-aliases (model/page-alias-set test-db "b") - alias-names (model/get-page-alias-names test-db "a") - b-ref-blocks (model/get-page-referenced-blocks test-db "b") - a-ref-blocks (model/get-page-referenced-blocks test-db "a")] - (are [x y] (= x y) - 4 (count a-aliases) - 4 (count b-aliases) - 1 (count b-ref-blocks) - 1 (count a-ref-blocks) - ["b" "c" "d"] alias-names))))) + (let [files [{:file/path "a.md" + :file/content "---\ntitle: a\nalias: b, c\n---"} + {:file/path "b.md" + :file/content "---\ntitle: b\nalias: a, d\n---"} + {:file/path "e.md" + :file/content "---\ntitle: e\n---\n## ref to [[b]]"}] + _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) + a-aliases (model/page-alias-set test-db "a") + b-aliases (model/page-alias-set test-db "b") + alias-names (model/get-page-alias-names test-db "a") + b-ref-blocks (model/get-page-referenced-blocks test-db "b") + a-ref-blocks (model/get-page-referenced-blocks test-db "a")] + (are [x y] (= x y) + 4 (count a-aliases) + 4 (count b-aliases) + 1 (count b-ref-blocks) + 1 (count a-ref-blocks) + ["b" "c" "d"] alias-names))) (deftest test-page-alias-set [] - (run-db! - (fn [] - (let [files [{:file/path "a.md" - :file/content "---\ntitle: a\nalias: [[b]]\n---"} - {:file/path "b.md" - :file/content "---\ntitle: b\nalias: [[c]]\n---"} - {:file/path "d.md" - :file/content "---\ntitle: d\n---\n## ref to [[b]]"}] - _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) - a-aliases (model/page-alias-set test-db "a") - b-aliases (model/page-alias-set test-db "b") - alias-names (model/get-page-alias-names test-db "a") - b-ref-blocks (model/get-page-referenced-blocks test-db "b") - a-ref-blocks (model/get-page-referenced-blocks test-db "a")] - (are [x y] (= x y) - 3 (count a-aliases) - 1 (count b-ref-blocks) - 1 (count a-ref-blocks) - ["b" "c"] alias-names))))) + (let [files [{:file/path "a.md" + :file/content "---\ntitle: a\nalias: [[b]]\n---"} + {:file/path "b.md" + :file/content "---\ntitle: b\nalias: [[c]]\n---"} + {:file/path "d.md" + :file/content "---\ntitle: d\n---\n## ref to [[b]]"}] + _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) + a-aliases (model/page-alias-set test-db "a") + b-aliases (model/page-alias-set test-db "b") + alias-names (model/get-page-alias-names test-db "a") + b-ref-blocks (model/get-page-referenced-blocks test-db "b") + a-ref-blocks (model/get-page-referenced-blocks test-db "a")] + (are [x y] (= x y) + 3 (count a-aliases) + 1 (count b-ref-blocks) + 1 (count a-ref-blocks) + ["b" "c"] alias-names))) (deftest test-page-alias-without-brackets [] - (run-db! - (fn [] - (let [files [{:file/path "a.md" - :file/content "---\ntitle: a\nalias: b\n---"} - {:file/path "b.md" - :file/content "---\ntitle: b\nalias: c\n---"} - {:file/path "d.md" - :file/content "---\ntitle: d\n---\n## ref to [[b]]"}] - _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) - a-aliases (model/page-alias-set test-db "a") - b-aliases (model/page-alias-set test-db "b") - alias-names (model/get-page-alias-names test-db "a") - b-ref-blocks (model/get-page-referenced-blocks test-db "b") - a-ref-blocks (model/get-page-referenced-blocks test-db "a")] - (are [x y] (= x y) - 3 (count a-aliases) - 1 (count b-ref-blocks) - 1 (count a-ref-blocks) - ["b" "c"] alias-names))))) + (let [files [{:file/path "a.md" + :file/content "---\ntitle: a\nalias: b\n---"} + {:file/path "b.md" + :file/content "---\ntitle: b\nalias: c\n---"} + {:file/path "d.md" + :file/content "---\ntitle: d\n---\n## ref to [[b]]"}] + _ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}) + a-aliases (model/page-alias-set test-db "a") + b-aliases (model/page-alias-set test-db "b") + alias-names (model/get-page-alias-names test-db "a") + b-ref-blocks (model/get-page-referenced-blocks test-db "b") + a-ref-blocks (model/get-page-referenced-blocks test-db "a")] + (are [x y] (= x y) + 3 (count a-aliases) + 1 (count b-ref-blocks) + 1 (count a-ref-blocks) + ["b" "c"] alias-names))) + +(use-fixtures :each + {:before config/start-test-db! + :after config/destroy-test-db!}) #_(cljs.test/test-ns 'frontend.db.model-test) diff --git a/src/test/frontend/db/query_dsl_test.cljs b/src/test/frontend/db/query_dsl_test.cljs new file mode 100644 index 0000000000..5157320be5 --- /dev/null +++ b/src/test/frontend/db/query_dsl_test.cljs @@ -0,0 +1,502 @@ +(ns frontend.db.query-dsl-test + (:require [frontend.db.query-dsl :as dsl] + [frontend.db :as db] + [frontend.db.conn :as conn] + [frontend.db.config :refer [test-db] :as config] + [datascript.core :as d] + [frontend.db-schema :as schema] + [frontend.handler.repo :as repo-handler] + [cljs.test :refer [deftest is are testing use-fixtures]])) + +;; TODO: quickcheck +;; 1. generate query filters +;; 2. find illegal queries which can't be executed by datascript +;; 3. find filters combinations which might break the current query implementation + +(defn import-test-data! + [] + (let [files [{:file/path "journals/2020_12_26.md" + :file/content "--- +title: Dec 26th, 2020 +tags: [[page-tag-1]], page-tag-2 +parent: [[child page 1]] +--- +## DONE 26-b1 [[page 1]] +:PROPERTIES: +:created_at: 1608968448113 +:last_modified_at: 1608968448113 +:prop_a: val_a +:prop_c: [[page a]], [[page b]], [[page c]] +:END: +## LATER 26-b2-modified-later [[page 2]] #tag1 +:PROPERTIES: +:created_at: 1608968448114 +:last_modified_at: 1608968448120 +:prop_b: val_b +:END: +## DONE [#A] 26-b3 [[page 1]] +:PROPERTIES: +:created_at: 1608968448115 +:last_modified_at: 1608968448115 +:END: +"} + {:file/path "journals/2020_12_27.md" + :file/content "--- +title: Dec 27th, 2020 +tags: page-tag-2, [[page-tag-3]] +parent: [[child page 1]], child page 2 +--- +## NOW [#A] b1 [[page 1]] +:PROPERTIES: +:created_at: 1609052958714 +:last_modified_at: 1609052958714 +:END: +## LATER [#B] b2-modified-later [[page 2]] +:PROPERTIES: +:created_at: 1609052959376 +:last_modified_at: 1609052974285 +:END: +## b3 [[page 1]] +:PROPERTIES: +:created_at: 1609052959954 +:last_modified_at: 1609052959954 +:prop_a: val_a +:END: +## b4 [[page 2]] +:PROPERTIES: +:created_at: 1609052961569 +:last_modified_at: 1609052961569 +:END: +## b5 +:PROPERTIES: +:created_at: 1609052963089 +:last_modified_at: 1609052963089 +:END:"} + {:file/path "journals/2020_12_28.md" + :file/content "--- +title: Dec 28th, 2020 +parent: child page 2 +--- +## 28-b1 [[page 1]] +:PROPERTIES: +:created_at: 1609084800000 +:last_modified_at: 1609084800000 +:END: +## 28-b2-modified-later [[page 2]] +:PROPERTIES: +:created_at: 1609084800001 +:last_modified_at: 1609084800020 +:END: +## 28-b3 [[page 1]] +:PROPERTIES: +:created_at: 1609084800002 +:last_modified_at: 1609084800002 +:END: +"}]] + (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false}))) + +(def parse (partial dsl/parse test-db)) + +(defn- q + [s] + (db/clear-query-state!) + (let [parse-result (parse s)] + {:query (:query parse-result) + :result (dsl/query test-db s)})) + +(defn q-count + [s] + (let [{:keys [query result]} (q s)] + {:query query + :count (if result + (count @result) + 0)})) + +(defn count-only + [s] + (:count (q-count s))) + +(defonce empty-result {:query nil :result nil}) + +(deftest test-parse + [] + (testing "nil or blank strings should be ignored" + (are [x y] (= (q x) y) + nil empty-result + "" empty-result + " " empty-result)) + + (testing "Non exists page should be ignored" + (are [x y] (= (q x) y) + "[[page-not-exist]]" empty-result + "[[another-page-not-exist]]" empty-result)) + + (testing "Single page query" + (are [x y] (= (q-count x) y) + "[[page 1]]" + {:query '[[?b :block/ref-pages [:page/name "page 1"]]] + :count 6} + + "[[page 2]]" + {:query '[[?b :block/ref-pages [:page/name "page 2"]]] + :count 4})) + + (testing "Block properties query" + (are [x y] (= (q-count x) y) + "(property prop_a val_a)" + {:query '[[?b :block/properties ?p] + [(get ?p "prop_a") ?v] + (or + [(= ?v "val_a")] + [(contains? ?v "val_a")])] + :count 2} + + "(property prop_b val_b)" + {:query '[[?b :block/properties ?p] + [(get ?p "prop_b") ?v] + (or + [(= ?v "val_b")] + [(contains? ?v "val_b")])] + :count 1} + + "(and (property prop_b val_b))" + {:query '[[?b :block/properties ?p] + [(get ?p "prop_b") ?v] + (or + [(= ?v "val_b")] + [(contains? ?v "val_b")])] + :count 1} + + "(and (property prop_c \"page c\"))" + {:query '[[?b :block/properties ?p] + [(get ?p "prop_c") ?v] + (or + [(= ?v "page c")] + [(contains? ?v "page c")])] + :count 1} + + ;; TODO: optimize + "(and (property prop_c \"page c\") (property prop_c \"page b\"))" + {:query '([?b :block/properties ?p] + [(get ?p "prop_c") ?v] + (or [(= ?v "page c")] [(contains? ?v "page c")]) + (or [(= ?v "page b")] [(contains? ?v "page b")])) + :count 1} + + "(or (property prop_c \"page c\") (property prop_b val_b))" + {:query '(or + (and [?b :block/properties ?p] + [(get ?p "prop_c") ?v] + (or [(= ?v "page c")] [(contains? ?v "page c")])) + (and [?b :block/properties ?p] + [(get ?p "prop_b") ?v] + (or [(= ?v "val_b")] [(contains? ?v "val_b")]))) + :count 2})) + + (testing "TODO queries" + (are [x y] (= (q-count x) y) + "(todo now)" + {:query '[[?b :block/marker ?marker] + [(contains? #{"NOW"} ?marker)]] + :count 1} + + "(todo NOW)" + {:query '[[?b :block/marker ?marker] + [(contains? #{"NOW"} ?marker)]] + :count 1} + + "(todo later)" + {:query '[[?b :block/marker ?marker] + [(contains? #{"LATER"} ?marker)]] + :count 2} + + "(todo now later)" + {:query '[[?b :block/marker ?marker] + [(contains? #{"NOW" "LATER"} ?marker)]] + :count 3} + + "(todo [now later])" + {:query '[[?b :block/marker ?marker] + [(contains? #{"NOW" "LATER"} ?marker)]] + :count 3})) + + (testing "Priority queries" + (are [x y] (= (q-count x) y) + "(priority A)" + {:query '[[?b :block/priority ?priority] + [(contains? #{"A"} ?priority)]] + :count 2} + + "(priority a)" + {:query '[[?b :block/priority ?priority] + [(contains? #{"A"} ?priority)]] + :count 2} + + "(priority a b)" + {:query '[[?b :block/priority ?priority] + [(contains? #{"A" "B"} ?priority)]] + :count 3} + + "(priority [a b])" + {:query '[[?b :block/priority ?priority] + [(contains? #{"A" "B"} ?priority)]] + :count 3} + + "(priority a b c)" + {:query '[[?b :block/priority ?priority] + [(contains? #{"A" "B" "C"} ?priority)]] + :count 3})) + + (testing "all-page-tags queries" + (are [x y] (= (q-count x) y) + "(all-page-tags)" + {:query '[[?t :tag/name ?tag] + [?p :page/name ?tag]] + :count 3})) + + (testing "page-tags queries" + (are [x y] (= (q-count x) y) + "(page-tags [[page-tag-1]])" + {:query '[[?p :page/tags ?t] + [?t :tag/name ?tag] + [(contains? #{"page-tag-1"} ?tag)]] + :count 1} + + "(page-tags page-tag-2)" + {:query '[[?p :page/tags ?t] + [?t :tag/name ?tag] + [(contains? #{"page-tag-2"} ?tag)]] + :count 2} + + "(page-tags page-tag-1 page-tag-2)" + {:query '[[?p :page/tags ?t] + [?t :tag/name ?tag] + [(contains? #{"page-tag-1" "page-tag-2"} ?tag)]] + :count 2} + + "(page-tags page-TAG-1 page-tag-2)" + {:query '[[?p :page/tags ?t] + [?t :tag/name ?tag] + [(contains? #{"page-tag-1" "page-tag-2"} ?tag)]] + :count 2} + + "(page-tags [page-tag-1 page-tag-2])" + {:query '[[?p :page/tags ?t] + [?t :tag/name ?tag] + [(contains? #{"page-tag-1" "page-tag-2"} ?tag)]] + :count 2})) + + (testing "page-property queries" + (are [x y] (= (q-count x) y) + "(page-property parent [[child page 1]])" + {:query '[[?p :page/properties ?prop] + [(get ?prop :parent) ?v] + (or + [(= ?v "child page 1")] + [(contains? ?v "child page 1")])] + :count 2} + + "(page-property parent \"child page 1\")" + {:query '[[?p :page/properties ?prop] + [(get ?prop :parent) ?v] + (or + [(= ?v "child page 1")] + [(contains? ?v "child page 1")])] + :count 2} + + "(and (page-property parent [[child page 1]]) (page-property parent [[child page 2]]))" + {:query '([?p :page/properties ?prop] + [(get ?prop :parent) ?v] + (or [(= ?v "child page 1")] [(contains? ?v "child page 1")]) + (or [(= ?v "child page 2")] [(contains? ?v "child page 2")])) + :count 1} + + "(or (page-property parent [[child page 1]]) (page-property parent [[child page 2]]))" + {:query '(or (and + [?p :page/properties ?prop] + [(get ?prop :parent) ?v] + (or [(= ?v "child page 1")] [(contains? ?v "child page 1")])) + (and + [?p :page/properties ?prop] + [(get ?prop :parent) ?v] + (or [(= ?v "child page 2")] [(contains? ?v "child page 2")]))) + :count 3})) + + ;; boolean queries + (testing "AND queries" + (are [x y] (= (q-count x) y) + "(and [[tag1]] [[page 2]])" + {:query '([?b :block/ref-pages [:page/name "tag1"]] + [?b :block/ref-pages [:page/name "page 2"]]) + :count 1}) + + (are [x y] (= (q-count x) y) + "(and [[tag1]] [[page 2]])" + {:query '([?b :block/ref-pages [:page/name "tag1"]] + [?b :block/ref-pages [:page/name "page 2"]]) + :count 1})) + + (testing "OR queries" + (are [x y] (= (q-count x) y) + "(or [[tag1]] [[page 2]])" + {:query '(or + (and [?b :block/ref-pages [:page/name "tag1"]]) + (and [?b :block/ref-pages [:page/name "page 2"]])) + :count 4})) + + (testing "NOT queries" + (are [x y] (= (q-count x) y) + "(not [[page 1]])" + {:query '([?b :block/uuid] + (not + [?b :block/ref-pages [:page/name "page 1"]])) + :count 8})) + + (testing "Between query" + (are [x y] (= (count-only x) y) + "(and (todo now later done) (between [[Dec 26th, 2020]] tomorrow))" + 5 + + ;; between with journal pages + "(and (todo now later done) (between [[Dec 27th, 2020]] [[Dec 28th, 2020]]))" + 2 + + ;; between with created_at + "(and (todo now later done) (between created_at [[Dec 26th, 2020]] tomorrow))" + 5 + + ;; between with last_modified_at + "(and (todo now later done) (between last_modified_at [[Dec 26th, 2020]] tomorrow))" + 5)) + + (testing "Nested boolean queries" + (are [x y] (= (q-count x) y) + "(and (todo done) (not [[page 1]]))" + {:query '([?b :block/marker ?marker] + [(contains? #{"DONE"} ?marker)] + (not [?b :block/ref-pages [:page/name "page 1"]])) + :count 0}) + + (are [x y] (= (q-count x) y) + "(and (todo now later) (or [[page 1]] [[page 2]]))" + {:query '([?b :block/marker ?marker] + [(contains? #{"NOW" "LATER"} ?marker)] + (or (and [?b :block/ref-pages [:page/name "page 1"]]) + (and [?b :block/ref-pages [:page/name "page 2"]]))) + :count 3}) + + (are [x y] (= (q-count x) y) + "(not (and (todo now later) (or [[page 1]] [[page 2]])))" + {:query '([?b :block/uuid] + (not + [?b :block/marker ?marker] + [(contains? #{"NOW" "LATER"} ?marker)] + (or + (and [?b :block/ref-pages [:page/name "page 1"]]) + (and [?b :block/ref-pages [:page/name "page 2"]])))) + :count 11}) + + ;; FIXME: not working + ;; (are [x y] (= (q-count x) y) + ;; "(or (priority a) (not (priority a)))" + ;; {:query '(or + ;; (and [?b :block/priority ?priority] [(contains? #{"A"} ?priority)]) + ;; (and (not [?b :block/priority ?priority] + ;; [(contains? #{"A"} ?priority)]))) + ;; :count 5}) + + (are [x y] (= (q-count x) y) + "(and (todo now later done) (or [[page 1]] (not [[page 1]])))" + {:query '([?b :block/marker ?marker] + [(contains? #{"NOW" "LATER" "DONE"} ?marker)] + (or + (and [?b :block/ref-pages [:page/name "page 1"]]) + (and (not [?b :block/ref-pages [:page/name "page 1"]])))) + :count 5})) + + (testing "sort-by (created_at defaults to desc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by created_at))") + :result + deref + (map #(get-in % [:block/properties "created_at"])))] + (is (= result + '(1609052959376 1609052958714 1608968448115 1608968448114 1608968448113))))) + + (testing "sort-by (created_at desc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by created_at desc))") + :result + deref + (map #(get-in % [:block/properties "created_at"])))] + (is (= result + '(1609052959376 1609052958714 1608968448115 1608968448114 1608968448113))))) + + (testing "sort-by (created_at asc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by created_at asc))") + :result + deref + (map #(get-in % [:block/properties "created_at"])))] + (is (= result + '(1608968448113 1608968448114 1608968448115 1609052958714 1609052959376))))) + + (testing "sort-by (last_modified_at defaults to desc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by last_modified_at))") + :result + deref + (map #(get-in % [:block/properties "last_modified_at"])))] + (is (= result + '(1609052974285 1609052958714 1608968448120 1608968448115 1608968448113))))) + + (testing "sort-by (last_modified_at desc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by last_modified_at desc))") + :result + deref + (map #(get-in % [:block/properties "last_modified_at"])))] + (is (= result + '(1609052974285 1609052958714 1608968448120 1608968448115 1608968448113))))) + + (testing "sort-by (last_modified_at desc)" + (db/clear-query-state!) + (let [result (->> (q "(and (todo now later done) + (sort-by last_modified_at asc))") + :result + deref + (map #(get-in % [:block/properties "last_modified_at"])))] + (is (= result + '(1608968448113 1608968448115 1608968448120 1609052958714 1609052974285)))))) + +(use-fixtures :once + {:before (fn [] + (config/start-test-db!) + (import-test-data!)) + :after config/destroy-test-db!}) + +#_(cljs.test/test-ns 'frontend.db.query-dsl-test) + +(comment + (require '[clojure.pprint :as pprint]) + (config/start-test-db!) + (import-test-data!) + + (dsl/query test-db "(all-page-tags)") + + ;; (or (priority a) (not (priority a))) + ;; FIXME: Error: Insufficient bindings: #{?priority} not bound in [(contains? #{"A"} ?priority)] + (pprint/pprint + (d/q + '[:find (pull ?b [*]) + :where + [?b :block/uuid] + (or (and [?b :block/priority ?priority] [(contains? #{"A"} ?priority)]) + (not [?b :block/priority #{"A"}] + [(contains? #{"A"} ?priority)]))] + (frontend.db/get-conn test-db))))