headmin 0.1.1

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 (175) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.nvmrc +1 -0
  4. data/.rubocop.yml +13 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +43 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +166 -0
  12. data/Rakefile +16 -0
  13. data/app/assets/images/avatar.jpg +0 -0
  14. data/app/controllers/admin/users/confirmations_controller.rb +31 -0
  15. data/app/controllers/admin/users/omniauth_callbacks_controller.rb +31 -0
  16. data/app/controllers/admin/users/passwords_controller.rb +35 -0
  17. data/app/controllers/admin/users/registrations_controller.rb +63 -0
  18. data/app/controllers/admin/users/sessions_controller.rb +28 -0
  19. data/app/controllers/admin/users/unlocks_controller.rb +31 -0
  20. data/app/controllers/concerns/headmin/acts_as_list.rb +16 -0
  21. data/app/controllers/concerns/headmin/authentication.rb +17 -0
  22. data/app/controllers/concerns/headmin/ckeditor.rb +27 -0
  23. data/app/controllers/concerns/headmin/filter.rb +5 -0
  24. data/app/controllers/concerns/headmin/pagination.rb +23 -0
  25. data/app/controllers/concerns/headmin/searchable.rb +15 -0
  26. data/app/controllers/concerns/headmin/sortable.rb +44 -0
  27. data/app/helpers/headmin/admin_helper.rb +65 -0
  28. data/app/helpers/headmin/filter_helper.rb +12 -0
  29. data/app/helpers/headmin/notification_helper.rb +31 -0
  30. data/app/views/admin/users/confirmations/new.html.erb +9 -0
  31. data/app/views/admin/users/mailer/confirmation_instructions.html.erb +5 -0
  32. data/app/views/admin/users/mailer/email_changed.html.erb +7 -0
  33. data/app/views/admin/users/mailer/password_change.html.erb +3 -0
  34. data/app/views/admin/users/mailer/reset_password_instructions.html.erb +8 -0
  35. data/app/views/admin/users/mailer/unlock_instructions.html.erb +7 -0
  36. data/app/views/admin/users/passwords/edit.html.erb +12 -0
  37. data/app/views/admin/users/passwords/new.html.erb +9 -0
  38. data/app/views/admin/users/registrations/edit.html.erb +24 -0
  39. data/app/views/admin/users/registrations/new.html.erb +11 -0
  40. data/app/views/admin/users/sessions/new.html.erb +13 -0
  41. data/app/views/admin/users/shared/_error_messages.html.erb +7 -0
  42. data/app/views/admin/users/shared/_links.html.erb +27 -0
  43. data/app/views/admin/users/unlocks/new.html.erb +10 -0
  44. data/app/views/headmin/_breadcrumbs.html.erb +23 -0
  45. data/app/views/headmin/_filters.html.erb +47 -0
  46. data/app/views/headmin/_form.html.erb +11 -0
  47. data/app/views/headmin/_heading.html.erb +9 -0
  48. data/app/views/headmin/_index.html.erb +12 -0
  49. data/app/views/headmin/_notifications.html.erb +12 -0
  50. data/app/views/headmin/_pagination.html.erb +13 -0
  51. data/app/views/headmin/_table.html.erb +13 -0
  52. data/app/views/headmin/filters/_date.html.erb +46 -0
  53. data/app/views/headmin/filters/_search.html.erb +22 -0
  54. data/app/views/headmin/filters/_select.html.erb +39 -0
  55. data/app/views/headmin/filters/filter/_button.html.erb +22 -0
  56. data/app/views/headmin/filters/filter/_menu_item.html.erb +12 -0
  57. data/app/views/headmin/filters/filter/_template.html.erb +13 -0
  58. data/app/views/headmin/forms/_actions.html.erb +32 -0
  59. data/app/views/headmin/forms/_errors.html.erb +20 -0
  60. data/app/views/headmin/forms/_group.html.erb +36 -0
  61. data/app/views/headmin/forms/fields/_checkbox.html.erb +23 -0
  62. data/app/views/headmin/forms/fields/_ckeditor.html.erb +28 -0
  63. data/app/views/headmin/forms/fields/_currency.html.erb +24 -0
  64. data/app/views/headmin/forms/fields/_date.html.erb +36 -0
  65. data/app/views/headmin/forms/fields/_email.html.erb +39 -0
  66. data/app/views/headmin/forms/fields/_file.html.erb +24 -0
  67. data/app/views/headmin/forms/fields/_image.html.erb +37 -0
  68. data/app/views/headmin/forms/fields/_label.html.erb +9 -0
  69. data/app/views/headmin/forms/fields/_multiple_select.html.erb +37 -0
  70. data/app/views/headmin/forms/fields/_password.html.erb +39 -0
  71. data/app/views/headmin/forms/fields/_repeater.html.erb +48 -0
  72. data/app/views/headmin/forms/fields/_select.html.erb +36 -0
  73. data/app/views/headmin/forms/fields/_select_tags.html.erb +32 -0
  74. data/app/views/headmin/forms/fields/_text.html.erb +39 -0
  75. data/app/views/headmin/forms/fields/_textarea.html.erb +29 -0
  76. data/app/views/headmin/forms/fields/_url.html.erb +38 -0
  77. data/app/views/headmin/forms/fields/_validation.html.erb +12 -0
  78. data/app/views/headmin/forms/fields/repeater/_row.html.erb +16 -0
  79. data/app/views/headmin/heading/_title.html.erb +24 -0
  80. data/app/views/headmin/kaminari/_first_page.html.erb +11 -0
  81. data/app/views/headmin/kaminari/_gap.html.erb +12 -0
  82. data/app/views/headmin/kaminari/_last_page.html.erb +12 -0
  83. data/app/views/headmin/kaminari/_next_page.html.erb +13 -0
  84. data/app/views/headmin/kaminari/_page.html.erb +12 -0
  85. data/app/views/headmin/kaminari/_paginator.html.erb +25 -0
  86. data/app/views/headmin/kaminari/_prev_page.html.erb +11 -0
  87. data/app/views/headmin/layout/_body.html.erb +9 -0
  88. data/app/views/headmin/layout/_content.html.erb +9 -0
  89. data/app/views/headmin/layout/_footer.html.erb +17 -0
  90. data/app/views/headmin/layout/_header.html.erb +13 -0
  91. data/app/views/headmin/layout/_main.html.erb +13 -0
  92. data/app/views/headmin/layout/_sidebar.html.erb +20 -0
  93. data/app/views/headmin/layout/dropdown/_divider.html.erb +9 -0
  94. data/app/views/headmin/layout/dropdown/_item.html.erb +17 -0
  95. data/app/views/headmin/layout/header/_account.html.erb +25 -0
  96. data/app/views/headmin/layout/header/_locale.html.erb +19 -0
  97. data/app/views/headmin/layout/sidebar/_bottom.html.erb +4 -0
  98. data/app/views/headmin/layout/sidebar/_menu.html.erb +9 -0
  99. data/app/views/headmin/layout/sidebar/menu/_account.html.erb +25 -0
  100. data/app/views/headmin/layout/sidebar/menu/_item.html.erb +16 -0
  101. data/app/views/headmin/layout/sidebar/menu/_locale.html.erb +18 -0
  102. data/app/views/headmin/table/_actions.html.erb +19 -0
  103. data/app/views/headmin/table/_body.html.erb +19 -0
  104. data/app/views/headmin/table/_foot.html.erb +15 -0
  105. data/app/views/headmin/table/_footer.html.erb +13 -0
  106. data/app/views/headmin/table/_head.html.erb +15 -0
  107. data/app/views/headmin/table/_header.html.erb +13 -0
  108. data/app/views/headmin/table/actions/_action.html.erb +10 -0
  109. data/app/views/headmin/table/actions/_delete.html.erb +8 -0
  110. data/app/views/headmin/table/actions/_export.html.erb +8 -0
  111. data/app/views/headmin/table/body/_association.html.erb +11 -0
  112. data/app/views/headmin/table/body/_boolean.erb +16 -0
  113. data/app/views/headmin/table/body/_currency.html.erb +10 -0
  114. data/app/views/headmin/table/body/_date.html.erb +11 -0
  115. data/app/views/headmin/table/body/_id.html.erb +3 -0
  116. data/app/views/headmin/table/body/_row.html.erb +9 -0
  117. data/app/views/headmin/table/body/_sort.html.erb +3 -0
  118. data/app/views/headmin/table/body/_string.html.erb +16 -0
  119. data/app/views/headmin/table/body/_text.html.erb +10 -0
  120. data/app/views/headmin/table/foot/_cell.html.erb +10 -0
  121. data/app/views/headmin/table/foot/_id.html.erb +9 -0
  122. data/app/views/headmin/table/head/_cell.html.erb +13 -0
  123. data/app/views/headmin/table/head/_empty.html.erb +1 -0
  124. data/app/views/headmin/table/head/_id.html.erb +9 -0
  125. data/app/views/headmin/table/head/_sort.html.erb +3 -0
  126. data/app/views/headmin/table/head/cell/_asc.html.erb +4 -0
  127. data/app/views/headmin/table/head/cell/_default.html.erb +7 -0
  128. data/app/views/headmin/table/head/cell/_desc.html.erb +4 -0
  129. data/app/views/layouts/admin/auth.html.erb +20 -0
  130. data/app/views/layouts/admin.html.erb +42 -0
  131. data/bin/console +15 -0
  132. data/bin/setup +8 -0
  133. data/config/locales/defaults/en.yml +215 -0
  134. data/config/locales/defaults/nl.yml +213 -0
  135. data/config/locales/devise/nl.yml +85 -0
  136. data/config/locales/en.yml +137 -0
  137. data/config/locales/nl.yml +138 -0
  138. data/dist/css/headmin.css +9874 -0
  139. data/dist/js/headmin.js +852 -0
  140. data/docs/README.md +4 -0
  141. data/docs/blocks.md +116 -0
  142. data/docs/devise.md +62 -0
  143. data/headmin.gemspec +35 -0
  144. data/lib/headmin/engine.rb +10 -0
  145. data/lib/headmin/version.rb +5 -0
  146. data/lib/headmin.rb +4 -0
  147. data/package.json +62 -0
  148. data/src/js/headmin/controllers/filter_controller.js +47 -0
  149. data/src/js/headmin/controllers/filters_controller.js +53 -0
  150. data/src/js/headmin/controllers/index_controller.js +79 -0
  151. data/src/js/headmin/controllers/repeater_controller.js +35 -0
  152. data/src/js/headmin/controllers/repeater_row_controller.js +54 -0
  153. data/src/js/headmin/controllers/table_actions_controller.js +22 -0
  154. data/src/js/headmin/controllers/table_controller.js +36 -0
  155. data/src/js/headmin/headmin.js +168 -0
  156. data/src/js/headmin.js +1 -0
  157. data/src/scss/headmin/filter.scss +33 -0
  158. data/src/scss/headmin/filters.scss +14 -0
  159. data/src/scss/headmin/form.scss +39 -0
  160. data/src/scss/headmin/general.scss +3 -0
  161. data/src/scss/headmin/layout/body.scss +6 -0
  162. data/src/scss/headmin/layout/sidebar.scss +22 -0
  163. data/src/scss/headmin/layout.scss +2 -0
  164. data/src/scss/headmin/login.scss +35 -0
  165. data/src/scss/headmin/table.scss +32 -0
  166. data/src/scss/headmin/utilities.scss +19 -0
  167. data/src/scss/headmin.scss +58 -0
  168. data/src/scss/vendor/bootstrap/variables.scss +71 -0
  169. data/src/scss/vendor/choices/cross-inverse.svg +6 -0
  170. data/src/scss/vendor/choices/cross.svg +6 -0
  171. data/src/scss/vendor/choices/custom.scss +28 -0
  172. data/src/scss/vendor/choices/variables.scss +16 -0
  173. data/webpack.config.js +30 -0
  174. data/yarn.lock +7512 -0
  175. metadata +220 -0
data/docs/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Features
2
+
3
+ - [Blocks and fields link](blocks.md)
4
+ - [Authentication with Devise](devise.md)
data/docs/blocks.md ADDED
@@ -0,0 +1,116 @@
1
+ # Blocks & Fields
2
+
3
+ ## Database
4
+ blocks
5
+ ```
6
+ id = 1 resource_type="Page" resource_id="1" name = 'people', position = 1
7
+ ```
8
+
9
+ fields
10
+ ```
11
+ id = 1 block_id="1" name = 'people_title' type = 'string' value = 'test'
12
+ id = 2 block_id="1" name = 'people_people' type = 'list'
13
+ id = 3 block_id="1" name = 'people_people_1_person' type = 'group' parent_id = 2
14
+ id = 4 block_id="1" name = 'people_people_1_person_name' type = 'string' parent_id = 3 value = 'Jef'
15
+ id = 5 block_id="1" name = 'people_people_2_person' type = 'group' parent_id = 2
16
+ id = 6 block_id="1" name = 'people_people_2_person_name' type = 'string' parent_id = 6 value = 'Gert-Jan'
17
+ ```
18
+
19
+ ## Models
20
+
21
+
22
+ ```rb
23
+ # models/page.rb
24
+
25
+ class Page < ActiveRecord::Model
26
+ has_many :blocks, as: :blockable
27
+ end
28
+ ```
29
+
30
+ ```rb
31
+ # models/block.rb
32
+
33
+ class Block < ActiveRecord::Model
34
+ belongs_to :blockable, polymorphic: true
35
+ has_many :fields
36
+
37
+ # db to hash
38
+ def fields_hash
39
+ {
40
+ title: 'test',
41
+ people: [
42
+ {name: 'Jef'},
43
+ {name: 'Gert-Jan'}
44
+ ]
45
+ }
46
+ end
47
+ end
48
+ ```
49
+
50
+ ```rb
51
+ # models/block/fields.rb
52
+
53
+ class Block::Field < ActiveRecord::Model
54
+ self.table_name = 'block_fields'
55
+
56
+ belongs_to :block
57
+ end
58
+ ```
59
+
60
+ ## Controllers
61
+ ```ruby
62
+ # website/pages_controller.rb
63
+
64
+ class PagesController < ApplicationController
65
+ def show
66
+ @page = Page.friendly.find(params[:slug])
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Views
72
+ ```erb
73
+ # admin/pages/_form.html.erb
74
+
75
+ <input type="text" name="title" value="test">
76
+
77
+ <%= render 'blocks/people', required: true, moveable: false %>
78
+ <%= render 'blocks/menu', required: true, moveable: false %>
79
+ <%= render 'blocks/contact_form', required: true, moveable: false %>
80
+ <%= render 'blocks/map', required: true, moveable: false %>
81
+
82
+ <%= render 'blocks/heading', required: false, moveable: true %>
83
+ <%= render 'blocks/text', required: false, moveable: true %>
84
+ <%= render 'blocks/image', required: false, moveable: true %>
85
+ <%= render 'blocks/video', required: false, moveable: true %>
86
+ ```
87
+
88
+ ```erb
89
+ # admin/blocks/_people.html.erb
90
+
91
+ <input type="text" name="people_title" value="title" value="test">
92
+ <div class="repeater" data-min="2" data-max="3">
93
+ <div class="group">
94
+ <input type="text" name="people_people_1_person_name" value="name" value="Jef">
95
+ <input type="text" name="people_people_2_person_name" value="name" value="Gert-Jan">
96
+ </div>
97
+ </div>
98
+ ```
99
+
100
+ ```erb
101
+ # website/pages/show.html.erb
102
+
103
+ <% @page.blocks.each do |block| %>
104
+ <%= render "block_#{block.name}", block: block %>
105
+ <% end %>
106
+ ```
107
+
108
+ ```erb
109
+ # website/blocks/_people.html.erb
110
+
111
+ <% fields = block.fields_hash %>
112
+ <%= fields[:title] %>
113
+ <% fields[:people].each do |person| %>
114
+ <%= person[:name] %>
115
+ <% end %>
116
+ ```
data/docs/devise.md ADDED
@@ -0,0 +1,62 @@
1
+ # Authentication
2
+
3
+ We use the gem Devise for authentication. In this example, two different user types will be set up:
4
+ Admin and normal User. Only admins have access to the headmin back office. It is optional if normal users
5
+ are allowed to login/register from the website.
6
+
7
+ ##Installation
8
+
9
+ #### Setup devise
10
+ Add the gem devise and bundle install. Create a user model:
11
+
12
+ ```
13
+ rails generate devise User
14
+ ```
15
+
16
+ Add the field "type" to the user model
17
+ ```
18
+ rails g migration AddTypeToUsers type:string
19
+ rails db:migrate
20
+ ```
21
+
22
+ In models/ add the file admin.rb
23
+ ```
24
+ class Admin < User
25
+ end
26
+ ```
27
+
28
+ #### Setup routes
29
+ In routes.rb
30
+ ```
31
+ namespace(:admin) do
32
+ devise_for :admins, path: 'users', singular: 'admin', controllers: {
33
+ sessions: 'admin/users/sessions',
34
+ registrations: 'admin/users/registrations',
35
+ passwords: 'admin/users/passwords',
36
+ unlocks: 'admin/users/unlocks',
37
+ confirmations: 'admin/users/confirmations',
38
+ }
39
+ end
40
+ ```
41
+
42
+ In case, no normal users are necessary, you can discard the next step:
43
+ ```
44
+ scope module: 'website' do
45
+ devise_for :users, controllers: {
46
+ sessions: 'website/users/sessions',
47
+ registrations: 'website/users/registrations',
48
+ passwords: 'website/users/passwords',
49
+ unlocks: 'website/users/unlocks',
50
+ confirmations: 'website/users/confirmations',
51
+ }
52
+ end
53
+ ```
54
+
55
+ #### Setup protected routes and configuration for headmin
56
+ In controllers/admin_controller.rb
57
+ ```
58
+ class AdminController < ApplicationController
59
+ alias_method :devise_current_user, :current_user
60
+ include Headmin::Authentication
61
+ end
62
+ ```
data/headmin.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/headmin/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "headmin"
7
+ spec.version = Headmin::VERSION
8
+ spec.authors = ["Jef Vlamings"]
9
+ spec.email = ["vlamingsjef@gmail.com"]
10
+
11
+ spec.summary = "Admin component library"
12
+ spec.description = "A complete library of commonly used components to build an admin interface in your Ruby on Rails project."
13
+ spec.homepage = "https://github.com/insiting/headmin"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/insiting/headmin"
19
+ spec.metadata["changelog_uri"] = "https://github.com/insiting/headmin/releases"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ # spec.add_dependency "example-gem", "~> 1.0"
32
+
33
+ # For more information and examples about making a new gem, checkout our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
@@ -0,0 +1,10 @@
1
+ module Headmin
2
+ class Engine < Rails::Engine
3
+ engine_name 'headmin'
4
+
5
+ # Add translations to main app
6
+ config.before_configuration do
7
+ config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"]
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Headmin
4
+ VERSION = "0.1.1"
5
+ end
data/lib/headmin.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "headmin/version"
4
+ require_relative "headmin/engine"
data/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "headmin",
3
+ "version": "0.1.1",
4
+ "description": "A complete library of commonly used components to build an admin interface in your Ruby on Rails project.",
5
+ "main": "src/js/headmin.js",
6
+ "sass": "src/scss/headmin.scss",
7
+ "style": "src/css/headmin.css",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/insiting/headmin.git"
11
+ },
12
+ "keywords": [
13
+ "headmin",
14
+ "insiting"
15
+ ],
16
+ "author": "Jef Vlamings <vlamingsjef@gmail.com>",
17
+ "license": "MIT",
18
+ "bugs": {
19
+ "url": "https://github.com/insiting/headmin/issues"
20
+ },
21
+ "homepage": "https://github.com/insiting/headmin#readme",
22
+ "dependencies": {
23
+ "@rails/ujs": "^6.0.0",
24
+ "bootstrap": "^5.0.0-beta2",
25
+ "choices.js": "^9.0.1",
26
+ "ckeditor5-build-classic-simple-upload-adapter-image-resize": "^1.0.4",
27
+ "flatpickr": "^4.6.9",
28
+ "popper": "^1.0.1",
29
+ "sortablejs": "^1.13.0",
30
+ "stimulus": "^2.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@babel/core": "^7.12.10",
34
+ "@babel/preset-env": "^7.12.11",
35
+ "autoprefixer": "^10.2.3",
36
+ "babel-loader": "^8.2.2",
37
+ "node-sass": "^5.0.0",
38
+ "node-sass-tilde-importer": "^1.0.2",
39
+ "onchange": "^7.1.0",
40
+ "path": "^0.12.7",
41
+ "postcss": "^8.2.4",
42
+ "postcss-cli": "^8.3.1",
43
+ "watch": "^1.0.2",
44
+ "webpack": "4.44.2",
45
+ "webpack-cli": "^4.4.0"
46
+ },
47
+ "browserslist": [
48
+ "last 2 versions",
49
+ "ie >= 11"
50
+ ],
51
+ "scripts": {
52
+ "autoprefixer": "postcss -u autoprefixer -b 'last 2 versions' -r dist/css/headmin.css",
53
+ "compile:sass": "node-sass --importer=node_modules/node-sass-tilde-importer src/scss/headmin.scss dist/css/headmin.css",
54
+ "build:css": "yarn compile:sass && yarn autoprefixer",
55
+ "build:js": "webpack",
56
+ "build": "yarn build:css & yarn build:js",
57
+ "watch:css": "onchange src/scss/**/*.scss -- yarn build:css",
58
+ "watch:js": "onchange src/js/**/*.js -- yarn build:js",
59
+ "watch": "yarn watch:css & yarn watch:js",
60
+ "dev": "yarn build & yarn watch"
61
+ }
62
+ }
@@ -0,0 +1,47 @@
1
+ import {Controller} from "stimulus"
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["button", "popup"]
6
+ }
7
+
8
+ // Attaches controller logic to the element itself
9
+ // This allows calling controller methods from the element in other controllers
10
+ connect () {
11
+ this.element['controller'] = this
12
+ }
13
+
14
+ toggle(event) {
15
+ event.preventDefault()
16
+ const expanded = this.buttonTarget.getAttribute('aria-expanded') === 'true'
17
+ if (expanded) {
18
+ this.close(null)
19
+ } else {
20
+ this.open()
21
+ }
22
+ }
23
+
24
+ open() {
25
+ this.buttonTarget.setAttribute('aria-expanded', 'true')
26
+ this.popupTarget.classList.remove('closed')
27
+ }
28
+
29
+ close(event) {
30
+ if (this.isClickedInside(event)) {
31
+ event.preventDefault()
32
+ return
33
+ }
34
+ this.buttonTarget.setAttribute('aria-expanded', 'false')
35
+ this.popupTarget.classList.add('closed')
36
+ }
37
+
38
+ isClickedInside(event) {
39
+ if (!event) {
40
+ return false
41
+ }
42
+ const inPopup = this.popupTarget.contains(event.target)
43
+ const inButton = this.buttonTarget.contains(event.target)
44
+ const inAddButton = event.target.dataset.action === "click->filters#add"
45
+ return (inPopup || inButton || inAddButton)
46
+ }
47
+ }
@@ -0,0 +1,53 @@
1
+ import {Controller} from "stimulus"
2
+ import {Headmin} from "../headmin";
3
+
4
+ export default class extends Controller {
5
+ static get targets() {
6
+ return ["form", "list", "input", "template", "button"]
7
+ }
8
+
9
+ add(event) {
10
+ event.preventDefault()
11
+ const name = event.target.dataset.filterName
12
+ const button = this.getButtonByName(name)
13
+ if (button) {
14
+ this.openFilter(button)
15
+ } else {
16
+ const template = this.getTemplateByName(name)
17
+ this.listTarget.insertAdjacentHTML('beforeend', template.innerHTML)
18
+ const button = this.getButtonByName(name)
19
+ Headmin.initPlugins()
20
+ }
21
+ }
22
+
23
+ remove(event) {
24
+ const filter = event.currentTarget.closest('.h-filter')
25
+ filter.remove()
26
+ this.formTarget.submit()
27
+ }
28
+
29
+ removeAll(event) {
30
+ this.listTarget.innerHTML = ""
31
+ this.formTarget.submit()
32
+ }
33
+
34
+ update(event) {
35
+ this.formTarget.submit()
36
+ }
37
+
38
+ getTemplateByName(name) {
39
+ return this.templateTargets.find(function (element) {
40
+ return element.dataset.filterName === name
41
+ })
42
+ }
43
+
44
+ getButtonByName(name) {
45
+ return this.buttonTargets.find(function (element) {
46
+ return element.dataset.filterName === name
47
+ })
48
+ }
49
+
50
+ openFilter(button) {
51
+ button.controller.open()
52
+ }
53
+ }
@@ -0,0 +1,79 @@
1
+ import {Controller} from "stimulus"
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["actions", "idCheckbox", "idsCheckbox"]
6
+ }
7
+
8
+ toggleIds(event) {
9
+ const checkbox = event.target
10
+ this.toggleIdsCheckboxes(checkbox.checked)
11
+ this.toggleIdCheckboxes(checkbox.checked)
12
+ this.syncFields()
13
+ }
14
+
15
+ toggleId(event) {
16
+ this.toggleIdsCheckboxes(false)
17
+ this.syncFields()
18
+ }
19
+
20
+ toggleIdsCheckboxes(checked) {
21
+ this.idsCheckboxTargets.forEach((checkbox) => {
22
+ checkbox.checked = checked
23
+ });
24
+ }
25
+
26
+ toggleIdCheckboxes(checked) {
27
+ this.idCheckboxTargets.forEach((checkbox) => {
28
+ checkbox.checked = checked
29
+ });
30
+ }
31
+
32
+ syncFields() {
33
+ this.removeIds()
34
+ if(this.idsCheckboxTarget.checked) {
35
+ this.addId('')
36
+ } else {
37
+ this.idCheckboxTargets.forEach((checkbox) => {
38
+ if(checkbox.checked) {
39
+ this.addId(checkbox.value)
40
+ }
41
+ });
42
+ }
43
+ }
44
+
45
+ addId(id) {
46
+ let field = this.getIdField(id)
47
+ if (!field) {
48
+ field = this.newIdField(id)
49
+ this.actionsTarget.insertAdjacentHTML('afterbegin', field)
50
+ }
51
+ }
52
+
53
+ removeIds() {
54
+ const fields = this.getIdFields()
55
+ fields.forEach((field) => {
56
+ this.actionsTarget.removeChild(field)
57
+ });
58
+ }
59
+
60
+ removeId(id) {
61
+ const field = this.getIdField(id)
62
+ if (field) {
63
+ this.actionsTarget.removeChild(field)
64
+ }
65
+ }
66
+
67
+ newIdField(id) {
68
+ const template = this.actionsTarget.querySelector('[data-index-target="idFieldTemplate"]')
69
+ return template.innerHTML.replace(/ID/g, id)
70
+ }
71
+
72
+ getIdFields() {
73
+ return this.actionsTarget.querySelectorAll(`input[name="ids[]"]`);
74
+ }
75
+
76
+ getIdField(id) {
77
+ return this.actionsTarget.querySelector(`input[name="ids[]"][value="${id}"]`);
78
+ }
79
+ }
@@ -0,0 +1,35 @@
1
+ import {Controller} from "stimulus"
2
+ import {Headmin} from "../headmin";
3
+
4
+ export default class extends Controller {
5
+ static get values() {
6
+ return {
7
+ id: String
8
+ }
9
+ }
10
+ static get targets() {
11
+ return ["links", "template"]
12
+ }
13
+
14
+ add_association(event) {
15
+ event.preventDefault()
16
+
17
+ let html = this.getTemplateHTML()
18
+ html = this.replaceIdsWithTimestamps(html)
19
+ this.addNewRow(html)
20
+ }
21
+
22
+ getTemplateHTML() {
23
+ return this.templateTarget.innerHTML
24
+ }
25
+
26
+ replaceIdsWithTimestamps(html) {
27
+ const regex = new RegExp(this.idValue, "g");
28
+ return html.replace(regex, new Date().getTime())
29
+ }
30
+
31
+ addNewRow(html) {
32
+ this.linksTarget.insertAdjacentHTML('beforebegin', html)
33
+ Headmin.initPlugins()
34
+ }
35
+ }
@@ -0,0 +1,54 @@
1
+ import {Controller} from "stimulus"
2
+ import {Headmin} from '../headmin'
3
+
4
+ export default class extends Controller {
5
+ static get targets() {
6
+ return ["row"]
7
+ }
8
+
9
+ add_association(event) {
10
+ event.preventDefault()
11
+
12
+ let html = this.getTemplateHTML()
13
+ html = this.replaceIdsWithTimestamps(html)
14
+ this.addRow(html)
15
+ }
16
+
17
+ remove_association(event) {
18
+ event.preventDefault()
19
+
20
+ let row = event.target.closest(".repeater-row")
21
+
22
+ if (row.dataset.newRecord === "true") {
23
+ // New records are simply removed from the page
24
+ row.remove()
25
+ } else {
26
+ // Existing records are hidden and flagged for deletion
27
+ row.querySelector("input[name*='_destroy']").value = 1
28
+ row.style.display = 'none'
29
+ }
30
+ }
31
+
32
+ addRow(html) {
33
+ this.rowTarget.insertAdjacentHTML('afterend', html)
34
+ Headmin.initPlugins()
35
+ }
36
+
37
+ replaceIdsWithTimestamps(html) {
38
+ const regex = new RegExp(this.idValue(), "g");
39
+ return html.replace(regex, new Date().getTime())
40
+ }
41
+
42
+ getTemplateHTML() {
43
+ const templateTarget = this.templateTarget()
44
+ return templateTarget.innerHTML
45
+ }
46
+
47
+ templateTarget() {
48
+ return this.rowTarget.closest('.repeater').querySelector('[data-repeater-target="template"]')
49
+ }
50
+
51
+ idValue() {
52
+ return this.rowTarget.closest('.repeater').dataset.repeaterIdValue
53
+ }
54
+ }
@@ -0,0 +1,22 @@
1
+ import {Controller} from "stimulus"
2
+
3
+ export default class extends Controller {
4
+ static get targets() {
5
+ return ["form", "select", "method"]
6
+ }
7
+
8
+ connect() {
9
+ this.wrapperClass = "table-actions"
10
+ }
11
+
12
+ update(event) {
13
+ event.preventDefault()
14
+ const option = this.selectTarget.options[this.selectTarget.selectedIndex]
15
+ const action = this.selectTarget.value
16
+ const method = option.dataset.method
17
+
18
+ // Replace form action
19
+ this.formTarget.action = action
20
+ this.methodTarget.value = method
21
+ }
22
+ }
@@ -0,0 +1,36 @@
1
+ import {Controller} from "stimulus"
2
+ import Sortable from "sortablejs";
3
+ import Rails from "@rails/ujs";
4
+
5
+ export default class extends Controller {
6
+ static get values() {
7
+ return {url: String}
8
+ }
9
+
10
+ static get targets() {
11
+ return ["table", "body"]
12
+ }
13
+
14
+ connect() {
15
+ new Sortable(this.bodyTarget, {
16
+ handle: '.table-drag-sort-handle',
17
+ onEnd: (event) => {
18
+ Rails.ajax({
19
+ url: this.urlValue,
20
+ type: "PATCH",
21
+ data: this.getIdsDataString(),
22
+ });
23
+ }
24
+ })
25
+ }
26
+
27
+ getIdsDataString() {
28
+ const table = this.tableTarget
29
+ let data = ""
30
+ const handles = [...table.querySelectorAll(".table-drag-sort-handle")]
31
+ handles.map(handle => {
32
+ data += `ids[]=${handle.dataset.id}&`
33
+ })
34
+ return data
35
+ }
36
+ }