-
Notifications
You must be signed in to change notification settings - Fork 24
Make logged communications editable inline, like comments #1942
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { Controller } from "@hotwired/stimulus"; | ||
|
|
||
| // Connects to data-controller="edit-toggle" | ||
| // | ||
| // Toggles a list of records between read-only view blocks and inline edit forms | ||
| // (used by the comments and communications boxes on the Person / Registration / | ||
| // Scholarship edit forms). Each persisted record renders a `.{view}` block and a | ||
| // hidden `.{edit}` block; clicking the toggle flips which is shown and swaps the | ||
| // button label. | ||
| // | ||
| // Configure the block classes per caller: | ||
| // data-edit-toggle-view-class-value="comment-view" | ||
| // data-edit-toggle-edit-class-value="comment-edit" | ||
| // data-edit-toggle-body-class-value="comment-body" (optional) | ||
| // | ||
| // When a body class is given, leaving edit mode syncs each edit textarea back | ||
| // into the matching truncated `.{body}` span so the view reflects unsaved edits. | ||
| // | ||
| export default class extends Controller { | ||
| static targets = ["editLabel", "viewLabel"]; | ||
| static values = { | ||
| viewClass: { type: String, default: "editable-view" }, | ||
| editClass: { type: String, default: "editable-edit" }, | ||
| bodyClass: { type: String, default: "" }, | ||
| truncate: { type: Number, default: 135 } | ||
| }; | ||
|
|
||
| connect() { | ||
| this.editing = false; | ||
| } | ||
|
|
||
| toggle() { | ||
| this.editing = !this.editing; | ||
|
|
||
| // When leaving edit mode, sync edit textareas into their truncated view. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ From Claude: Generalized from |
||
| if (!this.editing && this.bodyClassValue) { | ||
| this.element.querySelectorAll(".nested-fields").forEach((item) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be a good opportunity to update this controller to use stimulus targets vs. depending on class targets. |
||
| const textarea = item.querySelector(`.${this.editClassValue} textarea`); | ||
| const viewBody = item.querySelector( | ||
| `.${this.viewClassValue} .${this.bodyClassValue}` | ||
| ); | ||
| if (textarea && viewBody) { | ||
| const text = textarea.value; | ||
| const max = this.truncateValue; | ||
| viewBody.textContent = | ||
| text.length > max ? text.substring(0, max - 3) + "..." : text; | ||
| viewBody.title = text; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| this.element | ||
| .querySelectorAll(`.${this.viewClassValue}`) | ||
| .forEach((el) => (el.style.display = this.editing ? "none" : "")); | ||
| this.element | ||
| .querySelectorAll(`.${this.editClassValue}`) | ||
| .forEach((el) => (el.style.display = this.editing ? "" : "none")); | ||
|
|
||
| if (this.hasEditLabelTarget && this.hasViewLabelTarget) { | ||
| this.editLabelTarget.style.display = this.editing ? "none" : ""; | ||
| this.viewLabelTarget.style.display = this.editing ? "" : "none"; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,7 +99,7 @@ class Person < ApplicationRecord | |
| accepts_nested_attributes_for :affiliations, allow_destroy: true, | ||
| reject_if: proc { |attrs| attrs["organization_id"].blank? } | ||
| accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: proc { |attrs| attrs["body"].blank? } | ||
| accepts_nested_attributes_for :notifications, reject_if: proc { |attrs| attrs["email_subject"].blank? } | ||
| accepts_nested_attributes_for :notifications, allow_destroy: true, reject_if: proc { |attrs| attrs["email_subject"].blank? } | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ From Claude: |
||
|
|
||
| # Search Cop | ||
| include SearchCop | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -302,7 +302,7 @@ | |||||
| <% end %> | ||||||
|
|
||||||
| <%# ---- Comments ---- %> | ||||||
| <section class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm scroll-mt-24" id="comments-section" data-controller="comment-edit-toggle"> | ||||||
| <section class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm scroll-mt-24" id="comments-section" data-controller="edit-toggle" data-edit-toggle-view-class-value="comment-view" data-edit-toggle-edit-class-value="comment-edit" data-edit-toggle-body-class-value="comment-body"> | ||||||
| <div class="flex items-center gap-3 border-b border-gray-100 px-4 py-3"> | ||||||
| <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg <%= DomainTheme.bg_class_for(:comments, intensity: 100) %> <%= DomainTheme.text_class_for(:comments, intensity: 600) %>"> | ||||||
| <i class="fa-solid fa-message"></i> | ||||||
|
|
@@ -337,11 +337,11 @@ | |||||
| <button | ||||||
| type="button" | ||||||
| class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 cursor-pointer" | ||||||
| data-action="click->comment-edit-toggle#toggle" | ||||||
| data-action="click->edit-toggle#toggle" | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Click is implicit on buttons |
||||||
| > | ||||||
| <i class="fa-solid fa-pen text-[0.6rem]"></i> | ||||||
| <span data-comment-edit-toggle-target="editLabel">Edit comments</span> | ||||||
| <span data-comment-edit-toggle-target="viewLabel" style="display:none">Done editing</span> | ||||||
| <span data-edit-toggle-target="editLabel">Edit comments</span> | ||||||
| <span data-edit-toggle-target="viewLabel" style="display:none">Done editing</span> | ||||||
| </button> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,41 +1,36 @@ | ||
| <%# New, unsaved manual notification log β sender, channel, and subject on the | ||
| first row, body beneath. Fields use SimpleForm styling. Amber until the | ||
| registration form is saved; a blank sender means the system ("AWBW Portal"). | ||
| Persisted notifications render read-only in the registration's notifications | ||
| box, not through this partial. ---- %> | ||
| <%# ---- One manual-log communication, editable inline (mirrors the comment | ||
| edit-mode pattern). A persisted entry renders a read-only `.notification-view` | ||
| row plus a hidden `.notification-edit` form; the shared `edit-toggle` | ||
| controller flips between them. A new, unsaved entry renders the amber add | ||
| form directly. Only manually logged communications addressed to this record | ||
| render through this partial β automated notifications stay read-only in the | ||
| notifications box. A blank sender means the system ("AWBW Portal"). ---- %> | ||
| <% sender_users = User.where(super_user: true).includes(:person).to_a %> | ||
| <% sender_users |= [ current_user ] if current_user %> | ||
| <% sender_options = sender_users.sort_by { |u| u.full_name.to_s.downcase }.map { |u| [ u.full_name, u.id ] } %> | ||
| <% channel_default = Notification::MANUAL_CHANNELS.include?(f.object.channel) ? f.object.channel : "email" %> | ||
| <% record = local_assigns[:record] %> | ||
| <div class="nested-fields rounded-md border border-amber-200 bg-amber-50 p-3"> | ||
| <div class="nested-fields" data-paginated-fields-target="item"> | ||
| <%# Record this communication against its parent (the "Record" column in the | ||
| notifications index) β e.g. an event registration or a person. %> | ||
| <% if record %> | ||
| <%= f.hidden_field :noticeable_type, value: record.class.name %> | ||
| <%= f.hidden_field :noticeable_id, value: record.id %> | ||
| <% end %> | ||
| <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-6"> | ||
| <%= f.input :sender_id, | ||
| collection: sender_options, | ||
| include_blank: "AWBW Portal", | ||
| selected: f.object.sender_id || current_user&.id, | ||
| label: "From", | ||
| wrapper_html: { class: "sm:col-span-2" } %> | ||
| <%= f.input :channel, | ||
| collection: Notification::MANUAL_CHANNELS.map { |c| [ c.titleize, c ] }, | ||
| include_blank: false, | ||
| selected: channel_default, | ||
| label: "Channel", | ||
| wrapper_html: { class: "sm:col-span-1" } %> | ||
| <%= f.input :email_subject, label: "Subject", placeholder: "Topic or subject line", | ||
| input_html: { rows: 1 }, wrapper_html: { class: "sm:col-span-3" } %> | ||
| </div> | ||
|
|
||
| <%= f.input :email_body_text, as: :text, label: "Body", input_html: { rows: 3, placeholder: "What happened? (e.g. left voicemail, sent reminder)" } %> | ||
|
|
||
| <div class="text-right"> | ||
| <%= link_to_remove_association "Remove", f, | ||
| class: "text-sm text-gray-400 underline hover:text-red-600" %> | ||
| </div> | ||
| <% if f.object.persisted? %> | ||
| <%= f.hidden_field :id %> | ||
| <div class="notification-view"> | ||
| <%= render "notifications/notification_row", notification: f.object %> | ||
| </div> | ||
| <div class="notification-edit rounded-md border border-amber-200 bg-amber-50 p-3" style="display:none"> | ||
| <%= render "notifications/notification_form", f: f, sender_options: sender_options, | ||
| sender_selected: f.object.sender_id, channel_default: channel_default %> | ||
| </div> | ||
| <% else %> | ||
| <div class="rounded-md border border-amber-200 bg-amber-50 p-3"> | ||
| <%= render "notifications/notification_form", f: f, sender_options: sender_options, | ||
| sender_selected: f.object.sender_id || current_user&.id, channel_default: channel_default %> | ||
| </div> | ||
| <% end %> | ||
| </div> |
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 believe these could all be done with stimulus targets instead of setting a class as a selector.