Skip to content

Commit c202f9f

Browse files
committed
split mentions and tagging from the actual notes
Signed-off-by: Kai Wagner <kai.wagner@percona.com>
1 parent be5ad55 commit c202f9f

File tree

6 files changed

+133
-40
lines changed

6 files changed

+133
-40
lines changed

app/assets/stylesheets/components/notes.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@
107107
margin-bottom: var(--spacing-2);
108108
}
109109

110+
.note-form-fields {
111+
display: flex;
112+
flex-direction: column;
113+
gap: var(--spacing-2);
114+
}
115+
116+
.note-meta-fields {
117+
display: grid;
118+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
119+
gap: var(--spacing-3);
120+
}
121+
122+
.note-field label {
123+
display: block;
124+
font-weight: var(--font-weight-medium);
125+
margin-bottom: var(--spacing-1);
126+
color: var(--color-text-primary);
127+
}
128+
110129
.note-textarea {
111130
width: 100%;
112131
border-radius: var(--border-radius-sm);
@@ -116,6 +135,21 @@
116135
font-size: var(--font-size-base);
117136
}
118137

138+
.note-text-field {
139+
width: 100%;
140+
border-radius: var(--border-radius-sm);
141+
border: var(--border-width) solid var(--color-border);
142+
padding: var(--spacing-2);
143+
font-family: var(--font-family-base);
144+
font-size: var(--font-size-base);
145+
}
146+
147+
.note-hint {
148+
margin-top: 4px;
149+
color: var(--color-text-muted);
150+
font-size: var(--font-size-xs);
151+
}
152+
119153
.note-actions {
120154
display: flex;
121155
justify-content: flex-end;

app/controllers/notes_controller.rb

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ class NotesController < ApplicationController
77
def create
88
topic = Topic.find(note_params[:topic_id])
99
message = resolve_message(topic)
10-
note = NoteBuilder.new(author: current_user).create!(topic:, message:, body: note_params[:body])
10+
note = NoteBuilder.new(author: current_user).create!(
11+
topic:,
12+
message:,
13+
body: note_params[:body],
14+
tags: parsed_tags,
15+
mention_names: parsed_mentions
16+
)
1117

1218
redirect_to topic_path(topic, anchor: note_anchor(note)), notice: "Note added"
1319
rescue NoteBuilder::Error, ActiveRecord::RecordInvalid => e
@@ -16,6 +22,8 @@ def create
1622
body: note_params[:body],
1723
message_id: note_params[:message_id].presence,
1824
topic_id: note_params[:topic_id],
25+
tags_input: note_params[:tags_input],
26+
mentions_input: note_params[:mentions_input],
1927
error: e.message
2028
}
2129
redirect_back fallback_location: topic_path(topic)
@@ -24,7 +32,12 @@ def create
2432
def update
2533
return if performed?
2634

27-
NoteBuilder.new(author: current_user).update!(note: @note, body: note_params[:body])
35+
NoteBuilder.new(author: current_user).update!(
36+
note: @note,
37+
body: note_params[:body],
38+
tags: parsed_tags,
39+
mention_names: parsed_mentions
40+
)
2841

2942
redirect_to topic_path(@note.topic, anchor: note_anchor(@note)), notice: "Note updated"
3043
rescue NoteBuilder::Error, ActiveRecord::RecordInvalid => e
@@ -34,6 +47,8 @@ def update
3447
message_id: note_params[:message_id].presence,
3548
topic_id: note_params[:topic_id],
3649
note_id: @note.id,
50+
tags_input: note_params[:tags_input],
51+
mentions_input: note_params[:mentions_input],
3752
error: e.message
3853
}
3954
redirect_back fallback_location: topic_path(@note.topic)
@@ -67,7 +82,7 @@ def set_note
6782
end
6883

6984
def note_params
70-
params.require(:note).permit(:body, :topic_id, :message_id)
85+
params.require(:note).permit(:body, :topic_id, :message_id, :tags_input, :mentions_input)
7186
end
7287

7388
def resolve_message(topic)
@@ -82,4 +97,20 @@ def note_anchor(note)
8297
"thread-notes"
8398
end
8499
end
100+
101+
def parsed_tags
102+
split_tokens(note_params[:tags_input]) { |token| token.delete_prefix("#") }
103+
end
104+
105+
def parsed_mentions
106+
split_tokens(note_params[:mentions_input]) { |token| token.delete_prefix("@") }
107+
end
108+
109+
def split_tokens(raw, &block)
110+
Array(raw.to_s.split(/[,\s]+/))
111+
.map { |token| token.strip }
112+
.reject(&:blank?)
113+
.map(&block)
114+
.reject(&:blank?)
115+
end
85116
end

app/services/note_builder.rb

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ def initialize(author:)
77
@author = author
88
end
99

10-
def create!(topic:, message: nil, body:)
10+
def create!(topic:, message: nil, body:, tags: [], mention_names: [])
1111
raise Error, "You must be signed in to add a note" unless author
1212
body_text = body.to_s.strip
1313
raise Error, "Note cannot be blank" if body_text.blank?
1414

1515
ActiveRecord::Base.transaction do
1616
note = Note.new(topic:, message:, author:, last_editor: author, body: body_text)
1717
note.save!
18-
mentionables, tags = rebuild_mentions_and_tags!(note, body_text)
18+
mentionables, tags = rebuild_mentions_and_tags!(note, tags:, mention_names:)
1919
fan_out!(note:, mentionables:, mark_author_read: true)
2020
note
2121
end
2222
end
2323

24-
def update!(note:, body:)
24+
def update!(note:, body:, tags: [], mention_names: [])
2525
raise Error, "You do not have permission to edit this note" unless note.author_id == author.id
2626
body_text = body.to_s.strip
2727
raise Error, "Note cannot be blank" if body_text.blank?
@@ -30,7 +30,7 @@ def update!(note:, body:)
3030
note.note_edits.create!(editor: author, body: note.body) if note.body != body_text
3131
note.update!(body: body_text, last_editor: author)
3232

33-
mentionables, _tags = rebuild_mentions_and_tags!(note, body_text)
33+
mentionables, _tags = rebuild_mentions_and_tags!(note, tags:, mention_names:)
3434
fan_out!(note:, mentionables:, mark_author_read: false)
3535
note
3636
end
@@ -40,38 +40,37 @@ def update!(note:, body:)
4040

4141
attr_reader :author
4242

43-
def rebuild_mentions_and_tags!(note, body_text)
44-
mention_names = extract_mentions(body_text)
45-
mentionables = resolve_mentions(mention_names)
46-
tags = extract_tags(body_text)
43+
def rebuild_mentions_and_tags!(note, tags:, mention_names:)
44+
normalized_mentions = normalize_mentions(mention_names)
45+
mentionables = resolve_mentions(normalized_mentions)
46+
normalized_tags = normalize_tags(tags)
4747

4848
note.note_mentions.delete_all
4949
mentionables.each do |mentionable|
5050
note.note_mentions.create!(mentionable:)
5151
end
5252

5353
note.note_tags.delete_all
54-
tags.each do |tag|
54+
normalized_tags.each do |tag|
5555
note.note_tags.create!(tag:)
5656
end
5757

58-
[mentionables, tags]
58+
[mentionables, normalized_tags]
5959
end
6060

61-
def extract_mentions(text)
62-
text.to_s.scan(/(?:^|[^@\w])@([A-Za-z0-9_.-]+)/)
63-
.flatten
64-
.map { |m| NameReservation.normalize(m) }
65-
.reject(&:blank?)
66-
.uniq
61+
def normalize_mentions(mention_names)
62+
Array(mention_names)
63+
.map { |m| m.to_s.delete_prefix("@") }
64+
.map { |m| NameReservation.normalize(m) }
65+
.reject(&:blank?)
66+
.uniq
6767
end
6868

69-
def extract_tags(text)
70-
text.to_s.scan(/(?:^|[^#\w])#([A-Za-z0-9_.-]+)/)
71-
.flatten
72-
.map { |t| t.to_s.strip.downcase }
73-
.select { |tag| tag.match?(NoteTag::TAG_FORMAT) }
74-
.uniq
69+
def normalize_tags(tags)
70+
Array(tags)
71+
.map { |t| t.to_s.delete_prefix("#").strip.downcase }
72+
.select { |tag| tag.match?(NoteTag::TAG_FORMAT) }
73+
.uniq
7574
end
7675

7776
def resolve_mentions(names)

app/views/notes/_form.html.slim

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,33 @@
33
- note_error = flash[:note_error]&.with_indifferent_access
44

55
- prefill_body = form_note.body
6+
- prefill_tags = form_note.note_tags.map(&:tag).join(", ")
7+
- prefill_mentions = form_note.note_mentions.includes(:mentionable).map { |mention| note_mention_label(mention) }.join(", ")
68
- if note_error
79
- matches_note = note_error[:note_id].present? && form_note.persisted? && form_note.id == note_error[:note_id].to_i
810
- matches_new_note = !form_note.persisted? && note_error[:topic_id].to_i == topic.id && note_error[:message_id].to_s == message&.id.to_s
911
- if matches_note || matches_new_note
1012
- prefill_body = note_error[:body]
13+
- prefill_tags = note_error[:tags_input].to_s
14+
- prefill_mentions = note_error[:mentions_input].to_s
1115

1216
= form_with model: form_note, url: form_note.persisted? ? note_path(form_note) : notes_path, method: form_note.persisted? ? :patch : :post, data: { turbo: true } do |f|
1317
= hidden_field_tag "note[topic_id]", topic.id
1418
= hidden_field_tag "note[message_id]", message&.id
15-
.note-field
16-
= f.text_area :body, rows: 3, placeholder: "Add a note with @mentions and #tags…", required: true, class: "note-textarea", value: prefill_body
17-
- if note_error && prefill_body == note_error[:body]
18-
.note-error = note_error[:error]
19+
.note-form-fields
20+
.note-field
21+
= f.label :body, "Note"
22+
= f.text_area :body, rows: 4, placeholder: "Write your note", required: true, class: "note-textarea", value: prefill_body
23+
- if note_error && prefill_body == note_error[:body]
24+
.note-error = note_error[:error]
25+
.note-meta-fields
26+
.note-field
27+
= label_tag :note_mentions_input, "Mentions"
28+
= text_field_tag "note[mentions_input]", prefill_mentions, placeholder: "@alice, @team", class: "note-text-field"
29+
.note-hint "Notify people or teams. Separate with commas or spaces."
30+
.note-field
31+
= label_tag :note_tags_input, "Tags"
32+
= text_field_tag "note[tags_input]", prefill_tags, placeholder: "release, docs, follow-up", class: "note-text-field"
33+
.note-hint "Optional tags for filtering. Lowercase letters, numbers, dash, underscore, or dot."
1934
.note-actions
2035
= f.submit submit_label, class: "button-secondary"

db/seeds.rb

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -913,48 +913,56 @@ def mark_aware_until(user:, topic:, message:, timestamp:)
913913
alice_notes.create!(
914914
topic: patch_topic,
915915
message: msg3,
916-
body: "- Add WAL position to progress report\n- Split heap vs index counters\n- Autovacuum should emit too\n@ExampleCompany please sync on scope"
916+
body: "- Add WAL position to progress report\n- Split heap vs index counters\n- Autovacuum should emit too\nExampleCompany please sync on scope",
917+
mention_names: [example_team.name]
917918
)
918919
bob_notes.create!(
919920
topic: patch_topic,
920921
message: msg1,
921-
body: "Queued for next CF round; tracking in CF app.\n@ExampleCompany heads-up for review bandwidth"
922+
body: "Queued for next CF round; tracking in CF app.\nExampleCompany heads-up for review bandwidth",
923+
mention_names: [example_team.name]
922924
)
923925

924926
# RFC thread notes (thread + message, different authors)
925927
carol_notes.create!(
926928
topic: rfc_topic,
927-
body: "Thread summary: add index AM hooks, include sample AM + docs.\n@ExampleCompany track follow-ups"
929+
body: "Thread summary: add index AM hooks, include sample AM + docs.\nExampleCompany track follow-ups",
930+
mention_names: [example_team.name]
928931
)
929932
alice_notes.create!(
930933
topic: rfc_topic,
931934
message: rfc_msg2,
932-
body: "Docs + sample AM needed before commit. Add SGML + README.\n@ExampleCompany can we help draft?"
935+
body: "Docs + sample AM needed before commit. Add SGML + README.\nExampleCompany can we help draft?",
936+
mention_names: [example_team.name]
933937
)
934938

935939
# Discussion thread: mix of thread/message notes from different people
936940
carol_notes.create!(
937941
topic: discussion_topic,
938-
body: "Thread note: align logical slots with failover, add standby-safe flag.\n@ExampleCompany capture design risks"
942+
body: "Thread note: align logical slots with failover, add standby-safe flag.\nExampleCompany capture design risks",
943+
mention_names: [example_team.name]
939944
)
940945
alice_notes.create!(
941946
topic: discussion_topic,
942947
message: disc_msg4,
943-
body: "Message note: prototype slot fencing, watch cascading setups; prefer heartbeat piggyback.\n@ExampleCompany please review"
948+
body: "Message note: prototype slot fencing, watch cascading setups; prefer heartbeat piggyback.\nExampleCompany please review",
949+
mention_names: [example_team.name]
944950
)
945951

946952
# Committer topic: partially read with a note
947953
bob_notes.create!(
948954
topic: committer_topic,
949955
message: committer_messages[3],
950-
body: "Action: test lower freeze_age under heavy autovacuum.\n@ExampleCompany check lab capacity"
956+
body: "Action: test lower freeze_age under heavy autovacuum.\nExampleCompany check lab capacity",
957+
mention_names: [example_team.name]
951958
)
952959

953960
# Moderate topic note for visibility
954961
carol_notes.create!(
955962
topic: moderate_topic_1,
956963
message: moderate_msgs_1[5],
957-
body: "Pooling experiments look good; consider adaptive target.\n@ExampleCompany let's benchmark with PG16"
964+
body: "Pooling experiments look good; consider adaptive target.\nExampleCompany let's benchmark with PG16",
965+
mention_names: [example_team.name]
958966
)
959967

960968
# Notes on extra sampler topics for visibility across pages

spec/services/note_builder_spec.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
team_member = create(:user, username: "carl")
1212
create(:team_member, team:, user: team_member)
1313

14-
note = described_class.new(author: author).create!(topic:, message:, body: "Ping @bob and @team-two #Foo #bar")
14+
note = described_class.new(author: author).create!(
15+
topic:,
16+
message:,
17+
body: "Ping for review",
18+
tags: %w[foo bar],
19+
mention_names: %w[bob team-two]
20+
)
1521

1622
expect(note.note_tags.pluck(:tag)).to match_array(%w[foo bar])
1723
expect(note.note_mentions.map(&:mentionable)).to match_array([mentioned_user, team])
@@ -30,8 +36,8 @@
3036
carol = create(:user, username: "carol")
3137

3238
builder = described_class.new(author: author)
33-
note = builder.create!(topic:, message:, body: "Hi @bob and @devs")
34-
builder.update!(note:, body: "Hi @bob and @carol")
39+
note = builder.create!(topic:, message:, body: "Hi team", mention_names: %w[bob devs])
40+
builder.update!(note:, body: "Hi friends", mention_names: %w[bob carol])
3541

3642
old_activity = Activity.find_by(user: dev_member, subject: note)
3743
new_activity = Activity.find_by(user: carol, subject: note)

0 commit comments

Comments
 (0)