-
Notifications
You must be signed in to change notification settings - Fork 21
Improve user edit UX, incl add comments #1040
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
9fa2c8b
ab27aba
d32c117
1bdd3f6
181b4ff
99aa84a
9fe75fd
48531f8
c8a805a
bb1915c
4562045
ea5bae2
0baf051
cc0032d
df52c26
22e6c3d
c86a384
5d3313f
81d5982
1ac11ba
1a13d78
055fc6f
ccf2f0b
52fa05b
b1e849e
6dba1d9
2d4eabe
e118c04
5c6f869
3344916
bee12fa
ef38d41
98d9aaf
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 |
|---|---|---|
|
|
@@ -77,7 +77,7 @@ def edit | |
| :contact_methods, | ||
| :addresses, | ||
| { avatar_attachment: :blob }, | ||
| { comments: :created_by }, | ||
| { comments: [ :created_by, :updated_by ] }, | ||
| { sectorable_items: :sector }, | ||
| organization_people: { organization: :logo_attachment } | ||
| ).find(params[:id]).decorate | ||
|
|
@@ -88,7 +88,8 @@ def edit | |
| def create | ||
| @person = Person.new(person_params.except(:user_attributes)) | ||
| authorize! @person | ||
| @person.user ||= (User.find(params[:person][:user_attributes][:id]) if params[:person][:user_attributes]) | ||
| @person.user ||= User.find_by(id: params[:user_id]) if params[:user_id].present? | ||
| @person.user ||= User.find_by(id: params.dig(:person, :user_attributes, :id)) if params.dig(:person, :user_attributes, :id).present? | ||
|
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. retain user_id if sent in params bc came to new person from user page |
||
|
|
||
| unless params[:skip_duplicate_check].present? | ||
| duplicates = find_duplicate_people( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,8 @@ class UsersController < ApplicationController | |
| def index | ||
| authorize! | ||
| per_page = params[:number_of_items_per_page].presence || 25 | ||
| base_scope = authorized_scope(User.includes(avatar_attachment: :blob, | ||
| base_scope = authorized_scope(User.includes(:created_by, :updated_by, | ||
| avatar_attachment: :blob, | ||
| person: { avatar_attachment: :blob })) | ||
| filtered = base_scope.search_by_params(params).order(:first_name, :last_name) | ||
| @users_count = filtered.count | ||
|
|
@@ -16,6 +17,23 @@ def index | |
| def show | ||
| authorize! @user | ||
| @user = User.find(params[:id]).decorate | ||
| @comments = @user.comments.includes(:created_by).newest_first.paginate(page: params[:comments_page], per_page: 5) | ||
|
|
||
| user_auth_events = Ahoy::Event | ||
| .where("name LIKE 'auth.%' OR name LIKE 'update.user'") | ||
| .where( | ||
| "(CAST(JSON_EXTRACT(properties, '$.record_id') AS UNSIGNED) = :id AND JSON_UNQUOTE(JSON_EXTRACT(properties, '$.record_type')) = 'User') OR " \ | ||
| "(CAST(JSON_EXTRACT(properties, '$.resource_id') AS UNSIGNED) = :id AND JSON_UNQUOTE(JSON_EXTRACT(properties, '$.resource_type')) = 'User')", | ||
| id: @user.id | ||
| ) | ||
|
|
||
| @last_admin_event = user_auth_events.where(name: %w[auth.admin_granted auth.admin_revoked]).order(time: :desc).first | ||
| @last_lock_event = user_auth_events.where(name: %w[auth.account_locked auth.account_unlocked]).order(time: :desc).first | ||
|
|
||
| @account_events = user_auth_events | ||
| .includes(:user) | ||
| .order(time: :desc) | ||
| .paginate(page: params[:page], per_page: 10) | ||
|
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. add more event data to user show |
||
| end | ||
|
|
||
| def new | ||
|
|
@@ -25,6 +43,7 @@ def new | |
| end | ||
|
|
||
| def edit | ||
| @user = User.includes(comments: [ :created_by, :updated_by ]).find(params[:id]) | ||
| authorize! @user | ||
| set_form_variables | ||
| end | ||
|
|
@@ -37,9 +56,10 @@ def create | |
| unless params[:skip_duplicate_check].present? | ||
| email = @user.email | ||
| if email.present? && !email.downcase.end_with?("@example.com") | ||
| duplicates = find_duplicate_users(email) | ||
| person_id = params[:person_id].presence || params.dig(:user, :person_id).presence || @user.person_id | ||
| duplicates = find_duplicate_users(email, exclude_person_id: person_id) | ||
|
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. don't identify duplicates w people emails for an associated person |
||
| if duplicates.any? | ||
| redirect_to check_duplicates_users_path(email: email, person_id: params[:person_id].presence || params.dig(:user, :person_id).presence) | ||
| redirect_to check_duplicates_users_path(email: email, person_id: person_id) | ||
| return | ||
| end | ||
| end | ||
|
|
@@ -55,6 +75,8 @@ def create | |
| # assign person | ||
| person_id = params[:person_id].presence || params.dig(:user, :person_id).presence | ||
| @user.person = Person.find(person_id) if person_id | ||
| @user.created_by = current_user | ||
| @user.updated_by = current_user | ||
|
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. add audit tracking to user. we maybe want to do this more sitewide w a gem instead of direct. or maybe use ahoy records, but they really weren't designed for that. |
||
|
|
||
| if @user.save | ||
| # @user.notifications.create(notification_type: 0) | ||
|
|
@@ -70,7 +92,7 @@ def check_duplicates | |
|
|
||
| @email = params[:email] | ||
| @person_id = params[:person_id] | ||
| @duplicates = find_duplicate_users(@email) | ||
| @duplicates = find_duplicate_users(@email, exclude_person_id: @person_id) | ||
| @blocked = @duplicates.any? { |d| d[:blocked] } | ||
| end | ||
|
|
||
|
|
@@ -83,9 +105,16 @@ def update | |
| bypass_sign_in(@user) | ||
| end | ||
|
|
||
| if @user.update(user_params.except(:password, :password_confirmation)) | ||
| # @user.notifications.create(notification_type: 1) | ||
| redirect_to users_path, notice: "User was successfully updated." | ||
| @user.assign_attributes(user_params.except(:password, :password_confirmation)) | ||
| @user.updated_by = current_user | ||
| @user.comments.select(&:new_record?).each { |c| c.created_by = current_user } | ||
| @user.comments.select(&:changed?).each { |c| c.updated_by = current_user } | ||
|
|
||
| if @user.save | ||
| bypass_sign_in(@user) if @user == current_user | ||
| notice = "User was successfully updated." | ||
| notice += " A confirmation email has been sent to #{@user.unconfirmed_email}." if @user.unconfirmed_email.present? && @user.saved_change_to_unconfirmed_email? | ||
| redirect_to users_path, notice: notice | ||
|
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. initiate |
||
| else | ||
| flash[:alert] = "Unable to update user." | ||
| set_form_variables | ||
|
|
@@ -251,33 +280,41 @@ def password_params | |
| params.require(:user).permit(:current_password, :password, :password_confirmation) | ||
| end | ||
|
|
||
| def find_duplicate_users(email) | ||
| def find_duplicate_users(email, exclude_person_id: nil) | ||
| return [] if email.blank? | ||
|
|
||
| email_lower = email.downcase | ||
| duplicates = [] | ||
|
|
||
| # Check existing users with same email | ||
| User.where("LOWER(email) = ?", email_lower).limit(10).each do |user| | ||
| users_scope = User.where("LOWER(email) = ?", email_lower).includes(:person) | ||
| users_scope = users_scope.where.not(person_id: exclude_person_id) if exclude_person_id | ||
| users_scope.limit(10).each do |user| | ||
| duplicates << { | ||
| id: user.id, | ||
| name: user.person&.full_name || "#{user.first_name} #{user.last_name}".strip, | ||
| person_id: user.person_id, | ||
| email: user.email, | ||
| type: "user", | ||
| blocked: true | ||
| } | ||
| end | ||
|
|
||
| # Check people with matching email (who may not have a user yet) | ||
| Person.includes(:user) | ||
| .left_joins(:user) | ||
| # Check people with matching email or secondary email | ||
| exclude_person_ids = duplicates.map { |d| d[:person_id] }.compact | ||
| exclude_person_ids << exclude_person_id.to_i if exclude_person_id | ||
| people_scope = Person.includes(:user) | ||
| .where("LOWER(people.email) = :email OR LOWER(people.email_2) = :email", email: email_lower) | ||
| .where(users: { id: nil }) | ||
| .limit(10).each do |person| | ||
| people_scope = people_scope.where.not(id: exclude_person_ids) if exclude_person_ids.any? | ||
| people_scope.limit(10).each do |person| | ||
| primary_match = person.email&.downcase == email_lower | ||
| duplicates << { | ||
| id: person.id, | ||
| name: person.full_name, | ||
| email: person.email || person.email_2, | ||
| email: primary_match ? person.email : person.email_2, | ||
| email_field: primary_match ? "primary" : "secondary", | ||
| has_user: person.user.present?, | ||
| user_email: person.user&.email, | ||
| type: "person", | ||
| blocked: false | ||
| } | ||
|
|
@@ -288,7 +325,7 @@ def find_duplicate_users(email) | |
|
|
||
| def user_params | ||
| params.require(:user).permit( | ||
| :email, :comment, :person_id, :inactive, :primary_address, :time_zone, :super_user, | ||
| :email, :comment, :person_id, :inactive, :locked, :primary_address, :time_zone, :super_user, | ||
|
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. accept :locked as a param bc form now has it as a boolean checkbox even tho it's not a field on user |
||
|
|
||
| ##### legacy to remove later | ||
| :agency_id, :legacy, :legacy_id, :subscribecode, :avatar, :first_name, :last_name, # legacy to remove later | ||
|
|
@@ -297,6 +334,7 @@ def user_params | |
| ##### | ||
|
|
||
| organization_people_attributes: [ :id, :organization_id, :position, :title, :inactive, :primary_contact, :start_date, :end_date, :_destroy ], | ||
| comments_attributes: [ :id, :body ], | ||
| ) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,14 +2,13 @@ import { Controller } from "@hotwired/stimulus"; | |
|
|
||
| // Connects to data-controller="dirty-form" | ||
| // | ||
| // Tracks whether a form has unsaved changes and prompts on cancel. | ||
| // Listens for input changes and cocoon nested field additions/removals. | ||
| // | ||
| // data-dirty-form-target="cancel" on the cancel link | ||
| // Tracks whether a form has unsaved changes and warns before navigating away. | ||
| // Uses three layers of protection: | ||
| // 1. turbo:before-visit – catches Turbo Drive link clicks (custom message) | ||
| // 2. beforeunload – catches non-Turbo navigations / tab close (browser message) | ||
| // 3. confirmCancel action – explicit cancel button binding (custom message) | ||
|
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. add prompt if there are unsaved changes and you try to navigate away. |
||
| // | ||
| export default class extends Controller { | ||
| static targets = ["cancel"]; | ||
|
|
||
| connect() { | ||
| this.dirty = false; | ||
| this.handleChange = () => (this.dirty = true); | ||
|
|
@@ -18,12 +17,35 @@ export default class extends Controller { | |
| this.element.addEventListener("change", this.handleChange); | ||
| this.element.addEventListener("cocoon:after-insert", this.handleChange); | ||
| this.element.addEventListener("cocoon:after-remove", this.handleChange); | ||
|
|
||
| // Clear dirty flag on form submit so guards don't block saving | ||
| this.handleSubmit = () => (this.dirty = false); | ||
| this.element.addEventListener("submit", this.handleSubmit); | ||
|
|
||
| // Guard Turbo Drive navigations (links without data-turbo="false") | ||
| this.handleTurboVisit = (event) => { | ||
| if (this.dirty && !confirm("You have unsaved changes. Are you sure you want to leave?")) { | ||
| event.preventDefault(); | ||
| } | ||
| }; | ||
| document.addEventListener("turbo:before-visit", this.handleTurboVisit); | ||
|
|
||
| // Guard non-Turbo navigations (data-turbo="false" links, browser back, tab close) | ||
| this.handleBeforeUnload = (event) => { | ||
| if (this.dirty) { | ||
| event.preventDefault(); | ||
| event.returnValue = ""; | ||
| } | ||
| }; | ||
| window.addEventListener("beforeunload", this.handleBeforeUnload); | ||
| } | ||
|
|
||
| confirmCancel(event) { | ||
| if (this.dirty) { | ||
| if (!confirm("You have unsaved changes. Are you sure you want to leave?")) { | ||
| event.preventDefault(); | ||
| } else { | ||
| this.dirty = false; | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -33,5 +55,8 @@ export default class extends Controller { | |
| this.element.removeEventListener("change", this.handleChange); | ||
| this.element.removeEventListener("cocoon:after-insert", this.handleChange); | ||
| this.element.removeEventListener("cocoon:after-remove", this.handleChange); | ||
| this.element.removeEventListener("submit", this.handleSubmit); | ||
| document.removeEventListener("turbo:before-visit", this.handleTurboVisit); | ||
| window.removeEventListener("beforeunload", this.handleBeforeUnload); | ||
| } | ||
| } | ||
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.
this scope isn't jsut inactive flag anymore bc it takes into account other devise fields