TweetVault④
前回
TweetVault①TweetVault②
TweetVault③
絞り込み UI の改善
テキストボックスにタグを入力する形で複数条件の絞り込みに対応したけど、タグ一覧から選択するようにしたい。- Ctrl を押しながら選択した場合は複数選択にする。
- 選択済みの場合は解除。
- 後述の除外選択中のタグを Ctrl で選択した場合は状態を上書き。
- Alt を押しながら選択した場合は除外するタグの選択として扱う
- こちらも複数選択可。
- 選択済みの場合は解除。
- 通常選択中のタグを Alt で選択した場合は状態を上書き。
- 通常選択のハイライトが青なのに対して、こちらは赤にする。
- Ctrl も Alt も押下していない場合は従前の通り排他選択。
- 複数選択や除外選択されているタグがある場合は解除する。
frontend/src/app/page.tsx
Ctrl/Alt 押下時の処理を追加。
@@ -17,3 +17,3 @@
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
- const [selectedTag, setSelectedTag] = useState<string>();
+ const [selectedTag, setSelectedTag] = useState({inc:[], exc:[]});
const [searchQuery, setSearchQuery] = useState('');
@@ -54,3 +54,3 @@
const data = await api.getBookmarks({
- tag: selectedTag,
+ tag: JSON.stringify(selectedTag),
search: searchQuery,
@@ -117,4 +117,26 @@
- function handleTagSelect(tag: string | undefined) {
- setSelectedTag(tag);
+ function handleTagSelect(tag: string | undefined, e?: any) {
+ if (!tag) {
+ setSelectedTag({inc:[], exc:[]});
+ return;
+ }
+ setSelectedTag(prev => {
+ if (e && e.ctrlKey) {
+ return {
+ inc: prev.inc.includes(tag)
+ ? prev.inc.filter(item => item !== tag)
+ : [...prev.inc, tag],
+ exc: prev.exc.filter(item => item !== tag)
+ };
+ } else if (e && e.altKey) {
+ return {
+ exc: prev.exc.includes(tag)
+ ? prev.exc.filter(item => item !== tag)
+ : [...prev.exc, tag],
+ inc: prev.inc.filter(item => item !== tag)
+ };
+ } else {
+ return {inc:[tag], exc:[]};
+ }
+ });
setCurrentPage(1);選択中のタグを保持する selectedTag を、文字列から対象/対象外リストを保持するオブジェクトに変更。Ctrl/Alt 押下時の処理を追加。
frontend/src/components/bookmarks/SearchAndFilter.tsx
キーボードイベントの引き渡しを追加。
@@ -8,3 +8,3 @@
onTagSelect: (tag: string | undefined) => void;
- selectedTag?: string;
+ selectedTag?: {};
onDeleteTag?: (tagName: string) => void;
@@ -88,3 +88,3 @@
// If the deleted tag was selected, reset to show all bookmarks
- if (selectedTag === tagName) {
+ if (selectedTag.inc.includes(tagName) || selectedTag.exc.includes(tagName)) {
onTagSelect(undefined);
@@ -122,3 +122,3 @@
className={`rounded-full px-3 py-1 text-sm whitespace-nowrap ${
- !selectedTag
+ selectedTag.inc.length == 0 && selectedTag.exc.length == 0
? 'bg-blue-600 text-white'
@@ -132,6 +132,6 @@
<div
- onClick={() => onTagSelect(tag.name)}
+ onClick={(e) => onTagSelect(tag.name, e)}
className={`px-3 py-1 text-sm whitespace-nowrap flex items-center cursor-pointer ${
- selectedTag === tag.name
- ? 'bg-blue-600 text-white'
+ selectedTag.inc.includes(tag.name) ? 'bg-blue-600 text-white' :
+ selectedTag.exc.includes(tag.name) ? 'bg-red-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600'選択中タグに関する判定を修正。キーボードイベントの引き渡しを追加。
frontend/src/components/bookmarks/TagMenu.tsx
@@ -8,3 +8,3 @@
onSuccess: () => void;
- selectedTag?: string;
+ selectedTag?: {};
onDeleteTag?: (tagName: string) => void;
@@ -134,3 +134,4 @@
className={`p-1 rounded-full cursor-pointer ${
- selectedTag === tag.name ? 'hover:bg-blue-700' : 'text-gray-500 hover:bg-gray-200'
+ selectedTag.inc.includes(tag.name) ? 'hover:bg-blue-700' :
+ selectedTag.exc.includes(tag.name) ? 'hover:bg-red-700' : 'text-gray-500 hover:bg-gray-200'
}`}選択中タグに関する判定を修正。backend/internal/services/bookmark_service.go(前回からの差分)
テキストボックスの入力からタグ検索する処理は除去。複数キーワード対応は残す。
元の処理に合わせるためにいったんアンマーシャルしているのが微妙。
@@ -4,3 +4,2 @@
"encoding/json"
- "fmt"
"log"
@@ -8,2 +7,3 @@
"strings"
+ "slices"
@@ -197,6 +197,9 @@
- type Tag struct {
+ type RequestTag struct {
+ Include []string `json:"inc"`
+ Exclude []string `json:"exc"`
+ }
+ type QueryTag struct {
Name string `json:"name"`
}
- var tags []Tag
@@ -208,6 +211,24 @@
// Apply tag filter if provided
- if tag == "untagged" {
- query = query.Where("tags_json::jsonb = ?", "[]")
- } else if tag != "" {
- tags = append(tags, Tag{Name: tag})
+ if tag != "" {
+ var req RequestTag
+ if err := json.Unmarshal([]byte(tag), &req); err != nil {
+ return nil, 0, err
+ }
+ if slices.Contains(req.Include, "untagged") {
+ query = query.Where("tags_json::jsonb = ?", "[]")
+ } else {
+ if len(req.Include) != 0 {
+ var tags []QueryTag
+ for _, v := range req.Include {
+ tags = append(tags, QueryTag{Name: v})
+ }
+ tagsJson, _ := json.Marshal(tags)
+ query = query.Where("tags_json::jsonb @> ?", tagsJson)
+ }
+ for _, v := range req.Exclude {
+ tags := []QueryTag{{Name: v}}
+ tagsJson, _ := json.Marshal(tags)
+ query = query.Not("tags_json::jsonb @> ?", tagsJson)
+ }
+ }
}
@@ -218,18 +239,5 @@
for _, word := range words {
- if strings.HasPrefix(word, "#") {
- tags = append(tags, Tag{Name: strings.TrimPrefix(word, "#")})
- } else if strings.HasPrefix(word, "-#") {
- query = query.Not("tags_json::jsonb @> ?", fmt.Sprintf(`[{"name":"%s"}]`, strings.TrimPrefix(word, "-#")))
- } else {
- query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?",
- "%"+word+"%", "%"+word+"%", "%"+word+"%")
- }
- }
- }
- if len(tags) != 0 {
- jsonData, err := json.Marshal(tags)
- if err != nil {
- return nil, 0, err
+ query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?",
+ "%"+word+"%", "%"+word+"%", "%"+word+"%")
}
- query = query.Where("tags_json::jsonb @> ?", jsonData)
}検索の実体。テキストボックスの入力からタグ検索する処理は除去。複数キーワード対応は残す。
元の処理に合わせるためにいったんアンマーシャルしているのが微妙。