redmineup 1.0.10 → 1.1.13

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.gitlab-ci.yml +33 -0
  4. data/README.md +74 -153
  5. data/app/assets/javascripts/consumer.js +53 -0
  6. data/app/assets/javascripts/consumer_worker.js +219 -0
  7. data/app/assets/javascripts/jquery.colorPicker.min.js +26 -0
  8. data/app/assets/javascripts/select2_helpers.js +10 -0
  9. data/app/assets/stylesheets/redmineup.css +509 -0
  10. data/app/views/redmineup/_additional_assets.html.erb +1 -0
  11. data/config/locales/en.yml +1 -0
  12. data/config/locales/ru.yml +1 -0
  13. data/config/routes.rb +4 -0
  14. data/doc/CHANGELOG +99 -16
  15. data/doc/active-record-mixins.md +156 -0
  16. data/doc/assets-money-and-utilities.md +71 -0
  17. data/doc/tagging-and-select2.md +108 -0
  18. data/lib/action_cable/connection/rup_connection.rb +17 -0
  19. data/lib/action_cable/server/rup_configuration.rb +17 -0
  20. data/lib/action_cable/server/rup_server.rb +17 -0
  21. data/lib/redmineup/acts_as_taggable/tag.rb +26 -2
  22. data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +42 -2
  23. data/lib/redmineup/acts_as_viewed/up_acts_as_viewed.rb +1 -1
  24. data/lib/redmineup/engine.rb +12 -2
  25. data/lib/redmineup/helpers/datetime_helper.rb +23 -0
  26. data/lib/redmineup/helpers/external_assets_helper.rb +14 -6
  27. data/lib/redmineup/helpers/form_tag_helper.rb +43 -0
  28. data/lib/redmineup/helpers/tags_helper.rb +41 -0
  29. data/lib/redmineup/helpers/test_helper.rb +12 -0
  30. data/lib/redmineup/hooks/views_layouts_hook.rb +1 -4
  31. data/lib/redmineup/liquid/drops/version_drop.rb +0 -2
  32. data/lib/redmineup/money_helper.rb +10 -2
  33. data/lib/redmineup/patches/action_cable_base_patch.rb +25 -0
  34. data/lib/redmineup/patches/action_cable_patch.rb +19 -0
  35. data/lib/redmineup/patches/auto_completes_controller_patch.rb +17 -0
  36. data/lib/redmineup/patches/compatibility/routing_mapper_patch.rb +5 -18
  37. data/lib/redmineup/patches/compatibility/sprite_patch.rb +3 -11
  38. data/lib/redmineup/patches/compatibility_patch.rb +0 -6
  39. data/lib/redmineup/patches/liquid_patch.rb +13 -22
  40. data/lib/redmineup/version.rb +1 -1
  41. data/lib/redmineup.rb +47 -42
  42. data/test/acts_as_taggable/tag_test.rb +16 -0
  43. data/test/acts_as_votable/votable_test.rb +9 -9
  44. data/test/acts_as_votable/voter_test.rb +4 -4
  45. data/test/database.yml +3 -3
  46. data/test/fixtures/tags.yml +2 -0
  47. data/test/liquid/filters/additional_filter_test.rb +5 -1
  48. data/test/liquid/filters/arrays_filter_test.rb +6 -3
  49. data/test/schema.rb +6 -1
  50. data/test/tags_helper_view_test.rb +41 -0
  51. data/test/test_helper.rb +13 -12
  52. metadata +21 -5
  53. data/bitbucket-pipelines.yml +0 -50
  54. data/lib/redmineup/patches/compatibility/application_controller_patch.rb +0 -32
  55. data/lib/redmineup/patches/compatibility/user_patch.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2c096a3e3b503b00ba8d14af5ebc4092b9c6488314ac4673203819a476bcac9
4
- data.tar.gz: '065394c3419cebe86b2129756c64c98596cfc0408fec04786fef9a45624e497f'
3
+ metadata.gz: a4d2181d902dbfa9a13995df17ff80c6a9eaea15fd54c6a918a6695ced32ed40
4
+ data.tar.gz: 88ee188a9626a4483f6ebc108a5f2ae174de4bde94e5d04128f60b0f3d6d8799
5
5
  SHA512:
6
- metadata.gz: f146e9b9c3476a557ffb3e7a26cada20f290a8f9110271b1704c60d1ad1c6530ad98067914d3aeef97f558492dfe7553d044ab40b002e436ac78a9706af2db20
7
- data.tar.gz: 1a6a842839188f5f823a194585da91793593c0dde92fb8f90712a4d3165f50b4c83e875d81d7efd5a7924d004f8cef8ca786eb7f05b5d90d58cdf83c6d1d6805
6
+ metadata.gz: bbd631a43429432a3c3f2e208527b601dd3d1dca57d260d314f2808d485b96d8f8ef29baf31a6836c026c30cf5d453075082d6d65cf548efba7fe5393a862b75
7
+ data.tar.gz: 49ccf2ace3f1af01aa00a748a94c93f6deb994fd5bc0e0a23db7f4e98c7ed371aeff27b8561a0c8cce1aeee19d9e8e5333ae4c7ee77c699bb9c8bb1c092daff3
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
+ /.obsidian
3
4
  /Gemfile.lock
4
5
  /_yardoc/
5
6
  /coverage/
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,33 @@
1
+ default:
2
+ services:
3
+ - mysql:8
4
+ - postgres:15
5
+
6
+ variables:
7
+ MYSQL_DATABASE: redmineup_test
8
+ MYSQL_ROOT_PASSWORD: password
9
+ POSTGRES_DB: redmineup_test
10
+ POSTGRES_PASSWORD: password
11
+
12
+ workflow:
13
+ rules:
14
+ - if: $CI_PIPELINE_SOURCE != "merge_request_event"
15
+
16
+ ruby-3.1.7:
17
+ stage: test
18
+ image: ruby:3.1.7-bookworm
19
+ script:
20
+ - bundle install
21
+ - bundle exec rake test DB=sqlite
22
+ - bundle exec rake test DB=postgresql
23
+ - bundle exec rake test DB=mysql
24
+
25
+ ruby-4.0.2:
26
+ stage: test
27
+ image: ruby:4.0.2-bookworm
28
+ script:
29
+ - sed -i "s/spec.add_development_dependency 'pg'/spec.add_development_dependency 'pg'\n spec.add_development_dependency 'minitest-mock'/g" redmineup.gemspec
30
+ - bundle install
31
+ - bundle exec rake test DB=sqlite
32
+ - bundle exec rake test DB=postgresql
33
+ - bundle exec rake test DB=mysql
data/README.md CHANGED
@@ -1,204 +1,125 @@
1
- # Redmineup
2
-
3
- This gem is used at RedmineUP as a general place for shared functionality and
4
- assets. It contains **Chart.js** and **select2** JS libraries, various mixins
5
- for ActiveRecord models and other things you might find useful.
6
-
7
- Among mixins there are:
8
-
9
- * up_acts_as_draftable
10
- * up_acts_as_taggable
11
- * up_acts_as_viewable
12
- * up_acts_as_votable
13
-
1
+ # redmineup
2
+
3
+ `redmineup` is a shared gem used by RedmineUP plugins. It provides common
4
+ ActiveRecord mixins, view helpers, assets, settings, and compatibility patches
5
+ so plugin code stays focused on domain logic.
6
+
7
+ The gem is used in production plugins such as `redmine_contacts`,
8
+ `redmine_people`, `redmine_products`, `redmine_questions`, `redmine_drive`,
9
+ `redmineup_tags`, `redmine_agile`, and others.
10
+
11
+ ## What is included
12
+
13
+ - ActiveRecord mixins: `up_acts_as_taggable`, `up_acts_as_draftable`,
14
+ `up_acts_as_viewed`, `up_acts_as_votable`, `up_acts_as_voter`,
15
+ `up_acts_as_priceable`
16
+ - Tag helpers and tag UI helpers: `tag_link`, `tag_links`, `tag_cloud_links`,
17
+ `tag_list_field_tag`, `FormBuilder#tag_list_field`
18
+ - Select2 and chart integration helpers: `select2_tag`, `transform_to_select2`,
19
+ `redmineup_assets`, `chartjs_assets`
20
+ - Shared endpoint for tag autocompletion:
21
+ `auto_complete_taggable_tags_path`
22
+ - Money and currency formatting (`Redmineup::MoneyHelper`,
23
+ `Redmineup::Currency`, `Redmineup::Settings::Money`)
24
+ - Liquid filters and drops used across RedmineUP plugins
14
25
 
15
26
  ## Installation
16
27
 
17
- Add it to your plugin's Gemfile:
28
+ Add to your Redmine or plugin `Gemfile`:
29
+
18
30
  ```ruby
19
31
  gem 'redmineup'
20
32
  ```
21
33
 
22
- Then invoke the following command in your plugin's or Redmine directory:
23
- ```
24
- $ bundle install
25
- ```
26
-
27
- And now you can start using it.
28
-
34
+ Install dependencies:
29
35
 
30
- ## Usage
31
- ### Drafts
32
- This module allows saving and restoring drafts for different models. To be
33
- saved as a draft, an instance does not need to pass the validations. Drafts
34
- store not only model attributes, but also associations and virtual attributes.
35
- A draft could be linked to a given user, so every user can manage his/her own
36
- drafts independent of others. A draft might have a parent instance.
37
-
38
- First of all, drafts need to be saved somewhere, so let's create a migration:
39
- ```ruby
40
- class CreateDrafts < Rails.version > '5' ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration
41
- def change
42
- ActiveRecord::Base.create_drafts_table
43
- end
44
- end
36
+ ```bash
37
+ bundle install
45
38
  ```
46
39
 
47
- Then in the Redmine directory run:
48
- ```
49
- $ rake redmine:plugins:migrate
50
- ```
40
+ In plugin `init.rb`, guard the minimum supported version:
51
41
 
52
- Next, add `up_acts_as_draftable` to a model for which you want to save drafts:
53
42
  ```ruby
54
- class Message < ActiveRecord::Base
55
- up_acts_as_draftable
56
- end
43
+ requires_redmineup version_or_higher: '1.1.5'
57
44
  ```
58
45
 
59
- And that's it for the preparation, now you're ready to make use of drafts:
60
- ```ruby
61
- # You can save message as a draft.
62
- Message.new(subject: 'foo').save_draft
63
-
64
- # And later restore message from the draft.
65
- draft = Message.drafts(nil).last
66
- message = draft.restore
67
- puts message.subject
68
- # => foo
69
-
70
- # Draft can be overwritten.
71
- message.content = 'bar'
72
- puts message.save_draft
73
- # => true
74
-
75
- # You can also save draft linked to a particular user.
76
- Message.new(subject: 'baz').save_draft(user: current_user)
77
-
78
- # And restore message from some user's draft.
79
- user_draft = Message.drafts(current_user).last
80
- user_message = user_draft.restore
81
- puts user_message.subject
82
- # => baz
83
-
84
- # It's also possible to restore a bunch of messages at once.
85
- messages = Message.drafts(current_user).restore_all
86
- p messages.map(&:subject)
87
- # => ["baz"]
88
-
89
- # When a model instance is saved, corresponding draft is removed.
90
- puts Message.drafts(current_user).count
91
- # => 1
92
- user_message.board_id = Board.first.id
93
- user_message.save!
94
- puts Message.drafts(current_user).count
95
- # => 0
96
-
97
- # Drafts will be saved only for new (not persisted) or changed instances.
98
- puts Message.new.save_draft
99
- # => true
100
- persisted = Message.last
101
- puts persisted.save_draft
102
- # => false
103
- persisted.subject = 'other subject'
104
- puts persisted.save_draft
105
- # => true
106
- ```
46
+ ## Quick start
107
47
 
108
- ### Tags
109
- This module makes it possible to tag objects.
48
+ ### 1) Add taggable behavior to a model
110
49
 
111
- First, create a migration:
112
50
  ```ruby
113
- class CreateTags < Rails.version > '5' ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration
51
+ # db/migrate/XXXXXXXXXXXX_create_tags.rb
52
+ class CreateTags < ActiveRecord::Migration[6.1]
114
53
  def change
115
54
  ActiveRecord::Base.create_taggable_table
116
55
  end
117
56
  end
118
57
  ```
119
58
 
120
- Then in the Redmine directory run:
121
- ```
122
- $ rake redmine:plugins:migrate
123
- ```
124
-
125
- Next, add `up_acts_as_taggable` to a model for which you want to provide tags:
126
59
  ```ruby
127
- class Contact < ActiveRecord::Base
60
+ class Contact < ApplicationRecord
128
61
  up_acts_as_taggable
129
62
  end
130
63
  ```
131
64
 
132
- TODO: Add examples of usage.
133
-
134
- ### Viewings
135
- This module allows you to count views for some ActiveRecord model instances.
65
+ ### 2) Render tags using shared helpers
136
66
 
137
- To count views you'll need to create a migration:
138
67
  ```ruby
139
- class CreateViewings < Rails.version > '5' ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration
140
- def change
141
- ActiveRecord::Base.create_viewings_table
142
- end
68
+ # helper
69
+ def contact_tag_url(tag_name, options = {})
70
+ {
71
+ controller: 'contacts',
72
+ action: 'index',
73
+ set_filter: 1,
74
+ fields: [:tags],
75
+ values: { tags: [tag_name] },
76
+ operators: { tags: '=' }
77
+ }.merge(options)
143
78
  end
144
79
  ```
145
80
 
146
- To apply it, run the following in the Redmine directory:
147
- ```
148
- $ rake redmine:plugins:migrate
81
+ ```erb
82
+ <%= tag_cloud_links(Contact.available_tags(project: @project), :contact) %>
149
83
  ```
150
84
 
151
- Then add `up_acts_as_viewed` to a model for which you want to count views.
152
- ```ruby
153
- class Question < ActiveRecord::Base
154
- up_acts_as_viewed
155
- end
156
- ```
85
+ `tag_cloud_links(tags, :contact)` resolves links via `contact_tag_url(name)`.
157
86
 
158
- TODO: Provide some usage examples.
87
+ ### 3) Add Select2 tag editor to forms
159
88
 
160
- ### Votes
161
- With this module you can make your models votable and allow users to vote.
162
-
163
- As always, create a migration first:
164
- ```ruby
165
- class CreateVotes < Rails.version > '5' ? ActiveRecord::Migration[5.0] : ActiveRecord::Migration
166
- def change
167
- ActiveRecord::Base.create_votable_table
168
- end
169
- end
89
+ ```erb
90
+ <%= f.tag_list_field :tag_list, width: '95%' %>
170
91
  ```
171
92
 
172
- Then apply it by running the following command in the Redmine directory:
173
- ```
174
- $ rake redmine:plugins:migrate
175
- ```
93
+ or standalone:
176
94
 
177
- To make a model votable, add `up_acts_as_votable` to it:
178
- ```ruby
179
- class Question < ActiveRecord::Base
180
- up_acts_as_votable
181
- end
95
+ ```erb
96
+ <%= tag_list_field_tag 'contact[tag_list]', @contact.tag_list, taggable_type: 'Contact' %>
182
97
  ```
183
98
 
184
- TODO: Write about `up_acts_as_voter` and add usage examples.
99
+ ## Documentation
185
100
 
101
+ - [ActiveRecord mixins](doc/active-record-mixins.md)
102
+ - [Tagging and Select2 helpers](doc/tagging-and-select2.md)
103
+ - [Assets, money, and shared utilities](doc/assets-money-and-utilities.md)
186
104
 
187
105
  ## Development
188
106
 
189
- If you're planning to extend this gem, you will need to install development
190
- dependencies. To do this, execute the following command in the project's
191
- directory:
192
- ```
193
- $ bundle install
194
- ```
107
+ Install dependencies:
195
108
 
196
- After that you'll be able to run tests:
197
- ```
198
- $ bundle exec rake test
109
+ ```bash
110
+ bundle install
199
111
  ```
200
112
 
201
- SQLite in-memory database will be used by default, which is the fastest way to run tests. To run them using different database adapters, set `DB` environment variable to one of the available values — `mysql`, `postgresql`, `sqlite`. For example, to use PostgreSQL, invoke tests like so:
113
+ Run tests:
114
+
115
+ ```bash
116
+ bundle exec rake test
202
117
  ```
203
- $ bundle exec rake test DB=postgresql
118
+
119
+ Run tests against another DB adapter:
120
+
121
+ ```bash
122
+ bundle exec rake test DB=postgresql
204
123
  ```
124
+
125
+ Available `DB` values: `sqlite`, `mysql`, `postgresql`.
@@ -0,0 +1,53 @@
1
+ class activeCableConsumer {
2
+ constructor(consumer) {
3
+ this.consumer = consumer;
4
+ this.uuid = crypto.randomUUID();
5
+ this._connected = false;
6
+
7
+ const worker = new SharedWorker(
8
+ consumer.workerUrl || window.redmineupWorkerUrl,
9
+ );
10
+ this._port = worker.port;
11
+
12
+ this._port.onmessage = (e) => {
13
+ const { type, data } = e.data;
14
+ if (type === "connected") {
15
+ this._connected = true;
16
+ } else if (type === "disconnected") {
17
+ this._connected = false;
18
+ } else if (type === "message") {
19
+ this.processMessage(data);
20
+ }
21
+ this._port.postMessage({ type: "ack" });
22
+ };
23
+
24
+ this._port.start();
25
+
26
+ this._port.postMessage({
27
+ type: "connect",
28
+ uuid: this.uuid,
29
+ data: {
30
+ url: consumer.url,
31
+ channel: consumer.channel,
32
+ chatId: consumer.chatId,
33
+ },
34
+ });
35
+ }
36
+
37
+ processMessage(message) {
38
+ this.consumer.process(message);
39
+ }
40
+
41
+ isOpen() {
42
+ return this._connected;
43
+ }
44
+
45
+ send(data) {
46
+ this._port.postMessage({ type: "send", data });
47
+ return true;
48
+ }
49
+
50
+ disconnect() {
51
+ this._port.postMessage({ type: "disconnect" });
52
+ }
53
+ }
@@ -0,0 +1,219 @@
1
+ const connections = new Map();
2
+ const MAX_PENDING_ACK = 3;
3
+ const RETRY_BASE_DELAY = 5000;
4
+ const RETRY_MAX_DELAY = 320000;
5
+
6
+ self.onconnect = function (event) {
7
+ const clientPort = event.ports[0];
8
+ let clientUrl = null;
9
+ let clientUuid = null;
10
+ let channelIdentifier = null;
11
+
12
+ clientPort.onmessage = function (e) {
13
+ const { type, uuid, data } = e.data;
14
+
15
+ switch (type) {
16
+ case "connect": {
17
+ clientUuid = uuid;
18
+ clientUrl = data.url;
19
+ channelIdentifier = JSON.stringify({
20
+ channel: data.channel,
21
+ ...(data.chatId !== undefined && { chat_id: data.chatId }),
22
+ });
23
+
24
+ if (!connections.has(clientUrl)) {
25
+ connections.set(clientUrl, initSocketStruct());
26
+ }
27
+
28
+ const conn = connections.get(clientUrl);
29
+ const alreadySubscribed = hasChannelSubscribers(
30
+ conn,
31
+ channelIdentifier,
32
+ );
33
+
34
+ conn.clients.set(clientUuid, {
35
+ port: clientPort,
36
+ channelIdentifier,
37
+ pendingAck: 0,
38
+ });
39
+
40
+ if (!conn.socket || conn.socket.readyState > WebSocket.OPEN) {
41
+ initWebSocket(clientUrl);
42
+ } else if (
43
+ conn.socket.readyState === WebSocket.OPEN &&
44
+ !alreadySubscribed
45
+ ) {
46
+ sendSubscribe(conn.socket, channelIdentifier);
47
+ }
48
+ break;
49
+ }
50
+
51
+ case "send": {
52
+ if (clientUrl) {
53
+ const conn = connections.get(clientUrl);
54
+ if (
55
+ conn &&
56
+ conn.socket &&
57
+ conn.socket.readyState === WebSocket.OPEN
58
+ ) {
59
+ conn.socket.send(JSON.stringify(data));
60
+ }
61
+ }
62
+ break;
63
+ }
64
+
65
+ case "ack": {
66
+ if (clientUuid && clientUrl) {
67
+ const conn = connections.get(clientUrl);
68
+ if (conn) {
69
+ const client = conn.clients.get(clientUuid);
70
+ if (client) client.pendingAck = 0;
71
+ }
72
+ }
73
+ break;
74
+ }
75
+
76
+ case "disconnect": {
77
+ if (clientUuid && clientUrl) {
78
+ const conn = connections.get(clientUrl);
79
+ if (conn) {
80
+ conn.clients.delete(clientUuid);
81
+ const stillSubscribed = hasChannelSubscribers(
82
+ conn,
83
+ channelIdentifier,
84
+ );
85
+ if (
86
+ !stillSubscribed &&
87
+ conn.socket &&
88
+ conn.socket.readyState === WebSocket.OPEN
89
+ ) {
90
+ conn.socket.send(
91
+ JSON.stringify({
92
+ command: "unsubscribe",
93
+ identifier: channelIdentifier,
94
+ }),
95
+ );
96
+ }
97
+ if (conn.clients.size === 0) {
98
+ if (conn.socket) conn.socket.close();
99
+ connections.delete(clientUrl);
100
+ }
101
+ }
102
+ clientUuid = null;
103
+ clientUrl = null;
104
+ channelIdentifier = null;
105
+ }
106
+ break;
107
+ }
108
+ }
109
+ };
110
+
111
+ clientPort.start();
112
+ };
113
+
114
+ function sendSubscribe(socket, identifier) {
115
+ socket.send(JSON.stringify({ command: "subscribe", identifier }));
116
+ }
117
+
118
+ function initSocketStruct() {
119
+ return { socket: null, clients: new Map(), retryDelay: RETRY_BASE_DELAY };
120
+ }
121
+
122
+ function initWebSocket(url) {
123
+ const conn = connections.get(url);
124
+ if (!conn) return;
125
+
126
+ if (conn.socket) {
127
+ try {
128
+ conn.socket.onopen =
129
+ conn.socket.onmessage =
130
+ conn.socket.onclose =
131
+ conn.socket.onerror =
132
+ null;
133
+ conn.socket.close();
134
+ } catch (e) {}
135
+ }
136
+
137
+ const socket = new WebSocket(url);
138
+ conn.socket = socket;
139
+
140
+ conn.socket.onopen = function () {
141
+ if (conn.socket !== socket) return;
142
+ conn.retryDelay = RETRY_BASE_DELAY;
143
+ const seen = new Set();
144
+ for (const { channelIdentifier } of conn.clients.values()) {
145
+ if (!seen.has(channelIdentifier)) {
146
+ seen.add(channelIdentifier);
147
+ sendSubscribe(socket, channelIdentifier);
148
+ }
149
+ }
150
+ };
151
+
152
+ conn.socket.onmessage = function (event) {
153
+ if (conn.socket !== socket) return;
154
+ const messageData = (event.data && JSON.parse(event.data)) || {};
155
+
156
+ if (messageData.type === "confirm_subscription") {
157
+ routeToIdentifier(conn, messageData.identifier, { type: "connected" });
158
+ return;
159
+ }
160
+
161
+ if (messageData.type === "reject_subscription") {
162
+ routeToIdentifier(conn, messageData.identifier, { type: "rejected" });
163
+ return;
164
+ }
165
+
166
+ if (messageData.type === "ping" || !messageData.message) return;
167
+
168
+ routeToIdentifier(conn, messageData.identifier, {
169
+ type: "message",
170
+ data: messageData.message,
171
+ });
172
+ };
173
+
174
+ socket.onclose = function () {
175
+ if (conn.socket !== socket) return;
176
+ broadcastTo(conn, { type: "disconnected" });
177
+ const delay = conn.retryDelay;
178
+ conn.retryDelay = Math.min(conn.retryDelay * 2, RETRY_MAX_DELAY);
179
+ setTimeout(function () {
180
+ if (connections.has(url) && conn.socket === socket) initWebSocket(url);
181
+ }, delay);
182
+ };
183
+
184
+ socket.onerror = function (error) {
185
+ socket.close();
186
+ };
187
+ }
188
+
189
+ function hasChannelSubscribers(conn, channelIdentifier) {
190
+ for (const client of conn.clients.values()) {
191
+ if (client.channelIdentifier === channelIdentifier) return true;
192
+ }
193
+ return false;
194
+ }
195
+
196
+ function routeToIdentifier(conn, identifier, message) {
197
+ console.log("routeToIdentifier:", identifier, message);
198
+ for (const [key, client] of conn.clients) {
199
+ if (client.channelIdentifier === identifier) {
200
+ if (client.pendingAck >= MAX_PENDING_ACK) {
201
+ conn.clients.delete(key);
202
+ continue;
203
+ }
204
+ client.pendingAck++;
205
+ client.port.postMessage(message);
206
+ }
207
+ }
208
+ }
209
+
210
+ function broadcastTo(conn, message) {
211
+ for (const [key, client] of conn.clients) {
212
+ if (client.pendingAck >= MAX_PENDING_ACK) {
213
+ conn.clients.delete(key);
214
+ continue;
215
+ }
216
+ client.pendingAck++;
217
+ client.port.postMessage(message);
218
+ }
219
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Really Simple Color Picker in jQuery
3
+ *
4
+ * Licensed under the MIT (MIT-LICENSE.txt) licenses.
5
+ *
6
+ * Copyright (c) 2008-2012
7
+ * Lakshan Perera (www.laktek.com) & Daniel Lacy (daniellacy.com)
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to
11
+ * deal in the Software without restriction, including without limitation the
12
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
13
+ * sell copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in
17
+ * all copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
25
+ * IN THE SOFTWARE.
26
+ */(function(a){var b,c,d=0,e={control:a('<div class="colorPicker-picker">&nbsp;</div>'),palette:a('<div id="colorPicker_palette" class="colorPicker-palette" />'),swatch:a('<div class="colorPicker-swatch">&nbsp;</div>'),hexLabel:a('<label for="colorPicker_hex">Hex</label>'),hexField:a('<input type="text" id="colorPicker_hex" />')},f="transparent",g;a.fn.colorPicker=function(b){return this.each(function(){var c=a(this),g=a.extend({},a.fn.colorPicker.defaults,b),h=a.fn.colorPicker.toHex(c.val().length>0?c.val():g.pickerDefault),i=e.control.clone(),j=e.palette.clone().attr("id","colorPicker_palette-"+d),k=e.hexLabel.clone(),l=e.hexField.clone(),m=j[0].id,n,o;a.each(g.colors,function(b){n=e.swatch.clone(),g.colors[b]===f?(n.addClass(f).text("X"),a.fn.colorPicker.bindPalette(l,n,f)):(n.css("background-color","#"+this),a.fn.colorPicker.bindPalette(l,n)),n.appendTo(j)}),k.attr("for","colorPicker_hex-"+d),l.attr({id:"colorPicker_hex-"+d,value:h}),l.bind("keydown",function(b){if(b.keyCode===13){var d=a.fn.colorPicker.toHex(a(this).val());a.fn.colorPicker.changeColor(d?d:c.val())}b.keyCode===27&&a.fn.colorPicker.hidePalette()}),l.bind("keyup",function(b){var d=a.fn.colorPicker.toHex(a(b.target).val());a.fn.colorPicker.previewColor(d?d:c.val())}),a('<div class="colorPicker_hexWrap" />').append(k).appendTo(j),j.find(".colorPicker_hexWrap").append(l),g.showHexField===!1&&(l.hide(),k.hide()),a("body").append(j),j.hide(),i.css("background-color",h),i.bind("click",function(){c.is(":not(:disabled)")&&a.fn.colorPicker.togglePalette(a("#"+m),a(this))}),b&&b.onColorChange?i.data("onColorChange",b.onColorChange):i.data("onColorChange",function(){}),(o=c.data("text"))&&i.html(o),c.after(i),c.bind("change",function(){c.next(".colorPicker-picker").css("background-color",a.fn.colorPicker.toHex(a(this).val()))}),c.val(h);if(c[0].tagName.toLowerCase()==="input")try{c.attr("type","hidden")}catch(p){c.css("visibility","hidden").css("position","absolute")}else c.hide();d++})},a.extend(!0,a.fn.colorPicker,{toHex:function(a){if(a.match(/[0-9A-F]{6}|[0-9A-F]{3}$/i))return a.charAt(0)==="#"?a:"#"+a;if(!a.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/))return!1;var b=[parseInt(RegExp.$1,10),parseInt(RegExp.$2,10),parseInt(RegExp.$3,10)],c=function(a){if(a.length<2)for(var b=0,c=2-a.length;b<c;b++)a="0"+a;return a};if(b.length===3){var d=c(b[0].toString(16)),e=c(b[1].toString(16)),f=c(b[2].toString(16));return"#"+d+e+f}},checkMouse:function(d,e){var f=c,g=a(d.target).parents("#"+f.attr("id")).length;if(d.target===a(f)[0]||d.target===b[0]||g>0)return;a.fn.colorPicker.hidePalette()},hidePalette:function(){a(document).unbind("mousedown",a.fn.colorPicker.checkMouse),a(".colorPicker-palette").hide()},showPalette:function(c){var d=b.prev("input").val();c.css({top:b.offset().top+b.outerHeight(),left:b.offset().left}),a("#color_value").val(d),c.show(),a(document).bind("mousedown",a.fn.colorPicker.checkMouse)},togglePalette:function(d,e){e&&(b=e),c=d,c.is(":visible")?a.fn.colorPicker.hidePalette():a.fn.colorPicker.showPalette(d)},changeColor:function(c){b.css("background-color",c),b.prev("input").val(c).change(),a.fn.colorPicker.hidePalette(),b.data("onColorChange").call(b,a(b).prev("input").attr("id"),c)},previewColor:function(a){b.css("background-color",a)},bindPalette:function(c,d,e){e=e?e:a.fn.colorPicker.toHex(d.css("background-color")),d.bind({click:function(b){g=e,a.fn.colorPicker.changeColor(e)},mouseover:function(b){g=c.val(),a(this).css("border-color","#598FEF"),c.val(e),a.fn.colorPicker.previewColor(e)},mouseout:function(d){a(this).css("border-color","#000"),c.val(b.css("background-color")),c.val(g),a.fn.colorPicker.previewColor(g)}})}}),a.fn.colorPicker.defaults={pickerDefault:"FFFFFF",colors:["000000","993300","333300","000080","333399","333333","800000","FF6600","808000","008000","008080","0000FF","666699","808080","FF0000","FF9900","99CC00","339966","33CCCC","3366FF","800080","999999","FF00FF","FFCC00","FFFF00","00FF00","00FFFF","00CCFF","993366","C0C0C0","FF99CC","FFCC99","FFFF99","CCFFFF","99CCFF","FFFFFF"],addColors:[],showHexField:!0}})(jQuery);
@@ -123,6 +123,7 @@ function select2Tag(id, options) {
123
123
  $(function () {
124
124
  var selectField = $('select#' + id);
125
125
  selectField.select2(buildSelect2Options(options));
126
+ bindSelectHandler(selectField, options);
126
127
 
127
128
  var select2Instance = selectField.data('select2');
128
129
  select2Instance.on('results:message', function(params){
@@ -133,6 +134,15 @@ function select2Tag(id, options) {
133
134
  };
134
135
 
135
136
 
137
+ function bindSelectHandler(selectField, options) {
138
+ var callbackName = options['on_select'];
139
+ if (!callbackName) { return; }
140
+ if (typeof window[callbackName] !== 'function') { return; }
141
+
142
+ selectField.on('select2:select', window[callbackName]);
143
+ }
144
+
145
+
136
146
  function buildSelect2Options(options) {
137
147
  result = {
138
148
  placeholder: options['placeholder'] || '',