Skip to content

Commit 63f41af

Browse files
committed
Add saved searches functionality
1 parent 18577e5 commit 63f41af

39 files changed

Lines changed: 1357 additions & 4 deletions

Gemfile.lock

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ GEM
206206
marcel (1.1.0)
207207
matrix (0.4.3)
208208
mini_mime (1.1.5)
209+
mini_portile2 (2.8.9)
209210
minitest (5.26.2)
210211
msgpack (1.8.0)
211212
multi_xml (0.7.2)
@@ -227,6 +228,9 @@ GEM
227228
net-protocol
228229
net-ssh (7.3.0)
229230
nio4r (2.7.5)
231+
nokogiri (1.18.10)
232+
mini_portile2 (~> 2.8.2)
233+
racc (~> 1.4)
230234
nokogiri (1.18.10-aarch64-linux-gnu)
231235
racc (~> 1.4)
232236
nokogiri (1.18.10-aarch64-linux-musl)
@@ -450,7 +454,7 @@ GEM
450454
concurrent-ruby (~> 1.0)
451455
unicode-display_width (3.2.0)
452456
unicode-emoji (~> 4.1)
453-
unicode-emoji (4.1.0)
457+
unicode-emoji (4.2.0)
454458
uniform_notifier (1.18.0)
455459
uri (1.1.1)
456460
useragent (0.16.11)

app/assets/stylesheets/components/admin.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,61 @@
11
/* Admin Styles */
22

3+
/* Tab Bar */
4+
.tab-bar {
5+
display: flex;
6+
gap: var(--spacing-1);
7+
border-bottom: var(--border-width-thick) solid var(--color-border);
8+
margin-bottom: var(--spacing-6);
9+
}
10+
11+
.tab {
12+
display: inline-block;
13+
padding: var(--spacing-3) var(--spacing-5);
14+
text-decoration: none;
15+
color: var(--color-text-secondary);
16+
font-weight: var(--font-weight-medium);
17+
font-size: var(--font-size-sm);
18+
border-bottom: var(--border-width-thick) solid transparent;
19+
margin-bottom: calc(-1 * var(--border-width-thick));
20+
transition: color var(--transition-fast), border-color var(--transition-fast);
21+
}
22+
23+
.tab:hover {
24+
color: var(--color-text-primary);
25+
}
26+
27+
.tab.is-active {
28+
color: var(--color-primary-700);
29+
border-bottom-color: var(--color-primary-600);
30+
font-weight: var(--font-weight-semibold);
31+
}
32+
33+
/* Saved Searches Actions */
34+
.saved-searches-actions {
35+
margin-bottom: var(--spacing-6);
36+
}
37+
38+
/* Flash Alert (inline form errors) */
39+
.flash-alert {
40+
background-color: var(--color-danger-soft);
41+
color: var(--color-danger);
42+
border: var(--border-width) solid var(--color-danger);
43+
padding: var(--spacing-3) var(--spacing-4);
44+
border-radius: var(--border-radius-md);
45+
margin-bottom: var(--spacing-4);
46+
font-size: var(--font-size-sm);
47+
}
48+
49+
.flash-alert ul {
50+
list-style: none;
51+
padding: 0;
52+
margin: 0;
53+
}
54+
55+
.flash-alert li + li {
56+
margin-top: var(--spacing-1);
57+
}
58+
359
/* Admin Users Table */
460
.settings-page .email-table-wrap {
561
overflow-x: auto;
@@ -365,6 +421,27 @@ textarea.form-input {
365421
font-size: var(--font-size-sm);
366422
}
367423

424+
.button-danger-secondary {
425+
display: inline-flex;
426+
align-items: center;
427+
gap: var(--spacing-2);
428+
padding: var(--spacing-3) var(--spacing-5);
429+
font-size: var(--font-size-base);
430+
font-weight: var(--font-weight-medium);
431+
border-radius: var(--border-radius-md);
432+
cursor: pointer;
433+
text-decoration: none;
434+
transition: var(--transition-fast);
435+
background: var(--color-bg-card);
436+
color: var(--color-danger);
437+
border: var(--border-width) solid var(--color-border);
438+
}
439+
440+
.button-danger-secondary:hover {
441+
border-color: var(--color-danger);
442+
background: var(--color-danger-soft);
443+
}
444+
368445
/* Admin Actions in Topic Header */
369446
.admin-actions {
370447
margin-top: var(--spacing-3);

app/assets/stylesheets/components/settings.css

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767

6868
.settings-page input[type="text"],
6969
.settings-page input[type="email"],
70-
.settings-page input[type="password"] {
70+
.settings-page input[type="password"],
71+
.settings-page input[type="number"] {
7172
width: 100%;
7273
padding: var(--spacing-4) var(--spacing-6);
7374
border-radius: 999px;
@@ -79,7 +80,28 @@
7980

8081
.settings-page input[type="text"]:focus,
8182
.settings-page input[type="email"]:focus,
82-
.settings-page input[type="password"]:focus {
83+
.settings-page input[type="password"]:focus,
84+
.settings-page input[type="number"]:focus {
85+
outline: none;
86+
border-color: var(--color-border-focus);
87+
box-shadow: var(--shadow-focus);
88+
}
89+
90+
.settings-page textarea {
91+
width: 100%;
92+
padding: var(--spacing-3) var(--spacing-4);
93+
border-radius: var(--border-radius-md);
94+
border: var(--border-width) solid var(--color-border);
95+
background: var(--color-bg-input);
96+
font-size: var(--font-size-sm);
97+
color: var(--color-text-primary);
98+
font-family: var(--font-family-base);
99+
resize: vertical;
100+
min-height: 60px;
101+
margin: 0;
102+
}
103+
104+
.settings-page textarea:focus {
83105
outline: none;
84106
border-color: var(--color-border-focus);
85107
box-shadow: var(--shadow-focus);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
class Admin::SavedSearchesController < Admin::BaseController
4+
before_action :set_saved_search, only: [:edit, :update, :destroy]
5+
6+
def active_admin_section
7+
:saved_searches
8+
end
9+
10+
def index
11+
@tab = params[:tab] || "global"
12+
@saved_searches = case @tab
13+
when "user_templates"
14+
SavedSearch.user_templates.order(:position, :name)
15+
when "team_templates"
16+
SavedSearch.team_templates.order(:position, :name)
17+
else
18+
SavedSearch.global_searches.order(:position, :name)
19+
end
20+
end
21+
22+
def new
23+
@saved_search = SavedSearch.new(scope: scope_from_tab)
24+
end
25+
26+
def create
27+
@saved_search = SavedSearch.new(saved_search_params)
28+
if @saved_search.save
29+
redirect_to admin_saved_searches_path(tab: tab_from_scope), notice: "Saved search created"
30+
else
31+
render :new, status: :unprocessable_entity
32+
end
33+
end
34+
35+
def edit
36+
end
37+
38+
def update
39+
if @saved_search.update(saved_search_params)
40+
redirect_to admin_saved_searches_path(tab: tab_from_scope), notice: "Saved search updated"
41+
else
42+
render :edit, status: :unprocessable_entity
43+
end
44+
end
45+
46+
def destroy
47+
tab = tab_from_scope
48+
@saved_search.destroy
49+
redirect_to admin_saved_searches_path(tab: tab), notice: "Saved search deleted"
50+
end
51+
52+
private
53+
54+
def set_saved_search
55+
@saved_search = SavedSearch.find(params[:id])
56+
end
57+
58+
def saved_search_params
59+
params.require(:saved_search).permit(:name, :query, :scope, :position)
60+
end
61+
62+
def scope_from_tab
63+
case params[:tab]
64+
when "user_templates" then "user"
65+
when "team_templates" then "team"
66+
else "global"
67+
end
68+
end
69+
70+
def tab_from_scope
71+
case @saved_search.scope
72+
when "user" then "user_templates"
73+
when "team" then "team_templates"
74+
else "global"
75+
end
76+
end
77+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Settings
4+
class SavedSearchPreferencesController < Settings::BaseController
5+
def create
6+
saved_search = SavedSearch.find(params[:saved_search_id])
7+
hidden = ActiveModel::Type::Boolean.new.cast(params[:hidden])
8+
9+
pref = SavedSearchPreference.find_or_initialize_by(
10+
saved_search: saved_search,
11+
user: current_user
12+
)
13+
pref.hidden = hidden
14+
pref.save!
15+
16+
redirect_back fallback_location: settings_saved_searches_path, notice: hidden ? "Search hidden" : "Search shown"
17+
end
18+
end
19+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
module Settings
4+
class SavedSearchesController < Settings::BaseController
5+
before_action :set_saved_search, only: [:edit, :update, :destroy]
6+
7+
def index
8+
@saved_searches = current_user.saved_searches.order(:position, :name)
9+
@system_searches = SavedSearch.user_templates.order(:position, :name)
10+
@global_searches = SavedSearch.scope_global.order(:position, :name)
11+
@hidden_ids = SavedSearchPreference
12+
.where(user: current_user, hidden: true)
13+
.pluck(:saved_search_id)
14+
.to_set
15+
end
16+
17+
def new
18+
@saved_search = current_user.saved_searches.build(scope: "user")
19+
end
20+
21+
def create
22+
@saved_search = current_user.saved_searches.build(saved_search_params)
23+
@saved_search.scope = "user"
24+
if @saved_search.save
25+
redirect_to settings_saved_searches_path, notice: "Saved search created"
26+
else
27+
render :new, status: :unprocessable_entity
28+
end
29+
end
30+
31+
def edit
32+
end
33+
34+
def update
35+
if @saved_search.update(saved_search_params)
36+
redirect_to settings_saved_searches_path, notice: "Saved search updated"
37+
else
38+
render :edit, status: :unprocessable_entity
39+
end
40+
end
41+
42+
def destroy
43+
@saved_search.destroy
44+
redirect_to settings_saved_searches_path, notice: "Saved search deleted"
45+
end
46+
47+
private
48+
49+
def active_settings_section
50+
:saved_searches
51+
end
52+
53+
def set_saved_search
54+
@saved_search = current_user.saved_searches.find(params[:id])
55+
end
56+
57+
def saved_search_params
58+
params.require(:saved_search).permit(:name, :query)
59+
end
60+
end
61+
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
module Settings
4+
module Teams
5+
class SavedSearchesController < Settings::BaseController
6+
before_action :set_team
7+
before_action :require_team_member!
8+
before_action :set_saved_search, only: [:edit, :update, :destroy]
9+
before_action :require_team_admin!, only: [:new, :create, :edit, :update, :destroy]
10+
11+
def index
12+
@saved_searches = @team.saved_searches.order(:position, :name)
13+
@system_searches = SavedSearch.team_templates.order(:position, :name)
14+
@hidden_ids = SavedSearchPreference
15+
.where(user: current_user, hidden: true)
16+
.pluck(:saved_search_id)
17+
.to_set
18+
end
19+
20+
def new
21+
@saved_search = @team.saved_searches.build(scope: "team")
22+
end
23+
24+
def create
25+
@saved_search = @team.saved_searches.build(saved_search_params)
26+
@saved_search.scope = "team"
27+
if @saved_search.save
28+
redirect_to settings_team_saved_searches_path(@team), notice: "Saved search created"
29+
else
30+
render :new, status: :unprocessable_entity
31+
end
32+
end
33+
34+
def edit
35+
end
36+
37+
def update
38+
if @saved_search.update(saved_search_params)
39+
redirect_to settings_team_saved_searches_path(@team), notice: "Saved search updated"
40+
else
41+
render :edit, status: :unprocessable_entity
42+
end
43+
end
44+
45+
def destroy
46+
@saved_search.destroy
47+
redirect_to settings_team_saved_searches_path(@team), notice: "Saved search deleted"
48+
end
49+
50+
private
51+
52+
def active_settings_section
53+
:teams
54+
end
55+
56+
def set_team
57+
@team = Team.find(params[:team_id])
58+
end
59+
60+
def set_saved_search
61+
@saved_search = @team.saved_searches.find(params[:id])
62+
end
63+
64+
def require_team_member!
65+
unless user_signed_in? && @team.member?(current_user)
66+
render file: Rails.root.join("public/404.html"), status: :not_found, layout: false
67+
end
68+
end
69+
70+
def require_team_admin!
71+
unless @team.admin?(current_user)
72+
redirect_to settings_team_saved_searches_path(@team), alert: "Admins only"
73+
end
74+
end
75+
76+
def saved_search_params
77+
params.require(:saved_search).permit(:name, :query)
78+
end
79+
end
80+
end
81+
end

0 commit comments

Comments
 (0)