enhance: improve gallery view (#12698)

* enhance: improve gallery view

* fix: tune gallery card sizing

* fix: load gallery asset properties initially

* chore: cleanup code

* fix: gallery view should load block fully

* fix: align gallery card media

* fix: render asset class gallery cards

* fix: align gallery images with header

* fix: fit gallery images to card width

* fix: polish gallery view actions

* fix: improve gallery view behavior

* fix: guard gallery asset loading

* fix: hide asset placeholders while loading

* fix: clean up gallery view loading

* fix: improve gallery selection actions

* fix: clean up gallery selection state

* fix: improve grouped gallery view
This commit is contained in:
Tienson Qin
2026-05-25 18:28:10 +08:00
committed by GitHub
parent 2e8c749a7d
commit ae2ba48c81
14 changed files with 1171 additions and 186 deletions

View File

@@ -567,20 +567,37 @@
(into [] (filter filter-pred) entities))
entities)
group-by-page? (= group-by-property-ident :block/page)
readable-property-value-or-ent
(fn readable-property-value-or-ent [ent]
(let [pvalue (get ent group-by-property-ident)]
(if (de/entity? pvalue)
(if (match-property-value-as-entity? pvalue group-by-property)
pvalue
(db-property/property-value-content pvalue))
pvalue)))
group-values
(fn group-values [ent]
(let [pvalue (get ent group-by-property-ident)
values (if (and (not (de/entity? pvalue))
(coll? pvalue)
(not (map? pvalue)))
(seq pvalue)
[pvalue])]
(or
(seq
(map
(fn [value]
(if (de/entity? value)
(if (match-property-value-as-entity? value group-by-property)
value
(db-property/property-value-content value))
value))
values))
[nil])))
result (if group-by-property-ident
(let [groups-sort-by-property-ident (or (:db/ident (:logseq.property.view/sort-groups-by-property view))
:block/journal-day)
desc? (:logseq.property.view/sort-groups-desc? view)
result (->> filtered-entities
(group-by readable-property-value-or-ent)
(reduce (fn [groups ent]
(reduce
(fn [groups value]
(update groups value (fnil conj []) ent))
groups
(group-values ent)))
{})
(seq))
keyfn (fn [groups-sort-by-property-ident]
(fn [[by-value _]]
@@ -589,8 +606,8 @@
(let [v (get by-value groups-sort-by-property-ident)]
(if (and (= groups-sort-by-property-ident :block/journal-day) (not desc?)
(nil? (:block/journal-day by-value)))
;; Use MAX_SAFE_INTEGER so non-journal pages (without :block/journal-day) are sorted
;; after all journal pages when sorting by journal date.
;; Use MAX_SAFE_INTEGER so non-journal pages (without :block/journal-day) are sorted
;; after all journal pages when sorting by journal date.
js/Number.MAX_SAFE_INTEGER
v))
group-by-closed-values?
@@ -601,8 +618,8 @@
by-value)))]
(sort (common-util/by-sorting
(cond->
[{:get-value (keyfn groups-sort-by-property-ident)
:asc? (not desc?)}]
[{:get-value (keyfn groups-sort-by-property-ident)
:asc? (not desc?)}]
(not= groups-sort-by-property-ident :block/title)
(conj {:get-value (keyfn :block/title)
:asc? (not desc?)})))
@@ -610,34 +627,34 @@
(sort-entities db sorting filtered-entities))
data' (if group-by-property-ident
(map
(fn [[by-value entities]]
(let [by-value' (if (de/entity? by-value)
(select-keys by-value [:db/id :db/ident :block/uuid :block/title :block/name :logseq.property/value :logseq.property/icon :block/tags])
by-value)
pages? (not (some :block/page entities))
group (if (and list-view? (not pages?))
(let [parent-groups (->> entities
(group-by :block/parent)
(sort-by (fn [[parent _]] (:block/order parent))))]
(map
(fn [[_parent blocks]]
[(:block/uuid (first blocks))
(map (fn [b]
{:db/id (:db/id b)
:block/parent (:block/uuid (:block/parent b))})
(ldb/sort-by-order blocks))])
parent-groups))
(->> (sort-entities db sorting entities)
(map :db/id)))]
[by-value' group]))
result)
(fn [[by-value entities]]
(let [by-value' (if (de/entity? by-value)
(select-keys by-value [:db/id :db/ident :block/uuid :block/title :block/name :logseq.property/value :logseq.property/icon :block/tags])
by-value)
pages? (not (some :block/page entities))
group (if (and list-view? (not pages?))
(let [parent-groups (->> entities
(group-by :block/parent)
(sort-by (fn [[parent _]] (:block/order parent))))]
(map
(fn [[_parent blocks]]
[(:block/uuid (first blocks))
(map (fn [b]
{:db/id (:db/id b)
:block/parent (:block/uuid (:block/parent b))})
(ldb/sort-by-order blocks))])
parent-groups))
(->> (sort-entities db sorting entities)
(map :db/id)))]
[by-value' group]))
result)
(map :db/id result))
dedupe-data? (or (= feat-type :property-objects) query?)]
(cond->
{:count (count filtered-entities)
:data (if dedupe-data?
(distinct data')
data')}
{:count (count filtered-entities)
:data (if dedupe-data?
(distinct data')
data')}
(= feat-type :linked-references)
(merge (select-keys entities-result [:ref-pages-count :ref-matched-children-ids]))
query?

View File

@@ -460,6 +460,41 @@
:hide? true}
:queryable? true}
:logseq.property.view/gallery-asset-property {:title "Gallery asset property"
:schema
{:type :property
:hide? true
:public? false}}
:logseq.property.view/gallery-display-properties {:title "Gallery display properties"
:schema
{:type :property
:cardinality :many
:hide? true
:public? false}}
:logseq.property.view/gallery-card-size {:title "Gallery card size"
:schema
{:type :keyword
:hide? true
:public? false}
:properties {:logseq.property/scalar-default-value :default}
:rtc property-ignore-rtc}
:logseq.property.view/gallery-card-width {:title "Gallery card width"
:schema
{:type :raw-number
:hide? true
:public? false}
:rtc property-ignore-rtc}
:logseq.property.view/gallery-card-height {:title "Gallery card height"
:schema
{:type :raw-number
:hide? true
:public? false}
:rtc property-ignore-rtc}
:logseq.property.view/sort-groups-by-property {:title "View sort groups by"
:schema
{:type :property

View File

@@ -30,7 +30,7 @@
(map (juxt :major :minor)
[(parse-schema-version x) (parse-schema-version y)])))
(def version (parse-schema-version "65.32"))
(def version (parse-schema-version "65.33"))
(defn major-version
"Return a number.

View File

@@ -8,7 +8,7 @@
[conn feature-type & {:keys [view-for-id]}]
(let [tx (d/transact! conn [(cond-> {:db/id -100
:block/title "Test view"
:block/uuid #uuid "00000000-0000-0000-0000-000000000100"
:block/uuid (random-uuid)
:logseq.property.view/feature-type feature-type
:logseq.property.view/type :logseq.property.view/type.table}
view-for-id
@@ -86,6 +86,41 @@
(is (= 1 (:count result)))
(is (= ["B"] titles))))
(deftest get-view-data-class-objects-groups-by-title-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}}
:pages-and-blocks
[{:page {:block/title "A" :build/tags [:Topic]}}
{:page {:block/title "B" :build/tags [:Topic]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id :logseq.property.view/group-by-property :block/title]])
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id})
group-titles (map first (:data result))]
(is (= ["A" "B"] group-titles))))
(deftest get-view-data-class-objects-groups-by-many-values-test
(let [conn (db-test/create-conn-with-blocks
{:classes {:Topic {:block/title "Topic"}
:SciFi {:block/title "Sci-Fi"}
:Drama {:block/title "Drama"}}
:pages-and-blocks
[{:page {:block/title "Movie A" :build/tags [:Topic :SciFi :Drama]}}
{:page {:block/title "Movie B" :build/tags [:Topic :SciFi]}}]})
class-id (:db/id (d/entity @conn :user.class/Topic))
view-id (create-view-id conn :class-objects :view-for-id class-id)
_ (d/transact! conn [[:db/add view-id :logseq.property.view/group-by-property :block/tags]])
result (db-view/get-view-data @conn view-id {:view-feature-type :class-objects
:view-for-id class-id})
group->titles (into {}
(map (fn [[group rows]]
[(:block/title group)
(set (map (fn [id] (:block/title (d/entity @conn id))) rows))]))
(:data result))]
(is (= #{"Movie A" "Movie B"} (get group->titles "Sci-Fi")))
(is (= #{"Movie A"} (get group->titles "Drama")))))
(deftest get-view-data-linked-references-page-view-does-not-crash-on-missing-db-ident-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks

View File

@@ -226,13 +226,13 @@
(defonce *resizing-image? (atom false))
(rum/defc ^:large-vars/cleanup-todo asset-container
[asset-block src title metadata {:keys [breadcrumb? positioned? local? full-text]}]
[asset-block src title metadata {:keys [breadcrumb? positioned? local? full-text gallery-view?]}]
(let [asset-width (:logseq.property.asset/width asset-block)
asset-height (:logseq.property.asset/height asset-block)
asset-align (normalize-asset-align (:logseq.property.asset/align asset-block))]
(hooks/use-effect!
(fn []
(when (:block/uuid asset-block)
(when (and (seq src) (:block/uuid asset-block))
(when-not (or asset-width asset-height)
(measure-image!
src
@@ -244,9 +244,11 @@
(fn []))
[])
(let [*el-ref (rum/use-ref nil)
image-src (fs/asset-path-normalize src)
src' (if (or (string/starts-with? src "/")
(string/starts-with? src "~"))
image-src (when (seq src)
(fs/asset-path-normalize src))
src' (if (and (seq src)
(or (string/starts-with? src "/")
(string/starts-with? src "~")))
(str "file://" src)
src)
get-blockid #(some-> (rum/deref *el-ref) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
@@ -260,10 +262,11 @@
:ref *el-ref}
[:img.rounded-sm.relative.fade-in.fade-in-faster
(merge
{:loading "lazy"
:referrerPolicy "no-referrer"
:src src'
:title title}
(cond-> {:loading "lazy"
:referrerPolicy "no-referrer"
:src src'}
(not gallery-view?)
(assoc :title title))
metadata)]
(when (and (not breadcrumb?)
(not positioned?))
@@ -412,7 +415,8 @@
{:breadcrumb? breadcrumb?
:positioned? positioned?
:local? local?
:full-text full-text})]
:full-text full-text
:gallery-view? (:gallery-view? config)})]
(if (or (:disable-resize? config)
(:table-view? config)
(not resizable?))
@@ -516,9 +520,8 @@
(p/then (fn [url]
(reset! src (common-util/safe-decode-uri-component url))))
(p/catch #(js/console.log "Failed to load asset:" %))))
(:image-placeholder config)
(if (and (:image-placeholder config) (nil? @src))
(:image-placeholder config)
(if (nil? @src)
nil
(let [asset-block (:asset-block config)
ext (block-asset/link-ext @src href asset-block)
repo (state/get-current-repo)
@@ -1120,6 +1123,7 @@
[:div.asset-transfer-progress-bar
[:span {:style {:width (str percent "%")}}]]])
image? (contains? (common-config/img-formats) (keyword asset-type))
gallery-image? (and (:gallery-view? config) image?)
width (get-in block [:logseq.property.asset/resize-metadata :width])
asset-width (:logseq.property.asset/width block)
asset-height (:logseq.property.asset/height block)
@@ -1142,15 +1146,15 @@
href (or (:logseq.property.asset/external-url block)
(path/path-join (str "../" common-config/local-assets-dir) file))
content (cond
file-ready?
(asset-link (assoc config
:asset-block block
:image-placeholder img-placeholder)
(or file-ready? gallery-image?)
(asset-link (cond-> (assoc config :asset-block block)
(not gallery-image?)
(assoc :image-placeholder img-placeholder))
(:block/title block)
href
img-metadata
nil)
image?
(and image? (not gallery-image?) (false? file-exists?))
img-placeholder)]
(if progress-view
[:div.asset-transfer-shell
@@ -2267,7 +2271,7 @@
(when (and (state/developer-mode?) (.-metaKey event))
(js/console.debug "[block config]==" config)))}
[:span {:class (if (or (and control-show? (or collapsed? collapsable?))
(and collapsed? (or page-title? order-list? config/publishing? (util/mobile?))))
(and collapsed? (or order-list? config/publishing? (util/mobile?))))
"control-show cursor-pointer"
"control-hide")}
(ui/rotating-arrow collapsed?)]])
@@ -4234,7 +4238,7 @@
:on-mouse-leave (fn [_e]
(block-mouse-leave *control-show? block-id doc-mode?))}
(when (and (not property?) (not (:table-block-title? config)))
(when (and (not property?) (not (:table-block-title? config)) (not (:hide-block-control? config)))
(let [edit? (or editing?
(= uuid (:block/uuid (state/get-edit-block))))]
(block-control (assoc config :hide-bullet? (:page-title? config))
@@ -4382,7 +4386,7 @@
(defn- config-block-should-update?
[old-state new-state]
(let [config-compare-keys [:show-cloze? :hide-children? :own-order-list-type :own-order-list-index :original-block :edit? :hide-bullet? :ref-matched-children-ids]
(let [config-compare-keys [:show-cloze? :hide-children? :own-order-list-type :own-order-list-index :original-block :edit? :hide-bullet? :hide-block-control? :ref-matched-children-ids]
b1 (second (:rum/args old-state))
b2 (second (:rum/args new-state))
result (or

View File

@@ -1582,9 +1582,76 @@ html.is-mac {
text-overflow: ellipsis;
}
.block-content-wrapper svg, .view-actions svg {
.block-content-wrapper svg,
.view-actions svg {
width: 18px;
height: 18px;
display: block;
flex-shrink: 0;
}
.view-actions,
.view-actions > *,
.view-actions button,
.view-action-type,
.view-action-type .property-value-inner,
.view-action-type .property-value-inner > .jtrigger,
.view-action-type .select-item {
display: inline-flex;
align-items: center;
}
.view-actions button,
.view-action-type,
.view-action-type .property-value-inner,
.view-action-type .property-value-inner > .jtrigger {
justify-content: center;
width: 24px;
height: 24px;
min-height: 24px;
}
.view-action-type .property-value-inner {
border-radius: 6px;
cursor: pointer;
flex: 0 0 24px;
padding: 0;
}
.view-action-type .property-value-inner > .jtrigger {
flex: 0 0 24px;
}
.view-action-type .select-item {
justify-content: center;
line-height: 1;
}
.view-action-type .ui__icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.view-action-type:hover .property-value-inner {
background: var(--lx-gray-03, var(--ls-tertiary-background-color, var(--rx-gray-03)));
}
.view-actions button {
padding-left: 0 !important;
padding-right: 0 !important;
}
.filters-row {
overflow: hidden;
}
.ls-view-filter-value-item {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.katex svg {
@@ -1670,6 +1737,18 @@ html.is-mac {
@apply relative min-h-full;
}
@media (hover: hover) and (pointer: fine) {
.ls-page-title-container .ls-block-right > .block-tags {
opacity: 0;
transition: opacity 150ms ease-in;
}
.ls-page-title-container:hover .ls-block-right > .block-tags,
.ls-page-title-container:focus-within .ls-block-right > .block-tags {
opacity: 1;
}
}
.ls-code-editor-wrap {
@apply relative w-full overflow-hidden rounded;

View File

@@ -1,6 +1,63 @@
.ls-gallery-action-bar-slot {
@apply fixed inset-x-0 bottom-0 z-[100] flex justify-center px-4;
background: linear-gradient(to top, var(--ls-primary-background-color) 0%, var(--ls-primary-background-color) 42%, transparent 100%);
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
pointer-events: none;
}
.ls-gallery-action-bar {
@apply flex h-8 max-w-[calc(100vw-2rem)] flex-row items-center overflow-x-auto rounded-md border border-border bg-background;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.18), 0 2px 6px rgb(0 0 0 / 0.12);
pointer-events: auto;
}
.ls-gallery-action-select-all {
@apply flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center border-r border-border;
}
.ls-gallery-action-bar .ls-table-actions > div:first-child {
@apply px-2;
}
.ls-cards {
--ls-gallery-card-width: 220px;
--ls-gallery-card-height: 320px;
position: relative;
.ls-card-item {
@apply dark:shadow-gray-700 shadow-md rounded-md p-4 h-[15rem] w-full overflow-auto;
@apply w-full overflow-hidden rounded-md border border-transparent bg-transparent shadow-none transition-all;
@apply hover:border-border hover:shadow-sm active:border-primary active:shadow-md dark:hover:border-gray-700 dark:hover:shadow-gray-800;
height: var(--ls-gallery-card-height);
cursor: pointer;
display: flex;
align-items: stretch;
}
.ls-card-item.has-gallery-asset {
align-items: flex-start;
justify-content: center;
}
.ls-gallery-card-content {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
width: 100%;
height: 100%;
min-width: 0;
}
.ls-card-item.has-gallery-asset .ls-gallery-card-content {
width: fit-content;
max-width: 100%;
}
.ls-card-item.has-gallery-asset .ls-gallery-card-title {
text-align: left;
}
.ls-card-item.has-gallery-asset .ls-gallery-card-meta,
.ls-card-item.has-gallery-asset .ls-gallery-card-property {
text-align: left;
}
> div[data-virtuoso-scroller] {
@@ -8,10 +65,183 @@
}
.virtuoso-grid-list {
@apply flex flex-wrap gap-3;
@apply flex w-full flex-wrap justify-between gap-y-8;
column-gap: 1rem;
}
.virtuoso-grid-item {
@apply flex w-[290px];
@apply flex;
width: min(var(--ls-gallery-card-width), 100%);
flex: 0 1 min(var(--ls-gallery-card-width), 100%);
}
.ls-gallery-card-media {
@apply relative flex items-start justify-start overflow-hidden text-muted-foreground;
min-height: 0;
}
.ls-gallery-card-media .asset-action-bar {
display: none;
}
.ls-gallery-card-select {
@apply absolute right-2 top-2 z-10 h-7 w-7 opacity-0 transition-opacity;
}
.ls-gallery-card-select button[role="checkbox"] {
width: 18px !important;
height: 18px !important;
min-width: 18px;
min-height: 18px;
padding: 0 !important;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: rgb(255 255 255 / 0.7);
border-radius: 5px;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.2);
backdrop-filter: blur(2px);
}
.ls-gallery-card-select button[role="checkbox"] svg {
width: 13px;
height: 13px;
display: block;
}
.ls-gallery-card-select button[role="checkbox"][data-state="checked"] {
color: white;
background-color: rgb(2 132 199 / 0.9);
}
.ls-gallery-card-select button[role="checkbox"]:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgb(255 255 255 / 0.8), 0 1px 3px rgb(0 0 0 / 0.25);
}
.dark-theme .ls-gallery-card-select button[role="checkbox"] {
background-color: rgb(15 23 42 / 0.65);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.45);
}
.dark-theme .ls-gallery-card-select button[role="checkbox"][data-state="checked"] {
background-color: rgb(14 165 233 / 0.78);
}
.dark-theme .ls-gallery-card-select button[role="checkbox"]:focus-visible {
box-shadow: 0 0 0 2px rgb(15 23 42 / 0.85), 0 0 0 3px rgb(125 211 252 / 0.75);
}
.ls-card-item:hover .ls-gallery-card-select,
.ls-card-item.is-selected .ls-gallery-card-select {
@apply opacity-100;
}
.ls-card-item.is-selected {
@apply shadow-md;
}
.ls-card-item.has-gallery-asset .ls-gallery-card-media {
@apply items-end justify-center;
}
.ls-gallery-card-media .asset-container {
@apply flex h-full items-start justify-center;
margin-top: 0;
}
.ls-card-item.has-gallery-asset .ls-gallery-card-media .asset-container {
@apply items-end;
width: auto;
max-width: 100%;
}
.ls-gallery-card-media img {
@apply block max-h-full max-w-full object-contain;
width: auto !important;
height: auto !important;
object-position: center top;
transform: scale(1);
transform-origin: center center;
transition: transform 160ms ease;
will-change: transform;
}
.ls-gallery-card-media:hover img {
transform: scale(1.04);
}
.ls-gallery-card-media video,
.ls-gallery-card-media audio {
@apply max-h-full max-w-full;
}
.ls-gallery-card-meta {
@apply flex flex-col gap-1 px-1 py-1.5;
box-sizing: border-box;
width: 0;
min-width: 100%;
max-width: 100%;
}
.ls-card-item:not(.has-gallery-asset) .ls-gallery-card-meta {
width: 100%;
min-width: 0;
}
.ls-gallery-card-title {
@apply min-w-0 truncate text-sm font-medium leading-snug text-foreground opacity-100;
}
.ls-gallery-card-property {
@apply min-w-0 truncate text-sm font-normal leading-snug text-foreground opacity-90;
}
.ls-gallery-card-title * {
@apply text-sm font-normal leading-snug text-foreground opacity-100;
}
.ls-gallery-card-property * {
@apply text-sm font-normal leading-snug text-foreground opacity-100;
}
.ls-gallery-card-property .multi-values {
@apply block min-w-0;
white-space: normal;
}
.ls-gallery-card-property .multi-values > * {
display: inline;
}
.ls-gallery-card-property .multi-values > .opacity-50 {
@apply ml-0 opacity-100;
}
@media (max-width: 640px) {
.virtuoso-grid-list {
@apply gap-3;
}
.virtuoso-grid-item {
width: calc((100% - 0.75rem) / 2);
flex-basis: calc((100% - 0.75rem) / 2);
}
.ls-card-item {
height: auto;
min-height: 220px;
}
.ls-gallery-card-media {
height: clamp(140px, 48vw, 220px);
}
}
@media (max-width: 360px) {
.virtuoso-grid-item {
width: 100%;
flex-basis: 100%;
}
}
}

View File

@@ -1314,7 +1314,7 @@
(inline-text-cp (str value)))]))
(rum/defc single-value-select
[block property value select-opts {:keys [value-render] :as opts}]
[block property value select-opts {:keys [value-render popup-focus-trigger? popup-auto-focus-trigger?] :as opts}]
(let [*el (hooks/use-ref nil)
editing? (:editing? opts)
type (:logseq.property/type property)
@@ -1330,10 +1330,13 @@
trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
show-popup! (fn [target]
(shui/popup-show! target (fn [] (popup-content target))
{:align "start"
:as-dropdown? true
:auto-focus? true
:trigger-id trigger-id}))]
(cond->
{:align "start"
:as-dropdown? true
:auto-focus? (not (false? popup-auto-focus-trigger?))
:trigger-id trigger-id}
(some? popup-focus-trigger?)
(assoc :focus-trigger? popup-focus-trigger?))))]
(if editing?
(popup-content nil)
(let [show! (fn [e]

View File

@@ -6,6 +6,7 @@
[cljs-time.format :as tf]
[clojure.set :as set]
[clojure.string :as string]
[datascript.core :as d]
[datascript.impl.entity :as de]
[dommy.core :as dom]
[frontend.common.missionary :as c.m]
@@ -132,8 +133,51 @@
:class (str "flex transition-opacity "
(if (or show? checked?) "opacity-100" "opacity-0"))})]))
(rum/defc gallery-card-checkbox < rum/static
[{:keys [row-selected? row-toggle-selected! data state data-fns]} row]
(let [id (str (:db/id row) "-gallery-checkbox")
checked? (row-selected? row)
{:keys [last-selected-idx row-selection]} state
{:keys [set-last-selected-idx! set-row-selection!]} data-fns]
[:label.ls-gallery-card-select.flex.items-center.justify-center.cursor-pointer
{:html-for id
:on-click util/stop-propagation}
(shui/checkbox
{:id id
:checked checked?
:on-click (fn [e]
(when (and (.-shiftKey e) last-selected-idx)
(util/stop e)
(let [idx (.indexOf data (:db/id row))]
(when (not= last-selected-idx idx)
(let [new-ids (keep (fn [idx] (util/nth-safe data idx))
(range (min last-selected-idx idx)
(inc (max last-selected-idx idx))))]
(when (seq new-ids)
(set-row-selection! (update row-selection :selected-ids set/union (set new-ids)))))))))
:on-checked-change (fn [v]
(p/do!
(when v
(db-async/<get-block (state/get-current-repo) (:db/id row) {:skip-refresh? true
:children? false}))
(let [idx (.indexOf data (:db/id row))]
(if v
(set-last-selected-idx! idx)
(when (= idx last-selected-idx)
(set-last-selected-idx! nil))))
(row-toggle-selected! row-selection row v)))
:aria-label (t :view.table/select-row)
:class "flex"})]))
(defonce *last-header-action-target (atom nil))
(defn- prevent-view-action-button-focus
[^js e]
(let [target (.-target e)]
(when (and (some-> target (.closest "button, [tabindex]"))
(not (some-> target (.closest "input, textarea, select, [contenteditable='true']"))))
(.preventDefault e))))
(defn- header-dropdown-click-should-hide?
[target]
(let [menu-item (some-> target (.closest "[role='menuitem']"))
@@ -476,6 +520,252 @@
(defonce groups-sort-by-property-identity->name
(set/map-invert groups-sort-by-name->property-identity))
(def ^:private groupable-property-types
#{:checkbox :class :date :default :node :number :string :url})
(def ^:private groupable-many-property-types
#{:class :default :node})
(defn group-by-column?
[column]
(when-let [id (:id column)]
(when-not (= id :block/title)
(when-let [property (db/entity id)]
(and (contains? groupable-property-types (:logseq.property/type property))
(or (not (db-property/many? property))
(contains? groupable-many-property-types (:logseq.property/type property))))))))
(defn- set-view-property!
[view-entity property-ident value]
(property-handler/set-block-property! (:db/id view-entity) property-ident value))
(defn- property-ident->id
[property-ident]
(:db/id (db/entity property-ident)))
(defn- gallery-asset-columns
[columns]
(filter (fn [column]
(when-let [property (db/entity (:id column))]
(= :asset (:logseq.property/type property))))
columns))
(def ^:private gallery-default-card-dimensions
{:width 220
:height 320})
(def ^:private gallery-compact-card-dimensions
{:width 160
:height 232})
(def ^:private gallery-min-card-dimension 100)
(def ^:private gallery-max-card-dimension 1024)
(defn- clamp-gallery-card-dimension
[value]
(-> value
(max gallery-min-card-dimension)
(min gallery-max-card-dimension)))
(defn- gallery-column-ident
[column]
(or (:id column)
(:db/ident column)))
(defn- gallery-column-property
[db column]
(cond
(de/entity? column) column
(gallery-column-ident column) (d/entity db (gallery-column-ident column))))
(defn- gallery-asset-property-column?
[db column]
(= :asset (:logseq.property/type (gallery-column-property db column))))
(defn- gallery-asset-property-idents
[db columns]
(->> columns
(filter #(gallery-asset-property-column? db %))
(keep gallery-column-ident)
vec))
(defn- gallery-asset-property-ident
[db view columns]
(let [configured-ident (:db/ident (:logseq.property.view/gallery-asset-property view))
view-for (:logseq.property/view-for view)
feature-type (:logseq.property.view/feature-type view)
asset-tag? (= :logseq.class/Asset (:db/ident view-for))
tag-view? (and (= :class-objects feature-type)
(ldb/class? view-for))
query-view? (= :query-result feature-type)]
(cond
asset-tag?
:block/uuid
configured-ident
configured-ident
(or tag-view? query-view?)
(let [asset-idents (gallery-asset-property-idents db columns)]
(when (= 1 (count asset-idents))
(first asset-idents))))))
(defn- gallery-display-property-idents
[view columns asset-property-ident]
(let [configured-idents (set (keep :db/ident (:logseq.property.view/gallery-display-properties view)))
display-idents (if (seq configured-idents)
(->> columns
(keep gallery-column-ident)
(filter configured-idents)
vec)
[:block/title])]
(->> display-idents
(remove #{:select :id asset-property-ident})
vec)))
(defn- gallery-card-dimensions
[view]
(case (:logseq.property.view/gallery-card-size view)
:compact
gallery-compact-card-dimensions
:custom
(let [width (:logseq.property.view/gallery-card-width view)
height (:logseq.property.view/gallery-card-height view)]
(if (and (number? width) (number? height) (pos? width) (pos? height))
{:width (clamp-gallery-card-dimension width)
:height (clamp-gallery-card-dimension height)}
gallery-default-card-dimensions))
gallery-default-card-dimensions))
(defn- set-gallery-display-properties!
[view-entity property-idents]
(set-view-property! view-entity
:logseq.property.view/gallery-display-properties
(vec (keep property-ident->id property-idents))))
(defn- gallery-display-properties-menu
[view-entity columns]
(let [asset-property-ident (gallery-asset-property-ident (db/get-db) view-entity columns)
display-property-idents (set (gallery-display-property-idents view-entity columns asset-property-ident))
property-columns (remove #(contains? #{:select :id asset-property-ident} (:id %)) columns)]
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
(t :view.gallery/display-properties))
(shui/dropdown-menu-sub-content
(for [column property-columns]
(shui/dropdown-menu-checkbox-item
{:key (str "gallery-display-" (:id column))
:checked (contains? display-property-idents (:id column))
:onCheckedChange (fn [checked?]
(let [new-idents (if checked?
(conj display-property-idents (:id column))
(disj display-property-idents (:id column)))]
(set-gallery-display-properties! view-entity
(filter new-idents (map :id property-columns)))))
:onSelect (fn [e] (.preventDefault e))}
(:name column)))))))
(defn- gallery-asset-property-menu
[view-entity columns]
(let [asset-columns (seq (gallery-asset-columns columns))]
(when asset-columns
(let [asset-property-ident (gallery-asset-property-ident (db/get-db) view-entity columns)]
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
(t :view.gallery/asset-property))
(shui/dropdown-menu-sub-content
(for [column asset-columns]
(shui/dropdown-menu-checkbox-item
{:key (str "gallery-asset-" (:id column))
:checked (= asset-property-ident (:id column))
:onCheckedChange (fn [checked?]
(when checked?
(set-view-property! view-entity
:logseq.property.view/gallery-asset-property
(property-ident->id (:id column)))))
:onSelect (fn [e] (.preventDefault e))}
(:name column)))))))))
(defn- gallery-slider-value
[value]
(-> (js/Math.round value)
(max gallery-min-card-dimension)
(min gallery-max-card-dimension)))
(rum/defc gallery-card-size-slider
[label value on-change on-commit]
[:div.flex.flex-col.gap-2
[:div.flex.flex-row.items-center.justify-between.gap-3.text-sm.leading-none
[:span label]
[:span.font-medium.tabular-nums (str value \p \x)]]
(shui/slider
{:class "relative flex w-full touch-none select-none items-center"
:value #js [value]
:min gallery-min-card-dimension
:max gallery-max-card-dimension
:step 1
:on-value-change (fn [result]
(on-change (gallery-slider-value (first result))))
:on-value-commit (fn [result]
(on-commit (gallery-slider-value (first result))))}
(shui/slider-track
{:class "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"}
(shui/slider-range
{:class "absolute h-full bg-primary"}))
(shui/slider-thumb
{:class "block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none"}))])
(rum/defc gallery-custom-card-size-inputs
[view-entity dimensions set-size!]
(let [[width set-width!] (hooks/use-state (:width dimensions))
[height set-height!] (hooks/use-state (:height dimensions))
save-dimensions! (fn [width' height']
(p/do!
(set-size! :custom)
(set-view-property! view-entity :logseq.property.view/gallery-card-width width')
(set-view-property! view-entity :logseq.property.view/gallery-card-height height')))
stop-menu-input! (fn [e]
(when-not (= "Escape" (util/ekey e))
(util/stop-propagation e)))]
[:div
{:class "flex flex-col items-stretch gap-2 w-[320px] max-w-[calc(100vw-32px)] px-4 py-2"
:on-click util/stop-propagation
:on-key-down stop-menu-input!}
[:div.w-full.text-sm.leading-8 (t :view.gallery/custom-size)]
[:div.flex.flex-col.gap-4.w-full
(gallery-card-size-slider
(t :view.gallery/width)
width
set-width!
#(save-dimensions! % height))
(gallery-card-size-slider
(t :view.gallery/height)
height
set-height!
#(save-dimensions! width %))]]))
(defn- gallery-card-size-menu
[view-entity]
(let [size (:logseq.property.view/gallery-card-size view-entity)
dimensions (gallery-card-dimensions view-entity)
set-size! #(set-view-property! view-entity :logseq.property.view/gallery-card-size %)]
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
(t :view.gallery/card-size))
(shui/dropdown-menu-sub-content
(for [[value label] [[:default (t :view.gallery/default-size)]
[:compact (t :view.gallery/compact-size)]]]
(shui/dropdown-menu-checkbox-item
{:key (str "gallery-size-" (name value))
:checked (= value (or size :default))
:onCheckedChange #(when % (set-size! value))
:onSelect (fn [e] (.preventDefault e))}
label))
(gallery-custom-card-size-inputs view-entity dimensions set-size!)))))
(rum/defc groups-sort
[view-entity sort-by-value]
(let [property-ident (or (:db/ident sort-by-value) :block/journal-day)]
@@ -519,18 +809,15 @@
[view-entity columns {:keys [column-visible? rows column-toggle-visibility]} {:keys [group-by-property-ident]}]
(let [display-type (:db/ident (:logseq.property.view/type view-entity))
table? (= display-type :logseq.property.view/type.table)
gallery? (= display-type :logseq.property.view/type.gallery)
group-by-columns (concat (when (or
(contains? #{:linked-references :unlinked-references}
(:logseq.property.view/feature-type view-entity))
(:logseq.property/query view-entity))
[{:id :block/page
:name (t :view.table/page)}])
(filter (fn [column]
(when (:id column)
(when-let [p (db/entity (:id column))]
(and (not (db-property/many? p))
(contains? #{:default :number :checkbox :url :node :date}
(:logseq.property/type p)))))) columns))
(filter (fn [column]
(group-by-column? column)) columns))
group-by-page? (some #{:block/page} (map :id group-by-columns))]
(shui/dropdown-menu
(shui/dropdown-menu-trigger
@@ -541,7 +828,8 @@
:size :sm}
(ui/icon "dots" {:size 15})))
(shui/dropdown-menu-content
{:align "end"}
{:align "end"
:onCloseAutoFocus #(.preventDefault %)}
(shui/dropdown-menu-group
(when table?
(shui/dropdown-menu-sub
@@ -557,6 +845,12 @@
:onCheckedChange #(column-toggle-visibility column %)
:onSelect (fn [e] (.preventDefault e))}
(:name column))))))
(when gallery?
(gallery-display-properties-menu view-entity columns))
(when gallery?
(gallery-asset-property-menu view-entity columns))
(when gallery?
(gallery-card-size-menu view-entity))
(when (seq group-by-columns)
(shui/dropdown-menu-sub
(shui/dropdown-menu-sub-trigger
@@ -1205,7 +1499,8 @@
(fn []
(filter-property view-entity columns table opts))
{:align :end
:auto-focus? true}))}
:focus-trigger? false
:content-props {:onCloseAutoFocus #(.preventDefault %)}}))}
(ui/icon "filter")))
(defn operator->text
@@ -1342,7 +1637,7 @@
false
true)]
(shui/button
{:class "!px-2 rounded-none border-r"
{:class "!px-2 rounded-none border-r min-w-0 max-w-full overflow-hidden"
:variant "ghost"
:size :sm
:on-click (fn [e]
@@ -1423,25 +1718,26 @@
(keep #(db/entity [:block/uuid %]) value)
:else
value)]
[:div.flex.flex-row.items-center.gap-1.text-xs
(cond
(de/entity? value)
[:div (get-property-value-content value)]
[:div.ls-view-filter-value.flex.flex-row.items-center.gap-1.text-xs.min-w-0.max-w-full.overflow-hidden
(cond
(de/entity? value)
[:div.ls-view-filter-value-item (get-property-value-content value)]
(string? value)
[:div value]
(string? value)
[:div.ls-view-filter-value-item value]
(boolean? value)
[:div (str value)]
(boolean? value)
[:div.ls-view-filter-value-item (str value)]
(= value :empty)
[:div (t :view.filter/empty)]
(= value :empty)
[:div.ls-view-filter-value-item (t :view.filter/empty)]
(seq value)
(->> (map (fn [v] [:div (get-property-value-content v)]) value)
(interpose [:div (t :view.filter/or)]))
:else
(t :view/all))])))))
(seq value)
(->> (map (fn [v] [:span (get-property-value-content v)]) value)
(interpose [:span.flex-none ", "])
(into [:div.ls-view-filter-value-item]))
:else
(t :view/all))])))))
(rum/defc filter-value < rum/static
[view-entity table property operator value filters set-filters! idx opts]
@@ -1477,8 +1773,8 @@
(let [filters (get-in table [:state :filters])
{:keys [set-filters!]} data-fns]
(when (seq (:filters filters))
[:div.filters-row.flex.flex-row.items-center.gap-4.justify-between.flex-wrap.py-2
[:div.flex.flex-row.items-center.gap-2
[:div.filters-row.flex.flex-row.items-center.gap-4.justify-between.flex-wrap.py-2.min-w-0.max-w-full
[:div.flex.flex-row.items-center.gap-2.flex-wrap.min-w-0.max-w-full
(map-indexed
(fn [idx filter']
(let [[property-ident operator value] filter'
@@ -1489,7 +1785,7 @@
(some (fn [column] (when (= (:id column) property-ident)
{:db/ident (:id column)
:block/title (:name column)})) columns)))]
[:div.flex.flex-row.items-center.border.rounded
[:div.flex.flex-row.items-center.border.rounded.min-w-0.max-w-full
(shui/button
{:class "!px-2 rounded-none border-r"
:variant "ghost"
@@ -1600,7 +1896,7 @@
(set-sized-columns! sized-columns)))})
(rum/defc lazy-item
[data idx {:keys [properties list-view? scrolling?]} item-render]
[data idx {:keys [properties list-view? gallery-view? scrolling?]} item-render]
(let [item (util/nth-safe data idx)
db-id (cond (map? item) (:db/id item)
(number? item) item
@@ -1610,7 +1906,8 @@
(when (= :full (:block.temp/load-status e))
e)))
[item set-item!] (hooks/use-state entity)
opts (if list-view?
list-or-gallery? (or list-view? gallery-view?)
opts (if list-or-gallery?
{:skip-refresh? true
:children? false}
{:children? false
@@ -1622,7 +1919,7 @@
(m/sp
(when (and db-id (not item) (not scrolling?))
(let [block (c.m/<? (db-async/<get-block (state/get-current-repo) db-id opts))
block' (if list-view? (db/entity db-id) block)]
block' (if list-or-gallery? (db/entity db-id) block)]
(set-item! block')))))
[db-id scrolling?])
(let [item' (cond (map? item) item (number? item) {:db/id item})]
@@ -1717,33 +2014,220 @@
(lazy-item-render rows idx)
(str "partition-" idx)))))))
(rum/defc gallery-property-value
[block property-ident]
(if (= :block/title property-ident)
[:div.ls-gallery-card-title
(some->> (:block/title block)
string/trim
string/split-lines
first)]
(when-let [property (db/entity property-ident)]
[:div.ls-gallery-card-property
(pv/property-value block property {:view? true
:gallery-view? true})])))
(defn gallery-card-asset-block
[block asset-property-ident]
(let [asset-value (when (and block asset-property-ident (not= :block/uuid asset-property-ident))
(get block asset-property-ident))
->entity (fn [value]
(cond
(de/entity? value) value
(number? value) (db/entity value)
(uuid? value) (db/entity [:block/uuid value])
:else value))]
(cond
(= :block/uuid asset-property-ident)
block
(set? asset-value)
(some ->entity asset-value)
(sequential? asset-value)
(some ->entity asset-value)
:else
(->entity asset-value))))
(rum/defc gallery-card-item
[view-entity block config]
[:div.ls-card-item.content
{:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))}
[:div.-ml-4
(block-container (assoc config
:id (str (:block/uuid block))
:gallery-view? true
:view? true)
block)]])
[table view-entity block config {:keys [asset-property-ident display-property-idents]}]
(let [asset-block (gallery-card-asset-block block asset-property-ident)
asset-cp (state/get-component :block/asset-cp)
render-asset? (and asset-block (fn? asset-cp))
selected? ((:row-selected? table) block)]
[:div.ls-card-item.content
{:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))
:data-state (when selected? "selected")
:class (str (when render-asset? "has-gallery-asset")
(when selected? " is-selected"))
:on-click (fn [e]
(when-not (some-> (.-target e) (.closest (str "button, a, input, textarea, select, [role='menuitem'], "
".ls-gallery-card-media, .ls-gallery-card-property")))
(route-handler/redirect-to-page! (:block/uuid block))))}
[:div.ls-gallery-card-content
[:div.ls-gallery-card-media
(gallery-card-checkbox table block)
(when render-asset?
(asset-cp (assoc config :disable-resize? true :gallery-view? true) asset-block))]
[:div.ls-gallery-card-meta
(for [property-ident display-property-idents
:let [property-value (gallery-property-value block property-ident)]
:when property-value]
(rum/with-key
property-value
(str "gallery-property-" (:db/id block) "-" property-ident)))]]]))
(defn gallery-lazy-item-opts
[option]
(select-keys option [:properties]))
(defn view-row-ids
[rows]
(mapcat
(fn [row]
(cond
(number? row)
[row]
(map? row)
(when-let [id (:db/id row)]
[id])
(and (vector? row) (= 2 (count row)))
(view-row-ids (second row))
:else
[]))
rows))
(defn grouped-gallery-row-ids
[groups]
(vec (distinct (view-row-ids groups))))
(defn group-readable-property-value
[value]
(cond
(and (map? value) (or (:block/title value) (:logseq.property/value value)))
(db-property/property-value-content value)
(= (:db/ident value) :logseq.property/empty-placeholder)
(t :ui/empty)
:else
(str value)))
(rum/defc gallery-action-bar
[table option view-parent view-feature-type selected-rows]
(when (seq selected-rows)
(let [checkbox-id (str (:db/id (:view-entity table)) "-gallery-select-all")
checked? (or (:selected-all? table)
(and (:selected-some? table) "indeterminate"))]
[:div.ls-gallery-action-bar-slot
[:div.ls-gallery-action-bar
[:label.ls-gallery-action-select-all
{:html-for checkbox-id
:title (t :view.table/select-all)}
(shui/checkbox
{:id checkbox-id
:checked checked?
:on-checked-change (fn [value]
(p/do
(when value
(db-async/<get-blocks (state/get-current-repo) (:rows table) {}))
((:toggle-selected-all! table) table value)))
:aria-label (t :view.table/select-all)
:class "flex"})]
(action-bar table selected-rows
(assoc option
:on-delete-rows (fn [table selected-ids]
(on-delete-rows view-parent view-feature-type table selected-ids))))]])))
(rum/defcs gallery-view < rum/static mixins/container-id
[state {:keys [config]} table view-entity blocks *scroller-ref]
(let [config' (assoc config :container-id (:container-id state))]
[state {:keys [config view-parent view-feature-type] :as option} table view-entity blocks row-selection *scroller-ref]
(let [config' (assoc config :container-id (:container-id state))
columns (:columns table)
dimensions (gallery-card-dimensions view-entity)
asset-property-ident (gallery-asset-property-ident (db/get-db) view-entity columns)
display-property-idents (gallery-display-property-idents view-entity columns asset-property-ident)
selected-rows (shui/table-get-selection-rows row-selection (:rows table))
render-card (fn [idx]
(lazy-item blocks idx
(assoc (gallery-lazy-item-opts option)
:gallery-view? true)
(fn [block]
(gallery-card-item table view-entity block config'
{:asset-property-ident asset-property-ident
:display-property-idents display-property-idents}))))]
[:div.ls-cards
{:style {"--ls-gallery-card-width" (str (:width dimensions) "px")
"--ls-gallery-card-height" (str (:height dimensions) "px")}}
(when (seq blocks)
(ui/virtualized-grid
{:ref #(reset! *scroller-ref %)
:total-count (count blocks)
:custom-scroll-parent (get-scroll-parent config)
:skipAnimationFrameInResizeObserver true
:compute-item-key (fn [idx]
(str (:db/id view-entity) "-card-" idx))
:item-content (fn [idx]
(lazy-item (:data table) idx {}
(fn [block]
(gallery-card-item view-entity block config'))))}))]))
(if (:disable-virtualized? option)
[:div.virtuoso-grid-list
(for [idx (range (count blocks))]
[:div.virtuoso-grid-item
{:key (str (:db/id view-entity) "-card-" (util/nth-safe blocks idx))}
(render-card idx)])]
(ui/virtualized-grid
{:ref #(reset! *scroller-ref %)
:total-count (count blocks)
:custom-scroll-parent (get-scroll-parent config)
:skipAnimationFrameInResizeObserver true
:compute-item-key (fn [idx]
(str (:db/id view-entity) "-card-" (util/nth-safe blocks idx)))
:item-content render-card})))
(when-not (:hide-action-bar? option)
(gallery-action-bar table option view-parent view-feature-type selected-rows))]))
(rum/defc gallery-group
[view-entity option row-selection *scroller-ref groups idx table-map group-by-page? group-by-property]
(let [[value group] (nth groups idx)
table' (shui/table-option (assoc table-map :data group))
title (cond
(and group-by-page? (nil? value))
[:div.text-muted-foreground.text-sm
(t :view.table/pages)]
(some? value)
(group-readable-property-value value)
:else
(t :view.table/no-group-value (:block/title group-by-property)))]
[:div.ls-gallery-group
[:div.my-2 title]
(gallery-view (assoc option
:disable-virtualized? true
:hide-action-bar? true)
table'
view-entity
group
row-selection
*scroller-ref)]))
(rum/defc grouped-gallery-view < rum/static
[table-map table option view-entity groups row-selection group-by-property group-by-property-ident *scroller-ref]
(let [gallery-rows (grouped-gallery-row-ids groups)
gallery-action-table (shui/table-option
(assoc table-map
:data gallery-rows
:full-data (:full-data table)))
selected-rows (shui/table-get-selection-rows row-selection (:rows gallery-action-table))
group-by-page? (= :block/page group-by-property-ident)]
[:div.flex.flex-col.border-t.pt-2.gap-2
(virtualized-list
{:class "group-gallery-view"
:custom-scroll-parent (util/app-scroll-container-node)
:increase-viewport-by {:top 300 :bottom 300}
:compute-item-key (fn [idx]
(str "gallery-group-" (:db/id view-entity) "-" idx))
:skipAnimationFrameInResizeObserver true
:total-count (count groups)
:item-content
(fn [idx]
(gallery-group view-entity option row-selection *scroller-ref groups idx table-map group-by-page? group-by-property))}
false)
(gallery-action-bar gallery-action-table option (:view-parent option) (:view-feature-type option) selected-rows)]))
(defn- run-effects!
[option {:keys [data]} *scroller-ref gallery? set-ready?]
@@ -1833,7 +2317,9 @@
(shui/popup-show! (.-target e)
(fn [] (view-sorting-config table sorting columns))
{:align :end
:dropdown-menu? true}))}
:dropdown-menu? true
:focus-trigger? false
:content-props {:onCloseAutoFocus #(.preventDefault %)}}))}
(ui/icon "arrows-up-down")))
(rum/defc view-cp
@@ -1848,7 +2334,7 @@
(list-view option view-entity table *scroller-ref)
:logseq.property.view/type.gallery
(gallery-view option table view-entity (:rows table) *scroller-ref)
(gallery-view option table view-entity (:rows table) row-selection *scroller-ref)
(table-view table option row-selection *scroller-ref))]))
@@ -1948,21 +2434,25 @@
(t :view/rename))
(shui/dropdown-menu-sub-content
(when-let [block-container-cp (state/get-component :block/container)]
(block-container-cp {:display-title (display-view-title view)} view))))
(shui/dropdown-menu-item
{:key "Delete"
:on-click (fn []
(p/do!
(editor-handler/delete-block-aux! view)
(let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)]
(set-views! views')
(set-view-entity! (first views'))
(shui/popup-hide!))))}
(t :ui/delete))])
(block-container-cp {:display-title (display-view-title view)
:hide-block-control? true} view))))
(when (> (count views) 1)
(shui/dropdown-menu-item
{:key "Delete"
:on-click (fn []
(p/do!
(editor-handler/delete-block-aux! view)
(let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)]
(set-views! views')
(set-view-entity! (first views'))
(shui/popup-hide!))))}
(t :ui/delete)))])
{:as-dropdown? true
:dropdown-menu? true
:align "start"
:content-props {:onClick shui/popup-hide!}})
:focus-trigger? false
:content-props {:onClick shui/popup-hide!
:onCloseAutoFocus #(.preventDefault %)}})
(do
(set-view-entity! view)
(set-data! nil))))}
@@ -2018,7 +2508,8 @@
:opacity opacity
:references? references?)))]
[:div.view-actions.flex.items-center.gap-1.transition-opacity.ease-in.duration-300
{:class opacity}
{:class opacity
:on-mouse-down prevent-view-action-button-focus}
(when (seq additional-actions)
[:<> (for [action additional-actions]
@@ -2036,29 +2527,34 @@
:set-input! set-input!})]
[:div.view-action-type.text-muted-foreground.text-sm
(pv/property-value view-entity (db/entity :logseq.property.view/type) {:icon? true})]
(pv/property-value view-entity (db/entity :logseq.property.view/type) {:icon? true
:popup-focus-trigger? false
:popup-auto-focus-trigger? false})]
(more-actions view-entity columns table option)
(when add-new-object! (new-record-button table view-entity))]]))
(rum/defc group-item
[view-entity table' group group-by-property value option view-opts {:keys [list-view? group-by-page? readable-property-value]}]
[view-entity table' group group-by-property value option view-opts {:keys [list-view? gallery? group-by-page? readable-property-value]}]
(let [title [:div
{:class (when-not list-view? "my-2")}
(cond
group-by-page?
(if value
(let [c (state/get-component :block/page-cp)]
(c {:disable-preview? true} value))
[:div.text-muted-foreground.text-sm
(t :view.table/pages)])
group-by-page?
(if value
(let [c (state/get-component :block/page-cp)]
(if (fn? c)
(c {:disable-preview? true} value)
(readable-property-value value)))
[:div.text-muted-foreground.text-sm
(t :view.table/pages)])
(some? value)
(let [icon (pu/get-block-property-value value :logseq.property/icon)]
[:div.flex.flex-row.gap-1.items-center
(when icon (icon-component/icon icon {:color? true}))
(readable-property-value value)])
(some? value)
(let [icon (when (map? value)
(pu/get-block-property-value value :logseq.property/icon))]
[:div.flex.flex-row.gap-1.items-center
(when icon (icon-component/icon icon {:color? true}))
(readable-property-value value)])
:else
(t :view.table/no-group-value (:block/title group-by-property)))]
@@ -2067,7 +2563,8 @@
(assoc table' :rows group)
(assoc option
;; disabled virtualization for nested view
:disable-virtualized? true)
:disable-virtualized? true
:hide-action-bar? gallery?)
view-opts)]
(if (and list-view? (not (util/mobile?)))
[:div.-ml-2 render]
@@ -2168,40 +2665,35 @@
:add-new-object! add-new-object!}]
(if (and group-by-property-ident (not (number? (first (:rows table)))))
(when (and ready? (seq (:rows table)))
[:div.flex.flex-col.border-t.pt-2.gap-2
(virtualized-list
{:class (when list-view? "group-list-view")
:custom-scroll-parent (util/app-scroll-container-node (rum/deref *view-ref))
:increase-viewport-by {:top 300 :bottom 300}
:compute-item-key (fn [idx]
(str "table-group" idx))
:skipAnimationFrameInResizeObserver true
:total-count (count (:rows table))
:item-content (fn [idx]
(let [[value group] (nth (:rows table) idx)
add-new-object! (when (fn? add-new-object!)
(fn [_]
(add-new-object! view-entity table
{:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}})))
table' (shui/table-option (-> table-map
(assoc-in [:data-fns :add-new-object!] add-new-object!)
(assoc :data group ; data for this group
)))
readable-property-value #(cond (and (map? %) (or (:block/title %) (:logseq.property/value %)))
(db-property/property-value-content %)
(= (:db/ident %) :logseq.property/empty-placeholder)
(t :ui/empty)
:else
(str %))
group-by-page? (= :block/page group-by-property-ident)
key (str (:db/id view-entity) "-group-idx-" idx)]
(rum/with-key
(group-item view-entity table' group group-by-property value option view-opts
{:list-view? list-view?
:group-by-page? group-by-page?
:readable-property-value readable-property-value})
key)))}
disable-virtualized?)])
(if gallery?
(grouped-gallery-view table-map table option view-entity (:rows table) row-selection
group-by-property group-by-property-ident *scroller-ref)
[:div.flex.flex-col.border-t.pt-2.gap-2
(virtualized-list
{:class (when list-view? "group-list-view")
:custom-scroll-parent (util/app-scroll-container-node (rum/deref *view-ref))
:increase-viewport-by {:top 300 :bottom 300}
:compute-item-key (fn [idx]
(str "table-group" idx))
:skipAnimationFrameInResizeObserver true
:total-count (count (:rows table))
:item-content (fn [idx]
(let [[value group] (nth (:rows table) idx)
add-new-object! (when (fn? add-new-object!)
(fn [_]
(add-new-object! view-entity table
{:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}})))
table' (shui/table-option (-> table-map
(assoc-in [:data-fns :add-new-object!] add-new-object!)
(assoc :data group)))
key (str (:db/id view-entity) "-group-idx-" idx)]
(rum/with-key
(group-item view-entity table' group group-by-property value option view-opts
{:list-view? list-view?
:group-by-page? (= :block/page group-by-property-ident)
:readable-property-value group-readable-property-value})
key)))}
disable-virtualized?)]))
(view-cp view-entity table
(assoc option
:group-by-property-ident group-by-property-ident

View File

@@ -150,7 +150,12 @@
["65.29" {:fix add-single-block-comment-targets}]
["65.30" {:properties [:logseq.property/assignee]}]
["65.31" {:properties [:logseq.property.agent/session-id]}]
["65.32" {:fix repair-comment-classes-and-targets}]])
["65.32" {:fix repair-comment-classes-and-targets}]
["65.33" {:properties [:logseq.property.view/gallery-asset-property
:logseq.property.view/gallery-display-properties
:logseq.property.view/gallery-card-size
:logseq.property.view/gallery-card-width
:logseq.property.view/gallery-card-height]}]])
(let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
schema-version->updates)))]

View File

@@ -1915,6 +1915,15 @@
:view.filter/to "to"
:view.filter/type-to-search "Type to search"
:view.gallery/asset-property "Asset property"
:view.gallery/card-size "Card size"
:view.gallery/compact-size "Compact"
:view.gallery/custom-size "Custom"
:view.gallery/default-size "Default"
:view.gallery/display-properties "Display properties"
:view.gallery/height "Height"
:view.gallery/width "Width"
:view.table/ascending "Ascending"
:view.table/columns-visibility "Columns visibility"
:view.table/default-title (fn [total] (str total (if (<= total 1) " Node" " Nodes")))

View File

@@ -1897,6 +1897,15 @@
:view.filter/to "到"
:view.filter/type-to-search "输入以搜索"
:view.gallery/asset-property "资源属性"
:view.gallery/card-size "卡片尺寸"
:view.gallery/compact-size "紧凑"
:view.gallery/custom-size "自定义"
:view.gallery/default-size "默认"
:view.gallery/display-properties "显示属性"
:view.gallery/height "高度"
:view.gallery/width "宽度"
:view.table/ascending "升序"
:view.table/columns-visibility "列可见性"
:view.table/default-title "{1} 个节点"

View File

@@ -1,5 +1,6 @@
(ns frontend.components.views-test
(:require [cljs.test :refer [deftest is]]
[frontend.db]
[frontend.components.views :as views]))
(deftest build-columns-should-allow-name-property-when-no-object-name
@@ -38,3 +39,36 @@
ordered-ids [:c :b :a :c :b]
sorted (views/sort-columns columns ordered-ids)]
(is (= [:c :b :a] (map :id sorted)))))
(deftest gallery-lazy-item-opts-should-request-view-properties
(let [properties [:block/title :user.property/cover :block/uuid]]
(is (= {:properties properties}
(views/gallery-lazy-item-opts {:properties properties})))))
(deftest gallery-card-asset-block-should-use-row-for-asset-class
(let [block {:db/id 1
:block/title "Inception poster"
:block/uuid #uuid "11111111-1111-1111-1111-111111111111"}]
(is (= block
(views/gallery-card-asset-block block :block/uuid)))))
(deftest view-row-ids-should-flatten-grouped-rows
(is (= [1 2 3 4]
(#'views/view-row-ids
[[:group-a [1 2]]
[:group-b [3 4]]]))))
(deftest grouped-gallery-row-ids-should-deduplicate-grouped-rows
(is (= [1 2 3]
(#'views/grouped-gallery-row-ids
[[:group-a [1 2]]
[:group-b [{:db/id 2} 3]]]))))
(deftest group-by-column-should-exclude-name-and-include-many-properties
(with-redefs [frontend.db/entity (fn [id]
(case id
:block/title {:logseq.property/type :string}
:block/tags {:logseq.property/type :class
:db/cardinality :db.cardinality/many}))]
(is (not (views/group-by-column? {:id :block/title})))
(is (views/group-by-column? {:id :block/tags}))))

View File

@@ -285,3 +285,36 @@
(is (= #{(:db/id (d/entity migration-db [:block/uuid target-uuid]))}
(ref-ids (:logseq.property.comments/blocks
(d/entity migration-db [:block/uuid comments-area-uuid]))))))))
(deftest migrate-65-33-adds-gallery-view-properties
(let [conn (d/create-conn db-schema/schema)
property-idents [:logseq.property.view/gallery-asset-property
:logseq.property.view/gallery-display-properties
:logseq.property.view/gallery-card-size
:logseq.property.view/gallery-card-width
:logseq.property.view/gallery-card-height]]
(d/transact! conn [{:db/ident :logseq.kv/schema-version
:kv/value {:major 65 :minor 32}}])
(is (every? nil? (map #(d/entity @conn %) property-idents)))
(db-migrate/migrate conn :target-version {:major 65 :minor 33})
(is (= {:major 65 :minor 33}
(:kv/value (d/entity @conn :logseq.kv/schema-version))))
(is (every? some? (map #(d/entity @conn %) property-idents)))
(is (= :property
(:logseq.property/type
(d/entity @conn :logseq.property.view/gallery-asset-property))))
(is (= :db.cardinality/many
(:db/cardinality
(d/entity @conn :logseq.property.view/gallery-display-properties))))
(is (= :keyword
(:logseq.property/type
(d/entity @conn :logseq.property.view/gallery-card-size))))
(is (= :default
(:logseq.property/scalar-default-value
(d/entity @conn :logseq.property.view/gallery-card-size))))
(is (every? #(= :raw-number (:logseq.property/type (d/entity @conn %)))
[:logseq.property.view/gallery-card-width
:logseq.property.view/gallery-card-height]))))