diff --git a/clj-e2e/dev/user.clj b/clj-e2e/dev/user.clj index fbcbe1534b..2ee4424ccd 100644 --- a/clj-e2e/dev/user.clj +++ b/clj-e2e/dev/user.clj @@ -6,6 +6,7 @@ [logseq.e2e.config :as config] [logseq.e2e.editor-basic-test] [logseq.e2e.fixtures :as fixtures] + [logseq.e2e.flashcards-basic-test] [logseq.e2e.graph :as graph] [logseq.e2e.keyboard :as k] [logseq.e2e.locator :as loc] @@ -46,6 +47,11 @@ (->> (future (run-tests 'logseq.e2e.property-basic-test)) (swap! *futures assoc :property-test))) +(defn run-flashcards-basic-test + [] + (->> (future (run-tests 'logseq.e2e.flashcards-basic-test)) + (swap! *futures assoc :flashcards-test))) + (defn run-property-scoped-choices-test [] (->> (future (run-tests 'logseq.e2e.property-scoped-choices-test)) @@ -111,7 +117,8 @@ 'logseq.e2e.plugins-basic-test 'logseq.e2e.reference-basic-test 'logseq.e2e.property-basic-test - 'logseq.e2e.tag-basic-test) + 'logseq.e2e.tag-basic-test + 'logseq.e2e.flashcards-basic-test) (System/exit 0)) (defn start diff --git a/clj-e2e/src/logseq/e2e/api.clj b/clj-e2e/src/logseq/e2e/api.clj new file mode 100644 index 0000000000..354854d4b8 --- /dev/null +++ b/clj-e2e/src/logseq/e2e/api.clj @@ -0,0 +1,39 @@ +(ns logseq.e2e.api + (:require + [clojure.string :as string] + [jsonista.core :as json] + [wally.main :as w])) + +(defn- to-snake-case + "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores. + Examples: + 'HelloWorld' -> 'hello_world' + 'Hello World' -> 'hello_world' + 'hello-world' -> 'hello_world' + 'Hello__World' -> 'hello_world'" + [s] + (when (string? s) + (-> s + ;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores + (string/replace #"[-\s]+" "_") + ;; Split on uppercase letters (except at start) and join with underscore + (string/replace #"(? { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1) + args (json/write-value-as-string (vec args))] + ;; (prn "Debug: eval-js #" estr args) + (w/eval-js estr args))) diff --git a/clj-e2e/src/logseq/e2e/keyboard.clj b/clj-e2e/src/logseq/e2e/keyboard.clj index bebebda1d2..5dc1da72a6 100644 --- a/clj-e2e/src/logseq/e2e/keyboard.clj +++ b/clj-e2e/src/logseq/e2e/keyboard.clj @@ -17,6 +17,8 @@ (def arrow-up #(press "ArrowUp")) (def arrow-down #(press "ArrowDown")) +(def arrow-left #(press "ArrowLeft")) +(def arrow-right #(press "ArrowRight")) (def meta+shift+arrow-up #(press (str (if mac? "Meta" "Alt") "+Shift+ArrowUp"))) (def meta+shift+arrow-down #(press (str (if mac? "Meta" "Alt") "+Shift+ArrowDown"))) diff --git a/clj-e2e/test/logseq/e2e/flashcards_basic_test.clj b/clj-e2e/test/logseq/e2e/flashcards_basic_test.clj new file mode 100644 index 0000000000..dc3849004e --- /dev/null +++ b/clj-e2e/test/logseq/e2e/flashcards_basic_test.clj @@ -0,0 +1,83 @@ +(ns logseq.e2e.flashcards-basic-test + (:require [clojure.test :refer [deftest testing use-fixtures]] + [logseq.e2e.api :refer [ls-api-call!]] + [logseq.e2e.assert :as assert] + [logseq.e2e.fixtures :as fixtures] + [logseq.e2e.keyboard :as k] + [logseq.e2e.locator :as loc] + [logseq.e2e.util :as util] + [wally.main :as w] + [wally.repl :as repl])) + +(use-fixtures :once fixtures/open-page) + +(use-fixtures :each + fixtures/validate-graph) + +(defn- open-flashcards + [] + (util/double-esc) + (k/press ["t" "c"]) + (assert/assert-is-visible "#cards-modal")) + +(defn- select-cards-option + [label] + (w/click "#cards-modal [role='combobox']") + (w/click (loc/filter "[role='option']" :has-text label))) + +(defn- click-flashcards-plus + [] + (w/click "#ls-cards-add")) + +(defn- setup-flashcards-data! + [{:keys [page-name tag-a tag-b card-a card-b query-a query-b]}] + (ls-api-call! :editor.appendBlockInPage page-name (str card-a " #Card #" tag-a)) + (ls-api-call! :editor.appendBlockInPage page-name (str card-b " #Card #" tag-b)) + (let [cards (ls-api-call! :editor.getTag "logseq.class/Cards") + cards-id (get cards "id") + cards-a (ls-api-call! :editor.appendBlockInPage page-name "Cards A" + {:properties {:block/tags #{cards-id}}}) + cards-b (ls-api-call! :editor.appendBlockInPage page-name "Cards B" + {:properties {:block/tags #{cards-id}}}) + query-a-id (get cards-a ":logseq.property/query") + query-b-id (get cards-b ":logseq.property/query")] + (ls-api-call! :editor.updateBlock query-a-id query-a) + (ls-api-call! :editor.updateBlock query-b-id query-b))) + +(deftest flashcards-plus-and-switching-test + (testing "create #Cards blocks from flashcards dialog and switch card sets" + (let [tag-a "fc-tag-a" + tag-b "fc-tag-b" + card-a "Card A" + card-b "Card B" + query-a (str "[[" tag-a "]]") + query-b (str "[[" tag-b "]]")] + (util/goto-journals) + (let [page (ls-api-call! :editor.getCurrentPage) + page-name (get page "name")] + (setup-flashcards-data! + {:page-name page-name + :tag-a tag-a + :tag-b tag-b + :card-a card-a + :card-b card-b + :query-a query-a + :query-b query-b})) + + (open-flashcards) + (click-flashcards-plus) + (w/wait-for ".ls-block .tag:has-text('Cards')") + + (open-flashcards) + (select-cards-option "Cards A") + (assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-a)) + (assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-b) 0) + (assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1")) + + (select-cards-option "Cards B") + (assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-b)) + (assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-a) 0) + (assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1")) + + (select-cards-option "All cards") + (assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/2"))))) diff --git a/clj-e2e/test/logseq/e2e/plugins_basic_test.clj b/clj-e2e/test/logseq/e2e/plugins_basic_test.clj index f671c4b8b0..93fab3cea6 100644 --- a/clj-e2e/test/logseq/e2e/plugins_basic_test.clj +++ b/clj-e2e/test/logseq/e2e/plugins_basic_test.clj @@ -1,9 +1,8 @@ (ns logseq.e2e.plugins-basic-test (:require [clojure.set :as set] - [clojure.string :as string] [clojure.test :refer [deftest testing is use-fixtures]] - [jsonista.core :as json] + [logseq.e2e.api :refer [ls-api-call!]] [logseq.e2e.assert :as assert] [logseq.e2e.fixtures :as fixtures] [logseq.e2e.keyboard :as k] @@ -19,45 +18,11 @@ [property-name] (str ":plugin.property._test_plugin/" property-name)) -(defn- to-snake-case - "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores. - Examples: - 'HelloWorld' -> 'hello_world' - 'Hello World' -> 'hello_world' - 'hello-world' -> 'hello_world' - 'Hello__World' -> 'hello_world'" - [s] - (when (string? s) - (-> s - ;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores - (clojure.string/replace #"[-\s]+" "_") - ;; Split on uppercase letters (except at start) and join with underscore - (clojure.string/replace #"(? { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1) - args (json/write-value-as-string (vec args))] - ;; (prn "Debug: eval-js #" estr args) - (w/eval-js estr args))) - (defn- assert-api-ls-block! ([ret] (assert-api-ls-block! ret 1)) ([ret-or-uuid count] diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index b10508206a..65f454c03b 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -1947,21 +1947,7 @@ (= 1 (count block-ast-title)) (= "Link" (ffirst block-ast-title))) (assoc :node-ref-link-only? true))] - (map-inline config' block-ast-title)) - - (when (and (seq block-ast-title) (ldb/class-instance? - (entity-plus/entity-memoized (db/get-db) :logseq.class/Cards) - block)) - [(ui/tooltip - (shui/button - {:variant :ghost - :size :sm - :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground" - :on-click (fn [e] - (util/stop e) - (state/pub-event! [:modal/show-cards (:db/id block)]))} - "Practice") - [:div "Practice cards"])]))))))) + (map-inline config' block-ast-title)))))))) (rum/defc block-title-aux [config block {:keys [query? *show-query?]}] @@ -1995,13 +1981,26 @@ (when (fn? on-title-click) {:on-click on-title-click}))) (cond - (and query? (and blank? (or advanced-query? show-query?))) + (and query? blank? (or advanced-query? show-query?)) [:span.opacity-75.hover:opacity-100 "Untitled query"] (and query? blank?) (query-builder-component/builder query {}) :else (text-block-title config block)) query-setting + (when (ldb/class-instance? + (entity-plus/entity-memoized (db/get-db) :logseq.class/Cards) + block) + [(ui/tooltip + (shui/button + {:variant :ghost + :size :sm + :class "!px-1 text-xs text-muted-foreground" + :on-click (fn [e] + (util/stop e) + (state/pub-event! [:modal/show-cards (:db/id block)]))} + "Practice") + [:div "Practice cards"])]) (when-let [property (:logseq.property/created-from-property block)] (when-let [message (when (= :url (:logseq.property/type property)) (first (outliner-property/validate-property-value (db/get-db) property (:db/id block))))] diff --git a/src/main/frontend/extensions/fsrs.cljs b/src/main/frontend/extensions/fsrs.cljs index 841c75d139..45998f3e09 100644 --- a/src/main/frontend/extensions/fsrs.cljs +++ b/src/main/frontend/extensions/fsrs.cljs @@ -6,13 +6,16 @@ [frontend.components.block :as component-block] [frontend.components.macro :as component-macro] [frontend.context.i18n :refer [t]] + [frontend.date :as date] [frontend.db :as db] [frontend.db-mixins :as db-mixins] [frontend.db.async :as db-async] [frontend.db.model :as db-model] [frontend.db.query-dsl :as query-dsl] [frontend.handler.block :as block-handler] + [frontend.handler.editor :as editor-handler] [frontend.handler.property :as property-handler] + [frontend.handler.route :as route-handler] [frontend.modules.shortcut.core :as shortcut] [frontend.state :as state] [frontend.ui :as ui] @@ -81,8 +84,12 @@ (defn- > tx-data + (filter (fn [d] (and (= :block/tags (:a d)) (:added d)))) + (map :e) + (distinct))] + (mapcat + (fn [eid] + (when-let [block (d/entity db-after eid)] + (when (ldb/class-instance? query-class block) + (let [query-entity (:logseq.property/query block)] + (when-not (and query-entity (:block/uuid query-entity)) + (let [query-text (if (string? query-entity) query-entity "") + value-block (db-property-build/build-property-value-block + block + query-property + query-text + {:block-uuid (common-uuid/gen-uuid)}) + value-uuid (:block/uuid value-block)] + [value-block + (outliner-core/block-with-updated-at + {:db/id (:db/id block) + :logseq.property/query [:block/uuid value-uuid]})])))))) + tagged-block-ids))))) + (defn- invoke-hooks-for-imported-graph [conn {:keys [tx-meta] :as tx-report}] (let [refs-tx-report (outliner-pipeline/transact-new-db-graph-refs conn tx-report) full-tx-data (concat (:tx-data tx-report) (:tx-data refs-tx-report)) @@ -402,6 +436,7 @@ toggle-page-and-block-tx-data (when (empty? fix-inline-page-tx-data) (toggle-page-and-block db tx-report)) display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data tx-meta) + ensure-query-tx-data (ensure-query-property-on-tag-additions tx-report) commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (rtc-tx-or-download-graph? tx-meta)) (commands/run-commands tx-report)) insert-templates-tx (when-not (rtc-tx-or-download-graph? tx-meta) @@ -410,6 +445,7 @@ (concat revert-tx-data toggle-page-and-block-tx-data display-blocks-tx-data + ensure-query-tx-data commands-tx insert-templates-tx created-by-tx diff --git a/src/test/frontend/worker/pipeline_test.cljs b/src/test/frontend/worker/pipeline_test.cljs index 2a8b6abca1..d3d70ac95c 100644 --- a/src/test/frontend/worker/pipeline_test.cljs +++ b/src/test/frontend/worker/pipeline_test.cljs @@ -93,3 +93,38 @@ ;; return global fn back to previous behavior (ldb/register-transact-pipeline-fn! identity))) + +(deftest ensure-query-property-on-tag-additions-test + (let [graph test-helper/test-db-name-db-version + conn (db-test/create-conn-with-blocks + {:pages-and-blocks [{:page {:block/title "page1"} + :blocks [{:block/title "b1"} + {:block/title "b2"}]}] + :classes {:QueryChild {:build/class-extends [:logseq.class/Query]}}}) + page (ldb/get-page @conn "page1") + blocks (:block/_page page) + b1 (some #(when (= "b1" (:block/title %)) %) blocks) + b2 (some #(when (= "b2" (:block/title %)) %) blocks) + query-child (ldb/get-page @conn "QueryChild")] + (ldb/register-transact-pipeline-fn! + (fn [tx-report] + (worker-pipeline/transact-pipeline graph tx-report))) + + (testing "tagging with #Query adds query property" + (ldb/transact! conn [[:db/add (:db/id b1) :block/tags :logseq.class/Query]]) + (let [block (d/entity @conn (:db/id b1)) + query (:logseq.property/query block)] + (is query) + (is (uuid? (:block/uuid query))) + (is (= "" (:block/title query))))) + + (testing "tagging with class extending #Query adds query property" + (ldb/transact! conn [[:db/add (:db/id b2) :block/tags (:db/id query-child)]]) + (let [block (d/entity @conn (:db/id b2)) + query (:logseq.property/query block)] + (is query) + (is (uuid? (:block/uuid query))) + (is (= "" (:block/title query))))) + + ;; return global fn back to previous behavior + (ldb/register-transact-pipeline-fn! identity)))