Skip to content

Commit 41cbba3

Browse files
authored
enhance(ux): flashcards (#12299)
Enhances the flashcard user experience by adding automatic query property management, fixing critical bugs, and improving the UI for managing card sets. Key changes: 1. Automatic creation of query property blocks when tagging with #Query or subclasses via pipeline 2. Fixed critical bug in api-insert-new-block! where the end? parameter had inverted conditional logic 3. Added ability to create new #Cards blocks directly from the flashcard modal with a plus button
1 parent 1cf4fa9 commit 41cbba3

10 files changed

Lines changed: 289 additions & 76 deletions

File tree

clj-e2e/dev/user.clj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[logseq.e2e.config :as config]
77
[logseq.e2e.editor-basic-test]
88
[logseq.e2e.fixtures :as fixtures]
9+
[logseq.e2e.flashcards-basic-test]
910
[logseq.e2e.graph :as graph]
1011
[logseq.e2e.keyboard :as k]
1112
[logseq.e2e.locator :as loc]
@@ -46,6 +47,11 @@
4647
(->> (future (run-tests 'logseq.e2e.property-basic-test))
4748
(swap! *futures assoc :property-test)))
4849

50+
(defn run-flashcards-basic-test
51+
[]
52+
(->> (future (run-tests 'logseq.e2e.flashcards-basic-test))
53+
(swap! *futures assoc :flashcards-test)))
54+
4955
(defn run-property-scoped-choices-test
5056
[]
5157
(->> (future (run-tests 'logseq.e2e.property-scoped-choices-test))
@@ -111,7 +117,8 @@
111117
'logseq.e2e.plugins-basic-test
112118
'logseq.e2e.reference-basic-test
113119
'logseq.e2e.property-basic-test
114-
'logseq.e2e.tag-basic-test)
120+
'logseq.e2e.tag-basic-test
121+
'logseq.e2e.flashcards-basic-test)
115122
(System/exit 0))
116123

117124
(defn start

clj-e2e/src/logseq/e2e/api.clj

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
(ns logseq.e2e.api
2+
(:require
3+
[clojure.string :as string]
4+
[jsonista.core :as json]
5+
[wally.main :as w]))
6+
7+
(defn- to-snake-case
8+
"Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
9+
Examples:
10+
'HelloWorld' -> 'hello_world'
11+
'Hello World' -> 'hello_world'
12+
'hello-world' -> 'hello_world'
13+
'Hello__World' -> 'hello_world'"
14+
[s]
15+
(when (string? s)
16+
(-> s
17+
;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores
18+
(string/replace #"[-\s]+" "_")
19+
;; Split on uppercase letters (except at start) and join with underscore
20+
(string/replace #"(?<!^)([A-Z])" "_$1")
21+
;; Remove redundant underscores and trim
22+
(string/replace #"_+" "_")
23+
(string/trim)
24+
;; Convert to lowercase
25+
(string/lower-case))))
26+
27+
(defn ls-api-call!
28+
[api-keyword & args]
29+
(let [tag (name api-keyword)
30+
ns' (string/split tag #"\.")
31+
ns? (and (seq ns') (= (count ns') 2))
32+
inbuilt? (contains? #{"app" "editor"} (first ns'))
33+
ns1 (string/lower-case (if (and ns? (not inbuilt?))
34+
(str "sdk." (first ns')) "api"))
35+
name1 (if ns? (to-snake-case (last ns')) tag)
36+
estr (format "s => { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1)
37+
args (json/write-value-as-string (vec args))]
38+
;; (prn "Debug: eval-js #" estr args)
39+
(w/eval-js estr args)))

clj-e2e/src/logseq/e2e/keyboard.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
(def arrow-up #(press "ArrowUp"))
1919
(def arrow-down #(press "ArrowDown"))
20+
(def arrow-left #(press "ArrowLeft"))
21+
(def arrow-right #(press "ArrowRight"))
2022

2123
(def meta+shift+arrow-up #(press (str (if mac? "Meta" "Alt") "+Shift+ArrowUp")))
2224
(def meta+shift+arrow-down #(press (str (if mac? "Meta" "Alt") "+Shift+ArrowDown")))
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
(ns logseq.e2e.flashcards-basic-test
2+
(:require [clojure.test :refer [deftest testing use-fixtures]]
3+
[logseq.e2e.api :refer [ls-api-call!]]
4+
[logseq.e2e.assert :as assert]
5+
[logseq.e2e.fixtures :as fixtures]
6+
[logseq.e2e.keyboard :as k]
7+
[logseq.e2e.locator :as loc]
8+
[logseq.e2e.util :as util]
9+
[wally.main :as w]
10+
[wally.repl :as repl]))
11+
12+
(use-fixtures :once fixtures/open-page)
13+
14+
(use-fixtures :each
15+
fixtures/validate-graph)
16+
17+
(defn- open-flashcards
18+
[]
19+
(util/double-esc)
20+
(k/press ["t" "c"])
21+
(assert/assert-is-visible "#cards-modal"))
22+
23+
(defn- select-cards-option
24+
[label]
25+
(w/click "#cards-modal [role='combobox']")
26+
(w/click (loc/filter "[role='option']" :has-text label)))
27+
28+
(defn- click-flashcards-plus
29+
[]
30+
(w/click "#ls-cards-add"))
31+
32+
(defn- setup-flashcards-data!
33+
[{:keys [page-name tag-a tag-b card-a card-b query-a query-b]}]
34+
(ls-api-call! :editor.appendBlockInPage page-name (str card-a " #Card #" tag-a))
35+
(ls-api-call! :editor.appendBlockInPage page-name (str card-b " #Card #" tag-b))
36+
(let [cards (ls-api-call! :editor.getTag "logseq.class/Cards")
37+
cards-id (get cards "id")
38+
cards-a (ls-api-call! :editor.appendBlockInPage page-name "Cards A"
39+
{:properties {:block/tags #{cards-id}}})
40+
cards-b (ls-api-call! :editor.appendBlockInPage page-name "Cards B"
41+
{:properties {:block/tags #{cards-id}}})
42+
query-a-id (get cards-a ":logseq.property/query")
43+
query-b-id (get cards-b ":logseq.property/query")]
44+
(ls-api-call! :editor.updateBlock query-a-id query-a)
45+
(ls-api-call! :editor.updateBlock query-b-id query-b)))
46+
47+
(deftest flashcards-plus-and-switching-test
48+
(testing "create #Cards blocks from flashcards dialog and switch card sets"
49+
(let [tag-a "fc-tag-a"
50+
tag-b "fc-tag-b"
51+
card-a "Card A"
52+
card-b "Card B"
53+
query-a (str "[[" tag-a "]]")
54+
query-b (str "[[" tag-b "]]")]
55+
(util/goto-journals)
56+
(let [page (ls-api-call! :editor.getCurrentPage)
57+
page-name (get page "name")]
58+
(setup-flashcards-data!
59+
{:page-name page-name
60+
:tag-a tag-a
61+
:tag-b tag-b
62+
:card-a card-a
63+
:card-b card-b
64+
:query-a query-a
65+
:query-b query-b}))
66+
67+
(open-flashcards)
68+
(click-flashcards-plus)
69+
(w/wait-for ".ls-block .tag:has-text('Cards')")
70+
71+
(open-flashcards)
72+
(select-cards-option "Cards A")
73+
(assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-a))
74+
(assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-b) 0)
75+
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1"))
76+
77+
(select-cards-option "Cards B")
78+
(assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-b))
79+
(assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-a) 0)
80+
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1"))
81+
82+
(select-cards-option "All cards")
83+
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/2")))))

clj-e2e/test/logseq/e2e/plugins_basic_test.clj

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
(ns logseq.e2e.plugins-basic-test
22
(:require
33
[clojure.set :as set]
4-
[clojure.string :as string]
54
[clojure.test :refer [deftest testing is use-fixtures]]
6-
[jsonista.core :as json]
5+
[logseq.e2e.api :refer [ls-api-call!]]
76
[logseq.e2e.assert :as assert]
87
[logseq.e2e.fixtures :as fixtures]
98
[logseq.e2e.keyboard :as k]
@@ -19,45 +18,11 @@
1918
[property-name]
2019
(str ":plugin.property._test_plugin/" property-name))
2120

22-
(defn- to-snake-case
23-
"Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
24-
Examples:
25-
'HelloWorld' -> 'hello_world'
26-
'Hello World' -> 'hello_world'
27-
'hello-world' -> 'hello_world'
28-
'Hello__World' -> 'hello_world'"
29-
[s]
30-
(when (string? s)
31-
(-> s
32-
;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores
33-
(clojure.string/replace #"[-\s]+" "_")
34-
;; Split on uppercase letters (except at start) and join with underscore
35-
(clojure.string/replace #"(?<!^)([A-Z])" "_$1")
36-
;; Remove redundant underscores and trim
37-
(clojure.string/replace #"_+" "_")
38-
(clojure.string/trim)
39-
;; Convert to lowercase
40-
(clojure.string/lower-case))))
41-
4221
(defonce ^:private *property-idx (atom 0))
4322
(defn- new-property
4423
[]
4524
(str "p" (swap! *property-idx inc)))
4625

47-
(defn- ls-api-call!
48-
[tag & args]
49-
(let [tag (name tag)
50-
ns' (string/split tag #"\.")
51-
ns? (and (seq ns') (= (count ns') 2))
52-
inbuilt? (contains? #{"app" "editor"} (first ns'))
53-
ns1 (string/lower-case (if (and ns? (not inbuilt?))
54-
(str "sdk." (first ns')) "api"))
55-
name1 (if ns? (to-snake-case (last ns')) tag)
56-
estr (format "s => { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1)
57-
args (json/write-value-as-string (vec args))]
58-
;; (prn "Debug: eval-js #" estr args)
59-
(w/eval-js estr args)))
60-
6126
(defn- assert-api-ls-block!
6227
([ret] (assert-api-ls-block! ret 1))
6328
([ret-or-uuid count]

src/main/frontend/components/block.cljs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,21 +1947,7 @@
19471947
(= 1 (count block-ast-title))
19481948
(= "Link" (ffirst block-ast-title)))
19491949
(assoc :node-ref-link-only? true))]
1950-
(map-inline config' block-ast-title))
1951-
1952-
(when (and (seq block-ast-title) (ldb/class-instance?
1953-
(entity-plus/entity-memoized (db/get-db) :logseq.class/Cards)
1954-
block))
1955-
[(ui/tooltip
1956-
(shui/button
1957-
{:variant :ghost
1958-
:size :sm
1959-
:class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
1960-
:on-click (fn [e]
1961-
(util/stop e)
1962-
(state/pub-event! [:modal/show-cards (:db/id block)]))}
1963-
"Practice")
1964-
[:div "Practice cards"])])))))))
1950+
(map-inline config' block-ast-title))))))))
19651951

19661952
(rum/defc block-title-aux
19671953
[config block {:keys [query? *show-query?]}]
@@ -1995,13 +1981,26 @@
19951981
(when (fn? on-title-click)
19961982
{:on-click on-title-click})))
19971983
(cond
1998-
(and query? (and blank? (or advanced-query? show-query?)))
1984+
(and query? blank? (or advanced-query? show-query?))
19991985
[:span.opacity-75.hover:opacity-100 "Untitled query"]
20001986
(and query? blank?)
20011987
(query-builder-component/builder query {})
20021988
:else
20031989
(text-block-title config block))
20041990
query-setting
1991+
(when (ldb/class-instance?
1992+
(entity-plus/entity-memoized (db/get-db) :logseq.class/Cards)
1993+
block)
1994+
[(ui/tooltip
1995+
(shui/button
1996+
{:variant :ghost
1997+
:size :sm
1998+
:class "!px-1 text-xs text-muted-foreground"
1999+
:on-click (fn [e]
2000+
(util/stop e)
2001+
(state/pub-event! [:modal/show-cards (:db/id block)]))}
2002+
"Practice")
2003+
[:div "Practice cards"])])
20052004
(when-let [property (:logseq.property/created-from-property block)]
20062005
(when-let [message (when (= :url (:logseq.property/type property))
20072006
(first (outliner-property/validate-property-value (db/get-db) property (:db/id block))))]

0 commit comments

Comments
 (0)