redmineup 1.1.4 → 1.1.5
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +74 -153
- data/app/assets/stylesheets/redmineup.css +476 -0
- data/app/views/redmineup/_additional_assets.html.erb +1 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/ru.yml +1 -0
- data/config/routes.rb +2 -0
- data/doc/CHANGELOG +42 -26
- data/doc/active-record-mixins.md +156 -0
- data/doc/assets-money-and-utilities.md +71 -0
- data/doc/tagging-and-select2.md +108 -0
- data/lib/redmineup/acts_as_taggable/tag.rb +12 -2
- data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +7 -5
- data/lib/redmineup/engine.rb +12 -2
- data/lib/redmineup/helpers/external_assets_helper.rb +11 -5
- data/lib/redmineup/helpers/form_tag_helper.rb +42 -0
- data/lib/redmineup/helpers/tags_helper.rb +39 -3
- data/lib/redmineup/hooks/views_layouts_hook.rb +1 -5
- data/lib/redmineup/patches/auto_completes_controller_patch.rb +19 -0
- data/lib/redmineup/version.rb +1 -1
- data/test/acts_as_taggable/tag_test.rb +12 -9
- data/test/tags_helper_view_test.rb +41 -0
- data/test/test_helper.rb +2 -1
- metadata +10 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ActiveRecord mixins
|
|
2
|
+
|
|
3
|
+
This page documents the model mixins provided by `redmineup`.
|
|
4
|
+
|
|
5
|
+
## `up_acts_as_taggable`
|
|
6
|
+
|
|
7
|
+
Adds polymorphic tagging support.
|
|
8
|
+
|
|
9
|
+
### Migration
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class CreateTags < ActiveRecord::Migration[6.1]
|
|
13
|
+
def change
|
|
14
|
+
ActiveRecord::Base.create_taggable_table
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Useful migration helpers:
|
|
20
|
+
|
|
21
|
+
- `ActiveRecord::Base.create_taggable_table`
|
|
22
|
+
- `ActiveRecord::Base.drop_taggable_table`
|
|
23
|
+
- `ActiveRecord::Base.add_tags_column(:tags, name: :color, type: :integer, default: nil)`
|
|
24
|
+
- `ActiveRecord::Base.remove_tags_columns(:tags, :color)`
|
|
25
|
+
|
|
26
|
+
### Model
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class Product < ApplicationRecord
|
|
30
|
+
up_acts_as_taggable
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Runtime API
|
|
35
|
+
|
|
36
|
+
- `record.tag_list` / `record.tag_list = 'a, b'`
|
|
37
|
+
- `record.save_tags` (called via callbacks)
|
|
38
|
+
- `Model.available_tags(project: @project, limit: 30)`
|
|
39
|
+
- `Model.tagged_with(['tag1', 'tag2'])`
|
|
40
|
+
- `Model.find_related_tags('tag1')`
|
|
41
|
+
|
|
42
|
+
## `up_acts_as_draftable`
|
|
43
|
+
|
|
44
|
+
Adds draft save/restore workflow for unsaved or changed records.
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class CreateDrafts < ActiveRecord::Migration[6.1]
|
|
50
|
+
def change
|
|
51
|
+
ActiveRecord::Base.create_drafts_table
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Helpers:
|
|
57
|
+
|
|
58
|
+
- `ActiveRecord::Base.create_drafts_table`
|
|
59
|
+
- `ActiveRecord::Base.drop_drafts_table`
|
|
60
|
+
|
|
61
|
+
### Model
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class WikiPage < ApplicationRecord
|
|
65
|
+
up_acts_as_draftable parent: :wiki
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Runtime API
|
|
70
|
+
|
|
71
|
+
- `record.save_draft`
|
|
72
|
+
- `record.update_draft(attrs)`
|
|
73
|
+
- `Model.drafts(User.current)`
|
|
74
|
+
- `Model.from_draft(draft_or_id)`
|
|
75
|
+
|
|
76
|
+
## `up_acts_as_viewed`
|
|
77
|
+
|
|
78
|
+
Tracks unique views by user (or IP) and total views.
|
|
79
|
+
|
|
80
|
+
### Migration
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class CreateViewings < ActiveRecord::Migration[6.1]
|
|
84
|
+
def change
|
|
85
|
+
ActiveRecord::Base.create_viewings_table
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Optional helpers for models with embedded counters:
|
|
91
|
+
|
|
92
|
+
- `ActiveRecord::Base.generate_viewings_columns(table)`
|
|
93
|
+
- `Model.add_viewings_columns`
|
|
94
|
+
- `Model.remove_viewings_columns`
|
|
95
|
+
- `ActiveRecord::Base.drop_viewings_table`
|
|
96
|
+
|
|
97
|
+
### Model
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class Question < ApplicationRecord
|
|
101
|
+
up_acts_as_viewed
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Runtime API
|
|
106
|
+
|
|
107
|
+
- `record.view(request.remote_ip, User.current)`
|
|
108
|
+
- `record.viewed?`
|
|
109
|
+
- `record.view_count`
|
|
110
|
+
- `record.viewed_by?(request.remote_ip, User.current)`
|
|
111
|
+
|
|
112
|
+
## `up_acts_as_votable` and `up_acts_as_voter`
|
|
113
|
+
|
|
114
|
+
Adds voting capabilities for content and users.
|
|
115
|
+
|
|
116
|
+
### Migration
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class CreateVotes < ActiveRecord::Migration[6.1]
|
|
120
|
+
def change
|
|
121
|
+
ActiveRecord::Base.create_votable_table
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Helpers:
|
|
127
|
+
|
|
128
|
+
- `ActiveRecord::Base.create_votable_table`
|
|
129
|
+
- `ActiveRecord::Base.drop_votable_table`
|
|
130
|
+
|
|
131
|
+
### Models
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
class Question < ApplicationRecord
|
|
135
|
+
up_acts_as_votable
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class User < ApplicationRecord
|
|
139
|
+
up_acts_as_voter
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## `up_acts_as_priceable`
|
|
144
|
+
|
|
145
|
+
Generates currency-aware `*_to_s` methods for price fields.
|
|
146
|
+
|
|
147
|
+
### Model
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class Deal < ApplicationRecord
|
|
151
|
+
up_acts_as_priceable :amount, :tax_amount, :subtotal, :total
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This creates methods like `amount_to_s`, `tax_amount_to_s`, etc., backed by
|
|
156
|
+
`Redmineup::MoneyHelper#object_price`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Assets, money, and shared utilities
|
|
2
|
+
|
|
3
|
+
## Asset helpers
|
|
4
|
+
|
|
5
|
+
Provided by `Redmineup::ExternalAssetsHelper`:
|
|
6
|
+
|
|
7
|
+
- `redmineup_assets`
|
|
8
|
+
- `select2_assets` (alias for `redmineup_assets`)
|
|
9
|
+
- `chartjs_assets`
|
|
10
|
+
|
|
11
|
+
`redmineup_assets` includes:
|
|
12
|
+
|
|
13
|
+
- `consumer.js`
|
|
14
|
+
- `select2.js`
|
|
15
|
+
- `select2_helpers.js`
|
|
16
|
+
- `redmineup.css`
|
|
17
|
+
|
|
18
|
+
The gem also hooks `view_layouts_base_html_head` and renders
|
|
19
|
+
`redmineup/_additional_assets`, so shared assets can be included globally.
|
|
20
|
+
Helpers are idempotent and safe to call from plugin views.
|
|
21
|
+
|
|
22
|
+
## Money and currency
|
|
23
|
+
|
|
24
|
+
### Settings API
|
|
25
|
+
|
|
26
|
+
`Redmineup::Settings::Money` provides access to plugin-level money settings:
|
|
27
|
+
|
|
28
|
+
- `default_currency`
|
|
29
|
+
- `major_currencies`
|
|
30
|
+
- `default_tax`
|
|
31
|
+
- `tax_type`, `tax_exclusive?`
|
|
32
|
+
- `thousands_delimiter`
|
|
33
|
+
- `decimal_separator`
|
|
34
|
+
- `disable_taxes?`
|
|
35
|
+
|
|
36
|
+
### Formatting helpers
|
|
37
|
+
|
|
38
|
+
`Redmineup::MoneyHelper` provides:
|
|
39
|
+
|
|
40
|
+
- `price_to_currency(price, currency, options = {})`
|
|
41
|
+
- `object_price(record, :price)`
|
|
42
|
+
- `prices_collection_by_currency(hash, hide_zeros: true)`
|
|
43
|
+
- `collection_for_currencies_select`
|
|
44
|
+
- `all_currencies`
|
|
45
|
+
|
|
46
|
+
`up_acts_as_priceable` uses these helpers to generate `*_to_s` methods on
|
|
47
|
+
models.
|
|
48
|
+
|
|
49
|
+
### Admin UI
|
|
50
|
+
|
|
51
|
+
`Redmineup::Currency.add_admin_money_menu` registers a Redmine admin menu item
|
|
52
|
+
that points to:
|
|
53
|
+
|
|
54
|
+
- `redmineup_settings_path(:money)`
|
|
55
|
+
|
|
56
|
+
## Misc shared helpers and patches
|
|
57
|
+
|
|
58
|
+
- `Redmineup::CalendarsHelper#calendar_day_css_classes`
|
|
59
|
+
- Liquid patch for numeric conversion in `Liquid::StandardFilters`
|
|
60
|
+
- Liquid filters/drops under `lib/redmineup/liquid/**`
|
|
61
|
+
- Compatibility and ActionCable patches loaded at gem boot
|
|
62
|
+
|
|
63
|
+
## Version requirements in plugins
|
|
64
|
+
|
|
65
|
+
Plugins typically guard compatibility in `init.rb`:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
requires_redmineup version_or_higher: '1.1.5'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use this to fail fast when a plugin depends on newer helper or API behavior.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Tagging and Select2 helpers
|
|
2
|
+
|
|
3
|
+
This page covers view helpers used by RedmineUP plugins for tag rendering,
|
|
4
|
+
tag editing, and select2-backed filters.
|
|
5
|
+
|
|
6
|
+
## Tag rendering helpers
|
|
7
|
+
|
|
8
|
+
Provided by `Redmineup::TagsHelper`:
|
|
9
|
+
|
|
10
|
+
- `tag_link(url, tag_name, options = {})`
|
|
11
|
+
- `tag_links(tag_list, options = {}) { |tag| url }`
|
|
12
|
+
- `tag_cloud(tags, classes) { |tag, css_class| ... }`
|
|
13
|
+
- `tag_cloud_links(tags, key, options = {})`
|
|
14
|
+
- `tag_separator`
|
|
15
|
+
|
|
16
|
+
### Recommended pattern
|
|
17
|
+
|
|
18
|
+
Define `*_tag_url` per plugin context:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
def contact_tag_url(tag_name, options = {})
|
|
22
|
+
{
|
|
23
|
+
controller: 'contacts',
|
|
24
|
+
action: 'index',
|
|
25
|
+
set_filter: 1,
|
|
26
|
+
fields: [:tags],
|
|
27
|
+
values: { tags: [tag_name] },
|
|
28
|
+
operators: { tags: '=' }
|
|
29
|
+
}.merge(options)
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then render the cloud via convention:
|
|
34
|
+
|
|
35
|
+
```erb
|
|
36
|
+
<%= tag_cloud_links(Contact.available_tags(project: @project), :contact) %>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`tag_cloud_links(tags, :contact)` expects `contact_tag_url(name)` to be defined
|
|
40
|
+
in the current view context.
|
|
41
|
+
|
|
42
|
+
## Tag editors with Select2
|
|
43
|
+
|
|
44
|
+
Provided by `Redmineup::FormTagHelper`:
|
|
45
|
+
|
|
46
|
+
- `select2_tag(name, option_tags = nil, options = {})`
|
|
47
|
+
- `select2(...)` (alias)
|
|
48
|
+
- `tag_list_field_tag(name, value = [], options = {})`
|
|
49
|
+
- `transform_to_select2(type, options = {})`
|
|
50
|
+
|
|
51
|
+
`ActionView::Helpers::FormBuilder` is extended with:
|
|
52
|
+
|
|
53
|
+
- `f.tag_list_field(:tag_list, **options)`
|
|
54
|
+
|
|
55
|
+
### Example: form object API
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
<%= f.tag_list_field :tag_list, width: '95%' %>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Example: standalone field API
|
|
62
|
+
|
|
63
|
+
```erb
|
|
64
|
+
<%= tag_list_field_tag 'product[tag_list]', @product.tag_list, taggable_type: 'Product' %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Example: query/filter select2 transform
|
|
68
|
+
|
|
69
|
+
```erb
|
|
70
|
+
<%= transform_to_select2 'product_tags',
|
|
71
|
+
url: auto_complete_taggable_tags_path(project_id: @project, taggable_type: 'Product') %>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tag autocomplete endpoint
|
|
75
|
+
|
|
76
|
+
`redmineup` adds a shared route:
|
|
77
|
+
|
|
78
|
+
- `GET /auto_completes/taggable_tags`
|
|
79
|
+
|
|
80
|
+
Named route:
|
|
81
|
+
|
|
82
|
+
- `auto_complete_taggable_tags_path`
|
|
83
|
+
|
|
84
|
+
Typical params used by plugins:
|
|
85
|
+
|
|
86
|
+
- `taggable_type`: model class name (`Contact`, `Person`, `Product`, `Issue`, `DriveEntry`)
|
|
87
|
+
- `project_id`: optional project scope
|
|
88
|
+
- `limit`: optional result size limit
|
|
89
|
+
|
|
90
|
+
Response format:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[
|
|
94
|
+
{ "id": "important", "text": "important" },
|
|
95
|
+
{ "id": "vip", "text": "vip" }
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Real plugin usage
|
|
100
|
+
|
|
101
|
+
Patterns above are used by:
|
|
102
|
+
|
|
103
|
+
- `redmine_contacts` (`contact_tag_url`, `contact_tag_links`, `f.tag_list_field`)
|
|
104
|
+
- `redmine_people` (`people_tag_url`, `tag_cloud_links`, bulk edit tag forms)
|
|
105
|
+
- `redmine_products` (`product_tag_url`, `tag_list_field_tag`, select2 filters)
|
|
106
|
+
- `redmine_questions` (`question_tag_url`, `tag_cloud_links`)
|
|
107
|
+
- `redmine_drive` (`drive_entry_tag_url`, select2 tag filters)
|
|
108
|
+
- `redmineup_tags` (issue tag forms with `tag_list_field_tag`)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'digest/md5'
|
|
2
|
+
|
|
1
3
|
module Redmineup
|
|
2
4
|
module ActsAsTaggable #:nodoc:
|
|
3
5
|
class Tag < ActiveRecord::Base #:nodoc:
|
|
@@ -17,6 +19,10 @@ module Redmineup
|
|
|
17
19
|
where("LOWER(name) LIKE LOWER(?)", name).first || create(:name => name)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def self.color_from_name(str)
|
|
23
|
+
"##{Digest::MD5.hexdigest(str.to_s)[0..5]}"
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def ==(object)
|
|
21
27
|
super || (object.is_a?(Tag) && name == object.name)
|
|
22
28
|
end
|
|
@@ -30,11 +36,15 @@ module Redmineup
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def color
|
|
33
|
-
|
|
39
|
+
if self.class.column_names.include?('color')
|
|
40
|
+
db_color = read_attribute(:color)
|
|
41
|
+
return ('#' + "%06x" % db_color) unless db_color.nil?
|
|
42
|
+
end
|
|
43
|
+
self.class.color_from_name(name)
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
def color=(color)
|
|
37
|
-
write_attribute(:color, color.
|
|
47
|
+
write_attribute(:color, color.sub(/\A#/, '').hex) unless color.blank?
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
class << self
|
|
@@ -52,6 +52,7 @@ module Redmineup
|
|
|
52
52
|
if !self.connection.table_exists?(tag_name_table)
|
|
53
53
|
self.connection.create_table(tag_name_table) do |t|
|
|
54
54
|
t.column :name, :string
|
|
55
|
+
t.column :color, :integer
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
|
|
@@ -64,6 +65,7 @@ module Redmineup
|
|
|
64
65
|
# You should make sure that the column created is
|
|
65
66
|
# long enough to store the required class names.
|
|
66
67
|
t.column :taggable_type, :string
|
|
68
|
+
t.column :color, :integer, default: nil
|
|
67
69
|
|
|
68
70
|
t.column :created_at, :datetime
|
|
69
71
|
end
|
|
@@ -82,16 +84,16 @@ module Redmineup
|
|
|
82
84
|
#
|
|
83
85
|
# E.g.: ActiveRecord::Base.add_tags_column(:tags, name: :color, type: :string, default: nil)
|
|
84
86
|
def add_tags_column(table_name = :tags, **options)
|
|
85
|
-
tag_column = options
|
|
87
|
+
tag_column = options
|
|
86
88
|
tag_column.assert_valid_keys(:name, :type, :default)
|
|
87
89
|
|
|
88
90
|
raise ArgumentError, "Table `#{table_name}' not found" unless self.connection.table_exists?(table_name)
|
|
89
91
|
|
|
90
92
|
self.table_name = table_name
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
return if connection.column_exists?(table_name, tag_column[:name])
|
|
94
|
+
|
|
95
|
+
connection.add_column table_name, tag_column[:name], tag_column[:type], default: tag_column[:default]
|
|
96
|
+
reset_column_information
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
# Remove the up_acts_as_taggable specific columns
|
data/lib/redmineup/engine.rb
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
module Redmineup
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class Engine < Rails::Engine
|
|
3
|
+
config.to_prepare do
|
|
4
|
+
next unless defined?(::Redmine::Plugin)
|
|
5
|
+
|
|
6
|
+
require 'auto_completes_controller'
|
|
7
|
+
|
|
8
|
+
require File.expand_path('../redmineup/patches/compatibility_patch', __dir__)
|
|
9
|
+
require File.expand_path('../redmineup/patches/auto_completes_controller_patch', __dir__)
|
|
10
|
+
|
|
11
|
+
require File.expand_path('../redmineup/hooks/views_layouts_hook', __dir__)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
4
14
|
end
|
|
@@ -2,12 +2,18 @@ module Redmineup
|
|
|
2
2
|
module ExternalAssetsHelper
|
|
3
3
|
include ActionView::Helpers::JavaScriptHelper
|
|
4
4
|
|
|
5
|
+
def redmineup_assets
|
|
6
|
+
return if @redmineup_assets_included
|
|
7
|
+
|
|
8
|
+
@redmineup_assets_included = true
|
|
9
|
+
javascript_include_tag('consumer', plugin: GEM_NAME) +
|
|
10
|
+
javascript_include_tag('select2', plugin: GEM_NAME) +
|
|
11
|
+
javascript_include_tag('select2_helpers', plugin: GEM_NAME) +
|
|
12
|
+
stylesheet_link_tag('redmineup', plugin: GEM_NAME)
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
def select2_assets
|
|
6
|
-
|
|
7
|
-
@select2_tag_included = true
|
|
8
|
-
javascript_include_tag('select2', plugin: GEM_NAME) +
|
|
9
|
-
stylesheet_link_tag('select2', plugin: GEM_NAME) +
|
|
10
|
-
javascript_include_tag('select2_helpers', plugin: GEM_NAME)
|
|
16
|
+
redmineup_assets
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def chartjs_assets
|
|
@@ -49,6 +49,31 @@ module Redmineup
|
|
|
49
49
|
|
|
50
50
|
alias select2 select2_tag
|
|
51
51
|
|
|
52
|
+
# Renders a select2 tag field for a tag list (standalone, no form object).
|
|
53
|
+
# Analogous to text_field_tag.
|
|
54
|
+
#
|
|
55
|
+
# ==== Example
|
|
56
|
+
# tag_list_field_tag 'entry[tag_list]', entry.tag_list,
|
|
57
|
+
# url: auto_complete_taggable_tags_path(taggable_type: 'DriveEntry')
|
|
58
|
+
def tag_list_field_tag(name, value = [], options = {})
|
|
59
|
+
url = options.delete(:url)
|
|
60
|
+
taggable_type = options.delete(:taggable_type)
|
|
61
|
+
width = options.delete(:width)
|
|
62
|
+
project_id = options.delete(:project_id)
|
|
63
|
+
placeholder = options.delete(:placeholder)
|
|
64
|
+
select2_tag(
|
|
65
|
+
name,
|
|
66
|
+
options_for_select(value, value),
|
|
67
|
+
{
|
|
68
|
+
multiple: true,
|
|
69
|
+
width: width || '95%',
|
|
70
|
+
url: url || auto_complete_taggable_tags_path(taggable_type: taggable_type, project_id: project_id),
|
|
71
|
+
placeholder: placeholder || l(:lable_redmineup_add_tag),
|
|
72
|
+
tags: true
|
|
73
|
+
}.merge(options)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
52
77
|
# Transforms select filters of +type+ fields into select2
|
|
53
78
|
#
|
|
54
79
|
# ==== Examples
|
|
@@ -86,3 +111,20 @@ module Redmineup
|
|
|
86
111
|
|
|
87
112
|
end
|
|
88
113
|
end
|
|
114
|
+
|
|
115
|
+
# Extends Rails FormBuilder with tag_list_field, analogous to f.text_field.
|
|
116
|
+
#
|
|
117
|
+
# ==== Example
|
|
118
|
+
# <%= f.tag_list_field :tag_list %>
|
|
119
|
+
ActionView::Helpers::FormBuilder.class_eval do
|
|
120
|
+
def tag_list_field(method, **options)
|
|
121
|
+
url = options.fetch(:url) {
|
|
122
|
+
@template.auto_complete_taggable_tags_path(taggable_type: @object.class.name)
|
|
123
|
+
}
|
|
124
|
+
@template.tag_list_field_tag(
|
|
125
|
+
"#{@object_name}[#{method}]",
|
|
126
|
+
@object.public_send(method),
|
|
127
|
+
{url: url}.merge(options.except(:url))
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -10,9 +10,45 @@ module Redmineup
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
def tag_link(url, tag_name, options = {})
|
|
14
|
+
count = options.delete(:count)
|
|
15
|
+
inner = link_to(tag_name, url, options)
|
|
16
|
+
inner << content_tag('span', "(#{count})", class: 'tag-count') if count
|
|
17
|
+
css = monochrome? ? 'tag-label' : 'tag-label-color'
|
|
18
|
+
style = monochrome? ? {} : { style: "background-color: #{Redmineup::ActsAsTaggable::Tag.color_from_name(tag_name)}" }
|
|
19
|
+
content_tag(:span, inner, { class: css }.merge(style))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tag_separator
|
|
23
|
+
monochrome? ? ', ' : ' '
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def monochrome?
|
|
27
|
+
@monochrome ||= defined?(RedmineupTags) && !RedmineupTags.use_colors?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tag_links(tag_list, options = {}, &url_block)
|
|
31
|
+
return if tag_list.blank?
|
|
32
|
+
content_tag(:span, class: 'tag_list') do
|
|
33
|
+
safe_join(tag_list.map { |tag| tag_link(url_block.call(tag), tag, options.dup) }, tag_separator)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Renders span.tag_list from an array of tag objects (respond to .name and .count).
|
|
38
|
+
# Resolves URL via `{key}_tag_url(name)` in the current view context.
|
|
39
|
+
#
|
|
40
|
+
# tag_cloud_links(contact.available_tags, :contact) # → contact_tag_url(name)
|
|
41
|
+
# tag_cloud_links(tags_cloud, :people) # → people_tag_url(name)
|
|
42
|
+
def tag_cloud_links(tags, key, options = {})
|
|
43
|
+
return if tags.blank?
|
|
44
|
+
url_method = :"#{key}_tag_url"
|
|
45
|
+
raise NoMethodError, "#{url_method} is not defined in view context" unless respond_to?(url_method, true)
|
|
46
|
+
content_tag(:span, class: 'tag_list') do
|
|
47
|
+
safe_join(tags.map { |tag|
|
|
48
|
+
tag_link(send(url_method, tag.name), tag.name, options.merge(count: tag.count).dup)
|
|
49
|
+
}, tag_separator)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
16
52
|
|
|
17
53
|
end
|
|
18
54
|
end
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
module Redmineup
|
|
4
4
|
module Hooks
|
|
5
5
|
class ViewsLayoutsHook < Redmine::Hook::ViewListener
|
|
6
|
-
|
|
7
|
-
stylesheet_link_tag(:calendars, plugin: 'redmineup') +
|
|
8
|
-
stylesheet_link_tag(:money, plugin: 'redmineup') +
|
|
9
|
-
javascript_include_tag(:consumer, plugin: 'redmineup')
|
|
10
|
-
end
|
|
6
|
+
render_on :view_layouts_base_html_head, partial: 'redmineup/additional_assets'
|
|
11
7
|
end
|
|
12
8
|
end
|
|
13
9
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
module Patches
|
|
3
|
+
module AutoCompletesControllerPatch
|
|
4
|
+
DEFAULT_TAGS_LIMIT = 10
|
|
5
|
+
|
|
6
|
+
def taggable_tags
|
|
7
|
+
|
|
8
|
+
limit = params.delete(:limit) || DEFAULT_TAGS_LIMIT
|
|
9
|
+
klass = Object.const_get(params[:taggable_type].camelcase) if params[:taggable_type].present?
|
|
10
|
+
tags = klass && klass.available_tags(params.merge(limit: limit)) || Redmineup::ActsAsTaggable::Tag.limit(limit)
|
|
11
|
+
render json: tags.map { |tag| { id: tag.name, text: tag.name } }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless AutoCompletesController.included_modules.include?(Redmineup::Patches::AutoCompletesControllerPatch)
|
|
18
|
+
AutoCompletesController.include(Redmineup::Patches::AutoCompletesControllerPatch)
|
|
19
|
+
end
|
data/lib/redmineup/version.rb
CHANGED
|
@@ -34,16 +34,19 @@ class TagTest < ActiveSupport::TestCase
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def test_color
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
assert_equal '#aaaaaa', tags(:feature).color # 11184810 = 0xaaaaaa
|
|
38
|
+
assert_equal '#bbbbbb', tags(:bug).color # 12303291 = 0xbbbbbb
|
|
39
|
+
# no DB color — falls back to color_from_name, not nil
|
|
40
|
+
assert_equal Redmineup::ActsAsTaggable::Tag.color_from_name('error'), tags(:error).color
|
|
41
|
+
assert_equal Redmineup::ActsAsTaggable::Tag.color_from_name('question'), tags(:question).color
|
|
42
|
+
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
def test_color_from_name
|
|
45
|
+
color = Redmineup::ActsAsTaggable::Tag.color_from_name('error')
|
|
46
|
+
assert_match(/\A#[0-9a-f]{6}\z/, color)
|
|
47
|
+
assert_equal color, Redmineup::ActsAsTaggable::Tag.color_from_name('error') # deterministic
|
|
48
|
+
assert_not_equal color, Redmineup::ActsAsTaggable::Tag.color_from_name('other')
|
|
49
|
+
assert_match(/\A#[0-9a-f]{6}\z/, Redmineup::ActsAsTaggable::Tag.color_from_name(nil))
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def test_tag_is_equal_to_itself
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
|
2
|
+
|
|
3
|
+
class TagsHelperViewTest < ActionView::TestCase
|
|
4
|
+
include Redmineup::TagsHelper
|
|
5
|
+
include Redmineup::FormTagHelper
|
|
6
|
+
|
|
7
|
+
def stub_tag_url(name)
|
|
8
|
+
"/stub_tags/#{name}" # URL stub for tag_cloud_links(tags, :stub)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_tag_link
|
|
12
|
+
result = tag_link('/tags/error', 'error', count: 3)
|
|
13
|
+
assert_includes result, 'tag-label-color'
|
|
14
|
+
assert_includes result, 'background-color: #'
|
|
15
|
+
assert_includes result, '(3)'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_tag_links
|
|
19
|
+
result = tag_links(['error', 'bug']) { |t| "/tags/#{t}" }
|
|
20
|
+
assert_includes result, 'class="tag_list"'
|
|
21
|
+
assert_nil tag_links([]) { |t| "/tags/#{t}" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_tag_cloud_links
|
|
25
|
+
result = tag_cloud_links(Issue.tag_counts, :stub)
|
|
26
|
+
assert_includes result, 'class="tag_list"'
|
|
27
|
+
assert_includes result, '/stub_tags/'
|
|
28
|
+
assert_nil tag_cloud_links([], :stub)
|
|
29
|
+
assert_raises(NoMethodError) { tag_cloud_links(Issue.tag_counts, :unknown_xyz) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_tag_list_field_tag
|
|
33
|
+
result = tag_list_field_tag('issue[tag_list]', ['bug'],
|
|
34
|
+
url: '/auto_completes/taggable_tags',
|
|
35
|
+
width: '60%', tags: false,
|
|
36
|
+
placeholder: '+ add tag')
|
|
37
|
+
assert_includes result, 'name="issue[tag_list][]"'
|
|
38
|
+
assert_includes result, '"width":"60%"'
|
|
39
|
+
assert_includes result, '"tags":false'
|
|
40
|
+
end
|
|
41
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -13,7 +13,8 @@ Dir.glob(File.expand_path('models/*.rb', __dir__)).each { |f| require f }
|
|
|
13
13
|
class ActiveSupport::TestCase # :nodoc:
|
|
14
14
|
include ActiveRecord::TestFixtures
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
gem_fixtures_path = File.dirname(__FILE__) + '/fixtures/'
|
|
17
|
+
respond_to?(:fixture_paths=) ? self.fixture_paths = [gem_fixtures_path] : self.fixture_path = gem_fixtures_path
|
|
17
18
|
|
|
18
19
|
self.use_transactional_tests = true if RUBY_VERSION > '1.9.3'
|
|
19
20
|
self.use_instantiated_fixtures = false
|