feat: table columns pinning (#11693)

* Add property :logseq.property.table/pinned-columns

* feat: table column pinning

* enhance: hide :id column by default

---------

Co-authored-by: charlie <xyhp915@qq.com>
This commit is contained in:
Tienson Qin
2025-01-24 11:17:09 +08:00
committed by GitHub
parent 6170ba10d4
commit 926d05c185
18 changed files with 347 additions and 244 deletions

View File

@@ -1,7 +1,7 @@
(ns logseq.db.frontend.db-ident
"Helper fns for class and property :db/ident"
(:require [datascript.core :as d]
[clojure.string :as string]))
(:require [clojure.string :as string]
[datascript.core :as d]))
(defn ensure-unique-db-ident
"Ensures the given db-ident is unique. If a db-ident conflicts, it is made

View File

@@ -451,7 +451,15 @@
:rtc {:rtc/ignore-attr-when-init-upload true
:rtc/ignore-attr-when-init-download true
:rtc/ignore-attr-when-syncing true}}
:logseq.property.table/pinned-columns {:title "Table view pinned columns"
:schema
{:type :property
:cardinality :many
:hide? true
:public? false}
:rtc {:rtc/ignore-attr-when-init-upload true
:rtc/ignore-attr-when-init-download true
:rtc/ignore-attr-when-syncing true}}
:logseq.property/view-for {:title "This view belongs to"
:schema
{:type :node

View File

@@ -86,7 +86,6 @@
{:block/title value}))
common-util/block-with-timestamps)))
(defn build-property-values-tx-m
"Builds a map of property names to their property value blocks to be
transacted, given a block and a properties map with raw property values. The

View File

@@ -2,7 +2,7 @@
"Main datascript schemas for the Logseq app"
(:require [clojure.set :as set]))
(def version 62)
(def version 63)
;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
(def ^:large-vars/data-var schema

View File

@@ -147,11 +147,11 @@
(mark-block-as-built-in
(sqlite-util/build-new-class
(let [class-properties (mapv
(fn [db-ident]
(let [property (get db-ident->properties db-ident)]
(assert property (str "Built-in property " db-ident " is not defined yet"))
db-ident))
(:properties schema))]
(fn [db-ident]
(let [property (get db-ident->properties db-ident)]
(assert property (str "Built-in property " db-ident " is not defined yet"))
db-ident))
(:properties schema))]
(cond->
{:block/title title'
:block/name (common-util/page-name-sanity-lc title')

View File

@@ -6,6 +6,7 @@
["fs/promises" :as fsp]
["os" :as os]
["path" :as node-path]
#_:clj-kondo/ignore
[babashka.cli :as cli]
[cljs.pprint :as pprint]
[clojure.set :as set]
@@ -13,7 +14,6 @@
[datascript.core :as d]
[logseq.common.graph :as common-graph]
[logseq.graph-parser.exporter :as gp-exporter]
#_:clj-kondo/ignore
[logseq.outliner.cli :as outliner-cli]
[logseq.outliner.pipeline :as outliner-pipeline]
[nbb.classpath :as cp]
@@ -24,11 +24,11 @@
(def original-transact! d/transact!)
(defn dev-transact! [conn tx-data tx-meta]
(swap! tx-queue (fn [queue]
(let [new-queue (conj queue {:tx-data tx-data :tx-meta tx-meta})]
(let [new-queue (conj queue {:tx-data tx-data :tx-meta tx-meta})]
;; Only care about last few so vary 10 as needed
(if (> (count new-queue) 10)
(pop new-queue)
new-queue))))
(if (> (count new-queue) 10)
(pop new-queue)
new-queue))))
(original-transact! conn tx-data tx-meta))
(defn- build-graph-files

View File

@@ -2,8 +2,8 @@
"Script that generates all the permutations of property types and cardinality.
Also creates a page of queries that exercises most properties
NOTE: This script is also used in CI to confirm graph creation works"
(:require ["fs-extra$default" :as fse]
["fs" :as fs]
(:require ["fs" :as fs]
["fs-extra$default" :as fse]
["os" :as os]
["path" :as node-path]
[babashka.cli :as cli]

View File

@@ -180,22 +180,21 @@
(state/pub-event! [:editor/new-property {:block class
:class-schema? true}]))
;; Objects of built-in classes must not be deleted e.g. Tag, Property and Root
:on-delete-rows (when-not (:logseq.property/built-in? class)
(fn [table selected-rows]
(let [pages (->> selected-rows (filter ldb/page?) (remove :logseq.property/built-in?))
blocks (->> selected-rows (remove ldb/page?) (remove :logseq.property/built-in?))]
(p/do!
(set-data! (get-class-objects class))
(when-let [f (get-in table [:data-fns :set-row-selection!])]
(f {}))
(ui-outliner-tx/transact!
{:outliner-op :delete-blocks}
(when (seq blocks)
(outliner-op/delete-blocks! blocks nil))
(let [page-ids (map :db/id pages)
tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
(when (seq tx-data)
(outliner-op/transact! tx-data {:outliner-op :save-block}))))))))}))))
:on-delete-rows (fn [table selected-rows]
(let [pages (->> selected-rows (filter ldb/page?) (remove :logseq.property/built-in?))
blocks (->> selected-rows (remove ldb/page?) (remove :logseq.property/built-in?))]
(p/do!
(set-data! (get-class-objects class))
(when-let [f (get-in table [:data-fns :set-row-selection!])]
(f {}))
(ui-outliner-tx/transact!
{:outliner-op :delete-blocks}
(when (seq blocks)
(outliner-op/delete-blocks! blocks nil))
(let [page-ids (map :db/id pages)
tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
(when (seq tx-data)
(outliner-op/transact! tx-data {:outliner-op :save-block})))))))}))))
(rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
[state class {:keys [current-page? sidebar?]}]

View File

@@ -531,25 +531,25 @@
[:span.opacity-60 "effect?"]]]
[:div.flex.justify-end.pt-3
(shui/button
{:on-click (fn []
(if (or (string/blank? (util/trim-safe url))
(not (string/starts-with? url "https://")))
(.focus (rum/deref *input))
(let [url (string/replace-first url "https://github.com/" "")
matched (re-find #"([^\/]+)/([^\/]+)" url)]
(if-let [id (some-> matched (nth 2))]
(do
(set-pending! true)
(-> #js {:id id :repo (first matched)
:theme (:theme? opts)
:effect (:effect? opts)}
(js/window.logseq.api.__install_plugin)
(p/then #(shui/dialog-close!))
(p/catch #(notification/show! (str %) :error))
(p/finally #(set-pending! false))))
(notification/show! "Invalid GitHub repo url" :error)))))
:disabled pending}
(if pending (ui/loading "Installing") "Install"))]]))
{:on-click (fn []
(if (or (string/blank? (util/trim-safe url))
(not (string/starts-with? url "https://")))
(.focus (rum/deref *input))
(let [url (string/replace-first url "https://github.com/" "")
matched (re-find #"([^\/]+)/([^\/]+)" url)]
(if-let [id (some-> matched (nth 2))]
(do
(set-pending! true)
(-> #js {:id id :repo (first matched)
:theme (:theme? opts)
:effect (:effect? opts)}
(js/window.logseq.api.__install_plugin)
(p/then #(shui/dialog-close!))
(p/catch #(notification/show! (str %) :error))
(p/finally #(set-pending! false))))
(notification/show! "Invalid GitHub repo url" :error)))))
:disabled pending}
(if pending (ui/loading "Installing") "Install"))]]))
(rum/defc auto-check-for-updates-control
[]
@@ -979,7 +979,7 @@
(lazy-items-loader load-more-pages!)
[:div.flex.items-center.justify-center.py-28.flex-col.gap-2.opacity-30
(shui/tabler-icon "list-search" {:size 40})
[:span.text-sm "Nothing Founded."]])]]))
[:span.text-sm "Nothing Found."]])]]))
(rum/defcs waiting-coming-updates
< rum/reactive

View File

@@ -356,7 +356,7 @@ a.control-link {
.inner-wrap {
@apply flex items-center w-full justify-between gap-1 flex-wrap;
> strong {
> .property-setting-title {
@apply flex items-center gap-1 font-normal opacity-90;
}

View File

@@ -259,7 +259,7 @@
item-props)
[sub-open? set-sub-open!] (rum/use-state false)
toggle? (boolean? toggle-checked?)
id1 (str (or id icon (random-uuid)))
id1 (str (or id (random-uuid)))
id2 (str "d2-" id1)
or-close-menu-sub! (fn []
(when (and (not (shui-popup/get-popup :ls-icon-picker))
@@ -287,7 +287,7 @@
(wrap-menuitem
[:div.inner-wrap.cursor-pointer
{:class (util/classnames [{:disabled disabled?}])}
[:strong
[:div.property-setting-title
(some-> icon (name) (shui/tabler-icon {:size 14
:style {:margin-top "-1"}}))
[:span title]]
@@ -690,11 +690,14 @@
:logseq.property/hide?
%)}))
(when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
(dropdown-editor-menuitem {:icon :eye-off :title "Hide empty value" :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
:disabled? config/publishing?
:on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
:logseq.property/hide-empty-value
(not (:logseq.property/hide-empty-value property)))}))]
(dropdown-editor-menuitem
{:icon :eye-off :title "Hide empty value"
:toggle-checked? (boolean (:logseq.property/hide-empty-value property))
:disabled? config/publishing?
:on-toggle-checked-change (fn []
(db-property-handler/set-block-property! (:db/id property)
:logseq.property/hide-empty-value
(not (:logseq.property/hide-empty-value property))))}))]
(remove nil?))]
(when (> (count group') 0)
(cons (shui/dropdown-menu-separator) group'))))
@@ -718,15 +721,16 @@
{:icon :checkbox
:title (if class-schema? "Show as checkbox on tagged nodes" "Show as checkbox on node")
:disabled? config/publishing?
:desc (shui/switch
{:id "show as checkbox" :size "sm"
:checked checked?
:on-click util/stop-propagation
:on-checked-change
(fn [value]
(if value
(db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
(db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))})})))))
:desc (when owner-block
(shui/switch
{:id "show as checkbox" :size "sm"
:checked checked?
:on-click util/stop-propagation
:on-checked-change
(fn [value]
(if value
(db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
(db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
(when (and owner-block
;; Any property should be removable from Tag Properties

View File

@@ -129,3 +129,13 @@ html.is-resizing-buf {
.markdown-table {
width: 98%;
}
.sticky-columns {
@apply sticky left-0;
z-index: 99;
background-color: var(--lx-gray-01, var(--ls-primary-background-color, hsl(var(--background))));
}
.table-action-bar {
z-index: 100;
}

View File

@@ -8,6 +8,7 @@
[datascript.impl.entity :as de]
[dommy.core :as dom]
[frontend.components.dnd :as dnd]
[frontend.components.property.config :as property-config]
[frontend.components.property.value :as pv]
[frontend.components.select :as select]
[frontend.config :as config]
@@ -15,6 +16,7 @@
[frontend.date :as date]
[frontend.db :as db]
[frontend.db-mixins :as db-mixins]
[frontend.handler.db-based.property :as db-property-handler]
[frontend.handler.property :as property-handler]
[frontend.handler.ui :as ui-handler]
[frontend.hooks :as hooks]
@@ -79,15 +81,65 @@
(if (or show? checked?) "opacity-100" "opacity-0"))})]))
(defn header-cp
[{:keys [column-toggle-sorting! state]} column]
[{:keys [view-entity column-toggle-sorting! state]} column]
(let [sorting (:sorting state)
[asc?] (some (fn [item] (when (= (:id item) (:id column))
(when-some [asc? (:asc? item)]
[asc?]))) sorting)]
[asc?]))) sorting)
property (db/entity (:id column))
pinned? (when property
(contains? (set (map :db/id (:logseq.property.table/pinned-columns view-entity)))
(:db/id property)))
sub-content (fn [{:keys [id]}]
[:<>
(when property
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
[:div.flex.flex-row.items-center.gap-1
(ui/icon "settings" {:size 15})
[:div "Configure"]])
(shui/dropdown-menu-sub-content
[:div.ls-property-dropdown-editor.-m-1
(property-config/dropdown-editor property nil {})])))
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
[:div.flex.flex-row.items-center.gap-1
(ui/icon "arrows-down-up" {:size 15})
[:div.mr-4 "Set order"]
(cond asc? [:span.opacity-50.text-sm "ASC"]
(false? asc?) [:span.opacity-50.text-sm "DESC"]
:else nil)])
(shui/dropdown-menu-sub-content
{:on-click #(shui/popup-hide! id)}
(shui/dropdown-menu-item
{:key "asc"
:on-click #(column-toggle-sorting! column)}
"ASC")
(shui/dropdown-menu-item
{:key "desc"
:on-click #(column-toggle-sorting! column)}
"DESC")))
(when property
(shui/dropdown-menu-item
{:on-click (fn [_e]
(if pinned?
(db-property-handler/delete-property-value! (:db/id view-entity)
:logseq.property.table/pinned-columns
(:db/id property))
(property-handler/set-block-property! (state/get-current-repo)
(:db/id view-entity)
:logseq.property.table/pinned-columns
(:db/id property)))
(shui/popup-hide! id))}
[:div.flex.flex-row.items-center.gap-1
(ui/icon "pin" {:size 15})
[:div (if pinned? "Unpin" "Pin")]]))])]
(shui/button
{:variant "text"
:class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
:on-click #(column-toggle-sorting! column)}
:on-mouse-up (fn [^js e]
(when (string/blank? (some-> (.-target e) (.closest "[aria-roledescription=sortable]") (.-style) (.-transform)))
(shui/popup-show! (.-target e) sub-content {:align "start" :as-dropdown? true})))}
(let [title (str (:name column))]
[:span {:title title
:class "max-w-full overflow-hidden text-ellipsis"}
@@ -323,7 +375,7 @@
(when (fn? on-delete-rows)
(shui/button
{:variant "ghost"
:class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
:class "h-8 !pl-0 !px-2 !py-0 text-muted-foreground hover:text-foreground w-full justify-start"
:on-click (fn []
(on-delete-rows table selected-rows))}
(ui/icon "trash")))))
@@ -410,47 +462,62 @@
{:data-no-dnd true
:ref *el}]))
(defn- table-header
[table columns {:keys [show-add-property? add-property!] :as option} selected-rows]
(let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
(defn- table-header-cell
[table column]
(let [header-fn (:header column)
sized-columns (get-in table [:state :sized-columns])
items (mapv (fn [column]
{:id (:name column)
:value (:id column)
:content (let [header-fn (:header column)
width (get-column-size column sized-columns)
select? (= :select (:id column))]
[:div.ls-table-header-cell
{:style {:width width
:min-width width}
:class (when select? "!border-0")}
(if (fn? header-fn)
(header-fn table column)
header-fn)
set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
width (get-column-size column sized-columns)
select? (= :select (:id column))]
[:div.ls-table-header-cell
{:style {:width width
:min-width width}
:class (when select? "!border-0")}
(if (fn? header-fn)
(header-fn table column)
header-fn)
;; resize handle
(when-not (false? (:resizable? column))
(column-resizer column
(fn [size]
(set-sized-columns! (assoc sized-columns (:id column) size)))))])
:disabled? (= (:id column) :select)}) columns)
items (if show-add-property?
(conj items
{:id "add property"
:prop {:style {:width "-webkit-fill-available"
:min-width 160}
:on-click (fn [] (when (fn? add-property!) (add-property!)))}
:value :add-new-property
:content (add-property-button)
:disabled? true})
items)
(when-not (false? (:resizable? column))
(column-resizer column
(fn [size]
(set-sized-columns! (assoc sized-columns (:id column) size)))))]))
(defn- table-header
[table {:keys [show-add-property? add-property!] :as option} selected-rows]
(let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
pinned (get-in table [:state :pinned-columns])
unpinned (get-in table [:state :unpinned-columns])
build-item (fn [column]
{:id (:name column)
:value (:id column)
:content (table-header-cell table column)
:disabled? (= (:id column) :select)})
pinned-items (mapv build-item pinned)
unpinned-items (if show-add-property?
(conj (mapv build-item unpinned)
{:id "add property"
:prop {:style {:width "-webkit-fill-available"
:min-width 160}
:on-click (fn [] (when (fn? add-property!) (add-property!)))}
:value :add-new-property
:content (add-property-button)
:disabled? true})
(mapv build-item unpinned))
selection-rows-count (count selected-rows)]
(shui/table-header
(dnd/items items {:vertical? false
:on-drag-end (fn [ordered-columns _m]
(set-ordered-columns! ordered-columns))})
(when (seq pinned-items)
[:div.sticky-columns.flex.flex-row
(dnd/items pinned-items {:vertical? false
:on-drag-end (fn [ordered-columns _m]
(set-ordered-columns! ordered-columns))})])
(when (seq unpinned-items)
[:div.flex.flex-row
(dnd/items unpinned-items
{:vertical? false
:on-drag-end (fn [ordered-columns _m]
(set-ordered-columns! ordered-columns))})])
(when (pos? selection-rows-count)
[:div.absolute.top-0.left-8
[:div.table-action-bar.absolute.top-0.left-8
(action-bar table selected-rows option)]))))
(rum/defc row-cell < rum/static
@@ -468,41 +535,46 @@
(render table row column)))))
(rum/defc table-row-inner < rum/static
[{:keys [row-selected?] :as table} row columns props {:keys [show-add-property?]}]
[{:keys [row-selected?] :as table} row props {:keys [show-add-property?]}]
(let [[first-col-rendered? set-first-col-rendered!] (rum/use-state false)
columns (if show-add-property?
(conj (vec columns)
{:id :add-property
:cell (fn [_table _row _column])})
columns)
sized-columns (get-in table [:state :sized-columns])]
pinned-columns (get-in table [:state :pinned-columns])
unpinned (get-in table [:state :unpinned-columns])
unpinned-columns (if show-add-property?
(conj (vec unpinned)
{:id :add-property
:cell (fn [_table _row _column])})
unpinned)
sized-columns (get-in table [:state :sized-columns])
row-cell-f (fn [idx column]
(let [idx (inc idx)
id (str (:id row) "-" (:id column))
render (get column :cell)
width (get-column-size column sized-columns)
select? (= (:id column) :select)
add-property? (= (:id column) :add-property)
cell-opts {:key id
:select? select?
:add-property? add-property?
:style {:width width
:min-width width}}]
(when render
(row-cell table row column render cell-opts idx first-col-rendered? set-first-col-rendered!))))]
(shui/table-row
(merge
props
{:key (str (:id row))
:data-state (when (row-selected? row) "selected")})
(map-indexed
(fn [idx column]
(let [id (str (:id row) "-" (:id column))
render (get column :cell)
width (get-column-size column sized-columns)
select? (= (:id column) :select)
add-property? (= (:id column) :add-property)
cell-opts {:key id
:select? select?
:add-property? add-property?
:style {:width width
:min-width width}}]
(when render
(row-cell table row column render cell-opts idx first-col-rendered? set-first-col-rendered!))))
columns))))
[:div.sticky-columns.flex.flex-row
(map-indexed row-cell-f pinned-columns)]
[:div.flex.flex-row
(map-indexed row-cell-f unpinned-columns)])))
(rum/defc table-row < rum/reactive
[table row columns props option]
[table row props option]
(let [row' (db/sub-block (:id row))
;; merge entity temporal attributes
row (reduce (fn [e [k v]] (assoc e k v)) row' (.-kv ^js row))]
(table-row-inner table row columns props option)))
(table-row-inner table row props option)))
(rum/defc search
[input {:keys [on-change set-input!]}]
@@ -1091,13 +1163,12 @@
[])
(shui/table
(let [columns' (:columns table)
rows (:rows table)]
(let [rows (:rows table)]
[:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
{:ref *rows-wrap}
(when ready?
[:div.relative
(table-header table columns' option selected-rows)
(table-header table option selected-rows)
(ui/virtualized-list
{:ref #(reset! *scroller-ref %)
@@ -1110,7 +1181,7 @@
:total-count (count rows)
:item-content (fn [idx]
(let [row (nth rows idx)]
(table-row table row columns' {} option)))})
(table-row table row {} option)))})
(when add-new-object!
(shui/table-footer (add-new-row table)))])]))))
@@ -1184,7 +1255,7 @@
(state/set-state! :editor/virtualized-scroll-fn #(ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)))))
[sorting data])))
(rum/defc view-inner < rum/static
(rum/defc ^:large-vars/cleanup-todo view-inner < rum/static
[view-entity {:keys [data set-data! columns add-new-object! views-title title-key render-empty-title?] :as option
:or {render-empty-title? false}}
*scroller-ref]
@@ -1193,7 +1264,7 @@
[sorting set-sorting!] (rum/use-state (or sorting [{:id :block/updated-at, :asc? false}]))
filters (:logseq.property.table/filters view-entity)
[filters set-filters!] (rum/use-state (or filters []))
default-visible-columns (if-let [hidden-columns (:logseq.property.table/hidden-columns view-entity)]
default-visible-columns (if-let [hidden-columns (conj (:logseq.property.table/hidden-columns view-entity) :id)]
(zipmap hidden-columns (repeat false))
;; This case can happen for imported tables
(if (seq (:logseq.property.table/ordered-columns view-entity))
@@ -1220,7 +1291,20 @@
[input-filters set-input-filters!] (rum/use-state [input filters])
[row-selection set-row-selection!] (rum/use-state {})
columns (sort-columns columns ordered-columns)
table-map {:data data
select? (first (filter (fn [item] (= (:id item) :select)) columns))
id? (first (filter (fn [item] (= (:id item) :id)) columns))
pinned-properties (set (cond->> (map :db/ident (:logseq.property.table/pinned-columns view-entity))
id?
(cons :id)
select?
(cons :select)))
{pinned true unpinned false} (group-by (fn [item]
(contains? pinned-properties (:id item)))
(remove (fn [column]
(false? (get visible-columns (:id column))))
columns))
table-map {:view-entity view-entity
:data data
:columns columns
:state {:sorting sorting
:filters filters
@@ -1228,7 +1312,9 @@
:row-selection row-selection
:visible-columns visible-columns
:sized-columns sized-columns
:ordered-columns ordered-columns}
:ordered-columns ordered-columns
:pinned-columns pinned
:unpinned-columns unpinned}
:data-fns {:set-data! set-data!
:set-row-filter! set-row-filter!
:set-filters! set-filters!

View File

@@ -67,7 +67,7 @@
"Installs plugin given plugin map with id"
[{:keys [id] :as manifest}]
(when-not (and (:plugin/installing @state/state)
(installed? id))
(installed? id))
(state/set-state! :plugin/installing manifest)
(if (util/electron?)
(ipc/ipc :installMarketPlugin manifest)

View File

@@ -3,6 +3,7 @@
(:require [clojure.string :as string]
[datascript.impl.entity :as de]
[frontend.db :as db]
[frontend.db.async :as db-async]
[frontend.handler.common.page :as page-common-handler]
[frontend.handler.db-based.property :as db-property-handler]
[frontend.handler.editor :as editor-handler]
@@ -12,11 +13,10 @@
[logseq.common.util.page-ref :as page-ref]
[logseq.db]
[logseq.db.frontend.class :as db-class]
[logseq.outliner.validate :as outliner-validate]
[promesa.core :as p]
[frontend.db.async :as db-async]
[logseq.db.frontend.content :as db-content]
[logseq.shui.ui :as shui]))
[logseq.outliner.validate :as outliner-validate]
[logseq.shui.ui :as shui]
[promesa.core :as p]))
(defn- valid-tag?
"Returns a boolean indicating whether the new tag passes all valid checks.
@@ -60,25 +60,25 @@
(if (db/page-exists? (:block/title page-entity) #{:logseq.class/Page})
(notification/show! (str "A page with the name \"" (:block/title page-entity) "\" already exists.") :warning false)
(when-not (:logseq.property/built-in? page-entity)
(p/let [objects (db-async/<get-tag-objects (state/get-current-repo) (:db/id page-entity))]
(let [convert-fn
(fn convert-fn []
(let [page-txs [[:db/retract (:db/id page-entity) :db/ident]
[:db/retract (:db/id page-entity) :block/tags :logseq.class/Tag]
[:db/add (:db/id page-entity) :block/tags :logseq.class/Page]]
obj-txs (mapcat (fn [obj]
(let [tags (map #(db/entity (state/get-current-repo) (:db/id %)) (:block/tags obj))]
[{:db/id (:db/id obj)
:block/title (db-content/replace-tag-refs-with-page-refs (:block/title obj) tags)}
[:db/retract (:db/id obj) :block/tags (:db/id page-entity)]]))
objects)
txs (concat page-txs obj-txs)]
(db/transact! (state/get-current-repo) txs {:outliner-op :save-block})))]
(-> (shui/dialog-confirm!
"Converting a tag to page also removes tags from any nodes that have that tag. Are you ok with that?"
{:id :convert-tag-to-page
:data-reminder :ok})
(p/then convert-fn)))))))
(p/let [objects (db-async/<get-tag-objects (state/get-current-repo) (:db/id page-entity))]
(let [convert-fn
(fn convert-fn []
(let [page-txs [[:db/retract (:db/id page-entity) :db/ident]
[:db/retract (:db/id page-entity) :block/tags :logseq.class/Tag]
[:db/add (:db/id page-entity) :block/tags :logseq.class/Page]]
obj-txs (mapcat (fn [obj]
(let [tags (map #(db/entity (state/get-current-repo) (:db/id %)) (:block/tags obj))]
[{:db/id (:db/id obj)
:block/title (db-content/replace-tag-refs-with-page-refs (:block/title obj) tags)}
[:db/retract (:db/id obj) :block/tags (:db/id page-entity)]]))
objects)
txs (concat page-txs obj-txs)]
(db/transact! (state/get-current-repo) txs {:outliner-op :save-block})))]
(-> (shui/dialog-confirm!
"Converting a tag to page also removes tags from any nodes that have that tag. Are you ok with that?"
{:id :convert-tag-to-page
:data-reminder :ok})
(p/then convert-fn)))))))
(defn <create-class!
"Creates a class page and provides class-specific error handling"

View File

@@ -676,7 +676,8 @@
[61 {:properties [:logseq.property/type :logseq.property/hide? :logseq.property/public? :logseq.property/view-context :logseq.property/ui-position]
:fix (rename-properties {:property/schema.classes :logseq.property/classes
:property.value/content :logseq.property/value})}]
[62 {:fix remove-block-schema}]])
[62 {:fix remove-block-schema}]
[63 {:properties [:logseq.property.table/pinned-columns]}]])
(let [max-schema-version (apply max (map first schema-version->updates))]
(assert (<= db-schema/version max-schema-version))

View File

@@ -17,8 +17,8 @@
[frontend.fs :as fs]
[frontend.handler.code :as code-handler]
[frontend.handler.command-palette :as palette-handler]
[frontend.handler.common.plugin :as plugin-common-handler]
[frontend.handler.common.page :as page-common-handler]
[frontend.handler.common.plugin :as plugin-common-handler]
[frontend.handler.config :as config-handler]
[frontend.handler.db-based.property :as db-property-handler]
[frontend.handler.dnd :as editor-dnd-handler]
@@ -896,14 +896,14 @@
(when-let [k' (and (string? k) (some-> k (sanitize-user-property-name) (keyword)))]
(let [prefix (-resolve-property-prefix-for-db plugin)]
(p/let [k (if (qualified-keyword? k') k'
(api-block/get-db-ident-for-user-property-name (str prefix k)))
(api-block/get-db-ident-for-user-property-name (str prefix k)))
p (db-utils/pull k)] p))))
(defn ^:export get_property
[k]
(this-as this
(p/let [prop (-get-property this k)]
(bean/->js (sdk-utils/normalize-keyword-for-json prop)))))
(p/let [prop (-get-property this k)]
(bean/->js (sdk-utils/normalize-keyword-for-json prop)))))
(defn ^:export upsert_property
"schema:
@@ -915,87 +915,87 @@
"
[k ^js schema ^js opts]
(this-as this
(when-let [k' (and (string? k) (keyword k))]
(let [prefix (when (some-> js/window.LSPlugin (.-PluginLocal) (instance? this))
(str (.-id this) "."))]
(p/let [opts (or (some-> opts (bean/->clj)) {})
name (or (:name opts) (some-> (str k) (string/trim)))
k (if (qualified-keyword? k') k'
(api-block/get-db-ident-for-user-property-name (str prefix k)))
schema (or (some-> schema (bean/->clj)
(update-keys #(if (contains? #{:hide :public} %)
(keyword (str (name %) "?")) %))) {})
schema (cond-> schema
(string? (:cardinality schema))
(update :cardinality keyword)
(string? (:type schema))
(update :type keyword))
p (db-property-handler/upsert-property! k schema
(cond-> opts
name
(assoc :property-name name)))]
(bean/->js (sdk-utils/normalize-keyword-for-json p)))))))
(when-let [k' (and (string? k) (keyword k))]
(let [prefix (when (some-> js/window.LSPlugin (.-PluginLocal) (instance? this))
(str (.-id this) "."))]
(p/let [opts (or (some-> opts (bean/->clj)) {})
name (or (:name opts) (some-> (str k) (string/trim)))
k (if (qualified-keyword? k') k'
(api-block/get-db-ident-for-user-property-name (str prefix k)))
schema (or (some-> schema (bean/->clj)
(update-keys #(if (contains? #{:hide :public} %)
(keyword (str (name %) "?")) %))) {})
schema (cond-> schema
(string? (:cardinality schema))
(update :cardinality keyword)
(string? (:type schema))
(update :type keyword))
p (db-property-handler/upsert-property! k schema
(cond-> opts
name
(assoc :property-name name)))]
(bean/->js (sdk-utils/normalize-keyword-for-json p)))))))
(defn ^:export remove_property
[k]
(this-as this
(p/let [prop (-get-property this k)]
(when-let [uuid (:block/uuid prop)]
(page-common-handler/<delete! uuid nil nil)))))
(p/let [prop (-get-property this k)]
(when-let [uuid (:block/uuid prop)]
(page-common-handler/<delete! uuid nil nil)))))
;; block properties
(defn ^:export upsert_block_property
[block-uuid keyname value]
(this-as this
(p/let [keyname (sanitize-user-property-name keyname)
block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
repo (state/get-current-repo)
_ (db-async/<get-block repo block-uuid :children? false)
db? (config/db-based-graph? repo)
key (-> (if (keyword? key) (name keyname) keyname) (util/safe-lower-case))
key (if db?
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) key))
key)
_ (when (and db? (not (db-utils/entity key)))
(db-property-handler/upsert-property! key {} {:property-name keyname}))]
(property-handler/set-block-property! repo block-uuid key value))))
(p/let [keyname (sanitize-user-property-name keyname)
block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
repo (state/get-current-repo)
_ (db-async/<get-block repo block-uuid :children? false)
db? (config/db-based-graph? repo)
key (-> (if (keyword? key) (name keyname) keyname) (util/safe-lower-case))
key (if db?
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) key))
key)
_ (when (and db? (not (db-utils/entity key)))
(db-property-handler/upsert-property! key {} {:property-name keyname}))]
(property-handler/set-block-property! repo block-uuid key value))))
(defn ^:export remove_block_property
[block-uuid key]
(this-as this
(p/let [key (sanitize-user-property-name key)
block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
_ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
db? (config/db-based-graph? (state/get-current-repo))
key-ns? (and (keyword? key) (namespace key))
key (if key-ns? key (-> (if (keyword? key) (name key) key) (util/safe-lower-case)))
key (if (and db? (not key-ns?))
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) key))
key)]
(property-handler/remove-block-property!
(state/get-current-repo)
block-uuid key))))
(p/let [key (sanitize-user-property-name key)
block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
_ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
db? (config/db-based-graph? (state/get-current-repo))
key-ns? (and (keyword? key) (namespace key))
key (if key-ns? key (-> (if (keyword? key) (name key) key) (util/safe-lower-case)))
key (if (and db? (not key-ns?))
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) key))
key)]
(property-handler/remove-block-property!
(state/get-current-repo)
block-uuid key))))
(defn ^:export get_block_property
[block-uuid key]
(this-as this
(p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
_ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
(when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
(when (seq properties)
(let [key (sanitize-user-property-name key)
property-name (-> (if (keyword? key) (name key) key) (util/safe-lower-case))
property-value (or (get properties key)
(get properties (keyword property-name))
(get properties
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) property-name))))
property-value (if-let [property-id (:db/id property-value)]
(db/pull property-id) property-value)
ret (sdk-utils/normalize-keyword-for-json property-value)]
(bean/->js ret)))))))
(p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
_ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
(when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
(when (seq properties)
(let [key (sanitize-user-property-name key)
property-name (-> (if (keyword? key) (name key) key) (util/safe-lower-case))
property-value (or (get properties key)
(get properties (keyword property-name))
(get properties
(api-block/get-db-ident-for-user-property-name
(str (-resolve-property-prefix-for-db this) property-name))))
property-value (if-let [property-id (:db/id property-value)]
(db/pull property-id) property-value)
ret (sdk-utils/normalize-keyword-for-json property-value)]
(bean/->js ret)))))))
(def ^:export get_block_properties
(fn [block-uuid]

View File

@@ -1,5 +1,6 @@
(ns frontend.worker.undo-redo-test
(:require [cljs.pprint :as pp]
(:require ["fs" :as fs-node]
[cljs.pprint :as pp]
[clojure.test :as t :refer [deftest is testing use-fixtures]]
[clojure.test.check.generators :as gen]
[clojure.walk :as walk]
@@ -8,13 +9,12 @@
[frontend.test.generators :as t.gen]
[frontend.test.helper :as test-helper]
[frontend.worker.fixtures :as worker-fixtures]
[frontend.worker.state :as worker-state]
[frontend.worker.undo-redo :as undo-redo]
[logseq.db :as ldb]
[logseq.db.sqlite.util :as sqlite-util]
[logseq.outliner.op :as outliner-op]
[logseq.outliner.tree :as otree]
[frontend.worker.state :as worker-state]
["fs" :as fs-node]
[logseq.db.sqlite.util :as sqlite-util]))
[logseq.outliner.tree :as otree]))
(def ^:private page-uuid (random-uuid))
(def ^:private init-data (test-helper/initial-test-page-and-blocks {:page-uuid page-uuid}))
@@ -30,7 +30,6 @@
(worker-fixtures/listen-test-db-fixture [:gen-undo-ops])
worker-fixtures/listen-test-db-to-write-tx-log-json-file)
(def ^:private gen-non-exist-block-uuid gen/uuid)
(defn- gen-block-uuid
@@ -116,7 +115,6 @@
[?left :block/uuid ?left-uuid]]
db))))
(defn- check-block-count
[{:keys [op tx]} current-db]
(case (first op)
@@ -281,8 +279,6 @@
(fs-node/writeFileSync "debug.json" (sqlite-util/write-transit-str data))
(throw (js/Error "check debug.json"))))))
(comment
(deftest debug-test
(let [{:keys [origin-db db illegal-entity other]}