fat_free_crm 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fat_free_crm might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/assets/javascripts/application.js.erb +1 -0
- data/app/assets/javascripts/crm.js.coffee +6 -3
- data/app/assets/javascripts/crm_select2.js.coffee +4 -1
- data/app/assets/javascripts/crm_tags.js.coffee +4 -4
- data/app/assets/javascripts/crm_textarea_autocomplete.js.coffee +6 -9
- data/app/assets/javascripts/crm_validations.js.coffee +12 -0
- data/app/assets/stylesheets/bootstrap-custom.scss +3 -3
- data/app/assets/stylesheets/common.scss +9 -0
- data/app/assets/stylesheets/rails.scss +1 -1
- data/app/controllers/admin/fields_controller.rb +16 -13
- data/app/controllers/application_controller.rb +1 -1
- data/app/controllers/emails_controller.rb +1 -1
- data/app/controllers/entities/contacts_controller.rb +1 -1
- data/app/controllers/entities_controller.rb +1 -1
- data/app/controllers/lists_controller.rb +5 -4
- data/app/controllers/users_controller.rb +15 -3
- data/app/helpers/application_helper.rb +5 -5
- data/app/helpers/leads_helper.rb +1 -1
- data/app/helpers/opportunities_helper.rb +5 -3
- data/app/helpers/users_helper.rb +1 -1
- data/app/models/entities/campaign.rb +3 -3
- data/app/models/fields/custom_field_pair.rb +6 -7
- data/app/models/fields/field.rb +1 -3
- data/app/models/list.rb +1 -1
- data/app/models/observers/lead_observer.rb +2 -1
- data/app/models/observers/task_observer.rb +2 -1
- data/app/models/polymorphic/address.rb +2 -1
- data/app/models/polymorphic/comment.rb +3 -4
- data/app/models/polymorphic/task.rb +1 -1
- data/app/models/users/user.rb +7 -1
- data/app/views/accounts/_contact_info.html.haml +1 -1
- data/app/views/accounts/_top_section.html.haml +3 -3
- data/app/views/accounts/show.js.haml +1 -1
- data/app/views/accounts/update.js.haml +1 -0
- data/app/views/admin/custom_fields/_check_boxes_field.html.haml +4 -1
- data/app/views/admin/custom_fields/_date_pair_field.html.haml +1 -1
- data/app/views/campaigns/_top_section.html.haml +2 -2
- data/app/views/campaigns/show.js.haml +1 -1
- data/app/views/campaigns/update.js.haml +1 -0
- data/app/views/contacts/_top_section.html.haml +5 -5
- data/app/views/contacts/show.js.haml +2 -3
- data/app/views/contacts/update.js.haml +1 -0
- data/app/views/devise/sessions/new.html.haml +4 -5
- data/app/views/fields/_group.html.haml +5 -2
- data/app/views/fields/_group_table.html.haml +4 -5
- data/app/views/fields/_group_view.html.haml +4 -1
- data/app/views/fields/_sidebar_show.html.haml +5 -8
- data/app/views/fields/group.js.erb +3 -1
- data/app/views/layouts/application.html.haml +2 -4
- data/app/views/leads/_top_section.html.haml +4 -4
- data/app/views/leads/show.js.haml +1 -1
- data/app/views/leads/update.js.haml +1 -0
- data/app/views/opportunities/_top_section.html.haml +2 -2
- data/app/views/opportunities/show.js.haml +1 -1
- data/app/views/opportunities/update.js.haml +1 -0
- data/app/views/shared/_add_comment.html.haml +1 -1
- data/app/views/shared/_address.html.haml +1 -1
- data/app/views/tasks/_top_section.html.haml +4 -4
- data/config/application.rb +2 -0
- data/config/database.yml +9 -12
- data/config/environments/development.rb +15 -0
- data/config/initializers/action_mailer.rb +1 -1
- data/config/initializers/application_controller_renderer.rb +1 -0
- data/config/initializers/backtrace_silencers.rb +1 -0
- data/config/initializers/content_security_policy.rb +1 -0
- data/config/initializers/custom_field_ransack_translations.rb +2 -2
- data/config/initializers/devise.rb +1 -0
- data/config/initializers/inflections.rb +1 -0
- data/config/initializers/permissions_policy.rb +1 -0
- data/config/routes.rb +1 -3
- data/config/settings.default.yml +2 -1
- data/db/fat_free_crm_development.sqlite3 +0 -0
- data/db/fat_free_crm_test.sqlite3 +0 -0
- data/db/migrate/20230422234321_optionally_create_action_text_tables.action_text.rb +29 -0
- data/db/migrate/20230526211831_create_active_storage_tables.active_storage.rb +3 -3
- data/db/migrate/20230526212613_convert_to_active_storage.rb +11 -14
- data/db/schema.rb +16 -9
- data/db/seeds.rb +1 -0
- data/lib/fat_free_crm/callback.rb +0 -1
- data/lib/fat_free_crm/core_ext/string.rb +1 -1
- data/lib/fat_free_crm/custom_fields.rb +1 -0
- data/lib/fat_free_crm/mail_processor/base.rb +6 -10
- data/lib/fat_free_crm/version.rb +1 -1
- metadata +7 -31
- data/public/avatars/User/2/large_rails.png +0 -0
- data/public/avatars/User/2/medium_rails.png +0 -0
- data/public/avatars/User/2/original_rails.png +0 -0
- data/public/avatars/User/2/small_rails.png +0 -0
- data/public/avatars/User/2/thumb_rails.png +0 -0
- data/public/avatars/User/3/large_rails.png +0 -0
- data/public/avatars/User/3/medium_rails.png +0 -0
- data/public/avatars/User/3/original_rails.png +0 -0
- data/public/avatars/User/3/small_rails.png +0 -0
- data/public/avatars/User/3/thumb_rails.png +0 -0
- data/public/avatars/User/4/large_rails.png +0 -0
- data/public/avatars/User/4/medium_rails.png +0 -0
- data/public/avatars/User/4/original_rails.png +0 -0
- data/public/avatars/User/4/small_rails.png +0 -0
- data/public/avatars/User/4/thumb_rails.png +0 -0
- data/public/avatars/User/6/large_rails.png +0 -0
- data/public/avatars/User/6/medium_rails.png +0 -0
- data/public/avatars/User/6/original_rails.png +0 -0
- data/public/avatars/User/6/small_rails.png +0 -0
- data/public/avatars/User/6/thumb_rails.png +0 -0
- data/public/avatars/User/7/large_rails.png +0 -0
- data/public/avatars/User/7/medium_rails.png +0 -0
- data/public/avatars/User/7/original_rails.png +0 -0
- data/public/avatars/User/7/small_rails.png +0 -0
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 170884fee8c43222891b33e09f6a1996c6a33c70cf5839de3eae5ab987340ef1
|
4
|
+
data.tar.gz: d1f638adddae45577b72fc71d24b88f1ec7ca8513b98ae2a29ba9977ea5b5c28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5284a4f39f18595dfb12b87acf4052fb464a432675959f3c0e58a6edb3eeb59899e11bcff8433e3f251656bba35eea6e5f0abde513d4dc56c1c3fce98f576d3
|
7
|
+
data.tar.gz: 5644a00f3ebf9db88505d686591856d8614c4ac7908bf484fe5147494b656aacda669e236fcabdce48801d9e96cf19454b22f586aa2902ffb3e70c3bffd75a32
|
@@ -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
|
-
|
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
|
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.
|
14
|
+
tag: event.params.data.text
|
15
15
|
asset_id: asset_id
|
16
16
|
collapsed: "no"
|
17
17
|
})
|
18
18
|
|
19
|
-
$(document).on 'select2
|
20
|
-
$("#field_groups div[data-tag='" + event.
|
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
|
@@ -17,17 +17,14 @@
|
|
17
17
|
|
18
18
|
# Only autocomplete if search term starts with '@'
|
19
19
|
return [] unless text.indexOf("@") is 0
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
i++
|
27
|
-
cb words, text.replace("@", "")
|
20
|
+
$.ajax
|
21
|
+
url: "/users/auto_complete"
|
22
|
+
data:
|
23
|
+
term: text.replace("@", "")
|
24
|
+
success: (response) ->
|
25
|
+
cb response, text.replace("@", "")
|
28
26
|
|
29
27
|
selected: (text, data) ->
|
30
28
|
username_regEx = new RegExp("\\((@[^)]+)\\)")
|
31
29
|
text.match(username_regEx)[1]
|
32
|
-
|
33
30
|
) 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:
|
168
|
+
font-size: 1rem;
|
169
169
|
dt {
|
170
|
-
font-size:
|
170
|
+
font-size: 0.75rem;
|
171
171
|
}
|
172
172
|
tt {
|
173
|
-
font-size:
|
173
|
+
font-size: 0.75rem;
|
174
174
|
}
|
175
175
|
}
|
176
176
|
}
|
@@ -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[
|
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[
|
44
|
+
as = field_params["as"]
|
45
|
+
klass = Field.lookup_class(as).safe_constantize
|
45
46
|
@field =
|
46
47
|
if as.match?(/pair/)
|
47
|
-
|
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[
|
63
|
-
@field = CustomFieldPair.update_pair(
|
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[
|
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[
|
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[
|
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 =
|
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 =
|
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
|
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": %i[hint required disabled id], "1": %i[hint required disabled id])
|
118
121
|
end
|
119
122
|
|
120
123
|
def setup_current_tab
|
@@ -79,7 +79,7 @@ class ApplicationController < ActionController::Base
|
|
79
79
|
# See http://blog.nvisium.com/2014/09/understanding-protectfromforgery.html for more details.
|
80
80
|
#----------------------------------------------------------------------------
|
81
81
|
def handle_unverified_request
|
82
|
-
raise ActionController::InvalidAuthenticityToken
|
82
|
+
raise ActionController::InvalidAuthenticityToken unless ENV.fetch('CODESPACE_NAME', nil) && Rails.env.development?
|
83
83
|
end
|
84
84
|
|
85
85
|
#
|
@@ -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 @
|
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
|
-
|
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) = ?",
|
16
|
-
@list.update(
|
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(
|
19
|
+
@list = List.create(list_attr)
|
19
20
|
end
|
20
21
|
|
21
22
|
respond_with(@list)
|
@@ -63,9 +63,7 @@ class UsersController < ApplicationController
|
|
63
63
|
end
|
64
64
|
end
|
65
65
|
responds_to_parent do
|
66
|
-
|
67
|
-
# within the block and after yield in responds_to_parent.
|
68
|
-
render && (return if Rails.env.test?)
|
66
|
+
render
|
69
67
|
end
|
70
68
|
end
|
71
69
|
end
|
@@ -111,6 +109,20 @@ class UsersController < ApplicationController
|
|
111
109
|
@unassigned_opportunities = Opportunity.my(current_user).unassigned.pipeline.order(:stage).includes(:account, :user, :tags)
|
112
110
|
end
|
113
111
|
|
112
|
+
def auto_complete
|
113
|
+
@query = params[:term] || ''
|
114
|
+
@users = User.my(current_user).text_search(@query).limit(10).order(:first_name, :last_name)
|
115
|
+
|
116
|
+
respond_to do |format|
|
117
|
+
format.json do
|
118
|
+
results = @users.map do |a|
|
119
|
+
helpers.j(a.full_name + " (@" + a.username + ")")
|
120
|
+
end
|
121
|
+
render json: results
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
114
126
|
protected
|
115
127
|
|
116
128
|
def user_params
|
@@ -36,7 +36,7 @@ module ApplicationHelper
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def subtitle_link(id, text, hidden)
|
39
|
-
link_to("<small>#{hidden ? '&#
|
39
|
+
link_to("<small>#{hidden ? '▷' : '▽'}</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, "&#
|
102
|
+
content_tag(:span, "▷".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] ||
|
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
|
#----------------------------------------------------------------------------
|
@@ -155,7 +155,7 @@ module ApplicationHelper
|
|
155
155
|
#----------------------------------------------------------------------------
|
156
156
|
def link_to_email(email, length = nil, &_block)
|
157
157
|
name = (length ? truncate(email, length: length) : email)
|
158
|
-
bcc = Setting
|
158
|
+
bcc = Setting.email_dropbox
|
159
159
|
mailto = if bcc && bcc[:address].present?
|
160
160
|
"#{email}?bcc=#{bcc[:address]}"
|
161
161
|
else
|
data/app/helpers/leads_helper.rb
CHANGED
@@ -69,7 +69,7 @@ module LeadsHelper
|
|
69
69
|
#----------------------------------------------------------------------------
|
70
70
|
def lead_summary(lead)
|
71
71
|
summary = []
|
72
|
-
summary <<
|
72
|
+
summary << t(lead.status || :other)
|
73
73
|
|
74
74
|
if lead.company? && lead.title?
|
75
75
|
summary << t(:works_at, job_title: lead.title, company: lead.company)
|
@@ -17,7 +17,7 @@ module OpportunitiesHelper
|
|
17
17
|
def opportunity_summary(opportunity)
|
18
18
|
summary = []
|
19
19
|
amount = []
|
20
|
-
summary <<
|
20
|
+
summary << t(opportunity.stage || :other)
|
21
21
|
summary << number_to_currency(opportunity.weighted_amount, precision: 0)
|
22
22
|
unless %w[won lost].include?(opportunity.stage)
|
23
23
|
amount << number_to_currency(opportunity.amount.to_f, precision: 0)
|
@@ -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.
|
data/app/helpers/users_helper.rb
CHANGED
@@ -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" }
|
39
|
-
has_many :opportunities, -> { order "id DESC" }
|
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
|
@@ -103,7 +103,7 @@ class Campaign < ActiveRecord::Base
|
|
103
103
|
# Make sure end date > start date.
|
104
104
|
#----------------------------------------------------------------------------
|
105
105
|
def start_and_end_dates
|
106
|
-
errors.add(:ends_on, :dates_not_in_sequence) if
|
106
|
+
errors.add(:ends_on, :dates_not_in_sequence) if starts_on && ends_on && (starts_on > ends_on)
|
107
107
|
end
|
108
108
|
|
109
109
|
# Make sure at least one user has been selected if the campaign is being shared.
|
@@ -12,10 +12,9 @@ class CustomFieldPair < CustomField
|
|
12
12
|
#------------------------------------------------------------------------------
|
13
13
|
def self.create_pair(params)
|
14
14
|
fields = params['field']
|
15
|
-
|
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 =
|
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
|
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(
|
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 ||
|
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)
|
data/app/models/fields/field.rb
CHANGED
@@ -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
|
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
@@ -16,7 +16,8 @@ class LeadObserver < ActiveRecord::Observer
|
|
16
16
|
|
17
17
|
def after_update(item)
|
18
18
|
original = @@leads.delete(item.id)
|
19
|
-
|
19
|
+
|
20
|
+
log_activity(item, :reject) if original&.status != "rejected" && item.status == "rejected"
|
20
21
|
end
|
21
22
|
|
22
23
|
private
|
@@ -19,7 +19,8 @@ class TaskObserver < ActiveRecord::Observer
|
|
19
19
|
if original
|
20
20
|
return log_activity(item, :complete) if item.completed_at && original.completed_at.nil?
|
21
21
|
return log_activity(item, :reassign) if item.assigned_to != original.assigned_to
|
22
|
-
|
22
|
+
|
23
|
+
log_activity(item, :reschedule) if item.bucket != original.bucket
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
@@ -54,7 +54,8 @@ class Address < ActiveRecord::Base
|
|
54
54
|
exists = attributes['id'].present?
|
55
55
|
empty = %w[street1 street2 city state zipcode country full_address].map { |name| attributes[name].blank? }.all?
|
56
56
|
attributes[:_destroy] = 1 if exists && empty
|
57
|
-
|
57
|
+
|
58
|
+
!exists && empty
|
58
59
|
end
|
59
60
|
|
60
61
|
ActiveSupport.run_load_hooks(:fat_free_crm_address, self)
|
@@ -52,10 +52,9 @@ class Comment < ActiveRecord::Base
|
|
52
52
|
|
53
53
|
# Notify subscribed users when a comment is added, unless user created this comment
|
54
54
|
def notify_subscribers
|
55
|
-
commentable.subscribed_users.reject { |user_id| user_id == user.id }
|
56
|
-
|
57
|
-
|
58
|
-
end
|
55
|
+
users_to_notify = User.where(id: commentable.subscribed_users.reject { |user_id| user_id == user.id })
|
56
|
+
users_to_notify.select(&:emailable?).each do |subscriber|
|
57
|
+
SubscriptionMailer.comment_notification(subscriber, self).deliver_later
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
@@ -103,7 +103,7 @@ class Task < ActiveRecord::Base
|
|
103
103
|
scope :completed_last_month, -> { where('completed_at >= ? AND completed_at < ?', (Time.zone.now.beginning_of_month.utc - 1.day).beginning_of_month.utc, Time.zone.now.beginning_of_month.utc) }
|
104
104
|
|
105
105
|
scope :text_search, lambda { |query|
|
106
|
-
query = query.gsub(/[^\w\s
|
106
|
+
query = query.gsub(/[^\w\s\-.'\p{L}]/u, '').strip
|
107
107
|
where('upper(name) LIKE upper(?)', "%#{query}%")
|
108
108
|
}
|
109
109
|
|
data/app/models/users/user.rb
CHANGED
@@ -73,7 +73,7 @@ class User < ActiveRecord::Base
|
|
73
73
|
scope :by_name, -> { order('first_name, last_name, email') }
|
74
74
|
|
75
75
|
scope :text_search, lambda { |query|
|
76
|
-
query = query.gsub(/[^\w\s
|
76
|
+
query = query.gsub(/[^\w\s\-.'\p{L}]/u, '').strip
|
77
77
|
where('upper(username) LIKE upper(:s) OR upper(email) LIKE upper(:s) OR upper(first_name) LIKE upper(:s) OR upper(last_name) LIKE upper(:s)', s: "%#{query}%")
|
78
78
|
}
|
79
79
|
|
@@ -134,6 +134,12 @@ class User < ActiveRecord::Base
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
137
|
+
# Send emails to active users only
|
138
|
+
#----------------------------------------------------------------------------
|
139
|
+
def emailable?
|
140
|
+
confirmed? && !awaits_approval? && !suspended? && email.present?
|
141
|
+
end
|
142
|
+
|
137
143
|
#----------------------------------------------------------------------------
|
138
144
|
def preference
|
139
145
|
@preference ||= preferences.build
|
@@ -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
|
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)
|