fat_free_crm 0.22.0 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fat_free_crm might be problematic. Click here for more details.

Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/application.js.erb +1 -0
  3. data/app/assets/javascripts/crm.js.coffee +6 -3
  4. data/app/assets/javascripts/crm_select2.js.coffee +4 -1
  5. data/app/assets/javascripts/crm_tags.js.coffee +4 -4
  6. data/app/assets/javascripts/crm_validations.js.coffee +12 -0
  7. data/app/assets/stylesheets/bootstrap-custom.scss +3 -3
  8. data/app/assets/stylesheets/common.scss +9 -0
  9. data/app/assets/stylesheets/rails.scss +1 -1
  10. data/app/controllers/admin/fields_controller.rb +16 -13
  11. data/app/controllers/entities_controller.rb +1 -1
  12. data/app/controllers/lists_controller.rb +5 -4
  13. data/app/helpers/application_helper.rb +4 -4
  14. data/app/helpers/opportunities_helper.rb +4 -2
  15. data/app/helpers/users_helper.rb +1 -1
  16. data/app/models/entities/campaign.rb +2 -2
  17. data/app/models/fields/custom_field_pair.rb +6 -7
  18. data/app/models/fields/field.rb +1 -3
  19. data/app/models/list.rb +1 -1
  20. data/app/views/accounts/_contact_info.html.haml +1 -1
  21. data/app/views/accounts/_top_section.html.haml +3 -3
  22. data/app/views/accounts/show.js.haml +1 -1
  23. data/app/views/accounts/update.js.haml +1 -0
  24. data/app/views/admin/custom_fields/_check_boxes_field.html.haml +4 -1
  25. data/app/views/admin/custom_fields/_date_pair_field.html.haml +1 -1
  26. data/app/views/campaigns/_top_section.html.haml +2 -2
  27. data/app/views/campaigns/show.js.haml +1 -1
  28. data/app/views/campaigns/update.js.haml +1 -0
  29. data/app/views/contacts/_top_section.html.haml +5 -5
  30. data/app/views/contacts/show.js.haml +2 -3
  31. data/app/views/contacts/update.js.haml +1 -0
  32. data/app/views/devise/sessions/new.html.haml +4 -5
  33. data/app/views/fields/_group.html.haml +5 -2
  34. data/app/views/fields/_group_table.html.haml +4 -5
  35. data/app/views/fields/_group_view.html.haml +4 -1
  36. data/app/views/fields/_sidebar_show.html.haml +5 -8
  37. data/app/views/fields/group.js.erb +3 -1
  38. data/app/views/layouts/application.html.haml +1 -1
  39. data/app/views/leads/_top_section.html.haml +4 -4
  40. data/app/views/leads/show.js.haml +1 -1
  41. data/app/views/leads/update.js.haml +1 -0
  42. data/app/views/opportunities/_top_section.html.haml +2 -2
  43. data/app/views/opportunities/show.js.haml +1 -1
  44. data/app/views/opportunities/update.js.haml +1 -0
  45. data/app/views/shared/_add_comment.html.haml +1 -1
  46. data/app/views/shared/_address.html.haml +1 -1
  47. data/app/views/tasks/_top_section.html.haml +4 -4
  48. data/config/database.yml +1 -2
  49. data/config/initializers/action_mailer.rb +1 -1
  50. data/config/initializers/custom_field_ransack_translations.rb +2 -2
  51. data/config/settings.default.yml +2 -1
  52. data/db/migrate/20230526212613_convert_to_active_storage.rb +1 -1
  53. data/lib/fat_free_crm/custom_fields.rb +1 -0
  54. data/lib/fat_free_crm/version.rb +1 -1
  55. metadata +7 -31
  56. data/public/avatars/User/2/large_rails.png +0 -0
  57. data/public/avatars/User/2/medium_rails.png +0 -0
  58. data/public/avatars/User/2/original_rails.png +0 -0
  59. data/public/avatars/User/2/small_rails.png +0 -0
  60. data/public/avatars/User/2/thumb_rails.png +0 -0
  61. data/public/avatars/User/3/large_rails.png +0 -0
  62. data/public/avatars/User/3/medium_rails.png +0 -0
  63. data/public/avatars/User/3/original_rails.png +0 -0
  64. data/public/avatars/User/3/small_rails.png +0 -0
  65. data/public/avatars/User/3/thumb_rails.png +0 -0
  66. data/public/avatars/User/4/large_rails.png +0 -0
  67. data/public/avatars/User/4/medium_rails.png +0 -0
  68. data/public/avatars/User/4/original_rails.png +0 -0
  69. data/public/avatars/User/4/small_rails.png +0 -0
  70. data/public/avatars/User/4/thumb_rails.png +0 -0
  71. data/public/avatars/User/6/large_rails.png +0 -0
  72. data/public/avatars/User/6/medium_rails.png +0 -0
  73. data/public/avatars/User/6/original_rails.png +0 -0
  74. data/public/avatars/User/6/small_rails.png +0 -0
  75. data/public/avatars/User/6/thumb_rails.png +0 -0
  76. data/public/avatars/User/7/large_rails.png +0 -0
  77. data/public/avatars/User/7/medium_rails.png +0 -0
  78. data/public/avatars/User/7/original_rails.png +0 -0
  79. data/public/avatars/User/7/small_rails.png +0 -0
  80. data/public/avatars/User/7/thumb_rails.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 325977901a3011b43c20379a5e03317660a3cd2b702e36ceb7aa255ec0f6ff17
4
- data.tar.gz: 6badcc5bfcc41d304fe646a7ea07d1c4fce631b3a60efd08285752fe645e3572
3
+ metadata.gz: 6ed29c049db6b4f6c31a7c1f5a3a083c1ab92a87d1475ae0808777a8d5c87c9d
4
+ data.tar.gz: ea1c36a463eafd3c50db26e072a447e74a8d07095a9bd19d0149096f681dc36b
5
5
  SHA512:
6
- metadata.gz: 58f6a285322ca7ae834bbf2eeeaf0073a2c501f9dd71117b890c9d7a1403dd3b2e5f725e97d2b3814836f854595eb77f322e09a27a94202fb65ca7b28078cd47
7
- data.tar.gz: 004b7dadb4c2c77e4863010b59a87dc89500142bc52fe0d90f81e71532d964b287f1bd97365bdd9460898249c0278e0ca389eba9edba1260c3410d0131302d0b
6
+ metadata.gz: 31bf4871e24767881af295b9b686a62b5c7de6d56894d67f13af38d6721f8728b36d3a0535b05a9b7f14ebbc65d5648ee4dd9924ded07fcf3308fe04835ea428
7
+ data.tar.gz: 71240615f55f2e0ac3527223d240470d28c3ada768a5f7240c4aea3a1b976ced11e10a2d4335cd733971ba526719f06ff44b7a15ca41127bb63aea7dbc5dd4ab
@@ -17,6 +17,7 @@
17
17
  //= require crm_loginout
18
18
  //= require crm_tags
19
19
  //= require crm_sortable
20
+ //= require crm_validations
20
21
  //= require textarea_autocomplete
21
22
  //= require crm_textarea_autocomplete
22
23
  //= require crm_select2
@@ -9,8 +9,8 @@
9
9
  @[0].toUpperCase() + @.substring(1)
10
10
 
11
11
  window.crm =
12
- EXPANDED: "▼"
13
- COLLAPSED: "►"
12
+ EXPANDED: "▽"
13
+ COLLAPSED: "▷"
14
14
  searchRequest: null
15
15
  autocompleter: null
16
16
  base_url: ""
@@ -336,7 +336,10 @@
336
336
 
337
337
  # Country dropdown needs special treatment ;-)
338
338
  country = $("#" + from + "_attributes_country").select2("data")
339
- $("#" + to + "_attributes_country").select2("data", country, true)
339
+ if country.length == 1
340
+ country_dropdown = $("#" + to + "_attributes_country")
341
+ country_dropdown.val(country[0].id)
342
+ country_dropdown.trigger('change')
340
343
 
341
344
 
342
345
  #----------------------------------------------------------------------------
@@ -15,6 +15,7 @@
15
15
  $(this).select2
16
16
  'width':'resolve'
17
17
  placeholder: $(this).attr("placeholder")
18
+ allowClear: true
18
19
  ajax:
19
20
  url: $(this).data("url")
20
21
  dataType: 'json'
@@ -22,16 +23,18 @@
22
23
  $(this).select2
23
24
  'width':'resolve'
24
25
  placeholder: $(this).attr("placeholder")
26
+ allowClear: true
25
27
 
26
28
  if $(this).prop("disabled") == true
27
29
  $(this).next('.select2-container').disable()
28
- $(this).next('.select2-container').hide()
30
+ $(this).next('.select2-container').hide()
29
31
 
30
32
  $(".select2_tag").not(".select2-container, .select2-offscreen").each ->
31
33
  $(this).select2
32
34
  'width':'resolve'
33
35
  placeholder: $(this).data("placeholder")
34
36
  multiple: $(this).data("multiple")
37
+ allowClear: true
35
38
 
36
39
  $(document).ready ->
37
40
  crm.make_select2()
@@ -7,16 +7,16 @@
7
7
 
8
8
  # The multiselect tag list has listeners to load/remove fieldsets related to tags
9
9
  #----------------------------------------------------------------------------
10
- $(document).on 'select2-selecting', "[name*='tag_list']", (event) ->
10
+ $(document).on 'select2:select', "[name*='tag_list']", (event) ->
11
11
  url = $(this).data('url')
12
12
  asset_id = $(this).data('asset-id')
13
13
  $.get(url, {
14
- tag: event.val
14
+ tag: event.params.data.text
15
15
  asset_id: asset_id
16
16
  collapsed: "no"
17
17
  })
18
18
 
19
- $(document).on 'select2-removing', "[name*='tag_list']", (event) ->
20
- $("#field_groups div[data-tag='" + event.val + "']").remove()
19
+ $(document).on 'select2:unselect', "[name*='tag_list']", (event) ->
20
+ $("#field_groups div[data-tag='" + event.params.data.text + "']").remove()
21
21
 
22
22
  ) jQuery
@@ -0,0 +1,12 @@
1
+ #------------------------------------------------------------------------------
2
+ (($) ->
3
+
4
+ # Ensure that any html5 required fields are unhidden when invalid
5
+ #----------------------------------------------------------------------------
6
+ $(document).on 'click', 'form.simple_form input:submit', (event) ->
7
+ form = this.closest('form')
8
+ invalidInputs = form.querySelectorAll(':invalid')
9
+ $(invalidInputs).each ->
10
+ $(this).closest('.field_group').show()
11
+
12
+ ) jQuery
@@ -165,12 +165,12 @@ button.navbar-toggler:focus {
165
165
  dl {
166
166
  li {
167
167
  width: 23%;
168
- font-size: 2rem;
168
+ font-size: 1rem;
169
169
  dt {
170
- font-size: inherit;
170
+ font-size: 0.75rem;
171
171
  }
172
172
  tt {
173
- font-size: inherit;
173
+ font-size: 0.75rem;
174
174
  }
175
175
  }
176
176
  }
@@ -784,3 +784,12 @@ table.asset_attributes {
784
784
  background-color: darkgray;
785
785
  }
786
786
  }
787
+
788
+
789
+ // Login page
790
+ .form-group.user_remember_me {
791
+ border: 0px;
792
+ .form-check {
793
+ padding-left: 6px;
794
+ }
795
+ }
@@ -28,7 +28,7 @@
28
28
  padding: 0px; } } }
29
29
 
30
30
  .fieldWithErrors {
31
- input {
31
+ input, select {
32
32
  border: {
33
33
  bottom: 1px solid lightpink;
34
34
  right: 1px solid lightpink; };
@@ -33,7 +33,7 @@ class Admin::FieldsController < Admin::ApplicationController
33
33
  # GET /fields/1/edit AJAX
34
34
  #----------------------------------------------------------------------------
35
35
  def edit
36
- @field = Field.find(params[:id])
36
+ @field = Field.find(params["id"])
37
37
  respond_with(@field)
38
38
  end
39
39
 
@@ -41,12 +41,12 @@ class Admin::FieldsController < Admin::ApplicationController
41
41
  # POST /fields.xml AJAX
42
42
  #----------------------------------------------------------------------------
43
43
  def create
44
- as = field_params[:as]
44
+ as = field_params["as"]
45
+ klass= Field.lookup_class(as).safe_constantize
45
46
  @field =
46
47
  if as.match?(/pair/)
47
- CustomFieldPair.create_pair(params).first
48
+ klass.create_pair("pair" => pair_params, "field" => field_params).first
48
49
  elsif as.present?
49
- klass = find_class(Field.lookup_class(as))
50
50
  klass.create(field_params)
51
51
  else
52
52
  Field.new(field_params).tap(&:valid?)
@@ -59,10 +59,10 @@ class Admin::FieldsController < Admin::ApplicationController
59
59
  # PUT /fields/1.xml AJAX
60
60
  #----------------------------------------------------------------------------
61
61
  def update
62
- if field_params[:as].match?(/pair/)
63
- @field = CustomFieldPair.update_pair(params).first
62
+ if field_params["as"].match?(/pair/)
63
+ @field = CustomFieldPair.update_pair("pair" => pair_params, "field" => field_params).first
64
64
  else
65
- @field = Field.find(params[:id])
65
+ @field = Field.find(params["id"])
66
66
  @field.update(field_params)
67
67
  end
68
68
 
@@ -73,7 +73,7 @@ class Admin::FieldsController < Admin::ApplicationController
73
73
  # DELETE /fields/1.xml HTML and AJAX
74
74
  #----------------------------------------------------------------------------
75
75
  def destroy
76
- @field = Field.find(params[:id])
76
+ @field = Field.find(params["id"])
77
77
  @field.destroy
78
78
 
79
79
  respond_with(@field)
@@ -82,7 +82,7 @@ class Admin::FieldsController < Admin::ApplicationController
82
82
  # POST /fields/sort
83
83
  #----------------------------------------------------------------------------
84
84
  def sort
85
- field_group_id = params[:field_group_id].to_i
85
+ field_group_id = params["field_group_id"].to_i
86
86
  field_ids = params["fields_field_group_#{field_group_id}"] || []
87
87
 
88
88
  field_ids.each_with_index do |id, index|
@@ -96,13 +96,12 @@ class Admin::FieldsController < Admin::ApplicationController
96
96
  #----------------------------------------------------------------------------
97
97
  def subform
98
98
  field = field_params
99
- as = field[:as]
100
-
99
+ as = field_params["as"]
101
100
  @field = if (id = field[:id]).present?
102
101
  Field.find(id).tap { |f| f.as = as }
103
102
  else
104
103
  field_group_id = field[:field_group_id]
105
- klass = find_class(Field.lookup_class(as))
104
+ klass = Field.lookup_class(as).safe_constantize
106
105
  klass.new(field_group_id: field_group_id, as: as)
107
106
  end
108
107
 
@@ -114,7 +113,11 @@ class Admin::FieldsController < Admin::ApplicationController
114
113
  protected
115
114
 
116
115
  def field_params
117
- params[:field].permit!
116
+ params.require(:field).permit(:as, :collection_string, :disabled, :field_group_id, :hint, :label, :maxlength, :minlength, :name, :pair_id, :placeholder, :position, :required, :type, settings: {})
117
+ end
118
+
119
+ def pair_params
120
+ params.require(:pair).permit("0": [:hint, :required, :disabled, :id], "1": [:hint, :required, :disabled, :id])
118
121
  end
119
122
 
120
123
  def setup_current_tab
@@ -83,7 +83,7 @@ class EntitiesController < ApplicationController
83
83
  #----------------------------------------------------------------------------
84
84
  def field_group
85
85
  if @tag = Tag.find_by_name(params[:tag].strip)
86
- if @field_group = FieldGroup.find_by_tag_id_and_klass_name(@tag.id, klass.to_s)
86
+ if @field_groups = FieldGroup.where(tag_id: @tag.id, klass_name: klass.to_s).order(:label, :created_at)
87
87
  @asset = klass.find_by_id(params[:asset_id]) || klass.new
88
88
  render('fields/group') && return
89
89
  end
@@ -9,13 +9,14 @@ class ListsController < ApplicationController
9
9
  # POST /lists
10
10
  #----------------------------------------------------------------------------
11
11
  def create
12
- list_params[:user_id] = (current_user.id if params[:is_global].to_i.zero?)
12
+ list_attr = list_params.to_h
13
+ list_attr["user_id"] = current_user.id if params["is_global"] != "1"
13
14
 
14
15
  # Find any existing list with the same name (case insensitive)
15
- if @list = List.where("lower(name) = ?", list_params[:name].downcase).where(user_id: list_params[:user_id]).first
16
- @list.update(list_params)
16
+ if @list = List.where("lower(name) = ?", list_attr[:name].downcase).where(user_id: list_attr[:user_id]).first
17
+ @list.update(list_attr)
17
18
  else
18
- @list = List.create(list_params)
19
+ @list = List.create(list_attr)
19
20
  end
20
21
 
21
22
  respond_with(@list)
@@ -36,7 +36,7 @@ module ApplicationHelper
36
36
  end
37
37
 
38
38
  def subtitle_link(id, text, hidden)
39
- link_to("<small>#{hidden ? '&#9658;' : '&#9660;'}</small> #{sanitize text}".html_safe,
39
+ link_to("<small>#{hidden ? '&#9655;' : '&#9661;'}</small> #{sanitize text}".html_safe,
40
40
  url_for(controller: :home, action: :toggle, id: id),
41
41
  remote: true,
42
42
  onclick: "crm.flip_subtitle(this)")
@@ -99,7 +99,7 @@ module ApplicationHelper
99
99
 
100
100
  #----------------------------------------------------------------------------
101
101
  def arrow_for(id)
102
- content_tag(:span, "&#9658;".html_safe, id: "#{id}_arrow", class: :arrow)
102
+ content_tag(:span, "&#9655;".html_safe, id: "#{id}_arrow", class: :arrow)
103
103
  end
104
104
 
105
105
  #----------------------------------------------------------------------------
@@ -115,13 +115,13 @@ module ApplicationHelper
115
115
 
116
116
  #----------------------------------------------------------------------------
117
117
  def link_to_delete(record, options = {})
118
- confirm = options[:confirm] || nil
118
+ confirm = options[:confirm] || t(:confirm_delete, record.class.to_s.downcase)
119
119
 
120
120
  link_to(t(:delete) + "!",
121
121
  options[:url] || url_for(record),
122
122
  method: :delete,
123
123
  remote: true,
124
- confirm: confirm)
124
+ data: { confirm: confirm })
125
125
  end
126
126
 
127
127
  #----------------------------------------------------------------------------
@@ -41,8 +41,10 @@ module OpportunitiesHelper
41
41
  selected_campaign = Campaign.find_by_id(options[:selected])
42
42
  campaigns = ([selected_campaign] + Campaign.my(current_user).order(:name).limit(25)).compact.uniq
43
43
  collection_select :opportunity, :campaign_id, campaigns, :id, :name,
44
- { selected: options[:selected], prompt: t(:select_a_campaign) },
45
- style: 'width:330px;', class: 'select2'
44
+ { selected: options[:selected], prompt: t(:select_a_campaign), include_blank: true },
45
+ style: 'width:330px;', class: 'select2',
46
+ placeholder: t(:select_a_campaign),
47
+ "data-url": auto_complete_campaigns_path(format: 'json')
46
48
  end
47
49
 
48
50
  # Generates the inline revenue message for the opportunity list table.
@@ -25,7 +25,7 @@ module UsersHelper
25
25
  user_options = user_options_for_select(users, myself)
26
26
  select(asset, :assigned_to, user_options,
27
27
  { include_blank: t(:unassigned) },
28
- style: 'width: 160px;',
28
+ style: 'width: 160px;', "data-allow-clear" => false,
29
29
  class: 'select2')
30
30
  end
31
31
 
@@ -35,8 +35,8 @@ class Campaign < ActiveRecord::Base
35
35
  belongs_to :user, optional: true # TODO: Is this really optional?
36
36
  belongs_to :assignee, class_name: "User", foreign_key: :assigned_to, optional: true # TODO: Is this really optional?
37
37
  has_many :tasks, as: :asset, dependent: :destroy # , :order => 'created_at DESC'
38
- has_many :leads, -> { order "id DESC" }, dependent: :destroy
39
- has_many :opportunities, -> { order "id DESC" }, dependent: :destroy
38
+ has_many :leads, -> { order "id DESC" }
39
+ has_many :opportunities, -> { order "id DESC" }
40
40
  has_many :emails, as: :mediator
41
41
 
42
42
  serialize :subscribed_users, Array
@@ -12,10 +12,9 @@ class CustomFieldPair < CustomField
12
12
  #------------------------------------------------------------------------------
13
13
  def self.create_pair(params)
14
14
  fields = params['field']
15
- as = params['field']['as']
16
- pair = params.delete('pair')
15
+ pair = params['pair']
17
16
  base_params = fields.delete_if { |k, _v| !%w[field_group_id label as].include?(k) }
18
- klass = ("custom_field_" + as.gsub('pair', '_pair')).classify.constantize
17
+ klass = Field.lookup_class(fields['as']).safe_constantize
19
18
  field1 = klass.create(base_params.merge(pair['0']))
20
19
  field2 = klass.create(base_params.merge(pair['1']).merge('pair_id' => field1.id, 'required' => field1.required, 'disabled' => field1.disabled))
21
20
  [field1, field2]
@@ -25,19 +24,19 @@ class CustomFieldPair < CustomField
25
24
  #------------------------------------------------------------------------------
26
25
  def self.update_pair(params)
27
26
  fields = params['field']
28
- pair = params.delete('pair')
27
+ pair = params['pair']
29
28
  base_params = fields.delete_if { |k, _v| !%w[field_group_id label as].include?(k) }
30
- field1 = CustomFieldPair.find(params['id'])
29
+ field1 = CustomFieldPair.find(pair['0']['id'])
31
30
  field1.update(base_params.merge(pair['0']))
32
31
  field2 = field1.paired_with
33
32
  field2.update(base_params.merge(pair['1']).merge('required' => field1.required, 'disabled' => field1.disabled))
34
33
  [field1, field2]
35
34
  end
36
35
 
37
- # Returns the field that this field is paired with
36
+ # Returns the field that this field is paired with (bi-directional)
38
37
  #------------------------------------------------------------------------------
39
38
  def paired_with
40
- pair || CustomFieldPair.where(pair_id: id).first
39
+ pair || self.class.find_by_id(pair_id)
41
40
  end
42
41
 
43
42
  ActiveSupport.run_load_hooks(:fat_free_crm_custom_field_pair, self)
@@ -34,7 +34,7 @@ class Field < ActiveRecord::Base
34
34
  serialize :collection, Array
35
35
  serialize :settings, HashWithIndifferentAccess
36
36
 
37
- belongs_to :field_group, optional: true # TODO: Is this really optional?
37
+ belongs_to :field_group, optional: true
38
38
 
39
39
  scope :core_fields, -> { where(type: 'CoreField') }
40
40
  scope :custom_fields, -> { where("type != 'CoreField'") }
@@ -92,8 +92,6 @@ class Field < ActiveRecord::Base
92
92
 
93
93
  def render(value)
94
94
  case as
95
- when 'checkbox'
96
- value.to_s == '0' ? "no" : "yes"
97
95
  when 'date'
98
96
  value&.strftime(I18n.t("date.formats.mmddyy"))
99
97
  when 'datetime'
data/app/models/list.rb CHANGED
@@ -8,7 +8,7 @@
8
8
  class List < ActiveRecord::Base
9
9
  validates_presence_of :name
10
10
  validates_presence_of :url
11
- belongs_to :user
11
+ belongs_to :user, optional: true
12
12
 
13
13
  # Parses the controller from the url
14
14
  def controller
@@ -1,5 +1,5 @@
1
1
  - edit ||= false
2
- - collapsed = (@account.errors.empty? && session[:account_contact].nil?)
2
+ - collapsed = session[:account_contact].nil?
3
3
  = subtitle :account_contact, collapsed, t(:contact_info)
4
4
  .section
5
5
  %small#account_contact_intro{ hidden_if(!collapsed) }
@@ -2,9 +2,9 @@
2
2
  .section
3
3
  %table
4
4
  %tr
5
- %td(colspan="5")
5
+ %td{class: (@account.errors['name'].present? ? 'fieldWithErrors' : nil)}(colspan="5")
6
6
  .label.top.req #{t :name}:
7
- = f.text_field :name, autofocus: true, style: "width:500px"
7
+ = f.text_field :name, autofocus: true, style: "width:500px", required: "required"
8
8
  %tr
9
9
  %td
10
10
  .label #{t :assigned_to}:
@@ -12,7 +12,7 @@
12
12
  %td= spacer
13
13
  %td
14
14
  .label #{t :category}:
15
- = f.select :category, Setting.unroll(:account_category), { selected: (@account.category || "other").to_sym, include_blank: t(:other) }, { style: "width:160px", class: 'select2' }
15
+ = f.select :category, Setting.unroll(:account_category), { selected: (@account.category || "other").to_sym, include_blank: t(:other) }, { style: "width:160px", class: 'select2', placeholder: t(:other) }
16
16
  %td= spacer
17
17
  %td
18
18
  .label #{t :rating}:
@@ -1,5 +1,5 @@
1
1
  - entity_name = controller.controller_name.singularize.underscore #account
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
3
 
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
4
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
5
  = raw generate_js_for_popups(@entity, :tasks, :contacts, :opportunities)
@@ -7,6 +7,7 @@
7
7
  crm.flip_form('edit_#{entity_name}');
8
8
  crm.set_title('edit_#{entity_name}', '#{j @entity.name}');
9
9
  = refresh_sidebar(:show)
10
+ $('#summary').html('#{ j (render partial: "#{entity_name.pluralize}/sidebar_show", entity_name => @entity) }');
10
11
  - else
11
12
  $('##{id}').replaceWith('#{ j render(partial: entity_name, collection: [ @entity ]) }');
12
13
  $('##{id}').effect("highlight", { duration:1500 });
@@ -1,6 +1,9 @@
1
1
  %div
2
2
  .label.top.req
3
3
  = "Select Options (pipe separated):"
4
- = f.text_field :collection_string, class: 'field_collection_string', size: 78
4
+ = f.text_field :collection_string, class: 'field_collection_string', size: 78, placeholder: "Option 1|Option 2|Option 3"
5
5
 
6
6
  = render partial: 'admin/custom_fields/base_field', locals: {f: f}
7
+
8
+ - if f.object.new_record?
9
+ .info2 After saving, you must restart all instances of the Rails server to apply column serialization.
@@ -1,5 +1,5 @@
1
1
  - field1 = f.object || CustomFieldPair.new
2
- - field2 = f.object.respond_to?(:paired_with) ? field1.paired_with : CustomFieldPair.new
2
+ - field2 = (field1.pair || CustomFieldPair.new)
3
3
 
4
4
  %table{class: :pairs}
5
5
  = fields_for("pair[0]", field1) do |first|
@@ -2,9 +2,9 @@
2
2
  .section
3
3
  %table
4
4
  %tr
5
- %td(colspan="5")
5
+ %td{class: (@campaign.errors['name'].present? ? 'fieldWithErrors' : nil)}(colspan="5")
6
6
  .label.top.req #{t :name}:
7
- = f.text_field :name, autofocus: true, style: "width:500px"
7
+ = f.text_field :name, autofocus: true, style: "width:500px", required: "required"
8
8
  %tr
9
9
  %td
10
10
  .label #{t :start_date}:
@@ -1,5 +1,5 @@
1
1
  - entity_name = controller.controller_name.singularize.underscore #account
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
3
 
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
4
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
5
  = raw generate_js_for_popups(@entity, :tasks, :leads, :opportunities)
@@ -7,6 +7,7 @@
7
7
  crm.flip_form('edit_#{entity_name}');
8
8
  crm.set_title('edit_#{entity_name}', '#{h @entity.name}');
9
9
  = refresh_sidebar(:show)
10
+ $('#summary').html('#{ j (render partial: "#{entity_name.pluralize}/sidebar_show", entity_name => @entity) }');
10
11
  - else
11
12
  $('##{id}').replaceWith('#{ j render(partial: entity_name, collection: [ @entity ]) }');
12
13
  $('##{id}').effect("highlight", { duration:1500 });
@@ -3,13 +3,13 @@
3
3
  .section
4
4
  %table
5
5
  %tr
6
- %td
7
- .label.top.req{ class: "#{Setting.require_first_names ? 'req' : nil}" } #{t :first_name}:
8
- = f.text_field :first_name, autofocus: true
6
+ %td{class: (@contact.errors['first_name'].present? ? 'fieldWithErrors' : nil)}
7
+ .label.top{ class: "#{Setting.require_first_names ? 'req' : nil}" } #{t :first_name}:
8
+ = f.text_field :first_name, autofocus: true, required: (Setting.require_first_names ? "required" : nil)
9
9
  %td= spacer
10
- %td
10
+ %td{class: (@contact.errors['last_name'].present? ? 'fieldWithErrors' : nil)}
11
11
  .label.top{ class: "#{Setting.require_last_names ? 'req' : nil}" } #{t :last_name}:
12
- = f.text_field :last_name
12
+ = f.text_field :last_name, required: (Setting.require_last_names ? "required" : nil)
13
13
  %tr
14
14
  %td
15
15
  .label #{t :email}:
@@ -1,5 +1,4 @@
1
- - entity_name = controller.controller_name.singularize.underscore #account
1
+ - entity_name = controller.controller_name.singularize.underscore
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
-
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
3
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
4
  = raw generate_js_for_popups(@entity, :tasks, :opportunities)
@@ -8,6 +8,7 @@
8
8
  crm.flip_form('edit_#{entity_name}');
9
9
  crm.set_title('edit_#{entity_name}', '#{h @entity.full_name}');
10
10
  = refresh_sidebar(:show)
11
+ $('#summary').html('#{ j (render partial: "#{entity_name.pluralize}/sidebar_show", entity_name => @entity) }');
11
12
  - else
12
13
  $('##{id}').replaceWith('#{ j render(partial: entity_name, collection: [ @entity ]) }');
13
14
  $('##{id}').effect("highlight", { duration:1500 });
@@ -23,10 +23,9 @@
23
23
  .label= t(:password)
24
24
  = f.input_field :password
25
25
 
26
- %div(style="margin-left:12px")
26
+ .section
27
27
  = f.input :remember_me, as: :boolean, inline_label: t('remember_me')
28
- %br
29
- .buttonbar
30
- = f.submit t(:login)
31
- = t(:or)
28
+ = f.submit t(:login), class: 'btn btn-primary'
29
+
30
+ .section
32
31
  = link_to t(:forgot_password) + '?', new_password_path(resource_name)
@@ -1,6 +1,9 @@
1
1
  - if field_group.name != 'custom_fields'
2
- -# start a new section
3
- - collapsed = session[field_group.key].nil?
2
+ - # Ensure field groups containing validation errors are expanded
3
+ - required_field_names = field_group.fields.select(&:required?).map(&:name)
4
+ - fields_with_errors = f.object.errors.map{|e| e.attribute.to_s}
5
+ - force_open = (required_field_names & fields_with_errors).any?
6
+ - collapsed = session[field_group.key].nil? && !force_open
4
7
  %div{ id: "#{field_group.key}_container", :"data-tag" => field_group.tag.try(:name) }
5
8
  = subtitle field_group.key, collapsed, t(field_group.name, default: field_group.label)
6
9
  .section
@@ -2,12 +2,11 @@
2
2
  - field_group.fields.without_pairs.in_groups_of(2, false) do |group|
3
3
  %tr
4
4
  - group.each_with_index do |field, i|
5
- %td
5
+ %td{class: (f.object.errors[field.name].present? ? 'fieldWithErrors' : nil)}
6
6
  - if field.hint.present?
7
7
  = image_tag "info_tiny.png", title: field.hint, class: "tooltip-icon"
8
- - if field.as == 'check_boxes'
9
- - value = f.object.send(field.name)
10
- - checked = YAML.load(value.to_s)
11
- = f.input field.name, field.input_options.merge(checked: checked)
8
+ .label.top{class: (field.required? ? 'req': nil)}
9
+ = "#{field.label}:"
10
+ = f.input_field field.name, field.input_options
12
11
  - if i == 0
13
12
  %td= spacer
@@ -4,7 +4,10 @@
4
4
  %tr
5
5
  - group.each do |field|
6
6
  = col(field.label, (i == groups.size - 1) ? :last : nil) do
7
- = field.render_value(entity)
7
+ - if field.as == "text"
8
+ = simple_format(field.render_value(entity))
9
+ - else
10
+ = field.render_value(entity)
8
11
  - if group.size == 1
9
12
  %th.last
10
13
  %td.last
@@ -1,9 +1,6 @@
1
1
  - asset.field_groups.each do |field_group|
2
- - fg = field_group.fields.without_pairs
3
- - if fg.select{|f| asset.send(f.name).present? }.any?
4
- %div
5
- - unless field_group.name == 'custom_fields'
6
- .caption #{field_group.label_i18n}
7
- - fg.each do |field|
8
- - if (value = field.render_value(asset)).present?
9
- == #{field.label}:<br /> <b>#{truncate(value, length: 35)}</b><br />
2
+ - fields = field_group.fields.without_pairs
3
+ - if fields.select{|f| asset.send(f.name).present? }.any?
4
+ - unless field_group.name == 'custom_fields'
5
+ .caption #{field_group.label_i18n}
6
+ = render("fields/group_view", fields: fields, entity: asset) unless fields.nil?
@@ -1,3 +1,5 @@
1
1
  <%= simple_fields_for(@asset) do |f| %>
2
- $('#field_groups').append('<%= j render(partial: 'fields/group', locals: {f: f, field_group: @field_group, fields: @field_group.fields}) %>')
2
+ <% @field_groups.each do |field_group| %>
3
+ $('#field_groups').append('<%= j render(partial: 'fields/group', locals: {f: f, field_group: field_group, fields: field_group.fields}) %>')
4
+ <% end %>
3
5
  <% end %>
@@ -42,7 +42,7 @@
42
42
  = render "layouts/footer"
43
43
 
44
44
  %script{type: "text/javascript"}
45
- = "crm.base_url = '#{Setting.base_url}';" unless Setting.base_url.blank?
45
+ = "crm.base_url = '#{h Setting.base_url}';".html_safe unless Setting.base_url.blank?
46
46
  = get_browser_timezone_offset
47
47
  = content_for :javascript_epilogue
48
48
  = hook(:javascript_epilogue, self)
@@ -3,13 +3,13 @@
3
3
  .section
4
4
  %table
5
5
  %tr
6
- %td
6
+ %td{ class: (@lead.errors['first_name'].present? ? 'fieldWithErrors' : nil)}
7
7
  .label.top{ class: "#{Setting.require_first_names ? 'req' : nil}" } #{t :first_name}:
8
- = f.text_field :first_name, autofocus: true
8
+ = f.text_field :first_name, autofocus: true, required: (Setting.require_first_names ? "required" : nil)
9
9
  %td= spacer
10
- %td
10
+ %td{ class: (@lead.errors['last_name'].present? ? 'fieldWithErrors' : nil)}
11
11
  .label.top{ class: "#{Setting.require_last_names ? 'req' : nil}" } #{t :last_name}:
12
- = f.text_field :last_name
12
+ = f.text_field :last_name, required: (Setting.require_last_names ? "required" : nil)
13
13
  %tr
14
14
  %td
15
15
  .label #{t :email}:
@@ -1,5 +1,5 @@
1
1
  - entity_name = controller.controller_name.singularize.underscore #account
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
3
 
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
4
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
5
  = raw generate_js_for_popups(@entity, :tasks)
@@ -7,6 +7,7 @@
7
7
  crm.flip_form('edit_#{entity_name}');
8
8
  crm.set_title('edit_#{entity_name}', '#{h @entity.full_name}');
9
9
  = refresh_sidebar(:show)
10
+ $('#summary').html('#{ j (render partial: "#{entity_name.pluralize}/sidebar_show", entity_name => @entity) }');
10
11
  - else
11
12
  $('##{id}').replaceWith('#{ j render(partial: entity_name, collection: [ @entity ]) }');
12
13
  $('##{id}').effect("highlight", { duration:1500 });
@@ -2,9 +2,9 @@
2
2
  .section
3
3
  %table
4
4
  %tr
5
- %td
5
+ %td{class: (@opportunity.errors['name'].present? ? 'fieldWithErrors' : nil)}
6
6
  .label.req.top #{t :name}:
7
- = f.text_field :name, autofocus: true, style: "width:325px"
7
+ = f.text_field :name, autofocus: true, style: "width:325px", required: "required"
8
8
  %td= spacer
9
9
  %td
10
10
  .label.req.top #{t :stage}:
@@ -1,5 +1,5 @@
1
1
  - entity_name = controller.controller_name.singularize.underscore #account
2
2
  - @entity = instance_variable_get("@#{entity_name}")
3
3
 
4
- $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show.html", entity_name => @entity) }');
4
+ $('#main').html('#{ j (render template: "#{entity_name.pluralize}/show", formats: [:html], entity_name => @entity) }');
5
5
  = raw generate_js_for_popups(@entity, :tasks, :contacts)
@@ -7,6 +7,7 @@
7
7
  crm.flip_form('edit_#{entity_name}');
8
8
  crm.set_title('edit_#{entity_name}', '#{h @entity.name}');
9
9
  = refresh_sidebar(:show)
10
+ $('#summary').html('#{ j (render partial: "#{entity_name.pluralize}/sidebar_show", entity_name => @entity) }');
10
11
  - else
11
12
  $('##{id}').replaceWith('#{ j render(partial: entity_name, collection: [ @entity ]) }');
12
13
  $('##{id}').effect("highlight", { duration:1500 });
@@ -1,5 +1,5 @@
1
1
  - edit ||= false
2
- - collapsed = @comment_body.nil? && f.object.errors.empty?
2
+ - collapsed = session[:comment].nil?
3
3
  = subtitle :comment, collapsed, t(:comment)
4
4
  .section
5
5
  %small#comment_intro{ hidden_if(!collapsed) }
@@ -41,4 +41,4 @@
41
41
  = address_field(a, :zipcode, "width:80px;")
42
42
  %td= spacer
43
43
  %td
44
- = a.country_select(:country, priority_countries: priority_countries, include_blank: "", :"data-placeholder" => t(:select_a_country), style: "width:150px; margin-top:6px", class: 'select2')
44
+ = a.country_select(:country, {priority_countries: priority_countries, include_blank: true}, {data: { placeholder: t(:select_a_country)}, class: 'select2'})
@@ -3,10 +3,10 @@
3
3
  %tr
4
4
  %td(colspan="5")
5
5
  .label.top.req #{t :name}:
6
- = f.text_field :name, autofocus: true, style: "width:500px"
6
+ = f.text_field :name, autofocus: true, style: "width:500px", required: "required"
7
7
  %tr
8
8
  %td
9
- .label.req #{t :due}:
9
+ .label #{t :due}:
10
10
  - bucket = (params[:bucket].blank? ? @task.bucket : params[:bucket]) || "due_asap"
11
11
  - with_time = Setting.task_calendar_with_time
12
12
  - if @task.bucket != "specific_time"
@@ -18,11 +18,11 @@
18
18
  = f.text_field :calendar, value: f.object.due_at.strftime(fmt), style: "width:160px;", autocomplete: :off, class: (with_time ? 'datetime' : 'date')
19
19
  %td= spacer
20
20
  %td
21
- .label.req #{t :assign_to}:
21
+ .label #{t :assign_to}:
22
22
  = user_select(:task, all_users, current_user)
23
23
  %td= spacer
24
24
  %td
25
- .label.req #{t :category}:
25
+ .label #{t :category}:
26
26
  = f.select :category, @category, { selected: @task.category.blank? ? nil : @task.category.to_sym, include_blank: t(:select_blank) }, { style: "width:160px", class: 'select2' }
27
27
 
28
28
  - if Setting.background_info && Setting.background_info.include?(:task)
data/config/database.yml CHANGED
@@ -3,7 +3,7 @@ development: &development
3
3
  adapter: postgresql
4
4
  database: fat_free_crm_development
5
5
  username: postgres
6
- password:
6
+ password: postgres
7
7
  host: localhost
8
8
  port: 5432
9
9
  schema_search_path: public
@@ -23,4 +23,3 @@ production:
23
23
  staging:
24
24
  <<: *development
25
25
  database: fat_free_crm_staging
26
-
@@ -10,7 +10,7 @@
10
10
  # ActionMailer is setup in test mode later on
11
11
  #
12
12
  unless Rails.env.test?
13
- require './app/models/setting'
13
+ require 'setting'
14
14
 
15
15
  smtp_settings = Setting.smtp || {}
16
16
 
@@ -6,8 +6,8 @@
6
6
  # See MIT-LICENSE file or http://www.opensource.org/licenses/mit-license.php
7
7
  #------------------------------------------------------------------------------
8
8
  # Load field names for custom fields, for Ransack search
9
- require './app/models/setting'
10
- if Setting.database_and_table_exists?
9
+ require 'setting'
10
+ if Setting.database_and_table_exists? && ActiveRecord::Base.connection.table_exists?(:custom_fields)
11
11
  Rails.application.config.after_initialize do
12
12
  I18n.backend.load_translations
13
13
 
@@ -192,7 +192,8 @@
192
192
  #------------------------------------------------------------------------------
193
193
  # Specify which countries (if any) should appear at the top of country pickers
194
194
  # priority_countries:
195
- # - Burkina Faso
195
+ # - AU
196
+ # - BF
196
197
 
197
198
  # Main and Admin Tabs
198
199
  #------------------------------------------------------------------------------
@@ -16,7 +16,7 @@ class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
16
16
  end
17
17
 
18
18
  ActiveRecord::Base.connection.raw_connection.then do |conn|
19
- if conn.is_a?(PG::Connection)
19
+ if conn.is_a?(::PG::Connection)
20
20
  conn.prepare('active_storage_blobs', <<-SQL)
21
21
  INSERT INTO active_storage_blobs (
22
22
  key, filename, content_type, metadata, byte_size, checksum, created_at
@@ -8,6 +8,7 @@
8
8
 
9
9
  #
10
10
  # Register CustomFields when Field class is loaded
11
+
11
12
  ActiveSupport.on_load(:fat_free_crm_field) do # self == Field
12
13
  register(as: 'date_pair', klass: 'CustomFieldDatePair', type: 'date')
13
14
  register(as: 'datetime_pair', klass: 'CustomFieldDatetimePair', type: 'timestamp')
@@ -9,7 +9,7 @@ module FatFreeCRM
9
9
  module VERSION # :nodoc:
10
10
  MAJOR = 0
11
11
  MINOR = 22
12
- TINY = 0
12
+ TINY = 1
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fat_free_crm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.22.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Dvorkin
8
8
  - Stephen Kenworthy
9
9
  - Daniel O'Connor
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-01-09 00:00:00.000000000 Z
13
+ date: 2024-08-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -553,6 +553,7 @@ files:
553
553
  - app/assets/javascripts/crm_sortable.js.coffee
554
554
  - app/assets/javascripts/crm_tags.js.coffee
555
555
  - app/assets/javascripts/crm_textarea_autocomplete.js.coffee
556
+ - app/assets/javascripts/crm_validations.js.coffee
556
557
  - app/assets/javascripts/datepicker.js.coffee
557
558
  - app/assets/javascripts/format_buttons.js.coffee
558
559
  - app/assets/javascripts/lists.js.coffee
@@ -1217,31 +1218,6 @@ files:
1217
1218
  - public/404.html
1218
1219
  - public/422.html
1219
1220
  - public/500.html
1220
- - public/avatars/User/2/large_rails.png
1221
- - public/avatars/User/2/medium_rails.png
1222
- - public/avatars/User/2/original_rails.png
1223
- - public/avatars/User/2/small_rails.png
1224
- - public/avatars/User/2/thumb_rails.png
1225
- - public/avatars/User/3/large_rails.png
1226
- - public/avatars/User/3/medium_rails.png
1227
- - public/avatars/User/3/original_rails.png
1228
- - public/avatars/User/3/small_rails.png
1229
- - public/avatars/User/3/thumb_rails.png
1230
- - public/avatars/User/4/large_rails.png
1231
- - public/avatars/User/4/medium_rails.png
1232
- - public/avatars/User/4/original_rails.png
1233
- - public/avatars/User/4/small_rails.png
1234
- - public/avatars/User/4/thumb_rails.png
1235
- - public/avatars/User/6/large_rails.png
1236
- - public/avatars/User/6/medium_rails.png
1237
- - public/avatars/User/6/original_rails.png
1238
- - public/avatars/User/6/small_rails.png
1239
- - public/avatars/User/6/thumb_rails.png
1240
- - public/avatars/User/7/large_rails.png
1241
- - public/avatars/User/7/medium_rails.png
1242
- - public/avatars/User/7/original_rails.png
1243
- - public/avatars/User/7/small_rails.png
1244
- - public/avatars/User/7/thumb_rails.png
1245
1221
  - public/favicon.ico
1246
1222
  - public/robots.txt
1247
1223
  - vendor/assets/images/calendar_date_select/calendar.gif
@@ -1338,7 +1314,7 @@ homepage: http://fatfreecrm.com
1338
1314
  licenses:
1339
1315
  - MIT
1340
1316
  metadata: {}
1341
- post_install_message:
1317
+ post_install_message:
1342
1318
  rdoc_options: []
1343
1319
  require_paths:
1344
1320
  - lib
@@ -1353,8 +1329,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1353
1329
  - !ruby/object:Gem::Version
1354
1330
  version: '0'
1355
1331
  requirements: []
1356
- rubygems_version: 3.4.10
1357
- signing_key:
1332
+ rubygems_version: 3.5.9
1333
+ signing_key:
1358
1334
  specification_version: 4
1359
1335
  summary: Fat Free CRM
1360
1336
  test_files: []
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file