Browse Source

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 years ago
parent
commit
839b5f35f9

+ 57 - 15
.github/workflows/e2e.yml

@@ -24,18 +24,10 @@ env:
   BABASHKA_VERSION: '0.8.1'
 
 jobs:
-
-  e2e-test-repeat:
+  e2e-test-build:
+    name: Build Test Artifact
     runs-on: ubuntu-latest
-    continue-on-error: true # Don't block other runs in matrix for fast debugging
-    strategy:
-      matrix:
-        repeat: [1, 2, 3, 4] # Test 4 times for E2E robustness
-
     steps:
-      - name: Repeat message
-        run: echo running E2E test with repeat id ${{ matrix.repeat }}
-
       - name: Checkout
         uses: actions/checkout@v2
 
@@ -88,21 +80,71 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
 
-      # NOTE: require the app to be build in debug mode(compile instead of build).
+      # NOTE: require the app to be build in debug mode
       - name: Prepare E2E test build
         run: |
-          yarn gulp:build && clojure -M:cljs compile app publishing electron
-          (cd static && yarn install && yarn rebuild:better-sqlite3)
+          yarn gulp:build && clojure -M:cljs release app electron --debug
+
+      # NOTE: should include .shadow-cljs if in dev mode(compile)
+      - name: Create Archive for build
+        run: tar czf static.tar.gz static
+
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: logseq-e2e-artifact
+          path: static.tar.gz
+          retention-days: 1
+
+  e2e-test-run:
+    needs: [ e2e-test-build ]
+    name: Test Shard ${{ matrix.shard }} Repeat ${{ matrix.repeat }}
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        repeat: [1, 2]
+        shard: [1, 2, 3]
+
+    steps:
+      - name: Repeat message
+        run: echo ::info title=StartUp::E2E testing shard ${{ matrix.shard}}/3 repeat ${{ matrix.repeat }}
+
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Download test build artifact
+        uses: actions/download-artifact@v3
+        with:
+          name: logseq-e2e-artifact
+
+      - name: Extract test Artifact
+        run: tar xzf static.tar.gz
+
+      - name: Set up Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            yarn.lock
+            static/yarn.lock
+
+      - name: Fetch yarn deps for E2E test
+        run: |
+          yarn install
+          (cd static && yarn install && yarn rebuild:all)
+        env:
+          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
 
-      # Exits with 0 if yarn.lock is up to date or 1 if we forgot to update it
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
 
       - name: Run Playwright test
-        run: xvfb-run -- yarn e2e-test
+        run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
         env:
           CI: true
           DEBUG: "pw:api"
+          RELEASE: true # skip dev only test
 
       # - name: Save test artifacts
       #   if: ${{ failure() }}

+ 42 - 70
e2e-tests/editor.spec.ts

@@ -111,6 +111,9 @@ test(
   "but dont trigger RIME #3440 ",
   // cases should trigger [[]] #3251
   async ({ page, block }) => {
+    // This test requires dev mode
+    test.skip(process.env.RELEASE === 'true', 'not avaliable for release version')
+
     for (let [idx, events] of [
       kb_events.win10_pinyin_left_full_square_bracket,
       kb_events.macos_pinyin_left_full_square_bracket
@@ -221,9 +224,8 @@ test('undo and redo after starting an action should not destroy text #6267', asy
 
   // Then type more, start an action prompt, and undo
   await page.keyboard.type('text2 ', { delay: 50 })
-  for (const char of '[[') {
-    await page.keyboard.type(char, { delay: 50 })
-  }
+  await page.keyboard.type('[[', { delay: 50 })
+
   await expect(page.locator(`[data-modal-name="page-search"]`)).toBeVisible()
   if (IsMac) {
     await page.keyboard.press('Meta+z')
@@ -254,10 +256,8 @@ test('undo after starting an action should close the action menu #6269', async (
     // Open the action modal
     await block.mustType('text1 ')
     await page.waitForTimeout(550)
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(50)
-    }
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100) // Tolerable delay for the action menu to open
     await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
 
@@ -278,11 +278,9 @@ test('#6266 moving cursor outside of brackets should close autocomplete menu', a
     // First, left arrow
     await createRandomPage(page)
 
-    await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await block.mustFill('t ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100) // Sometimes it doesn't trigger without this
     await autocompleteMenu.expectVisible(modalName)
 
@@ -293,12 +291,9 @@ test('#6266 moving cursor outside of brackets should close autocomplete menu', a
     // Then, right arrow
     await createRandomPage(page)
 
-    await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
-    await page.waitForTimeout(100)
+    await block.mustFill('t ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await autocompleteMenu.expectVisible(modalName)
 
     await page.waitForTimeout(100)
@@ -315,13 +310,9 @@ test('#6266 moving cursor outside of parens immediately after searching should s
     await createRandomPage(page)
 
     // Open the autocomplete menu
-    // TODO: Maybe remove these "text " entries in tests that don't need them
-    await block.mustFill('')
-    await page.waitForTimeout(550)
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await block.mustFill('t ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     await page.keyboard.type("some block search text")
     await page.waitForTimeout(100) // Sometimes it doesn't trigger without this
@@ -339,12 +330,9 @@ test('pressing up and down should NOT close autocomplete menu', async ({ page, b
     await createRandomPage(page)
 
     // Open the autocomplete menu
-    await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
-    await page.waitForTimeout(100) // Sometimes it doesn't trigger without this
+    await block.mustFill('t ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await autocompleteMenu.expectVisible(modalName)
     const cursorPos = await block.selectionStart()
 
@@ -365,18 +353,15 @@ test('moving cursor inside of brackets should NOT close autocomplete menu', asyn
     await createRandomPage(page)
 
     // Open the autocomplete menu
-    await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await block.mustType('test ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     if (commandTrigger === '[[') {
       await autocompleteMenu.expectVisible(modalName)
     }
 
-    await page.keyboard.type("search")
-    await page.waitForTimeout(100)
+    await page.keyboard.type("search", { delay: 20 })
     await autocompleteMenu.expectVisible(modalName)
 
     // Move cursor, still inside the brackets
@@ -393,10 +378,8 @@ test('moving cursor inside of brackets when autocomplete menu is closed should N
 
     // Open the autocomplete menu
     await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100) // Sometimes it doesn't trigger without this
     await autocompleteMenu.expectVisible(modalName)
 
@@ -408,16 +391,14 @@ test('moving cursor inside of brackets when autocomplete menu is closed should N
     await page.waitForTimeout(100)
     await autocompleteMenu.expectHidden(modalName)
 
-    await page.keyboard.press('ArrowLeft')
-    await page.waitForTimeout(100)
+    await page.keyboard.press('ArrowLeft', { delay: 50 })
     await autocompleteMenu.expectHidden(modalName)
 
-    await page.keyboard.press('ArrowLeft')
-    await page.waitForTimeout(100)
+    await page.keyboard.press('ArrowLeft', { delay: 50 })
     await autocompleteMenu.expectHidden(modalName)
 
     // Type a letter, this should open the autocomplete menu
-    await page.keyboard.type('z')
+    await page.keyboard.type('z', { delay: 20 })
     await page.waitForTimeout(100)
     await autocompleteMenu.expectVisible(modalName)
   }
@@ -429,14 +410,12 @@ test('selecting text inside of brackets should NOT close autocomplete menu', asy
 
     // Open the autocomplete menu
     await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     await autocompleteMenu.expectVisible(modalName)
 
-    await page.keyboard.type("some page search text")
+    await page.keyboard.type("some page search text", { delay: 10 })
     await page.waitForTimeout(100)
     await autocompleteMenu.expectVisible(modalName)
 
@@ -452,15 +431,13 @@ test('pressing backspace and remaining inside of brackets should NOT close autoc
     await createRandomPage(page)
 
     // Open the autocomplete menu
-    await block.mustFill('')
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char)
-      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
-    }
+    await block.mustFill('test ')
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     await autocompleteMenu.expectVisible(modalName)
 
-    await page.keyboard.type("some page search text")
+    await page.keyboard.type("some page search text", { delay: 10 })
     await page.waitForTimeout(100)
     await autocompleteMenu.expectVisible(modalName)
 
@@ -478,9 +455,8 @@ test('press escape when autocomplete menu is open, should close autocomplete men
     // Open the action modal
     await block.mustFill('text ')
     await page.waitForTimeout(550)
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char) // Type it one character at a time, because too quickly can fail to trigger it sometimes
-    }
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
     await page.waitForTimeout(100)
@@ -501,9 +477,8 @@ test('press escape when link/image dialog is open, should restore focus to input
     // Open the action modal
     await block.mustFill('')
     await page.waitForTimeout(550)
-    for (const char of commandTrigger) {
-      await page.keyboard.type(char) // Type it one character at a time, because too quickly can fail to trigger it sometimes
-    }
+    await page.keyboard.type(commandTrigger, { delay: 20 })
+
     await page.waitForTimeout(100)
     await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
     await page.waitForTimeout(100)
@@ -525,12 +500,9 @@ test('should show text after soft return when node is collapsed #5074', async ({
   const delay = 100
   await createRandomPage(page)
 
-  await page.type('textarea >> nth=0', 'Before soft return')
-  await page.waitForTimeout(delay)
-  await page.keyboard.press('Shift+Enter')
-  await page.waitForTimeout(delay)
-  await page.type('textarea >> nth=0', 'After soft return')
-  await page.waitForTimeout(delay)
+  await page.type('textarea >> nth=0', 'Before soft return', { delay: 10 })
+  await page.keyboard.press('Shift+Enter', { delay: 10 })
+  await page.type('textarea >> nth=0', 'After soft return', { delay: 10 })
 
   await block.enterNext()
   expect(await block.indent()).toBe(true)

+ 11 - 10
e2e-tests/utils.ts

@@ -171,7 +171,7 @@ export async function openLeftSidebar(page: Page): Promise<void> {
 
 export async function loadLocalGraph(page: Page, path: string): Promise<void> {
   await setMockedOpenDirPath(page, path);
-  
+
   const onboardingOpenButton = page.locator('strong:has-text("Choose a folder")')
 
   if (await onboardingOpenButton.isVisible()) {
@@ -183,12 +183,12 @@ export async function loadLocalGraph(page: Page, path: string): Promise<void> {
       await page.click('#left-menu.button')
       await expect(sidebar).toHaveClass(/is-open/)
     }
-    
+
     await page.click('#left-sidebar #repo-switch');
     await page.waitForSelector('#left-sidebar .dropdown-wrapper >> text="Add new graph"',
-    { state: 'visible', timeout: 5000 })
+      { state: 'visible', timeout: 5000 })
     await page.click('text=Add new graph')
-    await page.waitForSelector('strong:has-text("Choose a folder")',{ state: 'visible', timeout: 5000 })
+    await page.waitForSelector('strong:has-text("Choose a folder")', { state: 'visible', timeout: 5000 })
 
     expect(page.locator('#repo-name')).toHaveText(pathlib.basename(path))
   }
@@ -210,11 +210,12 @@ export async function loadLocalGraph(page: Page, path: string): Promise<void> {
 
   // If there is an error notification from a previous test graph being deleted,
   // close it first so it doesn't cover up the UI
-  let locator = await page.locator('.notification-close-button').first()
+  let locator = page.locator('.notification-close-button').first()
   while (await locator?.isVisible()) {
     await locator.click()
     await page.waitForTimeout(250)
-    locator = await page.locator('.notification-close-button', {timeout: 1000}).first()
+
+    expect(locator.isVisible()).resolves.toBe(false)
   }
 
   console.log('Graph loaded for ' + path)
@@ -245,7 +246,7 @@ export function systemModifier(shortcut: string): string {
   }
 }
 
-export async function captureConsoleWithPrefix(page: Page, prefix: string, timeout: number=3000): Promise<string> {
+export async function captureConsoleWithPrefix(page: Page, prefix: string, timeout: number = 3000): Promise<string> {
   return new Promise((resolve, reject) => {
     let console_handler = (msg: ConsoleMessage) => {
       let text = msg.text()
@@ -263,12 +264,12 @@ export async function queryPermission(page: Page, permission: PermissionName): P
   // Check if WebAPI clipboard supported
   return await page.evaluate(async (eval_permission: PermissionName): Promise<boolean> => {
     if (typeof navigator.permissions == "undefined")
-        return Promise.resolve(false);
+      return Promise.resolve(false);
     return navigator.permissions.query({
-      name: eval_permission 
+      name: eval_permission
     }).then((result: PermissionStatus): boolean => {
       return (result.state == "granted" || result.state == "prompt")
-   })
+    })
   }, permission)
 }
 

+ 8 - 5
ios/App/App/FileSync/FileSync.swift

@@ -72,15 +72,18 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
     var md5: String
     var size: Int
     var ctime: Int64
+    var mtime: Int64
 
     public init?(of fileURL: URL) {
         do {
-            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey])
+            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey,
+                                                                     .creationDateKey])
             guard fileAttributes.isRegularFile! else {
                 return nil
             }
             size = fileAttributes.fileSize ?? 0
-            ctime = Int64((fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
+            mtime = Int64((fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
+            ctime = Int64((fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
 
             // incremental MD5 checksum
             let bufferSize = 512 * 1024
@@ -107,7 +110,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
     }
 
     public var description: String {
-        return "SyncMetadata(md5=\(md5), size=\(size))"
+        return "SyncMetadata(md5=\(md5), size=\(size), mtime=\(mtime))"
     }
 }
 
@@ -235,7 +238,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             if let meta = SyncMetadata(of: url) {
                 var metaObj: [String: Any] = ["md5": meta.md5,
                                               "size": meta.size,
-                                              "ctime": meta.ctime]
+                                              "mtime": meta.mtime]
                 if fnameEncryptionEnabled() {
                     metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
                 }
@@ -263,7 +266,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                         let filePath = fileURL.relativePath(from: baseURL)!
                         var metaObj: [String: Any] = ["md5": meta.md5,
                                                       "size": meta.size,
-                                                      "ctime": meta.ctime]
+                                                      "mtime": meta.mtime]
                         if fnameEncryptionEnabled() {
                             metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
                         }

+ 3 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.8.4",
+  "version": "0.8.5",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -13,6 +13,7 @@
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
     "rebuild:better-sqlite3": "electron-rebuild -v 19.0.12 -f -w better-sqlite3",
+    "rebuild:all": "electron-rebuild -v 19.0.12 -f",
     "postinstall": "install-app-deps"
   },
   "config": {
@@ -36,7 +37,7 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.36",
+    "@logseq/rsapi": "0.0.38",
     "electron-deeplink": "1.0.10",
     "abort-controller": "3.0.0"
   },

+ 4 - 0
src/main/frontend/components/file_sync.cljs

@@ -1,5 +1,7 @@
 (ns frontend.components.file-sync
   (:require [cljs.core.async :as async]
+            [cljs.core.async.interop :refer [p->c]]
+            [frontend.util.persist-var :as persist-var]
             [clojure.string :as string]
             [electron.ipc :as ipc]
             [frontend.components.lazy-editor :as lazy-editor]
@@ -197,6 +199,7 @@
 
                                     (state/set-modal! confirm-fn {:center? true :close-btn? false})))
         turn-on                #(async/go
+                                  (async/<! (p->c (persist-var/-load fs-sync/graphs-txid)))
                                   (cond
                                     @*beta-unavailable?
                                     (state/pub-event! [:file-sync/onboarding-tip :unavailable])
@@ -208,6 +211,7 @@
                                     nil
 
                                     (and synced-file-graph?
+                                         (second @fs-sync/graphs-txid)
                                          (async/<! (fs-sync/<check-remote-graph-exists (second @fs-sync/graphs-txid))))
                                     (fs-sync/sync-start)
 

+ 49 - 37
src/main/frontend/fs/sync.cljs

@@ -26,8 +26,7 @@
             [frontend.fs :as fs]
             [frontend.encrypt :as encrypt]
             [medley.core :refer [dedupe-by]]
-            [rum.core :as rum]
-            [goog.object :as gobj]))
+            [rum.core :as rum]))
 
 ;;; ### Commentary
 ;; file-sync related local files/dirs:
@@ -173,6 +172,7 @@
 
 (def ws-addr config/WS-URL)
 
+;; Warning: make sure to `persist-var/-load` graphs-txid before using it.
 (def graphs-txid (persist-var/persist-var nil "graphs-txid"))
 
 (declare assert-local-txid<=remote-txid)
@@ -489,6 +489,7 @@
 (deftype FileMetadata [size etag path encrypted-path last-modified remote? ^:mutable normalized-path]
   Object
   (get-normalized-path [_]
+    (assert (string? path) path)
     (when-not normalized-path
       (set! normalized-path
             (cond-> path
@@ -508,14 +509,21 @@
   (-hash [_] (hash {:etag etag :path path}))
 
   ILookup
-  (-lookup [this k]
-    (gobj/get this (name k)))
-  (-lookup [this k not-found]
-    (or (gobj/get this (name k)) not-found))
+  (-lookup [o k] (-lookup o k nil))
+  (-lookup [_ k not-found]
+    (case k
+      :size size
+      :etag etag
+      :path path
+      :encrypted-path encrypted-path
+      :last-modified last-modified
+      :remote? remote?
+      not-found))
+
 
   IPrintWithWriter
   (-pr-writer [_ w _opts]
-    (write-all w (str {:size size :etag etag :path path :remote? remote?}))))
+    (write-all w (str {:size size :etag etag :path path :remote? remote? :last-modified last-modified}))))
 
 
 
@@ -528,7 +536,7 @@
     "logseq/metadata.edn"})
 
 ;; TODO: use fn some to filter FileMetadata here, it cause too much loop
-(defn- diff-file-metadata-sets
+(defn diff-file-metadata-sets
   "Find the `FileMetadata`s that exists in s1 and does not exist in s2,
   compare by path+checksum+last-modified,
   if s1.path = s2.path & s1.checksum <> s2.checksum & s1.last-modified > s2.last-modified
@@ -812,7 +820,7 @@
                js->clj
                (map (fn [[path metadata]]
                       (->FileMetadata (get metadata "size") (get metadata "md5") path
-                                      (get metadata "encryptedFname") nil false nil)))
+                                      (get metadata "encryptedFname") (get metadata "mtime") false nil)))
                set)))))
 
   (<get-local-files-meta [_ _graph-uuid base-path filepaths]
@@ -825,7 +833,7 @@
              js->clj
              (map (fn [[path metadata]]
                     (->FileMetadata (get metadata "size") (get metadata "md5") path
-                                    (get metadata "encryptedFname") nil false nil)))
+                                    (get metadata "encryptedFname") (get metadata "mtime") false nil)))
              set))))
 
   (<rename-local-file [_ _graph-uuid base-path from to]
@@ -2139,7 +2147,7 @@
   "filter out FileChangeEvents checksum changed,
   compare checksum in FileChangeEvent and checksum calculated now"
   [es]
-  {:pre [(coll? es)
+  {:pre [(or (nil? es) (coll? es))
          (every? #(instance? FileChangeEvent %) es)]}
   (go
     (when (seq es)
@@ -2172,7 +2180,7 @@
 (defn- filter-too-huge-files
   "filter out files > `file-size-limit`"
   [es]
-  {:pre [(coll? es)
+  {:pre [(or (nil? es) (coll? es))
          (every? #(instance? FileChangeEvent %) es)]}
   (filterv filter-too-huge-files-aux es))
 
@@ -2720,6 +2728,7 @@
 
 (defn <check-remote-graph-exists
   [local-graph-uuid]
+  {:pre [(util/uuid-string? local-graph-uuid)]}
   (go
     (let [result (->> (<! (<list-remote-graphs remoteapi))
                       :Graphs
@@ -2731,36 +2740,39 @@
       result)))
 
 (defn sync-start []
-  (let [[user-uuid graph-uuid txid] @graphs-txid
-        *sync-state                 (atom (sync-state))
+  (let [*sync-state                 (atom (sync-state))
         current-user-uuid           (user/user-uuid)
         repo                        (state/get-current-repo)]
     (go
       ;; stop previous sync
       (<! (<sync-stop))
-      (when (and user-uuid graph-uuid txid
-                 (user/logged-in?)
-                 repo
-                 (not (config/demo-graph? repo)))
-        (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
-                                               (config/get-repo-dir repo) repo
-                                               txid *sync-state)]
-          (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
-            (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
-              (clear-graphs-txid! repo)
-              (do
-                (state/set-file-sync-state repo @*sync-state)
-                (state/set-file-sync-manager sm)
-
-                ;; update global state when *sync-state changes
-                (add-watch *sync-state ::update-global-state
-                           (fn [_ _ _ n]
-                             (state/set-file-sync-state repo n)))
-
-                (.start sm)
-
-                (offer! remote->local-full-sync-chan true)
-                (offer! full-sync-chan true)))))))))
+
+      (<! (p->c (persist-var/-load graphs-txid)))
+
+      (let [[user-uuid graph-uuid txid] @graphs-txid]
+        (when (and user-uuid graph-uuid txid
+                   (user/logged-in?)
+                   repo
+                   (not (config/demo-graph? repo)))
+          (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
+                                                 (config/get-repo-dir repo) repo
+                                                 txid *sync-state)]
+            (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
+              (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
+                (clear-graphs-txid! repo)
+                (do
+                  (state/set-file-sync-state repo @*sync-state)
+                  (state/set-file-sync-manager sm)
+
+                  ;; update global state when *sync-state changes
+                  (add-watch *sync-state ::update-global-state
+                             (fn [_ _ _ n]
+                               (state/set-file-sync-state repo n)))
+
+                  (.start sm)
+
+                  (offer! remote->local-full-sync-chan true)
+                  (offer! full-sync-chan true))))))))))
 
 ;;; ### some add-watches
 

+ 15 - 13
src/main/frontend/handler/file_sync.cljs

@@ -44,19 +44,21 @@
         (let [tx-info [0 r (user/user-uuid) (state/get-current-repo)]]
           (apply sync/update-graphs-txid! tx-info)
           (swap! refresh-file-sync-component not) tx-info)
-        (cond
-          ;; already processed this exception by events
-          ;; - :file-sync/storage-exceed-limit
-          ;; - :file-sync/graph-count-exceed-limit
-          (or (sync/storage-exceed-limit? r)
-              (sync/graph-count-exceed-limit? r))
-          nil
-
-          (contains? #{400 404} (get-in (ex-data r) [:err :status]))
-          (notification/show! (str "Create graph failed: already existed graph: " name) :warning true nil 4000)
-
-          :else
-          (notification/show! (str "Create graph failed:" r) :warning true nil 4000))))))
+        (do
+          (state/set-state! [:ui/loading? :graph/create-remote?] false)
+          (cond
+           ;; already processed this exception by events
+           ;; - :file-sync/storage-exceed-limit
+           ;; - :file-sync/graph-count-exceed-limit
+           (or (sync/storage-exceed-limit? r)
+               (sync/graph-count-exceed-limit? r))
+           nil
+
+           (contains? #{400 404} (get-in (ex-data r) [:err :status]))
+           (notification/show! (str "Create graph failed: already existed graph: " name) :warning true nil 4000)
+
+           :else
+           (notification/show! (str "Create graph failed:" r) :warning true nil 4000)))))))
 
 (defn <delete-graph
   [graph-uuid]

+ 3 - 3
src/main/frontend/modules/shortcut/dicts.cljc

@@ -76,11 +76,11 @@
    :editor/zoom-in                 "Zoom in editing block / Forwards otherwise"
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :ui/toggle-brackets             "Toggle whether to display brackets"
-   :go/search-in-page              "Search in the current page"
-   :go/electron-find-in-page       "Find in page"
+   :go/search-in-page              "Search blocks in the current page"
+   :go/electron-find-in-page       "Find text in page"
    :go/electron-jump-to-the-next   "Jump to the next match to your Find bar search"
    :go/electron-jump-to-the-previous "Jump to the previous match to your Find bar search"
-   :go/search                      "Full text search"
+   :go/search                      "Search pages and blocks"
    :go/journals                    "Go to journals"
    :go/backward                    "Backwards"
    :go/forward                     "Forwards"

+ 22 - 1
src/test/frontend/fs/sync_test.cljs

@@ -3,7 +3,6 @@
             [clojure.test :refer [deftest are]]))
 
 (deftest ignored?
-  []
   (are [x y] (= y (sync/ignored? x))
     ".git" true
     ".gitignore" true
@@ -20,3 +19,25 @@
     "pages/test.md" false
     "journals/2022_01_01.md" false
     ))
+
+
+(deftest diff-file-metadata-sets
+  (are [x y z] (= x (sync/diff-file-metadata-sets y z))
+    #{}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+
+    #{}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+    #{(sync/->FileMetadata 1 22 "3" 4 6 nil nil)}
+
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+    #{(sync/->FileMetadata 1 22 "3" 4 4 nil nil) (sync/->FileMetadata 1 22 "3" 44 5 nil nil)}
+
+    #{}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil)}
+    #{(sync/->FileMetadata 1 2 "3" 4 4 nil nil) (sync/->FileMetadata 1 2 "3" 4 6 nil nil)}
+
+    )
+  )