浏览代码

Merge branch 'master' into feat/tweet-shape

Gabriel Horner 2 年之前
父节点
当前提交
f9604e1b07

+ 5 - 2
bb.edn

@@ -63,8 +63,11 @@
   dev:validate-plugins-edn
   dev:validate-plugins-edn
   logseq.tasks.malli/validate-plugins-edn
   logseq.tasks.malli/validate-plugins-edn
 
 
-  dev:validate-config-edn
-  logseq.tasks.malli/validate-config-edn
+  dev:validate-repo-config-edn
+  logseq.tasks.malli/validate-repo-config-edn
+
+  dev:validate-global-config-edn
+  logseq.tasks.malli/validate-global-config-edn
 
 
   dev:lint
   dev:lint
   logseq.tasks.dev/lint
   logseq.tasks.dev/lint

+ 25 - 8
e2e-tests/fixtures.ts

@@ -55,6 +55,7 @@ base.beforeAll(async () => {
   })
   })
   context = electronApp.context()
   context = electronApp.context()
   await context.tracing.start({ screenshots: true, snapshots: true });
   await context.tracing.start({ screenshots: true, snapshots: true });
+  await context.tracing.startChunk();
 
 
   // NOTE: The following ensures App first start with the correct path.
   // NOTE: The following ensures App first start with the correct path.
   const info = await electronApp.evaluate(async ({ app }) => {
   const info = await electronApp.evaluate(async ({ app }) => {
@@ -133,14 +134,6 @@ base.beforeEach(async () => {
   }
   }
 })
 })
 
 
-base.afterAll(async () => {
-  // if (electronApp) {
-  //  await electronApp.close()
-  //}
-  // use .dump as extension to avoid unfolded when zip by github
-  await context.tracing.stop({ path: `e2e-dump/trace-${Date.now()}.zip.dump` });
-})
-
 // hijack electron app into the test context
 // hijack electron app into the test context
 // FIXME: add type to `block`
 // FIXME: add type to `block`
 export const test = base.extend<LogseqFixtures>({
 export const test = base.extend<LogseqFixtures>({
@@ -283,3 +276,27 @@ export const test = base.extend<LogseqFixtures>({
     await use(graphDir);
     await use(graphDir);
   },
   },
 });
 });
+
+
+let getTracingFilePath = function(): string {
+  return `e2e-dump/trace-${Date.now()}.zip.dump`
+}
+
+
+test.afterAll(async () => {
+  await context.tracing.stopChunk({ path: getTracingFilePath() });
+})
+
+
+/**
+ * Trace all tests in a file
+ */
+export let traceAll = function(){
+  test.beforeAll(async () => {
+    await context.tracing.startChunk();
+  })
+  
+  test.afterAll(async () => {
+    await context.tracing.stopChunk({ path: getTracingFilePath() });
+  })
+}

+ 15 - 5
scripts/src/logseq/tasks/malli.clj

@@ -4,6 +4,7 @@
             [malli.error :as me]
             [malli.error :as me]
             [frontend.schema.handler.plugin-config :as plugin-config-schema]
             [frontend.schema.handler.plugin-config :as plugin-config-schema]
             [frontend.schema.handler.global-config :as global-config-schema]
             [frontend.schema.handler.global-config :as global-config-schema]
+            [frontend.schema.handler.repo-config :as repo-config-schema]
             [clojure.pprint :as pprint]
             [clojure.pprint :as pprint]
             [clojure.edn :as edn]))
             [clojure.edn :as edn]))
 
 
@@ -20,16 +21,25 @@
       (pprint/pprint errors))
       (pprint/pprint errors))
     (println "Valid!")))
     (println "Valid!")))
 
 
-;; This fn should be split if the global and repo definitions diverge
-(defn validate-config-edn
-  "Validate a global or repo config.edn file"
-  [file]
+(defn- validate-file-with-schema
+  "Validate a file given its schema"
+  [file schema]
   (if-let [errors (->> file
   (if-let [errors (->> file
                        slurp
                        slurp
                        edn/read-string
                        edn/read-string
-                       (m/explain global-config-schema/Config-edn)
+                       (m/explain schema)
                        me/humanize)]
                        me/humanize)]
     (do
     (do
       (println "Found errors:")
       (println "Found errors:")
       (pprint/pprint errors))
       (pprint/pprint errors))
     (println "Valid!")))
     (println "Valid!")))
+
+(defn validate-repo-config-edn
+  "Validate a repo config.edn"
+  [file]
+  (validate-file-with-schema file global-config-schema/Config-edn))
+
+(defn validate-global-config-edn
+  "Validate a global config.edn"
+  [file]
+  (validate-file-with-schema file repo-config-schema/Config-edn))

+ 2 - 1
src/dev-cljs/gen_malli_kondo_config/core.cljs

@@ -3,7 +3,8 @@
   (:require-macros [gen-malli-kondo-config.collect :refer [collect-schema]])
   (:require-macros [gen-malli-kondo-config.collect :refer [collect-schema]])
   (:require [frontend.util]
   (:require [frontend.util]
             [frontend.util.list]
             [frontend.util.list]
-            [malli.clj-kondo :as mc]))
+            [malli.clj-kondo :as mc]
+            [malli.instrument]))
 
 
 
 
 (defn main [& _args]
 (defn main [& _args]

+ 1 - 1
src/electron/electron/utils.cljs

@@ -202,7 +202,7 @@
      (some #(string/includes? path (str "/" % "/"))
      (some #(string/includes? path (str "/" % "/"))
            ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
            ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
      (some #(string/ends-with? path %)
      (some #(string/ends-with? path %)
-           [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
+           [".DS_Store" "logseq/graphs-txid.edn"])
      ;; hidden directory or file
      ;; hidden directory or file
      (let [relpath (path/relative dir path)]
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)
        (or (re-find #"/\.[^.]+" relpath)

+ 4 - 3
src/main/frontend/commands.cljs

@@ -655,12 +655,13 @@
 (defmethod handle-step :editor/show-date-picker [[_ type]]
 (defmethod handle-step :editor/show-date-picker [[_ type]]
   (if (and
   (if (and
        (contains? #{:scheduled :deadline} type)
        (contains? #{:scheduled :deadline} type)
-       (when-let [value (gobj/get (state/get-input) "value")]
-         (string/blank? value)))
+       (string/blank? (gobj/get (state/get-input) "value")))
     (do
     (do
       (notification/show! [:div "Please add some content first."] :warning)
       (notification/show! [:div "Please add some content first."] :warning)
       (restore-state))
       (restore-state))
-    (state/set-editor-action! :datepicker)))
+    (do
+      (state/set-timestamp-block! nil)
+      (state/set-editor-action! :datepicker))))
 
 
 (defmethod handle-step :editor/click-hidden-file-input [[_ _input-id]]
 (defmethod handle-step :editor/click-hidden-file-input [[_ _input-id]]
   (when-let [input-file (gdom/getElement "upload-file")]
   (when-let [input-file (gdom/getElement "upload-file")]

+ 81 - 69
src/main/frontend/components/block.cljs

@@ -43,6 +43,7 @@
             [frontend.handler.dnd :as dnd]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file-sync :as file-sync]
             [frontend.handler.file-sync :as file-sync]
+            [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.query :as query-handler]
             [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.repeated :as repeated]
@@ -61,7 +62,6 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
             [frontend.util.text :as text-util]
             [frontend.util.text :as text-util]
-            [frontend.handler.notification :as notification]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
@@ -853,8 +853,8 @@
   [name arguments]
   [name arguments]
   (if (and (seq arguments)
   (if (and (seq arguments)
            (not= arguments ["null"]))
            (not= arguments ["null"]))
-    (util/format "{{{%s %s}}}" name (string/join ", " arguments))
-    (util/format "{{{%s}}}" name)))
+    (util/format "{{%s %s}}" name (string/join ", " arguments))
+    (util/format "{{%s}}" name)))
 
 
 (declare block-content)
 (declare block-content)
 (declare block-container)
 (declare block-container)
@@ -948,18 +948,19 @@
 
 
 (defn- render-macro
 (defn- render-macro
   [config name arguments macro-content format]
   [config name arguments macro-content format]
-  (if macro-content
-    (let [ast (->> (mldoc/->edn macro-content (gp-mldoc/default-config format))
-                   (map first))
-          paragraph? (and (= 1 (count ast))
-                          (= "Paragraph" (ffirst ast)))]
-      (if (and (not paragraph?)
-               (mldoc/block-with-title? (ffirst ast)))
-        [:div
-         (markup-elements-cp (assoc config :block/format format) ast)]
-        (inline-text format macro-content)))
-    [:span.warning {:title (str "Unsupported macro name: " name)}
-     (macro->text name arguments)]))
+  [:div.macro {:data-macro-name name}
+   
+   (if macro-content
+     (let [ast (->> (mldoc/->edn macro-content (gp-mldoc/default-config format))
+                    (map first))
+           paragraph? (and (= 1 (count ast))
+                           (= "Paragraph" (ffirst ast)))]
+       (if (and (not paragraph?)
+                (mldoc/block-with-title? (ffirst ast)))
+         (markup-elements-cp (assoc config :block/format format) ast)
+         (inline-text format macro-content)))
+     [:span.warning {:title (str "Unsupported macro name: " name)}
+      (macro->text name arguments)])])
 
 
 (rum/defc nested-link < rum/reactive
 (rum/defc nested-link < rum/reactive
   [config html-export? link]
   [config html-export? link]
@@ -1319,46 +1320,51 @@
 
 
 (defn- macro-video-cp
 (defn- macro-video-cp
   [_config arguments]
   [_config arguments]
-  (when-let [url (first arguments)]
-    (let [results (text-util/get-matched-video url)
-          src (match results
-                     [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
-                     (if (= (count id) 11) ["youtube-player" id] url)
-
-                     [_ _ _ "youtube-nocookie.com" _ id _]
-                     (str "https://www.youtube-nocookie.com/embed/" id)
-
-                     [_ _ _ "loom.com" _ id _]
-                     (str "https://www.loom.com/embed/" id)
-
-                     [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
-                     (str "https://player.vimeo.com/video/" id)
-
-                     [_ _ _ "bilibili.com" _ id & query]
-                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1"
-                          (when-let [page (second query)]
-                            (str "&page=" page)))
-
-                     :else
-                     url)]
-      (if (and (coll? src)
-               (= (first src) "youtube-player"))
-        (youtube/youtube-video (last src))
-        (when src
-          (let [width (min (- (util/get-width) 96) 560)
-                height (int (* width (/ (if (string/includes? src "player.bilibili.com")
-                                          360 315)
-                                        560)))]
-            [:iframe
-             {:allow-full-screen true
-              :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-              :framespacing "0"
-              :frame-border "no"
-              :border "0"
-              :scrolling "no"
-              :src src
-              :width width
-              :height height}]))))))
+  (if-let [url (first arguments)]
+    (if (gp-util/url? url)
+      (let [results (text-util/get-matched-video url)
+            src (match results
+                  [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
+                  (if (= (count id) 11) ["youtube-player" id] url)
+
+                  [_ _ _ "youtube-nocookie.com" _ id _]
+                  (str "https://www.youtube-nocookie.com/embed/" id)
+
+                  [_ _ _ "loom.com" _ id _]
+                  (str "https://www.loom.com/embed/" id)
+
+                  [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
+                  (str "https://player.vimeo.com/video/" id)
+
+                  [_ _ _ "bilibili.com" _ id & query]
+                  (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1"
+                       (when-let [page (second query)]
+                         (str "&page=" page)))
+
+                  :else
+                  url)]
+        (if (and (coll? src)
+                 (= (first src) "youtube-player"))
+          (youtube/youtube-video (last src))
+          (when src
+            (let [width (min (- (util/get-width) 96) 560)
+                  height (int (* width (/ (if (string/includes? src "player.bilibili.com")
+                                            360 315)
+                                          560)))]
+              [:iframe
+               {:allow-full-screen true
+                :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+                :framespacing "0"
+                :frame-border "no"
+                :border "0"
+                :scrolling "no"
+                :src src
+                :width width
+                :height height}]))))
+      [:span.warning.mr-1 {:title "Invalid URL"}
+       (macro->text "video" arguments)])
+    [:span.warning.mr-1 {:title "Empty URL"}
+     (macro->text "video" arguments)]))
 
 
 (defn- macro-else-cp
 (defn- macro-else-cp
   [name config arguments]
   [name config arguments]
@@ -1562,7 +1568,7 @@
 
 
          ["Entity" e]
          ["Entity" e]
          [:span {:dangerouslySetInnerHTML
          [:span {:dangerouslySetInnerHTML
-                 {:__html (:html (security/sanitize-html e))}}]
+                 {:__html (security/sanitize-html (:html e))}}]
 
 
          ["Latex_Fragment" [display s]] ;display can be "Displayed" or "Inline"
          ["Latex_Fragment" [display s]] ;display can be "Displayed" or "Inline"
          (if html-export?
          (if html-export?
@@ -2096,10 +2102,13 @@
         [:button.p-1.mr-2 p])]
         [:button.p-1.mr-2 p])]
      [:code "Property name begins with a non-numeric character and can contain alphanumeric characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character, the second character (if any) must be non-numeric."]]))
      [:code "Property name begins with a non-numeric character and can contain alphanumeric characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character, the second character (if any) must be non-numeric."]]))
 
 
-(rum/defcs timestamp-cp < rum/reactive
+(rum/defcs timestamp-cp
+  < rum/reactive
+  (rum/local false ::show-datepicker?)
   [state block typ ast]
   [state block typ ast]
-  (let [ts-block (state/sub :editor/set-timestamp-block)
-        active? #(= (get block :block/uuid) (get-in ts-block [:block :block/uuid]))]
+  (let [ts-block-id (state/sub [:editor/set-timestamp-block :block :block/uuid])
+        active? (= (get block :block/uuid) ts-block-id)
+        *show-datapicker? (get state ::show-datepicker?)]
     [:div.flex.flex-col.gap-4.timestamp
     [:div.flex.flex-col.gap-4.timestamp
      [:div.text-sm.flex.flex-row
      [:div.text-sm.flex.flex-row
       [:div.opacity-50.font-medium.timestamp-label
       [:div.opacity-50.font-medium.timestamp-label
@@ -2107,19 +2116,23 @@
       [:a.opacity-80.hover:opacity-100
       [:a.opacity-80.hover:opacity-100
        {:on-mouse-down (fn [e]
        {:on-mouse-down (fn [e]
                          (util/stop e)
                          (util/stop e)
-                         (if (active?)
-                          (do
-                            (reset! commands/*current-command nil)
-                            (state/clear-editor-action!)
-                            (state/set-timestamp-block! nil))
+                         (state/clear-editor-action!)
+                         (editor-handler/escape-editing false)
+                         (if active?
+                           (do
+                             (reset! *show-datapicker? false)
+                             (reset! commands/*current-command nil)
+                             (state/set-timestamp-block! nil))
                            (do
                            (do
+                             (reset! *show-datapicker? true)
                              (reset! commands/*current-command typ)
                              (reset! commands/*current-command typ)
-                             (state/set-editor-action! :datepicker)
                              (state/set-timestamp-block! {:block block
                              (state/set-timestamp-block! {:block block
                                                           :typ typ}))))}
                                                           :typ typ}))))}
        [:span.time-start "<"] [:time (repeated/timestamp->text ast)] [:span.time-stop ">"]]]
        [:span.time-start "<"] [:time (repeated/timestamp->text ast)] [:span.time-stop ">"]]]
-     (when (active?)
-          (datetime-comp/date-picker nil nil (repeated/timestamp->map ast)))]))
+     ;; date-picker in rendering-mode
+     (if (and active? @*show-datapicker?)
+       (datetime-comp/date-picker nil nil (repeated/timestamp->map ast))
+       (reset! *show-datapicker? false))]))
 
 
 (defn- target-forbidden-edit?
 (defn- target-forbidden-edit?
   [target]
   [target]
@@ -2970,8 +2983,7 @@
                         :tbody
                         :tbody
                         (mapv #(tr :td %) group)))
                         (mapv #(tr :td %) group)))
                      groups)]
                      groups)]
-    [:div.table-wrapper {:style {:max-width (min 700
-                                                 (gobj/get js/window "innerWidth"))}}
+    [:div.table-wrapper
      (->elem
      (->elem
       :table
       :table
       {:class "table-auto"
       {:class "table-auto"

+ 34 - 27
src/main/frontend/components/datetime.cljs

@@ -45,7 +45,7 @@
   [{:keys [num duration kind]}]
   [{:keys [num duration kind]}]
   (let [show? (rum/react *show-repeater?)]
   (let [show? (rum/react *show-repeater?)]
     (if (or show? (and num duration kind))
     (if (or show? (and num duration kind))
-      [:div.w.full.flex.flex-row.justify-left {:style {:height 32}}
+      [:div.w.full.flex.flex-row.justify-left
        [:input#repeater-num.form-input.mt-1.w-8.px-1.sm:w-20.sm:px-2.text-center
        [:input#repeater-num.form-input.mt-1.w-8.px-1.sm:w-20.sm:px-2.text-center
         {:default-value num
         {:default-value num
          :on-change (fn [event]
          :on-change (fn [event]
@@ -78,7 +78,7 @@
                                        :duration "d"}))}
                                        :duration "d"}))}
        "Add repeater"])))
        "Add repeater"])))
 
 
-(defn clear-timestamp!
+(defn- clear-timestamp!
   []
   []
   (reset! *timestamp default-timestamp-value)
   (reset! *timestamp default-timestamp-value)
   (reset! *show-time? false)
   (reset! *show-time? false)
@@ -86,6 +86,7 @@
   (state/set-state! :date-picker/date nil))
   (state/set-state! :date-picker/date nil))
 
 
 (defn- on-submit
 (defn- on-submit
+  "Submit handler of date picker"
   [e]
   [e]
   (when e (util/stop e))
   (when e (util/stop e))
   (let [{:keys [repeater] :as timestamp} @*timestamp
   (let [{:keys [repeater] :as timestamp} @*timestamp
@@ -96,15 +97,21 @@
         text (repeated/timestamp-map->text timestamp)
         text (repeated/timestamp-map->text timestamp)
         block-data (state/get-timestamp-block)
         block-data (state/get-timestamp-block)
         {:keys [block typ show?]} block-data
         {:keys [block typ show?]} block-data
+        editing-block-id (:block/uuid (state/get-edit-block))
         block-id (or (:block/uuid block)
         block-id (or (:block/uuid block)
-                     (:block/uuid (state/get-edit-block)))
+                     editing-block-id)
         typ (or @commands/*current-command typ)]
         typ (or @commands/*current-command typ)]
-    (editor-handler/set-block-timestamp! block-id
-                                         typ
-                                         text)
+    (if (and (state/editing?) (= editing-block-id block-id))
+      (editor-handler/set-editing-block-timestamp! typ
+                                                   text)
+      (editor-handler/set-block-timestamp! block-id
+                                           typ
+                                           text))
+
     (when show?
     (when show?
       (reset! show? false)))
       (reset! show? false)))
   (clear-timestamp!)
   (clear-timestamp!)
+  (state/set-timestamp-block! nil)
   (commands/restore-state))
   (commands/restore-state))
 
 
 (rum/defc time-repeater < rum/reactive
 (rum/defc time-repeater < rum/reactive
@@ -138,30 +145,30 @@
              (when-not (:date-picker/date @state/state)
              (when-not (:date-picker/date @state/state)
                (state/set-state! :date-picker/date (get ts :date (t/today)))))
                (state/set-state! :date-picker/date (get ts :date (t/today)))))
            state)}
            state)}
-  [id format _ts]
+  [dom-id format _ts]
   (let [current-command @commands/*current-command
   (let [current-command @commands/*current-command
         deadline-or-schedule? (and current-command
         deadline-or-schedule? (and current-command
                                    (contains? #{"deadline" "scheduled"}
                                    (contains? #{"deadline" "scheduled"}
                                               (string/lower-case current-command)))
                                               (string/lower-case current-command)))
         date (state/sub :date-picker/date)]
         date (state/sub :date-picker/date)]
-    (when (= :datepicker (state/sub :editor/action))
-      [:div#date-time-picker.flex.flex-row {:on-click (fn [e] (util/stop e))
-                                            :on-mouse-down (fn [e] (.stopPropagation e))}
-       (ui/datepicker
-        date
-        {:deadline-or-schedule? deadline-or-schedule?
-         :on-change
-         (fn [e date]
-           (util/stop e)
-           (let [date (t/to-default-time-zone date)
-                 journal (date/journal-name date)]
-             (when-not deadline-or-schedule?
+    [:div#date-time-picker.flex.flex-row {:on-click (fn [e] (util/stop e))
+                                          :on-mouse-down (fn [e] (.stopPropagation e))}
+     (ui/datepicker
+      date
+      {:deadline-or-schedule? deadline-or-schedule?
+       :on-change
+       (fn [e date]
+         (util/stop e)
+         (let [date (t/to-default-time-zone date)
+               journal (date/journal-name date)]
+           ;; deadline-or-schedule? is handled in on-sumbit, not here
+           (when-not deadline-or-schedule?
                ;; similar to page reference
                ;; similar to page reference
-               (editor-handler/insert-command! id
-                                               (page-ref/->page-ref journal)
-                                               format
-                                               {:command :page-ref})
-               (state/clear-editor-action!)
-               (reset! commands/*current-command nil))))})
-       (when deadline-or-schedule?
-         (time-repeater))])))
+             (editor-handler/insert-command! dom-id
+                                             (page-ref/->page-ref journal)
+                                             format
+                                             {:command :page-ref})
+             (state/clear-editor-action!)
+             (reset! commands/*current-command nil))))})
+     (when deadline-or-schedule?
+       (time-repeater))]))

+ 3 - 2
src/main/frontend/components/editor.cljs

@@ -381,8 +381,8 @@
                 :z-index    11}
                 :z-index    11}
                (when set-default-width?
                (when set-default-width?
                  {:width max-width})
                  {:width max-width})
-               (let [^js/HTMLElement editor
-                     (js/document.querySelector ".editor-wrapper")]
+               (when-let [^js/HTMLElement editor
+                          (js/document.querySelector ".editor-wrapper")]
                  (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
                  (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
                    {:right 0}
                    {:right 0}
                    {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))]
                    {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))]
@@ -555,6 +555,7 @@
       (= :property-value-search action)
       (= :property-value-search action)
       (animated-modal "property-value-search" (property-value-search id) true)
       (animated-modal "property-value-search" (property-value-search id) true)
 
 
+      ;; date-picker in editing-mode
       (= :datepicker action)
       (= :datepicker action)
       (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false)
       (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false)
 
 

+ 1 - 1
src/main/frontend/components/search.cljs

@@ -413,7 +413,7 @@
                    :theme       "monospace"}
                    :theme       "monospace"}
                   [:a.flex.fade-link.items-center
                   [:a.flex.fade-link.items-center
                    {:style {:margin-left 12}
                    {:style {:margin-left 12}
-                    :on-click #(state/pub-event! :modal/command-palette)}
+                    :on-click #(state/pub-event! [:modal/command-palette])}
                    (ui/icon "command" {:style {:font-size 20}})])])]]
                    (ui/icon "command" {:style {:font-size 20}})])])]]
    (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
    (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
          pages (->> (db/get-key-value :recent/pages)
          pages (->> (db/get-key-value :recent/pages)

+ 1 - 1
src/main/frontend/extensions/zotero/extractor.cljs

@@ -147,7 +147,7 @@
   (util/format "{{zotero-imported-file %s, %s}}" item-key (pr-str filename)))
   (util/format "{{zotero-imported-file %s, %s}}" item-key (pr-str filename)))
 
 
 (defn zotero-linked-file-macro [path]
 (defn zotero-linked-file-macro [path]
-  (util/format "{{zotero-linked-file %s}}" (pr-str (util/node-path.basename path))))
+  (util/format "{{zotero-linked-file %s}}" (pr-str (string/replace-first path "attachments:" ""))))
 
 
 (defmethod extract "attachment"
 (defmethod extract "attachment"
   [item]
   [item]

+ 65 - 0
src/main/frontend/handler/common/config_edn.cljs

@@ -0,0 +1,65 @@
+(ns frontend.handler.common.config-edn
+  "Common fns related to config.edn - global and repo"
+  (:require [malli.error :as me]
+            [malli.core :as m]
+            [goog.string :as gstring]
+            [clojure.string :as string]
+            [clojure.edn :as edn]
+            [frontend.handler.notification :as notification]))
+
+(defn- humanize-more
+  "Make error maps from me/humanize more readable for users. Doesn't try to handle
+nested keys or positional errors e.g. tuples"
+  [errors]
+  (map
+   (fn [[k v]]
+     (if (map? v)
+       [k (str "Has errors in the following keys - " (string/join ", " (keys v)))]
+       ;; Only show first error since we don't have a use case yet for multiple yet
+       [k (->> v flatten (remove nil?) first)]))
+   errors))
+
+(defn- validate-config-map
+  [m schema path]
+  (if-let [errors (->> m (m/explain schema) me/humanize)]
+    (do
+      (notification/show! (gstring/format "The file '%s' has the following errors:\n%s"
+                                          path
+                                          (->> errors
+                                               humanize-more
+                                               (map (fn [[k v]]
+                                                      (str k " - " v)))
+                                               (string/join "\n")))
+                          :error)
+      false)
+    true))
+
+(defn validate-config-edn
+  "Validates a global config.edn file for correctness and pops up an error
+  notification if invalid. Returns a boolean indicating if file is invalid.
+  Error messages are written with consideration that this validation is called
+  regardless of whether a file is written outside or inside Logseq."
+  [path file-body schema]
+  (let [parsed-body (try
+                      (edn/read-string file-body)
+                      (catch :default _ ::failed-to-read))]
+    (cond
+      (nil? parsed-body)
+      true
+
+      (= ::failed-to-read parsed-body)
+      (do
+        (notification/show! (gstring/format "Failed to read file '%s'. Make sure your config is wrapped
+in {}. Also make sure that the characters '( { [' have their corresponding closing character ') } ]'."
+                                            path)
+                            :error)
+        false)
+      ;; Custom error message is better than malli's "invalid type" error
+      (not (map? parsed-body))
+      (do
+        (notification/show! (gstring/format "The file '%s' is not valid. Make sure the config is wrapped in {}."
+                                            path)
+                            :error)
+        false)
+      :else
+      (validate-config-map parsed-body schema path))))

+ 3 - 9
src/main/frontend/handler/config.cljs

@@ -5,24 +5,18 @@
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
-            [borkdude.rewrite-edn :as rewrite]
-            [lambdaisland.glogi :as log]))
+            [borkdude.rewrite-edn :as rewrite]))
 
 
 (defn parse-repo-config
 (defn parse-repo-config
   "Parse repo configuration file content"
   "Parse repo configuration file content"
   [content]
   [content]
-  (try
-    (rewrite/parse-string content)
-    (catch :default e
-      (log/error :parse/config-failed e)
-      (state/pub-event! [:backup/broken-config (state/get-current-repo) content])
-      (rewrite/parse-string config/config-default-content))))
+  (rewrite/parse-string content))
 
 
 (defn- repo-config-set-key-value
 (defn- repo-config-set-key-value
   [path k v]
   [path k v]
   (when-let [repo (state/get-current-repo)]
   (when-let [repo (state/get-current-repo)]
     (when-let [content (db/get-file path)]
     (when-let [content (db/get-file path)]
-      (repo-config-handler/read-repo-config repo content)
+      (repo-config-handler/read-repo-config content)
       (let [result (parse-repo-config content)
       (let [result (parse-repo-config content)
             ks (if (vector? k) k [k])
             ks (if (vector? k) k [k])
             new-result (rewrite/assoc-in result ks v)
             new-result (rewrite/assoc-in result ks v)

+ 19 - 4
src/main/frontend/handler/editor.cljs

@@ -887,9 +887,8 @@
 
 
 (defn set-block-timestamp!
 (defn set-block-timestamp!
   [block-id key value]
   [block-id key value]
-  (let [key (string/lower-case key)
+  (let [key (string/lower-case (str key))
         block-id (if (string? block-id) (uuid block-id) block-id)
         block-id (if (string? block-id) (uuid block-id) block-id)
-        key (string/lower-case (str key))
         value (str value)]
         value (str value)]
     (when-let [block (db/pull [:block/uuid block-id])]
     (when-let [block (db/pull [:block/uuid block-id])]
       (let [{:block/keys [content]} block
       (let [{:block/keys [content]} block
@@ -903,6 +902,20 @@
               (state/set-edit-content! input-id new-content)
               (state/set-edit-content! input-id new-content)
               (save-block-if-changed! block new-content))))))))
               (save-block-if-changed! block new-content))))))))
 
 
+(defn set-editing-block-timestamp!
+  "Almost the same as set-block-timestamp! except for:
+   - it doesn't save the block
+   - it extracts current content from current input"
+  [key value]
+  (let [key (string/lower-case (str key))
+        value (str value)
+        content (state/get-edit-content)
+        new-content (-> (text-util/remove-timestamp content key)
+                        (text-util/add-timestamp key value))]
+    (when (not= content new-content)
+      (let [input-id (state/get-edit-input-id)]
+        (state/set-edit-content! input-id new-content)))))
+
 (defn set-blocks-id!
 (defn set-blocks-id!
   "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
   "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
    Accepts a list of uuids."
    Accepts a list of uuids."
@@ -3065,7 +3078,8 @@
 (defn shortcut-up-down [direction]
 (defn shortcut-up-down [direction]
   (fn [e]
   (fn [e]
     (when (and (not (auto-complete?))
     (when (and (not (auto-complete?))
-               (not (slide-focused?)))
+               (not (slide-focused?))
+               (not (state/get-timestamp-block)))
       (util/stop e)
       (util/stop e)
       (cond
       (cond
         (state/editing?)
         (state/editing?)
@@ -3122,7 +3136,8 @@
 
 
 (defn shortcut-left-right [direction]
 (defn shortcut-left-right [direction]
   (fn [e]
   (fn [e]
-    (when-not (auto-complete?)
+    (when (and (not (auto-complete?))
+               (not (state/get-timestamp-block)))
       (cond
       (cond
         (state/editing?)
         (state/editing?)
         (do
         (do

+ 0 - 12
src/main/frontend/handler/events.cljs

@@ -604,18 +604,6 @@
      (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
      (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
      :warning false (.-id o))))
      :warning false (.-id o))))
 
 
-(defmethod handle :backup/broken-config [[_ repo content]]
-  (when (and repo content)
-    (let [path (config/get-repo-config-path)
-          broken-path (string/replace path "/config.edn" "/broken-config.edn")]
-      (p/let [_ (fs/write-file! repo (config/get-repo-dir repo) broken-path content {})
-              _ (file-handler/alter-file repo path config/config-default-content {:skip-compare? true})]
-        (notification/show!
-         [:p.content
-          "It seems that your config.edn is broken. We've restored it with the default content and saved the previous content to the file logseq/broken-config.edn."]
-         :warning
-         false)))))
-
 (defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
 (defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
   (let [type (.-event event)
         payload (-> event
         payload (-> event

+ 18 - 8
src/main/frontend/handler/file.cljs

@@ -7,9 +7,12 @@
             [frontend.fs.nfs :as nfs]
             [frontend.fs.nfs :as nfs]
             [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.common.file :as file-common-handler]
             [frontend.handler.common.file :as file-common-handler]
+            [frontend.handler.common.config-edn :as config-edn-common-handler]
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.schema.handler.global-config :as global-config-schema]
+            [frontend.schema.handler.repo-config :as repo-config-schema]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
@@ -88,11 +91,17 @@
 (defn- validate-file
 (defn- validate-file
   "Returns true if valid and if false validator displays error message. Files
   "Returns true if valid and if false validator displays error message. Files
   that are not validated just return true"
   that are not validated just return true"
-  [path content]
-  (if (and
-       (config/global-config-enabled?)
-       (= (path/dirname path) (global-config-handler/global-config-dir)))
-    (global-config-handler/validate-config-edn path content)
+  [repo path content]
+  (cond
+    (= path (config/get-repo-config-path repo))
+    (config-edn-common-handler/validate-config-edn path content repo-config-schema/Config-edn)
+
+    (and
+     (config/global-config-enabled?)
+     (= (path/dirname path) (global-config-handler/global-config-dir)))
+    (config-edn-common-handler/validate-config-edn path content global-config-schema/Config-edn)
+
+    :else
     true))
     true))
 
 
 (defn- validate-and-write-file
 (defn- validate-and-write-file
@@ -109,7 +118,7 @@
         write-file-options' (merge write-file-options
         write-file-options' (merge write-file-options
                                    (when original-content {:old-content original-content}))]
                                    (when original-content {:old-content original-content}))]
     (p/do!
     (p/do!
-     (if (validate-file path content)
+     (if (validate-file repo path content)
        (do
        (do
          (fs/write-file! repo path-dir path content write-file-options')
          (fs/write-file! repo path-dir path content write-file-options')
          true)
          true)
@@ -125,7 +134,7 @@
                            skip-compare? false}}]
                            skip-compare? false}}]
   (let [path (gp-util/path-normalize path)
   (let [path (gp-util/path-normalize path)
         write-file! (if from-disk?
         write-file! (if from-disk?
-                      #(p/promise (validate-file path content))
+                      #(p/promise (validate-file repo path content))
                       #(validate-and-write-file repo path content {:skip-compare? skip-compare?}))
                       #(validate-and-write-file repo path content {:skip-compare? skip-compare?}))
         opts {:new-graph? new-graph?
         opts {:new-graph? new-graph?
               :from-disk? from-disk?
               :from-disk? from-disk?
@@ -149,7 +158,8 @@
                        (= path (config/get-custom-css-path repo))
                        (= path (config/get-custom-css-path repo))
                        (ui-handler/add-style-if-exists!)
                        (ui-handler/add-style-if-exists!)
 
 
-                       (= path (config/get-repo-config-path repo))
+                       (and (= path (config/get-repo-config-path repo))
+                            valid-result?)
                        (p/let [_ (repo-config-handler/restore-repo-config! repo content)]
                        (p/let [_ (repo-config-handler/restore-repo-config! repo content)]
                               (state/pub-event! [:shortcut/refresh]))
                               (state/pub-event! [:shortcut/refresh]))
 
 

+ 0 - 63
src/main/frontend/handler/global_config.cljs

@@ -4,16 +4,10 @@
   component depends on a repo."
   component depends on a repo."
   (:require [frontend.fs :as fs]
   (:require [frontend.fs :as fs]
             [frontend.handler.common.file :as file-common-handler]
             [frontend.handler.common.file :as file-common-handler]
-            [frontend.handler.notification :as notification]
-            [frontend.schema.handler.global-config :as global-config-schema]
             [frontend.state :as state]
             [frontend.state :as state]
             [promesa.core :as p]
             [promesa.core :as p]
             [shadow.resource :as rc]
             [shadow.resource :as rc]
-            [malli.error :as me]
-            [malli.core :as m]
-            [goog.string :as gstring]
             [clojure.edn :as edn]
             [clojure.edn :as edn]
-            [clojure.string :as string]
             [electron.ipc :as ipc]
             [electron.ipc :as ipc]
             ["path" :as path]))
             ["path" :as path]))
 
 
@@ -62,63 +56,6 @@
     (p/let [config-content (fs/read-file config-dir config-path)]
     (p/let [config-content (fs/read-file config-dir config-path)]
            (set-global-config-state! config-content))))
            (set-global-config-state! config-content))))
 
 
-(defn- humanize-more
-  "Make error maps from me/humanize more readable for users. Doesn't try to handle
-nested keys or positional errors e.g. tuples"
-  [errors]
-  (map
-   (fn [[k v]]
-     (if (map? v)
-       [k (str "Has errors in the following keys - " (string/join ", " (keys v)))]
-       ;; Only show first error since we don't have a use case yet for multiple yet
-       [k (->> v flatten (remove nil?) first)]))
-   errors))
-
-(defn- validate-config-map
-  [m path]
-  (if-let [errors (->> m (m/explain global-config-schema/Config-edn) me/humanize)]
-    (do
-      (notification/show! (gstring/format "The file '%s' has the following errors:\n%s"
-                                          path
-                                          (->> errors
-                                               humanize-more
-                                               (map (fn [[k v]]
-                                                      (str k " - " v)))
-                                               (string/join "\n")))
-                          :error)
-      false)
-    true))
-
-(defn validate-config-edn
-  "Validates a global config.edn file for correctness and pops up an error
-  notification if invalid. Returns a boolean indicating if file is invalid.
-  Error messages are written with consideration that this validation is called
-  regardless of whether a file is written outside or inside Logseq."
-  [path file-body]
-  (let [parsed-body (try
-                      (edn/read-string file-body)
-                      (catch :default _ ::failed-to-read))]
-    (cond
-      (nil? parsed-body)
-      true
-
-      (= ::failed-to-read parsed-body)
-      (do
-        (notification/show! (gstring/format "Failed to read file '%s'. Make sure your config is wrapped
-in {}. Also make sure that the characters '( { [' have their corresponding closing character ') } ]'."
-                                            path)
-                            :error)
-        false)
-      ;; Custom error message is better than malli's "invalid type" error
-      (not (map? parsed-body))
-      (do
-        (notification/show! (gstring/format "The file '%s' is not valid. Make sure the config is wrapped in {}."
-                                            path)
-                            :error)
-        false)
-      :else
-      (validate-config-map parsed-body path))))
-
 (defn start
 (defn start
   "This component has four responsibilities on start:
   "This component has four responsibilities on start:
 - Fetch root-dir for later use with config paths
 - Fetch root-dir for later use with config paths

+ 1 - 1
src/main/frontend/handler/repo.cljs

@@ -261,7 +261,7 @@
   (state/set-parsing-state! {:graph-loading? true})
   (state/set-parsing-state! {:graph-loading? true})
   (let [config (or (when-let [content (some-> (first (filter #(= (config/get-repo-config-path repo-url) (:file/path %)) nfs-files))
   (let [config (or (when-let [content (some-> (first (filter #(= (config/get-repo-config-path repo-url) (:file/path %)) nfs-files))
                                               :file/content)]
                                               :file/content)]
-                     (repo-config-handler/read-repo-config repo-url content))
+                     (repo-config-handler/read-repo-config content))
                    (state/get-config repo-url))
                    (state/get-config repo-url))
         ;; NOTE: Use config while parsing. Make sure it's the current journal title format
         ;; NOTE: Use config while parsing. Make sure it's the current journal title format
         _ (state/set-config! repo-url config)
         _ (state/set-config! repo-url config)

+ 5 - 11
src/main/frontend/handler/repo_config.cljs

@@ -6,11 +6,10 @@
   (:require [frontend.db :as db]
   (:require [frontend.db :as db]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.state :as state]
             [frontend.state :as state]
-            [frontend.handler.common :as common-handler]
             [frontend.handler.common.file :as file-common-handler]
             [frontend.handler.common.file :as file-common-handler]
-            [cljs.reader :as reader]
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [promesa.core :as p]
             [promesa.core :as p]
+            [clojure.edn :as edn]
             [frontend.spec :as spec]))
             [frontend.spec :as spec]))
 
 
 (defn- get-repo-config-content
 (defn- get-repo-config-content
@@ -18,19 +17,14 @@
   (db/get-file repo-url (config/get-repo-config-path)))
   (db/get-file repo-url (config/get-repo-config-path)))
 
 
 (defn read-repo-config
 (defn read-repo-config
-  "Converts file content to edn and handles read failure by backing up file and
-  reverting to a default file"
-  [repo content]
-  (common-handler/safe-read-string
-   content
-   (fn [_e]
-     (state/pub-event! [:backup/broken-config repo content])
-     (reader/read-string config/config-default-content))))
+  "Converts file content to edn"
+  [content]
+  (edn/read-string content))
 
 
 (defn set-repo-config-state!
 (defn set-repo-config-state!
   "Sets repo config state using given file content"
   "Sets repo config state using given file content"
   [repo-url content]
   [repo-url content]
-  (let [config (read-repo-config repo-url content)]
+  (let [config (read-repo-config content)]
     (state/set-config! repo-url config)
     (state/set-config! repo-url config)
     config))
     config))
 
 

+ 4 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -36,6 +36,9 @@
 ;; :inactive key is for commands that are not active for a given platform or feature condition
 ;; :inactive key is for commands that are not active for a given platform or feature condition
 ;; Avoid using single letter shortcuts to allow chords that start with those characters
 ;; Avoid using single letter shortcuts to allow chords that start with those characters
 (def ^:large-vars/data-var all-default-keyboard-shortcuts
 (def ^:large-vars/data-var all-default-keyboard-shortcuts
+  ;; BUG: Actually, "enter" is registered by mixin behind a "when inputing" guard
+  ;; So this setting item does not cover all cases.
+  ;; See-also: frontend.components.datetime/time-repeater
   {:date-picker/complete         {:binding "enter"
   {:date-picker/complete         {:binding "enter"
                                   :fn      ui-handler/shortcut-complete}
                                   :fn      ui-handler/shortcut-complete}
 
 
@@ -328,7 +331,7 @@
 
 
    :graph/re-index                 {:fn (fn []
    :graph/re-index                 {:fn (fn []
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-                                                 (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
+                                            (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
                                     :binding false}
                                     :binding false}
 
 
    :command/run                    {:binding "mod+shift+1"
    :command/run                    {:binding "mod+shift+1"

+ 9 - 0
src/main/frontend/schema/handler/repo_config.cljc

@@ -0,0 +1,9 @@
+(ns frontend.schema.handler.repo-config
+  "Malli schemas for repo-config"
+  (:require [frontend.schema.handler.common-config :as common-config]))
+
+;; For now this just references a common schema but repo-config and
+;; global-config could diverge
+(def Config-edn
+  "Schema for repo config.edn"
+  common-config/Config-edn)

+ 6 - 9
src/main/frontend/state.cljs

@@ -117,7 +117,7 @@
      :editor/content                        {}
      :editor/content                        {}
      :editor/block                          nil
      :editor/block                          nil
      :editor/block-dom-id                   nil
      :editor/block-dom-id                   nil
-     :editor/set-timestamp-block            nil
+     :editor/set-timestamp-block            nil             ;; click rendered block timestamp-cp to set timestamp
      :editor/last-input-time                nil
      :editor/last-input-time                nil
      :editor/document-mode?                 document-mode?
      :editor/document-mode?                 document-mode?
      :editor/args                           nil
      :editor/args                           nil
@@ -331,11 +331,6 @@
              new))
              new))
          configs))
          configs))
 
 
-(defn validate-current-config
-  "TODO: Temporal fix"
-  [config]
-  (when (map? config) config))
-
 (defn get-config
 (defn get-config
   "User config for the given repo or current repo if none given. All config fetching
   "User config for the given repo or current repo if none given. All config fetching
 should be done through this fn in order to get global config and config defaults"
 should be done through this fn in order to get global config and config defaults"
@@ -345,7 +340,7 @@ should be done through this fn in order to get global config and config defaults
    (merge-configs
    (merge-configs
     default-config
     default-config
     (get-in @state [:config ::global-config])
     (get-in @state [:config ::global-config])
-    (validate-current-config (get-in @state [:config repo-url])))))
+    (get-in @state [:config repo-url]))))
 
 
 (defonce publishing? (atom nil))
 (defonce publishing? (atom nil))
 
 
@@ -557,10 +552,10 @@ Similar to re-frame subscriptions"
   "Sub equivalent to get-config which should handle all sub user-config access"
   "Sub equivalent to get-config which should handle all sub user-config access"
   ([] (sub-config (get-current-repo)))
   ([] (sub-config (get-current-repo)))
   ([repo]
   ([repo]
-   (let [config (validate-current-config (sub :config))]
+   (let [config (sub :config)]
      (merge-configs default-config
      (merge-configs default-config
                     (get config ::global-config)
                     (get config ::global-config)
-                    (validate-current-config (get config repo))))))
+                    (get config repo)))))
 
 
 (defn enable-grammarly?
 (defn enable-grammarly?
   []
   []
@@ -1729,6 +1724,7 @@ Similar to re-frame subscriptions"
   (:system/events @state))
   (:system/events @state))
 
 
 (defn pub-event!
 (defn pub-event!
+  {:malli/schema [:=> [:cat vector?] :any]}
   [payload]
   [payload]
   (let [chan (get-events-chan)]
   (let [chan (get-events-chan)]
     (async/put! chan payload)))
     (async/put! chan payload)))
@@ -1836,6 +1832,7 @@ Similar to re-frame subscriptions"
                       :editor/block block
                       :editor/block block
                       :editor/editing? {edit-input-id true}
                       :editor/editing? {edit-input-id true}
                       :editor/last-key-code nil
                       :editor/last-key-code nil
+                      :editor/set-timestamp-block nil
                       :cursor-range cursor-range))))
                       :cursor-range cursor-range))))
         (when-let [input (gdom/getElement edit-input-id)]
         (when-let [input (gdom/getElement edit-input-id)]
           (let [pos (count cursor-range)]
           (let [pos (count cursor-range)]

+ 1 - 1
src/main/frontend/util/fs.cljs

@@ -32,7 +32,7 @@
                                        (str "/" % "/")
                                        (str "/" % "/")
                                        (str % "/"))) ignores)
                                        (str % "/"))) ignores)
        (some #(string/ends-with? path %)
        (some #(string/ends-with? path %)
-             [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
+             [".DS_Store" "logseq/graphs-txid.edn"])
       ;; hidden directory or file
       ;; hidden directory or file
        (let [relpath (path/relative dir path)]
        (let [relpath (path/relative dir path)]
          (or (re-find #"/\.[^.]+" relpath)
          (or (re-find #"/\.[^.]+" relpath)

+ 12 - 1
src/test/frontend/extensions/zotero/extractor_test.cljs

@@ -1,6 +1,6 @@
 (ns frontend.extensions.zotero.extractor-test
 (ns frontend.extensions.zotero.extractor-test
   (:require [clojure.edn :as edn]
   (:require [clojure.edn :as edn]
-            [clojure.test :as test :refer [deftest is testing]]
+            [clojure.test :as test :refer [deftest is testing are]]
             [shadow.resource :as rc]
             [shadow.resource :as rc]
             [frontend.extensions.zotero.extractor :as extractor]))
             [frontend.extensions.zotero.extractor :as extractor]))
 
 
@@ -65,6 +65,17 @@
 
 
       (testing "use parsed date when possible"
       (testing "use parsed date when possible"
         (is (= "[[Mar 28th, 2011]]" (-> properties :date))))))
         (is (= "[[Mar 28th, 2011]]" (-> properties :date))))))
+  
+  (testing "zotero imported file path"
+    (are [item-key filename open] (= (extractor/zotero-imported-file-macro item-key filename) open)
+      "9AUD8MNT" "a.pdf" "{{zotero-imported-file 9AUD8MNT, \"a.pdf\"}}"))
+
+  (testing "zotero linked file path"
+    (are [path open] (= (extractor/zotero-linked-file-macro path) open)
+      ;; TODO provide some real samples on multiple platforms
+      "attachments:abc/def/ghi.pdf" "{{zotero-linked-file \"abc/def/ghi.pdf\"}}"
+      ;; Chinese and blank
+      "attachments:书籍/人民邮电出版社/NSCA-CPT美国国家体能协会私人教练认证指南 第2版.pdf" "{{zotero-linked-file \"书籍/人民邮电出版社/NSCA-CPT美国国家体能协会私人教练认证指南 第2版.pdf\"}}"))
 
 
 ;; 2022.10.18. Should be deprecated since Hickory is invalid in Node test
 ;; 2022.10.18. Should be deprecated since Hickory is invalid in Node test
 ;; Skip until we find an alternative
 ;; Skip until we find an alternative

+ 45 - 0
src/test/frontend/handler/common/config_edn_test.cljs

@@ -0,0 +1,45 @@
+(ns frontend.handler.common.config-edn-test
+  (:require [clojure.test :refer [is testing deftest]]
+            [clojure.string :as string]
+            [frontend.handler.common.config-edn :as config-edn-common-handler]
+            [frontend.schema.handler.global-config :as global-config-schema]
+            [frontend.schema.handler.repo-config :as repo-config-schema]
+            [frontend.handler.notification :as notification]))
+
+(defn- validation-config-error-for
+  [config-body schema]
+  (let [error-message (atom nil)]
+    (with-redefs [notification/show! (fn [msg _] (reset! error-message msg))]
+      (is (= false
+             (config-edn-common-handler/validate-config-edn "config.edn" config-body schema)))
+      (str @error-message))))
+
+(deftest validate-config-edn
+  (testing "Valid cases"
+    (is (= true
+           (config-edn-common-handler/validate-config-edn
+            "config.edn" "{:preferred-workflow :todo}" global-config-schema/Config-edn))
+        "global config.edn")
+
+    (is (= true
+           (config-edn-common-handler/validate-config-edn
+            "config.edn" "{:preferred-workflow :todo}" repo-config-schema/Config-edn))
+        "repo config.edn"))
+
+  (doseq [[file-type schema] {"global config.edn" global-config-schema/Config-edn
+                              "repo config.edn" repo-config-schema/Config-edn}]
+    (testing (str "Invalid cases for " file-type)
+      (is (string/includes?
+           (validation-config-error-for ":export/bullet-indentation :two-spaces" schema)
+           "wrapped in {}")
+          (str "Not a map for " file-type))
+
+      (is (string/includes?
+           (validation-config-error-for "{:preferred-workflow :todo" schema)
+           "Failed to read")
+          (str "Invalid edn for " file-type))
+
+      (is (string/includes?
+           (validation-config-error-for "{:start-of-week 7}" schema)
+           "has the following errors")
+          (str "Invalid map for " file-type)))))

+ 0 - 32
src/test/frontend/handler/global_config_test.cljs

@@ -1,32 +0,0 @@
-(ns frontend.handler.global-config-test
-  (:require [clojure.test :refer [is testing deftest]]
-            [frontend.handler.global-config :as global-config-handler]
-            [clojure.string :as string]
-            [frontend.handler.notification :as notification]))
-
-(defn- validation-config-error-for
-  [config-body]
-  (let [error-message (atom nil)]
-      (with-redefs [notification/show! (fn [msg _] (reset! error-message msg))]
-        (is (= false
-               (global-config-handler/validate-config-edn "config.edn" config-body)))
-        (str @error-message))))
-
-(deftest validate-config-edn
-  (testing "Valid cases"
-    (is (= true
-           (global-config-handler/validate-config-edn
-            "config.edn" "{:preferred-workflow :todo}"))))
-
-  (testing "Invalid cases"
-    (is (string/includes?
-         (validation-config-error-for ":export/bullet-indentation :two-spaces")
-         "wrapped in {}"))
-
-    (is (string/includes?
-         (validation-config-error-for "{:preferred-workflow :todo")
-         "Failed to read"))
-
-    (is (string/includes?
-         (validation-config-error-for "{:start-of-week 7}")
-         "has the following errors"))))