Browse Source

Merge branch 'master' into feat/google-login

Tienson Qin 4 years ago
parent
commit
7dcc79ddf9
35 changed files with 1554 additions and 516 deletions
  1. 3 2
      resources/css/common.css
  2. 10 2
      src/main/api.cljs
  3. 7 6
      src/main/frontend/commands.cljs
  4. 92 55
      src/main/frontend/components/block.cljs
  5. 9 1
      src/main/frontend/components/block.css
  6. 3 3
      src/main/frontend/components/content.cljs
  7. 7 0
      src/main/frontend/components/content.css
  8. 2 1
      src/main/frontend/components/diff.cljs
  9. 1 1
      src/main/frontend/components/editor.cljs
  10. 2 1
      src/main/frontend/components/page.cljs
  11. 10 1
      src/main/frontend/components/right_sidebar.css
  12. 43 2
      src/main/frontend/components/search.cljs
  13. 109 97
      src/main/frontend/components/settings.cljs
  14. 1 1
      src/main/frontend/components/sidebar.css
  15. 7 0
      src/main/frontend/components/theme.css
  16. 27 27
      src/main/frontend/db/query_custom.cljs
  17. 390 0
      src/main/frontend/db/query_dsl.cljs
  18. 15 14
      src/main/frontend/db/react.cljs
  19. 1 13
      src/main/frontend/db_schema.cljs
  20. 2 0
      src/main/frontend/dicts.cljs
  21. 37 37
      src/main/frontend/extensions/code.cljs
  22. 1 1
      src/main/frontend/external/roam.cljs
  23. 11 3
      src/main/frontend/format/block.cljs
  24. 9 43
      src/main/frontend/format/mldoc.cljs
  25. 8 23
      src/main/frontend/graph.cljs
  26. 5 17
      src/main/frontend/handler.cljs
  27. 62 47
      src/main/frontend/handler/editor.cljs
  28. 25 18
      src/main/frontend/handler/graph.cljs
  29. 12 9
      src/main/frontend/handler/user.cljs
  30. 6 4
      src/main/frontend/search.cljs
  31. 5 0
      src/main/frontend/state.cljs
  32. 60 19
      src/main/frontend/text.cljs
  33. 12 0
      src/test/frontend/db/config.cljs
  34. 58 68
      src/test/frontend/db/model_test.cljs
  35. 502 0
      src/test/frontend/db/query_dsl_test.cljs

+ 3 - 2
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);
 }

+ 10 - 2
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)]

+ 7 - 6
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:

+ 92 - 55
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)

+ 9 - 1
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);

+ 3 - 3
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)

+ 7 - 0
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);
 }

+ 2 - 1
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

+ 1 - 1
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]

+ 2 - 1
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]
 

+ 10 - 1
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);
+    }
+  }
 }

+ 43 - 2
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))})])))
 

+ 109 - 97
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])
-
-        (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)
+        [:hr]
+
+        [: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")])]]]]
-
-           [: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]
-
-           (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]
-
-           [: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)])]])))
+                                              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_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))))]]]
+
+         [:hr]
+
+         (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]])
+
+         [: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)]]])))

+ 1 - 1
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;
 

+ 7 - 0
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) {

+ 27 - 27
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))))

+ 390 - 0
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]])"))

+ 15 - 14
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
   []

+ 1 - 13
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}})

+ 2 - 0
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 "开发者模式"

+ 37 - 37
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]

+ 1 - 1
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!

+ 11 - 3
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

+ 9 - 43
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

+ 8 - 23
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)))

+ 5 - 17
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?)

+ 62 - 47
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]

+ 25 - 18
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}))))

+ 12 - 9
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"))))))

+ 6 - 4
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)))

+ 5 - 0
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?
   []

+ 60 - 19
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]

+ 12 - 0
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!))

+ 58 - 68
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)

+ 502 - 0
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))))