diff --git a/deps/db/src/logseq/db/common/view.cljs b/deps/db/src/logseq/db/common/view.cljs index 66984c2a72..65111cb9dd 100644 --- a/deps/db/src/logseq/db/common/view.cljs +++ b/deps/db/src/logseq/db/common/view.cljs @@ -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? diff --git a/deps/db/src/logseq/db/frontend/property.cljs b/deps/db/src/logseq/db/frontend/property.cljs index 82c1e369db..35e5cbbfcc 100644 --- a/deps/db/src/logseq/db/frontend/property.cljs +++ b/deps/db/src/logseq/db/frontend/property.cljs @@ -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 diff --git a/deps/db/src/logseq/db/frontend/schema.cljs b/deps/db/src/logseq/db/frontend/schema.cljs index 365223d456..27bf22ae39 100644 --- a/deps/db/src/logseq/db/frontend/schema.cljs +++ b/deps/db/src/logseq/db/frontend/schema.cljs @@ -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. diff --git a/deps/db/test/logseq/db/common/view_test.cljs b/deps/db/test/logseq/db/common/view_test.cljs index b032c27857..62bd301f12 100644 --- a/deps/db/test/logseq/db/common/view_test.cljs +++ b/deps/db/test/logseq/db/common/view_test.cljs @@ -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 diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 5bd41bfc47..1c3fed2e07 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -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 diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index a90e92a017..712f6415fd 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -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; diff --git a/src/main/frontend/components/block/views.css b/src/main/frontend/components/block/views.css index a8ba9a4ced..80c14219c1 100644 --- a/src/main/frontend/components/block/views.css +++ b/src/main/frontend/components/block/views.css @@ -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%; + } } } diff --git a/src/main/frontend/components/property/value.cljs b/src/main/frontend/components/property/value.cljs index 5a3270260b..7281bca93f 100644 --- a/src/main/frontend/components/property/value.cljs +++ b/src/main/frontend/components/property/value.cljs @@ -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] diff --git a/src/main/frontend/components/views.cljs b/src/main/frontend/components/views.cljs index 955adece17..1169b7b73e 100644 --- a/src/main/frontend/components/views.cljs +++ b/src/main/frontend/components/views.cljs @@ -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/ 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/> (: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/ (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 diff --git a/src/main/frontend/worker/db/migrate.cljs b/src/main/frontend/worker/db/migrate.cljs index 1ac6e8fa62..853edefacd 100644 --- a/src/main/frontend/worker/db/migrate.cljs +++ b/src/main/frontend/worker/db/migrate.cljs @@ -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)))] diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn index a10bb823b1..8bdff65b27 100644 --- a/src/resources/dicts/en.edn +++ b/src/resources/dicts/en.edn @@ -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"))) diff --git a/src/resources/dicts/zh-cn.edn b/src/resources/dicts/zh-cn.edn index c518eb8935..d11455c3d8 100644 --- a/src/resources/dicts/zh-cn.edn +++ b/src/resources/dicts/zh-cn.edn @@ -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} 个节点" diff --git a/src/test/frontend/components/views_test.cljs b/src/test/frontend/components/views_test.cljs index db023f9399..4a0c03e0c1 100644 --- a/src/test/frontend/components/views_test.cljs +++ b/src/test/frontend/components/views_test.cljs @@ -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})))) diff --git a/src/test/frontend/worker/migrate_test.cljs b/src/test/frontend/worker/migrate_test.cljs index e3fd937211..c5be7968e3 100644 --- a/src/test/frontend/worker/migrate_test.cljs +++ b/src/test/frontend/worker/migrate_test.cljs @@ -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]))))