Selaa lähdekoodia

Merge branch 'master' into feat/tweet-shape

Gabriel Horner 2 vuotta sitten
vanhempi
sitoutus
f9604e1b07

+ 5 - 2
bb.edn

@@ -63,8 +63,11 @@
   dev: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
   logseq.tasks.dev/lint

+ 25 - 8
e2e-tests/fixtures.ts

@@ -55,6 +55,7 @@ base.beforeAll(async () => {
   })
   context = electronApp.context()
   await context.tracing.start({ screenshots: true, snapshots: true });
+  await context.tracing.startChunk();
 
   // NOTE: The following ensures App first start with the correct path.
   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
 // FIXME: add type to `block`
 export const test = base.extend<LogseqFixtures>({
@@ -283,3 +276,27 @@ export const test = base.extend<LogseqFixtures>({
     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]
             [frontend.schema.handler.plugin-config :as plugin-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.edn :as edn]))
 
@@ -20,16 +21,25 @@
       (pprint/pprint errors))
     (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
                        slurp
                        edn/read-string
-                       (m/explain global-config-schema/Config-edn)
+                       (m/explain schema)
                        me/humanize)]
     (do
       (println "Found errors:")
       (pprint/pprint errors))
     (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 [frontend.util]
             [frontend.util.list]
-            [malli.clj-kondo :as mc]))
+            [malli.clj-kondo :as mc]
+            [malli.instrument]))
 
 
 (defn main [& _args]

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

@@ -202,7 +202,7 @@
      (some #(string/includes? path (str "/" % "/"))
            ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
      (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
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)

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

@@ -655,12 +655,13 @@
 (defmethod handle-step :editor/show-date-picker [[_ type]]
   (if (and
        (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
       (notification/show! [:div "Please add some content first."] :warning)
       (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]]
   (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.editor :as editor-handler]
             [frontend.handler.file-sync :as file-sync]
+            [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
@@ -61,7 +62,6 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.property :as property]
             [frontend.util.text :as text-util]
-            [frontend.handler.notification :as notification]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
@@ -853,8 +853,8 @@
   [name arguments]
   (if (and (seq arguments)
            (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-container)
@@ -948,18 +948,19 @@
 
 (defn- render-macro
   [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
   [config html-export? link]
@@ -1319,46 +1320,51 @@
 
 (defn- macro-video-cp
   [_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
   [name config arguments]
@@ -1562,7 +1568,7 @@
 
          ["Entity" e]
          [:span {:dangerouslySetInnerHTML
-                 {:__html (:html (security/sanitize-html e))}}]
+                 {:__html (security/sanitize-html (:html e))}}]
 
          ["Latex_Fragment" [display s]] ;display can be "Displayed" or "Inline"
          (if html-export?
@@ -2096,10 +2102,13 @@
         [: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."]]))
 
-(rum/defcs timestamp-cp < rum/reactive
+(rum/defcs timestamp-cp
+  < rum/reactive
+  (rum/local false ::show-datepicker?)
   [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.text-sm.flex.flex-row
       [:div.opacity-50.font-medium.timestamp-label
@@ -2107,19 +2116,23 @@
       [:a.opacity-80.hover:opacity-100
        {:on-mouse-down (fn [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
+                             (reset! *show-datapicker? true)
                              (reset! commands/*current-command typ)
-                             (state/set-editor-action! :datepicker)
                              (state/set-timestamp-block! {:block block
                                                           :typ typ}))))}
        [: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?
   [target]
@@ -2970,8 +2983,7 @@
                         :tbody
                         (mapv #(tr :td %) group)))
                      groups)]
-    [:div.table-wrapper {:style {:max-width (min 700
-                                                 (gobj/get js/window "innerWidth"))}}
+    [:div.table-wrapper
      (->elem
       :table
       {:class "table-auto"

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

@@ -45,7 +45,7 @@
   [{:keys [num duration kind]}]
   (let [show? (rum/react *show-repeater?)]
     (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
         {:default-value num
          :on-change (fn [event]
@@ -78,7 +78,7 @@
                                        :duration "d"}))}
        "Add repeater"])))
 
-(defn clear-timestamp!
+(defn- clear-timestamp!
   []
   (reset! *timestamp default-timestamp-value)
   (reset! *show-time? false)
@@ -86,6 +86,7 @@
   (state/set-state! :date-picker/date nil))
 
 (defn- on-submit
+  "Submit handler of date picker"
   [e]
   (when e (util/stop e))
   (let [{:keys [repeater] :as timestamp} @*timestamp
@@ -96,15 +97,21 @@
         text (repeated/timestamp-map->text timestamp)
         block-data (state/get-timestamp-block)
         {:keys [block typ show?]} block-data
+        editing-block-id (:block/uuid (state/get-edit-block))
         block-id (or (:block/uuid block)
-                     (:block/uuid (state/get-edit-block)))
+                     editing-block-id)
         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?
       (reset! show? false)))
   (clear-timestamp!)
+  (state/set-timestamp-block! nil)
   (commands/restore-state))
 
 (rum/defc time-repeater < rum/reactive
@@ -138,30 +145,30 @@
              (when-not (:date-picker/date @state/state)
                (state/set-state! :date-picker/date (get ts :date (t/today)))))
            state)}
-  [id format _ts]
+  [dom-id format _ts]
   (let [current-command @commands/*current-command
         deadline-or-schedule? (and current-command
                                    (contains? #{"deadline" "scheduled"}
                                               (string/lower-case current-command)))
         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
-               (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}
                (when set-default-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)))
                    {:right 0}
                    {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))]
@@ -555,6 +555,7 @@
       (= :property-value-search action)
       (animated-modal "property-value-search" (property-value-search id) true)
 
+      ;; date-picker in editing-mode
       (= :datepicker action)
       (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"}
                   [:a.flex.fade-link.items-center
                    {: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}})])])]]
    (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
          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)))
 
 (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"
   [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.config :as config]
             [frontend.db :as db]
-            [borkdude.rewrite-edn :as rewrite]
-            [lambdaisland.glogi :as log]))
+            [borkdude.rewrite-edn :as rewrite]))
 
 (defn parse-repo-config
   "Parse repo configuration file 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
   [path k v]
   (when-let [repo (state/get-current-repo)]
     (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)
             ks (if (vector? k) k [k])
             new-result (rewrite/assoc-in result ks v)

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

@@ -887,9 +887,8 @@
 
 (defn set-block-timestamp!
   [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)
-        key (string/lower-case (str key))
         value (str value)]
     (when-let [block (db/pull [:block/uuid block-id])]
       (let [{:block/keys [content]} block
@@ -903,6 +902,20 @@
               (state/set-edit-content! input-id 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!
   "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
    Accepts a list of uuids."
@@ -3065,7 +3078,8 @@
 (defn shortcut-up-down [direction]
   (fn [e]
     (when (and (not (auto-complete?))
-               (not (slide-focused?)))
+               (not (slide-focused?))
+               (not (state/get-timestamp-block)))
       (util/stop e)
       (cond
         (state/editing?)
@@ -3122,7 +3136,8 @@
 
 (defn shortcut-left-right [direction]
   (fn [e]
-    (when-not (auto-complete?)
+    (when (and (not (auto-complete?))
+               (not (state/get-timestamp-block)))
       (cond
         (state/editing?)
         (do

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

@@ -604,18 +604,6 @@
      (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
      :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]]
   (let [type (.-event event)
         payload (-> event

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

@@ -7,9 +7,12 @@
             [frontend.fs.nfs :as nfs]
             [frontend.fs.capacitor-fs :as capacitor-fs]
             [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.global-config :as global-config-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.util :as util]
             [logseq.graph-parser.util :as gp-util]
@@ -88,11 +91,17 @@
 (defn- validate-file
   "Returns true if valid and if false validator displays error message. Files
   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))
 
 (defn- validate-and-write-file
@@ -109,7 +118,7 @@
         write-file-options' (merge write-file-options
                                    (when original-content {:old-content original-content}))]
     (p/do!
-     (if (validate-file path content)
+     (if (validate-file repo path content)
        (do
          (fs/write-file! repo path-dir path content write-file-options')
          true)
@@ -125,7 +134,7 @@
                            skip-compare? false}}]
   (let [path (gp-util/path-normalize path)
         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?}))
         opts {:new-graph? new-graph?
               :from-disk? from-disk?
@@ -149,7 +158,8 @@
                        (= path (config/get-custom-css-path repo))
                        (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)]
                               (state/pub-event! [:shortcut/refresh]))
 

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

@@ -4,16 +4,10 @@
   component depends on a repo."
   (:require [frontend.fs :as fs]
             [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]
             [promesa.core :as p]
             [shadow.resource :as rc]
-            [malli.error :as me]
-            [malli.core :as m]
-            [goog.string :as gstring]
             [clojure.edn :as edn]
-            [clojure.string :as string]
             [electron.ipc :as ipc]
             ["path" :as path]))
 
@@ -62,63 +56,6 @@
     (p/let [config-content (fs/read-file config-dir config-path)]
            (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
   "This component has four responsibilities on start:
 - 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})
   (let [config (or (when-let [content (some-> (first (filter #(= (config/get-repo-config-path repo-url) (:file/path %)) nfs-files))
                                               :file/content)]
-                     (repo-config-handler/read-repo-config repo-url content))
+                     (repo-config-handler/read-repo-config content))
                    (state/get-config repo-url))
         ;; NOTE: Use config while parsing. Make sure it's the current journal title format
         _ (state/set-config! repo-url config)

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

@@ -6,11 +6,10 @@
   (:require [frontend.db :as db]
             [frontend.config :as config]
             [frontend.state :as state]
-            [frontend.handler.common :as common-handler]
             [frontend.handler.common.file :as file-common-handler]
-            [cljs.reader :as reader]
             [frontend.fs :as fs]
             [promesa.core :as p]
+            [clojure.edn :as edn]
             [frontend.spec :as spec]))
 
 (defn- get-repo-config-content
@@ -18,19 +17,14 @@
   (db/get-file repo-url (config/get-repo-config-path)))
 
 (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!
   "Sets repo config state using given file content"
   [repo-url content]
-  (let [config (read-repo-config repo-url content)]
+  (let [config (read-repo-config content)]
     (state/set-config! repo-url 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
 ;; Avoid using single letter shortcuts to allow chords that start with those characters
 (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"
                                   :fn      ui-handler/shortcut-complete}
 
@@ -328,7 +331,7 @@
 
    :graph/re-index                 {:fn (fn []
                                           (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}
 
    :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/block                          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/document-mode?                 document-mode?
      :editor/args                           nil
@@ -331,11 +331,6 @@
              new))
          configs))
 
-(defn validate-current-config
-  "TODO: Temporal fix"
-  [config]
-  (when (map? config) config))
-
 (defn get-config
   "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"
@@ -345,7 +340,7 @@ should be done through this fn in order to get global config and config defaults
    (merge-configs
     default-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))
 
@@ -557,10 +552,10 @@ Similar to re-frame subscriptions"
   "Sub equivalent to get-config which should handle all sub user-config access"
   ([] (sub-config (get-current-repo)))
   ([repo]
-   (let [config (validate-current-config (sub :config))]
+   (let [config (sub :config)]
      (merge-configs default-config
                     (get config ::global-config)
-                    (validate-current-config (get config repo))))))
+                    (get config repo)))))
 
 (defn enable-grammarly?
   []
@@ -1729,6 +1724,7 @@ Similar to re-frame subscriptions"
   (:system/events @state))
 
 (defn pub-event!
+  {:malli/schema [:=> [:cat vector?] :any]}
   [payload]
   (let [chan (get-events-chan)]
     (async/put! chan payload)))
@@ -1836,6 +1832,7 @@ Similar to re-frame subscriptions"
                       :editor/block block
                       :editor/editing? {edit-input-id true}
                       :editor/last-key-code nil
+                      :editor/set-timestamp-block nil
                       :cursor-range cursor-range))))
         (when-let [input (gdom/getElement edit-input-id)]
           (let [pos (count cursor-range)]

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

@@ -32,7 +32,7 @@
                                        (str "/" % "/")
                                        (str % "/"))) ignores)
        (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
        (let [relpath (path/relative dir path)]
          (or (re-find #"/\.[^.]+" relpath)

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

@@ -1,6 +1,6 @@
 (ns frontend.extensions.zotero.extractor-test
   (: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]
             [frontend.extensions.zotero.extractor :as extractor]))
 
@@ -65,6 +65,17 @@
 
       (testing "use parsed date when possible"
         (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
 ;; 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"))))