Explorar o código

Merge branch 'feat/db' into feat/capacitor-new

charlie hai 7 meses
pai
achega
ede7c8f01e

+ 97 - 0
.github/workflows/clj-rtc-e2e.yml

@@ -0,0 +1,97 @@
+name: Clojure RTC E2E
+
+on:
+  push:
+    branches: [master, "feat/db"]
+    paths:
+      - 'clj-e2e/**'
+      - '.github/workflows/clj-rtc-e2e.yml'
+      - src/**
+      - deps/**
+      - packages/**
+  pull_request:
+    branches: [master, "feat/db"]
+    paths:
+      - 'clj-e2e/**'
+      - '.github/workflows/clj-rtc-e2e.yml'
+      - src/**
+      - deps/**
+      - packages/**
+
+env:
+  CLOJURE_VERSION: '1.11.1.1413'
+  # This is the latest node version we can run.
+  NODE_VERSION: '22'
+  BABASHKA_VERSION: '1.0.168'
+
+jobs:
+  rtc-e2e-test-build:
+    name: Test
+    runs-on: ubuntu-22.04
+    if: "contains(github.event.head_commit.message, 'rtc')"
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            yarn.lock
+
+      - name: Set up Clojure
+        uses: DeLaGuardo/[email protected]
+        with:
+          cli: ${{ env.CLOJURE_VERSION }}
+          bb: ${{ env.BABASHKA_VERSION }}
+
+      - name: Clojure cache
+        uses: actions/cache@v3
+        id: clojure-deps
+        with:
+          path: |
+            ~/.m2/repository
+            ~/.gitlibs
+          key: ${{ runner.os }}-clojure-deps-${{ hashFiles('deps.edn') }}
+          restore-keys: ${{ runner.os }}-clojure-deps-
+
+      - name: Fetch Clojure deps
+        if: steps.clojure-deps.outputs.cache-hit != 'true'
+        run: clojure -A:cljs -P
+
+      - name: Shadow-cljs cache
+        uses: actions/cache@v3
+        with:
+          path: .shadow-cljs
+          # ensure update cache every time
+          key: ${{ runner.os }}-shadow-cljs-${{ github.sha }}
+          # will match most recent upload
+          restore-keys: |
+            ${{ runner.os }}-shadow-cljs-
+
+      - name: Fetch yarn deps
+        run: yarn install
+        env:
+          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
+
+      # NOTE: require the app to be build with DEV-RELEASE flag
+      - name: Prepare E2E test build
+        run: |
+          yarn gulp:build && clojure -M:cljs release app --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug
+          rsync -avz --exclude node_modules --exclude android --exclude ios ./static/ ./public/static/
+          ls -lR ./public
+
+      - name: Run e2e tests
+        run: cd clj-e2e && timeout 30m bb run-rtc-extra-test
+        env:
+          DEBUG: "pw:api"
+
+      - name: Collect screenshots
+        if: ${{ failure() }}
+        uses: actions/upload-artifact@v4
+        with:
+          name: e2e-screenshots
+          path: clj-e2e/e2e-dump/*
+          retention-days: 1

+ 6 - 3
clj-e2e/bb.edn

@@ -16,9 +16,12 @@
         :task (do (clojure "-M:test -r \".*\\-basic\\-test$\"")
                   (System/exit 0))}
 
-  extra-test {:doc "run tests (ns'es ending in '-extra-test')"
-              :task (do (clojure "-M:test -r \".*\\-extra\\-test$\"")
-                        (System/exit 0))}
+  rtc-extra-test {:doc "run rtc-extra-test"
+                  :task (do (clojure "-M:test -n logseq.e2e.rtc-extra-test")
+                            (System/exit 0))}
+
+  -run-rtc-extra-test {:depends [serve prn rtc-extra-test]}
+  run-rtc-extra-test {:task (run '-run-rtc-extra-test {:parallel true})}
 
   -dev {:depends [serve prn test]}
 

+ 12 - 1
clj-e2e/src/logseq/e2e/assert.clj

@@ -1,6 +1,7 @@
 (ns logseq.e2e.assert
   (:import [com.microsoft.playwright.assertions PlaywrightAssertions])
-  (:require [wally.main :as w]))
+  (:require [clojure.test :as t]
+            [wally.main :as w]))
 
 (def assert-that PlaywrightAssertions/assertThat)
 
@@ -47,3 +48,13 @@
 (defn assert-selected-block-text
   [text]
   (assert-is-visible (format ".ls-block.selected :text('%s')" text)))
+
+(defn assert-graph-summary-equal
+  "`summary` is returned by `validate-graph`"
+  [summary1 summary2]
+  (let [compare-keys [:blocks :pages :classes :properties ;; :entities
+                      ]]
+
+    (t/is (= (select-keys summary1 compare-keys)
+             (select-keys summary2 compare-keys))
+          [summary1 summary2])))

+ 14 - 2
clj-e2e/src/logseq/e2e/graph.clj

@@ -1,7 +1,10 @@
 (ns logseq.e2e.graph
-  (:require [logseq.e2e.assert :as assert]
+  (:require [clojure.edn :as edn]
+            [clojure.string :as string]
+            [logseq.e2e.assert :as assert]
             [logseq.e2e.util :as util]
-            [wally.main :as w]))
+            [wally.main :as w]
+            [logseq.e2e.locator :as loc]))
 
 (defn- refresh-all-remote-graphs
   []
@@ -52,3 +55,12 @@
   (when wait-sync?
     (w/wait-for "button.cloud.on.idle" {:timeout 20000}))
   (assert/assert-graph-loaded?))
+
+(defn validate-graph
+  []
+  (util/search-and-click "(Dev) Validate current graph")
+  (assert/assert-is-visible (loc/and ".notifications div" (w/get-by-text "Your graph is valid")))
+  (let [content (.textContent (loc/and ".notifications div" (w/get-by-text "Your graph is valid")))
+        summary (edn/read-string (subs content (string/index-of content "{")))]
+    (w/click ".notifications .ls-icon-x")
+    summary))

+ 1 - 0
clj-e2e/src/logseq/e2e/page.clj

@@ -6,6 +6,7 @@
 
 (defn goto-page
   [page-name]
+  (assert (string? page-name) page-name)
   (util/search-and-click page-name))
 
 (defn new-page

+ 8 - 0
clj-e2e/src/logseq/e2e/rtc.clj

@@ -44,3 +44,11 @@
       (if (>= local-tx new-tx)
         local-tx
         (recur (dec i))))))
+
+(defn rtc-start
+  []
+  (util/search-and-click "(Dev) RTC Start"))
+
+(defn rtc-stop
+  []
+  (util/search-and-click "(Dev) RTC Stop"))

+ 15 - 0
clj-e2e/src/logseq/e2e/settings.clj

@@ -0,0 +1,15 @@
+(ns logseq.e2e.settings
+  (:require [logseq.e2e.assert :as assert]
+            [logseq.e2e.keyboard :as k]
+            [wally.main :as w]))
+
+(defn developer-mode
+  []
+  (w/click "button[title='More'] .ls-icon-dots")
+  (w/click ".ls-icon-settings")
+  (w/click "[data-id='advanced']")
+  (let [q (.last (w/-query ".ui__toggle [aria-checked='false']"))]
+    (when (.isVisible q)
+      (w/click q)))
+  (k/esc)
+  (assert/assert-in-normal-mode?))

+ 8 - 1
clj-e2e/src/logseq/e2e/util.clj

@@ -7,7 +7,8 @@
             [wally.main :as w]
             [wally.repl :as repl])
   (:import (com.microsoft.playwright Locator$PressSequentiallyOptions
-                                     Locator$FilterOptions)
+                                     Locator$FilterOptions
+                                     Page$GetByTextOptions)
            (com.microsoft.playwright TimeoutError)))
 
 (defn repeat-until-visible
@@ -189,3 +190,9 @@
 (defn -query-last
   [q]
   (.last (w/-query q)))
+
+(defn get-by-text
+  [text exact?]
+  (if exact?
+    (.getByText (w/get-page) text (.setExact (Page$GetByTextOptions.) true))
+    (.getByText (w/get-page) text)))

+ 3 - 1
clj-e2e/test/logseq/e2e/commands_basic_test.clj

@@ -15,7 +15,9 @@
 
 (use-fixtures :once fixtures/open-page)
 
-(use-fixtures :each fixtures/new-logseq-page)
+(use-fixtures :each
+  fixtures/new-logseq-page
+  fixtures/validate-graph)
 
 (deftest command-trigger-test
   (testing "/command trigger popup"

+ 26 - 4
clj-e2e/test/logseq/e2e/fixtures.clj

@@ -1,7 +1,10 @@
 (ns logseq.e2e.fixtures
-  (:require [logseq.e2e.config :as config]
+  (:require [logseq.e2e.assert :as assert]
+            [logseq.e2e.config :as config]
             [logseq.e2e.custom-report :as custom-report]
+            [logseq.e2e.graph :as graph]
             [logseq.e2e.page :as page]
+            [logseq.e2e.settings :as settings]
             [wally.main :as w]))
 
 ;; TODO: save trace
@@ -15,6 +18,9 @@
     (w/grant-permissions :clipboard-write :clipboard-read)
     (binding [custom-report/*pw-contexts* #{(.context (w/get-page))}]
       (w/navigate (str "http://localhost:" (or port @config/*port)))
+      (settings/developer-mode)
+      (w/refresh)
+      (assert/assert-graph-loaded?)
       (f))))
 
 (def *page1 (atom nil))
@@ -36,13 +42,17 @@
               w/*page* (delay (throw (ex-info "Don't use *page*, use *page1* and *page2* instead" {})))]
       (run!
        #(w/with-page %
-          (w/navigate (str "http://localhost:" port')))
+          (w/navigate (str "http://localhost:" port'))
+          (settings/developer-mode)
+          (w/refresh))
        [p1 p2])
       (f))
 
     ;; use with-page-open to release resources
     (w/with-page-open p1)
-    (w/with-page-open p2)))
+    (w/with-page-open p2)
+    (reset! *page1 nil)
+    (reset! *page2 nil)))
 
 (def ^:dynamic *pw-ctx* nil)
 (defn open-new-context
@@ -65,9 +75,21 @@
 
 (defn create-page
   []
-  (page/new-page (str "page " (swap! *page-number inc))))
+  (let [page-name (str "page " (swap! *page-number inc))]
+    (page/new-page page-name)
+    page-name))
 
 (defn new-logseq-page
   [f]
   (create-page)
   (f))
+
+(defn validate-graph
+  [f]
+  (f)
+  (if (and @*page1 @*page2)
+    (doseq [p [@*page1 @*page2]]
+      (w/with-page p
+        (graph/validate-graph)))
+
+    (graph/validate-graph)))

+ 24 - 7
clj-e2e/test/logseq/e2e/outliner_basic_test.clj

@@ -8,16 +8,18 @@
    [wally.main :as w]))
 
 (use-fixtures :once fixtures/open-page)
-(use-fixtures :each fixtures/new-logseq-page)
+(use-fixtures :each
+  fixtures/new-logseq-page
+  fixtures/validate-graph)
 
-(deftest create-test-page-and-insert-blocks
+(defn create-test-page-and-insert-blocks []
   ;; a page block and a child block
   (is (= 2 (util/blocks-count)))
   (b/new-blocks ["first block" "second block"])
   (util/exit-edit)
   (is (= 3 (util/blocks-count))))
 
-(deftest indent-and-outdent-test
+(defn indent-and-outdent []
   (b/new-blocks ["b1" "b2"])
   (testing "simple indent and outdent"
     (b/indent)
@@ -42,7 +44,7 @@
     (let [[x2 x3 x4 x5] (map (comp first util/bounding-xy #(w/find-one-by-text "span" %)) ["b2" "b3" "b4" "b5"])]
       (is (and (= x2 x4) (= x3 x5) (< x2 x3))))))
 
-(deftest move-up-down-test
+(defn move-up-down []
   (b/new-blocks ["b1" "b2" "b3" "b4"])
   (util/repeat-keyboard 2 "Shift+ArrowUp")
   (let [contents (util/get-page-blocks-contents)]
@@ -54,16 +56,16 @@
   (let [contents (util/get-page-blocks-contents)]
     (is (= contents ["b1" "b2" "b3" "b4"]))))
 
-(deftest delete-test
+(defn delete []
   (testing "Delete blocks case 1"
     (b/new-blocks ["b1" "b2" "b3" "b4"])
-    (b/delete-blocks)                   ; delete b4
+    (b/delete-blocks)                        ; delete b4
     (util/repeat-keyboard 2 "Shift+ArrowUp") ; select b3 and b2
     (b/delete-blocks)
     (is (= "b1" (util/get-edit-content)))
     (is (= 1 (util/page-blocks-count)))))
 
-(deftest delete-test-with-children
+(defn delete-test-with-children []
   (testing "Delete block with its children"
     (b/new-blocks ["b1" "b2" "b3" "b4"])
     (b/indent)
@@ -73,3 +75,18 @@
     (b/delete-blocks)
     (is (= "b1" (util/get-edit-content)))
     (is (= 1 (util/page-blocks-count)))))
+
+(deftest create-test-page-and-insert-blocks-test
+  (create-test-page-and-insert-blocks))
+
+(deftest indent-and-outdent-test
+  (indent-and-outdent))
+
+(deftest move-up-down-test
+  (move-up-down))
+
+(deftest delete-test
+  (delete))
+
+(deftest delete-test-with-children-test
+  (delete-test-with-children))

+ 3 - 1
clj-e2e/test/logseq/e2e/reference_basic_test.clj

@@ -10,7 +10,9 @@
 
 (use-fixtures :once fixtures/open-page)
 
-(use-fixtures :each fixtures/new-logseq-page)
+(use-fixtures :each
+  fixtures/new-logseq-page
+  fixtures/validate-graph)
 
 ;; block references
 (deftest self-reference

+ 2 - 0
clj-e2e/test/logseq/e2e/rtc_basic_test.clj

@@ -13,6 +13,8 @@
 
 (use-fixtures :once fixtures/open-2-pages)
 
+(use-fixtures :each fixtures/validate-graph)
+
 (deftest rtc-basic-test
   (let [graph-name (str "rtc-graph-" (.toEpochMilli (java.time.Instant/now)))
         page-names (map #(str "rtc-test-page" %) (range 4))]

+ 194 - 43
clj-e2e/test/logseq/e2e/rtc_extra_test.clj

@@ -1,35 +1,101 @@
 (ns logseq.e2e.rtc-extra-test
   (:require
-   [clojure.test :refer [deftest testing is use-fixtures run-tests]]
+   [clojure.test :refer [deftest testing is use-fixtures run-test]]
    [com.climate.claypoole :as cp]
+   [logseq.e2e.assert :as assert]
    [logseq.e2e.block :as b]
    [logseq.e2e.fixtures :as fixtures :refer [*page1 *page2]]
    [logseq.e2e.graph :as graph]
+   [logseq.e2e.keyboard :as k]
+   [logseq.e2e.locator :as loc]
+   [logseq.e2e.outliner-basic-test :as outliner-basic-test]
+   [logseq.e2e.page :as page]
    [logseq.e2e.rtc :as rtc]
+   [logseq.e2e.settings :as settings]
    [logseq.e2e.util :as util]
    [wally.main :as w]
    [wally.repl :as repl]))
 
-(def *graph-name (atom nil))
-(defn cleanup-fixture
+(defn- prepare-rtc-graph-fixture
+  "open 2 app instances, add a rtc graph, check this graph available on other instance"
   [f]
-  (f)
-  (w/with-page @*page2
-    (assert (some? @*graph-name))
-    (graph/remove-remote-graph @*graph-name)))
+  (let [graph-name (str "rtc-extra-test-graph-" (.toEpochMilli (java.time.Instant/now)))]
+    (cp/prun!
+     2
+     #(w/with-page %
+        (settings/developer-mode)
+        (w/refresh)
+        (util/login-test-account))
+     [@*page1 @*page2])
+    (w/with-page @*page1
+      (graph/new-graph graph-name true))
+    (w/with-page @*page2
+      (graph/wait-for-remote-graph graph-name)
+      (graph/switch-graph graph-name true))
+
+    (f)
+
+    ;; cleanup
+    (w/with-page @*page2
+      (graph/remove-remote-graph graph-name))))
+
+(defn- new-logseq-page
+  "new logseq page and switch to this page on both page1 and page2"
+  []
+  (let [*page-name (atom nil)
+        {:keys [_local-tx remote-tx]}
+        (w/with-page @*page1
+          (rtc/with-wait-tx-updated
+            (reset! *page-name (fixtures/create-page))))]
+    (w/with-page @*page2
+      (rtc/wait-tx-update-to remote-tx)
+      (page/goto-page @*page-name))))
+
+(defn- new-logseq-page-fixture
+  [f]
+  (new-logseq-page)
+  (f))
 
 (use-fixtures :once
   fixtures/open-2-pages
-  ;; cleanup-fixture
-  )
+  prepare-rtc-graph-fixture)
 
-(defn- offline
+(use-fixtures :each
+  new-logseq-page-fixture)
+
+(defn- with-stop-restart-rtc
+  [pw-page f]
+  (w/with-page pw-page
+    (rtc/rtc-stop))
+  (f)
+  (w/with-page pw-page
+    (rtc/rtc-start)))
+
+(defn- validate-2-graphs
   []
-  (.setOffline (.context (w/get-page)) true))
+  (let [[p1-summary p2-summary]
+        (map
+         (fn [p]
+           (w/with-page p
+             (graph/validate-graph)))
+         [@*page1 @*page2])]
+    (assert/assert-graph-summary-equal p1-summary p2-summary)))
 
-(defn- online
+(defn- validate-task-blocks
   []
-  (.setOffline (.context (w/get-page)) false))
+  (let [icon-names ["Backlog" "Todo" "InProgress50" "InReview" "Done" "Cancelled"]
+        icon-name->count
+        (w/with-page @*page2
+          (into
+           {}
+           (map
+            (fn [icon-name]
+              [icon-name (.count (w/-query (str ".ls-icon-" icon-name)))])
+            icon-names)))]
+    (prn :validate-task-blocks icon-name->count)
+    (w/with-page @*page1
+      (doseq [[icon-name count*] icon-name->count]
+        (assert/assert-have-count (str ".ls-icon-" icon-name) count*)))))
 
 (defn- insert-task-blocks
   [title-prefix]
@@ -39,37 +105,122 @@
     (util/input-command status)
     (util/input-command priority)))
 
-(deftest rtc-extra-test
-  (let [graph-name (str "rtc-extra-test-graph-" (.toEpochMilli (java.time.Instant/now)))]
-    (reset! *graph-name graph-name)
-    (testing "open 2 app instances, add a rtc graph, check this graph available on other instance"
-      (cp/prun!
-       2
-       #(w/with-page %
-          (util/login-test-account))
-       [@*page1 @*page2])
-      (w/with-page @*page1
-        (graph/new-graph graph-name true))
-      (w/with-page @*page2
-        (graph/wait-for-remote-graph graph-name)
-        (graph/switch-graph graph-name true)))
+(defn- update-task-blocks
+  [])
+
+(deftest rtc-task-blocks-test
+  (let [insert-task-blocks-in-page2
+        (fn [*latest-remote-tx]
+          (w/with-page @*page2
+            (let [{:keys [_local-tx remote-tx]}
+                  (rtc/with-wait-tx-updated
+                    (insert-task-blocks "t1"))]
+              (reset! *latest-remote-tx remote-tx))
+            ;; TODO: more operations
+            (util/exit-edit)))]
     (testing "rtc-stop app1, add some task blocks, then rtc-start on app1"
       (let [*latest-remote-tx (atom nil)]
+        (with-stop-restart-rtc @*page1 #(insert-task-blocks-in-page2 *latest-remote-tx))
         (w/with-page @*page1
-          (offline))
-        (w/with-page @*page2
-          (let [{:keys [_local-tx remote-tx]}
-                (rtc/with-wait-tx-updated
-                  (insert-task-blocks "t1"))]
-            (reset! *latest-remote-tx remote-tx))
-          ;; TODO: more operations
-          (util/exit-edit))
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-task-blocks)
+        (validate-2-graphs)))
+
+    (new-logseq-page)
+
+    (testing "perform same operations on page2 while keeping rtc connected on page1"
+      (let [*latest-remote-tx (atom nil)]
+        (insert-task-blocks-in-page2 *latest-remote-tx)
+        (w/with-page @*page1
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-task-blocks)
+        (validate-2-graphs)))))
+
+(defn- add-new-properties
+  [title-prefix]
+  (b/new-blocks (map #(str title-prefix "-" %) ["Text" "Number" "Date" "DateTime" "Checkbox" "Url" "Node"]))
+  (doseq [property-type ["Text" "Number" "Date" "DateTime" "Checkbox" "Url" "Node"]]
+    (let [property-name (str "p-" title-prefix "-" property-type)]
+      (w/click (util/get-by-text (str title-prefix "-" property-type) true))
+      (k/press "Control+e")
+      (util/input-command "Add new property")
+      (util/input property-name)
+      (w/click (w/get-by-text "New option:"))
+      (assert/assert-is-visible (w/get-by-text "Select a property type"))
+      (w/click (loc/and "span" (util/get-by-text property-type true)))
+      (case property-type
+        "Text" (util/input "Text")
+        "Number" (do (assert/assert-is-visible (format "input[placeholder='%s']" (str "Set " property-name)))
+                     (util/input "111")
+                     (w/click (w/get-by-text "New option:")))
+        ("DateTime" "Date") (do
+                              (assert/assert-is-visible ".ls-property-dialog")
+                              (k/enter)
+                              (k/esc))
+        "Checkbox" nil
+        "Url" nil
+        "Node" (do
+                 (w/click (w/get-by-text "Skip choosing tag"))
+                 (util/input (str title-prefix "-Node-value"))
+                 (w/click (w/get-by-text "New option:")))))))
+
+(deftest rtc-property-test
+  (let [insert-new-property-blocks-in-page2
+        (fn [*latest-remote-tx title-prefix]
+          (w/with-page @*page2
+            (let [{:keys [_local-tx remote-tx]}
+                  (rtc/with-wait-tx-updated
+                    (add-new-properties title-prefix))]
+              (reset! *latest-remote-tx remote-tx))))]
+    (testing "page1: rtc-stop
+page2: create some user properties with different type
+page1: rtc-start"
+      (let [*latest-remote-tx (atom nil)]
+        (with-stop-restart-rtc @*page1 #(insert-new-property-blocks-in-page2 *latest-remote-tx "rtc-property-test-1"))
+        (w/with-page @*page1
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-2-graphs)))
+
+    (new-logseq-page)
+
+    (testing "perform same operations on page2 while keeping rtc connected on page1"
+      (let [*latest-remote-tx (atom nil)]
+        (insert-new-property-blocks-in-page2 *latest-remote-tx "rtc-property-test-2")
+        (w/with-page @*page1
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-2-graphs)))))
+
+(deftest rtc-outliner-test
+  (doseq [test-fn [outliner-basic-test/create-test-page-and-insert-blocks
+                   outliner-basic-test/indent-and-outdent
+                   outliner-basic-test/move-up-down
+                   outliner-basic-test/delete
+                   outliner-basic-test/delete-test-with-children]]
+    (let [test-fn-in-page2 (fn [*latest-remote-tx]
+                             (w/with-page @*page2
+                               (let [{:keys [_local-tx remote-tx]}
+                                     (rtc/with-wait-tx-updated
+                                       (test-fn))]
+                                 (reset! *latest-remote-tx remote-tx))))]
+
+      ;; testing while rtc connected
+      (let [*latest-remote-tx (atom nil)]
+        (new-logseq-page)
+        (test-fn-in-page2 *latest-remote-tx)
         (w/with-page @*page1
-          (online)
-          (rtc/wait-tx-update-to @*latest-remote-tx)
-          ;; TODO: check blocks exist
-          )))
-    (testing "cleanup"
-      (w/with-page @*page2
-        (assert (some? @*graph-name))
-        (graph/remove-remote-graph @*graph-name)))))
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-2-graphs))
+
+      ;; testing while rtc off then on
+      (let [*latest-remote-tx (atom nil)]
+        (new-logseq-page)
+        (with-stop-restart-rtc @*page1 #(test-fn-in-page2 *latest-remote-tx))
+        (w/with-page @*page1
+          (rtc/wait-tx-update-to @*latest-remote-tx))
+        (validate-2-graphs)))))
+
+(comment
+  (let [title-prefix "xxxx"
+        property-type "Text"]
+    (w/with-page @*page1
+      (b/new-block (str title-prefix "-" property-type)))))

+ 4 - 1
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -461,7 +461,10 @@
   [:map
    [:db/ident [:= :logseq.property/empty-placeholder]]
    [:block/uuid :uuid]
-   [:block/tx-id {:optional true} :int]])
+   [:block/tx-id {:optional true} :int]
+   [:block/created-at {:optional true} :int]
+   [:block/updated-at {:optional true} :int]
+   [:block/properties {:optional true} block-properties]])
 
 (defn entity-dispatch-key [db ent]
   (let [d (if (:block/uuid ent) (d/entity db [:block/uuid (:block/uuid ent)]) ent)

+ 3 - 3
deps/outliner/src/logseq/outliner/property.cljs

@@ -251,9 +251,9 @@
                           (= :logseq.property/empty-placeholder (:db/ident (d/entity @conn v)))))))
       v
       ;; only value-ref-property types should call this
-      (let [v' (if (and number-property? (string? v))
-                 (parse-double v)
-                 v)]
+      (when-let [v' (if (and number-property? (string? v))
+                      (parse-double v)
+                      v)]
         (find-or-create-property-value conn property-id v')))))
 
 (defn- throw-error-if-self-value

+ 0 - 3
src/main/frontend/components/block.cljs

@@ -767,9 +767,6 @@
                                          (do
                                            (prn :debug :unknown-title-error :title s
                                                 :data (db/pull (:db/id page-entity)))
-                                           (db/transact! [{:db/id (:db/id page-entity)
-                                                           :block/title "FIX unknown page"
-                                                           :block/name "fix unknown page"}])
                                            "Unknown title")
                                          (re-find db-content/id-ref-pattern s)
                                          (db-content/content-id-ref->page s (:block/refs page-entity))

+ 8 - 8
src/main/frontend/components/db_based/page.cljs

@@ -3,9 +3,9 @@
   (:require [frontend.components.property.config :as property-config]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
+            [frontend.util :as util]
             [logseq.shui.ui :as shui]
-            [rum.core :as rum]
-            [frontend.util :as util]))
+            [rum.core :as rum]))
 
 (rum/defc configure-property < rum/reactive db-mixins/query
   [page]
@@ -17,9 +17,9 @@
                          (util/stop e))
       :on-click (fn [^js e]
                   (shui/popup-show! (.-target e)
-                    (fn []
-                      (property-config/dropdown-editor page nil {:debug? (.-altKey e)}))
-                    {:content-props {:class "ls-property-dropdown-editor as-root"}
-                     :align "start"
-                     :as-dropdown? true}))}
-      "Configure property")))
+                                    (fn []
+                                      (property-config/property-dropdown page nil {:debug? (.-altKey e)}))
+                                    {:content-props {:class "ls-property-dropdown as-root"}
+                                     :align "start"
+                                     :as-dropdown? true}))}
+     "Configure property")))

+ 3 - 3
src/main/frontend/components/property.cljs

@@ -279,10 +279,10 @@
     :on-click (fn [^js/MouseEvent e]
                 (shui/popup-show! (.-target e)
                                   (fn []
-                                    (property-config/dropdown-editor property block {:debug? (.-altKey e)
-                                                                                     :class-schema? class-schema?}))
+                                    (property-config/property-dropdown property block {:debug? (.-altKey e)
+                                                                                       :class-schema? class-schema?}))
                                   {:content-props
-                                   {:class "ls-property-dropdown-editor as-root"
+                                   {:class "ls-property-dropdown as-root"
                                     :onEscapeKeyDown (fn [e]
                                                        (util/stop e)
                                                        (shui/popup-hide!)

+ 2 - 2
src/main/frontend/components/property.css

@@ -357,8 +357,8 @@ a.control-link {
   }
 }
 
-.ls-property-dropdown-editor {
-  @apply w-[300px] p-1.5;
+.ls-property-dropdown {
+  @apply w-[260px] p-1.5;
 
   .ui__dropdown-menu-item, .ui__dropdown-menu-sub-trigger {
   }

+ 167 - 159
src/main/frontend/components/property/config.cljs

@@ -392,7 +392,7 @@
                            :content (choice-item-content property block opts)}))
                       choices)]
 
-    [:div.ls-property-dropdown-editor.ls-property-choices-sub-pane
+    [:div.ls-property-dropdown.ls-property-choices-sub-pane
      (when (seq choices)
        [:<>
         [:ul.choices-list
@@ -504,7 +504,7 @@
                            (set-sub-open! false)
                            (restore-root-highlight-item! id)))
         item-props {:on-select handle-select!}]
-    [:div.ls-property-dropdown-editor.ls-property-ui-position-sub-pane
+    [:div.ls-property-dropdown.ls-property-ui-position-sub-pane
      (for [[k v] position-labels]
        (let [item-props (assoc item-props :data-value k)]
          (dropdown-editor-menuitem
@@ -554,7 +554,7 @@
                           (map (fn [type]
                                  {:label (property-type-label type)
                                   :value type})))]
-    [:div.ls-property-dropdown-editor.ls-property-type-sub-pane
+    [:div.ls-property-dropdown.ls-property-type-sub-pane
      (for [{:keys [label value]} schema-types]
        (let [option {:id label
                      :title label
@@ -579,9 +579,10 @@
                     :submenu-content (fn [] (pdv/default-value-config property))}))]
     (dropdown-editor-menuitem (assoc option :disabled? config/publishing?))))
 
-(rum/defc ^:large-vars/cleanup-todo dropdown-editor-impl
+(defn ^:large-vars/cleanup-todo property-dropdown-options
   "property: block entity"
-  [property owner-block values {:keys [class-schema? debug?]}]
+  [property owner-block values {:keys [class-schema? debug? with-title? more-options]
+                                :or {with-title? true}}]
   (let [title (:block/title property)
         property-type (:logseq.property/type property)
         property-type-label' (some-> property-type (property-type-label))
@@ -591,165 +592,172 @@
         icon (when icon [:span.float-left.w-4.h-4.overflow-hidden.leading-4.relative
                          (icon-component/icon icon {:size 15})])
         built-in? (ldb/built-in? property)
-        disabled? (or built-in? config/publishing?)]
-    [:<>
-     [:h3.font-medium.px-2.pt-2.pb-2.opacity-90.flex.items-center.gap-1
-      (shui/tabler-icon "adjustments-alt") [:span "Configure property"]]
-     (dropdown-editor-menuitem {:icon :pencil :title "Property name" :desc [:span.flex.items-center.gap-1 icon title]
-                                :submenu-content (fn [ops] (name-edit-pane property (assoc ops :disabled? disabled?)))})
-     (let [disabled?' (or disabled? (and property-type (seq values)))]
-       (dropdown-editor-menuitem {:icon :letter-t
-                                  :title "Property type"
-                                  :desc (if disabled?'
-                                          (ui/tooltip
+        disabled? (or built-in? config/publishing?)
+        class-schema? (and (ldb/class? owner-block) class-schema?)
+        special-built-in-prop? (contains? #{:block/title :block/tags :block/created-at :block/updated-at} (:db/ident property))]
+    (->>
+     [(when with-title?
+        [:h3.font-medium.px-2.py-4.opacity-90.flex.items-center.gap-1
+         (shui/tabler-icon "adjustments-alt") [:span "Configure property"]])
+      (when-not special-built-in-prop?
+        (dropdown-editor-menuitem {:icon :pencil :title "Property name" :desc [:span.flex.items-center.gap-1 icon title]
+                                   :submenu-content (fn [ops] (name-edit-pane property (assoc ops :disabled? disabled?)))}))
+      (let [disabled?' (or disabled? (and property-type (seq values)))]
+        (dropdown-editor-menuitem {:icon :letter-t
+                                   :title "Property type"
+                                   :desc (if disabled?'
+                                           (ui/tooltip
                                             [:span (str property-type-label')]
                                             [:div.w-96
                                              "The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."])
-                                          (str property-type-label'))
-                                  :disabled? disabled?'
-                                  :submenu-content (fn [ops]
-                                                     (property-type-sub-pane property ops))}))
-
-     (when (and (= property-type :node)
-             (not (contains? #{:logseq.property/parent} (:db/ident property))))
-       (dropdown-editor-menuitem {:icon :hash
-                                  :disabled? disabled?
-                                  :title "Specify node tags"
-                                  :desc ""
-                                  :submenu-content (fn [_ops]
-                                                     [:div.px-4
-                                                      (class-select property {:default-open? false})])}))
-
-     (when (and (contains? db-property-type/default-value-ref-property-types property-type)
-                (not (db-property/many? property))
-                (not (and enable-closed-values?
-                          (seq (:property/closed-values property))))
-                (not= :logseq.property/enable-history? (:db/ident property)))
-       (default-value-subitem property))
-
-     (when enable-closed-values?
-       (let [values (:property/closed-values property)]
-         (dropdown-editor-menuitem {:icon :list :title "Available choices"
-                                    :desc (when (seq values) (str (count values) " choices"))
-                                    :submenu-content (fn [] (choices-sub-pane property {:disabled? config/publishing?}))})))
-
-     (when enable-closed-values?
-       (let [values (:property/closed-values property)]
-         (when (>= (count values) 2)
-           (dropdown-editor-menuitem
-            {:icon :checkbox
-             :title "Checkbox state mapping"
-             :disabled? config/publishing?
-             :submenu-content (fn []
-                                (checkbox-state-mapping values))}))))
-
-     (when (and (contains? db-property-type/cardinality-property-types property-type) (not disabled?))
-       (let [many? (db-property/many? property)]
-         (dropdown-editor-menuitem {:icon :checks :title "Multiple values"
-                                    :toggle-checked? many?
-                                    :on-toggle-checked-change
-                                    (fn []
-                                      (let [update-cardinality-fn #(db-property-handler/upsert-property! (:db/ident property)
-                                                                                                         {:db/cardinality (if many? :one :many)}
-                                                                                                         {})]
+                                           (str property-type-label'))
+                                   :disabled? disabled?'
+                                   :submenu-content (fn [ops]
+                                                      (property-type-sub-pane property ops))}))
+
+      (when (and (= property-type :node)
+                 (not (contains? #{:logseq.property/parent} (:db/ident property))))
+        (dropdown-editor-menuitem {:icon :hash
+                                   :disabled? disabled?
+                                   :title "Specify node tags"
+                                   :desc ""
+                                   :submenu-content (fn [_ops]
+                                                      [:div.px-4
+                                                       (class-select property {:default-open? false})])}))
+
+      (when (and (contains? db-property-type/default-value-ref-property-types property-type)
+                 (not (db-property/many? property))
+                 (not (and enable-closed-values?
+                           (seq (:property/closed-values property))))
+                 (not= :logseq.property/enable-history? (:db/ident property)))
+        (default-value-subitem property))
+
+      (when enable-closed-values?
+        (let [values (:property/closed-values property)]
+          (dropdown-editor-menuitem {:icon :list :title "Available choices"
+                                     :desc (when (seq values) (str (count values) " choices"))
+                                     :submenu-content (fn [] (choices-sub-pane property {:disabled? config/publishing?}))})))
+
+      (when enable-closed-values?
+        (let [values (:property/closed-values property)]
+          (when (>= (count values) 2)
+            (dropdown-editor-menuitem
+             {:icon :checkbox
+              :title "Checkbox state mapping"
+              :disabled? config/publishing?
+              :submenu-content (fn []
+                                 (checkbox-state-mapping values))}))))
+
+      (when (and (contains? db-property-type/cardinality-property-types property-type) (not disabled?))
+        (let [many? (db-property/many? property)]
+          (dropdown-editor-menuitem {:icon :checks :title "Multiple values"
+                                     :toggle-checked? many?
+                                     :on-toggle-checked-change
+                                     (fn []
+                                       (let [update-cardinality-fn #(db-property-handler/upsert-property! (:db/ident property)
+                                                                                                          {:db/cardinality (if many? :one :many)}
+                                                                                                          {})]
                                       ;; Only show dialog for existing values as it can be reversed for unused properties
-                                        (if (and (seq values) (not many?))
-                                          (-> (shui/dialog-confirm!
-                                               "This action cannot be undone. Do you want to change this property to have multiple values?")
-                                              (p/then update-cardinality-fn))
-                                          (update-cardinality-fn))))})))
-
-     (when (not= :logseq.property/enable-history? (:db/ident property))
-       (let [property-type (:logseq.property/type property)
-             group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                                     (contains? #{:default :number :date :checkbox :node} property-type)
-                                     (not
-                                      (and (= :default property-type)
-                                           (empty? (:property/closed-values property))
-                                           (contains? #{nil :properties} (:logseq.property/ui-position property)))))
-                            (let [position (:logseq.property/ui-position property)]
-                              (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
-                                                         :item-props {:class "ui__position-trigger-item"}
-                                                         :disabled? config/publishing?
-                                                         :submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :ui-position position)))})))
-
-                          (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                            (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:logseq.property/hide? property))
-                                                       :disabled? config/publishing?
-                                                       :on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
-                                                                                                                           :logseq.property/hide?
-                                                                                                                           %)}))
-                          (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                            (dropdown-editor-menuitem
-                             {:icon :eye-off :title "Hide empty value"
-                              :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
-                              :disabled? config/publishing?
-                              :on-toggle-checked-change (fn []
-                                                          (db-property-handler/set-block-property! (:db/id property)
-                                                                                                   :logseq.property/hide-empty-value
-                                                                                                   (not (:logseq.property/hide-empty-value property))))}))]
-                         (remove nil?))]
-         (when (> (count group') 0)
-           (cons (shui/dropdown-menu-separator) group'))))
-
-     (when owner-block
-       [:<>
-        (shui/dropdown-menu-separator)
-        (dropdown-editor-menuitem
-         {:icon :share-3 :title "Go to this property" :desc ""
-          :item-props {:class "opacity-90 focus:opacity-100"
-                       :on-select (fn []
-                                    (shui/popup-hide-all!)
-                                    (route-handler/redirect-to-page! (:block/uuid property)))}})])
-     (when (and enable-closed-values? owner-block)
-       (let [values (:property/closed-values property)]
-         (when (>= (count values) 2)
-           (let [checked? (contains?
-                           (set (map :db/id (:logseq.property/checkbox-display-properties owner-block)))
-                           (:db/id property))]
-             (dropdown-editor-menuitem
-              {:icon :checkbox
-               :title (if class-schema? "Show as checkbox on tagged nodes" "Show as checkbox on node")
-               :disabled? config/publishing?
-               :desc (when owner-block
-                       (shui/switch
-                        {:id "show as checkbox" :size "sm"
-                         :checked checked?
-                         :on-click util/stop-propagation
-                         :on-checked-change
-                         (fn [value]
-                           (if value
-                             (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
-                             (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
-
-     (when (and owner-block
+                                         (if (and (seq values) (not many?))
+                                           (-> (shui/dialog-confirm!
+                                                "This action cannot be undone. Do you want to change this property to have multiple values?")
+                                               (p/then update-cardinality-fn))
+                                           (update-cardinality-fn))))})))
+
+      (when (and (not= :logseq.property/enable-history? (:db/ident property))
+                 (not special-built-in-prop?))
+        (let [property-type (:logseq.property/type property)
+              group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                                      (contains? #{:default :number :date :checkbox :node} property-type)
+                                      (not
+                                       (and (= :default property-type)
+                                            (empty? (:property/closed-values property))
+                                            (contains? #{nil :properties} (:logseq.property/ui-position property)))))
+                             (let [position (:logseq.property/ui-position property)]
+                               (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
+                                                          :item-props {:class "ui__position-trigger-item"}
+                                                          :disabled? config/publishing?
+                                                          :submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :ui-position position)))})))
+
+                           (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                             (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:logseq.property/hide? property))
+                                                        :disabled? config/publishing?
+                                                        :on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
+                                                                                                                            :logseq.property/hide?
+                                                                                                                            %)}))
+                           (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                             (dropdown-editor-menuitem
+                              {:icon :eye-off :title "Hide empty value"
+                               :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
+                               :disabled? config/publishing?
+                               :on-toggle-checked-change (fn []
+                                                           (db-property-handler/set-block-property! (:db/id property)
+                                                                                                    :logseq.property/hide-empty-value
+                                                                                                    (not (:logseq.property/hide-empty-value property))))}))]
+                          (remove nil?))]
+          (when (> (count group') 0)
+            (cons (shui/dropdown-menu-separator) group'))))
+
+      (when (and owner-block (not special-built-in-prop?))
+        [:<>
+         (shui/dropdown-menu-separator)
+         (dropdown-editor-menuitem
+          {:icon :share-3 :title "Go to this property" :desc ""
+           :item-props {:class "opacity-90 focus:opacity-100"
+                        :on-select (fn []
+                                     (shui/popup-hide-all!)
+                                     (route-handler/redirect-to-page! (:block/uuid property)))}})])
+      (when (and enable-closed-values? owner-block)
+        (let [values (:property/closed-values property)]
+          (when (>= (count values) 2)
+            (let [checked? (contains?
+                            (set (map :db/id (:logseq.property/checkbox-display-properties owner-block)))
+                            (:db/id property))]
+              (dropdown-editor-menuitem
+               {:icon :checkbox
+                :title (if class-schema? "Show as checkbox on tagged nodes" "Show as checkbox on node")
+                :disabled? config/publishing?
+                :desc (when owner-block
+                        (shui/switch
+                         {:id "show as checkbox" :size "sm"
+                          :checked checked?
+                          :on-click util/stop-propagation
+                          :on-checked-change
+                          (fn [value]
+                            (if value
+                              (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
+                              (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
+
+      (when (and owner-block
                 ;; Any property should be removable from Tag Properties
-                (or class-schema?
-                    (not (and
-                          (ldb/class? owner-block)
-                          (contains? #{:logseq.property/parent} (:db/ident property))))))
-       (dropdown-editor-menuitem
-        {:id :delete-property :icon :x
-         :title (if class-schema? "Delete property from tag" "Delete property from node")
-         :desc "" :disabled? false
-         :item-props {:class "opacity-60 focus:!text-red-rx-09 focus:opacity-100"
-                      :on-select (fn [^js e]
-                                   (util/stop e)
-                                   (-> (p/do!
-                                        (handle-delete-property! owner-block property {:class-schema? class-schema?})
-                                        (shui/popup-hide-all!))
-                                       (p/catch (fn [] (restore-root-highlight-item! :delete-property)))))}}))
-     (when debug?
-       [:<>
-        (shui/dropdown-menu-separator)
-        (dropdown-editor-menuitem
-         {:icon :bug :title (str (:db/ident property)) :desc "" :disabled? false
-          :item-props {:class "opacity-60 focus:opacity-100 focus:!text-red-rx-08"
-                       :on-select (fn []
-                                    (dev-common-handler/show-entity-data (:db/id property))
-                                    (shui/popup-hide!))}})])]))
+                 (if class-schema?
+                   (contains? (set (map :db/id (:logseq.property.class/properties owner-block))) (:db/id property))
+                   (not (contains? #{:logseq.property/parent} (:db/ident property)))))
 
-(rum/defcs dropdown-editor < rum/reactive db-mixins/query
+        (dropdown-editor-menuitem
+         {:id :delete-property :icon :x
+          :title (if class-schema? "Delete property from tag" "Delete property from node")
+          :desc "" :disabled? false
+          :item-props {:class "opacity-60 focus:!text-red-rx-09 focus:opacity-100"
+                       :on-select (fn [^js e]
+                                    (util/stop e)
+                                    (-> (p/do!
+                                         (handle-delete-property! owner-block property {:class-schema? class-schema?})
+                                         (shui/popup-hide-all!))
+                                        (p/catch (fn [] (restore-root-highlight-item! :delete-property)))))}}))
+      (when debug?
+        [:<>
+         (shui/dropdown-menu-separator)
+         (dropdown-editor-menuitem
+          {:icon :bug :title (str (:db/ident property)) :desc "" :disabled? false
+           :item-props {:class "opacity-60 focus:opacity-100 focus:!text-red-rx-08"
+                        :on-select (fn []
+                                     (dev-common-handler/show-entity-data (:db/id property))
+                                     (shui/popup-hide!))}})])]
+     (concat more-options)
+     vec)))
+
+(rum/defcs property-dropdown < rum/reactive db-mixins/query
   {:init (fn [state]
            (let [*values (atom :loading)
                  property (first (:rum/args state))
@@ -762,4 +770,4 @@
         owner-block (when (:db/id owner-block) (db/sub-block (:db/id owner-block)))
         values (rum/react (::values state))]
     (when-not (= :loading values)
-      (dropdown-editor-impl property owner-block values opts))))
+      (vec (cons :<> (property-dropdown-options property owner-block values opts))))))

+ 3 - 1
src/main/frontend/components/property/value.cljs

@@ -1241,7 +1241,9 @@
                                  (set-editing! false))))]
     [:div.ls-number.flex.flex-1.jtrigger
      {:ref *ref
-      :on-click #(set-editing! true)}
+      :on-click #(do
+                   (state/clear-selection!)
+                   (set-editing! true))}
      (if editing?
        (shui/input
         {:ref *input-ref

+ 72 - 56
src/main/frontend/components/views.cljs

@@ -130,56 +130,59 @@
                   (contains? (set (map :db/id (:logseq.property.table/pinned-columns view-entity)))
                              (:db/id property)))
         sub-content (fn [{:keys [id]}]
-                      [:<>
-                       (shui/dropdown-menu-item
-                        {:key "asc"
-                         :on-click #(column-set-sorting! sorting column true)}
-                        [:div.flex.flex-row.items-center.gap-1
-                         (ui/icon "arrow-up" {:size 15})
-                         [:div "Sort ascending"]])
-                       (shui/dropdown-menu-item
-                        {:key "desc"
-                         :on-click #(column-set-sorting! sorting column false)}
-                        [:div.flex.flex-row.items-center.gap-1
-                         (ui/icon "arrow-down" {:size 15})
-                         [:div "Sort descending"]])
-                       (when property
-                         (shui/dropdown-menu-item
-                          {:on-click #(shui/popup-show! (.-target %)
-                                                        (fn []
-                                                          [:div.ls-property-dropdown-editor.-m-1
-                                                           (property-config/dropdown-editor property nil {})])
-                                                        {:align "start"})}
-                          [:div.flex.flex-row.items-center.gap-1
-                           (ui/icon "adjustments" {:size 15}) "Configure"]))
-                       (when (and db-based? property)
-                         (shui/dropdown-menu-item
-                          {:on-click (fn [_e]
-                                       (if pinned?
-                                         (db-property-handler/delete-property-value! (:db/id view-entity)
-                                                                                     :logseq.property.table/pinned-columns
-                                                                                     (:db/id property))
-                                         (property-handler/set-block-property! (state/get-current-repo)
-                                                                               (:db/id view-entity)
-                                                                               :logseq.property.table/pinned-columns
-                                                                               (:db/id property)))
-                                       (shui/popup-hide! id))}
-                          [:div.flex.flex-row.items-center.gap-1
-                           (ui/icon "pin" {:size 15})
-                           [:div (if pinned? "Unpin" "Pin")]]))])]
+                      (let [table-options [(shui/dropdown-menu-item
+                                            {:key "asc"
+                                             :on-click #(column-set-sorting! sorting column true)}
+                                            [:div.flex.flex-row.items-center.gap-1
+                                             (ui/icon "arrow-up" {:size 15})
+                                             [:div "Sort ascending"]])
+                                           (shui/dropdown-menu-item
+                                            {:key "desc"
+                                             :on-click #(column-set-sorting! sorting column false)}
+                                            [:div.flex.flex-row.items-center.gap-1
+                                             (ui/icon "arrow-down" {:size 15})
+                                             [:div "Sort descending"]])
+                                           (when (and db-based? property)
+                                             (shui/dropdown-menu-item
+                                              {:on-click (fn [_e]
+                                                           (if pinned?
+                                                             (db-property-handler/delete-property-value! (:db/id view-entity)
+                                                                                                         :logseq.property.table/pinned-columns
+                                                                                                         (:db/id property))
+                                                             (property-handler/set-block-property! (state/get-current-repo)
+                                                                                                   (:db/id view-entity)
+                                                                                                   :logseq.property.table/pinned-columns
+                                                                                                   (:db/id property)))
+                                                           (shui/popup-hide! id))}
+                                              [:div.flex.flex-row.items-center.gap-1
+                                               (ui/icon "pin" {:size 15})
+                                               [:div (if pinned? "Unpin" "Pin")]]))]
+                            tag (when-let [entity (:logseq.property/view-for view-entity)]
+                                  (when (ldb/class? entity)
+                                    entity))
+                            option (cond->
+                                    {:with-title? false
+                                     :more-options table-options}
+                                     (some? tag)
+                                     (assoc :class-schema? true))]
+                        [:div.ls-property-dropdown
+                         (property-config/property-dropdown property tag option)]))]
     (shui/button
      {:variant "text"
       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
-      :on-mouse-up (fn [^js e]
-                     (when-let [^js el (some-> (.-target e) (.closest "[aria-roledescription=sortable]"))]
-                       (when (and (or (nil? @*last-header-action-target)
-                                      (not= el @*last-header-action-target))
-                                  (string/blank? (some-> el (.-style) (.-transform))))
-                         (shui/popup-show! el sub-content
-                                           {:align "start" :as-dropdown? true
-                                            :on-before-hide (fn []
-                                                              (reset! *last-header-action-target el)
-                                                              (js/setTimeout #(reset! *last-header-action-target nil) 128))}))))}
+      :on-click (fn [^js e]
+                  (let [popup-id (str "table-column-" (:id column))]
+                    (when-let [^js el (some-> (.-target e) (.closest "[aria-roledescription=sortable]"))]
+                      (when (and (or (nil? @*last-header-action-target)
+                                     (not= el @*last-header-action-target))
+                                 (string/blank? (some-> el (.-style) (.-transform))))
+                        (shui/popup-show! el sub-content
+                                          {:id popup-id
+                                           :align "start"
+                                           :as-dropdown? true
+                                           :on-before-hide (fn []
+                                                             (reset! *last-header-action-target el)
+                                                             (js/setTimeout #(reset! *last-header-action-target nil) 128))})))))}
      (let [title (str (:name column))]
        [:span {:title title
                :class "max-w-full overflow-hidden text-ellipsis"}
@@ -211,6 +214,17 @@
        (container config' row)
        [:div])]))
 
+(defn- save-block-and-focus
+  [*ref set-focus-timeout! hide-popup?]
+  (let [node (rum/deref *ref)
+        cell (util/rec-get-node node "ls-table-cell")]
+    (p/do!
+     (editor-handler/save-current-block!)
+     (when hide-popup?
+       (shui/popup-hide!))
+     (state/exit-editing-and-set-selected-blocks! [cell])
+     (set-focus-timeout! (js/setTimeout #(.focus cell) 100)))))
+
 (rum/defc block-title
   "Used on table view"
   [block* {:keys [create-new-block width row property]}]
@@ -253,9 +267,16 @@
                                           [:div.ls-table-block.flex.flex-row.items-start
                                            {:style {:width width :max-width width :margin-right "6px"}
                                             :on-click util/stop-propagation}
-                                           (block-container {:popup? true
-                                                             :view? true
-                                                             :table-block-title? true} block)])))]
+                                           (block-container
+                                            {:popup? true
+                                             :view? true
+                                             :table-block-title? true
+                                             :on-key-down
+                                             (fn [e]
+                                               (when (= (util/ekey e) "Enter")
+                                                 (util/stop e)
+                                                 (save-block-and-focus *ref set-focus-timeout! true)))}
+                                            block)])))]
                           (p/do!
                            (shui/popup-show!
                             (.closest (.-target e) ".ls-table-cell")
@@ -263,12 +284,7 @@
                             {:id :ls-table-block-editor
                              :as-mask? true
                              :on-after-hide (fn []
-                                              (let [node (rum/deref *ref)
-                                                    cell (util/rec-get-node node "ls-table-cell")]
-                                                (p/do!
-                                                 (editor-handler/save-current-block!)
-                                                 (state/exit-editing-and-set-selected-blocks! [cell])
-                                                 (set-focus-timeout! (js/setTimeout #(.focus cell) 100)))))})
+                                              (save-block-and-focus *ref set-focus-timeout! false))})
                            (editor-handler/edit-block! block :max {:container-id :unknown-container})))))))}
      (if block
        [:div.flex.flex-row

+ 7 - 0
src/main/frontend/handler/common/developer.cljs

@@ -5,6 +5,7 @@
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.format.mldoc :as mldoc]
+            [frontend.handler.db-based.rtc :as rtc-handler]
             [frontend.handler.notification :as notification]
             [frontend.persist-db :as persist-db]
             [frontend.state :as state]
@@ -101,3 +102,9 @@
 
 (defn ^:export replace-graph-with-db-file []
   (state/pub-event! [:dialog-select/db-graph-replace]))
+
+(defn ^:export rtc-stop []
+  (rtc-handler/<rtc-stop!))
+
+(defn ^:export rtc-start []
+  (rtc-handler/<rtc-start! (state/get-current-repo)))

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

@@ -637,7 +637,15 @@
    :dev/validate-db   {:binding []
                        :db-graph? true
                        :inactive (not (state/developer-mode?))
-                       :fn :frontend.handler.common.developer/validate-db}})
+                       :fn :frontend.handler.common.developer/validate-db}
+   :dev/rtc-stop {:binding []
+                  :db-graph? true
+                  :inactive (not (state/developer-mode?))
+                  :fn :frontend.handler.common.developer/rtc-stop}
+   :dev/rtc-start {:binding []
+                   :db-graph? true
+                   :inactive (not (state/developer-mode?))
+                   :fn :frontend.handler.common.developer/rtc-start}})
 
 (let [keyboard-commands
       {::commands (set (keys all-built-in-keyboard-shortcuts))
@@ -866,6 +874,8 @@
           :dev/replace-graph-with-db-file
           :dev/validate-db
           :dev/fix-broken-graph
+          :dev/rtc-stop
+          :dev/rtc-start
           :ui/customize-appearance])
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
@@ -1059,6 +1069,8 @@
      :dev/replace-graph-with-db-file
      :dev/validate-db
      :dev/fix-broken-graph
+     :dev/rtc-stop
+     :dev/rtc-start
      :ui/clear-all-notifications]
 
     :shortcut.category/plugins

+ 3 - 2
src/main/frontend/worker/db/migrate.cljs

@@ -987,14 +987,15 @@
   (when (neg? compare-result)
     (js/console.warn (str "Current db schema-version is " db-schema/version ", max available schema-version is " max-schema-version))))
 
-(defn- ensure-built-in-data-exists!
+(defn ensure-built-in-data-exists!
   [conn]
   (let [*uuids (atom {})
         data (->> (sqlite-create-graph/build-db-initial-data "")
                   (keep (fn [data]
                           (if (map? data)
                             (cond
-                              (= (:db/ident data) :logseq.kv/schema-version)
+                              ;; Already created db-idents like :logseq.kv/graph-initial-schema-version should not be overwritten
+                              (= "logseq.kv" (some-> (:db/ident data) namespace))
                               nil
 
                               (= (:block/title data) "Contents")

+ 2 - 0
src/resources/dicts/en.edn

@@ -787,4 +787,6 @@
   :dev/replace-graph-with-db-file "(Dev) Replace graph with its db.sqlite file"
   :dev/validate-db "(Dev) Validate current graph"
   :dev/fix-broken-graph "(Dev) Fix current broken graph"
+  :dev/rtc-stop "(Dev) RTC Stop"
+  :dev/rtc-start "(Dev) RTC Start"
   :window/close "Close window"}}