Просмотр исходного кода

Merge branch 'master' into zotero/poc

Weihua Lu 4 лет назад
Родитель
Сommit
828e296f86
43 измененных файлов с 897 добавлено и 366 удалено
  1. 1 1
      libs/package.json
  2. 8 2
      libs/src/LSPlugin.caller.ts
  3. 3 3
      libs/src/LSPlugin.core.ts
  4. 3 0
      libs/src/LSPlugin.ts
  5. 11 1
      libs/src/LSPlugin.user.ts
  6. 26 0
      libs/src/helpers.ts
  7. 87 0
      libs/src/modules/LSPlugin.Storage.ts
  8. 6 2
      libs/webpack.config.js
  9. 9 9
      resources/css/common.css
  10. 126 11
      resources/js/lsplugin.core.js
  11. 1 0
      resources/js/lsplugin.user.js
  12. 1 1
      resources/package.json
  13. 6 0
      src/electron/electron/handler.cljs
  14. 3 0
      src/main/electron/listener.cljs
  15. 65 69
      src/main/frontend/components/block.cljs
  16. 170 138
      src/main/frontend/components/page.cljs
  17. 1 1
      src/main/frontend/components/page.css
  18. 10 1
      src/main/frontend/components/settings.cljs
  19. 11 0
      src/main/frontend/components/svg.cljs
  20. 8 0
      src/main/frontend/config.cljs
  21. 6 0
      src/main/frontend/date.cljs
  22. 23 13
      src/main/frontend/db/query_dsl.cljs
  23. 2 2
      src/main/frontend/db/query_react.cljs
  24. 3 0
      src/main/frontend/dicts.cljs
  25. 1 2
      src/main/frontend/extensions/graph.cljs
  26. 8 1
      src/main/frontend/format/block.cljs
  27. 22 18
      src/main/frontend/fs.cljs
  28. 3 1
      src/main/frontend/fs/node.cljs
  29. 1 0
      src/main/frontend/fs/protocol.cljs
  30. 20 0
      src/main/frontend/handler/common.cljs
  31. 3 0
      src/main/frontend/handler/dnd.cljs
  32. 27 22
      src/main/frontend/handler/editor.cljs
  33. 11 0
      src/main/frontend/handler/file.cljs
  34. 21 2
      src/main/frontend/handler/metadata.cljs
  35. 7 6
      src/main/frontend/handler/page.cljs
  36. 26 0
      src/main/frontend/handler/repo.cljs
  37. 31 34
      src/main/frontend/modules/outliner/core.cljs
  38. 5 7
      src/main/frontend/state.cljs
  39. 19 3
      src/main/frontend/util/property.cljs
  40. 1 1
      src/main/frontend/version.cljs
  41. 76 8
      src/main/logseq/api.cljs
  42. 23 0
      src/test/frontend/util/property_test.cljs
  43. 2 7
      templates/config.edn

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.1-alpha.24",
+  "version": "0.0.1-alpha.25",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

+ 8 - 2
libs/src/LSPlugin.caller.ts

@@ -220,7 +220,11 @@ class LSPluginCaller extends EventEmitter {
 
         this._call = async (...args: any) => {
           // parent all will get message before handshaked
-          await refChild.call(LSPMSGFn(pl.id), { type: args[0], payload: args[1] || {} })
+          await refChild.call(LSPMSGFn(pl.id), {
+            type: args[0], payload: Object.assign(args[1] || {}, {
+              $$pid: pl.id
+            })
+          })
         }
 
         this._callUserModel = async (type, payload: any) => {
@@ -263,7 +267,9 @@ class LSPluginCaller extends EventEmitter {
 
         // TODO: support sync call
         // @ts-ignore Call in same thread
-        this._pluginLocal?.emit(type, payload)
+        this._pluginLocal?.emit(type, Object.assign(payload, {
+          $$pid: pl.id
+        }))
 
         return actor?.promise
       }

+ 3 - 3
libs/src/LSPlugin.core.ts

@@ -7,7 +7,7 @@ import {
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
-  isObject, withFileProtocol
+  isObject, withFileProtocol, IS_DEV, getSDKPathRoot
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -477,7 +477,7 @@ class PluginLocal
 
     if (!entry.endsWith('.js')) return
 
-    let sdkPath = await invokeHostExportedApi('_callApplication', 'getAppPath')
+    let sdkPathRoot = await getSDKPathRoot()
     let entryPath = await invokeHostExportedApi(
       'write_user_tmp_file',
       `${this._id}_index.html`,
@@ -486,7 +486,7 @@ class PluginLocal
   <head>
     <meta charset="UTF-8">
     <title>logseq plugin entry</title>
-    <script src="${sdkPath}/js/lsplugin.user.js"></script>
+    <script src="${sdkPathRoot}/lsplugin.user.js"></script>
   </head>
   <body>
   <div id="app"></div>

+ 3 - 0
libs/src/LSPlugin.ts

@@ -1,6 +1,7 @@
 import EventEmitter from 'eventemitter3'
 import * as CSS from 'csstype'
 import { LSPluginCaller } from './LSPlugin.caller'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 export type PluginLocalIdentity = string
 
@@ -494,4 +495,6 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   App: IAppProxy & Record<string, any>
   Editor: IEditorProxy & Record<string, any>
   DB: IDBProxy
+
+  FileStorage: LSPluginFileStorage
 }

+ 11 - 1
libs/src/LSPlugin.user.ts

@@ -16,6 +16,7 @@ import Debug from 'debug'
 import * as CSS from 'csstype'
 import { snakeCase } from 'snake-case'
 import EventEmitter from 'eventemitter3'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 declare global {
   interface Window {
@@ -154,7 +155,7 @@ const editor: Partial<IEditorProxy> = {
       return false
     }
 
-    const key = + '_' + this.baseInfo.id
+    const key = tag + '_' + this.baseInfo.id
     const label = tag
     const type = 'block-context-menu-item'
 
@@ -192,6 +193,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
    */
   private _ui = new Map<number, uiState>()
 
+  private _fileStorage: LSPluginFileStorage
+
   /**
    * handler of before unload plugin
    * @private
@@ -226,6 +229,9 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
         actor?.reject(e)
       }
     })
+
+    // modules
+    this._fileStorage = new LSPluginFileStorage(this)
   }
 
   async ready (
@@ -400,6 +406,10 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
   get DB (): IDBProxy {
     return this._makeUserProxy(db)
   }
+
+  get FileStorage (): LSPluginFileStorage {
+    return this._fileStorage
+  }
 }
 
 export * from './LSPlugin'

+ 26 - 0
libs/src/helpers.ts

@@ -1,6 +1,7 @@
 import { StyleString, UIOptions } from './LSPlugin'
 import { PluginLocal } from './LSPlugin.core'
 import { snakeCase } from 'snake-case'
+import * as path from 'path'
 
 interface IObject {
   [key: string]: any;
@@ -13,6 +14,31 @@ declare global {
   }
 }
 
+export const IS_DEV = process.env.NODE_ENV === 'development'
+
+let _appPathRoot
+
+export async function getAppPathRoot (): Promise<string> {
+  if (_appPathRoot) {
+    return _appPathRoot
+  }
+
+  return (_appPathRoot =
+      await invokeHostExportedApi('_callApplication', 'getAppPath')
+  )
+}
+
+export async function getSDKPathRoot (): Promise<string> {
+  if (IS_DEV) {
+    // TODO: cache in preference file
+    return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
+  }
+
+  const appPathRoot = await getAppPathRoot()
+
+  return path.join(appPathRoot, 'js')
+}
+
 export function isObject (item: any) {
   return (item === Object(item) && !Array.isArray(item))
 }

+ 87 - 0
libs/src/modules/LSPlugin.Storage.ts

@@ -0,0 +1,87 @@
+import { LSPluginUser } from '../LSPlugin.user'
+
+export interface IAsyncStorage {
+  getItem (key: string): Promise<string | undefined>
+
+  setItem (key: string, value: string): Promise<void>
+
+  removeItem (key: string): Promise<void>
+
+  hasItem (key: string): Promise<boolean>
+
+  clear (): Promise<void>
+}
+
+/**
+ * A storage based on local files under specific context
+ */
+class LSPluginFileStorage implements IAsyncStorage {
+  /**
+   * @param ctx
+   */
+  constructor (
+    private ctx: LSPluginUser
+  ) {}
+
+  /**
+   * plugin id
+   */
+  get ctxId () {
+    return this.ctx.baseInfo.id
+  }
+
+  /**
+   * @param key A string as file name that support nested directory
+   * @param value Storage value
+   */
+  setItem (key: string, value: string): Promise<void> {
+    return this.ctx.caller.callAsync(`api:call`, {
+      method: 'write-plugin-storage-file',
+      args: [this.ctxId, key, value]
+    })
+  }
+
+  /**
+   * @param key
+   */
+  getItem (key: string): Promise<string | undefined> {
+    return this.ctx.caller.callAsync(`api:call`, {
+      method: 'read-plugin-storage-file',
+      args: [this.ctxId, key]
+    })
+  }
+
+  /**
+   * @param key
+   */
+  removeItem (key: string): Promise<void> {
+    return this.ctx.caller.call(`api:call`, {
+      method: 'unlink-plugin-storage-file',
+      args: [this.ctxId, key]
+    })
+  }
+
+  /**
+   * Clears the storage
+   */
+  clear (): Promise<void> {
+    return this.ctx.caller.call(`api:call`, {
+      method: 'clear-plugin-storage-files',
+      args: [this.ctxId]
+    })
+  }
+
+  /**
+   * @param key
+   */
+  hasItem (key: string): Promise<boolean> {
+    return this.ctx.caller.callAsync(`api:call`, {
+      method: 'exist-plugin-storage-file',
+      args: [this.ctxId, key]
+    })
+  }
+}
+
+export {
+  LSPluginFileStorage
+}

+ 6 - 2
libs/webpack.config.js

@@ -1,4 +1,5 @@
 const path = require('path')
+const webpack = require('webpack')
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
 
 module.exports = {
@@ -16,11 +17,14 @@ module.exports = {
     extensions: ['.tsx', '.ts', '.js'],
   },
   plugins: [
+    new webpack.ProvidePlugin({
+      process: 'process/browser',
+    }),
     // new BundleAnalyzerPlugin()
   ],
   output: {
-    library: "LSPluginEntry",
-    libraryTarget: "umd",
+    library: 'LSPluginEntry',
+    libraryTarget: 'umd',
     filename: 'lsplugin.user.js',
     path: path.resolve(__dirname, 'dist')
   },

+ 9 - 9
resources/css/common.css

@@ -90,11 +90,11 @@ html[data-theme='light'] {
   --ls-secondary-background-color: #f7f7f7;
   --ls-tertiary-background-color: #f1eee8;
   --ls-quaternary-background-color: #e8e5de;
-  --ls-table-tr-even-background-color: #f4f5f7;
+  --ls-table-tr-even-background-color: #f7f7f7;
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-secondary-color: #003761;
-  --ls-block-properties-background-color: #f7f6f4;
-  --ls-page-properties-background-color: #eae7e1;
+  --ls-block-properties-background-color: #f7f7f7;
+  --ls-page-properties-background-color: #f7f7f7;
   --ls-block-ref-link-text-color: #d8e1e8;
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
@@ -120,7 +120,7 @@ html[data-theme='light'] {
   --ls-page-blockquote-color: var(--ls-primary-text-color);
   --ls-page-blockquote-bg-color: #fbfaf8;
   --ls-page-blockquote-border-color: #799bbc;
-  --ls-page-inline-code-bg-color: #f7f6f4;
+  --ls-page-inline-code-bg-color: #f7f7f7;
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
@@ -128,7 +128,7 @@ html[data-theme='light'] {
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-icon-color: #908e8b;
   --ls-search-icon-color: var(--ls-icon-color);
-  --ls-a-chosen-bg: #f4f5f7;
+  --ls-a-chosen-bg: #f7f7f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
   --color-level-1: var(--ls-secondary-background-color);
   --color-level-2: var(--ls-tertiary-background-color);
@@ -724,14 +724,14 @@ li p:last-child,
   overflow-y: scroll;
 }
 
-.loader {
-  -webkit-animation: spin 2s infinite linear;
-}
-
 .loader-reverse {
     -webkit-animation: spin 2s infinite linear reverse;
 }
 
+.loader {
+    -webkit-animation: spin 2s infinite linear;
+}
+
 .canceled,
 .done {
   text-decoration: line-through;

Разница между файлами не показана из-за своего большого размера
+ 126 - 11
resources/js/lsplugin.core.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
resources/js/lsplugin.user.js


+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.2.3",
+  "version": "0.2.4",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",

+ 6 - 0
src/electron/electron/handler.cljs

@@ -18,6 +18,12 @@
 (defmethod handle :mkdir [_window [_ dir]]
   (fs/mkdirSync dir))
 
+(defmethod handle :mkdir-recur [_window [_ dir]]
+  (fs/mkdirSync dir #js {:recursive true}))
+
+(defmethod handle :rmdir-recur [_window [_ dir]]
+  (fs/rmdirSync dir #js {:recursive true}))
+
 ;; {encoding: 'utf8', withFileTypes: true}
 (defn- readdir
   [dir]

+ 3 - 0
src/main/electron/listener.cljs

@@ -8,6 +8,7 @@
             [promesa.core :as p]
             [electron.ipc :as ipc]
             [frontend.handler.notification :as notification]
+            [frontend.handler.metadata :as metadata-handler]
             [frontend.ui :as ui]))
 
 (defn listen-to-open-dir!
@@ -41,6 +42,8 @@
            (notification/show!
             (ui/loading "Logseq is saving the graphs to your local file system, please wait for several seconds.")
             :warning)
+           (doseq [repo repos]
+             (metadata-handler/set-pages-metadata! repo))
            (js/setTimeout
             (fn []
               (-> (p/all (map db/persist! repos))

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

@@ -330,49 +330,52 @@
 
 (rum/defc page-inner
   [config page-name href redirect-page-name page-entity contents-page? children html-export? label]
-  [:a.page-ref
-   {:data-ref page-name
-    :href href
-    :on-click (fn [e]
-                (util/stop e)
-                (let [create-first-block! (fn []
-                                            (when-not (editor-handler/add-default-title-property-if-needed! redirect-page-name)
-                                              (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)))]
-                  (if (gobj/get e "shiftKey")
-                   (do
-                     (js/setTimeout create-first-block! 310)
-                     (when-let [page-entity (db/entity [:block/name redirect-page-name])]
-                       (state/sidebar-add-block!
-                        (state/get-current-repo)
-                        (:db/id page-entity)
-                        :page
-                        {:page page-entity})))
-                   (do
-                     (create-first-block!)
-                     (route-handler/redirect! {:to :page
-                                               :path-params {:name redirect-page-name}}))))
-                (when (and contents-page?
-                           (state/get-left-sidebar-open?))
-                  (ui-handler/close-left-sidebar!)))}
-
-   (if (and (coll? children) (seq children))
-     (for [child children]
-       (if (= (first child) "Label")
-         (last child)
-         (let [{:keys [content children]} (last child)
-               page-name (subs content 2 (- (count content) 2))]
-           (rum/with-key (page-reference html-export? page-name (assoc config :children children) nil) page-name))))
-     (cond
-       (and label
-            (string? label)
-            (not (string/blank? label))) ; alias
-       label
+  (let [tag? (:tag? config)]
+    [:a
+     {:class (if tag? "tag" "page-ref")
+      :data-ref page-name
+      :href href
+      :on-click (fn [e]
+                  (util/stop e)
+                  (let [create-first-block! (fn []
+                                              (when-not (editor-handler/add-default-title-property-if-needed! redirect-page-name)
+                                                (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)))]
+                    (if (gobj/get e "shiftKey")
+                      (do
+                        (js/setTimeout create-first-block! 310)
+                        (when-let [page-entity (db/entity [:block/name redirect-page-name])]
+                          (state/sidebar-add-block!
+                           (state/get-current-repo)
+                           (:db/id page-entity)
+                           :page
+                           {:page page-entity})))
+                      (do
+                        (create-first-block!)
+                        (route-handler/redirect! {:to :page
+                                                  :path-params {:name redirect-page-name}}))))
+                  (when (and contents-page?
+                             (state/get-left-sidebar-open?))
+                    (ui-handler/close-left-sidebar!)))}
+
+     (if (and (coll? children) (seq children))
+       (for [child children]
+         (if (= (first child) "Label")
+           (last child)
+           (let [{:keys [content children]} (last child)
+                 page-name (subs content 2 (- (count content) 2))]
+             (rum/with-key (page-reference html-export? page-name (assoc config :children children) nil) page-name))))
+       (cond
+         (and label
+              (string? label)
+              (not (string/blank? label))) ; alias
+         label
 
-       (coll? label)
-       (->elem :span (map-inline config label))
+         (coll? label)
+         (->elem :span (map-inline config label))
 
-       :else
-       (get page-entity :block/original-name page-name)))])
+         :else
+         (let [s (get page-entity :block/original-name page-name)]
+           (if tag? (str "#" s) s))))]))
 
 (defn- use-delayed-open [open? page-name]
   "A react hook to debounce open? value.
@@ -737,19 +740,7 @@
     ["Tag" s]
     (when-let [s (block/get-tag item)]
       (let [s (text/page-ref-un-brackets! s)]
-        [:a.tag {:data-ref s
-                 :href (rfe/href :page {:name s})
-                 :on-click (fn [e]
-                             (let [repo (state/get-current-repo)
-                                   page (db/pull repo '[*] [:block/name (string/lower-case (util/url-decode s))])]
-                               (when (gobj/get e "shiftKey")
-                                 (state/sidebar-add-block!
-                                  repo
-                                  (:db/id page)
-                                  :page
-                                  {:page page})
-                                 (.preventDefault e))))}
-         (str "#" s)]))
+        (page-cp (assoc config :tag? true) {:block/name s})))
 
     ["Emphasis" [[kind] data]]
     (let [elem (case kind
@@ -800,10 +791,15 @@
           (block-reference (assoc config :reference? true) id label*))
 
         ["Page_ref" page]
-        (let [label* (if (seq (mldoc/plain->text label)) label nil)]
-          (if (and (string? page) (string/blank? page))
-            [:span (util/format "[[%s]]" page)]
-            (page-reference (:html-export? config) page config label*)))
+        (let [format (get-in config [:block :block/format])]
+          (if (and (= format :org)
+                   (show-link? config nil page page)
+                   (not (contains? #{"pdf" "mp4" "ogg" "webm"} (util/get-file-ext page))))
+            (image-link config url page nil nil page)
+            (let [label* (if (seq (mldoc/plain->text label)) label nil)]
+              (if (and (string? page) (string/blank? page))
+                [:span (util/format "[[%s]]" page)]
+                (page-reference (:html-export? config) page config label*)))))
 
         ["Search" s]
         (cond
@@ -1215,12 +1211,14 @@
 
 (rum/defcs block-control < rum/reactive
   [state config block uuid block-id body children collapsed? *ref-collapsed? *control-show?]
-  (let [has-child? (and
+  (let [has-children-blocks? (and (coll? children) (seq children))
+        has-child? (and
                     (not (:pre-block? block))
-                    (or (and (coll? children) (seq children))
-                        (seq body)))
+                    (or has-children-blocks? (seq body)))
         control-show? (and
-                       (seq (:block/title block))
+                       (or (and (seq (:block/title block))
+                                (seq body))
+                           has-children-blocks?)
                        (util/react *control-show?))
         ref-collapsed? (util/react *ref-collapsed?)
         dark? (= "dark" (state/sub :ui/theme))
@@ -1460,10 +1458,7 @@
            (rum/with-key elem (str (random-uuid)))))
 
        :else
-       (let [page-name (string/lower-case (str v))]
-         (if (db/entity [:block/name page-name])
-           (page-cp config {:block/name page-name})
-           (inline-text (:block/format block) (str v)))))]))
+       (inline-text (:block/format block) (str v)))]))
 
 (rum/defc properties-cp
   [config block]
@@ -2116,11 +2111,13 @@
            ;; exclude the current one, otherwise it'll loop forever
            remove-blocks (if current-block-uuid [current-block-uuid] nil)
            query-result (and query-atom (rum/react query-atom))
+           not-grouped-by-page? (and (string? query) (string/includes? query "(by-page false)"))
            result (when query-result
-                    (db/custom-query-result-transform query-result remove-blocks q))
+                    (db/custom-query-result-transform query-result remove-blocks q not-grouped-by-page?))
            view-f (and view (sci/eval-string (pr-str view)))
            only-blocks? (:block/uuid (first result))
            blocks-grouped-by-page? (and (seq result)
+                                        (not not-grouped-by-page?)
                                         (coll? (first result))
                                         (:block/name (ffirst result))
                                         (:block/uuid (first (second (first result))))
@@ -2161,8 +2158,7 @@
                                                               :path-params {:name name}})))}
                   (or original-name name)]])]
 
-             (and (seq result)
-                  (or only-blocks? blocks-grouped-by-page?))
+             (and (seq result) (or only-blocks? blocks-grouped-by-page?))
              (->hiccup result (cond-> (assoc config
                                              :custom-query? true
                                              ;; :breadcrumb-show? true

+ 170 - 138
src/main/frontend/components/page.cljs

@@ -295,14 +295,14 @@
               [:div.flex.flex-row.space-between
                [:div.flex-1.flex-row
                 [:a.page-title {:on-click (fn [e]
-                                 (.preventDefault e)
-                                 (when (gobj/get e "shiftKey")
-                                   (when-let [page (db/pull repo '[*] [:block/name page-name])]
-                                     (state/sidebar-add-block!
-                                      repo
-                                      (:db/id page)
-                                      :page
-                                      {:page page}))))}
+                                            (.preventDefault e)
+                                            (when (gobj/get e "shiftKey")
+                                              (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                                                (state/sidebar-add-block!
+                                                 repo
+                                                 (:db/id page)
+                                                 :page
+                                                 {:page page}))))}
                  [:h1.title {:style {:margin-left -2}}
                   (if page-original-name
                     (if (and (string/includes? page-original-name "[[")
@@ -316,66 +316,66 @@
                (when (not config/publishing?)
                  (let [contents? (= (string/lower-case (str page-name)) "contents")
                        links (fn [] (->>
-                                    [(when-not contents?
-                                       {:title   (t :page/add-to-favorites)
-                                        :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
-
-                                     {:title "Go to presentation mode"
-                                      :options {:on-click (fn []
-                                                            (state/sidebar-add-block!
-                                                             repo
-                                                             (:db/id page)
-                                                             :page-presentation
-                                                             {:page page}))}}
-                                     (when (and (not contents?)
-                                                (not journal?))
-                                       {:title   (t :page/rename)
-                                        :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
-
-                                     (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
-                                       [{:title   (t :page/open-in-finder)
-                                         :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
-                                        {:title   (t :page/open-with-default-app)
-                                         :options {:on-click #(js/window.apis.openPath file-path)}}])
-
-                                     (when-not contents?
-                                       {:title   (t :page/delete)
-                                        :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
-
-                                     (when (state/get-current-page)
-                                       {:title   (t :export)
-                                        :options {:on-click #(state/set-modal! export/export-page)}})
-
-                                     (when (util/electron?)
-                                       {:title   (t (if public? :page/make-private :page/make-public))
-                                        :options {:on-click
-                                                  (fn []
-                                                    (page-handler/update-public-attribute!
-                                                     page-name
-                                                     (if public? false true))
-                                                    (state/close-modal!))}})
-
-                                     (when plugin-handler/lsp-enabled?
-                                       (for [[_ {:keys [key label] :as cmd} action pid] (state/get-plugins-commands-with-type :page-menu-item)]
-                                         {:title label
-                                          :options {:on-click #(commands/exec-plugin-simple-command!
+                                     [(when-not contents?
+                                        {:title   (t :page/add-to-favorites)
+                                         :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                                      {:title "Go to presentation mode"
+                                       :options {:on-click (fn []
+                                                             (state/sidebar-add-block!
+                                                              repo
+                                                              (:db/id page)
+                                                              :page-presentation
+                                                              {:page page}))}}
+                                      (when (and (not contents?)
+                                                 (not journal?))
+                                        {:title   (t :page/rename)
+                                         :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
+
+                                      (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
+                                        [{:title   (t :page/open-in-finder)
+                                          :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                                         {:title   (t :page/open-with-default-app)
+                                          :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                                      (when-not contents?
+                                        {:title   (t :page/delete)
+                                         :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
+                                      (when (state/get-current-page)
+                                        {:title   (t :export)
+                                         :options {:on-click #(state/set-modal! export/export-page)}})
+
+                                      (when (util/electron?)
+                                        {:title   (t (if public? :page/make-private :page/make-public))
+                                         :options {:on-click
+                                                   (fn []
+                                                     (page-handler/update-public-attribute!
+                                                      page-name
+                                                      (if public? false true))
+                                                     (state/close-modal!))}})
+
+                                      (when plugin-handler/lsp-enabled?
+                                        (for [[_ {:keys [key label] :as cmd} action pid] (state/get-plugins-commands-with-type :page-menu-item)]
+                                          {:title label
+                                           :options {:on-click #(commands/exec-plugin-simple-command!
                                                                  pid (assoc cmd :page (state/get-current-page)) action)}}))
 
-                                     (when developer-mode?
-                                       {:title   "(Dev) Show page data"
-                                        :options {:on-click (fn []
-                                                              (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
-                                                                (println page-data)
-                                                                (notification/show!
-                                                                 [:div
-                                                                  [:pre.code page-data]
-                                                                  [:br]
-                                                                  (ui/button "Copy to clipboard"
-                                                                    :on-click #(.writeText js/navigator.clipboard page-data))]
-                                                                 :success
-                                                                 false)))}})]
-                                    (flatten)
-                                    (remove nil?)))]
+                                      (when developer-mode?
+                                        {:title   "(Dev) Show page data"
+                                         :options {:on-click (fn []
+                                                               (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
+                                                                 (println page-data)
+                                                                 (notification/show!
+                                                                  [:div
+                                                                   [:pre.code page-data]
+                                                                   [:br]
+                                                                   (ui/button "Copy to clipboard"
+                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
+                                                                  :success
+                                                                  false)))}})]
+                                     (flatten)
+                                     (remove nil?)))]
                    [:div.flex.flex-row
 
                     (when plugin-handler/lsp-enabled?
@@ -497,63 +497,63 @@
                   (util/format "%d page%s" c1 s1)
                   )]
                [:div.p-6
-               ;; [:div.flex.items-center.justify-between.mb-2
-               ;;  [:span "Layout"]
-               ;;  (ui/select
-               ;;    (mapv
-               ;;     (fn [item]
-               ;;       (if (= (:label item) layout)
-               ;;         (assoc item :selected "selected")
-               ;;         item))
-               ;;     [{:label "gForce"}
-               ;;      {:label "dagre"}])
-               ;;    (fn [value]
-               ;;      (set-setting! :layout value))
-               ;;    "graph-layout")]
-               [:div.flex.items-center.justify-between.mb-2
-                [:span "Journals"]
-                ;; FIXME: why it's not aligned well?
-                [:div.mt-1
-                 (ui/toggle journal?
-                            (fn []
-                              (let [value (not journal?)]
-                                (reset! *journal? value)
-                                (set-setting! :journal? value)))
-                            true)]]
-               [:div.flex.items-center.justify-between.mb-2
-                [:span "Orphan pages"]
-                [:div.mt-1
-                 (ui/toggle orphan-pages?
-                            (fn []
-                              (let [value (not orphan-pages?)]
-                                (reset! *orphan-pages? value)
-                                (set-setting! :orphan-pages? value)))
-                            true)]]
-               [:div.flex.items-center.justify-between.mb-2
-                [:span "Built-in pages"]
-                [:div.mt-1
-                 (ui/toggle builtin-pages?
-                            (fn []
-                              (let [value (not builtin-pages?)]
-                                (reset! *builtin-pages? value)
-                                (set-setting! :builtin-pages? value)))
-                            true)]]
-               (when (seq focus-nodes)
-                 [:div.flex.flex-col.mb-2
-                  [:p {:title "N hops from selected nodes"}
-                   "N hops from selected nodes"]
-                  (ui/tippy {:html [:div.pr-3 n-hops]}
-                            (ui/slider (or n-hops 10)
-                                       {:min 1
-                                        :max 10
-                                        :on-change #(reset! *n-hops (int %))}))])
-
-               [:a.opacity-70.opacity-100 {:on-click (fn []
-                                                       (swap! *graph-reset? not)
-                                                       (reset! *focus-nodes [])
-                                                       (reset! *n-hops nil)
-                                                       (state/clear-search-filters!))}
-                "Reset Graph"]]])))
+                ;; [:div.flex.items-center.justify-between.mb-2
+                ;;  [:span "Layout"]
+                ;;  (ui/select
+                ;;    (mapv
+                ;;     (fn [item]
+                ;;       (if (= (:label item) layout)
+                ;;         (assoc item :selected "selected")
+                ;;         item))
+                ;;     [{:label "gForce"}
+                ;;      {:label "dagre"}])
+                ;;    (fn [value]
+                ;;      (set-setting! :layout value))
+                ;;    "graph-layout")]
+                [:div.flex.items-center.justify-between.mb-2
+                 [:span "Journals"]
+                 ;; FIXME: why it's not aligned well?
+                 [:div.mt-1
+                  (ui/toggle journal?
+                             (fn []
+                               (let [value (not journal?)]
+                                 (reset! *journal? value)
+                                 (set-setting! :journal? value)))
+                             true)]]
+                [:div.flex.items-center.justify-between.mb-2
+                 [:span "Orphan pages"]
+                 [:div.mt-1
+                  (ui/toggle orphan-pages?
+                             (fn []
+                               (let [value (not orphan-pages?)]
+                                 (reset! *orphan-pages? value)
+                                 (set-setting! :orphan-pages? value)))
+                             true)]]
+                [:div.flex.items-center.justify-between.mb-2
+                 [:span "Built-in pages"]
+                 [:div.mt-1
+                  (ui/toggle builtin-pages?
+                             (fn []
+                               (let [value (not builtin-pages?)]
+                                 (reset! *builtin-pages? value)
+                                 (set-setting! :builtin-pages? value)))
+                             true)]]
+                (when (seq focus-nodes)
+                  [:div.flex.flex-col.mb-2
+                   [:p {:title "N hops from selected nodes"}
+                    "N hops from selected nodes"]
+                   (ui/tippy {:html [:div.pr-3 n-hops]}
+                             (ui/slider (or n-hops 10)
+                                        {:min 1
+                                         :max 10
+                                         :on-change #(reset! *n-hops (int %))}))])
+
+                [:a.opacity-70.opacity-100 {:on-click (fn []
+                                                        (swap! *graph-reset? not)
+                                                        (reset! *focus-nodes [])
+                                                        (reset! *n-hops nil)
+                                                        (state/clear-search-filters!))}
+                 "Reset Graph"]]])))
           (graph-filter-section
            [:span.font-medium "Search"]
            (fn [open?]
@@ -674,36 +674,68 @@
     (when (seq (:nodes graph))
       (page-graph-inner graph dark?))))
 
-(rum/defc all-pages < rum/reactive
+(defn- sort-pages-by
+  [by-item desc? pages]
+  (let [comp (if desc? > <)
+        by-item (if (= by-item :block/name)
+                  (fn [x] (string/lower-case (:block/name x)))
+                  by-item)]
+    (sort-by by-item comp pages)))
+
+(rum/defc sortable-title
+  [title key by-item desc?]
+  [:th
+   [:a {:on-click (fn []
+                    (reset! by-item key)
+                    (swap! desc? not))}
+    [:div.flex.items-center
+     [:span.mr-1 title]
+     (when (= @by-item key)
+       [:span
+        (if @desc? (svg/caret-down) (svg/caret-up))])]]])
+
+(rum/defcs all-pages < rum/reactive
+  (rum/local :block/updated-at ::sort-by-item)
+  (rum/local true ::desc?)
   ;; {:did-mount (fn [state]
   ;;               (let [current-repo (state/sub :git/current-repo)]
   ;;                 (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
   ;;               state)}
-  []
-  (let [current-repo (state/sub :git/current-repo)]
+  [state]
+  (let [current-repo (state/sub :git/current-repo)
+        *sort-by-item (get state ::sort-by-item)
+        *desc? (get state ::desc?)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.flex-1
        [:h1.title (t :all-pages)]
        (when current-repo
-         (let [pages (page-handler/get-pages-with-modified-at current-repo)]
+         (let [pages (->> (page-handler/get-all-pages current-repo)
+                         (map (fn [page] (assoc page :block/backlinks (count (:block/_refs (db/entity (:db/id page)))))))
+                         (sort-pages-by @*sort-by-item @*desc?))]
            [:table.table-auto
             [:thead
              [:tr
-              [:th (t :block/name)]
-              [:th (t :file/last-modified-at)]]]
+              (sortable-title (t :block/name) :block/name *sort-by-item *desc?)
+              (sortable-title (t :page/backlinks) :block/backlinks  *sort-by-item *desc?)
+              (sortable-title (t :page/created-at) :block/created-at *sort-by-item *desc?)
+              (sortable-title (t :page/updated-at) :block/updated-at *sort-by-item *desc?)]]
             [:tbody
-             (for [page pages]
-               [:tr {:key page}
+             (for [{:block/keys [name created-at updated-at backlinks] :as page} pages]
+               [:tr {:key name}
                 [:td [:a {:on-click (fn [e]
-                                      (let [repo (state/get-current-repo)
-                                            page (db/pull repo '[*] [:block/name (string/lower-case page)])]
+                                      (let [repo (state/get-current-repo)]
                                         (when (gobj/get e "shiftKey")
                                           (state/sidebar-add-block!
                                            repo
                                            (:db/id page)
                                            :page
-                                           {:page page}))))
-                          :href (rfe/href :page {:name page})}
-                      page]]
-                [:td [:span.text-gray-500.text-sm
-                      (t :file/no-data)]]])]]))])))
+                                           {:page (:block/name page)}))))
+                          :href (rfe/href :page {:name (:block/name page)})}
+                      (block/page-cp {} page)]]
+                [:td [:span.text-gray-500.text-sm backlinks]]
+                [:td [:span.text-gray-500.text-sm (if created-at
+                                                    (date/int->local-time created-at)
+                                                    "Unknown")]]
+                [:td [:span.text-gray-500.text-sm (if updated-at
+                                                    (date/int->local-time updated-at)
+                                                    "Unknown")]]])]]))])))

+ 1 - 1
src/main/frontend/components/page.css

@@ -68,7 +68,7 @@
 /* Change to another cursor style if Shift key is active */
 [data-active-keystroke*="Shift" i]
 :is(.journal-title, .page-title,
-    .block-ref, .page-ref, a.tag, 
+    .block-ref, .page-ref, a.tag,
     .bullet-container.cursor) {
   cursor: e-resize;
 }

+ 10 - 1
src/main/frontend/components/settings.cljs

@@ -342,6 +342,14 @@
             (let [value (not enable-all-pages-public?)]
               (config-handler/set-config! :publishing/all-pages-public? value)))))
 
+(defn enable-block-timestamps-row [t enable-block-timestamps?]
+  (toggle "block timestamps"
+          (t :settings-page/enable-block-time)
+          enable-block-timestamps?
+          (fn []
+            (let [value (not enable-block-timestamps?)]
+              (config-handler/set-config! :feature/enable-block-timestamps? value)))))
+
 (defn encryption-row [t enable-encryption?]
   (toggle "enable_encryption"
           (t :settings-page/enable-encryption)
@@ -430,7 +438,7 @@
         logical-outdenting? (state/logical-outdenting?)
         enable-tooltip? (state/enable-tooltip?)
         enable-git-auto-push? (state/enable-git-auto-push? current-repo)
-        enable-block-time? (state/enable-block-time?)
+        enable-block-timestamps? (state/enable-block-timestamps?)
         show-brackets? (state/show-brackets?)
         github-token (state/sub [:me :access-token])
         cors-proxy (state/sub [:me :cors_proxy])
@@ -457,6 +465,7 @@
         (file-format-row t preferred-format)
         (date-format-row t preferred-date-format)
         (workflow-row t preferred-workflow)
+        (enable-block-timestamps-row t enable-block-timestamps?)
         (show-brackets-row t show-brackets?)
         (outdenting-row t logical-outdenting?)
         (tooltip-row t enable-tooltip?)

+ 11 - 0
src/main/frontend/components/svg.cljs

@@ -310,6 +310,17 @@
         c37.027-10.806,61.375,4.323,61.375,4.323C218.946,192.781,201.513,216.553,201.513,216.553z"}]])
 
 
+(rum/defc caret-up
+  []
+  [:svg.h-4.w-4
+   {:aria-hidden "true"
+    :version "1.1"
+    :view-box "0 0 320 512"
+    :fill "currentColor"
+    :display "inline-block"}
+   [:path {:d "M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"}]])
+
+
 (rum/defc caret-down
   []
   [:svg.h-4.w-4

+ 8 - 0
src/main/frontend/config.cljs

@@ -292,6 +292,7 @@
 (def config-file "config.edn")
 (def custom-css-file "custom.css")
 (def metadata-file "metadata.edn")
+(def pages-metadata-file "pages-metadata.edn")
 
 (def config-default-content (rc/inline "config.edn"))
 
@@ -369,6 +370,13 @@
    (when repo
      (get-file-path repo (str app-name "/" metadata-file)))))
 
+(defn get-pages-metadata-path
+  ([]
+   (get-pages-metadata-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo (str app-name "/" pages-metadata-file)))))
+
 (defn get-custom-css-path
   ([]
    (get-custom-css-path (state/get-current-repo)))

+ 6 - 0
src/main/frontend/date.cljs

@@ -191,6 +191,12 @@
   (when day
     (format (tf/parse (tf/formatter "yyyyMMdd") (str day)))))
 
+(defn journal-day->ts
+  [day]
+  (when day
+    (-> (tf/parse (tf/formatter "yyyyMMdd") (str day))
+        (tc/to-long))))
+
 (defn journal-title->long
   [journal-title]
   (journal-title-> journal-title #(tc/to-long %)))

+ 23 - 13
src/main/frontend/db/query_dsl.cljs

@@ -299,16 +299,23 @@
                      (keyword (string/lower-case (name order)))
                      :desc)
              k (-> (string/lower-case (name k))
-                   (string/replace "-" "_"))]
-         (when (contains? #{"created_at" "last_modified_at"} k)
-           (let [comp (if (= order :desc) >= <=)]
-             (reset! sort-by
-                     (fn [result]
-                       (->> result
-                            flatten
-                            (clojure.core/sort-by #(get-in % [:block/properties k])
-                                                  comp))))
-             nil)))
+                   (string/replace "_" "-"))]
+         (let [get-value (cond
+                           (= k "created-at")
+                           :block/created-at
+
+                           (= k "updated-at")
+                           :block/updated-at
+
+                           :else
+                           #(get-in % [:block/properties k]))
+               comp (if (= order :desc) >= <=)]
+           (reset! sort-by
+                   (fn [result]
+                     (->> result
+                          flatten
+                          (clojure.core/sort-by get-value comp))))
+           nil))
 
        (= 'page fe)
        (let [page-name (string/lower-case (first (rest e)))
@@ -442,9 +449,12 @@
               result)
             (when-let [query (query-wrapper query blocks?)]
               (react/react-query repo
-                                 {:query query}
-                                 (if sort-by
-                                   {:transform-fn sort-by})))))))))
+                                 {:query query
+                                  :query-string query-string}
+                                 (cond->
+                                   {:use-cache? false}
+                                   sort-by
+                                   (assoc :transform-fn sort-by))))))))))
 
 (defn custom-query
   [repo query-m query-opts]

+ 2 - 2
src/main/frontend/db/query_react.cljs

@@ -50,7 +50,7 @@
     input))
 
 (defn custom-query-result-transform
-  [query-result remove-blocks q]
+  [query-result remove-blocks q not-grouped-by-page?]
   (try
     (let [repo (state/get-current-repo)
           result (db-utils/seq-flatten query-result)
@@ -75,7 +75,7 @@
                 (log/error :sci/call-error e)
                 result))
             result)
-          (if block?
+          (if (and block? (not not-grouped-by-page?))
             (db-utils/group-by-page result)
             result))))
     (catch js/Error e

+ 3 - 0
src/main/frontend/dicts.cljs

@@ -178,6 +178,9 @@
         :file/last-modified-at "Last modified at"
         :file/no-data "No data"
         :file/format-not-supported "Format .{1} is not supported."
+        :page/created-at "Created At"
+        :page/updated-at "Updated At"
+        :page/backlinks "Back Links"
         :editor/block-search "Search for a block"
         :editor/image-uploading "Uploading"
         :draw/invalid-file "Could not load this invalid excalidraw file"

+ 1 - 2
src/main/frontend/extensions/graph.cljs

@@ -63,8 +63,7 @@
       (let [page-name (string/lower-case node)]
         (.unhoverNode ^js graph node)
         (route-handler/redirect! {:to :page
-                                  :path-params {:name page-name}})
-        ))))
+                                  :path-params {:name page-name}})))))
 
 (defn reset-graph!
   [^js graph]

+ 8 - 1
src/main/frontend/format/block.cljs

@@ -555,7 +555,14 @@
                                   (with-page-refs with-id?)
                                   with-block-refs
                                   block-tags->pages)
-                        last-pos' (get-in block [:meta :start-pos])]
+                        last-pos' (get-in block [:meta :start-pos])
+                        {:keys [created-at updated-at]} (:properties properties)
+                        block (cond-> block
+                                (and created-at (integer? created-at))
+                                (assoc :block/created-at created-at)
+
+                                (and updated-at (integer? updated-at))
+                                (assoc :block/updated-at updated-at))]
                     (recur (conj headings block) [] (rest blocks) {} {} last-pos' (:level block) children []))
 
                   :else

+ 22 - 18
src/main/frontend/fs.cljs

@@ -43,6 +43,10 @@
   [dir]
   (protocol/mkdir! (get-fs dir) dir))
 
+(defn mkdir-recur!
+  [dir]
+  (protocol/mkdir-recur! (get-fs dir) dir))
+
 (defn readdir
   [dir]
   (protocol/readdir (get-fs dir) dir))
@@ -61,20 +65,20 @@
   [repo dir path content opts]
   (when content
     (let [fs-record (get-fs dir)]
-      (p/let [metadata-or-css? (or (string/ends-with? path config/metadata-file)
-                                  (string/ends-with? path config/custom-css-file))
-             content (if metadata-or-css? content (encrypt/encrypt content))]
-       (->
-        (p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
-          (when (= bfs-record fs-record)
-            (db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
-        (p/catch (fn [error]
-                   (log/error :file/write-failed {:dir dir
-                                                  :path path
-                                                  :error error})
-                   ;; Disable this temporarily
-                   ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
-                   )))))))
+      (p/let [metadata-or-css? (or (string/ends-with? path (str "/" config/metadata-file))
+                                   (string/ends-with? path (str "/" config/custom-css-file)))
+              content (if metadata-or-css? content (encrypt/encrypt content))]
+        (->
+         (p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
+           (when (= bfs-record fs-record)
+             (db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
+         (p/catch (fn [error]
+                    (log/error :file/write-failed {:dir dir
+                                                   :path path
+                                                   :error error})
+                    ;; Disable this temporarily
+                    ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
+                    )))))))
 
 (defn read-file
   ([dir path]
@@ -90,7 +94,7 @@
 (defn rename!
   [repo old-path new-path]
   (cond
-    ; See https://github.com/isomorphic-git/lightning-fs/issues/41
+                                        ; See https://github.com/isomorphic-git/lightning-fs/issues/41
     (= old-path new-path)
     (p/resolved nil)
 
@@ -146,9 +150,9 @@
       (p/let [stat (stat dir path)]
         true)
       (p/catch
-       (fn [_error]
-         (p/let [_ (write-file! repo dir path initial-content nil)]
-           false)))))))
+          (fn [_error]
+            (p/let [_ (write-file! repo dir path initial-content nil)]
+              false)))))))
 
 (defn file-exists?
   [dir path]

+ 3 - 1
src/main/frontend/fs/node.cljs

@@ -65,12 +65,14 @@
   protocol/Fs
   (mkdir! [this dir]
     (ipc/ipc "mkdir" dir))
+  (mkdir-recur! [this dir]
+    (ipc/ipc "mkdir-recur" dir))
   (readdir [this dir]                   ; recursive
     (ipc/ipc "readdir" dir))
   (unlink! [this path _opts]
     (ipc/ipc "unlink" path))
   (rmdir! [this dir]
-    nil)
+    (ipc/ipc "rmdir-recur" dir))
   (read-file [this dir path _options]
     (let [path (concat-path dir path)]
       (ipc/ipc "readFile" path)))

+ 1 - 0
src/main/frontend/fs/protocol.cljs

@@ -2,6 +2,7 @@
 
 (defprotocol Fs
   (mkdir! [this dir])
+  (mkdir-recur! [this dir])
   (readdir [this dir])
   (unlink! [this path opts])
   (rmdir! [this dir])

+ 20 - 0
src/main/frontend/handler/common.cljs

@@ -193,3 +193,23 @@
   {:title page-name
    ;; :date (date/get-date-time-string)
    })
+
+(defn fix-pages-timestamps
+  [pages]
+  (map (fn [{:block/keys [name created-at updated-at journal-day] :as p}]
+         (cond->
+           p
+
+           (nil? created-at)
+           (assoc :block/created-at
+                  (if journal-day
+                    (date/journal-day->ts journal-day)
+                    (util/time-ms)))
+
+           (nil? updated-at)
+           (assoc :block/updated-at
+                  ;; Not exact true
+                  (if journal-day
+                    (date/journal-day->ts journal-day)
+                    (util/time-ms)))))
+    pages))

+ 3 - 0
src/main/frontend/handler/dnd.cljs

@@ -42,6 +42,9 @@
     (cond
       alt-key?
       (do
+        (editor-handler/set-block-property! (:block/uuid current-block)
+                                            :id
+                                            (str (:block/uuid current-block)))
         (editor-handler/api-insert-new-block!
          (util/format "((%s))" (str (:block/uuid current-block)))
          {:block-uuid (:block/uuid target-block)

+ 27 - 22
src/main/frontend/handler/editor.cljs

@@ -635,13 +635,17 @@
                                  last-block-id (:db/id (last blocks))]
                              (when last-block-id
                                (db/pull last-block-id))))
+              format (or
+                      (:block/format block)
+                      (db/get-page-format (:db/id block))
+                      :markdown)
+              content (if (seq properties)
+                        (property/insert-properties format content properties)
+                        content)
               new-block (-> (select-keys block [:block/page :block/file :block/journal?
                                                 :block/journal-day])
                             (assoc :block/content content
-                                   :block/format (or
-                                                  (:block/format block)
-                                                  (db/get-page-format (:db/id block))
-                                                  :markdown))
+                                   :block/format format)
                             (wrap-parse-block)
                             (assoc :block/uuid (db/new-block-id)))
               new-block (if (:block/page new-block)
@@ -649,9 +653,6 @@
                           (assoc new-block :block/page (:db/id block)))
               new-block (if-let [db-id (:db/id (:block/file block))]
                           (assoc new-block :block/file db-id)
-                          new-block)
-              new-block (if (and (map? properties) (seq properties))
-                          (update new-block :block/properties (fn [m] (merge m properties)))
                           new-block)]
           (let [[block-m sibling?] (cond
                                      before?
@@ -895,29 +896,31 @@
                         (property/insert-property format content key value))
               block (outliner-core/block {:block/uuid block-id
                                           :block/properties properties
-                                          :block/content content})]
+                                          :block/content content})
+              input-pos (or (state/get-edit-pos) :max)]
           (outliner-core/save-node block)
 
+          (db/refresh! (state/get-current-repo)
+                       {:key :block/change
+                        :data [(db/pull [:block/uuid block-id])]})
+
           ;; update editing input content
           (when-let [editing-block (state/get-edit-block)]
-            (and (= (:block/uuid editing-block) block-id)
-                 (state/set-edit-content! (state/get-edit-input-id) content))))))))
+            (when (= (:block/uuid editing-block) block-id)
+              (edit-block! editing-block
+                           input-pos
+                           format
+                           (state/get-edit-input-id)))))))))
 
 (defn remove-block-property!
   [block-id key]
   (let [key (keyword key)]
-    (block-property-aux! block-id key nil))
-  (db/refresh! (state/get-current-repo)
-               {:key :block/change
-                :data [(db/pull [:block/uuid block-id])]}))
+    (block-property-aux! block-id key nil)))
 
 (defn set-block-property!
   [block-id key value]
   (let [key (keyword key)]
-    (block-property-aux! block-id key value))
-  (db/refresh! (state/get-current-repo)
-               {:key :block/change
-                :data [(db/pull [:block/uuid block-id])]}))
+    (block-property-aux! block-id key value)))
 
 (defn set-block-timestamp!
   [block-id key value]
@@ -1051,7 +1054,7 @@
 (defn copy-selection-blocks
   []
   (when-let [blocks (seq (get-selected-blocks-with-children))]
-    (let [repo (dom/attr (first blocks) "repo")
+    (let [repo (state/get-current-repo)
           ids (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
                                      (uuid id)) blocks))
                    (remove nil?))
@@ -1068,7 +1071,7 @@
     ;; remove embeds and references
     (let [blocks (remove (fn [block] (= "true" (dom/attr block "data-transclude"))) blocks)]
       (when (seq blocks)
-        (let [repo (dom/attr (first blocks) "repo")
+        (let [repo (state/get-current-repo)
               ids (distinct (map #(uuid (dom/attr % "blockid")) blocks))]
           (delete-blocks! repo ids))))))
 
@@ -2937,7 +2940,8 @@
   (remove-block-property! block-id :collapsed))
 
 (defn expand!
-  []
+  [e]
+  (util/stop e)
   (cond
     (state/editing?)
     (when-let [block-id (:block/uuid (state/get-edit-block))]
@@ -2970,7 +2974,8 @@
                 (expand-block! uuid)))))))))
 
 (defn collapse!
-  []
+  [e]
+  (util/stop e)
   (cond
     (state/editing?)
     (when-let [block-id (:block/uuid (state/get-edit-block))]

+ 11 - 0
src/main/frontend/handler/file.cljs

@@ -300,6 +300,17 @@
       (when-not file-exists?
         (reset-file! repo-url path default-content)))))
 
+(defn create-pages-metadata-file
+  [repo-url]
+  (let [repo-dir (config/get-repo-dir repo-url)
+        path (str config/app-name "/" config/pages-metadata-file)
+        file-path (str "/" path)
+        default-content "{}"]
+    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+            file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
+      (when-not file-exists?
+        (reset-file! repo-url path default-content)))))
+
 (defn edn-file-set-key-value
   [path k v]
   (when-let [repo (state/get-current-repo)]

+ 21 - 2
src/main/frontend/handler/metadata.cljs

@@ -4,9 +4,13 @@
             [cljs.reader :as reader]
             [frontend.config :as config]
             [frontend.db :as db]
+            [frontend.fs :as fs]
             [datascript.db :as ddb]
             [clojure.string :as string]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [frontend.util :as util]
+            [frontend.date :as date]
+            [frontend.handler.common :as common-handler]))
 
 (def default-metadata-str "{}")
 
@@ -33,10 +37,25 @@
               new-metadata (if encrypted?
                              (assoc new-metadata :db/encrypted? true)
                              new-metadata)
-              _ (prn "New metadata:\n" new-metadata)
               new-content (pr-str new-metadata)]
           (file-handler/set-file-content! repo path new-content))))))
 
+(defn set-pages-metadata!
+  [repo]
+  (let [path (config/get-pages-metadata-path)
+        all-pages (->> (db/get-all-pages repo)
+                       (common-handler/fix-pages-timestamps)
+                       (map #(select-keys % [:block/name :block/created-at :block/updated-at]))
+                       (vec))]
+    (-> (file-handler/create-pages-metadata-file repo)
+        (p/finally (fn []
+                     (let [new-content (pr-str all-pages)]
+                       (fs/write-file! repo
+                                       (config/get-repo-dir repo)
+                                       path
+                                       new-content
+                                       {})))))))
+
 (defn set-db-encrypted-secret!
   [encrypted-secret]
   (when-not (string/blank? encrypted-secret)

+ 7 - 6
src/main/frontend/handler/page.cljs

@@ -450,14 +450,15 @@
      (init-commands!)
      (shortcut/refresh!))))
 
-;; TODO: add use :file/last-modified-at
-(defn get-pages-with-modified-at
+(defn get-all-pages
   [repo]
-  (->> (db/get-modified-pages repo)
-       (remove util/file-page?)
-       (remove util/uuid-string?)
+  (->> (db/get-all-pages)
        (remove (fn [p]
-                 (db/built-in-pages-names (string/upper-case p))))))
+                 (let [name (:block/name p)]
+                   (or (util/file-page? name)
+                       (util/uuid-string? name)
+                       (db/built-in-pages-names (string/upper-case name))))))
+       (common-handler/fix-pages-timestamps)))
 
 (defn get-filters
   [page-name]

+ 26 - 0
src/main/frontend/handler/repo.cljs

@@ -184,6 +184,31 @@
                      (remove-non-exists-refs!))]
     (db/transact! repo-url all-data)))
 
+(defn- load-pages-metadata!
+  [repo file-paths files]
+  (try
+    (let [file (config/get-pages-metadata-path)]
+      (when (contains? (set file-paths) file)
+        (when-let [content (some #(when (= (:file/path %) file) (:file/content %)) files)]
+          (let [metadata (common-handler/safe-read-string content "Parsing pages metadata file failed: ")
+                pages (db/get-all-pages repo)
+                pages (zipmap (map :block/name pages) pages)
+                metadata (->>
+                          (filter (fn [{:block/keys [name created-at updated-at]}]
+                                    (when-let [page (get pages name)]
+                                      (and
+                                       (or
+                                        (nil? (:block/created-at page))
+                                        (>= created-at (:block/created-at page)))
+                                       (or
+                                        (nil? (:block/updated-at page))
+                                        (>= updated-at (:block/created-at page)))))) metadata)
+                          (remove nil?))]
+            (when (seq metadata)
+              (db/transact! repo metadata))))))
+    (catch js/Error e
+      (log/error :exception e))))
+
 (defn- parse-files-and-create-default-files-inner!
   [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata]
   (let [parsed-files (filter
@@ -200,6 +225,7 @@
                                     (:file/content %)) files)]
           (file-handler/restore-config! repo-url content true))))
     (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
+    (load-pages-metadata! repo-url file-paths files)
     (when first-clone?
       (if (and (not db-encrypted?) (state/enable-encryption? repo-url))
         (state/pub-event! [:modal/encryption-setup-dialog repo-url

+ 31 - 34
src/main/frontend/modules/outliner/core.cljs

@@ -10,7 +10,8 @@
             [clojure.set :as set]
             [clojure.zip :as zip]
             [frontend.modules.outliner.datascript :as ds]
-            [frontend.util :as util]))
+            [frontend.util :as util]
+            [frontend.util.property :as property]))
 
 (defrecord Block [data])
 
@@ -59,29 +60,6 @@
      (outliner-state/get-by-parent-id repo [:block/uuid id])
      (mapv block))))
 
-;; TODO: we might need to store created-at and updated-at as datom attributes
-;; instead of being attributes of properties.
-;; which might improve the db performance, we can improve it later
-(defn- with-timestamp
-  [m]
-  (let [updated-at (util/time-ms)
-        properties (assoc (:block/properties m)
-                          :id (:block/uuid m)
-                          :updated-at updated-at)
-        properties (if-let [created-at (get properties :created-at)]
-                     properties
-                     (assoc properties :created-at updated-at))
-        m (assoc m :block/properties properties)
-        page-id (or (get-in m [:block/page :db/id])
-                    (:db/id (:block/page (db/entity (:db/id m)))))
-        page (db/entity page-id)
-        page-properties (:block/properties page)
-        page-tx {:db/id page-id
-                 :block/properties (assoc page-properties
-                                          :updated-at updated-at
-                                          :created-at (get page-properties :created-at updated-at))}]
-    [m page-tx]))
-
 (defn- update-block-unordered
   [block]
   (let [parent (:block/parent block)
@@ -91,6 +69,19 @@
       (assoc block :block/unordered false)
       (assoc block :block/unordered true))))
 
+(defn- block-with-timestamps
+  [block]
+  (let [updated-at (util/time-ms)
+        block (cond->
+                (assoc block :block/updated-at updated-at)
+                (nil? (:block/created-at block))
+                (assoc :block/created-at updated-at))
+        content (property/insert-properties (:block/format block)
+                                            (or (:block/content block) "")
+                                            {:created-at (:block/created-at block)
+                                             :updated-at (:block/updated-at block)})]
+    (assoc block :block/content content)))
+
 ;; -get-id, -get-parent-id, -get-left-id return block-id
 ;; the :block/parent, :block/left should be datascript lookup ref
 
@@ -143,26 +134,32 @@
           m (-> (:data this)
                 (dissoc :block/children :block/meta)
                 (util/remove-nils))
-          other-tx (:db/other-tx m)]
+          m (if (state/enable-block-timestamps?) (block-with-timestamps m) m)
+          other-tx (:db/other-tx m)
+          id (:db/id (:data this))]
       (when (seq other-tx)
         (swap! txs-state (fn [txs]
                            (vec (concat txs other-tx)))))
 
-      (when-let [id (:db/id (:data this))]
+      (when id
         (swap! txs-state (fn [txs]
                            (vec
                             (concat txs
                                     (map (fn [attribute]
                                            [:db/retract id attribute])
-                                      db-schema/retract-attributes))))))
+                                      db-schema/retract-attributes)))))
+
+        (when-let [e (:block/page (db/entity id))]
+          (let [m {:db/id (:db/id e)
+                   :block/updated-at (util/time-ms)}
+                m (if (:block/created-at e)
+                    m
+                    (assoc m :block/created-at (util/time-ms)))]
+            (swap! txs-state conj m))))
+
       (swap! txs-state conj (dissoc m :db/other-tx))
-      this
-      ;; TODO: enable for the database-only version
-      ;; (let [[m page-tx] (with-timestamp (:data this))]
-      ;;  (swap! txs-state conj m page-tx)
-      ;;  m)
-      )
-    )
+
+      this))
 
   (-del [this txs-state children?]
     (assert (ds/outliner-txs-state? txs-state)

+ 5 - 7
src/main/frontend/state.cljs

@@ -260,13 +260,10 @@
   (not (false? (:git-auto-push
                 (get (sub-config) repo)))))
 
-(defn enable-block-time?
+(defn enable-block-timestamps?
   []
-  ;; (true? (:feature/enable-block-time?
-  ;;         (get (sub-config) (get-current-repo))))
-
-  ;; Disable block timestamps for now, because it doesn't work with undo/redo
-  false)
+  (true? (:feature/enable-block-timestamps?
+          (get (sub-config) (get-current-repo)))))
 
 (defn sub-graph-config
   []
@@ -551,7 +548,8 @@
 
 (defn get-edit-pos
   []
-  (.-selectionStart (get-input)))
+  (when-let [input (get-input)]
+    (.-selectionStart input)))
 
 (defn set-selection-start-block!
   [start-block]

+ 19 - 3
src/main/frontend/util/property.cljs

@@ -14,7 +14,7 @@
 
 (def built-in-properties
   (set/union
-   #{:id :custom-id :background-color :heading :collapsed :created-at :last-modified-at :created_at :last_modified_at}
+   #{:id :custom-id :background-color :heading :collapsed :created-at :updated-at :last-modified-at :created_at :last_modified_at}
    (set (map keyword config/markers))))
 
 (defn properties-built-in?
@@ -226,7 +226,21 @@
 
 (defn insert-properties
   [format content kvs]
-  (reduce (fn [content [k v]] (insert-property format content k v)) content kvs))
+  (reduce
+   (fn [content [k v]]
+     (let [k (if (string? k)
+               (keyword (-> (string/lower-case k)
+                            (string/replace " " "-")))
+               k)
+           v (if (coll? v)
+               (some->>
+                (seq v)
+                (distinct)
+                (map (fn [item] (util/format "[[%s]]" (text/page-ref-un-brackets! item))))
+                (string/join ", "))
+               v)]
+       (insert-property format content k v)))
+   content kvs))
 
 (defn remove-property
   ([format key content]
@@ -271,7 +285,9 @@
                                  (let [[k v] (util/split-first ":" (subs text 1))]
                                    (if (and k v)
                                      (let [k (string/replace k "_" "-")
-                                           k (if (contains? #{:id :custom_id :custom-id} (string/lower-case k)) "id" k)]
+                                           compare-k (keyword (string/lower-case k))
+                                           k (if (contains? #{:id :custom_id :custom-id} compare-k) "id" k)
+                                           k (if (contains? #{:last-modified-at} compare-k) "updated-at" k)]
                                        (str k ":: " (string/trim v)))
                                      text)))))
               after (subvec lines (inc end-idx))

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.2.3-1")
+(defonce version "0.2.4")

+ 76 - 8
src/main/logseq/api.cljs

@@ -93,16 +93,84 @@
           path (util/node-path.join path "package.json")]
       (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-mtime? true}))))
 
+(defn ^:private write_dotdir_file!
+  [file content sub-root]
+  (p/let [repo ""
+          path (plugin-handler/get-ls-dotdir-root)
+          path (util/node-path.join path sub-root)
+          exist? (fs/file-exists? path "")
+          _ (when-not exist? (fs/mkdir-recur! path))
+          user-path (util/node-path.join path file)
+          sub-dir? (string/starts-with? user-path path)
+          _ (if-not sub-dir? (do (log/info :debug user-path) (throw "write file denied")))
+          user-path-root (util/node-path.dirname user-path)
+          exist? (fs/file-exists? user-path-root "")
+          _ (when-not exist? (fs/mkdir-recur! user-path-root))
+          _ (fs/write-file! repo "" user-path content {:skip-mtime? true})]
+    user-path))
+
+(defn ^:private read_dotdir_file
+  [file sub-root]
+  (p/let [repo ""
+          path (plugin-handler/get-ls-dotdir-root)
+          path (util/node-path.join path sub-root)
+          user-path (util/node-path.join path file)
+          sub-dir? (string/starts-with? user-path path)
+          _ (if-not sub-dir? (do (log/info :debug user-path) (throw "read file denied")))
+          exist? (fs/file-exists? "" user-path)
+          _ (when-not exist? (do (log/info :debug user-path) (throw "file not existed")))
+          content (fs/read-file "" user-path)]
+    content))
+
+(defn ^:private unlink_dotdir_file!
+  [file sub-root]
+  (p/let [repo ""
+          path (plugin-handler/get-ls-dotdir-root)
+          path (util/node-path.join path sub-root)
+          user-path (util/node-path.join path file)
+          sub-dir? (string/starts-with? user-path path)
+          _ (if-not sub-dir? (do (log/info :debug user-path) (throw "access file denied")))
+          exist? (fs/file-exists? "" user-path)
+          _ (when-not exist? (do (log/info :debug user-path) (throw "file not existed")))
+          _ (fs/unlink! user-path {})]))
+
 (def ^:export write_user_tmp_file
   (fn [file content]
-    (p/let [repo ""
-            path (plugin-handler/get-ls-dotdir-root)
-            path (util/node-path.join path "tmp")
-            exist? (fs/file-exists? path "")
-            _ (when-not exist? (fs/mkdir! path))
-            path (util/node-path.join path file)
-            _ (fs/write-file! repo "" path content {:skip-mtime? true})]
-      path)))
+    (write_dotdir_file! file content "tmp")))
+
+(def ^:export write_plugin_storage_file
+  (fn [plugin-id file content]
+    (write_dotdir_file!
+     file content
+     (let [plugin-id (util/node-path.basename plugin-id)]
+       (util/node-path.join "storages" plugin-id)))))
+
+(def ^:export read_plugin_storage_file
+  (fn [plugin-id file]
+    (let [plugin-id (util/node-path.basename plugin-id)]
+      (read_dotdir_file
+       file (util/node-path.join "storages" plugin-id)))))
+
+(def ^:export unlink_plugin_storage_file
+  (fn [plugin-id file]
+    (let [plugin-id (util/node-path.basename plugin-id)]
+      (unlink_dotdir_file!
+       file (util/node-path.join "storages" plugin-id)))))
+
+(def ^:export exist_plugin_storage_file
+  (fn [plugin-id file]
+    (p/let [root (plugin-handler/get-ls-dotdir-root)
+            plugin-id (util/node-path.basename plugin-id)
+            exist? (fs/file-exists?
+                    (util/node-path.join root "storages" plugin-id)
+                    file)]
+      exist?)))
+
+(def ^:export clear_plugin_storage_files
+  (fn [plugin-id]
+    (p/let [root (plugin-handler/get-ls-dotdir-root)
+            plugin-id (util/node-path.basename plugin-id)]
+      (fs/rmdir! (util/node-path.join root  "storages" plugin-id)))))
 
 (def ^:export load_user_preferences
   (fn []

+ 23 - 0
src/test/frontend/util/property_test.cljs

@@ -77,6 +77,29 @@
   #+END_QUOTE" "c" "d")
     "c:: d\n#+BEGIN_QUOTE\n hello world\n  #+END_QUOTE"))
 
+(deftest test-insert-properties
+  (are [x y] (= x y)
+    (property/insert-properties :markdown "" {:foo "bar"})
+    "foo:: bar"
+
+    (property/insert-properties :markdown "" {"foo" "bar"})
+    "foo:: bar"
+
+    (property/insert-properties :markdown "" {"foo space" "bar"})
+    "foo-space:: bar"
+
+    (property/insert-properties :markdown "" {:foo #{"bar" "baz"}})
+    "foo:: [[bar]], [[baz]]"
+
+    (property/insert-properties :markdown "" {:foo ["bar" "bar" "baz"]})
+    "foo:: [[bar]], [[baz]]"
+
+    (property/insert-properties :markdown "a\nb\n" {:foo ["bar" "bar" "baz"]})
+    "a\nfoo:: [[bar]], [[baz]]\nb"
+
+    (property/insert-properties :markdown "" {:foo "\"bar, baz\""})
+    "foo:: \"bar, baz\""))
+
 (deftest test->new-properties
   (are [x y] (= (property/->new-properties x) y)
     ":PROPERTIES:\n:foo: bar\n:END:"

+ 2 - 7
templates/config.edn

@@ -9,13 +9,6 @@
  ;; or ":todo" for TODO/DOING style.
  :preferred-workflow :now
 
- ;; Git is only available in web app
- ;; Git settings
- :git-pull-secs 60
- :git-push-secs 10
- ;; Whether to enable git auto push
- :git-auto-push true
-
  ;; The app will ignore those directories or files.
  ;; E.g. "/archived" "/test.md"
  :hidden []
@@ -30,6 +23,8 @@
  ;; Default is true, you can also toggle this via setting page
  :ui/enable-tooltip? true
 
+ :feature/enable-block-timestamps? false
+
  ;; Specify a custom CSS import
  ;; This option take precedence over your local `logseq/custom.css` file
  ;; You may find a list of awesome logseq themes here:

Некоторые файлы не были показаны из-за большого количества измененных файлов