Prechádzať zdrojové kódy

Squashed commit of the following:

commit 99a2d333f91dd209c8a4a61899ab35b996140678
Author: rcmerci <[email protected]>
Date:   Sat Nov 5 00:04:17 2022 +0800

    dev: restart sync when code changed

commit 4682274e102e561bab51bbfba5006d6430309be5
Author: kv-gits <[email protected]>
Date:   Fri Nov 4 18:08:43 2022 +0300

    Update develop-logseq-on-windows.md

    JRE x64

commit fcb5d206b273b0d44f8d74c03bbb09e52a9b8829
Author: rcmerci <[email protected]>
Date:   Fri Nov 4 17:36:34 2022 +0800

    fix: calculate s3-key length

commit e780272d126572fe83205bdde8998a31ebe8357b
Author: rcmerci <[email protected]>
Date:   Fri Nov 4 17:32:19 2022 +0800

    enhance(sync): filter pages whose page-name is too long

commit d45e5507b583b3178aa253edeffb74667ab46a31
Author: Peng Xiao <[email protected]>
Date:   Thu Nov 3 13:10:58 2022 +0800

    fix: e2e issue

commit c03b8503b04cceb2652a563f803bafd433eccaab
Author: Peng Xiao <[email protected]>
Date:   Thu Nov 3 11:35:44 2022 +0800

    test(whiteboard): new e2e cases

commit fbbcb1f862b5d2aaf959b6dcaa30c82a2d2a98dc
Author: Peng Xiao <[email protected]>
Date:   Thu Nov 3 10:53:45 2022 +0800

    fix: onboarding condition

commit 26f08061ac66b84eec7a86dda3074e0a47fb6b69
Author: Peng Xiao <[email protected]>
Date:   Thu Nov 3 10:43:59 2022 +0800

    fix(whiteboard): add loading when populating onboarding whiteboard

commit e8d1c1baa72edd5adecb895b3dcf3cc0c045fb0b
Author: Peng Xiao <[email protected]>
Date:   Wed Nov 2 20:45:33 2022 +0800

    feat(whiteboard): onboarding whiteboard

commit 2f5680bd4978c866f5142c2c4364031c4f5589b3
Author: Peng Xiao <[email protected]>
Date:   Tue Nov 1 10:51:42 2022 +0800

    wip

commit 48cfa27552b7821dbe5a237804c05986ceb21b90
Author: Peng Xiao <[email protected]>
Date:   Mon Oct 31 20:46:44 2022 +0800

    wip onboarding template

commit 3f17fd2cebbf8b59d60d66f6b7c5500eb319ffbd
Author: yoyurec <[email protected]>
Date:   Thu Nov 3 13:38:25 2022 +0200

    fix: add missed mark css vars

commit 8dfab3bd13894f3fa1c8717c1a94ef5e1beff306
Author: Konstantinos Kaloutas <[email protected]>
Date:   Thu Nov 3 21:26:46 2022 +0200

    fix: allow custom protocols

commit dda618ca8fe18a2f6df60959e45d97afa0ff9aca
Author: Tienson Qin <[email protected]>
Date:   Fri Nov 4 22:32:13 2022 +0800

    fix: lint warning

commit 6c5c3bb96f90a1570b3624dbc42a11e7e1958b31
Author: Tienson Qin <[email protected]>
Date:   Fri Nov 4 20:26:19 2022 +0800

    fix: some files may not have ext
charlie 3 rokov pred
rodič
commit
da3f6b094d

+ 2 - 2
docs/develop-logseq-on-windows.md

@@ -15,7 +15,7 @@ This is a guide on creating Logseq development environment on Windows with `Powe
 
 ### An example of installing pre-requisites on Windows
 * Install [Chocolatey](https://chocolatey.org/)
-* Install JRE
+* Install JRE (only x64, x32 will not work)
 * Install NVM for Windows, Node.js, and Yarn
   ```
   choco install nvm
@@ -78,4 +78,4 @@ add the following pair to `deps.edn`:
 }
 ```
 
-The mirrors above are friendly to Chinese developers(with bad network), developers with self-hosted repositories can use their own services.
+The mirrors above are friendly to Chinese developers(with bad network), developers with self-hosted repositories can use their own services.

+ 20 - 5
e2e-tests/whiteboards.spec.ts

@@ -1,5 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
+import { IsMac } from './utils'
 
 test('enable whiteboards', async ({ page }) => {
     await expect(page.locator('.nav-header .whiteboard')).toBeHidden()
@@ -14,7 +15,21 @@ test('enable whiteboards', async ({ page }) => {
 test('create new whiteboard', async ({ page }) => {
     await page.click('.nav-header .whiteboard')
     await page.click('#tl-create-whiteboard')
-    await expect(page.locator('.logseq-tldraw')).toBeVisible()
+    await expect(page.locator('.logseq-tldraw')).toHaveCount(1)
+})
+
+test('check if the page contains the onboarding whiteboard', async ({ page }) => {
+    await expect(page.locator('.tl-text-shape-wrapper >> text=Welcome to')).toHaveCount(1)
+})
+
+test('cleanup the shapes', async ({ page }) => {
+    if (IsMac) {
+        await page.keyboard.press('Meta+a')
+    } else {
+        await page.keyboard.press('Control+a')
+    }
+    await page.keyboard.press('Delete')
+    await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
 })
 
 test('set whiteboard title', async ({ page }) => {
@@ -23,12 +38,12 @@ test('set whiteboard title', async ({ page }) => {
     await expect(page.locator('.whiteboard-page-title .title')).toContainText("Untitled");
 
     await page.click('.whiteboard-page-title')
-    await page.type('.whiteboard-page-title .title', title)
+    await page.fill('.whiteboard-page-title input', title)
     await page.keyboard.press('Enter')
     await expect(page.locator('.whiteboard-page-title .title')).toContainText(title);
 
     await page.click('.whiteboard-page-title')
-    await page.type('.whiteboard-page-title .title', "-2")
+    await page.fill('.whiteboard-page-title input', title + "-2")
     await page.keyboard.press('Enter')
 
     // Updating non-default title should pop up a confirmation dialog
@@ -83,7 +98,7 @@ test('quick add another whiteboard', async ({ page }) => {
     await page.click('#tl-create-whiteboard')
     
     await page.click('.whiteboard-page-title')
-    await page.type('.whiteboard-page-title .title', "my-whiteboard-3")
+    await page.fill('.whiteboard-page-title input', "my-whiteboard-3")
     await page.keyboard.press('Enter')
     
     const canvas = await page.waitForSelector('.logseq-tldraw');
@@ -97,7 +112,7 @@ test('quick add another whiteboard', async ({ page }) => {
     const quickAdd$ = page.locator('.tl-quick-search')
     await expect(quickAdd$).toBeVisible()
 
-    await page.type('.tl-quick-search input', 'my-whiteboard')
+    await page.fill('.tl-quick-search input', 'my-whiteboard')
     await quickAdd$.locator('.tl-quick-search-option >> text=my-whiteboard-2').first().click()
 
     await expect(quickAdd$).toBeHidden()

+ 6 - 2
resources/css/common.css

@@ -79,6 +79,8 @@ html[data-theme='dark'] {
   --ls-page-blockquote-color: var(--ls-primary-text-color);
   --ls-page-blockquote-bg-color: var(--ls-secondary-background-color);
   --ls-page-blockquote-border-color: var(--ls-border-color);
+  --ls-page-mark-color: #262626;
+  --ls-page-mark-bg-color: #fef3ac;
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-page-inline-code-bg-color: #01222a;
   --ls-scrollbar-foreground-color: #11505f;
@@ -156,6 +158,8 @@ html[data-theme='light'] {
   --ls-page-blockquote-color: var(--ls-primary-text-color);
   --ls-page-blockquote-bg-color: #fbfaf8;
   --ls-page-blockquote-border-color: #799bbc;
+  --ls-page-mark-color: #262626;
+  --ls-page-mark-bg-color: #fef3ac;
   --ls-page-inline-code-bg-color: #f7f7f7;
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
@@ -760,8 +764,8 @@ a.navigation {
 /* text mark/highlight */
 
 mark {
-  background: #fef3ac;
-  color: #262626;
+  background: var(--ls-page-mark-bg-color);
+  color: var(--ls-page-mark-color);
   padding: 2px 4px;
   border-radius: 3px;
 }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
resources/whiteboard/onboarding.edn


+ 11 - 14
src/main/frontend/components/file_sync.cljs

@@ -24,7 +24,6 @@
             [frontend.util :as util]
             [frontend.util.fs :as fs-util]
             [frontend.storage :as storage]
-            [logseq.graph-parser.config :as gp-config]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
@@ -481,19 +480,17 @@
 
              (when (seq history-files)
                (map-indexed (fn [i f] (:time f)
-                              (let [path        (:path f)
-                                    ext         (string/lower-case (util/get-file-ext path))
-                                    _supported? (gp-config/mldoc-support? ext)
-                                    full-path   (util/node-path.join (config/get-repo-dir current-repo) path)
-                                    page-name   (db/get-file-page full-path)]
-                                {:title [:div.files-history.cursor-pointer
-                                         {:key      i :class (when (= i 0) "is-first")
-                                          :on-click (fn []
-                                                      (if page-name
-                                                        (rfe/push-state :page {:name page-name})
-                                                        (rfe/push-state :file {:path full-path})))}
-                                         [:span.file-sync-item (js/decodeURIComponent (:path f))]
-                                         [:div.opacity-50 (ui/humanity-time-ago (:time f) nil)]]}))
+                              (when-let [path (:path f)]
+                                (let [full-path   (util/node-path.join (config/get-repo-dir current-repo) path)
+                                      page-name   (db/get-file-page full-path)]
+                                  {:title [:div.files-history.cursor-pointer
+                                           {:key      i :class (when (= i 0) "is-first")
+                                            :on-click (fn []
+                                                        (if page-name
+                                                          (rfe/push-state :page {:name page-name})
+                                                          (rfe/push-state :file {:path full-path})))}
+                                           [:span.file-sync-item (js/decodeURIComponent (:path f))]
+                                           [:div.opacity-50 (ui/humanity-time-ago (:time f) nil)]]})))
                             (take 10 history-files)))))
 
           ;; options

+ 2 - 3
src/main/frontend/core.cljs

@@ -49,7 +49,7 @@
     (rum/mount (page/current-page) node)
     (display-welcome-message)
     (persist-var/load-vars)
-    (when (and config/dev? (util/electron?))
+    (when config/dev?
       (js/setTimeout #(sync/sync-start) 1000))))
 
 (defn ^:export init []
@@ -70,7 +70,6 @@
   ;; stop is called before any code is reloaded
   ;; this is controlled by :before-load in the config
   (handler/stop!)
-  (when (and config/dev? (util/electron?))
-
+  (when config/dev?
     (sync/<sync-stop))
   (js/console.log "stop"))

+ 24 - 16
src/main/frontend/extensions/tldraw.cljs

@@ -13,7 +13,8 @@
             [frontend.util :as util]
             [goog.object :as gobj]
             [promesa.core :as p]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.ui :as ui]))
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 
@@ -76,18 +77,20 @@
                        (route-handler/redirect-to-page! page-name)))})
 
 (rum/defc tldraw-app
-  [name block-id]
-  (let [data (whiteboard-handler/page-name->tldr! name block-id)
-        [tln set-tln] (rum/use-state nil)]
-    (rum/use-layout-effect!
-     (fn []
-       (when (and tln name)
-         (when-let [^js api (gobj/get tln "api")]
-           (when (and block-id (parse-uuid block-id))
-             (. api selectShapes block-id)
-             (. api zoomToSelection))))
-       nil) [name block-id tln])
-    (when (and (not-empty name) (not-empty (gobj/get data "currentPageId")))
+  [page-name block-id]
+  (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
+        data (whiteboard-handler/page-name->tldr! page-name block-id)
+        [loaded? set-loaded?] (rum/use-state false)
+        on-mount (fn [tln]
+                   (when-let [^js api (gobj/get tln "api")]
+                     (p/then (when populate-onboarding?
+                               (whiteboard-handler/populate-onboarding-whiteboard api))
+                             #(do (when (and block-id (parse-uuid block-id))
+                                    (. api selectShapes block-id)
+                                    (. api zoomToSelection))
+                                  (set-loaded? true)))))]
+
+    (when data
       [:div.draw.tldraw.whiteboard.relative.w-full.h-full
        {:style {:overscroll-behavior "none"}
         :on-blur (fn [e]
@@ -96,10 +99,15 @@
         ;; wheel -> overscroll may cause browser navigation
         :on-wheel util/stop-propagation}
 
+       (when
+        (and populate-onboarding? (not loaded?))
+         [:div.absolute.inset-0.flex.items-center.justify-center
+          {:style {:z-index 200}}
+          (ui/loading "Loading onboarding whiteboard ...")])
        (tldraw {:renderers tldraw-renderers
-                :handlers (get-tldraw-handlers name)
-                :onMount (fn [app] (set-tln ^js app))
+                :handlers (get-tldraw-handlers page-name)
+                :onMount on-mount
                 :onPersist (fn [app]
                              (let [document (gobj/get app "serialized")]
-                               (whiteboard-handler/transact-tldr! name document)))
+                               (whiteboard-handler/transact-tldr! page-name document)))
                 :model data})])))

+ 38 - 19
src/main/frontend/fs/sync.cljs

@@ -1177,12 +1177,14 @@
               paths-or-exp
               (let [encrypted-path->path-map (zipmap encrypted-paths paths-or-exp)]
                 (into #{}
-                      (map #(->FileMetadata (:Size %)
-                                            (:Checksum %)
-                                            (get encrypted-path->path-map (:FilePath %))
-                                            (:FilePath %)
-                                            (:LastModified %)
-                                            true nil))
+                      (comp
+                       (filter #(not= "filepath too long" (:Error %)))
+                       (map #(->FileMetadata (:Size %)
+                                             (:Checksum %)
+                                             (get encrypted-path->path-map (:FilePath %))
+                                             (:FilePath %)
+                                             (:LastModified %)
+                                             true nil)))
                       r))))))))
 
   (<get-remote-graph [this graph-name-opt graph-uuid-opt]
@@ -2326,25 +2328,40 @@
     {:keep   (persistent! *keep)
      :delete (persistent! *delete)}))
 
+(defn- <filter-too-long-filename
+  [graph-uuid local-files-meta]
+  (go (let [origin-fnames    (mapv :path local-files-meta)
+            encrypted-fnames (<! (<encrypt-fnames rsapi graph-uuid origin-fnames))
+            fnames-map (zipmap origin-fnames encrypted-fnames)
+            local-files-meta-map (into {} (map (fn [meta] [(:path meta) meta])) local-files-meta)]
+        (sequence
+         (comp
+          (filter
+           (fn [[path _]]
+                                        ; 950 = (- 1024 36 36 2)
+                                        ; 1024 - length of 'user-uuid/graph-uuid/'
+             (<= (count (get fnames-map path)) 950)))
+          (map second))
+         local-files-meta-map))))
 
 (defrecord ^:large-vars/cleanup-todo
-  Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
-                       ^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
-                       ;; control chans
-                       private-immediately-local->remote-chan private-recent-edited-chan]
+ Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
+                      ^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
+                         ;; control chans
+                      private-immediately-local->remote-chan private-recent-edited-chan]
   Object
   (filter-file-change-events-fn [_]
     (fn [^FileChangeEvent e]
       (go (and (instance? FileChangeEvent e)
                (if-let [mtime (:mtime (.-stat e))]
-                 ;; if mtime is not nil, it should be after (- now 1min)
-                 ;; ignore events too early
+                   ;; if mtime is not nil, it should be after (- now 1min)
+                   ;; ignore events too early
                  (> (* 1000 mtime) (tc/to-long (t/minus (t/now) (t/minutes 1))))
                  true)
                (or (string/starts-with? (.-dir e) base-path)
                    (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
                (not (ignored? e))     ;not ignored
-               ;; download files will also trigger file-change-events, ignore them
+                 ;; download files will also trigger file-change-events, ignore them
                (not (contains? (:recent-remote->local-files @*sync-state)
                                (<! (<file-change-event=>recent-remote->local-file-item
                                     graph-uuid e))))))))
@@ -2419,7 +2436,7 @@
                   {:need-sync-remote true})
 
               (need-reset-local-txid? r*) ;; TODO: this cond shouldn't be true,
-              ;; but some potential bugs cause local-txid > remote-txid
+                ;; but some potential bugs cause local-txid > remote-txid
               (let [remote-graph-info-or-ex (<! (<get-remote-graph remoteapi nil graph-uuid))
                     remote-txid             (:TXId remote-graph-info-or-ex)]
                 (if (or (instance? ExceptionInfo remote-graph-info-or-ex) (nil? remote-txid))
@@ -2442,7 +2459,7 @@
               succ?                   ; succ
               (do
                 (println "sync-local->remote! update txid" r*)
-                ;; persist txid
+                  ;; persist txid
                 (<! (<update-graphs-txid! r* graph-uuid user-uuid repo))
                 (reset! *txid r*)
                 {:succ true})
@@ -2483,11 +2500,13 @@
                 (filter-local-files-in-deletion-logs local-all-files-meta deletion-logs-or-exp)
                 recent-10-days-range   ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
                 diff-local-files       (->> (diff-file-metadata-sets local-all-files-meta remote-all-files-meta)
+                                            (<filter-too-long-filename graph-uuid)
+                                            <!
                                             (sort-by (sort-file-metadata-fn :recent-days-range recent-10-days-range) >))
                 change-events
                 (sequence
                  (comp
-                  ;; convert to FileChangeEvent
+                    ;; convert to FileChangeEvent
                   (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
                                            {:size (:size %)} (:etag %)))
                   (remove ignored?))
@@ -2496,7 +2515,7 @@
                 _                      (swap! *sync-state #(sync-state-reset-full-local->remote-files % distinct-change-events))
                 change-events-partitions
                 (sequence
-                 ;; partition FileChangeEvents
+                   ;; partition FileChangeEvents
                  (partition-file-change-events upload-batch-size)
                  distinct-change-events)]
             (println "[full-sync(local->remote)]"
@@ -2507,7 +2526,7 @@
                                       :graph-uuid graph-uuid
                                       :full-sync? true
                                       :epoch      (tc/to-epoch (t/now))}})
-            ;; 1. delete local files
+              ;; 1. delete local files
             (loop [[f & fs] delete-local-files]
               (when f
                 (let [relative-p (relative-path f)]
@@ -2523,7 +2542,7 @@
                                  [fake-recent-remote->local-file-item])))))
                 (recur fs)))
 
-            ;; 2. upload local files
+              ;; 2. upload local files
             (loop [es-partitions change-events-partitions]
               (if @*stopped
                 {:stop true}

+ 48 - 2
src/main/frontend/handler/whiteboard.cljs

@@ -4,13 +4,15 @@
             [dommy.core :as dom]
             [frontend.db.model :as model]
             [frontend.db.utils :as db-utils]
+            [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
             [frontend.modules.outliner.core :as outliner]
             [frontend.modules.outliner.file :as outliner-file]
             [frontend.state :as state]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.whiteboard :as gp-whiteboard]
-            [frontend.handler.editor :as editor-handler]))
+            [promesa.core :as p]))
 
 (defn shape->block [shape page-name idx]
   (let [properties {:ls-type :whiteboard-shape
@@ -202,7 +204,7 @@
         tx {:block/left (select-keys last-root-block [:db/id])
             :block/uuid uuid
             :block/content (or content "")
-            :block/format :markdown ; fixme
+            :block/format :markdown ;; fixme to support org?
             :block/page {:block/name (util/page-name-sanity-lc page-name)}
             :block/parent {:block/name page-name}}]
     (db-utils/transact! [tx])
@@ -216,3 +218,47 @@
   [target]
   (when-let [shape-el (dom/closest target "[data-shape-id]")]
     (.getAttribute shape-el "data-shape-id")))
+
+(defn get-onboard-whiteboard-edn
+  []
+  (p/let [^js res (js/fetch "./whiteboard/onboarding.edn") ;; do we need to cache it?
+          text (.text res)
+          edn (gp-util/safe-read-string text)]
+    edn))
+
+(defn clone-whiteboard-from-edn
+  "Given a tldr, clone the whiteboard page into current active whiteboard"
+  ([edn]
+   (when-let [app (state/active-tldraw-app)]
+     (clone-whiteboard-from-edn edn (.-api app))))
+  ([{:keys [pages blocks]} api]
+   (let [page-block (first pages)
+         ;; FIXME: should also clone normal blocks
+         shapes (->> blocks
+                     (filter gp-whiteboard/shape-block?)
+                     (map gp-whiteboard/block->shape)
+                     (sort-by :index))
+         tldr-page (gp-whiteboard/page-block->tldr-page page-block)
+         assets (:assets tldr-page)
+         bindings (:bindings tldr-page)]
+     (.cloneShapesIntoCurrentPage ^js api (clj->js {:shapes shapes
+                                                    :assets assets
+                                                    :bindings bindings})))))
+(defn should-populate-onboarding-whiteboard?
+  "When there is not whiteboard, or there is only whiteboard that is the given page name, we should populate the onboarding whiteboard"
+  [page-name]
+  (let [whiteboards (model/get-all-whiteboards (state/get-current-repo))]
+    (and (or (empty? whiteboards)
+             (and
+              (= 1 (count whiteboards))
+              (= page-name (:block/name (first whiteboards)))))
+         (not (state/get-onboarding-whiteboard?)))))
+
+(defn populate-onboarding-whiteboard
+  [api]
+  (when (some? api)
+    (-> (p/let [edn (get-onboard-whiteboard-edn)]
+          (clone-whiteboard-from-edn edn api)
+          (state/set-onboarding-whiteboard! true))
+        (p/catch
+         (fn [e] (js/console.warn "Faield to populate onboarding whiteboard" e))))))

+ 2 - 1
src/main/frontend/security.cljs

@@ -2,7 +2,8 @@
   "Provide security focused fns like preventing XSS attacks"
   (:require ["dompurify" :as DOMPurify]))
 
-(def sanitization-options (clj->js {:ADD_TAGS ["iframe"]}))
+(def sanitization-options (clj->js {:ADD_TAGS ["iframe"]
+                                    :ALLOW_UNKNOWN_PROTOCOLS true}))
 
 (defn sanitize-html
   [html]

+ 11 - 0
src/main/frontend/state.cljs

@@ -264,6 +264,8 @@
      :ui/find-in-page                       nil
      :graph/importing                       nil
      :graph/importing-state                 {}
+
+     :whiteboard/onboarding-whiteboard?     (or (storage/get :ls-onboarding-whiteboard?) false)
      })))
 
 ;; Block ast state
@@ -1917,3 +1919,12 @@ Similar to re-frame subscriptions"
   {:pre [(map? v)
          (= #{:repo :old-path :new-path} (set (keys v)))]}
   (async/offer! (get-file-rename-event-chan) v))
+
+(defn set-onboarding-whiteboard!
+  [v]
+  (set-state! :whiteboard/onboarding-whiteboard? v)
+  (storage/set :ls-onboarding-whiteboard? v))
+
+(defn get-onboarding-whiteboard?
+  []
+  (get-in @state [:whiteboard/onboarding-whiteboard?]))

+ 7 - 1
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

@@ -236,7 +236,13 @@ export const ContextMenu = observer(function ContextMenu({
               {developerMode && (
                 <ReactContextMenu.Item
                   className="tl-menu-item"
-                  onClick={() => console.log(app.selectedShapesArray.map(s => toJS(s.serialized)))}
+                  onClick={() => {
+                    if (app.selectedShapesArray.length === 1) {
+                      console.log(toJS(app.selectedShapesArray[0].serialized))
+                    } else {
+                      console.log(app.selectedShapesArray.map(s => toJS(s.serialized)))
+                    }
+                  }}
                 >
                   (Dev) Print shape props
                 </ReactContextMenu.Item>

+ 13 - 84
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -33,21 +33,6 @@ const isValidURL = (url: string) => {
   }
 }
 
-const safeParseJson = (json: string) => {
-  try {
-    return JSON.parse(json)
-  } catch {
-    return null
-  }
-}
-
-const getWhiteboardsTldrFromText = (text: string) => {
-  const innerText = text.match(/<whiteboard-tldr>(.*)<\/whiteboard-tldr>/)?.[1]
-  if (innerText) {
-    return safeParseJson(decodeURIComponent(innerText))
-  }
-}
-
 interface VideoImageAsset extends TLAsset {
   size?: number[]
 }
@@ -71,7 +56,7 @@ function getFileType(filename: string) {
   return 'unknown'
 }
 
-type MaybeShapes = Shape['props'][] | null | undefined
+type MaybeShapes = TLShapeModel[] | null | undefined
 
 type CreateShapeFN<Args extends any[]> = (...args: Args) => Promise<MaybeShapes> | MaybeShapes
 
@@ -263,67 +248,12 @@ export function usePaste() {
       }
 
       function tryCreateClonedShapesFromJSON(rawText: string) {
-        const data = getWhiteboardsTldrFromText(rawText)
-        try {
-          if (data) {
-            const shapes = data.shapes as TLShapeModel[]
-            assetsToClone = data.assets as TLAsset[]
-            const commonBounds = BoundsUtils.getCommonBounds(
-              shapes.map(shape => ({
-                minX: shape.point?.[0] ?? point[0],
-                minY: shape.point?.[1] ?? point[1],
-                width: shape.size?.[0] ?? 4,
-                height: shape.size?.[1] ?? 4,
-                maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
-                maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
-              }))
-            )
-            const bindings = data.bindings as Record<string, TLBinding>
-            const shapesToCreate = shapes.map(shape => {
-              return {
-                ...shape,
-                id: uniqueId(),
-                point: [
-                  point[0] + shape.point![0] - commonBounds.minX,
-                  point[1] + shape.point![1] - commonBounds.minY,
-                ],
-              }
-            })
-
-            // Try to rebinding the shapes to the new assets
-            shapesToCreate
-              .flatMap(s => Object.values(s.handles ?? {}))
-              .forEach(h => {
-                if (!h.bindingId) {
-                  return
-                }
-                // try to bind the new shape
-                const binding = bindings[h.bindingId]
-                if (binding) {
-                  // if the copied binding from/to is in the source
-                  const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
-                  const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
-                  if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
-                    const newBinding: TLBinding = {
-                      ...binding,
-                      id: uniqueId(),
-                      fromId: shapesToCreate[oldFromIdx].id,
-                      toId: shapesToCreate[oldToIdx].id,
-                    }
-                    bindingsToCreate.push(newBinding)
-                    h.bindingId = newBinding.id
-                  } else {
-                    h.bindingId = undefined
-                  }
-                } else {
-                  console.warn('binding not found', h.bindingId)
-                }
-              })
-
-            return shapesToCreate as Shape['props'][]
-          }
-        } catch (err) {
-          console.error(err)
+        const result = app.api.getClonedShapesFromTldrString(rawText, point)
+        if (result) {
+          const { shapes, assets, bindings } = result
+          assetsToClone.push(...assets)
+          bindingsToCreate.push(...bindings)
+          return shapes
         }
         return null
       }
@@ -420,7 +350,7 @@ export function usePaste() {
 
       app.cursors.setCursor(TLCursor.Progress)
 
-      let newShapes: Shape['props'][] = []
+      let newShapes: TLShapeModel[] = []
       try {
         if (dataTransfer) {
           newShapes.push(...((await tryCreateShapesFromDataTransfer(dataTransfer)) ?? []))
@@ -442,13 +372,13 @@ export function usePaste() {
       })
 
       app.wrapUpdate(() => {
-        const allAssets = [...imageAssetsToCreate, ...assetsToClone]
-        if (allAssets.length > 0) {
-          app.createAssets(allAssets)
+        if (assetsToClone.length > 0) {
+          app.createAssets(assetsToClone)
         }
-        if (allShapesToAdd.length > 0) {
-          app.createShapes(allShapesToAdd)
+        if (newShapes.length > 0) {
+          app.createShapes(newShapes)
         }
+        app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
 
         if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1 && !fromDrop) {
           const source = app.selectedShapesArray[0]
@@ -456,7 +386,6 @@ export function usePaste() {
           app.createNewLineBinding(source, target)
         }
 
-        app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
         app.setSelectedShapes(allShapesToAdd.map(s => s.id))
         app.selectedTool.transition('idle') // clears possible editing states
         app.cursors.setCursor(TLCursor.Default)

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx

@@ -48,7 +48,7 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
       <HTMLContainer {...events} opacity={isErasing ? 0.2 : opacity}>
         {isBinding && <BindingIndicator mode="html" strokeWidth={4} size={[w, h]} />}
 
-        <div className="tl-image-shape-container">
+        <div data-asset-loaded={!!asset} className="tl-image-shape-container">
           {asset ? (
             <img
               src={handlers ? handlers.makeAssetUrl(asset.src) : asset.src}

+ 5 - 9
tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx

@@ -45,7 +45,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     parentId: 'page',
     type: 'text',
     point: [0, 0],
-    size: [100, 100],
+    size: [0, 0],
     isSizeLocked: true,
     text: '',
     lineHeight: 1.2,
@@ -53,7 +53,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     fontWeight: 400,
     italic: false,
     padding: 4,
-    fontFamily: "var(--ls-font-family), 'Helvetica Neue', Helvetica, Arial, sans-serif",
+    fontFamily: 'var(--ls-font-family)',
     borderRadius: 0,
     stroke: '',
     fill: '',
@@ -170,13 +170,9 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     }, [isEditing, onEditingEnd])
 
     React.useLayoutEffect(() => {
-      const { fontFamily, fontSize, fontWeight, lineHeight, padding } = this.props
-      const [width, height] = getTextLabelSize(
-        text,
-        { fontFamily, fontSize, fontWeight, lineHeight },
-        padding
-      )
-      this.update({ size: [width, height] })
+      if (this.props.size[0] === 0 || this.props.size[1] === 0) {
+        this.onResetBounds()
+      }
     }, [])
 
     return (

+ 4 - 1
tldraw/apps/tldraw-logseq/src/styles.css

@@ -740,7 +740,10 @@ button.tl-select-input-trigger {
 
 .tl-image-shape-container {
   @apply h-full w-full overflow-hidden flex items-center justify-center pointer-events-auto;
-  background-color: var(--ls-secondary-background-color);
+
+  &[data-asset-loaded="false"] {
+    background-color: var(--ls-secondary-background-color);
+  }
 }
 
 .tl-html-container {

+ 148 - 2
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -1,6 +1,6 @@
 import Vec from '@tldraw/vec'
-import type { TLEventMap } from '../../types'
-import { BoundsUtils } from '../../utils'
+import type { TLAsset, TLBinding, TLEventMap } from '../../types'
+import { BoundsUtils, uniqueId } from '../../utils'
 import type { TLShape, TLShapeModel } from '../shapes'
 import type { TLApp } from '../TLApp'
 
@@ -195,4 +195,150 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
   createNewLineBinding = (source: S | string, target: S | string) => {
     return this.app.createNewLineBinding(source, target)
   }
+
+  /** Clone shapes with given context */
+  cloneShapes = ({
+    shapes,
+    assets,
+    bindings,
+    point = [0, 0],
+  }: {
+    shapes: TLShapeModel[]
+    point: number[]
+    // assets & bindings are the context for creating shapes
+    assets: TLAsset[]
+    bindings: Record<string, TLBinding>
+  }) => {
+    const commonBounds = BoundsUtils.getCommonBounds(
+      shapes.map(shape => ({
+        minX: shape.point?.[0] ?? point[0],
+        minY: shape.point?.[1] ?? point[1],
+        width: shape.size?.[0] ?? 4,
+        height: shape.size?.[1] ?? 4,
+        maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
+        maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
+      }))
+    )
+
+    const clonedShapes = shapes.map(shape => {
+      return {
+        ...shape,
+        id: uniqueId(),
+        point: [
+          point[0] + shape.point![0] - commonBounds.minX,
+          point[1] + shape.point![1] - commonBounds.minY,
+        ],
+      }
+    })
+
+    const clonedBindings: TLBinding[] = []
+
+    // Try to rebinding the shapes with the given bindings
+    clonedShapes
+      .flatMap(s => Object.values(s.handles ?? {}))
+      .forEach(handle => {
+        if (!handle.bindingId) {
+          return
+        }
+        // try to bind the new shape
+        const binding = bindings[handle.bindingId]
+        if (binding) {
+          // if the copied binding from/to is in the source
+          const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
+          const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
+          if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
+            const newBinding: TLBinding = {
+              ...binding,
+              id: uniqueId(),
+              fromId: clonedShapes[oldFromIdx].id,
+              toId: clonedShapes[oldToIdx].id,
+            }
+            clonedBindings.push(newBinding)
+            handle.bindingId = newBinding.id
+          } else {
+            handle.bindingId = undefined
+          }
+        } else {
+          console.warn('binding not found', handle.bindingId)
+        }
+      })
+
+    const clonedAssets = assets.filter(asset => {
+      // do we need to create new asset id?
+      return clonedShapes.some(shape => shape.assetId === asset.id)
+    })
+    return {
+      shapes: clonedShapes,
+      assets: clonedAssets,
+      bindings: clonedBindings,
+    }
+  }
+
+  getClonedShapesFromTldrString = (text: string, point: number[]) => {
+    const safeParseJson = (json: string) => {
+      try {
+        return JSON.parse(json)
+      } catch {
+        return null
+      }
+    }
+
+    const getWhiteboardsTldrFromText = (text: string) => {
+      const innerText = text.match(/<whiteboard-tldr>(.*)<\/whiteboard-tldr>/)?.[1]
+      if (innerText) {
+        return safeParseJson(decodeURIComponent(innerText))
+      }
+    }
+
+    try {
+      const data = getWhiteboardsTldrFromText(text)
+      if (!data) return null
+      const { shapes, bindings, assets } = data
+
+      return this.cloneShapes({
+        shapes,
+        bindings,
+        assets,
+        point,
+      })
+    } catch (err) {
+      console.log(err)
+    }
+    return null
+  }
+
+  cloneShapesIntoCurrentPage = (opts: {
+    shapes: TLShapeModel[]
+    point: number[]
+    // assets & bindings are the context for creating shapes
+    assets: TLAsset[]
+    bindings: Record<string, TLBinding>
+  }) => {
+    const data = this.cloneShapes(opts)
+    if (data) {
+      this.addClonedShapes(data)
+    }
+    return this
+  }
+
+  addClonedShapes = (opts: ReturnType<TLApi['cloneShapes']>) => {
+    const { shapes, assets, bindings } = opts
+    if (assets.length > 0) {
+      this.app.createAssets(assets)
+    }
+    if (shapes.length > 0) {
+      this.app.createShapes(shapes)
+    }
+    this.app.currentPage.updateBindings(Object.fromEntries(bindings.map(b => [b.id, b])))
+    this.app.selectedTool.transition('idle') // clears possible editing states
+    return this
+  }
+
+  addClonedShapesFromTldrString = (text: string, point: number[]) => {
+    const data = this.getClonedShapesFromTldrString(text, point)
+    if (data) {
+      this.addClonedShapes(data)
+    }
+    return this
+  }
 }

+ 12 - 2
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -70,7 +70,7 @@ export class TLApp<
 
   keybindingRegistered = false
   uuid = uniqueId()
-  
+
   static id = 'app'
   static initial = 'select'
 
@@ -451,8 +451,18 @@ export class TLApp<
     return this
   }
 
+  @action removeUnusedAssets = (): this => {
+    const usedAssets = this.getCleanUpAssets()
+    Object.keys(this.assets).forEach(assetId => {
+      if (!usedAssets.some(asset => asset.id === assetId)) {
+        delete this.assets[assetId]
+      }
+    })
+    this.persist()
+    return this
+  }
+
   getCleanUpAssets<T extends TLAsset>(): T[] {
-    let deleted = false
     const usedAssets = new Set<T>()
 
     this.pages.forEach(p =>

+ 7 - 4
tldraw/packages/core/src/utils/getTextSize.ts

@@ -1,5 +1,5 @@
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-let melm: any
+let melm: HTMLElement
 
 interface TLTextMeasureStyles {
   fontStyle?: string
@@ -32,6 +32,7 @@ function getMeasurementDiv() {
     zIndex: '9999',
     userSelect: 'none',
     pointerEvents: 'none',
+    font: 'var(--ls-font-family)',
   })
 
   pre.tabIndex = -1
@@ -88,15 +89,17 @@ export function getTextLabelSize(
       return [10, 10]
     }
 
-    if (!melm.parent) document.body.appendChild(melm)
+    if (!melm.parentNode) document.body.appendChild(melm)
 
     melm.innerHTML = `${text}&#8203;`
     melm.style.font = font
     melm.style.padding = padding + 'px'
 
+    const rect = melm.getBoundingClientRect()
+
     // In tests, offsetWidth and offsetHeight will be 0
-    const width = melm.offsetWidth || 1
-    const height = melm.offsetHeight || 1
+    const width = Math.ceil(rect.width || 1)
+    const height = Math.ceil(rect.height || 1)
 
     saveCached(text, font, padding, [width, height])
   }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov