Merge branch 'master' into feat/ai-lab

This commit is contained in:
Tienson Qin
2023-05-18 15:45:52 +08:00
114 changed files with 3139 additions and 803 deletions

View File

@@ -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

5
.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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"}

View File

@@ -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.

View File

@@ -105,7 +105,6 @@
(def retract-attributes
#{
:block/refs
:block/path-refs
:block/tags
:block/alias
:block/marker

View File

@@ -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

View File

@@ -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

View File

@@ -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)]}

View File

@@ -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]))]

View File

@@ -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

View File

@@ -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))

View File

@@ -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&section-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]]

17
deps/shui/.clj-kondo/config.edn vendored Normal file
View File

@@ -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}}

29
deps/shui/README.md vendored Normal file
View File

@@ -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.

1
deps/shui/deps.edn vendored Normal file
View File

@@ -0,0 +1 @@
{:paths ["src"]}

View File

@@ -0,0 +1,2 @@
-
-

348
deps/shui/shui-graph/logseq/config.edn vendored Normal file
View File

@@ -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"

View File

View File

@@ -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

3
deps/shui/shui-graph/pages/Page 1.md vendored Normal file
View File

@@ -0,0 +1,3 @@
table-example:: true
-

1
deps/shui/shui-graph/pages/Page 2.md vendored Normal file
View File

@@ -0,0 +1 @@
table-example:: true

1
deps/shui/shui-graph/pages/Page 3.md vendored Normal file
View File

@@ -0,0 +1 @@
table-example:: true

View File

@@ -0,0 +1,4 @@
- [[About Shui]]
- [[shui/components]]
- [[shui/components/table]]
-

View File

@@ -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.
-

View File

@@ -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 | <any number> (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 }}

36
deps/shui/src/logseq/shui/context.cljs vendored Normal file
View File

@@ -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})

11
deps/shui/src/logseq/shui/core.cljs vendored Normal file
View File

@@ -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)

471
deps/shui/src/logseq/shui/table/v2.cljs vendored Normal file
View File

@@ -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)))))))

81
deps/shui/src/logseq/shui/util.cljs vendored Normal file
View File

@@ -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)))

View File

@@ -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

View File

@@ -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 }) => {

View File

@@ -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 }) => {

View File

@@ -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 () => {

View File

@@ -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</td>')
// 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</div>')
})
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()
})

View File

@@ -206,3 +206,10 @@ export async function getIsWebAPIClipboardSupported(page: Page): Promise<boolean
// @ts-ignore "clipboard-write" is not included in TS's type definition for permissionName
return await queryPermission(page, "clipboard-write") && await doesClipboardItemExists(page)
}
export async function navigateToStartOfBlock(page: Page, block: Block) {
const selectionStart = await block.selectionStart()
for (let i = 0; i < selectionStart; i++) {
await page.keyboard.press('ArrowLeft')
}
}

View File

@@ -83,7 +83,7 @@ test('draw a rectangle', async ({ page }) => {
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)

View File

@@ -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)";

View File

@@ -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: "/")
}
}

View File

@@ -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"
}
}
}

View File

@@ -55,6 +55,7 @@
<script defer src="/static/js/main.js"></script>
<script defer src="/static/js/amplify.js"></script>
<script defer src="/static/js/tabler.min.js"></script>
<script defer src="/static/js/tabler.ext.js"></script>
<script defer src="/static/js/code-editor.js"></script>
<script defer src="/static/js/tldraw.js"></script>
<script defer src="/static/js/excalidraw.js"></script>

View File

@@ -56,6 +56,7 @@ const portal = new MagicPortal(worker);
<script defer src="./js/main.js"></script>
<script defer src="./js/amplify.js"></script>
<script defer src="./js/tabler.min.js"></script>
<script defer src="./js/tabler.ext.js"></script>
<script defer src="./js/code-editor.js"></script>
<script defer src="./js/excalidraw.js"></script>
<script defer src="./js/tldraw.js"></script>

View File

@@ -55,6 +55,7 @@ const portal = new MagicPortal(worker);
<script defer src="./js/main.js"></script>
<script defer src="./js/amplify.js"></script>
<script defer src="./js/tabler.min.js"></script>
<script defer src="./js/tabler.ext.js"></script>
<script defer src="./js/code-editor.js"></script>
<script defer src="./js/excalidraw.js"></script>
<script defer src="./js/tldraw.js"></script>

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -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]}}}}

View File

@@ -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

View File

@@ -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;

View File

@@ -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]]

View File

@@ -588,7 +588,7 @@
(async/go
(set-loading? true)
(try
(let [files (async/<! (file-sync-handler/fetch-page-file-versions graph-uuid page-entity))]
(let [files (async/<! (file-sync-handler/<fetch-page-file-versions graph-uuid page-entity))]
(set-version-files files)
(set-page-fn (first files))
(set-list-ready? true))

View File

@@ -38,7 +38,9 @@
[logseq.graph-parser.util :as gp-util]
[medley.core :as medley]
[reitit.frontend.easy :as rfe]
[rum.core :as rum]))
[rum.core :as rum]
[logseq.graph-parser.util.page-ref :as page-ref]
[logseq.graph-parser.mldoc :as gp-mldoc]))
(defn- get-page-name
[state]
@@ -261,7 +263,7 @@
:else
(state/set-modal! (confirm-fn)))
(util/stop e))]
[:span.absolute.inset-0.edit-input-wrapper
[:span.absolute.inset-0.edit-input-wrapper.z-10
{:class (util/classnames [{:editing @*edit?}])}
[:input.edit-input
{:type "text"
@@ -296,7 +298,8 @@
(assoc state ::title-value (atom (nth (:rum/args state) 2))))}
[state page-name icon title _format fmt-journal?]
(when title
(let [*title-value (get state ::title-value)
(let [page (when page-name (db/entity [:block/name page-name]))
*title-value (get state ::title-value)
*edit? (get state ::edit?)
*input-value (get state ::input-value)
repo (state/get-current-repo)
@@ -305,7 +308,9 @@
untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
title (if hls-page?
[:a.asset-ref (pdf-utils/fix-local-asset-pagename title)]
(if fmt-journal? (date/journal-title->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?]

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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))]))]))]]]))))

View File

@@ -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")])]]

View File

@@ -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."

View File

@@ -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?))

View File

@@ -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?"

View File

@@ -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})

View File

@@ -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)

View File

@@ -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

View File

@@ -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}]

View File

@@ -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})

View File

@@ -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!

View File

@@ -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))))

View File

@@ -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]

View File

@@ -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 <fetch-page-file-versions [graph-uuid page]
[]
(let [file-id (:db/id (:block/file page))]
(when-let [path (:file/path (db/entity file-id))]
(let [base-path (config/get-repo-dir (state/get-current-repo))
base-path (if (string/starts-with? base-path "file://")
(gp-util/safe-decode-uri-component base-path)
base-path)
path* (string/replace-first (string/replace-first path base-path "") #"^/" "")]
(go
(let [version-list (:VersionList
(<! (sync/<get-remote-file-versions sync/remoteapi graph-uuid path*)))
local-version-list (<! (<list-file-local-versions page))
all-version-list (->> (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
(<! (sync/<get-remote-file-versions sync/remoteapi graph-uuid path)))
local-version-list (<! (<list-file-local-versions page))
all-version-list (->> (concat version-list local-version-list)
(sort-by #(or (:CreateTime %)
(:create-time %))
>))]
all-version-list)))))
(defn init-remote-graph

View File

@@ -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))]

View File

@@ -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])

View File

@@ -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

View File

@@ -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?))

View File

@@ -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 (->>

View File

@@ -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#}))))))))

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)))}

View File

@@ -92,7 +92,7 @@
(fn []
(rum/set-ref! *mounted true)
#(rum/set-ref! *mounted false))
[])
[])
#(rum/deref *mounted)))
(defn use-bounding-client-rect

View File

@@ -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?]

View File

@@ -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}))

View File

@@ -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
;; ===============

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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))))

View File

@@ -1,3 +1,3 @@
(ns ^:no-doc frontend.version)
(defonce version "0.9.4")
(defonce version "0.9.6")

View File

@@ -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]

View File

@@ -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)))))))

View File

@@ -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]]"}

View File

@@ -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")

View File

@@ -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)))))))

View File

@@ -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))

View File

@@ -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

View File

@@ -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"

View File

@@ -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))

View File

@@ -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]}])

View File

@@ -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!))

View File

@@ -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))))

View File

@@ -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)

Some files were not shown because too many files have changed in this diff Show More