shui/table introduction

This commit is contained in:
Ben Yorke
2023-01-19 19:40:04 +01:00
committed by Gabriel Horner
parent 42dd084544
commit 928742e66b
37 changed files with 1883 additions and 187 deletions

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

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

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

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

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

@@ -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.96.0",
@@ -133,6 +134,7 @@
"remove-accents": "0.4.2",
"sanitize-filename": "1.6.3",
"send-intent": "3.0.11",
"tailwind-capitalize-first-letter": "^1.0.4",
"threads": "1.6.5",
"url": "^0.11.0",
"yargs-parser": "20.2.4"

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

@@ -236,10 +236,10 @@
(cond
(and (util/electron?) (config/local-db? (state/get-current-repo)))
["Upload an asset" [[:editor/click-hidden-file-input :id]] "Upload file types like image, pdf, docx, etc.)"]
["Upload an asset" [[:editor/click-hidden-file-input :id]] "Upload file types like image, pdf, docx, etc.)"])]
;; ["Upload an image" [[:editor/click-hidden-file-input :id]]]
)]
(headings)

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]
@@ -52,6 +52,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]
@@ -73,6 +74,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]
@@ -292,8 +294,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
@@ -926,8 +928,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]
@@ -1106,8 +1108,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
@@ -1200,8 +1202,8 @@
(cond->
{:href (ar-url->http-url href)
:target "_blank"}
title
(assoc :title title))
title
(assoc :title title))
(map-inline config label))
:else
@@ -1210,11 +1212,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]
@@ -1223,7 +1225,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
@@ -2247,7 +2250,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]
@@ -2312,19 +2315,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]
@@ -3002,8 +3005,8 @@
:li
(cond->
{:checked checked?}
number
(assoc :value number))
number
(assoc :value number))
(vec-cat
[(->elem
:p
@@ -3021,52 +3024,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)))
@@ -3091,6 +3097,8 @@
[config col]
(map #(inline config %) col))
(declare ->hiccup)
(rum/defc src-cp < rum/static
[config options html-export?]
(when options

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

@@ -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?}))))
@@ -1244,8 +1244,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"
@@ -2735,7 +2735,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
@@ -3178,9 +3178,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

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

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

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

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

@@ -1,4 +1,29 @@
const plugin = require('tailwindcss/plugin')
const colors = require('tailwindcss/colors')
const radix = require('@radix-ui/colors')
const gradientColors = {
tomato: ["amber", "orange", "tomato", "red", "crimson"],
red: ["orange", "tomato", "red", "crimson", "pink"],
crimson: ["tomato", "red", "crimson", "pink", "plum"],
pink: ["red", "crimson", "pink", "plum", "purple"],
plum: ["crimson", "pink", "plum", "purple", "violet"],
purple: ["pink", "plum", "purple", "violet", "indigo"],
violet: ["plum", "purple", "violet", "indigo", "blue"],
indigo: ["purple", "violet", "indigo", "blue", "cyan"],
blue: ["violet", "indigo", "blue", "cyan", "teal"],
// sky: ["indigo", "blue", "sky", "cyan", "teal"],
cyan: ["indigo", "blue", "cyan", "teal", "green"],
teal: ["blue", "cyan", "teal", "green", "grass"],
// mint: ["cyan", "teal", "mint", "green", "grass"],
green: ["cyan", "teal", "green", "grass", "amber"],
grass: ["teal", "green", "grass", "amber", "orange"],
// lime: ["green", "grass", "lime", "yellow", "amber"],
// yellow: ["grass", "lime", "yellow", "amber", "orange"],
amber: ["green", "grass", "amber", "orange", "tomato"],
orange: ["grass", "amber", "orange", "tomato", "red"],
// brown: ["green", "grass", "brown", "tomato", "red"],
}
function exposeColorsToCssVars ({ addBase, theme }) {
function extractColorVars (colorObj, colorGroup = '') {
@@ -19,28 +44,160 @@ function exposeColorsToCssVars ({ addBase, theme }) {
})
}
function buildColor(color, custom) {
const base = custom || colors[color] || {}
for (const [xName, xValue] of Object.entries(radix[color] || {})) {
const regexResult = xName.match(/\d+$/)
if (!regexResult) { continue; }
const xStep = regexResult[0]
base[xStep] = xValue
}
return base
}
// this will allow us to use gradient color functions in the ui:
// grad-bg-tomato-3 OR grad-bg-tomato-3-alpha
// it will also loop through all 5 color stops, unless the stops are specified
// grad-bg-stops-3
// this will have a default repeating gradient at a step that can be configured with
// grad-bg-cycle-32
const addGradientColors = plugin(({ addBase, addComponents, addUtilities, config, ___theme }) => {
const dark = getDarkSelector(config)
addUtilities({
['.grad-bg-stops-3']: {
'--grad-bg-stops': "var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)",
},
['.grad-bg-stops-5']: {
'--grad-bg-stops': "var(--grad-bg-stop-a), var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d), var(--grad-bg-stop-e)",
},
['.grad-bg-cycle-12']: {
'background-image': 'repeatint-linear-gradient(to right, var(--grad-bg-stops))',
},
})
Object.values(gradientColors).forEach((stops, ___index) => {
const baseColor = stops[2]
const color = (scale, stopIndex = 2, suffix = "") => `--color-${stops[stopIndex]}${suffix}-${scale}`
addComponents({
// tailwind componnent for .grad-bg-COLOR-9
[`.grad-bg-${baseColor}-9`]: {
"--grad-bg-stop-a": `var(${color(9, 0)})`,
"--grad-bg-stop-b": `var(${color(9, 1)})`,
"--grad-bg-stop-c": `var(${color(9, 2)})`,
"--grad-bg-stop-d": `var(${color(9, 3)})`,
"--grad-bg-stop-e": `var(${color(9, 4)})`,
"--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`,
"background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`,
[dark]: {
"--grad-bg-stop-a": `var(${color(9, 0, "dark")})`,
"--grad-bg-stop-b": `var(${color(9, 1, "dark")})`,
"--grad-bg-stop-c": `var(${color(9, 2, "dark")})`,
"--grad-bg-stop-d": `var(${color(9, 3, "dark")})`,
"--grad-bg-stop-e": `var(${color(9, 4, "dark")})`,
}
},
// tailwind component for .grad-bg-COLOR-9-alpha
[`.grad-bg-${baseColor}-9-alpha`]: {
"--grad-bg-stop-a": `var(${color(9, 0)})`,
"--grad-bg-stop-b": `var(${color(9, 1)})`,
"--grad-bg-stop-c": `var(${color(9, 2)})`,
"--grad-bg-stop-d": `var(${color(9, 3)})`,
"--grad-bg-stop-e": `var(${color(9, 4)})`,
"--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`,
"background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`,
[dark]: {
"--grad-bg-stop-a": `var(${color(9, 0, "dark")})`,
"--grad-bg-stop-b": `var(${color(9, 1, "dark")})`,
"--grad-bg-stop-c": `var(${color(9, 2, "dark")})`,
"--grad-bg-stop-d": `var(${color(9, 3, "dark")})`,
"--grad-bg-stop-e": `var(${color(9, 4, "dark")})`,
}
},
})
})
})
function getDarkSelector(config) {
const darkMode = config("darkMode");
const prefix = config("prefix");
if (Array.isArray(darkMode)) {
if (darkMode.length < 2) {
throw new Error(
"To customize the dark mode selector, `darkMode` should contain two items. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name"
);
}
if (darkMode[0] !== "class") {
throw new Error(
'To customize the dark mode selector, `darkMode` should have "class" as its first item. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name'
);
}
return darkMode[1] + " &";
}
if (darkMode === "media") {
return "@media (prefers-color-scheme: dark)";
}
if (darkMode !== "class") {
throw new Error(
"Invalid `darkMode`. Documentation: https://tailwindcss.com/docs/dark-mode"
);
}
if (prefix) {
return `[class~="${prefix}dark"] &`;
}
return '[class~="dark"] &';
}
module.exports = {
darkMode: 'class',
content: [
'./src/**/*.js',
'./src/**/*.cljs',
'./resources/**/*.html'
'./resources/**/*.html',
'./deps/shui/src/**/*.cljs',
],
safelist: [
'bg-black', 'bg-white',
'bg-black', 'bg-white', 'capitalize-first',
{ pattern: /bg-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ },
{ pattern: /text-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ },
{ pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ }
{ pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ },
{ pattern: /bg-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /shadow-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /text-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /ring-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /from-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /via-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /to-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
{ pattern: /border-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(4|5|6|7|8)/ },
],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/line-clamp'),
require('tailwind-capitalize-first-letter'),
addGradientColors,
exposeColorsToCssVars
],
theme: {
extend: {
backgroundImage: {
'gradient-conic': 'conic-gradient(var(--tw-gradient-stops))',
'gradient-conic-bounce': 'conic-gradient(var(--tw-gradient-from), var(--tw-gradient-via), var(--tw-gradient-to), var(--tw-gradient-via), var(--tw-gradient-from))',
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
},
fontSize: {
'2xs': ['0.625rem', '0.875rem']
},
@@ -59,14 +216,44 @@ module.exports = {
}
},
colors: {
transparent: 'transparent',
current: 'currentColor',
// Tailwind colors
black: colors.black,
current: 'currentColor',
rose: colors.rose,
transparent: 'transparent',
white: colors.white,
gray: colors.neutral,
green: colors.green,
blue: colors.blue,
indigo: {
// Radix colors
amber: buildColor("amber"),
blue: buildColor("blue"),
bronze: buildColor("bronze"),
brown: buildColor("brown"),
crimson: buildColor("crimson"),
cyan: buildColor("cyan"),
gold: buildColor("gold"),
grass: buildColor("grass"),
green: buildColor("green"),
lime: buildColor("lime"),
mauve: buildColor("mauve"),
mint: buildColor("mint"),
olive: buildColor("olive"),
orange: buildColor("orange"),
pink: buildColor("pink"),
plum: buildColor("plum"),
purple: buildColor("purple"),
red: buildColor("red"),
sage: buildColor("sage"),
sand: buildColor("sand"),
sky: buildColor("sky"),
slate: buildColor("slate"),
teal: buildColor("teal"),
tomato: buildColor("tomato"),
violet: buildColor("violet"),
// Custom colors
gray: buildColor("gray", colors.neutral),
yellow: buildColor("yellow", colors.amber),
indigo: buildColor("indigo", {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
@@ -77,13 +264,36 @@ module.exports = {
700: '#005b8a',
800: '#075985',
900: '#0c4a6e',
},
red: colors.red,
yellow: colors.amber,
orange: colors.orange,
rose: colors.rose,
purple: colors.purple,
pink: colors.pink
}),
tomatodark: buildColor("tomatoDark"),
reddark: buildColor("redDark"),
crimsondark: buildColor("crimsonDark"),
pinkdark: buildColor("pinkDark"),
plumdark: buildColor("plumDark"),
purpledark: buildColor("purpleDark"),
violetdark: buildColor("violetDark"),
skydark: buildColor("skyDark"),
indigodark: buildColor("indigoDark"),
bluedark: buildColor("blueDark"),
cyandark: buildColor("cyanDark"),
mintdark: buildColor("mintDark"),
tealdark: buildColor("tealDark"),
greendark: buildColor("greenDark"),
limedark: buildColor("limeDark"),
grassdark: buildColor("grassDark"),
yellowdark: buildColor("yellowDark"),
amberdark: buildColor("amberDark"),
orangedark: buildColor("orangeDark"),
browndark: buildColor("brownDark"),
graydark: buildColor("grayDark"),
mauvedark: buildColor("mauveDark"),
slatedark: buildColor("slateDark"),
sagedark: buildColor("sageDark"),
olivedark: buildColor("oliveDark"),
sanddark: buildColor("sandDark"),
golddark: buildColor("goldDark"),
bronzedark: buildColor("bronzeDark"),
}
}
}

View File

@@ -733,6 +733,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@radix-ui/colors@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-0.1.8.tgz#b08c62536fc462a87632165fb28e9b18f9bd047e"
integrity sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw==
"@sentry/browser@6.19.7":
version "6.19.7"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f"
@@ -7048,6 +7053,11 @@ table@^6.6.0:
string-width "^4.2.3"
strip-ansi "^6.0.1"
tailwind-capitalize-first-letter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tailwind-capitalize-first-letter/-/tailwind-capitalize-first-letter-1.0.4.tgz#d7a07c1dda4a7555f2240d57154df394b0ee8db6"
integrity sha512-ZB8hBi68JI4aQ1cDUxuFWfMYTxgBvlzIdPPHSkFkMUlo7p2QlbMy0hVv/vAREAFmkUh9QfjuKQnOSbe4Gnqljg==
tailwindcss@3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.8.tgz#4f8520550d67a835d32f2f4021580f9fddb7b741"