-
-
Notifications
You must be signed in to change notification settings - Fork 522
New Case Contacts Table: expandable case contact rows #6823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a11ceca
2f4ddc0
9c83897
fe5c01d
17f7306
244839a
fb31759
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,37 @@ | |
| const { Notifier } = require('./notifier') | ||
| let pageNotifier | ||
|
|
||
| const MAX_VISIBLE_TOPIC_PILLS = 2 | ||
|
|
||
| function buildTopicPills (topics) { | ||
| if (!topics || topics.length === 0) return '' | ||
| const visible = topics.slice(0, MAX_VISIBLE_TOPIC_PILLS) | ||
| const overflowCount = topics.length - visible.length | ||
| const pills = visible | ||
| .map(topic => `<span class="badge badge-pill light-bg text-black">${topic}</span>`) | ||
| .join(' ') | ||
| const overflowPill = overflowCount > 0 | ||
| ? ` <span class="badge badge-pill light-bg text-black">+${overflowCount} More</span>` | ||
| : '' | ||
| return pills + overflowPill | ||
| } | ||
|
|
||
| function buildExpandedContent (data) { | ||
| const answers = (data.contact_topic_answers || []) | ||
| .map(answer => `<div class="expanded-topic"><strong>${answer.question}</strong><p>${answer.value}</p></div>`) | ||
| .join('') | ||
|
|
||
| const notes = data.notes && data.notes.trim() | ||
| ? `<div class="expanded-topic"><strong>Additional Notes</strong><p>${data.notes}</p></div>` | ||
| : '' | ||
|
|
||
| if (!answers && !notes) return '<p class="expanded-empty">No additional details.</p>' | ||
|
|
||
| return `<div class="expanded-content">${answers}${notes}</div>` | ||
| } | ||
|
|
||
| const defineCaseContactsTable = function () { | ||
| $('table#case_contacts').DataTable({ | ||
| const table = $('table#case_contacts').DataTable({ | ||
| scrollX: true, | ||
| searching: true, | ||
| processing: true, | ||
|
|
@@ -36,7 +65,7 @@ const defineCaseContactsTable = function () { | |
| data: null, | ||
| orderable: false, | ||
| searchable: false, | ||
| render: () => '<i class="fa-solid fa-chevron-down"></i>' | ||
| render: () => '<button type="button" class="expand-toggle" aria-expanded="false" aria-label="Expand row details"><i class="fa-solid fa-chevron-down" aria-hidden="true"></i></button>' | ||
| }, | ||
| { // Date column (index 2) | ||
| data: 'occurred_at', | ||
|
|
@@ -106,7 +135,7 @@ const defineCaseContactsTable = function () { | |
| { // Topics column (index 8) | ||
| data: 'contact_topics', | ||
| orderable: false, | ||
| render: (data) => data || '' | ||
| render: (data) => buildTopicPills(data) | ||
| }, | ||
| { // Draft column (index 9) | ||
| data: 'is_draft', | ||
|
|
@@ -125,6 +154,19 @@ const defineCaseContactsTable = function () { | |
| } | ||
| ] | ||
| }) | ||
|
|
||
| $('table#case_contacts tbody').on('click', '.expand-toggle', function () { | ||
| const tr = $(this).closest('tr') | ||
| const row = table.row(tr) | ||
|
|
||
| if (row.child.isShown()) { | ||
| row.child.hide() | ||
| $(this).removeClass('expanded').attr('aria-expanded', 'false') | ||
| } else { | ||
| row.child(buildExpandedContent(row.data())).show() | ||
| $(this).addClass('expanded').attr('aria-expanded', 'true') | ||
| } | ||
|
Comment on lines
+158
to
+168
|
||
| }) | ||
| } | ||
|
|
||
| $(() => { // JQuery's callback for the DOM loading | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| require "rails_helper" | ||
|
|
||
| RSpec.describe "Case Contact Table Row Expansion", type: :system, js: true do | ||
| let(:organization) { create(:casa_org) } | ||
| let(:admin) { create(:casa_admin, casa_org: organization) } | ||
| let(:casa_case) { create(:casa_case, casa_org: organization) } | ||
| let(:contact_topic) { create(:contact_topic, casa_org: organization, question: "What was discussed?") } | ||
| let!(:case_contact) do | ||
| create(:case_contact, :active, casa_case: casa_case, notes: "Important follow-up needed") | ||
| end | ||
|
|
||
| before do | ||
| create(:contact_topic_answer, | ||
| case_contact: case_contact, | ||
| contact_topic: contact_topic, | ||
| value: "Youth is doing well in school") | ||
| allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(true) | ||
| sign_in admin | ||
| visit case_contacts_new_design_path | ||
| end | ||
|
|
||
| it "shows the expanded content after clicking the chevron" do | ||
| find(".expand-toggle").click | ||
|
|
||
| expect(page).to have_content("What was discussed?") | ||
| expect(page).to have_content("Youth is doing well in school") | ||
| end | ||
|
|
||
| it "shows notes in the expanded content" do | ||
| find(".expand-toggle").click | ||
|
|
||
| expect(page).to have_content("Additional Notes") | ||
| expect(page).to have_content("Important follow-up needed") | ||
| end | ||
|
|
||
| it "hides the expanded content after clicking the chevron again" do | ||
| find(".expand-toggle").click | ||
| expect(page).to have_content("Youth is doing well in school") | ||
|
|
||
| find(".expand-toggle").click | ||
| expect(page).to have_no_content("Youth is doing well in school") | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contact_topic_answers uses safe navigation for a.contact_topic, which can result in { question: nil/blank } entries being returned. That will render an empty heading in the expanded row. Consider filtering out answers with missing/blank questions (or providing a fallback label) so the expanded UI always has a meaningful topic heading.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I investigated this and, from what I can tell, the scenario where
questioncould be blank is unreachable in practice, for two reasons:There is a database-level foreign key constraint on
contact_topic_answers.contact_topic_id— deleting aContactTopicwhile answers still reference it raises aPG::ForeignKeyViolation. I confirmed this by attempting to write a spec that creates an answer, then destroys its topic, and the spec failed with that error.The
ContactTopicAnswermodel validates thatcontact_topicis present whenevervalueis present. Combined with our existing.reject { |a| a.value.blank? }, every answer that reaches the.mapis guaranteed to have a non-nilcontact_topic.The
&.safe navigation operator ona.contact_topic&.questionis good defensive code and I've left it in place, but adding a second.rejectto guard against a state the database constraints prevent would add noise without real protection. I've left the code as-is.