diff --git a/.carve/ignore b/.carve/ignore index f0efb24db7..8cd677525c 100644 --- a/.carve/ignore +++ b/.carve/ignore @@ -82,3 +82,5 @@ logseq.graph-parser.nbb-test-runner/run-tests ;; For debugging frontend.fs.sync/debug-print-sync-events-loop frontend.fs.sync/stop-debug-print-sync-events-loop +;; Used in macro +frontend.state/get-current-edit-block-and-position diff --git a/.gitignore b/.gitignore index dd4194aaef..f631847aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,8 @@ android/app/src/main/assets/capacitor.config.json /public/static .yarn/ .yarnrc.yml + +deps/shui/.lsp +deps/shui/.lsp-cache +deps/shui/.clj-kondo +deps/shui/shui-graph/logseq/bak diff --git a/android/app/build.gradle b/android/app/build.gradle index c3e288615c..1bc29bab0f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.logseq.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 57 - versionName "0.9.4" + versionCode 59 + versionName "0.9.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/java/com/logseq/app/FsWatcher.java b/android/app/src/main/java/com/logseq/app/FsWatcher.java index 5bf70be0cd..3b07e9b5d9 100644 --- a/android/app/src/main/java/com/logseq/app/FsWatcher.java +++ b/android/app/src/main/java/com/logseq/app/FsWatcher.java @@ -10,6 +10,8 @@ import android.net.Uri; import java.io.*; +import java.net.URI; +import java.text.Normalizer; import java.util.HashMap; import java.util.Map; import java.util.Stack; @@ -90,8 +92,11 @@ public class FsWatcher extends Plugin { shouldRead = true; } - obj.put("path", Uri.fromFile(f)); - obj.put("dir", Uri.fromFile(new File(mPath))); + URI dir = (new File(mPath)).toURI(); + URI fpath = f.toURI(); + + obj.put("path", Normalizer.normalize(dir.relativize(fpath).toString(), Normalizer.Form.NFC)); + obj.put("dir", Uri.fromFile(new File(mPath))); // Uri is for Android. URI is for RFC compatible JSObject stat; switch (event) { diff --git a/deps.edn b/deps.edn index 7a9b82c70f..14439b3ddc 100644 --- a/deps.edn +++ b/deps.edn @@ -27,11 +27,12 @@ camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"} instaparse/instaparse {:mvn/version "1.4.10"} org.clojars.mmb90/cljs-cache {:mvn/version "0.1.4"} + fipp/fipp {:mvn/version "0.6.26"} logseq/common {:local/root "deps/common"} logseq/graph-parser {:local/root "deps/graph-parser"} logseq/publishing {:local/root "deps/publishing"} - metosin/malli {:mvn/version "0.10.0"} - fipp/fipp {:mvn/version "0.6.26"}} + logseq/shui {:local/root "deps/shui"} + metosin/malli {:mvn/version "0.10.0"}} :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"] :extra-deps {org.clojure/clojurescript {:mvn/version "1.11.54"} diff --git a/deps/common/src/logseq/common/path.cljs b/deps/common/src/logseq/common/path.cljs index af22e2abc7..8553ef6dce 100644 --- a/deps/common/src/logseq/common/path.cljs +++ b/deps/common/src/logseq/common/path.cljs @@ -6,7 +6,7 @@ (defn- safe-decode-uri-component [uri] (try - (js/decodeURIComponent uri) + (.normalize (js/decodeURIComponent uri) "NFC") (catch :default _ (js/console.error "decode-uri-component-failed" uri) uri))) @@ -157,7 +157,6 @@ (defn path-join "Join path segments, or URL base and path segments" [base & segments] - (cond ;; For debugging ; (nil? base) @@ -190,9 +189,10 @@ (defn path-normalize "Normalize path or URL" [path] - (if (is-file-url? path) - (url-normalize path) - (path-normalize-internal path))) + (-> (if (is-file-url? path) + (url-normalize path) + (path-normalize-internal path)) + (.normalize "NFC"))) (defn url-to-path "Extract path part of a URL, decoded. diff --git a/deps/db/src/logseq/db/schema.cljs b/deps/db/src/logseq/db/schema.cljs index a65e3893d8..8160955fbe 100644 --- a/deps/db/src/logseq/db/schema.cljs +++ b/deps/db/src/logseq/db/schema.cljs @@ -105,7 +105,6 @@ (def retract-attributes #{ :block/refs - :block/path-refs :block/tags :block/alias :block/marker diff --git a/deps/graph-parser/.carve/ignore b/deps/graph-parser/.carve/ignore index 1dd02aab9c..76e2443ea1 100644 --- a/deps/graph-parser/.carve/ignore +++ b/deps/graph-parser/.carve/ignore @@ -46,3 +46,7 @@ logseq.graph-parser.text/get-file-basename logseq.graph-parser.mldoc/mldoc-link? ;; public var logseq.graph-parser.schema.mldoc/block-ast-coll-schema +;; API +logseq.graph-parser.config/img-formats +;; API +logseq.graph-parser.config/text-formats diff --git a/deps/graph-parser/src/logseq/graph_parser.cljs b/deps/graph-parser/src/logseq/graph_parser.cljs index 243d77f0fe..6bb33fd287 100644 --- a/deps/graph-parser/src/logseq/graph_parser.cljs +++ b/deps/graph-parser/src/logseq/graph_parser.cljs @@ -89,7 +89,6 @@ Options available: {:keys [tx ast]} (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format) :date-formatter "MMM do, yyyy" - :supported-formats (gp-config/supported-formats) :uri-encoded? false :filename-format :legacy} extract-options diff --git a/deps/graph-parser/src/logseq/graph_parser/block.cljs b/deps/graph-parser/src/logseq/graph_parser/block.cljs index 530fd86575..f4bb4b3286 100644 --- a/deps/graph-parser/src/logseq/graph_parser/block.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/block.cljs @@ -35,40 +35,32 @@ (string/join)))) (defn- get-page-reference - [block supported-formats] + [block format] (let [page (cond (and (vector? block) (= "Link" (first block))) - (let [typ (first (:url (second block))) + (let [url-type (first (:url (second block))) value (second (:url (second block)))] ;; {:url ["File" "file:../pages/hello_world.org"], :label [["Plain" "hello world"]], :title nil} (or (and - (= typ "Page_ref") + (= url-type "Page_ref") (and (string? value) (not (or (gp-config/local-asset? value) (gp-config/draw? value)))) value) (and - (= typ "Search") + (= url-type "Search") (page-ref/page-ref? value) (text/page-ref-un-brackets! value)) - (and - (= typ "Search") - (not (contains? #{\# \* \/ \[} (first value))) - ;; FIXME: use `gp-util/get-format` instead - (let [ext (some-> (gp-util/get-file-ext value) keyword)] - (when (and (not (string/starts-with? value "http:")) - (not (string/starts-with? value "https:")) - (not (string/starts-with? value "file:")) - (not (gp-config/local-asset? value)) - (or (#{:excalidraw :tldr} ext) - (not (contains? supported-formats ext)))) - value))) + (and (= url-type "Search") + (= format :org) + (not (gp-config/local-asset? value)) + value) (and - (= typ "File") + (= url-type "File") (second (first (:label (second block))))))) (and (vector? block) (= "Nested_link" (first block))) @@ -329,7 +321,7 @@ nil)) (defn- with-page-refs - [{:keys [title body tags refs marker priority] :as block} with-id? supported-formats db date-formatter] + [{:keys [title body tags refs marker priority] :as block} with-id? db date-formatter] (let [refs (->> (concat tags refs [marker priority]) (remove string/blank?) (distinct)) @@ -340,7 +332,7 @@ (when-not (and (vector? form) (= (first form) "Custom") (= (second form) "query")) - (when-let [page (get-page-reference form supported-formats)] + (when-let [page (get-page-reference form (:format block))] (swap! *refs conj page)) (when-let [tag (get-tag form)] (let [tag (text/page-ref-un-brackets! tag)] @@ -431,9 +423,9 @@ (map (fn [page] (page-name->map page true db true date-formatter)) page-refs))) (defn- with-page-block-refs - [block with-id? supported-formats db date-formatter] + [block with-id? db date-formatter] (some-> block - (with-page-refs with-id? supported-formats db date-formatter) + (with-page-refs with-id? db date-formatter) with-block-refs block-tags->pages (update :refs (fn [col] (remove nil? col))))) @@ -507,7 +499,7 @@ (mapv macro->block @*result))) (defn with-pre-block-if-exists - [blocks body pre-block-properties encoded-content {:keys [supported-formats db date-formatter user-config]}] + [blocks body pre-block-properties encoded-content {:keys [db date-formatter user-config]}] (let [first-block (first blocks) first-block-start-pos (get-in first-block [:block/meta :start_pos]) @@ -539,7 +531,7 @@ :block/macros (extract-macros-from-ast body) :block/body body} {:keys [tags refs]} - (with-page-block-refs {:body body :refs property-refs} false supported-formats db date-formatter)] + (with-page-block-refs {:body body :refs property-refs} false db date-formatter)] (cond-> block tags (assoc :block/tags tags) @@ -557,7 +549,7 @@ properties)) (defn- construct-block - [block properties timestamps body encoded-content format pos-meta with-id? {:keys [block-pattern supported-formats db date-formatter]}] + [block properties timestamps body encoded-content format pos-meta with-id? {:keys [block-pattern db date-formatter]}] (let [id (get-custom-id-or-new-id properties) ref-pages-in-properties (->> (:page-refs properties) (remove string/blank?)) @@ -594,7 +586,7 @@ (merge block (timestamps->scheduled-and-deadline timestamps)) block) block (assoc block :body body) - block (with-page-block-refs block with-id? supported-formats db date-formatter) + block (with-page-block-refs block with-id? db date-formatter) block (update block :refs concat (:block-refs properties)) {:keys [created-at updated-at]} (:properties properties) block (cond-> block @@ -648,7 +640,7 @@ `content`: markdown or org-mode text. `with-id?`: If `with-id?` equals to true, all the referenced pages will have new db ids. `format`: content's format, it could be either :markdown or :org-mode. - `options`: Options supported are :user-config, :block-pattern :supported-formats, + `options`: Options supported are :user-config, :block-pattern, :extract-macros, :date-formatter, :page-name and :db" [blocks content with-id? format {:keys [user-config] :as options}] {:pre [(seq blocks) (string? content) (boolean? with-id?) (contains? #{:markdown :org} format)]} diff --git a/deps/graph-parser/src/logseq/graph_parser/cli.cljs b/deps/graph-parser/src/logseq/graph_parser/cli.cljs index 60e9f70d0c..79b9aec838 100644 --- a/deps/graph-parser/src/logseq/graph_parser/cli.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/cli.cljs @@ -48,7 +48,6 @@ TODO: Fail fast when process exits 1" [conn files {:keys [config] :as options}] (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config) :user-config config - :supported-formats (gp-config/supported-formats) :filename-format (or (:file/name-format config) :legacy) :extracted-block-ids (atom #{})} (select-keys options [:verbose]))] diff --git a/deps/graph-parser/src/logseq/graph_parser/config.cljs b/deps/graph-parser/src/logseq/graph_parser/config.cljs index 1ad2306e75..4d31d80c43 100644 --- a/deps/graph-parser/src/logseq/graph_parser/config.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/config.cljs @@ -1,7 +1,6 @@ (ns logseq.graph-parser.config "App config that is shared between graph-parser and rest of app" - (:require [clojure.set :as set] - [clojure.string :as string] + (:require [clojure.string :as string] [goog.object :as gobj])) (def app-name @@ -75,11 +74,6 @@ [] #{:gif :svg :jpeg :ico :png :jpg :bmp :webp}) -(defn supported-formats - [] - (set/union (text-formats) - (img-formats))) - (defn get-date-formatter [config] (or diff --git a/deps/graph-parser/src/logseq/graph_parser/property.cljs b/deps/graph-parser/src/logseq/graph_parser/property.cljs index fe4277bcb6..fa3495ac85 100644 --- a/deps/graph-parser/src/logseq/graph_parser/property.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/property.cljs @@ -51,7 +51,12 @@ "Properties used by logseq that user can edit" [] (into #{:title :icon :template :template-including-parent :public :filters :exclude-from-graph-view - :logseq.query/nlp-date + :logseq.query/nlp-date + ;; view props + :logseq.color + ;; table props + :logseq.table.version :logseq.table.compact :logseq.table.headers :logseq.table.hover + :logseq.table.borders :logseq.table.stripes :logseq.table.max-width ;; org-mode only :macro :filetags} editable-linkable-built-in-properties)) diff --git a/deps/graph-parser/test/logseq/graph_parser_test.cljs b/deps/graph-parser/test/logseq/graph_parser_test.cljs index e5c414133c..78478c1175 100644 --- a/deps/graph-parser/test/logseq/graph_parser_test.cljs +++ b/deps/graph-parser/test/logseq/graph_parser_test.cljs @@ -117,7 +117,7 @@ :where [?b :block/name ?name] [?b :block/type "whiteboard"]] - @conn)] + @conn)] (is (= pages #{["foo"] ["bar"]})))))) (defn- test-property-order [num-properties] @@ -153,11 +153,11 @@ "- desc:: \"#foo is not a ref\"" {:extract-options {:user-config {}}}) block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}]) - :in $ - :where [?b :block/properties]] - @conn) - (map first) - first)] + :in $ + :where [?b :block/properties]] + @conn) + (map first) + first)] (is (= {:desc "\"#foo is not a ref\""} (:block/properties block)) "Quoted value is unparsed") @@ -274,9 +274,9 @@ set) (set refs)) ; pre-block/page has expected refs - db-properties (first (map :block/refs blocks)) + db-properties (first (map :block/refs blocks)) ;; block has expected refs - block-db-properties (second (map :block/refs blocks)))))) + block-db-properties (second (map :block/refs blocks)))))) (deftest property-relationships (let [properties {:single-link "[[bar]]" @@ -324,7 +324,7 @@ (map #(select-keys % [:block/properties :block/invalid-properties])))) "Has correct (in)valid page properties"))) -(deftest correct-page-names-created +(deftest correct-page-names-created-from-title (testing "from title" (let [conn (ldb/start-conn) built-in-pages (set (map string/lower-case default-db/built-in-pages-names))] @@ -358,17 +358,41 @@ @conn) (map (comp :block/original-name first)) (remove built-in-pages) - set))))) + set)))))) - (testing "for file and web uris" +(deftest correct-page-names-created-from-page-refs + (testing "for file, mailto, web and other uris in markdown" (let [conn (ldb/start-conn) built-in-pages (set (map string/lower-case default-db/built-in-pages-names))] (graph-parser/parse-file conn "foo.md" - (str "- [Filename.txt](file:///E:/test/Filename.txt)\n" - "- [example](https://example.com)") - {}) - (is (= #{"foo"} + (str "- [title]([[bar]])\n" + ;; all of the uris below do not create pages + "- ![image.png](../assets/image_1630480711363_0.png)\n" + "- [Filename.txt](file:///E:/test/Filename.txt)\n" + "- [mail](mailto:test@test.com?subject=TestSubject)\n" + "- [onenote link](onenote:https://d.docs.live.net/b2127346582e6386a/blablabla/blablabla/blablabla%20blablabla.one#Etat%202019§ion-id={133DDF16-9A1F-4815-9A05-44303784442E6F94}&page-id={3AAB677F0B-328F-41D0-AFF5-66408819C085}&end)\n" + "- [lock file](deps/graph-parser/yarn.lock)" + "- [example](https://example.com)")) + (is (= #{"foo" "bar"} + (->> (d/q '[:find (pull ?b [*]) + :in $ + :where [?b :block/name]] + @conn) + (map (comp :block/name first)) + (remove built-in-pages) + set))))) + +(testing "for web and page uris in org" + (let [conn (ldb/start-conn) + built-in-pages (set (map string/lower-case default-db/built-in-pages-names))] + (graph-parser/parse-file conn + "foo.org" + (str "* [[bar][title]]\n" + ;; all of the uris below do not create pages + "* [[https://example.com][example]]\n" + "* [[../assets/conga_parrot.gif][conga]]")) + (is (= #{"foo" "bar"} (->> (d/q '[:find (pull ?b [*]) :in $ :where [?b :block/name]] diff --git a/deps/shui/.clj-kondo/config.edn b/deps/shui/.clj-kondo/config.edn new file mode 100644 index 0000000000..7d483887d9 --- /dev/null +++ b/deps/shui/.clj-kondo/config.edn @@ -0,0 +1,17 @@ +{ :config-in-ns + ;; :used-underscored-binding is turned off for components because of false positive + ;; for rum/defcs and _state. + {all-components {:linters {:used-underscored-binding {:level :off}}}} + + :linters + {;; Disable until it doesn't trigger false positives on rum/defcontext + :earmuffed-var-not-dynamic {:level :off}} + :hooks {:analyze-call {rum.core/defc hooks.rum/defc + rum.core/defcs hooks.rum/defcs + clojure.string/join hooks.path-invalid-construct/string-join}} + :lint-as {rum.core/defcc rum.core/defc + rum.core/with-context clojure.core/let + rum.core/defcontext clojure.core/def + rum.core/defc clojure.core/defn + rum.core/defcs clojure.core/defn + frontend.react/defc clojure.core/defn}} diff --git a/deps/shui/README.md b/deps/shui/README.md new file mode 100644 index 0000000000..b99bca2c23 --- /dev/null +++ b/deps/shui/README.md @@ -0,0 +1,29 @@ +## Description + +This library provides a set of UI components for use within logseq. + +## API + +This library is under the parent namespace `logseq.shui`. This library provides +several namespaces, all of which will be versioned, with the exception of `logseq.shui.context`. + +An example of a versioned namespace is the table namespace: + +`logseq.shui.table.v2` + +`root` components are exported from each versioned file to indicate the root component to be rendered: + +`logseq.shui.table.v2/root` + +Each root component should expect two arguments, `props` and `context`. + +## `props` + +Ultimately, components in shui will need to be used by JavaScript. While it is idiomatic in clojure to +use a list of properties, it is idiomatic in react to use a single props map. Shui components should therefore +stick to this convention when possible to ease the conversion between the two languages. + +## `context` + +Context is a set of functions that call back to the main application. These are abstracted out into a context +object to make it clear what is used internally, and what is used externally. diff --git a/deps/shui/deps.edn b/deps/shui/deps.edn new file mode 100644 index 0000000000..ccd9a316a2 --- /dev/null +++ b/deps/shui/deps.edn @@ -0,0 +1 @@ +{:paths ["src"]} diff --git a/deps/shui/shui-graph/journals/2023_03_27.md b/deps/shui/shui-graph/journals/2023_03_27.md new file mode 100644 index 0000000000..50c2753eb4 --- /dev/null +++ b/deps/shui/shui-graph/journals/2023_03_27.md @@ -0,0 +1,2 @@ +- +- \ No newline at end of file diff --git a/deps/shui/shui-graph/logseq/config.edn b/deps/shui/shui-graph/logseq/config.edn new file mode 100644 index 0000000000..0b98e38379 --- /dev/null +++ b/deps/shui/shui-graph/logseq/config.edn @@ -0,0 +1,348 @@ +{:meta/version 1 + + ;; Currently, we support either "Markdown" or "Org". + ;; This can overwrite your global preference so that + ;; maybe your personal preferred format is Org but you'd + ;; need to use Markdown for some projects. + ;; :preferred-format "" + + ;; Preferred workflow style. + ;; Value is either ":now" for NOW/LATER style, + ;; or ":todo" for TODO/DOING style. + :preferred-workflow :now + + ;; The app will ignore those directories or files. + ;; E.g. :hidden ["/archived" "/test.md" "../assets/archived"] + :hidden [] + + ;; When creating the new journal page, the app will use your template if there is one. + ;; You only need to input your template name here. + :default-templates + {:journals ""} + + ;; Set a custom date format for journal page title + ;; Example: + ;; :journal/page-title-format "EEE, do MMM yyyy" + + ;; Whether to enable hover on tooltip preview feature + ;; Default is true, you can also toggle this via setting page + :ui/enable-tooltip? true + + ;; Show brackets around page references + ;; :ui/show-brackets? true + + ;; Enable showing the body of blocks when referencing them. + :ui/show-full-blocks? false + + ;; Enable Block timestamp + :feature/enable-block-timestamps? false + + ;; Enable remove accents when searching. + ;; After toggle this option, please remember to rebuild your search index by press (cmd+c cmd+s). + :feature/enable-search-remove-accents? true + + ;; Enable journals + ;; :feature/enable-journals? true + + ;; Enable flashcards + ;; :feature/enable-flashcards? true + + ;; Enable Whiteboards + ;; :feature/enable-whiteboards? true + + ;; Disable the built-in Scheduled tasks and deadlines query + ;; :feature/disable-scheduled-and-deadline-query? true + + ;; Specify the number of days in the future to display in the + ;; scheduled tasks and deadlines query, with a default value of 0 which + ;; only displays tasks for today. + ;; Example usage: + ;; Display all scheduled tasks and deadlines in the next 7 days + ;; :scheduled/future-days 7 + + ;; Specify the date on which the week starts. + ;; Goes from 0 to 6 (Monday to Sunday), default to 6 + :start-of-week 6 + + ;; Specify a custom CSS import + ;; This option take precedence over your local `logseq/custom.css` file + ;; You may find a list of awesome logseq themes here: + ;; https://github.com/logseq/awesome-logseq#css-themes + ;; Example: + ;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');" + + ;; Specify a custom js import + ;; This option take precedence over your local `logseq/custom.js` file + ;; :custom-js-url "" + + ;; Set a custom Arweave gateway + ;; Default gateway: https://arweave.net + ;; :arweave/gateway "" + + ;; Set Bullet indentation when exporting + ;; default option: tab + ;; Possible options are for `:sidebar` are + ;; 1. `:eight-spaces` as eight spaces + ;; 2. `:four-spaces` as four spaces + ;; 3. `:two-spaces` as two spaces + ;; :export/bullet-indentation :tab + + ;; When :all-pages-public? true, export repo would export all pages within that repo. + ;; Regardless of whether you've set any page to public or not. + ;; Example: + ;; :publishing/all-pages-public? true + + ;; Specify default home page and sidebar status for Logseq + ;; If not specified, Logseq default opens journals page on startup + ;; value for `:page` is name of page + ;; Possible options for `:sidebar` are + ;; 1. `"Contents"` to open up `Contents` in sidebar by default + ;; 2. `page name` to open up some page in sidebar + ;; 3. Or multiple pages in an array ["Contents" "Page A" "Page B"] + ;; If `:sidebar` is not set, sidebar will be hidden + ;; Example: + ;; 1. Setup page "Changelog" as home page and "Contents" in sidebar + ;; :default-home {:page "Changelog", :sidebar "Contents"} + ;; 2. Setup page "Jun 3rd, 2021" as home page without sidebar + ;; :default-home {:page "Jun 3rd, 2021"} + ;; 3. Setup page "home" as home page with multiple pages in sidebar + ;; :default-home {:page "home" :sidebar ["page a" "page b"]} + :default-home {:page "Contents"} + + ;; Tell logseq to use a specific folder in the repo as a default location for notes + ;; if not specified, notes are stored in `pages` directory + ;; :pages-directory "your-directory" + + ;; Tell logseq to use a specific folder in the repo as a default location for journals + ;; if not specified, journals are stored in `journals` directory + ;; :journals-directory "your-directory" + + ;; Set this to true will convert + ;; `[[Grant Ideas]]` to `[[file:./grant_ideas.org][Grant Ideas]]` for org-mode + ;; For more, see https://github.com/logseq/logseq/issues/672 + ;; :org-mode/insert-file-link? true + + ;; Setup custom shortcuts under `:shortcuts` key + ;; Syntax: + ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a` + ;; 2. ` ` empty space between keys represents key chords. eg: `t s` means press `t` followed by `s` + ;; 3. `mod` means `Ctrl` for Windows/Linux and `Command` for Mac + ;; 4. use `false` to disable particular shortcut + ;; 5. you can define multiple bindings for one action, eg `["ctrl+j" "down"]` + ;; full list of configurable shortcuts are available below: + ;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs + ;; Example: + ;; :shortcuts + ;; {:editor/new-block "enter" + ;; :editor/new-line "shift+enter" + ;; :editor/insert-link "mod+shift+k" + ;; :editor/highlight false + ;; :ui/toggle-settings "t s" + ;; :editor/up ["ctrl+k" "up"] + ;; :editor/down ["ctrl+j" "down"] + ;; :editor/left ["ctrl+h" "left"] + ;; :editor/right ["ctrl+l" "right"]} + :shortcuts {} + + ;; By default, pressing `Enter` in the document mode will create a new line. + ;; Set this to `true` so that it's the same behaviour as the usual outliner mode. + :shortcut/doc-mode-enter-for-new-block? false + + ;; Block content larger than `block/content-max-length` will not be searchable + ;; or editable for performance. + :block/content-max-length 10000 + + ;; Whether to show command doc on hover + :ui/show-command-doc? true + + ;; Whether to show empty bullets for non-document mode (the default mode) + :ui/show-empty-bullets? false + + ;; Pre-defined :view function to use with advanced queries + :query/views + {:pprint + (fn [r] [:pre.code (pprint r)])} + + ;; Pre-defined :result-transform function for use with advanced queries + :query/result-transforms + {:sort-by-priority + (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))} + + ;; The app will show those queries in today's journal page, + ;; the "NOW" query asks the tasks which need to be finished "now", + ;; the "NEXT" query asks the future tasks. + :default-queries + {:journals + [{:title "🔨 NOW" + :query [:find (pull ?h [*]) + :in $ ?start ?today + :where + [?h :block/marker ?marker] + [(contains? #{"NOW" "DOING"} ?marker)] + [?h :block/page ?p] + [?p :block/journal? true] + [?p :block/journal-day ?d] + [(>= ?d ?start)] + [(<= ?d ?today)]] + :inputs [:14d :today] + :result-transform (fn [result] + (sort-by (fn [h] + (get h :block/priority "Z")) result)) + :collapsed? false} + {:title "📅 NEXT" + :query [:find (pull ?h [*]) + :in $ ?start ?next + :where + [?h :block/marker ?marker] + [(contains? #{"NOW" "LATER" "TODO"} ?marker)] + [?h :block/page ?p] + [?p :block/journal? true] + [?p :block/journal-day ?d] + [(> ?d ?start)] + [(< ?d ?next)]] + :inputs [:today :7d-after] + :collapsed? false}]} + + ;; Add your own commands to slash menu to speedup. + ;; E.g. + ;; :commands + ;; [ + ;; ["js" "Javascript"] + ;; ["md" "Markdown"] + ;; ] + :commands + [] + + ;; By default, a block can only be collapsed if it has some children. + ;; `:outliner/block-title-collapse-enabled? true` enables a block with a title + ;; (multiple lines) can be collapsed too. For example: + ;; - block title + ;; block content + :outliner/block-title-collapse-enabled? false + + ;; Macros replace texts and will make you more productive. + ;; For example: + ;; Change the :macros value below to: + ;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."} + ;; input "{{poem red,blue}}" + ;; becomes + ;; Rose is red, violet's blue. Life's ordered: Org assists you. + :macros {} + + ;; The default level to be opened for the linked references. + ;; For example, if we have some example blocks like this: + ;; - a [[page]] (level 1) + ;; - b (level 2) + ;; - c (level 3) + ;; - d (level 4) + ;; + ;; With the default value of level 2, `b` will be collapsed. + ;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed. + :ref/default-open-blocks-level 2 + + :ref/linked-references-collapsed-threshold 50 + + ;; Favorites to list on the left sidebar + :favorites [] + + ;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5) + ;; :srs/learning-fraction 0.5 + + ;; the initial interval after the first successful review of a card (default 4) + ;; :srs/initial-interval 4 + + ;; hide specific properties for blocks + ;; E.g. :block-hidden-properties #{:created-at :updated-at} + ;; :block-hidden-properties #{} + + ;; Enable all your properties to have corresponding pages + :property-pages/enabled? true + + ;; Properties to exclude from having property pages + ;; E.g.:property-pages/excludelist #{:duration :author} + ;; :property-pages/excludelist + + ;; By default, property value separated by commas will not be treated as + ;; page references. You can add properties to enable it. + ;; E.g. :property/separated-by-commas #{:alias :tags} + ;; :property/separated-by-commas #{} + + ;; Properties that are ignored when parsing property values for references + ;; :ignored-page-references-keywords #{"author" "startup"} + + ;; logbook setup + ;; :logbook/settings + ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated + ;; :enabled-in-all-blocks true ;display logbook in all blocks after timetracking + ;; :enabled-in-timestamped-blocks false ;don't display logbook at all + ;; } + + ;; Mobile photo uploading setup + ;; :mobile/photo + ;; {:allow-editing? true + ;; :quality 80} + + ;; Mobile features options + ;; Gestures + ;; :mobile + ;; {:gestures/disabled-in-block-with-tags ["kanban"]} + + ;; Extra CodeMirror options + ;; See https://codemirror.net/5/doc/manual.html#config for possible options + ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true} + + ;; Enable logical outdenting + ;; :editor/logical-outdenting? true + + ;; When both text and a file are in the clipboard, paste the file + ;; :editor/preferred-pasting-file? true + + ;; Quick capture templates for receiving contents from other apps. + ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded + ;; by received contents from other apps. Note: the {} cannot be omitted. + ;; - {time}: capture time + ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference + ;; - {text}: text that users selected before sharing. + ;; - {url}: url or assets path for media files stored in Logseq. + ;; You can also reorder them, or even only use one or two of them in the template. + ;; You can also insert or format any text in the template as shown in the following examples. + ;; :quick-capture-templates + ;; {:text "[[quick capture]] **{time}**: {text} from {url}" + ;; :media "[[quick capture]] **{time}**: {url}"} + + ;; Quick capture options + ;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page nil} + + ;; File sync options + ;; Ignore these files when syncing, regexp is supported. + ;; :file-sync/ignore-files [] + + ;; dwim (do what I mean) for Enter key when editing. + ;; Context-awareness of Enter key makes editing more easily + ; :dwim/settings { + ; :admonition&src? true + ; :markup? false + ; :block-ref? true + ; :page-ref? true + ; :properties? true + ; :list? true + ; } + + ;; Decide the way to escape the special characters in the page title. + ;; Warning: + ;; This is a dangerous operation. If you want to change the setting, + ;; should access the setting `Filename format` and follow the instructions. + ;; Or you have to rename all the affected files manually then re-index on all + ;; clients after the files are synced. Wrong handling may cause page titles + ;; containing special characters to be messy. + ;; Available values: + ;; :file/name-format :triple-lowbar + ;; ;use triple underscore `___` for slash `/` in page title + ;; ;use Percent-encoding for other invalid characters + :file/name-format :triple-lowbar + :feature/enable-whiteboards? true} + + ;; specify the format of the filename for journal files + ;; :journal/file-name-format "yyyy_MM_dd" + + diff --git a/deps/shui/shui-graph/logseq/custom.css b/deps/shui/shui-graph/logseq/custom.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/deps/shui/shui-graph/pages/About Shui.md b/deps/shui/shui-graph/pages/About Shui.md new file mode 100644 index 0000000000..4db5bf0a7b --- /dev/null +++ b/deps/shui/shui-graph/pages/About Shui.md @@ -0,0 +1,22 @@ +- ## What is shui? +- Shui is the component library for logseq. It has 3 main goals: + - 1. Provide an abstraction for specific components, separate from the main codebase + 2. Provide a consistent look and feel for the future of logseq + 3. Provide ready to use components to plugin authors to allow for a more consistent better user experience of plugin authors and users +- +- ## What are the general concepts of shui? +- Shui has a few core principles: + - ### Focus on a native core experience + - We want to provide a smooth, consistent, and native feel for all logseq features, first and foremost + - ### Specific output, general input + - Components should be generally reusable by their props, however should have the user experience themselves + - Eventually, getting to a highly composable components is a great goal, but we should start small and focused first + - ### UI is a marathon, not a sprint + - Components in shui should be versioned, and should expect to evolve over time + - We need to go from highly coupled, low reused components to a loosely coupled, highly reusable library. This will take time, and means components have to be adaptable over time + - Versioning is at the core of shui +- +- ## How to contribute to shui? +- In the logseq repo, there is a directory at `deps/shui`. Here you can find all of the shui components +- In the logseq repo, you can find a copy of this graph at `deps/shui/shui-graph`. Here you can find and add all the test cases needed for different `shui` components +- In the logseq repo, you can find tests under the `e2e-tests/shui`. To keep our infra streamlined, `shui` is bundled with and tested with the current CI for logseq \ No newline at end of file diff --git a/deps/shui/shui-graph/pages/Page 1.md b/deps/shui/shui-graph/pages/Page 1.md new file mode 100644 index 0000000000..41d91cdb9b --- /dev/null +++ b/deps/shui/shui-graph/pages/Page 1.md @@ -0,0 +1,3 @@ +table-example:: true + +- \ No newline at end of file diff --git a/deps/shui/shui-graph/pages/Page 2.md b/deps/shui/shui-graph/pages/Page 2.md new file mode 100644 index 0000000000..77faa066be --- /dev/null +++ b/deps/shui/shui-graph/pages/Page 2.md @@ -0,0 +1 @@ +table-example:: true diff --git a/deps/shui/shui-graph/pages/Page 3.md b/deps/shui/shui-graph/pages/Page 3.md new file mode 100644 index 0000000000..77faa066be --- /dev/null +++ b/deps/shui/shui-graph/pages/Page 3.md @@ -0,0 +1 @@ +table-example:: true diff --git a/deps/shui/shui-graph/pages/contents.md b/deps/shui/shui-graph/pages/contents.md new file mode 100644 index 0000000000..85b6529291 --- /dev/null +++ b/deps/shui/shui-graph/pages/contents.md @@ -0,0 +1,4 @@ +- [[About Shui]] +- [[shui/components]] + - [[shui/components/table]] + - \ No newline at end of file diff --git a/deps/shui/shui-graph/pages/shui___components.md b/deps/shui/shui-graph/pages/shui___components.md new file mode 100644 index 0000000000..9e90c08592 --- /dev/null +++ b/deps/shui/shui-graph/pages/shui___components.md @@ -0,0 +1,4 @@ +- Below is a list of components that can be found in the shui library +- [[shui/components/table]] + - The table component is used to render tabular data. +- \ No newline at end of file diff --git a/deps/shui/shui-graph/pages/shui___components___table.md b/deps/shui/shui-graph/pages/shui___components___table.md new file mode 100644 index 0000000000..6368f06a5a --- /dev/null +++ b/deps/shui/shui-graph/pages/shui___components___table.md @@ -0,0 +1,62 @@ +- ### Props + - logseq.table.version:: 2 + logseq.table.hover:: row + logseq.table.stripes:: true + logseq.table.borders:: false + | Prop Name | Description | Values | + | --- | --- | --- | + | `logseq.table.version` | The version of the table | 1, 2 | + | `logseq.table.hover` | The hover effect of the table | cell (default), row, col, both, none | + | `logseq.table.compact` | Whether to show a compact version of the data | false (default), true | + | `logseq.table.headers` | The casing that should be applied to the header cols | none (default), uppercase, capitalize, capitalize-first, lowercase | + | `logseq.table.borders` | Whether or not the table should have borders between all cells and rows | true (default), false | + | `logseq.table.stripes` | Whether or not the table should have alternately colored table rows | false (default), true | + | `logseq.table.max-width` | The maximum width (in rems) that should be applied to each column | (default 30) | + | `logseq.color` | The color accent of the table | red, orange, yellow, green, blue, purple | +- ### Examples + - #### Simplest possible markdown table + collapsed:: true + - logseq.table.version:: 1 + | Fruit | Color | + | Apples | Red | + | Bananas | Yellow | + - #### Longer more complicated markdown table, with various widths and input types + collapsed:: true + - logseq.table.version:: 2 + | Length | Text | EN | ZH | + | --- | --- | --- | --- | + | 70 | Logseq is a new note-taking app that has been making waves in the productivity community. | x | | + | 138 | With its unique approach to linking and organizing information, Logseq allows users to create a highly interconnected and personalized knowledge base. | x | | + | 194 | Unlike traditional note-taking apps, Logseq encourages users to embrace the power of plain text and markdown formatting, enabling them to easily manipulate and query their notes. | x | | + | 246 | From students to researchers, Logseq's flexible and intuitive interface makes it an ideal tool for anyone looking to optimize their note-taking and knowledge management workflow. | x | | + | 312 | Whether you're looking to organize your thoughts, collaborate with others, or simply streamline your note-taking process, Logseq offers a revolutionary approach that is sure to revolutionize the way you work. | x | | + | 35 | Logseq 是一款在生产力社群中备受瞩目的新型笔记应用。| | x | + | 59 | Logseq 以其独特的链接和组织信息方式,使用户能够创建高度互联且个性化的知识库。 | | x | 86 | 不同于传统笔记应用,Logseq 鼓励用户采用纯文本和 Markdown 格式,使其能够轻松地操作和查询笔记。 | | x | + | 123 | 从学生到研究人员,Logseq 灵活直观的界面使其成为任何想要优化笔记和知识管理工作流程的人的理想工具。| | x | + | 152 | 无论您是想整理自己的思路、与他人合作,还是简化笔记流程,Logseq 提供的革命性方法肯定会改变您的工作方式。| | x | + - #### Query table for blocks + - logseq.table.version:: 2 + query-table:: true + query-properties:: [:block] + logseq.table.borders:: false + {{query #table-example/block}} + - + - #### data + - Block 1 #table-example/block + table-example:: true + - Block 2 #table-example/block + table-example:: true + - Block 3 #table-example/block + table-example:: true + - #### Query table for pages + - {{query (page-property "table-example" "true")}} + logseq.table.version:: 2 + - [[Page 1]] + - [[Page 2]] + - [[Page 3]] + - #### Query table for mixed pages and blocks + - {{query (property "table-example" true)}} + query-table:: true + logseq.table.version:: 2 + - +- {{query }} \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/context.cljs b/deps/shui/src/logseq/shui/context.cljs new file mode 100644 index 0000000000..fd3bbafe8c --- /dev/null +++ b/deps/shui/src/logseq/shui/context.cljs @@ -0,0 +1,36 @@ +(ns logseq.shui.context) + +(defn inline->inline-block [inline block-config] + (fn [_context item] + (inline block-config item))) + +(defn inline->map-inline-block [inline block-config] + (let [inline* (inline->inline-block inline block-config)] + (fn [context col] + (map #(inline* context %) col)))) + +(defn make-context [{:keys [block-config app-config inline int->local-time-2]}] + {;; Shui needs access to the global configuration of the application + :config app-config + ;; Until components are converted over, they need to fallback to the old inline function + ;; Wrap the old inline function to allow for interception, but fallback to the old inline function + :inline-block (inline->inline-block inline block-config) + :map-inline-block (inline->map-inline-block inline block-config) + ;; Currently frontend component are provided an object map containin at least the following keys: + ;; These will be passed through in a whitelisted fashion so as to be able to track the dependencies + ;; back to the core application + ;; TODO: document the following + :block (:block block-config) ;; the db entity of the current block + :block? (:block? block-config) + :blocks-container-id (:blocks-container-id block-config) + :editor-box (:editor-box block-config) + :id (:id block-config) + :mode? (:mode? block-config) + :query-result (:query-result block-config) + :sidebar? (:sidebar? block-config) + :start-time (:start-time block-config) + :uuid (:uuid block-config) + :whiteboard? (:whiteboard? block-config) + ;; Some functions from logseq's application will be used in the shui components. To avoid circular dependencies, + ;; they will be provided via the context object + :int->local-time-2 int->local-time-2}) diff --git a/deps/shui/src/logseq/shui/core.cljs b/deps/shui/src/logseq/shui/core.cljs new file mode 100644 index 0000000000..245d1e2373 --- /dev/null +++ b/deps/shui/src/logseq/shui/core.cljs @@ -0,0 +1,11 @@ +(ns logseq.shui.core + (:require + [logseq.shui.table.v2 :as shui.table.v2] + [logseq.shui.context :as shui.context])) + +;; table component +(def table shui.table.v2/root) +(def table-v2 shui.table.v2/root) + +;; context +(def make-context shui.context/make-context) diff --git a/deps/shui/src/logseq/shui/table/v2.cljs b/deps/shui/src/logseq/shui/table/v2.cljs new file mode 100644 index 0000000000..d84fc229e0 --- /dev/null +++ b/deps/shui/src/logseq/shui/table/v2.cljs @@ -0,0 +1,471 @@ +(ns logseq.shui.table.v2 + (:require + [clojure.string :as str] + [logseq.shui.util :refer [use-ref-bounding-client-rect use-dom-bounding-client-rect $main-content] :as util] + [rum.core :as rum])) + +(declare table-cell) + +(def COLORS #{"tomato" "red" "crimson" "pink" "plum" "purple" "violet" "indigo" "blue" "sky" "cyan" "teal" "mint" "green" "grass" "lime" "yellow" "amber" "orange" "brown"}) +(def MAX_WIDTH 30 #_rem) ;; Max width in rem for a single column +(def MIN_WIDTH 4 #_rem) ;; Min width in rem for a single column + +;; in order to make sure the tailwind classes are included, +;; the values are pulled from the classes via regex. +;; the return values are simply the numbers in the classes. +(def CELL_PADDING (->> "px-[0.75rem]" (re-find #"\d+\.?\d*") js/parseFloat)) +(def CELL_PADDING_COMPACT (->> "px-[0.25rem]" (re-find #"\d+\.?\d*") js/parseFloat)) +(def BORDER_WIDTH (->> "border-[1px]" (re-find #"\d+\.?\d*") js/parseFloat)) + +;; -- Helpers ------------------------------------------------------------------ + +(defn get-in-first + ([obj path] (get-in obj path)) + ([obj path & more] (get-in obj path (apply get-in-first obj more)))) + +(defn get-in-first-fallback + ([obj path] (get-in obj path)) + ([obj path fallback] (get-in obj path fallback)) + ([obj path path-b & more] (get-in obj path (apply get-in-first-fallback obj path-b more)))) + +(defn read-prop [value] + (case value + "false" false + "true" true + value)) + +(defn get-view-prop + "Get the config for a specified item. Can be overridden in blocks, specified in config, + fallback to default config, or fallback to the provided parameters" + ([context kw] + (read-prop + (get-in-first context [:block :properties kw] + [:block :block/properties kw] + [:config kw]))) + ([context kw fallback] + (read-prop + (get-in-first-fallback context [:block :properties kw] + [:block :block/properties kw] + [:config kw] + fallback)))) + +(defn color->gray [color] + (case color + ("tomato" "red" "crimson" "pink" "plum" "purple" "violet") "mauve" + ("indigo" "blue" "sky" "cyan") "slate" + ("teal" "mint" "green") "sage" + ("grass" "lime") "olive" + ("yellow" "amber" "orange" "brown") "sand" + nil)) + +(defn rdx + ([color step] (str "bg-" color "-" step)) + ([param color step] (str (name param) "-" color "-" step))) + ; ([color step] (str "bg-" color "dark-" step)) + ; ([param color step] (str param "-" color "dark-" step)))) + + ; --ls-primary-background-color: #fff; + ; --ls-secondary-background-color: #f8f8f8; + ; --ls-tertiary-background-color: #f2f2f3; + ; --ls-quaternary-background-color: #ebeaea)); + +(defn lsx + "This is a temporary bridge between the radix color grading and the + current logseq theming variables. Should set the prop to the given css variable" + ([step] (lsx :bg step)) + ([param step] + (case step + 1 ({"bg" "bg-[color:var(--ls-primary-background-color)]"} (name param)) + 2 ({"bg" "bg-[color:var(--ls-secondary-background-color)]"} (name param)) + 3 ({"bg" "bg-[color:var(--ls-tertiary-background-color)]"} (name param)) + 4 ({"bg" "bg-[color:var(--ls-quaternary-background-color)]"} (name param)) + 5 ({"bg" "bg-[color:var(--ls-quinary-background-color)]"} (name param)) + 6 ({"bg" "bg-[color:var(--ls-senary-background-color)]"} (name param)) + 7 ({"bg" "bg-[color:var(--ls-border-color)]" + "border" "border-[color:var(--ls-border-color)]"} (name param)) + 11 ({"text" "text-[color:var(--ls-secondary-text-color)]"} (name param)) + 12 ({"text" "text-[color:var(--ls-primary-text-color)]"} (name param))))) + +(defn varc [color step] + (str "var(--color-" color "-" step ")")) + +(defn last-str + "Given an inline AST, return the last string element you can walk to" + [inline] + (cond + (keyword? inline) (name inline) + (string? inline) inline + (coll? inline) (last-str (last inline)) + :else (pr-str inline))) + +(comment + (last-str "A") + (last-str ["Plain" "A"]) + (last-str [["Plain" "A"]]) + (last-str [["Plain" "A"] + [["Emphasis" [["Italic"] [["Plain" "B"]]]]]])) + +(defn render-cell-in-context + "Some instances of the table provide us with raw data, others provide us with + inline ASTs. This function renders the content appropriately, passing the AST along + to map-inline if necessary." + [{:keys [map-inline-block int->local-time-2]} cell-data] + (cond + (sequential? cell-data) (map-inline-block [:table :v2] cell-data) + (string? cell-data) cell-data + (keyword? cell-data) (name cell-data) + (boolean? cell-data) (pr-str cell-data) + (number? cell-data) (if-let [date (int->local-time-2 cell-data)] + date cell-data))) + +(defn map-with-all-indices [data] + (let [!row-index (volatile! -1)] + (for [[group-index group] (map-indexed vector data) + [group-row-index row] (map-indexed vector group) + :let [row-index (vswap! !row-index inc)]] + [group-index group-row-index row-index group row]))) + +(defn get-columns [block data] + (->> (or (some-> (get-in block [:block/properties :logseq.table.cols]) + (str/split #", ?")) + (map last-str (ffirst data))) + (map (comp str/lower-case str/trim)))) + +(defn cell-bg-classes + "We track the cell the cursor last entered and update the cells according to the configured + hover preference: cell, row, col, both, or none. + We also have to account for the header cells and stripes cells" + [{:keys [row-index col-index hover header? gray color stripes? cursor]}] + (let [;; check how the cursor position overlaps with the current cell + row-highlighted? (= row-index (second cursor)) + col-highlighted? (= col-index (first cursor)) + cell-highlighted? (and row-highlighted? col-highlighted?) + ;; check how the cell needs to be highlighted + highlight-row? (and row-highlighted? (#{"row" "both"} hover)) + highlight-col? (and col-highlighted? (#{"col" "both"} hover)) + highlight-cell? (and cell-highlighted? (#{"cell" "row" "col" "both"} hover))] + (cond + highlight-cell? (if header? (lsx 6) (lsx 4)) + highlight-row? (if header? (lsx 5) (lsx 3)) + highlight-col? (if header? (lsx 5) (lsx 3)) + header? (lsx 4) + (and stripes? (even? row-index)) (lsx 2) + :else (lsx 1)))) + +(defn cell-rounded-classes + "Depending on where the cell is, and whether there is a gradient accent, we need to round specific corners + The cond-> is used to account for single row or single column talbes that may have multiple rounded corners." + [{:keys [color row-index col-index total-rows total-cols]}] + (let [no-gradient-accent? (nil? color)] + (cond-> "" + (and no-gradient-accent? (= [row-index col-index] [0 0])) (str " rounded-tl") + (and no-gradient-accent? (= [row-index col-index] [0 (dec total-cols)])) (str " rounded-tr") + (= [row-index col-index] [(dec total-rows) 0]) (str " rounded-bl") + (= [row-index col-index] [(dec total-rows) (dec total-cols)]) (str " rounded-br")))) + +(defn cell-text-transform-classes [{:keys [headers header?]}] + (when header? + (cond-> (get #{"uppercase" "capitalize" "lowercase" "none" "capitalize-first"} headers "none") + (= headers "capitalize-first") (str " lowercase")))) + +(defn cell-padding-classes [{:keys [compact? header?]}] + (cond + #_compact_th (and compact? header?) (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5") + #_compact_td compact? (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5") + #_padded_th header? (str "px-[" CELL_PADDING "rem] py-1.5") + #_padded_td :else (str "px-[" CELL_PADDING "rem] py-2"))) + +(defn cell-text-classes [{:keys [header?]}] + (if header? + (str (lsx :text 11) " text-sm tracking-wide font-bold") + (str (lsx :text 12) " text-base"))) + +(defn cell-classes [table-opts] + (str/join " " + [(cell-bg-classes table-opts) + (cell-rounded-classes table-opts) + (cell-text-classes table-opts) + (cell-text-transform-classes table-opts) + (cell-padding-classes table-opts)])) + +;; -- Handlers ----------------------------------------------------------------- + +(defn handle-cell-pointer-down [e {:keys [cell-focus col-index row-index]}] + (when (not= cell-focus [col-index row-index]) + (.stopPropagation e) + (.preventDefault e))) + +(defn handle-cell-click + "When a cell is clicked, we need to update the cursor position and the selected cells" + [e {:keys [cell-focus set-cell-focus header? col-index row-index]} cell-ref] + ; (.stopPropagation e) + (.preventDefault e) + (when-not (= cell-focus [col-index row-index]) + (set-cell-focus [col-index row-index]))) + + +(defn handle-cell-keydown + "When a cell is focused, we need to update the cursor position and the selected cells" + [e {:keys [cell-focus set-cell-focus header? col-index row-index total-rows total-cols]}] + (when (= cell-focus [col-index row-index]) + (and (case (.-key e) + "ArrowUp" (if (= row-index 0) + (set-cell-focus [col-index row-index]) + (set-cell-focus [col-index (dec row-index)])) + "ArrowDown" (if (= row-index (dec total-rows)) + (set-cell-focus [col-index row-index]) + (set-cell-focus [col-index (inc row-index)])) + "ArrowLeft" (cond + ;; if we are in the top left, then do not move the focus + (and (= col-index 0) (= row-index 0)) + (set-cell-focus [col-index row-index]) + ;; if we are in the first column, then move to the last column of the previous row + (= col-index 0) + (set-cell-focus [(dec total-cols) (dec row-index)]) + ;; otherwise, move to the previous column + :else + (set-cell-focus [(dec col-index) row-index])) + "ArrowRight" (cond + ;; if we are in the bottom right, then do not move the focus + (and (= col-index (dec total-cols)) (= row-index (dec total-rows))) + (set-cell-focus [col-index row-index]) + ;; if we are in the last column, then move to the first column of the next row + (= col-index (dec total-cols)) + (set-cell-focus [0 (inc row-index)]) + ;; otherwise, move to the next column + :else + (set-cell-focus [(inc col-index) row-index])) + nil) + ;; Prevent default actions when the table handles it itself + (.preventDefault e) + (.stopPropagation e)))) + + +;; -- Hooks -------------------------------------------------------------------- + +(defn use-atom + "A hook that wraps use-state to allow for interaction with + the state as if it were an atom" + [initial-value] + (let [atom-ref (rum/use-ref (atom initial-value)) + atom-current (.. atom-ref -current) + [state set-state] (rum/use-state initial-value)] + (rum/use-effect! (fn [] + (set-state @atom-current) + identity) + [atom-current]) + [state atom-current])) + +(defn use-dynamic-widths [data] + (let [[static atomic] (use-atom {}) + add-column-width (fn [col-index width] + (when (< (get @atomic col-index 0) (min MAX_WIDTH width)) + (swap! atomic assoc col-index (min MAX_WIDTH width)) + ;; rum is complaining that we can only return teardown functions + identity))] + ;; Reset the minimum widths when the data changes + (rum/use-effect! (fn [] (reset! atomic {}) identity) + [data]) + [static add-column-width])) + +(defn use-table-flow-at-width [table-px max-cols-px] + (let [[overflow set-overflow] (rum/use-state false) + [underflow set-underflow] (rum/use-state false) + handle-container-width (fn [container-px] + (set-underflow (< max-cols-px container-px)) + (set-overflow (< container-px table-px)))] + [overflow underflow handle-container-width])) + +;; -- Components (V2) ----------------------------------------------------------- + +(rum/defc table-scrollable-overflow [handle-root-width-change child] + (let [[set-root-ref root-rect root-ref] (use-ref-bounding-client-rect) + main-content-rect (use-dom-bounding-client-rect ($main-content)) + + left-adjustment (- (:left root-rect) (:left main-content-rect)) + right-adjustment (- (:width main-content-rect) + (- (:right root-rect) (:left main-content-rect))) + + ;; Because in a scrollable container, we need to account for the scrollbar being clicked, + ;; we add a handler to prevent the table from switching to the input on click. + ;; This also prevents the table from switching to eiditng mode when the left or right area + ;; of the table is clicked, but that feels natural to me. + handle-pointer-down (fn [e] + (when (= root-ref (.. e -target -parentElement)) + (.preventDefault e)))] + (rum/use-effect! #(handle-root-width-change (:width root-rect)) [(:width root-rect)]) + [:div {:ref set-root-ref} + [:div {:style {:width (:width main-content-rect) + :margin-left (- (:left main-content-rect) (:left root-rect)) + :padding-left left-adjustment + :padding-right right-adjustment + :overflow-x "scroll"} + :class "mt-2" + :on-pointer-down handle-pointer-down} + child]])) + +(rum/defc table-gradient-accent [{:keys [color]}] + [:div.rounded-t.h-2.-ml-px.-mt-px.-mr-px + {:style {:grid-column "1 / -1" :order -999} + :class (str "grad-bg-" color "-9") + :data-testid "v2-table-gradient-accent"}]) + +(rum/defc table-header-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}] + [:<> + (for [[cell-index cell] (map-indexed vector cells) + :let [col-index (get cell-col-map cell-index)] + :when col-index] + ^{:key cell-index} + (table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index :header? true)))]) + +(rum/defc table-data-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}] + [:<> + (for [[cell-index cell] (map-indexed vector cells) + :let [col-index (get cell-col-map cell-index)] + :when col-index] + ^{:key cell-index} + (table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index)))]) + +(rum/defc table-cell [handle-cell-width-change cell {:keys [row-index col-index render-cell show-separator? total-cols set-cell-hover cell-focus table-underflow?] :as opts}] + (let [cell-ref (rum/use-ref nil) + cell-order (+ (* row-index total-cols) col-index) + static-width (get-in opts [:static-widths col-index]) + dynamic-width (when-not static-width + (get-in opts [:dynamic-widths col-index]))] + ;; Whenever the cell changes, we need to calculate new bounds for the given content + ;; -innerText is used here to strip out formatting, this may turn out to not work for all given block types + (rum/use-layout-effect! #(->> (.. cell-ref -current -innerText) + (count) + (handle-cell-width-change col-index)) + [cell]) + + ;; Whenever the cell becomes focused, we set it's tabIndex. When the tabIndex is set, call focus on the element + (rum/use-layout-effect! #(when (= cell-focus [col-index row-index]) + ; (.. cell-ref -current -tabIndex 0) + (some-> cell-ref .-current .focus)) + ; (.execCommand js/document "selectAll")) + [cell-focus]) + [:div {:ref cell-ref + :class (cell-classes opts) + :style (cond-> {:box-sizing :border-box} + (not table-underflow?) (assoc :max-width (str MAX_WIDTH "rem")) + static-width (assoc :width (str static-width "rem")) + dynamic-width (assoc :min-width (str (max MIN_WIDTH dynamic-width) "rem")) + cell-order (assoc :order cell-order) + show-separator? (assoc :margin-top 3)) + :tab-index (when (= cell-focus [col-index row-index]) "-1") + :on-pointer-enter #(set-cell-hover [col-index row-index]) + :on-click #(handle-cell-click % opts cell-ref) + :on-pointer-down #(handle-cell-pointer-down % opts) + ; :on-pointer-up handle-cell-interrupt + :on-key-down #(handle-cell-keydown % opts)} + (render-cell cell)])) + +(rum/defc table-container [{:keys [columns borders? table-overflow? total-table-width gray set-cell-hover] :as opts} & children] + (let [grid-template-columns (str "repeat(" (count columns) ", minmax(max-content, 1fr))")] + [:div.grid.border.rounded {:style {:grid-template-columns grid-template-columns + :gap (when borders? BORDER_WIDTH) + :width (when table-overflow? total-table-width)} + :class (str (lsx 7) " " (lsx :border 7)) + :data-testid "v2-table-container" + :on-pointer-leave #(set-cell-hover [])} + children])) + +(rum/defc root + [{:keys [data] :as _props} {:keys [block] :as context}] + (let [;; In order to highlight cells in the same row or column of the hovered cell, + ;; we need to know the row and column that the cursor is in + [[_cell-hover-x _cell-hover-y :as cell-hover] set-cell-hover] (rum/use-state []) + [[_cell-focus-x _cell-focus-y :as cell-focus] set-cell-focus] (rum/use-state []) + + ;; Depending on the content of the table, we roughly adjust the width of the column + ;; to do this we need to keep track of the .innerText.length of each cell and update + ;; it whenever it changes + [dynamic-widths handle-cell-width-change] (use-dynamic-widths data) + + ;; We need to call into the view config several times, so we can memoize it + ;; TODO: insert global config here + get-view-prop* (partial get-view-prop context) + + ;; Most of the config options will be repeated and reused throughout the table, so store + ;; all of it's state in a single map for consistency + table-opts {; user configurable properties (sometimes with defaults) + :color (get-view-prop* :logseq.color) + :headers (get-view-prop* :logseq.table.headers "none") + :borders? (get-view-prop* :logseq.table.borders true) + :compact? (get-view-prop* :logseq.table.compact false) + :hover (get-view-prop* :logseq.table.hover "cell") + :stripes? (get-view-prop* :logseq.table.stripes false) + :gray (color->gray (get-in context [:config :logseq.color])) + :columns (get-columns block data) + + ; non configurable properties + :cell-hover cell-hover + :cell-focus cell-focus + :cursor (or (not-empty cell-focus) (not-empty cell-hover)) + :dynamic-widths dynamic-widths + :render-cell (partial render-cell-in-context context) + :set-cell-hover set-cell-hover + :set-cell-focus set-cell-focus + :total-rows (reduce + 0 (map count data))} + + ;; The total table width has to account for the borders and the padding + ;; everything is tracked in rems, except for the border, since it's so small + cell-padding-width (* 2 (if (:compact? table-opts) CELL_PADDING_COMPACT CELL_PADDING)) + total-border-width (* (count (:columns table-opts)) BORDER_WIDTH) + total-table-width (->> (vals dynamic-widths) + (map (partial + cell-padding-width)) + (reduce + 0) + (util/rem->px) + (+ total-border-width)) + total-max-col-width (-> (count (:columns table-opts)) + (* MAX_WIDTH) + (util/rem->px) + (+ total-border-width)) + + ;; The table is actually rendered differently when it needs to be scrollable. + ;; Keep track of whether the ideal table size overflows it's container size, + ;; and provide a handler to be called whenever the container width changes + [table-overflow? table-underflow? handle-root-width-change] (use-table-flow-at-width total-table-width total-max-col-width) + + ;; Because the data may come in a different order than it should be presented, + ;; we need to distinguish between these and provide a conversion. + ;; The order the data is stored in is referred to as the cell order. + ;; The order the data is displayed as is referred to as the col order. + ;; Since these are called on every render of every cell, and are not dynamic, they are computed up front + cell-col-map (->> (ffirst data) + (map-indexed (juxt #(identity %1) + #(.indexOf (:columns table-opts) (.toLowerCase (last-str %2))))) + (remove (comp #{-1} second)) + (into {})) + + ;; There are a couple more computed table properties that are best calculated + ;; after the initial object is creaated + table-opts (assoc table-opts :total-cols (count (:columns table-opts)) + :total-table-width total-table-width + :table-overflow? table-overflow? + :table-underflow? table-underflow? + :cell-col-map cell-col-map)] + ; (js/console.log "shui table opts context" (clj->js context)) + ; (js/console.log "shui table opts" (clj->js table-opts)) + ; (js/console.log "shui table opts" (pr-str table-opts)) + ;; Scrollable Container: if the table is larger than the container, manage the scrolling effects here + (table-scrollable-overflow handle-root-width-change + ;; Grid Container: control the outermost table related element (border radius, grid, etc) + (table-container table-opts + ;; Gradient Accent: the accent color at the top of the application + (when (:color table-opts) + (table-gradient-accent table-opts)) + ;; Rows: the actual table rows + (for [[group-index group-row-index row-index _group row] (map-with-all-indices data) + :let [show-separator? (and (= 0 group-row-index) (< 1 group-index)) + opts (assoc table-opts :group-index group-index + :group-row-index group-row-index + :row-index row-index + :show-separator? show-separator?)]] + (if (= 0 group-index) + ;; Table Header: Rows in the first section are rendered as headers + ^{:key row-index} (table-header-row handle-cell-width-change row opts) + ;; Table Body: The rest of the data is rendered as cells + ^{:key row-index} (table-data-row handle-cell-width-change row opts))))))) + diff --git a/deps/shui/src/logseq/shui/util.cljs b/deps/shui/src/logseq/shui/util.cljs new file mode 100644 index 0000000000..c0e33fac77 --- /dev/null +++ b/deps/shui/src/logseq/shui/util.cljs @@ -0,0 +1,81 @@ +(ns logseq.shui.util + (:require + [clojure.string :as s] + [rum.core :refer [use-state use-effect!] :as rum] + [goog.dom :as gdom])) + + +;; /--------------- app ------------\ +;; /-------- left --------\ \ +;; /l-side\ \ /- r-side --\ +;; +;; |--------|-------------------|-------------| \ head +;; |--------|-------------------| | / +;; | | | | +;; | | | | +;; | | | | +;; |--------|-------------------|-------------| + +(def $app (partial gdom/getElement "app-container")) +(def $left (partial gdom/getElement "left-container")) +(def $head (partial gdom/getElement "head-container")) +(def $main (partial gdom/getElement "main-container")) +(def $main-content (partial gdom/getElement "main-content-container")) +(def $left-sidebar (partial gdom/getElement "left-sidebar")) +(def $right-sidebar (partial gdom/getElement "right-sidebar")) + +(defn el->clj-rect [el] + (let [rect (.getBoundingClientRect el)] + {:top (.-top rect) + :left (.-left rect) + :bottom (.-bottom rect) + :right (.-right rect) + :width (.-width rect) + :height (.-height rect) + :x (.-x rect) + :y (.-y rect)})) + +(defn clj-rect-observer [update!] + (js/ResizeObserver. + (fn [entries] + (when (.-contentRect (first (js->clj entries))) + (update!))))) + +(defn use-dom-bounding-client-rect + ([el] (use-dom-bounding-client-rect el nil)) + ([el tick] + (let [[rect set-rect] (rum/use-state nil)] + (rum/use-effect! + (if el + (fn [] + (let [update! #(set-rect (el->clj-rect el)) + observer (clj-rect-observer update!)] + (update!) + (.observe observer el) + #(.disconnect observer))) + #()) + [el tick]) + rect))) + +(defn use-ref-bounding-client-rect + ([] (use-ref-bounding-client-rect nil)) + ([tick] + (let [[ref set-ref] (rum/use-state nil) + rect (use-dom-bounding-client-rect ref tick)] + [set-ref rect ref])) + ([ref tick] [nil (use-dom-bounding-client-rect ref tick)])) + + +(defn rem->px [rem] + (-> js/document.documentElement + js/getComputedStyle + (.-fontSize) + (js/parseFloat) + (* rem))) + +(defn px->rem [px] + (->> js/document.documentElement + js/getComputedStyle + (.-fontSize) + (js/parseFloat) + (/ px))) diff --git a/docs/dev-practices.md b/docs/dev-practices.md index 1be7496a6e..281f23fb51 100644 --- a/docs/dev-practices.md +++ b/docs/dev-practices.md @@ -139,7 +139,17 @@ By convention, a namespace's tests are found at a corresponding namespace of the same name with an added `-test` suffix. For example, tests for `frontend.db.model` are found in `frontend.db.model-test`. -There are a couple different ways to develop with tests: +There are a couple different ways to run tests: + +* [Focus tests](#focus-tests) - Run one or more tests from the CLI +* [Autorun tests](#autorun-tests) - Autorun tests from the CLI +* [Repl tests](#repl-tests) - Run tests from REPL + +There a couple types of tests and they can overlap with each other: + +* [Database tests](#database-tests) - Tests that involve a datascript DB. +* [Performance tests](#performance-tests) - Tests that aim to measure and enforce a performance characteristic. +* [Async tests](#async-tests) - Tests that run async code and require some helpers. #### Focus Tests @@ -166,6 +176,15 @@ To run tests automatically on file save, run `clojure -M:test watch test the `:ns-regexp` option e.g. `clojure -M:test watch test --config-merge '{:autorun true :ns-regexp "frontend.util.page-property-test"}'`. +#### REPL tests + +Most unit tests e.g. ones that are browser compatible and don't require node libraries, can be run from the REPL. To do so: + +* Start a REPL for your editor. See [here for an example](https://github.com/logseq/logseq/blob/master/docs/develop-logseq.md#repl-setup). +* Load a test namespace. +* Run `(cljs.test/run-tests)` to run tests for the current test namespace. + + #### Database tests To write a test that uses a datascript db: @@ -188,7 +207,7 @@ To write a performance test: For examples of these tests, see `frontend.db.query-dsl-test` and `frontend.db.model-test`. -### Async Unit Testing +#### Async Tests Async unit testing is well supported in ClojureScript. https://clojurescript.org/tools/testing#async-testing is a good guide for how to diff --git a/e2e-tests/basic.spec.ts b/e2e-tests/basic.spec.ts index 0d2f581dda..6d835a0e0d 100644 --- a/e2e-tests/basic.spec.ts +++ b/e2e-tests/basic.spec.ts @@ -141,8 +141,8 @@ test('template', async ({ page, block }) => { await block.waitForBlocks(5) - // NOTE: use delay to type slower, to trigger auto-completion UI. - await block.clickNext() + // See-also: #9354 + await block.enterNext() await block.mustType('/template') await page.click('[title="Insert a created template here"]') @@ -154,6 +154,19 @@ test('template', async ({ page, block }) => { await popupMenuItem.click() await block.waitForBlocks(9) + + + await block.clickNext() + await block.mustType('/template') + + await page.click('[title="Insert a created template here"]') + // type to search template name + await page.keyboard.type(randomTemplate.substring(0, 3), { delay: 100 }) + + await popupMenuItem.waitFor({ timeout: 2000 }) // wait for template search + await popupMenuItem.click() + + await block.waitForBlocks(13) // 9 + 4 }) test('auto completion square brackets', async ({ page, block }) => { diff --git a/e2e-tests/editor.spec.ts b/e2e-tests/editor.spec.ts index 65fb6de47b..d680ae5912 100644 --- a/e2e-tests/editor.spec.ts +++ b/e2e-tests/editor.spec.ts @@ -260,7 +260,7 @@ test('undo and redo after starting an action should not destroy text #6267', asy // And it should keep what was undone as a redo action await page.keyboard.press(modKey + '+Shift+z') - await expect(page.locator('text="text2"')).toHaveCount(1) + await expect(page.locator('text="text1 text2 [[]]"')).toHaveCount(1) }) test('undo after starting an action should close the action menu #6269', async ({ page, block }) => { diff --git a/e2e-tests/fixtures.ts b/e2e-tests/fixtures.ts index 394f818809..16aea59a6b 100644 --- a/e2e-tests/fixtures.ts +++ b/e2e-tests/fixtures.ts @@ -21,22 +21,24 @@ export let graphDir = path.resolve(testTmpDir, "#e2e-test", repoName) // NOTE: This following is a console log watcher for error logs. // Save and print all logs when error happens. -let logs: string +let logs: string = ''; const consoleLogWatcher = (msg: ConsoleMessage) => { - // console.log(msg.text()) - const text = msg.text() - logs += text + '\n' + const text = msg.text(); - expect(text, logs).not.toMatch(/^(Failed to|Uncaught)/) + // List of error messages to ignore + const ignoreErrors = [ + /net::ERR_CONNECTION_REFUSED/, + /^Error with Permissions-Policy header:/ + ]; - // youtube video - // Error with Permissions-Policy header: Origin trial controlled feature not enabled: 'ch-ua-reduced'. - if (!text.match(/^Error with Permissions-Policy header:/)) { - expect(text, logs).not.toMatch(/^Error/) + // If the text matches any of the ignoreErrors, return early + if (ignoreErrors.some(error => text.match(error))) { + return; } - // NOTE: React warnings will be logged as error. - // expect(msg.type()).not.toBe('error') + logs += text + '\n'; + expect(text, logs).not.toMatch(/^(Failed to|Uncaught|Assert failed)/); + expect(text, logs).not.toMatch(/^Error/); } base.beforeAll(async () => { diff --git a/e2e-tests/shui/table.spec.js b/e2e-tests/shui/table.spec.js new file mode 100644 index 0000000000..4d4c73bc67 --- /dev/null +++ b/e2e-tests/shui/table.spec.js @@ -0,0 +1,304 @@ +import { expect } from '@playwright/test' +import fs from 'fs/promises' +import path from 'path' +import { test } from '../fixtures' +import { randomString, editFirstBlock, navigateToStartOfBlock, createRandomPage } from '../utils' + +test.setTimeout(60000) + +const KEY_DELAY = 100 + +// The following function assumes that the block is currently in edit mode, +// and it just enters a simple table +const inputSimpleTable = async (page) => { + await page.keyboard.type('| Header A | Header B |') + await page.keyboard.press('Shift+Enter') + await page.keyboard.type('| A1 | B1 |') + await page.keyboard.press('Shift+Enter') + await page.keyboard.type('| A2 | B2 |') + await page.keyboard.press('Escape') + await page.waitForTimeout(KEY_DELAY) +} + +// The following function does not assume any state, and will prepend the provided lines to the +// first block of the document +const prependPropsToFirstBlock = async (page, block, ...props) => { + await editFirstBlock(page) + await page.waitForTimeout(KEY_DELAY) + await navigateToStartOfBlock(page, block) + await page.waitForTimeout(KEY_DELAY) + + for (const prop of props) { + await page.keyboard.type(prop) + await page.waitForTimeout(KEY_DELAY) + await page.keyboard.press('Shift+Enter') + await page.waitForTimeout(KEY_DELAY) + } + + await page.keyboard.press('Escape') + await page.waitForTimeout(KEY_DELAY) +} + +const setPropInFirstBlock = async (page, block, prop, value) => { + await editFirstBlock(page) + await page.waitForTimeout(KEY_DELAY) + await navigateToStartOfBlock(page, block) + await page.waitForTimeout(KEY_DELAY) + + const inputValue = await page.inputValue('textarea >> nth=0') + + const match = inputValue.match(new RegExp(`${prop}::(.*)(\n|$)`)) + + if (!match) { + await page.keyboard.press('Shift+Enter') + await page.waitForTimeout(KEY_DELAY) + await page.keyboard.press('ArrowUp') + await page.waitForTimeout(KEY_DELAY) + await page.keyboard.type(`${prop}:: ${value}`) + // await page.waitForTimeout(1000) + // await page.waitForTimeout(KEY_DELAY) + // await page.keyboard.type(prop + ':: ' + value) + // await page.waitForTimeout(1000) + // await page.keyboard.press('Shift+Enter') + await page.waitForTimeout(KEY_DELAY) + await page.keyboard.press('Escape') + return await page.waitForTimeout(KEY_DELAY) + } + + const [propLine, propValue, propTernary] = match + const startIndex = match.index + const endIndex = startIndex + propLine.length - propTernary.length + + // Go to the of the prop + for (let i = 0; i < endIndex; i++) { + await page.keyboard.press('ArrowRight') + } + + // Delete the value of the prop + for (let i = 0; i < propValue.length; i++) { + await page.keyboard.press('Backspace') + } + + // Input the new value of the prop + await page.keyboard.type(" " + value.trim()) + await page.waitForTimeout(KEY_DELAY) + await page.keyboard.press('Escape') + return await page.waitForTimeout(KEY_DELAY) +} + + +test('table can have it\'s version changed via props', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a v1 table + inputSimpleTable(page) + + // find and confirm existence of first data cell + await expect(await page.locator('table tbody tr >> nth=0').innerHTML()).toContain('A1') + + // change to a version 2 table + await setPropInFirstBlock(page, block, 'logseq.table.version', '2') + + // find and confirm existence of first data cell in new format + await expect(await page.getByTestId('v2-table-container').innerHTML()).toContain('A1') +}) + +test('table can configure logseq.color::', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a v1 table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + + // check for default general config + await expect(await page.getByTestId('v2-table-gradient-accent')).not.toBeVisible() + + await setPropInFirstBlock(page, block, 'logseq.color', 'red') + + // check for gradient accent + await expect(await page.getByTestId('v2-table-gradient-accent')).toBeVisible() +}) + +test('table can configure logseq.table.hover::', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a v1 table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + + await page.waitForTimeout(KEY_DELAY) + await page.getByText('A1', { exact: true }).hover() + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]') + await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + + await setPropInFirstBlock(page, block, 'logseq.table.hover', 'row') + + await page.waitForTimeout(KEY_DELAY) + await page.getByText('A1', { exact: true }).hover() + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]') + await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + + await setPropInFirstBlock(page, block, 'logseq.table.hover', 'col') + + await page.waitForTimeout(KEY_DELAY) + await page.getByText('A1', { exact: true }).hover() + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]') + await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + + await setPropInFirstBlock(page, block, 'logseq.table.hover', 'both') + + await page.waitForTimeout(KEY_DELAY) + await page.getByText('A1', { exact: true }).hover() + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]') + await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + + await setPropInFirstBlock(page, block, 'logseq.table.hover', 'none') + + await page.waitForTimeout(KEY_DELAY) + await page.getByText('A1', { exact: true }).hover() + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-quaternary-background-color)]') + await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') + await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]') +}) + +test('table can configure logseq.table.headers', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + + // Check none (default) + await expect(await page.getByText('Header A', { exact: true })).toBeVisible() + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A") + + // Check none (explicit) + await setPropInFirstBlock(page, block, 'logseq.table.headers', 'none') + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A") + + // Check uppercase + await setPropInFirstBlock(page, block, 'logseq.table.headers', 'uppercase') + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("HEADER A") + + // Check lowercase + await setPropInFirstBlock(page, block, 'logseq.table.headers', 'lowercase') + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("header a") + + // Check capitalize + await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize') + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A") + + // Check capitalize-first + await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize-first') + await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header a") +}) + +test('table can configure logseq.table.borders', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + + // Check true (default) + await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/) + + // Check true (explicit) + await setPropInFirstBlock(page, block, 'logseq.table.borders', 'true') + await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/) + + // Check false + await setPropInFirstBlock(page, block, 'logseq.table.borders', 'false') + await expect(await page.getByTestId('v2-table-container')).not.toHaveCSS("gap", /^[1-9].*/) +}) + +test('table can configure logseq.table.stripes', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + await page.waitForTimeout(KEY_DELAY) + + // Check false (default) + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]") + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]") + + // Check false (explicit) + await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'false') + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]") + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]") + + // Check false + await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'true') + await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]") + await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-secondary-background-color)]") +}) + +test('table can configure logseq.table.compact', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + await page.waitForTimeout(KEY_DELAY) + + // Check false (default) + const defaultClasses = await page.getByText('A1', { exact: true }).getAttribute('class') + + // Check false (explicit) + await setPropInFirstBlock(page, block, 'logseq.table.compact', 'false') + const falseClasses = await page.getByText('A1', { exact: true }).getAttribute('class') + + // Check false + await setPropInFirstBlock(page, block, 'logseq.table.compact', 'true') + const trueClasses = await page.getByText('A1', { exact: true }).getAttribute('class') + + const getPX = (str) => { + const match = str.match(/px-\[([0-9\.]*)[a-z]*\]/) + return match ? parseFloat(match[1]) : null + } + + await expect(getPX(defaultClasses)).toEqual(getPX(falseClasses)) + await expect(getPX(defaultClasses)).toBeGreaterThan(getPX(trueClasses)) +}) + +test('table can configure logseq.table.cols::', async ({ page, block, graphDir }) => { + const pageTitle = await createRandomPage(page) + + // create a v1 table + await page.keyboard.type('logseq.table.version:: 2') + await page.keyboard.press('Shift+Enter') + await inputSimpleTable(page) + + // check for default general config + await expect(await page.getByText('A1', { exact: true })).toBeVisible() + await expect(await page.getByText('B1', { exact: true })).toBeVisible() + + await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A, Header B') + await expect(await page.getByText('A1', { exact: true })).toBeVisible() + await expect(await page.getByText('B1', { exact: true })).toBeVisible() + + await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A') + await expect(await page.getByText('A1', { exact: true })).toBeVisible() + await expect(await page.getByText('B1', { exact: true })).not.toBeVisible() + + await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header B') + await expect(await page.getByText('A1', { exact: true })).not.toBeVisible() + await expect(await page.getByText('B1', { exact: true })).toBeVisible() +}) diff --git a/e2e-tests/utils.ts b/e2e-tests/utils.ts index 5abff4220a..503ea7d638 100644 --- a/e2e-tests/utils.ts +++ b/e2e-tests/utils.ts @@ -206,3 +206,10 @@ export async function getIsWebAPIClipboardSupported(page: Page): Promise { const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('r') + await page.keyboard.type('wr') await page.mouse.move(bounds.x + 5, bounds.y + 5) await page.mouse.down() @@ -130,7 +130,7 @@ test('connect rectangles with an arrow', async ({ page }) => { const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('c') + await page.keyboard.type('wc') await page.mouse.move(bounds.x + 20, bounds.y + 20) await page.mouse.down() @@ -159,6 +159,38 @@ test('undo the delete action', async ({ page }) => { await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1) }) +test('convert the first rectangle to ellipse', async ({ page }) => { + await page.keyboard.press('Escape') + await page.waitForTimeout(1000) + await page.click('.logseq-tldraw .tl-box-container:first-of-type') + await page.mouse.move(0, 0) // move mouse to trigger a rerender of the context bar + await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor') + await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]') + + await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(1) + await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1) +}) + +test('change the color of the ellipse', async ({ page }) => { + await page.click('.tl-context-bar .tl-color-bg') + await page.click('.tl-context-bar .tl-color-palette .bg-red-500') + + await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-red)') +}) + +test('undo the color switch', async ({ page }) => { + await page.keyboard.press(modKey + '+z') + + await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-default)') +}) + +test('undo the shape conversion', async ({ page }) => { + await page.keyboard.press(modKey + '+z') + + await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2) + await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(0) +}) + test('locked elements should not be removed', async ({ page }) => { await page.keyboard.press('Escape') await page.waitForTimeout(1000) @@ -205,7 +237,7 @@ test('create a block', async ({ page }) => { const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('s') + await page.keyboard.type('ws') await page.mouse.dblclick(bounds.x + 5, bounds.y + 5) await page.waitForTimeout(100) @@ -240,7 +272,7 @@ test('copy/paste url to create an iFrame shape', async ({ page }) => { const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('t') + await page.keyboard.type('wt') await page.mouse.move(bounds.x + 5, bounds.y + 5) await page.mouse.down() await page.waitForTimeout(100) @@ -259,7 +291,7 @@ test('copy/paste twitter status url to create a Tweet shape', async ({ page }) = const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('t') + await page.keyboard.type('wt') await page.mouse.move(bounds.x + 5, bounds.y + 5) await page.mouse.down() await page.waitForTimeout(100) @@ -278,7 +310,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page }) const canvas = await page.waitForSelector('.logseq-tldraw') const bounds = (await canvas.boundingBox())! - await page.keyboard.press('t') + await page.keyboard.type('wt') await page.mouse.move(bounds.x + 5, bounds.y + 5) await page.mouse.down() await page.waitForTimeout(100) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 7d0094b5c1..b777c4ee75 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.4; + MARKETING_VERSION = 0.9.6; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -542,7 +542,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.4; + MARKETING_VERSION = 0.9.6; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -567,7 +567,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.9.4; + MARKETING_VERSION = 0.9.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; @@ -594,7 +594,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.9.4; + MARKETING_VERSION = 0.9.6; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/App/App/FsWatcher.swift b/ios/App/App/FsWatcher.swift index 578d389fbe..50c9ebded7 100644 --- a/ios/App/App/FsWatcher.swift +++ b/ios/App/App/FsWatcher.swift @@ -46,12 +46,16 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate { } public func receivedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?) { + guard let baseUrl = baseUrl else { + // unwatch, ignore incoming + return + } // NOTE: Event in js {dir path content stat{mtime}} switch event { case .Unlink: DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.notifyListeners("watcher", data: ["event": "unlink", - "dir": self.baseUrl?.description as Any, + "dir": baseUrl.description as Any, "path": url.description ]) } @@ -61,8 +65,8 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate { content = try? String(contentsOf: url, encoding: .utf8) } self.notifyListeners("watcher", data: ["event": event.description, - "dir": baseUrl?.description as Any, - "path": url.description, + "dir": baseUrl.description as Any, + "path": url.relativePath(from: baseUrl)?.precomposedStringWithCanonicalMapping as Any, "content": content as Any, "stat": ["mtime": metadata?.contentModificationTimestamp ?? 0, "ctime": metadata?.creationTimestamp ?? 0, @@ -265,3 +269,29 @@ public class PollingWatcher { self.metaDb = newMetaDb } } + + +extension URL { + func relativePath(from base: URL) -> String? { + // Ensure that both URLs represent files: + guard self.isFileURL && base.isFileURL else { + return nil + } + + // Remove/replace "." and "..", make paths absolute: + let destComponents = self.standardizedFileURL.pathComponents + let baseComponents = base.standardizedFileURL.pathComponents + + // Find number of common path components: + var i = 0 + while i < destComponents.count && i < baseComponents.count + && destComponents[i] == baseComponents[i] { + i += 1 + } + + // Build relative path: + var relComponents = Array(repeating: "..", count: baseComponents.count - i) + relComponents.append(contentsOf: destComponents[i...]) + return relComponents.joined(separator: "/") + } +} diff --git a/package.json b/package.json index ac8c76d3b7..8a02dd57e4 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@logseq/capacitor-file-sync": "0.0.24", "@logseq/diff-merge": "^0.0.2", "@logseq/react-tweet-embed": "1.3.1-1", + "@radix-ui/colors": "^0.1.8", "@sentry/react": "^6.18.2", "@sentry/tracing": "^6.18.2", "@tabler/icons": "1.119.0", @@ -134,6 +135,7 @@ "sanitize-filename": "1.6.3", "send-intent": "3.0.11", "sse.js": "^0.6.1", + "tailwind-capitalize-first-letter": "^1.0.4", "threads": "1.6.5", "url": "^0.11.0", "yargs-parser": "20.2.4" @@ -157,4 +159,4 @@ "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0", "pixi-graph-fork/@pixi/math": "6.2.0" } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index 651bbcf265..1021107a83 100644 --- a/public/index.html +++ b/public/index.html @@ -55,6 +55,7 @@ + diff --git a/resources/electron.html b/resources/electron.html index e7ebedef83..0eb9662f76 100644 --- a/resources/electron.html +++ b/resources/electron.html @@ -56,6 +56,7 @@ const portal = new MagicPortal(worker); + diff --git a/resources/index.html b/resources/index.html index 074b2d252e..892095b5b0 100644 --- a/resources/index.html +++ b/resources/index.html @@ -55,6 +55,7 @@ const portal = new MagicPortal(worker); + diff --git a/resources/js/tabler.ext.js b/resources/js/tabler.ext.js new file mode 100644 index 0000000000..a6f092299d --- /dev/null +++ b/resources/js/tabler.ext.js @@ -0,0 +1,11 @@ +(()=>{function e(e,o,n,C){Object.defineProperty(e,o,{get:n,set:C,enumerable:!0,configurable:!0})}var o="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},C={},r=o.parcelRequiree92c;null==r&&((r=function(e){if(e in n)return n[e].exports;if(e in C){var o=C[e];delete C[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,o){C[e]=o},o.parcelRequiree92c=r),r.register("9Hk4c",(function(o,n){ + /** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + var C,i,t;e(o.exports,"Fragment",(()=>C),(e=>C=e)),e(o.exports,"jsx",(()=>i),(e=>i=e)),e(o.exports,"jsxs",(()=>t),(e=>t=e));var l=r("6yBNn"),s=Symbol.for("react.element"),c=Symbol.for("react.fragment"),d=Object.prototype.hasOwnProperty,a=l.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,H={key:!0,ref:!0,__self:!0,__source:!0};function w(e,o,n){var C,r={},i=null,t=null;for(C in void 0!==n&&(i=""+n),void 0!==o.key&&(i=""+o.key),void 0!==o.ref&&(t=o.ref),o)d.call(o,C)&&!H.hasOwnProperty(C)&&(r[C]=o[C]);if(e&&e.defaultProps)for(C in o=e.defaultProps)void 0===r[C]&&(r[C]=o[C]);return{$$typeof:s,type:e,key:i,ref:t,props:r,_owner:a.current}}C=c,i=w,t=w})),r.register("6yBNn",(function(e,o){e.exports=React}));var i;i=r("9Hk4c"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),r("6yBNn"),window.tablerIcons=window.tablerIcons||{},window.tablerIcons.IconAddLink=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtAddLink",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M12.2933 4.29278C13.1438 3.4422 14.2975 2.96436 15.5004 2.96436C16.7033 2.96436 17.8569 3.4422 18.7075 4.29278C19.5581 5.14336 20.0359 6.29699 20.0359 7.49989C20.0359 8.70279 19.5581 9.85642 18.7075 10.707L14.7109 14.7036C14.2927 15.1294 13.7939 15.4678 13.2436 15.699C12.6917 15.9309 12.099 16.0504 11.5004 16.0504C10.9017 16.0504 10.309 15.9309 9.7571 15.699C9.20517 15.4671 8.70507 15.1274 8.28608 14.6997C7.89956 14.3053 7.90602 13.6721 8.30051 13.2856C8.695 12.8991 9.32813 12.9055 9.71465 13.3C9.94742 13.5376 10.2253 13.7263 10.5319 13.8552C10.8385 13.984 11.1678 14.0504 11.5004 14.0504C11.833 14.0504 12.1622 13.984 12.4688 13.8552C12.7755 13.7263 13.0533 13.5376 13.2861 13.3L13.2932 13.2927L13.2933 13.2928L17.2933 9.29278C17.7688 8.81728 18.0359 8.17235 18.0359 7.49989C18.0359 6.82742 17.7688 6.1825 17.2933 5.707C16.8178 5.23149 16.1728 4.96436 15.5004 4.96436C14.8279 4.96436 14.183 5.23149 13.7075 5.707L13.2075 6.207C12.8169 6.59752 12.1838 6.59752 11.7933 6.207C11.4027 5.81647 11.4027 5.18331 11.7933 4.79278L12.2933 4.29278ZM8.75711 8.30085C9.30904 8.06892 9.9017 7.94946 10.5004 7.94946C11.0991 7.94946 11.6917 8.06892 12.2436 8.30085C12.7956 8.53277 13.2957 8.87249 13.7147 9.30012C14.1012 9.69461 14.0947 10.3277 13.7002 10.7143C13.3057 11.1008 12.6726 11.0943 12.2861 10.6998C12.0533 10.4623 11.7755 10.2735 11.4689 10.1447C11.1622 10.0158 10.833 9.94946 10.5004 9.94946C10.1678 9.94946 9.83853 10.0158 9.5319 10.1447C9.22527 10.2735 8.94743 10.4623 8.71466 10.6998L8.70752 10.7071L8.70748 10.7071L4.70748 14.7071C4.23198 15.1826 3.96484 15.8275 3.96484 16.5C3.96484 17.1724 4.23198 17.8174 4.70748 18.2929C5.18299 18.7684 5.82791 19.0355 6.50038 19.0355C7.17284 19.0355 7.81777 18.7684 8.29327 18.2929L8.79327 17.7929C9.18379 17.4023 9.81696 17.4023 10.2075 17.7929C10.598 18.1834 10.598 18.8166 10.2075 19.2071L9.70748 19.7071C8.85691 20.5577 7.70328 21.0355 6.50038 21.0355C5.29748 21.0355 4.14385 20.5577 3.29327 19.7071C2.44269 18.8565 1.96484 17.7029 1.96484 16.5C1.96484 15.2971 2.44269 14.1434 3.29327 13.2929L7.28985 9.29629C7.70807 8.87046 8.20683 8.53208 8.75711 8.30085ZM19.0005 14C19.5528 14 20.0005 14.4477 20.0005 15V17H22.0005C22.5528 17 23.0005 17.4477 23.0005 18C23.0005 18.5523 22.5528 19 22.0005 19H20.0005V21C20.0005 21.5523 19.5528 22 19.0005 22C18.4482 22 18.0005 21.5523 18.0005 21V19H16.0005C15.4482 19 15.0005 18.5523 15.0005 18C15.0005 17.4477 15.4482 17 16.0005 17H18.0005V15C18.0005 14.4477 18.4482 14 19.0005 14Z",fill:o})})},window.tablerIcons.IconAppFeature=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsxs)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtAppFeature",...C,xmlns:"http://www.w3.org/2000/svg",children:[(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2 7C2 5.34315 3.34315 4 5 4H19C20.6569 4 22 5.34315 22 7V17C22 18.6569 20.6569 20 19 20H15.0209C15.0064 20.0003 14.992 20.0003 14.9776 20H9.02243C9.00802 20.0003 8.99357 20.0003 8.97908 20H5C3.34315 20 2 18.6569 2 17V7ZM13.7192 18L13.5299 17.2425C13.4447 16.9018 13.5445 16.5413 13.7929 16.2929L14.8787 15.2071L13.3586 14.9899C13.0337 14.9435 12.7523 14.7407 12.6056 14.4472L12 13.2361L11.3944 14.4472C11.2477 14.7407 10.9663 14.9435 10.6414 14.9899L9.12132 15.2071L10.2071 16.2929C10.4555 16.5413 10.5553 16.9018 10.4701 17.2425L10.2808 18H13.7192ZM15.7808 18H19C19.5523 18 20 17.5523 20 17V7C20 6.44772 19.5523 6 19 6H5C4.44772 6 4 6.44772 4 7V17C4 17.5523 4.44772 18 5 18H8.21922L8.39254 17.3067L6.29289 15.2071C6.02506 14.9393 5.93154 14.5431 6.05132 14.1838C6.17109 13.8244 6.48361 13.5636 6.85858 13.5101L9.83989 13.0841L11.1056 10.5528C11.275 10.214 11.6212 10 12 10C12.3788 10 12.725 10.214 12.8944 10.5528L14.1601 13.0841L17.1414 13.5101C17.5164 13.5636 17.8289 13.8244 17.9487 14.1838C18.0685 14.5431 17.9749 14.9393 17.7071 15.2071L15.6075 17.3067L15.7808 18Z",fill:o}),(0,i.jsx)("path",{d:"M6 6.99C6.55228 6.99 7 7.43771 7 7.99V8C7 8.55229 6.55228 9 6 9C5.44772 9 5 8.55229 5 8V7.99C5 7.43771 5.44772 6.99 6 6.99Z",fill:o}),(0,i.jsx)("path",{d:"M9 6.99C9.55228 6.99 10 7.43771 10 7.99V8C10 8.55228 9.55228 9 9 9C8.44772 9 8 8.55228 8 8V7.99C8 7.43771 8.44772 6.99 9 6.99Z",fill:o})]})},window.tablerIcons.IconBlockSearch=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtBlockSearch",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8 5C8 4.44772 8.44772 4 9 4H20C20.5523 4 21 4.44772 21 5V9C21 9.55228 20.5523 10 20 10H9C8.44772 10 8 9.55228 8 9V5ZM10 6V8H19V6H10ZM0 7C0 5.89543 0.89543 5 2 5C3.10457 5 4 5.89543 4 7C4 8.10457 3.10457 9 2 9C0.89543 9 0 8.10457 0 7ZM11 13C11 12.4477 11.4477 12 12 12H14C14.5523 12 15 12.4477 15 13C15 13.5523 14.5523 14 14 14H13V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V13ZM3 15C3 13.8954 3.89543 13 5 13C6.10457 13 7 13.8954 7 15C7 16.1046 6.10457 17 5 17C3.89543 17 3 16.1046 3 15ZM18.5 16C17.6716 16 17 16.6716 17 17.5C17 18.3284 17.6716 19 18.5 19C19.3284 19 20 18.3284 20 17.5C20 16.6716 19.3284 16 18.5 16ZM15 17.5C15 15.567 16.567 14 18.5 14C20.433 14 22 15.567 22 17.5C22 18.1028 21.8476 18.6699 21.5793 19.1651L23.7071 21.2929C24.0976 21.6834 24.0976 22.3166 23.7071 22.7071C23.3166 23.0976 22.6834 23.0976 22.2929 22.7071L20.1651 20.5793C19.6699 20.8476 19.1028 21 18.5 21C16.567 21 15 19.433 15 17.5Z",fill:o})})},window.tablerIcons.IconBlock=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtBlock",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M9 10C9 9.44772 9.44772 9 10 9H22C22.5523 9 23 9.44772 23 10V14C23 14.5523 22.5523 15 22 15H10C9.44772 15 9 14.5523 9 14V10ZM11 11V13H21V11H11ZM1 12C1 10.8954 1.89543 10 3 10C4.10457 10 5 10.8954 5 12C5 13.1046 4.10457 14 3 14C1.89543 14 1 13.1046 1 12Z",fill:o})})},window.tablerIcons.IconCloudExclamation=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtCloudExclamation",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M13.4366 6.03387C12.8308 5.92373 12.2062 5.92205 11.5997 6.02894C10.9931 6.13582 10.4204 6.34848 9.91384 6.65116C8.89016 7.26279 8.20994 8.19494 7.98007 9.21902C7.87759 9.67556 7.47225 10 7.00435 10C6.00786 10 5.06063 10.3791 4.36896 11.0407C3.6788 11.7008 3.2998 12.5864 3.2998 13.5C3.2998 14.4136 3.6788 15.2992 4.36896 15.9593C5.06063 16.6209 6.00786 17 7.00435 17H8C8.55228 17 9 17.4477 9 18C9 18.5523 8.55228 19 8 19H7.00435C5.5054 19 4.05939 18.4308 2.98651 17.4046C1.91213 16.3769 1.2998 14.9734 1.2998 13.5C1.2998 12.0266 1.91213 10.6231 2.98651 9.59538C3.87696 8.74365 5.02444 8.20677 6.24589 8.04882C6.72397 6.76252 7.65926 5.66843 8.88803 4.93427C9.60923 4.50337 10.4128 4.20727 11.2526 4.05929C12.0923 3.91131 12.9556 3.91362 13.7943 4.06613C14.6332 4.21864 15.4349 4.5191 16.1534 4.95398C16.8721 5.38893 17.4955 5.95128 17.9839 6.61332C18.4724 7.27568 18.8155 8.02416 18.988 8.817C19.0731 9.20796 19.1156 9.60469 19.1155 10.0014C20.2687 10.0298 21.3686 10.5003 22.1863 11.318C23.0302 12.1619 23.5044 13.3065 23.5044 14.5C23.5044 15.6935 23.0302 16.8381 22.1863 17.682C21.3424 18.5259 20.1978 19 19.0044 19H16C15.4477 19 15 18.5523 15 18C15 17.4477 15.4477 17 16 17H19.0044C19.6674 17 20.3033 16.7366 20.7721 16.2678C21.241 15.7989 21.5044 15.163 21.5044 14.5C21.5044 13.837 21.241 13.2011 20.7721 12.7322C20.3033 12.2634 19.6674 12 19.0044 12H18.0044C17.7007 12 17.4134 11.862 17.2236 11.6249C17.0339 11.3878 16.9621 11.0773 17.0286 10.781C17.1427 10.273 17.1444 9.75071 17.0337 9.24219C16.9231 8.7334 16.7009 8.24319 16.3744 7.80054C16.0476 7.35756 15.6221 6.9702 15.1178 6.66498C14.6134 6.35969 14.0423 6.144 13.4366 6.03387ZM12 14C12.5523 14 13 14.4477 13 15V18C13 18.5523 12.5523 19 12 19C11.4477 19 11 18.5523 11 18V15C11 14.4477 11.4477 14 12 14ZM12 21C12.5523 21 13 21.4477 13 22V22.01C13 22.5623 12.5523 23.01 12 23.01C11.4477 23.01 11 22.5623 11 22.01V22C11 21.4477 11.4477 21 12 21Z",fill:o})})},window.tablerIcons.IconConnector=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtConnector",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11 5C11 4.44772 11.4477 4 12 4H19C19.5523 4 20 4.44772 20 5V12C20 12.5523 19.5523 13 19 13C18.4477 13 18 12.5523 18 12V7.41421L5.70711 19.7071C5.31658 20.0976 4.68342 20.0976 4.29289 19.7071C3.90237 19.3166 3.90237 18.6834 4.29289 18.2929L16.5858 6H12C11.4477 6 11 5.55228 11 5Z",fill:o})})},window.tablerIcons.IconGroup=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtGroup",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1 2C1 1.44772 1.44772 1 2 1H4C4.55228 1 5 1.44772 5 2H19C19 1.44772 19.4477 1 20 1H22C22.5523 1 23 1.44772 23 2V4C23 4.55228 22.5523 5 22 5V19C22.5523 19 23 19.4477 23 20V22C23 22.5523 22.5523 23 22 23H20C19.4477 23 19 22.5523 19 22H5C5 22.5523 4.55228 23 4 23H2C1.44772 23 1 22.5523 1 22V20C1 19.4477 1.44772 19 2 19V5C1.44772 5 1 4.55228 1 4V2ZM4 5V19C4.55228 19 5 19.4477 5 20H19C19 19.4477 19.4477 19 20 19V5C19.4477 5 19 4.55228 19 4H5C5 4.55228 4.55228 5 4 5ZM6 7C6 6.44772 6.44772 6 7 6H13C13.5523 6 14 6.44772 14 7V9H17C17.5523 9 18 9.44772 18 10V17C18 17.5523 17.5523 18 17 18H10C9.44772 18 9 17.5523 9 17V14H7C6.44772 14 6 13.5523 6 13V7ZM11 14V16H16V11H14V13C14 13.5523 13.5523 14 13 14H11ZM12 8H8V12H12V8Z",fill:o})})},window.tablerIcons.IconHAuto=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtH-auto",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2 6C2 5.44772 2.44772 5 3 5H5C5.55228 5 6 5.44772 6 6C6 6.55228 5.55228 7 5 7V11H11V7C10.4477 7 10 6.55228 10 6C10 5.44772 10.4477 5 11 5H13C13.5523 5 14 5.44772 14 6C14 6.55228 13.5523 7 13 7V17C13.5523 17 14 17.4477 14 18C14 18.5523 13.5523 19 13 19H11C10.4477 19 10 18.5523 10 18C10 17.4477 10.4477 17 11 17V13H5V17C5.55228 17 6 17.4477 6 18C6 18.5523 5.55228 19 5 19H3C2.44772 19 2 18.5523 2 18C2 17.4477 2.44772 17 3 17V7C2.44772 7 2 6.55228 2 6ZM19 11C18.7348 11 18.4804 11.1054 18.2929 11.2929C18.1054 11.4804 18 11.7348 18 12V14H20V12C20 11.7348 19.8946 11.4804 19.7071 11.2929C19.5196 11.1054 19.2652 11 19 11ZM22 12C22 11.2043 21.6839 10.4413 21.1213 9.87868C20.5587 9.31607 19.7957 9 19 9C18.2043 9 17.4413 9.31607 16.8787 9.87868C16.3161 10.4413 16 11.2043 16 12V18C16 18.5523 16.4477 19 17 19C17.5523 19 18 18.5523 18 18V16H20V18C20 18.5523 20.4477 19 21 19C21.5523 19 22 18.5523 22 18V12Z",fill:o})})},window.tablerIcons.IconHeadingOff=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtHeadingOff",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2.29289 2.29289C2.68342 1.90237 3.31658 1.90237 3.70711 2.29289L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L18.5858 20H15C14.4477 20 14 19.5523 14 19C14 18.4477 14.4477 18 15 18H16V17.4142L11.5858 13H8V18H9C9.55228 18 10 18.4477 10 19C10 19.5523 9.55228 20 9 20H5C4.44772 20 4 19.5523 4 19C4 18.4477 4.44772 18 5 18H6V7.41421L2.29289 3.70711C1.90237 3.31658 1.90237 2.68342 2.29289 2.29289ZM8 9.41421V11H9.58579L8 9.41421ZM8 5C8 4.44772 8.44772 4 9 4H10C10.5523 4 11 4.44772 11 5C11 5.55228 10.5523 6 10 6H9C8.44772 6 8 5.55228 8 5ZM14 5C14 4.44772 14.4477 4 15 4H19C19.5523 4 20 4.44772 20 5C20 5.55228 19.5523 6 19 6H18V12C18 12.5523 17.5523 13 17 13C16.4477 13 16 12.5523 16 12V6H15C14.4477 6 14 5.55228 14 5Z",fill:o})})},window.tablerIcons.IconInternalLink=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtInternalLink",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4 6C4 4.34315 5.34315 3 7 3H18C19.6569 3 21 4.34315 21 6V17C21 18.6569 19.6569 20 18 20H11C10.4477 20 10 19.5523 10 19C10 18.4477 10.4477 18 11 18H18C18.5523 18 19 17.5523 19 17V6C19 5.44772 18.5523 5 18 5H7C6.44772 5 6 5.44772 6 6V13C6 13.5523 5.55228 14 5 14C4.44772 14 4 13.5523 4 13V6ZM8 10C8 9.44772 8.44772 9 9 9H14C14.5523 9 15 9.44772 15 10V15C15 15.5523 14.5523 16 14 16C13.4477 16 13 15.5523 13 15V12.4142L4.70711 20.7071C4.31658 21.0976 3.68342 21.0976 3.29289 20.7071C2.90237 20.3166 2.90237 19.6834 3.29289 19.2929L11.5858 11H9C8.44772 11 8 10.5523 8 10Z",fill:o})})},window.tablerIcons.IconLinkToBlock=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtLinkToBlock",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M9 8C9 7.44772 9.44772 7 10 7H22C22.5523 7 23 7.44772 23 8V12C23 12.5523 22.5523 13 22 13C21.4477 13 21 12.5523 21 12V9H11V12C11 12.5523 10.5523 13 10 13C9.44772 13 9 12.5523 9 12V8ZM1 10C1 8.89543 1.89543 8 3 8C4.10457 8 5 8.89543 5 10C5 11.1046 4.10457 12 3 12C1.89543 12 1 11.1046 1 10ZM15.2929 11.2929C15.6834 10.9024 16.3166 10.9024 16.7071 11.2929L19.7071 14.2929C20.0976 14.6834 20.0976 15.3166 19.7071 15.7071C19.3166 16.0976 18.6834 16.0976 18.2929 15.7071L17 14.4142V17C17 18.1046 17.8954 19 19 19H23C23.5523 19 24 19.4477 24 20C24 20.5523 23.5523 21 23 21H19C16.7909 21 15 19.2091 15 17V14.4142L13.7071 15.7071C13.3166 16.0976 12.6834 16.0976 12.2929 15.7071C11.9024 15.3166 11.9024 14.6834 12.2929 14.2929L15.2929 11.2929Z",fill:o})})},window.tablerIcons.IconLinkToPage=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtLinkToPage",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4 4C4 2.34315 5.34315 1 7 1H17C18.6569 1 20 2.34315 20 4V18C20 18.5523 19.5523 19 19 19C18.4477 19 18 18.5523 18 18V4C18 3.44772 17.5523 3 17 3H7C6.44772 3 6 3.44772 6 4V18C6 18.5523 5.55228 19 5 19C4.44772 19 4 18.5523 4 18V4ZM8 6C8 5.44772 8.44772 5 9 5H13C13.5523 5 14 5.44772 14 6C14 6.55228 13.5523 7 13 7H9C8.44772 7 8 6.55228 8 6ZM11.2929 13.2929C11.6834 12.9024 12.3166 12.9024 12.7071 13.2929L15.7071 16.2929C16.0976 16.6834 16.0976 17.3166 15.7071 17.7071C15.3166 18.0976 14.6834 18.0976 14.2929 17.7071L13 16.4142V19C13 20.1046 13.8954 21 15 21H19C19.5523 21 20 21.4477 20 22C20 22.5523 19.5523 23 19 23H15C12.7909 23 11 21.2091 11 19L11 16.4142L9.70711 17.7071C9.31658 18.0976 8.68342 18.0976 8.29289 17.7071C7.90237 17.3166 7.90237 16.6834 8.29289 16.2929L11.2929 13.2929Z",fill:o})})},window.tablerIcons.IconLinkToWhiteboard=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtLinkToWhiteboard",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2 5C2 3.34315 3.34315 2 5 2H19C20.6569 2 22 3.34315 22 5V15C22 16.6569 20.6569 18 19 18H17C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16H19C19.5523 16 20 15.5523 20 15V5C20 4.44772 19.5523 4 19 4H14V7C14 7.55228 13.5523 8 13 8H7C6.44772 8 6 7.55228 6 7V4H5C4.44772 4 4 4.44772 4 5V17C4 17.5523 3.55228 18 3 18C2.44772 18 2 17.5523 2 17V5ZM8 4V6H12V4H8ZM17 6C17.5523 6 18 6.44772 18 7V13C18 13.5523 17.5523 14 17 14C16.4477 14 16 13.5523 16 13V7C16 6.44772 16.4477 6 17 6ZM9.29289 12.2929C9.68342 11.9024 10.3166 11.9024 10.7071 12.2929L13.7071 15.2929C14.0976 15.6834 14.0976 16.3166 13.7071 16.7071C13.3166 17.0976 12.6834 17.0976 12.2929 16.7071L11 15.4142V18C11 19.1046 11.8954 20 13 20H17C17.5523 20 18 20.4477 18 21C18 21.5523 17.5523 22 17 22H13C10.7909 22 9 20.2091 9 18L9 15.4142L7.70711 16.7071C7.31658 17.0976 6.68342 17.0976 6.29289 16.7071C5.90237 16.3166 5.90237 15.6834 6.29289 15.2929L9.29289 12.2929Z",fill:o})})},window.tablerIcons.IconNewBlock=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtNewBlock",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8 5C8 4.44772 8.44772 4 9 4H20C20.5523 4 21 4.44772 21 5V9C21 9.55228 20.5523 10 20 10H9C8.44772 10 8 9.55228 8 9V5ZM10 6V8H19V6H10ZM0 7C0 5.89543 0.89543 5 2 5C3.10457 5 4 5.89543 4 7C4 8.10457 3.10457 9 2 9C0.89543 9 0 8.10457 0 7ZM11 13C11 12.4477 11.4477 12 12 12H16C16.5523 12 17 12.4477 17 13C17 13.5523 16.5523 14 16 14H13V16C13.5523 16 14 16.4477 14 17C14 17.5523 13.5523 18 13 18H12C11.4477 18 11 17.5523 11 17V13ZM3 15C3 13.8954 3.89543 13 5 13C6.10457 13 7 13.8954 7 15C7 16.1046 6.10457 17 5 17C3.89543 17 3 16.1046 3 15ZM20 13C20.5523 13 21 13.4477 21 14V16H23C23.5523 16 24 16.4477 24 17C24 17.5523 23.5523 18 23 18H21V20C21 20.5523 20.5523 21 20 21C19.4477 21 19 20.5523 19 20V18H17C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16H19V14C19 13.4477 19.4477 13 20 13Z",fill:o})})},window.tablerIcons.IconNewPage=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtNewPage",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4 5C4 3.34315 5.34315 2 7 2H17C18.6569 2 20 3.34315 20 5V12C20 12.5523 19.5523 13 19 13C18.4477 13 18 12.5523 18 12V5C18 4.44772 17.5523 4 17 4H7C6.44772 4 6 4.44772 6 5V17C6 17.5523 6.44772 18 7 18H12C12.5523 18 13 18.4477 13 19C13 19.5523 12.5523 20 12 20H7C5.34315 20 4 18.6569 4 17V5ZM8 7C8 6.44772 8.44772 6 9 6H13C13.5523 6 14 6.44772 14 7C14 7.55228 13.5523 8 13 8H9C8.44772 8 8 7.55228 8 7ZM19 15C19.5523 15 20 15.4477 20 16V18H22C22.5523 18 23 18.4477 23 19C23 19.5523 22.5523 20 22 20H20V22C20 22.5523 19.5523 23 19 23C18.4477 23 18 22.5523 18 22V20H16C15.4477 20 15 19.5523 15 19C15 18.4477 15.4477 18 16 18H18V16C18 15.4477 18.4477 15 19 15Z",fill:o})})},window.tablerIcons.IconNewWhiteboardElement=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtNewWhiteboardElement",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M6 4C5.44772 4 5 4.44772 5 5V6C5 6.55228 4.55228 7 4 7C3.44772 7 3 6.55228 3 6V5C3 3.34315 4.34315 2 6 2H8C8.55228 2 9 2.44772 9 3C9 3.55228 8.55228 4 8 4H6ZM15 3C15 2.44772 15.4477 2 16 2H18C19.6569 2 21 3.34315 21 5V6C21 6.55228 20.5523 7 20 7C19.4477 7 19 6.55228 19 6V5C19 4.44772 18.5523 4 18 4H16C15.4477 4 15 3.55228 15 3ZM7 9C7 8.44772 7.44772 8 8 8H16C16.5523 8 17 8.44772 17 9V13C17 13.5523 16.5523 14 16 14H8C7.44772 14 7 13.5523 7 13V9ZM9 10V12H15V10H9ZM4 15C4.55228 15 5 15.4477 5 16V17C5 17.5523 5.44772 18 6 18H8C8.55228 18 9 18.4477 9 19C9 19.5523 8.55228 20 8 20H6C4.34315 20 3 18.6569 3 17V16C3 15.4477 3.44772 15 4 15ZM20 15C20.5523 15 21 15.4477 21 16V18H23C23.5523 18 24 18.4477 24 19C24 19.5523 23.5523 20 23 20H21V22C21 22.5523 20.5523 23 20 23C19.4477 23 19 22.5523 19 22V20H17C16.4477 20 16 19.5523 16 19C16 18.4477 16.4477 18 17 18H19V16C19 15.4477 19.4477 15 20 15Z",fill:o})})},window.tablerIcons.IconNewWhiteboard=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtNewWhiteboard",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1 6C1 4.34315 2.34315 3 4 3H18C19.6569 3 21 4.34315 21 6V11C21 11.5523 20.5523 12 20 12C19.4477 12 19 11.5523 19 11V6C19 5.44772 18.5523 5 18 5H13V8C13 8.55228 12.5523 9 12 9H6C5.44772 9 5 8.55228 5 8V5H4C3.44772 5 3 5.44772 3 6V13H10C10.5523 13 11 13.4477 11 14V17H13C13.5523 17 14 17.4477 14 18C14 18.5523 13.5523 19 13 19H4C2.34315 19 1 17.6569 1 16V6ZM9 17V15H3V16C3 16.5523 3.44772 17 4 17H9ZM7 5V7H11V5H7ZM20 14C20.5523 14 21 14.4477 21 15V17H23C23.5523 17 24 17.4477 24 18C24 18.5523 23.5523 19 23 19H21V21C21 21.5523 20.5523 22 20 22C19.4477 22 19 21.5523 19 21V19H17C16.4477 19 16 18.5523 16 18C16 17.4477 16.4477 17 17 17H19V15C19 14.4477 19.4477 14 20 14Z",fill:o})})},window.tablerIcons.IconObjectCompact=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtObjectCompact",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M3 3C3 2.44772 3.44772 2 4 2H20C20.5523 2 21 2.44772 21 3V9C21 9.55228 20.5523 10 20 10H18C17.4477 10 17 9.55228 17 9C17 8.44772 17.4477 8 18 8H19V4H5V8H6C6.55228 8 7 8.44772 7 9C7 9.55228 6.55228 10 6 10H4C3.44772 10 3 9.55228 3 9V3ZM11.2929 7.29289C11.6834 6.90237 12.3166 6.90237 12.7071 7.29289L16.7071 11.2929C17.0976 11.6834 17.0976 12.3166 16.7071 12.7071C16.3166 13.0976 15.6834 13.0976 15.2929 12.7071L13 10.4142V19C13 19.5523 12.5523 20 12 20C11.4477 20 11 19.5523 11 19V10.4142L8.70711 12.7071C8.31658 13.0976 7.68342 13.0976 7.29289 12.7071C6.90237 12.3166 6.90237 11.6834 7.29289 11.2929L11.2929 7.29289Z",fill:o})})},window.tablerIcons.IconObjectExpanded=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtObjectExpanded",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M3 3C3 2.44772 3.44772 2 4 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V3ZM5 4V8H8C8.55228 8 9 8.44772 9 9C9 9.55228 8.55228 10 8 10H5V20H19V10H16C15.4477 10 15 9.55228 15 9C15 8.44772 15.4477 8 16 8H19V4H5ZM12 6C12.5523 6 13 6.44772 13 7V14.5858L15.2929 12.2929C15.6834 11.9024 16.3166 11.9024 16.7071 12.2929C17.0976 12.6834 17.0976 13.3166 16.7071 13.7071L12.7071 17.7071C12.3166 18.0976 11.6834 18.0976 11.2929 17.7071L7.29289 13.7071C6.90237 13.3166 6.90237 12.6834 7.29289 12.2929C7.68342 11.9024 8.31658 11.9024 8.70711 12.2929L11 14.5858V7C11 6.44772 11.4477 6 12 6Z",fill:o})})},window.tablerIcons.IconOpenAsPage=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtOpenAsPage",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4 6C4 4.34315 5.34315 3 7 3H17C18.6569 3 20 4.34315 20 6V18C20 19.6569 18.6569 21 17 21H16C15.4477 21 15 20.5523 15 20C15 19.4477 15.4477 19 16 19H17C17.5523 19 18 18.5523 18 18V6C18 5.44772 17.5523 5 17 5H7C6.44772 5 6 5.44772 6 6V18C6 18.5523 6.44772 19 7 19H8C8.55228 19 9 19.4477 9 20C9 20.5523 8.55228 21 8 21H7C5.34315 21 4 19.6569 4 18V6ZM8 8C8 7.44772 8.44772 7 9 7H13C13.5523 7 14 7.44772 14 8C14 8.55228 13.5523 9 13 9H9C8.44772 9 8 8.55228 8 8ZM11.2929 13.2929C11.6834 12.9024 12.3166 12.9024 12.7071 13.2929L14.7071 15.2929C15.0976 15.6834 15.0976 16.3166 14.7071 16.7071C14.3166 17.0976 13.6834 17.0976 13.2929 16.7071L13 16.4142V23C13 23.5523 12.5523 24 12 24C11.4477 24 11 23.5523 11 23L11 16.4142L10.7071 16.7071C10.3166 17.0976 9.68342 17.0976 9.29289 16.7071C8.90237 16.3166 8.90237 15.6834 9.29289 15.2929L11.2929 13.2929Z",fill:o})})},window.tablerIcons.IconPageSearch=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtPageSearch",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4 6C4 4.34315 5.34315 3 7 3H17C18.6569 3 20 4.34315 20 6V11C20 11.5523 19.5523 12 19 12C18.4477 12 18 11.5523 18 11V6C18 5.44772 17.5523 5 17 5H7C6.44772 5 6 5.44772 6 6V18C6 18.5523 6.44772 19 7 19H10C10.5523 19 11 19.4477 11 20C11 20.5523 10.5523 21 10 21H7C5.34315 21 4 19.6569 4 18V6ZM8 8C8 7.44772 8.44772 7 9 7H13C13.5523 7 14 7.44772 14 8C14 8.55228 13.5523 9 13 9H9C8.44772 9 8 8.55228 8 8ZM16.5 16C15.6716 16 15 16.6716 15 17.5C15 18.3284 15.6716 19 16.5 19C17.3284 19 18 18.3284 18 17.5C18 16.6716 17.3284 16 16.5 16ZM13 17.5C13 15.567 14.567 14 16.5 14C18.433 14 20 15.567 20 17.5C20 18.1028 19.8476 18.6699 19.5793 19.1651L21.7071 21.2929C22.0976 21.6834 22.0976 22.3166 21.7071 22.7071C21.3166 23.0976 20.6834 23.0976 20.2929 22.7071L18.1651 20.5793C17.6699 20.8476 17.1028 21 16.5 21C14.567 21 13 19.433 13 17.5Z",fill:o})})},window.tablerIcons.IconPage=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtPage",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M7 4C6.44772 4 6 4.44772 6 5V19C6 19.5523 6.44771 20 7 20H17C17.5523 20 18 19.5523 18 19V5C18 4.44772 17.5523 4 17 4H7ZM4 5C4 3.34315 5.34315 2 7 2H17C18.6569 2 20 3.34315 20 5V19C20 20.6569 18.6569 22 17 22H7C5.34315 22 4 20.6569 4 19V5ZM8 7C8 6.44772 8.44772 6 9 6H13C13.5523 6 14 6.44772 14 7C14 7.55228 13.5523 8 13 8H9C8.44772 8 8 7.55228 8 7Z",fill:o})})},window.tablerIcons.IconReferencesHide=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtReferencesHide",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.2929 2.29289C11.6834 1.90237 12.3166 1.90237 12.7071 2.29289L14.7071 4.29289C15.0976 4.68342 15.0976 5.31658 14.7071 5.70711C14.3166 6.09763 13.6834 6.09763 13.2929 5.70711L12 4.41421L10.7071 5.70711C10.3166 6.09763 9.68342 6.09763 9.29289 5.70711C8.90237 5.31658 8.90237 4.68342 9.29289 4.29289L11.2929 2.29289ZM1 11C1 9.34315 2.34315 8 4 8H20C21.6569 8 23 9.34315 23 11V19C23 20.6569 21.6569 22 20 22H4C2.34315 22 1 20.6569 1 19V11ZM4 10C3.44772 10 3 10.4477 3 11V19C3 19.5523 3.44772 20 4 20H20C20.5523 20 21 19.5523 21 19V11C21 10.4477 20.5523 10 20 10H4ZM5 13C5 12.4477 5.44772 12 6 12H10C10.5523 12 11 12.4477 11 13C11 13.5523 10.5523 14 10 14H6C5.44772 14 5 13.5523 5 13ZM8 16C8.55228 16 9 16.4477 9 17V17.01C9 17.5623 8.55228 18.01 8 18.01C7.44772 18.01 7 17.5623 7 17.01V17C7 16.4477 7.44772 16 8 16ZM18 18H12C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16H18C18.5523 16 19 16.4477 19 17C19 17.5523 18.5523 18 18 18Z",fill:o})})},window.tablerIcons.IconReferencesShow=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtReferencesShow",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M9.29289 2.29289C9.68342 1.90237 10.3166 1.90237 10.7071 2.29289L12 3.58579L13.2929 2.29289C13.6834 1.90237 14.3166 1.90237 14.7071 2.29289C15.0976 2.68342 15.0976 3.31658 14.7071 3.70711L12.7071 5.70711C12.3166 6.09763 11.6834 6.09763 11.2929 5.70711L9.29289 3.70711C8.90237 3.31658 8.90237 2.68342 9.29289 2.29289ZM1 11C1 9.34315 2.34315 8 4 8H20C21.6569 8 23 9.34315 23 11V19C23 20.6569 21.6569 22 20 22H4C2.34315 22 1 20.6569 1 19V11ZM4 10C3.44772 10 3 10.4477 3 11V19C3 19.5523 3.44772 20 4 20H20C20.5523 20 21 19.5523 21 19V11C21 10.4477 20.5523 10 20 10H4ZM5 13C5 12.4477 5.44772 12 6 12H10C10.5523 12 11 12.4477 11 13C11 13.5523 10.5523 14 10 14H6C5.44772 14 5 13.5523 5 13ZM8 16C8.55228 16 9 16.4477 9 17V17.01C9 17.5623 8.55228 18.01 8 18.01C7.44772 18.01 7 17.5623 7 17.01V17C7 16.4477 7.44772 16 8 16ZM18 18H12C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16H18C18.5523 16 19 16.4477 19 17C19 17.5523 18.5523 18 18 18Z",fill:o})})},window.tablerIcons.IconSelectCursor=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtSelectCursor",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.29292 4.29289C4.56075 4.02506 4.95692 3.93154 5.31625 4.05132L19.3163 8.71798C19.7 8.84589 19.9688 9.19233 19.9975 9.59578C20.0262 9.99922 19.809 10.3802 19.4472 10.5611L16.6659 11.9518L19.4483 14.7343C19.8389 15.1248 19.8389 15.758 19.4483 16.1485L16.1485 19.4483C15.758 19.8388 15.1248 19.8388 14.7343 19.4483L11.9518 16.6658L10.5611 19.4472C10.3802 19.809 9.99925 20.0262 9.5958 19.9975C9.19236 19.9688 8.84591 19.6999 8.71801 19.3162L4.05134 5.31623C3.93157 4.95689 4.02509 4.56073 4.29292 4.29289ZM6.58117 6.58114L9.85194 16.3934L10.7834 14.5305C10.9272 14.2429 11.2004 14.0421 11.5177 13.9906C11.835 13.9391 12.1577 14.0433 12.385 14.2706L15.4414 17.327L17.327 15.4414L14.2706 12.3849C14.0433 12.1576 13.9391 11.8349 13.9906 11.5177C14.0421 11.2004 14.243 10.9272 14.5305 10.7834L16.3935 9.85191L6.58117 6.58114Z",fill:o})})},window.tablerIcons.IconText=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtText",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M5 5C5 4.44772 5.44772 4 6 4H18C18.5523 4 19 4.44772 19 5V7C19 7.55228 18.5523 8 18 8C17.4477 8 17 7.55228 17 7V6H13V18H14C14.5523 18 15 18.4477 15 19C15 19.5523 14.5523 20 14 20H10C9.44772 20 9 19.5523 9 19C9 18.4477 9.44772 18 10 18H11V6H7V7C7 7.55228 6.55228 8 6 8C5.44772 8 5 7.55228 5 7V5Z",fill:o})})},window.tablerIcons.IconUngroup=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtUngroup",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2 3C2 2.44772 2.44772 2 3 2H5C5.55228 2 6 2.44772 6 3H12C12 2.44772 12.4477 2 13 2H15C15.5523 2 16 2.44772 16 3V5C16 5.55228 15.5523 6 15 6V8H18C18 7.44772 18.4477 7 19 7H21C21.5523 7 22 7.44772 22 8V10C22 10.5523 21.5523 11 21 11V18C21.5523 18 22 18.4477 22 19V21C22 21.5523 21.5523 22 21 22H19C18.4477 22 18 21.5523 18 21H11C11 21.5523 10.5523 22 10 22H8C7.44772 22 7 21.5523 7 21V19C7 18.4477 7.44772 18 8 18V15H6C6 15.5523 5.55228 16 5 16H3C2.44772 16 2 15.5523 2 15V13C2 12.4477 2.44772 12 3 12V6C2.44772 6 2 5.55228 2 5V3ZM5 6V12C5.55228 12 6 12.4477 6 13H12C12 12.4477 12.4477 12 13 12V6C12.4477 6 12 5.55228 12 5H6C6 5.55228 5.55228 6 5 6ZM15 12V10H18C18 10.5523 18.4477 11 19 11V18C18.4477 18 18 18.4477 18 19H11C11 18.4477 10.5523 18 10 18V15H12C12 15.5523 12.4477 16 13 16H15C15.5523 16 16 15.5523 16 15V13C16 12.4477 15.5523 12 15 12Z",fill:o})})},window.tablerIcons.IconWhiteboardElement=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtWhiteboardElement",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M6 5C5.44772 5 5 5.44772 5 6V7C5 7.55228 4.55228 8 4 8C3.44772 8 3 7.55228 3 7V6C3 4.34315 4.34315 3 6 3H8C8.55228 3 9 3.44772 9 4C9 4.55228 8.55228 5 8 5H6ZM15 4C15 3.44772 15.4477 3 16 3H18C19.6569 3 21 4.34315 21 6V7C21 7.55228 20.5523 8 20 8C19.4477 8 19 7.55228 19 7V6C19 5.44772 18.5523 5 18 5H16C15.4477 5 15 4.55228 15 4ZM7 10C7 9.44772 7.44772 9 8 9H16C16.5523 9 17 9.44772 17 10V14C17 14.5523 16.5523 15 16 15H8C7.44772 15 7 14.5523 7 14V10ZM9 11V13H15V11H9ZM4 16C4.55228 16 5 16.4477 5 17V18C5 18.5523 5.44772 19 6 19H8C8.55228 19 9 19.4477 9 20C9 20.5523 8.55228 21 8 21H6C4.34315 21 3 19.6569 3 18V17C3 16.4477 3.44772 16 4 16ZM20 16C20.5523 16 21 16.4477 21 17V18C21 19.6569 19.6569 21 18 21H16C15.4477 21 15 20.5523 15 20C15 19.4477 15.4477 19 16 19H18C18.5523 19 19 18.5523 19 18V17C19 16.4477 19.4477 16 20 16Z",fill:o})})},window.tablerIcons.IconWhiteboardSearch=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtWhiteboardSearch",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1 6C1 4.34315 2.34315 3 4 3H18C19.6569 3 21 4.34315 21 6V11C21 11.5523 20.5523 12 20 12C19.4477 12 19 11.5523 19 11V6C19 5.44772 18.5523 5 18 5H13V8C13 8.55228 12.5523 9 12 9H6C5.44772 9 5 8.55228 5 8V5H4C3.44772 5 3 5.44772 3 6V13H10C10.5523 13 11 13.4477 11 14V17H12C12.5523 17 13 17.4477 13 18C13 18.5523 12.5523 19 12 19H4C2.34315 19 1 17.6569 1 16V6ZM9 17V15H3V16C3 16.5523 3.44772 17 4 17H9ZM7 5V7H11V5H7ZM18.5 16C17.6716 16 17 16.6716 17 17.5C17 18.3284 17.6716 19 18.5 19C19.3284 19 20 18.3284 20 17.5C20 16.6716 19.3284 16 18.5 16ZM15 17.5C15 15.567 16.567 14 18.5 14C20.433 14 22 15.567 22 17.5C22 18.1028 21.8476 18.6699 21.5793 19.1651L23.7071 21.2929C24.0976 21.6834 24.0976 22.3166 23.7071 22.7071C23.3166 23.0976 22.6834 23.0976 22.2929 22.7071L20.1651 20.5793C19.6699 20.8476 19.1028 21 18.5 21C16.567 21 15 19.433 15 17.5Z",fill:o})})},window.tablerIcons.IconWhiteboard=function({size:e=24,color:o="currentColor",stroke:n=0,...C}){return(0,i.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",strokeWidth:n,stroke:o,strokeLinecap:"round",strokeLinejoin:"round",className:"icon iconTabler iconTablerExtWhiteboard",...C,xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)("path",{d:"M18 9C18 8.44772 17.5523 8 17 8C16.4477 8 16 8.44772 16 9H18ZM16 15C16 15.5523 16.4477 16 17 16C17.5523 16 18 15.5523 18 15H16ZM3 14C2.44772 14 2 14.4477 2 15C2 15.5523 2.44772 16 3 16V14ZM11 15H12C12 14.4477 11.5523 14 11 14V15ZM10 19C10 19.5523 10.4477 20 11 20C11.5523 20 12 19.5523 12 19H10ZM14 5C14 4.44772 13.5523 4 13 4C12.4477 4 12 4.44772 12 5H14ZM13 9V10C13.5523 10 14 9.55228 14 9H13ZM7 9H6C6 9.55228 6.44772 10 7 10V9ZM8 5C8 4.44772 7.55228 4 7 4C6.44772 4 6 4.44772 6 5H8ZM5 6H19V4H5V6ZM20 7V17H22V7H20ZM19 18H5V20H19V18ZM4 17V7H2V17H4ZM5 18C4.44772 18 4 17.5523 4 17H2C2 18.6569 3.34315 20 5 20V18ZM20 17C20 17.5523 19.5523 18 19 18V20C20.6569 20 22 18.6569 22 17H20ZM19 6C19.5523 6 20 6.44772 20 7H22C22 5.34315 20.6569 4 19 4V6ZM5 4C3.34315 4 2 5.34315 2 7H4C4 6.44772 4.44772 6 5 6V4ZM16 9V15H18V9H16ZM3 16H11V14H3V16ZM10 15V19H12V15H10ZM12 5V9H14V5H12ZM13 8H7V10H13V8ZM8 9V5H6V9H8Z",fill:o})})}})(); \ No newline at end of file diff --git a/resources/package.json b/resources/package.json index 238e889c88..75ad337bf9 100644 --- a/resources/package.json +++ b/resources/package.json @@ -1,7 +1,7 @@ { "name": "Logseq", "productName": "Logseq", - "version": "0.9.4", + "version": "0.9.6", "main": "electron.js", "author": "Logseq", "license": "AGPL-3.0", diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 7bfe14325d..e09c3f91f5 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -21,8 +21,8 @@ :depends-on #{:main}} :tldraw {:entries [frontend.extensions.tldraw] - :depends-on #{:main}} - } + :depends-on #{:main}}} + :output-dir "./static/js" :asset-path "/static/js" :release {:asset-path "https://asset.logseq.com/static/js"} @@ -95,8 +95,8 @@ :depends-on #{:main}} :tldraw {:entries [frontend.extensions.tldraw] - :depends-on #{:main}} - } + :depends-on #{:main}}} + :output-dir "./static/js/publishing" :asset-path "static/js" :closure-defines {frontend.config/PUBLISHING true @@ -109,5 +109,5 @@ :redef false}} :devtools {:before-load frontend.core/stop :after-load frontend.core/start - :preloads [devtools.preload]}} - }} + :preloads [devtools.preload]}}}} + diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 43a07cbd84..9ea72da4b6 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -23,8 +23,8 @@ [frontend.context.i18n :refer [t]] [frontend.date :as date] [frontend.db :as db] - [frontend.db-mixins :as db-mixins] [frontend.db.model :as model] + [frontend.db-mixins :as db-mixins] [frontend.extensions.highlight :as highlight] [frontend.extensions.latex :as latex] [frontend.extensions.lightbox :as lightbox] @@ -53,6 +53,7 @@ [frontend.mobile.util :as mobile-util] [frontend.modules.outliner.tree :as tree] [frontend.security :as security] + [frontend.shui :refer [get-shui-component-version make-shui-context]] [frontend.state :as state] [frontend.template :as template] [frontend.ui :as ui] @@ -74,6 +75,7 @@ [logseq.graph-parser.util.block-ref :as block-ref] [logseq.graph-parser.util.page-ref :as page-ref] [logseq.graph-parser.whiteboard :as gp-whiteboard] + [logseq.shui.core :as shui] [medley.core :as medley] [promesa.core :as p] [reitit.frontend.easy :as rfe] @@ -293,8 +295,8 @@ (js/setTimeout #(reset! *resizing-image? false) 200))) :onClick (fn [e] (when @*resizing-image? (util/stop e)))} - (and (:width metadata) (not (util/mobile?))) - (assoc :style {:width (:width metadata)})) + (and (:width metadata) (not (util/mobile?))) + (assoc :style {:width (:width metadata)})) {}) [:div.asset-container {:key "resize-asset-container"} [:img.rounded-sm.relative @@ -938,9 +940,8 @@ inner)]) [:span.warning.mr-1 {:title "Block ref invalid"} (block-ref/->block-ref id)])) - [:span.warning.mr-1 {:title "Block ref invalid"} - (block-ref/->block-ref id)] -)) + [:span.warning.mr-1 {:title "Block ref invalid"} + (block-ref/->block-ref id)])) (defn inline-text ([format v] @@ -1119,8 +1120,8 @@ {:href (path/path-join "file://" path) :data-href path :target "_blank"} - title - (assoc :title title)) + title + (assoc :title title)) (map-inline config label))) :else @@ -1213,8 +1214,8 @@ (cond-> {:href (ar-url->http-url href) :target "_blank"} - title - (assoc :title title)) + title + (assoc :title title)) (map-inline config label)) :else @@ -1223,11 +1224,11 @@ (cond-> {:href href :target "_blank"} - title - (assoc :title title)) + title + (assoc :title title)) (map-inline config label))))))) -(declare ->hiccup) +(declare ->hiccup inline) (defn wrap-query-components [config] @@ -1236,7 +1237,8 @@ :->elem ->elem :page-cp page-cp :inline-text inline-text - :map-inline map-inline})) + :map-inline map-inline + :inline inline})) ;;;; Macro component render functions (defn- macro-query-cp @@ -1751,7 +1753,7 @@ order-list? (boolean own-number-list?) order-list-idx (:own-order-list-index config) collapsable? (editor-handler/collapsable? uuid {:semantic? true})] - [:div.block-control-wrap.mr-1.flex.flex-row.items-center.sm:mr-2 + [:div.block-control-wrap.flex.flex-row.items-center {:class (util/classnames [{:is-order-list order-list? :bullet-closed collapsed?}])} (when (or (not fold-button-right?) collapsable?) @@ -2222,7 +2224,10 @@ (editor-handler/clear-selection!) (editor-handler/unhighlight-blocks!) (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block) - cursor-range (util/caret-range (gdom/getElement block-id)) + cursor-range (some-> (gdom/getElement block-id) + (dom/by-class "block-content-wrapper") + first + util/caret-range) {:block/keys [content format]} block content (->> content (property/remove-built-in-properties format) @@ -2260,7 +2265,7 @@ (and (not block-content?) (seq (:block/children block)) (= move-to :nested))) - (dnd-separator move-to block-content?)))))) + (dnd-separator move-to block-content?)))))) (defn clock-summary-cp [block body] @@ -2331,19 +2336,19 @@ content (if (string? content) (string/trim content) "") mouse-down-key (if (util/ios?) :on-click - :on-mouse-down ; TODO: it seems that Safari doesn't work well with on-mouse-down - ) + :on-mouse-down) ; TODO: it seems that Safari doesn't work well with on-mouse-down + attrs (cond-> {:blockid (str uuid) :data-type (name block-type) :style {:width "100%" :pointer-events (when stop-events? "none")}} - (not (string/blank? (:hl-color properties))) - (assoc :data-hl-color (:hl-color properties)) + (not (string/blank? (:hl-color properties))) + (assoc :data-hl-color (:hl-color properties)) - (not block-ref?) - (assoc mouse-down-key (fn [e] - (block-content-on-mouse-down e block block-id content edit-input-id))))] + (not block-ref?) + (assoc mouse-down-key (fn [e] + (block-content-on-mouse-down e block block-id content edit-input-id))))] [:div.block-content.inline (cond-> {:id (str "block-content-" uuid) :on-mouse-up (fn [e] @@ -3021,8 +3026,8 @@ :li (cond-> {:checked checked?} - number - (assoc :value number)) + number + (assoc :value number)) (vec-cat [(->elem :p @@ -3040,52 +3045,55 @@ (defn table [config {:keys [header groups col_groups]}] - (let [tr (fn [elm cols] - (->elem - :tr - (mapv (fn [col] - (->elem - elm - {:scope "col" - :class "org-left"} - (map-inline config col))) - cols))) - tb-col-groups (try - (mapv (fn [number] - (let [col-elem [:col {:class "org-left"}]] - (->elem - :colgroup - (repeat number col-elem)))) - col_groups) - (catch :default _e - [])) - head (when header - [:thead (tr :th header)]) - groups (mapv (fn [group] - (->elem - :tbody - (mapv #(tr :td %) group))) - groups)] - [:div.table-wrapper - (->elem - :table - {:class "table-auto" - :border 2 - :cell-spacing 0 - :cell-padding 6 - :rules "groups" - :frame "hsides"} - (vec-cat - tb-col-groups - (cons head groups)))])) + (case (get-shui-component-version :table config) + 2 (shui/table-v2 {:data (concat [[header]] groups)} + (make-shui-context config inline)) + 1 (let [tr (fn [elm cols] + (->elem + :tr + (mapv (fn [col] + (->elem + elm + {:scope "col" + :class "org-left"} + (map-inline config col))) + cols))) + tb-col-groups (try + (mapv (fn [number] + (let [col-elem [:col {:class "org-left"}]] + (->elem + :colgroup + (repeat number col-elem)))) + col_groups) + (catch :default _e + [])) + head (when header + [:thead (tr :th header)]) + groups (mapv (fn [group] + (->elem + :tbody + (mapv #(tr :td %) group))) + groups)] + [:div.table-wrapper + (->elem + :table + {:class "table-auto" + :border 2 + :cell-spacing 0 + :cell-padding 6 + :rules "groups" + :frame "hsides"} + (vec-cat + tb-col-groups + (cons head groups)))]))) (defn logbook-cp [log] (let [clocks (filter #(string/starts-with? % "CLOCK:") log) - clocks (reverse (sort-by str clocks)) + clocks (reverse (sort-by str clocks))] ;; TODO: display states change log ; states (filter #(not (string/starts-with? % "CLOCK:")) log) - ] + (when (seq clocks) (let [tr (fn [elm cols] (->elem :tr (mapv (fn [col] (->elem elm col)) cols))) @@ -3110,6 +3118,8 @@ [config col] (map #(inline config %) col)) +(declare ->hiccup) + (rum/defc src-cp < rum/static [config options html-export?] (when options diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index da835e1d00..33441e031d 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -201,11 +201,14 @@ } .block-control-wrap { - height: 24px; - margin-top: 0; + @apply h-[24px] mt-0 pr-[6px]; &.is-order-list { - @apply relative right-0 mr-0; + @apply mr-0 pr-0; + + .bullet-link-wrap { + @apply relative left-[-3px]; + } } } @@ -531,12 +534,14 @@ } &.as-order-list { - @apply w-[28px] whitespace-nowrap justify-start; + @apply w-[22px] whitespace-nowrap justify-center pl-[3px]; } .bullet { @apply rounded-full w-[6px] h-[6px]; + font-size: 14px; + background-color: var(--ls-block-bullet-color, #394b59); transition: transform 0.2s; diff --git a/src/main/frontend/components/datetime.cljs b/src/main/frontend/components/datetime.cljs index 0c82c95a1a..a27642a6cf 100644 --- a/src/main/frontend/components/datetime.cljs +++ b/src/main/frontend/components/datetime.cljs @@ -46,7 +46,7 @@ (let [show? (rum/react *show-repeater?)] (if (or show? (and num duration kind)) [:div.w.full.flex.flex-row.justify-left - [:input#repeater-num.form-input.mt-1.w-8.px-1.sm:w-20.sm:px-2.text-center + [:input#repeater-num.form-input.w-8.mr-2.px-1.sm:w-20.sm:px-2.text-center {:default-value num :on-change (fn [event] (let [value (util/evalue event)] @@ -66,7 +66,7 @@ (swap! *timestamp assoc-in [:repeater :duration] value)) nil) - [:a.ml-1.self-center {:on-click (fn [] + [:a.ml-2.self-center {:on-click (fn [] (reset! *show-repeater? false) (swap! *timestamp assoc :repeater {}))} svg/close]] diff --git a/src/main/frontend/components/file_sync.cljs b/src/main/frontend/components/file_sync.cljs index a161e0b713..0dfc07b122 100644 --- a/src/main/frontend/components/file_sync.cljs +++ b/src/main/frontend/components/file_sync.cljs @@ -588,7 +588,7 @@ (async/go (set-loading? true) (try - (let [files (async/custom-format title) title)) + (if fmt-journal? + (date/journal-title->custom-format title) + title)) old-name (or title page-name)] [:h1.page-title.flex.cursor-pointer.gap-1.w-full {:class (when-not whiteboard-page? "title") @@ -313,16 +318,17 @@ (when (util/right-click? e) (state/set-state! :page-title/context {:page page-name}))) :on-click (fn [e] - (.preventDefault e) - (if (gobj/get e "shiftKey") - (when-let [page (db/pull repo '[*] [:block/name page-name])] - (state/sidebar-add-block! - repo - (:db/id page) - :page)) - (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?)) - (reset! *input-value (if untitled? "" old-name)) - (reset! *edit? true))))} + (when-not (= (.-nodeName (.-target e)) "INPUT") + (.preventDefault e) + (if (gobj/get e "shiftKey") + (when-let [page (db/pull repo '[*] [:block/name page-name])] + (state/sidebar-add-block! + repo + (:db/id page) + :page)) + (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?)) + (reset! *input-value (if untitled? "" old-name)) + (reset! *edit? true)))))} (when (not= icon "") [:span.page-icon icon]) [:div.page-title-sizer-wrapper.relative (when @*edit? @@ -338,9 +344,13 @@ {:data-value @*input-value :data-ref page-name :style {:opacity (when @*edit? 0)}} - (cond @*edit? [:span {:style {:white-space "pre"}} (rum/react *input-value)] - untitled? [:span.opacity-50 (t :untitled)] - :else title)]]]))) + (let [nested? (and (string/includes? title page-ref/left-brackets) + (string/includes? title page-ref/right-brackets))] + (cond @*edit? [:span {:style {:white-space "pre"}} (rum/react *input-value)] + untitled? [:span.opacity-50 (t :untitled)] + nested? (component-block/map-inline {} (gp-mldoc/inline->edn title (gp-mldoc/default-config + (:block/format page)))) + :else title))]]]))) (defn- page-mouse-over [e *control-show? *all-collapsed?] diff --git a/src/main/frontend/components/page.css b/src/main/frontend/components/page.css index 43ff0d6f57..ad93a8ecd4 100644 --- a/src/main/frontend/components/page.css +++ b/src/main/frontend/components/page.css @@ -249,6 +249,7 @@ box-shadow: none; padding-left: 5px; padding-top: 5px; + padding-bottom: 4px; &-wrapper { @apply rounded; @@ -276,7 +277,7 @@ a.page-title { } > .title { - @apply w-full pointer-events-none overflow-hidden overflow-ellipsis; + @apply w-full overflow-hidden overflow-ellipsis; } .edit-input { diff --git a/src/main/frontend/components/plugins.css b/src/main/frontend/components/plugins.css index 871ac7f303..342e664807 100644 --- a/src/main/frontend/components/plugins.css +++ b/src/main/frontend/components/plugins.css @@ -836,6 +836,16 @@ > .injected-ui-item-pagebar { @apply pr-3 opacity-30 hover:opacity-100 transition-opacity; } + + > .list-wrap { + @apply flex items-center flex-nowrap overflow-x-hidden pt-[14px]; + } + + a.button { + @apply flex items-center; + + color: var(--ls-primary-text-color); + } } .toolbar-plugins-manager { diff --git a/src/main/frontend/components/query.cljs b/src/main/frontend/components/query.cljs index 4404610a45..cdd9e789fc 100644 --- a/src/main/frontend/components/query.cljs +++ b/src/main/frontend/components/query.cljs @@ -50,7 +50,7 @@ view-f result group-by-page?]}] - (let [{:keys [->hiccup ->elem inline-text page-cp map-inline]} config + (let [{:keys [->hiccup ->elem inline-text page-cp map-inline inline]} config *query-error query-error-atom only-blocks? (:block/uuid (first result)) blocks-grouped-by-page? (and group-by-page? @@ -59,6 +59,7 @@ (:block/name (ffirst result)) (:block/uuid (first (second (first result)))) true)] + (println "this should be a function" inline) (if @*query-error (do (log/error :exception @*query-error) @@ -77,10 +78,10 @@ (util/hiccup-keywordize result)) page-list? - (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text) + (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text inline) table? - (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text) + (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text inline) (and (seq result) (or only-blocks? blocks-grouped-by-page?)) (->hiccup result diff --git a/src/main/frontend/components/query_table.cljs b/src/main/frontend/components/query_table.cljs index 9705eae2ac..9283803fd6 100644 --- a/src/main/frontend/components/query_table.cljs +++ b/src/main/frontend/components/query_table.cljs @@ -3,13 +3,15 @@ [frontend.date :as date] [frontend.db :as db] [frontend.db.query-dsl :as query-dsl] + [frontend.format.block :as block] [frontend.handler.common :as common-handler] [frontend.handler.editor.property :as editor-property] + [frontend.shui :refer [get-shui-component-version make-shui-context]] [frontend.state :as state] [frontend.util :as util] [frontend.util.clock :as clock] [frontend.util.property :as property] - [frontend.format.block :as block] + [logseq.shui.core :as shui] [medley.core :as medley] [rum.core :as rum] [logseq.graph-parser.text :as text])) @@ -42,9 +44,9 @@ (defn- locale-compare "Use locale specific comparison for strings and general comparison for others." [x y] - (if (and (number? x) (number? y)) - (< x y) - (.localeCompare (str x) (str y) (state/sub :preferred-language) #js {:numeric true}))) + (if (and (number? x) (number? y)) + (< x y) + (.localeCompare (str x) (str y) (state/sub :preferred-language) #js {:numeric true}))) (defn- sort-result [result {:keys [sort-by-column sort-desc? sort-nlp-date? page?]}] (if (some? sort-by-column) @@ -98,7 +100,7 @@ keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)] keys)) -(defn- get-columns [current-block result {:keys [page?]}] +(defn get-columns [current-block result {:keys [page?]}] (let [query-properties (some-> (get-in current-block [:block/properties :query-properties] "") (common-handler/safe-read-string "Parsing query properties failed")) query-properties (if page? (remove #{:block} query-properties) query-properties) @@ -152,10 +154,21 @@ ;; Fallback to original properties for page blocks (get-in row [:block/properties column])))])) +(defn build-column-text [row column] + (case column + :page (or (get-in row [:block/page :block/original-name]) + (get-in row [:block/original-name]) + (get-in row [:block/content])) + :block (or (get-in row [:block/original-name]) + (get-in row [:block/content])) + (or (get-in row [:block/properties column]) + (get-in row [:block/properties-text-values column]) + (get-in row [(keyword :block column)])))) + (rum/defcs result-table < rum/reactive (rum/local false ::select?) (rum/local false ::mouse-down?) - [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text] + [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text inline] (when current-block (let [select? (get state ::select?) *mouse-down? (::mouse-down? state) @@ -168,53 +181,65 @@ ;; as user needs to know if there result is sorted sort-state (get-sort-state current-block) sort-result (sort-result result (assoc sort-state :page? page?)) - property-separated-by-commas? (partial text/separated-by-commas? (state/get-config))] - [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e)) - :style {:width "100%"} - :class (when-not page? "query-table")} - [:table.table-auto - [:thead - [:tr.cursor - (for [column columns] - (let [title (if (and (= column :clock-time) (integer? clock-time-total)) - (util/format "clock-time(total: %s)" (clock/seconds->days:hours:minutes:seconds - clock-time-total)) - (name column))] - (sortable-title title column sort-state (:block/uuid current-block))))]] - [:tbody - (for [row sort-result] - (let [format (:block/format row)] + property-separated-by-commas? (partial text/separated-by-commas? (state/get-config)) + table-version (get-shui-component-version :table config) + result-as-text (for [row sort-result] + (for [column columns] + (build-column-text row column))) + render-column-value (fn [row-format cell-format value] + (cond + ;; elements should be rendered as they are provided + (= :element cell-format) value + ;; collections are treated as a comma separated list of page-cps + (coll? value) (->> (map #(page-cp {} {:block/name %}) value) + (interpose [:span ", "])) + ;; boolean values need to first be stringified + (boolean? value) (str value) + ;; string values will attempt to be rendered as pages, falling back to + ;; inline-text when no page entity is found + (string? value) (if-let [page (db/entity [:block/name (util/page-name-sanity-lc value)])] + (page-cp {} page) + (inline-text row-format value)) + ;; anything else should just be rendered as provided + :else value))] + + (case table-version + 2 (shui/table-v2 {:data (conj [[columns]] result-as-text)} + (make-shui-context config inline)) + 1 [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e)) + :style {:width "100%"} + :class (when-not page? "query-table")} + [:table.table-auto + [:thead [:tr.cursor (for [column columns] - (let [value (build-column-value row - column - {:page? page? - :->elem ->elem - :map-inline map-inline - :config config - :comma-separated-property? (property-separated-by-commas? column)})] - [:td.whitespace-nowrap {:on-mouse-down (fn [] - (reset! *mouse-down? true) - (reset! select? false)) - :on-mouse-move (fn [] (reset! select? true)) - :on-mouse-up (fn [] - (when (and @*mouse-down? (not @select?)) - (state/sidebar-add-block! - (state/get-current-repo) - (:db/id row) - :block-ref) - (reset! *mouse-down? false)))} - (when value - (if (= :element (first value)) - (second value) - (let [value (second value)] - (if (coll? value) - (let [vals (for [row value] - (page-cp {} {:block/name row}))] - (interpose [:span ", "] vals)) - (cond - (boolean? value) (str value) - (string? value) (if-let [page (db/entity [:block/name (util/page-name-sanity-lc value)])] - (page-cp {} page) - (inline-text format value)) - :else value)))))]))]))]]]))) + (let [title (if (and (= column :clock-time) (integer? clock-time-total)) + (util/format "clock-time(total: %s)" (clock/seconds->days:hours:minutes:seconds + clock-time-total)) + (name column))] + (sortable-title title column sort-state (:block/uuid current-block))))]] + [:tbody + (for [row sort-result] + (let [format (:block/format row)] + [:tr.cursor + (for [column columns] + (let [value (build-column-value row + column + {:page? page? + :->elem ->elem + :map-inline map-inline + :config config + :comma-separated-property? (property-separated-by-commas? column)})] + [:td.whitespace-nowrap {:on-mouse-down (fn [] + (reset! *mouse-down? true) + (reset! select? false)) + :on-mouse-move (fn [] (reset! select? true)) + :on-mouse-up (fn [] + (when (and @*mouse-down? (not @select?)) + (state/sidebar-add-block! + (state/get-current-repo) + (:db/id row) + :block-ref) + (reset! *mouse-down? false)))} + (when value + (apply render-column-value format value))]))]))]]])))) diff --git a/src/main/frontend/components/settings.cljs b/src/main/frontend/components/settings.cljs index c8f2b37b1f..9e59d36a0d 100644 --- a/src/main/frontend/components/settings.cljs +++ b/src/main/frontend/components/settings.cljs @@ -668,14 +668,20 @@ [] [:div.panel-wrap [:div.text-sm.my-4 + (ui/admonition + :tip + [:p "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."]) [:span.text-sm.opacity-50.my-4 - "You can view a page's edit history by clicking the three horizontal dots " - "in the top-right corner and selecting \"View page history\". " - "Logseq uses "] + "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."] + [:br][:br] + [:span.text-sm.opacity-50.my-4 + "For professional users, Logseq also supports using "] [:a {:href "https://git-scm.com/" :target "_blank"} "Git"] [:span.text-sm.opacity-50.my-4 - " for version control."]] + " for version control."] + [:span.text-sm.opacity-50.my-4 + "Use Git at your own risk as general Git issues are not supported by the Logseq team"]] [:br] (switch-git-auto-commit-row t) (git-auto-commit-seconds t) @@ -878,9 +884,7 @@ [[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")] [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")] - (when (and - (util/electron?) - (not (file-sync-handler/synced-file-graph? current-repo))) + (when (util/electron?) [:git "git" (t :settings-page/tab-version-control) (ui/icon "history")]) ;; (when (util/electron?) @@ -890,7 +894,7 @@ [:ai "ai" "AI" (ui/icon "wand")] - [:features "features" (t :settings-page/tab-features) (ui/icon "square-asterisk")] + [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature")] (when plugins-of-settings [:plugins-setting "plugins" (t :settings-of-plugins) (ui/icon "puzzle")])]] diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index e5eae575ee..c2c755ec88 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -770,7 +770,11 @@ independent of format as format specific heading characters are stripped" :include-start? true :scoped-block-id scoped-block-id})) - (contains? #{:save-block :delete-blocks} outliner-op) + (and (= :delete-blocks outliner-op) + (<= (count @result) initial-blocks-length)) ; load more blocks + nil + + (= :save-block outliner-op) @result (contains? #{:insert-blocks :collapse-expand-blocks :move-blocks} outliner-op) @@ -845,6 +849,7 @@ independent of format as format specific heading characters are stripped" (db-utils/pull repo-url pull-keys id))) block-eids) (db-utils/pull-many repo-url pull-keys block-eids)) blocks (remove (fn [b] (nil? (:block/content b))) blocks)] + (map (fn [b] (assoc b :block/page bare-page-map)) blocks)))} nil) react))))) @@ -957,15 +962,8 @@ independent of format as format specific heading characters are stripped" "Doesn't include nested children." [repo block-uuid] (when-let [db (conn/get-db repo)] - (-> (d/q - '[:find [(pull ?b [*]) ...] - :in $ ?parent-id - :where - [?parent :block/uuid ?parent-id] - [?b :block/parent ?parent]] - db - block-uuid) - (sort-by-left (db-utils/entity [:block/uuid block-uuid]))))) + (when-let [parent (db-utils/entity repo [:block/uuid block-uuid])] + (sort-by-left (:block/_parent parent) parent)))) (defn get-block-children "Including nested children." diff --git a/src/main/frontend/db/react.cljs b/src/main/frontend/db/react.cljs index 0af0431e5b..e8115dbaf5 100644 --- a/src/main/frontend/db/react.cljs +++ b/src/main/frontend/db/react.cljs @@ -16,7 +16,7 @@ ;;; keywords specs for reactive query, used by `react/q` calls ;; ::block ;; pull-block react-query -(s/def ::block (s/tuple #(= ::block %) uuid?)) +(s/def ::block (s/tuple #(= ::block %) int?)) ;; ::page-blocks ;; get page-blocks react-query (s/def ::page-blocks (s/tuple #(= ::page-blocks %) int?)) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index 891c0138aa..0cc8a3a4be 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -372,6 +372,8 @@ :file-sync/other-user-graph "Current local graph is bound to other user's remote graph. So can't start syncing." :file-sync/graph-deleted "The current remote graph has been deleted" + :file-sync/rsapi-cannot-upload-err "Unable to start synchronization, please check if the local time is correct." + :notification/clear-all "Clear all"} @@ -1684,6 +1686,7 @@ :file-sync/other-user-graph "当前本地图谱绑定在其他用户的远程图谱上。因此无法启动同步。" :file-sync/graph-deleted "当前远程图谱已经删除" + :file-sync/rsapi-cannot-upload-err "无法同步,请检查本机时间是否准确" :notification/clear-all "清除全部通知"} @@ -2728,7 +2731,116 @@ :asset/maximize "Maksimer bilde" :asset/open-in-browser "Åpne bilde i nettleser" :asset/show-in-folder "Vis bilde i mappe" - :linked-references/filter-search "Søk i lenkede referanser"} + :linked-references/filter-search "Søk i lenkede referanser" + :all-whiteboards "Alle whiteboard" + :auto-heading "Automatisk overskrift" + :heading "Overskrift {1}" + :new-whiteboard "Nytt whiteboard" + :remove-heading "Fjern overskrift" + :untitled "Uten navn" + :accessibility/skip-to-main-content "Hopp til hovedinnhold" + :color/blue "Blå" + :color/gray "Grå" + :color/green "Grønn" + :color/pink "Rosa" + :color/purple "Lilla" + :color/red "Rød" + :color/yellow "Gul" + :content/copy-block-url "Kopier blokk URL" + :content/copy-export-as "Kopier / Eksporter som.." + :content/copy-ref "Kopier denne referansen" + :content/delete-ref "Slett denne referansen" + :content/replace-with-embed "Erstatt med innebygging" + :content/replace-with-text "Erstatt med tekst" + :context-menu/input-template-name "Hva heter malen?" + :context-menu/make-a-flashcard "Lag et Flashcard" + :context-menu/make-a-template "Lag en Mal" + :context-menu/preview-flashcard "Forhåndsvis Flashcard" + :context-menu/template-exists-warning "Malen eksisterer allerde!" + :context-menu/template-include-parent-block "Inkluder overordnet blokk i malen?" + :context-menu/toggle-number-list "Veksle nummerliste" + :dev/show-block-ast "(Dev) Vis blokk AST" + :dev/show-block-data "(Dev) Vis blokk data" + :dev/show-page-ast "(Dev) Vis side AST" + :dev/show-page-data "(Dev) Vis side data" + :editor/collapse-block-children "Skjul alle" + :editor/cycle-todo "Roterer TODO statusen for gjeldende element" + :editor/delete-selection "Slett valgte blokker" + :editor/expand-block-children "Utvid alle" + :file/validate-existing-file-error "Siden eksisterer allerede i en annen fil: {1}, nåværen..." + :file-rn/all-action "Utfør alle Handlinger!" + :file-rn/apply-rename "Utfør omdøping av filen" + :file-rn/close-panel "Lukk Panel" + :file-rn/confirm-proceed "Oppdater format!" + :file-rn/filename-desc-1 "Denne innstillingen konfigurerer hvordan en side blir lagret til en ..." + :file-rn/filename-desc-2 "Noen tegn som \"/\" eller \"?\" er ikke gyldige for en..." + :file-rn/filename-desc-3 "Logseq erstatter ugyldige tegn med deres URL ..." + :file-rn/filename-desc-4 "Skilletegnet for navnerom \"/\" brukes også av \"_..." + :file-rn/format-deprecated "Du bruker for øyeblikket et utdatert format. Oppdat..." + :file-rn/instruct-1 "Det er en to-trinns prosess å oppdatere formatet for filnavn:" + :file-rn/instruct-2 "1. Klikk " + :file-rn/instruct-3 "2. Følg instruksjonene under for å gi filen et nytt navn..." + :file-rn/legend "🟢 Valgfri omdøping; 🟡 Omdøping kreves..." + :file-rn/need-action "Omdøping av fil er anbefalt for å matche de nye..." + :file-rn/no-action "Bra jobba! Well done! Ingen ytterligere tiltak kreves." + :file-rn/optional-rename "Forslag: " + :file-rn/or-select-actions " eller gi filer nytt navn individuelt under, så " + :file-rn/or-select-actions-2 ". Disse handlingene er ikke tilgjengelige når du lukker ..." + :file-rn/otherwise-breaking "Eller tittelen vil bli" + :file-rn/re-index "Re-indksering er sterkt anbefalt etter at filene er..." + :file-rn/rename "Omdøp fil \"{1}\" til \"{2}\"" + :file-rn/select-confirm-proceed "Dev: skriv format" + :file-rn/select-format "(Uviklermodus Operasjon, Farlig!) Velg filenav..." + :file-rn/suggest-rename "Handling kreves: " + :file-rn/unreachable-title "Advarsel! Navnet på siden vil bli {1} under nåvære.." + :left-side-bar/create "Opprett" + :left-side-bar/new-whiteboard "Nytt whiteboard" + :notification/clear-all "Fjern alt" + :on-boarding/tour-whiteboard-home "{1} Hjem for dine whiteboards" + :on-boarding/tour-whiteboard-home-description "Whiteboards har sin egen seksjon i appen hvo..." + :on-boarding/tour-whiteboard-new "{1} Lag nytt whiteboard" + :on-boarding/tour-whiteboard-new-description "Det er mange måter å lage et nytt whiteboard på..." + :on-boarding/welcome-whiteboard-modal-description "Whiteboards er et fantastisk verktøy for brainstorming og ..." + :on-boarding/welcome-whiteboard-modal-skip "Hopp over" + :on-boarding/welcome-whiteboard-modal-start "Start med whiteboard" + :on-boarding/welcome-whiteboard-modal-title "Et nytt lerret for dine tanker." + :page/logseq-is-having-a-problem "Logseq har et problem. Prøver å få den tilbake ..." + :page/show-whiteboards "Vis whiteboards" + :page/something-went-wrong "Noe gikk galt" + :page/step "Steg {1}" + :page/try "Prøv" + :pdf/doc-metadata "Dokument metadata" + :pdf/hl-block-colored "Farget merkelapp for å label for utheve blokk" + :plugin/found-n-updates "Fant {1} oppdatering" + :plugin/found-updates "Nye oppateringer" + :plugin/update-all-selected "Oppdater alle valgte" + :plugin/updates-downloading "Laster ned oppdateringer" + :plugin.install-from-file/menu-title "Installer fra plugins.edn" + :plugin.install-from-file/notice "Følgende plugins vil erstatte dine plugins:" + :plugin.install-from-file/success "Alle plugins er installert!" + :plugin.install-from-file/title "Installer plugins fra plugins.edn" + :right-side-bar/history "(Dev) Angre/Gjør om logg" + :right-side-bar/whiteboards "Whiteboards" + :search/items "elementer" + :search-item/block "Blokk" + :search-item/file "Fil" + :search-item/page "Side" + :search-item/whiteboard "Whiteboard" + :select/default-select-multiple "Velg en eller flere" + :settings-page/alpha-features "Alpha funksjoner" + :settings-page/auto-expand-block-refs "Utvid blokkreferanser automatisk når zoomet inn..." + :settings-page/beta-features "Beta funksjoner" + :settings-page/clear-cache-warning "Tømming av hurtigbufferen vil forkaste dine åpne grafer. Du m..." + :settings-page/custom-date-format-warning "Re-indeksering kreves! Eksisterernde dagbokreferanse vi..." + :settings-page/disable-sentry-desc "Logseq vil aldri samle inn dine lokale graf sin databas..." + :settings-page/edit-setting "Rediger" + :settings-page/enable-whiteboards "Whiteboards" + :settings-page/filename-format "Filnavn format" + :settings-page/login-prompt "For å få tilgang til nye funksjoner før alle andre må du..." + :settings-page/preferred-pasting-file "Foretrekk innliming av fil" + :settings-page/show-full-blocks "Vis alle linjer av en blokkreferanse" + :settings-page/tab-assets "Ressurser" + :whiteboard/link-whiteboard-or-block "Lenk whiteboard/side/blokk"} :pt-BR {:on-boarding/demo-graph "Esse é um grafo de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta." :on-boarding/add-graph "Adicionar grafo" @@ -2811,6 +2923,7 @@ :settings-page/spell-checker "Verificador ortográfico" :settings-page/disable-sentry "Enviar dados de utilização e diagnósticos para Logseq" :settings-page/preferred-outdenting "Ativar dedentação lógica" + :settings-page/auto-expand-block-refs "Expandir as referências de bloco automaticamente ao aumentar o zoom" :settings-page/custom-date-format "Formato de data preferido" :settings-page/preferred-file-format "Formato de Arquivo preferido" :settings-page/preferred-workflow "Fluxo de trabalho preferido" @@ -3138,6 +3251,10 @@ :left-side-bar/new-whiteboard "Novo quadro branco" :left-side-bar/nav-favorites "Favoritos" :left-side-bar/nav-recent-pages "Recente" + :page/something-went-wrong "Algo deu errado" + :page/logseq-is-having-a-problem "Logseq está tendo um problema. Para tentar colocá-lo de volta em um estado de funcionamento, por favor tente os seguintes passos seguros em ordem:" + :page/step "Passo {1}" + :page/try "Tentar" :page/presentation-mode "Modo de apresentação" :page/delete-confirmation "Tem a certeza de que quer apagar esta página e o respetivo ficheiro?" :page/open-in-finder "Abrir em pasta" @@ -3206,8 +3323,12 @@ :color/pink "Rosa" :editor/copy "Copiar" :editor/cut "Cortar" + :content/copy-export-as "Copiar / Exportar como.." + :content/copy-block-url "Copiar URL do bloco" :content/copy-block-ref "Copiar referência do bloco" :content/copy-block-emebed "Copiar bloco para incorporar" + :content/copy-ref "Copiar esta referência" + :content/delete-ref "Apagar esta referência" :content/open-in-sidebar "Abrir na barra lateral" :content/click-to-edit "Clicar para editar" :settings-page/git-confirm "É necessário reiniciar a aplicação após atualizar as definições do Git." @@ -3391,12 +3512,14 @@ :command-palette/prompt "Introduza um comando" :select/default-prompt "Selecione um" + :select/default-select-multiple "Selecione um ou vários" :select.graph/prompt "Selecione um grafo" :select.graph/empty-placeholder-description "Sem grafos correspondentes. Quer adicionar outro?" :select.graph/add-graph "Sim, adicionar outro grafo" :file-sync/other-user-graph "O grafo local atual está ligado ao grafo remoto de outro utilizador. Portanto, a sincronização não pode ser iniciada." :file-sync/graph-deleted "O grafo remoto atual foi apagado" + :file-sync/rsapi-cannot-upload-err "Não foi possível iniciar a sincronização, verifique se a hora local está correta." :notification/clear-all "Limpar tudo"} @@ -4410,6 +4533,7 @@ :content/open-in-sidebar "Kenar çubuğunda aç" :content/click-to-edit "Düzenlemek için tıklayın" :context-menu/make-a-flashcard "Bilgi Kartı Oluştur" + :context-menu/toggle-number-list "Numaralı liste olarak değiştir" :context-menu/preview-flashcard "Bilgi Kartını Önizle" :context-menu/make-a-template "Bir Şablon Oluştur" :context-menu/input-template-name "Şablonun adı nedir?" diff --git a/src/main/frontend/format/block.cljs b/src/main/frontend/format/block.cljs index ca341b0e17..2c4e4a4230 100644 --- a/src/main/frontend/format/block.cljs +++ b/src/main/frontend/format/block.cljs @@ -9,7 +9,6 @@ [frontend.handler.notification :as notification] [frontend.state :as state] [logseq.graph-parser.block :as gp-block] - [logseq.graph-parser.config :as gp-config] [logseq.graph-parser.property :as gp-property] [logseq.graph-parser.mldoc :as gp-mldoc] [lambdaisland.glogi :as log])) @@ -23,7 +22,6 @@ and handles unexpected failure." (gp-block/extract-blocks blocks content with-id? format {:user-config (state/get-config) :block-pattern (config/get-block-pattern format) - :supported-formats (gp-config/supported-formats) :db (db/get-db (state/get-current-repo)) :date-formatter (state/get-date-formatter) :page-name page-name}) diff --git a/src/main/frontend/fs/capacitor_fs.cljs b/src/main/frontend/fs/capacitor_fs.cljs index 8b87e0954c..9f1a42c0b3 100644 --- a/src/main/frontend/fs/capacitor_fs.cljs +++ b/src/main/frontend/fs/capacitor_fs.cljs @@ -238,8 +238,7 @@ (when-not contents-matched? (backup-file repo-dir :backup-dir fpath disk-content)) (db/set-file-last-modified-at! repo rpath mtime) - (p/let [content content] - (db/set-file-content! repo rpath content)) + (db/set-file-content! repo rpath content) (when ok-handler (ok-handler repo fpath result)) result) diff --git a/src/main/frontend/fs/sync.cljs b/src/main/frontend/fs/sync.cljs index 3cd60b98a4..c332f4a576 100644 --- a/src/main/frontend/fs/sync.cljs +++ b/src/main/frontend/fs/sync.cljs @@ -1680,6 +1680,10 @@ [r] (some->> (ex-cause r) str (re-find #"graph-not-exist"))) +(defn- stop-sync-by-rsapi-response? + [r] + (some->> (ex-cause r) str (re-find #"Request is not yet valid"))) + ;; type = "change" | "add" | "unlink" (deftype FileChangeEvent [type dir path stat checksum] @@ -2594,10 +2598,15 @@ (do (println :graph-has-been-deleted r*) {:graph-has-been-deleted true}) + (stop-sync-by-rsapi-response? r*) + (do (println :stop-sync-caused-by-rsapi-err-response r*) + (notification/show! (t :file-sync/rsapi-cannot-upload-err) :warning false) + {:stop true}) + paused? {:pause true} - succ? ; succ + succ? ; succ (do (println "sync-local->remote! update txid" r*) ;; persist txid diff --git a/src/main/frontend/handler/block.cljs b/src/main/frontend/handler/block.cljs index 1a7e673c29..5a475b376f 100644 --- a/src/main/frontend/handler/block.cljs +++ b/src/main/frontend/handler/block.cljs @@ -6,7 +6,6 @@ [frontend.db :as db] [frontend.db.model :as db-model] [frontend.db.react :as react] - [frontend.db.utils :as db-utils] [frontend.mobile.haptics :as haptics] [frontend.modules.outliner.core :as outliner-core] [frontend.modules.outliner.transaction :as outliner-tx] @@ -70,14 +69,9 @@ (util/distinct-by :db/id)))))) (defn indentable? - [{:block/keys [parent] :as block}] + [{:block/keys [parent left]}] (when parent - (let [parent-block (db-utils/pull (:db/id parent)) - first-child (first - (db-model/get-block-immediate-children - (state/get-current-repo) - (:block/uuid parent-block)))] - (not= (:db/id block) (:db/id first-child))))) + (not= parent left))) (defn outdentable? [{:block/keys [level] :as _block}] diff --git a/src/main/frontend/handler/common/file.cljs b/src/main/frontend/handler/common/file.cljs index c374ab4ba7..337ee3281b 100644 --- a/src/main/frontend/handler/common/file.cljs +++ b/src/main/frontend/handler/common/file.cljs @@ -5,7 +5,6 @@ [frontend.db :as db] [logseq.graph-parser :as graph-parser] [logseq.graph-parser.util :as gp-util] - [logseq.graph-parser.config :as gp-config] [frontend.fs.diff-merge :as diff-merge] [frontend.fs :as fs] [frontend.context.i18n :refer [t]] @@ -75,13 +74,13 @@ :fs/reset-event - the event that triggered the file update :fs/local-file-change - file changed on local disk :fs/remote-file-change - file changed on remote" - [repo-url file content {:fs/keys [event] :as options}] + [repo-url file-path content {:fs/keys [event] :as options}] (let [db-conn (db/get-db repo-url false)] (case event ;; the file is already in db, so we can use the existing file's blocks ;; to do the diff-merge :fs/local-file-change - (graph-parser/parse-file db-conn file content (assoc-in options [:extract-options :resolve-uuid-fn] diff-merge-uuids-2ways)) + (graph-parser/parse-file db-conn file-path content (assoc-in options [:extract-options :resolve-uuid-fn] diff-merge-uuids-2ways)) ;; TODO Junyi: 3 ways to handle remote file change ;; The file is on remote, so we should have @@ -91,7 +90,7 @@ ;; 2. a "remote version" just fetched from remote ;; default to parse the file - (graph-parser/parse-file db-conn file content options)))) + (graph-parser/parse-file db-conn file-path content options)))) (defn reset-file! "Main fn for updating a db with the results of a parsed file" @@ -107,7 +106,6 @@ {:user-config (state/get-config) :date-formatter (state/get-date-formatter) :block-pattern (config/get-block-pattern (gp-util/get-format file-path)) - :supported-formats (gp-config/supported-formats) :filename-format (state/get-filename-format repo-url)} ;; To avoid skipping the `:or` bounds for keyword destructuring (when (some? extracted-block-ids) {:extracted-block-ids extracted-block-ids}) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 985b3288aa..0b8851c249 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -426,7 +426,7 @@ (not has-children?))] (outliner-tx/transact! {:outliner-op :insert-blocks} - (save-current-block! {:current-block current-block}) + (save-current-block! {:current-block current-block}) (outliner-core/insert-blocks! [new-block] current-block {:sibling? sibling? :keep-uuid? keep-uuid? :replace-empty-target? replace-empty-target?})))) @@ -753,30 +753,28 @@ (outliner-core/delete-blocks! [block] {:children? children?}))))) (defn- move-to-prev-block - ([repo sibling-block format id value] - (move-to-prev-block repo sibling-block format id value true)) - ([repo sibling-block format id value edit?] - (when (and repo sibling-block) - (when-let [sibling-block-id (dom/attr sibling-block "blockid")] - (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])] - (let [original-content (util/trim-safe (:block/content block)) - value' (-> (property/remove-built-in-properties format original-content) - (drawer/remove-logbook)) - new-value (str value' value) - tail-len (count value) - pos (max - (if original-content - (gobj/get (utf8/encode original-content) "length") - 0) - 0)] - (when edit? - (edit-block! block pos id - {:custom-content new-value - :tail-len tail-len - :move-cursor? false})) - {:prev-block block - :new-content new-value - :pos pos})))))) + [repo sibling-block format id value move?] + (when (and repo sibling-block) + (when-let [sibling-block-id (dom/attr sibling-block "blockid")] + (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])] + (let [original-content (util/trim-safe (:block/content block)) + value' (-> (property/remove-built-in-properties format original-content) + (drawer/remove-logbook)) + new-value (str value' value) + tail-len (count value) + pos (max + (if original-content + (gobj/get (utf8/encode original-content) "length") + 0) + 0) + f (fn [] (edit-block! block pos id + {:custom-content new-value + :tail-len tail-len + :move-cursor? false}))] + (when move? (f)) + {:prev-block block + :new-content new-value + :move-fn f}))))) (declare save-block!) @@ -802,7 +800,7 @@ (when block-parent-id (let [block-parent (gdom/getElement block-parent-id) sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent) - {:keys [prev-block new-content]} (move-to-prev-block repo sibling-block format id value) + {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false) concat-prev-block? (boolean (and prev-block new-content)) transact-opts (cond-> {:outliner-op :delete-block} @@ -812,7 +810,8 @@ (outliner-tx/transact! transact-opts (when concat-prev-block? (save-block! repo prev-block new-content)) - (delete-block-aux! block delete-children?)))))))))) + (delete-block-aux! block delete-children?)) + (move-fn))))))))) (state/set-editor-op! nil))) (defn delete-blocks! @@ -829,7 +828,8 @@ (move-to-prev-block repo sibling-block (:block/format block) (dom/attr sibling-block "id") - ""))))) + "" + true))))) (defn- set-block-property-aux! [block-or-id key value] @@ -1231,10 +1231,17 @@ (defn save-block! ([repo block-or-uuid content] + (save-block! repo block-or-uuid content {})) + ([repo block-or-uuid content {:keys [properties] :or {}}] (let [block (if (or (uuid? block-or-uuid) (string? block-or-uuid)) (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)] - (save-block! {:block block :repo repo} content))) + (save-block! + {:block block :repo repo} + (if (seq properties) + (property/insert-properties (:block/format block) content properties) + content) + ))) ([{:keys [block repo] :as _state} value] (let [repo (or repo (state/get-current-repo))] (when (db/entity repo [:block/uuid (:block/uuid block)]) @@ -1244,8 +1251,8 @@ [blocks] (outliner-tx/transact! {:outliner-op :save-block} - (doseq [[block value] blocks] - (save-block-if-changed! block value)))) + (doseq [[block value] blocks] + (save-block-if-changed! block value)))) (defn save-current-block! "skip-properties? if set true, when editing block is likely be properties, skip saving" @@ -1832,8 +1839,9 @@ (and (= content "1. ") (= last-input-char " ") input-id edit-block (not (own-order-number-list? edit-block))) (do - (state/pub-event! [:editor/toggle-own-number-list edit-block]) - (state/set-edit-content! input-id "")) + (state/set-edit-content! input-id "") + (-> (p/delay 10) + (p/then #(state/pub-event! [:editor/toggle-own-number-list edit-block])))) (and (= last-input-char (state/get-editor-command-trigger)) (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos))) @@ -1964,7 +1972,7 @@ :or {exclude-properties [] edit? true}}] (let [editing-block (when-let [editing-block (state/get-edit-block)] - (some-> (db/pull (:db/id editing-block)) + (some-> (db/pull [:block/uuid (:block/uuid editing-block)]) (assoc :block/content (state/get-edit-content)))) has-unsaved-edits (and editing-block (not= (:block/content (db/pull (:db/id editing-block))) @@ -2455,8 +2463,9 @@ :else (profile "Insert block" - (do (save-current-block!) - (insert-new-block! state))))))))) + (outliner-tx/transact! {:outliner-op :insert-blocks} + (save-current-block!) + (insert-new-block! state))))))))) (defn- inside-of-single-block "When we are in a single block wrapper, we should always insert a new line instead of new block" @@ -2610,15 +2619,18 @@ ^js input (state/get-input) current-pos (cursor/pos input) value (gobj/get input "value") - right (outliner-core/get-right-node (outliner-core/block current-block)) + right (outliner-core/get-right-sibling (:db/id current-block)) current-block-has-children? (db/has-children? (:block/uuid current-block)) collapsed? (util/collapsed? current-block) first-child (:data (tree/-get-down (outliner-core/block current-block))) next-block (if (or collapsed? (not current-block-has-children?)) - (:data right) + (when right (db/pull (:db/id right))) first-child)] (cond - (and collapsed? right (db/has-children? (tree/-get-id right))) + (nil? next-block) + nil + + (and collapsed? right (db/has-children? (:block/uuid right))) nil (and (not collapsed?) first-child (db/has-children? (:block/uuid first-child))) @@ -2760,7 +2772,7 @@ (outliner-tx/transact! {:outliner-op :move-blocks :real-outliner-op :indent-outdent} - (outliner-core/indent-outdent-blocks! [block] indent?))) + (outliner-core/indent-outdent-blocks! [block] indent?))) (state/set-editor-op! :nil))) (defn keydown-tab-handler @@ -3148,7 +3160,7 @@ (state/selection?) (shortcut-delete-selection e) - (whiteboard?) + (and (whiteboard?) (not (state/editing?))) (.deleteShapes (.-api ^js (state/active-tldraw-app))) :else @@ -3203,9 +3215,9 @@ ;; if the move is to cross block boundary, select the whole block (or (and (= direction :up) (cursor/textarea-cursor-rect-first-row? cursor-rect)) (and (= direction :down) (cursor/textarea-cursor-rect-last-row? cursor-rect))) - (select-block-up-down direction) + (select-block-up-down direction) ;; simulate text selection - (cursor/select-up-down input direction anchor cursor-rect))) + (cursor/select-up-down input direction anchor cursor-rect))) (select-block-up-down direction)))) (defn open-selected-block! diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 5e0b56f723..3d89470a79 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -70,8 +70,7 @@ [logseq.db.schema :as db-schema] [logseq.graph-parser.config :as gp-config] [promesa.core :as p] - [rum.core :as rum] - [logseq.common.path :as path])) + [rum.core :as rum])) ;; TODO: should we move all events here? @@ -609,10 +608,9 @@ (plugin/open-waiting-updates-modal!)) (plugin-handler/set-auto-checking! false)))))) -(defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data tx-meta] :as payload}]] +(defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data] :as payload}]] (when-let [payload (and (seq blocks) - (merge payload {:tx-data (map #(into [] %) tx-data) - :tx-meta (dissoc tx-meta :editor-cursor)}))] + (merge payload {:tx-data (map #(into [] %) tx-data)}))] (plugin-handler/hook-plugin-db :changed payload) (plugin-handler/hook-plugin-block-changes payload))) @@ -624,13 +622,7 @@ (defmethod handle :mobile-file-watcher/changed [[_ ^js event]] (let [type (.-event event) - payload (js->clj event :keywordize-keys true) - dir (:dir payload) - payload (-> payload - (update :path - (fn [path] - (when (string? path) - (path/relative-path dir path)))))] + payload (js->clj event :keywordize-keys true)] (fs-watcher/handle-changed! type payload) (when (file-sync-handler/enable-sync?) (sync/file-watch-handler type payload)))) diff --git a/src/main/frontend/handler/export/text.cljs b/src/main/frontend/handler/export/text.cljs index 28fb94136d..a3fa16eb9e 100644 --- a/src/main/frontend/handler/export/text.cljs +++ b/src/main/frontend/handler/export/text.cljs @@ -164,33 +164,32 @@ (defn- block-table [{:keys [header groups]}] - (when (seq header) - (let [level (dec (get *state* :current-level 1)) - sep-line (raw-text "|" (string/join "|" (repeat (count header) "---")) "|") - header-line - (concatv (mapcatv - (fn [h] (concatv [space (raw-text "|") space] (mapcatv inline-ast->simple-ast h))) - header) - [space (raw-text "|")]) - group-lines - (mapcatv - (fn [group] - (mapcatv - (fn [row] - (concatv [(indent-with-2-spaces level)] - (mapcatv - (fn [col] - (concatv [(raw-text "|") space] - (mapcatv inline-ast->simple-ast col) - [space])) - row) - [(raw-text "|") (newline* 1)])) - group)) - groups)] - (concatv [(newline* 1) (indent-with-2-spaces level)] - header-line - [(newline* 1) (indent-with-2-spaces level) sep-line (newline* 1)] - group-lines)))) + (let [level (dec (get *state* :current-level 1)) + sep-line (raw-text "|" (string/join "|" (repeat (count header) "---")) "|") + header-line + (concatv (mapcatv + (fn [h] (concatv [space (raw-text "|") space] (mapcatv inline-ast->simple-ast h))) + header) + [space (raw-text "|")]) + group-lines + (mapcatv + (fn [group] + (mapcatv + (fn [row] + (concatv [(indent-with-2-spaces level)] + (mapcatv + (fn [col] + (concatv [(raw-text "|") space] + (mapcatv inline-ast->simple-ast col) + [space])) + row) + [(raw-text "|") (newline* 1)])) + group)) + groups)] + (concatv [(newline* 1) (indent-with-2-spaces level)] + (when (seq header) header-line) + (when (seq header) [(newline* 1) (indent-with-2-spaces level) sep-line (newline* 1)]) + group-lines))) (defn- block-comment [s] diff --git a/src/main/frontend/handler/file_sync.cljs b/src/main/frontend/handler/file_sync.cljs index 17428b7733..2f775caec8 100644 --- a/src/main/frontend/handler/file_sync.cljs +++ b/src/main/frontend/handler/file_sync.cljs @@ -16,7 +16,6 @@ [cljs-time.coerce :as tc] [cljs-time.core :as t] [frontend.storage :as storage] - [logseq.graph-parser.util :as gp-util] [lambdaisland.glogi :as log])) (def *beta-unavailable? (volatile! false)) @@ -163,24 +162,19 @@ version-file-paths) (remove nil?)))))))) -(defn fetch-page-file-versions [graph-uuid page] +(defn > (concat version-list local-version-list) - (sort-by #(or (:CreateTime %) - (:create-time %)) - >))] - all-version-list)))))) + (go + (when-let [path (:file/path (db/entity file-id))] + (let [version-list (:VersionList + (> (concat version-list local-version-list) + (sort-by #(or (:CreateTime %) + (:create-time %)) + >))] + all-version-list))))) (defn init-remote-graph diff --git a/src/main/frontend/handler/paste.cljs b/src/main/frontend/handler/paste.cljs index dbe8692eb6..8b91176113 100644 --- a/src/main/frontend/handler/paste.cljs +++ b/src/main/frontend/handler/paste.cljs @@ -87,7 +87,9 @@ ;; See https://developer.chrome.com/blog/web-custom-formats-for-the-async-clipboard-api/ ;; for a similar example (defn get-copied-blocks [] - (p/let [clipboard-items (when (and js/window (gobj/get js/window "navigator") js/navigator.clipboard) + ;; NOTE: Avoid using navigator clipboard API on Android, it will report a permission error + (p/let [clipboard-items (when (and (not (mobile-util/native-android?)) + js/window (gobj/get js/window "navigator") js/navigator.clipboard) (js/navigator.clipboard.read)) blocks-blob ^js (when clipboard-items (let [types (.-types ^js (first clipboard-items))] diff --git a/src/main/frontend/handler/whiteboard.cljs b/src/main/frontend/handler/whiteboard.cljs index 7e75ed6409..3cbe53a707 100644 --- a/src/main/frontend/handler/whiteboard.cljs +++ b/src/main/frontend/handler/whiteboard.cljs @@ -89,11 +89,12 @@ (let [assets (js->clj-keywordize (.getCleanUpAssets app)) new-shapes (.-shapes tl-page) shapes-index (map #(gobj/get % "id") new-shapes) + shape-id->index (zipmap shapes-index (range (.-length new-shapes))) upsert-shapes (->> (set/difference new-id-nonces db-id-nonces) (map (fn [{:keys [id]}] (-> (.-serialized ^js (.getShapeById tl-page id)) js->clj-keywordize - (assoc :index (.indexOf shapes-index id))))) + (assoc :index (get shape-id->index id))))) (set)) old-ids (set (map :id db-id-nonces)) new-ids (set (map :id new-id-nonces)) @@ -134,13 +135,15 @@ (defn transact-tldr-delta! [page-name ^js app replace?] (let [tl-page ^js (second (first (.-pages app))) shapes (.-shapes ^js tl-page) - shapes-index (map #(gobj/get % "id") shapes) - new-id-nonces (set (map (fn [shape] + page-block (model/get-page page-name) + prev-shapes-index (get-in page-block [:block/properties :logseq.tldraw.page :shapes-index]) + shape-id->prev-index (zipmap prev-shapes-index (range (count prev-shapes-index))) + new-id-nonces (set (map-indexed (fn [idx shape] (let [id (.-id shape)] - {:id id - :nonce (if (= shape.id (.indexOf shapes-index id)) - (.-nonce shape) - (.getTime (js/Date.)))})) shapes)) + {:id id + :nonce (if (= idx (get shape-id->prev-index id)) + (.-nonce shape) + (js/Date.now))})) shapes)) repo (state/get-current-repo) db-id-nonces (or (get-in @*last-shapes-nonce [repo page-name]) diff --git a/src/main/frontend/modules/editor/undo_redo.cljs b/src/main/frontend/modules/editor/undo_redo.cljs index f42729091e..d0c4512e24 100644 --- a/src/main/frontend/modules/editor/undo_redo.cljs +++ b/src/main/frontend/modules/editor/undo_redo.cljs @@ -44,7 +44,7 @@ [txs] (filterv (fn [[_ a & y]] (= :block/content a)) - txs)) + txs)) (defn get-content-from-stack "For test." @@ -60,22 +60,21 @@ (when-let [stack @undo-stack] (when (seq stack) (let [removed-e (peek stack) - popped-stack (pop stack) - prev-e (peek popped-stack)] + popped-stack (pop stack)] (reset! undo-stack popped-stack) - [removed-e prev-e]))))) + removed-e))))) (defn push-redo [txs] (let [redo-stack (get-redo-stack)] - (swap! redo-stack conj txs))) + (swap! redo-stack conj txs))) (defn pop-redo [] (let [redo-stack (get-redo-stack)] - (when-let [removed-e (peek @redo-stack)] - (swap! redo-stack pop) - removed-e))) + (when-let [removed-e (peek @redo-stack)] + (swap! redo-stack pop) + removed-e))) (defn page-pop-redo [page-id] @@ -119,7 +118,7 @@ (and redo? (not add?)) :db/retract (and (not redo?) (not add?)) :db/add)] [op id attr value tx])) - txs))) + txs))) ;;;; Invokes @@ -128,7 +127,7 @@ (let [conn (conn/get-db false)] (d/transact! conn txs tx-meta))) -(defn page-pop-undo +(defn- page-pop-undo [page-id] (let [undo-stack (get-undo-stack)] (when-let [stack @undo-stack] @@ -144,7 +143,7 @@ others (vec (concat before after))] (reset! undo-stack others) (prn "[debug] undo remove: " (nth stack idx')) - [(nth stack idx') others]))))))) + (nth stack idx')))))))) (defn- smart-pop-undo [] @@ -154,56 +153,77 @@ (pop-undo)) (pop-undo))) +(defn- set-editor-content! + "Prevent block auto-save during undo/redo." + [] + (when-let [block (state/get-edit-block)] + (state/set-edit-content! (state/get-edit-input-id) + (:block/content (db/entity (:db/id block)))))) + +(defn- get-next-tx-editor-cursor + [tx-id] + (let [result (->> (sort (keys (:history/tx->editor-cursor @state/state))) + (split-with #(not= % tx-id)) + second)] + (when (> (count result) 1) + (when-let [next-tx-id (nth result 1)] + (get-in @state/state [:history/tx->editor-cursor next-tx-id]))))) + +(defn- get-previous-tx-id + [tx-id] + (let [result (->> (sort (keys (:history/tx->editor-cursor @state/state))) + (split-with #(not= % tx-id)) + first)] + (when (>= (count result) 1) + (last result)))) + +(defn- get-previous-tx-editor-cursor + [tx-id] + (when-let [prev-tx-id (get-previous-tx-id tx-id)] + (get-in @state/state [:history/tx->editor-cursor prev-tx-id]))) + (defn undo [] - (let [[e prev-e] (smart-pop-undo)] - (when e - (let [{:keys [txs tx-meta]} e - new-txs (get-txs false txs) - undo-delete-concat-block? (and (= :delete-block (:outliner-op tx-meta)) - (seq (:concat-data tx-meta))) - editor-cursor (cond - undo-delete-concat-block? - (let [data (:concat-data tx-meta)] - (assoc (:editor-cursor e) - :last-edit-block {:block/uuid (:last-edit-block data)} - :pos (if (:end? data) :max 0))) - - ;; same block - (= (get-in e [:editor-cursor :last-edit-block :block/uuid]) - (get-in prev-e [:editor-cursor :last-edit-block :block/uuid])) - (:editor-cursor prev-e) - - :else - (:editor-cursor e))] - - (push-redo e) - (transact! new-txs (merge {:undo? true} - tx-meta - (select-keys e [:pagination-blocks-range]))) - - (when undo-delete-concat-block? - (when-let [block (state/get-edit-block)] - (state/set-edit-content! (state/get-edit-input-id) - (:block/content (db/entity (:db/id block)))))) - - (when (:whiteboard/transact? tx-meta) - (state/pub-event! [:whiteboard/undo e])) - (assoc e - :txs-op new-txs - :editor-cursor editor-cursor))))) + (when-let [e (smart-pop-undo)] + (let [{:keys [txs tx-meta tx-id]} e + new-txs (get-txs false txs) + current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id]) + save-block? (= (:outliner-op tx-meta) :save-block) + prev-editor-cursor (get-previous-tx-editor-cursor tx-id) + editor-cursor (if (and save-block? + (= (:block/uuid (:last-edit-block prev-editor-cursor)) + (:block/uuid (state/get-edit-block)))) + prev-editor-cursor + current-editor-cursor)] + (push-redo e) + (transact! new-txs (merge {:undo? true} + tx-meta + (select-keys e [:pagination-blocks-range]))) + (set-editor-content!) + (when (:whiteboard/transact? tx-meta) + (state/pub-event! [:whiteboard/undo e])) + (assoc e + :txs-op new-txs + :editor-cursor editor-cursor)))) (defn redo [] - (when-let [{:keys [txs tx-meta] :as e} (smart-pop-redo)] - (let [new-txs (get-txs true txs)] + (when-let [{:keys [txs tx-meta tx-id] :as e} (smart-pop-redo)] + (let [new-txs (get-txs true txs) + current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id]) + editor-cursor (if (= (:outliner-op tx-meta) :save-block) + current-editor-cursor + (get-next-tx-editor-cursor tx-id))] (push-undo e) (transact! new-txs (merge {:redo? true} tx-meta (select-keys e [:pagination-blocks-range]))) + (set-editor-content!) (when (:whiteboard/transact? tx-meta) (state/pub-event! [:whiteboard/redo e])) - (assoc e :txs-op new-txs)))) + (assoc e + :txs-op new-txs + :editor-cursor editor-cursor)))) (defn toggle-undo-redo-mode! [] @@ -231,14 +251,14 @@ #{:block/created-at :block/updated-at}))) (reset-redo) (if (:replace? tx-meta) - (let [[removed-e _prev-e] (pop-undo) + (let [removed-e (pop-undo) entity (update removed-e :txs concat tx-data)] (push-undo entity)) (let [updated-blocks (db-report/get-blocks tx-report) - entity {:blocks updated-blocks + entity {:tx-id (get-in tx-report [:tempids :db/current-tx]) + :blocks updated-blocks :txs tx-data :tx-meta tx-meta - :editor-cursor (:editor-cursor tx-meta) :pagination-blocks-range (get-in [:ui/pagination-blocks-range (get-in tx-report [:db-after :max-tx])] @state/state) :app-state (select-keys @state/state [:route-match diff --git a/src/main/frontend/modules/outliner/core.cljs b/src/main/frontend/modules/outliner/core.cljs index e56631cdbd..e084b2856b 100644 --- a/src/main/frontend/modules/outliner/core.cljs +++ b/src/main/frontend/modules/outliner/core.cljs @@ -16,8 +16,7 @@ [logseq.graph-parser.util :as gp-util] [cljs.spec.alpha :as s])) -(s/def ::block-map (s/keys :req [:db/id] - :opt [:block/page :block/left :block/parent])) +(s/def ::block-map (s/keys :opt [:db/id :block/uuid :block/page :block/left :block/parent])) (s/def ::block-map-or-entity (s/or :entity de/entity? :map ::block-map)) @@ -26,8 +25,14 @@ (defn block [m] - (assert (map? m) (util/format "block data must be map, got: %s %s" (type m) m)) - (->Block m)) + (assert (or (map? m) (de/entity? m)) (util/format "block data must be map or entity, got: %s %s" (type m) m)) + (if (de/entity? m) + (->Block {:db/id (:db/id m) + :block/uuid (:block/uuid m) + :block/page (:block/page m) + :block/left (:block/left m) + :block/parent (:block/parent m)}) + (->Block m))) (defn get-data [block] @@ -145,42 +150,49 @@ m) other-tx (:db/other-tx m) id (:db/id (:data this)) - block-entity (db/entity id) - remove-self-page #(remove (fn [b] - (= (:db/id b) (:db/id (:block/page block-entity)))) %) - old-refs (remove-self-page (:block/refs block-entity)) - new-refs (remove-self-page (:block/refs m))] + block-entity (db/entity id)] (when (seq other-tx) (swap! txs-state (fn [txs] (vec (concat txs other-tx))))) (when id + ;; Retract attributes to prepare for tx which rewrites block attributes (swap! txs-state (fn [txs] (vec (concat txs (map (fn [attribute] [:db/retract id attribute]) - db-schema/retract-attributes))))) + db-schema/retract-attributes))))) + ;; Update block's page attributes (when-let [e (:block/page block-entity)] - (let [m' {:db/id (:db/id e) - :block/updated-at (util/time-ms)} - m' (if (:block/created-at e) - m' - (assoc m' :block/created-at (util/time-ms))) - m' (if (or (:block/pre-block? block-entity) - (:block/pre-block? m)) - (let [properties (:block/properties m) - alias (set (:alias properties)) - tags (set (:tags properties)) - alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias) - tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags)] - (assoc m' - :block/alias alias - :block/tags tags - :block/properties properties)) - m')] - (swap! txs-state conj m')) + (let [m' (cond-> {:db/id (:db/id e) + :block/updated-at (util/time-ms)} + (not (:block/created-at e)) + (assoc :block/created-at (util/time-ms))) + txs (if (or (:block/pre-block? block-entity) + (:block/pre-block? m)) + (let [properties (:block/properties m) + alias (set (:alias properties)) + tags (set (:tags properties)) + alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias) + tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags) + deleteable-page-attributes {:block/alias alias + :block/tags tags + :block/properties properties + :block/properties-text-values (:block/properties-text-values m)} + ;; Retract page attributes to allow for deletion of page attributes + page-retractions + (mapv #(vector :db/retract (:db/id e) %) (keys deleteable-page-attributes))] + (conj page-retractions (merge m' deleteable-page-attributes))) + [m'])] + (swap! txs-state into txs))) + + ;; Remove orphaned refs from block + (let [remove-self-page #(remove (fn [b] + (= (:db/id b) (:db/id (:block/page block-entity)))) %) + old-refs (remove-self-page (:block/refs block-entity)) + new-refs (remove-self-page (:block/refs m))] (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs))) (swap! txs-state conj (dissoc m :db/other-tx)) @@ -228,11 +240,6 @@ children (db-model/get-block-immediate-children (state/get-current-repo) parent-id)] (map block children)))) -(defn get-right-node - [node] - {:pre [(tree/satisfied-inode? node)]} - (tree/-get-right node)) - (defn get-right-sibling [db-id] (when db-id @@ -404,7 +411,7 @@ (let [level-blocks (blocks-with-level blocks)] (filter (fn [b] (= 1 (:block/level b))) level-blocks))) -(defn get-right-siblings +(defn- get-right-siblings "Get `node`'s right siblings." [node] {:pre [(tree/satisfied-inode? node)]} @@ -474,7 +481,7 @@ (:db/id target-block)) get-new-id (fn [block lookup] (cond - (or (map? lookup) (vector? lookup)) + (or (map? lookup) (vector? lookup) (de/entity? lookup)) (when-let [uuid (if (and (vector? lookup) (= (first lookup) :block/uuid)) (get uuids (last lookup)) (get id->new-uuid (:db/id lookup)))] @@ -507,6 +514,13 @@ (dissoc :db/id))))) blocks))) +(defn- get-target-block + [target-block] + (if (:db/id target-block) + (db/pull (:db/id target-block)) + (when (:block/uuid target-block) + (db/pull [:block/uuid (:block/uuid target-block)])))) + (defn insert-blocks "Insert blocks as children (or siblings) of target-node. Args: @@ -524,7 +538,7 @@ [blocks target-block {:keys [sibling? keep-uuid? outliner-op replace-empty-target?] :as opts}] {:pre [(seq blocks) (s/valid? ::block-map-or-entity target-block)]} - (let [target-block' (db/pull (:db/id target-block)) + (let [target-block' (get-target-block target-block) _ (assert (some? target-block') (str "Invalid target: " target-block)) sibling? (if (page-block? target-block') false sibling?) move? (contains? #{:move-blocks :move-blocks-up-down :indent-outdent-blocks} outliner-op) @@ -710,7 +724,9 @@ [blocks target-block {:keys [sibling? outliner-op]}] [:pre [(seq blocks) (s/valid? ::block-map-or-entity target-block)]] - (let [non-consecutive-blocks? (seq (db-model/get-non-consecutive-blocks blocks)) + (let [target-block (get-target-block target-block) + _ (assert (some? target-block) (str "Invalid target: " target-block)) + non-consecutive-blocks? (seq (db-model/get-non-consecutive-blocks blocks)) original-position? (move-to-original-position? blocks target-block sibling? non-consecutive-blocks?)] (when (and (not (contains? (set (map :db/id blocks)) (:db/id target-block))) (not original-position?)) diff --git a/src/main/frontend/modules/outliner/datascript.cljc b/src/main/frontend/modules/outliner/datascript.cljc index e058ffd15c..5a5928775a 100644 --- a/src/main/frontend/modules/outliner/datascript.cljc +++ b/src/main/frontend/modules/outliner/datascript.cljc @@ -3,6 +3,7 @@ #?(:cljs (:require-macros [frontend.modules.outliner.datascript])) #?(:cljs (:require [datascript.core :as d] [frontend.db.conn :as conn] + [frontend.db :as db] [frontend.modules.outliner.pipeline :as pipelines] [frontend.modules.editor.undo-redo :as undo-redo] [frontend.state :as state] @@ -45,9 +46,14 @@ v))) x)))))) +#?(:cljs + (defn get-tx-id + [tx-report] + (get-in tx-report [:tempids :db/current-tx]))) + #?(:cljs (defn transact! - [txs opts] + [txs opts before-editor-cursor] (let [txs (remove-nil-from-transaction txs) txs (map (fn [m] (if (map? m) (dissoc m @@ -65,9 +71,15 @@ (try (let [repo (get opts :repo (state/get-current-repo)) conn (conn/get-db repo false) - editor-cursor (state/get-current-edit-block-and-position) - meta (merge opts {:editor-cursor editor-cursor}) - rs (d/transact! conn txs (assoc meta :outliner/transact? true))] + rs (d/transact! conn txs (assoc opts :outliner/transact? true)) + tx-id (get-tx-id rs)] + (swap! state/state assoc-in [:history/tx->editor-cursor tx-id] before-editor-cursor) + + ;; update the current edit block to include full information + (when-let [block (state/get-edit-block)] + (when (and (:block/uuid block) (not (:db/id block))) + (state/set-state! :editor/block (db/pull [:block/uuid (:block/uuid block)])))) + (when true ; TODO: add debug flag (let [eids (distinct (mapv first (:tx-data rs))) left&parent-list (->> diff --git a/src/main/frontend/modules/outliner/transaction.cljc b/src/main/frontend/modules/outliner/transaction.cljc index 738486659d..5f0ad51f7b 100644 --- a/src/main/frontend/modules/outliner/transaction.cljc +++ b/src/main/frontend/modules/outliner/transaction.cljc @@ -27,7 +27,8 @@ `(let [transact-data# frontend.modules.outliner.core/*transaction-data* opts# (if transact-data# (assoc ~opts :nested-transaction? true) - ~opts)] + ~opts) + before-editor-cursor# (frontend.state/get-current-edit-block-and-position)] (if transact-data# (do ~@body) (binding [frontend.modules.outliner.core/*transaction-data* (transient [])] @@ -40,7 +41,7 @@ opts## (merge (dissoc opts# :additional-tx) tx-meta#)] (when (seq all-tx#) ;; If it's empty, do nothing (when-not (:nested-transaction? opts#) ; transact only for the whole transaction - (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts##)] + (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts## before-editor-cursor#)] {:tx-report result# :tx-data all-tx# :tx-meta tx-meta#})))))))) diff --git a/src/main/frontend/modules/shortcut/config.cljs b/src/main/frontend/modules/shortcut/config.cljs index e69b6cff8d..2c7b9148a7 100644 --- a/src/main/frontend/modules/shortcut/config.cljs +++ b/src/main/frontend/modules/shortcut/config.cljs @@ -72,34 +72,34 @@ :pdf/find {:binding "alt+f" :fn pdf-utils/open-finder} - :whiteboard/select {:binding ["1" "s"] + :whiteboard/select {:binding ["1" "w s"] :fn #(.selectTool ^js (state/active-tldraw-app) "select")} - :whiteboard/pan {:binding ["2" "p"] + :whiteboard/pan {:binding ["2" "w p"] :fn #(.selectTool ^js (state/active-tldraw-app) "move")} - :whiteboard/portal {:binding "3" + :whiteboard/portal {:binding ["3" "w b"] :fn #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")} - :whiteboard/pencil {:binding ["4" "d"] + :whiteboard/pencil {:binding ["4" "w d"] :fn #(.selectTool ^js (state/active-tldraw-app) "pencil")} - :whiteboard/highlighter {:binding ["5" "h"] + :whiteboard/highlighter {:binding ["5" "w h"] :fn #(.selectTool ^js (state/active-tldraw-app) "highlighter")} - :whiteboard/eraser {:binding ["6" "e"] + :whiteboard/eraser {:binding ["6" "w e"] :fn #(.selectTool ^js (state/active-tldraw-app) "erase")} - :whiteboard/connector {:binding ["7" "c"] + :whiteboard/connector {:binding ["7" "w c"] :fn #(.selectTool ^js (state/active-tldraw-app) "line")} - :whiteboard/text {:binding ["8" "t"] + :whiteboard/text {:binding ["8" "w t"] :fn #(.selectTool ^js (state/active-tldraw-app) "text")} - :whiteboard/rectangle {:binding ["9" "r"] + :whiteboard/rectangle {:binding ["9" "w r"] :fn #(.selectTool ^js (state/active-tldraw-app) "box")} - :whiteboard/ellipse {:binding "o" + :whiteboard/ellipse {:binding ["o" "w o"] :fn #(.selectTool ^js (state/active-tldraw-app) "ellipse")} :whiteboard/reset-zoom {:binding "shift+0" @@ -141,7 +141,7 @@ :whiteboard/ungroup {:binding "mod+shift+g" :fn #(.unGroup (.-api ^js (state/active-tldraw-app)))} - :whiteboard/toggle-grid {:binding "shift+g" + :whiteboard/toggle-grid {:binding "t g" :fn #(.toggleGrid (.-api ^js (state/active-tldraw-app)))} :auto-complete/complete {:binding "enter" diff --git a/src/main/frontend/modules/shortcut/dicts.cljc b/src/main/frontend/modules/shortcut/dicts.cljc index fc6e7154c9..1609c01fd1 100644 --- a/src/main/frontend/modules/shortcut/dicts.cljc +++ b/src/main/frontend/modules/shortcut/dicts.cljc @@ -1140,7 +1140,47 @@ :command.ui/goto-plugins "Gå til dashbord for utvidelser" ;; :command.ui/open-new-window "Åpne et nytt vindu" :command.ui/select-theme-color "Velg tilgjengelige temafarger" - :command.ui/toggle-cards "Veksle kort"} + :command.ui/toggle-cards "Veksle kort" + :command.dev/show-block-ast "(Dev) Vis blokk AST" + :command.dev/show-block-data "(Dev) Vis blokk data" + :command.dev/show-page-ast "(Dev) Vis side AST" + :command.dev/show-page-data "(Dev) Vis side data" + :command.editor/copy-page-url "Kopier side url" + :command.editor/new-whiteboard "Nytt whiteboard" + :command.editor/select-parent "Velg overordnet blokk" + :command.editor/toggle-number-list "Veksle nummerliste" + :command.editor/toggle-undo-redo-mode "Veksle angremodus (global eller kun side)" + :command.go/whiteboards "Gå til whiteboards" + :command.graph/export-as-html "Eksporter offentlig graf som html" + :command.pdf/find "Pdf: Søk tekst i nåværende pdf doc" + :command.sidebar/close-top "Lukker øverste objekt i høyre sidestolpe" + :command.ui/clear-all-notifications "Fjern alle varsler" + :command.ui/install-plugins-from-file "Installer plugins fra plugins.edn" + :command.whiteboard/bring-forward "Flytt fremover" + :command.whiteboard/bring-to-front "Flytt fremst" + :command.whiteboard/connector "Koblingsverktøy" + :command.whiteboard/ellipse "Ellipseverktøy" + :command.whiteboard/eraser "Sletteverktøy" + :command.whiteboard/group "Velg gruppe" + :command.whiteboard/highlighter "Merkepenn" + :command.whiteboard/lock "Lås seleksjon" + :command.whiteboard/pan "Panoreringsverktøy" + :command.whiteboard/pencil "Blyantverktøy" + :command.whiteboard/portal "Portalverktøy" + :command.whiteboard/rectangle "Rektangelverktøy" + :command.whiteboard/reset-zoom "Tilbakestill zoom" + :command.whiteboard/select "Valg-verktøy" + :command.whiteboard/send-backward "Flytt bakover" + :command.whiteboard/send-to-back "Flytt bakerst" + :command.whiteboard/text "Tekst-verktøy" + :command.whiteboard/toggle-grid "Veksle rutenett på lerretet" + :command.whiteboard/ungroup "Del opp gruppe" + :command.whiteboard/unlock "Lås opp seleksjon" + :command.whiteboard/zoom-in "Zoom inn" + :command.whiteboard/zoom-out "Zoom ut" + :command.whiteboard/zoom-to-fit "Zoom til tegning" + :command.whiteboard/zoom-to-selection "Zoom for å passe seleksjonen" + :shortcut.category/whiteboard "Whiteboard"} :pt-PT {:shortcut.category/formatting "Formatação" :shortcut.category/basics "Básico" @@ -1660,6 +1700,7 @@ :shortcut.category/block-command-editing "Blok düzenleme komutuları" :shortcut.category/block-selection "Blok seçimi (seçimden çıkmak için Esc tuşuna basın)" :shortcut.category/toggle "Aç/Kapat" + :shortcut.category/whiteboard "Beyaz tahta" :shortcut.category/others "Diğer" :command.date-picker/complete "Tarih seçici: Seçilen günü seç" :command.date-picker/prev-day "Tarih seçici: Önceki günü seç" @@ -1732,6 +1773,31 @@ :command.editor/zoom-in "Düzenlenen bloğu yakınlaştır / Aksi takdirde ileri git" :command.editor/zoom-out "Düzenlenen bloğu uzaklaştır / Aksi takdirde geri git" :command.editor/toggle-undo-redo-mode "Geri alma / yineleme modunu değiştir (yalnızca sayfa veya genel)" + :command.editor/toggle-number-list "Numaralı liste olarak değiştir" + :command.whiteboard/select "Seçim aracı" + :command.whiteboard/pan "Kaydırma aracı" + :command.whiteboard/portal "Portal aracı" + :command.whiteboard/pencil "Kalem aracı" + :command.whiteboard/highlighter "Vurgulayıcı aracı" + :command.whiteboard/eraser "Silgi aracı" + :command.whiteboard/connector "Bağlayıcı aracı" + :command.whiteboard/text "Metin aracı" + :command.whiteboard/rectangle "Dikdörtgen aracı" + :command.whiteboard/ellipse "Elips aracı" + :command.whiteboard/reset-zoom "Yakınlaştırmayı sıfırla" + :command.whiteboard/zoom-to-fit "Çizimi yakınlaştır" + :command.whiteboard/zoom-to-selection "Seçimi sığacak kadar yakınlaştır" + :command.whiteboard/zoom-out "Uzaklaştır" + :command.whiteboard/zoom-in "Yakınlaştır" + :command.whiteboard/send-backward "Geriye git" + :command.whiteboard/send-to-back "Geriye taşı" + :command.whiteboard/bring-forward "İleriye git" + :command.whiteboard/bring-to-front "Öne taşı" + :command.whiteboard/lock "Seçimi kilitle" + :command.whiteboard/unlock "Seçimin Kilidini aç" + :command.whiteboard/group "Seçimi gruplandır" + :command.whiteboard/ungroup "Seçimi gruptan çıkar" + :command.whiteboard/toggle-grid "Tuval ızgarasını değiştir" :command.ui/toggle-brackets "Köşeli ayraçların görüntülenip görüntülenmeyeceğini değiştir" :command.go/search-in-page "Geçerli sayfada ara" :command.go/electron-find-in-page "Sayfada bul" diff --git a/src/main/frontend/page.cljs b/src/main/frontend/page.cljs index 87c4ca32f8..967b485c31 100644 --- a/src/main/frontend/page.cljs +++ b/src/main/frontend/page.cljs @@ -52,11 +52,11 @@ [:div.flex.flex-col.items-start [:div.text-2xs.font-bold.uppercase.toned-down (t :page/step "1")] [:div [:span.highlighted.font-bold "Rebuild"] [:span.toned-down " search index"]]] - [:div - (ui/button (t :page/try) - :small? true - :on-click (fn [] - (search-handler/rebuild-indices! true)))]] + [:div + (ui/button (t :page/try) + :small? true + :on-click (fn [] + (search-handler/rebuild-indices! true)))]] [:div.flex.flex-row.justify-between.align-items.mb-2.items-center.separator-top.py-4 [:div.flex.flex-col.items-start [:div.text-2xs.font-bold.uppercase.toned-down (t :page/step "2")] @@ -92,7 +92,7 @@ (ui/inject-dynamic-style-node!) (quick-tour/init) (plugin-handler/host-mounted!) - (assoc state ::teardown (setup-fns!) )) + (assoc state ::teardown (setup-fns!))) :will-unmount (fn [state] (when-let [teardown (::teardown state)] (teardown)))} diff --git a/src/main/frontend/rum.cljs b/src/main/frontend/rum.cljs index de4f7f291d..b4afe69c5c 100644 --- a/src/main/frontend/rum.cljs +++ b/src/main/frontend/rum.cljs @@ -92,7 +92,7 @@ (fn [] (rum/set-ref! *mounted true) #(rum/set-ref! *mounted false)) - []) + []) #(rum/deref *mounted))) (defn use-bounding-client-rect diff --git a/src/main/frontend/schema/handler/common_config.cljc b/src/main/frontend/schema/handler/common_config.cljc index 4aae6697a8..15d96da531 100644 --- a/src/main/frontend/schema/handler/common_config.cljc +++ b/src/main/frontend/schema/handler/common_config.cljc @@ -62,6 +62,7 @@ :string]] [:ref/default-open-blocks-level :int] [:ref/linked-references-collapsed-threshold :int] + [:graph/settings [:map-of :keyword :boolean]] [:favorites [:vector :string]] ;; There isn't a :float yet [:srs/learning-fraction float?] diff --git a/src/main/frontend/shui.cljs b/src/main/frontend/shui.cljs new file mode 100644 index 0000000000..3519961020 --- /dev/null +++ b/src/main/frontend/shui.cljs @@ -0,0 +1,25 @@ +(ns frontend.shui + "Glue between frontend code and deps/shui for convenience" + (:require + [frontend.date :refer [int->local-time-2]] + [frontend.state :as state] + [logseq.shui.context :refer [make-context]])) + +(def default-versions {:logseq.table.version 1}) + +(defn get-shui-component-version + "Returns the version of the shui component, checking first + the block properties, then the global config, then the defaults." + [component-name block-config] + (let [version-key (keyword (str "logseq." (name component-name) ".version"))] + (js/parseFloat + (or (get-in block-config [:block :block/properties version-key]) + (get-in (state/get-config) [version-key]) + (get-in default-versions [version-key]) + 1)))) + +(defn make-shui-context [block-config inline] + (make-context {:block-config block-config + :app-config (state/get-config) + :inline inline + :int->local-time-2 int->local-time-2})) diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index e51c5d7b1e..cd2349aa75 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -259,9 +259,9 @@ ;; :file-sync/progress {} ;; :file-sync/start-time {} ;; :file-sync/last-synced-at {}} - :file-sync/graph-state {:current-graph-uuid nil + :file-sync/graph-state {:current-graph-uuid nil} ;; graph-uuid -> ... - } + :user/info {:UserGroups (storage/get :user-groups)} :encryption/graph-parsing? false @@ -285,7 +285,10 @@ :chat/current-conversation nil :ai/preferred-translate-target-lang (storage/get :ai/preferred-translate-target-lang) :ai/engines {} - :ai/current-service "Built-in OpenAI"}))) + :ai/current-service "Built-in OpenAI" + + ;; db tx-id -> editor cursor + :history/tx->editor-cursor {}}))) ;; Block ast state ;; =============== diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index ad82892f25..9987d81f02 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -730,26 +730,26 @@ [state {:keys [on-mouse-down header title-trigger? collapsed?]}] (let [control? (get state ::control?)] [:div.content - [:div.flex-1.flex-row.foldable-title (cond-> - {:on-mouse-over #(reset! control? true) - :on-mouse-out #(reset! control? false)} - title-trigger? - (assoc :on-mouse-down on-mouse-down - :class "cursor")) - [:div.flex.flex-row.items-center - (when-not (mobile-util/native-platform?) - [:a.block-control.opacity-50.hover:opacity-100.mr-2 - (cond-> - {:style {:width 14 - :height 16 - :margin-left -30}} - (not title-trigger?) - (assoc :on-mouse-down on-mouse-down)) - [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")} - (rotating-arrow @collapsed?)]]) - (if (fn? header) - (header @collapsed?) - header)]]])) + [:div.flex-1.flex-row.foldable-title (cond-> + {:on-mouse-over #(reset! control? true) + :on-mouse-out #(reset! control? false)} + title-trigger? + (assoc :on-mouse-down on-mouse-down + :class "cursor")) + [:div.flex.flex-row.items-center + (when-not (mobile-util/native-platform?) + [:a.block-control.opacity-50.hover:opacity-100.mr-2 + (cond-> + {:style {:width 14 + :height 16 + :margin-left -30}} + (not title-trigger?) + (assoc :on-mouse-down on-mouse-down)) + [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")} + (rotating-arrow @collapsed?)]]) + (if (fn? header) + (header @collapsed?) + header)]]])) (rum/defcs foldable < db-mixins/query rum/reactive (rum/local false ::collapsed?) @@ -852,10 +852,10 @@ [:option (cond-> {:key label :value (or value label)} ;; NOTE: value might be an empty string, `or` is safe here - disabled - (assoc :disabled disabled) - selected - (assoc :selected selected)) + disabled + (assoc :disabled disabled) + selected + (assoc :selected selected)) label])])) (rum/defc radio-list diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index c52141e364..5d493172b4 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -1274,7 +1274,7 @@ #?(:cljs (defn scroll-editor-cursor [^js/HTMLElement el & {:keys [to-vw-one-quarter?]}] - (when (and el (or (mobile-util/native-platform?) mobile?)) + (when (and el (or (mobile-util/native-platform?) (mobile?))) (let [box-rect (.getBoundingClientRect el) box-top (.-top box-rect) box-bottom (.-bottom box-rect) diff --git a/src/main/frontend/util/cursor.cljs b/src/main/frontend/util/cursor.cljs index e254be1226..e49ff32511 100644 --- a/src/main/frontend/util/cursor.cljs +++ b/src/main/frontend/util/cursor.cljs @@ -226,7 +226,7 @@ (defn- move-cursor-up-down [input direction] - (move-cursor-to input (next-cursor-pos-up-down direction (get-caret-pos input)))) + (move-cursor-to input (next-cursor-pos-up-down direction (get-caret-pos input)))) (defn move-cursor-up [input] (move-cursor-up-down input :up)) diff --git a/src/main/frontend/util/text.cljs b/src/main/frontend/util/text.cljs index 090f8af624..fb2daa34d9 100644 --- a/src/main/frontend/util/text.cljs +++ b/src/main/frontend/util/text.cljs @@ -3,7 +3,8 @@ a good ns to be in yet" (:require [clojure.string :as string] [goog.string :as gstring] - [frontend.util :as util])) + [frontend.util :as util] + [logseq.common.path :as path])) (defonce between-re #"\(between ([^\)]+)\)") @@ -142,10 +143,11 @@ ;; FIXME: distinguish from get-repo-name (defn get-graph-name-from-path [path] - (when (string? path) - (let [parts (->> (string/split path #"/") - (take-last 2))] - (-> (if (not= (first parts) "0") - (util/string-join-path parts) - (last parts)) - js/decodeURIComponent)))) + (let [path (if (path/is-file-url? path) + (path/url-to-path path) + path) + parts (->> (string/split path #"/") + (take-last 2))] + (if (not= (first parts) "0") + (util/string-join-path parts) + (last parts)))) diff --git a/src/main/frontend/version.cljs b/src/main/frontend/version.cljs index 646a7e002d..2b900d6c56 100644 --- a/src/main/frontend/version.cljs +++ b/src/main/frontend/version.cljs @@ -1,3 +1,3 @@ (ns ^:no-doc frontend.version) -(defonce version "0.9.4") +(defonce version "0.9.6") diff --git a/src/main/logseq/api.cljs b/src/main/logseq/api.cljs index 3c14830b53..65b6c9d328 100644 --- a/src/main/logseq/api.cljs +++ b/src/main/logseq/api.cljs @@ -43,7 +43,8 @@ [frontend.handler.shell :as shell] [frontend.modules.layout.core] [frontend.handler.code :as code-handler] - [frontend.handler.search :as search-handler])) + [frontend.handler.search :as search-handler] + [logseq.api.block :as api-block])) ;; Alert: this namespace shouldn't invoke any reactive queries @@ -651,13 +652,13 @@ nil))) (def ^:export update_block - (fn [block-uuid content ^js _opts] + (fn [block-uuid content ^js opts] (let [repo (state/get-current-repo) edit-input (state/get-edit-input-id) editing? (and edit-input (string/ends-with? edit-input (str block-uuid)))] (if editing? (state/set-edit-content! edit-input content) - (editor-handler/save-block! repo (sdk-utils/uuid-or-throw-error block-uuid) content)) + (editor-handler/save-block! repo (sdk-utils/uuid-or-throw-error block-uuid) content (bean/->clj opts))) nil))) (def ^:export move_block @@ -676,24 +677,7 @@ target-block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error target-block-uuid))] (editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil))) -(def ^:export get_block - (fn [id-or-uuid ^js opts] - (when-let [block (cond - (number? id-or-uuid) (db-utils/pull id-or-uuid) - (string? id-or-uuid) (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid)))] - (when-not (contains? block :block/name) - (when-let [uuid (:block/uuid block)] - (let [{:keys [includeChildren]} (bean/->clj opts) - repo (state/get-current-repo) - block (if includeChildren - ;; nested children results - (first (outliner-tree/blocks->vec-tree - (db-model/get-block-and-children repo uuid) uuid)) - ;; attached shallow children - (assoc block :block/children - (map #(list :uuid (get-in % [:data :block/uuid])) - (db/get-block-immediate-children repo uuid))))] - (bean/->js (sdk-utils/normalize-keyword-for-json block)))))))) +(def ^:export get_block api-block/get_block) (def ^:export get_current_block (fn [^js opts] @@ -703,7 +687,7 @@ (gdom/getElement (state/get-editing-block-dom-id))) (.getAttribute "blockid") (db-model/get-block-by-uuid)))] - (get_block (:db/id block) opts)))) + (get_block (:block/uuid block) opts)))) (def ^:export get_previous_sibling_block (fn [block-uuid] @@ -715,8 +699,9 @@ (def ^:export get_next_sibling_block (fn [block-uuid] (when-let [block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error block-uuid))] - (when-let [right-siblings (outliner/get-right-siblings (outliner/->Block block))] - (bean/->js (sdk-utils/normalize-keyword-for-json (:data (first right-siblings)))))))) + (when-let [right-sibling (outliner/get-right-sibling (:db/id block))] + (let [block (db/pull (:id right-sibling))] + (bean/->js (sdk-utils/normalize-keyword-for-json block))))))) (def ^:export set_block_collapsed (fn [block-uuid ^js opts] diff --git a/src/main/logseq/api/block.cljs b/src/main/logseq/api/block.cljs new file mode 100644 index 0000000000..d2eeed6881 --- /dev/null +++ b/src/main/logseq/api/block.cljs @@ -0,0 +1,28 @@ +(ns logseq.api.block + "Block related apis" + (:require [frontend.db.model :as db-model] + [frontend.db.utils :as db-utils] + [cljs-bean.core :as bean] + [frontend.state :as state] + [frontend.modules.outliner.tree :as outliner-tree] + [frontend.db :as db] + [logseq.sdk.utils :as sdk-utils])) + +(defn get_block + [id-or-uuid ^js opts] + (when-let [block (if (number? id-or-uuid) + (db-utils/pull id-or-uuid) + (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid)))] + (when-not (contains? block :block/name) + (when-let [uuid (:block/uuid block)] + (let [{:keys [includeChildren]} (bean/->clj opts) + repo (state/get-current-repo) + block (if includeChildren + ;; nested children results + (first (outliner-tree/blocks->vec-tree + (db-model/get-block-and-children repo uuid) uuid)) + ;; attached shallow children + (assoc block :block/children + (map #(list :uuid (:block/uuid %)) + (db/get-block-immediate-children repo uuid))))] + (bean/->js (sdk-utils/normalize-keyword-for-json block))))))) diff --git a/src/test/frontend/db/model_test.cljs b/src/test/frontend/db/model_test.cljs index 7b43307e10..39b5778567 100644 --- a/src/test/frontend/db/model_test.cljs +++ b/src/test/frontend/db/model_test.cljs @@ -163,6 +163,22 @@ foo:: bar"}]) (catch :default e (ex-message e))))))) +(deftest get-block-immediate-children + (load-test-files [{:file/path "pages/page1.md" + :file/content "\n +- parent + - child 1 + - grandchild 1 + - child 2 + - grandchild 2 + - child 3"}]) + (let [parent (-> (d/q '[:find (pull ?b [*]) :where [?b :block/content "parent"]] + (conn/get-db test-helper/test-db)) + ffirst)] + (is (= ["child 1" "child 2" "child 3"] + (map :block/content + (model/get-block-immediate-children test-helper/test-db (:block/uuid parent))))))) + (deftest get-property-values (load-test-files [{:file/path "pages/Feature.md" :file/content "type:: [[Class]]"} diff --git a/src/test/frontend/fs_test.cljs b/src/test/frontend/fs_test.cljs index 879b90e135..c4204d81f5 100644 --- a/src/test/frontend/fs_test.cljs +++ b/src/test/frontend/fs_test.cljs @@ -2,6 +2,7 @@ (:require [clojure.test :refer [is use-fixtures]] [frontend.test.fixtures :as fixtures] [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]] + [frontend.test.node-helper :as test-node-helper] [frontend.fs :as fs] [promesa.core :as p] ["fs" :as fs-node] @@ -11,7 +12,7 @@ (deftest-async create-if-not-exists-creates-correctly ;; dir needs to be an absolute path for fn to work correctly - (let [dir (node-path/resolve (test-helper/create-tmp-dir)) + (let [dir (node-path/resolve (test-node-helper/create-tmp-dir)) some-file (node-path/join dir "something.txt")] (-> @@ -29,7 +30,7 @@ (fs-node/rmdirSync dir)))))) (deftest-async create-if-not-exists-does-not-create-correctly - (let [dir (node-path/resolve (test-helper/create-tmp-dir)) + (let [dir (node-path/resolve (test-node-helper/create-tmp-dir)) some-file (node-path/join dir "something.txt")] (fs-node/writeFileSync some-file "OLD") diff --git a/src/test/frontend/handler/editor_test.cljs b/src/test/frontend/handler/editor_test.cljs index 5c89431be4..d30d6f31ed 100644 --- a/src/test/frontend/handler/editor_test.cljs +++ b/src/test/frontend/handler/editor_test.cljs @@ -1,9 +1,15 @@ (ns frontend.handler.editor-test (:require [frontend.handler.editor :as editor] - [clojure.test :refer [deftest is testing are]] + [frontend.db :as db] + [clojure.test :refer [deftest is testing are use-fixtures]] + [datascript.core :as d] + [frontend.test.helper :as test-helper :refer [load-test-files]] + [frontend.db.model :as model] [frontend.state :as state] [frontend.util.cursor :as cursor])) +(use-fixtures :each test-helper/start-and-destroy-db) + (deftest extract-nearest-link-from-text-test (testing "Page, block and tag links" (is (= "page1" @@ -213,3 +219,39 @@ "No page search within backticks")) ;; Reset state (state/set-editor-action! nil)) + +(deftest save-block-aux! + (load-test-files [{:file/path "pages/page1.md" + :file/content "\n +- b1 #foo"}]) + (testing "updating block's content changes content and preserves path-refs" + (let [conn (db/get-db test-helper/test-db false) + block (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name]}]) + :where [?b :block/content "b1 #foo"]] + @conn) + ffirst) + prev-path-refs (set (map :block/name (:block/path-refs block))) + _ (assert (= #{"page1" "foo"} prev-path-refs) + "block has expected :block/path-refs") + ;; Use same options as edit-box-on-change! + _ (editor/save-block-aux! block "b12 #foo" {:skip-properties? true}) + updated-block (d/pull @conn '[* {:block/path-refs [:block/name]}] [:block/uuid (:block/uuid block)])] + (is (= "b12 #foo" (:block/content updated-block)) "Content updated correctly") + (is (= prev-path-refs + (set (map :block/name (:block/path-refs updated-block)))) + "Path-refs remain the same")))) + +(deftest save-block! + (testing "Saving blocks with and without properties" + (test-helper/load-test-files [{:file/path "foo.md" + :file/content "# foo"}]) + (let [repo test-helper/test-db + block-uuid (:block/uuid (model/get-block-by-page-name-and-block-route-name repo "foo" "foo"))] + (editor/save-block! repo block-uuid "# bar") + (is (= "# bar" (:block/content (model/query-block-by-uuid block-uuid)))) + + (editor/save-block! repo block-uuid "# foo" {:properties {:foo "bar"}}) + (is (= "# foo\nfoo:: bar" (:block/content (model/query-block-by-uuid block-uuid)))) + + (editor/save-block! repo block-uuid "# bar") + (is (= "# bar" (:block/content (model/query-block-by-uuid block-uuid))))))) diff --git a/src/test/frontend/handler/plugin_config_test.cljs b/src/test/frontend/handler/plugin_config_test.cljs index c01f6790d4..0bcdae4178 100644 --- a/src/test/frontend/handler/plugin_config_test.cljs +++ b/src/test/frontend/handler/plugin_config_test.cljs @@ -1,6 +1,7 @@ (ns frontend.handler.plugin-config-test (:require [clojure.test :refer [is use-fixtures testing deftest]] [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]] + [frontend.test.node-helper :as test-node-helper] [frontend.test.fixtures :as fixtures] [frontend.handler.plugin-config :as plugin-config-handler] [frontend.handler.global-config :as global-config-handler] @@ -17,7 +18,7 @@ (defn- create-global-config-dir [] - (let [dir (test-helper/create-tmp-dir "config") + (let [dir (test-node-helper/create-tmp-dir "config") root-dir (node-path/dirname dir)] (reset! global-config-handler/root-dir root-dir) dir)) diff --git a/src/test/frontend/handler/repo_conversion_test.cljs b/src/test/frontend/handler/repo_conversion_test.cljs index 1ac0db602f..7768ee35c3 100644 --- a/src/test/frontend/handler/repo_conversion_test.cljs +++ b/src/test/frontend/handler/repo_conversion_test.cljs @@ -98,7 +98,7 @@ ;; only increase over time as the docs graph rarely has deletions (testing "Counts" (is (= 211 (count files)) "Correct file count") - (is (= 42312 (count (d/datoms db :eavt))) "Correct datoms count") + (is (= 42304 (count (d/datoms db :eavt))) "Correct datoms count") (is (= 3600 (ffirst diff --git a/src/test/frontend/handler/repo_test.cljs b/src/test/frontend/handler/repo_test.cljs index 199723eb98..f59069930f 100644 --- a/src/test/frontend/handler/repo_test.cljs +++ b/src/test/frontend/handler/repo_test.cljs @@ -2,7 +2,6 @@ (:require [cljs.test :refer [deftest use-fixtures testing is]] [frontend.handler.repo :as repo-handler] [frontend.test.helper :as test-helper :refer [load-test-files]] - [frontend.state :as state] [logseq.graph-parser.cli :as gp-cli] [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper] [logseq.graph-parser.util.block-ref :as block-ref] @@ -12,13 +11,7 @@ ["path" :as node-path] ["fs" :as fs])) -(use-fixtures :each {:before (fn [] - ;; Set current-repo explicitly since it's not the default - (state/set-current-repo! test-helper/test-db) - (test-helper/start-test-db!)) - :after (fn [] - (state/set-current-repo! nil) - (test-helper/destroy-test-db!))}) +(use-fixtures :each test-helper/start-and-destroy-db) (deftest ^:integration parse-and-load-files-to-db (let [graph-dir "src/test/docs-0.9.2" diff --git a/src/test/frontend/modules/outliner/core_test.cljs b/src/test/frontend/modules/outliner/core_test.cljs index 269ecf71cd..6a7fe15240 100644 --- a/src/test/frontend/modules/outliner/core_test.cljs +++ b/src/test/frontend/modules/outliner/core_test.cljs @@ -10,7 +10,7 @@ [clojure.walk :as walk] [logseq.graph-parser.block :as gp-block] [datascript.core :as d] - [frontend.test.helper :as test-helper] + [frontend.test.helper :as test-helper :refer [load-test-files]] [clojure.set :as set])) (def test-db test-helper/test-db) @@ -440,6 +440,63 @@ '(16 17) (map :block/uuid (tree/get-sorted-block-and-children test-db (:db/id (get-block 16)))))))) +(defn- save-block! + [block] + (outliner-tx/transact! {:graph test-db} + (outliner-core/save-block! block))) + +(deftest save-test + (load-test-files [{:file/path "pages/page1.md" + :file/content "alias:: foo, bar +tags:: tag1, tag2 +- block #blarg #bar"}]) + (testing "save deletes a page's tags" + (let [conn (db/get-db test-helper/test-db false) + pre-block (->> (d/q '[:find (pull ?b [*]) + :where [?b :block/pre-block? true]] + @conn) + ffirst) + _ (save-block! (-> pre-block + (update :block/properties dissoc :tags) + (update :block/properties-text-values dissoc :tags))) + updated-page (-> (d/q '[:find (pull ?bp [* {:block/alias [*]}]) + :where [?b :block/pre-block? true] + [?b :block/page ?bp]] + @conn) + ffirst)] + (is (nil? (:block/tags updated-page)) + "Page's tags are deleted") + (is (= #{"foo" "bar"} (set (map :block/name (:block/alias updated-page)))) + "Page's aliases remain the same") + (is (= {:block/properties {:alias #{"foo" "bar"}} + :block/properties-text-values {:alias "foo, bar"}} + (select-keys updated-page [:block/properties :block/properties-text-values])) + "Page property attributes are correct") + (is (= {:block/properties {:alias #{"foo" "bar"}} + :block/properties-text-values {:alias "foo, bar"}} + (-> (d/q '[:find (pull ?b [*]) + :where [?b :block/pre-block? true]] + @conn) + ffirst + (select-keys [:block/properties :block/properties-text-values]))) + "Pre-block property attributes are correct"))) + + (testing "save deletes orphaned pages when a block's refs change" + (let [conn (db/get-db test-helper/test-db false) + pages (set (map first (d/q '[:find ?bn :where [?b :block/name ?bn]] @conn))) + _ (assert (set/subset? #{"blarg" "bar"} pages) "Pages from block exist") + block-with-refs (ffirst (d/q '[:find (pull ?b [* {:block/refs [*]}]) + :where [?b :block/content "block #blarg #bar"]] + @conn)) + _ (save-block! (-> block-with-refs + (assoc :block/content "block" + :block/refs []))) + updated-pages (set (map first (d/q '[:find ?bn :where [?b :block/name ?bn]] @conn)))] + (is (not (contains? updated-pages "blarg")) + "Deleted, orphaned page no longer exists") + (is (contains? updated-pages "bar") + "Deleted but not orphaned page still exists")))) + ;;; Fuzzy tests (def init-id (atom 100)) diff --git a/src/test/frontend/modules/outliner/pipeline_test.cljs b/src/test/frontend/modules/outliner/pipeline_test.cljs index 698328f4af..2dcca1b49c 100644 --- a/src/test/frontend/modules/outliner/pipeline_test.cljs +++ b/src/test/frontend/modules/outliner/pipeline_test.cljs @@ -1,18 +1,11 @@ (ns frontend.modules.outliner.pipeline-test (:require [cljs.test :refer [deftest is use-fixtures testing]] [datascript.core :as d] - [frontend.state :as state] [frontend.db :as db] [frontend.modules.outliner.pipeline :as pipeline] [frontend.test.helper :as test-helper :refer [load-test-files]])) -(use-fixtures :each {:before (fn [] - ;; Set current-repo explicitly since it's not the default - (state/set-current-repo! test-helper/test-db) - (test-helper/start-test-db!)) - :after (fn [] - (state/set-current-repo! nil) - (test-helper/destroy-test-db!))}) +(use-fixtures :each test-helper/start-and-destroy-db) (defn- get-blocks [db] (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}]) diff --git a/src/test/frontend/test/helper.cljs b/src/test/frontend/test/helper.cljs index 437a754e6e..e4c3d6e52a 100644 --- a/src/test/frontend/test/helper.cljs +++ b/src/test/frontend/test/helper.cljs @@ -1,9 +1,8 @@ (ns frontend.test.helper "Common helper fns for tests" (:require [frontend.handler.repo :as repo-handler] - [frontend.db.conn :as conn] - ["path" :as node-path] - ["fs" :as fs-node])) + [frontend.state :as state] + [frontend.db.conn :as conn])) (defonce test-db "test-db") @@ -25,15 +24,14 @@ This can be called in synchronous contexts as no async fns should be invoked" ;; Set :refresh? to avoid creating default files in after-parse {:re-render? false :verbose false :refresh? true})) -(defn create-tmp-dir - "Creates a temporary directory under tmp/. If a subdir is given, creates an - additional subdirectory under the newly created temp directory." - ([] (create-tmp-dir nil)) - ([subdir] - (when-not (fs-node/existsSync "tmp") (fs-node/mkdirSync "tmp")) - (let [dir (fs-node/mkdtempSync (node-path/join "tmp" "unit-test-"))] - (if subdir - (do - (fs-node/mkdirSync (node-path/join dir subdir)) - (node-path/join dir subdir)) - dir)))) +(defn start-and-destroy-db + "Sets up a db connection and current repo like fixtures/reset-datascript. It + also seeds the db with the same default data that the app does and destroys a db + connection when done with it." + [f] + ;; Set current-repo explicitly since it's not the default + (state/set-current-repo! test-db) + (start-test-db!) + (f) + (state/set-current-repo! nil) + (destroy-test-db!)) diff --git a/src/test/frontend/test/node_helper.cljs b/src/test/frontend/test/node_helper.cljs new file mode 100644 index 0000000000..bade47d95e --- /dev/null +++ b/src/test/frontend/test/node_helper.cljs @@ -0,0 +1,17 @@ +(ns frontend.test.node-helper + "Common helper fns for node tests" + (:require ["path" :as node-path] + ["fs" :as fs-node])) + +(defn create-tmp-dir + "Creates a temporary directory under tmp/. If a subdir is given, creates an + additional subdirectory under the newly created temp directory." + ([] (create-tmp-dir nil)) + ([subdir] + (when-not (fs-node/existsSync "tmp") (fs-node/mkdirSync "tmp")) + (let [dir (fs-node/mkdtempSync (node-path/join "tmp" "unit-test-"))] + (if subdir + (do + (fs-node/mkdirSync (node-path/join dir subdir)) + (node-path/join dir subdir)) + dir)))) diff --git a/src/test/logseq/api_test.cljs b/src/test/logseq/api_test.cljs new file mode 100644 index 0000000000..18211b3a31 --- /dev/null +++ b/src/test/logseq/api_test.cljs @@ -0,0 +1,40 @@ +(ns logseq.api-test + (:require [cljs.test :refer [use-fixtures deftest is]] + [frontend.test.helper :as test-helper] + [frontend.db :as db] + [logseq.api.block :as api-block] + [frontend.state :as state] + [cljs-bean.core :as bean])) + +(use-fixtures :each {:before test-helper/start-test-db! + :after test-helper/destroy-test-db!}) + +(deftest get-block + (with-redefs [state/get-current-repo (constantly test-helper/test-db)] + (db/transact! test-helper/test-db + [{:db/id 10000 + :block/uuid #uuid "4406f839-6410-43b5-87db-25e9b8f54cc0" + :block/content "1"} + {:db/id 10001 + :block/uuid #uuid "d9b7b45f-267f-4794-9569-f43d1ce77172" + :block/content "2"} + {:db/id 10002 + :block/uuid #uuid "adae3006-f03e-4814-a1f5-f17f15b86556" + :block/parent 10001 + :block/left 10001 + :block/content "3"} + {:db/id 10003 + :block/uuid #uuid "0c3053c3-2dab-4769-badd-14ce16d8ba8d" + :block/parent 10002 + :block/left 10002 + :block/content "4"}]) + + (is (= (:content (bean/->clj (api-block/get_block 10000 #js {}))) "1")) + (is (= (:content (bean/->clj (api-block/get_block "d9b7b45f-267f-4794-9569-f43d1ce77172" #js {}))) "2")) + (is (= (:content (bean/->clj (api-block/get_block #uuid "d9b7b45f-267f-4794-9569-f43d1ce77172" #js {}))) "2")) + (is (= {:id 10001, :content "2", :uuid "d9b7b45f-267f-4794-9569-f43d1ce77172", :children [["uuid" "adae3006-f03e-4814-a1f5-f17f15b86556"]]} + (bean/->clj (api-block/get_block 10001 #js {:includeChildren false})))) + (is (= {:content "2", :uuid "d9b7b45f-267f-4794-9569-f43d1ce77172", :id 10001, :children [{:content "3", :left {:id 10001}, :parent {:id 10001}, :uuid "adae3006-f03e-4814-a1f5-f17f15b86556", :id 10002, :level 1, :children [{:content "4", :left {:id 10002}, :parent {:id 10002}, :uuid "0c3053c3-2dab-4769-badd-14ce16d8ba8d", :id 10003, :level 2, :children []}]}]} + (bean/->clj (api-block/get_block 10001 #js {:includeChildren true})))))) + +#_(cljs.test/run-tests) diff --git a/tailwind.config.js b/tailwind.config.js index a60ba9ba0f..40a34dc759 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,29 @@ +const plugin = require('tailwindcss/plugin') const colors = require('tailwindcss/colors') +const radix = require('@radix-ui/colors') + +const gradientColors = { + tomato: ["amber", "orange", "tomato", "red", "crimson"], + red: ["orange", "tomato", "red", "crimson", "pink"], + crimson: ["tomato", "red", "crimson", "pink", "plum"], + pink: ["red", "crimson", "pink", "plum", "purple"], + plum: ["crimson", "pink", "plum", "purple", "violet"], + purple: ["pink", "plum", "purple", "violet", "indigo"], + violet: ["plum", "purple", "violet", "indigo", "blue"], + indigo: ["purple", "violet", "indigo", "blue", "cyan"], + blue: ["violet", "indigo", "blue", "cyan", "teal"], + // sky: ["indigo", "blue", "sky", "cyan", "teal"], + cyan: ["indigo", "blue", "cyan", "teal", "green"], + teal: ["blue", "cyan", "teal", "green", "grass"], + // mint: ["cyan", "teal", "mint", "green", "grass"], + green: ["cyan", "teal", "green", "grass", "amber"], + grass: ["teal", "green", "grass", "amber", "orange"], + // lime: ["green", "grass", "lime", "yellow", "amber"], + // yellow: ["grass", "lime", "yellow", "amber", "orange"], + amber: ["green", "grass", "amber", "orange", "tomato"], + orange: ["grass", "amber", "orange", "tomato", "red"], + // brown: ["green", "grass", "brown", "tomato", "red"], +} function exposeColorsToCssVars ({ addBase, theme }) { function extractColorVars (colorObj, colorGroup = '') { @@ -19,28 +44,160 @@ function exposeColorsToCssVars ({ addBase, theme }) { }) } +function buildColor(color, custom) { + const base = custom || colors[color] || {} + + for (const [xName, xValue] of Object.entries(radix[color] || {})) { + const regexResult = xName.match(/\d+$/) + if (!regexResult) { continue; } + const xStep = regexResult[0] + base[xStep] = xValue + } + + return base +} + +// this will allow us to use gradient color functions in the ui: +// grad-bg-tomato-3 OR grad-bg-tomato-3-alpha +// it will also loop through all 5 color stops, unless the stops are specified +// grad-bg-stops-3 +// this will have a default repeating gradient at a step that can be configured with +// grad-bg-cycle-32 +const addGradientColors = plugin(({ addBase, addComponents, addUtilities, config, ___theme }) => { + const dark = getDarkSelector(config) + + addUtilities({ + ['.grad-bg-stops-3']: { + '--grad-bg-stops': "var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)", + }, + ['.grad-bg-stops-5']: { + '--grad-bg-stops': "var(--grad-bg-stop-a), var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d), var(--grad-bg-stop-e)", + }, + ['.grad-bg-cycle-12']: { + 'background-image': 'repeatint-linear-gradient(to right, var(--grad-bg-stops))', + }, + }) + + Object.values(gradientColors).forEach((stops, ___index) => { + const baseColor = stops[2] + const color = (scale, stopIndex = 2, suffix = "") => `--color-${stops[stopIndex]}${suffix}-${scale}` + + addComponents({ + // tailwind componnent for .grad-bg-COLOR-9 + [`.grad-bg-${baseColor}-9`]: { + "--grad-bg-stop-a": `var(${color(9, 0)})`, + "--grad-bg-stop-b": `var(${color(9, 1)})`, + "--grad-bg-stop-c": `var(${color(9, 2)})`, + "--grad-bg-stop-d": `var(${color(9, 3)})`, + "--grad-bg-stop-e": `var(${color(9, 4)})`, + "--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`, + "background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`, + + [dark]: { + "--grad-bg-stop-a": `var(${color(9, 0, "dark")})`, + "--grad-bg-stop-b": `var(${color(9, 1, "dark")})`, + "--grad-bg-stop-c": `var(${color(9, 2, "dark")})`, + "--grad-bg-stop-d": `var(${color(9, 3, "dark")})`, + "--grad-bg-stop-e": `var(${color(9, 4, "dark")})`, + } + }, + // tailwind component for .grad-bg-COLOR-9-alpha + [`.grad-bg-${baseColor}-9-alpha`]: { + "--grad-bg-stop-a": `var(${color(9, 0)})`, + "--grad-bg-stop-b": `var(${color(9, 1)})`, + "--grad-bg-stop-c": `var(${color(9, 2)})`, + "--grad-bg-stop-d": `var(${color(9, 3)})`, + "--grad-bg-stop-e": `var(${color(9, 4)})`, + "--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`, + "background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`, + + [dark]: { + "--grad-bg-stop-a": `var(${color(9, 0, "dark")})`, + "--grad-bg-stop-b": `var(${color(9, 1, "dark")})`, + "--grad-bg-stop-c": `var(${color(9, 2, "dark")})`, + "--grad-bg-stop-d": `var(${color(9, 3, "dark")})`, + "--grad-bg-stop-e": `var(${color(9, 4, "dark")})`, + } + }, + }) + }) +}) + +function getDarkSelector(config) { + const darkMode = config("darkMode"); + const prefix = config("prefix"); + + if (Array.isArray(darkMode)) { + if (darkMode.length < 2) { + throw new Error( + "To customize the dark mode selector, `darkMode` should contain two items. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name" + ); + } + + if (darkMode[0] !== "class") { + throw new Error( + 'To customize the dark mode selector, `darkMode` should have "class" as its first item. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name' + ); + } + + return darkMode[1] + " &"; + } + + if (darkMode === "media") { + return "@media (prefers-color-scheme: dark)"; + } + + if (darkMode !== "class") { + throw new Error( + "Invalid `darkMode`. Documentation: https://tailwindcss.com/docs/dark-mode" + ); + } + + if (prefix) { + return `[class~="${prefix}dark"] &`; + } + + return '[class~="dark"] &'; +} + module.exports = { darkMode: 'class', content: [ './src/**/*.js', './src/**/*.cljs', - './resources/**/*.html' + './resources/**/*.html', + './deps/shui/src/**/*.cljs', ], safelist: [ - 'bg-black', 'bg-white', + 'bg-black', 'bg-white', 'capitalize-first', { pattern: /bg-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ }, { pattern: /text-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ }, - { pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ } + { pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ }, + { pattern: /bg-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /shadow-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /text-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /ring-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /from-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /via-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /to-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ }, + { pattern: /border-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(4|5|6|7|8)/ }, ], plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/aspect-ratio'), require('@tailwindcss/line-clamp'), + require('tailwind-capitalize-first-letter'), + addGradientColors, exposeColorsToCssVars ], theme: { extend: { + backgroundImage: { + 'gradient-conic': 'conic-gradient(var(--tw-gradient-stops))', + 'gradient-conic-bounce': 'conic-gradient(var(--tw-gradient-from), var(--tw-gradient-via), var(--tw-gradient-to), var(--tw-gradient-via), var(--tw-gradient-from))', + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + }, fontSize: { '2xs': ['0.625rem', '0.875rem'] }, @@ -59,14 +216,44 @@ module.exports = { } }, colors: { - transparent: 'transparent', - current: 'currentColor', + // Tailwind colors black: colors.black, + current: 'currentColor', + rose: colors.rose, + transparent: 'transparent', white: colors.white, - gray: colors.neutral, - green: colors.green, - blue: colors.blue, - indigo: { + + // Radix colors + amber: buildColor("amber"), + blue: buildColor("blue"), + bronze: buildColor("bronze"), + brown: buildColor("brown"), + crimson: buildColor("crimson"), + cyan: buildColor("cyan"), + gold: buildColor("gold"), + grass: buildColor("grass"), + green: buildColor("green"), + lime: buildColor("lime"), + mauve: buildColor("mauve"), + mint: buildColor("mint"), + olive: buildColor("olive"), + orange: buildColor("orange"), + pink: buildColor("pink"), + plum: buildColor("plum"), + purple: buildColor("purple"), + red: buildColor("red"), + sage: buildColor("sage"), + sand: buildColor("sand"), + sky: buildColor("sky"), + slate: buildColor("slate"), + teal: buildColor("teal"), + tomato: buildColor("tomato"), + violet: buildColor("violet"), + + // Custom colors + gray: buildColor("gray", colors.neutral), + yellow: buildColor("yellow", colors.amber), + indigo: buildColor("indigo", { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', @@ -77,13 +264,36 @@ module.exports = { 700: '#005b8a', 800: '#075985', 900: '#0c4a6e', - }, - red: colors.red, - yellow: colors.amber, - orange: colors.orange, - rose: colors.rose, - purple: colors.purple, - pink: colors.pink + }), + + tomatodark: buildColor("tomatoDark"), + reddark: buildColor("redDark"), + crimsondark: buildColor("crimsonDark"), + pinkdark: buildColor("pinkDark"), + plumdark: buildColor("plumDark"), + purpledark: buildColor("purpleDark"), + violetdark: buildColor("violetDark"), + skydark: buildColor("skyDark"), + indigodark: buildColor("indigoDark"), + bluedark: buildColor("blueDark"), + cyandark: buildColor("cyanDark"), + mintdark: buildColor("mintDark"), + tealdark: buildColor("tealDark"), + greendark: buildColor("greenDark"), + limedark: buildColor("limeDark"), + grassdark: buildColor("grassDark"), + yellowdark: buildColor("yellowDark"), + amberdark: buildColor("amberDark"), + orangedark: buildColor("orangeDark"), + browndark: buildColor("brownDark"), + graydark: buildColor("grayDark"), + mauvedark: buildColor("mauveDark"), + slatedark: buildColor("slateDark"), + sagedark: buildColor("sageDark"), + olivedark: buildColor("oliveDark"), + sanddark: buildColor("sandDark"), + golddark: buildColor("goldDark"), + bronzedark: buildColor("bronzeDark"), } } } diff --git a/templates/config.edn b/templates/config.edn index e1972c2844..9da61e259e 100644 --- a/templates/config.edn +++ b/templates/config.edn @@ -1,137 +1,165 @@ {:meta/version 1 - ;; Currently, we support either "Markdown" or "Org". - ;; This can overwrite your global preference so that - ;; maybe your personal preferred format is Org but you'd - ;; need to use Markdown for some projects. - ;; :preferred-format "" + ;; Set the preferred format. + ;; Available options: + ;; - Markdown (default) + ;; - Org + ;; :preferred-format "Markdown" - ;; Preferred workflow style. - ;; Value is either ":now" for NOW/LATER style, - ;; or ":todo" for TODO/DOING style. + ;; Set the preferred workflow style. + ;; Available options: + ;; - :now for NOW/LATER style (default) + ;; - :todo for TODO/DOING style :preferred-workflow :now - ;; The app will ignore those directories or files. - ;; E.g. :hidden ["/archived" "/test.md" "../assets/archived"] + ;; Exclude directories/files. + ;; Example usage: + ;; :hidden ["/archived" "/test.md" "../assets/archived"] :hidden [] - ;; When creating the new journal page, the app will use your template if there is one. - ;; You only need to input your template name here. + ;; Define the default journal page template. + ;; Enter the template name between the quotes. :default-templates {:journals ""} - ;; Set a custom date format for journal page title - ;; Example: - ;; :journal/page-title-format "EEE, do MMM yyyy" + ;; Set a custom date format for the journal page title. + ;; Default value: "MMM do, yyyy" + ;; e.g., "Jan 19th, 2038" + ;; Example usage e.g., "Tue 19th, Jan 2038" + ;; :journal/page-title-format "EEE do, MMM yyyy" - ;; Whether to enable hover on tooltip preview feature - ;; Default is true, you can also toggle this via setting page + ;; Specify the journal filename format using a valid date format string. + ;; !Warning: + ;; This configuration is not retroactive and affects only new journals. + ;; To show old journal files in the app, manually rename the files in the + ;; journal directory to match the new format. + ;; Default value: "yyyy_MM_dd" + ;; :journal/file-name-format "yyyy_MM_dd" + + ;; Enable tooltip preview on hover. + ;; Default value: true :ui/enable-tooltip? true - ;; Show brackets around page references + ;; Display brackets [[]] around page references. + ;; Default value: true ;; :ui/show-brackets? true - ;; Enable showing the body of blocks when referencing them. + ;; Display all lines of a block when referencing ((block)). + ;; Default value: false :ui/show-full-blocks? false - ;; Expand block references automatically when zoom-in + ;; Automatically expand block references when zooming in. + ;; Default value: true :ui/auto-expand-block-refs? true - ;; Enable Block timestamp + ;; Enable Block timestamps. + ;; Default value: false :feature/enable-block-timestamps? false - ;; Enable remove accents when searching. - ;; After toggle this option, please remember to rebuild your search index by press (cmd+c cmd+s). + ;; Disable accent marks when searching. + ;; After changing this setting, rebuild the search index by pressing (^C ^S). + ;; Default value: true :feature/enable-search-remove-accents? true - ;; Enable journals + ;; Enable journals. + ;; Default value: true ;; :feature/enable-journals? true - ;; Enable flashcards + ;; Enable flashcards. + ;; Default value: true ;; :feature/enable-flashcards? true - ;; Enable Whiteboards + ;; Enable whiteboards. + ;; Default value: true ;; :feature/enable-whiteboards? true - ;; Disable the built-in Scheduled tasks and deadlines query - ;; :feature/disable-scheduled-and-deadline-query? true + ;; Disable the journal's built-in 'Scheduled tasks and deadlines' query. + ;; Default value: false + ;; :feature/disable-scheduled-and-deadline-query? false - ;; Specify the number of days in the future to display in the - ;; scheduled tasks and deadlines query, with a default value of 7 which - ;; displays tasks for the next 7 days. + ;; Specify the number of days displayed in the future for + ;; the 'scheduled tasks and deadlines' query. ;; Example usage: - ;; Display all scheduled and deadline blocks for the next 14 days + ;; Display all scheduled and deadline blocks for the next 14 days: ;; :scheduled/future-days 14 + ;; Default value: 7 + ;; :scheduled/future-days 7 - ;; Specify the date on which the week starts. - ;; Goes from 0 to 6 (Monday to Sunday), default to 6 + ;; Specify the first day of the week. + ;; Available options: + ;; - integer from 0 to 6 (Monday to Sunday) + ;; Default value: 6 (Sunday) :start-of-week 6 - ;; Specify a custom CSS import - ;; This option take precedence over your local `logseq/custom.css` file - ;; You may find a list of awesome logseq themes here: - ;; https://github.com/logseq/awesome-logseq#css-themes - ;; Example: + ;; Specify a custom CSS import. + ;; This option takes precedence over the local `logseq/custom.css` file. + ;; Example usage: ;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');" - ;; Specify a custom js import - ;; This option take precedence over your local `logseq/custom.js` file - ;; :custom-js-url "" + ;; Specify a custom JS import. + ;; This option takes precedence over the local `logseq/custom.js` file. + ;; Example usage: + ;; :custom-js-url "https://cdn.logseq.com/custom.js" ;; Set a custom Arweave gateway - ;; Default gateway: https://arweave.net - ;; :arweave/gateway "" + ;; Default value: https://arweave.net + ;; :arweave/gateway "https://arweave.net" - ;; Set Bullet indentation when exporting - ;; default option: tab - ;; Possible options for `:export/bullet-indentation` are - ;; 1. `:eight-spaces` as eight spaces - ;; 2. `:four-spaces` as four spaces - ;; 3. `:two-spaces` as two spaces + ;; Set bullet indentation when exporting + ;; Available options: + ;; - `:eight-spaces` as eight spaces + ;; - `:four-spaces` as four spaces + ;; - `:two-spaces` as two spaces + ;; - `:tab` as a tab character (default) ;; :export/bullet-indentation :tab - ;; When :all-pages-public? true, export repo would export all pages within that repo. - ;; Regardless of whether you've set any page to public or not. - ;; Example: - ;; :publishing/all-pages-public? true + ;; Publish all pages within the Graph + ;; Regardless of whether individual pages have been marked as public. + ;; Default value: false + ;; :publishing/all-pages-public? false - ;; Specify default home page and sidebar status for Logseq - ;; If not specified, Logseq default opens journals page on startup - ;; value for `:page` is name of page - ;; Possible options for `:sidebar` are - ;; 1. `"Contents"` to open up `Contents` in sidebar by default - ;; 2. `page name` to open up some page in sidebar - ;; 3. Or multiple pages in an array ["Contents" "Page A" "Page B"] - ;; If `:sidebar` is not set, sidebar will be hidden - ;; Example: - ;; 1. Setup page "Changelog" as home page and "Contents" in sidebar + ;; Define the default home page and sidebar status. + ;; If unspecified, the journal page will be loaded on startup and the right sidebar will stay hidden. + ;; The `:page` value represents the name of the page displayed at startup. + ;; Available options for `:sidebar` are: + ;; - "Contents" to display the Contents page in the right sidebar. + ;; - A specific page name to display in the right sidebar. + ;; - An array of multiple pages, e.g., ["Contents" "Page A" "Page B"]. + ;; If `:sidebar` remains unset, the right sidebar will stay hidden. + ;; Examples: + ;; 1. Set "Changelog" as the home page and display "Contents" in the right sidebar: ;; :default-home {:page "Changelog", :sidebar "Contents"} - ;; 2. Setup page "Jun 3rd, 2021" as home page without sidebar + ;; 2. Set "Jun 3rd, 2021" as the home page without the right sidebar: ;; :default-home {:page "Jun 3rd, 2021"} - ;; 3. Setup page "home" as home page with multiple pages in sidebar - ;; :default-home {:page "home" :sidebar ["page a" "page b"]} + ;; 3. Set "home" as the home page and display multiple pages in the right sidebar: + ;; :default-home {:page "home", :sidebar ["Page A" "Page B"]} - ;; Tell logseq to use a specific folder in the repo as a default location for notes - ;; if not specified, notes are stored in `pages` directory - ;; :pages-directory "your-directory" + ;; Set the default location for storing notes. + ;; Default value: "pages" + ;; :pages-directory "pages" - ;; Tell logseq to use a specific folder in the repo as a default location for journals - ;; if not specified, journals are stored in `journals` directory - ;; :journals-directory "your-directory" + ;; Set the default location for storing journals. + ;; Default value: "journals" + ;; :journals-directory "journals" - ;; Set this to true will convert - ;; `[[Grant Ideas]]` to `[[file:./grant_ideas.org][Grant Ideas]]` for org-mode - ;; For more, see https://github.com/logseq/logseq/issues/672 - ;; :org-mode/insert-file-link? true + ;; Set the default location for storing whiteboards. + ;; Default value: "whiteboards" + ;; :whiteboards-directory "whiteboards" - ;; Setup custom shortcuts under `:shortcuts` key + ;; Enabling this option converts + ;; [[Grant Ideas]] to [[file:./grant_ideas.org][Grant Ideas]] for org-mode. + ;; For more information, visit https://github.com/logseq/logseq/issues/672 + ;; :org-mode/insert-file-link? false + + ;; Configure custom shortcuts. ;; Syntax: - ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a` - ;; 2. ` ` empty space between keys represents key chords. eg: `t s` means press `t` followed by `s` - ;; 3. `mod` means `Ctrl` for Windows/Linux and `Command` for Mac - ;; 4. use `false` to disable particular shortcut - ;; 5. you can define multiple bindings for one action, eg `["ctrl+j" "down"]` - ;; full list of configurable shortcuts are available below: + ;; 1. + indicates simultaneous key presses, e.g., `Ctrl+Shift+a`. + ;; 2. A space between keys represents key chords, e.g., `t s` means + ;; pressing `t` followed by `s`. + ;; 3. mod refers to `Ctrl` for Windows/Linux and `Command` for Mac. + ;; 4. Use false to disable a specific shortcut. + ;; 5. You can define multiple bindings for a single action, e.g., ["ctrl+j" "down"]. + ;; The full list of configurable shortcuts is available at: ;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs ;; Example: ;; :shortcuts @@ -146,33 +174,38 @@ ;; :editor/right ["ctrl+l" "right"]} :shortcuts {} - ;; By default, pressing `Enter` in the document mode will create a new line. - ;; Set this to `true` so that it's the same behaviour as the usual outliner mode. + ;; Configure the behavior of pressing Enter in document mode. + ;; if set to true, pressing Enter will create a new block. + ;; Default value: false :shortcut/doc-mode-enter-for-new-block? false ;; Block content larger than `block/content-max-length` will not be searchable ;; or editable for performance. + ;; Default value: 10000 :block/content-max-length 10000 - ;; Whether to show command doc on hover + ;; Display command documentation on hover. + ;; Default value: true :ui/show-command-doc? true - ;; Whether to show empty bullets for non-document mode (the default mode) + ;; Display empty bullet points. + ;; Default value: false :ui/show-empty-bullets? false - ;; Pre-defined :view function to use with advanced queries + ;; Pre-defined :view function to use with advanced queries. :query/views {:pprint (fn [r] [:pre.code (pprint r)])} - ;; Pre-defined :result-transform function for use with advanced queries + ;; Advanced queries `:result-transform` function. + ;; Transform the query result before displaying it. :query/result-transforms {:sort-by-priority (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))} - ;; The app will show those queries in today's journal page, - ;; the "NOW" query asks the tasks which need to be finished "now", - ;; the "NEXT" query asks the future tasks. + ;; The following queries will be displayed at the bottom of today's journal page. + ;; The "NOW" query returns tasks with "NOW" or "DOING" status. + ;; The "NEXT" query returns tasks with "NOW", "LATER", or "TODO" status. :default-queries {:journals [{:title "🔨 NOW" @@ -207,25 +240,26 @@ :group-by-page? false :collapsed? false}]} - ;; Add your own commands to slash menu to speedup. - ;; E.g. + ;; Add custom commands to the command palette + ;; Example usage: ;; :commands ;; [ - ;; ["js" "Javascript"] - ;; ["md" "Markdown"] - ;; ] - :commands - [] + ;; ["js" "Javascript"] + ;; ["md" "Markdown"] + ;; ] + :commands [] - ;; By default, a block can only be collapsed if it has some children. - ;; `:outliner/block-title-collapse-enabled? true` enables a block with a title - ;; (multiple lines) can be collapsed too. For example: + ;; Enable collapsing blocks with titles but no children. + ;; By default, only blocks with children can be collapsed. + ;; Setting `:outliner/block-title-collapse-enabled?` to true allows collapsing + ;; blocks with titles (multiple lines) and content. For example: ;; - block title ;; block content + ;; Default value: false :outliner/block-title-collapse-enabled? false ;; Macros replace texts and will make you more productive. - ;; For example: + ;; Example usage: ;; Change the :macros value below to: ;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."} ;; input "{{poem red,blue}}" @@ -233,119 +267,148 @@ ;; Rose is red, violet's blue. Life's ordered: Org assists you. :macros {} - ;; The default level to be opened for the linked references. - ;; For example, if we have some example blocks like this: + ;; Configure the default expansion level for linked references. + ;; For example, consider the following block hierarchy: ;; - a [[page]] (level 1) ;; - b (level 2) ;; - c (level 3) ;; - d (level 4) ;; - ;; With the default value of level 2, `b` will be collapsed. - ;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed. + ;; With the default value of level 2, block b will be collapsed. + ;; If the level's value is set to 3, block c will be collapsed. + ;; Default value: 2 :ref/default-open-blocks-level 2 + ;; Configure the threshold for linked references before collapsing. + ;; Default value: 50 :ref/linked-references-collapsed-threshold 50 + ;; Graph view configuration. + ;; Example usage: + ;; :graph/settings + ;; {:orphan-pages? true ; Default value: true + ;; :builtin-pages? false ; Default value: false + ;; :excluded-pages? false ; Default value: false + ;; :journal? false} ; Default value: false + ;; Favorites to list on the left sidebar :favorites [] - ;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5) + ;; Set flashcards interval. + ;; Expected value: + ;; - Float between 0 and 1 + ;; higher values result in faster changes to the next review interval. + ;; Default value: 0.5 ;; :srs/learning-fraction 0.5 - ;; the initial interval after the first successful review of a card (default 4) + ;; Set the initial interval after the first successful review of a card. + ;; Default value: 4 ;; :srs/initial-interval 4 - ;; hide specific properties for blocks - ;; E.g. :block-hidden-properties #{:created-at :updated-at} - ;; :block-hidden-properties #{} + ;; Hide specific block properties. + ;; Example usage: + ;; :block-hidden-properties #{:public :icon} - ;; Enable all your properties to have corresponding pages + ;; Create a page for all properties. + ;; Default value: true :property-pages/enabled? true ;; Properties to exclude from having property pages - ;; E.g.:property-pages/excludelist #{:duration :author} - ;; :property-pages/excludelist + ;; Example usage: + ;; :property-pages/excludelist #{:duration :author} ;; By default, property value separated by commas will not be treated as ;; page references. You can add properties to enable it. - ;; E.g. :property/separated-by-commas #{:alias :tags} - ;; :property/separated-by-commas #{} + ;; Example usage: + ;; :property/separated-by-commas #{:alias :tags} ;; Properties that are ignored when parsing property values for references - ;; :ignored-page-references-keywords #{:author :startup} - - ;; logbook setup + ;; Example usage: + ;; :ignored-page-references-keywords #{:author :website} + + ;; logbook configuration. ;; :logbook/settings ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated ;; :enabled-in-all-blocks true ;display logbook in all blocks after timetracking ;; :enabled-in-timestamped-blocks false ;don't display logbook at all ;; } - ;; Mobile photo uploading setup + ;; Mobile photo upload configuration. ;; :mobile/photo ;; {:allow-editing? true - ;; :quality 80} + ;; :quality 80} ;; Mobile features options ;; Gestures - ;; :mobile + ;; Example usage: + ;; :mobile ;; {:gestures/disabled-in-block-with-tags ["kanban"]} ;; Extra CodeMirror options ;; See https://codemirror.net/5/doc/manual.html#config for possible options - ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true} + ;; Example usage: + ;; :editor/extra-codemirror-options + ;; {:lineWrapping false ; Default value: false + ;; :lineNumbers true ; Default value: true + ;; :readOnly false} ; Default value: false ;; Enable logical outdenting - ;; :editor/logical-outdenting? true + ;; Default value: false + ;; :editor/logical-outdenting? false - ;; When both text and a file are in the clipboard, paste the file - ;; :editor/preferred-pasting-file? true + ;; Prefer pasting the file when text and a file are in the clipboard. + ;; Default value: false + ;; :editor/preferred-pasting-file? false - ;; Quick capture templates for receiving contents from other apps. + ;; Quick capture templates for receiving content from other apps. ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded - ;; by received contents from other apps. Note: the {} cannot be omitted. + ;; by receiving content from other apps. Note: the {} cannot be omitted. ;; - {time}: capture time ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference ;; - {text}: text that users selected before sharing. - ;; - {url}: url or assets path for media files stored in Logseq. - ;; You can also reorder them, or even only use one or two of them in the template. - ;; You can also insert or format any text in the template as shown in the following examples. + ;; - {url}: URL or assets path for media files stored in Logseq. + ;; You can also reorder them or use only one or two of them in the template. + ;; You can also insert or format any text in the template, as shown in the following examples. ;; :quick-capture-templates ;; {:text "[[quick capture]] **{time}**: {text} from {url}" ;; :media "[[quick capture]] **{time}**: {url}"} - ;; Quick capture options - ;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page "my page"} + ;; Quick capture options. + ;; - insert-today? Insert the capture at the end of today's journal page (boolean). + ;; - redirect-page? Redirect to the quick capture page after capturing (boolean). + ;; - default-page The default page to capture to if insert-today? is false (string). + ;; :quick-capture-options + ;; {:insert-today? false ;; Default value: true + ;; :redirect-page? false ;; Default value: false + ;; :default-page "quick capture"} ;; Default page: "quick capture" ;; File sync options ;; Ignore these files when syncing, regexp is supported. ;; :file-sync/ignore-files [] - ;; dwim (do what I mean) for Enter key when editing. - ;; Context-awareness of Enter key makes editing more easily - ; :dwim/settings { - ; :admonition&src? true - ; :markup? false - ; :block-ref? true - ; :page-ref? true - ; :properties? true - ; :list? true - ; } + ;; Configure the Enter key behavior for + ;; context-aware editing with DWIM (Do What I Mean). + ;; context-aware Enter key behavior implies that pressing Enter will + ;; have different outcomes based on the context. + ;; For instance, pressing Enter within a list generates a new list item, + ;; whereas pressing Enter in a block reference opens the referenced block. + ;; :dwim/settings + ;; {:admonition&src? true ;; Default value: true + ;; :markup? false ;; Default value: false + ;; :block-ref? true ;; Default value: true + ;; :page-ref? true ;; Default value: true + ;; :properties? true ;; Default value: true + ;; :list? false} ;; Default value: false - ;; Decide the way to escape the special characters in the page title. + ;; Configure the escaping method for special characters in page titles. ;; Warning: - ;; This is a dangerous operation. If you want to change the setting, - ;; should access the setting `Filename format` and follow the instructions. - ;; Or you have to rename all the affected files manually then re-index on all - ;; clients after the files are synced. Wrong handling may cause page titles - ;; containing special characters to be messy. - ;; Available values: - ;; :file/name-format :triple-lowbar - ;; ;use triple underscore `___` for slash `/` in page title - ;; ;use Percent-encoding for other invalid characters - :file/name-format :triple-lowbar - - ;; specify the format of the filename for journal files - ;; :journal/file-name-format "yyyy_MM_dd" - - } + ;; This is a dangerous operation. To modify the setting, + ;; access the 'Filename format' setting and follow the instructions. + ;; Otherwise, You may need to manually rename all affected files and + ;; re-index them on all clients after synchronization. + ;; Incorrect handling may result in messy page titles. + ;; Available options: + ;; - :triple-lowbar (default) + ;; ;use triple underscore `___` for slash `/` in page title + ;; ;use Percent-encoding for other invalid characters + :file/name-format :triple-lowbar} diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx index 5136393259..ce6f2197f1 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx @@ -133,9 +133,7 @@ const LogseqPortalViewModeAction = observer(() => {
{collapsed ? 'Expand' : 'Collapse'}
) @@ -253,7 +251,7 @@ const NoFillAction = observer(() => { const app = useApp() const shapes = filterShapeByAction('NoFill') const handleChange = React.useCallback((v: boolean) => { - shapes.forEach(s => s.update({ noFill: v })) + app.selectedShapesArray.forEach(s => s.update({ noFill: v })) app.persist() }, []) @@ -279,14 +277,14 @@ const SwatchAction = observer(() => { >('Swatch') const handleSetColor = React.useCallback((color: string) => { - shapes.forEach(s => { + app.selectedShapesArray.forEach(s => { s.update({ fill: color, stroke: color }) }) app.persist() }, []) const handleSetOpacity = React.useCallback((opacity: number) => { - shapes.forEach(s => { + app.selectedShapesArray.forEach(s => { s.update({ opacity: opacity }) }) app.persist() diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx index 0ae9138f3a..12beea3ee1 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx @@ -262,7 +262,7 @@ export const ContextMenu = observer(function ContextMenu({ Deselect all )} - {app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => !s.props.isLocked) && ( + {!app.readOnly && app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => !s.props.isLocked) && ( runAndTransition(() => app.setLocked(true))} @@ -272,7 +272,7 @@ export const ContextMenu = observer(function ContextMenu({ )} - {app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => s.props.isLocked) && ( + {!app.readOnly && app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => s.props.isLocked) && ( runAndTransition(() => app.setLocked(false))} diff --git a/tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx b/tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx index be24bb2a8f..f992d16aa1 100644 --- a/tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx @@ -56,6 +56,7 @@ export function SelectInput({ position="popper" sideOffset={14} align="center" + onKeyDown={e => e.stopPropagation()} > diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx index 3fb4a3c512..9930befbf7 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx @@ -98,7 +98,11 @@ export class EllipseShape extends TLEllipseShape { ) return ( -
+
{ ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => { const ref = React.useRef(null) + const app = useApp() return ( {
diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx index 25f64a0955..da56ea47b8 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx @@ -166,8 +166,6 @@ export class TextShape extends TLTextShape { elm.select() } }) - } else { - onEditingEnd?.() } }, [isEditing, onEditingEnd]) diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx index 77d560ef5b..25b237c470 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx @@ -85,7 +85,7 @@ export class TweetShape extends TLBoxShape {
diff --git a/tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx b/tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx index 0d636fd5ca..0efecdbbe2 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx +++ b/tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core' -import { HTMLContainer, TLComponentProps } from '@tldraw/react' +import { HTMLContainer, TLComponentProps, useApp} from '@tldraw/react' import { action, computed } from 'mobx' import { observer } from 'mobx-react-lite' import { withClampedStyles } from './style-props' @@ -45,6 +45,8 @@ export class YouTubeShape extends TLBoxShape { } ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => { + const app = useApp() + return ( {
) => { const { selectedIds, - selectedShapes, inputs: { shiftKey }, } = this.app - if (info.type === TLTargetType.Shape && !selectedShapes.has(info.shape)) { + if (info.type === TLTargetType.Shape && !selectedIds.has(info.shape.id)) { const shape = this.app.getParentGroup(info.shape) ?? info.shape if (shiftKey) { this.app.setSelectedShapes([...Array.from(selectedIds.values()), shape.id]) diff --git a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts index b5d49cb096..07052cbe9e 100644 --- a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts +++ b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts @@ -125,7 +125,6 @@ export class TranslatingState< onExit = () => { // Resume the history when we exit this.app.history.resume() - this.app.persist() // Reset initial data this.didClone = false diff --git a/yarn.lock b/yarn.lock index e06e39c053..3b5f4db4f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -733,6 +733,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@radix-ui/colors@^0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-0.1.8.tgz#b08c62536fc462a87632165fb28e9b18f9bd047e" + integrity sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw== + "@sentry/browser@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f" @@ -7053,6 +7058,11 @@ table@^6.6.0: string-width "^4.2.3" strip-ansi "^6.0.1" +tailwind-capitalize-first-letter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tailwind-capitalize-first-letter/-/tailwind-capitalize-first-letter-1.0.4.tgz#d7a07c1dda4a7555f2240d57154df394b0ee8db6" + integrity sha512-ZB8hBi68JI4aQ1cDUxuFWfMYTxgBvlzIdPPHSkFkMUlo7p2QlbMy0hVv/vAREAFmkUh9QfjuKQnOSbe4Gnqljg== + tailwindcss@3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.8.tgz#4f8520550d67a835d32f2f4021580f9fddb7b741"